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}