]> xenbits.xensource.com Git - xenclient/toolstack.git/commitdiff
Round out http API; improve handling of 0.9 protocol
authorPrashanth Mundkur <prashanth.mundkur@citrix.com>
Mon, 4 May 2009 17:16:32 +0000 (10:16 -0700)
committerPrashanth Mundkur <prashanth.mundkur@citrix.com>
Mon, 4 May 2009 18:07:21 +0000 (11:07 -0700)
libs/http/http.ml

index c54cb66bf3a1debfe21dde3e9f495931f2062d79..60cae3fda74621592827c002a08549da2b26f57d 100644 (file)
@@ -16,6 +16,8 @@
 
 (* TODO: Put these functions in some libs *)
 
+let optval = function | Some v -> v | None -> assert false
+
 let is_space c = c = ' ' || c = '\t' || c = '\r' || c = '\n'
 
 let rev_string_of_chars cl =
@@ -39,7 +41,7 @@ let hex_value c =
        | _ -> 0 (* should never happen if guarded with is_hex *)
 
 let is_ctl_char c =
-       (* Note that this considers '\t', '\n' and '\r' as control chars. *)
+       (* Note that this considers '\t', '\n' and '\r' as control chars. *)
        let code = Char.code c in
        code < 32 || code > 127
 
@@ -160,7 +162,7 @@ module Headers = struct
                        (match c with
                         | '\r' -> s.cursor <- Header_CR
                         | '\n' -> s.cursor <- Done
-                        | _    ->  s.cursor <- In_field_name [ c ]
+                        | _    -> s.cursor <- In_field_name [ c ]
                        )
                | Header_CR ->
                        if c = '\n' then s.cursor <- Done
@@ -189,7 +191,7 @@ module Headers = struct
                         | ' ' | '\t' ->
                                (* compress whitespace *)
                                let nclist = if starts_with_space clist then clist else c :: clist in
-                               s.cursor <- In_field_value (name, nclist)
+                               s.cursor <- In_field_value (name, nclist)
                         | _ -> s.cursor <- In_field_value (name, c :: clist)
                        )
                | In_field_value_CR (name, clist) ->
