blob: f1fb7323905a2e441039d5551a591b7d4aa2a53e [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.asyncweb.common.codec;
import java.util.List;
import java.util.Map;
import org.apache.asyncweb.common.Cookie;
import org.apache.asyncweb.common.DefaultCookie;
import org.apache.asyncweb.common.DefaultHttpResponse;
import org.apache.asyncweb.common.HttpHeaderConstants;
import org.apache.asyncweb.common.HttpResponse;
import org.apache.asyncweb.common.HttpResponseStatus;
import org.apache.asyncweb.common.HttpVersion;
import org.apache.asyncweb.common.MutableCookie;
import org.apache.asyncweb.common.MutableHttpResponse;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.filter.codec.ProtocolDecoderException;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.statemachine.ConsumeToEndOfSessionDecodingState;
import org.apache.mina.filter.codec.statemachine.CrLfDecodingState;
import org.apache.mina.filter.codec.statemachine.DecodingState;
import org.apache.mina.filter.codec.statemachine.DecodingStateMachine;
import org.apache.mina.filter.codec.statemachine.FixedLengthDecodingState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses HTTP requests.
* Clients should register a <code>HttpRequestParserListener</code>
* in order to receive notifications at important stages of request
* building.<br/>
*
* <code>HttpRequestParser</code>s should not be built for each request
* as each parser constructs an underlying state machine which is
* relatively costly to build.<br/> Instead, parsers should be pooled.<br/>
*
* Note, however, that a parser <i>must</i> be <code>prepare</code>d before
* each new parse.
*
* @author The Apache MINA Project (dev@mina.apache.org)
* @version $Rev$, $Date$
*/
abstract class HttpResponseDecodingState extends DecodingStateMachine {
private static final Logger LOG = LoggerFactory
.getLogger(HttpResponseDecodingState.class);
/**
* The header which provides a requests transfer coding
*/
private static final String TRANSFER_CODING = "transfer-encoding";
/**
* The chunked coding
*/
private static final String CHUNKED = "chunked";
/**
* The header which provides a requests content length
*/
private static final String CONTENT_LENGTH = "content-length";
/**
* Indicates the start of a coding extension
*/
private static final char EXTENSION_CHAR = ';';
public static final String COOKIE_COMMENT = "comment";
public static final String COOKIE_DOMAIN = "domain";
public static final String COOKIE_EXPIRES = "expires";
public static final String COOKIE_MAX_AGE = "max-age";
public static final String COOKIE_PATH = "path";
public static final String COOKIE_SECURE = "secure";
public static final String COOKIE_VERSION = "version";
/**
* The request we are building
*/
private MutableHttpResponse response;
@Override
protected DecodingState init() throws Exception {
response = new DefaultHttpResponse();
return SKIP_EMPTY_LINES;
}
@Override
protected void destroy() throws Exception {
}
private final DecodingState SKIP_EMPTY_LINES = new CrLfDecodingState() {
@Override
protected DecodingState finishDecode(boolean foundCRLF,
ProtocolDecoderOutput out) throws Exception {
if (foundCRLF) {
return this;
} else {
return READ_RESPONSE_LINE;
}
}
};
private final DecodingState READ_RESPONSE_LINE = new HttpResponseLineDecodingState() {
@Override
protected DecodingState finishDecode(List<Object> childProducts,
ProtocolDecoderOutput out) throws Exception {
if (childProducts.size() < 3) {
// Session is closed.
return null;
}
response.setProtocolVersion((HttpVersion) childProducts.get(0));
final HttpResponseStatus status = HttpResponseStatus.forId((Integer) childProducts.get(1));
if (status.isFinalResponse()) {
response.setStatus(status);
String reasonPhrase = (String) childProducts.get(2);
if (reasonPhrase.length() > 0) {
response.setStatusReasonPhrase(reasonPhrase);
}
return READ_HEADERS;
} else {
return SKIP_HEADERS;
}
}
};
private final DecodingState SKIP_HEADERS = new HttpHeaderDecodingState() {
@Override
@SuppressWarnings("unchecked")
protected DecodingState finishDecode(
List<Object> childProducts, ProtocolDecoderOutput out) throws Exception {
return READ_RESPONSE_LINE;
}
};
private final DecodingState READ_HEADERS = new HttpHeaderDecodingState() {
@Override
@SuppressWarnings("unchecked")
protected DecodingState finishDecode(List<Object> childProducts,
ProtocolDecoderOutput out) throws Exception {
Map<String, List<String>> headers = (Map<String, List<String>>) childProducts
.get(0);
// Parse cookies
List<String> cookies = headers.get(HttpHeaderConstants.KEY_SET_COOKIE);
if (cookies != null && !cookies.isEmpty()) {
for (String cookie : cookies) {
response.addCookie(parseCookie(cookie));
}
}
response.setHeaders(headers);
if (LOG.isDebugEnabled()) {
LOG.debug("Decoded header: " + response.getHeaders());
}
// Select appropriate body decoding state.
boolean isChunked = false;
if (response.getProtocolVersion() == HttpVersion.HTTP_1_1) {
LOG.debug("Request is HTTP 1/1. Checking for transfer coding");
isChunked = isChunked(response);
} else {
LOG.debug("Request is not HTTP 1/1. Using content length");
}
DecodingState nextState;
if (isChunked) {
LOG.debug("Using chunked decoder for request");
nextState = new ChunkedBodyDecodingState() {
@Override
protected DecodingState finishDecode(
List<Object> childProducts,
ProtocolDecoderOutput out) throws Exception {
if (childProducts.size() != 1) {
int chunkSize = 0;
for (Object product : childProducts) {
IoBuffer chunk = (IoBuffer) product;
chunkSize += chunk.remaining();
}
IoBuffer body = IoBuffer.allocate(chunkSize);
for (Object product : childProducts) {
IoBuffer chunk = (IoBuffer) product;
body.put(chunk);
}
body.flip();
response.setContent(body);
} else {
response.setContent((IoBuffer) childProducts.get(0));
}
out.write(response);
return null;
}
};
} else {
int length = getContentLength(response);
if (length > 0) {
if (LOG.isDebugEnabled()) {
LOG.debug(
"Using fixed length decoder for request with " +
"length " + length);
}
// TODO max length limitation.
nextState = new FixedLengthDecodingState(length) {
@Override
protected DecodingState finishDecode(IoBuffer readData,
ProtocolDecoderOutput out) throws Exception {
response.setContent(readData);
out.write(response);
return null;
}
};
} else if (length < 0) {
if (LOG.isDebugEnabled()) {
LOG.debug(
"Using consume-to-disconnection decoder for " +
"request with unspecified length.");
}
// FIXME hard-coded max length.
nextState = new ConsumeToEndOfSessionDecodingState(1048576) {
@Override
protected DecodingState finishDecode(IoBuffer readData,
ProtocolDecoderOutput out) throws Exception {
response.setContent(readData);
out.write(response);
return null;
}
};
} else {
LOG.debug("No entity body for this request");
out.write(response);
nextState = null;
}
}
return nextState;
}
private Cookie parseCookie(String cookieHeader) throws DateParseException {
MutableCookie cookie = null;
String pairs[] = cookieHeader.split(";");
for (int i = 0; i < pairs.length; i++) {
String nameValue[] = pairs[i].trim().split("=");
String name = nameValue[0].trim();
String value = (nameValue.length == 2) ? nameValue[1].trim() : null;
//First pair is the cookie name/value
if (i == 0) {
cookie = new DefaultCookie(name, value);
} else if (name.equalsIgnoreCase(COOKIE_COMMENT)) {
cookie.setComment(value);
} else if (name.equalsIgnoreCase(COOKIE_PATH)) {
cookie.setPath(value);
} else if (name.equalsIgnoreCase(COOKIE_SECURE)) {
cookie.setSecure(true);
} else if (name.equalsIgnoreCase(COOKIE_VERSION)) {
cookie.setVersion(Integer.parseInt(value));
} else if (name.equalsIgnoreCase(COOKIE_MAX_AGE)) {
int age = Integer.parseInt(value);
cookie.setMaxAge(age);
} else if (name.equalsIgnoreCase(COOKIE_EXPIRES)) {
long createdDate = System.currentTimeMillis();
int age = (int)(DateUtil.parseDate(value).getTime() - createdDate) / 1000;
cookie.setCreatedDate(createdDate);
cookie.setMaxAge(age);
} else if (name.equalsIgnoreCase(COOKIE_DOMAIN)) {
cookie.setDomain(value);
}
}
return cookie;
}
/**
* Obtains the content length from the specified request
*
* @param response The request
* @return The content length, or -1 if not specified
* @throws HttpDecoderException If an invalid content length is specified
*/
private int getContentLength(HttpResponse response)
throws ProtocolDecoderException {
int length = -1;
String lengthValue = response.getHeader(CONTENT_LENGTH);
if (lengthValue != null) {
try {
length = Integer.parseInt(lengthValue);
} catch (NumberFormatException e) {
HttpCodecUtils.throwDecoderException(
"Invalid content length: " + length,
HttpResponseStatus.BAD_REQUEST);
}
}
return length;
}
/**
* Determines whether a specified request employs a chunked
* transfer coding
*
* @param response The request
* @return <code>true</code> iff the request employs a
* chunked transfer coding
* @throws HttpDecoderException
* If the request employs an unsupported coding
*/
private boolean isChunked(HttpResponse response)
throws ProtocolDecoderException {
boolean isChunked = false;
String coding = response.getHeader(TRANSFER_CODING);
if (coding != null) {
int extensionIndex = coding.indexOf(EXTENSION_CHAR);
if (extensionIndex != -1) {
coding = coding.substring(0, extensionIndex);
}
if (CHUNKED.equalsIgnoreCase(coding)) {
isChunked = true;
} else {
// As we only support chunked encoding, any other encoding
// is unsupported
HttpCodecUtils.throwDecoderException(
"Unknown transfer coding " + coding,
HttpResponseStatus.NOT_IMPLEMENTED);
}
}
return isChunked;
}
};
}