From 2926cb76823abf95881ee25cf331680988231acf Mon Sep 17 00:00:00 2001 From: XiangRongLin <41164160+XiangRongLin@users.noreply.github.com> Date: Sat, 23 Jan 2021 09:02:11 +0100 Subject: [PATCH] Respect expires header when checking for new version It was called to many times and acted similar to a DOS attack. --- .../schabi/newpipe/CheckForNewAppVersion.java | 63 +++++++++------- .../org/schabi/newpipe/NewVersionManager.kt | 28 ++++++++ app/src/main/res/values/settings_keys.xml | 1 + .../schabi/newpipe/NewVersionManagerTest.kt | 72 +++++++++++++++++++ 4 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/NewVersionManager.kt create mode 100644 app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index f8497ea27..630fb01ff 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -10,22 +10,19 @@ import android.content.pm.Signature; import android.net.ConnectivityManager; import android.net.Uri; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; - import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; - -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.ErrorInfo; -import org.schabi.newpipe.report.UserAction; - +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.security.MessageDigest; @@ -34,11 +31,9 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Maybe; -import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.schedulers.Schedulers; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.ErrorInfo; +import org.schabi.newpipe.report.UserAction; public final class CheckForNewAppVersion { private CheckForNewAppVersion() { } @@ -176,6 +171,7 @@ public final class CheckForNewAppVersion { @Nullable public static Disposable checkNewVersion(@NonNull final App app) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); + final NewVersionManager manager = new NewVersionManager(); // Check if user has enabled/disabled update checking // and if the current apk is a github one or not. @@ -183,31 +179,48 @@ public final class CheckForNewAppVersion { return null; } - return Maybe - .fromCallable(() -> { - if (!isConnected(app)) { - return null; - } + final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0); + if (manager.isExpired(expiry)) { + return null; + } - // Make a network request to get latest NewPipe data. - return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody(); - }) + return Maybe + .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()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( 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. try { final JsonObject githubStableObject = JsonParser.object() - .from(response).getObject("flavors").getObject("github") - .getObject("stable"); + .from(response.responseBody()).getObject("flavors") + .getObject("github").getObject("stable"); final String versionName = githubStableObject - .getString("version"); + .getString("version"); final int versionCode = githubStableObject - .getInt("version_code"); + .getInt("version_code"); final String apkLocationUrl = githubStableObject - .getString("apk"); + .getString("apk"); compareAppVersionAndShowNotification(app, versionName, apkLocationUrl, versionCode); diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt b/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt new file mode 100644 index 000000000..36de1ecfc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt @@ -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() + } +} diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 1c4ad11a3..011bd850a 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -342,6 +342,7 @@ update_app_key update_pref_screen_key + update_expiry_key system diff --git a/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt b/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt new file mode 100644 index 000000000..d2dacc783 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt @@ -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) + } +}