Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file header.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515open!Coreopen!Asyncopen!ImportincludeCohttp.Header(** Host Header: https://tools.ietf.org/html/rfc7230#section-5.4
The Host header field (defined at [2]) has the following ABNF grammar:
{[
Host = uri-host [ ":" port ] ; Section 2.7.1
]}
Following what "uri-host" is defined as:
which can be expanded as follows (https://tools.ietf.org/html/rfc7230#appendix-B ):
{[
uri-host = <host, see [RFC3986], Section 3.2.2>
]}
which is explained by (https://tools.ietf.org/html/rfc3986#section-3.2.2 ) as
{[
host = IP-literal / IPv4address / reg-name
]}
so "uri-host" is an IP address or what is colloquially referred to as a hostname.
Therefore, a host header is a host and optional port.
Example:
For example, a GET request to the origin server for
<http://www.example.org/pub/WWW/> would begin with:
GET /pub/WWW/ HTTP/1.1
Host: www.example.org
*)lethost_header_name="host"(** From https://tools.ietf.org/html/rfc6454 "The Web Origin Concept"
{v
3.2.1. Examples
All of the following resources have the same origin:
http://example.com/
http://example.com:80/
http://example.com/path/file
Each of the URIs has the same scheme, host, and port components.
Each of the following resources has a different origin from the
others.
http://example.com/
http://example.com:8080/
http://www.example.com/
https://example.com:80/
https://example.com/
http://example.org/
http://ietf.org/
In each case, at least one of the scheme, host, and port component
will differ from the others in the list.
v}
{v
5. Comparing Origins
Two origins are "the same" if, and only if, they are identical. In
particular:
o If the two origins are scheme/host/port triples, the two origins
are the same if, and only if, they have identical schemes, hosts,
and ports.
o An origin that is a globally unique identifier cannot be the same
as an origin that is a scheme/host/port triple.
Two URIs are same-origin if their origins are the same.
NOTE: A URI is not necessarily same-origin with itself. For
example, a data URI [RFC2397] is not same-origin with itself
because data URIs do not use a server-based naming authority and
therefore have globally unique identifiers as origins.
v}
{v
6.1. Unicode Serialization of an Origin
The unicode-serialization of an origin is the value returned by the
following algorithm:
1. If the origin is not a scheme/host/port triple, then return the
string
null
(i.e., the code point sequence U+006E, U+0075, U+006C, U+006C)
and abort these steps.
2. Otherwise, let result be the scheme part of the origin triple.
3. Append the string "://" to result.
4. Append each component of the host part of the origin triple
(converted as follows) to the result, separated by U+002E FULL
STOP code points ("."):
1. If the component is an A-label, use the corresponding U-label
instead (see [RFC5890] and [RFC5891]).
2. Otherwise, use the component verbatim.
5. If the port part of the origin triple is different from the
default port for the protocol given by the scheme part of the
origin triple:
1. Append a U+003A COLON code point (":") and the given port, in
base ten, to result.
6. Return result.
v}
{v
7.1. Syntax
The Origin header field has the following syntax:
origin = "Origin:" OWS origin-list-or-null OWS
origin-list-or-null = %x6E %x75 %x6C %x6C / origin-list
origin-list = serialized-origin *( SP serialized-origin )
serialized-origin = scheme "://" host [ ":" port ]
; <scheme>, <host>, <port> from RFC 3986
v}
*)letorigin_header_name="origin"moduleWeb_host_and_port:sigtypet=private{header:string;host:string;port:intoption}[@@derivingsexp_of]valof_origin_header:string->([`Schemeofstring]*t)Or_error.tvalof_host_header:string->scheme:string->tOr_error.tvalvalidate_equal:ignore_port:bool->t->t->unitOr_error.tend=structtypet={header:string;host:string;port:intoption}[@@derivingfields,sexp_of]letvalidate_equal~ignore_portt1t2=letmatchescompareaccfield=letf1=Field.getfieldt1inletf2=Field.getfieldt2inletresult=ifnot(comparef1f2)thenerror_s[%message"parts do not match"~part:(Field.namefield)]elseOk()inresult::accinletjust_for_debuggingacc(_:_Field.t)=accinFields.fold~init:[]~host:(matches[%compare.equal:string])~port:(ifignore_portthenjust_for_debuggingelsematches[%compare.equal:intoption])~header:just_for_debugging|>Or_error.all_unit|>Or_error.tag_s~tag:[%message""~_:(t1:t)~_:(t2:t)];;letremove_superfluous_port_specification~scheme~port=letdefault_port=matchschemewith|"http"|"ws"->Ok80|"https"|"wss"->Ok443|(_:string)->error_s[%message"Unknown scheme"~_:scheme]inOr_error.mapdefault_port~f:(fundefault_port->letopenOption.Let_syntaxinlet%bindspecified_port=portinifdefault_port<>specified_portthenSomespecified_portelseNone);;letof_host_headervalue~scheme=letopenOr_error.Let_syntaxinletheader=host_header_nameinOr_error.try_with_join(fun()->matchString.lsplit2~on:':'valuewith|Some(host,port)->let%mapport=remove_superfluous_port_specification~scheme~port:(Some(Int.of_stringport))in{header;host;port}|None->Ok{header;host=value;port=None});;letof_origin_headerorigin=letopenOr_error.Let_syntaxinOr_error.try_with_join(fun()->leturi=Uri.of_stringorigininmatchUri.schemeuriwith|None->error_s[%message"No scheme"origin]|Somescheme->let%bindport=remove_superfluous_port_specification~scheme~port:(Uri.porturi)in(matchUri.hosturiwith|None->error_s[%message"No host"origin]|Somehost->Ok(`Schemescheme,{header=origin_header_name;host;port})));;endletorigin_and_host_headers_match~origin~host~ignore_port=letopenOr_error.Let_syntaxinlet%bind`Schemescheme,origin=Web_host_and_port.of_origin_headerorigininlet%bindhost=Web_host_and_port.of_host_headerhost~schemeinWeb_host_and_port.validate_equal~ignore_portoriginhost;;letorigin_and_allowlist_entry_match~origin~allowlist_entry~ignore_port=letopenOr_error.Let_syntaxinlet%bind`Schemeorigin_scheme,origin=Web_host_and_port.of_origin_headerorigininlet%bind`Schemeallowed_scheme,allowed=Web_host_and_port.of_origin_headerallowlist_entryinlet%bind()=matchString.equalorigin_schemeallowed_schemewith|true->Ok()|false->error_s[%message"origin scheme and allowlist-entry scheme do not match"origin_schemeallowed_scheme]inWeb_host_and_port.validate_equal~ignore_portoriginallowed;;moduleExpect_test_config=Core.Expect_test_configlet%test_module_=(modulestructletcheck~host~origin=print_s[%sexp(origin_and_host_headers_match~origin~host~ignore_port:false:unitOr_error.t)];;let%expect_test"Host matching"=check~host:"ontology"~origin:"https://ontology";[%expect{| (Ok ()) |}];check~host:"bond-webs:8443"~origin:"https://bond-webs:8443";[%expect{| (Ok ()) |}];check~host:"site-without-port"~origin:"https://site-without-port:1337";[%expect{|
(Error
((((header origin) (host site-without-port) (port (1337)))
((header host) (host site-without-port) (port ())))
("parts do not match" (part port)))) |}];;(* If we are running a service at [hostname], and a website at [hostnameä] which has
malicious javascript, we ought to reject it. *)let%expect_test"Host matching fails on unicode URIs, no work has been put into \
supporting them"=check~host:"internal-siteä.attacker.co.uk"~origin:"https://internal-siteä.attacker.co.uk";[%expect{|
(Error
((((header origin) (host internal-site) (port ()))
((header host) (host "internal-site\195\164.attacker.co.uk") (port ())))
("parts do not match" (part host)))) |}];(* This test ought to fail, and doesn't
It demonstrates a bug in [Uri.of_string] which incorrectly succeeds on
this invalid URI.
This hinges on the attacker's ability to persuade an uncompromised web browser to
send an unparseable origin header.
*)check~host:(* our service's address *)"internal-site"~origin:(* the attacker's web address *)"https://internal-siteä.attacker.co.uk";[%expect{| (Ok ()) |}];;(* https://tools.ietf.org/html/rfc6454#section-3.2.1 explains that all of the
following have the same origin:
{v http://example.com/
http://example.com:80/
http://example.com/path/file
v}
since the origin only compares the scheme, host, and port; and the default port for
protocol http is 80. *)let%expect_test"Implicit port"=check~host:"example.com"~origin:"http://example.com:80";check~host:"example.com:80"~origin:"http://example.com:80";check~host:"example.com"~origin:"https://example.com:443";check~host:"example.com:443"~origin:"https://example.com:443";check~host:"example.com"~origin:"ws://example.com:80";check~host:"example.com"~origin:"wss://example.com:443";[%expect{|
(Ok ())
(Ok ())
(Ok ())
(Ok ())
(Ok ())
(Ok ()) |}];;end);;letorigin_and_host_match?(ignore_port=false)t=lethost=getthost_header_nameinletorigin=gettorigin_header_nameinmatchOption.bothhostoriginwith|None->error_s[%message"Missing one of origin or host header"(origin:stringoption)(host:stringoption)]|Some(host,origin)->origin_and_host_headers_match~origin~host~ignore_port;;letorigin_matches_host_or_is_one_of?(ignore_port=false)t~origins=matchorigin_and_host_match~ignore_porttwith|Ok()->Ok()|Error(_:Error.t)ashost_match_error->(matchgettorigin_header_namewith|None->error_s[%message"No origin header present"]|Someorigin->ifList.is_emptyoriginsthenhost_match_errorelse(matchOr_error.find_map_okorigins~f:(funallowlist_entry->origin_and_allowlist_entry_match~origin~ignore_port~allowlist_entry)with|Ok()->Ok()|Error(_:Error.t)->Or_error.combine_errors_unit[host_match_error;error_s[%message"The origin is not in the allowlist"~origin~allowed:(origins:stringlist)]]));;let%test_module_=(modulestructletmaybe_addvalue~nameheaders=matchvaluewith|None->headers|Somevalue->addheadersnamevalue;;letinit_header~origin~host=init()|>maybe_addhost~name:host_header_name|>maybe_addorigin~name:origin_header_name;;letcheck~host~origin~f=letresult=f(init_header~origin~host)inprint_s[%sexp(result:unitOr_error.t)];;let%expect_test"Full parse of header"=letcheck=check~f:origin_and_host_matchincheck~host:None~origin:(Some"http://somehost");[%expect{|
(Error
("Missing one of origin or host header" (origin (http://somehost))
(host ()))) |}];check~host:(Some"asdf")~origin:None;[%expect{| (Error ("Missing one of origin or host header" (origin ()) (host (asdf)))) |}];check~host:(Some"asdf")~origin:(Some"https://somehost");[%expect{|
(Error
((((header origin) (host somehost) (port ()))
((header host) (host asdf) (port ())))
("parts do not match" (part host)))) |}];check~host:(Some"somehost")~origin:(Some"https://somehost:994");[%expect{|
(Error
((((header origin) (host somehost) (port (994)))
((header host) (host somehost) (port ())))
("parts do not match" (part port)))) |}];check~host:(Some"wrong")~origin:(Some"https://somehost:994");[%expect{|
(Error
((((header origin) (host somehost) (port (994)))
((header host) (host wrong) (port ())))
("parts do not match" (part port)) ("parts do not match" (part host)))) |}];check~host:(Some"somehost:994")~origin:(Some"https://somehost:994");[%expect{| (Ok ()) |}];;let%expect_test"origin_matches_host_or_is_one_of"=letcheck~origins=check~f:(origin_matches_host_or_is_one_of~origins)incheck~host:None~origin:(Some"http://somehost")~origins:[];[%expect{|
(Error
("Missing one of origin or host header" (origin (http://somehost))
(host ()))) |}];lethost=Some"somehost"incheck~host~origin:(Some"http://somehost")~origins:[];[%expect{| (Ok ()) |}];check~host~origin:(Some"http://somehost")~origins:["http://host"];[%expect{| (Ok ()) |}];check~host:(Some"somehost")~origin:(Some"http://host")~origins:["http://host"];[%expect{| (Ok ()) |}];check~host:(Some"somehost")~origin:(Some"http://host")~origins:["http://otherhost"];[%expect{|
(Error
(((((header origin) (host host) (port ()))
((header host) (host somehost) (port ())))
("parts do not match" (part host)))
("The origin is not in the allowlist" (origin http://host)
(allowed (http://otherhost))))) |}];;let%expect_test"port ignoring in allowed origins"=letcheck~ignore_port=check~f:(origin_matches_host_or_is_one_of~ignore_port~origins:["https://host"])~host:(Some"somehost:80")~origin:(Some"https://host:8443")incheck~ignore_port:false;[%expect{|
(Error
(((((header origin) (host host) (port (8443)))
((header host) (host somehost) (port (80))))
("parts do not match" (part port)) ("parts do not match" (part host)))
("The origin is not in the allowlist" (origin https://host:8443)
(allowed (https://host))))) |}];check~ignore_port:true;[%expect{| (Ok ()) |}];;let%expect_test"scheme checked in allowed origins"=letcheck~origins=check~f:(origin_matches_host_or_is_one_of~origins)incheck~host:(Some"somehost:80")~origin:(Some"https://host")~origins:["https://host"];[%expect{| (Ok ()) |}];check~host:(Some"somehost:80")~origin:(Some"https://host")~origins:["http://host"];[%expect{|
(Error
(((((header origin) (host host) (port ()))
((header host) (host somehost) (port (80))))
("parts do not match" (part port)) ("parts do not match" (part host)))
("The origin is not in the allowlist" (origin https://host)
(allowed (http://host))))) |}];check~host:(Some"somehost:80")~origin:(Some"https://host")~origins:["host"];[%expect{|
(Error
(((((header origin) (host host) (port ()))
((header host) (host somehost) (port (80))))
("parts do not match" (part port)) ("parts do not match" (part host)))
("The origin is not in the allowlist" (origin https://host)
(allowed (host))))) |}];;end);;letwebsocket_subprotocol_header="sec-websocket-protocol"letwebsocket_subprotocolst=get_multitwebsocket_subprotocol_header|>List.concat_map~f:(String.split~on:',');;letadd_websocket_subprotocolt~subprotocol:value=addtwebsocket_subprotocol_headervalue;;