@@ -198,12 +200,12 @@ module Headers = struct
                | In_field_value_LF (name, clist) ->
                        (match c with
                         | ' ' | '\t' ->
-                               (* header has line-wrapped; compress whitespace *)
+                               (* header has line-wrapped; compress whitespace *)
                                let nclist = if starts_with_space clist then clist else c :: clist in
-                               s.cursor <- In_field_value (name, nclist)
+                               s.cursor <- In_field_value (name, nclist)
                         | _ ->
                                (* end-of-header line; strip trailing whitespace if any *)
-                               s.headers <- (add_header name
+                               s.headers <- (add_header name
                                                (rev_string_of_chars (strip_leading_spaces clist))
                                                s.headers);
                                (match c with
@@ -231,8 +233,7 @@ module Request_header = struct
        type cursor =
                | Start
                | In_method of char list
-               | In_method_SP
-               | In_method_CR
+               | Method_SP
                | In_uri of char list
                | Uri_SP
                | Uri_CR
@@ -246,8 +247,7 @@ module Request_header = struct
                | Start -> "Start"
                | In_method cl ->
                        Printf.sprintf "In-method \"%s\"" (rev_string_of_chars cl)
-               | In_method_SP -> "In-method-SP"
-               | In_method_CR -> "In-method-CR"
+               | Method_SP -> "Method-SP"
                | In_uri cl ->
                        Printf.sprintf "In-uri \"%s\"" (rev_string_of_chars cl)
                | Uri_CR -> "Uri-CR"
@@ -263,20 +263,23 @@ module Request_header = struct
        type state =
        {
                mutable cursor: cursor;
-               mutable meth: meth option;
-               mutable uri: string option;
-               mutable version: version option;
-               mutable headers: header_fields;
+               mutable s_meth: meth option;
+               mutable s_uri: string option;
+               mutable s_version: version option;
+               mutable s_headers: header_fields;
        }
 
        type error =
                | Unsupported_version of string
+               | Incomplete_request_line
                | Parse_error of cursor * char
                | Internal_error of string
 
        let string_of_error = function
                | Unsupported_version s ->
                        Printf.sprintf "Unsupported version in request: \"%s\"" s
+               | Incomplete_request_line ->
+                       Printf.sprintf "Incomplete request line"
                | Parse_error (cursor, c) ->
                        Printf.sprintf "Request parse error in state %s at char '%C'"
                                (string_of_cursor cursor) c
@@ -290,14 +293,19 @@ module Request_header = struct
        let init_state () =
        {
                cursor = Start;
-               meth = None;
-               uri = None;
-               version = Some HTTP09;
-               headers = [];
+               s_meth = None;
+               s_uri = None;
+               s_version = Some HTTP09;
+               s_headers = [];
        }
 
        let is_done s = s.cursor = Done
 
+       (* From http://tools.ietf.org/html/rfc1945#section-4:
+          Simple-Request  = "GET" SP Request-URI CRLF                  ; HTTP/0.9
+          Request-Line    = Method SP Request-URI SP HTTP-Version CRLF ; HTTP/{1.0,1.1}
+       *)
+
        let parse_char s c =
                let raise_bad_char () = raise_error (Parse_error (s.cursor, c)) in
                match s.cursor with
@@ -306,7 +314,8 @@ module Request_header = struct
                        else if not (is_space c) then raise_bad_char ()
                | In_method cl ->
                        (match c with
-                        | c when is_space c ->
+                        | '\r' | '\n' -> raise_error Incomplete_request_line
+                        | ' ' | '\t' ->
                                let meth = match rev_string_of_chars cl with
                                 | "GET"     -> Get
                                 | "PUT"     -> Put
@@ -318,27 +327,20 @@ module Request_header = struct
                                 | "OPTIONS" -> Options
                                 | m         -> Extension m
                                in
-                               s.meth <- Some meth;
-                               (match c with
-                                | '\r' -> s.cursor <- In_method_CR
-                                | '\n' -> s.cursor <- Done (* HTTP 0.9 *)
-                                | _    -> s.cursor <- In_method_SP
-                               )
+                               s.s_meth <- Some meth;
+                               s.cursor <- Method_SP
                         | _ ->
                                s.cursor <- In_method (c :: cl)
                        )
-               | In_method_CR ->
-                       if c = '\n' then s.cursor <- Done (* HTTP 0.9 *)
-                       else raise_bad_char ()
-               | In_method_SP ->
+               | Method_SP ->
                        (match c with
-                        | '\r' | '\n' -> s.cursor <- Done (* HTTP 0.9 *)
+                        | '\r' | '\n' -> raise_error Incomplete_request_line
                         | ' '  | '\t' -> ()
                         | _ -> s.cursor <- In_uri [ c ]
                        )
                | In_uri cl ->
                        if is_space c then begin
-                               s.uri <- Some (rev_string_of_chars cl);
+                               s.s_uri <- Some (rev_string_of_chars cl);
                                (match c with
                                 | ' ' | '\t' -> s.cursor <- Uri_SP
                                 | '\r'       -> s.cursor <- Uri_CR
@@ -359,8 +361,8 @@ module Request_header = struct
                | In_version cl ->
                        if is_space c then begin
                                (match rev_string_of_chars cl with
-                                | "HTTP/1.0" -> s.version <- Some HTTP10
-                                | "HTTP/1.1" -> s.version <- Some HTTP11
+                                | "HTTP/1.0" -> s.s_version <- Some HTTP10
+                                | "HTTP/1.1" -> s.s_version <- Some HTTP11
                                 | v -> raise_error (Unsupported_version v)
                                );
                                (match c with
@@ -385,10 +387,42 @@ module Request_header = struct
                | In_headers hs ->
                        Headers.parse_char hs c;
                        if Headers.is_done hs then begin
-                               s.headers <- List.rev hs.Headers.headers;
+                               s.s_headers <- List.rev hs.Headers.headers;
                                s.cursor <- Done
                        end
                | Done -> raise_error (Internal_error "parse called on finished request!")
+
+       type t =
+       {
+               version: version;
+               meth: meth;
+               uri: string;
+               headers: header_fields;
+       }
+
+       type parse_result =
+               | Result of t
+               | Parse_incomplete of state
+
+       let get_parse_result state =
+               match state.cursor with
+               | Done -> Some { version = optval state.s_version;
+                                meth = optval state.s_meth;
+                                uri = optval state.s_uri;
+                                headers = state.s_headers
+                              }
+               | _ -> None
+
+       let parse state str =
+               let len = String.length str in
+               let i = ref 0 in
+               while get_parse_result state = None && !i < len do
+                       parse_char state str.[!i];
+                       incr i
+               done;
+               match get_parse_result state with
+               | Some v -> Result v
+               | None -> Parse_incomplete state
 end
 
 
@@ -422,10 +456,10 @@ module Response_header = struct
        type state =
        {
                mutable cursor: cursor;
-               mutable version: version option;
-               mutable status_code: int option;
-               mutable reason_phrase: string option;
-               mutable headers: header_fields;
+               mutable s_version: version option;
+               mutable s_status_code: int option;
+               mutable s_reason_phrase: string option;
+               mutable s_headers: header_fields;
        }
 
        type error =
@@ -455,14 +489,21 @@ module Response_header = struct
        let init_state () =
        {
                cursor = Start;
-               version = None;
-               status_code = None;
-               reason_phrase = None;
-               headers = [];
+               s_version = None;
+               s_status_code = None;
+               s_reason_phrase = None;
+               s_headers = [];
        }
 
        let is_done s = s.cursor = Done
 
+       (* From http://tools.ietf.org/html/rfc1945#section-4:
+          Simple-Response = [ Entity-Body ]                               ; HTTP/0.9
+          Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF ; HTTP/{1.0,1.1}
+
+          So, we can only be parsing a full status line.
+       *)
+
        let parse_char s c =
                let raise_bad_char () = raise_error (Parse_error (s.cursor, c)) in
                match s.cursor with
@@ -473,8 +514,8 @@ module Response_header = struct
                        (match c with
                         | ' ' | '\t' ->
                                (match rev_string_of_chars cl with
-                                | "HTTP/1.0" -> s.version <- Some HTTP10
-                                | "HTTP/1.1" -> s.version <- Some HTTP11
+                                | "HTTP/1.0" -> s.s_version <- Some HTTP10
+                                | "HTTP/1.1" -> s.s_version <- Some HTTP11
                                 | v -> raise_error (Unsupported_version v)
                                );
                                s.cursor <- Version_SP
@@ -493,13 +534,13 @@ module Response_header = struct
                        let nsc = 10 * sc + (digit_value c) in
                        (match c with
                         | ' ' | '\t' ->
-                               s.status_code <- Some nsc;
+                               s.s_status_code <- Some nsc;
                                s.cursor <- Status_code_SP
                         | '\r' ->
-                               s.status_code <- Some nsc;
+                               s.s_status_code <- Some nsc;
                                s.cursor <- Resp_line_CR
                         | '\n' ->
-                               s.status_code <- Some nsc;
+                               s.s_status_code <- Some nsc;
                                s.cursor <- In_headers (Headers.init_state ())
                         | _ when is_digit c ->
                                if nd >= 3 then raise_error (Unsupported_status_code nsc)
@@ -520,7 +561,7 @@ module Response_header = struct
                        (match c with
                         | ' ' | '\t' -> ()
                         | '\r' | '\n' ->
-                               s.reason_phrase <- Some (rev_string_of_chars clist);
+                               s.s_reason_phrase <- Some (rev_string_of_chars clist);
                                s.cursor <- (if c = '\r'
                                             then Resp_line_CR
                                             else In_headers (Headers.init_state ()))
@@ -535,10 +576,42 @@ module Response_header = struct
                | In_headers hs ->
                        Headers.parse_char hs c;
                        if Headers.is_done hs then begin
-                               s.headers <- List.rev hs.Headers.headers;
+                               s.s_headers <- List.rev hs.Headers.headers;
                                s.cursor <- Done
                        end
                | Done -> raise_error (Internal_error "parse called on finished request!")
+
+       type t =
+       {
+               version: version;
+               status_code: int;
+               reason_phrase: string;
+               headers: header_fields;
+       }
+
+       type parse_result =
+               | Result of t
+               | Parse_incomplete of state
+
+       let get_parse_result state =
+               match state.cursor with
+               | Done -> Some { version = optval state.s_version;
+                                status_code = optval state.s_status_code;
+                                reason_phrase = optval state.s_reason_phrase;
+                                headers = state.s_headers
+                              }
+               | _    -> None
+
+       let parse state str =
+               let len = String.length str in
+               let i = ref 0 in
+               while get_parse_result state = None && !i < len do
+                       parse_char state str.[!i];
+                       incr i
+               done;
+               match get_parse_result state with
+               | Some v -> Result v
+               | None -> Parse_incomplete state
 end
 
 
@@ -582,7 +655,7 @@ module Payload = struct
                mutable remaining_length: int64;
                max_payload_length: int64;
                mutable body: Buffer.t;
-               mutable trailers: header_fields
+               mutable headers: header_fields
        }
 
        let is_done s = s.cursor = Done
@@ -617,12 +690,12 @@ module Payload = struct
                | [] -> (* should never happen *) false
        with Not_found -> false
 
-       let get_content_length hdrs =
+       let get_content_length resp =
        try
-               match lookup_header "Content-Length" hdrs with
+               match lookup_header "Content-Length" resp.Response_header.headers with
                | v :: _ -> (try `Some (Int64.of_string v) with Failure _ -> `Error)
                | [] -> (* should never happen *) `None
-       with Not_found -> `None
+       with Not_found -> if resp.Response_header.version = HTTP09 then `HTTP09 else `None
 
        let content_type_is_multipart_byteranges hdrs =
        try
@@ -646,19 +719,20 @@ module Payload = struct
                remaining_length = -1L;
                max_payload_length = max_payload_length;
                body = Buffer.create 512;
-               trailers = []
+               headers = []
        }
        let init_from_response ?(max_payload_length=Int64.of_int (10*1024*1024)) resp =
                let hdrs = resp.Response_header.headers in
                let chunked = transfer_encoding_uses_chunked hdrs in
-               let content_length = get_content_length hdrs in
+               let content_length = get_content_length resp in
                let multipart_body = content_type_is_multipart_byteranges hdrs in
                let default = init_default_state max_payload_length in
                match resp.Response_header.status_code, content_length, chunked, multipart_body with
-               | Some sc, _, _, _
+               | sc, _, _, _
                        (* http://tools.ietf.org/html/rfc2616#section-4.4, Item 1. *)
                        when sc/100 = 1 || sc = 204 || sc = 304 -> No_payload
 
+               | _, `HTTP09, _, _  -> Payload default (* Connection: close *)
                | _, `Some 0L, _, _ -> No_payload
                | _, `Some l, _, _ ->  Payload { default with
                                                   content_length = Length l;
@@ -680,19 +754,11 @@ module Payload = struct
                let raise_bad_char () = raise_error (Parse_error (s.cursor, c)) in
                match s.cursor with
                | In_body ->
+                       Buffer.add_char s.body c;
                        if s.remaining_length > 0L then begin
-                               Buffer.add_char s.body c;
                                s.remaining_length <- Int64.pred s.remaining_length;
                                if s.remaining_length = 0L then s.cursor <- Done
                        end
-
-               | In_trailer hs ->
-                       Headers.parse_char hs c;
-                       if Headers.is_done hs then begin
-                               s.trailers <- List.rev hs.Headers.headers;
-                               s.cursor <- Done
-                       end
-
                | Start_chunk_length ->
                        if is_hex c then begin
                                s.remaining_length <- Int64.of_int (hex_value c);
@@ -744,5 +810,43 @@ module Payload = struct
                | Chunk_CR ->
                        if c = '\n' then s.cursor <- Start_chunk_length
                        else raise_bad_char ()
+               | In_trailer hs ->
+                       Headers.parse_char hs c;
+                       if Headers.is_done hs then begin
+                               s.headers <- List.rev hs.Headers.headers;
+                               s.cursor <- Done
+                       end
                | Done -> raise_error (Internal_error "parse called on finished request!")
+
+       type t =
+       {
+               content: Buffer.t;
+               trailers: header_fields
+       }
+
+       type parse_result =
+               | Result of t
+               | Parse_incomplete of state
+
+       let get_parse_result state =
+               match state.cursor with
+               | Done -> Some { content = state.body;
+                                trailers = state.headers
+                              }
+               | _    -> None
+
+       let parse state str =
+               let len = String.length str in
+               let i = ref 0 in
+               while get_parse_result state = None && !i < len do
+                       parse_char state str.[!i];
+                       incr i
+               done;
+               match get_parse_result state with
+               | Some v -> Result v
+               | None -> Parse_incomplete state
+
+       let connection_closed state =
+               if state.content_length = Connection_close then
+               state.cursor <- Done
 end