Skip to content

Commit a4ef822

Browse files
committed
Merge pull request #451 from jekh/proxy-auth-support
Add support for chained proxy authorization
2 parents 7c5a203 + 72a63c4 commit a4ef822

File tree

8 files changed

+196
-8
lines changed

8 files changed

+196
-8
lines changed

browsermob-core-littleproxy/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.google.common.collect.MapMaker;
66
import com.google.common.net.HostAndPort;
77
import io.netty.channel.ChannelHandlerContext;
8+
import io.netty.handler.codec.http.HttpHeaders;
9+
import io.netty.handler.codec.http.HttpObject;
810
import io.netty.handler.codec.http.HttpRequest;
911
import net.lightbody.bmp.client.ClientUtil;
1012
import net.lightbody.bmp.core.har.Har;
@@ -47,6 +49,7 @@
4749
import net.lightbody.bmp.proxy.http.RequestInterceptor;
4850
import net.lightbody.bmp.proxy.http.ResponseInterceptor;
4951
import net.lightbody.bmp.proxy.util.BrowserMobProxyUtil;
52+
import net.lightbody.bmp.util.BrowserMobHttpUtil;
5053
import org.apache.http.HttpRequestInterceptor;
5154
import org.apache.http.HttpResponseInterceptor;
5255
import org.java_bandwidthlimiter.StreamManager;
@@ -66,11 +69,9 @@
6669
import org.slf4j.Logger;
6770
import org.slf4j.LoggerFactory;
6871

69-
import javax.xml.bind.DatatypeConverter;
7072
import java.net.InetAddress;
7173
import java.net.InetSocketAddress;
7274
import java.net.UnknownHostException;
73-
import java.nio.charset.StandardCharsets;
7475
import java.util.ArrayList;
7576
import java.util.Arrays;
7677
import java.util.Collection;
@@ -322,6 +323,11 @@ public void setUpstreamMaxKB(long upstreamMaxKB) {
322323
.concurrencyLevel(1)
323324
.makeMap();
324325

326+
/**
327+
* Base64-encoded credentials to use to authenticate with the upstream proxy.
328+
*/
329+
private volatile String chainedProxyCredentials;
330+
325331
public BrowserMobProxyServer() {
326332
this(0);
327333
}
@@ -414,6 +420,16 @@ public void lookupChainedProxies(HttpRequest httpRequest, Queue<ChainedProxy> ch
414420
public InetSocketAddress getChainedProxyAddress() {
415421
return upstreamProxy;
416422
}
423+
424+
@Override
425+
public void filterRequest(HttpObject httpObject) {
426+
String chainedProxyAuth = chainedProxyCredentials;
427+
if (chainedProxyAuth != null) {
428+
if (httpObject instanceof HttpRequest) {
429+
HttpHeaders.addHeader((HttpRequest)httpObject, HttpHeaders.Names.PROXY_AUTHORIZATION, "Basic " + chainedProxyAuth);
430+
}
431+
}
432+
}
417433
});
418434
}
419435
}
@@ -857,15 +873,13 @@ public void autoAuthorization(String domain, String username, String password, A
857873
switch (authType) {
858874
case BASIC:
859875
// base64 encode the "username:password" string
860-
String credentialsToEncode = username + ':' + password;
861-
byte[] credentialsAsUsAscii = credentialsToEncode.getBytes(StandardCharsets.US_ASCII);
862-
String base64EncodedCredentials = DatatypeConverter.printBase64Binary(credentialsAsUsAscii);
876+
String base64EncodedCredentials = BrowserMobHttpUtil.base64EncodeBasicCredentials(username, password);
863877

864878
basicAuthCredentials.put(domain, base64EncodedCredentials);
865879
break;
866880

867881
default:
868-
throw new UnsupportedOperationException("AuthType " + authType + " is not supported");
882+
throw new UnsupportedOperationException("AuthType " + authType + " is not supported for HTTP Authorization");
869883
}
870884
}
871885

