| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| package org.apache.asyncweb.client.codec; |
| |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.nio.charset.CharsetDecoder; |
| import java.util.Date; |
| import java.util.regex.Pattern; |
| |
| import org.apache.asyncweb.client.util.DateUtil; |
| import org.apache.asyncweb.client.util.NameValuePair; |
| import org.apache.mina.common.ByteBuffer; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Utility class for helping to decode the HTTP Protocol. |
| */ |
| public class HttpDecoder { |
| static final Logger LOG = LoggerFactory.getLogger(HttpDecoder.class); |
| |
| /** The Constant CHUNKED. */ |
| public static final String CHUNKED = "chunked"; |
| |
| /** The Constant CONNECTION. */ |
| public static final String CONNECTION = "Connection"; |
| |
| /** The Constant CLOSE as a value for the Connection header */ |
| public static final String CLOSE = "close"; |
| |
| /** The Constant COOKIE_COMMENT. */ |
| public static final String COOKIE_COMMENT = "comment"; |
| |
| /** The Constant COOKIE_DOMAIN. */ |
| public static final String COOKIE_DOMAIN = "domain"; |
| |
| /** The Constant COOKIE_EXPIRES. */ |
| public static final String COOKIE_EXPIRES = "expires"; |
| |
| /** The Constant COOKIE_MAX_AGE. */ |
| public static final String COOKIE_MAX_AGE = "max-age"; |
| |
| /** The Constant COOKIE_PATH. */ |
| public static final String COOKIE_PATH = "path"; |
| |
| /** The Constant COOKIE_SECURE. */ |
| public static final String COOKIE_SECURE = "secure"; |
| |
| /** The Constant COOKIE_VERSION. */ |
| public static final String COOKIE_VERSION = "version"; |
| |
| /** The Constant LOCATION. */ |
| public static final String LOCATION = "Location"; |
| |
| /** The Constant SET_COOKIE. */ |
| public static final String SET_COOKIE = "Set-Cookie"; |
| |
| /** The Constant WWW_AUTH. */ |
| public static final String WWW_AUTH = "WWW-Authenticate"; |
| |
| /** The Constant TRANSFER_ENCODING. */ |
| public static final String TRANSFER_ENCODING = "Transfer-Encoding"; |
| |
| /** Carriage return character. */ |
| private static final byte CR = 13; |
| |
| /** Line feed character. */ |
| private static final byte LF = 10; |
| |
| /** Single space character. */ |
| private static final char SP = 32; |
| |
| /** Horizontal tab character. */ |
| private static final char HT = 9; |
| |
| |
| /** The decoder instances as threadlocals. */ |
| private static final ThreadLocal<CharsetDecoder> decoder = new ThreadLocal<CharsetDecoder>() { |
| @Override |
| protected CharsetDecoder initialValue() { |
| return Charset.forName(HttpMessage.HTTP_ELEMENT_CHARSET).newDecoder(); |
| } |
| }; |
| |
| /** Cached folding pattern. */ |
| private static final Pattern folding = Pattern.compile("\\r\\n([ \\t])"); |
| |
| /** |
| * Finds a line from a ByteBuffer that ends with a CR/LF and returns the line as a String. |
| * |
| * @param in ByteBuffer containing data |
| * |
| * @return a <code>String</code> representing the decoded line |
| * |
| * @throws Exception for any Exception that is encountered |
| */ |
| public String decodeLine(ByteBuffer in) throws Exception { |
| int beginPos = in.position(); |
| int limit = in.limit(); |
| boolean lastIsCR = false; |
| int terminatorPos = -1; |
| |
| for (int i = beginPos; i < limit; i++) { |
| byte b = in.get(i); |
| if (b == CR) { |
| lastIsCR = true; |
| } else { |
| if (b == LF && lastIsCR) { |
| terminatorPos = i; |
| break; |
| } |
| lastIsCR = false; |
| } |
| } |
| |
| //Check if we don't have enough data to process or found a full readable line |
| if (terminatorPos == -1) { |
| return null; |
| } |
| |
| String result = null; |
| if (terminatorPos > 1) { |
| ByteBuffer line = in.slice(); |
| line.limit(terminatorPos - beginPos - 1); |
| result = line.getString(decoder.get()); |
| } |
| |
| in.position(terminatorPos + 1); |
| |
| return result; |
| } |
| |
| /** |
| * Finds a header line from a ByteBuffer that ends with a CR/LF and returns |
| * the line as a String. Header lines are different from other lines in one |
| * important aspect; a single header can span multiple lines. Such folding |
| * occurs if a CR/LF is followed by a LWSP characters (a space or a |
| * horizontal tab). This method returns complete lines that correspond to |
| * a single header if the header is folded. |
| * |
| * @param in ByteBuffer containing data |
| * |
| * @return a <code>String</code> representing the decoded line |
| * |
| * @throws Exception for any Exception that is encountered |
| */ |
| public String decodeHeaderLine(ByteBuffer in) throws Exception { |
| int beginPos = in.position(); |
| int limit = in.limit(); |
| boolean lastIsCR = false; |
| boolean lastIsCRLF = false; |
| int terminatorPos = -1; |
| |
| for (int i = beginPos; i < limit; i++) { |
| byte b = in.get(i); |
| if (!lastIsCR && b == CR) { |
| lastIsCR = true; |
| } else if (lastIsCR && !lastIsCRLF && b == LF) { |
| // still need to peek one more byte... unless we begin with |
| // CRLF in which case we have encountered the end of the header |
| // section |
| if (i != beginPos + 1) { |
| lastIsCRLF = true; |
| } else { |
| // it is an empty line |
| terminatorPos = i; |
| break; |
| } |
| } else if (lastIsCRLF) { |
| // we have read until CRLF: see if there is continuation |
| if (isLWSPChar((char)b)) { |
| // folded header; reset the flags and continue |
| lastIsCR = false; |
| lastIsCRLF = false; |
| } else { |
| // we have reached the end of the line; move back by one and |
| // break |
| terminatorPos = i - 1; |
| break; |
| } |
| } |
| } |
| |
| // Check if we don't have enough data to process or found a full |
| // readable line |
| if (terminatorPos == -1) { |
| return null; |
| } |
| |
| String result = null; |
| if (terminatorPos > 1) { |
| ByteBuffer line = in.slice(); |
| line.limit(terminatorPos - beginPos - 1); |
| result = line.getString(decoder.get()); |
| } |
| |
| in.position(terminatorPos + 1); |
| |
| return result; |
| } |
| |
| /** |
| * Decodes the status code and message from a HTTP response and places the values in a |
| * {@link HttpResponseMessage} object. |
| * |
| * @param line <code>String</code> containing <code>HTTP/1.<i>X</i> <i>Message</i></code> |
| * @param msg the <code>HttpResponseMessage</code> for which to place the result |
| * |
| * @throws Exception on any Exception that may occur |
| * |
| * @see HttpResponseMessage |
| */ |
| public void decodeStatus(String line, HttpResponseMessage msg) throws Exception { |
| String magic = line.substring(0, 8); |
| if (!magic.equals("HTTP/1.1") && !magic.equals("HTTP/1.0")) { |
| throw new IOException("Invalid HTTP response"); |
| } |
| |
| String status = line.substring(9, 12); |
| msg.setStatusCode(Integer.parseInt(status)); |
| msg.setStatusMessage(line.substring(13)); |
| } |
| |
| /** |
| * Decodes headers and footers (for HTTP/1.1) and stuffs them into a {@link HttpResponseMessage} response. |
| * |
| * @param line the <code>String</code> line containing the header or footer |
| * @param msg the {@link HttpResponseMessage} response message |
| * |
| * @throws Exception if any exception occurs |
| */ |
| public void decodeHeader(String line, HttpResponseMessage msg) throws Exception { |
| LOG.debug("Processing Header Line: {}", line); |
| |
| // first, get rid of the CRLF from linear whitespace |
| line = folding.matcher(line).replaceAll("$1"); |
| int pos = line.indexOf(":"); |
| String name = line.substring(0, pos); |
| String value = trimHeaderValue(line.substring(pos + 1)); |
| NameValuePair nvp = new NameValuePair(name, value); |
| msg.addHeader(nvp); |
| |
| if (name.equalsIgnoreCase(SET_COOKIE)) { |
| Cookie cookie = decodeCookie(value, msg); |
| if (cookie != null) { |
| msg.addCookie(cookie); |
| } |
| } |
| |
| if (name.equalsIgnoreCase(HttpMessage.CONTENT_TYPE)) { |
| msg.setContentType(value); |
| } |
| |
| if (name.equalsIgnoreCase(HttpMessage.CONTENT_LENGTH)) { |
| msg.setContentLength(Integer.parseInt(value)); |
| } |
| |
| if (name.equalsIgnoreCase(CONNECTION)) { |
| msg.setConnection(value); |
| } |
| |
| if (name.equalsIgnoreCase(LOCATION)) { |
| msg.setLocation(value); |
| } |
| |
| if (name.equalsIgnoreCase(TRANSFER_ENCODING) && value != null && value.equalsIgnoreCase(CHUNKED)) { |
| msg.setChunked(true); |
| } |
| |
| if (name.equalsIgnoreCase(WWW_AUTH)) { |
| msg.addChallenge(nvp); |
| } |
| |
| } |
| |
| private String trimHeaderValue(String original) { |
| int start = 0; |
| int end = original.length(); |
| |
| // remove any preceding LWSP chars from the string |
| while (start < end && isLWSPChar(original.charAt(start))) { |
| start++; |
| } |
| |
| // remove any trailing LWSP chars from the string |
| while (start < end && isLWSPChar(original.charAt(end - 1))) { |
| end--; |
| } |
| |
| return original.substring(start, end); |
| } |
| |
| /** |
| * Returns whether the character is a LWSP character (SPACE | HTAB) as |
| * specified by RFC 822. |
| */ |
| boolean isLWSPChar(char ch) { |
| return ch == SP || ch == HT; |
| } |
| |
| /** |
| * Decodes size records for chunked HTTP transcoding. |
| * |
| * @param line the line containing the size |
| * |
| * @return the <code>int</code> representing the size |
| * |
| * @throws Exception if any exception occurs |
| */ |
| public int decodeSize(String line) throws Exception { |
| String strippedLine = line.trim().toLowerCase(); |
| for (int i = 0; i < strippedLine.length(); i++) { |
| char ch = strippedLine.charAt(i); |
| //Once we hit a non-numeric character, parse the number we have |
| if (ch < '0' || (ch > '9' && ch < 'a') || ch > 'f') { |
| return Integer.parseInt(strippedLine.substring(0, i), 16); |
| } |
| } |
| |
| //We got here, so the entire line passes |
| return Integer.parseInt(strippedLine, 16); |
| } |
| |
| /** |
| * Decodes content from non-chunked transcoding. It adds exactly the amount |
| * of data specified in the content length header. |
| * |
| * @param in the <code>ByteBuffer</code> containing the content at the the |
| * current position |
| * @param msg the <code>HttpResponseMessage</code> message to place the |
| * decoded content |
| * |
| * @throws Exception if any exception occurs |
| */ |
| public void decodeContent(ByteBuffer in, HttpResponseMessage msg) throws Exception { |
| addContent(in, msg, msg.getContentLength()); |
| } |
| |
| /** |
| * Decodes content from non-chunked transcoding. It adds all the remaining |
| * bytes from the buffer into the response body. |
| * |
| * @param in the <code>ByteBuffer</code> containing the content at the |
| * current position |
| * @param msg the <code>HttpResponseMessage</code> message to place the |
| * decoded content |
| * |
| * @throws Exception if any exception occurs |
| */ |
| public void decodeRemainingContent(ByteBuffer in, HttpResponseMessage msg) throws Exception { |
| if (in.hasRemaining()) { |
| addContent(in, msg, in.remaining()); |
| } |
| } |
| |
| /** |
| * Transfer a specific size of content from the buffer |
| * to the message. |
| * |
| * @param in The input buffer |
| * @param msg The message that will receive the content. |
| * @param size The number of bytes to transfer. |
| * |
| * @exception Exception |
| */ |
| private void addContent(ByteBuffer in, HttpResponseMessage msg, int size) throws Exception { |
| byte[] content = new byte[size]; |
| in.get(content); |
| msg.addContent(content); |
| } |
| |
| /** |
| * Decodes content from chunked transcoding. |
| * |
| * @param in the <code>ByteBuffer</code> containing the content at the the current position |
| * @param msg the <code>HttpResponseMessage</code> message to place the decoded content |
| * |
| * @throws Exception if any exception occurs |
| */ |
| public void decodeChunkedContent(ByteBuffer in, HttpResponseMessage msg) throws Exception { |
| int toRead = msg.getExpectedToRead(); |
| if ((in.get(in.position() + toRead) != CR) && (in.get(in.position() + toRead + 1) != LF)) { |
| throw new IOException("Invalid HTTP response - chunk does not end with CRLF"); |
| |
| } |
| byte content[] = new byte[toRead]; |
| in.get(content); |
| msg.addContent(content); |
| |
| //Pop the CRLF |
| in.get(); |
| in.get(); |
| } |
| |
| /** |
| * Decodes a cookie header and returns the decoded cookie. |
| * |
| * @param cookieStr the cookie <code>String</code> header line |
| * |
| * @return the decoded <code>Cookie</cookie> |
| * |
| * @throws Exception if any exception occurs |
| * @see Cookie |
| */ |
| public Cookie decodeCookie(String cookieStr, HttpResponseMessage msg) throws Exception { |
| LOG.debug("Processing Cookie Line: {}", cookieStr); |
| |
| Cookie cookie = null; |
| |
| String pairs[] = cookieStr.split(";"); |
| for (int i = 0; i < pairs.length; i++) { |
| String nameValue[] = pairs[i].trim().split("="); |
| String name = nameValue[0].trim(); |
| |
| //Check to see if the value was passed-in. |
| //According to rfc-2109 value is an optional property and could be blank. |
| boolean valuePresent = nameValue.length == 2; |
| |
| //First one is the cookie name/value pair |
| if (i == 0) { |
| if (valuePresent) { |
| cookie = new Cookie(name, nameValue[1].trim()); |
| } else { |
| //Value is not passed-in, so creating a cookie with an empty value. |
| cookie = new Cookie(name, ""); |
| } |
| continue; |
| } |
| |
| if (name.equalsIgnoreCase(COOKIE_SECURE)) { |
| cookie.setSecure(true); |
| } |
| |
| if (valuePresent) { |
| if (name.equalsIgnoreCase(COOKIE_COMMENT)) { |
| cookie.setComment(nameValue[1].trim()); |
| continue; |
| } |
| |
| if (name.equalsIgnoreCase(COOKIE_PATH)) { |
| cookie.setPath(nameValue[1].trim()); |
| } |
| |
| if (name.equalsIgnoreCase(COOKIE_VERSION)) { |
| cookie.setVersion(Integer.parseInt(nameValue[1])); |
| } |
| |
| if (name.equalsIgnoreCase(COOKIE_MAX_AGE)) { |
| int age = Integer.parseInt(nameValue[1]); |
| cookie.setExpires(new Date(System.currentTimeMillis() + age * 1000L)); |
| } |
| |
| if (name.equalsIgnoreCase(COOKIE_EXPIRES)) { |
| cookie.setExpires(DateUtil.parseDate(nameValue[1])); |
| } |
| |
| if (name.equalsIgnoreCase(COOKIE_DOMAIN)) { |
| cookie.setDomain(nameValue[1]); |
| } |
| } |
| } |
| |
| // supply the hostname as the domain if it is missing |
| if (cookie.getDomain() == null) { |
| cookie.setDomain(msg.getRequestURL().getHost()); |
| } |
| |
| // use the path (up to the rightmost "/") as the path attribute if it is |
| // missing |
| if (cookie.getPath() == null) { |
| String path = msg.getRequestURL().getPath(); |
| int lastSlash = path.lastIndexOf('/'); |
| if (lastSlash == -1) { |
| // if the slash is absent, treat it as the root context |
| path = "/"; |
| } else if (lastSlash < path.length() - 1) { |
| // get it up to the rightmost slash |
| path = path.substring(0, lastSlash+1); |
| } |
| cookie.setPath(path); |
| } |
| |
| return cookie; |
| } |
| } |