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

org.littleshoot.proxy.HttpRelayingHandler Maven / Gradle / Ivy

package org.littleshoot.proxy;

import java.util.LinkedList;
import java.util.Queue;

import org.apache.commons.lang.StringUtils;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that simply relays traffic from a remote server the proxy is 
 * connected to back to the browser.
 */
public class HttpRelayingHandler extends SimpleChannelUpstreamHandler {
    
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    private volatile boolean readingChunks;
    
    private final Channel browserToProxyChannel;

    private final ChannelGroup channelGroup;

    private final HttpFilter httpFilter;

    private HttpResponse originalHttpResponse;

    /**
     * The current, most recent HTTP request we're processing. This changes
     * as multiple requests come in on the same persistent HTTP 1.1 connection.
     */
    private HttpRequest currentHttpRequest;

    private final RelayListener relayListener;

    private final String hostAndPort;

    private boolean closeEndsResponseBody;

    /**
     * Creates a new {@link HttpRelayingHandler} with the specified connection
     * to the browser.
     * 
     * @param browserToProxyChannel The browser connection.
     * @param channelGroup Keeps track of channels to close on shutdown.
     * @param hostAndPort Host and port we're relaying to.
     */
    public HttpRelayingHandler(final Channel browserToProxyChannel, 
        final ChannelGroup channelGroup, 
        final RelayListener relayListener, final String hostAndPort) {
        this (browserToProxyChannel, channelGroup, new NoOpHttpFilter(),
            relayListener, hostAndPort);
    }
    
    /**
     * Creates a new {@link HttpRelayingHandler} with the specified connection
     * to the browser.
     * 
     * @param browserToProxyChannel The browser connection.
     * @param channelGroup Keeps track of channels to close on shutdown.
     * @param filter The HTTP filter.
     * @param hostAndPort Host and port we're relaying to.
     */
    public HttpRelayingHandler(final Channel browserToProxyChannel,
        final ChannelGroup channelGroup, final HttpFilter filter,
        final RelayListener relayListener, final String hostAndPort) {
        this.browserToProxyChannel = browserToProxyChannel;
        this.channelGroup = channelGroup;
        this.httpFilter = filter;
        this.relayListener = relayListener;
        this.hostAndPort = hostAndPort;
    }