@@ -874,6 +888,18 @@ public void stopAutoAuthorization(String domain) {
874888
basicAuthCredentials.remove(domain);
875889
}
876890

891+
@Override
892+
public void chainedProxyAuthorization(String username, String password, AuthType authType) {
893+
switch (authType) {
894+
case BASIC:
895+
chainedProxyCredentials = BrowserMobHttpUtil.base64EncodeBasicCredentials(username, password);
896+
break;
897+
898+
default:
899+
throw new UnsupportedOperationException("AuthType " + authType + " is not supported for Proxy Authorization");
900+
}
901+
}
902+
877903
/**
878904
* @deprecated use {@link #setReadBandwidthLimit(long)}
879905
*/

browsermob-core-littleproxy/src/test/groovy/net/lightbody/bmp/proxy/AutoAuthTest.groovy

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ class AutoAuthTest extends MockServerTest {
4343
proxy.setTrustAllServers(true)
4444
proxy.start()
4545

46-
proxy.newHar()
47-
4846
ProxyServerTest.getNewHttpClient(proxy.port).withCloseable {
4947
String responseBody = IOUtils.toStringAndClose(it.execute(new HttpGet("http://localhost:${mockServerPort}/basicAuthHttp")).getEntity().getContent());
5048
assertEquals("Did not receive expected response from mock server", "success", responseBody);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package net.lightbody.bmp.proxy
2+
3+
import net.lightbody.bmp.BrowserMobProxy
4+
import net.lightbody.bmp.BrowserMobProxyServer
5+
import net.lightbody.bmp.proxy.auth.AuthType
6+
import net.lightbody.bmp.proxy.test.util.MockServerTest
7+
import net.lightbody.bmp.proxy.test.util.ProxyServerTest
8+
import net.lightbody.bmp.proxy.util.IOUtils
9+
import org.apache.http.client.methods.CloseableHttpResponse
10+
import org.apache.http.client.methods.HttpGet
11+
import org.junit.After
12+
import org.junit.Test
13+
import org.littleshoot.proxy.HttpProxyServer
14+
import org.littleshoot.proxy.ProxyAuthenticator
15+
import org.littleshoot.proxy.impl.DefaultHttpProxyServer
16+
import org.mockserver.matchers.Times
17+
18+
import static org.junit.Assert.assertEquals
19+
import static org.mockserver.model.HttpRequest.request
20+
import static org.mockserver.model.HttpResponse.response
21+
22+
class ChainedProxyAuthTest extends MockServerTest {
23+
BrowserMobProxy proxy
24+
25+
HttpProxyServer upstreamProxy
26+
27+
@After
28+
void tearDown() {
29+
if (proxy?.started) {
30+
proxy.abort()
31+
}
32+
33+
upstreamProxy?.abort()
34+
}
35+
36+
@Test
37+
void testAutoProxyAuthSuccessful() {
38+
String proxyUser = "proxyuser"
39+
String proxyPassword = "proxypassword"
40+
41+
upstreamProxy = DefaultHttpProxyServer.bootstrap()
42+
.withProxyAuthenticator(new ProxyAuthenticator() {
43+
@Override
44+
boolean authenticate(String user, String password) {
45+
return proxyUser.equals(user) && proxyPassword.equals(password)
46+
}
47+
48+
@Override
49+
String getRealm() {
50+
return "some-realm"
51+
}
52+
})
53+
.withPort(0)
54+
.start()
55+
56+
mockServer.when(request()
57+
.withMethod("GET")
58+
.withPath("/proxyauth"),
59+
Times.exactly(1))
60+
.respond(response()
61+
.withStatusCode(200)
62+
.withBody("success"))
63+
64+
proxy = new BrowserMobProxyServer();
65+
proxy.setChainedProxy(upstreamProxy.getListenAddress())
66+
proxy.chainedProxyAuthorization(proxyUser, proxyPassword, AuthType.BASIC)
67+
proxy.setTrustAllServers(true)
68+
proxy.start()
69+
70+
ProxyServerTest.getNewHttpClient(proxy.port).withCloseable {
71+
String responseBody = IOUtils.toStringAndClose(it.execute(new HttpGet("https://localhost:${mockServerPort}/proxyauth")).getEntity().getContent());
72+
assertEquals("Did not receive expected response from mock server", "success", responseBody);
73+
};
74+
}
75+
76+
@Test
77+
void testAutoProxyAuthFailure() {
78+
String proxyUser = "proxyuser"
79+
String proxyPassword = "proxypassword"
80+
81+
upstreamProxy = DefaultHttpProxyServer.bootstrap()
82+
.withProxyAuthenticator(new ProxyAuthenticator() {
83+
@Override
84+
boolean authenticate(String user, String password) {
85+
return proxyUser.equals(user) && proxyPassword.equals(password)
86+
}
87+
88+
@Override
89+
String getRealm() {
90+
return "some-realm"
91+
}
92+
})
93+
.withPort(0)
94+
.start()
95+
96+
mockServer.when(request()
97+
.withMethod("GET")
98+
.withPath("/proxyauth"),
99+
Times.exactly(1))
100+
.respond(response()
101+
.withStatusCode(500)
102+
.withBody("shouldn't happen"))
103+
104+
proxy = new BrowserMobProxyServer();
105+
proxy.setChainedProxy(upstreamProxy.getListenAddress())
106+
proxy.chainedProxyAuthorization(proxyUser, "wrongpassword", AuthType.BASIC)
107+
proxy.setTrustAllServers(true)
108+
proxy.start()
109+
110+
ProxyServerTest.getNewHttpClient(proxy.port).withCloseable {
111+
CloseableHttpResponse response = it.execute(new HttpGet("https://localhost:${mockServerPort}/proxyauth"))
112+
assertEquals("Expected to receive a Bad Gateway due to incorrect proxy authentication credentials", 502, response.getStatusLine().statusCode)
113+
};
114+
}
115+
}

browsermob-core/src/main/java/net/lightbody/bmp/BrowserMobProxy.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,16 @@ public interface BrowserMobProxy {
303303
*/
304304
void stopAutoAuthorization(String domain);
305305

306+
/**
307+
* Enables chained proxy authorization using the Proxy-Authorization header described in RFC 7235, section 4.4 (https://tools.ietf.org/html/rfc7235#section-4.4).
308+
* Currently, only {@link AuthType#BASIC} authentication is supported.
309+
*
310+
* @param username the username to use to authenticate with the chained proxy
311+
* @param password the password to use to authenticate with the chained proxy
312+
* @param authType the auth type to use (currently, must be BASIC)
313+
*/
314+
void chainedProxyAuthorization(String username, String password, AuthType authType);
315+
306316
/**
307317
* Adds a rewrite rule for the specified URL-matching regular expression. If there are any existing rewrite rules, the new rewrite
308318
* rule will be applied last, after all other rewrite rules are applied. The specified urlPattern will be replaced with the specified

browsermob-core/src/main/java/net/lightbody/bmp/proxy/ProxyServer.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,11 @@ public void stopAutoAuthorization(String domain) {
507507
LOG.warn("Legacy ProxyServer implementation does not support stopping auto authorization");
508508
}
509509

510+
@Override
511+
public void chainedProxyAuthorization(String username, String password, AuthType authType) {
512+
LOG.warn("Legacy ProxyServer implementation does not support chained proxy authorization");
513+
}
514+
510515
public void endPage() {
511516
if (currentPage == null) {
512517
return;

browsermob-core/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
1313

14+
import javax.xml.bind.DatatypeConverter;
1415
import java.io.ByteArrayInputStream;
1516
import java.io.ByteArrayOutputStream;
1617
import java.io.IOException;
@@ -277,4 +278,19 @@ public static String removeMatchingPort(String hostWithPort, int portNumber) {
277278
}
278279
}
279280

281+
/**
282+
* Base64-encodes the specified username and password for Basic Authorization for HTTP requests or upstream proxy
283+
* authorization. The format of Basic auth is "username:password" as a base64 string.
284+
*
285+
* @param username username to encode
286+
* @param password password to encode
287+
* @return a base-64 encoded string containing <code>username:password</code>
288+
*/
289+
public static String base64EncodeBasicCredentials(String username, String password) {
290+
String credentialsToEncode = username + ':' + password;
291+
// using UTF-8, which is the modern de facto standard, and which retains compatibility with US_ASCII for ASCII characters,
292+
// as required by RFC 7616, section 3: http://tools.ietf.org/html/rfc7617#section-3
293+
byte[] credentialsAsUtf8Bytes = credentialsToEncode.getBytes(StandardCharsets.UTF_8);
294+
return DatatypeConverter.printBase64Binary(credentialsAsUtf8Bytes);
295+
}
280296
}

browsermob-rest/src/main/java/net/lightbody/bmp/proxy/ProxyManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import com.google.inject.Provider;
99
import com.google.inject.Singleton;
1010
import com.google.inject.name.Named;
11+
import net.lightbody.bmp.BrowserMobProxy;
1112
import net.lightbody.bmp.BrowserMobProxyServer;
1213
import net.lightbody.bmp.exception.ProxyExistsException;
1314
import net.lightbody.bmp.exception.ProxyPortsExhaustedException;
15+
import net.lightbody.bmp.proxy.auth.AuthType;
1416
import org.slf4j.Logger;
1517
import org.slf4j.LoggerFactory;
1618

@@ -150,6 +152,13 @@ public LegacyProxyServer create(Map<String, String> options, Integer port, Strin
150152
}
151153

152154
if (options != null) {
155+
// this is a short-term work-around for Proxy Auth in the REST API until the upcoming REST API refactor
156+
String proxyUsername = options.remove("proxyUsername");
157+
String proxyPassword = options.remove("proxyPassword");
158+
if (proxyUsername != null && proxyPassword != null) {
159+
((BrowserMobProxy)proxy).chainedProxyAuthorization(proxyUsername, proxyPassword, AuthType.BASIC);
160+
}
161+
153162
LOG.debug("Apply options `{}` to new ProxyServer...", options);
154163
proxy.setOptions(options);
155164
}

browsermob-rest/src/main/java/net/lightbody/bmp/proxy/bricks/ProxyResource.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public Reply<?> newProxy(Request<String> request) {
7474
String systemProxyHost = System.getProperty("http.proxyHost");
7575
String systemProxyPort = System.getProperty("http.proxyPort");
7676
String httpProxy = request.param("httpProxy");
77+
String proxyUsername = request.param("proxyUsername");
78+
String proxyPassword = request.param("proxyPassword");
79+
7780
Hashtable<String, String> options = new Hashtable<String, String>();
7881

7982
// If the upstream proxy is specified via query params that should override any default system level proxy.
@@ -83,6 +86,12 @@ public Reply<?> newProxy(Request<String> request) {
8386
options.put("httpProxy", String.format("%s:%s", systemProxyHost, systemProxyPort));
8487
}
8588

89+
// this is a short-term work-around for Proxy Auth in the REST API until the upcoming REST API refactor
90+
if (proxyUsername != null && proxyPassword != null) {
91+
options.put("proxyUsername", proxyUsername);
92+
options.put("proxyPassword", proxyPassword);
93+
}
94+
8695
String paramBindAddr = request.param("bindAddress");
8796
Integer paramPort = request.param("port") == null ? null : Integer.parseInt(request.param("port"));
8897

0 commit comments

Comments
 (0)