/*
* 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/04/16 i18n
// ZAP: 2011/05/15 Support for exclusions
// ZAP: 2011/11/15 Warn the user if the host is unknown
// ZAP: 2012/03/15 Changed to sort the ProxyListeners. Set the name of the proxy server thread.
// ZAP: 2012/04/25 Added @Override annotation to the appropriate method.
// ZAP: 2012/12/27 Added PersistentConnectionListener list, setter & getter.
// ZAP: 2013/05/02 Re-arranged all modifiers into Java coding standard order
// ZAP: 2014/01/22 Add the possibility to bound the proxy to all interfaces if null IP address has been set
// ZAP: 2014/03/23 Issue 1022: Proxy - Allow to override a proxied message
// ZAP: 2014/08/14 Issue 1312: Misleading error message when unable to bind the local proxy to specified address
// ZAP: 2015/11/04 Issue 1920: Report the host:port ZAP is listening on in daemon mode, or exit if it cant
// ZAP: 2016/05/30 Issue 2494: ZAP Proxy is not showing the HTTP CONNECT Request in history tab
// ZAP: 2016/09/22 JavaDoc tweaks
// ZAP: 2016/11/08 Tweak how exception's message is checked to show a specific error/info message
// ZAP: 2017/03/15 Disable API by default and allow thread name to be set
package org.parosproxy.paros.core.proxy;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.URI;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.network.ConnectionParam;
import org.parosproxy.paros.network.HttpUtil;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.PersistentConnectionListener;
public class ProxyServer implements Runnable {
protected Thread thread = null;
protected static final int PORT_TIME_OUT = 0;
protected ServerSocket proxySocket = null;
protected boolean isProxyRunning = false;
protected ProxyParam proxyParam = new ProxyParam();
protected ConnectionParam connectionParam = new ConnectionParam();
protected Vector<ProxyListener> listenerList = new Vector<>();
protected Vector<OverrideMessageProxyListener> overrideListeners = new Vector<>();
protected Vector<PersistentConnectionListener> persistentConnectionListenerList = new Vector<>();
private final List<ConnectRequestProxyListener> connectRequestProxyListeners;
// ZAP: Added listenersComparator.
private static Comparator<ArrangeableProxyListener> listenersComparator;
protected boolean serialize = false;
protected boolean enableCacheProcessing = false;
protected Vector<CacheProcessingItem> cacheProcessingList = new Vector<>();
private List<Pattern> excludeUrls = null;
private boolean enableApi = false;
private static Logger log = Logger.getLogger(ProxyServer.class);
private String threadName = "ZAP-ProxyServer";
/**
* @return Returns the enableCacheProcessing.
*/
public boolean isEnableCacheProcessing() {
return enableCacheProcessing;
}
/**
* @param enableCacheProcessing The enableCacheProcessing to set.
*/
public void setEnableCacheProcessing(boolean enableCacheProcessing) {
this.enableCacheProcessing = enableCacheProcessing;
if (!enableCacheProcessing) {
cacheProcessingList.clear();
}
}
/**
* @return Returns the serialize.
*/
public boolean isSerialize() {
return serialize;
}
public ProxyServer() {
this(null);
}
public ProxyServer(String threadName) {
connectRequestProxyListeners = new ArrayList<>(1);
if (threadName != null) {
this.threadName = threadName;
}
}
public void setProxyParam(ProxyParam param) {
proxyParam = param;
}
public ProxyParam getProxyParam() {
return proxyParam;
}
public void setConnectionParam(ConnectionParam connection) {
connectionParam = connection;
}
public ConnectionParam getConnectionParam() {
return connectionParam;
}
/**
* Starts the proxy server.
* <p>
* If the proxy server was already running it's stopped first.
*
* @param ip the IP/address the server should bind to
* @param port the port
* @param isDynamicPort {@code true} if it should use another port if the given one is already in use, {@code false}
* otherwise.
* @return the port the server is listening to, or {@code -1} if not able to start
*/
public synchronized int startServer(String ip, int port, boolean isDynamicPort) {
if (isProxyRunning) {
stopServer();
}
isProxyRunning = false;
// ZAP: Set the name of the thread.
thread = new Thread(this, threadName);
thread.setDaemon(true);
// the priority below should be higher than normal to allow fast accept on the server socket
thread.setPriority(Thread.NORM_PRIORITY + 1);
proxySocket = null;
for (int i = 0; i < 20 && proxySocket == null; i++) {
try {
proxySocket = createServerSocket(ip, port);
proxySocket.setSoTimeout(PORT_TIME_OUT);
isProxyRunning = true;
} catch (UnknownHostException e) {
// ZAP: Warn the user if the host is unknown
if (View.isInitialised()) {
View.getSingleton().showWarningDialog(Constant.messages.getString("proxy.error.host.unknow") + " " + ip);
} else {
System.out.println(Constant.messages.getString("proxy.error.host.unknow") + " " + ip);
}
return -1;
} catch (BindException e) {
String message = e.getMessage();
if (message == null || message.isEmpty()) {
handleUnknownException(e);
return -1;
}
if (message.startsWith("Cannot assign requested address")) {
showErrorMessage(Constant.messages.getString("proxy.error.address") + " " + ip);
return -1;
} else if (message.startsWith("Permission denied") || message.startsWith("Address already in use")) {
if (!isDynamicPort) {
showErrorMessage(Constant.messages.getString("proxy.error.port") + " " + ip + ":" + port);
return -1;
} else if (port < 65535) {
port++;
}
} else {
handleUnknownException(e);
return -1;
}
} catch (IOException e) {
handleUnknownException(e);
return -1;
}
}
if (proxySocket == null) {
return -1;
}
thread.start();
return proxySocket.getLocalPort();
}
private static void showErrorMessage(String error) {
if (View.isInitialised()) {
View.getSingleton().showWarningDialog(error);
} else {
log.error(error);
System.out.println(error);
}
}
private static void handleUnknownException(Exception e) {
log.error("Failed to start the proxy server: ", e);
showErrorMessage(Constant.messages.getString("proxy.error.generic") + e.getLocalizedMessage());
}
/**
* Stops the proxy server.
*
* @return {@code true} if the proxy server was stopped, {@code false} if it was not running.
*/
public synchronized boolean stopServer() {
if (!isProxyRunning) {
return false;
}
isProxyRunning = false;
HttpUtil.closeServerSocket(proxySocket);
try {
thread.join(); //(PORT_TIME_OUT);
} catch (Exception e) {
}
proxySocket = null;
return true;
}
@Override
public void run() {
Socket clientSocket;
ProxyThread process;
while (isProxyRunning) {
try {
clientSocket = proxySocket.accept();
process = createProxyProcess(clientSocket);
process.start();
} catch (SocketTimeoutException e) {
// nothing, socket time reached only.
} catch (IOException e) {
// unknown IO exception - continue but with delay to avoid eating up CPU time if continue
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
}
}
}
}
protected ServerSocket createServerSocket(String ip, int port) throws UnknownHostException, IOException {
// ServerSocket socket = new ServerSocket(port, 300, InetAddress.getByName(ip)getProxyParam().getProxyIp()));
//
// ZAP: added the possibility to bound to all interfaces (using null as InetAddress)
// when the ip is null or an empty string
InetAddress addr = null;
if ((ip != null) && !ip.isEmpty()) {
addr = InetAddress.getByName(ip);
}
ServerSocket socket = new ServerSocket(port, 400, addr);
return socket;
}
protected ProxyThread createProxyProcess(Socket clientSocket) {
ProxyThread process = new ProxyThread(this, clientSocket);
return process;
}
protected void writeOutput(String s) {
}
public void addProxyListener(ProxyListener listener) {
listenerList.add(listener);
// ZAP: Added to sort the listeners.
Collections.sort(listenerList, getListenersComparator());
}
public void removeProxyListener(ProxyListener listener) {
listenerList.remove(listener);
}
synchronized List<ProxyListener> getListenerList() {
return listenerList;
}
public void addPersistentConnectionListener(PersistentConnectionListener listener) {
persistentConnectionListenerList.add(listener);
Collections.sort(persistentConnectionListenerList, getListenersComparator());
}
public void removePersistentConnectionListener(PersistentConnectionListener listener) {
persistentConnectionListenerList.remove(listener);
}
synchronized List<PersistentConnectionListener> getPersistentConnectionListenerList() {
return persistentConnectionListenerList;
}
public void addOverrideMessageProxyListener(OverrideMessageProxyListener listener) {
overrideListeners.add(listener);
Collections.sort(overrideListeners, getListenersComparator());
}
public void removeOverrideMessageProxyListener(OverrideMessageProxyListener listener) {
overrideListeners.remove(listener);
}
List<OverrideMessageProxyListener> getOverrideMessageProxyListeners() {
return overrideListeners;
}
/**
* Adds the given {@code listener}, that will be notified of the received CONNECT requests.
*
* @param listener the listener that will be added
* @throws IllegalArgumentException if the given {@code listener} is {@code null}.
* @since 2.5.0
*/
public void addConnectRequestProxyListener(ConnectRequestProxyListener listener) {
connectRequestProxyListeners.add(listener);
}
/**
* Validates that the given {@code listener} is not {@code null}, throwing an {@code IllegalArgumentException} if it is.
*
* @param listener the listener that will be validated
* @throws IllegalArgumentException if the given {@code listener} is {@code null}.
*/
private static void validateListenerNotNull(Object listener) {
if (listener == null) {
throw new IllegalArgumentException("Parameter listener must not be null.");
}
}
/**
* Removes the given {@code listener}, to no longer be notified of the received CONNECT requests.
*
* @param listener the listener that should be removed
* @throws IllegalArgumentException if the given {@code listener} is {@code null}.
* @since 2.5.0
*/
public void removeConnectRequestProxyListener(ConnectRequestProxyListener listener) {
validateListenerNotNull(listener);
connectRequestProxyListeners.remove(listener);
}
/**
* Gets the {@code ConnectRequestProxyListener}s added.
*
* @return an unmodifiable {@code List} with the {@code ConnectRequestProxyListener}s, never {@code null}
* @since 2.5.0
*/
List<ConnectRequestProxyListener> getConnectRequestProxyListeners() {
return Collections.unmodifiableList(connectRequestProxyListeners);
}
public boolean isAnyProxyThreadRunning() {
return ProxyThread.isAnyProxyThreadRunning();
}
/**
* @param serialize The serialize to set.
*/
public void setSerialize(boolean serialize) {
this.serialize = serialize;
}
public void addCacheProcessingList(CacheProcessingItem item) {
cacheProcessingList.add(item);
}
Vector<CacheProcessingItem> getCacheProcessingList() {
return cacheProcessingList;
}
public void setExcludeList(List<String> urls) {
excludeUrls = new ArrayList<>(urls.size());
for (String url : urls) {
Pattern p = Pattern.compile(url, Pattern.CASE_INSENSITIVE);
excludeUrls.add(p);
}
}
public boolean excludeUrl(URI uri) {
boolean ignore = false;
if (excludeUrls != null) {
String uriString = uri.toString();
for (Pattern p : excludeUrls) {
if (p.matcher(uriString).matches()) {
ignore = true;
if (log.isDebugEnabled()) {
log.debug("URL excluded: " + uriString + " Regex: " + p.pattern());
}
break;
}
}
}
return ignore;
}
// ZAP: Added the method.
private Comparator<ArrangeableProxyListener> getListenersComparator() {
if (listenersComparator == null) {
createListenersComparator();
}
return listenersComparator;
}
// ZAP: Added the method.
private synchronized void createListenersComparator() {
if (listenersComparator == null) {
listenersComparator = new Comparator<ArrangeableProxyListener>() {
@Override
public int compare(ArrangeableProxyListener o1, ArrangeableProxyListener o2) {
int order1 = o1.getArrangeableListenerOrder();
int order2 = o2.getArrangeableListenerOrder();
if (order1 < order2) {
return -1;
} else if (order1 > order2) {
return 1;
}
return 0;
}
};
}
}
public void setEnableApi(boolean enableApi) {
this.enableApi = enableApi;
}
public boolean isEnableApi() {
return enableApi;
}
}