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