Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file cohttp_async_websocket.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615open!Coreopen!Asyncopen!ImportmoduleHeader=Headerletwebsocket_accept_header_name="Sec-Websocket-Accept"moduleServer=structmoduleOn_connection=structtypet={set_response_headers:Header.t;should_overwrite_sec_accept_header:bool;handle_connection:Websocket.t->unitDeferred.t}letcreate?(set_response_headers=Header.init())?(should_overwrite_sec_accept_header=true)handle_connection={set_response_headers;should_overwrite_sec_accept_header;handle_connection};;endtypewebsocket_handler=inet:Socket.Address.Inet.t->subprotocol:stringoption->Cohttp.Request.t->On_connection.tDeferred.t(* {v
[1] https://tools.ietf.org/html/rfc6455#section-1.3
1.3. Opening Handshake
_This section is non-normative._
The opening handshake is intended to be compatible with HTTP-based
server-side software and intermediaries, so that a single port can be
used by both HTTP clients talking to that server and WebSocket
clients talking to that server. To this end, the WebSocket client's
handshake is an HTTP Upgrade request:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
In compliance with [RFC2616], header fields in the handshake may be
sent by the client in any order, so the order in which different
header fields are received is not significant.
The "Request-URI" of the GET method [RFC2616] is used to identify the
endpoint of the WebSocket connection, both to allow multiple domains
to be served from one IP address and to allow multiple WebSocket
endpoints to be served by a single server.
The client includes the hostname in the |Host| header field of its
handshake as per [RFC2616], so that both the client and the server
can verify that they agree on which host is in use.
Additional header fields are used to select options in the WebSocket
Protocol. Typical options available in this version are the
subprotocol selector (|Sec-WebSocket-Protocol|), list of extensions
support by the client (|Sec-WebSocket-Extensions|), |Origin| header
field, etc. The |Sec-WebSocket-Protocol| request-header field can be
used to indicate what subprotocols (application-level protocols
layered over the WebSocket Protocol) are acceptable to the client.
The server selects one or none of the acceptable protocols and echoes
that value in its handshake to indicate that it has selected that
protocol.
Sec-WebSocket-Protocol: chat
The |Origin| header field [RFC6454] is used to protect against
unauthorized cross-origin use of a WebSocket server by scripts using
the WebSocket API in a web browser. The server is informed of the
script origin generating the WebSocket connection request. If the
server does not wish to accept connections from this origin, it can
choose to reject the connection by sending an appropriate HTTP error
code. This header field is sent by browser clients; for non-browser
clients, this header field may be sent if it makes sense in the
context of those clients.
Finally, the server has to prove to the client that it received the
client's WebSocket handshake, so that the server doesn't accept
connections that are not WebSocket connections. This prevents an
attacker from tricking a WebSocket server by sending it carefully
crafted packets using XMLHttpRequest [XMLHttpRequest] or a form
submission.
To prove that the handshake was received, the server has to take two
pieces of information and combine them to form a response. The first
piece of information comes from the |Sec-WebSocket-Key| header field
in the client handshake:
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
For this header field, the server has to take the value (as present
in the header field, e.g., the base64-encoded [RFC4648] version minus
any leading and trailing whitespace) and concatenate this with the
Globally Unique Identifier (GUID, [RFC4122]) "258EAFA5-E914-47DA-
95CA-C5AB0DC85B11" in string form, which is unlikely to be used by
network endpoints that do not understand the WebSocket Protocol. A
SHA-1 hash (160 bits) [FIPS.180-3], base64-encoded (see Section 4 of
[RFC4648]), of this concatenation is then returned in the server's
handshake.
Concretely, if as in the example above, the |Sec-WebSocket-Key|
header field had the value "dGhlIHNhbXBsZSBub25jZQ==", the server
would concatenate the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
to form the string "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-
C5AB0DC85B11". The server would then take the SHA-1 hash of this,
giving the value 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6
0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea. This value is
then base64-encoded (see Section 4 of [RFC4648]), to give the value
"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=". This value would then be echoed in
the |Sec-WebSocket-Accept| header field.
The handshake from the server is much simpler than the client
handshake. The first line is an HTTP Status-Line, with the status
code 101:
HTTP/1.1 101 Switching Protocols
Any status code other than 101 indicates that the WebSocket handshake
has not completed and that the semantics of HTTP still apply. The
headers follow the status code.
The |Connection| and |Upgrade| header fields complete the HTTP
Upgrade. The |Sec-WebSocket-Accept| header field indicates whether
the server is willing to accept the connection. If present, this
header field must include a hash of the client's nonce sent in
|Sec-WebSocket-Key| along with a predefined GUID. Any other value
must not be interpreted as an acceptance of the connection by the
server.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
These fields are checked by the WebSocket client for scripted pages.
If the |Sec-WebSocket-Accept| value does not match the expected
value, if the header field is missing, or if the HTTP status code is
not 101, the connection will not be established, and WebSocket frames
will not be sent.
Option fields can also be included. In this version of the protocol,
the main option field is |Sec-WebSocket-Protocol|, which indicates
the subprotocol that the server has selected. WebSocket clients
verify that the server included one of the values that was specified
in the WebSocket client's handshake. A server that speaks multiple
subprotocols has to make sure it selects one based on the client's
handshake and specifies it in its handshake.
Sec-WebSocket-Protocol: chat
The server can also set cookie-related option fields to _set_
cookies, as described in [RFC6265].
v}
Client Request:
GET <resourcename> HTTP/1.1
Host: server.example.com *
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <key>
Origin: <origin>
Sec-WebSocket-Protocol: <protocol>
Sec-WebSocket-Version: <version>
Server Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <key>
*)letwebsocket_handshake_headers~initial_headers:header~sec_websocket_key~should_overwrite_sec_accept_header~subprotocol=letmaybe_overwrite_sec_accept_headerheader=ifshould_overwrite_sec_accept_headerthenHeader.replaceheaderwebsocket_accept_header_name(Websocket.sec_websocket_accept_header_value~sec_websocket_key)elseheaderinletheader=Header.add_unless_existsheader"Upgrade""websocket"inletheader=Header.add_unless_existsheader"Connection""upgrade"inletheader=Header.add_transfer_encodingheaderUnknowninletheader=maybe_overwrite_sec_accept_headerheaderinOption.value_mapsubprotocol~default:header~f:(funsubprotocol->Header.add_websocket_subprotocolheader~subprotocol);;moduleExpect_test_config=Core.Expect_test_config(* {v https://tools.ietf.org/html/rfc6455#section-10.2
10.2. Origin Considerations
Servers that are not intended to process input from any web page but
only for certain sites SHOULD verify the |Origin| field is an origin
they expect. If the origin indicated is unacceptable to the server,
then it SHOULD respond to the WebSocket handshake with a reply
containing HTTP 403 Forbidden status code.
The |Origin| header field protects from the attack cases when the
untrusted party is typically the author of a JavaScript application
that is executing in the context of the trusted client. The client
itself can contact the server and, via the mechanism of the |Origin|
header field, determine whether to extend those communication
privileges to the JavaScript application. The intent is not to
prevent non-browsers from establishing connections but rather to
ensure that trusted browsers under the control of potentially
malicious JavaScript cannot fake a WebSocket handshake.
v}
*)letdetect_request_type_and_authorize~auth~inetheaders=letopenResult.Let_syntaxinletmaybe_websocket_key=Header.getheaders"sec-websocket-key"inlet%map()=authinetheaders~is_websocket_request:(Option.is_somemaybe_websocket_key)inmatchmaybe_websocket_keywith|None->`Not_a_websocket_request|Somesec_websocket_key->`Websocket_request(`Sec_websocket_keysec_websocket_key);;letdefault_auth(_:Socket.Address.Inet.t)header~is_websocket_request=ifis_websocket_requestthenHeader.origin_and_host_matchheaderelseOk();;let%test_module_=(modulestructletirrelevant_inet=Socket.Address.Inet.create_bind_any~port:0letcheck~authheaders=print_s[%sexp(detect_request_type_and_authorize~inet:irrelevant_inet~authheaders:[`Not_a_websocket_request|`Websocket_requestof[`Sec_websocket_keyofstring]]Or_error.t)];;let%expect_test"Only perform websocket validation if the request is for a \
websocket upgrade"=letcheck=check~auth:default_authincheck(Header.of_list["host","valid-host";"origin","https://bogus"]);[%expect{| (Ok Not_a_websocket_request) |}];check(Header.of_list["sec-websocket-key","not-important"]);[%expect{|
(Error ("Missing one of origin or host header" (origin ()) (host ()))) |}];check(Header.of_list["origin","http://h";"host","h";"sec-websocket-key","not-important"]);[%expect{|
(Ok (Websocket_request (Sec_websocket_key not-important))) |}];;let%expect_test"detect_request_type_and_authorize provides correct \
[is_websocket_request] and faithfully returns the result of the \
auth function"=letauthresponseaddressheaders~is_websocket_request=print_s[%sexp{address:Socket.Address.Inet.t;is_websocket_request:bool;headers:Header.t}];responseinletcheckresponseheaders=check~auth:(authresponse)headersinletnon_websocket_headers=Header.of_list["host","valid-host";"origin","https://bogus"]inletwebsocket_headers=Header.of_list["sec-websocket-key","not-important"]inletfail=error_s[%message"fail"]incheck(Ok())non_websocket_headers;[%expect{|
((address 0.0.0.0:PORT) (is_websocket_request false)
(headers ((host valid-host) (origin https://bogus))))
(Ok Not_a_websocket_request) |}];checkfailnon_websocket_headers;[%expect{|
((address 0.0.0.0:PORT) (is_websocket_request false)
(headers ((host valid-host) (origin https://bogus))))
(Error fail) |}];check(Ok())websocket_headers;[%expect{|
((address 0.0.0.0:PORT) (is_websocket_request true)
(headers ((sec-websocket-key not-important))))
(Ok (Websocket_request (Sec_websocket_key not-important))) |}];checkfailwebsocket_headers;[%expect{|
((address 0.0.0.0:PORT) (is_websocket_request true)
(headers ((sec-websocket-key not-important))))
(Error fail) |}];;end);;letforbiddenrequeste=Log.Global.error_s[%message"Failed to validate apparent websocket request"~_:(e:Error.t)~_:(request:Request.t)];return(`Response(Response.make()~status:`Forbidden,Body.empty));;letcreate~non_ws_request?opcode?(should_process_request=default_auth)?(websocket_subprotocol_selection=Fn.const(`SubprotocolNone))(f:websocket_handler)~bodyinetrequest=letheaders=request.Request.headersinmatchdetect_request_type_and_authorize~auth:should_process_request~inetheaderswith|Errore->forbiddenrequeste|Ok(`Websocket_request(`Sec_websocket_keysec_websocket_key))->let(`Subprotocolsubprotocol)=websocket_subprotocol_selectionrequestinlet%bind{set_response_headers;should_overwrite_sec_accept_header;handle_connection}=f~inet~subprotocolrequestinletheaders=websocket_handshake_headers~initial_headers:set_response_headers~sec_websocket_key~should_overwrite_sec_accept_header~subprotocolinletio_handlerreaderwriter=letwebsocket=Websocket.create?opcode~role:ServerreaderwriterinDeferred.all_unit[handle_connectionwebsocket;Deferred.ignore_m(Websocket.close_finishedwebsocket)]inletresponse=Response.make()~encoding:(Header.get_transfer_encodingheaders)~status:`Switching_protocols~headersinreturn(`Expert(response,io_handler))|Ok`Not_a_websocket_request->let%mapr=non_ws_request~bodyinetrequestin`Responser;;endmoduleClient=structletrandom_key()=letchars=String.init16~f:(fun_->Char.of_int_exn(Random.int(Char.to_intChar.max_value)))inBase64.encode_exnchars;;letwebsocket_header?(headers=Header.init())hnp~scheme=lethnp_str=Host_and_port.to_stringhnpinletheader=Header.add_listheaders["Upgrade","websocket";"Connection","Upgrade";"Sec-Websocket-Key",random_key();"Sec-Websocket-Version","13"]inletheader=Header.add_unless_existsheader"Host"hnp_strinHeader.add_unless_existsheader"Origin"(scheme^"://"^hnp_str);;letwebsocket_request?headershost_and_porturi=letinsecure_websocket_scheme="ws"inRequest.make~encoding:Chunked~headers:(websocket_header?headershost_and_port~scheme:(Uri.schemeuri|>Option.value~default:insecure_websocket_scheme))~meth:`GET~version:`HTTP_1_1uri;;moduleRequest_=Cohttp.Request.Make(Cohttp_async.Io)moduleResponse_=Cohttp.Response.Make(Cohttp_async.Io)letread_websocket_response(request:Request.t)reader=match%mapResponse_.readreaderwith|(`Eof|`Invalid_)asresponse->error_s[%message"Bad response to websocket request"(response:[`Eof|`Invalidofstring])]|`Okresponse->(matchresponse.statuswith|`Switching_protocols->letwebsocket_key="Sec-Websocket-Key"in(matchHeader.getrequest.headerswebsocket_keywith|None->(* This should never happen, the header must be provided, see
https://tools.ietf.org/html/rfc6455#section-1.3 *)error_s[%message"Request missing required header"~header:websocket_key]|Somesec_websocket_key->(* From https://tools.ietf.org/html/rfc6455#section-4.1:
4. If the response lacks a |Sec-WebSocket-Accept| header field or
the |Sec-WebSocket-Accept| contains a value other than the
base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
Key| (as a string, not base64-decoded) with the string "258EAFA5-
E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
trailing whitespace, the client MUST _Fail the WebSocket
Connection_.
*)letexpected_sec_websocket_accept=Websocket.sec_websocket_accept_header_value~sec_websocket_keyin(matchHeader.getresponse.headerswebsocket_accept_header_namewith|Somesec_websocket_accept->ifString.equalsec_websocket_acceptexpected_sec_websocket_acceptthenOkresponseelseerror_s[%message"Bad value for header"~header:websocket_accept_header_name~value:sec_websocket_accept~expected:expected_sec_websocket_accept]|None->error_s[%message"Missing header"~header:websocket_accept_header_nameexpected_sec_websocket_accept]))|status->error_s[%message"Response status code not supported, expected a 101: switching protocols"(status:Code.status_code)(response.headers:Header.t)~code:(Code.code_of_statusstatus:int)]);;letwrap_in_ssl?hostname_for_sslreaderwriter=letapp_to_ssl_r,app_to_ssl_w=Pipe.create()inletssl_to_app_r,ssl_to_app_w=Pipe.create()inlet%bindconnection=letverify_modes=ifPpx_inline_test_lib.am_runningthenSome[]elseNoneinAsync_ssl.Ssl.client~app_to_ssl:app_to_ssl_r~ssl_to_app:ssl_to_app_w~net_to_ssl:(Reader.pipereader)~ssl_to_net:(Writer.pipewriter)?verify_modes?hostname:hostname_for_ssl()>>|Or_error.ok_exninlet%bindapp_to_ssl,`Closed_and_flushed_downstream_=Writer.of_pipe(Info.of_string"app_to_ssl")app_to_ssl_win(* When the pipe (app_to_ssl) is closed, there will be a short period of time when
the [writer] will still be open. Any message sent to the writer during that time
will be lost. *)Writer.set_raise_when_consumer_leavesapp_to_sslfalse;let%mapssl_to_app=Reader.of_pipe(Info.of_string"ssl_to_app")ssl_to_app_rinletclose()=Async_ssl.Ssl.Connection.closeconnection;let%bind()=Reader.closessl_to_appinDeferred.ignore_m(Async_ssl.Ssl.Connection.closedconnection:unitOr_error.tDeferred.t)inclose,ssl_to_app,app_to_ssl;;lethost_and_port_of_uriuri=matchUri.hosturiwith|None->Or_error.error_s[%message"No host given in URI"(uri:Uri_sexp.t)]|Somehost->(matchUri.porturiwith|Someport->Ok(Host_and_port.create~host~port)|None->(matchUri.schemeuriwith|Some"ws"->Ok(Host_and_port.create~host~port:80)|Some"wss"->Ok(Host_and_port.create~host~port:443)|Somescheme->Or_error.error_s[%message"No port given in URI and using an unknown scheme, couldn't determine \
port"(scheme:string)(uri:Uri_sexp.t)]|None->Or_error.error_s[%message"Neither port nor scheme given in URI, couldn't determine port"(uri:Uri_sexp.t)]));;leturi_is_ssluri=matchUri.schemeuriwith|Some"wss"->true|_->false;;letcreate?bind_to_address?force_ssl_overriding_SNI_hostname?opcode?headersuri=matchhost_and_port_of_uriuriwith|Error_aserror->returnerror|Okhost_and_port->letshutdown=Ivar.create()in(match%bindMonitor.try_with~run:`Schedule(fun()->Tcp.connect(Tcp.Where_to_connect.of_host_and_port?bind_to_addresshost_and_port))~rest:(`Call(funexn->Log.Global.sexp[%message"Connection closed. Closing websocket client."(exn:exn)];Ivar.fill_if_emptyshutdown()))with|Errorexn->return(Or_error.of_exnexn)|Ok(_,reader,writer)->let%bindclose_tcp_connection,reader,writer=matchforce_ssl_overriding_SNI_hostnamewith|Somehostname_for_ssl->wrap_in_ssl~hostname_for_sslreaderwriter|None->(matchuri_is_ssluriwith|true->wrap_in_ssl?hostname_for_ssl:force_ssl_overriding_SNI_hostnamereaderwriter|false->letclose()=Writer.closewriterinreturn(close,reader,writer))inletrequest=websocket_request?headershost_and_porturiinlet%bind()=Request_.write_headerrequestwriterin(match%bindread_websocket_responserequestreaderwith|Error_aserror->let%bind()=close_tcp_connection()inIvar.fill_if_emptyshutdown();returnerror|Okresponse->letopenDeferred.Let_syntaxinletws=Websocket.create?opcode~role:Clientreaderwriterinletreader,writer=Websocket.pipeswsindon't_wait_for(let%bind()=Deferred.any[Ivar.readshutdown;Pipe.closedwriter;Pipe.closedreader]inPipe.closewriter;let%bind()=Pipe.closedwriterinlet%bind()=close_tcp_connection()inPipe.close_readreader;let%bindreason,msg,info=Websocket.close_finishedwsinLog.Global.sexp[%message"Websocket closed"(reason:Websocket.Connection_close_reason.t)(msg:string)(info:(Info.toption[@sexp.omit_nil]))];return());return(Ok(response,ws))));;letwith_websocket_client?opcode?headersuri~f=match%bindcreate?opcode?headersuriwith|Error_aserr->returnerr|Ok(response,ws)->let%bindresult=fresponsewsinlet_reader,writer=Websocket.pipeswsinPipe.closewriter;return(Okresult);;end