Fix: Prevent AuthManager state pollution on connection verification (#348)

* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-23 20:49:58 +03:00
1 parent c21e3e9ed0
commit 2c6e437a5b
42 files changed
+296 -667

No files matched your search

@@ -2,7 +2,6 @@ package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.data.local.AuthManager
import okhttp3.CertificatePinner
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -24,8 +23,6 @@ object ApiClient {
@Volatile
private var service: HermesApiService? = null
private val connectionPool = okhttp3.ConnectionPool()
/** The current [HermesApiService] instance. Lazily created on first access. */
val hermesApi: HermesApiService
get() {
@@ -42,6 +39,47 @@ object ApiClient {
}
}
/** Creates a standalone, temporary [HermesApiService] without modifying the global instance. */
fun createTempService(
host: String,
port: Int,
token: String,
): HermesApiService {
val tempAuthInterceptor =
Interceptor { chain ->
val request =
if (token.isNotBlank()) {
chain
.request()
.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
} else {
chain.request()
}
chain.proceed(request)
}
val tempOkHttp =
OkHttpClient
.Builder()
.addInterceptor(tempAuthInterceptor)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
val tempRetrofit =
Retrofit
.Builder()
.baseUrl("http://$host:$port/")
.client(tempOkHttp)
.addConverterFactory(GsonConverterFactory.create())
.build()
return tempRetrofit.create(HermesApiService::class.java)
}
// ── Internal ─────────────────────────────────────────────────────────
private fun buildService(): HermesApiService {
@@ -75,18 +113,11 @@ object ApiClient {
chain.proceed(request)
}
// SEC-11: No certificate pinning is configured by default since the app
// intentionally uses HTTP for LAN and hosts are dynamic.
// CertificatePinner infrastructure is provided here if HTTPS is used with a known host.
val certificatePinner = CertificatePinner.Builder().build()
val okHttp =
OkHttpClient
.Builder()
.connectionPool(connectionPool)
.addInterceptor(authInterceptor)
.addInterceptor(logging)
.certificatePinner(certificatePinner)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)