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