From 88dacc17da5319be987a9146e360a311e36d6c71 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 4 Jan 2026 19:47:18 -0500 Subject: [PATCH] Create HttpUtil --- .../java/io/heckel/ntfy/msg/ApiService.kt | 37 ++--------- .../ntfy/msg/DownloadAttachmentWorker.kt | 19 ++---- .../io/heckel/ntfy/msg/DownloadIconWorker.kt | 19 +----- .../io/heckel/ntfy/msg/UserActionWorker.kt | 19 +----- .../io/heckel/ntfy/service/WsConnection.kt | 10 +-- .../io/heckel/ntfy/ui/SettingsActivity.kt | 10 +-- .../main/java/io/heckel/ntfy/util/CertUtil.kt | 17 +---- .../main/java/io/heckel/ntfy/util/HttpUtil.kt | 64 +++++++++++++++++++ 8 files changed, 87 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt index e84f5632..cddd1d99 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -9,7 +9,7 @@ import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.User import io.heckel.ntfy.util.ALL_PRIORITIES -import io.heckel.ntfy.util.CertUtil +import io.heckel.ntfy.util.HttpUtil import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.PRIORITY_DEFAULT import io.heckel.ntfy.util.topicUrl @@ -19,7 +19,6 @@ import io.heckel.ntfy.util.topicUrlJsonPoll import okhttp3.Call import okhttp3.Callback import okhttp3.Credentials -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody @@ -27,39 +26,13 @@ import okhttp3.Response import java.io.IOException import java.net.URLEncoder import java.nio.charset.StandardCharsets.UTF_8 -import java.util.concurrent.TimeUnit import kotlin.random.Random class ApiService(private val context: Context) { private val repository = Repository.getInstance(context) - private val sslManager = CertUtil.getInstance(context) private val gson = Gson() private val parser = NotificationParser() - private fun createClient(baseUrl: String): OkHttpClient { - return sslManager.getOkHttpClientBuilder(baseUrl) - .callTimeout(15, TimeUnit.SECONDS) - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - } - - private fun createPublishClient(baseUrl: String): OkHttpClient { - return sslManager.getOkHttpClientBuilder(baseUrl) - .callTimeout(5, TimeUnit.MINUTES) - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - } - - private fun createSubscriberClient(baseUrl: String): OkHttpClient { - return sslManager.getOkHttpClientBuilder(baseUrl) - .readTimeout(77, TimeUnit.SECONDS) - .build() - } - suspend fun publish( baseUrl: String, topic: String, @@ -123,7 +96,7 @@ class ApiService(private val context: Context) { .put(body ?: message.toRequestBody()) .build() Log.d(TAG, "Publishing to $request") - val httpCall = createPublishClient(baseUrl).newCall(request) + val httpCall = HttpUtil.longCallClient(context, baseUrl).newCall(request) onCancelAvailable?.invoke { httpCall.cancel() } // Notify caller that HTTP request can now be canceled httpCall.execute().use { response -> if (response.code == 401 || response.code == 403) { @@ -154,7 +127,7 @@ class ApiService(private val context: Context) { val customHeaders = repository.getCustomHeaders(baseUrl) val request = requestBuilder(url, user, customHeaders).build() - createClient(baseUrl).newCall(request).execute().use { response -> + HttpUtil.defaultClient(context, baseUrl).newCall(request).execute().use { response -> if (!response.isSuccessful) { throw Exception("Unexpected response ${response.code} when polling topic $url") } @@ -182,7 +155,7 @@ class ApiService(private val context: Context) { Log.d(TAG, "Opening subscription connection to $url") val customHeaders = repository.getCustomHeaders(baseUrl) val request = requestBuilder(url, user, customHeaders).build() - val call = createSubscriberClient(baseUrl).newCall(request) + val call = HttpUtil.subscriberClient(context, baseUrl).newCall(request) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { try { @@ -221,7 +194,7 @@ class ApiService(private val context: Context) { val url = topicUrlAuth(baseUrl, topic) val customHeaders = repository.getCustomHeaders(baseUrl) val request = requestBuilder(url, user, customHeaders).build() - createClient(baseUrl).newCall(request).execute().use { response -> + HttpUtil.defaultClient(context, baseUrl).newCall(request).execute().use { response -> if (response.isSuccessful) { return true } else if (user == null && response.code == 404) { diff --git a/app/src/main/java/io/heckel/ntfy/msg/DownloadAttachmentWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/DownloadAttachmentWorker.kt index 14ab62ae..337f00dc 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadAttachmentWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadAttachmentWorker.kt @@ -20,29 +20,16 @@ import io.heckel.ntfy.db.Attachment import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription -import io.heckel.ntfy.util.CertUtil +import io.heckel.ntfy.util.HttpUtil import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.ensureSafeNewFile import io.heckel.ntfy.util.extractBaseUrl -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.io.File import java.util.concurrent.TimeUnit class DownloadAttachmentWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { - private val certUtil = CertUtil.getInstance(context) - - private fun createClient(url: String): OkHttpClient { - val baseUrl = extractBaseUrl(url) - return certUtil.getOkHttpClientBuilder(baseUrl) - .callTimeout(15, TimeUnit.MINUTES) - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - } - private val notifier = NotificationService(context) private lateinit var repository: Repository private lateinit var subscription: Subscription @@ -80,7 +67,9 @@ class DownloadAttachmentWorker(private val context: Context, params: WorkerParam .url(attachment.url) .addHeader("User-Agent", ApiService.USER_AGENT) .build() - createClient(attachment.url).newCall(request).execute().use { response -> + val client = HttpUtil + .longCallClient(context, extractBaseUrl(attachment.url)) + client.newCall(request).execute().use { response -> Log.d(TAG, "Download: headers received: $response") if (!response.isSuccessful) { throw Exception("Unexpected response: ${response.code}") diff --git a/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt index 6c807eb2..dd9bcb9f 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt @@ -11,30 +11,16 @@ import io.heckel.ntfy.db.Icon import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription -import io.heckel.ntfy.util.CertUtil +import io.heckel.ntfy.util.HttpUtil import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.extractBaseUrl import io.heckel.ntfy.util.sha256 -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.io.File import java.util.Date -import java.util.concurrent.TimeUnit class DownloadIconWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { - private val certUtil = CertUtil.getInstance(context) - - private fun createClient(url: String): OkHttpClient { - val baseUrl = extractBaseUrl(url) - return certUtil.getOkHttpClientBuilder(baseUrl) - .callTimeout(1, TimeUnit.MINUTES) - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - } - private val notifier = NotificationService(context) private lateinit var repository: Repository private lateinit var subscription: Subscription @@ -79,7 +65,8 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) .url(icon.url) .addHeader("User-Agent", ApiService.USER_AGENT) .build() - createClient(icon.url).newCall(request).execute().use { response -> + val client = HttpUtil.defaultClient(context, extractBaseUrl(icon.url)) + client.newCall(request).execute().use { response -> Log.d(TAG, "Headers received: $response, Content-Length: ${response.headers["Content-Length"]}") if (!response.isSuccessful) { throw Exception("Unexpected response: ${response.code}") diff --git a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt index 9367936c..fdfb8cbc 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt @@ -14,28 +14,14 @@ import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_BROADCAST import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_HTTP -import io.heckel.ntfy.util.CertUtil +import io.heckel.ntfy.util.HttpUtil import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.extractBaseUrl -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.util.Locale -import java.util.concurrent.TimeUnit class UserActionWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { - private val certUtil = CertUtil.getInstance(context) - - private fun createClient(url: String): OkHttpClient { - val baseUrl = extractBaseUrl(url) - return certUtil.getOkHttpClientBuilder(baseUrl) - .callTimeout(60, TimeUnit.SECONDS) - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - } - private val notifier = NotificationService(context) private val broadcaster = BroadcastService(context) private lateinit var repository: Repository @@ -93,9 +79,10 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) : builder.addHeader(key, value) } val request = builder.build() + val client = HttpUtil.defaultClient(context, extractBaseUrl(url)) Log.d(TAG, "Executing HTTP request: ${method.uppercase(Locale.getDefault())} ${action.url}") - createClient(url).newCall(request).execute().use { response -> + client.newCall(request).execute().use { response -> if (response.isSuccessful) { save(action.copy(progress = ACTION_PROGRESS_SUCCESS, error = null)) return diff --git a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt index f7189d1a..4bf51964 100644 --- a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt @@ -11,7 +11,7 @@ import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.User import io.heckel.ntfy.msg.ApiService.Companion.requestBuilder import io.heckel.ntfy.msg.NotificationParser -import io.heckel.ntfy.util.CertUtil +import io.heckel.ntfy.util.HttpUtil import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.topicShortUrl import io.heckel.ntfy.util.topicUrlWs @@ -20,7 +20,6 @@ import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import java.util.Calendar -import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference import kotlin.random.Random @@ -47,13 +46,8 @@ class WsConnection( private val alarmManager: AlarmManager ) : Connection { private val parser = NotificationParser() - private val certUtil = CertUtil.getInstance(context) private val client: OkHttpClient by lazy { - certUtil.getOkHttpClientBuilder(connectionId.baseUrl) - .readTimeout(0, TimeUnit.MILLISECONDS) - .pingInterval(1, TimeUnit.MINUTES) // The server pings us too, so this doesn't matter much - .connectTimeout(10, TimeUnit.SECONDS) - .build() + HttpUtil.wsClient(context, connectionId.baseUrl) } private var errorCount = 0 private var webSocket: WebSocket? = null diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index 3aa8fb94..4b542b01 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -39,7 +39,6 @@ import io.heckel.ntfy.service.SubscriberServiceManager import io.heckel.ntfy.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.text.SimpleDateFormat @@ -774,12 +773,8 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere .url(EXPORT_LOGS_UPLOAD_URL) .put(log.toRequestBody()) .build() - val client = OkHttpClient.Builder() - .callTimeout(1, TimeUnit.MINUTES) // Total timeout for entire request - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() + val client = HttpUtil + .longCallClient(context, extractBaseUrl(EXPORT_LOGS_UPLOAD_URL)) try { client.newCall(request).execute().use { response -> if (!response.isSuccessful) { @@ -1117,6 +1112,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere private const val EXPORT_LOGS_UPLOAD_ORIGINAL = "upload_original" private const val EXPORT_LOGS_UPLOAD_SCRUBBED = "upload_scrubbed" private const val EXPORT_LOGS_UPLOAD_URL = "https://nopaste.net/?f=json" // Run by binwiederhier; see https://github.com/binwiederhier/pcopy + private const val EXPORT_LOGS_UPLOAD_TIMEOUT_MINUTES = 1L private const val EXPORT_LOGS_UPLOAD_NOTIFY_SIZE_THRESHOLD = 100 * 1024 // Show "Uploading ..." if log larger than X } } diff --git a/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt b/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt index f0d8a2c7..9fedfe4d 100644 --- a/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt +++ b/app/src/main/java/io/heckel/ntfy/util/CertUtil.kt @@ -37,21 +37,9 @@ import kotlin.collections.addAll */ class CertUtil private constructor(context: Context) { private val appContext: Context = context.applicationContext - private val repository: Repository by lazy { Repository.Companion.getInstance(appContext) } + private val repository: Repository by lazy { Repository.getInstance(appContext) } - /** - * Get an OkHttpClient.Builder configured with custom SSL for a specific server - */ - fun getOkHttpClientBuilder(baseUrl: String): OkHttpClient.Builder { - val builder = OkHttpClient.Builder() - applySSLConfiguration(builder, baseUrl) - return builder - } - - /** - * Apply SSL configuration to an OkHttpClient.Builder for a specific server - */ - fun applySSLConfiguration(builder: OkHttpClient.Builder, baseUrl: String) { + fun withTLSConfig(builder: OkHttpClient.Builder, baseUrl: String): OkHttpClient.Builder { try { val trustManagers = mutableListOf() val keyManagers = mutableListOf() @@ -119,6 +107,7 @@ class CertUtil private constructor(context: Context) { } catch (e: Exception) { Log.e(TAG, "Failed to configure SSL for $baseUrl", e) } + return builder } /** diff --git a/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt b/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt new file mode 100644 index 00000000..70bb0449 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt @@ -0,0 +1,64 @@ +package io.heckel.ntfy.util + +import android.content.Context +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit + +/** + * Utility class for creating OkHttpClient instances with appropriate configurations. + * All clients are configured with SSL/TLS settings from CertUtil for custom certificate support. + */ +object HttpUtil { + /** + * Client for regular API calls (auth, poll, etc.). + */ + fun defaultClient(context: Context, baseUrl: String): OkHttpClient { + return defaultBuilder(context, baseUrl).build() + } + + /** + * Client with a longer call timeout (5 minutes). + * Allows for large file uploads or downloads. + */ + fun longCallClient(context: Context, baseUrl: String): OkHttpClient { + return defaultBuilder(context, baseUrl) + .callTimeout(5, TimeUnit.MINUTES) + .build() + } + + /** + * Client for long-polling/streaming subscriptions. + */ + fun subscriberClient(context: Context, baseUrl: String): OkHttpClient { + return emptyBuilder(context, baseUrl) + .readTimeout(77, TimeUnit.SECONDS) // Long enough to allow for server-side keepalive messages + .build() + } + + /** + * Client for WebSocket connections. + * No read timeout, 1 minute ping interval, 10s connect timeout. + */ + fun wsClient(context: Context, baseUrl: String): OkHttpClient { + return emptyBuilder(context, baseUrl) + .readTimeout(0, TimeUnit.MILLISECONDS) + .pingInterval(1, TimeUnit.MINUTES) // Technically not necessary, the server also pings us + .connectTimeout(10, TimeUnit.SECONDS) + .build() + } + + fun defaultBuilder(context: Context, baseUrl: String): OkHttpClient.Builder { + return emptyBuilder(context, baseUrl) + .callTimeout(60, TimeUnit.SECONDS) // Increased to 60s (from 15s) to reduce client variance + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + } + + fun emptyBuilder(context: Context, baseUrl: String): OkHttpClient.Builder { + return CertUtil + .getInstance(context) + .withTLSConfig(OkHttpClient.Builder(), baseUrl) + } +} +