diff --git a/app/build.gradle b/app/build.gradle index b1f08360e..406832c17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,6 +189,8 @@ dependencies { testImplementation "org.mockito:mockito-inline:4.4.0" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" + testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.test.ext:junit:1.1.3" diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java deleted file mode 100644 index a3e1a815f..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java +++ /dev/null @@ -1,88 +0,0 @@ -/* Copyright 2018 charlag - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.network; - -import android.util.Log; -import androidx.annotation.NonNull; - -import com.keylesspalace.tusky.db.AccountEntity; -import com.keylesspalace.tusky.db.AccountManager; - -import java.io.IOException; - -import okhttp3.*; - -/** - * Created by charlag on 31/10/17. - */ - -public final class InstanceSwitchAuthInterceptor implements Interceptor { - private final AccountManager accountManager; - - public InstanceSwitchAuthInterceptor(AccountManager accountManager) { - this.accountManager = accountManager; - } - - @NonNull - @Override - public Response intercept(@NonNull Chain chain) throws IOException { - - Request originalRequest = chain.request(); - - // only switch domains if the request comes from retrofit - if (originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) { - AccountEntity currentAccount = accountManager.getActiveAccount(); - - Request.Builder builder = originalRequest.newBuilder(); - - String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER); - if (instanceHeader != null) { - // use domain explicitly specified in custom header - builder.url(swapHost(originalRequest.url(), instanceHeader)); - builder.removeHeader(MastodonApi.DOMAIN_HEADER); - } else if (currentAccount != null) { - String accessToken = currentAccount.getAccessToken(); - if (!accessToken.isEmpty()) { - //use domain of current account - builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) - .header("Authorization", - String.format("Bearer %s", currentAccount.getAccessToken())); - } - } - Request newRequest = builder.build(); - - if (MastodonApi.PLACEHOLDER_DOMAIN.equals(newRequest.url().host())) { - Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url()); - return new Response.Builder() - .code(400) - .message("Bad Request") - .protocol(Protocol.HTTP_2) - .body(ResponseBody.create("", MediaType.parse("text/plain"))) - .request(chain.request()) - .build(); - } - return chain.proceed(newRequest); - - } else { - return chain.proceed(originalRequest); - } - } - - @NonNull - private static HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) { - return url.newBuilder().host(host).build(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt new file mode 100644 index 000000000..3ca7a8116 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt @@ -0,0 +1,82 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.network + +import android.util.Log +import com.keylesspalace.tusky.db.AccountManager +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.IOException + +class InstanceSwitchAuthInterceptor(private val accountManager: AccountManager) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest: Request = chain.request() + + // only switch domains if the request comes from retrofit + return if (originalRequest.url.host == MastodonApi.PLACEHOLDER_DOMAIN) { + + val builder: Request.Builder = originalRequest.newBuilder() + val instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER) + + if (instanceHeader != null) { + // use domain explicitly specified in custom header + builder.url(swapHost(originalRequest.url, instanceHeader)) + builder.removeHeader(MastodonApi.DOMAIN_HEADER) + } else { + val currentAccount = accountManager.activeAccount + + if (currentAccount != null) { + val accessToken = currentAccount.accessToken + if (accessToken.isNotEmpty()) { + // use domain of current account + builder.url(swapHost(originalRequest.url, currentAccount.domain)) + .header("Authorization", "Bearer %s".format(accessToken)) + } + } + } + + val newRequest: Request = builder.build() + + if (MastodonApi.PLACEHOLDER_DOMAIN == newRequest.url.host) { + Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url) + return Response.Builder() + .code(400) + .message("Bad Request") + .protocol(Protocol.HTTP_2) + .body("".toResponseBody("text/plain".toMediaType())) + .request(chain.request()) + .build() + } + + chain.proceed(newRequest) + } else { + chain.proceed(originalRequest) + } + } + + companion object { + private fun swapHost(url: HttpUrl, host: String): HttpUrl { + return url.newBuilder().host(host).build() + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt b/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt new file mode 100644 index 000000000..aa070489d --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt @@ -0,0 +1,143 @@ +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock + +class InstanceSwitchAuthInterceptorTest { + + private val mockWebServer = MockWebServer() + + @Before + fun setup() { + mockWebServer.start() + } + + @After + fun teardown() { + mockWebServer.shutdown() + } + + @Test + fun `should make regular request when requested`() { + + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { null } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url(mockWebServer.url("/test")) + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + } + + @Test + fun `should make request to instance requested in special header`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { + AccountEntity( + id = 1, + domain = "test.domain", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test") + .header(MastodonApi.DOMAIN_HEADER, mockWebServer.hostName) + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + + assertNull(mockWebServer.takeRequest().getHeader("Authorization")) + } + + @Test + fun `should make request to current instance when requested and user is logged in`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { + AccountEntity( + id = 1, + domain = mockWebServer.hostName, + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test") + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + + assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization")) + } + + @Test + fun `should fail to make request when request to current instance is requested but no user is logged in`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { null } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + "/test") + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(400, response.code) + assertEquals(0, mockWebServer.requestCount) + } +}