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