PageRenderTime 53ms CodeModel.GetById 16ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/src/kilim/http/HttpRequest.java

http://github.com/kilim/kilim
Java | 376 lines | 270 code | 44 blank | 62 comment | 71 complexity | 947d177d671e38bc591b69779e34f080 MD5 | raw file
  1/* Copyright (c) 2006, Sriram Srinivasan
  2 *
  3 * You may distribute this software under the terms of the license 
  4 * specified in the file "License"
  5 */
  6
  7package kilim.http;
  8
  9import java.io.IOException;
 10import java.io.UnsupportedEncodingException;
 11import java.net.URLDecoder;
 12import java.nio.ByteBuffer;
 13
 14import kilim.Pausable;
 15import kilim.nio.EndPoint;
 16
 17/**
 18 * This object encapsulates a bytebuffer (via HttpMsg). HttpRequestParser creates an instance of this object, but only
 19 * converts a few of the important fields into Strings; the rest are maintained as ranges (offset + length) in the
 20 * bytebuffer. Use {@link #getHeader(String)} to get the appropriate field.
 21 */
 22public class HttpRequest extends HttpMsg {
 23    // All the header related members of this class are initialized by the HttpRequestParser class.
 24
 25    /**
 26     * The original header. All string variables that pertain to the message's header are either subsequences of this
 27     * header, or interned (all known keywords).
 28     */
 29    public String method;
 30
 31    /**
 32     * The UTF8 decoded path from the HTTP header.
 33     */
 34    public String uriPath;
 35
 36    public int    nFields;
 37    /**
 38     * Keys present in the HTTP header
 39     */
 40    public String keys[];
 41
 42    // range variables encode the offset and length within the header. The strings corresponding
 43    // to these variables are created lazily.
 44    public int    versionRange;
 45    public int    uriFragmentRange;
 46    public int    queryStringRange;
 47    public int[]  valueRanges;
 48
 49    public int    contentOffset;
 50    public int    contentLength;
 51
 52    /**
 53     * The read cursor, used in the read* methods.
 54     */
 55    public int    iread;
 56
 57    public HttpRequest() {
 58        keys = new String[5];
 59        valueRanges = new int[5];
 60    }
 61
 62    /** 
 63     * Get the value for a given key
 64     * @param key
 65     * @return null if the key is not present in the header.
 66     */
 67    public String getHeader(String key) {
 68        for (int i = 0; i < nFields; i++) {
 69            if (key.equalsIgnoreCase(keys[i])) {
 70                return extractRange(valueRanges[i]);
 71            }
 72        }
 73        return ""; // no point returning null
 74    }
 75
 76    /**
 77     * @return the query part of the URI. 
 78     */
 79    public String getQuery() {
 80        return extractRange(queryStringRange);
 81    }
 82    
 83    public String version() {
 84        return extractRange(versionRange);
 85    }
 86    
 87    public boolean keepAlive() {
 88        return isOldHttp() ? "Keep-Alive".equals(getHeader("Connection")) : !("close".equals(getHeader("Connection")));
 89    }
 90    public KeyValues getQueryComponents() {
 91        String q = getQuery();
 92        return getQueryComponents(q);
 93    }
 94    public KeyValues getQueryComponents(String q) {
 95        int len = q.length();
 96        if (q == null || len == 0)
 97            return new KeyValues(0);
 98
 99        int numPairs = 0;
100        for (int i = 0; i < len; i++) {
101            if (q.charAt(i) == '=')
102                numPairs++;
103        }
104        KeyValues components = new KeyValues(numPairs);
105
106        int beg = 0;
107        String key = null;
108        boolean url_encoded = false;
109        for (int i = 0; i <= len; i++) {
110            char c = (i == len) ? '&' // pretending there's an artificial marker at the end of the string, to capture
111                                      // the last component
112                    : q.charAt(i);
113
114            if (c == '+' || c == '%')
115                url_encoded = true;
116            if (c == '=' || c == '&') {
117                String comp = q.substring(beg, i);
118                if (url_encoded) {
119                    try {
120                        comp = URLDecoder.decode(comp, "UTF-8");
121                    } catch (UnsupportedEncodingException ignore) {
122                    }
123                }
124                if (key == null) {
125                    key = comp;
126                } else {
127                    components.put(key, comp);
128                    key = null;
129                }
130                beg = i + 1;
131                url_encoded = false; // for next time
132            }
133        }
134        return components;
135    }
136
137    public String uriFragment() {
138        return extractRange(uriFragmentRange);
139    }
140
141    public String toString() {
142        StringBuilder sb = new StringBuilder(500);
143        sb.append("method: ").append(method).append('\n').append("version: ").append(version()).append('\n').append(
144                "path = ").append(uriPath).append('\n').append("uri_fragment = ").append(uriFragment()).append('\n')
145                .append("query = ").append(getQueryComponents()).append('\n');
146        for (int i = 0; i < nFields; i++) {
147            sb.append(keys[i]).append(": ").append(extractRange(valueRanges[i])).append('\n');
148        }
149
150        return sb.toString();
151    }
152
153    /**
154     * @return true if version is 1.0 or earlier
155     */
156    public boolean isOldHttp() {
157        final byte b1 = (byte) '1';
158        int offset = versionRange >> 16;
159        return (buffer.get(offset) < b1 || buffer.get(offset + 2) < b1);
160    }
161
162    /**
163     * Clear the request object so that it can be reused for the next message. 
164     */
165    public void reuse() {
166        method = null;
167        uriPath = null;
168        versionRange = 0;
169        uriFragmentRange = queryStringRange = 0;
170        contentOffset = 0;
171        contentLength = 0;
172
173        if (buffer != null) {
174            buffer.clear();
175        }
176        for (int i = 0; i < nFields; i++) {
177            keys[i] = null;
178        }
179        nFields = 0;
180    }
181
182    
183    /*
184     * Internal methods 
185     */
186    public void readFrom(EndPoint endpoint) throws Pausable, IOException {
187        iread = 0;
188        readHeader(endpoint);
189        readBody(endpoint);
190    }
191
192    public void readHeader(EndPoint endpoint) throws Pausable, IOException {
193        buffer = ByteBuffer.allocate(1024);
194        int headerLength = 0;
195        int n;
196        do {
197            n = readLine(endpoint); // includes 2 bytes for CRLF
198            headerLength += n;
199        } while (n > 2 || headerLength <= 2); // until blank line (CRLF), but just blank line is not enough.
200        // dumpBuffer(buffer);
201        HttpRequestParser.initHeader(this, headerLength);
202        contentOffset = headerLength; // doesn't mean there's necessarily any content.
203        String cl = getHeader("Content-Length");
204        if (cl.length() > 0) {
205            try {
206                contentLength = Integer.parseInt(cl);
207            } catch (NumberFormatException nfe) {
208                throw new IOException("Malformed Content-Length hdr");
209            }
210        } else if ((getHeader("Transfer-Encoding").indexOf("chunked") >= 0)
211                || (getHeader("TE").indexOf("chunked") >= 0)) {
212            contentLength = -1;
213        } else {
214            contentLength = 0;
215        }
216    }
217
218    public void dumpBuffer(ByteBuffer buffer) {
219        byte[] ba = buffer.array();
220        int len = buffer.position();
221        for (int i = 0; i < len; i++) {
222            System.out.print((char) ba[i]);
223        }
224    }
225
226    public void addField(String key, int valRange) {
227        if (keys.length == nFields) {
228            keys = (String[]) Utils.growArray(keys, 5);
229            valueRanges = Utils.growArray(valueRanges, 5);
230        }
231        keys[nFields] = key;
232        valueRanges[nFields] = valRange;
233        nFields++;
234    }
235
236
237    // complement of HttpRequestParser.encodeRange
238    public String extractRange(int range) {
239        int beg = range >> 16;
240        int end = range & 0xFFFF;
241        return extractRange(beg, end);
242    }
243
244    public String extractRange(int beg, int end) {
245        return new String(buffer.array(), beg, (end - beg));
246    }
247    public byte [] extractBytes(int beg, int end) {
248        return java.util.Arrays.copyOfRange(buffer.array(),beg,end);
249    }
250
251
252    /*
253     * Read entire content into request's buffer
254     */
255    public void readBody(EndPoint endpoint) throws Pausable, IOException {
256        iread = contentOffset;
257        if (contentLength > 0) {
258            fill(endpoint, contentOffset, contentLength);
259            iread = contentOffset + contentLength;
260        } else if (contentLength == -1) {
261            // CHUNKED
262            readAllChunks(endpoint);
263        }
264        readTrailers(endpoint);
265    }
266
267    public void readTrailers(EndPoint endpoint) {
268    }
269
270    /*
271     * Read all chunks until  a chunksize of 0 is received, then consolidate the chunks into a single contiguous chunk.
272     * At the end of this method, the entire content is available in the requests buffer, starting at contentOffset and
273     * of length contentLength.
274     */
275    public void readAllChunks(EndPoint endpoint) throws IOException, Pausable {
276        IntList chunkRanges = new IntList(); // alternate numbers in this list refer to the start and end offsets of chunks.
277        do {
278            int n = readLine(endpoint); // read chunk size text into buffer
279            int beg = iread;
280            int size = parseChunkSize(buffer, iread - n, iread); // Parse size in hex, ignore extension
281            if (size == 0)
282                break;
283            // If the chunk has not already been read in, do so
284            fill(endpoint, iread, size+2 /*chunksize + CRLF*/);
285            // record chunk start and end
286            chunkRanges.add(beg); 
287            chunkRanges.add(beg + size); // without the CRLF
288            iread += size + 2; // for the next round.
289        } while (true);
290
291        // / consolidate all chunkRanges
292        if (chunkRanges.numElements == 0) {
293            contentLength = 0;
294            return;
295        }
296        contentOffset = chunkRanges.get(0); // first chunk's beginning
297        int endOfLastChunk = chunkRanges.get(1); // first chunk's end
298
299        byte[] bufa = buffer.array();
300        for (int i = 2; i < chunkRanges.numElements; i += 2) {
301            int beg = chunkRanges.get(i);
302            int chunkSize = chunkRanges.get(i + 1) - beg;
303            System.arraycopy(bufa, beg, bufa, endOfLastChunk, chunkSize);
304            endOfLastChunk += chunkSize;
305        }
306        // TODO move all trailer stuff up
307        contentLength = endOfLastChunk - contentOffset;
308        
309        // At this point, the contentOffset and contentLen give the entire content 
310    }
311    
312
313    public static byte CR = (byte) '\r';
314    public static byte LF = (byte) '\n';
315    static final byte  b0 = (byte) '0', b9 = (byte) '9';
316    static final byte  ba = (byte) 'a', bf = (byte) 'f';
317    static final byte  bA = (byte) 'A', bF = (byte) 'F';
318    static final byte  SEMI = (byte)';';
319
320    public static int parseChunkSize(ByteBuffer buffer, int start, int end) throws IOException {
321        byte[] bufa = buffer.array();
322        int size = 0;
323        for (int i = start; i < end; i++) {
324            byte b = bufa[i];
325            if (b >= b0 && b <= b9) {
326                size = size * 16 + (b - b0);
327            } else if (b >= ba && b <= bf) {
328                size = size * 16 + ((b - ba) + 10);
329            } else if (b >= bA && b <= bF) {
330                size = size * 16 + ((b - bA) + 10);
331            } else if (b == CR || b == SEMI) { 
332                // SEMI-colon starts a chunk extension. We ignore extensions currently.
333                break;
334            } else {
335                throw new IOException("Error parsing chunk size; unexpected char " + b + " at offset " + i);
336            }
337        }
338        return size;
339    }
340
341    // topup if request's buffer doesn't have all the bytes yet.
342    public void fill(EndPoint endpoint, int offset, int size) throws IOException, Pausable {
343        int total = offset + size;
344        int currentPos = buffer.position();
345        if (total > buffer.position()) {
346            buffer = endpoint.fill(buffer, (total - currentPos));
347        }
348    }
349
350    public int readLine(EndPoint endpoint) throws IOException, Pausable {
351        int ireadSave = iread;
352        int i = ireadSave;
353        while (true) {
354            int end = buffer.position();
355            byte[] bufa = buffer.array();
356            for (; i < end; i++) {
357                if (bufa[i] == CR) {
358                    ++i;
359                    if (i >= end) {
360                        buffer = endpoint.fill(buffer, 1);
361                        bufa = buffer.array(); // fill could have changed the buffer.
362                        end = buffer.position();
363                    }
364                    if (bufa[i] != LF) {
365                        throw new IOException("Expected LF at " + i);
366                    }
367                    ++i;
368                    int lineLength = i - ireadSave;
369                    iread = i;
370                    return lineLength;
371                }
372            }
373            buffer = endpoint.fill(buffer, 1); // no CRLF found. fill a bit more and start over.
374        }
375    }
376}