commit cad2f90dc1d990248972bbf3acb18cb8dc198ac6
parent d29862f332a9e9132b4fbf77ba3836d5e59cfbe5
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 12 Jun 2024 11:08:49 -0600
[wallet] Implement native networking
bug 0008920
Diffstat:
5 files changed, 215 insertions(+), 20 deletions(-)
diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt
@@ -18,8 +18,6 @@ package net.taler.merchantlib
import io.ktor.client.HttpClient
import io.ktor.client.call.body
-import io.ktor.client.engine.okhttp.OkHttp
-import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
import io.ktor.client.request.get
@@ -29,11 +27,10 @@ import io.ktor.client.request.setBody
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.HttpHeaders.Authorization
import io.ktor.http.contentType
-import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import kotlinx.serialization.json.Json
+import net.taler.common.getDefaultHttpClient
import net.taler.merchantlib.Response.Companion.response
class MerchantApi(
@@ -109,18 +106,3 @@ class MerchantApi(
header(Authorization, "Bearer ${merchantConfig.apiKey}")
}
}
-
-fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) {
- expectSuccess = true
- engine {
- config {
- retryOnConnectionFailure(true)
- }
- }
- install(ContentNegotiation) {
- json(Json {
- encodeDefaults = false
- ignoreUnknownKeys = true
- })
- }
-}
diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle
@@ -77,4 +77,12 @@ dependencies {
api 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
testImplementation "junit:junit:$junit_version"
testImplementation 'org.json:json:20220320'
+
+ // Networking
+ api "io.ktor:ktor-client:$ktor_version"
+ api "io.ktor:ktor-client-okhttp:$ktor_version"
+ implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
+ implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
+ implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
+ implementation("io.ktor:ktor-client-logging:$ktor_version")
}
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.ANDROID
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.http.HttpMethod
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.json.Json
+
+fun getDefaultHttpClient(
+ withJson: Boolean = true,
+ timeoutMs: Long? = null,
+ followRedirect: Boolean = false,
+): HttpClient = HttpClient(OkHttp) {
+ expectSuccess = true
+ followRedirects = followRedirect
+ engine {
+ config {
+ retryOnConnectionFailure(true)
+ }
+ }
+ install(ContentNegotiation) {
+ if (withJson) {
+ json(Json {
+ encodeDefaults = false
+ ignoreUnknownKeys = true
+ })
+ }
+ }
+ install(HttpTimeout) {
+ requestTimeoutMillis = if (timeoutMs != null && timeoutMs > 0) {
+ timeoutMs
+ } else {
+ HttpTimeout.INFINITE_TIMEOUT_MS
+ }
+ }
+ install(Logging) {
+ logger = Logger.ANDROID
+ level = LogLevel.HEADERS
+ }
+}
+
+fun String.toHttpMethod(): HttpMethod? = when(this) {
+ "GET" -> HttpMethod.Get
+ "POST" -> HttpMethod.Post
+ "PUT" -> HttpMethod.Put
+ "PATCH" -> HttpMethod.Patch
+ "DELETE" -> HttpMethod.Delete
+ "OPTIONS" -> HttpMethod.Options
+ else -> null
+}
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
@@ -47,12 +47,13 @@ class BackendManager(
private val walletCore = TalerWalletCore()
private val requestManager = RequestManager()
+ private val networkInterface = NetworkInterface()
init {
// TODO using Dagger/Hilt and @Singleton would be nice as well
if (initialized.getAndSet(true)) error("Already initialized")
walletCore.setMessageHandler { onMessageReceived(it) }
- walletCore.setCurlHttpClient()
+ walletCore.setHttpClient(networkInterface)
if (BuildConfig.DEBUG) walletCore.setStdoutHandler {
Log.d(TAG_CORE, it)
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt b/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt
@@ -0,0 +1,130 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.backend
+
+import android.util.Log
+import io.ktor.client.call.body
+import io.ktor.client.plugins.ClientRequestException
+import io.ktor.client.plugins.ServerResponseException
+import io.ktor.client.request.header
+import io.ktor.client.request.headers
+import io.ktor.client.request.request
+import io.ktor.client.request.setBody
+import io.ktor.client.request.url
+import io.ktor.util.toMap
+import io.ktor.utils.io.errors.IOException
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.serialization.SerializationException
+import net.taler.common.getDefaultHttpClient
+import net.taler.common.toHttpMethod
+import net.taler.qtart.Networking
+import net.taler.wallet.TAG
+import java.util.concurrent.ConcurrentHashMap
+
+@OptIn(DelicateCoroutinesApi::class)
+class NetworkInterface: Networking.RequestHandler {
+ private val requests: ConcurrentHashMap<Int, Job> = ConcurrentHashMap()
+
+ override fun handleRequest(
+ req: Networking.RequestInfo,
+ id: Int,
+ sendResponse: (resp: Networking.ResponseInfo) -> Unit
+ ) {
+ Log.d(TAG, "HTTP: handleRequest($req, $id")
+// if (req.debug) debugHttpRequest(req)
+ debugHttpRequest(req)
+
+ requests[id] = GlobalScope.launch {
+ val resp = try {
+ getDefaultHttpClient(
+ timeoutMs = req.timeoutMs,
+ followRedirect = req.redirectMode == Networking.RedirectMode.Transparent,
+ ).request {
+ url(req.url)
+
+ method = req.method.toHttpMethod() ?: error("invalid method")
+
+ headers {
+ req.headers.forEach {
+ val parts = it.split(':', limit = 2)
+ if (parts.size == 2) header(parts[0].trim(), parts[1].trim())
+ }
+ }
+
+ if (req.body != null) {
+ setBody(req.body)
+ }
+ }
+ } catch (e: ClientRequestException) {
+ Log.d(TAG, e.message)
+ null
+ } catch (e: ServerResponseException) {
+ Log.d(TAG, e.message)
+ null
+ } catch (e: IOException) {
+ Log.d(TAG, e.message ?: "IOException")
+ null
+ } catch (e: SerializationException) {
+ Log.d(TAG, e.message ?: "SerializationException")
+ null
+ } ?: return@launch
+
+ // HTTP response status code or 0 on error.
+ val status = if (resp.status.value in 200 until 300) resp.status.value else 0
+
+ // When status is 0, error message.
+ val errorMsg = if (status == 0) "There was an error" else null
+
+ Log.d(TAG, "Sending response to wallet-core")
+ sendResponse(
+ Networking.ResponseInfo(
+ requestId = id,
+ status = status,
+ errorMsg = errorMsg,
+ headers = resp.headers.toMap()
+ .map { (k, v) -> "$k: $v" }
+ .toTypedArray(),
+ body = resp.body(),
+ )
+ )
+ }
+ }
+
+ override fun cancelRequest(id: Int): Boolean {
+ Log.d(TAG, "HTTP: cancelRequest($id")
+ requests[id]?.let { job ->
+ job.cancel()
+ requests.remove(id)
+ }
+
+ return true
+ }
+
+ private fun debugHttpRequest(req: Networking.RequestInfo) {
+ Log.d(TAG, "HTTP request: body = ${req.body}")
+ req.headers.forEachIndexed { i, header ->
+ Log.d(TAG, "HTTP: header[$i] = $header")
+ }
+ Log.d(TAG, "HTTP request: method = ${req.method}")
+ Log.d(TAG, "HTTP request: redirectMode = ${req.redirectMode}")
+ Log.d(TAG, "HTTP request: timeoutMs = ${req.timeoutMs}")
+ Log.d(TAG, "HTTP request: url = ${req.url}")
+ }
+}
+\ No newline at end of file