Skip to content

Experimental: uses ktor to perform HTTP calls #935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: release/1.21.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ dependencies {
implementation 'androidx.media3:media3-exoplayer:1.4.0'
implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation 'io.github.webrtc-sdk:android:125.6422.06.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
Expand Down
1 change: 0 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_configuration"
android:supportsRtl="true"
android:theme="@style/Theme.Session.DayNight"
tools:replace="android:allowBackup,android:label" >
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.configs.ConfigUploader;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
Expand Down Expand Up @@ -119,6 +118,8 @@
import dagger.hilt.android.HiltAndroidApp;
import kotlin.Deprecated;
import kotlin.Unit;
import kotlinx.coroutines.Dispatchers;
import kotlinx.coroutines.DispatchersKt;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.R;

Expand Down Expand Up @@ -394,22 +395,22 @@ public boolean isAppVisible() {
// Loki

private void initializeSecurityProvider() {
try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
} catch (ClassNotFoundException e) {
Log.e(TAG, "Failed to find AesGcmCipher class");
throw new ProviderInitializationException();
}

int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);

if (aesPosition < 0) {
Log.e(TAG, "Failed to install AesGcmProvider()");
throw new ProviderInitializationException();
}

int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 2);
// try {
// Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
// } catch (ClassNotFoundException e) {
// Log.e(TAG, "Failed to find AesGcmCipher class");
// throw new ProviderInitializationException();
// }
//
// int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
// Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
//
// if (aesPosition < 0) {
// Log.e(TAG, "Failed to install AesGcmProvider()");
// throw new ProviderInitializationException();
// }

int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 1);
Log.i(TAG, "Installed Conscrypt provider: " + conscryptPosition);

if (conscryptPosition < 0) {
Expand Down Expand Up @@ -549,4 +550,7 @@ public void wakeUpDeviceAndDismissKeyguardIfRequired() {
}
}

static {
System.setProperty(DispatchersKt.IO_PARALLELISM_PROPERTY_NAME, "10");
}
}
27 changes: 0 additions & 27 deletions app/src/main/res/xml/network_security_configuration.xml

This file was deleted.

