All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.lightbody.bmp.BrowserMobProxyServer Maven / Gradle / Ivy

package net.lightbody.bmp;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapMaker;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import net.lightbody.bmp.client.ClientUtil;
import net.lightbody.bmp.core.har.Har;
import net.lightbody.bmp.core.har.HarLog;
import net.lightbody.bmp.core.har.HarNameVersion;
import net.lightbody.bmp.core.har.HarPage;
import net.lightbody.bmp.filters.*;
import net.lightbody.bmp.mitm.KeyStoreFileCertificateSource;
import net.lightbody.bmp.mitm.TrustSource;
import net.lightbody.bmp.mitm.keys.ECKeyGenerator;
import net.lightbody.bmp.mitm.keys.RSAKeyGenerator;
import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager;
import net.lightbody.bmp.proxy.*;
import net.lightbody.bmp.proxy.auth.AuthType;
import net.lightbody.bmp.proxy.dns.AdvancedHostResolver;
import net.lightbody.bmp.proxy.dns.DelegatingHostResolver;
import net.lightbody.bmp.util.BrowserMobHttpUtil;
import net.lightbody.bmp.util.BrowserMobProxyUtil;
import org.littleshoot.proxy.*;
import org.littleshoot.proxy.impl.ClientDetails;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import org.littleshoot.proxy.impl.ProxyUtils;
import org.littleshoot.proxy.impl.ThreadPoolConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

/**
 * A LittleProxy-based implementation of {@link net.lightbody.bmp.BrowserMobProxy}.
 */
public class BrowserMobProxyServer implements BrowserMobProxy {
	private static final Logger log = LoggerFactory.getLogger(BrowserMobProxyServer.class);

	private static final Object LOCK = new Object();

	public static final String DEFAULT_PAGE_REF = "Default";
	public static final String DEFAULT_PAGE_TITLE = "Default";

	private static final HarNameVersion HAR_CREATOR_VERSION = new HarNameVersion("BrowserMob Proxy", BrowserMobProxyUtil.getVersionString());

	/* Default MITM resources */
	private static final String RSA_KEYSTORE_RESOURCE = "/src/main/resources/sslSupport/ca-keystore-rsa.p12";
	private static final String EC_KEYSTORE_RESOURCE = "/src/main/resources/sslSupport/ca-keystore-ec.p12";
	private static final String KEYSTORE_TYPE = "PKCS12";
	private static final String KEYSTORE_PRIVATE_KEY_ALIAS = "key";
	private static final String KEYSTORE_PASSWORD = "password";

	/**
	 * The default pseudonym to use when adding the Via header to proxied requests.
	 */
	public static final String VIA_HEADER_ALIAS = "browsermobproxy";

	/**
	 * True only after the proxy has been successfully started.
	 */
	private final AtomicBoolean started = new AtomicBoolean(false);

	/**
	 * True only after the proxy has been successfully started, then successfully stopped or aborted.
	 */
	private final AtomicBoolean stopped = new AtomicBoolean(false);

	/**
	 * Tracks the current page count, for use when auto-generating HAR page names.
	 */
	private final AtomicInteger harPageCount = new AtomicInteger(0);

	/**
	 * When true, MITM will be disabled. The proxy will no longer intercept HTTPS requests, but they will still be proxied.
	 */
	private volatile boolean mitmDisabled = false;

	/**
	 * The MITM manager that will be used for HTTPS requests.
	 */
	private volatile MitmManager mitmManager;

	/**
	 * The list of filterFactories that will generate the filters that implement browsermob-proxy behavior.
	 */
	private final List filterFactories = new CopyOnWriteArrayList<>();

	/**
	 * List of rejected URL patterns
	 */
	private volatile Collection blacklistEntries = new CopyOnWriteArrayList<>();

	/**
	 * List of URLs to rewrite
	 */
	private volatile CopyOnWriteArrayList rewriteRules = new CopyOnWriteArrayList<>();

	/**
	 * The LittleProxy instance that performs all proxy operations.
	 */
	private volatile HttpProxyServer proxyServer;

	/**
	 * No capture types are enabled by default.
	 */
	private volatile EnumSet harCaptureTypes = EnumSet.noneOf(CaptureType.class);

	/**
	 * The current HAR being captured.
	 */
	private volatile Har har;
	/**
	 * The current HarPage to which new requests will be associated.
	 */
	private volatile HarPage currentHarPage;
	/**
	 * Maximum bandwidth to consume when reading responses from servers.
	 */
	private volatile long readBandwidthLimitBps;
	/**
	 * Maximum bandwidth to consume when writing requests to servers.
	 */
	private volatile long writeBandwidthLimitBps;
	/**
	 * List of accepted URL patterns. Unlisted URL patterns will be rejected with the response code contained in the Whitelist.
	 */
	private final AtomicReference whitelist = new AtomicReference<>(Whitelist.WHITELIST_DISABLED);

