(*---------------------------------------------------------------------------- * Copyright (c) 2017 Inhabited Type LLC. * Copyright (c) 2019 Antonio N. 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 author nor the names of his contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE 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 AUTHORS 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. *---------------------------------------------------------------------------*) open Faraday module IOVec = Httpun_types.IOVec type frame_info = { flags : Flags.t ; stream_id : Stream_identifier.t ; padding : Bigstringaf.t ; max_frame_payload : int } let write_uint24 t n = let write_octet t o = write_uint8 t (o land 0xff) in write_octet t (n lsr 16); write_octet t (n lsr 8); write_octet t n let write_frame_header t frame_header = let { Frame.payload_length; flags; stream_id; frame_type } = frame_header in write_uint24 t payload_length; write_uint8 t (Frame.FrameType.serialize frame_type); write_uint8 t flags; BE.write_uint32 t stream_id let write_frame_with_padding t info frame_type length writer = let header, writer = if Bigstringaf.length info.padding = 0 then let header = { Frame.payload_length = length ; flags = info.flags ; stream_id = info.stream_id ; frame_type } in header, writer else let pad_length = Bigstringaf.length info.padding in let writer' t = write_uint8 t pad_length; writer t; schedule_bigstring ~off:0 ~len:pad_length t info.padding in let header = { Frame.payload_length = length + pad_length + 1 ; flags = Flags.set_padded info.flags ; stream_id = info.stream_id ; frame_type } in header, writer' in write_frame_header t header; writer t let write_data_frame t ?off ?len info body = let writer t = write_string t ?off ?len body in let length = match len with Some len -> len | None -> String.length body in write_frame_with_padding t info Data length writer let schedule_data_frame t info ?off ?len bstr = let writer t = schedule_bigstring t ?off ?len bstr in let length = match len with Some len -> len | None -> Bigstringaf.length bstr in write_frame_with_padding t info Data length writer let write_priority t { Priority.exclusive; stream_dependency; weight } = let stream_dependency_id = if exclusive then Priority.set_exclusive stream_dependency else stream_dependency in BE.write_uint32 t stream_dependency_id; (* From RFC7540§6.3: * An unsigned 8-bit integer representing a priority weight for the stream * (see Section 5.3). Add one to the value to obtain a weight between 1 and * 256. * * Note: we store priority with values from 1 to 256, so decrement here. *) write_uint8 t (weight - 1) let bounded_schedule_iovecs t ~len iovecs = let rec loop t remaining iovecs = match remaining, iovecs with | 0, _ | _, [] -> () | remaining, { IOVec.buffer; off; len } :: xs -> if remaining < len then schedule_bigstring t ~off ~len:remaining buffer else ( schedule_bigstring t ~off ~len buffer; loop t (remaining - len) xs) in loop t len iovecs let write_headers_frame t info ~priority ?len iovecs = let len = match len with Some len -> len | None -> IOVec.lengthv iovecs in if priority == Priority.default_priority then (* See RFC7540§6.3: * Just the Header Block Fragment length if no priority. *) let writer t = bounded_schedule_iovecs t ~len iovecs in write_frame_with_padding t info Headers len writer else (* See RFC7540§6.2: * Exclusive Bit & Stream Dependency (4 octets) + Weight (1 octet) + * Header Block Fragment length. *) let payload_length = len + 5 in let info' = { info with flags = Flags.set_priority info.flags } in let writer t = write_priority t priority; bounded_schedule_iovecs t ~len iovecs in write_frame_with_padding t info' Headers payload_length writer let write_priority_frame t info priority = let header = { Frame.flags = info.flags ; stream_id = info.stream_id (* See RFC7540§6.3: * Stream Dependency (4 octets) + Weight (1 octet). *) ; payload_length = 5 ; frame_type = Priority } in write_frame_header t header; write_priority t priority let write_rst_stream_frame t info e = let header = { Frame.flags = info.flags ; stream_id = info.stream_id (* From RFC7540§6.4: * The RST_STREAM frame contains a single unsigned, 32-bit integer * identifying the error code (Section 7). *) ; payload_length = 4 ; frame_type = RSTStream } in write_frame_header t header; BE.write_uint32 t (Error_code.serialize e) let write_settings_frame t info settings = let header = { Frame.flags = info.flags ; stream_id = info.stream_id (* 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. *) ; payload_length = List.length settings * 6 ; frame_type = Settings } in write_frame_header t header; Settings.write_settings_payload t settings let write_push_promise_frame t info ~promised_id ?len iovecs = let len = match len with Some len -> len | None -> IOVec.lengthv iovecs in let payload_length = (* From RFC7540§6.6: * The PUSH_PROMISE frame includes the unsigned 31-bit identifier of the * stream the endpoint plans to create along with a set of headers that * provide additional context for the stream. *) 4 + len in let writer t = BE.write_uint32 t promised_id; bounded_schedule_iovecs t ~len iovecs in write_frame_with_padding t info PushPromise payload_length writer let default_ping_payload = (* From RFC7540§6.7: * In addition to the frame header, PING frames MUST contain 8 octets of * opaque data in the payload. *) let bstr = Bigstringaf.create 8 in for i = 0 to 7 do Bigstringaf.unsafe_set bstr i '\000' done; bstr let write_ping_frame t info ?(off = 0) payload = (* From RFC7540§6.7: * In addition to the frame header, PING frames MUST contain 8 octets of * opaque data in the payload. *) let payload_length = 8 in let header = { Frame.flags = info.flags ; stream_id = info.stream_id ; payload_length ; frame_type = Ping } in write_frame_header t header; schedule_bigstring ~off ~len:payload_length t payload let write_go_away_frame t info stream_id error_code debug_data = let debug_data_len = Bigstringaf.length debug_data in let header = { Frame.flags = info.flags ; stream_id = info.stream_id (* See RFC7540§6.8: * Last-Stream-ID (4 octets) + Error Code (4 octets) + Additional * Debug Data (opaque) *) ; payload_length = 8 + debug_data_len ; frame_type = GoAway } in write_frame_header t header; BE.write_uint32 t stream_id; BE.write_uint32 t (Error_code.serialize error_code); schedule_bigstring t ~off:0 ~len:debug_data_len debug_data let write_window_update_frame t info window_size = let header = { Frame.flags = info.flags ; stream_id = info.stream_id (* From RFC7540§6.9: * The payload of a WINDOW_UPDATE frame is one reserved bit plus an * unsigned 31-bit integer indicating the number of octets that the * sender can transmit in addition to the existing flow-control * window. *) ; payload_length = 4 ; frame_type = WindowUpdate } in write_frame_header t header; BE.write_uint32 t window_size let write_continuation_frame t info ?len iovecs = let len = match len with Some len -> len | None -> IOVec.lengthv iovecs in let header = { Frame.flags = info.flags ; stream_id = info.stream_id ; payload_length = len ; frame_type = Continuation } in write_frame_header t header; bounded_schedule_iovecs t ~len iovecs let write_unknown_frame t ~code info payload = let payload_length = Bigstringaf.length payload in let header = { Frame.flags = info.flags ; stream_id = info.stream_id ; payload_length ; frame_type = Unknown code } in write_frame_header t header; schedule_bigstring t ~off:0 ~len:payload_length payload let write_connection_preface t = (* From RFC7540§3.5: * In HTTP/2, each endpoint is required to send a connection preface as a * final confirmation of the protocol in use and to establish the initial * settings for the HTTP/2 connection. [...] The client connection preface * starts with a sequence of 24 octets, [...] the string * PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n. *) write_string t Frame.connection_preface module Writer = struct type t = { buffer : Bigstringaf.t (* The buffer that the encoder uses for buffered writes. Managed by * the control module for the encoder. *) ; encoder : Faraday.t (* The encoder that handles encoding for writes. Uses the [buffer] * referenced above internally. *) ; mutable drained_bytes : int (* The number of bytes that were not written due to the output stream * being closed before all buffered output could be written. Useful * for detecting error cases. *) ; mutable wakeup : Optional_thunk.t } let create buffer_size = let buffer = Bigstringaf.create buffer_size in let encoder = Faraday.of_bigstring buffer in { buffer; encoder; drained_bytes = 0; wakeup = Optional_thunk.none } let faraday t = t.encoder let make_frame_info ?(padding = Bigstringaf.empty) ?(flags = Flags.default_flags) ?(max_frame_size = Config.default.read_buffer_size) stream_id = { flags; stream_id; padding; max_frame_payload = max_frame_size } let write_connection_preface t settings_list = write_connection_preface t.encoder; let frame_info = make_frame_info Stream_identifier.connection in (* From RFC7540§3.5: * This sequence MUST be followed by a SETTINGS frame (Section 6.5), * which MAY be empty. *) write_settings_frame t.encoder frame_info settings_list let chunk_data_frames ?(off = 0) ~f frame_info total_length = let { max_frame_payload; _ } = frame_info in if max_frame_payload < total_length then let rec loop ~off remaining = if max_frame_payload < remaining then ( (* Note: If we're splitting data into several frames, only the last * one should contain the END_STREAM flag, so unset it here if it's * set. *) let frame_info = { frame_info with flags = Flags.clear_end_stream frame_info.flags } in f ~off ~len:max_frame_payload frame_info; loop ~off:(off + max_frame_payload) (remaining - max_frame_payload)) else f ~off ~len:remaining frame_info in loop ~off total_length else f ~off ~len:total_length frame_info let write_data t frame_info ?off ?len str = if not (is_closed t.encoder) then let total_length = match len with Some len -> len | None -> String.length str in chunk_data_frames frame_info ?off total_length ~f:(fun ~off ~len frame_info -> write_data_frame t.encoder frame_info ~off ~len str) let schedule_data t frame_info ?off ?len bstr = if not (is_closed t.encoder) then let total_length = match len with Some len -> len | None -> Bigstringaf.length bstr in chunk_data_frames frame_info ?off total_length ~f:(fun ~off ~len frame_info -> schedule_data_frame t.encoder frame_info ~off ~len bstr) (* Chunk header block fragments into HEADERS|PUSH_PROMISE + CONTINUATION * frames. *) let chunk_header_block_fragments t frame_info ?(has_priority = false) ~(write_frame : Faraday.t -> frame_info -> ?len:int -> Bigstringaf.t iovec list -> unit) faraday = let block_size = Faraday.pending_bytes faraday in let total_length = if has_priority then (* See RFC7540§6.2: Exclusive Bit & Stream Dependency (4 octets) + Weight (1 octet) + Header Block Fragment length. *) block_size + 5 else block_size in let { max_frame_payload; _ } = frame_info in if max_frame_payload < total_length then ( let headers_block_len = if has_priority then max_frame_payload - 5 else max_frame_payload in ignore (Faraday.serialize faraday (fun iovecs -> write_frame t.encoder frame_info ~len:headers_block_len iovecs; `Ok headers_block_len)); let rec loop remaining = if max_frame_payload < remaining then ( (* Note: Don't reuse flags from frame info as CONTINUATION frames * only define END_HEADERS. * * From RFC7540§6.10: * The CONTINUATION frame defines the following flag: * * END_HEADERS (0x4): When set, bit 2 indicates that this frame * ends a header block (Section 4.3). *) let frame_info = { frame_info with flags = Flags.default_flags } in ignore (Faraday.serialize faraday (fun iovecs -> write_continuation_frame t.encoder frame_info ~len:max_frame_payload iovecs; `Ok max_frame_payload)); loop (remaining - max_frame_payload)) else let frame_info = { frame_info with flags = Flags.(set_end_header default_flags) } in ignore (Faraday.serialize faraday (fun iovecs -> write_continuation_frame t.encoder frame_info ~len:remaining iovecs; `Ok remaining)) in loop (block_size - headers_block_len)) else let frame_info = { frame_info with flags = Flags.set_end_header frame_info.flags } in ignore (Faraday.serialize faraday (fun iovecs -> let len = IOVec.lengthv iovecs in write_frame t.encoder frame_info ~len iovecs; `Ok len)) let encode_headers hpack_encoder faraday headers = List.iter (fun header -> Hpack.Encoder.encode_header hpack_encoder faraday header) (Headers.to_hpack_list headers) let write_request_like_frame t hpack_encoder ~write_frame frame_info request = let { Request.meth; target; scheme; headers } = request in let faraday = Faraday.create 0x1000 in Hpack.Encoder.encode_header hpack_encoder faraday { Headers.name = ":method" ; value = Httpun_types.Method.to_string meth ; sensitive = false }; if meth <> `CONNECT then ( (* From RFC7540§8.3: * The :scheme and :path pseudo-header fields MUST be omitted. *) Hpack.Encoder.encode_header hpack_encoder faraday { Headers.name = ":path"; value = target; sensitive = false }; Hpack.Encoder.encode_header hpack_encoder faraday { Headers.name = ":scheme"; value = scheme; sensitive = false }); encode_headers hpack_encoder faraday headers; chunk_header_block_fragments t frame_info ~write_frame faraday let write_request_headers t hpack_encoder ~priority frame_info request = if not (is_closed t.encoder) then let write_frame = write_headers_frame ~priority in write_request_like_frame t hpack_encoder ~write_frame frame_info request let write_push_promise t hpack_encoder frame_info ~promised_id request = if not (is_closed t.encoder) then let write_frame = write_push_promise_frame ~promised_id in write_request_like_frame t hpack_encoder ~write_frame frame_info request let write_response_headers t hpack_encoder frame_info response = if not (is_closed t.encoder) then ( let { Response.status; headers; _ } = response in let faraday = Faraday.create 0x1000 in (* From RFC7540§8.1.2.4: * For HTTP/2 responses, a single :status pseudo-header field is defined * that carries the HTTP status code field (see [RFC7231], Section 6). * This pseudo-header field MUST be included in all responses; otherwise, * the response is malformed (Section 8.1.2.6). *) Hpack.Encoder.encode_header hpack_encoder faraday { Headers.name = ":status" ; value = Status.to_string status ; sensitive = false }; encode_headers hpack_encoder faraday headers; chunk_header_block_fragments t frame_info ~write_frame:(write_headers_frame ~priority:Priority.default_priority) ~has_priority:false faraday) let write_response_trailers t hpack_encoder frame_info trailers = if not (is_closed t.encoder) then ( let faraday = Faraday.create 0x1000 in (* From RFC7540§8.1: * optionally, one HEADERS frame, followed by zero or more * CONTINUATION frames containing the trailer-part, if present (see * [RFC7230], Section 4.1.2). *) encode_headers hpack_encoder faraday trailers; chunk_header_block_fragments t frame_info ~write_frame:(write_headers_frame ~priority:Priority.default_priority) ~has_priority:false faraday) let write_rst_stream t frame_info e = if not (is_closed t.encoder) then write_rst_stream_frame t.encoder frame_info e let write_window_update t frame_info n = if not (is_closed t.encoder) then write_window_update_frame t.encoder frame_info n let schedule_iovecs t ~len frame_info iovecs = if not (is_closed t.encoder) then let writer t ~len ~iovecs = bounded_schedule_iovecs t ~len iovecs in chunk_data_frames frame_info len ~f:(fun ~off ~len frame_info -> write_frame_with_padding t.encoder frame_info Data len (writer ~iovecs:(IOVec.shiftv iovecs off) ~len)) let write_priority t frame_info priority = if not (is_closed t.encoder) then write_priority_frame t.encoder frame_info priority let write_settings t frame_info settings = if not (is_closed t.encoder) then write_settings_frame t.encoder frame_info settings let write_ping t frame_info ?off payload = if not (is_closed t.encoder) then write_ping_frame t.encoder frame_info ?off payload let write_go_away t frame_info ~debug_data ~last_stream_id error = if not (is_closed t.encoder) then write_go_away_frame t.encoder frame_info last_stream_id error debug_data let on_wakeup_writer t k = if Faraday.is_closed t.encoder then failwith "on_wakeup_writer on closed conn" else if Optional_thunk.is_some t.wakeup then failwith "on_wakeup: only one callback can be registered at a time" else t.wakeup <- Optional_thunk.some k let wakeup t = let f = t.wakeup in t.wakeup <- Optional_thunk.none; Optional_thunk.call_if_some f let flush t f = flush_with_reason t.encoder (fun reason -> let result = match reason with | Nothing_pending | Shift -> `Written | Drain -> `Closed in f result) let unyield t = (* Faraday doesn't have a function to take the serializer out of a yield state. In the meantime, `flush` does it. *) flush t (fun _reason -> ()) let yield t = Faraday.yield t.encoder let close t = Faraday.close t.encoder let close_and_drain t = Faraday.close t.encoder; let drained = Faraday.drain t.encoder in t.drained_bytes <- t.drained_bytes + drained let is_closed t = Faraday.is_closed t.encoder let drained_bytes t = t.drained_bytes let report_result t result = match result with | `Closed -> close_and_drain t | `Ok len -> shift t.encoder len let next t = match Faraday.operation t.encoder with | `Close -> `Close (drained_bytes t) | `Yield -> `Yield | `Writev iovecs -> `Write iovecs end