Skip to content

Commit 2926cb7

Browse files
committed
Respect expires header when checking for new version
It was called to many times and acted similar to a DOS attack.
1 parent cce896e commit 2926cb7

4 files changed

Lines changed: 139 additions & 25 deletions

File tree

app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,19 @@
1010
import android.net.ConnectivityManager;
1111
import android.net.Uri;
1212
import android.util.Log;
13-
1413
import androidx.annotation.NonNull;
1514
import androidx.annotation.Nullable;
1615
import androidx.core.app.NotificationCompat;
1716
import androidx.core.app.NotificationManagerCompat;
1817
import androidx.core.content.ContextCompat;
1918
import androidx.preference.PreferenceManager;
20-
2119
import com.grack.nanojson.JsonObject;
2220
import com.grack.nanojson.JsonParser;
2321
import com.grack.nanojson.JsonParserException;
24-
25-
import org.schabi.newpipe.report.ErrorActivity;
26-
import org.schabi.newpipe.report.ErrorInfo;
27-
import org.schabi.newpipe.report.UserAction;
28-
22+
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
23+
import io.reactivex.rxjava3.core.Maybe;
24+
import io.reactivex.rxjava3.disposables.Disposable;
25+
import io.reactivex.rxjava3.schedulers.Schedulers;
2926
import java.io.ByteArrayInputStream;
3027
import java.io.InputStream;
3128
import java.security.MessageDigest;
@@ -34,11 +31,9 @@
3431
import java.security.cert.CertificateException;
3532
import java.security.cert.CertificateFactory;
3633
import java.security.cert.X509Certificate;
37-
38-
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
39-
import io.reactivex.rxjava3.core.Maybe;
40-
import io.reactivex.rxjava3.disposables.Disposable;
41-
import io.reactivex.rxjava3.schedulers.Schedulers;
34+
import org.schabi.newpipe.report.ErrorActivity;
35+
import org.schabi.newpipe.report.ErrorInfo;
36+
import org.schabi.newpipe.report.UserAction;
4237

