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