A batteries included HTTP/1.1 client in OCaml
at main 160 lines 5.8 kB view raw
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. *)