Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManagerImpl
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriterImpl
import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.service.DownloadService
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -80,11 +81,13 @@ object DataManagerModule {
fileManager: FileManager,
dispatcherManager: DispatcherManager,
buildInfoManager: BuildInfoManager,
baseUrlsProvider: BaseUrlsProvider,
): FlightRecorderWriter = FlightRecorderWriterImpl(
clock = clock,
fileManager = fileManager,
dispatcherManager = dispatcherManager,
buildInfoManager = buildInfoManager,
baseUrlsProvider = baseUrlsProvider,
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.core.data.util.toFormattedPattern
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.util.redactHostnamesInMessage
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
import java.io.BufferedWriter
import java.io.File
Expand All @@ -30,6 +33,7 @@ internal class FlightRecorderWriterImpl(
private val fileManager: FileManager,
private val dispatcherManager: DispatcherManager,
private val buildInfoManager: BuildInfoManager,
private val baseUrlsProvider: BaseUrlsProvider,
) : FlightRecorderWriter {
override suspend fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData) {
fileManager.delete(File(File(fileManager.logsDirectory), data.fileName))
Expand Down Expand Up @@ -103,16 +107,33 @@ internal class FlightRecorderWriterImpl(
bw.append(it)
}
bw.append(" – ")
bw.append(message)
bw.append(message.redactUrls()) // Apply hostname redaction
throwable?.let {
bw.append(" – ")
bw.append(it.getStackTraceString())
bw.append(it.getStackTraceString().redactUrls()) // Also redact stack traces
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we merge these changes first?
#6616

}
bw.newLine()
}
}
}
}

