(* $Id: nethttp.mlp 1588 2011-04-28 13:59:54Z gerd $ * ---------------------------------------------------------------------- * Nethttp: Basic definitions for the HTTP protocol *) type protocol_version = int * int type protocol_attribute = [ `Secure_https ] type protocol = [ `Http of (protocol_version * protocol_attribute list) | `Other ] let string_of_protocol = function | `Http((m,n),_) -> "HTTP/" ^ string_of_int m ^ "." ^ string_of_int n | `Other -> failwith "string_of_protocol" let http_re = Netstring_str.regexp "HTTP/\\([0-9]+\\)\\.\\([0-9]+\\)$" let protocol_of_string s = match Netstring_str.string_match http_re s 0 with | Some m -> ( try `Http ((int_of_string (Netstring_str.matched_group m 1 s), int_of_string (Netstring_str.matched_group m 2 s)), []) with Failure _ -> `Other (* Probably denial-of-service attack! *) ) | None -> `Other type http_status = (* 1xx: (informational) *) [ `Continue | `Switching_protocols (* 2xx: (successful) *) | `Ok | `Created | `Accepted | `Non_authoritative | `No_content | `Reset_content | `Partial_content (* 3xx: (redirection) *) | `Multiple_choices | `Moved_permanently | `Found | `See_other | `Not_modified | `Use_proxy | `Temporary_redirect (* 4xx: (client error) *) | `Bad_request | `Unauthorized | `Payment_required | `Forbidden | `Not_found | `Method_not_allowed | `Not_acceptable | `Proxy_auth_required | `Request_timeout | `Conflict | `Gone | `Length_required | `Precondition_failed | `Request_entity_too_large | `Request_uri_too_long | `Unsupported_media_type | `Requested_range_not_satisfiable | `Expectation_failed (* 5xx: (server error) *) | `Internal_server_error | `Not_implemented | `Bad_gateway | `Service_unavailable | `Gateway_timeout | `Http_version_not_supported ] let int_of_http_status = function (* 1xx: (informational) *) | `Continue -> 100 | `Switching_protocols -> 101 (* 2xx: (successful) *) | `Ok -> 200 | `Created -> 201 | `Accepted -> 202 | `Non_authoritative -> 203 | `No_content -> 204 | `Reset_content -> 205 | `Partial_content -> 206 (* 3xx: (redirection) *) | `Multiple_choices -> 300 | `Moved_permanently -> 301 | `Found -> 302 | `See_other -> 303 | `Not_modified -> 304 | `Use_proxy -> 305 | `Temporary_redirect -> 307 (* 4xx: (client error) *) | `Bad_request -> 400 | `Unauthorized -> 401 | `Payment_required -> 402 | `Forbidden -> 403 | `Not_found -> 404 | `Method_not_allowed -> 405 | `Not_acceptable -> 406 | `Proxy_auth_required -> 407 | `Request_timeout -> 408 | `Conflict -> 409 | `Gone -> 410 | `Length_required -> 411 | `Precondition_failed -> 412 | `Request_entity_too_large -> 413 | `Request_uri_too_long -> 414 | `Unsupported_media_type -> 415 | `Requested_range_not_satisfiable -> 416 | `Expectation_failed -> 417 (* 5xx: (server error) *) | `Internal_server_error -> 500 | `Not_implemented -> 501 | `Bad_gateway -> 502 | `Service_unavailable -> 503 | `Gateway_timeout -> 504 | `Http_version_not_supported -> 505 let string_of_http_status = function (* 1xx: (informational) *) | `Continue -> "Continue" | `Switching_protocols -> "Switching Protocols" (* 2xx: (successful) *) | `Ok -> "OK" | `Created -> "Created" | `Accepted -> "Accepted" | `Non_authoritative -> "Non-authoritative Information" | `No_content -> "No Content" | `Reset_content -> "Reset Content" | `Partial_content -> "Partial Content" (* 3xx: (redirection) *) | `Multiple_choices -> "Multiple Choices" | `Moved_permanently -> "Moved Permanently" | `Found -> "Found" | `See_other -> "See Other" | `Not_modified -> "Not Modified" | `Use_proxy -> "Use Proxy" | `Temporary_redirect -> "Temporary Redirect" (* 4xx: (client error) *) | `Bad_request -> "Bad Request" | `Unauthorized -> "Unauthorized" | `Payment_required -> "Payment Required" | `Forbidden -> "Forbidden" | `Not_found -> "Not Found" | `Method_not_allowed -> "Method Not Allowed" | `Not_acceptable -> "Not Acceptable" | `Proxy_auth_required -> "Proxy Authorization Required" | `Request_timeout -> "Request Timeout" | `Conflict -> "Conflict" | `Gone -> "Gone" | `Length_required -> "Length Required" | `Precondition_failed -> "Precondition Failed" | `Request_entity_too_large -> "Request Entity Too Large" | `Request_uri_too_long -> "Request URI Too Long" | `Unsupported_media_type -> "Unsupported Media Type" | `Requested_range_not_satisfiable -> "Request Range Not Satisfiable" | `Expectation_failed -> "Expectation Failed" (* 5xx: (server error) *) | `Internal_server_error -> "Internal Server Error" | `Not_implemented -> "Not Implemented" | `Bad_gateway -> "Bad Gateway" | `Service_unavailable -> "Service Unavailable" | `Gateway_timeout -> "Gateway Timeout" | `Http_version_not_supported -> "HTTP Version Not Supported" let http_status_of_int = function (* 1xx: (informational) *) | 100 -> `Continue | 101 -> `Switching_protocols (* 2xx: (successful) *) | 200 -> `Ok | 201 -> `Created | 202 -> `Accepted | 203 -> `Non_authoritative | 204 -> `No_content | 205 -> `Reset_content | 206 -> `Partial_content (* 3xx: (redirection) *) | 300 -> `Multiple_choices | 301 -> `Moved_permanently | 302 -> `Found | 303 -> `See_other | 304 -> `Not_modified | 305 -> `Use_proxy | 307 -> `Temporary_redirect (* 4xx: (client error) *) | 400 -> `Bad_request | 401 -> `Unauthorized | 402 -> `Payment_required | 403 -> `Forbidden | 404 -> `Not_found | 405 -> `Method_not_allowed | 406 -> `Not_acceptable | 407 -> `Proxy_auth_required | 408 -> `Request_timeout | 409 -> `Conflict | 410 -> `Gone | 411 -> `Length_required | 412 -> `Precondition_failed | 413 -> `Request_entity_too_large | 414 -> `Request_uri_too_long | 415 -> `Unsupported_media_type | 416 -> `Requested_range_not_satisfiable | 417 -> `Expectation_failed (* 5xx: (server error) *) | 500 -> `Internal_server_error | 501 -> `Not_implemented | 502 -> `Bad_gateway | 503 -> `Service_unavailable | 504 -> `Gateway_timeout | 505 -> `Http_version_not_supported | _ -> raise Not_found type http_method = string * string (** Method name, URI *) type cache_control_token = [ `No_store | `Max_age of int | `Max_stale of int option | `Min_fresh of int | `No_transform | `Only_if_cached | `Public | `Private of string list | `No_cache of string list | `Must_revalidate | `Proxy_revalidate | `S_maxage of int | `Extension of string * string option ] type etag = [ `Weak of string | `Strong of string ] let weak_validator_match e1 e2 = match (e1,e2) with | (`Strong s1, `Strong s2) -> s1 = s2 | (`Strong s1, `Weak w2) -> s1 = w2 | (`Weak w1, `Strong s2) -> w1 = s2 | (`Weak w1, `Weak w2) -> w1 = w2 let strong_validator_match e1 e2 = match (e1,e2) with | (`Strong s1, `Strong s2) -> s1 = s2 | _ -> false exception Bad_header_field of string class type http_header = Netmime.mime_header class type http_header_ro = Netmime.mime_header_ro class type http_trailer = Netmime.mime_header class type http_trailer_ro = Netmime.mime_header_ro type netscape_cookie = { cookie_name : string; cookie_value : string; cookie_expires : float option; cookie_domain : string option; cookie_path : string option; cookie_secure : bool; } type cookie = netscape_cookie let status_re = Netstring_str.regexp "^\\([0-9]+\\)\\([ \t]+\\(.*\\)\\)?$" let status_of_cgi_header hdr = let (code, phrase) = try let status = hdr # field "Status" in ( match Netstring_str.string_match status_re status 0 with | Some m -> (int_of_string (Netstring_str.matched_group m 1 status), (try Netstring_str.matched_group m 3 status with Not_found -> "") ) | None -> failwith "Bad Status response header field" (* Don't know what to do *) ) with Not_found -> (* Maybe there is a [Location] header: *) ( try let _location = hdr # field "Location" in (302, "Found") with Not_found -> (* Default: 200 OK *) (200, "OK") ) in (* Repair [phrase] if empty: *) let phrase = if phrase = "" then ( try string_of_http_status (http_status_of_int code) with Not_found -> "Unknown" ) else phrase in (code, phrase) ;; let query_re = Netstring_str.regexp "^\\([^?]*\\)\\?\\(.*\\)$" let decode_query req_uri = match Netstring_str.string_match query_re req_uri 0 with | Some m -> (Netstring_str.matched_group m 1 req_uri, Netstring_str.matched_group m 2 req_uri) | None -> (req_uri, "") let host4_re = Netstring_str.regexp "\\([^]: \t[]+\\)\\(:\\([0-9]+\\)\\)?$" (* CHECK *) let host6_re = Netstring_str.regexp "\\[\\([^ \t]+\\)\\]\\(:\\([0-9]+\\)\\)?$" let split_host_port s = match Netstring_str.string_match host4_re s 0 with | Some m -> let host_name = Netstring_str.matched_group m 1 s in let host_port = try Some(int_of_string(Netstring_str.matched_group m 3 s)) with | Not_found -> None in (host_name, host_port) | None -> ( match Netstring_str.string_match host6_re s 0 with | Some m -> let host_name = Netstring_str.matched_group m 1 s in let host_port = try Some(int_of_string(Netstring_str.matched_group m 3 s)) with | Not_found -> None in (host_name, host_port) | None -> failwith "Invalid hostname" ) let uripath_encode s = let l = Neturl.split_path s in let l' = List.map (Netencoding.Url.encode ~plus:false) l in Neturl.join_path l' let uripath_decode s = let l = Neturl.split_path s in let l' = List.map (fun u -> let u' = Netencoding.Url.decode ~plus:false u in if String.contains u' '/' then failwith "Nethttp.uripath_decode"; u') l in Neturl.join_path l' let rev_split is_cut s = (* exported *) let rec seek_cut acc i0 i1 = if i1 >= String.length s then (String.sub s i0 (i1 - i0)) :: acc else if is_cut(String.unsafe_get s i1) then skip ((String.sub s i0 (i1 - i0)) :: acc) (i1 + 1) (i1 + 1) else seek_cut acc i0 (i1 + 1) and skip acc i0 i1 = if i1 >= String.length s then acc else if is_cut(String.unsafe_get s i1) then skip acc i0 (i1 + 1) else seek_cut acc i1 i1 in skip [] 0 0 module Cookie = struct (* This module has been written by Christophe Troestler. For full copyright message see netcgi.ml *) (* Cookies are chosen to be mutable because they are stored on the client -- there is no rollback possible -- and mutability kind of reflects that... *) type t = { mutable name : string; mutable value : string; mutable max_age : int option; mutable domain : string option; mutable path : string option; mutable secure : bool; mutable comment : string; mutable comment_url : string; mutable ports : int list option; } let make ?max_age ?domain ?path ?(secure=false) ?(comment="") ?(comment_url="") ?ports name value = { name = name; value = value; max_age = max_age; domain = domain; path = path; secure = secure; comment = comment; comment_url = comment_url; ports = ports; } (* Old version of cookies *) let of_netscape_cookie c = { name = c.cookie_name; value = c.cookie_value; max_age = (match c.cookie_expires with | None -> None | Some t -> Some(truncate(t -. Unix.time()))); domain = c.cookie_domain; path = c.cookie_path; secure = c.cookie_secure; comment = ""; comment_url = ""; ports = None } let to_netscape_cookie cookie = { cookie_name = cookie.name; cookie_value = cookie.value; cookie_expires = (match cookie.max_age with | None -> None | Some t -> Some(float t +. Unix.time())); cookie_domain = cookie.domain; cookie_path = cookie.path; cookie_secure = cookie.secure; } let name cookie = cookie.name let value cookie = cookie.value let max_age cookie = cookie.max_age let domain cookie = cookie.domain let path cookie = cookie.path let secure cookie = cookie.secure let comment cookie = cookie.comment let comment_url cookie = cookie.comment_url let ports cookie = cookie.ports let set_value cookie v = cookie.value <- v let set_max_age cookie t = cookie.max_age <- t let set_domain cookie dom = cookie.domain <- dom let set_path cookie s = cookie.path <- s let set_secure cookie b = cookie.secure <- b let set_comment cookie s = cookie.comment <- s let set_comment_url cookie s = cookie.comment_url <- s let set_ports cookie p = cookie.ports <- p (* Set -------------------------------------------------- *) (* Escape '"', '\\',... and surround the string with quotes. *) let escape s0 = let len = String.length s0 in let encoded_length = ref len in for i = 0 to len - 1 do match String.unsafe_get s0 i with | '\"' | '\\' | '\n' | '\r' -> incr encoded_length | '\000' .. '\031' -> decr encoded_length (* ignore *) | _ -> () done; let s = String.create (!encoded_length + 2) in String.unsafe_set s 0 '\"'; let j = ref 1 in for i = 0 to len - 1 do (match String.unsafe_get s0 i with | '\"' | '\\' as c -> String.unsafe_set s !j '\\'; incr j; String.unsafe_set s !j c; incr j | '\n' -> String.unsafe_set s !j '\\'; incr j; String.unsafe_set s !j 'n'; incr j | '\r' -> String.unsafe_set s !j '\\'; incr j; String.unsafe_set s !j 'r'; incr j | '\000' .. '\031' -> () (* Ignore these control chars, useless for comments *) | c -> String.unsafe_set s !j c; incr j ); done; String.unsafe_set s !j '\"'; s (* [gen_cookie c] returns a buffer containing an attribute suitable for "Set-Cookie" (RFC 2109) and "Set-Cookie2" (RFC 2965). which is backward compatible with Netscape spec. It is the minimal denominator. *) let gen_cookie c = let buf = Buffer.create 128 in (* Encode, do not quote, key-val for compatibility with old browsers. *) Buffer.add_string buf (Netencoding.Url.encode ~plus:false c.name); Buffer.add_string buf "="; Buffer.add_string buf (Netencoding.Url.encode ~plus:false c.value); Buffer.add_string buf ";Version=1"; (* FIXME: Although values of Domain and Path can be quoted since RFC2109, it seems that browsers do not understand them -- they take the quotes as part of the value. One way to get correct headers is to strip [d] and [p] of unsafe chars -- if they have any. *) (match c.domain with | None -> () | Some d -> Buffer.add_string buf ";Domain="; Buffer.add_string buf d); (match c.path with | None -> () | Some p -> Buffer.add_string buf ";Path="; Buffer.add_string buf p); if c.secure then Buffer.add_string buf ";secure"; (match c.max_age with | None -> () | Some s -> Buffer.add_string buf ";Max-Age="; Buffer.add_string buf (if s > 0 then string_of_int s else "0"); (* For compatibility with old browsers: *) Buffer.add_string buf ";Expires="; Buffer.add_string buf (if s > 0 then Netdate.mk_mail_date (Unix.time() +. float s) else "Thu, 1 Jan 1970 00:00:00 GMT"); ); if c.comment <> "" then ( Buffer.add_string buf ";Comment="; Buffer.add_string buf (escape c.comment); ); buf let set_set_cookie_ct (http_header:#Netmime.mime_header) cookies = let add_cookie (c1, c2) c = let buf = gen_cookie c in (* In any case, we set a "Set-Cookie" header *) let c1 = (Buffer.contents buf) :: c1 in let c2 = if c.comment_url = "" && c.ports = None then c2 else ( (* When this is relevant, also set a "Set-Cookie2" header *) if c.comment_url <> "" then ( Buffer.add_string buf ";CommentURL="; Buffer.add_string buf (escape c.comment_url)); (match c.ports with | None -> () | Some p -> Buffer.add_string buf ";Port=\""; Buffer.add_string buf (String.concat "," (List.map string_of_int p)); Buffer.add_string buf "\"" ); (Buffer.contents buf) :: c2 ) in (c1, c2) in let cookie, cookie2 = List.fold_left add_cookie ([], []) cookies in http_header#update_multiple_field "Set-Cookie" cookie; (* "Set-Cookie2" must come after in order, when they are understood, to override the "Set-Cookie". *) http_header#update_multiple_field "Set-Cookie2" cookie2 (* Get -------------------------------------------------- *) (* According to RFC 2068: quoted-string = ( <"> *(qdtext) <"> ) qdtext = <any TEXT except '\"'> quoted-pair = "\\" CHAR As there a no details, we decode the usual escapes and treat other "\x" as simply "x". *) let unescape_range s low up = if low >= up then "" else let len = up - low in let s = String.sub s low len in let rec decode i j = if i < len then ( match String.unsafe_get s i with | '\\' -> let i = i + 1 in if i < len then ( (match String.unsafe_get s i with | '\"' | '\\' as c -> String.unsafe_set s j c | 'n' -> String.unsafe_set s j '\n' | 'r' -> String.unsafe_set s j '\r' | 't' -> String.unsafe_set s j '\t' | c -> String.unsafe_set s j c ); decode (i + 1) (j + 1) ) else j | c -> String.unsafe_set s j c; decode (i + 1) (j + 1) ) else j in let j = decode 0 0 in if j < len then String.sub s 0 j else s let ports_of_string s = let l = rev_split (fun c -> c = ',' || c = ' ') s in List.fold_left (fun pl p -> try int_of_string p :: pl with _ -> pl) [] l (* Given a new key-val data, update the list of cookies accordingly (new cookie or update attributes of the current one). *) let add_key_val key value cl = if key <> "" && String.unsafe_get key 0 = '$' then (* Keys starting with '$' are for control; ignore the ones we do not know about. *) (match cl with | [] -> [] | c :: _ -> (if key = "$Path" then c.path <- Some value else if key = "$Domain" then c.domain <- Some value else if key = "$Port" then c.ports <- Some (ports_of_string value)); cl ) else make key value :: cl let decode_range s start _end = Netencoding.Url.decode ~pos:start ~len:(_end - start) s (* The difference between version 0 and version 1 cookies is that the latter start with $Version (present 1st or omitted). Our decoding function can handle both versions transparently, so $Version is ignored. In the absence of "=", the string is treated as the VALUE. *) (* [get_key cs i0 i len] scan the cookie string [cs] and get the key-val pairs. keys and values are stripped of heading and trailing spaces, except for quoted values. *) let rec get_key cs i0 i len cl = if i >= len then let value = decode_range cs i0 len in if value = "" then cl else make "" value :: cl else match String.unsafe_get cs i with | ',' | ';' -> (* No "=", interpret as a value as Mozilla does. We choose this over MSIE which is reported to return just "n" instead of "n=" when the value is empty. *) let cl = make "" (decode_range cs i0 i) :: cl in skip_space_before_key cs (i + 1) len cl | '=' -> let i1 = i + 1 in skip_value_space cs i1 len (decode_range cs i0 i) cl | c -> get_key cs i0 (i + 1) len cl and skip_space_before_key cs i len cl = if i >= len then cl else match String.unsafe_get cs i with | ' ' | '\t' | '\n' | '\r' -> skip_space_before_key cs (i + 1) len cl | _ -> get_key cs i i len cl and skip_value_space cs i len key cl = if i >= len then add_key_val key "" cl (* no value *) else match String.unsafe_get cs i with | ' ' | '\t' | '\n' | '\r' -> (* skip linear white space *) skip_value_space cs (i + 1) len key cl | '\"' -> get_quoted_value cs (i + 1) (i + 1) len key cl | _ -> get_value cs i i len key cl and get_value cs i0 i len key cl = if i >= len then add_key_val key (decode_range cs i0 len) cl else match String.unsafe_get cs i with | ',' | ';' -> let cl = add_key_val key (decode_range cs i0 i) cl in (* Usually there is a space after ';' to skip *) skip_space_before_key cs (i + 1) len cl | _ -> get_value cs i0 (i + 1) len key cl and get_quoted_value cs i0 i len key cl = if i >= len then (* quoted string not closed; try anyway *) add_key_val key (unescape_range cs i0 len) cl else match String.unsafe_get cs i with | '\\' -> get_quoted_value cs i0 (i + 2) len key cl | '\"' -> let cl = add_key_val key (unescape_range cs i0 i) cl in skip_to_next cs (i + 1) len cl | _ -> get_quoted_value cs i0 (i + 1) len key cl and skip_to_next cs i len cl = if i >= len then cl else match String.unsafe_get cs i with | ',' | ';' -> skip_space_before_key cs (i + 1) len cl | _ -> skip_to_next cs (i + 1) len cl let get_cookie_ct (http_header:#http_header_ro) = let cookies = http_header#multiple_field "Cookie" in let cl = List.fold_left (fun cl cs -> get_key cs 0 0 (String.length cs) cl) [] cookies in (* The order of cookies is important for the Netscape ones since "more specific path mapping should be sent before cookies with less specific path mappings" -- for those, there will be only a single "Cookie" line. *) List.rev cl end module Header = struct open Netmime open Mimestring (* As scanner we use the scanner for mail header fields from Mimestring. It * is very configurable. *) let std_special_chars = [ ','; ';'; '=' ] (* CHECK: Maybe we should add more characters, e.g. '@'. They are not * used in HTTP, and including them here would cause that field values * containing them are rejected. Maybe we want that. *) let scan_value ?(specials = std_special_chars) s = let scanner = create_mime_scanner ~specials ~scan_options:[] s in Stream.from (fun _ -> Some (snd (scan_token scanner))) (* ---- Parser combinators for stream parsers: ---- *) let rec parse_comma_separated_list subparser stream = (* The [subparser] is required to return its value when it finds a * comma (i.e. [Special ','], or when it finds [End]. These tokens * must not be swallowed. *) match stream with parser | [< expr = subparser; rest = parse_comma_separated_rest subparser >] -> expr :: rest | [< >] -> [] and parse_comma_separated_rest subparser stream = match stream with parser | [< '(Special ','); _ = parse_commas; list = parse_comma_separated_list subparser >] -> list | [< 'End >] -> [] and parse_commas stream = match stream with parser | [< '(Special ','); _ = parse_commas >] -> () | [< >] -> () let merge_lists mh fieldparser fieldname = let fields = mh # multiple_field fieldname in if fields = [] then raise Not_found; List.flatten (List.map fieldparser fields) let parse_field mh fn_name f_parse fieldname = try let field = mh # field fieldname in f_parse (scan_value field) with | Stream.Failure | Stream.Error _ -> raise (Bad_header_field fn_name) let parse_comma_separated_field ?specials mh fn_name f_parse fieldname = let fieldparser field = try parse_comma_separated_list f_parse (scan_value ?specials field) with | Stream.Failure | Stream.Error _ -> raise (Bad_header_field fn_name) in merge_lists mh fieldparser fieldname (* ----- Common parsers/printer: ---- *) let parse_token_list mh fn_name fieldname = let parse_token stream = match stream with parser | [< '(Atom tok) >] -> tok in parse_comma_separated_field mh fn_name parse_token fieldname let parse_token_or_qstring stream = match stream with parser | [< '(Atom tok) >] -> tok | [< '(QString v) >] -> v let rec parse_params stream = match stream with parser | [< '(Special ';'); '(Atom name); '(Special '='); v = parse_token_or_qstring; rest = parse_params >]-> (name,v) :: rest | [< >] -> [] let parse_extended_token_list mh fn_name fieldname = (* token [ '=' (token|qstring) ( ';' token '=' (token|qstring) ) * ] *) let rec parse_extended_token stream = match stream with parser | [< '(Atom tok); extension = parse_equation >] -> ( match extension with Some (eq_val, params) -> (tok, Some eq_val, params) | None -> (tok, None, []) ) and parse_equation stream = match stream with parser | [< '(Special '='); v = parse_token_or_qstring; params = parse_params >] -> Some (v, params) | [< >] -> None in parse_comma_separated_field mh fn_name parse_extended_token fieldname let qstring_indicator_re = Netstring_str.regexp "[]\\\"()<>@,;:/[?={} \x00-\x1f\x7f]" (* Netstring_pcre.regexp "[\\\\\"()<>@,;:/[\\]?={} \\x00-\\x1f\\x7f]" *) let qstring_re = Netstring_str.regexp "[\\\"]" (* Netstring_pcre.regexp "[\\\\\\\"]" *) let qstring_of_value s = (* Returns a qstring *) "\"" ^ Netstring_str.global_replace qstring_re "\\\\\\0" s ^ "\"" (* Escape qstring_re with a backslash *) let string_of_value s = (* Returns a token or a qstring, depending on the value of [s] *) try ignore(Netstring_str.search_forward qstring_indicator_re s 0); qstring_of_value s with Not_found -> s let string_of_params l = if l = [] then "" else ";" ^ String.concat ";" (List.map (fun (n,s) -> n ^ "=" ^ string_of_value s) l) let string_of_extended_token fn_name = function | (tok, None, []) -> tok | (tok, None, _) -> invalid_arg fn_name | (tok, Some eq_val, params) -> tok ^ "=" ^ eq_val ^ string_of_params params let parse_parameterized_token_list mh fn_name fieldname = (* token ( ';' token '=' (token|qstring) ) * *) let rec parse_parameterized_token stream = match stream with parser | [< '(Atom tok); params = parse_params >] -> (tok, params) in parse_comma_separated_field mh fn_name parse_parameterized_token fieldname let string_of_parameterized_token (tok, params) = tok ^ string_of_params params let q_split ( l : (string * (string * string) list) list ) : (string * (string * string) list * (string * string) list) list = (* Find the "q" param, and split [params] at that position *) let rec split params = match params with | [] -> ([], []) | ("q", q) :: rest -> ([], params) | other :: rest -> let before, after = split rest in (other :: before), after in List.map (fun (tok, params) -> let before, after = split params in (tok, before, after)) l let q_merge fn_name (tok, params, q_params) = if List.mem_assoc "q" params then invalid_arg fn_name; ( match q_params with | ( "q", _ ) :: _ | [] -> (tok, (params @ q_params)) | _ -> invalid_arg fn_name ) let date_of_string fn_name s = try Netdate.parse_epoch s with Invalid_argument _ -> raise(Bad_header_field fn_name) let string_of_date f = Netdate.format ~fmt:"%a, %d %b %Y %H:%M:%S GMT" (Netdate.create ~zone:0 f) let sort_by_q ?(default=1.0) toks_with_params = (* Sorts [toks_with_params] such that the highest [q] values come first. * Tokens with a [q] value of 0 are removed. Tokens without [q] value * are assumed to have the [default] value. This is also done with * unparseable [q] values. *) List.map snd (List.stable_sort (fun (q1, tok_param1) (q2, tok_param2) -> Pervasives.compare q2 q1) (List.filter (fun (q, tok_param) -> q > 0.0) (List.map (fun (tok, params) -> try let q_str = List.assoc "q" params in (float_of_string q_str, (tok, params)) with | Not_found -> (default, (tok, params)) | Failure _ -> (default, (tok, params)) ) toks_with_params))) let sort_by_q' ?default tok_with_params_and_qparams = List.map (fun ((tok, tok_params), q_params) -> (tok, tok_params, q_params)) (sort_by_q ?default (List.map (fun (tok, tok_params, q_params) -> ((tok, tok_params), q_params)) tok_with_params_and_qparams)) (* ---- The field accessors: ---- *) let get_accept mh = q_split (parse_parameterized_token_list mh "Nethttp.get_accept" "Accept") let set_accept mh av = let s = String.concat "," (List.map (fun triple -> string_of_parameterized_token (q_merge "Nethttp.set_accept" triple)) av) in mh # update_field "Accept" s let best_media_type mh supp = let supp' = (* All of [supp] not mentioned in the [Accept] field *) let toks = try get_accept mh with Not_found -> [] in List.filter (fun supp_type -> not (List.exists (fun (t,_,_) -> t=supp_type) toks)) supp in let rec find_best toks = match toks with | (tok, params, qparams) :: toks' -> ( if List.mem tok supp then (tok, params) else let (main_type, sub_type) = Mimestring.split_mime_type tok in if sub_type = "*" then ( try (List.find (fun supp_type -> (main_type = "*") || (sub_type = "*" && main_type = fst(Mimestring.split_mime_type supp_type)) ) supp', params) with Not_found -> find_best toks' ) else find_best toks' ) | [] -> (* Nothing acceptable: *) ("", []) in try let mt_list = sort_by_q' (get_accept mh) in (* or Not_found *) find_best mt_list with Not_found -> ("*/*", []) let get_accept_charset mh = parse_parameterized_token_list mh "Nethttp.get_accept_charset" "Accept-Charset" let set_accept_charset mh l = mh # update_field "Accept-Charset" (String.concat "," (List.map string_of_parameterized_token l)) let best_tok_of_list toks supp = let tok = List.find (fun tok -> tok = "*" || List.mem tok supp) toks in if tok = "*" then List.find (fun tok -> not (List.mem tok toks)) supp else tok let best_charset mh supp = try let toks_with_params = get_accept_charset mh in (* or Not_found *) (* Special handling of ISO-8859-1: *) let toks_with_params' = if not(List.mem_assoc "*" toks_with_params) && not(List.exists (fun (tok,_) -> String.lowercase tok = "iso-8859-1") toks_with_params) then toks_with_params @ [ "ISO-8859-1", ["q", "1.0"] ] else toks_with_params in let toks' = List.map fst (sort_by_q toks_with_params') in best_tok_of_list toks' supp with Not_found -> "*" let get_accept_encoding mh = parse_parameterized_token_list mh "Nethttp.get_accept_encoding" "Accept-Encoding" let set_accept_encoding mh l = mh # update_field "Accept-Encoding" (String.concat "," (List.map string_of_parameterized_token l)) let best_encoding mh supp = try let toks_with_params = sort_by_q (get_accept_encoding mh) in best_tok_of_list (List.map fst toks_with_params) supp with Not_found -> "identity" let get_accept_language mh = parse_parameterized_token_list mh "Nethttp.get_accept_language" "Accept-Language" let set_accept_language mh l = mh # update_field "Accept-Language" (String.concat "," (List.map string_of_parameterized_token l)) let get_accept_ranges mh = parse_token_list mh "Nethttp.get_accept_ranges" "Accept-Ranges" let set_accept_ranges mh toks = mh # update_field "Accept-Ranges" (String.concat "," toks) let get_age mh = try float_of_string (mh # field "Age") with Failure _ -> raise(Bad_header_field "Nethttp.get_age") let set_age mh v = mh # update_field "Age" (Printf.sprintf "%0.f" v) let get_allow mh = parse_token_list mh "Nethttp.get_allow" "Allow" let set_allow mh toks = mh # update_field "Allow" (String.concat "," toks) let comma_split_re = Netstring_str.regexp "\\([ \t]*,\\)+[ \t]*" let comma_split = Netstring_str.split comma_split_re let parse_opt_eq_token stream = match stream with parser | [< '(Special '='); v = (fun stream -> match stream with parser | [< '(Atom v) >] -> v | [< '(QString v) >] -> v); >] -> Some v | [< >] -> None let parse_cc_directive stream = match stream with parser | [< '(Atom "no-cache"); name_opt = parse_opt_eq_token >] -> ( match name_opt with | None -> `No_cache [] | Some names -> `No_cache(comma_split names) ) | [< '(Atom "no-store") >] -> `No_store | [< '(Atom "max-age"); '(Special '='); '(Atom seconds) >] -> `Max_age(int_of_string seconds) | [< '(Atom "max-stale"); delta_opt = parse_opt_eq_token >] -> ( match delta_opt with | None -> `Max_stale None | Some seconds -> `Max_stale(Some(int_of_string seconds)) ) | [< '(Atom "min-fresh"); '(Special '='); '(Atom seconds) >] -> `Min_fresh(int_of_string seconds) | [< '(Atom "no-transform") >] -> `No_transform | [< '(Atom "only-if-cached") >] -> `Only_if_cached | [< '(Atom "public") >] -> `Public | [< '(Atom "private"); name_opt = parse_opt_eq_token >] -> ( match name_opt with | None -> `Private [] | Some names -> `Private(comma_split names) ) | [< '(Atom "must-revalidate") >] -> `Must_revalidate | [< '(Atom "proxy-revalidate") >] -> `Proxy_revalidate | [< '(Atom "s-maxage"); '(Special '='); '(Atom seconds)>] -> `S_maxage(int_of_string seconds) | [< '(Atom extension); val_opt = parse_opt_eq_token >] -> `Extension(extension, val_opt) let get_cache_control mh = parse_comma_separated_field mh "Nethttp.get_cache_control" parse_cc_directive "Cache-Control" let set_cache_control mh l = let s = String.concat "," (List.map (function | `No_store -> "no-store" | `Max_age n -> "max-age=" ^ string_of_int n | `Max_stale None -> "max-stale" | `Max_stale(Some n) -> "max-stale=" ^ string_of_int n | `Min_fresh n -> "min-fresh=" ^ string_of_int n | `No_transform -> "no-transform" | `Only_if_cached -> "only-if-cached" | `Public -> "public" | `Private names -> "private=\"" ^ String.concat "," names ^ "\"" | `No_cache [] -> "no-cache" | `No_cache names -> "no-cache=\"" ^ String.concat "," names ^ "\"" | `Must_revalidate -> "must-revalidate" | `Proxy_revalidate -> "proxy-revalidate" | `S_maxage n -> "s-maxage=" ^ string_of_int n | `Extension(tok,None) -> tok | `Extension(tok, Some param) -> tok ^ "=" ^ string_of_value param ) l) in mh # update_field "Cache-Control" s let get_connection mh = parse_token_list mh "Nethttp.get_connection" "Connection" let set_connection mh toks = mh # update_field "Connection" (String.concat "," toks) let get_content_encoding mh = parse_token_list mh "Nethttp.get_content_encoding" "Content-Encoding" let set_content_encoding mh toks = mh # update_field "Content-Encoding" (String.concat "," toks) let get_content_language mh = parse_token_list mh "Nethttp.get_content_language" "Content-Language" let set_content_language mh toks = mh # update_field "Content-Language" (String.concat "," toks) let get_content_length mh = try Int64.of_string (mh # field "Content-Length") with Failure _ -> raise (Bad_header_field "Nethttp.get_content_length") let set_content_length mh n = mh # update_field "Content-Length" (Int64.to_string n) let get_content_location mh = mh # field "Content-Location" let set_content_location mh s = mh # update_field "Content-Location" s let get_content_md5 mh = mh # field "Content-MD5" let set_content_md5 mh s = mh # update_field "Content-MD5" s let parse_byte_range_resp_spec stream = match stream with parser | [< '(Special '*') >] -> None | [< '(Atom first); '(Special '-'); '(Atom last) >] -> Some(Int64.of_string first, Int64.of_string last) let parse_byte_range_resp_length stream = match stream with parser | [< '(Special '*') >] -> None | [< '(Atom length) >] -> Some(Int64.of_string length) let parse_content_range_spec stream = match stream with parser | [< '(Atom "bytes"); br=parse_byte_range_resp_spec; '(Special '/'); l=parse_byte_range_resp_length; 'End >] -> `Bytes(br,l) let get_content_range mh = let s = mh # field "Content-Range" in let stream = scan_value ~specials:[ ','; ';'; '='; '*'; '-'; '/' ] s in try parse_content_range_spec stream with | Stream.Failure | Stream.Error _ | Failure _ -> raise (Bad_header_field "Nethttp.get_content_range") let set_content_range mh (`Bytes(range_opt,length_opt)) = let s = ( match range_opt with | Some (first,last) -> Int64.to_string first ^ "-" ^ Int64.to_string last | None -> "*" ) ^ "/" ^ ( match length_opt with | Some length -> Int64.to_string length | None -> "*" ) in mh # update_field "Content-Range" s let get_content_type mh = try List.hd (parse_parameterized_token_list mh "Nethttp.get_content_type" "Content-Type") with Failure _ -> raise(Bad_header_field "Nethttp.get_content_type") let set_content_type mh (tok,params) = mh # update_field "Content-Type" (string_of_parameterized_token (tok,params)) let get_date mh = date_of_string "Nethttp.get_date" (mh # field "Date") let set_date mh d = mh # update_field "Date" (string_of_date d) let parse_etag_token stream = match stream with parser | [< '(Atom "W"); '(Special '/'); '(QString e) >] -> `Weak e | [< '(QString e) >] -> `Strong e let parse_etag stream = match stream with parser | [< etag=parse_etag_token; 'End >] -> etag let get_etag mh = let s = mh # field "ETag" in let stream = scan_value ~specials:[ ','; ';'; '='; '/' ] s in try parse_etag stream with | Stream.Failure | Stream.Error _ | Failure _ -> raise (Bad_header_field "Nethttp.get_etag") let string_of_etag = function | `Weak s -> "W/" ^ qstring_of_value s | `Strong s -> qstring_of_value s let set_etag mh etag = mh # update_field "ETag" (string_of_etag etag) let get_expect mh = parse_extended_token_list mh "Nethttp.get_expect" "Expect" let set_expect mh expectation = mh # update_field "Expect" (String.concat "," (List.map (string_of_extended_token "Nethttp.set_expect") expectation)) let get_expires mh = date_of_string "Nethttp.get_expires" (mh # field "Expires") let set_expires mh d = mh # update_field "Expires" (string_of_date d) let get_from mh = mh # field "From" let set_from mh v = mh # update_field "From" v let get_host mh = let s = mh # field "Host" in try split_host_port s with | Failure _ -> raise(Bad_header_field "Nethttp.get_host") let set_host mh (host,port_opt) = let s = host ^ ( match port_opt with Some p -> ":" ^ string_of_int p | None -> "") in mh # update_field "Host" s let parse_etag_or_star_tok stream = match stream with parser | [< '(Special '*') >] -> None | [< etag=parse_etag_token >] -> Some etag let get_etag_list mh fn_name fieldname = let specials = [ ','; ';'; '='; '/'; '*' ] in let l = parse_comma_separated_field ~specials mh fn_name parse_etag_or_star_tok fieldname in if List.mem None l then None else Some(List.map (function Some e -> e | None -> assert false) l) let set_etag_list mh fieldname l_opt = let v = match l_opt with | None -> "*" | Some l -> String.concat "," (List.map string_of_etag l) in mh # update_field fieldname v let get_if_match mh = get_etag_list mh "Nethttp.get_if_match" "If-Match" let set_if_match mh = set_etag_list mh "If-Match" let get_if_modified_since mh = date_of_string "Nethttp.get_if_modified_since" (mh # field "If-Modified-Since") let set_if_modified_since mh d = mh # update_field "If-Modified-Since" (string_of_date d) let get_if_none_match mh = get_etag_list mh "Nethttp.get_if_none_match" "If-None-Match" let set_if_none_match mh = set_etag_list mh "If-None-Match" let get_if_range mh = let s = mh # field "If-Range" in let stream = scan_value ~specials:[ ','; ';'; '='; '/' ] s in try `Etag (parse_etag stream) with | Stream.Failure | Stream.Error _ | Failure _ -> `Date (date_of_string "Nethttp.get_if_range" s) let set_if_range mh v = let s = match v with | `Etag e -> string_of_etag e | `Date d -> string_of_date d in mh # update_field "If-Range" s let get_if_unmodified_since mh = date_of_string "Nethttp.get_if_unmodified_since" (mh # field "If-Unmodified-Since") let set_if_unmodified_since mh d = mh # update_field "If-Unmodified-Since" (string_of_date d) let get_last_modified mh = date_of_string "Nethttp.get_last_modified" (mh # field "Last-Modified") let set_last_modified mh d = mh # update_field "Last-Modified" (string_of_date d) let get_location mh = mh # field "Location" let set_location mh s = mh # update_field "Location" s let get_max_forwards mh = try int_of_string (mh # field "Max-Forwards") with Failure _ -> raise(Bad_header_field "Nethttp.get_max_forwards") let set_max_forwards mh n = mh # update_field "Max-Forwards" (string_of_int n) let parse_pragma_directive stream = match stream with parser | [< '(Atom tok); param_opt = parse_opt_eq_token >] -> (tok, param_opt) let get_pragma mh = parse_comma_separated_field mh "Nethttp.get_pragma" parse_pragma_directive "Pragma" let set_pragma mh l = let s = String.concat "," (List.map (function | (tok, None) -> tok | (tok, Some param) -> tok ^ "=" ^ string_of_value param) l) in mh # update_field "Pragma" s let parse_opt_last_pos stream = match stream with parser | [< '(Atom last) >] -> Some(Int64.of_string last) | [< >] -> None let rec parse_byte_range_spec stream = match stream with parser | [< '(Atom first); '(Special '-'); last=parse_opt_last_pos; r=parse_byte_range_spec_rest >] -> (Some (Int64.of_string first), last) :: r | [< '(Special '-'); '(Atom suffix_length); r=parse_byte_range_spec_rest >] -> (None, Some(Int64.of_string suffix_length)) :: r | [< >] -> [] and parse_byte_range_spec_rest stream = match stream with parser | [< '(Special ','); _=parse_commas; r=parse_byte_range_spec >] -> r | [< >] -> [] let parse_ranges_specifier stream = match stream with parser | [< '(Atom "bytes"); '(Special '='); r=parse_byte_range_spec; 'End >] -> `Bytes r let get_range mh = let s = mh # field "Range" in let stream = scan_value ~specials:[ ','; ';'; '='; '*'; '-'; '/' ] s in try parse_ranges_specifier stream with | Stream.Failure | Stream.Error _ | Failure _ -> raise (Bad_header_field "Nethttp.get_range") let set_range mh (`Bytes l) = let s = "bytes=" ^ String.concat "," (List.map (function | (Some first, Some last) -> Int64.to_string first ^ "-" ^ Int64.to_string last | (Some first, None) -> Int64.to_string first ^ "-" | (None, Some last) -> "-" ^ Int64.to_string last | (None, None) -> invalid_arg "Nethttp.set_range") l) in mh # update_field "Range" s let get_referer mh = mh # field "Referer" let get_referrer = get_referer let set_referer mh s = mh # update_field "Referer" s let set_referrer = set_referer let get_retry_after mh = let s = mh # field "Retry-After" in try `Seconds(int_of_string s) with Failure _ -> `Date(date_of_string "Nethttp.get_retry_after" s) let set_retry_after mh v = let s = match v with | `Seconds n -> string_of_int n | `Date d -> string_of_date d in mh # update_field "Retry-After" s let get_server mh = mh # field "Server" let set_server mh name = mh # update_field "Server" name let get_te mh = q_split (parse_parameterized_token_list mh "Nethttp.get_te" "TE") let set_te mh te = let s = String.concat "," (List.map (fun triple -> string_of_parameterized_token (q_merge "Nethttp.set_te" triple)) te) in mh # update_field "TE" s let get_trailer mh = parse_token_list mh "Nethttp.get_trailer" "Trailer" let set_trailer mh fields = mh # update_field "Trailer" (String.concat "," fields) let get_transfer_encoding mh = parse_parameterized_token_list mh "Nethttp.get_transfer_encoding" "Transfer-Encoding" let set_transfer_encoding mh te = let s = String.concat "," (List.map string_of_parameterized_token te) in mh # update_field "Transfer-Encoding" s let get_upgrade mh = parse_token_list mh "Nethttp.get_upgrade" "Upgrade" let set_upgrade mh fields = mh # update_field "Upgrade" (String.concat "," fields) let get_user_agent mh = mh # field "User-Agent" let set_user_agent mh s = mh # update_field "User-Agent" s let get_vary mh = let l = parse_token_list mh "Nethttp.get_vary" "Vary" in if List.mem "*" l then `Star else `Fields l let set_vary mh v = let s = match v with | `Star -> "*" | `Fields l -> String.concat "," l in mh # update_field "Vary" s (* --- Authentication --- *) let parse_challenges mh fn_name fieldname = let rec parse_auth_params stream = match stream with parser | [< '(Atom ap_name); '(Special '='); ap_val = parse_token_or_qstring; rest = parse_auth_param_rest >] -> (ap_name, ap_val) :: rest and parse_auth_param_rest stream = match Stream.npeek 3 stream with | [ (Special ','); (Atom _); (Special '=') ] -> ( match stream with parser | [< '(Special ','); '(Atom ap_name); '(Special '='); ap_val = parse_token_or_qstring; rest = parse_auth_param_rest >] -> (ap_name, ap_val) :: rest | [< >] -> (* should not happen... *) [] ) | _ -> [] and parse_challenge stream = match stream with parser | [< '(Atom auth_scheme); auth_params = parse_auth_params >] -> (auth_scheme, auth_params) in parse_comma_separated_field mh fn_name parse_challenge fieldname let mk_challenges fields = String.concat "," (List.map (fun (auth_name, auth_params) -> auth_name ^ " " ^ (String.concat "," (List.map (fun (p_name, p_val) -> p_name ^ "=" ^ string_of_value p_val) auth_params)) ) fields) let get_www_authenticate mh = parse_challenges mh "Nethttp.get_www_authenticate" "WWW-Authenticate" let set_www_authenticate mh fields = mh # update_field "WWW-Authenticate" (mk_challenges fields) let get_proxy_authenticate mh = parse_challenges mh "Nethttp.get_proxy_authenticate" "Proxy-Authenticate" let set_proxy_authenticate mh fields = mh # update_field "Proxy-Authenticate" (mk_challenges fields) let ws_re = Netstring_str.regexp "[ \t\r\n]+";; let parse_credentials mh fn_name fieldname = let rec parse_creds stream = match stream with parser | [< '(Atom auth_name); params = parse_auth_params >] -> (auth_name, params) and parse_auth_params stream = match stream with parser | [< '(Atom ap_name); '(Special '='); ap_val = parse_token_or_qstring; rest = parse_auth_param_rest >] -> (ap_name, ap_val) :: rest and parse_auth_param_rest stream = match stream with parser | [< '(Special ','); '(Atom ap_name); '(Special '='); ap_val = parse_token_or_qstring; rest = parse_auth_param_rest >] -> (ap_name, ap_val) :: rest | [< >] -> [] in (* Basic authentication is a special case! *) let v = mh # field fieldname in (* or Not_found *) match Netstring_str.split ws_re v with | [ name; creds ] when String.lowercase name = "basic" -> ("basic", ["credentials", creds]) | _ -> parse_field mh fn_name parse_creds fieldname let mk_credentials (auth_name, auth_params) = if String.lowercase auth_name = "basic" then ( let creds = try List.assoc "credentials" auth_params with Not_found -> failwith "Nethttp.mk_credentials: basic credentials not found" in "Basic " ^ creds ) else auth_name ^ " " ^ (String.concat "," (List.map (fun (p_name, p_val) -> p_name ^ "=" ^ string_of_value p_val) auth_params)) let get_authorization mh = parse_credentials mh "Nethttp.get_authorization" "authorization" let set_authorization mh v = mh # update_field "Authorization" (mk_credentials v) let get_proxy_authorization mh = parse_credentials mh "Nethttp.get_proxy_authorization" "proxy-authorization" let set_proxy_authorization mh v = mh # update_field "Proxy-Authorization" (mk_credentials v) (* --- Cookies --- *) exception No_equation of string let split_name_is_value s = (* Recognizes a string "name=value" and returns the pair (name,value). * If the string has the wrong format, the function will raise * No_equation, and the argument of the exception is the unparseable * string. *) try let p = String.index s '=' in (String.sub s 0 p, String.sub s (p+1) (String.length s - p - 1)) with Not_found -> raise(No_equation s) let spaces_at_beginning_re = Netstring_str.regexp "^[ \t\r\n]+" let spaces_at_end_re = Netstring_str.regexp "[ \t\r\n]+$" let strip_spaces s = (* Remove leading and trailing spaces: *) Netstring_str.global_replace spaces_at_end_re "" (Netstring_str.global_replace spaces_at_beginning_re "" s) let split_cookies_re = Netstring_str.regexp "[ \t\r\n]*;[ \t\r\n]*" ;; let get_cookie mh = let cstrings = mh # multiple_field "Cookie" in (* Remove leading and trailing spaces: *) let cstrings' = List.map strip_spaces cstrings in let partss = List.map (fun cstring -> Netstring_str.split split_cookies_re cstring ) cstrings' in let parts = List.flatten partss in List.map (fun part -> let n,v = try split_name_is_value part with No_equation _ -> (part, "") (* Because it is reported that MSIE returns just "n" instead * of "n=" when the value v is empty *) in let n_dec = Netencoding.Url.decode n in let v_dec = Netencoding.Url.decode v in (n_dec, v_dec) ) parts let get_cookie_ct = Cookie.get_cookie_ct let set_cookie mh l = let s = String.concat ";" (List.map (fun (n,v) -> Netencoding.Url.encode n ^ "=" ^ Netencoding.Url.encode v) l) in mh # update_field "Cookie" s (* CHECK let nv_re = Pcre.regexp "^([a-zA-Z0-9_.]+)(=(.*))?$" *) let nv_re = Netstring_str.regexp "^\\([^=;]+\\)\\(=\\(.*\\)\\)?$" let get_set_cookie_1 s = let nv_list = List.map (fun item -> ( match Netstring_str.string_match nv_re item 0 with | None -> raise(Bad_header_field "Nethttp.Header.get_set_cookie") | Some m -> let name = Netstring_str.matched_group m 1 item in let value = try Netstring_str.matched_group m 3 item with Not_found -> "" in (name, value) ) ) (Netstring_str.split split_cookies_re s) in match nv_list with | (n,v) :: params -> let params = List.map (fun (n,v) -> (String.lowercase n, v)) params in { cookie_name = Netencoding.Url.decode ~plus:false n; cookie_value = Netencoding.Url.decode ~plus:false v; cookie_expires = (try let exp_str = List.assoc "expires" params in Some(Netdate.since_epoch (Netdate.parse exp_str)) with | Not_found -> None); cookie_domain = ( try Some(List.assoc "domain" params) with | Not_found -> None ); cookie_path = ( try Some(List.assoc "path" params) with | Not_found -> None ); cookie_secure = ( try List.mem_assoc "secure" params with | Not_found -> false ) } | _ -> raise(Bad_header_field "Nethttp.Header.get_set_cookie") let get_set_cookie mh = let fields = mh # multiple_field "Set-Cookie" in List.map get_set_cookie_1 fields let set_set_cookie mh l = let cookie_fields = List.map (fun c -> let enc_name = Netencoding.Url.encode ~plus:false c.cookie_name in let enc_value = Netencoding.Url.encode ~plus:false c.cookie_value in enc_name ^ "=" ^ enc_value ^ ( match c.cookie_expires with None -> "" | Some t -> ";EXPIRES=" ^ Netdate.mk_usenet_date t ) ^ (match c.cookie_domain with None -> "" | Some d -> ";DOMAIN=" ^ d ) ^ (match c.cookie_path with None -> "" | Some p -> ";PATH=" ^ p ) ^ if c.cookie_secure then ";SECURE" else "" ) l in mh # update_multiple_field "Set-cookie" cookie_fields let set_set_cookie_ct = Cookie.set_set_cookie_ct end