Skip to content

Commit f67c8a2

Browse files
committed
Merge pull request #364 from jekh/add-auto-basic-auth
Add auto basic authentication to LittleProxy implementation
2 parents a360756 + f9634de commit f67c8a2

File tree

9 files changed

+256
-59
lines changed

9 files changed

+256
-59
lines changed

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

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import net.lightbody.bmp.core.har.HarPage;
1414
import net.lightbody.bmp.exception.NameResolutionException;
1515
import net.lightbody.bmp.filters.AddHeadersFilter;
16+
import net.lightbody.bmp.filters.AutoBasicAuthFilter;
1617
import net.lightbody.bmp.filters.BlacklistFilter;
1718
import net.lightbody.bmp.filters.BrowserMobHttpFilterChain;
1819
import net.lightbody.bmp.filters.HarCaptureFilter;
@@ -63,9 +64,11 @@
6364
import org.slf4j.Logger;
6465
import org.slf4j.LoggerFactory;
6566

67+
import javax.xml.bind.DatatypeConverter;
6668
import java.net.InetAddress;
6769
import java.net.InetSocketAddress;
6870
import java.net.UnknownHostException;
71+
import java.nio.charset.StandardCharsets;
6972
import java.util.ArrayList;
7073
import java.util.Arrays;
7174
import java.util.Collection;
@@ -304,6 +307,14 @@ public void setUpstreamMaxKB(long upstreamMaxKB) {
304307
*/
305308
private final StreamManagerLegacyAdapter streamManagerAdapter = new StreamManagerLegacyAdapter();
306309

310+
/**
311+
* A mapping of hostnames to base64-encoded Basic auth credentials that will be added to the Authorization header for
312+
* matching requests.
313+
*/
314+
private final ConcurrentMap<String, String> basicAuthCredentials = new MapMaker()
315+
.concurrencyLevel(1)
316+
.makeMap();
317+
307318
public BrowserMobProxyServer() {
308319
this(0);
309320
}
@@ -832,20 +843,24 @@ public void setLatency(long latency, TimeUnit timeUnit) {
832843

833844
@Override
834845
public void autoAuthorization(String domain, String username, String password, AuthType authType) {
835-
if (errorOnUnsupportedOperation) {
836-
throw new UnsupportedOperationException("No LittleProxy implementation for operation: " + new Throwable().getStackTrace()[0].getMethodName());
837-
} else {
838-
log.warn("No LittleProxy implementation for operation: " + new Throwable().getStackTrace()[0].getMethodName());
846+
switch (authType) {
847+
case BASIC:
848+
// base64 encode the "username:password" string
849+
String credentialsToEncode = username + ':' + password;
850+
byte[] credentialsAsUsAscii = credentialsToEncode.getBytes(StandardCharsets.US_ASCII);
851+
String base64EncodedCredentials = DatatypeConverter.printBase64Binary(credentialsAsUsAscii);
852+
853+
basicAuthCredentials.put(domain, base64EncodedCredentials);
854+
break;
855+
856+
default:
857+
throw new UnsupportedOperationException("AuthType " + authType + " is not supported");
839858
}
840859
}
841860

842861
@Override
843862
public void stopAutoAuthorization(String domain) {
844-
if (errorOnUnsupportedOperation) {
845-
throw new UnsupportedOperationException("No LittleProxy implementation for operation: " + new Throwable().getStackTrace()[0].getMethodName());
846-
} else {
847-
log.warn("No LittleProxy implementation for operation: " + new Throwable().getStackTrace()[0].getMethodName());
848-
}
863+
basicAuthCredentials.remove(domain);
849864
}
850865

851866
/**
@@ -931,7 +946,7 @@ public void setRequestTimeout(int requestTimeoutMs) {
931946
* @deprecated use {@link #autoAuthorization(String, String, String, net.lightbody.bmp.proxy.auth.AuthType)}
932947
*/
933948
@Deprecated
934-
// @Override
949+
@Override
935950
public void autoBasicAuthorization(String domain, String username, String password) {
936951
autoAuthorization(domain, username, password, AuthType.BASIC);
937952
}
@@ -1446,6 +1461,13 @@ public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerCont
14461461
}
14471462
});
14481463

