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      */
106     public AbstractHttpLogicHandler(final ProxyIoSession proxyIoSession) {
107         super(proxyIoSession);
108     }
109 
110     /**
111      * Handles incoming data during the handshake process. Should consume only the
112      * handshake data from the buffer, leaving any extra data in place.
113      * 
114      * @param nextFilter the next filter
115      * @param buf the buffer holding received data
116      */
117     public synchronized void messageReceived(final NextFilter nextFilter, final IoBuffer buf) throws ProxyAuthException {
118         LOGGER.debug(" messageReceived()");
119 
120         IoBufferDecoder decoder = (IoBufferDecoder) getSession().getAttribute(DECODER);
121         if (decoder == null) {
122             decoder = new IoBufferDecoder(HTTP_DELIMITER);
123             getSession().setAttribute(DECODER, decoder);
124         }
125 
126         try {
127             if (parsedResponse == null) {
128 
129                 responseData = decoder.decodeFully(buf);
130                 if (responseData == null) {
131                     return;
132                 }
133 
134                 // Handle the response                                
135                 String responseHeader = responseData.getString(getProxyIoSession().getCharset().newDecoder());
136                 entityBodyStartPosition = responseData.position();
137 
138                 LOGGER.debug("  response header received:\n{}",
139                         responseHeader.replace("\r", "\\r").replace("\n", "\\n\n"));
140 
141                 // Parse the response
142                 parsedResponse = decodeResponse(responseHeader);
143 
144                 // Is handshake complete ?
145                 if (parsedResponse.getStatusCode() == 200
146                         || (parsedResponse.getStatusCode() >= 300 && parsedResponse.getStatusCode() <= 307)) {
147                     buf.position(0);
148                     setHandshakeComplete();
149                     return;
150                 }
151 
152                 String contentLengthHeader = StringUtilities.getSingleValuedHeader(parsedResponse.getHeaders(),
153                         "Content-Length");
154 
155                 if (contentLengthHeader == null) {
156                     contentLength = 0;
157                 } else {
158                     contentLength = Integer.parseInt(contentLengthHeader.trim());
159                     decoder.setContentLength(contentLength, true);
160                 }
161             }
162 
163             if (!hasChunkedData) {
164                 if (contentLength > 0) {
165                     IoBuffer tmp = decoder.decodeFully(buf);
166                     if (tmp == null) {
167                         return;
168                     }
169                     responseData.setAutoExpand(true);
170                     responseData.put(tmp);
171                     contentLength = 0;
172                 }
173 
174                 if ("chunked".equalsIgnoreCase(StringUtilities.getSingleValuedHeader(parsedResponse.getHeaders(),
175                         "Transfer-Encoding"))) {
176                     // Handle Transfer-Encoding: Chunked
177                     LOGGER.debug("Retrieving additional http response chunks");
178                     hasChunkedData = true;
179                     waitingChunkedData = true;
180                 }
181             }
182 
183             if (hasChunkedData) {
184                 // Read chunks
185                 while (waitingChunkedData) {
186                     if (contentLength == 0) {
187                         decoder.setDelimiter(CRLF_DELIMITER, false);
188                         IoBuffer tmp = decoder.decodeFully(buf);
189                         if (tmp == null) {
190                             return;
191                         }
192 
193                         String chunkSize = tmp.getString(getProxyIoSession().getCharset().newDecoder());
194                         int pos = chunkSize.indexOf(';');
195                         if (pos >= 0) {
196                             chunkSize = chunkSize.substring(0, pos);
197                         } else {
198                             chunkSize = chunkSize.substring(0, chunkSize.length() - 2);
199                         }
200                         contentLength = Integer.decode("0x" + chunkSize);
201                         if (contentLength > 0) {
202                             contentLength += 2; // also read chunk's trailing CRLF
203                             decoder.setContentLength(contentLength, true);
204                         }
205                     }
206 
207                     if (contentLength == 0) {
208                         waitingChunkedData = false;
209                         waitingFooters = true;
210                         entityBodyLimitPosition = responseData.position();
211                         break;
212                     }
213 
214                     IoBuffer tmp = decoder.decodeFully(buf);
215                     if (tmp == null) {
216                         return;
217                     }
218                     contentLength = 0;
219                     responseData.put(tmp);
220                     buf.position(buf.position());
221                 }
222 
223                 // Read footers
224                 while (waitingFooters) {
225                     decoder.setDelimiter(CRLF_DELIMITER, false);
226                     IoBuffer tmp = decoder.decodeFully(buf);
227                     if (tmp == null) {
228                         return;
229                     }
230 
231                     if (tmp.remaining() == 2) {
232                         waitingFooters = false;
233                         break;
234                     }
235 
236                     // add footer to headers                    
237                     String footer = tmp.getString(getProxyIoSession().getCharset().newDecoder());
238                     String[] f = footer.split(":\\s?", 2);
239                     StringUtilities.addValueToHeader(parsedResponse.getHeaders(), f[0], f[1], false);
240                     responseData.put(tmp);
241                     responseData.put(CRLF_DELIMITER);
242                 }
243             }
244 
245             responseData.flip();
246 
247             LOGGER.debug("  end of response received:\n{}",
248                     responseData.getString(getProxyIoSession().getCharset().newDecoder()));
249 
250             // Retrieve entity body content
251             responseData.position(entityBodyStartPosition);
252             responseData.limit(entityBodyLimitPosition);
253             parsedResponse.setBody(responseData.getString(getProxyIoSession().getCharset().newDecoder()));
254 
255             // Free the response buffer
256             responseData.free();
257             responseData = null;
258 
259             handleResponse(parsedResponse);
260 
261             parsedResponse = null;
262             hasChunkedData = false;
263             contentLength = -1;
264             decoder.setDelimiter(HTTP_DELIMITER, true);
265 
266             if (!isHandshakeComplete()) {
267                 doHandshake(nextFilter);
268             }
269         } catch (Exception ex) {
270             if (ex instanceof ProxyAuthException) {
271                 throw ((ProxyAuthException) ex);
272             }
273 
274             throw new ProxyAuthException("Handshake failed", ex);
275         }
276     }
277 
278     /**
279      * Handles a HTTP response from the proxy server.
280      * 
281      * @param response The response.
282      */
283     public abstract void handleResponse(final HttpProxyResponse response) throws ProxyAuthException;
284 
285     /**
286      * Calls writeRequest0(NextFilter, HttpProxyRequest) to write the request. 
287      * If needed a reconnection to the proxy is done previously.
288      * 
289      * @param nextFilter the next filter
290      * @param request the http request
291      */
292     public void writeRequest(final NextFilter nextFilter, final HttpProxyRequest request) {
293         ProxyIoSession proxyIoSession = getProxyIoSession();
294 
295         if (proxyIoSession.isReconnectionNeeded()) {
296             reconnect(nextFilter, request);
297         } else {
298             writeRequest0(nextFilter, request);
299         }
300     }
301 
302     /**
303      * Encodes a HTTP request and sends it to the proxy server.
304      * 
305      * @param nextFilter the next filter
306      * @param request the http request
307      */
308     private void writeRequest0(final NextFilter nextFilter, final HttpProxyRequest request) {
309         try {
310             String data = request.toHttpString();
311             IoBuffer buf = IoBuffer.wrap(data.getBytes(getProxyIoSession().getCharsetName()));
312 
313             LOGGER.debug("   write:\n{}", data.replace("\r", "\\r").replace("\n", "\\n\n"));
314 
315             writeData(nextFilter, buf);
316 
317         } catch (UnsupportedEncodingException ex) {
318             closeSession("Unable to send HTTP request: ", ex);
319         }
320     }
321 
322     /**
323      * Method to reconnect to the proxy when it decides not to maintain the connection 
324      * during handshake.
325      * 
326      * @param nextFilter the next filter
327      * @param request the http request
328      */
329     private void reconnect(final NextFilter nextFilter, final HttpProxyRequest request) {
330         LOGGER.debug("Reconnecting to proxy ...");
331 
332         final ProxyIoSession proxyIoSession = getProxyIoSession();
333 
334         // Fires reconnection
335         proxyIoSession.getConnector().connect(new IoSessionInitializer<ConnectFuture>() {
336             public void initializeSession(final IoSession session, ConnectFuture future) {
337                 LOGGER.debug("Initializing new session: {}", session);
338                 session.setAttribute(ProxyIoSession.PROXY_SESSION, proxyIoSession);
339                 proxyIoSession.setSession(session);
340                 LOGGER.debug("  setting up proxyIoSession: {}", proxyIoSession);
341                 future.addListener(new IoFutureListener<ConnectFuture>() {
342                     public void operationComplete(ConnectFuture future) {
343                         // Reconnection is done so we send the
344                         // request to the proxy
345                         proxyIoSession.setReconnectionNeeded(false);
346                         writeRequest0(nextFilter, request);
347                     }
348                 });
349             }
350         });
351     }
352 
353     /**
354      * Parse a HTTP response from the proxy server.
355      * 
356      * @param response The response string.
357      */
358     protected HttpProxyResponse decodeResponse(final String response) throws Exception {
359         LOGGER.debug("  parseResponse()");
360 
361         // Break response into lines
362         String[] responseLines = response.split(HttpProxyConstants.CRLF);
363 
364         // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
365         // BUG FIX : Trimed to prevent failures with some proxies that add 
366         // extra space chars like "Microsoft-IIS/5.0" ...
367         String[] statusLine = responseLines[0].trim().split(" ", 2);
368 
369         if (statusLine.length < 2) {
370             throw new Exception("Invalid response status line (" + statusLine + "). Response: " + response);
371         }
372 
373         // Status code is 3 digits
374         if (!statusLine[1].matches("^\\d\\d\\d")) {
375             throw new Exception("Invalid response code (" + statusLine[1] + "). Response: " + response);
376         }
377 
378         Map<String, List<String>> headers = new HashMap<String, List<String>>();
379 
380         for (int i = 1; i < responseLines.length; i++) {
381             String[] args = responseLines[i].split(":\\s?", 2);
382             StringUtilities.addValueToHeader(headers, args[0], args[1], false);
383         }
384 
385         return new HttpProxyResponse(statusLine[0], statusLine[1], headers);
386     }
387 }