Create HttpUtil

This commit is contained in:
Philipp Heckel 2026-01-04 19:47:18 -05:00
parent 4a51d04ec4
commit 88dacc17da
8 changed files with 87 additions and 108 deletions

View file

@ -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) {

View file

@ -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}")

View file

@ -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}")

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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<TrustManager>()
val keyManagers = mutableListOf<KeyManager>()
@ -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
}
/**

View file

@ -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)
}
}