Skip to content

Commit

Permalink
[BWA-86] Debug Menu #1 - network layer (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrebispo5 authored Nov 18, 2024
1 parent abbf656 commit 5b53b50
Show file tree
Hide file tree
Showing 20 changed files with 1,362 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.bitwarden.authenticator.data.auth.datasource.disk.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Represents URLs for various Bitwarden domains.
*
* @property base The overall base URL.
* @property api Separate base URL for the "/api" domain (if applicable).
* @property identity Separate base URL for the "/identity" domain (if applicable).
* @property icon Separate base URL for the icon domain (if applicable).
* @property notifications Separate base URL for the notifications domain (if applicable).
* @property webVault Separate base URL for the web vault domain (if applicable).
* @property events Separate base URL for the events domain (if applicable).
*/
@Serializable
data class EnvironmentUrlDataJson(
@SerialName("base")
val base: String,

@SerialName("api")
val api: String? = null,

@SerialName("identity")
val identity: String? = null,

@SerialName("icons")
val icon: String? = null,

@SerialName("notifications")
val notifications: String? = null,

@SerialName("webVault")
val webVault: String? = null,

@SerialName("events")
val events: String? = null,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Default [EnvironmentUrlDataJson] for the US region.
*/
val DEFAULT_US: EnvironmentUrlDataJson =
EnvironmentUrlDataJson(base = "https://vault.bitwarden.com")

/**
* Default [EnvironmentUrlDataJson] for the EU region.
*/
val DEFAULT_EU: EnvironmentUrlDataJson =
EnvironmentUrlDataJson(base = "https://vault.bitwarden.eu")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
@file:OmitFromCoverage

package com.bitwarden.authenticator.data.platform.datasource.network.core

import com.bitwarden.authenticator.data.platform.annotation.OmitFromCoverage
import com.bitwarden.authenticator.data.platform.util.asFailure
import com.bitwarden.authenticator.data.platform.util.asSuccess
import okhttp3.Request
import okio.IOException
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.lang.reflect.Type

/**
* The integer code value for a "No Content" response.
*/
private const val NO_CONTENT_RESPONSE_CODE: Int = 204

/**
* A [Call] for wrapping a network request into a [Result].
*/
@Suppress("TooManyFunctions")
class ResultCall<T>(
private val backingCall: Call<T>,
private val successType: Type,
) : Call<Result<T>> {
override fun cancel(): Unit = backingCall.cancel()

override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)

override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback.onResponse(this@ResultCall, Response.success(response.toResult()))
}

override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
}
},
)

@Suppress("TooGenericExceptionCaught")
override fun execute(): Response<Result<T>> =
try {
Response.success(
backingCall
.execute()
.toResult(),
)
} catch (ioException: IOException) {
Response.success(ioException.toFailure())
} catch (runtimeException: RuntimeException) {
Response.success(runtimeException.toFailure())
}

override fun isCanceled(): Boolean = backingCall.isCanceled

override fun isExecuted(): Boolean = backingCall.isExecuted

override fun request(): Request = backingCall.request()

override fun timeout(): Timeout = backingCall.timeout()

/**
* Synchronously send the request and return its response as a [Result].
*/
fun executeForResult(): Result<T> = requireNotNull(execute().body())

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

