/*********************************************************************** * * $CVSHeader$ * * This file is part of WebScarab, an Open Web Application Security * Project utility. For details, please see http://www.owasp.org/ * * Copyright (c) 2002 - 2004 Rogan Dawes * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * 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 * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Getting Source * ============== * * Source for this application is maintained at Sourceforge.net, a * repository for free software projects. * * For details, please see http://www.sourceforge.net/projects/owasp * */ /* * $Id: Proxy.java,v 1.24 2005/05/18 15:23:31 rogan Exp $ */ package org.owasp.webscarab.plugin.proxy; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.TreeMap; import java.util.logging.Logger; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import org.bouncycastle.operator.OperatorCreationException; import org.owasp.webscarab.model.ConversationID; import org.owasp.webscarab.model.HttpUrl; import org.owasp.webscarab.model.Preferences; import org.owasp.webscarab.model.Request; import org.owasp.webscarab.model.Response; import org.owasp.webscarab.model.StoreException; import org.owasp.webscarab.plugin.Framework; import org.owasp.webscarab.plugin.Hook; import org.owasp.webscarab.plugin.Plugin; /** * The Proxy plugin supports multiple Listeners, and starts and stops them as * instructed. All requests and responses are submitted to the model, unless * there is an error while retrieving the response. */ public class Proxy implements Plugin { private boolean _running = false; private Framework _framework = null; private ProxyUI _ui = null; private ArrayList<ProxyPlugin> _plugins = new ArrayList<ProxyPlugin>(); private TreeMap<ListenerSpec, Listener> _listeners = new TreeMap<ListenerSpec, Listener>(); private Logger _logger = Logger.getLogger(getClass().getName()); private String _status = "Stopped"; private int _pending = 0; private static HashMap<String, SSLSocketFactory> _factoryMap = new HashMap<String, SSLSocketFactory>(); private static char[] _keystorepass = "password".toCharArray(); private static char[] _keypassword = "password".toCharArray(); private SSLSocketFactoryFactory _certGenerator = null; private static String _certDir = "./certs/"; private Proxy.ConnectionHook _allowConnection = new ConnectionHook( "Allow connection", "Called when a new connection is received from a browser\n" + "use connection.getAddress() and connection.closeConnection() to decide and react"); private Proxy.ConnectionHook _interceptRequest = new ConnectionHook( "Intercept request", "Called when a new request has been submitted by the browser\n" + "use connection.getRequest() and connection.setRequest(request) to perform changes"); private Proxy.ConnectionHook _interceptResponse = new ConnectionHook( "Intercept response", "Called when the request has been submitted to the server, and the response " + "has been recieved.\n" + "use connection.getResponse() and connection.setResponse(response) to perform changes"); /** * Creates a Proxy Object with a reference to the Framework. Creates (but * does not start) the configured Listeners. * * @param model * The Model to submit requests and responses to */ public Proxy(Framework framework) { _framework = framework; parseListenerConfig(); try { _certGenerator = new SSLSocketFactoryFactory(".keystore", "JKS", "password".toCharArray()); _certGenerator.setReuseKeys(true); } catch (NoClassDefFoundError e) { _certGenerator = null; } catch (GeneralSecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (OperatorCreationException e) { e.printStackTrace(); } } public Hook[] getScriptingHooks() { return new Hook[] { _allowConnection, _interceptRequest, _interceptResponse }; } public Object getScriptableObject() { return null; } /** * called by Listener to determine whether to allow a connection or not */ void allowClientConnection(ScriptableConnection connection) { _allowConnection.runScripts(connection); } /** * called by Connectionhandler via Listener to perform any required * modifications to the Request */ void interceptRequest(ScriptableConnection connection) { _interceptRequest.runScripts(connection); } /** * called by Connectionhandler via Listener to perform any required * modifications to the Response */ void interceptResponse(ScriptableConnection connection) { _interceptResponse.runScripts(connection); } public void setUI(ProxyUI ui) { _ui = ui; if (_ui != null) _ui.setEnabled(_running); } public void addPlugin(ProxyPlugin plugin) { _plugins.add(plugin); } /** * retrieves the named plugin, if it exists * * @param name * the name of the plugin * @return the plugin if it exists, or null */ public ProxyPlugin getPlugin(String name) { ProxyPlugin plugin = null; Iterator<ProxyPlugin> it = _plugins.iterator(); while (it.hasNext()) { plugin = it.next(); if (plugin.getPluginName().equals(name)) return plugin; } return null; } /** * The plugin name * * @return The name of the plugin * */ public String getPluginName() { return new String("Proxy"); } /** * returns a list of keys describing the configured Listeners * * @return the list of keys */ public ListenerSpec[] getProxies() { if (_listeners.size() == 0) { return new ListenerSpec[0]; } return (ListenerSpec[]) _listeners.keySet() .toArray(new ListenerSpec[0]); } /** * called by ConnectionHandler to see which plugins have been configured. * * @return an array of ProxyPlugin's */ protected ProxyPlugin[] getPlugins() { ProxyPlugin[] plugins = new ProxyPlugin[_plugins.size()]; for (int i = 0; i < _plugins.size(); i++) { plugins[i] = _plugins.get(i); } return plugins; } /** * used by the User Interface to start a new proxy listening with the * specified parameters * * @param spec * the details of the Listener * @throws IOException * if there are any problems starting the Listener */ public void addListener(ListenerSpec spec) { createListener(spec); startListener(_listeners.get(spec)); String key = getKey(spec); Preferences.setPreference("Proxy.listener." + key + ".base", spec .getBase() == null ? "" : spec.getBase().toString()); Preferences.setPreference("Proxy.listener." + key + ".primary", spec .isPrimaryProxy() == true ? "yes" : "no"); String value = null; Iterator<ListenerSpec> i = _listeners.keySet().iterator(); while (i.hasNext()) { key = getKey(i.next()); if (value == null) { value = key; } else { value = value + ", " + key; } } Preferences.setPreference("Proxy.listeners", value); } private String getKey(ListenerSpec spec) { return spec.getAddress() + ":" + spec.getPort(); } private void startListener(Listener l) { Thread t = new Thread(l, "Listener-" + getKey(l.getListenerSpec())); t.setDaemon(true); t.start(); if (_ui != null) _ui.proxyStarted(l.getListenerSpec()); } private boolean stopListener(Listener l) { boolean stopped = l.stop(); if (stopped && _ui != null) _ui.proxyStopped(l.getListenerSpec()); return stopped; } /** * Used to stop the referenced listener * * @param key * the Listener to stop * @return true if the proxy was successfully stopped, false otherwise */ public boolean removeListener(ListenerSpec spec) { Listener l = _listeners.get(spec); if (l == null) return false; if (stopListener(l)) { _listeners.remove(spec); if (_ui != null) _ui.proxyRemoved(spec); String key = getKey(spec); Preferences.remove("Proxy.listener." + key + ".base"); Preferences.remove("Proxy.listener." + key + ".simulator"); Preferences.remove("Proxy.listener." + key + ".primary"); String value = null; Iterator<ListenerSpec> i = _listeners.keySet().iterator(); while (i.hasNext()) { key = getKey(i.next()); if (value == null) { value = key; } else { value = value + ", " + key; } } if (value == null) { value = ""; } Preferences.setPreference("Proxy.listeners", value); return true; } else { return false; } } /** * Starts the Listeners */ public void run() { Iterator<ListenerSpec> it = _listeners.keySet().iterator(); while (it.hasNext()) { ListenerSpec spec = it.next(); try { spec.verifyAvailable(); Listener l = _listeners.get(spec); if (l == null) { createListener(spec); l = _listeners.get(spec); } startListener(l); } catch (IOException ioe) { _logger.warning("Unable to start listener " + spec); if (_ui != null) _ui.proxyStartError(spec, ioe); removeListener(spec); } } _running = true; if (_ui != null) _ui.setEnabled(_running); _status = "Started, Idle"; } /** * Stops the Listeners * * @return true if successful, false otherwise */ public boolean stop() { _running = false; Iterator<ListenerSpec> it = _listeners.keySet().iterator(); while (it.hasNext()) { ListenerSpec spec = it.next(); Listener l = _listeners.get(spec); if (l != null && !stopListener(l)) { _logger .severe("Failed to stop Listener-" + l.getListenerSpec()); _running = true; } } if (_ui != null) _ui.setEnabled(_running); _status = "Stopped"; return !_running; } /** * used by ConnectionHandler to notify the Proxy (and any listeners) that it * is handling a particular request * * @param request * the request to log * @return the conversation ID */ protected ConversationID gotRequest(Request request) { ConversationID id = _framework.reserveConversationID(); if (_ui != null) _ui.requested(id, request.getMethod(), request.getURL()); _pending++; _status = "Started, " + _pending + " in progress"; return id; } /** * used by ConnectionHandler to notify the Proxy (and any listeners) that it * has handled a particular request and response, and that it should be * logged and analysed * * @param id * the Conversation ID * @param response * the Response */ protected void gotResponse(ConversationID id, Response response) { if (_ui != null) _ui.received(id, response.getStatusLine()); _framework.addConversation(id, response.getRequest(), response, getPluginName()); _pending--; _status = "Started, " + (_pending > 0 ? (_pending + " in progress") : "Idle"); } protected SSLSocketFactory getSocketFactory(String host, X509Certificate baseCrt) { synchronized (_factoryMap) { // If it has been loaded already, use it if (_factoryMap.containsKey(host)) return (SSLSocketFactory) _factoryMap.get(host); SSLSocketFactory factory; // Check if there is a specific keypair to use File p12 = new File(_certDir + host + ".p12"); factory = loadSocketFactory(p12, host); if (factory != null) { _factoryMap.put(host, factory); return factory; } // See if we can generate one directly factory = generateSocketFactory(host, baseCrt); if (factory != null) { _factoryMap.put(host, factory); return factory; } // Has the default keypair been loaded already? if (_factoryMap.containsKey(null)) { _logger.info("Using default SSL keystore for " + host); return (SSLSocketFactory) _factoryMap.get(null); } // Check for a user-provided "default keypair" p12 = new File(_certDir + "server.p12"); factory = loadSocketFactory(p12, host); if (factory != null) { _factoryMap.put(null, factory); return factory; } // Fall back to the distribution-provided keypair _logger.info("Loading default SSL keystore from internal resource"); InputStream is = getClass().getClassLoader().getResourceAsStream( "server.p12"); if (is == null) { _logger .severe("WebScarab JAR was built without a certificate!"); _logger.severe("SSL Intercept not available!"); return null; } factory = loadSocketFactory(is, "WebScarab JAR"); _factoryMap.put(null, factory); return factory; } } private SSLSocketFactory loadSocketFactory(File p12, String host) { if (p12.exists() && p12.canRead()) { _logger.info("Loading SSL keystore for " + host + " from " + p12); try { InputStream is = new FileInputStream(p12); return loadSocketFactory(is, p12.getPath()); } catch (IOException ioe) { _logger.severe("Error reading from " + p12 + ": " + ioe.getLocalizedMessage()); } } return null; } private SSLSocketFactory loadSocketFactory(InputStream is, String source) { try { KeyManagerFactory kmf = null; SSLContext sslcontext = null; KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(is, _keystorepass); kmf = KeyManagerFactory.getInstance("X509"); kmf.init(ks, _keypassword); sslcontext = SSLContext.getInstance("TLSv1"); sslcontext.init(kmf.getKeyManagers(), null, null); return sslcontext.getSocketFactory(); } catch (IOException ioe) { _logger.info("Error reading SSL keystore from " + source + ": " + ioe.getLocalizedMessage()); } catch (GeneralSecurityException gse) { _logger.info("Error reading SSL keystore from " + source + ": " + gse.getLocalizedMessage()); } return null; } private SSLSocketFactory generateSocketFactory(String host, X509Certificate baseCrt) { if (_certGenerator == null) return null; try { _logger.info("Generating custom SSL keystore for " + host); return _certGenerator.getSocketFactory(host, baseCrt); } catch (IOException ioe) { _logger.info("Error generating custom SSL keystore for " + host + ": " + ioe); } catch (GeneralSecurityException gse) { _logger.info("Error generating custom SSL keystore for " + host + ": " + gse); } catch (OperatorCreationException oce) { _logger.info("Error generating custom SSL keystore for " + host + ": " + oce); } return null; } /** * notifies any observers that the request failed to complete, and the * reason for it * * @param reason * the reason for failure * @param id * the conversation ID */ protected void failedResponse(ConversationID id, String reason) { if (_ui != null) _ui.aborted(id, reason); _pending--; _status = "Started, " + (_pending > 0 ? (_pending + " in progress") : "Idle"); } private void parseListenerConfig() { String prop = "Proxy.listeners"; String value = Preferences.getPreference(prop); if (value == null || value.trim().equals("")) { _logger.warning("No proxies configured!?"); value = "127.0.0.1:8008"; } String[] listeners = value.trim().split(" *,+ *"); String addr; int port = 0; HttpUrl base; boolean primary = false; for (int i = 0; i < listeners.length; i++) { addr = listeners[i].substring(0, listeners[i].indexOf(":")); try { port = Integer.parseInt(listeners[i].substring( listeners[i].indexOf(":") + 1).trim()); } catch (NumberFormatException nfe) { System.err.println("Error parsing port for " + listeners[i] + ", skipping it!"); continue; } prop = "Proxy.listener." + listeners[i] + ".base"; value = Preferences.getPreference(prop, ""); if (value.equals("")) { base = null; } else { try { base = new HttpUrl(value); } catch (MalformedURLException mue) { _logger.severe("Malformed 'base' parameter for listener '" + listeners[i] + "'"); break; } } prop = "Proxy.listener." + listeners[i] + ".primary"; value = Preferences.getPreference(prop, "false"); primary = value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes"); _listeners.put(new ListenerSpec(addr, port, base, primary), null); } } private void createListener(ListenerSpec spec) { Listener l = new Listener(this, spec); _listeners.put(spec, l); if (_ui != null) _ui.proxyAdded(spec); } public void flush() throws StoreException { // we do not run our own store, but our plugins might Iterator<ProxyPlugin> it = _plugins.iterator(); while (it.hasNext()) { ProxyPlugin plugin = it.next(); plugin.flush(); } } public boolean isBusy() { return _pending > 0; } public String getStatus() { return _status; } public boolean isModified() { return false; } public void analyse(ConversationID id, Request request, Response response, String origin) { // we do no analysis } public void setSession(String type, Object store, String session) throws StoreException { // we have no listeners to remove Iterator<ProxyPlugin> it = _plugins.iterator(); while (it.hasNext()) { ProxyPlugin plugin = it.next(); plugin.setSession(type, store, session); } } public boolean isRunning() { return _running; } private class ConnectionHook extends Hook { public ConnectionHook(String name, String description) { super(name, description); } public void runScripts(ScriptableConnection connection) { if (_bsfManager == null) return; synchronized (_bsfManager) { try { _bsfManager.declareBean("connection", connection, connection.getClass()); super.runScripts(); _bsfManager.undeclareBean("connection"); } catch (Exception e) { _logger .severe("Declaring or undeclaring a bean should not throw an exception! " + e); } } } } }