1492 lines
62 KiB
OCaml
1492 lines
62 KiB
OCaml
(*----------------------------------------------------------------------------
|
|
* 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.
|
|
*---------------------------------------------------------------------------*)
|
|
|
|
module AB = Angstrom.Buffered
|
|
module Reader = Parse.Reader
|
|
module Writer = Serialize.Writer
|
|
module StreamsTbl = Scheduler.StreamsTbl
|
|
|
|
module Scheduler = Scheduler.Make (struct
|
|
include Stream
|
|
|
|
type t = Respd.t
|
|
|
|
let flush_write_body = Respd.flush_request_body
|
|
let requires_output = Respd.requires_output
|
|
end)
|
|
|
|
module Queue = struct
|
|
include Queue
|
|
|
|
let take_opt t = match is_empty t with true -> None | false -> Some (take t)
|
|
end
|
|
|
|
type error = Respd.error
|
|
type trailers_handler = Headers.t -> unit
|
|
type response_handler = Response.t -> Body.Reader.t -> unit
|
|
type error_handler = error -> unit
|
|
|
|
type t =
|
|
{ mutable settings : Settings.t
|
|
; reader : Reader.frame
|
|
; writer : Writer.t
|
|
; config : Config.t
|
|
; streams : Scheduler.t
|
|
; mutable current_stream_id : Stream_identifier.t
|
|
; mutable max_pushed_stream_id : Stream_identifier.t
|
|
; mutable current_server_streams : int
|
|
; mutable receiving_headers_for_stream : Stream_identifier.t option
|
|
; mutable did_send_go_away : bool
|
|
; mutable unacked_settings : int
|
|
; pending_pings : ((unit, [ `EOF ]) result -> unit) Queue.t
|
|
; error_handler : error -> unit
|
|
; push_handler : Request.t -> (response_handler, unit) result
|
|
(* From RFC7540§4.3:
|
|
* Header compression is stateful. One compression context and one
|
|
* decompression context are used for the entire connection. *)
|
|
; hpack_encoder : Hpack.Encoder.t
|
|
; hpack_decoder : Hpack.Decoder.t
|
|
}
|
|
|
|
let default_push_handler = Sys.opaque_identity (fun _ -> Ok (fun _ _ -> ()))
|
|
let is_closed t = Reader.is_closed t.reader && Writer.is_closed t.writer
|
|
let shutdown_reader t = Reader.force_close t.reader
|
|
|
|
let flush_request_body t =
|
|
Scheduler.flush t.streams (t.current_stream_id, t.max_pushed_stream_id)
|
|
|
|
let shutdown_writer t =
|
|
flush_request_body t;
|
|
Writer.close t.writer;
|
|
Writer.wakeup t.writer
|
|
|
|
(* Handling frames against closed streams is hard. See:
|
|
* https://docs.google.com/presentation/d/1iG_U2bKTc9CnKr0jPTrNfmxyLufx_cK2nNh9VjrKH6s
|
|
*)
|
|
let was_closed_or_implicitly_closed t stream_id =
|
|
if Stream_identifier.is_request stream_id
|
|
then Stream_identifier.(stream_id <= t.current_stream_id)
|
|
else Stream_identifier.(stream_id <= t.max_pushed_stream_id)
|
|
|
|
let report_error =
|
|
let shutdown_rw t =
|
|
shutdown_reader t;
|
|
shutdown_writer t
|
|
in
|
|
fun t e ->
|
|
match e with
|
|
| Error.ConnectionError (error, data) ->
|
|
if not t.did_send_go_away
|
|
then (
|
|
(* From RFC7540§5.4.1:
|
|
* An endpoint that encounters a connection error SHOULD first send a
|
|
* GOAWAY frame (Section 6.8) with the stream identifier of the last
|
|
* stream that it successfully received from its peer. The GOAWAY frame
|
|
* includes an error code that indicates why the connection is
|
|
* terminating. After sending the GOAWAY frame for an error condition,
|
|
* the endpoint MUST close the TCP connection. *)
|
|
let debug_data =
|
|
if String.length data = 0
|
|
then Bigstringaf.empty
|
|
else Bigstringaf.of_string ~off:0 ~len:(String.length data) data
|
|
in
|
|
let frame_info = Writer.make_frame_info Stream_identifier.connection in
|
|
(* TODO: Only write if not already shutdown. *)
|
|
Writer.write_go_away
|
|
t.writer
|
|
frame_info
|
|
~debug_data
|
|
~last_stream_id:
|
|
(if Stream_identifier.(t.current_stream_id === -1l)
|
|
then Stream_identifier.connection
|
|
else t.current_stream_id)
|
|
error;
|
|
t.did_send_go_away <- true;
|
|
if error <> Error_code.NoError
|
|
then t.error_handler (`Protocol_error (error, data));
|
|
Writer.flush t.writer (fun _reason ->
|
|
(* XXX: We need to allow lower numbered streams to complete before
|
|
* shutting down. *)
|
|
shutdown_rw t);
|
|
Writer.wakeup t.writer)
|
|
| StreamError (stream_id, error) ->
|
|
(match Scheduler.find t.streams stream_id with
|
|
| Some respd ->
|
|
Respd.report_error respd (`Protocol_error (error, "")) error
|
|
| None ->
|
|
if not (was_closed_or_implicitly_closed t stream_id)
|
|
then
|
|
(* Possible if the stream was going to enter the Idle state (first time
|
|
* we saw e.g. a PRIORITY frame for it) but had e.g. a
|
|
* FRAME_SIZE_ERROR. *)
|
|
let frame_info = Writer.make_frame_info stream_id in
|
|
Writer.write_rst_stream t.writer frame_info error);
|
|
Writer.wakeup t.writer
|
|
|
|
let report_connection_error t ?(reason = "") error =
|
|
report_error t (ConnectionError (error, reason))
|
|
|
|
let report_stream_error t stream_id error =
|
|
report_error t (StreamError (stream_id, error))
|
|
|
|
let shutdown t =
|
|
(* From RFC7540§6.8:
|
|
* A server that is attempting to gracefully shut down a connection SHOULD
|
|
* send an initial GOAWAY frame with the last stream identifier set to
|
|
* 2^31-1 and a NO_ERROR code. *)
|
|
report_connection_error t Error_code.NoError
|
|
|
|
let set_error_and_handle t stream error error_code =
|
|
Respd.report_error stream error error_code;
|
|
Writer.wakeup t.writer
|
|
|
|
let report_exn t exn =
|
|
if not (is_closed t)
|
|
then
|
|
let reason = Printexc.to_string exn in
|
|
report_connection_error t ~reason Error_code.InternalError
|
|
|
|
let send_window_update :
|
|
type a. t -> a Scheduler.PriorityTreeNode.node -> int32 -> unit
|
|
=
|
|
fun t stream n ->
|
|
let send_window_update_frame stream_id n =
|
|
let valid_inflow = Scheduler.add_inflow stream n in
|
|
assert valid_inflow;
|
|
let frame_info = Writer.make_frame_info stream_id in
|
|
Writer.write_window_update t.writer frame_info n
|
|
in
|
|
if Int32.compare n 0l > 0
|
|
then (
|
|
let max_window_size = Settings.WindowSize.max_window_size in
|
|
let stream_id = Scheduler.stream_id stream in
|
|
let rec loop n =
|
|
if n > max_window_size
|
|
then (
|
|
send_window_update_frame stream_id max_window_size;
|
|
loop (Int32.sub n max_window_size))
|
|
else send_window_update_frame stream_id n
|
|
in
|
|
loop n;
|
|
Writer.wakeup t.writer)
|
|
|
|
let handle_push_promise_headers t respd headers =
|
|
(* From RFC7540§8.2.2:
|
|
* The header fields in PUSH_PROMISE and any subsequent CONTINUATION frames
|
|
* MUST be a valid and complete set of request header fields (Section
|
|
* 8.1.2.3). *)
|
|
match Headers.method_path_and_scheme_or_malformed headers with
|
|
| `Malformed ->
|
|
(* From RFC7540§8.2.2:
|
|
* If a client receives a PUSH_PROMISE that does not include a complete
|
|
* and valid set of header fields [...] it MUST respond with a stream
|
|
* error (Section 5.4.2) of type PROTOCOL_ERROR. *)
|
|
report_stream_error t respd.Stream.id Error_code.ProtocolError
|
|
| `Valid (meth, path, scheme) ->
|
|
let meth = Httpun_types.Method.of_string meth in
|
|
(match
|
|
meth, Headers.get_pseudo headers "authority", Message.body_length headers
|
|
with
|
|
| (#Httpun_types.Method.standard as meth), _, _
|
|
when not Httpun_types.Method.(is_cacheable meth && is_safe meth) ->
|
|
report_stream_error t respd.id Error_code.ProtocolError
|
|
| _, _, `Fixed len when not (Int64.equal len 0L) ->
|
|
(* From RFC7540§8.2:
|
|
* Clients that receive a promised request that is not cacheable,
|
|
* that is not known to be safe or that indicates the presence of a
|
|
* request body MUST reset the promised stream with a stream error
|
|
* (Section 5.4.2) of type PROTOCOL_ERROR.
|
|
*
|
|
* From RFC7231§4.2.3 (Cacheable Methods):
|
|
* [...] this specification defines GET, HEAD, and POST as cacheable
|
|
* [...].
|
|
*
|
|
* From RFC7231§4.2.1 (Safe Methods):
|
|
* Of the request methods defined by this specification, the GET, HEAD,
|
|
* OPTIONS, and TRACE methods are defined to be safe.
|
|
*
|
|
* Note: the intersection of safe and cacheable are the GET and HEAD
|
|
* methods. *)
|
|
report_stream_error t respd.id Error_code.ProtocolError
|
|
(* From RFC7540§8.2:
|
|
* The server MUST include a value in the :authority pseudo-header field
|
|
* for which the server is authoritative (see Section 10.1). A client
|
|
* MUST treat a PUSH_PROMISE for which the server is not authoritative as
|
|
* a stream error (Section 5.4.2) of type PROTOCOL_ERROR. *)
|
|
| _, None, _ | _, _, `Error _ ->
|
|
report_stream_error t respd.id Error_code.ProtocolError
|
|
| _ ->
|
|
let request = Request.create ~scheme ~headers meth path in
|
|
(match t.push_handler request with
|
|
| Ok response_handler ->
|
|
(* From RFC7540§8.2:
|
|
* Promised requests [...] MUST NOT include a request body. *)
|
|
let request_body = Body.Writer.create_empty ~writer:t.writer in
|
|
(* From RFC7540§5.1:
|
|
* reserved (remote): [...] Receiving a HEADERS frame causes the
|
|
* stream to transition to "half-closed (local)". *)
|
|
respd.state <-
|
|
Active
|
|
( HalfClosed Stream.WaitingForPeer
|
|
, { Respd.request
|
|
; request_body
|
|
; response_handler
|
|
; trailers_handler = ignore
|
|
} )
|
|
| Error _ ->
|
|
(* From RFC7540§6.6:
|
|
* Recipients of PUSH_PROMISE frames can choose to reject promised
|
|
* streams by returning a RST_STREAM referencing the promised stream
|
|
* identifier back to the sender of the PUSH_PROMISE. *)
|
|
Stream.reset_stream respd Error_code.Cancel))
|
|
|
|
let handle_response_headers t stream ~end_stream active_request headers =
|
|
let (Scheduler.Stream { descriptor = respd; _ }) = stream in
|
|
(* From RFC7540§8.1.2.6:
|
|
* Clients MUST NOT accept a malformed response.
|
|
*
|
|
* Note: in the branches where a malformed response is detected, the response
|
|
* handler is not called. *)
|
|
match Headers.get_multi_pseudo headers "status" with
|
|
| [ status ] ->
|
|
let response = Response.create ~headers (Status.of_string status) in
|
|
(* Note: we don't need to check for `end_stream` flag + a non-zero body
|
|
* length, as the spec allows for non-zero content-length headers and no
|
|
* DATA frames.
|
|
*
|
|
* From RFC7540§8.1.2.6:
|
|
* A response that is defined to have no payload, as described in
|
|
* [RFC7230], Section 3.3.2, can have a non-zero content-length header
|
|
* field, even though no content is included in DATA frames. *)
|
|
(match Message.body_length headers with
|
|
| `Error _ ->
|
|
set_error_and_handle
|
|
t
|
|
respd
|
|
(`Invalid_response_body_length response)
|
|
ProtocolError
|
|
| `Fixed _ | `Unknown ->
|
|
let response_body =
|
|
if end_stream
|
|
then Body.Reader.empty
|
|
else
|
|
Body.Reader.create
|
|
(Bigstringaf.create t.config.response_body_buffer_size)
|
|
~done_reading:(fun len ->
|
|
let len = Int32.of_int len in
|
|
send_window_update t t.streams len;
|
|
send_window_update t stream len)
|
|
in
|
|
let new_response_state =
|
|
Respd.create_active_response response response_body
|
|
in
|
|
respd.state <-
|
|
Active
|
|
( (if Stream.is_open respd
|
|
then Open new_response_state
|
|
else HalfClosed new_response_state)
|
|
, active_request );
|
|
active_request.response_handler response response_body;
|
|
if end_stream
|
|
then (
|
|
(* Deliver EOF to the response body, as the handler might be waiting
|
|
* on it to act. *)
|
|
Body.Reader.close response_body;
|
|
(* From RFC7540§5.1:
|
|
* [...] an endpoint receiving an END_STREAM flag causes the stream
|
|
* state to become "half-closed (remote)". *)
|
|
Respd.close_stream respd))
|
|
| _ ->
|
|
(* 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). *)
|
|
let message =
|
|
"HTTP/2 responses must include a single `:status` pseudo-header"
|
|
in
|
|
set_error_and_handle t respd (`Malformed_response message) ProtocolError
|
|
|
|
let handle_headers t ~end_stream stream headers =
|
|
let (Scheduler.Stream { descriptor = respd; _ }) = stream in
|
|
(* From RFC7540§5.1.2:
|
|
* Endpoints MUST NOT exceed the limit set by their peer. An endpoint that
|
|
* receives a HEADERS frame that causes its advertised concurrent stream
|
|
* limit to be exceeded MUST treat this as a stream error (Section 5.4.2)
|
|
* of type PROTOCOL_ERROR or REFUSED_STREAM. *)
|
|
if Int32.(
|
|
compare
|
|
(of_int (t.current_server_streams + 1))
|
|
t.config.max_concurrent_streams)
|
|
> 0
|
|
then
|
|
if t.unacked_settings > 0
|
|
then
|
|
(* From RFC7540§8.1.4:
|
|
* The REFUSED_STREAM error code can be included in a RST_STREAM frame
|
|
* to indicate that the stream is being closed prior to any processing
|
|
* having occurred. Any request that was sent on the reset stream can
|
|
* be safely retried.
|
|
*
|
|
* Note: if there are pending SETTINGS to acknowledge, assume there was a
|
|
* race condition and let the client retry. *)
|
|
report_stream_error t respd.Stream.id Error_code.RefusedStream
|
|
else report_stream_error t respd.Stream.id Error_code.ProtocolError
|
|
else (
|
|
(* From RFC7540§5.1.2:
|
|
* Streams that are in the "open" state or in either of the "half-closed"
|
|
* states count toward the maximum number of streams that an endpoint is
|
|
* permitted to open. *)
|
|
t.current_server_streams <- t.current_server_streams + 1;
|
|
match respd.state with
|
|
| Reserved _ ->
|
|
respd.state <- Reserved Stream.FullHeaders;
|
|
handle_push_promise_headers t respd headers
|
|
| Active (active_state, active_request) ->
|
|
(match active_state with
|
|
| Open _ -> respd.state <- Active (Open FullHeaders, active_request)
|
|
| HalfClosed _ ->
|
|
respd.state <- Active (HalfClosed FullHeaders, active_request));
|
|
handle_response_headers t stream ~end_stream active_request headers
|
|
| _ ->
|
|
(* Unreachable. This function is only invoked if the stream is active. *)
|
|
assert false)
|
|
|
|
let handle_headers_block
|
|
t
|
|
?(is_trailers = false)
|
|
stream
|
|
partial_headers
|
|
flags
|
|
headers_block
|
|
=
|
|
let open AB in
|
|
let (Scheduler.Stream { descriptor = respd; _ }) = stream in
|
|
let end_headers = Flags.test_end_header flags in
|
|
(* From RFC7540§6.10:
|
|
* An endpoint receiving HEADERS, PUSH_PROMISE, or CONTINUATION
|
|
* frames needs to reassemble header blocks and perform decompression
|
|
* even if the frames are to be discarded *)
|
|
let parse_state' =
|
|
AB.feed partial_headers.Stream.parse_state (`Bigstring headers_block)
|
|
in
|
|
if end_headers
|
|
then (
|
|
t.receiving_headers_for_stream <- None;
|
|
let parse_state' = AB.feed parse_state' `Eof in
|
|
match parse_state' with
|
|
| Done (_, Ok headers) ->
|
|
if not is_trailers
|
|
then
|
|
(* `handle_headers` will take care of transitioning the stream state *)
|
|
let end_stream = partial_headers.end_stream in
|
|
handle_headers t ~end_stream stream headers
|
|
else if Headers.trailers_valid headers
|
|
then (
|
|
Respd.deliver_trailer_headers respd headers;
|
|
let response_body = Respd.response_body_exn respd in
|
|
Body.Reader.close response_body)
|
|
else
|
|
(* From RFC7540§8.1.2.1:
|
|
* Pseudo-header fields MUST NOT appear in trailers. Endpoints MUST
|
|
* treat a request or response that contains undefined or invalid
|
|
* pseudo-header fields as malformed (Section 8.1.2.6). *)
|
|
let message = "Pseudo-header fields must not appear in trailers" in
|
|
set_error_and_handle t respd (`Malformed_response message) ProtocolError
|
|
(* From RFC7540§4.3:
|
|
* A decoding error in a header block MUST be treated as a connection
|
|
* error (Section 5.4.1) of type COMPRESSION_ERROR. *)
|
|
| Done (_, Error _) | Partial _ ->
|
|
report_connection_error t Error_code.CompressionError
|
|
| Fail (_, _, message) ->
|
|
report_connection_error t ~reason:message Error_code.CompressionError)
|
|
else partial_headers.parse_state <- parse_state'
|
|
|
|
let handle_trailer_headers = handle_headers_block ~is_trailers:true
|
|
|
|
let create_partial_headers t flags headers_block =
|
|
let end_headers = Flags.test_end_header flags in
|
|
let headers_block_length = Bigstringaf.length headers_block in
|
|
let initial_buffer_size =
|
|
if end_headers
|
|
then headers_block_length
|
|
else
|
|
(* Conservative estimate that there's only going to be one CONTINUATION
|
|
* frame. *)
|
|
2 * headers_block_length
|
|
in
|
|
{ Stream.parse_state =
|
|
AB.parse
|
|
~initial_buffer_size
|
|
(Hpack.Decoder.decode_headers t.hpack_decoder)
|
|
; end_stream = Flags.test_end_stream flags
|
|
}
|
|
|
|
let handle_first_response_bytes
|
|
t
|
|
stream
|
|
active_request
|
|
frame_header
|
|
headers_block
|
|
=
|
|
let (Scheduler.Stream { descriptor; _ }) = stream in
|
|
let { Frame.flags; stream_id; _ } = frame_header in
|
|
let partial_headers = create_partial_headers t flags headers_block in
|
|
let remote_state = Stream.PartialHeaders partial_headers in
|
|
descriptor.Stream.state <-
|
|
(if Stream.is_open descriptor
|
|
then Active (Open remote_state, active_request)
|
|
else Active (HalfClosed remote_state, active_request));
|
|
if not (Flags.test_end_header flags)
|
|
then t.receiving_headers_for_stream <- Some stream_id;
|
|
handle_headers_block t stream partial_headers flags headers_block
|
|
|
|
let process_trailer_headers t stream active_response frame_header headers_block =
|
|
let (Scheduler.Stream { descriptor = respd; _ }) = stream in
|
|
let { Frame.stream_id; flags; _ } = frame_header in
|
|
let end_stream = Flags.test_end_stream flags in
|
|
if not end_stream
|
|
then
|
|
(* From RFC7540§8.1:
|
|
* A HEADERS frame (and associated CONTINUATION frames) can only appear
|
|
* at the start or end of a stream. An endpoint that receives a HEADERS
|
|
* frame without the END_STREAM flag set after receiving a final
|
|
* (non-informational) status code MUST treat the corresponding request
|
|
* or response as malformed (Section 8.1.2.6). *)
|
|
let message =
|
|
"HEADERS frames containing trailers must set the END_STREAM flag"
|
|
in
|
|
set_error_and_handle t respd (`Malformed_response message) ProtocolError
|
|
else
|
|
let partial_headers =
|
|
{ Stream.parse_state =
|
|
AB.parse (Hpack.Decoder.decode_headers t.hpack_decoder)
|
|
(* obviously true at this point. *)
|
|
; end_stream
|
|
}
|
|
in
|
|
active_response.Respd.trailers_parser <- Some partial_headers;
|
|
if not Flags.(test_end_header flags)
|
|
then t.receiving_headers_for_stream <- Some stream_id;
|
|
(* trailer headers: RFC7230§4.4 *)
|
|
handle_trailer_headers t stream partial_headers flags headers_block
|
|
|
|
let process_headers_frame t { Frame.frame_header; _ } headers_block =
|
|
let { Frame.stream_id; _ } = frame_header in
|
|
match Scheduler.get_node t.streams stream_id with
|
|
| None ->
|
|
(* If we're receiving a response for a stream that's no longer in the
|
|
* priority tree, assume this is a network race - we canceled a request
|
|
* but a responnse was already in flight.
|
|
*
|
|
* However, if the stream identifer is greater than the largest stream
|
|
* identifier we have produced, they should know better. In this case,
|
|
* send an RST_STREAM. *)
|
|
if Stream_identifier.(
|
|
stream_id >= t.current_stream_id && is_request stream_id)
|
|
then report_stream_error t stream_id Error_code.StreamClosed
|
|
| Some (Scheduler.Stream { descriptor; _ } as stream) ->
|
|
(match descriptor.state with
|
|
| Idle ->
|
|
(* From RFC7540§6.2:
|
|
* HEADERS frames can be sent on a stream in the "idle", "reserved
|
|
* (local)", "open", or "half-closed (remote)" state. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
| Active ((Open WaitingForPeer | HalfClosed WaitingForPeer), active_request)
|
|
->
|
|
handle_first_response_bytes
|
|
t
|
|
stream
|
|
active_request
|
|
frame_header
|
|
headers_block
|
|
| Active
|
|
( ( Open (FullHeaders | PartialHeaders _)
|
|
| HalfClosed (FullHeaders | PartialHeaders _) )
|
|
, _ ) ->
|
|
assert false
|
|
(* if we're getting a HEADERS frame at this point, they must be
|
|
* trailers, and the END_STREAM flag needs to be set. *)
|
|
| Active
|
|
( ( Open (ActiveMessage active_response)
|
|
| HalfClosed (ActiveMessage active_response) )
|
|
, _ ) ->
|
|
process_trailer_headers
|
|
t
|
|
stream
|
|
active_response
|
|
frame_header
|
|
headers_block;
|
|
Stream.finish_stream descriptor Finished
|
|
| Closed { reason = ResetByThem _; _ } ->
|
|
(* From RFC7540§5.1:
|
|
* closed: [...] An endpoint that receives any frame other than
|
|
* PRIORITY after receiving a RST_STREAM MUST treat that as a
|
|
* stream error (Section 5.4.2) of type STREAM_CLOSED. *)
|
|
report_stream_error t stream_id Error_code.StreamClosed
|
|
(* From RFC7540§5.1:
|
|
* reserved (local): [...] Receiving any type of frame other than
|
|
* RST_STREAM, PRIORITY, or WINDOW_UPDATE on a stream in this state
|
|
* MUST be treated as a connection error (Section 5.4.1) of type
|
|
* PROTOCOL_ERROR. *)
|
|
| Reserved _ | Closed _ ->
|
|
(* From RFC7540§5.1:
|
|
* Similarly, an endpoint that receives any frames after receiving
|
|
* a frame with the END_STREAM flag set MUST treat that as a
|
|
* connection error (Section 5.4.1) of type STREAM_CLOSED [...]. *)
|
|
report_connection_error t Error_code.StreamClosed)
|
|
|
|
let process_data_frame t { Frame.frame_header; _ } bstr =
|
|
let open Scheduler in
|
|
let { Frame.flags; stream_id; payload_length; _ } = frame_header in
|
|
let payload_len32 = Int32.of_int payload_length in
|
|
match Scheduler.get_node t.streams stream_id with
|
|
| Some (Stream { descriptor; _ } as stream) ->
|
|
(match descriptor.state with
|
|
| Active
|
|
( ( Open (ActiveMessage response_info)
|
|
| HalfClosed (ActiveMessage response_info) )
|
|
, _ ) ->
|
|
let { Respd.response; response_body; response_body_bytes; _ } =
|
|
response_info
|
|
in
|
|
response_info.response_body_bytes <-
|
|
Int64.(add response_body_bytes (of_int (Bigstringaf.length bstr)));
|
|
(* First, calculate whether we're allowed to receive this frame based
|
|
* on the _current_ inflow. *)
|
|
let allowed_to_receive =
|
|
Scheduler.(allowed_to_receive t.streams stream payload_len32)
|
|
in
|
|
(* Then, deduct inflow from the connection flow-control window, as
|
|
* mandated by the protocol.
|
|
*
|
|
* From RFC7540§6.9:
|
|
* A receiver that receives a flow-controlled frame MUST always account
|
|
* for its contribution against the connection flow-control window,
|
|
* unless the receiver treats this as a connection error (Section 5.4.1).
|
|
* This is necessary even if the frame is in error. *)
|
|
Scheduler.deduct_inflow t.streams payload_len32;
|
|
if not allowed_to_receive
|
|
then (
|
|
(* From RFC7540§6.9:
|
|
* A receiver MAY respond with a stream error (Section 5.4.2) or
|
|
* connection error (Section 5.4.1) of type FLOW_CONTROL_ERROR if it
|
|
* is unable to accept a frame. *)
|
|
send_window_update t t.streams payload_len32;
|
|
report_stream_error t stream_id Error_code.FlowControlError)
|
|
else (
|
|
Scheduler.deduct_inflow stream payload_len32;
|
|
match Message.body_length response.headers with
|
|
| `Fixed len
|
|
(* Getting more than the server declared *)
|
|
when Int64.compare response_info.response_body_bytes len > 0 ->
|
|
(* Give back connection-level flow-controlled bytes (we use payload
|
|
* length to include any padding bytes that the frame might have
|
|
* included - which were ignored at parse time). *)
|
|
send_window_update t t.streams payload_len32;
|
|
(* From RFC7540§8.1.2.6:
|
|
* A request or response is also malformed if the value of a
|
|
* content-length header field does not equal the sum of the
|
|
* DATA frame payload lengths that form the body. *)
|
|
set_error_and_handle
|
|
t
|
|
descriptor
|
|
(`Invalid_response_body_length response)
|
|
ProtocolError
|
|
| _ ->
|
|
let end_stream = Flags.test_end_stream flags in
|
|
(* From RFC7540§6.9.1:
|
|
* The receiver of a frame sends a WINDOW_UPDATE frame as it
|
|
* consumes data and frees up space in flow-control windows.
|
|
* Separate WINDOW_UPDATE frames are sent for the stream- and
|
|
* connection-level flow-control windows.
|
|
*
|
|
* Note: we send these WINDOW_UPDATE frames once the body bytes
|
|
* have been surfaced to the application. This is done in the
|
|
* record field `done_reading` of `Body.t`. *)
|
|
let faraday = Body.Reader.unsafe_faraday response_body in
|
|
if not (Faraday.is_closed faraday)
|
|
then (
|
|
Faraday.schedule_bigstring faraday bstr;
|
|
if end_stream then Body.Reader.close response_body);
|
|
Respd.flush_response_body descriptor;
|
|
if end_stream && not (Respd.requires_output descriptor)
|
|
then
|
|
(* From RFC7540§6.1:
|
|
* When set, bit 0 indicates that this frame is the last that
|
|
* the endpoint will send for the identified stream. Setting
|
|
* this flag causes the stream to enter one of the
|
|
* "half-closed" states or the "closed" state (Section 5.1).
|
|
*
|
|
* Transition to the "closed" state if this is the last DATA frame
|
|
* that the server will send and we're done sending. *)
|
|
Stream.finish_stream descriptor Finished)
|
|
| Idle ->
|
|
(* From RFC7540§5.1:
|
|
* idle: [...] Receiving any frame other than HEADERS or PRIORITY on
|
|
* a stream in this state MUST be treated as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
(* This is technically in the half-closed (local) state *)
|
|
| Closed { reason = ResetByUs NoError; _ } ->
|
|
(* From RFC7540§6.9:
|
|
* A receiver that receives a flow-controlled frame MUST always
|
|
* account for its contribution against the connection flow-control
|
|
* window, unless the receiver treats this as a connection error
|
|
* (Section 5.4.1). This is necessary even if the frame is in
|
|
* error. *)
|
|
send_window_update t t.streams payload_len32
|
|
(* From RFC7540§6.4:
|
|
* [...] after sending the RST_STREAM, the sending endpoint MUST be
|
|
* prepared to receive and process additional frames sent on the
|
|
* stream that might have been sent by the peer prior to the arrival
|
|
* of the RST_STREAM.
|
|
*
|
|
* Note: after some writer yields / wake ups, we will have stopped
|
|
* keeping state information for the stream. This functions effectively
|
|
* as a way of only accepting frames after an RST_STREAM from us up to
|
|
* a time limit. *)
|
|
| _ ->
|
|
send_window_update t t.streams payload_len32;
|
|
(* From RFC7540§6.1:
|
|
* If a DATA frame is received whose stream is not in "open" or
|
|
* "half-closed (local)" state, the recipient MUST respond with a
|
|
* stream error (Section 5.4.2) of type STREAM_CLOSED. *)
|
|
report_stream_error t stream_id Error_code.StreamClosed)
|
|
| None ->
|
|
if not (was_closed_or_implicitly_closed t stream_id)
|
|
then
|
|
(* From RFC7540§5.1:
|
|
* idle: [...] Receiving any frame other than HEADERS or PRIORITY on
|
|
* a stream in this state MUST be treated as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
|
|
let on_close_stream t id ~active closed =
|
|
if active
|
|
then
|
|
(* From RFC7540§5.1.2:
|
|
* Streams that are in the "open" state or in either of the "half-closed"
|
|
* states count toward the maximum number of streams that an endpoint is
|
|
* permitted to open. *)
|
|
t.current_server_streams <- t.current_server_streams - 1;
|
|
Scheduler.mark_for_removal t.streams id closed
|
|
|
|
let process_priority_frame t { Frame.frame_header; _ } priority =
|
|
let { Frame.stream_id; _ } = frame_header in
|
|
let { Priority.stream_dependency; _ } = priority in
|
|
if Stream_identifier.(stream_id === stream_dependency)
|
|
then
|
|
(* From RFC7540§5.3.1:
|
|
* A stream cannot depend on itself. An endpoint MUST treat this as a
|
|
* stream error (Section 5.4.2) of type PROTOCOL_ERROR. *)
|
|
report_stream_error t stream_id Error_code.ProtocolError
|
|
else
|
|
match Scheduler.get_node t.streams stream_id with
|
|
| Some stream -> Scheduler.reprioritize_stream t.streams ~priority stream
|
|
| None ->
|
|
(* From RFC7540§5.3:
|
|
* A client can assign a priority for a new stream by including
|
|
* prioritization information in the HEADERS frame (Section 6.2) that
|
|
* opens the stream. At any other time, the PRIORITY frame (Section
|
|
* 6.3) can be used to change the priority of a stream.
|
|
*
|
|
* Note: The spec mostly only mentions that clients are the endpoints
|
|
* that make use of PRIORITY frames. As such, we don't make too
|
|
* much of an effort to process PRIORITY frames coming from a
|
|
* server. If we know about a stream, we reprioritize it (meaning
|
|
* prioritization is an input to the process of allocating
|
|
* resources when flushing request bodies). Otherwise, we ignore
|
|
* it. We don't, however, report any errors if the frame is
|
|
* well-formed, as section 5. clearly mentions that PRIORITY frames
|
|
* must be accepted in all stream states.
|
|
*
|
|
* From RFC7540§5.1:
|
|
* Note that PRIORITY can be sent and received in any stream state. *)
|
|
()
|
|
|
|
let process_rst_stream_frame t { Frame.frame_header; _ } error_code =
|
|
let { Frame.stream_id; _ } = frame_header in
|
|
match Scheduler.find t.streams stream_id with
|
|
| Some respd ->
|
|
(match respd.state, error_code with
|
|
| Idle, _ ->
|
|
(* From RFC7540§6.4:
|
|
* RST_STREAM frames MUST NOT be sent for a stream in the "idle"
|
|
* state. If a RST_STREAM frame identifying an idle stream is
|
|
* received, the recipient MUST treat this as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
| Active _, Error_code.NoError ->
|
|
(* If we're active (i.e. not done sending the request body), finish the
|
|
* stream, in order to mark it for cleanup.
|
|
*
|
|
* Note: we don't close the request body here because the client may be
|
|
* in the process of writing to it, and while we're not going to send
|
|
* those bytes to the output channel, we don't want to fail when writing
|
|
* either. *)
|
|
Stream.finish_stream respd (ResetByThem error_code)
|
|
| Closed _, _ ->
|
|
(* From RFC7540§8.1:
|
|
* A server can send a complete response prior to the client sending an
|
|
* entire request if the response does not depend on any portion of the
|
|
* request that has not been sent and received. When this is true, a
|
|
* server MAY request that the client abort transmission of a request
|
|
* without error by sending a RST_STREAM with an error code of NO_ERROR
|
|
* after sending a complete response (i.e., a frame with the END_STREAM
|
|
* flag).
|
|
*
|
|
* If we're done sending the request there's nothing to do here, allow
|
|
* the stream to finish successfully.
|
|
*
|
|
* From RFC7540§5.1:
|
|
* Endpoints MUST ignore WINDOW_UPDATE or RST_STREAM frames received
|
|
* in this state, though endpoints MAY choose to treat frames that
|
|
* arrive a significant time after sending END_STREAM as a connection
|
|
* error (Section 5.4.1) of type PROTOCOL_ERROR.
|
|
*
|
|
* We ignore further RST_STREAM frames.
|
|
*)
|
|
(* XXX(anmonteiro): When we add logging support, add something here. *)
|
|
()
|
|
| _ ->
|
|
(* From RFC7540§6.4:
|
|
* The RST_STREAM frame fully terminates the referenced stream and
|
|
* causes it to enter the "closed" state. After receiving a
|
|
* RST_STREAM on a stream, the receiver MUST NOT send additional
|
|
* frames for that stream, with the exception of PRIORITY.
|
|
*
|
|
* Note:
|
|
* This match branch also accepts streams in the `Closed` state. We
|
|
* do that to comply with the following:
|
|
*
|
|
* From RFC7540§6.4:
|
|
* [...] after sending the RST_STREAM, the sending endpoint MUST be
|
|
* prepared to receive and process additional frames sent on the
|
|
* stream that might have been sent by the peer prior to the arrival
|
|
* of the RST_STREAM. *)
|
|
Stream.finish_stream respd (ResetByThem error_code);
|
|
(* From RFC7540§5.4.2:
|
|
* To avoid looping, an endpoint MUST NOT send a RST_STREAM in response
|
|
* to a RST_STREAM frame.
|
|
*
|
|
* Note: the {!Respd.report_error} function does not send an RST_STREAM
|
|
* frame for streams in the closed state. So we close the stream before
|
|
* reporting the error. *)
|
|
set_error_and_handle t respd (`Protocol_error (error_code, "")) error_code)
|
|
| None ->
|
|
(* We might have removed the stream from the hash table. If its stream
|
|
* id is smaller than or equal to the max client stream id we've generated,
|
|
* then it must have been closed. *)
|
|
if not (was_closed_or_implicitly_closed t stream_id)
|
|
then
|
|
(* From RFC7540§6.4:
|
|
* RST_STREAM frames MUST NOT be sent for a stream in the "idle"
|
|
* state. If a RST_STREAM frame identifying an idle stream is
|
|
* received, the recipient MUST treat this as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR.
|
|
*
|
|
* Note:
|
|
* If we didn't find the stream in the hash table it must be "idle". *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
|
|
let process_settings_frame t { Frame.frame_header; _ } settings =
|
|
let open Scheduler in
|
|
let { Frame.flags; _ } = frame_header in
|
|
(* We already checked that an acked SETTINGS is empty. Don't need to do
|
|
* anything else in that case *)
|
|
if Flags.(test_ack flags)
|
|
then (
|
|
t.unacked_settings <- t.unacked_settings - 1;
|
|
if t.unacked_settings < 0
|
|
then
|
|
(* The server is ACKing a SETTINGS frame that we didn't send *)
|
|
report_connection_error
|
|
t
|
|
~reason:"Received unexpected SETTINGS frame with acknowledgement"
|
|
Error_code.ProtocolError)
|
|
else
|
|
match Settings.check_settings_list ~is_client:true settings with
|
|
| Ok () ->
|
|
(* From RFC7540§6.5:
|
|
* Each parameter in a SETTINGS frame replaces any existing value for
|
|
* that parameter. Parameters are processed in the order in which they
|
|
* appear, and a receiver of a SETTINGS frame does not need to maintain
|
|
* any state other than the current value of its parameters. *)
|
|
let new_settings =
|
|
List.fold_left
|
|
(fun (acc : Settings.t) item ->
|
|
match item with
|
|
| Settings.HeaderTableSize x ->
|
|
(* From RFC7540§6.5.2:
|
|
* Allows the sender to inform the remote endpoint of the maximum
|
|
* size of the header compression table used to decode header
|
|
* blocks, in octets. *)
|
|
Hpack.Encoder.set_capacity t.hpack_encoder x;
|
|
{ acc with header_table_size = x }
|
|
| EnablePush x ->
|
|
(* We've already verified that this setting is either 0 or 1 in the
|
|
* call to `Settings.check_settings_list` above. *)
|
|
{ acc with enable_push = x = 1 }
|
|
| MaxConcurrentStreams x -> { acc with max_concurrent_streams = x }
|
|
| InitialWindowSize new_val ->
|
|
(* From RFC7540§6.9.2:
|
|
* In addition to changing the flow-control window for streams
|
|
* that are not yet active, a SETTINGS frame can alter the
|
|
* initial flow-control window size for streams with active
|
|
* flow-control windows (that is, streams in the "open" or
|
|
* "half-closed (remote)" state). When the value of
|
|
* SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust
|
|
* the size of all stream flow-control windows that it
|
|
* maintains by the difference between the new value and the
|
|
* old value.
|
|
*
|
|
* [...] A SETTINGS frame cannot alter the connection
|
|
* flow-control window. *)
|
|
let old_val = t.settings.initial_window_size in
|
|
let growth = Int32.sub new_val old_val in
|
|
let exception Local in
|
|
(match
|
|
Scheduler.iter
|
|
~f:(fun stream ->
|
|
(* From RFC7540§6.9.2:
|
|
* An endpoint MUST treat a change to
|
|
* SETTINGS_INITIAL_WINDOW_SIZE that causes any
|
|
* flow-control window to exceed the maximum size as a
|
|
* connection error (Section 5.4.1) of type
|
|
* FLOW_CONTROL_ERROR. *)
|
|
if not (Scheduler.add_flow stream growth) then raise Local)
|
|
t.streams
|
|
with
|
|
| () -> ()
|
|
| exception Local ->
|
|
report_connection_error
|
|
t
|
|
~reason:
|
|
(Format.sprintf
|
|
"Window size for stream would exceed %ld"
|
|
Settings.WindowSize.max_window_size)
|
|
Error_code.FlowControlError);
|
|
{ acc with initial_window_size = new_val }
|
|
| MaxFrameSize x ->
|
|
Scheduler.iter
|
|
~f:(fun (Stream { descriptor; _ }) ->
|
|
if Respd.requires_output descriptor
|
|
then descriptor.max_frame_size <- x)
|
|
t.streams;
|
|
{ acc with max_frame_size = x }
|
|
| MaxHeaderListSize x -> { acc with max_header_list_size = Some x })
|
|
t.settings
|
|
settings
|
|
in
|
|
t.settings <- new_settings;
|
|
let frame_info =
|
|
Writer.make_frame_info
|
|
~flags:Flags.(set_ack default_flags)
|
|
Stream_identifier.connection
|
|
in
|
|
(* From RFC7540§6.5:
|
|
* ACK (0x1): [...] When this bit is set, the payload of the SETTINGS
|
|
* frame MUST be empty. *)
|
|
Writer.write_settings t.writer frame_info [];
|
|
t.unacked_settings <- t.unacked_settings + 1;
|
|
Writer.wakeup t.writer
|
|
| Error error -> report_error t error
|
|
|
|
let reserve_stream t { Frame.frame_header; _ } promised_stream_id headers_block =
|
|
let { Frame.flags; stream_id; _ } = frame_header in
|
|
(* From RFC7540§6.6:
|
|
* The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in
|
|
* advance of streams the sender intends to initiate. *)
|
|
let respd =
|
|
Stream.create
|
|
promised_stream_id
|
|
~max_frame_size:t.settings.max_frame_size
|
|
t.writer
|
|
t.error_handler
|
|
(on_close_stream t promised_stream_id)
|
|
in
|
|
(* From RFC7540§5.3.5:
|
|
* All streams are initially assigned a non-exclusive dependency on stream
|
|
* 0x0. Pushed streams (Section 8.2) initially depend on their associated
|
|
* stream. In both cases, streams are assigned a default weight of 16. *)
|
|
let stream : Scheduler.nonroot Scheduler.node =
|
|
Scheduler.add
|
|
t.streams
|
|
~priority:{ Priority.default_priority with stream_dependency = stream_id }
|
|
~initial_send_window_size:t.settings.initial_window_size
|
|
~initial_recv_window_size:t.config.initial_window_size
|
|
respd
|
|
in
|
|
let partial_headers = create_partial_headers t flags headers_block in
|
|
respd.state <- Reserved (PartialHeaders partial_headers);
|
|
if not (Flags.test_end_header flags)
|
|
then t.receiving_headers_for_stream <- Some promised_stream_id;
|
|
handle_headers_block t stream partial_headers flags headers_block
|
|
|
|
let process_push_promise_frame
|
|
t
|
|
({ Frame.frame_header; _ } as frame)
|
|
promised_stream_id
|
|
headers_block
|
|
=
|
|
let { Frame.stream_id; _ } = frame_header in
|
|
(* At this point, `promised_stream_id` has already been validated by the
|
|
* parser *)
|
|
if not t.settings.enable_push
|
|
then
|
|
(* From RFC7540§6.6:
|
|
* PUSH_PROMISE MUST NOT be sent if the SETTINGS_ENABLE_PUSH setting of
|
|
* the peer endpoint is set to 0. An endpoint that has set this setting
|
|
* and has received acknowledgement MUST treat the receipt of a
|
|
* PUSH_PROMISE frame as a connection error (Section 5.4.1) of type
|
|
* PROTOCOL_ERROR. *)
|
|
report_connection_error
|
|
t
|
|
~reason:"Push is disabled for this connection"
|
|
Error_code.ProtocolError
|
|
else if not Stream_identifier.(promised_stream_id > t.max_pushed_stream_id)
|
|
then
|
|
(* From RFC7540§6.6:
|
|
* A receiver MUST treat the receipt of a PUSH_PROMISE that promises an
|
|
* illegal stream identifier (Section 5.1.1) as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. Note that an illegal stream
|
|
* identifier is an identifier for a stream that is not currently in the
|
|
* "idle" state. *)
|
|
report_connection_error
|
|
t
|
|
~reason:"Illegal stream identifier promised by PUSH_PROMISE"
|
|
Error_code.ProtocolError
|
|
else
|
|
let send_connection_error () =
|
|
report_connection_error
|
|
t
|
|
~reason:
|
|
"Received PUSH_PROMISE on a stream that is neither open nor \
|
|
half-closed (local)"
|
|
Error_code.ProtocolError
|
|
in
|
|
t.max_pushed_stream_id <- promised_stream_id;
|
|
match Scheduler.find t.streams stream_id with
|
|
| None ->
|
|
(* From RFC7540§6.6:
|
|
* A receiver MUST treat the receipt of a PUSH_PROMISE on a stream that
|
|
* is neither "open" nor "half-closed (local)" as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
send_connection_error ()
|
|
| Some respd ->
|
|
(match respd.state with
|
|
| Active ((Open _ | HalfClosed _), _) ->
|
|
reserve_stream t frame promised_stream_id headers_block
|
|
| _ -> send_connection_error ())
|
|
|
|
let ok = Ok ()
|
|
|
|
let process_ping_frame t { Frame.frame_header; _ } payload =
|
|
let { Frame.flags; _ } = frame_header in
|
|
(* From RFC7540§6.7:
|
|
* ACK (0x1): When set, bit 0 indicates that this PING frame is a PING
|
|
* response. [...] An endpoint MUST NOT respond to PING frames containing
|
|
* this flag. *)
|
|
if Flags.test_ack flags
|
|
then
|
|
match Queue.take_opt t.pending_pings with
|
|
| Some callback -> callback ok
|
|
| None ->
|
|
(* server is ACKing a PING that we didn't send? *)
|
|
report_connection_error
|
|
t
|
|
~reason:"Unexpected PING acknowledgement"
|
|
Error_code.ProtocolError
|
|
else
|
|
(* From RFC7540§6.7:
|
|
* Receivers of a PING frame that does not include an ACK flag MUST send
|
|
* a PING frame with the ACK flag set in response, with an identical
|
|
* payload. PING responses SHOULD be given higher priority than any other
|
|
* frame. *)
|
|
let frame_info =
|
|
Writer.make_frame_info
|
|
(* From RFC7540§6.7:
|
|
* ACK (0x1): When set, bit 0 indicates that this PING frame is a
|
|
* PING response. An endpoint MUST set this flag in PING
|
|
* responses. *)
|
|
~flags:Flags.(set_ack default_flags)
|
|
Stream_identifier.connection
|
|
in
|
|
(* From RFC7540§6.7:
|
|
* Receivers of a PING frame that does not include an ACK flag MUST send
|
|
* a PING frame with the ACK flag set in response, with an identical
|
|
* payload. *)
|
|
Writer.write_ping t.writer frame_info payload;
|
|
Writer.wakeup t.writer
|
|
|
|
let process_goaway_frame t _frame ~last_stream_id:_ ~data error =
|
|
let reason =
|
|
match Bigstringaf.length data with
|
|
| 0 -> None
|
|
| len -> Some (Bigstringaf.substring data ~off:0 ~len)
|
|
in
|
|
(* TODO(anmonteiro): I think we need to allow lower numbered streams to
|
|
* complete. *)
|
|
report_connection_error t ?reason error
|
|
|
|
let add_window_increment :
|
|
type a. t -> a Scheduler.PriorityTreeNode.node -> int32 -> unit
|
|
=
|
|
fun t stream increment ->
|
|
let open Scheduler in
|
|
let did_add = Scheduler.add_flow stream increment in
|
|
let stream_id = Scheduler.stream_id stream in
|
|
let new_flow =
|
|
match stream with
|
|
| Connection { flow; _ } -> flow
|
|
| Stream { flow; _ } -> flow
|
|
in
|
|
if did_add
|
|
then (
|
|
if Int32.compare new_flow 0l > 0
|
|
then
|
|
(* Don't bother waking up the writer if the new flow doesn't allow
|
|
* the stream to write. *)
|
|
Writer.wakeup t.writer)
|
|
else if Stream_identifier.is_connection stream_id
|
|
then
|
|
report_connection_error
|
|
t
|
|
~reason:
|
|
(Printf.sprintf
|
|
"Window size for stream would exceed %ld"
|
|
Settings.WindowSize.max_window_size)
|
|
Error_code.FlowControlError
|
|
else report_stream_error t stream_id Error_code.FlowControlError
|
|
|
|
let process_window_update_frame t { Frame.frame_header; _ } window_increment =
|
|
let open Scheduler in
|
|
let { Frame.stream_id; _ } = frame_header in
|
|
(* From RFC7540§6.9:
|
|
* The WINDOW_UPDATE frame can be specific to a stream or to the entire
|
|
* connection. In the former case, the frame's stream identifier indicates
|
|
* the affected stream; in the latter, the value "0" indicates that the
|
|
* entire connection is the subject of the frame. *)
|
|
if Stream_identifier.is_connection stream_id
|
|
then add_window_increment t t.streams window_increment
|
|
else
|
|
match Scheduler.get_node t.streams stream_id with
|
|
| Some (Stream { descriptor; _ } as stream_node) ->
|
|
(match descriptor.state with
|
|
| Idle ->
|
|
(* From RFC7540§5.1:
|
|
* idle: [...] Receiving any frame other than HEADERS or PRIORITY on
|
|
* a stream in this state MUST be treated as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
| Active _
|
|
(* From RFC7540§5.1:
|
|
* reserved (local): [...] A PRIORITY or WINDOW_UPDATE frame MAY be
|
|
* received in this state. *)
|
|
| Reserved _ ->
|
|
add_window_increment t stream_node window_increment
|
|
| Closed _ ->
|
|
(* From RFC7540§5.1:
|
|
* Endpoints MUST ignore WINDOW_UPDATE or RST_STREAM frames received
|
|
* in this state, though endpoints MAY choose to treat frames that
|
|
* arrive a significant time after sending END_STREAM as a connection
|
|
* error (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
())
|
|
| None ->
|
|
if not (was_closed_or_implicitly_closed t stream_id)
|
|
then
|
|
(* From RFC7540§5.1:
|
|
* idle: [...] Receiving any frame other than HEADERS or PRIORITY on
|
|
* a stream in this state MUST be treated as a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
|
|
let process_continuation_frame t { Frame.frame_header; _ } headers_block =
|
|
let { Frame.stream_id; flags; _ } = frame_header in
|
|
match Scheduler.get_node t.streams stream_id with
|
|
| Some (Scheduler.Stream { descriptor; _ } as stream) ->
|
|
(match descriptor.state with
|
|
| Active
|
|
( ( Open (PartialHeaders partial_headers)
|
|
| HalfClosed (PartialHeaders partial_headers) )
|
|
, _ ) ->
|
|
handle_headers_block t stream partial_headers flags headers_block
|
|
| Active
|
|
( ( Open (ActiveMessage { trailers_parser = Some partial_headers; _ })
|
|
| HalfClosed
|
|
(ActiveMessage { trailers_parser = Some partial_headers; _ }) )
|
|
, _ ) ->
|
|
handle_trailer_headers t stream partial_headers flags headers_block
|
|
| _ ->
|
|
(* TODO: maybe need to handle the case where the stream has been closed
|
|
* due to a stream error. *)
|
|
(* From RFC7540§6.10:
|
|
* A RST_STREAM is the last frame that an endpoint can send on a
|
|
* stream. The peer that sends the RST_STREAM frame MUST be prepared
|
|
* to receive any frames that were sent or enqueued for sending by
|
|
* the remote peer. These frames can be ignored, except where they
|
|
* modify connection state (such as the state maintained for header
|
|
* compression (Section 4.3) or flow control). *)
|
|
report_connection_error t Error_code.ProtocolError)
|
|
| None ->
|
|
(* From RFC7540§6.10:
|
|
* A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE or
|
|
* CONTINUATION frame without the END_HEADERS flag set. A recipient that
|
|
* observes violation of this rule MUST respond with a connection error
|
|
* (Section 5.4.1) of type PROTOCOL_ERROR. *)
|
|
report_connection_error t Error_code.ProtocolError
|
|
|
|
(* From RFC7540§1:
|
|
* HTTP/2 [...] allows interleaving of request and response messages on the
|
|
* same connection and uses an efficient coding for HTTP header fields. *)
|
|
let create ?(config = Config.default) ?push_handler ~error_handler () =
|
|
let push_handler =
|
|
match push_handler with
|
|
| Some push_handler -> push_handler
|
|
| None -> default_push_handler
|
|
in
|
|
let settings =
|
|
{ (Config.to_settings config) with
|
|
(* If the caller is not going to process PUSH_PROMISE frames, just
|
|
* disable it. *)
|
|
enable_push =
|
|
config.enable_server_push && push_handler != default_push_handler
|
|
}
|
|
in
|
|
let rec connection_preface_handler recv_frame settings_list =
|
|
let t = Lazy.force t in
|
|
(* Now process the client's SETTINGS frame. `process_settings_frame` will
|
|
* take care of calling `wakeup_writer`. *)
|
|
process_settings_frame t recv_frame settings_list
|
|
and frame_handler r =
|
|
let t = Lazy.force t in
|
|
match r with
|
|
| Error e -> report_error t e
|
|
| Ok ({ Frame.frame_payload; frame_header } as frame) ->
|
|
(match t.receiving_headers_for_stream with
|
|
| Some stream_id
|
|
when (not Stream_identifier.(stream_id === frame_header.stream_id))
|
|
|| frame_header.frame_type <> Continuation ->
|
|
(* From RFC7540§6.2:
|
|
* A HEADERS frame without the END_HEADERS flag set MUST be followed
|
|
* by a CONTINUATION frame for the same stream. A receiver MUST treat
|
|
* the receipt of any other type of frame or a frame on a different
|
|
* stream as a connection error (Section 5.4.1) of type
|
|
* PROTOCOL_ERROR. *)
|
|
report_connection_error
|
|
t
|
|
~reason:
|
|
"HEADERS or PUSH_PROMISE without the END_HEADERS flag set must be \
|
|
followed by a CONTINUATION frame for the same stream"
|
|
Error_code.ProtocolError
|
|
| _ ->
|
|
(match frame_payload with
|
|
| Headers (_priority, headers_block) ->
|
|
process_headers_frame t frame headers_block
|
|
| Data bs -> process_data_frame t frame bs
|
|
| Priority priority -> process_priority_frame t frame priority
|
|
| RSTStream error_code -> process_rst_stream_frame t frame error_code
|
|
| Settings settings -> process_settings_frame t frame settings
|
|
| PushPromise (promised_stream_id, bs) ->
|
|
process_push_promise_frame t frame promised_stream_id bs
|
|
| Ping data -> process_ping_frame t frame data
|
|
| GoAway (last_stream_id, error, data) ->
|
|
process_goaway_frame t frame ~last_stream_id ~data error
|
|
| WindowUpdate window_size ->
|
|
process_window_update_frame t frame window_size
|
|
| Continuation headers_block ->
|
|
process_continuation_frame t frame headers_block
|
|
| Unknown _ ->
|
|
(* From RFC7540§5.1:
|
|
* Frames of unknown types are ignored. *)
|
|
()))
|
|
and t =
|
|
lazy
|
|
{ settings
|
|
; config
|
|
(* From RFC7540§5.1.1:
|
|
* Streams initiated by a client MUST use odd-numbered stream
|
|
* identifiers *)
|
|
; current_stream_id = -1l
|
|
; max_pushed_stream_id = 0l
|
|
; current_server_streams = 0
|
|
; receiving_headers_for_stream = None
|
|
; did_send_go_away = false
|
|
; unacked_settings = 0
|
|
; pending_pings = Queue.create ()
|
|
; error_handler
|
|
; push_handler
|
|
; reader =
|
|
Reader.client_frames
|
|
~max_frame_size:settings.max_frame_size
|
|
connection_preface_handler
|
|
frame_handler
|
|
; writer = Writer.create settings.max_frame_size
|
|
; streams =
|
|
Scheduler.make_root ()
|
|
(* From RFC7540§4.3:
|
|
* Header compression is stateful. One compression context and one
|
|
* decompression context are used for the entire connection. *)
|
|
; hpack_encoder = Hpack.Encoder.(create settings.header_table_size)
|
|
; hpack_decoder = Hpack.Decoder.(create settings.header_table_size)
|
|
}
|
|
in
|
|
let t = Lazy.force t in
|
|
(* Check if the settings for the connection are different than the default
|
|
* HTTP/2 settings. In the event that they are, we need to send a non-empty
|
|
* SETTINGS frame advertising our configuration. *)
|
|
let settings = Settings.settings_for_the_connection t.settings in
|
|
(* 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. The
|
|
* connection flow-control window is also 65,535 octets.
|
|
*
|
|
* XXX(anmonteiro): the starting setting for the initial window size for
|
|
* _sending_ is the default of 65535 octets. We're effectively overwriting it
|
|
* here to enforce this default after abusing the settings implementation to
|
|
* send our (receiving) in-flow setting to the peer. Throughout other parts
|
|
* of the code we (should) refer to it through
|
|
* [t.config.initial_window_size]. This should probably be cleaned up in the
|
|
* future. *)
|
|
t.settings <-
|
|
{ t.settings with
|
|
initial_window_size = Settings.default.initial_window_size
|
|
};
|
|
(* Send the client connection preface *)
|
|
Writer.write_connection_preface t.writer settings;
|
|
(* If a higher value for initial window size is configured, add more tokens
|
|
* to the connection (we have no streams at this point) -- the peer is
|
|
* allowed to send more than the defaults.
|
|
*
|
|
* From RFC7540§6.9.2:
|
|
* The connection flow-control window can only be changed using
|
|
* WINDOW_UPDATE frames. *)
|
|
(if t.config.initial_window_size > Settings.default.initial_window_size
|
|
then
|
|
let diff =
|
|
Int32.sub
|
|
t.config.initial_window_size
|
|
Settings.default.initial_window_size
|
|
in
|
|
send_window_update t t.streams diff);
|
|
t
|
|
|
|
let create_and_add_stream t ~error_handler =
|
|
let max_frame_size = t.settings.max_frame_size in
|
|
t.current_stream_id <- Int32.add t.current_stream_id 2l;
|
|
let stream_id = t.current_stream_id in
|
|
let respd =
|
|
Stream.create
|
|
stream_id
|
|
~max_frame_size
|
|
t.writer
|
|
error_handler
|
|
(on_close_stream t stream_id)
|
|
in
|
|
(* TODO: custom priority *)
|
|
let _stream : Scheduler.nonroot Scheduler.node =
|
|
Scheduler.add
|
|
t.streams
|
|
~priority:Priority.default_priority
|
|
~initial_send_window_size:t.settings.initial_window_size
|
|
~initial_recv_window_size:t.config.initial_window_size
|
|
respd
|
|
in
|
|
respd
|
|
|
|
(* Meant to be called after receiving an HTTP/1.1 `101 Switching_protocols`
|
|
* response upgrading to HTTP/2. *)
|
|
let create_h2c
|
|
?config
|
|
?push_handler
|
|
~headers
|
|
~target
|
|
~meth
|
|
~error_handler
|
|
(response_handler, response_error_handler)
|
|
=
|
|
match Headers.of_http1 ~headers ~target ~meth with
|
|
| Ok headers ->
|
|
(* From RFC7540§3.2:
|
|
* Upon receiving the 101 response, the client MUST send a connection
|
|
* preface (Section 3.5), which includes a SETTINGS frame. *)
|
|
let t = create ?config ?push_handler ~error_handler () in
|
|
let respd = create_and_add_stream t ~error_handler:response_error_handler in
|
|
assert (Stream_identifier.(t.current_stream_id === 1l));
|
|
assert (Stream_identifier.(respd.id === 1l));
|
|
let request = Request.create ~headers ~scheme:"http" meth target in
|
|
(* From RFC7540§3.2:
|
|
* The HTTP/1.1 request that is sent prior to upgrade is assigned a
|
|
* stream identifier of 1 (see Section 5.1.1) with default priority
|
|
* values (Section 5.3.5). Stream 1 is implicitly "half-closed" from the
|
|
* client toward the server (see Section 5.1), since the request is
|
|
* completed as an HTTP/1.1 request. *)
|
|
respd.state <-
|
|
Active
|
|
( HalfClosed WaitingForPeer
|
|
, { request
|
|
(* The request body is no more than a placeholder. The HTTP/1.1
|
|
* connection that we're upgrading from already sent it to the
|
|
* server. Application code knows what it is.
|
|
*
|
|
* From RFC7540§3.2:
|
|
* Requests that contain a payload body MUST be sent in their
|
|
* entirety before the client can send HTTP/2 frames. This means
|
|
* that a large request can block the use of the connection until
|
|
* it is completely sent. *)
|
|
; request_body = Body.Writer.create_empty ~writer:t.writer
|
|
; response_handler
|
|
; trailers_handler = ignore
|
|
} );
|
|
Writer.wakeup t.writer;
|
|
Ok t
|
|
| Error msg -> Error msg
|
|
|
|
let request
|
|
t
|
|
?(flush_headers_immediately = false)
|
|
?(trailers_handler = ignore)
|
|
request
|
|
~error_handler
|
|
~response_handler
|
|
=
|
|
let max_frame_size = t.settings.max_frame_size in
|
|
let respd = create_and_add_stream t ~error_handler in
|
|
let request_body =
|
|
Body.Writer.create (Bigstringaf.create max_frame_size) ~writer:t.writer
|
|
in
|
|
let frame_info =
|
|
Writer.make_frame_info
|
|
~max_frame_size:t.settings.max_frame_size
|
|
~flags:Flags.default_flags
|
|
respd.id
|
|
in
|
|
Writer.write_request_headers
|
|
t.writer
|
|
t.hpack_encoder
|
|
~priority:
|
|
(* TODO: allow setting the priority of the request. *)
|
|
Priority.default_priority
|
|
frame_info
|
|
request;
|
|
respd.state <-
|
|
Active
|
|
( Open WaitingForPeer
|
|
, { request; request_body; response_handler; trailers_handler } );
|
|
if not flush_headers_immediately then Writer.yield t.writer;
|
|
Writer.wakeup t.writer;
|
|
(* Closing the request body puts the stream in the half-closed (local) state.
|
|
* This is handled by {!Respd.flush_request_body}, which transitions the
|
|
* state once it verifies that there's no more data to send for the
|
|
* stream. *)
|
|
request_body
|
|
|
|
(* XXX: we store PING callbacks in FIFO order. Would it ever be the case that
|
|
* the receipt of a PING frame acknowledges a later callback? If so, we'd need
|
|
* to disallow sending custom PING payloads and generate a random payload that
|
|
* we store in a Hashtbl. *)
|
|
let ping t ?payload ?(off = 0) callback =
|
|
let payload =
|
|
match payload with
|
|
| None -> Serialize.default_ping_payload
|
|
| Some payload ->
|
|
if Bigstringaf.length payload - off < 8
|
|
then failwith "PING payload must have at least 8 octets in length";
|
|
payload
|
|
in
|
|
(* From RFC7540§6.7:
|
|
* ACK (0x1): When set, bit 0 indicates that this PING frame is a PING
|
|
* response.
|
|
*
|
|
* Note: this is not a PING response, quite the opposite, so we don't set
|
|
* the ACK flag. *)
|
|
let frame_info = Writer.make_frame_info Stream_identifier.connection in
|
|
Queue.add callback t.pending_pings;
|
|
Writer.write_ping t.writer frame_info ~off payload;
|
|
Writer.wakeup t.writer
|
|
|
|
let next_read_operation t =
|
|
if Reader.is_closed t.reader then shutdown_reader t;
|
|
match Reader.next t.reader with
|
|
| (`Read | `Close) as operation -> operation
|
|
| `Error e ->
|
|
report_error t e;
|
|
(match e with
|
|
| ConnectionError _ ->
|
|
(* From RFC7540§5.4.1:
|
|
* A connection error is any error that prevents further processing
|
|
* of the frame layer or corrupts any connection state. *)
|
|
`Close
|
|
| StreamError _ ->
|
|
(* From RFC7540§5.4.2:
|
|
* A stream error is an error related to a specific stream that does
|
|
* not affect processing of other streams. *)
|
|
`Read)
|
|
|
|
let read t bs ~off ~len = Reader.read_with_more t.reader bs ~off ~len Incomplete
|
|
|
|
let unexpected_eof t =
|
|
Scheduler.iter
|
|
~f:(fun (Stream { descriptor; _ }) ->
|
|
match descriptor.state with
|
|
| Idle | Reserved _ | Closed _ -> ()
|
|
| Active _ ->
|
|
Respd.report_error
|
|
descriptor
|
|
(`Malformed_response "unexpected eof")
|
|
ProtocolError)
|
|
t.streams;
|
|
Queue.iter (fun f -> f (Error `EOF)) t.pending_pings;
|
|
report_connection_error t ~reason:"unexpected eof" ProtocolError
|
|
|
|
let read_eof t bs ~off ~len =
|
|
let bytes_read = Reader.read_with_more t.reader bs ~off ~len Complete in
|
|
unexpected_eof t;
|
|
bytes_read
|
|
|
|
(* XXX(anmonteiro): this function is here to please the Gluten `RUNTIME`
|
|
* interface.
|
|
*
|
|
* We don't expect this function to ever be called. H2 never issues `Yield`
|
|
* commands because the connection is multiplexed, and it's therefore always
|
|
* looking to read frames from the peer. *)
|
|
let yield_reader _t k = k ()
|
|
|
|
let next_write_operation t =
|
|
flush_request_body t;
|
|
Writer.next t.writer
|
|
|
|
let yield_writer t k = Writer.on_wakeup_writer t.writer k
|
|
let report_write_result t result = Writer.report_result t.writer result
|