Skip to content

Commit 0eb9b30

Browse files
lazergmeta-codesync[bot]
authored andcommitted
Fix Android WebSocket cookie lookup stripping the URL path (#58365)
Summary: `WebSocketModule.getCookie` looks up cookies through `getDefaultOrigin(uri)`, which strips the URL down to `scheme://host[:port]` before handing it to `ForwardingCookieHandler`. Android's `CookieManager` matches cookies against the full URL (domain and path), so any cookie set with a `Path` other than `/` gets silently dropped from the WebSocket handshake, breaking auth/session cookies scoped to a sub-path (e.g. `/signal-r/hubs/messages`). `getDefaultOrigin` is also used to build the `origin` header for the handshake, where stripping the path is correct per the WebSocket protocol, so it can't just be changed in place without affecting that header too. This adds a separate `getCookieLookupUri` that does the same `ws(s)://` to `http(s)://` scheme mapping but keeps the path, query, and fragment, and uses it only for the cookie lookup. It also drops the URI's userinfo from that lookup, since it plays no role in cookie matching and shouldn't be forwarded into the CookieManager call. ## Changelog: [ANDROID] [FIXED] - Fix WebSocket cookie lookup dropping path-scoped cookies Pull Request resolved: #58365 Test Plan: Added `WebSocketModuleTest`, exercising `getCookieLookupUri` through reflection and asserting the path, port, and query survive the ws/wss -> http/https conversion. Couldn't run it through the repo's own Gradle/Robolectric setup in this environment (`react-native-gradle-plugin` isn't resolvable without the full monorepo build), so I compiled the real companion object with `kotlinc` standalone and ran the same reflection lookup against it directly, confirming both cases pass and that the private companion method resolves without a `NoSuchMethodException`. Fixes #58358 Reviewed By: cipolleschi Differential Revision: D119093059 Pulled By: javache fbshipit-source-id: c5404937c97b497d3eae8c45433785f057602c29
1 parent 07a6e1f commit 0eb9b30

2 files changed

Lines changed: 79 additions & 9 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.kt

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ public class WebSocketModule(context: ReactApplicationContext) :
427427
*/
428428
private fun getCookie(uri: String): String? {
429429
try {
430-
val origin = URI(getDefaultOrigin(uri))
430+
val origin = getCookieLookupUri(uri)
431431
val cookieMap = cookieHandler.get(origin, HashMap<String, List<String>>())
432432
val cookieList = cookieMap["Cookie"]
433433
if (cookieList.isNullOrEmpty()) {
@@ -459,6 +459,16 @@ public class WebSocketModule(context: ReactApplicationContext) :
459459
customClientBuilder?.apply(builder)
460460
}
461461

462+
/** Map a WebSocket URI's scheme to its HTTP(S) equivalent, e.g. "wss" -> "https". */
463+
private fun httpSchemeFor(requestURI: URI): String =
464+
when (requestURI.scheme) {
465+
"wss" -> "https"
466+
"ws" -> "http"
467+
"http",
468+
"https" -> requestURI.scheme
469+
else -> ""
470+
}
471+
462472
/**
463473
* Get the default HTTP(S) origin for a specific WebSocket URI
464474
*
@@ -468,14 +478,7 @@ public class WebSocketModule(context: ReactApplicationContext) :
468478
private fun getDefaultOrigin(uri: String): String {
469479
try {
470480
val requestURI = URI(uri)
471-
val scheme =
472-
when (requestURI.scheme) {
473-
"wss" -> "https"
474-
"ws" -> "http"
475-
"http",
476-
"https" -> requestURI.scheme
477-
else -> ""
478-
}
481+
val scheme = httpSchemeFor(requestURI)
479482

480483
val defaultOrigin =
481484
if (requestURI.port != -1) {
@@ -489,5 +492,31 @@ public class WebSocketModule(context: ReactApplicationContext) :
489492
throw IllegalArgumentException("Unable to set $uri as default origin header")
490493
}
491494
}
495+
496+
/**
497+
* Get the URI used to look up cookies for a specific WebSocket URI, keeping its path so that
498+
* path-scoped cookies are matched correctly. Query and fragment are dropped since cookies are
499+
* scoped by path, not by query or fragment (RFC 6265). userInfo is also dropped so that
500+
* credentials embedded in the URL are never forwarded to the cookie store.
501+
*
502+
* @param uri
503+
* @return A URI with the endpoint converted to HTTP protocol (http[s]://host[:port]/path)
504+
*/
505+
private fun getCookieLookupUri(uri: String): URI {
506+
try {
507+
val requestURI = URI(uri)
508+
return URI(
509+
httpSchemeFor(requestURI),
510+
null,
511+
requestURI.host,
512+
requestURI.port,
513+
requestURI.path,
514+
null,
515+
null,
516+
)
517+
} catch (e: URISyntaxException) {
518+
throw IllegalArgumentException("Unable to get cookie lookup URI from $uri")
519+
}
520+
}
492521
}
493522
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.modules.websocket
9+
10+
import java.net.URI
11+
import org.assertj.core.api.Assertions.assertThat
12+
import org.junit.Test
13+
14+
class WebSocketModuleTest {
15+
16+
private fun getCookieLookupUri(uri: String): URI {
17+
val method =
18+
WebSocketModule.Companion::class
19+
.java
20+
.getDeclaredMethod(
21+
"getCookieLookupUri",
22+
String::class.java,
23+
)
24+
method.isAccessible = true
25+
return method.invoke(WebSocketModule.Companion, uri) as URI
26+
}
27+
28+
@Test
29+
fun getCookieLookupUri_keepsPathForCookieMatching() {
30+
val uri = getCookieLookupUri("wss://my.domain/signal-r/hubs/messages")
31+
32+
assertThat(uri.toString()).isEqualTo("https://my.domain/signal-r/hubs/messages")
33+
}
34+
35+
@Test
36+
fun getCookieLookupUri_keepsPortAndDropsQuery() {
37+
val uri = getCookieLookupUri("ws://my.domain:8080/path?token=abc")
38+
39+
assertThat(uri.toString()).isEqualTo("http://my.domain:8080/path")
40+
}
41+
}

0 commit comments

Comments
 (0)