/* * Created on May 25, 2004 * * Paros and its related class files. * * Paros is an HTTP/HTTPS proxy for assessing web application security. * Copyright (C) 2003-2004 Chinotec Technologies Company * * This program is free software; you can redistribute it and/or * modify it under the terms of the Clarified Artistic License * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Clarified Artistic License for more details. * * You should have received a copy of the Clarified Artistic License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // ZAP: 2011/05/09 Support for API // ZAP: 2011/05/15 Support for exclusions // ZAP: 2012/03/15 Removed unnecessary castings from methods notifyListenerRequestSend, // notifyListenerResponseReceive and isProcessCache. Set the name of the proxy thread. // Replaced the class HttpBody with the new class HttpRequestBody and replaced the method // call from readBody to readRequestBody of the class HttpInputStream. // ZAP: 2012/04/25 Added @Override annotation to the appropriate method. // ZAP: 2012/05/11 Do not close connections in final clause of run() method, // if boolean attribute keepSocketOpen is set to true. // ZAP: 2012/08/07 Issue 342 Support the HttpSenderListener // ZAP: 2012/11/04 Issue 408: Add support to encoding transformations, added an // option to control whether the "Accept-Encoding" request-header field is // modified/removed or not. // ZAP: 2012/12/27 Added support for PersistentConnectionListener. // ZAP: 2013/01/04 Do beginSSL() on HTTP CONNECT only if port requires so. // ZAP: 2013/03/03 Issue 547: Deprecate unused classes and methods // ZAP: 2013/04/11 Issue 621: Handle requests to the proxy URL // ZAP: 2013/04/14 Issue 622: Local proxy unable to correctly detect requests to itself // ZAP: 2013/06/17 Issue 686: Log HttpException (as error) in the ProxyThread // ZAP: 2013/12/13 Issue 939: ZAP should accept SSL connections on non-standard ports automatically // ZAP: 2014/03/06 Issue 1063: Add option to decode all gzipped content // ZAP: 2014/03/23 Tidy up, extracted a method that writes an HTTP response and moved the // code responsible to decode a GZIP response to a method // ZAP: 2014/03/23 Fixed an issue with ProxyThread that happened when the proxy was set to listen on // any address in which case the requests to the proxy itself were not correctly detected. // ZAP: 2014/03/23 Issue 122: ProxyThread logging timeout readings with incorrect message (URL) // ZAP: 2014/03/23 Issue 585: Proxy - "502 Bad Gateway" errors responded as "504 Gateway Timeout" // ZAP: 2014/03/23 Issue 969: Proxy - Do not include the response body when answering unsuccessful HEAD requests // ZAP: 2014/03/23 Issue 1017: Proxy set to 0.0.0.0 causes incorrect PAC file to be generated // ZAP: 2014/03/23 Issue 1022: Proxy - Allow to override a proxied message // ZAP: 2014/04/17 Issue 1156: Proxy gzip decoder doesn't update content length in response headers // ZAP: 2014/05/01 Issue 1156: Proxy gzip decoder removes newlines in decoded response // ZAP: 2014/05/01 Issue 1168: Add support for deflate encoded responses // ZAP: 2015/01/04 Issue 1334: ZAP does not handle API requests on reused connections // ZAP: 2015/02/24 Issue 1540: Allow proxy scripts to fake responses // ZAP: 2015/07/17 Show stack trace of the exceptions on proxy errors // ZAP: 2016/03/18 Issue 2318: ZAP Error [java.net.SocketTimeoutException]: Read timed out when running on AWS EC2 instance // ZAP: 2016/04/13 Notify of timeouts when reading a response // ZAP: 2016/04/14 Delay the write of response to not attempt to write a response again when handling IOException // ZAP: 2016/04/29 Adjust exception logging levels and log when timeouts happen // ZAP: 2016/05/30 Issue 2494: ZAP Proxy is not showing the HTTP CONNECT Request in history tab // ZAP: 2016/06/13 Remove all unsupported encodings (instead of just some) // ZAP: 2016/09/22 JavaDoc tweaks // ZAP: 2016/11/28 Correct proxy errors' Content-Length value. // ZAP: 2016/12/07 Allow to extend the ProxyThread and use a custom HttpSender // ZAP: 2016/12/23 Make SocketTimeoutException less verbose for general use // ZAP: 2017/02/08 Differentiate client read timeout after CONNECT, from server read timeout. // ZAP: 2017/02/08 Change CONNECT response to contain just the status line, helps Android emulator consume the response. // ZAP: 2017/02/20 Issue 2699: Make SSLException handling more user friendly // ZAP: 2017/02/23 Issue 3227: Limit API access to whitelisted IP addresses // ZAP: 2017/03/15 Disable API by default // ZAP: 2017/03/26 Check the public address when behind NAT. package org.parosproxy.paros.core.proxy; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.io.FilterInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Vector; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import javax.net.ssl.SSLException; import org.apache.commons.httpclient.HttpException; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.log4j.Logger; import org.ice4j.TransportAddress; import org.ice4j.ice.harvest.AwsCandidateHarvester; import org.parosproxy.paros.Constant; import org.parosproxy.paros.db.RecordHistory; import org.parosproxy.paros.model.Model; import org.parosproxy.paros.network.ConnectionParam; import org.parosproxy.paros.network.HttpHeader; import org.parosproxy.paros.network.HttpInputStream; import org.parosproxy.paros.network.HttpMalformedHeaderException; import org.parosproxy.paros.network.HttpMessage; import org.parosproxy.paros.network.HttpOutputStream; import org.parosproxy.paros.network.HttpRequestHeader; import org.parosproxy.paros.network.HttpResponseHeader; import org.parosproxy.paros.network.HttpSender; import org.parosproxy.paros.network.HttpUtil; import org.parosproxy.paros.security.MissingRootCertificateException; import org.zaproxy.zap.PersistentConnectionListener; import org.zaproxy.zap.ZapGetMethod; import org.zaproxy.zap.extension.api.API; import org.zaproxy.zap.network.HttpRequestBody; public class ProxyThread implements Runnable { // private static final int BUFFEREDSTREAM_SIZE = 4096; private static final String CONNECT_HTTP_200 = "HTTP/1.1 200 Connection established\r\n\r\n"; // private static ArrayList processForwardList = new ArrayList(); private static Logger log = Logger.getLogger(ProxyThread.class); private static final String BAD_GATEWAY_RESPONSE_STATUS = "502 Bad Gateway"; private static final String GATEWAY_TIMEOUT_RESPONSE_STATUS = "504 Gateway Timeout"; // change httpSender to static to be shared among proxies to reuse keep-alive connections protected ProxyServer parentServer = null; protected ProxyParam proxyParam = null; protected ConnectionParam connectionParam = null; protected Thread thread = null; protected Socket inSocket = null; protected Socket outSocket = null; protected HttpInputStream httpIn = null; protected HttpOutputStream httpOut = null; protected ProxyThread originProcess = this; private HttpSender httpSender = null; private Object semaphore = this; // ZAP: New attribute to allow for skipping disconnect private boolean keepSocketOpen = false; private static Object semaphoreSingleton = new Object(); private static int id = 1; private static Vector<Thread> proxyThreadList = new Vector<>(); /** * Used to obtain the public address of AWS EC2 instance. * <p> * Lazily initialised. * * @see #getAwsCandidateHarvester() * @see #isOwnPublicAddress(InetAddress) */ private static AwsCandidateHarvester awsCandidateHarvester; protected ProxyThread(ProxyServer server, Socket socket) { this(server, socket, null); } /** * Constructs a {@code ProxyThread} with the given proxy server, socket and HTTP sender. * * @param server the parent proxy server. * @param socket the connected socket to read/write the messages. * @param httpSender the object used to send the messages, might be {@code null} in which case a default is used. * @since 2.6.0 */ protected ProxyThread(ProxyServer server, Socket socket, HttpSender httpSender) { parentServer = server; proxyParam = parentServer.getProxyParam(); connectionParam = parentServer.getConnectionParam(); this.httpSender = httpSender; inSocket = socket; try { inSocket.setTcpNoDelay(true); // ZAP: Set timeout inSocket.setSoTimeout(connectionParam.getTimeoutInSecs() * 1000); } catch (SocketException e) { // ZAP: Log exceptions log.warn(e.getMessage(), e); } thread = new Thread(this, "ZAP-ProxyThread-" + id++); // ZAP: Set the name of the thread. thread.setDaemon(true); thread.setPriority(Thread.NORM_PRIORITY-1); } public void start() { thread.start(); } /** * @param targethost the host where you want to connect to * @throws IOException if an error occurred while establishing the SSL/TLS connection */ private void beginSSL(String targethost) throws IOException { // ZAP: added parameter 'targethost' try { inSocket = HttpSender.getSSLConnector().createTunnelServerSocket(targethost, inSocket); } catch (MissingRootCertificateException e) { throw new MissingRootCertificateException(e); // throw again, cause will be catched later. } catch (Exception e) { // ZAP: transform for further processing throw new IOException("Error while establishing SSL connection for '" + targethost + "'!", e); } httpIn = new HttpInputStream(inSocket); httpOut = new HttpOutputStream(inSocket.getOutputStream()); } private static boolean isSslTlsHandshake(byte[] bytes) { if (bytes.length < 3) { throw new IllegalArgumentException("The parameter bytes must have at least 3 bytes."); } // Check if ContentType is handshake(22) if (bytes[0] == 0x16) { // Check if "valid" ProtocolVersion >= SSLv3 (TLSv1, TLSv1.1, ...) or SSLv2 if (bytes[1] >= 0x03 || (bytes[1] == 0x00 && bytes[2] == 0x02)) { return true; } } return false; } @Override public void run() { proxyThreadList.add(thread); boolean isSecure = this instanceof ProxyThreadSSL; HttpRequestHeader firstHeader = null; try { BufferedInputStream bufferedInputStream = new BufferedInputStream(inSocket.getInputStream(), 2048); inSocket = new CustomStreamsSocket(inSocket, bufferedInputStream, inSocket.getOutputStream()); httpIn = new HttpInputStream(inSocket); httpOut = new HttpOutputStream(inSocket.getOutputStream()); firstHeader = httpIn.readRequestHeader(isSecure); firstHeader.setSenderAddress(inSocket.getInetAddress()); if (firstHeader.getMethod().equalsIgnoreCase(HttpRequestHeader.CONNECT)) { HttpMessage connectMsg = new HttpMessage(firstHeader); connectMsg.setTimeSentMillis(System.currentTimeMillis()); try { httpOut.write(CONNECT_HTTP_200); httpOut.flush(); connectMsg.setResponseHeader(CONNECT_HTTP_200); connectMsg.setTimeElapsedMillis((int) (System.currentTimeMillis() - connectMsg.getTimeSentMillis())); notifyConnectMessage(connectMsg); byte[] bytes = new byte[3]; bufferedInputStream.mark(3); bufferedInputStream.read(bytes); bufferedInputStream.reset(); if (isSslTlsHandshake(bytes)) { isSecure = true; beginSSL(firstHeader.getHostName()); } firstHeader = httpIn.readRequestHeader(isSecure); firstHeader.setSenderAddress(inSocket.getInetAddress()); processHttp(firstHeader, isSecure); } catch (MissingRootCertificateException e) { // Unluckily Firefox and Internet Explorer will not show this message. // We should find a way to let the browsers display this error message. // May we can redirect to some kind of ZAP custom error page. final HttpMessage errmsg = new HttpMessage(firstHeader); setErrorResponse(errmsg, BAD_GATEWAY_RESPONSE_STATUS, e, "ZAP SSL Error"); writeHttpResponse(errmsg, httpOut); throw new IOException(e); } } else { processHttp(firstHeader, isSecure); } } catch (SocketTimeoutException e) { // ZAP: Log the exception if (firstHeader != null) { if (HttpRequestHeader.CONNECT.equalsIgnoreCase(firstHeader.getMethod())) { log.warn("Timeout reading (client) message after CONNECT to " + firstHeader.getURI()); } else { log.warn("Timeout accessing " + firstHeader.getURI()); } } else { log.warn("Socket timeout while reading first message."); if (log.isDebugEnabled()) { log.debug(e, e); } } } catch (HttpMalformedHeaderException e) { log.warn("Malformed Header: ", e); } catch (HttpException e) { log.error(e.getMessage(), e); } catch (IOException e) { log.debug("IOException: ", e); } finally { proxyThreadList.remove(thread); // ZAP: do only close if flag is false if (!keepSocketOpen) { disconnect(); } } } /** * Notifies the {@code ConnectRequestProxyListener}s that a HTTP CONNECT request was received from a client. * * @param connectMessage the HTTP CONNECT request received from a client */ private void notifyConnectMessage(HttpMessage connectMessage) { for (ConnectRequestProxyListener listener : parentServer.getConnectRequestProxyListeners()) { try { listener.receivedConnectRequest(connectMessage); } catch (Exception e) { log.error("An error occurred while notifying listener:", e); } } } private static void setErrorResponse(HttpMessage msg, String responseStatus, Exception cause) throws HttpMalformedHeaderException { setErrorResponse(msg, responseStatus, cause, "ZAP Error"); } private static void setErrorResponse(HttpMessage msg, String responseStatus, Exception cause, String errorType) throws HttpMalformedHeaderException { StringBuilder strBuilder = new StringBuilder(); if (cause instanceof SSLException) { strBuilder.append(Constant.messages.getString("network.ssl.error.connect")); strBuilder.append(msg.getRequestHeader().getURI().toString()).append('\n'); strBuilder.append(Constant.messages.getString("network.ssl.error.exception")).append(cause.getMessage()) .append('\n'); strBuilder.append(Constant.messages.getString("network.ssl.error.exception.rootcause")) .append(ExceptionUtils.getRootCauseMessage(cause)).append('\n'); strBuilder.append(Constant.messages.getString("network.ssl.error.help", Constant.messages.getString("network.ssl.error.help.url"))); log.warn(strBuilder.toString()); if (log.isDebugEnabled()) { log.debug(cause, cause); strBuilder.append("\n\nStack Trace:\n"); for (String stackTraceFrame : ExceptionUtils.getRootCauseStackTrace(cause)) { strBuilder.append(stackTraceFrame).append('\n'); } } } else { strBuilder.append(errorType) .append(" [") .append(cause.getClass().getName()) .append("]: ") .append(cause.getLocalizedMessage()) .append("\n\nStack Trace:\n"); for (String stackTraceFrame : ExceptionUtils.getRootCauseStackTrace(cause)) { strBuilder.append(stackTraceFrame).append('\n'); } } setErrorResponse(msg, responseStatus, strBuilder.toString()); } private static void setErrorResponse(HttpMessage msg, String responseStatus, String message) throws HttpMalformedHeaderException { HttpResponseHeader responseHeader = new HttpResponseHeader("HTTP/1.1 " + responseStatus); responseHeader.setHeader(HttpHeader.CONTENT_TYPE, "text/plain; charset=UTF-8"); responseHeader.setHeader(HttpHeader.CONTENT_LENGTH, Integer.toString(message.getBytes(StandardCharsets.UTF_8).length)); msg.setResponseHeader(responseHeader); if (!HttpRequestHeader.HEAD.equals(msg.getRequestHeader().getMethod())) { msg.setResponseBody(message); } } private static void writeHttpResponse(HttpMessage msg, HttpOutputStream outputStream) throws IOException { outputStream.write(msg.getResponseHeader()); outputStream.flush(); if (msg.getResponseBody().length() > 0) { outputStream.write(msg.getResponseBody().getBytes()); outputStream.flush(); } } protected void processHttp(HttpRequestHeader requestHeader, boolean isSecure) throws IOException { HttpRequestBody reqBody = null; // ZAP: Replaced the class HttpBody with the class HttpRequestBody. boolean isFirstRequest = true; HttpMessage msg = null; // reduce socket timeout after first read inSocket.setSoTimeout(2500); do { if (isFirstRequest) { isFirstRequest = false; } else { try { requestHeader = httpIn.readRequestHeader(isSecure); requestHeader.setSenderAddress(inSocket.getInetAddress()); } catch (SocketTimeoutException e) { // ZAP: Log the exception if (log.isDebugEnabled()) { log.debug("Timed out while reading a new HTTP request."); } return; } } if (parentServer.isEnableApi() && API.getInstance().handleApiRequest(requestHeader, httpIn, httpOut, isRecursive(requestHeader))) { // It was an API request return; } msg = new HttpMessage(); msg.setRequestHeader(requestHeader); if (msg.getRequestHeader().getContentLength() > 0) { reqBody = httpIn.readRequestBody(requestHeader); // ZAP: Changed to call the method readRequestBody. msg.setRequestBody(reqBody); } if (proxyParam.isRemoveUnsupportedEncodings()) { removeUnsupportedEncodings(msg); } if (isProcessCache(msg)) { continue; } // System.out.println("send required: " + msg.getRequestHeader().getURI().toString()); if (parentServer.isSerialize()) { semaphore = semaphoreSingleton; } else { semaphore = this; } boolean send = true; synchronized (semaphore) { if (notifyOverrideListenersRequestSend(msg)) { send = false; } else if (! notifyListenerRequestSend(msg)) { // One of the listeners has told us to drop the request return; } try { // bug occur where response cannot be processed by various listener // first so streaming feature was disabled // getHttpSender().sendAndReceive(msg, httpOut, buffer); if (send) { if (msg.getResponseHeader().isEmpty()) { // Normally the response is empty. // The only reason it wont be is if a script or other ext has deliberately 'hijacked' this request // We dont jsut set send=false as this then means it wont appear in the History tab getHttpSender().sendAndReceive(msg); } decodeResponseIfNeeded(msg); if (!notifyOverrideListenersResponseReceived(msg)) { if (!notifyListenerResponseReceive(msg)) { // One of the listeners has told us to drop the response return; } } } // notifyWrittenToForwardProxy(); } catch (HttpException e) { // System.out.println("HttpException"); throw e; } catch (SocketTimeoutException e) { String message = Constant.messages.getString( "proxy.error.readtimeout", msg.getRequestHeader().getURI(), connectionParam.getTimeoutInSecs()); log.warn(message); setErrorResponse(msg, GATEWAY_TIMEOUT_RESPONSE_STATUS, message); notifyListenerResponseReceive(msg); } catch (IOException e) { setErrorResponse(msg, BAD_GATEWAY_RESPONSE_STATUS, e); notifyListenerResponseReceive(msg); //throw e; } try { writeHttpResponse(msg, httpOut); } catch (IOException e) { StringBuilder strBuilder = new StringBuilder(200); strBuilder.append("Failed to write/forward the HTTP response to the client: "); strBuilder.append(e.getClass().getName()); if (e.getMessage() != null) { strBuilder.append(": ").append(e.getMessage()); } log.warn(strBuilder.toString()); } } // release semaphore ZapGetMethod method = (ZapGetMethod) msg.getUserObject(); keepSocketOpen = notifyPersistentConnectionListener(msg, inSocket, method); if (keepSocketOpen) { // do not wait for close break; } } while (!isConnectionClose(msg) && !inSocket.isClosed()); } private FilterInputStream buildStreamDecoder(String encoding, ByteArrayInputStream bais) throws IOException { if (encoding.equalsIgnoreCase(HttpHeader.DEFLATE)) { return new InflaterInputStream(bais, new Inflater(true)); } else { return new GZIPInputStream(bais); } } private void decodeResponseIfNeeded(HttpMessage msg) { String encoding = msg.getResponseHeader().getHeader(HttpHeader.CONTENT_ENCODING); if (proxyParam.isAlwaysDecodeGzip() && encoding != null && !encoding.equalsIgnoreCase(HttpHeader.IDENTITY)) { encoding = Pattern.compile("^x-", Pattern.CASE_INSENSITIVE).matcher(encoding).replaceAll(""); if (!encoding.equalsIgnoreCase(HttpHeader.DEFLATE) && !encoding.equalsIgnoreCase(HttpHeader.GZIP)) { log.warn("Unsupported content encoding method: " + encoding); return; } // Uncompress content try (ByteArrayInputStream bais = new ByteArrayInputStream(msg.getResponseBody().getBytes()); FilterInputStream fis = buildStreamDecoder(encoding, bais); BufferedInputStream bis = new BufferedInputStream(fis); ByteArrayOutputStream out = new ByteArrayOutputStream();) { int readLength; byte[] readBuffer = new byte[1024]; while ((readLength = bis.read(readBuffer, 0,1024)) != -1) { out.write(readBuffer, 0, readLength); } msg.setResponseBody(out.toByteArray()); msg.getResponseHeader().setHeader(HttpHeader.CONTENT_ENCODING, null); if (msg.getResponseHeader().getHeader(HttpHeader.CONTENT_LENGTH) != null) { msg.getResponseHeader().setHeader(HttpHeader.CONTENT_LENGTH, Integer.toString(out.size())); } } catch (IOException e) { log.error("Unable to uncompress gzip content: " + e.getMessage(), e); } } } private boolean isConnectionClose(HttpMessage msg) { if (msg == null || msg.getResponseHeader().isEmpty()) { return true; } if (msg.getRequestHeader().isConnectionClose()) { return true; } if (msg.getResponseHeader().isConnectionClose()) { return true; } if (msg.getResponseHeader().getContentLength() == -1 && msg.getResponseBody().length() > 0) { // no length and body > 0 must terminate otherwise cannot there is no way for client browser to know the length. // terminate early can give better response by client. return true; } return false; } protected void disconnect() { try { if (httpIn != null) { httpIn.close(); } } catch (Exception e) { if (log.isDebugEnabled()) { log.debug(e.getMessage(), e); } } try { if (httpOut != null) { httpOut.close(); } } catch (Exception e) { if (log.isDebugEnabled()) { log.debug(e.getMessage(), e); } } HttpUtil.closeSocket(inSocket); if (httpSender != null) { httpSender.shutdown(); } } /** * Go through each observers to process a request in each observers. * The method can be modified in each observers. * @param httpMessage the request that was received from the client and may be forwarded to the server * @return {@code true} if the message should be forwarded to the server, {@code false} otherwise */ private boolean notifyListenerRequestSend(HttpMessage httpMessage) { if (parentServer.excludeUrl(httpMessage.getRequestHeader().getURI())) { return true; } ProxyListener listener = null; List<ProxyListener> listenerList = parentServer.getListenerList(); for (int i=0;i<listenerList.size();i++) { listener = listenerList.get(i); try { if (! listener.onHttpRequestSend(httpMessage)) { return false; } } catch (Exception e) { log.error("An error occurred while notifying listener:", e); } } return true; } /** * Go thru each observers and process the http message in each observers. * The msg can be changed by each observers. * @param httpMessage the response that was received from the server and may be forwarded to the client * @return {@code true} if the message should be forwarded to the client, {@code false} otherwise */ private boolean notifyListenerResponseReceive(HttpMessage httpMessage) { if (parentServer.excludeUrl(httpMessage.getRequestHeader().getURI())) { return true; } ProxyListener listener = null; List<ProxyListener> listenerList = parentServer.getListenerList(); for (int i=0;i<listenerList.size();i++) { listener = listenerList.get(i); try { if (!listener.onHttpResponseReceive(httpMessage)) { return false; } } catch (Exception e) { log.error("An error occurred while notifying listener:", e); } } return true; } private boolean notifyOverrideListenersRequestSend(HttpMessage httpMessage) { for (OverrideMessageProxyListener listener : parentServer.getOverrideMessageProxyListeners()) { try { if (listener.onHttpRequestSend(httpMessage)) { return true; } } catch (Exception e) { log.error("An error occurred while notifying listener:", e); } } return false; } private boolean notifyOverrideListenersResponseReceived(HttpMessage httpMessage) { for (OverrideMessageProxyListener listener : parentServer.getOverrideMessageProxyListeners()) { try { if (listener.onHttpResponseReceived(httpMessage)) { return true; } } catch (Exception e) { log.error("An error occurred while notifying listener:", e); } } return false; } /** * Go thru each listener and offer him to take over the connection. The * first observer that returns true gets exclusive rights. * * @param httpMessage Contains HTTP request & response. * @param inSocket Encapsulates the TCP connection to the browser. * @param method Provides more power to process response. * * @return Boolean to indicate if socket should be kept open. */ private boolean notifyPersistentConnectionListener(HttpMessage httpMessage, Socket inSocket, ZapGetMethod method) { boolean keepSocketOpen = false; PersistentConnectionListener listener = null; List<PersistentConnectionListener> listenerList = parentServer.getPersistentConnectionListenerList(); for (int i=0; i<listenerList.size(); i++) { listener = listenerList.get(i); try { if (listener.onHandshakeResponse(httpMessage, inSocket, method)) { // inform as long as one listener wishes to overtake the connection keepSocketOpen = true; break; } } catch (Exception e) { log.error("An error occurred while notifying listener:", e); } } return keepSocketOpen; } /** * Tells whether or not the given {@code header} has a request to the (parent) proxy itself. * <p> * The request is to the proxy itself if the following conditions are met: * <ol> * <li>The requested port is the one that the proxy is bound to;</li> * <li>The requested domain is {@link API#API_DOMAIN} or, the requested address is one of the addresses the proxy is * listening to.</li> * </ol> * * @param header the request that will be checked * @return {@code true} if it is a request to the proxy itself, {@code false} otherwise. * @see #isProxyAddress(InetAddress) */ private boolean isRecursive(HttpRequestHeader header) { try { if (header.getHostPort() == inSocket.getLocalPort()) { String targetDomain = header.getHostName(); if (API.API_DOMAIN.equals(targetDomain)) { return true; } if (isProxyAddress(InetAddress.getByName(targetDomain))) { return true; } } } catch (Exception e) { // ZAP: Log exceptions log.warn(e.getMessage(), e); } return false; } /** * Tells whether or not the given {@code address} is one of address(es) the (parent) proxy is listening to. * <p> * If the proxy is listening to any address it checks whether the given {@code address} is a local address or if it belongs * to a network interface. If not listening to any address, it checks if it's the one it is listening to. * * @param address the address that will be checked * @return {@code true} if it is one of the addresses the proxy is listening to, {@code false} otherwise. * @see #isLocalAddress(InetAddress) * @see #isNetworkInterfaceAddress(InetAddress) */ private boolean isProxyAddress(InetAddress address) { if (parentServer.getProxyParam().isProxyIpAnyLocalAddress()) { if (isLocalAddress(address) || isNetworkInterfaceAddress(address) || isOwnPublicAddress(address)) { return true; } } else if (address.equals(inSocket.getLocalAddress())) { return true; } return false; } /** * Tells whether or not the given {@code address} is a loopback, a site local or any local address. * * @param address the address that will be checked * @return {@code true} if the address is loopback, site local or any local address, {@code false} otherwise. * @see InetAddress#isLoopbackAddress() * @see InetAddress#isSiteLocalAddress() * @see InetAddress#isAnyLocalAddress() */ private static boolean isLocalAddress(InetAddress address) { return address.isLoopbackAddress() || address.isSiteLocalAddress() || address.isAnyLocalAddress(); } /** * Tells whether or not the given {@code address} belongs to any of the network interfaces. * * @param address the address that will be checked * @return {@code true} if the address belongs to any of the network interfaces, {@code false} otherwise. * @see NetworkInterface#getByInetAddress(InetAddress) */ private static boolean isNetworkInterfaceAddress(InetAddress address) { try { if (NetworkInterface.getByInetAddress(address) != null) { return true; } } catch (SocketException e) { log.warn("Failed to check if an address is from a network interface:", e); } return false; } /** * Tells whether or not the given {@code address} is a public address of the host, when behind NAT. * <p> * Returns {@code false} if the proxy is not behind NAT. * <p> * <strong>Implementation Note:</strong> Only AWS EC2 NAT detection is supported, by requesting the public IP address from * <a href= "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-instance-addressing.html#working-with-ip-addresses"> * AWS EC2 instance's metadata</a>. * * @param address the address that will be checked * @return {@code true} if the address is public address of the host, {@code false} otherwise. * @see ProxyParam#isBehindNat() */ private boolean isOwnPublicAddress(InetAddress address) { if (!proxyParam.isBehindNat()) { return false; } // Support just AWS for now. TransportAddress publicAddress = getAwsCandidateHarvester().getMask(); if (publicAddress == null) { return false; } return Arrays.equals(address.getAddress(), publicAddress.getAddress().getAddress()); } private static AwsCandidateHarvester getAwsCandidateHarvester() { if (awsCandidateHarvester == null) { createAwsCandidateHarvester(); } return awsCandidateHarvester; } private static synchronized void createAwsCandidateHarvester() { if (awsCandidateHarvester == null) { awsCandidateHarvester = new AwsCandidateHarvester(); } } private void removeUnsupportedEncodings(HttpMessage msg) { String encoding = msg.getRequestHeader().getHeader(HttpHeader.ACCEPT_ENCODING); if (encoding == null) { return; } // No encodings supported in practise (HttpResponseBody needs to support them, which it doesn't, yet). msg.getRequestHeader().setHeader(HttpHeader.ACCEPT_ENCODING, null); } protected HttpSender getHttpSender() { if (httpSender == null) { httpSender = new HttpSender(connectionParam, true, HttpSender.PROXY_INITIATOR); } return httpSender; } static boolean isAnyProxyThreadRunning() { return !proxyThreadList.isEmpty(); } protected boolean isProcessCache(HttpMessage msg) throws IOException { if (!parentServer.isEnableCacheProcessing()) { return false; } if (parentServer.getCacheProcessingList().isEmpty()) { return false; } CacheProcessingItem item = parentServer.getCacheProcessingList().get(0); if (msg.equals(item.message)) { HttpMessage newMsg = item.message.cloneAll(); msg.setResponseHeader(newMsg.getResponseHeader()); msg.setResponseBody(newMsg.getResponseBody()); writeHttpResponse(msg, httpOut); return true; } else { try { RecordHistory history = Model.getSingleton().getDb().getTableHistory().getHistoryCache(item.reference, msg); if (history == null) { return false; } msg.setResponseHeader(history.getHttpMessage().getResponseHeader()); msg.setResponseBody(history.getHttpMessage().getResponseBody()); writeHttpResponse(msg, httpOut); // System.out.println("cached:" + msg.getRequestHeader().getURI().toString()); return true; } catch (Exception e) { return true; } } // return false; } }