private fun Response<T>.toResult(): Result<T> =
if (!this.isSuccessful) {
HttpException(this).toFailure()
} else {
val body = this.body()
@Suppress("UNCHECKED_CAST")
when {
// We got a nonnull T as the body, just return it.
body != null -> body.asSuccess()
// We expected the body to be null since the successType is Unit, just return Unit.
successType == Unit::class.java -> (Unit as T).asSuccess()
// We allow null for 204's, just return null.
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
// All other null bodies result in an error.
else -> IllegalStateException("Unexpected null body!").toFailure()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.bitwarden.authenticator.data.platform.datasource.network.core

import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type

/**
* A [CallAdapter] for wrapping network requests into [kotlin.Result].
*/
class ResultCallAdapter<T>(
private val successType: Type,
) : CallAdapter<T, Call<Result<T>>> {

override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bitwarden.authenticator.data.platform.datasource.network.core

import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

/**
* A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result].
*/
class ResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): CallAdapter<*, *>? {
check(returnType is ParameterizedType) { "$returnType must be parameterized" }
val containerType = getParameterUpperBound(0, returnType)

if (getRawType(containerType) != Result::class.java) return null
check(containerType is ParameterizedType) { "$containerType must be parameterized" }

val requestType = getParameterUpperBound(0, containerType)

return if (getRawType(returnType) == Call::class.java) {
ResultCallAdapter<Any>(successType = requestType)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.bitwarden.authenticator.data.platform.datasource.network.di

import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.HeadersInterceptor
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.Retrofits
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.RetrofitsImpl
import com.bitwarden.authenticator.data.platform.datasource.network.serializer.ZonedDateTimeSerializer
import dagger.Module
import dagger.Provides
Expand All @@ -17,6 +21,23 @@ import javax.inject.Singleton
* It initializes and configures the networking components.
*/
object PlatformNetworkModule {
@Provides
@Singleton
fun providesHeadersInterceptor(): HeadersInterceptor = HeadersInterceptor()

@Provides
@Singleton
fun provideRetrofits(
baseUrlInterceptors: BaseUrlInterceptors,
headersInterceptor: HeadersInterceptor,
json: Json,
): Retrofits =
RetrofitsImpl(
baseUrlInterceptors = baseUrlInterceptors,
headersInterceptor = headersInterceptor,
json = json,
)

@Provides
@Singleton
fun providesJson(): Json = Json {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.bitwarden.authenticator.data.platform.datasource.network.interceptor

import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.Response

/**
* A [Interceptor] that optionally takes the current base URL of a request and replaces it with
* the currently set [baseUrl]
*/
class BaseUrlInterceptor : Interceptor {

/**
* The base URL to use as an override, or `null` if no override should be performed.
*/
var baseUrl: String? = null
set(value) {
field = value
baseHttpUrl = baseUrl?.let { requireNotNull(it.toHttpUrlOrNull()) }
}

private var baseHttpUrl: HttpUrl? = null

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

// If no base URL is set, we can simply skip
val base = baseHttpUrl ?: return chain.proceed(request)

// Update the base URL used.
return chain.proceed(
request
.newBuilder()
.url(
request
.url
.replaceBaseUrlWith(base),
)
.build(),
)
}
}

/**
* Given a [HttpUrl], replaces the existing base URL with the given [baseUrl].
*/
private fun HttpUrl.replaceBaseUrlWith(
baseUrl: HttpUrl,
) = baseUrl
.newBuilder()
.addEncodedPathSegments(
this
.encodedPathSegments
.joinToString(separator = "/"),
)
.encodedQuery(this.encodedQuery)
.build()
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.bitwarden.authenticator.data.platform.datasource.network.interceptor

import com.bitwarden.authenticator.data.platform.repository.model.Environment
import com.bitwarden.authenticator.data.platform.repository.util.baseApiUrl
import javax.inject.Inject
import javax.inject.Singleton

/**
* An overall container for various [BaseUrlInterceptor] implementations for different API groups.
*/
@Singleton
class BaseUrlInterceptors @Inject constructor() {
var environment: Environment = Environment.Us
set(value) {
field = value
updateBaseUrls(environment = value)
}

/**
* An interceptor for "/api" calls.
*/
val apiInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()

init {
// Ensure all interceptors begin with a default value
environment = Environment.Us
}

private fun updateBaseUrls(environment: Environment) {
val environmentUrlData = environment.environmentUrlData
apiInterceptor.baseUrl = environmentUrlData.baseApiUrl
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.bitwarden.authenticator.data.platform.datasource.network.interceptor

import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_KEY_CLIENT_NAME
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_KEY_CLIENT_VERSION
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_KEY_DEVICE_TYPE
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_KEY_USER_AGENT
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_NAME
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_VERSION
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_DEVICE_TYPE
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT
import okhttp3.Interceptor
import okhttp3.Response

/**
* Interceptor responsible for adding various headers to all API requests.
*/
class HeadersInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(
chain.request()
.newBuilder()
.header(HEADER_KEY_USER_AGENT, HEADER_VALUE_USER_AGENT)
.header(HEADER_KEY_CLIENT_NAME, HEADER_VALUE_CLIENT_NAME)
.header(HEADER_KEY_CLIENT_VERSION, HEADER_VALUE_CLIENT_VERSION)
.header(HEADER_KEY_DEVICE_TYPE, HEADER_VALUE_DEVICE_TYPE)
.build(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.bitwarden.authenticator.data.platform.datasource.network.retrofit

import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import retrofit2.Retrofit
import retrofit2.http.Url

/**
* A collection of various [Retrofit] instances that serve different purposes.
*/
interface Retrofits {

/**
* Allows access to "/api" calls that do not require authentication.
*
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
*/
val unauthenticatedApiRetrofit: Retrofit

/**
* Allows access to static API calls (ex: external APIs).
*
* @param isAuthenticated Indicates if the [Retrofit] instance should use authentication.
* @param baseUrl The static base url associated with this retrofit instance. This can be
* overridden with the [Url] annotation.
*/
fun createStaticRetrofit(
isAuthenticated: Boolean = false,
baseUrl: String = "https://api.bitwarden.com",
): Retrofit
}
Loading

0 comments on commit 5b53b50

Please sign in to comment.