/* Copyright (c) 2006, Sriram Srinivasan * * You may distribute this software under the terms of the license * specified in the file "License" */ package kilim.http; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.ByteBuffer; import kilim.Pausable; import kilim.nio.EndPoint; /** * This object encapsulates a bytebuffer (via HttpMsg). HttpRequestParser creates an instance of this object, but only * converts a few of the important fields into Strings; the rest are maintained as ranges (offset + length) in the * bytebuffer. Use {@link #getHeader(String)} to get the appropriate field. */ public class HttpRequest extends HttpMsg { // All the header related members of this class are initialized by the HttpRequestParser class. /** * The original header. All string variables that pertain to the message's header are either subsequences of this * header, or interned (all known keywords). */ public String method; /** * The UTF8 decoded path from the HTTP header. */ public String uriPath; public int nFields; /** * Keys present in the HTTP header */ public String keys[]; // range variables encode the offset and length within the header. The strings corresponding // to these variables are created lazily. public int versionRange; public int uriFragmentRange; public int queryStringRange; public int[] valueRanges; public int contentOffset; public int contentLength; /** * The read cursor, used in the read* methods. */ public int iread; public HttpRequest() { keys = new String[5]; valueRanges = new int[5]; } /** * Get the value for a given key * @param key * @return null if the key is not present in the header. */ public String getHeader(String key) { for (int i = 0; i < nFields; i++) { if (key.equalsIgnoreCase(keys[i])) { return extractRange(valueRanges[i]); } } return ""; // no point returning null } /** * @return the query part of the URI. */ public String getQuery() { return extractRange(queryStringRange); } public String version() { return extractRange(versionRange); } public boolean keepAlive() { return isOldHttp() ? "Keep-Alive".equals(getHeader("Connection;")) : !("close".equals(getHeader("Connection"))); } public KeyValues getQueryComponents() { String q = getQuery(); int len = q.length(); if (q == null || len == 0) return new KeyValues(0); int numPairs = 0; for (int i = 0; i < len; i++) { if (q.charAt(i) == '=') numPairs++; } KeyValues components = new KeyValues(numPairs); int beg = 0; String key = null; boolean url_encoded = false; for (int i = 0; i <= len; i++) { char c = (i == len) ? '&' // pretending there's an artificial marker at the end of the string, to capture // the last component : q.charAt(i); if (c == '+' || c == '%') url_encoded = true; if (c == '=' || c == '&') { String comp = q.substring(beg, i); if (url_encoded) { try { comp = URLDecoder.decode(comp, "UTF-8"); } catch (UnsupportedEncodingException ignore) { } } if (key == null) { key = comp; } else { components.put(key, comp); key = null; } beg = i + 1; url_encoded = false; // for next time } } return components; } public String uriFragment() { return extractRange(uriFragmentRange); } public String toString() { StringBuilder sb = new StringBuilder(500); sb.append("method: ").append(method).append('\n').append("version: ").append(version()).append('\n').append( "path = ").append(uriPath).append('\n').append("uri_fragment = ").append(uriFragment()).append('\n') .append("query = ").append(getQueryComponents()).append('\n'); for (int i = 0; i < nFields; i++) { sb.append(keys[i]).append(": ").append(extractRange(valueRanges[i])).append('\n'); } return sb.toString(); } /** * @return true if version is 1.0 or earlier */ public boolean isOldHttp() { final byte b1 = (byte) '1'; int offset = versionRange >> 16; return (buffer.get(offset) < b1 || buffer.get(offset + 2) < b1); } /** * Clear the request object so that it can be reused for the next message. */ public void reuse() { method = null; uriPath = null; versionRange = 0; uriFragmentRange = queryStringRange = 0; contentOffset = 0; contentLength = 0; if (buffer != null) { buffer.clear(); } for (int i = 0; i < nFields; i++) { keys[i] = null; } nFields = 0; } /* * Internal methods */ public void readFrom(EndPoint endpoint) throws Pausable, IOException { iread = 0; readHeader(endpoint); readBody(endpoint); } public void readHeader(EndPoint endpoint) throws Pausable, IOException { buffer = ByteBuffer.allocate(1024); int headerLength = 0; int n; do { n = readLine(endpoint); // includes 2 bytes for CRLF headerLength += n; } while (n > 2 || headerLength <= 2); // until blank line (CRLF), but just blank line is not enough. // dumpBuffer(buffer); HttpRequestParser.initHeader(this, headerLength); contentOffset = headerLength; // doesn't mean there's necessarily any content. String cl = getHeader("Content-Length"); if (cl.length() > 0) { try { contentLength = Integer.parseInt(cl); } catch (NumberFormatException nfe) { throw new IOException("Malformed Content-Length hdr"); } } else if ((getHeader("Transfer-Encoding").indexOf("chunked") >= 0) || (getHeader("TE").indexOf("chunked") >= 0)) { contentLength = -1; } else { contentLength = 0; } } public void dumpBuffer(ByteBuffer buffer) { byte[] ba = buffer.array(); int len = buffer.position(); for (int i = 0; i < len; i++) { System.out.print((char) ba[i]); } } public void addField(String key, int valRange) { if (keys.length == nFields) { keys = (String[]) Utils.growArray(keys, 5); valueRanges = Utils.growArray(valueRanges, 5); } keys[nFields] = key; valueRanges[nFields] = valRange; nFields++; } // complement of HttpRequestParser.encodeRange public String extractRange(int range) { int beg = range >> 16; int end = range & 0xFFFF; return extractRange(beg, end); } public String extractRange(int beg, int end) { return new String(buffer.array(), beg, (end - beg)); } /* * Read entire content into request's buffer */ public void readBody(EndPoint endpoint) throws Pausable, IOException { iread = contentOffset; if (contentLength > 0) { fill(endpoint, contentOffset, contentLength); iread = contentOffset + contentLength; } else if (contentLength == -1) { // CHUNKED readAllChunks(endpoint); } readTrailers(endpoint); } public void readTrailers(EndPoint endpoint) { } /* * Read all chunks until a chunksize of 0 is received, then consolidate the chunks into a single contiguous chunk. * At the end of this method, the entire content is available in the requests buffer, starting at contentOffset and * of length contentLength. */ public void readAllChunks(EndPoint endpoint) throws IOException, Pausable { IntList chunkRanges = new IntList(); // alternate numbers in this list refer to the start and end offsets of chunks. do { int n = readLine(endpoint); // read chunk size text into buffer int beg = iread; int size = parseChunkSize(buffer, iread - n, iread); // Parse size in hex, ignore extension if (size == 0) break; // If the chunk has not already been read in, do so fill(endpoint, iread, size+2 /*chunksize + CRLF*/); // record chunk start and end chunkRanges.add(beg); chunkRanges.add(beg + size); // without the CRLF iread += size + 2; // for the next round. } while (true); // / consolidate all chunkRanges if (chunkRanges.numElements == 0) { contentLength = 0; return; } contentOffset = chunkRanges.get(0); // first chunk's beginning int endOfLastChunk = chunkRanges.get(1); // first chunk's end byte[] bufa = buffer.array(); for (int i = 2; i < chunkRanges.numElements; i += 2) { int beg = chunkRanges.get(i); int chunkSize = chunkRanges.get(i + 1) - beg; System.arraycopy(bufa, beg, bufa, endOfLastChunk, chunkSize); endOfLastChunk += chunkSize; } // TODO move all trailer stuff up contentLength = endOfLastChunk - contentOffset; // At this point, the contentOffset and contentLen give the entire content } public static byte CR = (byte) '\r'; public static byte LF = (byte) '\n'; static final byte b0 = (byte) '0', b9 = (byte) '9'; static final byte ba = (byte) 'a', bf = (byte) 'f'; static final byte bA = (byte) 'A', bF = (byte) 'F'; static final byte SEMI = (byte)';'; public static int parseChunkSize(ByteBuffer buffer, int start, int end) throws IOException { byte[] bufa = buffer.array(); int size = 0; for (int i = start; i < end; i++) { byte b = bufa[i]; if (b >= b0 && b <= b9) { size = size * 16 + (b - b0); } else if (b >= ba && b <= bf) { size = size * 16 + ((b - ba) + 10); } else if (b >= bA && b <= bF) { size = size * 16 + ((b - bA) + 10); } else if (b == CR || b == SEMI) { // SEMI-colon starts a chunk extension. We ignore extensions currently. break; } else { throw new IOException("Error parsing chunk size; unexpected char " + b + " at offset " + i); } } return size; } // topup if request's buffer doesn't have all the bytes yet. public void fill(EndPoint endpoint, int offset, int size) throws IOException, Pausable { int total = offset + size; int currentPos = buffer.position(); if (total > buffer.position()) { buffer = endpoint.fill(buffer, (total - currentPos)); } } public int readLine(EndPoint endpoint) throws IOException, Pausable { int ireadSave = iread; int i = ireadSave; while (true) { int end = buffer.position(); byte[] bufa = buffer.array(); for (; i < end; i++) { if (bufa[i] == CR) { ++i; if (i >= end) { buffer = endpoint.fill(buffer, 1); bufa = buffer.array(); // fill could have changed the buffer. end = buffer.position(); } if (bufa[i] != LF) { throw new IOException("Expected LF at " + i); } ++i; int lineLength = i - ireadSave; iread = i; return lineLength; } } buffer = endpoint.fill(buffer, 1); // no CRLF found. fill a bit more and start over. } } }