	/**
	 * Additional headers that will be sent with every request. The map is declared as a ConcurrentMap to indicate that writes may be performed
	 * by other threads concurrently (e.g. due to an incoming REST call), but the concurrencyLevel is set to 1 because modifications to the
	 * additionalHeaders are rare, and in most cases happen only once, at start-up.
	 */
	private volatile ConcurrentMap additionalHeaders = new MapMaker().concurrencyLevel(1).makeMap();

	/**
	 * The amount of time to wait while connecting to a server.
	 */
	private volatile int connectTimeoutMs;

	/**
	 * The amount of time a connection to a server can remain idle while receiving data from the server.
	 */
	private volatile int idleConnectionTimeoutSec;

	/**
	 * The amount of time to wait before forwarding the response to the client.
	 */
	private volatile int latencyMs;

	/**
	 * Set to true once the HAR capture filter has been added to the filter chain.
	 */
	private final AtomicBoolean harCaptureFilterEnabled = new AtomicBoolean(false);

	/**
	 * Set to true when LittleProxy has been bootstrapped with the default chained proxy. This allows modifying the chained proxy
	 * after the proxy has been started.
	 */
	private final AtomicBoolean bootstrappedWithDefaultChainedProxy = new AtomicBoolean(false);

	/**
	 * The address of an upstream chained proxy to route traffic through.
	 */
	private volatile InetSocketAddress upstreamProxyAddress;

	/**
	 * The chained proxy manager that manages upstream proxies.
	 */
	private volatile ChainedProxyManager chainedProxyManager;

	/**
	 * The address of the network interface from which the proxy will initiate connections.
	 */
	private volatile InetAddress serverBindAddress;

	/**
	 * The TrustSource that will be used to validate servers' certificates. If null, will not validate server certificates.
	 */
	private volatile TrustSource trustSource = TrustSource.defaultTrustSource();

	/**
	 * When true, use Elliptic Curve keys and certificates when impersonating upstream servers.
	 */
	private volatile boolean useEcc = false;

	/**
	 * Resolver to use when resolving hostnames to IP addresses. This is a bridge between {@link org.littleshoot.proxy.HostResolver} and
	 * {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver}. It allows the resolvers to be changed on-the-fly without re-bootstrapping the
	 * littleproxy server. The default resolver (native JDK resolver) can be changed using {@link #setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)} and
	 * supplying one of the pre-defined resolvers in {@link ClientUtil}, such as {@link ClientUtil#createDnsJavaWithNativeFallbackResolver()}
	 * or {@link ClientUtil#createDnsJavaResolver()}. You can also build your own resolver, or use {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver}
	 * to chain together multiple DNS resolvers.
	 */
	private final DelegatingHostResolver delegatingResolver = new DelegatingHostResolver(ClientUtil.createNativeCacheManipulatingResolver());

	private final ActivityMonitor activityMonitor = new ActivityMonitor();

	/**
	 * The acceptor and worker thread configuration for the Netty thread pools.
	 */
	private volatile ThreadPoolConfiguration threadPoolConfiguration;

	/**
	 * A mapping of hostnames to base64-encoded Basic auth credentials that will be added to the Authorization header for
	 * matching requests.
	 */
	private final ConcurrentMap basicAuthCredentials = new MapMaker()
			.concurrencyLevel(1)
			.makeMap();

	/**
	 * Base64-encoded credentials to use to authenticate with the upstream proxy.
	 */
	private volatile String chainedProxyCredentials;

	public BrowserMobProxyServer() {
	}

	@Override
	public void start(int port, InetAddress clientBindAddress, InetAddress serverBindAddress) {
		boolean notStarted = started.compareAndSet(false, true);
		if (!notStarted) {
			throw new IllegalStateException("Proxy server is already started. Not restarting.");
		}

		InetSocketAddress clientBindSocket;
		if (clientBindAddress == null) {
			// if no client bind address was specified, bind to the wildcard address
			clientBindSocket = new InetSocketAddress(port);
		} else {
			clientBindSocket = new InetSocketAddress(clientBindAddress, port);
		}

		this.serverBindAddress = serverBindAddress;

		// initialize all the default BrowserMob filter factories that provide core BMP functionality
		addBrowserMobFilters();

		HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap()
				.withFiltersSource(new HttpFiltersSource() {
					@Override
					public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext channelHandlerContext) {
						return new BrowserMobHttpFilterChain(BrowserMobProxyServer.this, originalRequest, channelHandlerContext);
					}

					@Override
					public int getMaximumRequestBufferSizeInBytes() {
						return getMaximumRequestBufferSize();
					}

					@Override
					public int getMaximumResponseBufferSizeInBytes() {
						return getMaximumResponseBufferSize();
					}
				})
				.withServerResolver(delegatingResolver)
				.withAddress(clientBindSocket)
				.withConnectTimeout(connectTimeoutMs)
				.withIdleConnectionTimeout(idleConnectionTimeoutSec)
				.withProxyAlias(VIA_HEADER_ALIAS);

