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.HttpEndOfContent;
32  import org.apache.mina.http.api.HttpMethod;
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   * The HTTP decoder
40   * 
41   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
42   */
43  public class HttpServerDecoder implements ProtocolDecoder {
44      private static final Logger LOGGER = LoggerFactory.getLogger(HttpServerCodec.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      /** Regex to parse HttpRequest Request Line */
56      public static final Pattern REQUEST_LINE_PATTERN = Pattern.compile(" ");
57  
58      /** Regex to parse out QueryString from HttpRequest */
59      public static final Pattern QUERY_STRING_PATTERN = Pattern.compile("\\?");
60  
61      /** Regex to parse out parameters from query string */
62      public static final Pattern PARAM_STRING_PATTERN = Pattern.compile("\\&|;");
63  
64      /** Regex to parse out key/value pairs */
65      public static final Pattern KEY_VALUE_PATTERN = Pattern.compile("=");
66  
67      /** Regex to parse raw headers and body */
68      public static final Pattern RAW_VALUE_PATTERN = Pattern.compile("\\r\\n\\r\\n");
69  
70      /** Regex to parse raw headers from body */
71      public static final Pattern HEADERS_BODY_PATTERN = Pattern.compile("\\r\\n");
72  
73      /** Regex to parse header name and value */
74      public static final Pattern HEADER_VALUE_PATTERN = Pattern.compile(":");
75  
76      /** Regex to split cookie header following RFC6265 Section 5.4 */
77      public static final Pattern COOKIE_SEPARATOR_PATTERN = Pattern.compile(";");
78  
79      /**
80       * {@inheritDoc}
81       */
82      @Override
83      public void decode(IoSession session, IoBuffer msg, ProtocolDecoderOutput out) {
84          DecoderState/../../org/apache/mina/http/DecoderState.html#DecoderState">DecoderState state = (DecoderState) session.getAttribute(DECODER_STATE_ATT);
85          
86          if (null == state) {
87              session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
88              state = (DecoderState) session.getAttribute(DECODER_STATE_ATT);
89          }
90          
91          switch (state) {
92              case HEAD:
93                  if (LOGGER.isDebugEnabled()) {
94                      LOGGER.debug("decoding HEAD");
95                  }
96                  
97                  // grab the stored a partial HEAD request
98                  ByteBuffer oldBuffer = (ByteBuffer) session.getAttribute(PARTIAL_HEAD_ATT);
99                  // concat the old buffer and the new incoming one
100                 // now let's decode like it was a new message
101                 msg = IoBuffer.allocate(oldBuffer.remaining() + msg.remaining()).put(oldBuffer).put(msg).flip();
102                 
103             case NEW:
104                 if (LOGGER.isDebugEnabled()) {
105                     LOGGER.debug("decoding NEW");
106                 }
107                 
108                 HttpRequestImpl rq = parseHttpRequestHead(msg.buf());
109     
110                 if (rq == null) {
111                     // we copy the incoming BB because it's going to be recycled by the inner IoProcessor for next reads
112                     ByteBuffer partial = ByteBuffer.allocate(msg.remaining());
113                     partial.put(msg.buf());
114                     partial.flip();
115                     // no request decoded, we accumulate
116                     session.setAttribute(PARTIAL_HEAD_ATT, partial);
117                     session.setAttribute(DECODER_STATE_ATT, DecoderState.HEAD);
118                     break;
119                 } else {
120                     out.write(rq);
121                     // is it a request with some body content ?
122                     String contentLen = rq.getHeader("content-length");
123     
124                     if (contentLen != null) {
125                         if (LOGGER.isDebugEnabled()) {
126                             LOGGER.debug("found content len : {}", contentLen);
127                         }
128                         
129                         session.setAttribute(BODY_REMAINING_BYTES, Integer.valueOf(contentLen));
130                         session.setAttribute(DECODER_STATE_ATT, DecoderState.BODY);
131                         // fallthrough, process body immediately
132                     } else {
133                         if (LOGGER.isDebugEnabled()) {
134                             LOGGER.debug("request without content");
135                         }
136                         
137                         session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
138                         out.write(new HttpEndOfContent());
139                         break;
140                     }
141                 }
142     
143             case BODY:
144                 if (LOGGER.isDebugEnabled()) {
145                     LOGGER.debug("decoding BODY: {} bytes", msg.remaining());
146                 }
147                 
148                 int chunkSize = msg.remaining();
149                 
150                 // send the chunk of body
151                 if (chunkSize != 0) {
152                     IoBuffer wb = IoBuffer.allocate(msg.remaining());
153                     wb.put(msg);
154                     wb.flip();
155                     out.write(wb);
156                 }
157                 
158                 msg.position(msg.limit());
159                 // do we have reach end of body ?
160                 int remaining = (Integer) session.getAttribute(BODY_REMAINING_BYTES);
161                 remaining -= chunkSize;
162     
163                 if (remaining <= 0) {
164                     if (LOGGER.isDebugEnabled()) {
165                         LOGGER.debug("end of HTTP body");
166                     }
167                     
168                     session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
169                     session.removeAttribute(BODY_REMAINING_BYTES);
170                     out.write(new HttpEndOfContent());
171                 } else {
172                     session.setAttribute(BODY_REMAINING_BYTES, Integer.valueOf(remaining));
173                 }
174     
175                 break;
176     
177             default:
178                 throw new HttpException(HttpStatus.CLIENT_ERROR_BAD_REQUEST, "Unknonwn decoder state : " + state);
179         }
180     }
181 
182     /**
183      * {@inheritDoc}
184      */
185     @Override
186     public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
187     }
188 
189     /**
190      * {@inheritDoc}
191      */
192     @Override
193     public void dispose(IoSession session) throws Exception {
194     }
195 
196     private HttpRequestImpl parseHttpRequestHead(ByteBuffer buffer) {
197         String raw = new String(buffer.array(), 0, buffer.limit());
198         String[] headersAndBody = RAW_VALUE_PATTERN.split(raw, -1);
199 
200         if (headersAndBody.length <= 1) {
201             // we didn't receive the full HTTP head
202             return null;
203         }
204 
205         String[] headerFields = HEADERS_BODY_PATTERN.split(headersAndBody[0]);
206         headerFields = ArrayUtil.dropFromEndWhile(headerFields, "");
207 
208         String requestLine = headerFields[0];
209         Map<String, String> generalHeaders = new HashMap<>();
210 
211         for (int i = 1; i < headerFields.length; i++) {
212             String[] header = HEADER_VALUE_PATTERN.split(headerFields[i]);
213             generalHeaders.put(header[0].toLowerCase(), header[1].trim());
214         }
215 
216         String[] elements = REQUEST_LINE_PATTERN.split(requestLine);
217         HttpMethod method = HttpMethod.valueOf(elements[0]);
218         HttpVersion version = HttpVersion.fromString(elements[2]);
219         String[] pathFrags = QUERY_STRING_PATTERN.split(elements[1]);
220         String requestedPath = pathFrags[0];
221         String queryString = pathFrags.length == 2 ? pathFrags[1] : "";
222 
223         // we put the buffer position where we found the beginning of the HTTP body
224         buffer.position(headersAndBody[0].length() + 4);
225 
226         return new HttpRequestImpl(version, method, requestedPath, queryString, generalHeaders);
227     }
228 }