    @Override
    public void messageReceived(final ChannelHandlerContext ctx, 
        final MessageEvent me) throws Exception {
        
        final Object messageToWrite;
        
        // This boolean is a flag for whether or not to write a closing, empty
        // "end" buffer after writing the response. We need to do this to 
        // handle the way Netty creates HttpChunks from responses that aren't 
        // in fact chunked from the remote server using 
        // Transfer-Encoding: chunked. Netty turns these into pseudo-chunked
        // responses in cases where the response would otherwise fill up too
        // much memory or where the length of the response body is unknown.
        // This is handy because it means we can start streaming response
        // bodies back to the browser without reading the entire response.
        // The problem is that in these pseudo-cases the last chunk is encoded
        // to null, and this thwarts normal ChannelFutures from propagating
        // operationComplete events on writes to appropriate channel listeners.
        // We work around this by writing an empty buffer in those cases and
        // using the empty buffer's future instead to handle any operations 
        // we need to when responses are fully written back to clients.
        final boolean writeEndBuffer;
        
        if (!readingChunks) {
            final HttpResponse hr = (HttpResponse) me.getMessage();
            log.info("Received raw response: {}", hr);
            
            // We need to make a copy here because the response will be 
            // modified in various ways before we need to do things like
            // analyze response headers for whether or not to close the 
            // connection (which may not happen for awhile for large, chunked
            // responses, for example).
            originalHttpResponse = ProxyUtils.copyMutableResponseFields(hr, 
                new DefaultHttpResponse(hr.getProtocolVersion(), hr.getStatus()));
            final HttpResponse response;
            
            // Double check the Transfer-Encoding, since it gets tricky.
            final String te = hr.getHeader(HttpHeaders.Names.TRANSFER_ENCODING);
            if (StringUtils.isNotBlank(te) && 
                te.equalsIgnoreCase(HttpHeaders.Values.CHUNKED)) {
                if (hr.getProtocolVersion() != HttpVersion.HTTP_1_1) {
                    log.warn("Fixing HTTP version.");
                    response = ProxyUtils.copyMutableResponseFields(hr, 
                        new DefaultHttpResponse(HttpVersion.HTTP_1_1, hr.getStatus()));
                    if (!response.containsHeader(HttpHeaders.Names.TRANSFER_ENCODING)) {
                        log.info("Adding chunked encoding header");
                        response.addHeader(HttpHeaders.Names.TRANSFER_ENCODING, 
                            HttpHeaders.Values.CHUNKED);
                    }
                }
                else {
                    response = hr;
                }
            }
            else {
                response = hr;
            }

            if (response.isChunked()) {
                log.info("Starting to read chunks");
                readingChunks = true;
                writeEndBuffer = false;
            }
            else {
                writeEndBuffer = true;
            }
            
            final HttpResponse filtered = 
                this.httpFilter.filterResponse(response);
            messageToWrite = filtered;
            
            // An HTTP response is associated with a single request, so we
            // can pop the correct request off the queue.
            // 
            // TODO: I'm a little unclear as to when the request queue would
            // ever actually be empty, but it is from time to time in practice.
            // We've seen this particularly when behind proxies that govern
            // access control on local networks, likely related to redirects.
            if (!this.requestQueue.isEmpty()) {
                this.currentHttpRequest = this.requestQueue.remove();
                if (this.currentHttpRequest == null) {
                    log.warn("Got null HTTP request object.");
                }
            } else {
                log.info("Request queue is empty!");
            }
        } else {
            log.info("Processing a chunk");
            final HttpChunk chunk = (HttpChunk) me.getMessage();
            if (chunk.isLast()) {
                readingChunks = false;
                writeEndBuffer = true;
            }
            else {
                writeEndBuffer = false;
            }
            messageToWrite = chunk;
        }
        
        if (browserToProxyChannel.isConnected()) {
            // We need to determine whether or not to close connections based
            // on the HTTP request and response *before* the response has 
            // been modified for sending to the browser.
            final boolean closeRemote = 
                shouldCloseRemoteConnection(this.currentHttpRequest, 
                    originalHttpResponse, messageToWrite);
            final boolean closePending =
                shouldCloseBrowserConnection(this.currentHttpRequest, 
                    originalHttpResponse, messageToWrite);
            
            final boolean wroteFullResponse =
                wroteFullResponse(originalHttpResponse, messageToWrite);
            
            if (closeRemote && closeEndsResponseBody(originalHttpResponse)) {
                this.closeEndsResponseBody = true;
            }
                
            ChannelFuture future = 
                this.browserToProxyChannel.write(
                    new ProxyHttpResponse(this.currentHttpRequest, originalHttpResponse, 
                        messageToWrite));

            if (writeEndBuffer) {
                // See the comment on this flag variable above.
                future = browserToProxyChannel.write(ChannelBuffers.EMPTY_BUFFER);
            }
            
            // If we've written the full response, we need to notify the 
            // request handler. This is because sometimes the remote server
            // will signify the end of an HTTP response body through closing
            // the connection. Each incoming client connection can spawn 
            // multiple external server connections, however, so each external 
            // connection close can't necessarily be propagated to result
            // in closing the client connection. In fact, that should only 
            // happen if we're received responses to all outgoing requests or
            // all other external connections are already closed. We notify
            // the request handler of complete HTTP responses here to allow
            // it to adhere to that logic.
            if (wroteFullResponse) {
                log.debug("Notifying request handler of completed response.");
                future.addListener(new ChannelFutureListener() {
                    public void operationComplete(final ChannelFuture cf) 
                        throws Exception {
                        relayListener.onRelayHttpResponse(browserToProxyChannel, 
                            hostAndPort, currentHttpRequest);
                    }
                });
            }
            if (closeRemote) {
                log.debug("Closing remote connection after writing to browser");
                
                
                // We close after the future has completed to make sure that
                // all the response data is written to the browser -- 
                // closing immediately could trigger a close to the browser 
                // as well before all the data has been written. Note that
                // in many cases a call to close the remote connection will
                // ultimately result in the connection to the browser closing,
                // particularly when there are no more remote connections
                // associated with that browser connection.
                future.addListener(new ChannelFutureListener() {
                    public void operationComplete(final ChannelFuture cf) 
                        throws Exception {
                        if (me.getChannel().isConnected()) {
                            me.getChannel().close();
                        }
                    }
                });
            }
            
            if (closePending) {
                log.debug("Closing connection to browser after writes");
                future.addListener(new ChannelFutureListener() {
                    public void operationComplete(final ChannelFuture cf) 
                        throws Exception {
                        log.info("Closing browser connection on flush!!");
                        ProxyUtils.closeOnFlush(browserToProxyChannel);
                    }
                });
            }
            if (wroteFullResponse && (!closePending && !closeRemote)) {
                log.debug("Making remote channel available for requests");
                this.relayListener.onChannelAvailable(hostAndPort,
                    Channels.succeededFuture(me.getChannel()));
            }
        }
        else {
            log.debug("Channel not open. Connected? {}", 
                browserToProxyChannel.isConnected());
            // This will undoubtedly happen anyway, but just in case.
            if (me.getChannel().isConnected()) {
                log.warn("Closing channel to remote server -- received a " +
                    "response after the browser connection is closed?");
                me.getChannel().close();
            }
        }
        log.info("Finished processing message");
    }
    
