package org.apache.mina.http;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.mina.api.IdleStatus;
import org.apache.mina.api.IoFilter;
import org.apache.mina.api.IoSession;
import org.apache.mina.filterchain.ReadFilterChainController;
import org.apache.mina.filterchain.WriteFilterChainController;
import org.apache.mina.http.api.HttpEndOfContent;
import org.apache.mina.http.api.HttpMethod;
import org.apache.mina.http.api.HttpResponse;
import org.apache.mina.http.api.HttpVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Charsets;
public class HttpServerCodec implements IoFilter {
private static final Logger LOG = LoggerFactory.getLogger(HttpServerCodec.class);
/** Key for decoder current state */
private static final String DECODER_STATE_ATT = "http.ds";
/** Key for the partial HTTP requests head */
private static final String PARTIAL_HEAD_ATT = "http.ph";
/** Key for the number of bytes remaining to read for completing the body */
private static final String BODY_REMAINING_BYTES = "http.brb";
/** Regex to parse HttpRequest Request Line */
public static final Pattern REQUEST_LINE_PATTERN = Pattern.compile(" ");
/** Regex to parse out QueryString from HttpRequest */
public static final Pattern QUERY_STRING_PATTERN = Pattern.compile("\\?");
/** Regex to parse out parameters from query string */
public static final Pattern PARAM_STRING_PATTERN = Pattern.compile("\\&|;");
/** Regex to parse out key/value pairs */
public static final Pattern KEY_VALUE_PATTERN = Pattern.compile("=");
/** Regex to parse raw headers and body */
public static final Pattern RAW_VALUE_PATTERN = Pattern.compile("\\r\\n\\r\\n");
/** Regex to parse raw headers from body */
public static final Pattern HEADERS_BODY_PATTERN = Pattern.compile("\\r\\n");
/** Regex to parse header name and value */
public static final Pattern HEADER_VALUE_PATTERN = Pattern.compile(": ");
/** Regex to split cookie header following RFC6265 Section 5.4 */
public static final Pattern COOKIE_SEPARATOR_PATTERN = Pattern.compile(";");
@Override
public void sessionCreated(IoSession session) {
session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
}
@Override
public void sessionOpened(IoSession session) {
}
@Override
public void sessionClosed(IoSession session) {
session.removeAttribute(DECODER_STATE_ATT);
session.removeAttribute(PARTIAL_HEAD_ATT);
}
@Override
public void sessionIdle(IoSession session, IdleStatus status) {
// TODO Auto-generated method stub
}
@Override
public void messageReceived(IoSession session, Object message, ReadFilterChainController controller) {
if (!(message instanceof ByteBuffer)) {
throw new RuntimeException("invalid message type : " + message.getClass());
}
ByteBuffer msg = (ByteBuffer) message;
DecoderState state = session.getAttribute(DECODER_STATE_ATT);
switch (state) {
case HEAD:
LOG.debug("decoding HEAD");
// grab the stored a partial HEAD request
ByteBuffer oldBuffer = session.getAttribute(PARTIAL_HEAD_ATT);
// concat the old buffer and the new incoming one
msg = ByteBuffer.allocate(oldBuffer.remaining() + msg.remaining()).put(oldBuffer).put(msg);
msg.flip();
// now let's decode like it was a new message
case NEW:
LOG.debug("decoding NEW");
HttpRequestImpl rq = parseHttpRequestHead(msg);
if (rq == null) {
// no request decoded, we accumulate
session.setAttribute(PARTIAL_HEAD_ATT, msg);
session.setAttribute(DECODER_STATE_ATT, DecoderState.HEAD);
return;
} else {
controller.callReadNextFilter(session, rq);
// is it a request with some body content ?
if (rq.getMethod() == HttpMethod.POST || rq.getMethod() == HttpMethod.PUT) {
LOG.debug("request with content");
session.setAttribute(DECODER_STATE_ATT, DecoderState.BODY);
String contentLen = rq.getHeader("content-length");
if (contentLen != null) {
LOG.debug("found content len : {}", contentLen);
session.setAttribute(BODY_REMAINING_BYTES, new Integer(contentLen));
} else {
throw new RuntimeException("no content length !");
}
} else {
LOG.debug("request without content");
session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
}
}
break;
case BODY:
LOG.debug("decoding BODY");
int chunkSize = msg.remaining();
// send the chunk of body
controller.callReadNextFilter(session, msg);
// do we have reach end of body ?
int remaining = session.getAttribute(BODY_REMAINING_BYTES);
remaining -= chunkSize;
if (remaining <= 0) {
LOG.debug("end of HTTP body");
controller.callReadNextFilter(session, new HttpEndOfContent());
session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
session.removeAttribute(BODY_REMAINING_BYTES);
} else {
session.setAttribute(BODY_REMAINING_BYTES, new Integer(remaining));
}
break;
default:
throw new RuntimeException("Unknonwn decoder state : " + state);
}
}
@Override
public void messageWriting(IoSession session, Object message, WriteFilterChainController controller) {
if (message instanceof HttpResponse) {
HttpResponse msg = (HttpResponse) message;
StringBuilder sb = new StringBuilder(msg.getStatus().line());
for (Map.Entry<String, String> header : msg.getHeaders().entrySet()) {
sb.append(header.getKey());
sb.append(": ");
sb.append(header.getValue());
sb.append("\r\n");
}
sb.append("\r\n");
byte[] bytes = sb.toString().getBytes(Charsets.UTF_8);
controller.callWriteNextFilter(session, ByteBuffer.wrap(bytes));
} else if (message instanceof ByteBuffer) {
controller.callWriteNextFilter(session, message);
} else if (message instanceof HttpEndOfContent) {
// end of HTTP content
// keep alive ?
session.close(false);
}
}
private HttpRequestImpl parseHttpRequestHead(ByteBuffer buffer) {
String raw = new String(buffer.array(), 0, buffer.limit(), Charsets.ISO_8859_1);
String[] headersAndBody = RAW_VALUE_PATTERN.split(raw, -1);
if (headersAndBody.length <= 1) {
// we didn't receive the full HTTP head
return null;
}
String[] headerFields = HEADERS_BODY_PATTERN.split(headersAndBody[0]);
headerFields = ArrayUtil.dropFromEndWhile(headerFields, "");
String requestLine = headerFields[0];
Map<String, String> generalHeaders = new HashMap<String, String>();
for (int i = 1; i < headerFields.length; i++) {
String[] header = HEADER_VALUE_PATTERN.split(headerFields[i]);
generalHeaders.put(header[0].toLowerCase(), header[1]);
}
String[] elements = REQUEST_LINE_PATTERN.split(requestLine);
HttpMethod method = HttpMethod.valueOf(elements[0]);
HttpVersion version = HttpVersion.fromString(elements[2]);
String[] pathFrags = QUERY_STRING_PATTERN.split(elements[1]);
String requestedPath = pathFrags[0];
// we put the buffer position where we found the beginning of the HTTP body
buffer.position(headersAndBody[0].length());
return new HttpRequestImpl(version, method, requestedPath, generalHeaders);
}
private enum DecoderState {
NEW, // waiting for a new HTTP requests, the session is new of last request was completed
HEAD, // accumulating the HTTP request head (everything before the body)
BODY // receiving HTTP body slices
}
}