BIT-725, BIT-328: Add base URL interceptors and dynamically change environments (#160)

This commit is contained in:
Brian Yencho
2023-10-25 14:24:02 -05:00
committed by Álison Fernandes
parent 8bdda9bffd
commit dc6d37dc32
18 changed files with 676 additions and 78 deletions

View File

@@ -5,18 +5,16 @@ import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Url
/**
* Defines raw calls under the /identity API.
*/
interface IdentityApi {
@POST
@POST("/connect/token")
@Suppress("LongParameterList")
@FormUrlEncoded
suspend fun getToken(
@Url url: String,
@Field(value = "scope", encoded = true) scope: String,
@Field(value = "client_id") clientId: String,
@Field(value = "username") email: String,

View File

@@ -6,15 +6,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedSe
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.di.NetworkModule
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Named
import javax.inject.Singleton
/**
@@ -27,23 +25,30 @@ object NetworkModule {
@Provides
@Singleton
fun providesAccountService(
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
retrofits: Retrofits,
json: Json,
): AccountsService = AccountsServiceImpl(retrofit.create(), json)
): AccountsService = AccountsServiceImpl(
accountsApi = retrofits.unauthenticatedApiRetrofit.create(),
json = json,
)
@Provides
@Singleton
fun providesIdentityService(
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
retrofits: Retrofits,
json: Json,
): IdentityService = IdentityServiceImpl(retrofit.create(), json)
): IdentityService = IdentityServiceImpl(
api = retrofits.unauthenticatedIdentityRetrofit.create(),
json = json,
)
@Provides
@Singleton
fun providesHaveIBeenPwnedService(
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
retrofits: Retrofits,
): HaveIBeenPwnedService = HaveIBeenPwnedServiceImpl(
retrofit.newBuilder()
retrofits
.staticRetrofitBuilder
.baseUrl("https://api.pwnedpasswords.com")
.build()
.create(),

View File

@@ -13,8 +13,6 @@ import java.util.UUID
class IdentityServiceImpl constructor(
private val api: IdentityApi,
private val json: Json,
// TODO: use correct base URL here BIT-328
private val baseUrl: String = "https://vault.bitwarden.com",
private val deviceModelProvider: DeviceModelProvider = DeviceModelProvider(),
) : IdentityService {
@@ -24,8 +22,6 @@ class IdentityServiceImpl constructor(
captchaToken: String?,
): Result<GetTokenResponseJson> = api
.getToken(
// TODO: use correct base URL here BIT-328
url = "$baseUrl/identity/connect/token",
scope = "api+offline_access",
clientId = "mobile",
authEmail = email.base64UrlEncode(),

View File

@@ -1,8 +1,9 @@
package com.x8bit.bitwarden.data.platform.datasource.network.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsImpl
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.LocalDateTimeSerializer
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigServiceImpl
@@ -13,13 +14,8 @@ import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.create
import java.time.LocalDateTime
import javax.inject.Named
import javax.inject.Singleton
/**
@@ -30,13 +26,11 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object NetworkModule {
const val AUTHORIZED: String = "authorized"
const val UNAUTHORIZED: String = "unauthorized"
@Provides
@Singleton
fun providesConfigService(@Named(UNAUTHORIZED) retrofit: Retrofit): ConfigService =
ConfigServiceImpl(retrofit.create())
fun providesConfigService(
retrofits: Retrofits,
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
@Provides
@Singleton
@@ -44,50 +38,16 @@ object NetworkModule {
@Provides
@Singleton
fun providesOkHttpClientBuilder(): OkHttpClient.Builder =
OkHttpClient.Builder().addInterceptor(
HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY)
},
)
@Provides
@Singleton
fun providesRetrofitBuilder(
json: Json,
): Retrofit.Builder {
val contentType = "application/json".toMediaType()
return Retrofit.Builder().baseUrl("https://api.bitwarden.com")
.addConverterFactory(json.asConverterFactory(contentType))
.addCallAdapterFactory(ResultCallAdapterFactory())
}
@Provides
@Singleton
@Named(UNAUTHORIZED)
fun providesUnauthorizedRetrofit(
okHttpClientBuilder: OkHttpClient.Builder,
retrofitBuilder: Retrofit.Builder,
): Retrofit =
retrofitBuilder
.client(
okHttpClientBuilder.build(),
)
.build()
@Provides
@Singleton
@Named(AUTHORIZED)
fun providesAuthorizedRetrofit(
okHttpClientBuilder: OkHttpClient.Builder,
retrofitBuilder: Retrofit.Builder,
fun provideRetrofits(
authTokenInterceptor: AuthTokenInterceptor,
): Retrofit =
retrofitBuilder
.client(
okHttpClientBuilder.addInterceptor(authTokenInterceptor).build(),
)
.build()
baseUrlInterceptors: BaseUrlInterceptors,
json: Json,
): Retrofits =
RetrofitsImpl(
authTokenInterceptor = authTokenInterceptor,
baseUrlInterceptors = baseUrlInterceptors,
json = json,
)
@OptIn(ExperimentalSerializationApi::class)
@Provides

View File

@@ -0,0 +1,58 @@
package com.x8bit.bitwarden.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()

View File

@@ -0,0 +1,65 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.util.orNullIfBlank
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()
/**
* An interceptor for "/identity" calls.
*/
val identityInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
/**
* An interceptor for "/events" calls.
*/
val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
init {
// Ensure all interceptors begin with a default value
environment = Environment.Us
}
private fun updateBaseUrls(environment: Environment) {
val environmentUrlData = environment.environmentUrlData
val baseUrl = environmentUrlData.base.trim()
// Determine the required base URLs
val apiUrl: String
val identityUrl: String
val eventsUrl: String
if (baseUrl.isNotEmpty()) {
apiUrl = "$baseUrl/api"
identityUrl = "$baseUrl/identity"
eventsUrl = "$baseUrl/events"
} else {
apiUrl =
environmentUrlData.api.orNullIfBlank() ?: "https://api.bitwarden.com"
identityUrl =
environmentUrlData.identity.orNullIfBlank() ?: "https://identity.bitwarden.com"
eventsUrl =
environmentUrlData.events.orNullIfBlank() ?: "https://events.bitwarden.com"
}
// Update the base URLs
apiInterceptor.baseUrl = apiUrl
identityInterceptor.baseUrl = identityUrl
eventsInterceptor.baseUrl = eventsUrl
}
}

View File

@@ -0,0 +1,39 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import retrofit2.Retrofit
/**
* A collection of various [Retrofit] instances that serve different purposes.
*/
interface Retrofits {
/**
* Allows access to "/api" calls that must be authenticated.
*
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
*/
val authenticatedApiRetrofit: Retrofit
/**
* 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 "/identity" calls that do not require authentication.
*
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
*/
val unauthenticatedIdentityRetrofit: Retrofit
/**
* Allows access to static API calls (ex: external APIs) that do not therefore require
* authentication with Bitwarden's servers.
*
* No base URL is supplied as part of the builder and no longer is added to make this URL
* dynamically updatable.
*/
val staticRetrofitBuilder: Retrofit.Builder
}

View File

@@ -0,0 +1,122 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
/**
* Primary implementation of [Retrofits].
*/
class RetrofitsImpl(
authTokenInterceptor: AuthTokenInterceptor,
baseUrlInterceptors: BaseUrlInterceptors,
json: Json,
) : Retrofits {
//region Authenticated Retrofits
override val authenticatedApiRetrofit: Retrofit by lazy {
createAuthenticatedRetrofit(
baseUrlInterceptor = baseUrlInterceptors.apiInterceptor,
)
}
//endregion Authenticated Retrofits
//region Unauthenticated Retrofits
override val unauthenticatedApiRetrofit: Retrofit by lazy {
createUnauthenticatedRetrofit(
baseUrlInterceptor = baseUrlInterceptors.apiInterceptor,
)
}
override val unauthenticatedIdentityRetrofit: Retrofit by lazy {
createUnauthenticatedRetrofit(
baseUrlInterceptor = baseUrlInterceptors.identityInterceptor,
)
}
//endregion Unauthenticated Retrofits
//region Other Retrofits
override val staticRetrofitBuilder: Retrofit.Builder by lazy {
baseRetrofitBuilder
.client(
baseOkHttpClient
.newBuilder()
.addInterceptor(loggingInterceptor)
.build(),
)
}
//endregion Other Retrofits
//region Helper properties and functions
private val loggingInterceptor: HttpLoggingInterceptor by lazy {
HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
}
private val baseOkHttpClient: OkHttpClient =
OkHttpClient.Builder()
.build()
private val authenticatedOkHttpClient: OkHttpClient by lazy {
baseOkHttpClient
.newBuilder()
.addInterceptor(authTokenInterceptor)
.build()
}
private val baseRetrofit: Retrofit by lazy {
baseRetrofitBuilder
.baseUrl("https://api.bitwarden.com")
.build()
}
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
Retrofit.Builder()
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addCallAdapterFactory(ResultCallAdapterFactory())
.client(baseOkHttpClient)
}
private fun createAuthenticatedRetrofit(
baseUrlInterceptor: BaseUrlInterceptor,
): Retrofit =
baseRetrofit
.newBuilder()
.client(
authenticatedOkHttpClient
.newBuilder()
.addInterceptor(baseUrlInterceptor)
.addInterceptor(loggingInterceptor)
.build(),
)
.build()
private fun createUnauthenticatedRetrofit(
baseUrlInterceptor: BaseUrlInterceptor,
): Retrofit =
baseRetrofit
.newBuilder()
.client(
baseOkHttpClient
.newBuilder()
.addInterceptor(baseUrlInterceptor)
.addInterceptor(loggingInterceptor)
.build(),
)
.build()
//endregion Helper properties and functions
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@@ -15,6 +16,7 @@ class NetworkConfigRepositoryImpl(
private val authRepository: AuthRepository,
private val authTokenInterceptor: AuthTokenInterceptor,
private val environmentRepository: EnvironmentRepository,
private val baseUrlInterceptors: BaseUrlInterceptors,
dispatcher: CoroutineDispatcher,
) : NetworkConfigRepository {
@@ -35,7 +37,7 @@ class NetworkConfigRepositoryImpl(
environmentRepository
.environmentStateFlow
.onEach { environment ->
// TODO: Update base URL interceptors (BIT-725)
baseUrlInterceptors.environment = environment
}
.launchIn(scope)
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository.di
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepository
@@ -37,11 +38,13 @@ object RepositoryModule {
authRepository: AuthRepository,
authTokenInterceptor: AuthTokenInterceptor,
environmentRepository: EnvironmentRepository,
baseUrlInterceptors: BaseUrlInterceptors,
): NetworkConfigRepository =
NetworkConfigRepositoryImpl(
authRepository = authRepository,
authTokenInterceptor = authTokenInterceptor,
environmentRepository = environmentRepository,
baseUrlInterceptors = baseUrlInterceptors,
dispatcher = Dispatchers.IO,
)
}

View File

@@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.platform.util
/**
* Returns the original [String] only if:
*
* - it is non-null
* - it is not blank (where blank refers to empty strings of those containing only white space)
*
* Otherwise `null` is returned.
*/
fun String?.orNullIfBlank(): String? =
this?.takeUnless { it.isBlank() }

View File

@@ -1,14 +1,12 @@
package com.x8bit.bitwarden.data.vault.datasource.network.di
import com.x8bit.bitwarden.data.platform.datasource.network.di.NetworkModule
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Named
import javax.inject.Singleton
/**
@@ -20,6 +18,7 @@ object NetworkModule {
@Provides
@Singleton
fun provideSyncApiService(@Named(NetworkModule.AUTHORIZED) retrofit: Retrofit): SyncApi =
retrofit.create()
fun provideSyncApiService(
retrofits: Retrofits,
): SyncApi = retrofits.authenticatedApiRetrofit.create()
}