		if (serverBindAddress != null) {
			bootstrap.withNetworkInterface(new InetSocketAddress(serverBindAddress, 0));
		}


		if (!mitmDisabled) {
			if (mitmManager == null) {
				mitmManager = ImpersonatingMitmManager.builder()
						.rootCertificateSource(new KeyStoreFileCertificateSource(
								KEYSTORE_TYPE,
								useEcc ? EC_KEYSTORE_RESOURCE : RSA_KEYSTORE_RESOURCE,
								KEYSTORE_PRIVATE_KEY_ALIAS,
								KEYSTORE_PASSWORD))
						.serverKeyGenerator(useEcc ? new ECKeyGenerator() : new RSAKeyGenerator())
						.trustSource(trustSource)
						.build();
			}

			bootstrap.withManInTheMiddle(mitmManager);
		}

		if (readBandwidthLimitBps > 0 || writeBandwidthLimitBps > 0) {
			bootstrap.withThrottling(readBandwidthLimitBps, writeBandwidthLimitBps);
		}

		if (chainedProxyManager != null) {
			bootstrap.withChainProxyManager(chainedProxyManager);
		} else if (upstreamProxyAddress != null) {
			// indicate that the proxy was bootstrapped with the default chained proxy manager, which allows changing the
			// chained proxy after the proxy is started.
			bootstrappedWithDefaultChainedProxy.set(true);

			bootstrap.withChainProxyManager(new ChainedProxyManager() {
				@Override
				public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies, ClientDetails clientDetails) {
					final InetSocketAddress upstreamProxy = upstreamProxyAddress;
					if (upstreamProxy != null) {
						chainedProxies.add(new ChainedProxyAdapter() {
							@Override
							public InetSocketAddress getChainedProxyAddress() {
								return upstreamProxy;
							}

							@Override
							public void filterRequest(HttpObject httpObject) {
								String chainedProxyAuth = chainedProxyCredentials;
								if (chainedProxyAuth != null) {
									if(ProxyUtils.isCONNECT(httpObject) || !((HttpRequest) httpObject).uri().startsWith("/")) {
										HttpHeaders.addHeader((HttpRequest) httpObject, HttpHeaderNames.PROXY_AUTHORIZATION, "Basic " + chainedProxyAuth);
									}
								}
							}
						});
					}
				}
			});
		}

		if (threadPoolConfiguration != null) {
			bootstrap.withThreadPoolConfiguration(threadPoolConfiguration);
		}

		proxyServer = bootstrap.start();

		addHarCaptureFilter();
	}

	@Override
	public boolean isStarted() {
		return started.get();
	}

	@Override
	public void start(int port) {
		this.start(port, null, null);
	}

	@Override
	public void start(int port, InetAddress bindAddress) {
		this.start(port, bindAddress, null);

	}

	@Override
	public void start() {
		this.start(0);
	}

	@Override
	public void stop() {
		stop(true);
	}

	@Override
	public void abort() {
		stop(false);
	}

	protected void stop(boolean graceful) {
		if (isStarted()) {
			if (stopped.compareAndSet(false, true)) {
				if (proxyServer != null) {
					if (graceful) {
						proxyServer.stop();
					} else {
						proxyServer.abort();
					}
				} else {
					log.warn("Attempted to stop proxy server, but proxy was never successfully started.");
				}
			} else {
				throw new IllegalStateException("Proxy server is already stopped. Cannot re-stop.");
			}
		} else {
			throw new IllegalStateException("Proxy server has not been started");
		}
	}

	@Override
	public InetAddress getClientBindAddress() {
		if (started.get()) {
			return proxyServer.getListenAddress().getAddress();
		} else {
			return null;
		}
	}

	@Override
	public int getPort() {
		if (started.get()) {
			return proxyServer.getListenAddress().getPort();
		} else {
			return 0;
		}
	}

	@Override
	public InetAddress getServerBindAddress() {
		return serverBindAddress;
	}

	@Override
	public Har getHar() {
		return har;
	}

	@Override
	public Har newHar() {
		return newHar(null);
	}

	@Override
	public Har newHar(String initialPageRef) {
		return newHar(initialPageRef, null);
	}

	@Override
	public Har newHar(String initialPageRef, String initialPageTitle) {
		return newHar(initialPageRef, initialPageTitle, true);
	}

	private Har newHar(String initialPageRef, String initialPageTitle, boolean createPage) {
		Har oldHar = getHar();

		addHarCaptureFilter();

		harPageCount.set(0);

		this.har = new Har(new HarLog(HAR_CREATOR_VERSION));

		if (createPage) {
			newPage(initialPageRef, initialPageTitle);
		}

		return oldHar;
	}

	@Override
	public void setHarCaptureTypes(Set harCaptureSettings) {
		if (harCaptureSettings == null || harCaptureSettings.isEmpty()) {
			harCaptureTypes = EnumSet.noneOf(CaptureType.class);
		} else {
			harCaptureTypes = EnumSet.copyOf(harCaptureSettings);
		}
	}

	@Override
	public void setHarCaptureTypes(CaptureType... captureTypes) {
		if (captureTypes == null) {
			setHarCaptureTypes(EnumSet.noneOf(CaptureType.class));
		} else {
			setHarCaptureTypes(EnumSet.copyOf(Arrays.asList(captureTypes)));
		}
	}

	@Override
	public EnumSet getHarCaptureTypes() {
		return EnumSet.copyOf(harCaptureTypes);
	}

	@Override
	public void enableHarCaptureTypes(Set captureTypes) {
		harCaptureTypes.addAll(captureTypes);
	}

	@Override
	public void enableHarCaptureTypes(CaptureType... captureTypes) {
		if (captureTypes == null) {
			enableHarCaptureTypes(EnumSet.noneOf(CaptureType.class));
		} else {
			enableHarCaptureTypes(EnumSet.copyOf(Arrays.asList(captureTypes)));
		}
	}

	@Override
	public void disableHarCaptureTypes(Set captureTypes) {
		harCaptureTypes.removeAll(captureTypes);

	}

	@Override
	public void disableHarCaptureTypes(CaptureType... captureTypes) {
		if (captureTypes == null) {
			disableHarCaptureTypes(EnumSet.noneOf(CaptureType.class));
		} else {
			disableHarCaptureTypes(EnumSet.copyOf(Arrays.asList(captureTypes)));
		}
	}

	@Override
	public Har newPage() {
		return newPage(null);
	}

	@Override
	public Har newPage(String pageRef) {
		return newPage(pageRef, null);
	}

	@Override
	public Har newPage(String pageRef, String pageTitle) {
		har = getOrCreateHar(pageRef, pageTitle, false);

		Har endOfPageHar = null;

		if (currentHarPage != null) {
			String currentPageRef = currentHarPage.getId();

			// end the previous page, so that page-wide timings are populated
			endPage();

			// the interface requires newPage() to return the Har as it was immediately after the previous page was ended.
			endOfPageHar = BrowserMobProxyUtil.copyHarThroughPageRef(har, currentPageRef);
		}

		if (pageRef == null) {
			pageRef = "Page " + harPageCount.getAndIncrement();
		}

		if (pageTitle == null) {
			pageTitle = pageRef;
		}

		HarPage newPage = new HarPage(pageRef, pageTitle);
		har.getLog().addPage(newPage);

		currentHarPage = newPage;

		return endOfPageHar;
	}

	@Override
	public Har endHar() {
		Har oldHar = getHar();

		// end the page and populate timings
		endPage();

		this.har = null;

		return oldHar;
	}

	@Override
	public void setReadBandwidthLimit(long bytesPerSecond) {
		this.readBandwidthLimitBps = bytesPerSecond;

		if (isStarted()) {
			proxyServer.setThrottle(this.readBandwidthLimitBps, this.writeBandwidthLimitBps);
		}
	}

	@Override
	public long getReadBandwidthLimit() {
		return readBandwidthLimitBps;
	}

	@Override
	public void setWriteBandwidthLimit(long bytesPerSecond) {
		this.writeBandwidthLimitBps = bytesPerSecond;

		if (isStarted()) {
			proxyServer.setThrottle(this.readBandwidthLimitBps, this.writeBandwidthLimitBps);
		}
	}

	@Override
	public long getWriteBandwidthLimit() {
		return writeBandwidthLimitBps;
	}

	public void endPage() {
		if (har == null) return;

		HarPage previousPage = this.currentHarPage;
		this.currentHarPage = null;

		if (previousPage == null) {
			return;
		}

		previousPage.getPageTimings().setOnLoad(new Date().getTime() - previousPage.getStartedDateTime().getTime());
	}

	@Override
	public void addHeaders(Map headers) {
		ConcurrentMap newHeaders = new MapMaker().concurrencyLevel(1).makeMap();
		newHeaders.putAll(headers);

		this.additionalHeaders = newHeaders;
	}

	@Override
	public void setLatency(long latency, TimeUnit timeUnit) {
		this.latencyMs = (int) TimeUnit.MILLISECONDS.convert(latency, timeUnit);
	}

	@Override
	public void autoAuthorization(String domain, String username, String password, AuthType authType) {
		switch (authType) {
			case BASIC:
				// base64 encode the "username:password" string
				String base64EncodedCredentials = BrowserMobHttpUtil.base64EncodeBasicCredentials(username, password);

				basicAuthCredentials.put(domain, base64EncodedCredentials);
				break;

			default:
				throw new UnsupportedOperationException("AuthType " + authType + " is not supported for HTTP Authorization");
		}
	}

	@Override
	public void stopAutoAuthorization(String domain) {
		basicAuthCredentials.remove(domain);
	}

	@Override
	public void chainedProxyAuthorization(String username, String password, AuthType authType) {
		switch (authType) {
			case BASIC:
				chainedProxyCredentials = BrowserMobHttpUtil.base64EncodeBasicCredentials(username, password);
				break;

			default:
				throw new UnsupportedOperationException("AuthType " + authType + " is not supported for Proxy Authorization");
		}
	}

	@Override
	public void setConnectTimeout(int connectTimeout, TimeUnit timeUnit) {
		this.connectTimeoutMs = (int) TimeUnit.MILLISECONDS.convert(connectTimeout, timeUnit);

		if (isStarted()) {
			proxyServer.setConnectTimeout((int) TimeUnit.MILLISECONDS.convert(connectTimeout, timeUnit));
		}
	}

	/**
	 * The LittleProxy implementation only allows idle connection timeouts to be specified in seconds. idleConnectionTimeouts greater than
	 * 0 but less than 1 second will be set to 1 second; otherwise, values will be truncated (i.e. 1500ms will become 1s).
	 */
	@Override
	public void setIdleConnectionTimeout(int idleConnectionTimeout, TimeUnit timeUnit) {
		long timeout = TimeUnit.SECONDS.convert(idleConnectionTimeout, timeUnit);
		if (timeout == 0 && idleConnectionTimeout > 0) {
			this.idleConnectionTimeoutSec = 1;
		} else {
			this.idleConnectionTimeoutSec = (int) timeout;
		}

		if (isStarted()) {
			proxyServer.setIdleConnectionTimeout(idleConnectionTimeoutSec);
		}
	}

	@Override
	public void setRequestTimeout(int requestTimeout, TimeUnit timeUnit) {
		//TODO: implement Request Timeouts using LittleProxy. currently this only sets an idle connection timeout, if the idle connection
		// timeout is higher than the specified requestTimeout.
		if (idleConnectionTimeoutSec == 0 || idleConnectionTimeoutSec > TimeUnit.SECONDS.convert(requestTimeout, timeUnit)) {
			setIdleConnectionTimeout(requestTimeout, timeUnit);
		}
	}

	@Override
	public void rewriteUrl(String pattern, String replace) {
		rewriteRules.add(new RewriteRule(pattern, replace));
	}

	@Override
	public void rewriteUrls(Map rewriteRules) {
		List newRules = new ArrayList<>(rewriteRules.size());
		for (Map.Entry rewriteRule : rewriteRules.entrySet()) {
			RewriteRule newRule = new RewriteRule(rewriteRule.getKey(), rewriteRule.getValue());
			newRules.add(newRule);
		}

		this.rewriteRules = new CopyOnWriteArrayList<>(newRules);
	}

	@Override
	public void clearRewriteRules() {
		rewriteRules.clear();
	}

	@Override
	public void blacklistRequests(String pattern, int responseCode) {
		blacklistEntries.add(new BlacklistEntry(pattern, responseCode));
	}

	@Override
	public void blacklistRequests(String pattern, int responseCode, String method) {
		blacklistEntries.add(new BlacklistEntry(pattern, responseCode, method));
	}

	@Override
	public void setBlacklist(Collection blacklist) {
		this.blacklistEntries = new CopyOnWriteArrayList<>(blacklist);
	}

	@Override
	public Collection getBlacklist() {
		return Collections.unmodifiableCollection(blacklistEntries);
	}

	@Override
	public boolean isWhitelistEnabled() {
		return whitelist.get().isEnabled();
	}

	@Override
	public Collection getWhitelistUrls() {
		ImmutableList.Builder builder = ImmutableList.builder();
		for (Pattern pattern : whitelist.get().getPatterns()) {
			builder.add(pattern.pattern());
		}

		return builder.build();
	}

	@Override
	public int getWhitelistStatusCode() {
		return whitelist.get().getStatusCode();
	}

	@Override
	public void clearBlacklist() {
		blacklistEntries.clear();
	}

	@Override
	public void whitelistRequests(Collection urlPatterns, int statusCode) {
		this.whitelist.set(new Whitelist(urlPatterns, statusCode));
	}

	@Override
	public void addWhitelistPattern(String urlPattern) {
		// to make sure this method is threadsafe, we need to guarantee that the "snapshot" of the whitelist taken at the beginning
		// of the method has not been replaced by the time we have constructed a new whitelist at the end of the method
		boolean whitelistUpdated = false;
		while (!whitelistUpdated) {
			Whitelist currentWhitelist = this.whitelist.get();
			if (!currentWhitelist.isEnabled()) {
				throw new IllegalStateException("Whitelist is disabled. Cannot add patterns to a disabled whitelist.");
			}

			// retrieve the response code and list of patterns from the current whitelist, the construct a new list of patterns that contains
			// all of the old whitelist's patterns + this new pattern
			int statusCode = currentWhitelist.getStatusCode();
			List newPatterns = new ArrayList<>(currentWhitelist.getPatterns().size() + 1);
			for (Pattern pattern : currentWhitelist.getPatterns()) {
				newPatterns.add(pattern.pattern());
			}
			newPatterns.add(urlPattern);

			// create a new (immutable) Whitelist object with the new pattern list and status code
			Whitelist newWhitelist = new Whitelist(newPatterns, statusCode);

			// replace the current whitelist with the new whitelist only if the current whitelist has not changed since we started
			whitelistUpdated = this.whitelist.compareAndSet(currentWhitelist, newWhitelist);
		}
	}

	/**
	 * Whitelist the specified request patterns, returning the specified responseCode for non-whitelisted
	 * requests.
	 *
	 * @param patterns     regular expression strings matching URL patterns to whitelist. if empty or null,
	 *                     the whitelist will be enabled but will not match any URLs.
	 * @param responseCode the HTTP response code to return for non-whitelisted requests
	 */
	public void whitelistRequests(String[] patterns, int responseCode) {
		if (patterns == null || patterns.length == 0) {
			this.enableEmptyWhitelist(responseCode);
		} else {
			this.whitelistRequests(Arrays.asList(patterns), responseCode);
		}
	}

	@Override
	public void enableEmptyWhitelist(int statusCode) {
		whitelist.set(new Whitelist(statusCode));
	}

	@Override
	public void disableWhitelist() {
		whitelist.set(Whitelist.WHITELIST_DISABLED);
	}

	@Override
	public void addHeader(String name, String value) {
		additionalHeaders.put(name, value);
	}

	@Override
	public void removeHeader(String name) {
		additionalHeaders.remove(name);
	}

	@Override
	public void removeAllHeaders() {
		additionalHeaders.clear();
	}

	@Override
	public Map getAllHeaders() {
		return ImmutableMap.copyOf(additionalHeaders);
	}

	@Override
	public void setHostNameResolver(AdvancedHostResolver resolver) {
		delegatingResolver.setResolver(resolver);
	}

	@Override
	public AdvancedHostResolver getHostNameResolver() {
		return delegatingResolver.getResolver();
	}

	@Override
	public boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUnit) {
		return activityMonitor.waitForQuiescence(quietPeriod, timeout, timeUnit);
	}

	/**
	 * Instructs this proxy to route traffic through an upstream proxy.
	 *
	 * Note: Using {@link #setChainedProxyManager(ChainedProxyManager)} will supersede any value set by this method. A chained
	 * proxy must be set before the proxy is started, though it can be changed after the proxy is started.
	 *
	 * @param chainedProxyAddress address of the upstream proxy
	 */
	@Override
	public void setChainedProxy(InetSocketAddress chainedProxyAddress) {
		if (isStarted() && !bootstrappedWithDefaultChainedProxy.get()) {
			throw new IllegalStateException("Cannot set a chained proxy after the proxy is started if the proxy was started without a chained proxy.");
		}

		upstreamProxyAddress = chainedProxyAddress;
	}

	@Override
	public InetSocketAddress getChainedProxy() {
		return upstreamProxyAddress;
	}

	/**
	 * Allows access to the LittleProxy {@link ChainedProxyManager} for fine-grained control of the chained proxies. To enable a single
	 * chained proxy, {@link BrowserMobProxy#setChainedProxy(InetSocketAddress)} is generally more convenient.
	 *
	 * Note: The chained proxy manager must be enabled before calling {@link #start()}.
	 *
	 * @param chainedProxyManager chained proxy manager to enable
	 */
	public void setChainedProxyManager(ChainedProxyManager chainedProxyManager) {
		if (isStarted()) {
			throw new IllegalStateException("Cannot configure chained proxy manager after proxy has started.");
		}

		this.chainedProxyManager = chainedProxyManager;
	}

	/**
	 * Configures the Netty thread pool used by the LittleProxy back-end. See {@link ThreadPoolConfiguration} for details.
	 *
	 * @param threadPoolConfiguration thread pool configuration to use
	 */
	public void setThreadPoolConfiguration(ThreadPoolConfiguration threadPoolConfiguration) {
		if (isStarted()) {
			throw new IllegalStateException("Cannot configure thread pool after proxy has started.");
		}

		this.threadPoolConfiguration = threadPoolConfiguration;
	}

	@Override
	public void addFirstHttpFilterFactory(HttpFiltersSource filterFactory) {
		filterFactories.add(0, filterFactory);
	}

	@Override
	public void addLastHttpFilterFactory(HttpFiltersSource filterFactory) {
		filterFactories.add(filterFactory);
	}

	/**
	 * Note: The current implementation of this method forces a maximum response size of 2 MiB. To adjust the maximum response size, or
	 * to disable aggregation (which disallows access to the {@link net.lightbody.bmp.util.HttpMessageContents}), you may add the filter source
	 * directly: addFirstHttpFilterFactory(new ResponseFilterAdapter.FilterSource(filter, bufferSizeInBytes));
	 */
	@Override
	public void addResponseFilter(ResponseFilter filter) {
		addLastHttpFilterFactory(new ResponseFilterAdapter.FilterSource(filter));
	}

	/**
	 * Note: The current implementation of this method forces a maximum request size of 2 MiB. To adjust the maximum request size, or
	 * to disable aggregation (which disallows access to the {@link net.lightbody.bmp.util.HttpMessageContents}), you may add the filter source
	 * directly: addFirstHttpFilterFactory(new RequestFilterAdapter.FilterSource(filter, bufferSizeInBytes));
	 */
	@Override
	public void addRequestFilter(RequestFilter filter) {
		addFirstHttpFilterFactory(new RequestFilterAdapter.FilterSource(filter));
	}

	@Override
	public Map getRewriteRules() {
		ImmutableMap.Builder builder = ImmutableMap.builder();
		for (RewriteRule rewriteRule : rewriteRules) {
			builder.put(rewriteRule.getPattern().pattern(), rewriteRule.getReplace());
		}

		return builder.build();
	}

	@Override
	public void removeRewriteRule(String urlPattern) {
		// normally removing elements from the list we are iterating over would not be possible, but since this is a CopyOnWriteArrayList
		// the iterator it returns is a "snapshot" of the list that will not be affected by removal (and that does not support removal, either)
		for (RewriteRule rewriteRule : rewriteRules) {
			if (rewriteRule.getPattern().pattern().equals(urlPattern)) {
				rewriteRules.remove(rewriteRule);
			}
		}
	}

	public boolean isStopped() {
		return stopped.get();
	}

	public HarPage getCurrentHarPage() {
		return currentHarPage;
	}

	public void addHttpFilterFactory(HttpFiltersSource filterFactory) {
		filterFactories.add(filterFactory);
	}

	public List getFilterFactories() {
		return filterFactories;
	}

	@Override
	public void setMitmDisabled(boolean mitmDisabled) throws IllegalStateException {
		if (isStarted()) {
			throw new IllegalStateException("Cannot disable MITM after the proxy has been started");
		}

		this.mitmDisabled = mitmDisabled;
	}

	@Override
	public void setMitmManager(MitmManager mitmManager) {
		this.mitmManager = mitmManager;
	}

	@Override
	public void setTrustAllServers(boolean trustAllServers) {
		if (isStarted()) {
			throw new IllegalStateException("Cannot disable upstream server verification after the proxy has been started");
		}

		if (trustAllServers) {
			trustSource = null;
		} else {
			if (trustSource == null) {
				trustSource = TrustSource.defaultTrustSource();
			}
		}
	}

	@Override
	public void setTrustSource(TrustSource trustSource) {
		if (isStarted()) {
			throw new IllegalStateException("Cannot change TrustSource after proxy has been started");
		}

		this.trustSource = trustSource;
	}

	public boolean isMitmDisabled() {
		return this.mitmDisabled;
	}

	public void setUseEcc(boolean useEcc) {
		this.useEcc = useEcc;
	}

	/**
	 * Adds the basic browsermob-proxy filters, except for the relatively-expensive HAR capture filter.
	 */
	protected void addBrowserMobFilters() {
		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new ResolvedHostnameCacheFilter(originalRequest, ctx);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new RegisterRequestFilter(originalRequest, ctx, activityMonitor);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new HttpsOriginalHostCaptureFilter(originalRequest, ctx);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new BlacklistFilter(originalRequest, ctx, getBlacklist());
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				Whitelist currentWhitelist = whitelist.get();
				return new WhitelistFilter(originalRequest, ctx, isWhitelistEnabled(), currentWhitelist.getStatusCode(), currentWhitelist.getPatterns());
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new AutoBasicAuthFilter(originalRequest, ctx, basicAuthCredentials);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new RewriteUrlFilter(originalRequest, ctx, rewriteRules);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new HttpsHostCaptureFilter(originalRequest, ctx);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest) {
				return new AddHeadersFilter(originalRequest, additionalHeaders);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest) {
				return new LatencyFilter(originalRequest, latencyMs);
			}
		});

		addHttpFilterFactory(new HttpFiltersSourceAdapter() {
			@Override
			public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
				return new UnregisterRequestFilter(originalRequest, ctx, activityMonitor);
			}
		});
	}

	private int getMaximumRequestBufferSize() {
		int maxBufferSize = 0;
		for (HttpFiltersSource source : filterFactories) {
			int requestBufferSize = source.getMaximumRequestBufferSizeInBytes();
			if (requestBufferSize > maxBufferSize) {
				maxBufferSize = requestBufferSize;
			}
		}

		return maxBufferSize;
	}

	private int getMaximumResponseBufferSize() {
		int maxBufferSize = 0;
		for (HttpFiltersSource source : filterFactories) {
			int requestBufferSize = source.getMaximumResponseBufferSizeInBytes();
			if (requestBufferSize > maxBufferSize) {
				maxBufferSize = requestBufferSize;
			}
		}

		return maxBufferSize;
	}

	/**
	 * Enables the HAR capture filter if it has not already been enabled. The filter will be added to the end of the filter chain.
	 * The HAR capture filter is relatively expensive, so this method is only called when a HAR is requested.
	 */
	protected void addHarCaptureFilter() {
		if (harCaptureFilterEnabled.compareAndSet(false, true)) {
			// the HAR capture filter is (relatively) expensive, so only enable it when a HAR is being captured. furthermore,
			// restricting the HAR capture filter to requests where the HAR exists, as well as  excluding HTTP CONNECTs
			// from the HAR capture filter, greatly simplifies the filter code.
			addHttpFilterFactory(new HttpFiltersSourceAdapter() {
				@Override
				public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
					Har har = getOrCreateHar();
					if (har != null && !ProxyUtils.isCONNECT(originalRequest)) {
						return new HarCaptureFilter(originalRequest, ctx, har, getCurrentPageRef(), getHarCaptureTypes());
					} else {
						return null;
					}
				}
			});

			// HTTP CONNECTs are a special case, since they require special timing and error handling
			addHttpFilterFactory(new HttpFiltersSourceAdapter() {
				@Override
				public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
					Har har = getOrCreateHar();
					if (har != null && ProxyUtils.isCONNECT(originalRequest)) {
						return new HttpConnectHarCaptureFilter(originalRequest, ctx, har, getCurrentPageRef());
					} else {
						return null;
					}
				}
			});
		}
	}
	private Har getOrCreateHar(String initialPageRef, String initialPageTitle, boolean createPage) {
		if (har == null) {
			synchronized (LOCK) {
				if (har == null) {
					newHar(initialPageRef, initialPageTitle, createPage);
				}
			}
		}
		return har;
	}

	private Har getOrCreateHar() {
		return getOrCreateHar(DEFAULT_PAGE_REF, DEFAULT_PAGE_TITLE, true);
	}

	private String getCurrentPageRef() {
		HarPage harPage = getCurrentHarPage();
		harPage = harPage == null ? getOrCreateDefaultPage() : harPage;

		return harPage.getId();
	}

	private HarPage getOrCreateDefaultPage() {
		return getDefaultPage().orElseGet(this::addDefaultPage);
	}

	private HarPage addDefaultPage() {
		HarPage newPage = new HarPage();
		newPage.setTitle(DEFAULT_PAGE_REF);
		newPage.setId(DEFAULT_PAGE_REF);
		newPage.setStartedDateTime(new Date());
		getHar().getLog().getPages().add(newPage);
		return newPage;
	}

	private Optional getDefaultPage() {
		return getHar().getLog().getPages().stream()
				.filter(p -> p.getTitle().equals(DEFAULT_PAGE_REF))
				.findFirst();
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy