blob: 55722d894313e3bcb385a450d6936765bcd87ed0 [file] [log] [blame]
/*
* 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.ahc.codec;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import org.apache.ahc.util.DateUtil;
import org.apache.ahc.util.NameValuePair;
import org.apache.asyncweb.common.Cookie;
import org.apache.asyncweb.common.DefaultCookie;
import org.apache.asyncweb.common.MutableCookie;
import org.apache.mina.core.buffer.IoBuffer;
/**
* Utility class for helping to decode the HTTP Protocol.
*/
public class HttpDecoder {
/** 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_HTTP_ONLY. */
public static final String COOKIE_HTTP_ONLY = "HTTPOnly";
/** 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;
/** The decoder. */
private CharsetDecoder decoder = Charset.forName(HttpMessage.HTTP_ELEMENT_CHARSET).newDecoder();
/**
* 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(IoBuffer 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) {
IoBuffer line = in.slice();
line.limit(terminatorPos - beginPos - 1);
result = line.getString(decoder);
}
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 {
int pos = line.indexOf(": ");
String name = line.substring(0, pos);
String value = line.substring(pos + 2);
NameValuePair nvp = new NameValuePair(name, value);
msg.addHeader(nvp);
if (name.equalsIgnoreCase(SET_COOKIE)) {
Cookie cookie = decodeCookie(value);
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);
}
}
/**
* 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.
*
* @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(IoBuffer in, HttpResponseMessage msg) throws Exception {
byte content[] = new byte[msg.getContentLength()];
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(IoBuffer 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) throws Exception {
MutableCookie 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();
//First one is the cookie name/value pair
if (i == 0) {
cookie = new DefaultCookie(name, nameValue[1].trim());
continue;
}
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_SECURE)) {
cookie.setSecure(true);
}
if (name.equalsIgnoreCase(COOKIE_HTTP_ONLY)) {
cookie.setHttpOnly(true);
}
if (name.equalsIgnoreCase(COOKIE_VERSION)) {
cookie.setVersion(Integer.parseInt(nameValue[1]));
}
if (name.equalsIgnoreCase(COOKIE_MAX_AGE)) {
int age = Integer.parseInt(nameValue[1]);
cookie.setMaxAge(age);
}
if (name.equalsIgnoreCase(COOKIE_EXPIRES)) {
long createdDate = System.currentTimeMillis();
int age = (int)(DateUtil.parseDate(nameValue[1]).getTime() - createdDate) / 1000;
cookie.setCreatedDate(createdDate);
cookie.setMaxAge(age);
}
if (name.equalsIgnoreCase(COOKIE_DOMAIN)) {
cookie.setDomain(nameValue[1]);
}
}
return cookie;
}
}