001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.proxy.handlers.http;
021
022import java.io.UnsupportedEncodingException;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026
027import org.apache.mina.core.buffer.IoBuffer;
028import org.apache.mina.core.filterchain.IoFilter.NextFilter;
029import org.apache.mina.core.future.ConnectFuture;
030import org.apache.mina.core.future.IoFutureListener;
031import org.apache.mina.core.session.IoSession;
032import org.apache.mina.core.session.IoSessionInitializer;
033import org.apache.mina.proxy.AbstractProxyLogicHandler;
034import org.apache.mina.proxy.ProxyAuthException;
035import org.apache.mina.proxy.session.ProxyIoSession;
036import org.apache.mina.proxy.utils.IoBufferDecoder;
037import org.apache.mina.proxy.utils.StringUtilities;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * AbstractHttpLogicHandler.java - Base class for HTTP proxy {@link AbstractProxyLogicHandler} implementations. 
043 * Provides HTTP request encoding/response decoding functionality.
044 * 
045 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
046 * @since MINA 2.0.0-M3
047 */
048public abstract class AbstractHttpLogicHandler extends AbstractProxyLogicHandler {
049    private final static Logger LOGGER = LoggerFactory.getLogger(AbstractHttpLogicHandler.class);
050
051    private final static String DECODER = AbstractHttpLogicHandler.class.getName() + ".Decoder";
052
053    private final static byte[] HTTP_DELIMITER = new byte[] { '\r', '\n', '\r', '\n' };
054
055    private final static byte[] CRLF_DELIMITER = new byte[] { '\r', '\n' };
056
057    // Parsing vars
058
059    /**
060     * Temporary buffer to accumulate the HTTP response from the proxy.
061     */
062    private IoBuffer responseData = null;
063
064    /**
065     * The parsed http proxy response
066     */
067    private HttpProxyResponse parsedResponse = null;
068
069    /**
070     * The content length of the proxy response.
071     */
072    private int contentLength = -1;
073
074    // HTTP/1.1 vars
075
076    /**
077     * A flag that indicates that this is a HTTP/1.1 response with chunked data.and that some chunks are missing.   
078     */
079    private boolean hasChunkedData;
080
081    /**
082     * A flag that indicates that some chunks of data are missing to complete the HTTP/1.1 response.   
083     */
084    private boolean waitingChunkedData;
085
086    /**
087     * A flag that indicates that chunked data has been read and that we're now reading the footers.   
088     */
089    private boolean waitingFooters;
090
091    /**
092     * Contains the position of the entity body start in the <code>responseData</code> {@link IoBuffer}.
093     */
094    private int entityBodyStartPosition;
095
096    /**
097     * Contains the limit of the entity body start in the <code>responseData</code> {@link IoBuffer}.
098     */
099    private int entityBodyLimitPosition;
100
101    /**
102     * Creates a new {@link AbstractHttpLogicHandler}.
103     * 
104     * @param proxyIoSession the {@link ProxyIoSession} in use.
105     */
106    public AbstractHttpLogicHandler(final ProxyIoSession proxyIoSession) {
107        super(proxyIoSession);
108    }
109
110    /**
111     * Handles incoming data during the handshake process. Should consume only the
112     * handshake data from the buffer, leaving any extra data in place.
113     * 
114     * @param nextFilter the next filter
115     * @param buf the buffer holding received data
116     */
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            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     * @return The decoded HttpResponse
359     * @throws Exception If we get an error while decoding the response
360     */
361    protected HttpProxyResponse decodeResponse(final String response) throws Exception {
362        LOGGER.debug("  parseResponse()");
363
364        // Break response into lines
365        String[] responseLines = response.split(HttpProxyConstants.CRLF);
366
367        // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
368        // BUG FIX : Trimed to prevent failures with some proxies that add 
369        // extra space chars like "Microsoft-IIS/5.0" ...
370        String[] statusLine = responseLines[0].trim().split(" ", 2);
371
372        if (statusLine.length < 2) {
373            throw new Exception("Invalid response status line (" + statusLine + "). Response: " + response);
374        }
375
376        // Status code is 3 digits
377        if (!statusLine[1].matches("^\\d\\d\\d")) {
378            throw new Exception("Invalid response code (" + statusLine[1] + "). Response: " + response);
379        }
380
381        Map<String, List<String>> headers = new HashMap<String, List<String>>();
382
383        for (int i = 1; i < responseLines.length; i++) {
384            String[] args = responseLines[i].split(":\\s?", 2);
385            StringUtilities.addValueToHeader(headers, args[0], args[1], false);
386        }
387
388        return new HttpProxyResponse(statusLine[0], statusLine[1], headers);
389    }
390}