package org.browsermob.proxy;
import org.apache.http.Header;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.conn.ConnectTimeoutException;
import org.browsermob.proxy.http.*;
import org.browsermob.proxy.jetty.http.*;
import org.browsermob.proxy.jetty.jetty.Server;
import org.browsermob.proxy.jetty.util.InetAddrPort;
import org.browsermob.proxy.jetty.util.URI;
import org.browsermob.proxy.selenium.SeleniumProxyHandler;
import org.browsermob.proxy.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class BrowserMobProxyHandler extends SeleniumProxyHandler {
private static final Log LOG = new Log();
private static final int HEADER_BUFFER_DEFAULT = 2;
private Server jettyServer;
private int headerBufferMultiplier = HEADER_BUFFER_DEFAULT;
private BrowserMobHttpClient httpClient;
protected final Set<SslRelay> sslRelays = new HashSet<SslRelay>();
public BrowserMobProxyHandler() {
super(true, "", "", false, false);
setShutdownLock(new Object());
// set the tunnel timeout to something larger than the default 30 seconds
// we're doing this because SSL connections taking longer than this timeout
// will result in a socket connection close that does NOT get handled by the
// normal socket connection closing reportError(). Further, it has been seen
// that Firefox will actually retry the connection, causing very strange
// behavior observed in case http://browsermob.assistly.com/agent/case/27843
//
// You can also reproduce it by simply finding some slow loading SSL site
// that takes greater than 30 seconds to response.
//
// Finally, it should be noted that we're setting this timeout to some value
// that we anticipate will be larger than any reasonable response time of a
// real world request. We don't set it to -1 because the underlying ProxyHandler
// will not use it if it's <= 0. We also don't set it to Long.MAX_VALUE because
// we don't yet know if this will cause a serious resource drain, so we're
// going to try something like 5 minutes for now.
setTunnelTimeoutMs(300000);
}
@Override
public void handleConnect(String pathInContext, String pathParams, HttpRequest request, HttpResponse response) throws HttpException, IOException {
URI uri = request.getURI();
String original = uri.toString();
String host = original;
String port = null;
int colon = original.indexOf(':');
if (colon != -1) {
host = original.substring(0, colon);
port = original.substring(colon + 1);
}
String altHost = httpClient.remappedHost(host);
if (altHost != null) {
if (port != null) {
uri.setURI(altHost + ":" + port);
} else {
uri.setURI(altHost);
}
}
super.handleConnect(pathInContext, pathParams, request, response);
}
@Override
protected void wireUpSslWithCyberVilliansCA(String host, SeleniumProxyHandler.SslRelay listener) {
List<String> originalHosts = httpClient.originalHosts(host);
if (originalHosts != null && !originalHosts.isEmpty()) {
if (originalHosts.size() == 1) {
host = originalHosts.get(0);
} else {
// Warning: this is NASTY, but people rarely even run across this and those that do are solved by this
// ok, this really isn't legal in real SSL land, but we'll make an exception and just pretend it's a wildcard
String first = originalHosts.get(0);
host = "*" + first.substring(first.indexOf('.'));
}
}
super.wireUpSslWithCyberVilliansCA(host, listener);
}
@Override
protected SslRelay getSslRelayOrCreateNew(URI uri, InetAddrPort addrPort, HttpServer server) throws Exception {
SslRelay relay = super.getSslRelayOrCreateNew(uri, addrPort, server);
relay.setNukeDirOrFile(null);
synchronized (sslRelays) {
sslRelays.add(relay);
}
if (!relay.isStarted()) {
server.addListener(relay);
startRelayWithPortTollerance(server, relay, 1);
}
return relay;
}
private void startRelayWithPortTollerance(HttpServer server, SslRelay relay, int tries) throws Exception {
if (tries >= 5) {
throw new BindException("Unable to bind to several ports, most recently " + relay.getPort() + ". Giving up");
}
try {
if (server.isStarted()) {
relay.start();
} else {
throw new RuntimeException("Can't start SslRelay: server is not started (perhaps it was just shut down?)");
}
} catch (BindException e) {
// doh - the port is being used up, let's pick a new port
LOG.info("Unable to bind to port %d, going to try port %d now", relay.getPort(), relay.getPort() + 1);
relay.setPort(relay.getPort() + 1);
startRelayWithPortTollerance(server, relay, tries + 1);
}
}
@Override
protected HttpTunnel newHttpTunnel(HttpRequest httpRequest, HttpResponse httpResponse, InetAddress inetAddress, int i, int i1) throws IOException {
// we're opening up a new tunnel, so let's make sure that the associated SslRelay (which may or may not be new) has the proper buffer settings
adjustListenerBuffers();
return super.newHttpTunnel(httpRequest, httpResponse, inetAddress, i, i1);
}
@SuppressWarnings({"unchecked"})
protected long proxyPlainTextRequest(final URL url, String pathInContext, String pathParams, HttpRequest request, final HttpResponse response) throws IOException {
try {
String urlStr = url.toString();
// We don't want selenium-related showing up in the detailed transaction logs
if (urlStr.contains("/selenium-server/")) {
return super.proxyPlainTextRequest(url, pathInContext, pathParams, request, response);
}
// we also don't URLs that Firefox always loads on startup showing up, or even wasting bandwidth.
// so for these we just nuke them right on the spot!
if (urlStr.startsWith("https://sb-ssl.google.com:443/safebrowsing")
|| urlStr.startsWith("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml")
|| urlStr.startsWith("http://fxfeeds.mozilla.com/firefox/headlines.xml")
|| urlStr.startsWith("http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml")
|| urlStr.startsWith("http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml")) {
// don't even xfer these!
request.setHandled(true);
return -1;
}
// this request must have come in just as we were shutting down, since there is no more associted http client
// so let's just handle it like we do any other request we don't know what to do with :)
if (httpClient == null) {
// don't even xfer these!
request.setHandled(true);
return -1;
// for debugging purposes, NOT to be used in product!
// httpClient = new BrowserMobHttpClient(Integer.MAX_VALUE);
// httpClient.setDecompress(false);
// httpClient.setFollowRedirects(false);
}
BrowserMobHttpRequest httpReq = null;
if ("GET".equals(request.getMethod())) {
httpReq = httpClient.newGet(urlStr, request);
} else if ("POST".equals(request.getMethod())) {
httpReq = httpClient.newPost(urlStr, request);
} else if ("PUT".equals(request.getMethod())) {
httpReq = httpClient.newPut(urlStr, request);
} else if ("DELETE".equals(request.getMethod())) {
httpReq = httpClient.newDelete(urlStr, request);
} else if ("OPTIONS".equals(request.getMethod())) {
httpReq = httpClient.newOptions(urlStr, request);
} else if ("HEAD".equals(request.getMethod())) {
httpReq = httpClient.newHead(urlStr, request);
} else {
LOG.warn("Unexpected request method %s, giving up", request.getMethod());
request.setHandled(true);
return -1;
}
// copy request headers
boolean isGet = "GET".equals(request.getMethod());
boolean hasContent = false;
Enumeration enm = request.getFieldNames();
long contentLength = 0;
while (enm.hasMoreElements()) {
String hdr = (String) enm.nextElement();
if (!isGet && HttpFields.__ContentType.equals(hdr)) {
hasContent = true;
}
if (!isGet && HttpFields.__ContentLength.equals(hdr)) {
contentLength = Long.parseLong(request.getField(hdr));
continue;
}
Enumeration vals = request.getFieldValues(hdr);
while (vals.hasMoreElements()) {
String val = (String) vals.nextElement();
if (val != null) {
if (!isGet && HttpFields.__ContentLength.equals(hdr) && Integer.parseInt(val) > 0) {
hasContent = true;
}
if (!_DontProxyHeaders.containsKey(hdr)) {
httpReq.addRequestHeader(hdr, val);
}
}
}
}
try {
// do input thang!
InputStream in = request.getInputStream();
if (hasContent) {
httpReq.setRequestInputStream(in, contentLength);
}
} catch (Exception e) {
LOG.fine(e.getMessage(), e);
}
// execute the request
httpReq.setOutputStream(response.getOutputStream());
httpReq.setRequestCallback(new RequestCallback() {
@Override
public void handleStatusLine(StatusLine statusLine) {
response.setStatus(statusLine.getStatusCode());
response.setReason(statusLine.getReasonPhrase());
}
@Override
public void handleHeaders(Header[] headers) {
for (Header header : headers) {
if (reportHeader(header)) {
response.addField(header.getName(), header.getValue());
}
}
}
@Override
public boolean reportHeader(Header header) {
// don't pass in things like Transfer-Encoding and other headers that are being masked by the underlying HttpClient impl
return !_DontProxyHeaders.containsKey(header.getName()) && !_ProxyAuthHeaders.containsKey(header.getName());
}
@Override
public void reportError(Exception e) {
BrowserMobProxyHandler.reportError(e, url, response);
}
});
BrowserMobHttpResponse httpRes = httpReq.execute();
// ALWAYS mark the request as handled if we actually handled it. Otherwise, Jetty will think non 2xx responses
// mean it wasn't actually handled, resulting in totally valid 304 Not Modified requests turning in to 404 responses
// from Jetty. NOT good :(
request.setHandled(true);
return httpRes.getEntry().getResponse().getBodySize();
} catch (BadURIException e) {
// this is a known error case (see MOB-93)
LOG.info(e.getMessage());
BrowserMobProxyHandler.reportError(e, url, response);
return -1;
} catch (Exception e) {
LOG.info("Exception while proxying " + url, e);
BrowserMobProxyHandler.reportError(e, url, response);
return -1;
}
}
private static void reportError(Exception e, URL url, HttpResponse response) {
FirefoxErrorContent error = FirefoxErrorContent.GENERIC;
if (e instanceof UnknownHostException) {
error = FirefoxErrorContent.DNS_NOT_FOUND;
} else if (e instanceof ConnectException) {
error = FirefoxErrorContent.CONN_FAILURE;
} else if (e instanceof ConnectTimeoutException) {
error = FirefoxErrorContent.NET_TIMEOUT;
} else if (e instanceof NoHttpResponseException) {
error = FirefoxErrorContent.NET_RESET;
} else if (e instanceof EOFException) {
error = FirefoxErrorContent.NET_INTERRUPT;
} else if (e instanceof IllegalArgumentException && e.getMessage().startsWith("Host name may not be null")){
error = FirefoxErrorContent.DNS_NOT_FOUND;
} else if (e instanceof BadURIException){
error = FirefoxErrorContent.MALFORMED_URI;
}
String shortDesc = String.format(error.getShortDesc(), url.getHost());
String text = String.format(FirefoxErrorConstants.ERROR_PAGE, error.getTitle(), shortDesc, error.getLongDesc());
try {
response.setStatus(HttpResponse.__502_Bad_Gateway);
response.setContentLength(text.length());
response.getOutputStream().write(text.getBytes());
} catch (IOException e1) {
LOG.warn("IOException while trying to report an HTTP error");
}
}
public void autoBasicAuthorization(String domain, String username, String password) {
httpClient.autoBasicAuthorization(domain, username, password);
}
public void rewriteUrl(String match, String replace) {
httpClient.rewriteUrl(match, replace);
}
public void remapHost(String source, String target) {
httpClient.remapHost(source, target);
}
public void setJettyServer(Server jettyServer) {
this.jettyServer = jettyServer;
}
public void adjustListenerBuffers(int headerBufferMultiplier) {
// limit to 10 so there can't be any out of control memory consumption by a rogue script
if (headerBufferMultiplier > 10) {
headerBufferMultiplier = 10;
}
this.headerBufferMultiplier = headerBufferMultiplier;
adjustListenerBuffers();
}
public void resetListenerBuffers() {
this.headerBufferMultiplier = HEADER_BUFFER_DEFAULT;
adjustListenerBuffers();
}
public void adjustListenerBuffers() {
// configure the listeners to have larger buffers. We do this because we've seen cases where the header is
// too large. Normally this would happen on "meaningless" JS includes for ad networks, but we eventually saw
// it in a way that caused a Selenium script not to work due to too many headers (see tom.schwenk@musictoday.com)
HttpListener[] listeners = jettyServer.getListeners();
for (HttpListener listener : listeners) {
if (listener instanceof SocketListener) {
SocketListener sl = (SocketListener) listener;
if (sl.getBufferReserve() != 512 * headerBufferMultiplier) {
sl.setBufferReserve(512 * headerBufferMultiplier);
}
if (sl.getBufferSize() != 8192 * headerBufferMultiplier) {
sl.setBufferSize(8192 * headerBufferMultiplier);
}
}
}
}
public void setHttpClient(BrowserMobHttpClient httpClient) {
this.httpClient = httpClient;
}
public void cleanup() {
synchronized (sslRelays) {
for (SslRelay relay : sslRelays) {
if (relay.getHttpServer() != null && relay.isStarted()) {
relay.getHttpServer().removeListener(relay);
}
}
sslRelays.clear();
}
}
}