1464+
addHttpFilterFactory(new HttpFiltersSourceAdapter() {
1465+
@Override
1466+
public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
1467+
return new AutoBasicAuthFilter(originalRequest, ctx, basicAuthCredentials);
1468+
}
1469+
});
1470+
14491471
addHttpFilterFactory(new HttpFiltersSourceAdapter() {
14501472
@Override
14511473
public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package net.lightbody.bmp.filters;
2+
3+
import io.netty.channel.ChannelHandlerContext;
4+
import io.netty.handler.codec.http.HttpHeaders;
5+
import io.netty.handler.codec.http.HttpObject;
6+
import io.netty.handler.codec.http.HttpRequest;
7+
import io.netty.handler.codec.http.HttpResponse;
8+
import org.littleshoot.proxy.impl.ProxyUtils;
9+
10+
import java.util.Map;
11+
12+
/**
13+
* A filter that adds Basic authentication information to non-CONNECT requests. Takes a map of domain names to base64-encoded
14+
* Basic auth credentials as a constructor parameter. If a key in the map matches the hostname of a filtered request, an Authorization
15+
* header will be added to the request.
16+
* <p/>
17+
* The Authorization header itself is specified in RFC 7235, section 4.2: https://tools.ietf.org/html/rfc7235#section-4.2
18+
* The Basic authentication scheme is specified in RFC 2617, section 2: https://tools.ietf.org/html/rfc2617#section-2
19+
*/
20+
public class AutoBasicAuthFilter extends HttpsAwareFiltersAdapter {
21+
private final Map<String, String> credentialsByHostname;
22+
23+
public AutoBasicAuthFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Map<String, String> credentialsByHostname) {
24+
super(originalRequest, ctx);
25+
26+
this.credentialsByHostname = credentialsByHostname;
27+
}
28+
29+
@Override
30+
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
31+
if (credentialsByHostname.isEmpty()) {
32+
return null;
33+
}
34+
35+
if (httpObject instanceof HttpRequest) {
36+
HttpRequest httpRequest = (HttpRequest) httpObject;
37+
38+
// providing authorization during a CONNECT is generally not useful
39+
if (ProxyUtils.isCONNECT(httpRequest)) {
40+
return null;
41+
}
42+
43+
String hostname = getHost(httpRequest);
44+
45+
// if there is an entry in the credentials map matching this hostname, add the credentials to the request
46+
String base64CredentialsForHostname = credentialsByHostname.get(hostname);
47+
if (base64CredentialsForHostname != null) {
48+
httpRequest.headers().add(HttpHeaders.Names.AUTHORIZATION, "Basic " + base64CredentialsForHostname);
49+
}
50+
}
51+
52+
return null;
53+
}
54+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ protected void captureConnectTiming() {
620620
* @param httpRequest HTTP request to take the hostname from
621621
*/
622622
protected void populateAddressFromCache(HttpRequest httpRequest) {
623-
String serverHost = getHostAndPort(httpRequest);
623+
String serverHost = getHost(httpRequest);
624624

625625
if (serverHost != null && !serverHost.isEmpty()) {
626626
String resolvedAddress = ResolvedHostnameCacheFilter.getPreviouslyResolvedAddressForHost(serverHost);

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

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,37 +41,6 @@ public boolean isHttps() {
4141
}
4242
}
4343

44-
/**
45-
* Returns the host and port of this HTTPS request, including any modifications by other filters.
46-
*
47-
* @return host and port of this HTTPS request
48-
* @throws IllegalStateException if this is not an HTTPS request
49-
*/
50-
public String getHttpsRequestHostAndPort() throws IllegalStateException {
51-
if (!isHttps()) {
52-
throw new IllegalStateException("Request is not HTTPS. Cannot get host and port on non-HTTPS request using this method.");
53-
}
54-
55-
Attribute<String> hostnameAttr = ctx.attr(AttributeKey.<String>valueOf(HOST_ATTRIBUTE_NAME));
56-
return hostnameAttr.get();
57-
}
58-
59-
/**
60-
* Returns the original host and port of this HTTPS request, as sent by the client. Does not reflect any modifications
61-
* by other filters.
62-
*
63-
* @return host and port of this HTTPS request
64-
* @throws IllegalStateException if this is not an HTTPS request
65-
*/
66-
public String getHttpsOriginalRequestHostAndPort() throws IllegalStateException {
67-
if (!isHttps()) {
68-
throw new IllegalStateException("Request is not HTTPS. Cannot get original host and port on non-HTTPS request using this method.");
69-
}
70-
71-
Attribute<String> hostnameAttr = ctx.attr(AttributeKey.<String>valueOf(ORIGINAL_HOST_ATTRIBUTE_NAME));
72-
return hostnameAttr.get();
73-
}
74-
7544
/**
7645
* Returns the full, absolute URL of the specified request for both HTTP and HTTPS URLs. The request may reflect
7746
* modifications from this or other filters. This filter instance must be currently handling the specified request;
@@ -91,19 +60,14 @@ public String getFullUrl(HttpRequest modifiedRequest) {
9160
// To get the full URL, we need to retrieve the Scheme, Host + Port, Path, and Query Params from the request.
9261
// Scheme: the scheme (HTTP/HTTPS) may or may not be part of the request, and so must be generated based on the
9362
// type of connection.
94-
// Host and Port: for HTTP requests, the host and port can be read from the request itself using the URI and/or
95-
// Host header. for HTTPS requests, the host and port are not available in the request. by using the
96-
// getHttpsRequestHostAndPort() helper method in HttpsAwareFiltersAdapter, we can capture the host and port for
97-
// HTTPS requests.
98-
// Path + Query Params + Fragment: these elements are contained in the HTTP request
63+
// Host and Port: available for HTTP and HTTPS requests using the getHostAndPort() helper method.
64+
// Path + Query Params + Fragment: these elements are contained in the HTTP request for both HTTP and HTTPS
65+
String hostAndPort = getHostAndPort(modifiedRequest);
66+
String path = BrowserMobHttpUtil.getPathFromRequest(modifiedRequest);
9967
String url;
10068
if (isHttps()) {
101-
String hostAndPort = getHttpsRequestHostAndPort();
102-
String path = BrowserMobHttpUtil.getPathFromRequest(modifiedRequest);
10369
url = "https://" + hostAndPort + path;
10470
} else {
105-
String hostAndPort = BrowserMobHttpUtil.getHostAndPortFromRequest(modifiedRequest);
106-
String path = BrowserMobHttpUtil.getPathFromRequest(modifiedRequest);
10771
url = "http://" + hostAndPort + path;
10872
}
10973
return url;
@@ -120,14 +84,14 @@ public String getOriginalUrl() {
12084
}
12185

12286
/**
123-
* Returns the host and port of the specified request for both HTTP and HTTPS requests. The request may reflect
87+
* Returns the hostname (but not the port) the specified request for both HTTP and HTTPS requests. The request may reflect
12488
* modifications from this or other filters. This filter instance must be currently handling the specified request;
12589
* otherwise the results are undefined.
12690
*
12791
* @param modifiedRequest a possibly-modified version of the request currently being processed
128-
* @return host and port of the specified request
92+
* @return hostname of the specified request, without the port
12993
*/
130-
public String getHostAndPort(HttpRequest modifiedRequest) {
94+
public String getHost(HttpRequest modifiedRequest) {
13195
String serverHost;
13296
if (isHttps()) {
13397
HostAndPort hostAndPort = HostAndPort.fromString(getHttpsRequestHostAndPort());
@@ -137,4 +101,55 @@ public String getHostAndPort(HttpRequest modifiedRequest) {
137101
}
138102
return serverHost;
139103
}
104+
105+
/**
106+
* Returns the host and port of the specified request for both HTTP and HTTPS requests. The request may reflect
107+
* modifications from this or other filters. This filter instance must be currently handling the specified request;
108+
* otherwise the results are undefined.
109+
*
110+
* @param modifiedRequest a possibly-modified version of the request currently being processed
111+
* @return host and port of the specified request
112+
*/
113+
public String getHostAndPort(HttpRequest modifiedRequest) {
114+
// For HTTP requests, the host and port can be read from the request itself using the URI and/or
115+
// Host header. for HTTPS requests, the host and port are not available in the request. by using the
116+
// getHttpsRequestHostAndPort() helper method, we can retrieve the host and port for HTTPS requests.
117+
if (isHttps()) {
118+
return getHttpsRequestHostAndPort();
119+
} else {
120+
return BrowserMobHttpUtil.getHostAndPortFromRequest(modifiedRequest);
121+
}
122+
}
123+
124+
/**
125+
* Returns the host and port of this HTTPS request, including any modifications by other filters.
126+
*
127+
* @return host and port of this HTTPS request
128+
* @throws IllegalStateException if this is not an HTTPS request
129+
*/
130+
private String getHttpsRequestHostAndPort() throws IllegalStateException {
131+
if (!isHttps()) {
132+
throw new IllegalStateException("Request is not HTTPS. Cannot get host and port on non-HTTPS request using this method.");
133+
}
134+
135+
Attribute<String> hostnameAttr = ctx.attr(AttributeKey.<String>valueOf(HOST_ATTRIBUTE_NAME));
136+
return hostnameAttr.get();
137+
}
138+
139+
/**
140+
* Returns the original host and port of this HTTPS request, as sent by the client. Does not reflect any modifications
141+
* by other filters.
142+
* TODO: evaluate this (unused) method and its capture mechanism in HttpsOriginalHostCaptureFilter; remove if not useful.
143+
*
144+
* @return host and port of this HTTPS request
145+
* @throws IllegalStateException if this is not an HTTPS request
146+
*/
147+
private String getHttpsOriginalRequestHostAndPort() throws IllegalStateException {
148+
if (!isHttps()) {
149+
throw new IllegalStateException("Request is not HTTPS. Cannot get original host and port on non-HTTPS request using this method.");
150+
}
151+
152+
Attribute<String> hostnameAttr = ctx.attr(AttributeKey.<String>valueOf(ORIGINAL_HOST_ATTRIBUTE_NAME));
153+
return hostnameAttr.get();
154+
}
140155
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/**
1414
* Captures the host for HTTPS requests and stores the value in the ChannelHandlerContext for use by {@link HttpsAwareFiltersAdapter}
1515
* filters. This filter reads the host from the HttpRequest during the HTTP CONNECT call, and therefore MUST be invoked
16-
* after any other filters which modify the host.
16+
* <b>after</b> any other filters which modify the host.
1717
* Note: If the request uses the default HTTPS port (443), it will be removed from the hostname captured by this filter.
1818
*/
1919
public class HttpsHostCaptureFilter extends HttpFiltersAdapter {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99

1010
/**
1111
* Captures the original host for HTTPS requests and stores the value in the ChannelHandlerContext for use by {@link HttpsAwareFiltersAdapter}
12-
* filters. This filter sets the isHttps attribute on the ChannelHandlerContext during the HTTP CONNECT and therefore MUST be invoked before
12+
* filters. This filter sets the isHttps attribute on the ChannelHandlerContext during the HTTP CONNECT and therefore MUST be invoked <b>before</b>
1313
* any other filters calling any of the methods in {@link HttpsAwareFiltersAdapter}.
14+
* This filter extends {@link HttpsHostCaptureFilter} and so also sets the host attribute on the channel for use by filters
15+
* that modify the original host during the CONNECT. If the hostname is modified by filters, it will be overwritten when the {@link HttpsHostCaptureFilter}
16+
* is processed later in the filter chain.
1417
*/
15-
public class HttpsOriginalHostCaptureFilter extends HttpFiltersAdapter {
18+
public class HttpsOriginalHostCaptureFilter extends HttpsHostCaptureFilter {
1619
public HttpsOriginalHostCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx) {
1720
super(originalRequest, ctx);
1821

0 commit comments

Comments
 (0)