/**
* Redacts ONLY the user's configured self-hosted server hostname.
*
* Preserves ALL Bitwarden domains (including QA/dev).
*/
private fun String.redactUrls(): String {
// Get configured hostnames from BaseUrlsProvider
val configuredHosts = setOf(
baseUrlsProvider.getBaseApiUrl().toHttpUrlOrNull()?.host,
baseUrlsProvider.getBaseIdentityUrl().toHttpUrlOrNull()?.host,
baseUrlsProvider.getBaseEventsUrl().toHttpUrlOrNull()?.host,
).filterNotNull().toSet()

// Delegate to HostnameRedactionUtil for all redaction logic
return this.redactHostnamesInMessage(configuredHosts)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.bitwarden.network.core

import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.model.NetworkResult
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okio.IOException
import okio.Timeout
Expand All @@ -23,10 +25,12 @@ private const val NO_CONTENT_RESPONSE_CODE: Int = 204
internal class NetworkResultCall<T>(
private val backingCall: Call<T>,
private val successType: Type,
private val baseUrlsProvider: BaseUrlsProvider? = null,
) : Call<NetworkResult<T>> {
override fun cancel(): Unit = backingCall.cancel()

override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(backingCall, successType)
override fun clone(): Call<NetworkResult<T>> =
NetworkResultCall(backingCall, successType, baseUrlsProvider)

override fun enqueue(callback: Callback<NetworkResult<T>>): Unit = backingCall.enqueue(
object : Callback<T> {
Expand Down Expand Up @@ -67,8 +71,32 @@ internal class NetworkResultCall<T>(
fun executeForResult(): NetworkResult<T> = requireNotNull(execute().body())

private fun Throwable.toFailure(): NetworkResult<T> {
// We rebuild the URL without query params, we do not want to log those
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
val originalUrl = backingCall.request().url.toUrl()

// Check if this is a hardcoded default URL that will be replaced by BaseUrlInterceptor
// Match against the defaults from RetrofitsImpl.kt line 111 and EnvironmentUrlDataJson
val actualHost = if (baseUrlsProvider != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a let

when (originalUrl.host) {
"api.bitwarden.com" -> baseUrlsProvider.getBaseApiUrl().toHttpUrlOrNull()?.host
Copy link
Collaborator

@david-livefront david-livefront Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can avoid all this complexity with an Interceptor that handles the error logging.

class NetworkErrorLogInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        return chain
            .proceed(chain.request())
            .also {
                if (!it.isSuccessful) {
                    val url = it.request.url.toUrl().run { "$protocol://$authority$path" }
                    Timber.e("Network Error: $url")
                }
            }
    }
}

"identity.bitwarden.com" -> baseUrlsProvider.getBaseIdentityUrl()
.toHttpUrlOrNull()?.host

"events.bitwarden.com" -> baseUrlsProvider.getBaseEventsUrl()
.toHttpUrlOrNull()?.host

else -> null
}
} else {
null
}

// Rebuild the URL without query params, using actual host if available
val url = if (actualHost != null) {
"${originalUrl.protocol}://$actualHost${originalUrl.path}"
} else {
"${originalUrl.protocol}://${originalUrl.authority}${originalUrl.path}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to rebuild here, can't we return originalUrl?

}

Timber.w(this, "Network Error: $url")
return NetworkResult.Failure(this)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bitwarden.network.core

import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.model.NetworkResult
import retrofit2.Call
import retrofit2.CallAdapter
Expand All @@ -10,8 +11,10 @@ import java.lang.reflect.Type
*/
internal class NetworkResultCallAdapter<T>(
private val successType: Type,
private val baseUrlsProvider: BaseUrlsProvider,
) : CallAdapter<T, Call<NetworkResult<T>>> {

override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<NetworkResult<T>> = NetworkResultCall(call, successType)
override fun adapt(call: Call<T>): Call<NetworkResult<T>> =
NetworkResultCall(call, successType, baseUrlsProvider)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bitwarden.network.core

import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.model.NetworkResult
import retrofit2.Call
import retrofit2.CallAdapter
Expand All @@ -10,7 +11,9 @@ import java.lang.reflect.Type
/**
* A [retrofit2.CallAdapter.Factory] for wrapping network requests into [NetworkResult].
*/
internal class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
internal class NetworkResultCallAdapterFactory(
private val baseUrlsProvider: BaseUrlsProvider,
) : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
Expand All @@ -25,7 +28,10 @@ internal class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
val requestType = getParameterUpperBound(0, containerType)

return if (getRawType(returnType) == Call::class.java) {
NetworkResultCallAdapter<Any>(successType = requestType)
NetworkResultCallAdapter<Any>(
successType = requestType,
baseUrlsProvider = baseUrlsProvider,
)
} else {
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.bitwarden.annotation.OmitFromCoverage
*/
@OmitFromCoverage
internal class BaseUrlInterceptors(
private val baseUrlsProvider: BaseUrlsProvider,
val baseUrlsProvider: BaseUrlsProvider,
) {
/**
* An interceptor for "/api" calls.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import timber.log.Timber
@Suppress("LongParameterList")
internal class RetrofitsImpl(
authTokenManager: AuthTokenManager,
baseUrlInterceptors: BaseUrlInterceptors,
private val baseUrlInterceptors: BaseUrlInterceptors,
cookieInterceptor: CookieInterceptor,
headersInterceptor: HeadersInterceptor,
json: Json,
Expand Down Expand Up @@ -115,7 +115,9 @@ internal class RetrofitsImpl(
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
Retrofit.Builder()
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
.addCallAdapterFactory(
NetworkResultCallAdapterFactory(baseUrlInterceptors.baseUrlsProvider),
)
.client(baseOkHttpClient)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.bitwarden.network.util

/**
* List of official Bitwarden cloud hostnames that are safe to log.
*/
private val BITWARDEN_HOSTS = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")

/**
* Redacts hostnames in a log message by replacing bare hostnames with [REDACTED_SELF_HOST].
*
* Only redacts hostnames that match [configuredHosts] AND are not official Bitwarden domains.
* Preserves all Bitwarden domains (including QA/dev environments).
*
* @param configuredHosts Set of hostnames from BaseUrlsProvider
* @return Message with hostnames redacted as [REDACTED_SELF_HOST]
*/
fun String.redactHostnamesInMessage(configuredHosts: Set<String>): String =
configuredHosts.fold(this) { result, hostname ->
val escapedHostname = Regex.escape(hostname)
val bareHostnamePattern = Regex("""\b$escapedHostname\b""")
bareHostnamePattern.replace(result) { hostname.redactIfSelfHosted() }
}

private fun String.redactIfSelfHosted(): String {
val isBitwardenHost = BITWARDEN_HOSTS.any { this.endsWith(it) }
return if (isBitwardenHost) this else "[REDACTED_SELF_HOST]"
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.bitwarden.network.core

import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.model.NetworkResult
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
Expand All @@ -13,12 +16,18 @@ import retrofit2.http.GET

class NetworkResultCallAdapterTest {

private val mockBaseUrlsProvider = mockk<BaseUrlsProvider> {
every { getBaseApiUrl() } returns "https://api.bitwarden.com"
every { getBaseIdentityUrl() } returns "https://identity.bitwarden.com"
every { getBaseEventsUrl() } returns "https://events.bitwarden.com"
}

private val server: MockWebServer = MockWebServer().apply { start() }
private val testService: FakeService =
Retrofit.Builder()
.baseUrl(server.url("/").toString())
// add the adapter being tested
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
.addCallAdapterFactory(NetworkResultCallAdapterFactory(mockBaseUrlsProvider))
.build()
.create()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class RetrofitsTest {
mockIntercept { isAuthInterceptorCalled = true }
}
private val baseUrlInterceptors = mockk<BaseUrlInterceptors> {
every { baseUrlsProvider } returns mockk(relaxed = true)
every { apiInterceptor } returns mockk {
mockIntercept { isApiInterceptorCalled = true }
}
Expand Down
Loading
Loading