    private boolean closeEndsResponseBody(final HttpResponse res) {
        final String cl = res.getHeader(HttpHeaders.Names.CONTENT_LENGTH);
        if (StringUtils.isNotBlank(cl)) {
            return false;
        }
        final String te = res.getHeader(HttpHeaders.Names.TRANSFER_ENCODING);
        if (StringUtils.isNotBlank(te) && 
            te.equalsIgnoreCase(HttpHeaders.Values.CHUNKED))  {
            return false;
        }
        return true;
    }

    private boolean wroteFullResponse(final HttpResponse res, 
        final Object messageToWrite) {
        // Thanks to Emil Goicovici for identifying a bug in the initial
        // logic for this.
        if (res.isChunked()) {
            if (messageToWrite instanceof HttpResponse) {
                
                return false;
            }
            return ProxyUtils.isLastChunk(messageToWrite);
        }
        return true;
    }

    private boolean shouldCloseBrowserConnection(final HttpRequest req, 
        final HttpResponse res, final Object msg) {
        if (res.isChunked()) {
            // If the response is chunked, we want to return unless it's
            // the last chunk. If it is the last chunk, then we want to pass
            // through to the same close semantics we'd otherwise use.
            if (msg != null) {
                if (!ProxyUtils.isLastChunk(msg)) {
                    log.info("Not closing on middle chunk for {}", req.getUri());
                    return false;
                }
                else {
                    log.info("Last chunk...using normal closing rules");
                }
            }
        }
        
        // Switch the de-facto standard "Proxy-Connection" header to 
        // "Connection" when we pass it along to the remote host.
        final String proxyConnectionKey = "Proxy-Connection";
        if (req.containsHeader(proxyConnectionKey)) {
            final String header = req.getHeader(proxyConnectionKey);
            req.removeHeader(proxyConnectionKey);
            if (req.getProtocolVersion() == HttpVersion.HTTP_1_1) {
                log.info("Switching Proxy-Connection to Connection for " +
                    "analyzing request for close");
                req.setHeader("Connection", header);
            }
        }
        
        if (!HttpHeaders.isKeepAlive(req)) {
            log.info("Closing since request is not keep alive:");
            // Here we simply want to close the connection because the 
            // browser itself has requested it be closed in the request.
            return true;
        }
        log.info("Not closing browser/client to proxy connection " +
            "for request: {}", req);
        return false;
    }

