Add support for fetching remote deprecation.

master
Greyson Parrelli 2020-09-08 18:03:56 -04:00 committed by GitHub
parent c946a7a1d5
commit 2784285d47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 559 additions and 39 deletions

View File

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
@ -70,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager;
@ -157,6 +159,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
checkBuildExpiration();
}
@Override
@ -192,6 +195,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
return persistentLogger;
}
public void checkBuildExpiration() {
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build expired!");
SignalStore.misc().markDeprecated();
}
}
private void initializeSecurityProvider() {
try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");

View File

@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.Util;
public class ExpiredBuildReminder extends Reminder {
@ -20,7 +20,7 @@ public class ExpiredBuildReminder extends Reminder {
}
public static boolean isEligible() {
return Util.getDaysTillBuildExpiry() <= 0;
return SignalStore.misc().isClientDeprecated();
}
}

View File

@ -6,6 +6,8 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.concurrent.TimeUnit;
public class OutdatedBuildReminder extends Reminder {
public OutdatedBuildReminder(final Context context) {
@ -15,7 +17,7 @@ public class OutdatedBuildReminder extends Reminder {
}
private static CharSequence getPluralsText(final Context context) {
int days = Util.getDaysTillBuildExpiry() - 1;
int days = getDaysUntilExpiry() - 1;
if (days == 0) {
return context.getString(R.string.reminder_header_outdated_build_details_today);
}
@ -28,7 +30,10 @@ public class OutdatedBuildReminder extends Reminder {
}
public static boolean isEligible() {
return Util.getDaysTillBuildExpiry() <= 10;
return getDaysUntilExpiry() <= 10;
}
private static int getDaysUntilExpiry() {
return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry());
}
}

View File

@ -8,9 +8,8 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.Util;
import java.util.List;
@ -26,7 +25,7 @@ public abstract class SendJob extends BaseJob {
@Override
public final void onRun() throws Exception {
if (Util.getDaysTillBuildExpiry() <= 0) {
if (SignalStore.misc().isClientDeprecated()) {
throw new TextSecureExpiredException(String.format("TextSecure expired (build %d, now %d)",
BuildConfig.BUILD_TIMESTAMP,
System.currentTimeMillis()));

View File

@ -8,6 +8,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time";
private static final String USERNAME_SHOW_REMINDER = "username.show.reminder";
private static final String CLIENT_DEPRECATED = "misc.client_deprecated";
MiscellaneousValues(@NonNull KeyValueStore store) {
super(store);
@ -45,4 +46,12 @@ public final class MiscellaneousValues extends SignalStoreValues {
public boolean shouldShowUsernameReminder() {
return getBoolean(USERNAME_SHOW_REMINDER, true);
}
public boolean isClientDeprecated() {
return getBoolean(CLIENT_DEPRECATED, false);
}
public void markDeprecated() {
putBoolean(CLIENT_DEPRECATED, true);
}
}

View File

@ -17,6 +17,7 @@ import com.google.android.collect.Sets;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.OptionalUtil;
@ -203,18 +204,11 @@ public final class LinkPreviewUtil {
@SuppressLint("ObsoleteSdkInt")
public long getDate() {
SimpleDateFormat format;
if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
} else {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
}
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
values.get(KEY_PUBLISHED_TIME_2),
values.get(KEY_MODIFIED_TIME_1),
values.get(KEY_MODIFIED_TIME_2))
.map(dateString -> parseDate(format, dateString))
.map(DateUtils::parseIso8601)
.filter(time -> time > 0)
.findFirst()
.orElse(0L);
@ -223,19 +217,6 @@ public final class LinkPreviewUtil {
public @NonNull Optional<String> getDescription() {
return OptionalUtil.absentIfEmpty(values.get(KEY_DESCRIPTION_URL));
}
private static long parseDate(DateFormat dateFormat, String dateString) {
if (Util.isEmpty(dateString)) {
return 0;
}
try {
return dateFormat.parse(dateString).getTime();
} catch (ParseException e) {
Log.w(TAG, "Failed to parse date.", e);
return 0;
}
}
}
public interface HtmlDecoder {

View File

@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.net;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* Disallows network requests when your client has been deprecated. When the client is deprecated,
* we simply fake a 499 response.
*/
public final class DeprecatedClientPreventionInterceptor implements Interceptor {
private static final String TAG = Log.tag(DeprecatedClientPreventionInterceptor.class);
@Override
public @NonNull Response intercept(@NonNull Chain chain) throws IOException {
if (SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Preventing request because client is deprecated.");
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.receivedResponseAtMillis(System.currentTimeMillis())
.message("")
.body(ResponseBody.create(null, ""))
.code(499)
.build();
} else {
return chain.proceed(chain.request());
}
}
}

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.net;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Response;
/**
* Marks the client as remotely-deprecated when it receives a 499 response.
*/
public final class RemoteDeprecationDetectorInterceptor implements Interceptor {
private static final String TAG = Log.tag(RemoteDeprecationDetectorInterceptor.class);
@Override
public @NonNull Response intercept(@NonNull Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
if (response.code() == 499 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Received 499. Client version is deprecated.");
SignalStore.misc().markDeprecated();
}
return response;
}
}

View File

@ -10,6 +10,6 @@ import org.thoughtcrime.securesms.BuildConfig;
public class StandardUserAgentInterceptor extends UserAgentInterceptor {
public StandardUserAgentInterceptor() {
super("Signal-Android " + BuildConfig.VERSION_NAME + " (API " + Build.VERSION.SDK_INT + ")");
super("Signal-Android/" + BuildConfig.VERSION_NAME + " Android/" + Build.VERSION.SDK_INT);
}
}

View File

@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.net.CustomDns;
import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor;
import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor;
import org.thoughtcrime.securesms.net.SequentialDns;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.util.Base64;
@ -21,6 +23,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -161,7 +164,7 @@ public class SignalServiceNetworkAccess {
final SignalStorageUrl omanGoogleStorage = new SignalStorageUrl("https://www.google.com.om/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
final List<Interceptor> interceptors = Collections.singletonList(new StandardUserAgentInterceptor());
final List<Interceptor> interceptors = Arrays.asList(new StandardUserAgentInterceptor(), new RemoteDeprecationDetectorInterceptor(), new DeprecatedClientPreventionInterceptor());
final Optional<Dns> dns = Optional.of(DNS);
final byte[] zkGroupServerPublicParams;

View File

@ -16,13 +16,18 @@
*/
package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.text.format.DateFormat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
@ -170,4 +175,33 @@ public class DateUtils extends android.text.format.DateUtils {
private static String getLocalizedPattern(String template, Locale locale) {
return DateFormat.getBestDateTimePattern(locale, template);
}
/**
* e.g. 2020-09-04T19:17:51Z
* https://www.iso.org/iso-8601-date-and-time-format.html
*
* Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences.
*
* @return The timestamp if able to be parsed, otherwise -1.
*/
@SuppressLint("ObsoleteSdkInt")
public static long parseIso8601(@Nullable String date) {
SimpleDateFormat format;
if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
} else {
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
}
if (Util.isEmpty(date)) {
return -1;
}
try {
return format.parse(date).getTime();
} catch (ParseException e) {
Log.w(TAG, "Failed to parse date.", e);
return -1;
}
}
}

View File

@ -64,6 +64,7 @@ public final class FeatureFlags {
private static final String MENTIONS = "android.mentions";
private static final String VERIFY_V2 = "android.verifyV2";
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -82,7 +83,8 @@ public final class FeatureFlags {
INTERNAL_USER,
USERNAMES,
MENTIONS,
VERIFY_V2
VERIFY_V2,
CLIENT_EXPIRATION
);
/**
@ -107,7 +109,8 @@ public final class FeatureFlags {
GROUPS_V2_CREATE_VERSION,
GROUPS_V2_JOIN_VERSION,
VERIFY_V2,
CDS_VERSION
CDS_VERSION,
CLIENT_EXPIRATION
);
/**
@ -280,6 +283,11 @@ public final class FeatureFlags {
return getBoolean(VERIFY_V2, false);
}
/** The raw client expiration JSON string. */
public static String clientExpiration() {
return getString(CLIENT_EXPIRATION, null);
}
/**
* Whether the user can choose phone number privacy settings, and;
* Whether to fetch and store the secondary certificate
@ -463,6 +471,20 @@ public final class FeatureFlags {
return defaultValue;
}
private static String getString(@NonNull String key, String defaultValue) {
String forced = (String) FORCED_VALUES.get(key);
if (forced != null) {
return forced;
}
Object remote = REMOTE_VALUES.get(key);
if (remote instanceof String) {
return (String) remote;
}
return defaultValue;
}
private static Map<String, Object> parseStoredConfig(String stored) {
Map<String, Object> parsed = new HashMap<>();
@ -511,14 +533,11 @@ public final class FeatureFlags {
}
}
private static final class MissingFlagRequirementError extends Error {
}
@VisibleForTesting
static final class UpdateResult {
private final Map<String, Object> memory;
private final Map<String, Object> disk;
private final Map<String, Change> memoryChanges;
private final Map<String, Change> memoryChanges;
UpdateResult(@NonNull Map<String, Object> memory, @NonNull Map<String, Object> disk, @NonNull Map<String, Change> memoryChanges) {
this.memory = memory;

View File

@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Stream;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class RemoteDeprecation {
private static final String TAG = Log.tag(RemoteDeprecation.class);
private RemoteDeprecation() { }
/**
* @return The amount of time (in milliseconds) until this client version expires, or -1 if
* there's no pending expiration.
*/
public static long getTimeUntilDeprecation() {
return getTimeUntilDeprecation(FeatureFlags.clientExpiration(), System.currentTimeMillis(), BuildConfig.VERSION_NAME);
}
/**
* @return The amount of time (in milliseconds) until this client version expires, or -1 if
* there's no pending expiration.
*/
@VisibleForTesting
static long getTimeUntilDeprecation(String json, long currentTime, @NonNull String currentVersion) {
if (Util.isEmpty(json)) {
return -1;
}
try {
SemanticVersion ourVersion = Objects.requireNonNull(SemanticVersion.parse(currentVersion));
ClientExpiration[] expirations = JsonUtils.fromJson(json, ClientExpiration[].class);
ClientExpiration expiration = Stream.of(expirations)
.filter(c -> c.getVersion() != null && c.getExpiration() != -1)
.filter(c -> c.requireVersion().compareTo(ourVersion) > 0)
.sortBy(ClientExpiration::getExpiration)
.findFirst()
.orElse(null);
if (expiration != null) {
return Math.max(expiration.getExpiration() - currentTime, 0);
}
} catch (IOException e) {
Log.w(TAG, e);
}
return -1;
}
private static final class ClientExpiration {
@JsonProperty
private final String minVersion;
@JsonProperty
private final String iso8601;
ClientExpiration(@Nullable @JsonProperty("minVersion") String minVersion,
@Nullable @JsonProperty("iso8601") String iso8601)
{
this.minVersion = minVersion;
this.iso8601 = iso8601;
}
public @Nullable SemanticVersion getVersion() {
return SemanticVersion.parse(minVersion);
}
public @NonNull SemanticVersion requireVersion() {
return Objects.requireNonNull(getVersion());
}
public long getExpiration() {
return DateUtils.parseIso8601(iso8601);
}
}
}

View File

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.ComparatorCompat;
import java.util.Comparator;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class SemanticVersion implements Comparable<SemanticVersion> {
private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)$");
private static final Comparator<SemanticVersion> MAJOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.major, s2.major);
private static final Comparator<SemanticVersion> MINOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.minor, s2.minor);
private static final Comparator<SemanticVersion> PATCH_COMPARATOR = (s1, s2) -> Integer.compare(s1.patch, s2.patch);
private static final Comparator<SemanticVersion> COMPARATOR = ComparatorCompat.chain(MAJOR_COMPARATOR)
.thenComparing(MINOR_COMPARATOR)
.thenComparing(PATCH_COMPARATOR);
private final int major;
private final int minor;
private final int patch;
public SemanticVersion(int major, int minor, int patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
}
public static @Nullable SemanticVersion parse(@Nullable String value) {
if (value == null) {
return null;
}
Matcher matcher = VERSION_PATTERN.matcher(value);
if (Util.isEmpty(value) || !matcher.matches()) {
return null;
}
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
int patch = Integer.parseInt(matcher.group(3));
return new SemanticVersion(major, minor, patch);
}
@Override
public int compareTo(SemanticVersion other) {
return COMPARATOR.compare(this, other);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SemanticVersion that = (SemanticVersion) o;
return major == that.major &&
minor == that.minor &&
patch == that.patch;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch);
}
}

View File

@ -49,6 +49,7 @@ import com.google.i18n.phonenumbers.Phonenumber;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.whispersystems.libsignal.util.guava.Optional;
@ -77,6 +78,8 @@ import java.util.concurrent.TimeUnit;
public class Util {
private static final String TAG = Util.class.getSimpleName();
private static final long BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90);
private static volatile Handler handler;
public static <T> List<T> asList(T... elements) {
@ -458,9 +461,25 @@ public class Util {
return secret;
}
public static int getDaysTillBuildExpiry() {
int age = (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP);
return 90 - age;
/**
* @return The amount of time (in ms) until this build of Signal will be considered 'expired'.
* Takes into account both the build age as well as any remote deprecation values.
*/
public static long getTimeUntilBuildExpiry() {
if (SignalStore.misc().isClientDeprecated()) {
return 0;
}
long buildAge = System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP;
long timeUntilBuildDeprecation = BUILD_LIFESPAN - buildAge;
long timeUntilRemoteDeprecation = RemoteDeprecation.getTimeUntilDeprecation();
if (timeUntilRemoteDeprecation != -1) {
long timeUntilDeprecation = Math.min(timeUntilBuildDeprecation, timeUntilRemoteDeprecation);
return Math.max(timeUntilDeprecation, 0);
} else {
return Math.max(timeUntilBuildDeprecation, 0);
}
}
@TargetApi(VERSION_CODES.LOLLIPOP)

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.testutil;
import org.thoughtcrime.securesms.logging.Log;
public class EmptyLogger extends Log.Logger {
@Override
public void v(String tag, String message, Throwable t) { }
@Override
public void d(String tag, String message, Throwable t) { }
@Override
public void i(String tag, String message, Throwable t) { }
@Override
public void w(String tag, String message, Throwable t) { }
@Override
public void e(String tag, String message, Throwable t) { }
@Override
public void wtf(String tag, String message, Throwable t) { }
@Override
public void blockUntilAllWritesFinished() { }
}

View File

@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.util;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.testutil.EmptyLogger;
import java.util.Arrays;
import java.util.Collection;
import static junit.framework.TestCase.assertEquals;
@RunWith(Parameterized.class)
public class RemoteExpirationTest_getTimeUntilDeprecation {
private final String json;
private final long currentDate;
private final String currentVersion;
private final long timeUntilExpiration;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
// Null json, invalid
{ null, DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", -1 },
// Empty json, no expiration
{ "[]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", -1 },
// Badly formatted minVersion, no expiration
{ "[ {\"minVersion\": \"1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// Badly formatted date, no expiration
{ "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"20-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// Missing minVersion, no expiration
{ "[ {\"iso8601\": \"20-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// Missing date, no expiration
{ "[ {\"minVersion\": \"1.1.1\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// Missing expiration and date, no expiration
{ "[ {} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// Invalid inner object, no expiration
{ "[ { \"a\": 1 } ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// Invalid json, no expiration
{ "[ {", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// We meet the min version, no expiration
{ "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 },
// We exceed the min version, no expiration
{ "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.2", -1 },
// We expire in 1 second
{ "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", 1000 },
// We have already expired
{ "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:02Z"), "1.1.0", 0 },
// Use the closest expiration when multiple ones are listed
{ "[ {\"minVersion\": \"1.1.2\", \"iso8601\": \"2020-02-01T00:00:00Z\"}," +
"{\"minVersion\": \"1.1.3\", \"iso8601\": \"2020-03-01T00:00:00Z\"}," +
"{\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", 1000 },
});
}
public RemoteExpirationTest_getTimeUntilDeprecation(String json, long currentDate, String currentVersion, long timeUntilExpiration) {
this.json = json;
this.currentDate = currentDate;
this.currentVersion = currentVersion;
this.timeUntilExpiration = timeUntilExpiration;
}
@BeforeClass
public static void setup() {
Log.initialize(new EmptyLogger());
}
@Test
public void getTimeUntilExpiration() {
assertEquals(timeUntilExpiration, RemoteDeprecation.getTimeUntilDeprecation(json, currentDate, currentVersion));
}
}

View File

@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import static junit.framework.TestCase.assertEquals;
@RunWith(Parameterized.class)
public class SemanticVersionTest_compareTo {
private final SemanticVersion first;
private final SemanticVersion second;
private final int output;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{ new SemanticVersion(1, 0, 0), new SemanticVersion(0, 1, 0), 1 },
{ new SemanticVersion(1, 0, 0), new SemanticVersion(0, 0, 1), 1 },
{ new SemanticVersion(1, 0, 0), new SemanticVersion(0, 0, 0), 1 },
{ new SemanticVersion(0, 1, 0), new SemanticVersion(0, 0, 1), 1 },
{ new SemanticVersion(0, 1, 0), new SemanticVersion(0, 0, 0), 1 },
{ new SemanticVersion(0, 0, 1), new SemanticVersion(0, 0, 0), 1 },
{ new SemanticVersion(1, 1, 0), new SemanticVersion(1, 0, 0), 1 },
{ new SemanticVersion(1, 1, 1), new SemanticVersion(1, 1, 0), 1 },
{ new SemanticVersion(0, 0, 1), new SemanticVersion(1, 0, 0), -1 },
{ new SemanticVersion(1, 1, 1), new SemanticVersion(1, 1, 1), 0 },
{ new SemanticVersion(0, 0, 0), new SemanticVersion(0, 0, 0), 0 },
});
}
public SemanticVersionTest_compareTo(SemanticVersion first, SemanticVersion second, int output) {
this.first = first;
this.second = second;
this.output = output;
}
@Test
public void compareTo() {
assertEquals(output, first.compareTo(second));
}
}

View File

@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import java.util.Arrays;
import java.util.Collection;
import static junit.framework.TestCase.assertEquals;
@RunWith(Parameterized.class)
public class SemanticVersionTest_parse {
private final String input;
private final SemanticVersion output;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{ "0.0.0", new SemanticVersion(0, 0, 0)},
{ "1.2.3", new SemanticVersion(1, 2, 3)},
{ "111.222.333", new SemanticVersion(111, 222, 333)},
{ "v1.2.3", null },
{ "1.2.3x", null },
{ "peter.ben.parker", null },
{ "", null}
});
}
public SemanticVersionTest_parse(String input, SemanticVersion output) {
this.input = input;
this.output = output;
}
@Test
public void parse() {
assertEquals(output, SemanticVersion.parse(input));
}
}

View File

@ -0,0 +1,4 @@
package org.whispersystems.signalservice.api.push.exceptions;
public class DeprecatedVersionException extends NonSuccessfulResponseCodeException {
}

View File

@ -49,6 +49,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
import org.whispersystems.signalservice.api.push.exceptions.ContactManifestMismatchException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
@ -1459,6 +1460,8 @@ public class PushServiceSocket {
throw new LockedException(accountLockFailure.length,
accountLockFailure.timeRemaining,
basicStorageCredentials);
case 499:
throw new DeprecatedVersionException();
}
if (responseCode != 200 && responseCode != 204) {
@ -1688,6 +1691,8 @@ public class PushServiceSocket {
}
case 429:
throw new RateLimitException("Rate limit exceeded: " + response.code());
case 499:
throw new DeprecatedVersionException();
}
throw new NonSuccessfulResponseCodeException("Response: " + response);