Skip to content

Commit 72919d3

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 72919d3

File tree

11 files changed

+1059
-0
lines changed

11 files changed

+1059
-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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
// Use safe string-based path matching instead of regex to prevent ReDoS attacks
79+
return matchesPathPattern(path, requestPath);
80+
}
81+
82+
/**
83+
* Safely matches a path pattern against a request path without using regex. Handles path
84+
* parameters like {session-id} by treating them as wildcards.
85+
*
86+
* @param pattern The path pattern to match against
87+
* @param requestPath The actual request path
88+
* @return true if the paths match
89+
*/
90+
private boolean matchesPathPattern(String pattern, String requestPath) {
91+
// Split both paths into segments
92+
String[] patternSegments = pattern.split("/", -1); // keep trailing empty segments
93+
String[] requestSegments = requestPath.split("/", -1);
94+
95+
// Paths must have the same number of segments
96+
if (patternSegments.length != requestSegments.length) {
97+
return false;
98+
}
99+
100+
// Compare each segment
101+
for (int i = 0; i < patternSegments.length; i++) {
102+
String patternSegment = patternSegments[i];
103+
String requestSegment = requestSegments[i];
104+
105+
// If both are empty (leading/trailing slash), continue
106+
if (patternSegment.isEmpty() && requestSegment.isEmpty()) {
107+
continue;
108+
}
109+
// If pattern segment is a path parameter (enclosed in {}), it matches any non-empty segment
110+
if (isPathParameter(patternSegment)) {
111+
if (requestSegment.isEmpty()) {
112+
return false;
113+
}
114+
} else {
115+
// For literal segments, they must match exactly
116+
if (!patternSegment.equals(requestSegment)) {
117+
return false;
118+
}
119+
}
120+
}
121+
122+
return true;
123+
}
124+
125+
/**
126+
* Checks if a path segment is a path parameter (enclosed in curly braces).
127+
*
128+
* @param segment The path segment to check
129+
* @return true if it's a path parameter
130+
*/
131+
private boolean isPathParameter(String segment) {
132+
return segment.startsWith("{") && segment.endsWith("}") && segment.length() > 2;
133+
}
134+
135+
@Override
136+
public String toString() {
137+
return method + ":" + path;
138+
}
139+
140+
@Override
141+
public boolean equals(Object obj) {
142+
if (this == obj) return true;
143+
if (obj == null || getClass() != obj.getClass()) return false;
144+
BlockedRoute that = (BlockedRoute) obj;
145+
return method.equals(that.method) && path.equals(that.path);
146+
}
147+
148+
@Override
149+
public int hashCode() {
150+
return method.hashCode() * 31 + path.hashCode();
151+
}
152+
}
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.beust.jcommander.Parameter;
2525
import com.google.auto.service.AutoService;
2626
import java.util.Collections;
27+
import java.util.List;
2728
import java.util.Set;
2829
import org.openqa.selenium.grid.config.ConfigValue;
2930
import org.openqa.selenium.grid.config.HasRoles;
@@ -74,6 +75,28 @@ public class RouterFlags implements HasRoles {
7475
@ConfigValue(section = ROUTER_SECTION, name = "disable-ui", example = "true")
7576
public boolean disableUi = false;
7677

78+
@Parameter(
79+
names = {"--blocked-routes"},
80+
description =
81+
"Route to block in format 'METHOD:path'. Can be specified multiple times."
82+
+ " Example: --blocked-routes DELETE:/session/{session-id} --blocked-routes"
83+
+ " DELETE:/se/grid/distributor/node/{node-id}")
84+
@ConfigValue(
85+
section = ROUTER_SECTION,
86+
name = "blocked-routes",
87+
example =
88+
"[\"DELETE:/session/{session-id}\", \"DELETE:/se/grid/distributor/node/{node-id}\"]")
89+
public List<String> blockedRoutes;
90+
91+
@Parameter(
92+
names = {"--blocked-delete-session"},
93+
arity = 1,
94+
description =
95+
"A flag to prevent deleting a session proactively by blocking DELETE requests to"
96+
+ " /session/{session-id} route")
97+
@ConfigValue(section = ROUTER_SECTION, name = "blocked-delete-session", example = "true")
98+
public boolean blockedDeleteSession = false;
99+
77100
@Override
78101
public Set<Role> getRoles() {
79102
return Collections.singleton(ROUTER_ROLE);

0 commit comments

Comments
 (0)