diff --git a/java/src/org/openqa/selenium/grid/commands/Standalone.java b/java/src/org/openqa/selenium/grid/commands/Standalone.java index ec3c0080d5262..fd8315ff95d7d 100644 --- a/java/src/org/openqa/selenium/grid/commands/Standalone.java +++ b/java/src/org/openqa/selenium/grid/commands/Standalone.java @@ -222,7 +222,7 @@ protected Handlers createHandlers(Config config) { httpHandler = combine(httpHandler, Route.get("/readyz").to(() -> readinessCheck)); Node node = createNode(config, bus, distributor, combinedHandler); - return new Handlers(httpHandler, new ProxyNodeWebsockets(clientFactory, node)); + return new Handlers(httpHandler, new ProxyNodeWebsockets(clientFactory, node, subPath)); } @Override diff --git a/java/src/org/openqa/selenium/grid/node/ProxyNodeWebsockets.java b/java/src/org/openqa/selenium/grid/node/ProxyNodeWebsockets.java index ddff2ddcd7693..1bc11a5bed575 100644 --- a/java/src/org/openqa/selenium/grid/node/ProxyNodeWebsockets.java +++ b/java/src/org/openqa/selenium/grid/node/ProxyNodeWebsockets.java @@ -56,18 +56,20 @@ public class ProxyNodeWebsockets ImmutableSet.of("goog:chromeOptions", "moz:debuggerAddress", "ms:edgeOptions"); private final HttpClient.Factory clientFactory; private final Node node; + private final String gridSubPath; - public ProxyNodeWebsockets(HttpClient.Factory clientFactory, Node node) { + public ProxyNodeWebsockets(HttpClient.Factory clientFactory, Node node, String gridSubPath) { this.clientFactory = Objects.requireNonNull(clientFactory); this.node = Objects.requireNonNull(node); + this.gridSubPath = gridSubPath; } @Override public Optional> apply(String uri, Consumer downstream) { - UrlTemplate.Match fwdMatch = FWD_TEMPLATE.match(uri); - UrlTemplate.Match cdpMatch = CDP_TEMPLATE.match(uri); - UrlTemplate.Match bidiMatch = BIDI_TEMPLATE.match(uri); - UrlTemplate.Match vncMatch = VNC_TEMPLATE.match(uri); + UrlTemplate.Match fwdMatch = FWD_TEMPLATE.match(uri, gridSubPath); + UrlTemplate.Match cdpMatch = CDP_TEMPLATE.match(uri, gridSubPath); + UrlTemplate.Match bidiMatch = BIDI_TEMPLATE.match(uri, gridSubPath); + UrlTemplate.Match vncMatch = VNC_TEMPLATE.match(uri, gridSubPath); if (bidiMatch == null && cdpMatch == null && vncMatch == null && fwdMatch == null) { return Optional.empty(); diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java index bbec41c58ed24..cdaaf1038479e 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java @@ -162,6 +162,21 @@ public boolean isManagedDownloadsEnabled() { return config.getBool(NODE_SECTION, "enable-managed-downloads").orElse(Boolean.FALSE); } + public String getGridSubPath() { + return normalizeSubPath(getPublicGridUri().map(URI::getPath).orElse("")); + } + + public static String normalizeSubPath(String prefix) { + prefix = prefix.trim(); + if (!prefix.startsWith("/")) { + prefix = "/" + prefix; // Prefix with a '/' if absent. + } + if (prefix.endsWith("/")) { + prefix = prefix.substring(0, prefix.length() - 1); // Remove the trailing '/' if present. + } + return prefix; + } + public Node getNode() { return config.getClass(NODE_SECTION, "implementation", Node.class, DEFAULT_NODE_IMPLEMENTATION); } diff --git a/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java b/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java index 59ea96ea8c3c0..c86429aeaff64 100644 --- a/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java +++ b/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java @@ -171,7 +171,8 @@ protected Handlers createHandlers(Config config) { Route httpHandler = Route.combine(node, get("/readyz").to(() -> readinessCheck)); - return new Handlers(httpHandler, new ProxyNodeWebsockets(clientFactory, node)); + return new Handlers( + httpHandler, new ProxyNodeWebsockets(clientFactory, node, nodeOptions.getGridSubPath())); } @Override diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java index 1c7b2f31b156c..0e5d283158536 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java @@ -838,9 +838,7 @@ private Session createExternalSession( private URI rewrite(String path) { try { String scheme = "https".equals(gridUri.getScheme()) ? "wss" : "ws"; - if (gridUri.getPath() != null && !gridUri.getPath().equals("/")) { - path = gridUri.getPath() + path; - } + path = NodeOptions.normalizeSubPath(gridUri.getPath()) + path; return new URI( scheme, gridUri.getUserInfo(), gridUri.getHost(), gridUri.getPort(), path, null, null); } catch (URISyntaxException e) { diff --git a/java/src/org/openqa/selenium/remote/http/UrlTemplate.java b/java/src/org/openqa/selenium/remote/http/UrlTemplate.java index 0e8e2d5737b46..1cf0b6fa0e231 100644 --- a/java/src/org/openqa/selenium/remote/http/UrlTemplate.java +++ b/java/src/org/openqa/selenium/remote/http/UrlTemplate.java @@ -139,6 +139,18 @@ public UrlTemplate.Match match(String matchAgainst) { return compiled.apply(matchAgainst); } + /** + * @return A {@link Match} with all parameters filled if successful, null otherwise. Remove + * subPath from matchAgainst before matching. + */ + public UrlTemplate.Match match(String matchAgainst, String prefix) { + if (matchAgainst == null || prefix == null) { + return null; + } + matchAgainst = matchAgainst.replaceFirst(prefix, ""); + return compiled.apply(matchAgainst); + } + @SuppressWarnings("InnerClassMayBeStatic") public class Match { private final String url; diff --git a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java index e0050ab161bfc..43d413cdab362 100644 --- a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java +++ b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java @@ -673,6 +673,28 @@ void settingTheHubWithDefaultValueSetsTheGridUrlToTheNonLoopbackAddress() { .isEqualTo(Optional.of(URI.create(nonLoopbackAddressUrl))); } + @Test + void settingSubPathForNodeServerExtractFromGridUrl() { + String[] rawConfig = + new String[] { + "[node]", "grid-url = \"http://localhost:4444/mySubPath\"", + }; + Config config = new TomlConfig(new StringReader(String.join("\n", rawConfig))); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getGridSubPath()).isEqualTo("/mySubPath"); + } + + @Test + void settingSubPathForNodeServerExtractFromHub() { + String[] rawConfig = + new String[] { + "[node]", "hub = \"http://0.0.0.0:4444/mySubPath\"", + }; + Config config = new TomlConfig(new StringReader(String.join("\n", rawConfig))); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getGridSubPath()).isEqualTo("/mySubPath"); + } + @Test void notSettingSlotMatcherAvailable() { String[] rawConfig = diff --git a/java/test/org/openqa/selenium/remote/http/UrlTemplateTest.java b/java/test/org/openqa/selenium/remote/http/UrlTemplateTest.java index c8703235885aa..3c345517b9363 100644 --- a/java/test/org/openqa/selenium/remote/http/UrlTemplateTest.java +++ b/java/test/org/openqa/selenium/remote/http/UrlTemplateTest.java @@ -67,6 +67,15 @@ void itIsFineForTheFirstCharacterToBeAPattern() { assertThat(match.getParameters()).isEqualTo(ImmutableMap.of("cake", "cheese")); } + @Test + void shouldMatchAgainstUrlTemplateExcludePrefix() { + UrlTemplate.Match match = + new UrlTemplate("/session/{id}/se/vnc").match("/prefix/session/1234/se/vnc", "/prefix"); + + assertThat(match.getUrl()).isEqualTo("/session/1234/se/vnc"); + assertThat(match.getParameters()).isEqualTo(ImmutableMap.of("id", "1234")); + } + @Test void aNullMatchDoesNotCauseANullPointerExceptionToBeThrown() { assertThat(new UrlTemplate("/").match(null)).isNull();