View Javadoc

    1   /*
    2    *  Licensed to the Apache Software Foundation (ASF) under one
    3    *  or more contributor license agreements.  See the NOTICE file
    4    *  distributed with this work for additional information
    5    *  regarding copyright ownership.  The ASF licenses this file
    6    *  to you under the Apache License, Version 2.0 (the
    7    *  "License"); you may not use this file except in compliance
    8    *  with the License.  You may obtain a copy of the License at
    9    *
    10   *    http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   *  Unless required by applicable law or agreed to in writing,
    13   *  software distributed under the License is distributed on an
    14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   *  KIND, either express or implied.  See the License for the
    16   *  specific language governing permissions and limitations
    17   *  under the License.
    18   *
    19   */
    20  package org.apache.mina.proxy.handlers.http;
    21
    22  import java.io.UnsupportedEncodingException;
    23  import java.util.HashMap;
    24  import java.util.List;
    25  import java.util.Map;
    26
    27  import org.apache.mina.core.buffer.IoBuffer;
    28  import org.apache.mina.core.filterchain.IoFilter.NextFilter;
    29  import org.apache.mina.core.future.ConnectFuture;
    30  import org.apache.mina.core.future.IoFutureListener;
    31  import org.apache.mina.core.session.IoSession;
    32  import org.apache.mina.core.session.IoSessionInitializer;
    33  import org.apache.mina.proxy.AbstractProxyLogicHandler;
    34  import org.apache.mina.proxy.ProxyAuthException;
    35  import org.apache.mina.proxy.session.ProxyIoSession;
    36  import org.apache.mina.proxy.utils.IoBufferDecoder;
    37  import org.apache.mina.proxy.utils.StringUtilities;
    38  import org.slf4j.Logger;
    39  import org.slf4j.LoggerFactory;
    40
    41  /**
    42   * AbstractHttpLogicHandler.java - Base class for HTTP proxy {@link AbstractProxyLogicHandler} implementations.
    43   * Provides HTTP request encoding/response decoding functionality.
    44   *
    45   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
    46   * @since MINA 2.0.0-M3
    47   */
    48  public abstract class AbstractHttpLogicHandler extends AbstractProxyLogicHandler {
    49      private final static Logger LOGGER = LoggerFactory.getLogger(AbstractHttpLogicHandler.class);
    50
    51      private final static String DECODER = AbstractHttpLogicHandler.class.getName() + ".Decoder";
    52
    53      private final static byte[] HTTP_DELIMITER = new byte[] { '\r', '\n', '\r', '\n' };
    54
    55      private final static byte[] CRLF_DELIMITER = new byte[] { '\r', '\n' };
    56
    57      // Parsing vars
    58
    59      /**
    60       * Temporary buffer to accumulate the HTTP response from the proxy.
    61       */
    62      private IoBuffer responseData = null;
    63
    64      /**
    65       * The parsed http proxy response
    66       */
    67      private HttpProxyResponse parsedResponse = null;
    68
    69      /**
    70       * The content length of the proxy response.
    71       */
    72      private int contentLength = -1;
    73
    74      // HTTP/1.1 vars
    75
    76      /**
    77       * A flag that indicates that this is a HTTP/1.1 response with chunked data.and that some chunks are missing.
    78       */
    79      private boolean hasChunkedData;
    80
    81      /**
    82       * A flag that indicates that some chunks of data are missing to complete the HTTP/1.1 response.
    83       */
    84      private boolean waitingChunkedData;
    85
    86      /**
    87       * A flag that indicates that chunked data has been read and that we're now reading the footers.
    88       */
    89      private boolean waitingFooters;
    90
    91      /**
    92       * Contains the position of the entity body start in the <code>responseData</code> {@link IoBuffer}.
    93       */
    94      private int entityBodyStartPosition;
    95
    96      /**
    97       * Contains the limit of the entity body start in the <code>responseData</code> {@link IoBuffer}.
    98       */
    99      private int entityBodyLimitPosition;
    100
    101     /**
    102      * Creates a new {@link AbstractHttpLogicHandler}.
    103      *
    104      * @param proxyIoSession the {@link ProxyIoSession} in use.
    105      * @param request the requested url to negotiate with the proxy.
    106      */
    107     public AbstractHttpLogicHandler(final ProxyIoSession proxyIoSession) {
    108         super(proxyIoSession);
    109     }
    110
    111     /**
    112      * Handles incoming data during the handshake process. Should consume only the
    113      * handshake data from the buffer, leaving any extra data in place.
    114      *
    115      * @param nextFilter the next filter
    116      * @param buf the buffer holding received data
    117      */
    118     public synchronized void messageReceived(final NextFilter nextFilter, final IoBuffer buf) throws ProxyAuthException {
    119         LOGGER.debug(" messageReceived()");
    120
    121         IoBufferDecoder decoder = (IoBufferDecoder) getSession().getAttribute(DECODER);
    122         if (decoder == null) {
    123             decoder = new IoBufferDecoder(HTTP_DELIMITER);
    124             getSession().setAttribute(DECODER, decoder);
    125         }
    126
    127         try {
    128             if (parsedResponse == null) {
    129
    130                 responseData = decoder.decodeFully(buf);
    131                 if (responseData == null) {
    132                     return;
    133                 }
    134
    135                 // Handle the response
    136                 String responseHeader = responseData.getString(getProxyIoSession().getCharset().newDecoder());
    137                 entityBodyStartPosition = responseData.position();
    138
    139                 LOGGER.debug("  response header received:\n{}",
    140                         responseHeader.replace("\r", "\\r").replace("\n", "\\n\n"));
    141
    142                 // Parse the response
    143                 parsedResponse = decodeResponse(responseHeader);
    144
    145                 // Is handshake complete ?
    146                 if (parsedResponse.getStatusCode() == 200
    147                         || (parsedResponse.getStatusCode() >= 300 && parsedResponse.getStatusCode() <= 307)) {
    148                     buf.position(0);
    149                     setHandshakeComplete();
    150                     return;
    151                 }
    152
    153                 String contentLengthHeader = StringUtilities.getSingleValuedHeader(parsedResponse.getHeaders(),
    154                         "Content-Length");
    155
    156                 if (contentLengthHeader == null) {
    157                     contentLength = 0;
    158                 } else {
    159                     contentLength = Integer.parseInt(contentLengthHeader.trim());
    160                     decoder.setContentLength(contentLength, true);
    161                 }
    162             }
    163
    164             if (!hasChunkedData) {
    165                 if (contentLength > 0) {
    166                     IoBuffer tmp = decoder.decodeFully(buf);
    167                     if (tmp == null) {
    168                         return;
    169                     }
    170                     responseData.setAutoExpand(true);
    171                     responseData.put(tmp);
    172                     contentLength = 0;
    173                 }
    174
    175                 if ("chunked".equalsIgnoreCase(StringUtilities.getSingleValuedHeader(parsedResponse.getHeaders(),
    176                         "Transfer-Encoding"))) {
    177                     // Handle Transfer-Encoding: Chunked
    178                     LOGGER.debug("Retrieving additional http response chunks");
    179                     hasChunkedData = true;
    180                     waitingChunkedData = true;
    181                 }
    182             }
    183
    184             if (hasChunkedData) {
    185                 // Read chunks
    186                 while (waitingChunkedData) {
    187                     if (contentLength == 0) {
    188                         decoder.setDelimiter(CRLF_DELIMITER, false);
    189                         IoBuffer tmp = decoder.decodeFully(buf);
    190                         if (tmp == null) {
    191                             return;
    192                         }
    193
    194                         String chunkSize = tmp.getString(getProxyIoSession().getCharset().newDecoder());
    195                         int pos = chunkSize.indexOf(';');
    196                         if (pos >= 0) {
    197                             chunkSize = chunkSize.substring(0, pos);
    198                         } else {
    199                             chunkSize = chunkSize.substring(0, chunkSize.length() - 2);
    200                         }
    201                         contentLength = Integer.decode("0x" + chunkSize);
    202                         if (contentLength > 0) {
    203                             contentLength += 2; // also read chunk's trailing CRLF
    204                             decoder.setContentLength(contentLength, true);
    205                         }
    206                     }
    207
    208                     if (contentLength == 0) {
    209                         waitingChunkedData = false;
    210                         waitingFooters = true;
    211                         entityBodyLimitPosition = responseData.position();
    212                         break;
    213                     }
    214
    215                     IoBuffer tmp = decoder.decodeFully(buf);
    216                     if (tmp == null) {
    217                         return;
    218                     }
    219                     contentLength = 0;
    220                     responseData.put(tmp);
    221                     buf.position(buf.position());
    222                 }
    223
    224                 // Read footers
    225                 while (waitingFooters) {
    226                     decoder.setDelimiter(CRLF_DELIMITER, false);
    227                     IoBuffer tmp = decoder.decodeFully(buf);
    228                     if (tmp == null) {
    229                         return;
    230                     }
    231
    232                     if (tmp.remaining() == 2) {
    233                         waitingFooters = false;
    234                         break;
    235                     }
    236
    237                     // add footer to headers
    238                     String footer = tmp.getString(getProxyIoSession().getCharset().newDecoder());
    239                     String[] f = footer.split(":\\s?", 2);
    240                     StringUtilities.addValueToHeader(parsedResponse.getHeaders(), f[0], f[1], false);
    241                     responseData.put(tmp);
    242                     responseData.put(CRLF_DELIMITER);
    243                 }
    244             }
    245
    246             responseData.flip();
    247
    248             LOGGER.debug("  end of response received:\n{}",
    249                     responseData.getString(getProxyIoSession().getCharset().newDecoder()));
    250
    251             // Retrieve entity body content
    252             responseData.position(entityBodyStartPosition);
    253             responseData.limit(entityBodyLimitPosition);
    254             parsedResponse.setBody(responseData.getString(getProxyIoSession().getCharset().newDecoder()));
    255
    256             // Free the response buffer
    257             responseData.free();
    258             responseData = null;
    259
    260             handleResponse(parsedResponse);
    261
    262             parsedResponse = null;
    263             hasChunkedData = false;
    264             contentLength = -1;
    265             decoder.setDelimiter(HTTP_DELIMITER, true);
    266
    267             if (!isHandshakeComplete()) {
    268                 doHandshake(nextFilter);
    269             }
    270         } catch (Exception ex) {
    271             if (ex instanceof ProxyAuthException) {
    272                 throw ((ProxyAuthException) ex);
    273             }
    274
    275             throw new ProxyAuthException("Handshake failed", ex);
    276         }
    277     }
    278
    279     /**
    280      * Handles a HTTP response from the proxy server.
    281      *
    282      * @param response The response.
    283      */
    284     public abstract void handleResponse(final HttpProxyResponse response) throws ProxyAuthException;
    285
    286     /**
    287      * Calls{@link #writeRequest0(NextFilter, HttpProxyRequest)} to write the request.
    288      * If needed a reconnection to the proxy is done previously.
    289      *
    290      * @param nextFilter the next filter
    291      * @param request the http request
    292      */
    293     public void writeRequest(final NextFilter nextFilter, final HttpProxyRequest request) {
    294         ProxyIoSession proxyIoSession = getProxyIoSession();
    295
    296         if (proxyIoSession.isReconnectionNeeded()) {
    297             reconnect(nextFilter, request);
    298         } else {
    299             writeRequest0(nextFilter, request);
    300         }
    301     }
    302
    303     /**
    304      * Encodes a HTTP request and sends it to the proxy server.
    305      *
    306      * @param nextFilter the next filter
    307      * @param request the http request
    308      */
    309     private void writeRequest0(final NextFilter nextFilter, final HttpProxyRequest request) {
    310         try {
    311             String data = request.toHttpString();
    312             IoBuffer buf = IoBuffer.wrap(data.getBytes(getProxyIoSession().getCharsetName()));
    313
    314             LOGGER.debug("   write:\n{}", data.replace("\r", "\\r").replace("\n", "\\n\n"));
    315
    316             writeData(nextFilter, buf);
    317
    318         } catch (UnsupportedEncodingException ex) {
    319             closeSession("Unable to send HTTP request: ", ex);
    320         }
    321     }
    322
    323     /**
    324      * Method to reconnect to the proxy when it decides not to maintain the connection
    325      * during handshake.
    326      *
    327      * @param nextFilter the next filter
    328      * @param request the http request
    329      */
    330     private void reconnect(final NextFilter nextFilter, final HttpProxyRequest request) {
    331         LOGGER.debug("Reconnecting to proxy ...");
    332
    333         final ProxyIoSession proxyIoSession = getProxyIoSession();
    334
    335         // Fires reconnection
    336         proxyIoSession.getConnector().connect(new IoSessionInitializer<ConnectFuture>() {
    337             public void initializeSession(final IoSession session, ConnectFuture future) {
    338                 LOGGER.debug("Initializing new session: {}", session);
    339                 session.setAttribute(ProxyIoSession.PROXY_SESSION, proxyIoSession);
    340                 proxyIoSession.setSession(session);
    341                 LOGGER.debug("  setting up proxyIoSession: {}", proxyIoSession);
    342                 future.addListener(new IoFutureListener<ConnectFuture>() {
    343                     public void operationComplete(ConnectFuture future) {
    344                         // Reconnection is done so we send the
    345                         // request to the proxy
    346                         proxyIoSession.setReconnectionNeeded(false);
    347                         writeRequest0(nextFilter, request);
    348                     }
    349                 });
    350             }
    351         });
    352     }
    353
    354     /**
    355      * Parse a HTTP response from the proxy server.
    356      *
    357      * @param response The response string.
    358      */
    359     protected HttpProxyResponse decodeResponse(final String response) throws Exception {
    360         LOGGER.debug("  parseResponse()");
    361
    362         // Break response into lines
    363         String[] responseLines = response.split(HttpProxyConstants.CRLF);
    364
    365         // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
    366         // BUG FIX : Trimed to prevent failures with some proxies that add
    367         // extra space chars like "Microsoft-IIS/5.0" ...
    368         String[] statusLine = responseLines[0].trim().split(" ", 2);
    369
    370         if (statusLine.length < 2) {
    371             throw new Exception("Invalid response status line (" + statusLine + "). Response: " + response);
    372         }
    373
    374         // Status code is 3 digits
    375         if (statusLine[1].matches("^\\d\\d\\d")) {
    376             throw new Exception("Invalid response code (" + statusLine[1] + "). Response: " + response);
    377         }
    378
    379         Map<String, List<String>> headers = new HashMap<String, List<String>>();
    380
    381         for (int i = 1; i < responseLines.length; i++) {
    382             String[] args = responseLines[i].split(":\\s?", 2);
    383             StringUtilities.addValueToHeader(headers, args[0], args[1], false);
    384         }
    385
    386         return new HttpProxyResponse(statusLine[0], statusLine[1], headers);
    387     }
    388 }