    /**
     * Determines if the remote connection should be closed based on the 
     * request and response pair. If the request is HTTP 1.0 with no 
     * keep-alive header, for example, the connection should be closed.
     * 
     * This in part determines if we should close the connection. Here's the  
     * relevant section of RFC 2616:
     * 
     * "HTTP/1.1 defines the "close" connection option for the sender to 
     * signal that the connection will be closed after completion of the 
     * response. For example,
     * 
     * Connection: close
     * 
     * in either the request or the response header fields indicates that the 
     * connection SHOULD NOT be considered `persistent' (section 8.1) after 
     * the current request/response is complete."
     */
    private boolean shouldCloseRemoteConnection(final HttpRequest req, 
        final HttpResponse res, final Object msg) {
        if (res.isChunked()) {
            // If the response is chunked, we want to return unless it's
            // the last chunk. If it is the last chunk, then we want to pass
            // through to the same close semantics we'd otherwise use.
            if (msg != null) {
                if (!ProxyUtils.isLastChunk(msg)) {
                    log.info("Not closing on middle chunk");
                    return false;
                }
                else {
                    log.info("Last chunk...using normal closing rules");
                }
            }
        }
        if (!HttpHeaders.isKeepAlive(req)) {
            log.info("Closing since request is not keep alive:{}, ", req);
            // Here we simply want to close the connection because the 
            // browser itself has requested it be closed in the request.
            return true;
        }
        if (!HttpHeaders.isKeepAlive(res)) {
            log.info("Closing since response is not keep alive:{}", res);
            // In this case, we want to honor the Connection: close header 
            // from the remote server and close that connection. We don't
            // necessarily want to close the connection to the browser, however
            // as it's possible it has other connections open.
            return true;
        }
        log.info("Not closing -- response probably keep alive for:\n{}", res);
        return false;
    }
    
    @Override
    public void channelOpen(final ChannelHandlerContext ctx, 
        final ChannelStateEvent cse) throws Exception {
        final Channel ch = cse.getChannel();
        log.info("New channel opened from proxy to web: {}", ch);
        if (this.channelGroup != null) {
            this.channelGroup.add(ch);
        }
    }

    @Override
    public void channelClosed(final ChannelHandlerContext ctx, 
        final ChannelStateEvent e) throws Exception {
        log.info("Got closed event on proxy -> web connection: {}",
            e.getChannel());
        
        // We shouldn't close the connection to the browser 
        // here, as there can be multiple connections to external sites for
        // a single connection from the browser.
        final int unansweredRequests = this.requestQueue.size();
        log.info("Unanswered requests: {}", unansweredRequests);
        this.relayListener.onRelayChannelClose(browserToProxyChannel, 
            this.hostAndPort, unansweredRequests, this.closeEndsResponseBody);
    }

    @Override
    public void exceptionCaught(final ChannelHandlerContext ctx, 
        final ExceptionEvent e) throws Exception {
        log.warn("Caught exception on proxy -> web connection: "+
            e.getChannel(), e.getCause());
        if (e.getChannel().isConnected()) {
            log.warn("Closing open connection");
            ProxyUtils.closeOnFlush(e.getChannel());
        }
        // This can happen if we couldn't make the initial connection due
        // to something like an unresolved address, for example, or a timeout.
        // There will not have been be any requests written on an unopened
        // connection, so there should not be any further action to take here.
    }

    private final Queue requestQueue = 
        new LinkedList();
    
    /**
     * Adds this HTTP request. We need to keep track of all encoded requests
     * because we ultimately need the request data to determine whether or not
     * we can cache responses. It's a queue because we're dealing with HTTP 1.1
     * persistent connections, and we need to match all requests with responses.
     * 
     * NOTE that this is the original, unmodified request in this case without
     * hop-by-hop headers stripped and without HTTP request filters applied.
     * It's the raw request we received from the client connection.
     * 
     * See ProxyHttpRequestEncoder.
     * 
     * @param request The HTTP request to add.
     */
    public void requestEncoded(final HttpRequest request) {
        this.requestQueue.add(request);
    }
    
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy