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.http;
    21
    22  import java.nio.ByteBuffer;
    23  import java.util.HashMap;
    24  import java.util.Map;
    25  import java.util.regex.Pattern;
    26
    27  import org.apache.mina.core.buffer.IoBuffer;
    28  import org.apache.mina.core.session.IoSession;
    29  import org.apache.mina.filter.codec.ProtocolDecoder;
    30  import org.apache.mina.filter.codec.ProtocolDecoderOutput;
    31  import org.apache.mina.http.api.DefaultHttpResponse;
    32  import org.apache.mina.http.api.HttpEndOfContent;
    33  import org.apache.mina.http.api.HttpStatus;
    34  import org.apache.mina.http.api.HttpVersion;
    35  import org.slf4j.Logger;
    36  import org.slf4j.LoggerFactory;
    37
    38  public class HttpClientDecoder implements ProtocolDecoder {
    39      private static final Logger LOG = LoggerFactory.getLogger(HttpClientCodec.class);
    40
    41      /** Key for decoder current state */
    42      private static final String DECODER_STATE_ATT = "http.ds";
    43
    44      /** Key for the partial HTTP requests head */
    45      private static final String PARTIAL_HEAD_ATT = "http.ph";
    46
    47      /** Key for the number of bytes remaining to read for completing the body */
    48      private static final String BODY_REMAINING_BYTES = "http.brb";
    49
    50      /** Key for indicating chunked data */
    51      private static final String BODY_CHUNKED = "http.ckd";
    52
    53      /** Regex to parse HttpRequest Request Line */
    54      public static final Pattern REQUEST_LINE_PATTERN = Pattern.compile(" ");
    55
    56      /** Regex to parse HttpRequest Request Line */
    57      public static final Pattern RESPONSE_LINE_PATTERN = Pattern.compile(" ");
    58
    59      /** Regex to parse out QueryString from HttpRequest */
    60      public static final Pattern QUERY_STRING_PATTERN = Pattern.compile("\\?");
    61
    62      /** Regex to parse out parameters from query string */
    63      public static final Pattern PARAM_STRING_PATTERN = Pattern.compile("\\&|;");
    64
    65      /** Regex to parse out key/value pairs */
    66      public static final Pattern KEY_VALUE_PATTERN = Pattern.compile("=");
    67
    68      /** Regex to parse raw headers and body */
    69      public static final Pattern RAW_VALUE_PATTERN = Pattern.compile("\\r\\n\\r\\n");
    70
    71      /** Regex to parse raw headers from body */
    72      public static final Pattern HEADERS_BODY_PATTERN = Pattern.compile("\\r\\n");
    73
    74      /** Regex to parse header name and value */
    75      public static final Pattern HEADER_VALUE_PATTERN = Pattern.compile(": ");
    76
    77      /** Regex to split cookie header following RFC6265 Section 5.4 */
    78      public static final Pattern COOKIE_SEPARATOR_PATTERN = Pattern.compile(";");
    79
    80      public void decode(final IoSession session, final IoBuffer msg, final ProtocolDecoderOutput out) {
    81          DecoderState state = (DecoderState)session.getAttribute(DECODER_STATE_ATT);
    82          if (null == state) {
    83          	session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
    84          	state = (DecoderState)session.getAttribute(DECODER_STATE_ATT);
    85          }
    86          switch (state) {
    87          case HEAD:
    88              LOG.debug("decoding HEAD");
    89              // grab the stored a partial HEAD request
    90              final ByteBuffer oldBuffer = (ByteBuffer)session.getAttribute(PARTIAL_HEAD_ATT);
    91              // concat the old buffer and the new incoming one
    92              IoBuffer.allocate(oldBuffer.remaining() + msg.remaining()).put(oldBuffer).put(msg).flip();
    93              // now let's decode like it was a new message
    94
    95          case NEW:
    96              LOG.debug("decoding NEW");
    97              final DefaultHttpResponse rp = parseHttpReponseHead(msg.buf());
    98
    99              if (rp == null) {
    100                 // we copy the incoming BB because it's going to be recycled by the inner IoProcessor for next reads
    101                 final ByteBuffer partial = ByteBuffer.allocate(msg.remaining());
    102                 partial.put(msg.buf());
    103                 partial.flip();
    104                 // no request decoded, we accumulate
    105                 session.setAttribute(PARTIAL_HEAD_ATT, partial);
    106                 session.setAttribute(DECODER_STATE_ATT, DecoderState.HEAD);
    107             } else {
    108             	out.write(rp);
    109                 // is it a response with some body content ?
    110                 LOG.debug("response with content");
    111                 session.setAttribute(DECODER_STATE_ATT, DecoderState.BODY);
    112
    113                 final String contentLen = rp.getHeader("content-length");
    114
    115                 if (contentLen != null) {
    116                     LOG.debug("found content len : {}", contentLen);
    117                     session.setAttribute(BODY_REMAINING_BYTES, Integer.valueOf(contentLen));
    118                 } else if ("chunked".equalsIgnoreCase(rp.getHeader("transfer-encoding"))) {
    119                     LOG.debug("no content len but chunked");
    120                     session.setAttribute(BODY_CHUNKED, Boolean.valueOf("true"));
    121                 } else if ("close".equalsIgnoreCase(rp.getHeader("connection"))) {
    122                 	session.close(true);
    123                 } else {
    124                     throw new HttpException(HttpStatus.CLIENT_ERROR_LENGTH_REQUIRED, "no content length !");
    125                 }
    126             }
    127
    128             break;
    129
    130         case BODY:
    131             LOG.debug("decoding BODY: {} bytes", msg.remaining());
    132             final int chunkSize = msg.remaining();
    133             // send the chunk of body
    134             if (chunkSize != 0) {
    135             	final IoBuffer wb = IoBuffer.allocate(msg.remaining());
    136     			wb.put(msg);
    137     			wb.flip();
    138     			out.write(wb);
    139             }
    140             msg.position(msg.limit());
    141
    142             // do we have reach end of body ?
    143             int remaining = 0;
    144
    145             // if chunked, remaining is the msg.remaining()
    146             if( session.getAttribute(BODY_CHUNKED) != null ) {
    147             	remaining = chunkSize;
    148             } else {
    149             	// otherwise, manage with content-length
    150                 remaining = (Integer) session.getAttribute(BODY_REMAINING_BYTES);
    151                 remaining -= chunkSize;
    152             }
    153
    154             if (remaining <= 0 ) {
    155                 LOG.debug("end of HTTP body");
    156                 session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
    157                 session.removeAttribute(BODY_REMAINING_BYTES);
    158                 if( session.getAttribute(BODY_CHUNKED) != null ) {
    159                 	session.removeAttribute(BODY_CHUNKED);
    160                 }
    161                 out.write(new HttpEndOfContent());
    162             } else {
    163             	if( session.getAttribute(BODY_CHUNKED) == null ) {
    164             		session.setAttribute(BODY_REMAINING_BYTES, Integer.valueOf(remaining));
    165             	}
    166             }
    167
    168             break;
    169
    170         default:
    171             throw new HttpException(HttpStatus.SERVER_ERROR_INTERNAL_SERVER_ERROR, "Unknonwn decoder state : " + state);
    172         }
    173     }
    174
    175     public void finishDecode(final IoSession session, final ProtocolDecoderOutput out) throws Exception {
    176     }
    177
    178     public void dispose(final IoSession session) throws Exception {
    179     }
    180
    181     private DefaultHttpResponse parseHttpReponseHead(final ByteBuffer buffer) {
    182         // Java 6 >> String raw = new String(buffer.array(), 0, buffer.limit(), Charset.forName("UTF-8"));
    183         final String raw = new String(buffer.array(), 0, buffer.limit());
    184         final String[] headersAndBody = RAW_VALUE_PATTERN.split(raw, -1);
    185         if (headersAndBody.length <= 1) {
    186             // we didn't receive the full HTTP head
    187             return null;
    188         }
    189
    190         String[] headerFields = HEADERS_BODY_PATTERN.split(headersAndBody[0]);
    191         headerFields = ArrayUtil.dropFromEndWhile(headerFields, "");
    192
    193         final String requestLine = headerFields[0];
    194         final Map<String, String> generalHeaders = new HashMap<String, String>();
    195
    196         for (int i = 1; i < headerFields.length; i++) {
    197             final String[] header = HEADER_VALUE_PATTERN.split(headerFields[i]);
    198             generalHeaders.put(header[0].toLowerCase(), header[1]);
    199         }
    200
    201         final String[] elements = RESPONSE_LINE_PATTERN.split(requestLine);
    202         HttpStatus status = null;
    203         final int statusCode = Integer.valueOf(elements[1]);
    204         for (int i = 0; i < HttpStatus.values().length; i++) {
    205         	status = HttpStatus.values()[i];
    206 			if (statusCode == status.code()) {
    207 				break;
    208 			}
    209 		}
    210         final HttpVersion version = HttpVersion.fromString(elements[0]);
    211
    212         // we put the buffer position where we found the beginning of the HTTP body
    213         buffer.position(headersAndBody[0].length() + 4);
    214
    215         return new DefaultHttpResponse(version, status, generalHeaders);
    216     }
    217 }