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 @@ -30,40 +30,51 @@
import android.security.KeyChainAliasCallback;

import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import com.reactnativecommunity.webview.RNCWebViewManager;
import expo.modules.filesystem.legacy.FileSystemLegacyModule;
import chat.rocket.reactnative.networking.ExpoImageClient;

public class SSLPinningTurboModule extends NativeSSLPinningSpec implements KeyChainAliasCallback {

private Promise promise;
private static String alias;
private static ReactApplicationContext reactContext;
private static OkHttpClient sharedClient;
private static String sharedClientAlias;

public static OkHttpClient getSharedOkHttpClient() {
if (sharedClient != null) {
public static synchronized OkHttpClient getSharedOkHttpClient() {
if (sharedClient != null && Objects.equals(sharedClientAlias, alias)) {
return sharedClient;
}
if (alias != null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.cookieJar(new ReactCookieJarContainer());
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.cookieJar(new ReactCookieJarContainer())
.addInterceptor(chain -> {
okhttp3.Request original = chain.request();
if (original.header("User-Agent") != null) {
return chain.proceed(original);
}
okhttp3.Request request = original.newBuilder()
.header("User-Agent", UserAgent.get())
.build();
return chain.proceed(request);
});

if (alias != null) {
SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
X509TrustManager trustManager = getTrustManagerFactory();
if (sslSocketFactory != null) {
builder.sslSocketFactory(sslSocketFactory, trustManager);
}

sharedClient = builder.build();
return sharedClient;
}
return null;

sharedClient = builder.build();
sharedClientAlias = alias;
return sharedClient;
}

public SSLPinningTurboModule(ReactApplicationContext reactContext) {
Expand All @@ -85,25 +96,7 @@ public void apply(OkHttpClient.Builder builder) {
}

protected OkHttpClient getOkHttpClient() {
OkHttpClient shared = getSharedOkHttpClient();
if (shared != null) {
return shared;
}
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.cookieJar(new ReactCookieJarContainer());

if (alias != null) {
SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
X509TrustManager trustManager = getTrustManagerFactory();
if (sslSocketFactory != null) {
builder.sslSocketFactory(sslSocketFactory, trustManager);
}
}

return builder.build();
return getSharedOkHttpClient();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package chat.rocket.reactnative.networking;

import chat.rocket.reactnative.BuildConfig;

/**
* Shared User-Agent for native Android HTTP requests.
*/
public class UserAgent {
public static String get() {
String systemVersion = android.os.Build.VERSION.RELEASE;
String appVersion = BuildConfig.VERSION_NAME;
int buildNumber = BuildConfig.VERSION_CODE;
return String.format("RC Mobile; android %s; v%s (%d)", systemVersion, appVersion, buildNumber);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.concurrent.TimeoutException;

import chat.rocket.reactnative.BuildConfig;
import chat.rocket.reactnative.networking.UserAgent;

/**
* Shared utility methods for notification handling
Expand Down Expand Up @@ -46,10 +47,7 @@ public static String sanitizeUrl(String url) {
* @return User-Agent string
*/
public static String getUserAgent() {
String systemVersion = android.os.Build.VERSION.RELEASE;
String appVersion = BuildConfig.VERSION_NAME;
int buildNumber = BuildConfig.VERSION_CODE;
return String.format("RC Mobile; android %s; v%s (%d)", systemVersion, appVersion, buildNumber);
return UserAgent.get();
}

/**
Expand Down Expand Up @@ -109,4 +107,3 @@ public static Bitmap fetchAvatarBitmap(
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ class DDPClient {
companion object {
private const val TAG = "RocketChat.DDPClient"
private val sharedClient: OkHttpClient by lazy {
SSLPinningTurboModule.getSharedOkHttpClient() ?: OkHttpClient.Builder()
SSLPinningTurboModule.getSharedOkHttpClient()
.newBuilder()
.pingInterval(30, TimeUnit.SECONDS)
.build()
}
Expand Down Expand Up @@ -349,4 +350,8 @@ class DDPClient {
internal fun testSetActiveWebSocket(ws: WebSocket?) {
this.webSocket = ws
}

internal fun testPingIntervalMillis(): Int {
return client.pingIntervalMillis
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class MediaCallsAnswerRequest(
private const val TAG = "RocketChat.MediaCallsAnswerRequest"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
private val httpClient: OkHttpClient by lazy {
val base = SSLPinningTurboModule.getSharedOkHttpClient() ?: OkHttpClient()
val base = SSLPinningTurboModule.getSharedOkHttpClient()
base.newBuilder()
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(5, TimeUnit.SECONDS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package chat.rocket.reactnative.networking

import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28])
class SSLPinningTurboModuleTest {
private lateinit var server: MockWebServer

@Before
fun setUp() {
server = MockWebServer()
server.start()
}

@After
fun tearDown() {
server.shutdown()
}

@Test
fun `shared OkHttp client injects RC Mobile User-Agent when absent`() {
server.enqueue(MockResponse().setBody("{}"))

val client = SSLPinningTurboModule.getSharedOkHttpClient()
val request = Request.Builder()
.url(server.url("/api/v1/test"))
.build()

client.newCall(request).execute().use { response ->
assertEquals(200, response.code)
}

val userAgent = server.takeRequest().getHeader("User-Agent")
assertNotNull("User-Agent header must be present", userAgent)
assertTrue(
"User-Agent must start with 'RC Mobile' (got: $userAgent)",
userAgent!!.startsWith("RC Mobile")
)
}

@Test
fun `shared OkHttp client preserves explicit User-Agent`() {
server.enqueue(MockResponse().setBody("{}"))

val client = SSLPinningTurboModule.getSharedOkHttpClient()
val request = Request.Builder()
.url(server.url("/api/v1/test"))
.header("User-Agent", "Custom UA")
.build()

client.newCall(request).execute().use { response ->
assertEquals(200, response.code)
}

assertEquals("Custom UA", server.takeRequest().getHeader("User-Agent"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ private class StubWebSocket : WebSocket {
@Config(sdk = [28])
class DDPClientTest {

@Test
fun `shared client preserves websocket ping interval`() {
val client = DDPClient()

assertEquals(TimeUnit.SECONDS.toMillis(30).toInt(), client.testPingIntervalMillis())
}

@Test
fun `connected before connect timeout invokes callback exactly once`() {
val client = DDPClient()
Expand Down