Respect expires header when checking for new version

It was called to many times and acted similar to a DOS attack.
This commit is contained in:
XiangRongLin 2021-01-23 09:02:11 +01:00
parent cce896e900
commit 2926cb7682
4 changed files with 139 additions and 25 deletions

View File

@ -10,22 +10,19 @@ import android.content.pm.Signature;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException; import com.grack.nanojson.JsonParserException;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import org.schabi.newpipe.report.ErrorActivity; import io.reactivex.rxjava3.core.Maybe;
import org.schabi.newpipe.report.ErrorInfo; import io.reactivex.rxjava3.disposables.Disposable;
import org.schabi.newpipe.report.UserAction; import io.reactivex.rxjava3.schedulers.Schedulers;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -34,11 +31,9 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import org.schabi.newpipe.report.ErrorActivity;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import org.schabi.newpipe.report.ErrorInfo;
import io.reactivex.rxjava3.core.Maybe; import org.schabi.newpipe.report.UserAction;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class CheckForNewAppVersion { public final class CheckForNewAppVersion {
private CheckForNewAppVersion() { } private CheckForNewAppVersion() { }
@ -176,6 +171,7 @@ public final class CheckForNewAppVersion {
@Nullable @Nullable
public static Disposable checkNewVersion(@NonNull final App app) { public static Disposable checkNewVersion(@NonNull final App app) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
final NewVersionManager manager = new NewVersionManager();
// Check if user has enabled/disabled update checking // Check if user has enabled/disabled update checking
// and if the current apk is a github one or not. // and if the current apk is a github one or not.
@ -183,31 +179,48 @@ public final class CheckForNewAppVersion {
return null; return null;
} }
return Maybe final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
.fromCallable(() -> { if (manager.isExpired(expiry)) {
if (!isConnected(app)) { return null;
return null; }
}
// Make a network request to get latest NewPipe data. return Maybe
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody(); .fromCallable(() -> {
}) if (!isConnected(app)) {
return null;
}
// Make a network request to get latest NewPipe data.
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
})
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
response -> { response -> {
try {
final long newExpiry = manager
.coerceExpiry(response.getHeader("expires"));
prefs.edit()
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
.apply();
} catch (final Exception e) {
if (DEBUG) {
Log.w(TAG, "Could not extract and save new expiry date", e);
}
}
// Parse the json from the response. // Parse the json from the response.
try { try {
final JsonObject githubStableObject = JsonParser.object() final JsonObject githubStableObject = JsonParser.object()
.from(response).getObject("flavors").getObject("github") .from(response.responseBody()).getObject("flavors")
.getObject("stable"); .getObject("github").getObject("stable");
final String versionName = githubStableObject final String versionName = githubStableObject
.getString("version"); .getString("version");
final int versionCode = githubStableObject final int versionCode = githubStableObject
.getInt("version_code"); .getInt("version_code");
final String apkLocationUrl = githubStableObject final String apkLocationUrl = githubStableObject
.getString("apk"); .getString("apk");
compareAppVersionAndShowNotification(app, versionName, compareAppVersionAndShowNotification(app, versionName,
apkLocationUrl, versionCode); apkLocationUrl, versionCode);

View File

@ -0,0 +1,28 @@
package org.schabi.newpipe
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class NewVersionManager {
fun isExpired(expiry: Long): Boolean {
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
}
/**
* Coerce expiry date time in between 6 hours and 72 hours from now
*
* @return Epoch second of expiry date time
*/
fun coerceExpiry(expiryString: String?): Long {
val now = ZonedDateTime.now()
return expiryString?.let {
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
expiry = maxOf(expiry, now.plusHours(6))
expiry = minOf(expiry, now.plusHours(72))
expiry.toEpochSecond()
} ?: now.plusHours(6).toEpochSecond()
}
}

View File

@ -342,6 +342,7 @@
<!-- Updates --> <!-- Updates -->
<string name="update_app_key" translatable="false">update_app_key</string> <string name="update_app_key" translatable="false">update_app_key</string>
<string name="update_pref_screen_key" translatable="false">update_pref_screen_key</string> <string name="update_pref_screen_key" translatable="false">update_pref_screen_key</string>
<string name="update_expiry_key" translatable="false">update_expiry_key</string>
<!-- Localizations --> <!-- Localizations -->
<string name="default_localization_key" translatable="false">system</string> <string name="default_localization_key" translatable="false">system</string>

View File

@ -0,0 +1,72 @@
package org.schabi.newpipe
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.math.abs
class NewVersionManagerTest {
private lateinit var manager: NewVersionManager
@Before
fun setup() {
manager = NewVersionManager()
}
@Test
fun `Expiry is reached`() {
val oneHourEarlier = Instant.now().atZone(ZoneId.of("GMT")).minusHours(1)
val expired = manager.isExpired(oneHourEarlier.toEpochSecond())
assertTrue(expired)
}
@Test
fun `Expiry is not reached`() {
val oneHourLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(1)
val expired = manager.isExpired(oneHourLater.toEpochSecond())
assertFalse(expired)
}
/**
* Equal within a range of 5 seconds
*/
private fun assertNearlyEqual(a: Long, b: Long) {
assertTrue(abs(a - b) < 5)
}
@Test
fun `Expiry must be returned as is because it is inside the acceptable range of 6-72 hours`() {
val sixHoursLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(6)
val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(sixHoursLater))
assertNearlyEqual(sixHoursLater.toEpochSecond(), coerced)
}
@Test
fun `Expiry must be increased to 6 hours if below`() {
val tooLow = Instant.now().atZone(ZoneId.of("GMT")).plusHours(5)
val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooLow))
assertNearlyEqual(tooLow.plusHours(1).toEpochSecond(), coerced)
}
@Test
fun `Expiry must be decreased to 72 hours if above`() {
val tooHigh = Instant.now().atZone(ZoneId.of("GMT")).plusHours(73)
val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooHigh))
assertNearlyEqual(tooHigh.minusHours(1).toEpochSecond(), coerced)
}
}