mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat: add in-app update flow and native e2e verification (#1112)
* feat(app-update): add in-app update flow with native e2e coverage * docs(ai-workflow): infer closed-issue labels automatically * fix(app-update): address native updater review findings * fix(android): validate updater redirect hosts * fix(electron): harden updater file names
This commit is contained in:
@@ -20,6 +20,8 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
@@ -29,6 +31,8 @@ import okhttp3.Response;
|
||||
public class AppUpdaterPlugin extends Plugin {
|
||||
private static final String TAG = "AppUpdaterPlugin";
|
||||
private static final String EMULATOR_HOST = "10.0.2.2";
|
||||
private static final String GITHUB_HOST = "github.com";
|
||||
private static final String GITHUB_CONTENT_HOST = "githubusercontent.com";
|
||||
|
||||
@PluginMethod
|
||||
public void downloadAndInstallUpdate(PluginCall call) {
|
||||
@@ -63,36 +67,59 @@ public class AppUpdaterPlugin extends Plugin {
|
||||
return (getContext().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
||||
}
|
||||
|
||||
private String sanitizeUrl(String value) {
|
||||
static boolean isAllowedDownloadUrl(
|
||||
String value, boolean allowGitHubContentHosts, boolean allowDebugHosts) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
String sanitized = value.trim();
|
||||
Uri uri = Uri.parse(sanitized);
|
||||
if (sanitized.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final URI uri;
|
||||
try {
|
||||
uri = URI.create(sanitized);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
String host = uri.getHost();
|
||||
|
||||
if (scheme == null || host == null) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
String normalizedScheme = scheme.toLowerCase();
|
||||
String normalizedHost = host.toLowerCase();
|
||||
String normalizedScheme = scheme.toLowerCase(Locale.ROOT);
|
||||
String normalizedHost = host.toLowerCase(Locale.ROOT);
|
||||
|
||||
if ("https".equals(normalizedScheme) && "github.com".equals(normalizedHost)) {
|
||||
return sanitized;
|
||||
if ("https".equals(normalizedScheme)
|
||||
&& (GITHUB_HOST.equals(normalizedHost)
|
||||
|| (allowGitHubContentHosts
|
||||
&& (GITHUB_CONTENT_HOST.equals(normalizedHost)
|
||||
|| normalizedHost.endsWith("." + GITHUB_CONTENT_HOST))))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isDebugBuild()
|
||||
if (allowDebugHosts
|
||||
&& ("http".equals(normalizedScheme) || "https".equals(normalizedScheme))
|
||||
&& (EMULATOR_HOST.equals(normalizedHost)
|
||||
|| "127.0.0.1".equals(normalizedHost)
|
||||
|| "localhost".equals(normalizedHost))) {
|
||||
return sanitized;
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private String sanitizeUrl(String value) {
|
||||
if (!isAllowedDownloadUrl(value, false, isDebugBuild())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String value) {
|
||||
@@ -126,6 +153,11 @@ public class AppUpdaterPlugin extends Plugin {
|
||||
throw new IOException("Unexpected response " + response.code());
|
||||
}
|
||||
|
||||
String finalUrl = response.request().url().toString();
|
||||
if (!isAllowedDownloadUrl(finalUrl, true, isDebugBuild())) {
|
||||
throw new IOException("Unexpected redirected download host");
|
||||
}
|
||||
|
||||
try (InputStream inputStream = response.body().byteStream();
|
||||
OutputStream outputStream = new java.io.FileOutputStream(tempFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package fivechan.android;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class AppUpdaterPluginTest {
|
||||
|
||||
@Test
|
||||
public void initialDownload_requiresGithubHost() {
|
||||
assertTrue(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://github.com/bitsocialnet/5chan/releases/download/v0.7.3/5chan.apk",
|
||||
false,
|
||||
false));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://objects.githubusercontent.com/github-production-release-asset.apk",
|
||||
false,
|
||||
false));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://evil.example/5chan.apk", false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redirectedDownload_allowsGithubContentHostsOnly() {
|
||||
assertTrue(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://objects.githubusercontent.com/github-production-release-asset.apk",
|
||||
true,
|
||||
false));
|
||||
assertTrue(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://release-assets.githubusercontent.com/file.apk", true, false));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://githubusercontent.evil.example/file.apk", true, false));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://evil.example/file.apk", true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void debugDownloads_allowOnlyLocalHosts() {
|
||||
assertTrue(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"http://10.0.2.2:56405/5chan.apk", false, true));
|
||||
assertTrue(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"https://localhost:56405/5chan.apk", false, true));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"http://192.168.1.8:56405/5chan.apk", false, true));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"http://10.0.2.2:56405/5chan.apk", false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedOrUnsupportedUrls_areRejected() {
|
||||
assertFalse(AppUpdaterPlugin.isAllowedDownloadUrl(null, true, true));
|
||||
assertFalse(AppUpdaterPlugin.isAllowedDownloadUrl("not a url", true, true));
|
||||
assertFalse(
|
||||
AppUpdaterPlugin.isAllowedDownloadUrl(
|
||||
"ftp://github.com/bitsocialnet/5chan/file.apk", true, true));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user