/* * This file is part of the OWASP Proxy, a free intercepting proxy library. * Copyright (C) 2008-2010 Rogan Dawes <rogan@dawes.za.net> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to: * The Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.owasp.proxy.http.server; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.util.logging.Level; import java.util.logging.Logger; import org.owasp.proxy.daemon.ConnectionHandler; import org.owasp.proxy.daemon.TargetedConnectionHandler; import org.owasp.proxy.http.MessageFormatException; import org.owasp.proxy.http.MessageUtils; import org.owasp.proxy.http.MutableRequestHeader; import org.owasp.proxy.http.RequestHeader; import org.owasp.proxy.http.StreamingRequest; import org.owasp.proxy.http.StreamingResponse; import org.owasp.proxy.io.ChunkedInputStream; import org.owasp.proxy.io.CopyInputStream; import org.owasp.proxy.io.EofNotifyingInputStream; import org.owasp.proxy.io.FixedLengthInputStream; import org.owasp.proxy.model.URI; import org.owasp.proxy.ssl.EncryptedConnectionHandler; import org.owasp.proxy.util.AsciiString; public class HttpProxyConnectionHandler implements ConnectionHandler, TargetedConnectionHandler, EncryptedConnectionHandler { private final static byte[] NO_CONNECT_HEADER = AsciiString .getBytes("HTTP/1.0 503 Service unavailable" + " - CONNECT not supported\r\n\r\n"); private final static byte[] NO_CONNECT_MESSAGE = AsciiString .getBytes("There is no CONNECT handler available"); private final static byte[] ERROR_HEADER = AsciiString .getBytes("HTTP/1.0 500 OWASP Proxy Error\r\n" + "Content-Type: text/html\r\nConnection: close\r\n\r\n"); private final static String ERROR_MESSAGE1 = "<html><head><title>OWASP Proxy Error</title></head>" + "<body><h1>OWASP Proxy Error</h1>" + "OWASP Proxy encountered an error fetching the following request : <br/><pre>"; private final static String ERROR_MESSAGE2 = "</pre><br/>The error was: <br/><pre>"; private final static String ERROR_MESSAGE3 = "</pre></body></html>"; private final static Logger logger = Logger .getLogger(HttpProxyConnectionHandler.class.toString()); private HttpRequestHandler requestHandler; private TargetedConnectionHandler connectHandler = null; static { logger.setLevel(Level.FINE); } public HttpProxyConnectionHandler(HttpRequestHandler requestHandler) { this.requestHandler = requestHandler; } public void setConnectHandler(TargetedConnectionHandler connectHandler) { this.connectHandler = connectHandler; } /* * (non-Javadoc) * * @see org.owasp.proxy.daemon.ConnectionHandler#handleConnection(java.net.Socket ) */ public void handleConnection(Socket socket) throws IOException { handleConnection(socket, null); } /* * (non-Javadoc) * * @see org.owasp.proxy.daemon.ifbased.ConnectionHandler#handleConnection(java .net.Socket, * java.net.InetSocketAddress) */ public void handleConnection(Socket socket, InetSocketAddress target) throws IOException { handleConnection(socket, target, false); } protected StreamingResponse createErrorResponse(StreamingRequest request, Exception e) throws IOException { StringBuilder buff = new StringBuilder(); StreamingResponse response = new StreamingResponse.Impl(); response.setHeader(ERROR_HEADER); buff.append(ERROR_MESSAGE1); buff.append(AsciiString.create(request.getHeader())); buff.append(ERROR_MESSAGE2); StringWriter out = new StringWriter(); e.printStackTrace(new PrintWriter(out)); buff.append(out.getBuffer()); buff.append(ERROR_MESSAGE3); response.setContent(new ByteArrayInputStream(AsciiString.getBytes(buff .toString()))); return response; } private void doConnect(Socket socket, MutableRequestHeader request) throws IOException, GeneralSecurityException, MessageFormatException { String resource = request.getResource(); int colon = resource.indexOf(':'); if (colon == -1) throw new MessageFormatException("Malformed CONNECT line : '" + resource + "'", request.getHeader()); String host = resource.substring(0, colon); if (host.length() == 0) throw new MessageFormatException("Malformed CONNECT line : '" + resource + "'", request.getHeader()); int port; try { port = Integer.parseInt(resource.substring(colon + 1)); } catch (NumberFormatException nfe) { throw new MessageFormatException("Malformed CONNECT line : '" + resource + "'", request.getHeader()); } InetSocketAddress target = new InetSocketAddress(host, port); OutputStream out = socket.getOutputStream(); if (connectHandler == null) { out.write(NO_CONNECT_HEADER); out.write(NO_CONNECT_MESSAGE); out.flush(); } else { out.write("HTTP/1.0 200 Ok\r\n\r\n".getBytes()); out.flush(); // start over from the beginning to handle this // connection as an SSL connection connectHandler.handleConnection(socket, target); } } private StreamingRequest readRequest(InputStream in) throws IOException, MessageFormatException { logger.fine("Entering readRequest()"); // read the whole header. ByteArrayOutputStream copy = new ByteArrayOutputStream(); CopyInputStream cis = new CopyInputStream(in, copy); try { String line; do { line = cis.readLine(); } while (line != null && !"".equals(line)); } catch (IOException e) { byte[] headerBytes = copy.toByteArray(); if (headerBytes == null || headerBytes.length == 0) return null; throw new MessageFormatException("Incomplete request header", e, headerBytes); } byte[] headerBytes = copy.toByteArray(); // empty request line, connection closed? if (headerBytes == null || headerBytes.length == 0) return null; StreamingRequest request = new StreamingRequest.Impl(); request.setHeader(headerBytes); String transferCoding = request.getHeader("Transfer-Encoding"); String contentLength = request.getHeader("Content-Length"); if (transferCoding != null && transferCoding.trim().equalsIgnoreCase("chunked")) { in = new ChunkedInputStream(in, true); // don't unchunk } else if (contentLength != null) { try { in = new FixedLengthInputStream(in, Integer .parseInt(contentLength)); } catch (NumberFormatException nfe) { IOException ioe = new IOException( "Invalid content-length header: " + contentLength); ioe.initCause(nfe); throw ioe; } } else { in = null; } request.setContent(in); return request; } private void extractTargetFromResource(MutableRequestHeader request) throws MessageFormatException { String resource = request.getResource(); try { URI uri = new URI(resource); request.setSsl("https".equals(uri.getScheme())); int port = uri.getPort() > 0 ? uri.getPort() : request.isSsl() ? 443 : 80; request.setTarget(new InetSocketAddress(uri.getHost(), port)); request.setResource(uri.getResource()); } catch (URISyntaxException use) { throw new MessageFormatException( "Couldn't parse resource as a URI", use); } } private void extractTargetFromHost(MutableRequestHeader request) throws MessageFormatException { String host = request.getHeader("Host"); int colon = host.indexOf(':'); if (colon > -1) { try { String h = host.substring(0, colon); int port = Integer.parseInt(host.substring(colon + 1).trim()); request.setTarget(new InetSocketAddress(h, port)); } catch (NumberFormatException nfe) { throw new MessageFormatException( "Couldn't parse target port from Host: header", nfe); } } else { int port = request.isSsl() ? 443 : 80; request.setTarget(new InetSocketAddress(host, port)); } } /* * (non-Javadoc) * * @see org.owasp.proxy.daemon.ifbased.EncryptedConnectionHandler#handleConnection (java.net.Socket, * java.net.InetSocketAddress, boolean) */ public void handleConnection(Socket socket, InetSocketAddress target, boolean ssl) throws IOException { try { InetAddress source = socket.getInetAddress(); InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); boolean close; String version = null, connection = null; final StateHolder holder = new StateHolder(); do { if (!holder.state.equals(State.READY)) throw new IllegalStateException( "Trying to read a new request in state " + holder.state); StreamingRequest request = null; try { request = readRequest(in); holder.state = State.REQUEST_HEADER; } catch (IOException ioe) { logger.info("Error reading request: " + ioe.getMessage()); return; } if (request == null) return; if ("CONNECT".equals(request.getMethod())) { doConnect(socket, request); return; } else if (!request.getResource().startsWith("/")) { extractTargetFromResource(request); } else if (target != null) { request.setTarget(target); request.setSsl(ssl); } else if (request.getHeader("Host") != null) { extractTargetFromHost(request); request.setSsl(ssl); } else { throw new MessageFormatException( "No idea where this request is going!", request .getHeader()); } InputStream requestContent = request.getContent(); if (requestContent != null) { request.setContent(new EofNotifyingInputStream( requestContent) { @Override public void eof() { // all request content has been read holder.state = State.REQUEST_CONTENT; } }); } else { // nonexistent content has been read :-) holder.state = State.REQUEST_CONTENT; } request.setContent(requestContent); StreamingResponse response = null; if (MessageUtils.isExpectContinue(request)) { System.err .println("Expecting a Continue response for " + request.getTarget() + " " + request.getResource()); StreamingRequest cont = new StreamingRequest.Impl(request); try { response = requestHandler.handleRequest(source, cont, false); holder.state = State.RESPONSE_HEADER; } catch (IOException ioe) { response = createErrorResponse(cont, ioe); } if ("100".equals(response.getStatus())) { try { out.write(response.getHeader()); } catch (IOException ioe) { // client gone return; } } } boolean isContinue = response != null && "100".equals(response.getStatus()); if (response == null || isContinue) { try { response = requestHandler.handleRequest(source, request, isContinue); holder.state = State.RESPONSE_HEADER; } catch (IOException ioe) { response = createErrorResponse(request, ioe); } } if (!writeResponse(request, response, out)) return; holder.state = State.READY; version = response.getVersion(); connection = response.getHeader("Connection"); if ("HTTP/1.1".equals(version)) { close = false; } else { close = true; } if ("close".equals(connection)) { close = true; } else if ("Keep-Alive".equalsIgnoreCase(connection)) { close = false; } if (!close && response.getHeader("Transfer-Encoding") == null && response.getHeader("Content-Length") == null) { // Close connection: no T-E or C-L close = true; } } while (!close); } catch (GeneralSecurityException gse) { logger.severe(gse.getMessage()); } catch (IOException ioe) { logger.info(ioe.getMessage()); } catch (MessageFormatException mfe) { logger.info(mfe.getMessage()); mfe.printStackTrace(); if (mfe.getHeader() != null) { logger.info("Header was " + new String(mfe.getHeader())); } logger.info("Target was " + target); } finally { try { requestHandler.dispose(); } catch (IOException ioe) { logger.warning("Error disposing of requestHandler resources: " + ioe.getMessage()); } } } private boolean writeResponse(RequestHeader request, StreamingResponse response, OutputStream out) throws IOException { try { out.write(response.getHeader()); } catch (IOException ioe) { // client gone return false; } InputStream content = null; try { if (!request.getMethod().equalsIgnoreCase("head")) { content = response.getContent(); } } catch (MessageFormatException e) { // ignore } if (content != null) { int count = 0; try { byte[] buff = new byte[4096]; int got; while ((got = content.read(buff)) > -1) { try { out.write(buff, 0, got); count += got; } catch (IOException ioe) { // client gone content.close(); return false; } } out.flush(); } catch (IOException ioe) { // server closed logger.fine("Request was " + request); logger.fine("Incomplete response content because " + ioe.getMessage()); logger.fine("Read " + count + " bytes"); throw ioe; } } return true; } private static class StateHolder { public State state = State.READY; } private enum State { READY, REQUEST_HEADER, REQUEST_CONTENT, RESPONSE_HEADER, RESPONSE_CONTENT } }