(*---------------------------------------------------------------------------- * Copyright (c) 2019-2020 António Nuno 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 copyright holder nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 COPYRIGHT HOLDER 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. *---------------------------------------------------------------------------*) module WindowSize = struct type t = int32 (* From RFC7540§6.9.2: * When an HTTP/2 connection is first established, new streams are created * with an initial flow-control window size of 65,535 octets. *) let default_initial_window_size = 65535l (* From RFC7540§6.9: * The legal range for the increment to the flow-control window is 1 to * 2^31-1 (2,147,483,647) octets. *) let max_window_size = Int32.max_int (* Ideally `n` here would be an unsigned 32-bit integer, but OCaml doesn't * support them. We avoid introducing a new dependency on an unsigned integer * library by letting it overflow at parse time and checking if bit 31 is set * here, since * `Window.max_window_size` is never allowed to be above * 2^31-1 (see `max_window_size` above). * See http://caml.inria.fr/pub/ml-archives/caml-list/2004/07/f1c483068cc62075c916f7ad7d640ce0.fr.html * for more info. *) let is_window_overflow n = Util.test_bit_int32 n 31 end type setting = | HeaderTableSize of int | EnablePush of int | MaxConcurrentStreams of int32 | InitialWindowSize of int32 | MaxFrameSize (* this means payload size *) of int | MaxHeaderListSize of int type settings_list = setting list (* From RFC7540§6.5.1: * The payload of a SETTINGS frame consists of zero or more parameters, * each consisting of an unsigned 16-bit setting identifier and an * unsigned 32-bit value. *) let octets_per_setting = 6 let serialize_key = function | HeaderTableSize _ -> 0x1 | EnablePush _ -> 0x2 | MaxConcurrentStreams _ -> 0x3 | InitialWindowSize _ -> 0x4 | MaxFrameSize _ -> 0x5 | MaxHeaderListSize _ -> 0x6 let check_value ~is_client = function | EnablePush v -> if v <> 0 && v <> 1 then (* From RFC7540§6.5.2 * The initial value is 1, which indicates that server push is * permitted. Any value other than 0 or 1 MUST be treated as a * connection error (Section 5.4.1) of type PROTOCOL_ERROR. *) Error Error.( ConnectionError (ProtocolError, "SETTINGS_ENABLE_PUSH must be 0 or 1")) else if is_client && v = 1 then (* From RFC7540§8.2: * Clients MUST reject any attempt to change the * SETTINGS_ENABLE_PUSH setting to a value other than 0 by * treating the message as a connection error (Section 5.4.1) of * type PROTOCOL_ERROR. *) Error Error.( ConnectionError (ProtocolError, "Server must not try to enable SETTINGS_ENABLE_PUSH")) else Ok () | InitialWindowSize v when WindowSize.is_window_overflow v -> (* From RFC7540§6.5.2 * Values above the maximum flow-control window size of 2^31-1 MUST be * treated as a connection error (Section 5.4.1) of type * FLOW_CONTROL_ERROR. *) Error Error.( ConnectionError ( FlowControlError , Format.sprintf "Window size must be less than or equal to %ld" WindowSize.max_window_size )) | MaxFrameSize v when v < 16384 || v > 16777215 -> (* From RFC7540§6.5.2 * The initial value is 214 (16,384) octets. The value advertised by an * endpoint MUST be between this initial value and the maximum allowed * frame size (224-1 or 16,777,215 octets), inclusive. Values outside * this range MUST be treated as a connection error (Section 5.4.1) of * type PROTOCOL_ERROR. *) Error Error.( ConnectionError (ProtocolError, "Max frame size must be in the 16384 - 16777215 range")) | _ -> Ok () (* Check incoming settings and report an error if any. *) let check_settings_list ?(is_client = false) settings = let rec loop = function | [] -> Ok () | x :: xs -> (match check_value ~is_client x with | Ok () -> loop xs | Error _ as err -> err) in loop settings type t = { header_table_size : int ; enable_push : bool ; max_concurrent_streams : int32 ; (* Indicates the amount tokens the peer allows an H2 endpoint to send. *) initial_window_size : WindowSize.t ; max_frame_size : int ; max_header_list_size : int option } (* From RFC7540§11.3 *) let default = { header_table_size = 0x1000 ; enable_push = true (* From RFC7540§6.5.2: * SETTINGS_MAX_CONCURRENT_STREAMS (0x3): [...] Initially, there is no * limit to this value. *) ; max_concurrent_streams = Int32.max_int ; initial_window_size = WindowSize.default_initial_window_size ; max_frame_size = 0x4000 ; max_header_list_size = None } let settings_for_the_connection settings = let settings_list = if settings.max_frame_size <> default.max_frame_size then [ MaxFrameSize settings.max_frame_size ] else [] in let settings_list = if settings.max_concurrent_streams <> default.max_concurrent_streams then MaxConcurrentStreams settings.max_concurrent_streams :: settings_list else settings_list in let settings_list = if settings.initial_window_size <> default.initial_window_size then (* FIXME: don't convert *) InitialWindowSize settings.initial_window_size :: settings_list else settings_list in let settings_list = if settings.enable_push <> default.enable_push then EnablePush (if settings.enable_push then 1 else 0) :: settings_list else settings_list in settings_list let parse_settings_payload num_settings = let open Angstrom in let rec parse_inner acc remaining = (* From RFC7540§6.5.3: * The values in the SETTINGS frame MUST be processed in the order * they appear, with no other frame processing between values. *) if remaining <= 0 then return (List.rev acc) else lift2 (fun k (v : int32) -> match k with | 0x1 -> HeaderTableSize (Int32.to_int v) :: acc | 0x2 -> EnablePush (Int32.to_int v) :: acc | 0x3 -> MaxConcurrentStreams v :: acc | 0x4 -> InitialWindowSize v :: acc | 0x5 -> MaxFrameSize (Int32.to_int v) :: acc | 0x6 -> MaxHeaderListSize (Int32.to_int v) :: acc | _ -> (* Note: This ignores unknown settings. * * From RFC7540§6.5.3: * Unsupported parameters MUST be ignored. *) acc) BE.any_uint16 BE.any_int32 >>= fun acc' -> parse_inner acc' (remaining - 1) in parse_inner [] num_settings let write_settings_payload t settings_list = let open Faraday in List.iter (fun setting -> (* From RFC7540§6.5.1: * The payload of a SETTINGS frame consists of zero or more parameters, * each consisting of an unsigned 16-bit setting identifier and an * unsigned 32-bit value. *) BE.write_uint16 t (serialize_key setting); match setting with | MaxConcurrentStreams value | InitialWindowSize value -> BE.write_uint32 t value | HeaderTableSize value | EnablePush value | MaxFrameSize value | MaxHeaderListSize value -> BE.write_uint32 t (Int32.of_int value)) settings_list let of_settings_list settings = List.fold_left (fun (acc : t) item -> match item with | HeaderTableSize x -> { acc with header_table_size = x } | EnablePush x -> { acc with enable_push = x = 1 } | MaxConcurrentStreams x -> { acc with max_concurrent_streams = x } | InitialWindowSize new_val -> { acc with initial_window_size = new_val } | MaxFrameSize x -> { acc with max_frame_size = x } | MaxHeaderListSize x -> { acc with max_header_list_size = Some x }) default settings let of_base64 encoded = match Base64.decode ~alphabet:Base64.uri_safe_alphabet encoded with | Ok settings_payload -> let settings_payload_length = String.length settings_payload / octets_per_setting in (match Angstrom.parse_string ~consume:All (parse_settings_payload settings_payload_length) settings_payload with | Ok settings -> Ok (of_settings_list settings) | Error _ as e -> e) | Error (`Msg msg) -> Error msg let to_base64 t = let settings = settings_for_the_connection t in let faraday = Faraday.create (List.length settings * 6) in write_settings_payload faraday settings; let settings_hex = Faraday.serialize_to_string faraday in match Base64.encode ~alphabet:Base64.uri_safe_alphabet settings_hex with | Ok r -> Ok r | Error (`Msg msg) -> Error msg let pp_hum formatter t = let pp_elem formatter setting = let key, value = match setting with | HeaderTableSize v -> "HEADER_TABLE_SIZE", Int64.of_int v | EnablePush v -> "ENABLE_PUSH", Int64.of_int v | MaxConcurrentStreams v -> "MAX_CONCURRENT_STREAMS", Int64.of_int32 v | InitialWindowSize v -> "INITIAL_WINDOW_SIZE", Int64.of_int32 v | MaxFrameSize v -> "MAX_FRAME_SIZE", Int64.of_int v | MaxHeaderListSize v -> "MAX_HEADER_LIST_SIZE", Int64.of_int v in Format.fprintf formatter "@[(%S %Ld)@]" key value in Format.fprintf formatter "@[("; Format.pp_print_list pp_elem formatter (settings_for_the_connection t); Format.fprintf formatter ")@]"