From 3d479b29e5f809dfc1985d3ca2ea1be4b6977ab7 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 4 Jan 2022 16:37:51 -0300 Subject: [PATCH] Initial work on separate download library --- app/build.gradle | 1 + .../fdroid/fdroid/net/HttpDownloaderTest.java | 2 +- .../java/org/fdroid/fdroid/FDroidApp.java | 14 +- .../org/fdroid/fdroid/net/HttpDownloader.java | 140 ++------ .../org/fdroid/fdroid/net/HttpPoster.java | 17 + .../fdroid/fdroid/nearby/LocalHTTPDTest.java | 4 +- download/.gitignore | 1 + download/build.gradle | 90 ++++++ download/src/androidMain/AndroidManifest.xml | 2 + .../org/fdroid/download/JvmDownloadManager.kt | 51 +++ .../org/fdroid/download/DownloadManager.kt | 104 ++++++ .../org/fdroid/download/DownloadRequest.kt | 11 + .../kotlin/org/fdroid/download/HeadInfo.kt | 7 + gradle.properties | 5 + gradle/verification-keyring.gpg | Bin 268207 -> 272152 bytes gradle/verification-metadata.xml | 298 +++++++++++++++++- settings.gradle | 1 + 17 files changed, 628 insertions(+), 120 deletions(-) create mode 100644 download/.gitignore create mode 100644 download/build.gradle create mode 100644 download/src/androidMain/AndroidManifest.xml create mode 100644 download/src/commonJvmAndroid/kotlin/org/fdroid/download/JvmDownloadManager.kt create mode 100644 download/src/commonMain/kotlin/org/fdroid/download/DownloadManager.kt create mode 100644 download/src/commonMain/kotlin/org/fdroid/download/DownloadRequest.kt create mode 100644 download/src/commonMain/kotlin/org/fdroid/download/HeadInfo.kt diff --git a/app/build.gradle b/app/build.gradle index e4725a811..cd2cdeb6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,6 +149,7 @@ android { } dependencies { + implementation project(":download") implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.gridlayout:gridlayout:1.0.0' diff --git a/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java b/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java index 4356f4564..8f0422076 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java @@ -28,7 +28,7 @@ public class HttpDownloaderTest { ArrayList tempUrls = new ArrayList<>(Arrays.asList( "https://f-droid.org/repo/index-v1.jar", // sites that use SNI for HTTPS - "https://mirrors.kernel.org/debian/dists/stable/Release", + "https://mirrors.edge.kernel.org/debian/dists/stable/Release", "https://fdroid.tetaneutral.net/fdroid/repo/index-v1.jar", "https://ftp.fau.de/fdroid/repo/index-v1.jar", //"https://microg.org/fdroid/repo/index-v1.jar", diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 4c568b875..2677c3c05 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -64,7 +64,6 @@ import org.fdroid.fdroid.nearby.SDCardScannerService; import org.fdroid.fdroid.nearby.WifiStateChangeService; import org.fdroid.fdroid.net.ConnectivityMonitorService; import org.fdroid.fdroid.net.Downloader; -import org.fdroid.fdroid.net.HttpDownloader; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.work.CleanCacheWorker; @@ -126,6 +125,9 @@ public class FDroidApp extends Application implements androidx.work.Configuratio public static final SubnetUtils.SubnetInfo UNSET_SUBNET_INFO = new SubnetUtils("0.0.0.0/32").getInfo(); + @Nullable + public static volatile String queryString; + private static volatile LongSparseArray lastWorkingMirrorArray = new LongSparseArray<>(1); private static volatile int numTries = Integer.MAX_VALUE; private static volatile int timeout = Downloader.DEFAULT_TIMEOUT; @@ -468,8 +470,8 @@ public class FDroidApp extends Application implements androidx.work.Configuratio final String queryStringKey = "http-downloader-query-string"; if (preferences.sendVersionAndUUIDToServers()) { - HttpDownloader.queryString = atStartTime.getString(queryStringKey, null); - if (HttpDownloader.queryString == null) { + queryString = atStartTime.getString(queryStringKey, null); + if (queryString == null) { UUID uuid = UUID.randomUUID(); ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE * 2); buffer.putLong(uuid.getMostSignificantBits()); @@ -481,10 +483,8 @@ public class FDroidApp extends Application implements androidx.work.Configuratio if (versionName != null) { builder.append("&client_version=").append(versionName); } - HttpDownloader.queryString = builder.toString(); - } - if (!atStartTime.contains(queryStringKey)) { - atStartTime.edit().putString(queryStringKey, HttpDownloader.queryString).apply(); + queryString = builder.toString(); + atStartTime.edit().putString(queryStringKey, queryString).apply(); } } else { atStartTime.edit().remove(queryStringKey).apply(); diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 4c2705954..1a2ba2322 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -24,24 +24,22 @@ package org.fdroid.fdroid.net; import android.annotation.TargetApi; import android.net.Uri; import android.os.Build; -import android.text.TextUtils; -import android.util.Base64; import org.apache.commons.io.FileUtils; +import org.fdroid.download.DownloadRequest; +import org.fdroid.download.HeadInfo; +import org.fdroid.download.JvmDownloadManager; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; import java.io.BufferedInputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.MalformedURLException; -import java.net.SocketTimeoutException; import java.net.URL; - -import info.guardianproject.netcipher.NetCipher; +import java.util.Collections; +import java.util.List; /** * Download files over HTTP, with support for proxies, {@code .onion} addresses, @@ -53,22 +51,16 @@ import info.guardianproject.netcipher.NetCipher; public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; - public static final String HEADER_FIELD_ETAG = "ETag"; - + private final JvmDownloadManager downloadManager = + new JvmDownloadManager(Utils.getUserAgent(), FDroidApp.queryString); private final String username; private final String password; - private URL sourceUrl; - private HttpURLConnection connection; + private final URL sourceUrl; + private boolean newFileAvailableOnServer; - private long fileFullSize = -1L; - /** - * String to append to all HTTP downloads, created in {@link FDroidApp#onCreate()} - */ - public static String queryString; - HttpDownloader(Uri uri, File destFile) - throws FileNotFoundException, MalformedURLException { + HttpDownloader(Uri uri, File destFile) throws MalformedURLException { this(uri, destFile, null, null); } @@ -83,7 +75,7 @@ public class HttpDownloader extends Downloader { * @throws MalformedURLException */ HttpDownloader(Uri uri, File destFile, String username, String password) - throws FileNotFoundException, MalformedURLException { + throws MalformedURLException { super(uri, destFile); this.sourceUrl = new URL(urlString); this.username = username; @@ -92,8 +84,10 @@ public class HttpDownloader extends Downloader { @Override protected InputStream getDownloadersInputStream() throws IOException { - setupConnection(false); - return new BufferedInputStream(connection.getInputStream()); + List mirrors = Collections.singletonList(""); // TODO get real mirrors here + DownloadRequest request = new DownloadRequest(urlString, mirrors, username, password, isSwapUrl(sourceUrl)); + // TODO why do we need to wrap this in a BufferedInputStream here? + return new BufferedInputStream(downloadManager.getBlocking(request)); } /** @@ -130,115 +124,47 @@ public class HttpDownloader extends Downloader { */ @Override public void download() throws IOException, InterruptedException { - // get the file size from the server - HttpURLConnection tmpConn = getConnection(); - tmpConn.setRequestMethod("HEAD"); - - int contentLength = -1; - int statusCode = tmpConn.getResponseCode(); - tmpConn.disconnect(); - newFileAvailableOnServer = false; - switch (statusCode) { - case HttpURLConnection.HTTP_OK: - String headETag = tmpConn.getHeaderField(HEADER_FIELD_ETAG); - contentLength = tmpConn.getContentLength(); - fileFullSize = contentLength; - if (!TextUtils.isEmpty(cacheTag)) { - if (cacheTag.equals(headETag)) { - Utils.debugLog(TAG, urlString + " cached, not downloading: " + headETag); - return; - } else { - String calcedETag = String.format("\"%x-%x\"", - tmpConn.getLastModified() / 1000, contentLength); - if (cacheTag.equals(calcedETag)) { - Utils.debugLog(TAG, urlString + " cached based on calced ETag, not downloading: " + - calcedETag); - return; - } - } - } - newFileAvailableOnServer = true; - break; - case HttpURLConnection.HTTP_NOT_FOUND: - notFound = true; - return; - default: - Utils.debugLog(TAG, "HEAD check of " + urlString + " returned " + statusCode + ": " - + tmpConn.getResponseMessage()); + boolean isSwap = isSwapUrl(sourceUrl); + DownloadRequest request = + new DownloadRequest(urlString, Collections.singletonList(""), username, password, isSwap); + HeadInfo headInfo = downloadManager.headBlocking(request, cacheTag); + fileFullSize = headInfo.getContentLength() == null ? -1 : headInfo.getContentLength(); + if (!headInfo.getETagChanged()) { + // ETag has not changed, don't download again + Utils.debugLog(TAG, urlString + " cached, not downloading."); + newFileAvailableOnServer = false; + return; } + newFileAvailableOnServer = true; boolean resumable = false; long fileLength = outputFile.length(); - if (fileLength > contentLength) { + if (fileLength > fileFullSize) { FileUtils.deleteQuietly(outputFile); - } else if (fileLength == contentLength && outputFile.isFile()) { + } else if (fileLength == fileFullSize && outputFile.isFile()) { + Utils.debugLog(TAG, "Already have outputFile, not download. " + outputFile.getAbsolutePath()); return; // already have it! } else if (fileLength > 0) { resumable = true; } - setupConnection(resumable); Utils.debugLog(TAG, "downloading " + urlString + " (is resumable: " + resumable + ")"); downloadFromStream(resumable); - cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG); } public static boolean isSwapUrl(Uri uri) { return isSwapUrl(uri.getHost(), uri.getPort()); } - public static boolean isSwapUrl(URL url) { + static boolean isSwapUrl(URL url) { return isSwapUrl(url.getHost(), url.getPort()); } - public static boolean isSwapUrl(String host, int port) { + static boolean isSwapUrl(String host, int port) { return port > 1023 // only root can use <= 1023, so never a swap repo && host.matches("[0-9.]+") // host must be an IP address && FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are } - HttpURLConnection getConnection() throws SocketTimeoutException, IOException { - HttpURLConnection connection; - if (isSwapUrl(sourceUrl)) { - // swap never works with a proxy, its unrouted IP on the same subnet - connection = (HttpURLConnection) sourceUrl.openConnection(); - connection.setRequestProperty("Connection", "Close"); // avoid keep-alive - } else { - if (queryString != null) { - connection = NetCipher.getHttpURLConnection(new URL(urlString + "?" + queryString)); - } else { - connection = NetCipher.getHttpURLConnection(sourceUrl); - } - } - - connection.setRequestProperty("User-Agent", Utils.getUserAgent()); - connection.setConnectTimeout(getTimeout()); - connection.setReadTimeout(getTimeout()); - - if (Build.VERSION.SDK_INT < 19) { // gzip encoding can be troublesome on old Androids - connection.setRequestProperty("Accept-Encoding", "identity"); - } - - if (username != null && password != null) { - // add authorization header from username / password if set - String authString = username + ":" + password; - connection.setRequestProperty("Authorization", "Basic " - + Base64.encodeToString(authString.getBytes(), Base64.NO_WRAP)); - } - return connection; - } - - private void setupConnection(boolean resumable) throws IOException { - if (connection != null) { - return; - } - connection = getConnection(); - - if (resumable) { - // partial file exists, resume the download - connection.setRequestProperty("Range", "bytes=" + outputFile.length() + "-"); - } - } - // Testing in the emulator for me, showed that figuring out the // filesize took about 1 to 1.5 seconds. // To put this in context, downloading a repo of: @@ -264,8 +190,6 @@ public class HttpDownloader extends Downloader { @Override public void close() { - if (connection != null) { - connection.disconnect(); - } + // TODO abort ongoing download somehow } } diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpPoster.java b/app/src/main/java/org/fdroid/fdroid/net/HttpPoster.java index 52a194177..226922f04 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpPoster.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpPoster.java @@ -2,6 +2,9 @@ package org.fdroid.fdroid.net; import android.net.Uri; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Utils; + import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; @@ -10,6 +13,9 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URL; + +import info.guardianproject.netcipher.NetCipher; /** * HTTP POST a JSON string to the URL configured in the constructor. @@ -44,4 +50,15 @@ public class HttpPoster extends HttpDownloader { throw new IOException("HTTP POST failed with " + statusCode + " " + connection.getResponseMessage()); } } + + private HttpURLConnection getConnection() throws IOException { + HttpURLConnection connection; + if (FDroidApp.queryString != null) { + connection = NetCipher.getHttpURLConnection(new URL(urlString + "?" + FDroidApp.queryString)); + } else { + connection = NetCipher.getHttpURLConnection(new URL(urlString)); + } + connection.setRequestProperty("User-Agent", Utils.getUserAgent()); + return connection; + } } diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java b/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java index fbd0e9170..edec9f894 100644 --- a/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java +++ b/app/src/testFull/java/org/fdroid/fdroid/nearby/LocalHTTPDTest.java @@ -75,6 +75,8 @@ import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) public class LocalHTTPDTest { + private static final String HEADER_FIELD_ETAG = "ETag"; + private static ClassLoader classLoader; private static LocalHTTPD localHttpd; private static Thread serverStartThread; @@ -217,7 +219,7 @@ public class LocalHTTPDTest { assertEquals(indexFile.length(), connection.getContentLength()); assertNotEquals(0, connection.getContentLength()); - String etag = connection.getHeaderField(HttpDownloader.HEADER_FIELD_ETAG); + String etag = connection.getHeaderField(HEADER_FIELD_ETAG); assertFalse(TextUtils.isEmpty(etag)); assertEquals(200, connection.getResponseCode()); diff --git a/download/.gitignore b/download/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/download/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/download/build.gradle b/download/build.gradle new file mode 100644 index 000000000..9fad9fed0 --- /dev/null +++ b/download/build.gradle @@ -0,0 +1,90 @@ +plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'com.android.library' +} + +group = 'org.fdroid' +version = '0.1' + +kotlin { + jvm { + compilations.all { + kotlinOptions.jvmTarget = '1.8' + } + testRuns["test"].executionTask.configure { + useJUnit() + } + } + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } + def hostOs = System.getProperty("os.name") + def isMingwX64 = hostOs.startsWith("Windows") + def nativeTarget + if (hostOs == "Mac OS X") nativeTarget = macosX64('native') + else if (hostOs == "Linux") nativeTarget = linuxX64("native") + else if (isMingwX64) nativeTarget = mingwX64("native") + else throw new GradleException("Host OS is not supported in Kotlin/Native.") + + ext { + ktor_version = "2.0.0-beta-1" + } + + android() + sourceSets { + commonMain { + dependencies { + implementation "io.ktor:ktor-client-core:$ktor_version" + } + } + commonTest { + dependencies { + implementation kotlin('test') + } + } + jvmMain { + kotlin.srcDir('src/commonJvmAndroid/kotlin') + dependencies { + implementation "io.ktor:ktor-client-cio:$ktor_version" + } + } + jvmTest { + + } + androidMain { + kotlin.srcDir('src/commonJvmAndroid/kotlin') + dependencies { + implementation "io.ktor:ktor-client-okhttp:$ktor_version" + } + } + androidTest { + dependencies { + implementation 'junit:junit:4.13.2' + } + } + nativeMain { + dependencies { + implementation "io.ktor:ktor-client-curl:$ktor_version" + } + } + nativeTest { + + } + } +} + +android { + compileSdkVersion 30 + sourceSets.main.manifest.srcFile('src/androidMain/AndroidManifest.xml') + defaultConfig { + minSdkVersion 22 + targetSdkVersion 25 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lintOptions { + disable "InvalidPackage" // FIXME remove when Ktor 2.0 has been released + } +} diff --git a/download/src/androidMain/AndroidManifest.xml b/download/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..5cedd19b9 --- /dev/null +++ b/download/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/download/src/commonJvmAndroid/kotlin/org/fdroid/download/JvmDownloadManager.kt b/download/src/commonJvmAndroid/kotlin/org/fdroid/download/JvmDownloadManager.kt new file mode 100644 index 000000000..7467b5f1c --- /dev/null +++ b/download/src/commonJvmAndroid/kotlin/org/fdroid/download/JvmDownloadManager.kt @@ -0,0 +1,51 @@ +package org.fdroid.download + +import io.ktor.client.plugins.ResponseException +import io.ktor.utils.io.jvm.javaio.toInputStream +import kotlinx.coroutines.runBlocking +import java.io.IOException +import java.io.InputStream +import java.util.Date + +public class JvmDownloadManager( + userAgent: String, + queryString: String?, +) : DownloadManager(userAgent, queryString) { + + @Throws(IOException::class) + fun headBlocking(request: DownloadRequest, eTag: String?): HeadInfo = runBlocking { + val headInfo = head(request, eTag) ?: throw IOException() + if (eTag == null) return@runBlocking headInfo + // If the ETag does not match, it could be because the file is on a mirror + // running a different webserver, e.g. Apache vs Nginx. + // Content-Length and Last-Modified could be used as well. + // Nginx and Apache 2.4 defaults use only those two values to generate the ETag. + // Unfortunately, other webservers and CDNs have totally different methods. + // And mirrors that are syncing using a method other than rsync + // could easily have different Last-Modified times on the exact same file. + // On top of that, some services like GitHub's and GitLab's raw file support + // do not set the header at all. + val lastModified = try { + // this method is not available multi-platform, so for now only done in JVM + Date.parse(headInfo.lastModified) / 1000 + } catch (e: Exception) { + 0L + } + val calculatedEtag: String = + String.format("\"%x-%x\"", lastModified, headInfo.contentLength) + if (calculatedEtag == eTag) headInfo.copy(eTagChanged = false) + else headInfo + } + + @JvmOverloads + @Throws(IOException::class) + fun getBlocking(request: DownloadRequest, skipFirstBytes: Long? = null): InputStream = + runBlocking { + try { + get(request, skipFirstBytes).toInputStream() + } catch (e: ResponseException) { + throw IOException(e) + } + } + +} diff --git a/download/src/commonMain/kotlin/org/fdroid/download/DownloadManager.kt b/download/src/commonMain/kotlin/org/fdroid/download/DownloadManager.kt new file mode 100644 index 000000000..6e12e9cd7 --- /dev/null +++ b/download/src/commonMain/kotlin/org/fdroid/download/DownloadManager.kt @@ -0,0 +1,104 @@ +package org.fdroid.download + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.ResponseException +import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.onDownload +import io.ktor.client.request.get +import io.ktor.client.request.head +import io.ktor.client.request.header +import io.ktor.client.request.parameter +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.HttpHeaders.Authorization +import io.ktor.http.HttpHeaders.Connection +import io.ktor.http.HttpHeaders.ETag +import io.ktor.http.HttpHeaders.LastModified +import io.ktor.http.contentLength +import io.ktor.util.encodeBase64 +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.charsets.Charsets +import io.ktor.utils.io.core.toByteArray +import kotlin.jvm.JvmOverloads + +public open class DownloadManager( + private val userAgent: String, + queryString: String? = null, +) { + + private val httpClient by lazy { + HttpClient { + followRedirects = false + expectSuccess = true + developmentMode = true // TODO remove + engine { + proxy = null // TODO use proxy except when swap + threadsCount = 4 + pipelining = true + } + install(UserAgent) { + agent = userAgent + } + } + } + private val parameters = queryString?.split('&')?.map { p -> + val (key, value) = p.split('=') + Pair(key, value) + } + + // TODO try to force onion addresses over proxy like NetCipher.getHttpURLConnection() + + /** + * Performs a HEAD request and returns [HeadInfo]. + * + * This is useful for checking if the repository index has changed before downloading it again. + * However, due to non-standard ETags on mirrors, change detection is unreliable. + */ + suspend fun head(request: DownloadRequest, eTag: String?): HeadInfo? { + val authString = constructBasicAuthValue(request) + val response = try { + httpClient.head(request.url) { + // add authorization header from username / password if set + if (authString != null) header(Authorization, authString) + } + } catch (e: ResponseException) { + println(e) + return null + } + val contentLength = response.contentLength() + val lastModified = response.headers[LastModified] + if (eTag != null && response.headers[ETag] == eTag) { + return HeadInfo(false, contentLength, lastModified) + } + return HeadInfo(true, contentLength, lastModified) + } + + @JvmOverloads + suspend fun get(request: DownloadRequest, skipFirstBytes: Long? = null): ByteReadChannel { + val authString = constructBasicAuthValue(request) + val response = httpClient.get(request.url) { + // add query string parameters if existing + parameters?.forEach { (key, value) -> + parameter(key, value) + } + // add authorization header from username / password if set + if (authString != null) header(Authorization, authString) + // add range header if set + if (skipFirstBytes != null) header("Range", "bytes=${skipFirstBytes}-") + // avoid keep-alive for swap due to strange errors observed in the past + if (request.isSwap) header(Connection, "Close") + + onDownload { bytesSentTotal, contentLength -> + println("Received $bytesSentTotal bytes from $contentLength") + } + } + return response.bodyAsChannel() + } + + private fun constructBasicAuthValue(request: DownloadRequest): String? { + if (request.username == null || request.password == null) return null + val authString = "${request.username}:${request.password}" + val authBuf = authString.toByteArray(Charsets.UTF_8).encodeBase64() + return "Basic $authBuf" + } + +} diff --git a/download/src/commonMain/kotlin/org/fdroid/download/DownloadRequest.kt b/download/src/commonMain/kotlin/org/fdroid/download/DownloadRequest.kt new file mode 100644 index 000000000..7a881fe42 --- /dev/null +++ b/download/src/commonMain/kotlin/org/fdroid/download/DownloadRequest.kt @@ -0,0 +1,11 @@ +package org.fdroid.download + +import kotlin.jvm.JvmOverloads + +data class DownloadRequest @JvmOverloads constructor( + val url: String, + val mirrors: List, + val username: String? = null, + val password: String? = null, + val isSwap: Boolean = false, +) diff --git a/download/src/commonMain/kotlin/org/fdroid/download/HeadInfo.kt b/download/src/commonMain/kotlin/org/fdroid/download/HeadInfo.kt new file mode 100644 index 000000000..8e2a90a8a --- /dev/null +++ b/download/src/commonMain/kotlin/org/fdroid/download/HeadInfo.kt @@ -0,0 +1,7 @@ +package org.fdroid.download + +data class HeadInfo( + val eTagChanged: Boolean, + val contentLength: Long?, + val lastModified: String?, +) diff --git a/gradle.properties b/gradle.properties index 031f6b9ec..baf7a83fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,8 @@ org.gradle.jvmargs=-Xms1g -Xmx2g -XX:MaxPermSize=2g android.enableJetifier=true android.useAndroidX=true + +kotlin.code.style=official +kotlin.mpp.stability.nowarn=true +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false diff --git a/gradle/verification-keyring.gpg b/gradle/verification-keyring.gpg index f2830afda4bb6746a4474c42b001a9ea35209915..9d19b2644bf11c4423d1c99b851d2dfa978df84f 100644 GIT binary patch delta 3968 zcma*pc{tSVy8!T+u}`+_MaRDmA4BpID=hnmF@K zYXK6;#aC+?l^ggjPPhYl5&6liN2t_{;wV#GWB@&x&SiZg(`ay6SAfxig1_%jf1H8D0ix5Z+SnXfv$_Ff=1`;Rdwu zNT>Sg_WO17cTM8wI#ohn@ok`aW##S}PJ<`2K?37+_%EiCsMNP~PrmlbJvRg6zFKk| z^b5yoGR6Q-U$BKbyqrNKf73|1bAA352cJ0wDa`A+in77^mTG=g54SLnPZYRAE*A1# zS{$z~bJa>2daxwlFyl84)5tkV-$Dm*+x>e?9=HJb(JuWji9(f4N@#@OHXH z>5ohf2f!y6lxP4>bfH)H}@BDG4{@{y24|?JecV>R(13+VSrf@pM^+$Vl{jYG?GTnjk z+0ep(2dY+%J1E#*dg#0igXT~I`y0k%9&N2Xb!u?iZDH;7WDP!EE*K18g)e-E9WxXU_z7n`j z*tkt@0ZcJXH$uYvgWW;7_4XWIag;Usi<7-q77Dj4u1zW)irSmhTTP z@&5%EG!4QJ`wN#R2n1zf{|A@sPiD|F)xzr^VhWmbJ|F{;7LC;?UG+kN6`=9ukYgIz zm&D%revVxyd!gd!?yoa`uXwve-s%3@Apq-dYLqU)#(I)LS^alTi|2AO`pa)p^8jhM ztN2BxZ*q94-21`2T%)~_{{CA#gCf zmCi*p_CEi(RpOC!^>njDI1(6BD89H5VH3emm$)0PMWcO4=ZimlW4e+WNK!w{Sl4ak zJJEPXupQ)9eq5M4+%9&{?5vcjE^PUini!@l;vOA;UpZ*aiSv|oa6gSQX~wx-%m61y z;MhFiBJ|Y_C3}E=BI<=#*p6;p$l*7syFX7WZGv!uoF_`bL1?YeKwvlPVzN=zZQ0ZP z;!pH2^u}1h{ltrF%w;FR_AUucB`!M=e8U4l)QUXcl%|>bH>fa7`GVo*Rbp}-Aa$OPOyZy&@-Y%eLp(&e&wBG-LE9?(0 zU>1>^ZQQ}dAtU7qvfS_!o-stf))>gFIIJeKw4d`vLoSio0-v;M&mesmR=Ii^PuAr} zJnQ=-t_u*GgE1tjvf#F?!ANsSVVk=yF>Nw6+_--IcXV|o8`}YTzm_zS*(wa>d&4eJ zS`W#f&jgW7R4j4pM4w=G6f7~tF0KQ756$~|;BqZ?eN$sl=w?jSJdhM~gN}8eStn?! zd0!plH16*)w84quws8IwlC|>8#_P-HZ!z~QpFJ+{lNI%>pk^n<6p?%oVl<|NcR1?E zQ`DU8ckGKg1H*36SeJJJxvh&VHi#Dkuk6T@jns@`qJDCx29=x1|3y^`K#YdHm;QsF)dqDI<&e%mP>6FWHJmmBr!90`Dp) z;q4X$wH|fQoCkYUm71ojua!+2(OnOwrPZ)?w|%_2SxgA7rHPprtK}*Pxx&pb1JMb% zM|;V^Hi)e5cw77Z{@7}iH=L1Cl((gt3lY=$qMDLTEyK!To-kNz4^9a<3H8g+K(2xZ zD#z`$0-g>{P=S&JUNqH#Z)scBuW057Miic)jujtm@GAHnGa7zn$#fN7=zgHO<40Oa zuWh;Bf>hX)zM$i+dU@EFR3^HtB*1?78|&n}ypd`l4NAudat2xWUZdCec1X;x8uc+s zmSP`l{18o5=@o-0Z-b1uRfhHNVWWsmXO{NMW)5t%fl7T*``#f9qHwRNvynGGceJ|C6Eg=xvo+=rkghTy3e+k+D^#K#ltb>hDs8Z8Yb z`7^q)}l5cI+33I=6Wh#XB{b~@vgjrV^&!)5~#JIAk}5))b_BE zIWy=KaH4Bj9`Jke73PM|urFOoStHqhhWUl>*l$!ir(iWfUQ{X2O}<$f=V0G~B`y9u zg|?ld>${ND;3`%rST*Qs-u;v>#ZSD$U@K#kC|Qs-TX8^2eUnlYq9b3(DinHOIJ~i? z5iUHj$>r=Ts~;|AY~!&XejL`BgI9u!+CfW!azzQZq2<*|c<%hr`E#$+{`4}@mO3} z#!KZ^!}%v}7Hh7D-oyJUj|n1YZ%7t#TmN8bG}SS?Q8l-|U@pSXZ22V24P^ICAkNb0 zgTAqLT1u17l``RE&Fa{?$m(#9xv=-rfPP9R{6G34@=sr!c`(;`hRK6Jp>=_L8$+j) zFY?O3>K4H8*|{L+ZqW8&{Vq`AnwG*9I44S<)v4|DWvS?0Gg?&2pBL@|yj9G-NcM`; zr*7U^QNDpsG_ocswIDQ1f$N> zpDcaCp#-&kXji_-cUetdG&3of@5(KhMO?Ubjw3NXZ*$*~pW5YJ5=K;pHpOR^QjTb> zU)1kmA4DqJMOPVAkz_Z#Eor`>zFe5!SO})a#=M?>%q04IJ;B*H6i{5mSQ|uEkajo~ z6?)!BC)NX>a)|lLJ616y_v3Z;EB3FYDfVWYWeSsw70Iu!bc|{bu%*2kSw=4#99#-g zzv-VStnAjQ`3%oL{u@HDkgVE^$X3B37Fb);r_Hjnu3hPIkc-Z9fZsJ_Q;y=<7Uo7d zDhRWfL_n`T=lObmGwitx`_pH)JGTY{j%M>uH(7ZBcX0g=F{E70R^=^)p;n4@>c<8> zJxD_UNa_E-bRZ%8%>Q%g(4IbZtgg$%|6Hz6a5=Jjfn|H1(tOCe;@PuoLtRW!b76T6 zaR!)1t(1pK5)YBLBr{7$OeHt+^ROq{za47RYEs~O;{u<=MABb5Eqa~C5??hhy#sl? z<!woB^oJx~87HJ^L&wLJMLHeqN({^xz0JjGKR&bvs+H+1zW z6Y=>EjnzTU;o+8P(Wj{skA__6A+96U@q2*7RMM9sKHlBSQRD!}G@*oTfxA7%0xl(1 z_Odipa}OSio0gBYEk_ma+c;nQy%$Idq?^HsfF9b^9}F*TzcBw8)05pSq3;ec|M@-k z#bPWoR>=Fs2z20htqobhaJOsOcj7o2d*s_z%8E=AjCYE7oA+31-W8{_WU6a}_%iL? zKyJSvP|o`n_WW|Urx_?6UZfQdtx5jQm&lNl_W3H?n?+0|>-979vLf7h>xzPp7DMcd ztCMv@q)K2b)jZQ(o~9$UKc#z+b9HlOt3R?)uJM@OsHQy={|-ON9y(2T+JA8U3xOF{ A(EtDd delta 17 YcmbPnPGJ3dfrb{w7N!>FEiCKB0YQZZbpQYW diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9133a831b..82600014a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3,6 +3,10 @@ false true + + + + @@ -37,8 +41,12 @@ + - + + + + @@ -78,6 +86,7 @@ + @@ -88,7 +97,10 @@ - + + + + @@ -133,16 +145,25 @@ + - + + + + + + + + + @@ -1949,6 +1970,11 @@ + + + + + @@ -2109,6 +2135,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3002,6 +3261,11 @@ + + + + + @@ -3087,6 +3351,19 @@ + + + + + + + + + + + + + @@ -3097,12 +3374,22 @@ + + + + + + + + + + @@ -3350,6 +3637,11 @@ + + + + + diff --git a/settings.gradle b/settings.gradle index e7b4def49..8bf885599 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':app' +include ':download'