4338
public final class CheckForNewAppVersion {
4439
private CheckForNewAppVersion() { }
@@ -176,38 +171,56 @@ public static boolean isGithubApk(@NonNull final App app) {
176171
@Nullable
177172
public static Disposable checkNewVersion(@NonNull final App app) {
178173
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
174+
final NewVersionManager manager = new NewVersionManager();
179175

180176
// Check if user has enabled/disabled update checking
181177
// and if the current apk is a github one or not.
182178
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
183179
return null;
184180
}
185181

182+
final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0);
183+
if (manager.isExpired(expiry)) {
184+
return null;
185+
}
186+
186187
return Maybe
187-
.fromCallable(() -> {
188-
if (!isConnected(app)) {
189-
return null;
190-
}
191-
192-
// Make a network request to get latest NewPipe data.
193-
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
194-
})
188+
.fromCallable(() -> {
189+
if (!isConnected(app)) {
190+
return null;
191+
}
192+
193+
// Make a network request to get latest NewPipe data.
194+
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL);
195+
})
195196
.subscribeOn(Schedulers.io())
196197
.observeOn(AndroidSchedulers.mainThread())
197198
.subscribe(
198199
response -> {
200+
try {
201+
final long newExpiry = manager
202+
.coerceExpiry(response.getHeader("expires"));
203+
prefs.edit()
204+
.putLong(app.getString(R.string.update_expiry_key), newExpiry)
205+
.apply();
206+
} catch (final Exception e) {
207+
if (DEBUG) {
208+
Log.w(TAG, "Could not extract and save new expiry date", e);
209+
}
210+
}
211+
199212
// Parse the json from the response.
200213
try {
201214
final JsonObject githubStableObject = JsonParser.object()
202-
.from(response).getObject("flavors").getObject("github")
203-
.getObject("stable");
215+
.from(response.responseBody()).getObject("flavors")
216+
.getObject("github").getObject("stable");
204217

205218
final String versionName = githubStableObject
206-
.getString("version");
219+
.getString("version");
207220
final int versionCode = githubStableObject
208-
.getInt("version_code");
221+
.getInt("version_code");
209222
final String apkLocationUrl = githubStableObject
210-
.getString("apk");
223+
.getString("apk");
211224

212225
compareAppVersionAndShowNotification(app, versionName,
213226
apkLocationUrl, versionCode);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.schabi.newpipe
2+
3+
import java.time.Instant
4+
import java.time.ZonedDateTime
5+
import java.time.format.DateTimeFormatter
6+
7+
class NewVersionManager {
8+
9+
fun isExpired(expiry: Long): Boolean {
10+
return Instant.ofEpochSecond(expiry).isBefore(Instant.now())
11+
}
12+
13+
/**
14+
* Coerce expiry date time in between 6 hours and 72 hours from now
15+
*
16+
* @return Epoch second of expiry date time
17+
*/
18+
fun coerceExpiry(expiryString: String?): Long {
19+
val now = ZonedDateTime.now()
20+
return expiryString?.let {
21+
22+
var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString))
23+
expiry = maxOf(expiry, now.plusHours(6))
24+
expiry = minOf(expiry, now.plusHours(72))
25+
expiry.toEpochSecond()
26+
} ?: now.plusHours(6).toEpochSecond()
27+
}
28+
}

app/src/main/res/values/settings_keys.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@
342342
<!-- Updates -->
343343
<string name="update_app_key" translatable="false">update_app_key</string>
344344
<string name="update_pref_screen_key" translatable="false">update_pref_screen_key</string>
345+
<string name="update_expiry_key" translatable="false">update_expiry_key</string>
345346

346347
<!-- Localizations -->
347348
<string name="default_localization_key" translatable="false">system</string>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.schabi.newpipe
2+
3+
import org.junit.Assert.assertFalse
4+
import org.junit.Assert.assertTrue
5+
import org.junit.Before
6+
import org.junit.Test
7+
import java.time.Instant
8+
import java.time.ZoneId
9+
import java.time.format.DateTimeFormatter
10+
import kotlin.math.abs
11+
12+
class NewVersionManagerTest {
13+
14+
private lateinit var manager: NewVersionManager
15+
16+
@Before
17+
fun setup() {
18+
manager = NewVersionManager()
19+
}
20+
21+
@Test
22+
fun `Expiry is reached`() {
23+
val oneHourEarlier = Instant.now().atZone(ZoneId.of("GMT")).minusHours(1)
24+
25+
val expired = manager.isExpired(oneHourEarlier.toEpochSecond())
26+
27+
assertTrue(expired)
28+
}
29+
30+
@Test
31+
fun `Expiry is not reached`() {
32+
val oneHourLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(1)
33+
34+
val expired = manager.isExpired(oneHourLater.toEpochSecond())
35+
36+
assertFalse(expired)
37+
}
38+
39+
/**
40+
* Equal within a range of 5 seconds
41+
*/
42+
private fun assertNearlyEqual(a: Long, b: Long) {
43+
assertTrue(abs(a - b) < 5)
44+
}
45+
46+
@Test
47+
fun `Expiry must be returned as is because it is inside the acceptable range of 6-72 hours`() {
48+
val sixHoursLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(6)
49+
50+
val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(sixHoursLater))
51+
52+
assertNearlyEqual(sixHoursLater.toEpochSecond(), coerced)
53+
}
54+
55+
@Test
56+
fun `Expiry must be increased to 6 hours if below`() {
57+
val tooLow = Instant.now().atZone(ZoneId.of("GMT")).plusHours(5)
58+
59+
val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooLow))
60+
61+
assertNearlyEqual(tooLow.plusHours(1).toEpochSecond(), coerced)
62+
}
63+
64+
@Test
65+
fun `Expiry must be decreased to 72 hours if above`() {
66+
val tooHigh = Instant.now().atZone(ZoneId.of("GMT")).plusHours(73)
67+
68+
val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooHigh))
69+
70+
assertNearlyEqual(tooHigh.minusHours(1).toEpochSecond(), coerced)
71+
}
72+
}

0 commit comments

Comments
 (0)