364 lines
13 KiB
OCaml
364 lines
13 KiB
OCaml
|
|
(*----------------------------------------------------------------------------
|
||
|
|
* Copyright (c) 2017 Inhabited Type LLC.
|
||
|
|
* Copyright (c) 2019 Antonio N. Monteiro.
|
||
|
|
*
|
||
|
|
* All rights reserved.
|
||
|
|
*
|
||
|
|
* Redistribution and use in source and binary forms, with or without
|
||
|
|
* modification, are permitted provided that the following conditions
|
||
|
|
* are met:
|
||
|
|
*
|
||
|
|
* 1. Redistributions of source code must retain the above copyright
|
||
|
|
* notice, this list of conditions and the following disclaimer.
|
||
|
|
*
|
||
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||
|
|
* notice, this list of conditions and the following disclaimer in the
|
||
|
|
* documentation and/or other materials provided with the distribution.
|
||
|
|
*
|
||
|
|
* 3. Neither the name of the author nor the names of his contributors
|
||
|
|
* may be used to endorse or promote products derived from this software
|
||
|
|
* without specific prior written permission.
|
||
|
|
*
|
||
|
|
* THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
|
||
|
|
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||
|
|
* DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
|
||
|
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||
|
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||
|
|
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
*---------------------------------------------------------------------------*)
|
||
|
|
|
||
|
|
type name = string
|
||
|
|
type value = string
|
||
|
|
|
||
|
|
type header = Hpack.header =
|
||
|
|
{ name : name
|
||
|
|
; value : value
|
||
|
|
; sensitive : bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type t = header list
|
||
|
|
|
||
|
|
let empty : t = []
|
||
|
|
|
||
|
|
let of_rev_list hs =
|
||
|
|
List.map (fun (name, value) -> { name; value; sensitive = false }) hs
|
||
|
|
|
||
|
|
let of_list t = of_rev_list (List.rev t)
|
||
|
|
let to_rev_list t = List.map (fun { name; value; _ } -> name, value) t
|
||
|
|
let to_list t = List.rev (to_rev_list t)
|
||
|
|
let to_hpack_list t = List.rev t
|
||
|
|
|
||
|
|
exception Local
|
||
|
|
|
||
|
|
module CI = struct
|
||
|
|
let char_is_upper c = c >= 0x41 && c <= 0x5a
|
||
|
|
let lower c = if char_is_upper c then c + 32 else c
|
||
|
|
|
||
|
|
let equal x y =
|
||
|
|
let len = String.length x in
|
||
|
|
len = String.length y
|
||
|
|
&&
|
||
|
|
match
|
||
|
|
for i = 0 to len - 1 do
|
||
|
|
let c1 = Char.code (String.unsafe_get x i) in
|
||
|
|
let c2 = Char.code (String.unsafe_get y i) in
|
||
|
|
if c1 = c2 then () else if lower c1 <> lower c2 then raise Local
|
||
|
|
done
|
||
|
|
with
|
||
|
|
| () -> true
|
||
|
|
| exception Local -> false
|
||
|
|
|
||
|
|
let is_lowercase x =
|
||
|
|
let len = String.length x in
|
||
|
|
match
|
||
|
|
for i = 0 to len - 1 do
|
||
|
|
let c1 = Char.code (String.unsafe_get x i) in
|
||
|
|
if char_is_upper c1 then raise Local else ()
|
||
|
|
done
|
||
|
|
with
|
||
|
|
| () -> true
|
||
|
|
| exception Local -> false
|
||
|
|
end
|
||
|
|
|
||
|
|
let rec mem t name =
|
||
|
|
match t with
|
||
|
|
| { name = name'; _ } :: t' -> CI.equal name name' || mem t' name
|
||
|
|
| _ -> false
|
||
|
|
|
||
|
|
(* TODO: do we need to keep a list of never indexed fields? *)
|
||
|
|
let add t ?(sensitive = false) name value = { name; value; sensitive } :: t
|
||
|
|
let add_list t ls = of_rev_list ls @ t (* XXX(seliopou): do better here *)
|
||
|
|
|
||
|
|
let add_multi =
|
||
|
|
let rec loop_outer t lss =
|
||
|
|
match lss with [] -> t | (n, vs) :: lss' -> loop_inner t n vs lss'
|
||
|
|
and loop_inner t n vs lss =
|
||
|
|
match vs with
|
||
|
|
| [] -> loop_outer t lss
|
||
|
|
| v :: vs' ->
|
||
|
|
loop_inner ({ name = n; value = v; sensitive = false } :: t) n vs' lss
|
||
|
|
in
|
||
|
|
loop_outer
|
||
|
|
|
||
|
|
let add_unless_exists t ?(sensitive = false) name value =
|
||
|
|
if mem t name then t else { name; value; sensitive } :: t
|
||
|
|
|
||
|
|
let replace t ?(sensitive = false) name value =
|
||
|
|
let rec loop t n nv seen =
|
||
|
|
match t with
|
||
|
|
| [] -> if not seen then raise Local else []
|
||
|
|
| ({ name = n'; _ } as nv') :: t ->
|
||
|
|
if CI.equal n n'
|
||
|
|
then if seen then loop t n nv true else nv :: loop t n nv true
|
||
|
|
else nv' :: loop t n nv seen
|
||
|
|
in
|
||
|
|
try loop t name { name; value; sensitive } false with Local -> t
|
||
|
|
|
||
|
|
let remove t name =
|
||
|
|
let rec loop s n seen =
|
||
|
|
match s with
|
||
|
|
| [] -> if not seen then raise Local else []
|
||
|
|
| ({ name = n'; _ } as nv') :: s' ->
|
||
|
|
if CI.equal n n' then loop s' n true else nv' :: loop s' n seen
|
||
|
|
in
|
||
|
|
try loop t name false with Local -> t
|
||
|
|
|
||
|
|
let get t name =
|
||
|
|
let rec loop t n =
|
||
|
|
match t with
|
||
|
|
| [] -> None
|
||
|
|
| { name = n'; value; _ } :: t' ->
|
||
|
|
if CI.equal n n' then Some value else loop t' n
|
||
|
|
in
|
||
|
|
loop t name
|
||
|
|
|
||
|
|
let get_exn t name =
|
||
|
|
let rec loop t =
|
||
|
|
match t with
|
||
|
|
| [] -> raise Not_found
|
||
|
|
| { name = n; value; _ } :: t' -> if CI.equal name n then value else loop t'
|
||
|
|
in
|
||
|
|
loop t
|
||
|
|
|
||
|
|
let get_pseudo t name = get t (":" ^ name)
|
||
|
|
let get_pseudo_exn t name = get_exn t (":" ^ name)
|
||
|
|
|
||
|
|
let get_multi t name =
|
||
|
|
let rec loop t acc =
|
||
|
|
match t with
|
||
|
|
| [] -> acc
|
||
|
|
| { name = n; value; _ } :: t' ->
|
||
|
|
if CI.equal name n then loop t' (value :: acc) else loop t' acc
|
||
|
|
in
|
||
|
|
loop t []
|
||
|
|
|
||
|
|
let get_multi_pseudo t name = get_multi t (":" ^ name)
|
||
|
|
|
||
|
|
module Pseudo = struct
|
||
|
|
let reserved_request = [ ":method"; ":scheme"; ":authority"; ":path" ]
|
||
|
|
let reserved_response = [ ":status" ]
|
||
|
|
|
||
|
|
(* 0x3A is the char code for `:` *)
|
||
|
|
let is_pseudo name = Char.code (String.unsafe_get name 0) = 0x3A
|
||
|
|
end
|
||
|
|
|
||
|
|
let iter ~f t = List.iter (fun { name; value; _ } -> f name value) t
|
||
|
|
|
||
|
|
let fold ~f ~init t =
|
||
|
|
List.fold_left (fun acc { name; value; _ } -> f name value acc) init t
|
||
|
|
|
||
|
|
let exists ~f t = List.exists (fun { name; value; _ } -> f name value) t
|
||
|
|
|
||
|
|
let valid_headers ?(is_request = true) t =
|
||
|
|
match get t "connection", get t "TE" with
|
||
|
|
| Some _, _ ->
|
||
|
|
(* From RFC7540§8.1.2.2:
|
||
|
|
* HTTP/2 does not use the Connection header field to indicate
|
||
|
|
* connection-specific header fields; in this protocol,
|
||
|
|
* connection-specific metadata is conveyed by other means. An endpoint
|
||
|
|
* MUST NOT generate an HTTP/2 message containing connection-specific
|
||
|
|
* header fields; any message containing connection-specific header
|
||
|
|
* fields MUST be treated as malformed (Section 8.1.2.6). *)
|
||
|
|
false
|
||
|
|
| _, Some value when value <> "trailers" ->
|
||
|
|
(* From RFC7540§8.1.2.2:
|
||
|
|
* The only exception to this is the TE header field, which MAY be
|
||
|
|
* present in an HTTP/2 request; when it is, it MUST NOT contain any
|
||
|
|
* value other than "trailers". *)
|
||
|
|
false
|
||
|
|
| _ ->
|
||
|
|
let pseudo_ended = ref false in
|
||
|
|
let invalid =
|
||
|
|
exists
|
||
|
|
~f:(fun name _ ->
|
||
|
|
let is_pseudo = Pseudo.is_pseudo name in
|
||
|
|
let pseudo_did_end = !pseudo_ended in
|
||
|
|
if (not is_pseudo) && not pseudo_did_end then pseudo_ended := true;
|
||
|
|
(* From RFC7540§8.1.2:
|
||
|
|
* [...] header field names MUST be converted to lowercase
|
||
|
|
* prior to their encoding in HTTP/2. A request or response
|
||
|
|
* containing uppercase header field names MUST be treated as
|
||
|
|
* malformed (Section 8.1.2.6). *)
|
||
|
|
(not CI.(is_lowercase name))
|
||
|
|
(* From RFC7540§8.1.2.1:
|
||
|
|
* Pseudo-header fields are only valid in the context in
|
||
|
|
* which they are defined. [...] pseudo-header fields defined
|
||
|
|
* for responses MUST NOT appear in requests. [...] Endpoints
|
||
|
|
* MUST treat a request or response that contains undefined
|
||
|
|
* or invalid pseudo-header fields as malformed (Section
|
||
|
|
* 8.1.2.6). *)
|
||
|
|
|| (is_pseudo
|
||
|
|
&& not
|
||
|
|
(List.mem
|
||
|
|
name
|
||
|
|
(if is_request
|
||
|
|
then Pseudo.reserved_request
|
||
|
|
else Pseudo.reserved_response)))
|
||
|
|
|| (* From RFC7540§8.1.2.1:
|
||
|
|
* All pseudo-header fields MUST appear in the header block before
|
||
|
|
* regular header fields. Any request or response that contains a
|
||
|
|
* pseudo-header field that appears in a header block after a
|
||
|
|
* regular header field MUST be treated as malformed (Section
|
||
|
|
* 8.1.2.6). *)
|
||
|
|
(is_pseudo && pseudo_did_end))
|
||
|
|
(to_hpack_list t)
|
||
|
|
in
|
||
|
|
not invalid
|
||
|
|
|
||
|
|
let valid_request_headers t = valid_headers t
|
||
|
|
let valid_response_headers t = valid_headers ~is_request:false t
|
||
|
|
|
||
|
|
let method_path_and_scheme_or_malformed t =
|
||
|
|
match
|
||
|
|
( get_multi_pseudo t "method"
|
||
|
|
, get_multi_pseudo t "scheme"
|
||
|
|
, get_multi_pseudo t "path" )
|
||
|
|
with
|
||
|
|
| _, [ ("http" | "https") ], [ path ] when String.length path = 0 ->
|
||
|
|
(* From RFC7540§8.1.2.6:
|
||
|
|
* This pseudo-header field MUST NOT be empty for http or https URIs;
|
||
|
|
* http or https URIs that do not contain a path component MUST include a
|
||
|
|
* value of '/'. *)
|
||
|
|
`Malformed
|
||
|
|
(* From RFC7540§8.1.2.3:
|
||
|
|
* All HTTP/2 requests MUST include exactly one valid value for the
|
||
|
|
* :method, :scheme, and :path pseudo-header fields, unless it is a
|
||
|
|
* CONNECT request (Section 8.3). *)
|
||
|
|
| [ ("CONNECT" as meth) ], [], [] ->
|
||
|
|
(* From RFC7540§8.3:
|
||
|
|
* The HTTP header field mapping works as defined in Section 8.1.2.3
|
||
|
|
* ("Request Pseudo-Header Fields"), with a few differences.
|
||
|
|
* Specifically:
|
||
|
|
*
|
||
|
|
* - The :method pseudo-header field is set to CONNECT.
|
||
|
|
* - The :scheme and :path pseudo-header fields MUST be omitted.
|
||
|
|
* - The :authority pseudo-header field contains the host and port to
|
||
|
|
* connect to (equivalent to the authority-form of the request-target
|
||
|
|
* of CONNECT requests (see [RFC7230], Section 5.3)).
|
||
|
|
*
|
||
|
|
* A CONNECT request that does not conform to these restrictions is
|
||
|
|
* malformed (Section 8.1.2.6). *)
|
||
|
|
if mem t ":authority" then `Valid (meth, "", "") else `Malformed
|
||
|
|
| [ "CONNECT" ], _, _ -> `Malformed
|
||
|
|
| [ meth ], [ scheme ], [ path ] ->
|
||
|
|
if valid_request_headers t then `Valid (meth, path, scheme) else `Malformed
|
||
|
|
| _ -> `Malformed
|
||
|
|
|
||
|
|
let trailers_valid t =
|
||
|
|
let invalid =
|
||
|
|
exists
|
||
|
|
~f:(fun name _ ->
|
||
|
|
(* From RFC7540§8.1.2:
|
||
|
|
* [...] header field names MUST be converted to lowercase prior to
|
||
|
|
* their encoding in HTTP/2. A request or response containing
|
||
|
|
* uppercase header field names MUST be treated as malformed
|
||
|
|
* (Section 8.1.2.6). *)
|
||
|
|
(not (CI.is_lowercase name))
|
||
|
|
|| (* From RFC7540§8.1.2.1:
|
||
|
|
* Pseudo-header fields MUST NOT appear in trailers. Endpoints MUST
|
||
|
|
* treat a request or response that contains undefined or invalid
|
||
|
|
* pseudo-header fields as malformed (Section 8.1.2.6). *)
|
||
|
|
Pseudo.is_pseudo name)
|
||
|
|
t
|
||
|
|
in
|
||
|
|
not invalid
|
||
|
|
|
||
|
|
let is_valid_h2c_connection connection =
|
||
|
|
let values = String.split_on_char ',' connection in
|
||
|
|
let values = List.map String.trim values in
|
||
|
|
(* From RFC7540§3.2.1:
|
||
|
|
* [...] Since the upgrade is only intended to apply to the immediate
|
||
|
|
* connection, a client sending the HTTP2-Settings header field MUST also
|
||
|
|
* send HTTP2-Settings as a connection option in the Connection header
|
||
|
|
* field to prevent it from being forwarded (see Section 6.1 of [RFC7230]).
|
||
|
|
*)
|
||
|
|
match
|
||
|
|
( List.find_opt (fun x -> CI.equal x "upgrade") values
|
||
|
|
, List.find_opt (fun x -> CI.equal x "http2-settings") values )
|
||
|
|
with
|
||
|
|
| Some _, Some _ -> true
|
||
|
|
| _ -> false
|
||
|
|
|
||
|
|
let of_http1 ~headers ~meth ~target =
|
||
|
|
let module Headers = Httpun_types.Headers in
|
||
|
|
match Headers.get headers "host" with
|
||
|
|
| Some host ->
|
||
|
|
(* From RFC7540§8.1.2.3:
|
||
|
|
* Clients that generate HTTP/2 requests directly SHOULD use the
|
||
|
|
* :authority pseudo-header field instead of the Host header field. *)
|
||
|
|
let headers =
|
||
|
|
Headers.fold
|
||
|
|
~f:(fun name value acc ->
|
||
|
|
if CI.equal name "host" || CI.equal name "connection"
|
||
|
|
then
|
||
|
|
(* From RFC7540§8.1.2.2:
|
||
|
|
* HTTP/2 does not use the Connection header field to indicate
|
||
|
|
* connection-specific header fields; in this protocol,
|
||
|
|
* connection-specific metadata is conveyed by other means. An
|
||
|
|
* endpoint MUST NOT generate an HTTP/2 message containing
|
||
|
|
* connection-specific header fields; any message containing
|
||
|
|
* connection-specific header fields MUST be treated as malformed
|
||
|
|
* (Section 8.1.2.6). *)
|
||
|
|
acc
|
||
|
|
else
|
||
|
|
let name =
|
||
|
|
(* From RFC7540§8.1.2:
|
||
|
|
* header field names MUST be converted to lowercase prior to
|
||
|
|
* their encoding in HTTP/2. *)
|
||
|
|
if CI.is_lowercase name then name else String.lowercase_ascii name
|
||
|
|
in
|
||
|
|
(name, value) :: acc)
|
||
|
|
~init:
|
||
|
|
[ ":authority", host
|
||
|
|
; ":method", Httpun_types.Method.to_string meth
|
||
|
|
; ":path", target
|
||
|
|
; ":scheme", "https"
|
||
|
|
]
|
||
|
|
headers
|
||
|
|
in
|
||
|
|
Ok (of_rev_list headers)
|
||
|
|
| None -> Error "Missing `Host` header field"
|
||
|
|
|
||
|
|
let to_string t =
|
||
|
|
let b = Buffer.create 128 in
|
||
|
|
List.iter
|
||
|
|
(fun (name, value) ->
|
||
|
|
Buffer.add_string b name;
|
||
|
|
Buffer.add_string b ": ";
|
||
|
|
Buffer.add_string b value;
|
||
|
|
Buffer.add_string b "\r\n")
|
||
|
|
(to_list t);
|
||
|
|
Buffer.add_string b "\r\n";
|
||
|
|
Buffer.contents b
|
||
|
|
|
||
|
|
let pp_hum fmt t =
|
||
|
|
let pp_elem fmt (name, value) = Format.fprintf fmt "@[(%S %S)@]" name value in
|
||
|
|
Format.fprintf fmt "@[(";
|
||
|
|
Format.pp_print_list pp_elem fmt (to_list t);
|
||
|
|
Format.fprintf fmt ")@]"
|