forked from
anil.recoil.org/ocaml-requests
A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** WebSocket Protocol Support (RFC 6455)
7
8 This module provides functions for the WebSocket HTTP upgrade handshake.
9 WebSocket connections are established by upgrading an HTTP/1.1 connection
10 using the Upgrade mechanism.
11
12 {2 Basic Usage}
13
14 {[
15 (* Client: initiate WebSocket upgrade *)
16 let key = Websocket.generate_key () in
17 let headers = Websocket.upgrade_headers ~key () in
18 (* ... send request with these headers ... *)
19
20 (* Client: validate server response *)
21 match Websocket.validate_upgrade_response ~key ~status ~headers with
22 | Ok () -> (* Connection upgraded successfully *)
23 | Error reason -> (* Handshake failed *)
24 ]}
25
26 @see <https://www.rfc-editor.org/rfc/rfc6455>
27 RFC 6455: The WebSocket Protocol *)
28
29(** {1 Constants} *)
30
31val protocol_version : string
32(** The WebSocket protocol version string per RFC 6455 (always {v 13 v}).
33 Used as the value for the Sec-WebSocket-Version header. *)
34
35val magic_guid : string
36(** The magic GUID used in Sec-WebSocket-Accept computation.
37 @see <https://www.rfc-editor.org/rfc/rfc6455#section-1.3>
38 RFC 6455 Section 1.3. *)
39
40(** {1 Sec-WebSocket-Key}
41
42 @see <https://www.rfc-editor.org/rfc/rfc6455#section-4.1>
43 RFC 6455 Section 4.1 *)
44
45val generate_key : unit -> string
46(** [generate_key ()] creates a random Sec-WebSocket-Key value.
47
48 Generates a cryptographically random 16-byte nonce and base64-encodes it.
49 The result is suitable for use in the Sec-WebSocket-Key header. *)
50
51(** {1 Sec-WebSocket-Accept}
52
53 @see <https://www.rfc-editor.org/rfc/rfc6455#section-4.2.2>
54 RFC 6455 Section 4.2.2 *)
55
56val compute_accept : key:string -> string
57(** [compute_accept ~key] computes the expected Sec-WebSocket-Accept value.
58
59 The computation is: [base64(SHA-1(key + magic_guid))]
60
61 @param key The Sec-WebSocket-Key sent by the client
62 @return The expected Sec-WebSocket-Accept value. *)
63
64val validate_accept : key:string -> accept:string -> bool
65(** [validate_accept ~key ~accept] validates a server's Sec-WebSocket-Accept.
66
67 @param key The Sec-WebSocket-Key that was sent
68 @param accept The Sec-WebSocket-Accept received from the server
69 @return [true] if the accept value is correct. *)
70
71(** {1 Sec-WebSocket-Protocol}
72
73 @see <https://www.rfc-editor.org/rfc/rfc6455#section-11.3.4>
74 RFC 6455 Section 11.3.4 *)
75
76val parse_protocols : string -> string list
77(** [parse_protocols s] parses a Sec-WebSocket-Protocol header value.
78
79 Example: ["graphql-ws, graphql-transport-ws"] ->
80 [["graphql-ws"; "graphql-transport-ws"]]. *)
81
82val protocols_to_string : string list -> string
83(** [protocols_to_string protocols] formats protocols as a header value. *)
84
85val select_protocol :
86 offered:string list -> supported:string list -> string option
87(** [select_protocol ~offered ~supported] selects a mutually acceptable
88 protocol.
89
90 @param offered The protocols offered by the client
91 @param supported The protocols we support (in preference order)
92 @return The selected protocol, or [None] if no match. *)
93
94(** {1 Sec-WebSocket-Extensions}
95
96 @see <https://www.rfc-editor.org/rfc/rfc6455#section-9> RFC 6455 Section 9
97 @see <https://www.rfc-editor.org/rfc/rfc7692>
98 RFC 7692: Compression Extensions *)
99
100type extension = { name : string; params : (string * string option) list }
101(** An extension with optional parameters.
102
103 Example: [permessage-deflate; client_max_window_bits] *)
104
105val parse_extensions : string -> extension list
106(** [parse_extensions s] parses a Sec-WebSocket-Extensions header value.
107
108 Example: ["permessage-deflate; client_max_window_bits"]. *)
109
110val extensions_to_string : extension list -> string
111(** [extensions_to_string extensions] formats extensions as a header value. *)
112
113val has_extension : name:string -> extension list -> bool
114(** [has_extension ~name extensions] checks if an extension is present. *)
115
116val extension_params :
117 name:string -> extension list -> (string * string option) list option
118(** [extension_params ~name extensions] gets parameters for an extension. *)
119
120(** {1 Handshake Helpers} *)
121
122val upgrade_headers :
123 key:string ->
124 ?protocols:string list ->
125 ?extensions:extension list ->
126 ?origin:string ->
127 unit ->
128 Headers.t
129(** [upgrade_headers ~key ?protocols ?extensions ?origin ()] builds headers for
130 a WebSocket upgrade request.
131
132 Sets the following headers:
133 - [Upgrade: websocket]
134 - [Connection: Upgrade]
135 - [Sec-WebSocket-Key: {key}]
136 - [Sec-WebSocket-Version: 13]
137 - [Sec-WebSocket-Protocol: ...] (if protocols provided)
138 - [Sec-WebSocket-Extensions: ...] (if extensions provided)
139 - [Origin: ...] (if origin provided)
140
141 @param key The Sec-WebSocket-Key (use {!generate_key} to create)
142 @param protocols Optional list of subprotocols to request
143 @param extensions Optional list of extensions to request
144 @param origin Optional Origin header value. *)
145
146val validate_upgrade_response :
147 key:string -> status:int -> headers:Headers.t -> (unit, string) result
148(** [validate_upgrade_response ~key ~status ~headers] validates a WebSocket
149 upgrade response.
150
151 Checks that:
152 - Status code is 101 (Switching Protocols)
153 - Upgrade header is "websocket"
154 - Connection header includes "Upgrade"
155 - Sec-WebSocket-Accept is correct for the given key
156
157 @param key The Sec-WebSocket-Key that was sent
158 @param status The HTTP status code
159 @param headers The response headers
160 @return [Ok ()] if valid, [Error reason] if invalid. *)