Skip to content

Commit 1c45dc9

Browse files
committed
[grid] Add config blocked-routes and specific blocked-delete-session in Router
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent c187279 commit 1c45dc9

File tree

10 files changed

+789
-0
lines changed

10 files changed

+789
-0
lines changed

java/src/org/openqa/selenium/grid/commands/Hub.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.net.MalformedURLException;
3232
import java.net.URL;
3333
import java.util.Collections;
34+
import java.util.List;
3435
import java.util.Set;
3536
import java.util.logging.Level;
3637
import java.util.logging.Logger;
@@ -48,6 +49,8 @@
4849
import org.openqa.selenium.grid.log.LoggingOptions;
4950
import org.openqa.selenium.grid.router.ProxyWebsocketsIntoGrid;
5051
import org.openqa.selenium.grid.router.Router;
52+
import org.openqa.selenium.grid.router.httpd.BlockedRoute;
53+
import org.openqa.selenium.grid.router.httpd.BlockedRoutesFilter;
5154
import org.openqa.selenium.grid.router.httpd.RouterOptions;
5255
import org.openqa.selenium.grid.security.BasicAuthenticationFilter;
5356
import org.openqa.selenium.grid.security.Secret;
@@ -207,6 +210,13 @@ protected Handlers createHandlers(Config config) {
207210
httpHandler = httpHandler.with(new BasicAuthenticationFilter(uap.username(), uap.password()));
208211
}
209212

213+
// Apply blocked routes filter
214+
List<BlockedRoute> blockedRoutes = routerOptions.getBlockedRoutes();
215+
if (!blockedRoutes.isEmpty()) {
216+
LOG.info("Blocking " + blockedRoutes.size() + " route(s): " + blockedRoutes);
217+
httpHandler = BlockedRoutesFilter.with(httpHandler, blockedRoutes);
218+
}
219+
210220
// Allow the liveness endpoint to be reached, since k8s doesn't make it easy to authenticate
211221
// these checks
212222
httpHandler = combine(httpHandler, Route.get("/readyz").to(() -> readinessCheck));

java/src/org/openqa/selenium/grid/commands/Standalone.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.net.URI;
3333
import java.net.URL;
3434
import java.util.Collections;
35+
import java.util.List;
3536
import java.util.Set;
3637
import java.util.logging.Level;
3738
import java.util.logging.Logger;
@@ -53,6 +54,8 @@
5354
import org.openqa.selenium.grid.node.ProxyNodeWebsockets;
5455
import org.openqa.selenium.grid.node.config.NodeOptions;
5556
import org.openqa.selenium.grid.router.Router;
57+
import org.openqa.selenium.grid.router.httpd.BlockedRoute;
58+
import org.openqa.selenium.grid.router.httpd.BlockedRoutesFilter;
5659
import org.openqa.selenium.grid.router.httpd.RouterOptions;
5760
import org.openqa.selenium.grid.security.BasicAuthenticationFilter;
5861
import org.openqa.selenium.grid.security.Secret;
@@ -213,6 +216,13 @@ protected Handlers createHandlers(Config config) {
213216
httpHandler = httpHandler.with(new BasicAuthenticationFilter(uap.username(), uap.password()));
214217
}
215218

