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         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      * @throws ProxyAuthException If we get an error during the proxy authentication
283      */
284     public abstract void handleResponse(final HttpProxyResponse response) throws ProxyAuthException;
285 
286     /**
287      * Calls writeRequest0(NextFilter, HttpProxyRequest) to write the request. 
288      * If needed a reconnection to the proxy is done previously.
289      * 
290      * @param nextFilter the next filter
291      * @param request the http request
292      */
293     public void writeRequest(final NextFilter nextFilter, final HttpProxyRequest request) {
294         ProxyIoSession proxyIoSession = getProxyIoSession();
295 
296         if (proxyIoSession.isReconnectionNeeded()) {
297             reconnect(nextFilter, request);
298         } else {
299             writeRequest0(nextFilter, request);
300         }
301     }
302 
303     /**
304      * Encodes a HTTP request and sends it to the proxy server.
305      * 
306      * @param nextFilter the next filter
307      * @param request the http request
308      */
309     private void writeRequest0(final NextFilter nextFilter, final HttpProxyRequest request) {
310         try {
311             String data = request.toHttpString();
312             IoBuffer buf = IoBuffer.wrap(data.getBytes(getProxyIoSession().getCharsetName()));
313 
314             LOGGER.debug("   write:\n{}", data.replace("\r", "\\r").replace("\n", "\\n\n"));
315 
316             writeData(nextFilter, buf);
317 
318         } catch (UnsupportedEncodingException ex) {
319             closeSession("Unable to send HTTP request: ", ex);
320         }
321     }
322 
323     /**
324      * Method to reconnect to the proxy when it decides not to maintain the connection 
325      * during handshake.
326      * 
327      * @param nextFilter the next filter
328      * @param request the http request
329      */
330     private void reconnect(final NextFilter nextFilter, final HttpProxyRequest request) {
331         LOGGER.debug("Reconnecting to proxy ...");
332 
333         final ProxyIoSession proxyIoSession = getProxyIoSession();
334 
335         // Fires reconnection
336         proxyIoSession.getConnector().connect(new IoSessionInitializer<ConnectFuture>() {
337             @Override
338             public void initializeSession(final IoSession session, ConnectFuture future) {
339                 LOGGER.debug("Initializing new session: {}", session);
340                 session.setAttribute(ProxyIoSession.PROXY_SESSION, proxyIoSession);
341                 proxyIoSession.setSession(session);
342                 LOGGER.debug("  setting up proxyIoSession: {}", proxyIoSession);
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      * Parse a HTTP response from the proxy server.
353      * 
354      * @param response The response string.
355      * @return The decoded HttpResponse
356      * @throws Exception If we get an error while decoding the response
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 line [1] is 3 digits, space and optional error text
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<>();
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 }