| /* |
| * 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; |
| } |
| } |