Skip to content

Commit 15b7de0

Browse files
henryjuclaude
andauthored
SCANJLIB-308 Improve ProxyTest (#284)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c183f9b commit 15b7de0

File tree

4 files changed

+425
-414
lines changed

4 files changed

+425
-414
lines changed

its/it-tests/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
<artifactId>jetty-proxy</artifactId>
8888
<version>${jetty.version}</version>
8989
</dependency>
90+
<dependency>
91+
<groupId>org.wiremock</groupId>
92+
<artifactId>wiremock-standalone</artifactId>
93+
<version>3.10.0</version>
94+
<scope>test</scope>
95+
</dependency>
9096
</dependencies>
9197

9298
<build>
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
* SonarScanner Java Library - ITs
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package com.sonar.scanner.lib.it;
21+
22+
import com.sonar.orchestrator.util.NetworkUtils;
23+
import java.io.IOException;
24+
import java.net.InetAddress;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.Base64;
27+
import java.util.Collection;
28+
import java.util.concurrent.ConcurrentLinkedDeque;
29+
import javax.servlet.ServletException;
30+
import javax.servlet.ServletRequest;
31+
import javax.servlet.ServletResponse;
32+
import javax.servlet.http.HttpServletRequest;
33+
import javax.servlet.http.HttpServletResponse;
34+
import org.eclipse.jetty.client.api.Request;
35+
import org.eclipse.jetty.http.HttpHeader;
36+
import org.eclipse.jetty.proxy.ConnectHandler;
37+
import org.eclipse.jetty.proxy.ProxyServlet;
38+
import org.eclipse.jetty.security.ConstraintMapping;
39+
import org.eclipse.jetty.security.ConstraintSecurityHandler;
40+
import org.eclipse.jetty.security.HashLoginService;
41+
import org.eclipse.jetty.security.SecurityHandler;
42+
import org.eclipse.jetty.security.ServerAuthException;
43+
import org.eclipse.jetty.security.UserAuthentication;
44+
import org.eclipse.jetty.security.UserStore;
45+
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
46+
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
47+
import org.eclipse.jetty.server.Authentication;
48+
import org.eclipse.jetty.server.Authentication.User;
49+
import org.eclipse.jetty.server.Handler;
50+
import org.eclipse.jetty.server.HttpConfiguration;
51+
import org.eclipse.jetty.server.HttpConnectionFactory;
52+
import org.eclipse.jetty.server.Server;
53+
import org.eclipse.jetty.server.ServerConnector;
54+
import org.eclipse.jetty.server.UserIdentity;
55+
import org.eclipse.jetty.server.handler.DefaultHandler;
56+
import org.eclipse.jetty.server.handler.HandlerCollection;
57+
import org.eclipse.jetty.servlet.ServletContextHandler;
58+
import org.eclipse.jetty.servlet.ServletHandler;
59+
import org.eclipse.jetty.util.security.Constraint;
60+
import org.eclipse.jetty.util.security.Credential;
61+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
62+
63+
class ProxyServer {
64+
65+
// Static deques — needed so Jetty can instantiate TrackingProxyServlet via reflection
66+
// (no-arg constructor, so can't inject instance references). Safe because only one
67+
// proxy runs at a time and stop() clears them.
68+
private static final ConcurrentLinkedDeque<String> requestsSeenByProxy = new ConcurrentLinkedDeque<>();
69+
private static final ConcurrentLinkedDeque<String> connectRequestsSeenByProxy = new ConcurrentLinkedDeque<>();
70+
71+
private final Server server;
72+
private final int port;
73+
74+
private ProxyServer(Server server, int port) {
75+
this.server = server;
76+
this.port = port;
77+
}
78+
79+
/** Starts an unauthenticated proxy. */
80+
static ProxyServer start() throws Exception {
81+
return start(false, null, null);
82+
}
83+
84+
/** Starts a proxy requiring Basic auth (Proxy-Authorization) on all requests and CONNECT tunnels. */
85+
static ProxyServer start(String user, String password) throws Exception {
86+
return start(true, user, password);
87+
}
88+
89+
private static ProxyServer start(boolean withProxyAuth, String user, String password) throws Exception {
90+
int port = NetworkUtils.getNextAvailablePort(InetAddress.getLocalHost());
91+
92+
QueuedThreadPool threadPool = new QueuedThreadPool();
93+
threadPool.setMaxThreads(500);
94+
95+
Server server = new Server(threadPool);
96+
97+
HttpConfiguration httpConfig = new HttpConfiguration();
98+
httpConfig.setSecureScheme("https");
99+
httpConfig.setSendServerVersion(true);
100+
httpConfig.setSendDateHeader(false);
101+
102+
TrackingConnectHandler connectHandler = new TrackingConnectHandler(withProxyAuth, user, password);
103+
connectHandler.setHandler(proxyHandler(withProxyAuth, user, password));
104+
105+
HandlerCollection handlers = new HandlerCollection();
106+
handlers.setHandlers(new Handler[] {connectHandler, new DefaultHandler()});
107+
server.setHandler(handlers);
108+
109+
ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
110+
http.setPort(port);
111+
server.addConnector(http);
112+
113+
server.start();
114+
return new ProxyServer(server, port);
115+
}
116+
117+
int getPort() {
118+
return port;
119+
}
120+
121+
Collection<String> getRequestsSeenByProxy() {
122+
return requestsSeenByProxy;
123+
}
124+
125+
Collection<String> getConnectRequestsSeenByProxy() {
126+
return connectRequestsSeenByProxy;
127+
}
128+
129+
void stop() throws Exception {
130+
server.stop();
131+
requestsSeenByProxy.clear();
132+
connectRequestsSeenByProxy.clear();
133+
}
134+
135+
private static ServletContextHandler proxyHandler(boolean withProxyAuth, String user, String password) {
136+
ServletContextHandler contextHandler = new ServletContextHandler();
137+
if (withProxyAuth) {
138+
contextHandler.setSecurityHandler(basicAuth(user, password, "Private!"));
139+
}
140+
contextHandler.setServletHandler(newServletHandler());
141+
return contextHandler;
142+
}
143+
144+
private static SecurityHandler basicAuth(String username, String password, String realm) {
145+
HashLoginService l = new HashLoginService(realm);
146+
147+
UserStore userStore = new UserStore();
148+
userStore.addUser(username, Credential.getCredential(password), new String[] {"user"});
149+
l.setUserStore(userStore);
150+
151+
Constraint constraint = new Constraint();
152+
constraint.setName(Constraint.__BASIC_AUTH);
153+
constraint.setRoles(new String[] {"user"});
154+
constraint.setAuthenticate(true);
155+
156+
ConstraintMapping cm = new ConstraintMapping();
157+
cm.setConstraint(constraint);
158+
cm.setPathSpec("/*");
159+
160+
ConstraintSecurityHandler csh = new ConstraintSecurityHandler();
161+
csh.setAuthenticator(new ProxyAuthenticator());
162+
csh.setRealmName("myrealm");
163+
csh.addConstraintMapping(cm);
164+
csh.setLoginService(l);
165+
166+
return csh;
167+
}
168+
169+
private static ServletHandler newServletHandler() {
170+
ServletHandler handler = new ServletHandler();
171+
handler.addServletWithMapping(TrackingProxyServlet.class, "/*");
172+
return handler;
173+
}
174+
175+
/**
176+
* ConnectHandler subclass that:
177+
* <ul>
178+
* <li>Optionally requires {@code Proxy-Authorization} on CONNECT requests</li>
179+
* <li>Records the host:port of every successfully-authenticated CONNECT</li>
180+
* </ul>
181+
* <p>
182+
* When authentication is required and credentials are missing, the handler sends a well-formed
183+
* {@code 407} response. This allows the JDK {@link java.net.Authenticator} to read the challenge,
184+
* supply credentials, and retry the CONNECT on a new connection.
185+
*/
186+
private static class TrackingConnectHandler extends ConnectHandler {
187+
188+
private final boolean requireAuth;
189+
private final String user;
190+
private final String password;
191+
192+
TrackingConnectHandler(boolean requireAuth, String user, String password) {
193+
this.requireAuth = requireAuth;
194+
this.user = user;
195+
this.password = password;
196+
}
197+
198+
@Override
199+
protected void handleConnect(org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request,
200+
HttpServletResponse response, String serverAddress) {
201+
if (requireAuth && !hasValidCredentials(request)) {
202+
response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED);
203+
response.setHeader("Proxy-Authenticate", "Basic realm=\"proxy\"");
204+
response.setContentLength(0);
205+
baseRequest.setHandled(true);
206+
return;
207+
}
208+
connectRequestsSeenByProxy.add(serverAddress);
209+
super.handleConnect(baseRequest, request, response, serverAddress);
210+
}
211+
212+
private boolean hasValidCredentials(HttpServletRequest request) {
213+
String credentials = request.getHeader("Proxy-Authorization");
214+
if (credentials != null && credentials.startsWith("Basic ")) {
215+
String decoded = new String(Base64.getDecoder().decode(credentials.substring(6)), StandardCharsets.ISO_8859_1);
216+
int colon = decoded.indexOf(':');
217+
if (colon > 0) {
218+
String reqUser = decoded.substring(0, colon);
219+
String reqPass = decoded.substring(colon + 1);
220+
return user.equals(reqUser) && password.equals(reqPass);
221+
}
222+
}
223+
return false;
224+
}
225+
}
226+
227+
// Must stay public static for Jetty servlet reflection
228+
public static class TrackingProxyServlet extends ProxyServlet {
229+
230+
@Override
231+
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
232+
requestsSeenByProxy.add(request.getRequestURI());
233+
super.service(request, response);
234+
}
235+
236+
@Override
237+
protected void sendProxyRequest(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest) {
238+
super.sendProxyRequest(clientRequest, proxyResponse, proxyRequest);
239+
}
240+
}
241+
242+
/**
243+
* Authenticator for HTTP forward proxy that reads {@code Proxy-Authorization} instead of the
244+
* standard {@code Authorization} header.
245+
* Inspired from Jetty's {@code BasicAuthenticator} but adapted for proxy auth.
246+
*/
247+
private static class ProxyAuthenticator extends LoginAuthenticator {
248+
249+
@Override
250+
public String getAuthMethod() {
251+
return Constraint.__BASIC_AUTH;
252+
}
253+
254+
@Override
255+
public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException {
256+
HttpServletRequest request = (HttpServletRequest) req;
257+
HttpServletResponse response = (HttpServletResponse) res;
258+
String credentials = request.getHeader(HttpHeader.PROXY_AUTHORIZATION.asString());
259+
260+
try {
261+
if (!mandatory) {
262+
return new DeferredAuthentication(this);
263+
}
264+
265+
if (credentials != null) {
266+
int space = credentials.indexOf(' ');
267+
if (space > 0) {
268+
String method = credentials.substring(0, space);
269+
if ("basic".equalsIgnoreCase(method)) {
270+
credentials = credentials.substring(space + 1);
271+
credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1);
272+
int i = credentials.indexOf(':');
273+
if (i > 0) {
274+
String username = credentials.substring(0, i);
275+
String password = credentials.substring(i + 1);
276+
UserIdentity user = login(username, password, request);
277+
if (user != null) {
278+
return new UserAuthentication(getAuthMethod(), user);
279+
}
280+
}
281+
}
282+
}
283+
}
284+
285+
if (DeferredAuthentication.isDeferred(response)) {
286+
return Authentication.UNAUTHENTICATED;
287+
}
288+
289+
response.setHeader(HttpHeader.PROXY_AUTHENTICATE.asString(), "basic realm=\"" + _loginService.getName() + '"');
290+
response.sendError(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED);
291+
return Authentication.SEND_CONTINUE;
292+
} catch (IOException e) {
293+
throw new ServerAuthException(e);
294+
}
295+
}
296+
297+
@Override
298+
public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException {
299+
return true;
300+
}
301+
}
302+
}

0 commit comments

Comments
 (0)