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