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 cddd1d99..ccea3e29 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -1,10 +1,7 @@ package io.heckel.ntfy.msg import android.content.Context -import android.os.Build import com.google.gson.Gson -import io.heckel.ntfy.BuildConfig -import io.heckel.ntfy.db.CustomHeader import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.User @@ -18,14 +15,11 @@ import io.heckel.ntfy.util.topicUrlJson import io.heckel.ntfy.util.topicUrlJsonPoll import okhttp3.Call import okhttp3.Callback -import okhttp3.Credentials -import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.io.IOException import java.net.URLEncoder -import java.nio.charset.StandardCharsets.UTF_8 import kotlin.random.Random class ApiService(private val context: Context) { @@ -92,7 +86,7 @@ class ApiService(private val context: Context) { url } val customHeaders = repository.getCustomHeaders(baseUrl) - val request = requestBuilder(urlWithQuery, user, customHeaders) + val request = HttpUtil.requestBuilder(urlWithQuery, user, customHeaders) .put(body ?: message.toRequestBody()) .build() Log.d(TAG, "Publishing to $request") @@ -126,7 +120,7 @@ class ApiService(private val context: Context) { Log.d(TAG, "Polling topic $url") val customHeaders = repository.getCustomHeaders(baseUrl) - val request = requestBuilder(url, user, customHeaders).build() + val request = HttpUtil.requestBuilder(url, user, customHeaders).build() HttpUtil.defaultClient(context, baseUrl).newCall(request).execute().use { response -> if (!response.isSuccessful) { throw Exception("Unexpected response ${response.code} when polling topic $url") @@ -154,7 +148,7 @@ class ApiService(private val context: Context) { val url = topicUrlJson(baseUrl, topics, sinceVal) Log.d(TAG, "Opening subscription connection to $url") val customHeaders = repository.getCustomHeaders(baseUrl) - val request = requestBuilder(url, user, customHeaders).build() + val request = HttpUtil.requestBuilder(url, user, customHeaders).build() val call = HttpUtil.subscriberClient(context, baseUrl).newCall(request) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { @@ -193,7 +187,7 @@ class ApiService(private val context: Context) { } val url = topicUrlAuth(baseUrl, topic) val customHeaders = repository.getCustomHeaders(baseUrl) - val request = requestBuilder(url, user, customHeaders).build() + val request = HttpUtil.requestBuilder(url, user, customHeaders).build() HttpUtil.defaultClient(context, baseUrl).newCall(request).execute().use { response -> if (response.isSuccessful) { return true @@ -217,7 +211,6 @@ class ApiService(private val context: Context) { ) companion object { - val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" private const val TAG = "NtfyApiService" // These constants have corresponding values in the server codebase! @@ -225,18 +218,5 @@ class ApiService(private val context: Context) { const val EVENT_MESSAGE = "message" const val EVENT_KEEPALIVE = "keepalive" const val EVENT_POLL_REQUEST = "poll_request" - - fun requestBuilder(url: String, user: User?, customHeaders: List = emptyList()): Request.Builder { - val builder = Request.Builder() - .url(url) - .addHeader("User-Agent", USER_AGENT) - if (user != null) { - builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8)) - } - customHeaders.forEach { header -> - builder.addHeader(header.name, header.value) - } - return builder - } } } 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 337f00dc..0adbbde9 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadAttachmentWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadAttachmentWorker.kt @@ -24,7 +24,6 @@ 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.Request import okhttp3.Response import java.io.File import java.util.concurrent.TimeUnit @@ -63,12 +62,8 @@ class DownloadAttachmentWorker(private val context: Context, params: WorkerParam Log.d(TAG, "Downloading attachment from ${attachment.url}") try { - val request = Request.Builder() - .url(attachment.url) - .addHeader("User-Agent", ApiService.USER_AGENT) - .build() - val client = HttpUtil - .longCallClient(context, extractBaseUrl(attachment.url)) + val request = HttpUtil.requestBuilder(attachment.url).build() + val client = HttpUtil.longCallClient(context, extractBaseUrl(attachment.url)) client.newCall(request).execute().use { response -> Log.d(TAG, "Download: headers received: $response") if (!response.isSuccessful) { 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 dd9bcb9f..4b0b2d0a 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt @@ -15,7 +15,6 @@ 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.Request import okhttp3.Response import java.io.File import java.util.Date @@ -61,10 +60,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) private fun downloadIcon(iconFile: File) { Log.d(TAG, "Downloading icon from ${icon.url}") try { - val request = Request.Builder() - .url(icon.url) - .addHeader("User-Agent", ApiService.USER_AGENT) - .build() + val request = HttpUtil.requestBuilder(icon.url).build() 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"]}") 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 fdfb8cbc..54f014e7 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/UserActionWorker.kt @@ -17,7 +17,6 @@ import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_HTTP import io.heckel.ntfy.util.HttpUtil import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.extractBaseUrl -import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.util.Locale @@ -71,10 +70,8 @@ class UserActionWorker(private val context: Context, params: WorkerParameters) : val method = action.method ?: DEFAULT_HTTP_ACTION_METHOD val defaultBody = if (listOf("GET", "HEAD").contains(method)) null else "" val body = action.body ?: defaultBody - val builder = Request.Builder() - .url(url) + val builder = HttpUtil.requestBuilder(url) .method(method, body?.toRequestBody()) - .addHeader("User-Agent", ApiService.USER_AGENT) action.headers?.forEach { (key, value) -> builder.addHeader(key, value) } 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 4bf51964..5174f98b 100644 --- a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt @@ -9,7 +9,6 @@ import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository 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.HttpUtil import io.heckel.ntfy.util.Log @@ -82,7 +81,7 @@ class WsConnection( val sinceId = since.get() val sinceVal = sinceId ?: "all" val urlWithSince = topicUrlWs(baseUrl, topicsStr, sinceVal) - val request = requestBuilder(urlWithSince, user, customHeaders).build() + val request = HttpUtil.requestBuilder(urlWithSince, user, customHeaders).build() Log.d(TAG, "$shortUrl (gid=$globalId): Opening $urlWithSince with listener ID $nextListenerId ...") webSocket = client.newWebSocket(request, Listener(nextListenerId)) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt index d2a3ef67..8d076de1 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt @@ -73,7 +73,7 @@ class CertificateSettingsFragment : BasePreferenceFragment(), val pref = Preference(preferenceScreen.context) pref.title = parseCommonName(cert.subjectX500Principal.name) pref.summary = if (isValid(cert)) { - getString(R.string.settings_advanced_certificates_trusted_item_summary_not_expired, issuer, dateFormat.format(cert.notAfter)) + getString(R.string.settings_advanced_certificates_trusted_item_summary_not_expired, dateFormat.format(cert.notAfter), issuer) } else { getString(R.string.settings_advanced_certificates_trusted_item_summary_expired, issuer) } @@ -114,7 +114,7 @@ class CertificateSettingsFragment : BasePreferenceFragment(), val issuer = parseCommonName(cert.issuerX500Principal.name) pref.title = parseCommonName(cert.subjectX500Principal.name) pref.summary = if (isValid(cert)) { - getString(R.string.settings_advanced_certificates_client_item_summary_not_expired, issuer, dateFormat.format(cert.notAfter), shortUrl(clientCert.baseUrl)) + getString(R.string.settings_advanced_certificates_client_item_summary_not_expired, dateFormat.format(cert.notAfter), issuer, shortUrl(clientCert.baseUrl)) } else { getString(R.string.settings_advanced_certificates_client_item_summary_expired, issuer, shortUrl(clientCert.baseUrl)) } 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 4b542b01..e4ea68f9 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.Request import okhttp3.RequestBody.Companion.toRequestBody import java.text.SimpleDateFormat import java.util.* @@ -769,12 +768,10 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } val gson = Gson() - val request = Request.Builder() - .url(EXPORT_LOGS_UPLOAD_URL) + val request = HttpUtil.requestBuilder(EXPORT_LOGS_UPLOAD_URL) .put(log.toRequestBody()) .build() - val client = HttpUtil - .longCallClient(context, extractBaseUrl(EXPORT_LOGS_UPLOAD_URL)) + val client = HttpUtil.longCallClient(context, extractBaseUrl(EXPORT_LOGS_UPLOAD_URL)) try { client.newCall(request).execute().use { response -> if (!response.isSuccessful) { diff --git a/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt b/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt index 70bb0449..5aabd0a9 100644 --- a/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt +++ b/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt @@ -1,14 +1,23 @@ package io.heckel.ntfy.util import android.content.Context +import android.os.Build +import io.heckel.ntfy.BuildConfig +import io.heckel.ntfy.db.CustomHeader +import io.heckel.ntfy.db.User +import okhttp3.Credentials import okhttp3.OkHttpClient +import okhttp3.Request +import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.TimeUnit /** - * Utility class for creating OkHttpClient instances with appropriate configurations. + * Utility class for creating OkHttpClient instances and Request builders. * All clients are configured with SSL/TLS settings from CertUtil for custom certificate support. */ object HttpUtil { + val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" + /** * Client for regular API calls (auth, poll, etc.). */ @@ -60,5 +69,18 @@ object HttpUtil { .getInstance(context) .withTLSConfig(OkHttpClient.Builder(), baseUrl) } + + fun requestBuilder(url: String, user: User? = null, customHeaders: List = emptyList()): Request.Builder { + val builder = Request.Builder() + .url(url) + .addHeader("User-Agent", USER_AGENT) + if (user != null) { + builder.addHeader("Authorization", Credentials.basic(user.username, user.password, UTF_8)) + } + customHeaders.forEach { header -> + builder.addHeader(header.name, header.value) + } + return builder + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4df699c..c6aae26e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -426,13 +426,13 @@ Manage certificates Trust self-signed server certificates and manage client certificates for mTLS Trusted server certificates - Issued by %1$s, expires %2$s - Issued by %1$s, expired + Expires %1$s, issued by %2$s + Expired, issued by %1$s, Add a trusted certificate Import a server or CA certificate into the trust store (PEM). HTTPS and WebSocket connections will trust this certificate. Client certificates (mTLS) - Issued by %1$s, expires %2$s, used by %3$s - Issued by %1$s, expired, used by %2$s + Expires %1$s, issued by %2$s, used by %3$s + Expired, issued by %1$s, used by %2$s Add a client certificate Import certificate for mutual TLS authentication (PKCS#12). This certificate will be used when connecting to the server. Invalid certificate file