mirror of
https://github.com/bitwarden/android.git
synced 2026-04-25 15:28:09 -05:00
[PM-33262] feat: Add cookie support to Glide image requests (#6627)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.network.ssl.createMtlsOkHttpClient
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Registry
|
||||
@@ -9,6 +10,7 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
@@ -24,6 +26,7 @@ import java.io.InputStream
|
||||
*
|
||||
* The configuration mirrors the SSL setup used in RetrofitsImpl for API calls.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@GlideModule
|
||||
class BitwardenAppGlideModule : AppGlideModule() {
|
||||
|
||||
@@ -37,6 +40,11 @@ class BitwardenAppGlideModule : AppGlideModule() {
|
||||
* Provides access to the [CertificateManager] for mTLS certificate management.
|
||||
*/
|
||||
fun certificateManager(): CertificateManager
|
||||
|
||||
/**
|
||||
* Provides access to the [NetworkCookieManager] for cookie-based authentication.
|
||||
*/
|
||||
fun networkCookieManager(): NetworkCookieManager
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
@@ -46,12 +54,20 @@ class BitwardenAppGlideModule : AppGlideModule() {
|
||||
entryPoint = BitwardenGlideEntryPoint::class.java,
|
||||
)
|
||||
val certificateManager = entryPoint.certificateManager()
|
||||
val networkCookieManager = entryPoint.networkCookieManager()
|
||||
|
||||
// Register OkHttpUrlLoader that uses our mTLS OkHttpClient
|
||||
// Build OkHttpClient with mTLS and cookie support
|
||||
val client = certificateManager
|
||||
.createMtlsOkHttpClient()
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(GlideCookieInterceptor(networkCookieManager))
|
||||
.build()
|
||||
|
||||
// Register OkHttpUrlLoader that uses our mTLS + cookie OkHttpClient
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(certificateManager.createMtlsOkHttpClient()),
|
||||
OkHttpUrlLoader.Factory(client),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.provider.CookieProvider
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
private const val HEADER_COOKIE = "Cookie"
|
||||
private const val HTTP_302 = 302
|
||||
|
||||
/**
|
||||
* Interceptor that attaches cookies to Glide image requests for enterprise environments
|
||||
* requiring cookie-based authentication.
|
||||
*
|
||||
* Unlike [com.bitwarden.network.interceptor.CookieInterceptor], this interceptor does not
|
||||
* trigger cookie acquisition. It throws [CookieRedirectException] on HTTP 302 responses
|
||||
* to prevent Glide from following redirects and caching invalid content.
|
||||
*
|
||||
* @property cookieProvider Provider for retrieving cookies by hostname.
|
||||
*/
|
||||
class GlideCookieInterceptor(
|
||||
private val cookieProvider: CookieProvider,
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val hostname = originalRequest.url.host
|
||||
val cookieHeader = cookieProvider
|
||||
.getCookies(hostname)
|
||||
.joinToString("; ") { "${it.name}=${it.value}" }
|
||||
|
||||
if (cookieHeader.isEmpty()) return chain.proceed(originalRequest).also(::check302)
|
||||
|
||||
val request = originalRequest
|
||||
.newBuilder()
|
||||
.header(HEADER_COOKIE, cookieHeader)
|
||||
.build()
|
||||
return chain.proceed(request).also(::check302)
|
||||
}
|
||||
|
||||
private fun check302(response: Response) {
|
||||
if (response.code == HTTP_302) {
|
||||
response.close()
|
||||
throw CookieRedirectException(
|
||||
hostname = response.request.url.host,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,17 @@ class BitwardenAppGlideModuleTest {
|
||||
|
||||
assertTrue(hasCertificateManagerMethod)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BitwardenGlideEntryPoint should declare networkCookieManager method`() {
|
||||
val entryPointInterface = BitwardenAppGlideModule::class.java
|
||||
.declaredClasses
|
||||
.firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" }
|
||||
|
||||
val methods = requireNotNull(entryPointInterface).declaredMethods
|
||||
val hasNetworkCookieManagerMethod =
|
||||
methods.any { it.name == "networkCookieManager" }
|
||||
|
||||
assertTrue(hasNetworkCookieManagerMethod)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.interceptor.FakeInterceptorChain
|
||||
import com.bitwarden.network.model.NetworkCookie
|
||||
import com.bitwarden.network.provider.CookieProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
class GlideCookieInterceptorTest {
|
||||
|
||||
private val mockCookieProvider: CookieProvider = mockk()
|
||||
|
||||
private val interceptor = GlideCookieInterceptor(
|
||||
cookieProvider = mockCookieProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `intercept should proceed without cookie header when no cookies available`() {
|
||||
val originalRequest = Request.Builder()
|
||||
.url("https://vault.bitwarden.com/icons/icon.png")
|
||||
.build()
|
||||
val chain = FakeInterceptorChain(originalRequest)
|
||||
|
||||
every {
|
||||
mockCookieProvider.getCookies("vault.bitwarden.com")
|
||||
} returns emptyList()
|
||||
|
||||
val response = interceptor.intercept(chain)
|
||||
|
||||
assertEquals(originalRequest, response.request)
|
||||
assertNull(response.request.header("Cookie"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should attach single cookie correctly`() {
|
||||
val originalRequest = Request.Builder()
|
||||
.url("https://vault.bitwarden.com/icons/icon.png")
|
||||
.build()
|
||||
val chain = FakeInterceptorChain(originalRequest)
|
||||
|
||||
every {
|
||||
mockCookieProvider.getCookies("vault.bitwarden.com")
|
||||
} returns listOf(
|
||||
NetworkCookie(name = "awselb", value = "session123"),
|
||||
)
|
||||
|
||||
val response = interceptor.intercept(chain)
|
||||
|
||||
assertEquals("awselb=session123", response.request.header("Cookie"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should attach multiple cookies in correct format`() {
|
||||
val originalRequest = Request.Builder()
|
||||
.url("https://vault.bitwarden.com/icons/icon.png")
|
||||
.build()
|
||||
val chain = FakeInterceptorChain(originalRequest)
|
||||
|
||||
every {
|
||||
mockCookieProvider.getCookies("vault.bitwarden.com")
|
||||
} returns listOf(
|
||||
NetworkCookie(name = "awselb", value = "session123"),
|
||||
NetworkCookie(name = "awselbcors", value = "cors456"),
|
||||
)
|
||||
|
||||
val response = interceptor.intercept(chain)
|
||||
|
||||
assertEquals(
|
||||
"awselb=session123; awselbcors=cors456",
|
||||
response.request.header("Cookie"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should throw CookieRedirectException on 302 response without cookies`() {
|
||||
val originalRequest = Request.Builder()
|
||||
.url("https://vault.bitwarden.com/icons/icon.png")
|
||||
.build()
|
||||
|
||||
val redirectResponse = Response.Builder()
|
||||
.code(302)
|
||||
.message("Found")
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.request(originalRequest)
|
||||
.header("Location", "https://idp.example.com/auth")
|
||||
.build()
|
||||
|
||||
val chain = FakeInterceptorChain(
|
||||
request = originalRequest,
|
||||
responseProvider = { redirectResponse },
|
||||
)
|
||||
|
||||
every {
|
||||
mockCookieProvider.getCookies("vault.bitwarden.com")
|
||||
} returns emptyList()
|
||||
|
||||
val exception = assertThrows<CookieRedirectException> {
|
||||
interceptor.intercept(chain)
|
||||
}
|
||||
|
||||
assertEquals("vault.bitwarden.com", exception.hostname)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should throw CookieRedirectException on 302 response with cookies`() {
|
||||
val originalRequest = Request.Builder()
|
||||
.url("https://vault.bitwarden.com/icons/icon.png")
|
||||
.build()
|
||||
|
||||
val redirectResponse = Response.Builder()
|
||||
.code(302)
|
||||
.message("Found")
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.request(originalRequest)
|
||||
.header("Location", "https://idp.example.com/auth")
|
||||
.build()
|
||||
|
||||
val chain = FakeInterceptorChain(
|
||||
request = originalRequest,
|
||||
responseProvider = { redirectResponse },
|
||||
)
|
||||
|
||||
every {
|
||||
mockCookieProvider.getCookies("vault.bitwarden.com")
|
||||
} returns listOf(
|
||||
NetworkCookie(name = "awselb", value = "session123"),
|
||||
)
|
||||
|
||||
val exception = assertThrows<CookieRedirectException> {
|
||||
interceptor.intercept(chain)
|
||||
}
|
||||
|
||||
assertEquals("vault.bitwarden.com", exception.hostname)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should not call needsBootstrap or acquireCookies`() {
|
||||
val originalRequest = Request.Builder()
|
||||
.url("https://vault.bitwarden.com/icons/icon.png")
|
||||
.build()
|
||||
val chain = FakeInterceptorChain(originalRequest)
|
||||
|
||||
every {
|
||||
mockCookieProvider.getCookies("vault.bitwarden.com")
|
||||
} returns emptyList()
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
verify(exactly = 0) {
|
||||
mockCookieProvider.needsBootstrap(any())
|
||||
mockCookieProvider.acquireCookies(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user