1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ org.gradle.jvmargs=-Xmx3096M -Dkotlin.daemon.jvm.options\="-Xmx3096M"
gradlePluginVersion=8.5.2
googleServicesVersion=4.3.12
kotlinVersion=2.0.0
ktorVersion=3.0.3
kspVersion=2.0.0-1.0.23
navVersion=2.8.0-beta05
android.useAndroidX=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.selects.onTimeout
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind
Expand Down Expand Up @@ -81,7 +82,7 @@ object SnodeAPI {
private const val minimumSnodePoolCount = 12
private const val minimumSwarmSnodeCount = 3
// Use port 4433 to enforce pinned certificates
private val seedNodePort = 4443
private val seedNodePort = 443

private val seedNodePool = if (SnodeModule.shared.useTestNet) setOf(
"http://public.loki.foundation:38157"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ object Util {
@JvmStatic
@Throws(IOException::class)
fun copy(`in`: InputStream, out: OutputStream?): Long {
val buffer = ByteArray(8192)
val buffer = ByteArray(24560)
var read: Int
var total: Long = 0
while (`in`.read(buffer).also { read = it } != -1) {
Expand Down
3 changes: 3 additions & 0 deletions libsignal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"

implementation("io.ktor:ktor-client-cio:$ktorVersion")

testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:3.11.1"
testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

//TODO: Rewrite this class so that it doesn't rely on specific cipher length assumptions
public class ProfileCipherInputStream extends FilterInputStream {

private final Cipher cipher;
Expand All @@ -35,12 +36,9 @@ public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
Util.readFully(in, nonce);

this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (NoSuchAlgorithmException | NoSuchPaddingException |
InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
} catch (InvalidKeyException e) {
throw new IOException(e);
}
Expand All @@ -67,21 +65,21 @@ public int read(byte[] output, int outputOffset, int outputLength) throws IOExce
synchronized (CIPHER_LOCK) {
if (read == -1) {
if (cipher.getOutputSize(0) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
throw new RuntimeException("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
}

finished = true;
return cipher.doFinal(output, outputOffset);
} else {
if (cipher.getOutputSize(read) > outputLength) {
throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
throw new RuntimeException("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
}

return cipher.update(ciphertext, 0, read, output, outputOffset);
}
}
} catch (IllegalBlockSizeException | ShortBufferException e) {
throw new AssertionError(e);
throw new RuntimeException(e);
} catch (BadPaddingException e) {
throw new IOException(e);
}
Expand Down
153 changes: 61 additions & 92 deletions libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt
Original file line number Diff line number Diff line change
@@ -1,72 +1,50 @@
package org.session.libsignal.utilities

import android.annotation.SuppressLint
import android.net.http.X509TrustManagerExtensions
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.timeout
import io.ktor.client.request.headers
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsBytes
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager


object HTTP {
var isConnectedToNetwork: (() -> Boolean) = { false }

private val seedNodeConnection by lazy {

OkHttpClient().newBuilder()
.callTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.build()
private val seedNodeClient: HttpClient by lazy {
HttpClient(CIO)
}

private val defaultConnection by lazy {
// Snode to snode communication uses self-signed certificates but clients can safely ignore this
val trustManager = object : X509TrustManager {
private val serviceNodeClient: HttpClient by lazy {
HttpClient(CIO) {
engine {
https {
@SuppressLint("CustomX509TrustManager")
trustManager = object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }

override fun checkClientTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
}
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM)
OkHttpClient().newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.hostnameVerifier { _, _ -> true }
.callTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.build()
}
@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }

private fun getDefaultConnection(timeout: Long): OkHttpClient {
// Snode to snode communication uses self-signed certificates but clients can safely ignore this
val trustManager = object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
}
}

override fun checkClientTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
requestTimeout = TimeUnit.SECONDS.toMillis(timeout)
}
}
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM)
return OkHttpClient().newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.hostnameVerifier { _, _ -> true }
.callTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.build()
}

private const val timeout: Long = 120
Expand Down Expand Up @@ -105,55 +83,46 @@ object HTTP {
* Sync. Don't call from the main thread.
*/
suspend fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
val request = Request.Builder().url(url)
.removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value
.removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // Set a fake value
when (verb) {
Verb.GET -> request.get()
Verb.PUT, Verb.POST -> {
if (body == null) { throw Exception("Invalid request body.") }
val contentType = "application/json; charset=utf-8".toMediaType()
@Suppress("NAME_SHADOWING") val body = RequestBody.create(contentType, body)
if (verb == Verb.PUT) request.put(body) else request.post(body)
}
Verb.DELETE -> request.delete()
}
return try {
when {
// Custom timeout
timeout != HTTP.timeout -> {
if (useSeedNodeConnection) {
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
}
getDefaultConnection(timeout)
val client = if (useSeedNodeConnection) seedNodeClient else serviceNodeClient

try {
val response = client.request(url) {
method = HttpMethod.parse(verb.rawValue)

headers {
remove("User-Agent")
remove("Accept-Language")

append("User-Agent", "WhatsApp")
append("Accept-Language", "en-us")
}
useSeedNodeConnection -> seedNodeConnection
else -> defaultConnection
}.newCall(request.build()).await().use { response ->
when (val statusCode = response.code) {
200 -> response.body!!.bytes()
else -> {
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")
throw HTTPRequestFailedException(statusCode, null)
}

if (verb == Verb.PUT || verb == Verb.POST) {
check(body != null) { "Invalid request body." }
contentType(ContentType.Application.Json)
setBody(body)
}

timeout {
requestTimeoutMillis = TimeUnit.SECONDS.toMillis(timeout)
}
}

when (val code = response.status) {
HttpStatusCode.OK -> return response.bodyAsBytes()
else -> {
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $code.")
throw HTTPRequestFailedException(code.value, null)
}
}

} catch (exception: Exception) {
Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.")
Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.", exception)

if (!isConnectedToNetwork()) { throw HTTPNoNetworkException() }

// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
throw HTTPRequestFailedException(0, null, "HTTP request failed due to: ${exception.message}")
}
}

@Suppress("OPT_IN_USAGE")
private val httpCallDispatcher = Dispatchers.IO.limitedParallelism(15)

private suspend fun Call.await(): Response {
return withContext(httpCallDispatcher) {
execute()
}
}
}