219+
// Apply blocked routes filter
220+
List<BlockedRoute> blockedRoutes = routerOptions.getBlockedRoutes();
221+
if (!blockedRoutes.isEmpty()) {
222+
LOG.info("Blocking " + blockedRoutes.size() + " route(s): " + blockedRoutes);
223+
httpHandler = BlockedRoutesFilter.with(httpHandler, blockedRoutes);
224+
}
225+
216226
// Allow the liveness endpoint to be reached, since k8s doesn't make it easy to authenticate
217227
// these checks
218228
httpHandler = combine(httpHandler, Route.get("/readyz").to(() -> readinessCheck));
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.router.httpd;
19+
20+
/** Represents a blocked route with HTTP method and path. */
21+
public class BlockedRoute {
22+
private final String method;
23+
private final String path;
24+
25+
public BlockedRoute(String method, String path) {
26+
this.method = method.toUpperCase();
27+
this.path = path;
28+
}
29+
30+
public String getMethod() {
31+
return method;
32+
}
33+
34+
public String getPath() {
35+
return path;
36+
}
37+
38+
/**
39+
* Creates a BlockedRoute from a string in the format "METHOD:path".
40+
*
41+
* @param routeStr String representation of blocked route
42+
* @return BlockedRoute instance
43+
* @throws IllegalArgumentException if the format is invalid
44+
*/
45+
public static BlockedRoute fromString(String routeStr) {
46+
if (routeStr == null || routeStr.trim().isEmpty()) {
47+
throw new IllegalArgumentException("Route string cannot be null or empty");
48+
}
49+
50+
String[] parts = routeStr.split(":", 2);
51+
if (parts.length != 2) {
52+
throw new IllegalArgumentException(
53+
"Invalid route format. Expected 'METHOD:path', got: " + routeStr);
54+
}
55+
56+
String method = parts[0].trim().toUpperCase();
57+
String path = parts[1].trim();
58+
59+
if (method.isEmpty() || path.isEmpty()) {
60+
throw new IllegalArgumentException("Method and path cannot be empty. Got: " + routeStr);
61+
}
62+
63+
return new BlockedRoute(method, path);
64+
}
65+
66+
/**
67+
* Checks if the given HTTP method and request path match this blocked route.
68+
*
69+
* @param requestMethod HTTP method of the request
70+
* @param requestPath Path of the request
71+
* @return true if the route should be blocked
72+
*/
73+
public boolean matches(String requestMethod, String requestPath) {
74+
if (!method.equals(requestMethod.toUpperCase())) {
75+
return false;
76+
}
77+
78+
// For exact path matching, we need to handle path parameters like {session-id}
79+
// Convert the blocked path pattern to a regex pattern
80+
String pattern = path.replaceAll("\\{[^}]+\\}", "[^/]+");
81+
return requestPath.matches(pattern);
82+
}
83+
84+
@Override
85+
public String toString() {
86+
return method + ":" + path;
87+
}
88+
89+
@Override
90+
public boolean equals(Object obj) {
91+
if (this == obj) return true;
92+
if (obj == null || getClass() != obj.getClass()) return false;
93+
BlockedRoute that = (BlockedRoute) obj;
94+
return method.equals(that.method) && path.equals(that.path);
95+
}
96+
97+
@Override
98+
public int hashCode() {
99+
return method.hashCode() * 31 + path.hashCode();
100+
}
101+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.router.httpd;
19+
20+
import java.net.URI;
21+
import java.util.List;
22+
import java.util.logging.Logger;
23+
import org.openqa.selenium.remote.http.Contents;
24+
import org.openqa.selenium.remote.http.HttpHandler;
25+
import org.openqa.selenium.remote.http.HttpRequest;
26+
import org.openqa.selenium.remote.http.HttpResponse;
27+
import org.openqa.selenium.remote.http.Routable;
28+
29+
/** Filter that blocks requests matching specified routes. */
30+
public class BlockedRoutesFilter implements HttpHandler {
31+
32+
private static final Logger LOG = Logger.getLogger(BlockedRoutesFilter.class.getName());
33+
private final List<BlockedRoute> blockedRoutes;
34+
private final HttpHandler delegate;
35+
36+
public BlockedRoutesFilter(List<BlockedRoute> blockedRoutes, HttpHandler delegate) {
37+
this.blockedRoutes = blockedRoutes;
38+
this.delegate = delegate;
39+
}
40+
41+
@Override
42+
public HttpResponse execute(HttpRequest request) {
43+
String method = request.getMethod().toString();
44+
String path = URI.create(request.getUri()).getPath();
45+
46+
// Check if the request matches any blocked route
47+
for (BlockedRoute blockedRoute : blockedRoutes) {
48+
if (blockedRoute.matches(method, path)) {
49+
LOG.warning(
50+
"Blocked request: "
51+
+ method
52+
+ " "
53+
+ path
54+
+ " (matches blocked route: "
55+
+ blockedRoute
56+
+ ")");
57+
return new HttpResponse()
58+
.setStatus(403) // Forbidden
59+
.setContent(
60+
Contents.utf8String("Route blocked by configuration: " + method + " " + path));
61+
}
62+
}
63+
64+
// If not blocked, delegate to the next handler
65+
return delegate.execute(request);
66+
}
67+
68+
/** Creates a Routable that applies the blocked routes filter. */
69+
public static Routable with(Routable routable, List<BlockedRoute> blockedRoutes) {
70+
if (blockedRoutes == null || blockedRoutes.isEmpty()) {
71+
return routable;
72+
}
73+
74+
return new Routable() {
75+
@Override
76+
public HttpResponse execute(HttpRequest req) {
77+
return new BlockedRoutesFilter(blockedRoutes, routable).execute(req);
78+
}
79+
80+
@Override
81+
public boolean matches(HttpRequest req) {
82+
return routable.matches(req);
83+
}
84+
};
85+
}
86+
}

java/src/org/openqa/selenium/grid/router/httpd/RouterFlags.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ public class RouterFlags implements HasRoles {
7474
@ConfigValue(section = ROUTER_SECTION, name = "disable-ui", example = "true")
7575
public boolean disableUi = false;
7676

77+
@Parameter(
78+
names = {"--blocked-routes"},
79+
arity = 1,
80+
description =
81+
"Comma-separated list of routes to block in format 'METHOD:path'. Example:"
82+
+ " 'DELETE:/session/{session-id},DELETE:/se/grid/distributor/node/{node-id}'")
83+
@ConfigValue(
84+
section = ROUTER_SECTION,
85+
name = "blocked-routes",
86+
example = "DELETE:/session/{session-id},DELETE:/se/grid/distributor/node/{node-id}")
87+
public String blockedRoutes;
88+
89+
@Parameter(
90+
names = {"--blocked-delete-session"},
91+
arity = 1,
92+
description =
93+
"A flag to prevent deleting a session proactively by blocking DELETE requests to"
94+
+ " /session/{session-id} route")
95+
@ConfigValue(section = ROUTER_SECTION, name = "blocked-delete-session", example = "true")
96+
public boolean blockedDeleteSession = false;
97+
7798
@Override
7899
public Set<Role> getRoles() {
79100
return Collections.singleton(ROUTER_ROLE);

java/src/org/openqa/selenium/grid/router/httpd/RouterOptions.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
package org.openqa.selenium.grid.router.httpd;
1919

20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
2023
import org.openqa.selenium.grid.config.Config;
2124

2225
public class RouterOptions {
@@ -51,4 +54,51 @@ public String subPath() {
5154
public boolean disableUi() {
5255
return config.get(ROUTER_SECTION, "disable-ui").map(Boolean::parseBoolean).orElse(false);
5356
}
57+
58+
/**
59+
* Returns a list of blocked routes from the configuration. Each blocked route should be specified
60+
* in the format "METHOD:path" (e.g., "DELETE:/session/{session-id}"). Multiple routes can be
61+
* specified as comma-separated values. If the blocked-delete-session flag is enabled,
62+
* DELETE:/session/{session-id} will be automatically added.
63+
*
64+
* @return List of blocked routes
65+
*/
66+
public List<BlockedRoute> getBlockedRoutes() {
67+
List<BlockedRoute> routes =
68+
config
69+
.get(ROUTER_SECTION, "blocked-routes")
70+
.map(
71+
blockedRoutesStr -> {
72+
if (blockedRoutesStr.trim().isEmpty()) {
73+
return List.<BlockedRoute>of();
74+
}
75+
76+
return Stream.of(blockedRoutesStr.split(","))
77+
.map(String::trim)
78+
.filter(s -> !s.isEmpty())
79+
.map(BlockedRoute::fromString)
80+
.collect(Collectors.toList());
81+
})
82+
.orElse(List.of());
83+
84+
// Add DELETE session route if the flag is enabled
85+
boolean blockedDeleteSession =
86+
config.getBool(ROUTER_SECTION, "blocked-delete-session").orElse(false);
87+
88+
if (blockedDeleteSession) {
89+
BlockedRoute deleteSessionRoute = new BlockedRoute("DELETE", "/session/{session-id}");
90+
// Only add if not already present
91+
if (routes.stream()
92+
.noneMatch(
93+
route ->
94+
"DELETE".equals(route.getMethod())
95+
&& "/session/{session-id}".equals(route.getPath()))) {
96+
routes =
97+
Stream.concat(routes.stream(), Stream.of(deleteSessionRoute))
98+
.collect(Collectors.toList());
99+
}
100+
}
101+
102+
return routes;
103+
}
54104
}

java/src/org/openqa/selenium/grid/router/httpd/RouterServer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.net.URL;
3838
import java.time.Duration;
3939
import java.util.Collections;
40+
import java.util.List;
4041
import java.util.Set;
4142
import java.util.logging.Level;
4243
import java.util.logging.Logger;
@@ -173,6 +174,13 @@ protected Handlers createHandlers(Config config) {
173174
route = route.with(new BasicAuthenticationFilter(uap.username(), uap.password()));
174175
}
175176

177+
// Apply blocked routes filter
178+
List<BlockedRoute> blockedRoutes = routerOptions.getBlockedRoutes();
179+
if (!blockedRoutes.isEmpty()) {
180+
LOG.info("Blocking " + blockedRoutes.size() + " route(s): " + blockedRoutes);
181+
route = BlockedRoutesFilter.with(route, blockedRoutes);
182+
}
183+
176184
HttpHandler readinessCheck =
177185
req -> {
178186
boolean ready = router.isReady();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("@rules_jvm_external//:defs.bzl", "artifact")
2+
load("//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite")
3+
4+
java_test_suite(
5+
name = "httpd-tests",
6+
size = "small",
7+
srcs = glob(["*Test.java"]),
8+
deps = [
9+
"//java/src/org/openqa/selenium/grid/config",
10+
"//java/src/org/openqa/selenium/grid/router/httpd",
11+
"//java/src/org/openqa/selenium/remote/http",
12+
"//java/test/org/openqa/selenium/testing:test-base",
13+
artifact("com.google.guava:guava"),
14+
artifact("org.junit.jupiter:junit-jupiter-api"),
15+
artifact("org.assertj:assertj-core"),
16+
artifact("org.mockito:mockito-core"),
17+
] + JUNIT5_DEPS,
18+
)

0 commit comments

Comments
 (0)