mte/unikernel/duniverse/ocaml-h2/lib/reqd.ml

524 lines
20 KiB
OCaml
Raw Normal View History

2025-11-11 02:07:51 +01:00
(*----------------------------------------------------------------------------
* 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 Writer = Serialize.Writer
type error =
[ `Bad_request
| `Internal_server_error
| `Exn of exn
]
type error_handler =
?request:Request.t -> error -> (Headers.t -> Body.Writer.t) -> unit
type response_state =
| Waiting
| Fixed of
{ response : Response.t
; mutable iovec :
[ `String of string | `Bigstring of Bigstringaf.t ]
Httpun_types.IOVec.t
}
| Streaming of
{ response : Response.t
; response_body : Body.Writer.t
; trailers : Headers.t
}
| Complete of Response.t
type request_info =
{ request : Request.t
; request_body : Body.Reader.t
; mutable request_body_bytes : int64
}
type active_state = (request_info, request_info) Stream.active_state
type active_stream =
{ body_buffer_size : int
; encoder : Hpack.Encoder.t
; mutable response_state : response_state
(* We're not doing anything with these yet, we could probably have a
* `Reqd.schedule_read_trailers` function that would be called once
* trailer headers are emitted. *)
; mutable trailers_parser : Stream.partial_headers option
; mutable trailers : Headers.t option
; create_push_stream :
Stream_identifier.t
-> (t, [ `Push_disabled | `Stream_ids_exhausted ]) result
}
and state =
(active_state, active_stream, request_info * active_stream) Stream.state
and t = (state, error, error_handler) Stream.t
let create_active_request request request_body =
{ request; request_body; request_body_bytes = Int64.zero }
let create_active_stream encoder body_buffer_size create_push_stream =
{ body_buffer_size
; encoder
; response_state = Waiting
; trailers_parser = None
; trailers = None
; create_push_stream
}
let request (t : t) =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _ | FullHeaders), _) ->
assert false
| Active ((Open (ActiveMessage { request; _ }) | HalfClosed { request; _ }), _)
| Reserved ({ request; _ }, _) ->
request
| Closed _ -> assert false
let request_body (t : t) =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _ | FullHeaders), _) ->
assert false
| Active
( ( Open (ActiveMessage { request_body; _ })
| HalfClosed { request_body; _ } )
, _ ) ->
request_body
| Reserved _ ->
(* From RFC7540§8.1:
* Promised requests MUST NOT include a request body. *)
failwith
"h2.Reqd.request_body: Promised requests must not include a request body"
| Closed _ -> failwith "h2.Reqd.request_body: Stream already closed"
let response (t : t) =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) -> None
| Active
( (Open (FullHeaders | ActiveMessage _) | HalfClosed _)
, { response_state; _ } )
| Reserved (_, { response_state; _ }) ->
(match response_state with
| Waiting -> None
| Streaming { response; _ } | Fixed { response; _ } | Complete response ->
Some response)
| Closed _ -> None
let response_exn (t : t) =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) ->
failwith "h2.Reqd.response_exn: response has not started"
| Active
( (Open (FullHeaders | ActiveMessage _) | HalfClosed _)
, { response_state; _ } )
| Reserved (_, { response_state; _ }) ->
(match response_state with
| Waiting -> failwith "h2.Reqd.response_exn: response has not started"
| Streaming { response; _ } | Fixed { response; _ } | Complete response ->
response)
| Closed _ -> assert false
let send_fixed_response (t : t) s response data =
match s.response_state with
| Waiting ->
let iovec, length =
match data with
| `String s ->
let len = String.length s in
let iovec = { Httpun_types.IOVec.buffer = `String s; off = 0; len } in
iovec, len
| `Bigstring b ->
let len = Bigstringaf.length b in
let iovec =
{ Httpun_types.IOVec.buffer = `Bigstring b; off = 0; len }
in
iovec, len
in
let should_send_data = length <> 0 in
let frame_info =
Writer.make_frame_info
~max_frame_size:t.max_frame_size
~flags:
(if should_send_data
then Flags.default_flags
else Flags.(set_end_stream default_flags))
t.id
in
Writer.write_response_headers t.writer s.encoder frame_info response;
(* From RFC7540§8.1:
* An HTTP request/response exchange fully consumes a single stream.
* [...] A response starts with a HEADERS frame and ends with a frame
* bearing END_STREAM, which places the stream in the "closed" state. *)
if should_send_data
then s.response_state <- Fixed { response; iovec }
else s.response_state <- Complete response;
Writer.wakeup t.writer
| Streaming _ -> failwith "h2.Reqd.respond_with_*: response already started"
| Fixed _ -> failwith "h2.Reqd.respond_with_*: response already sent"
| Complete _ -> failwith "h2.Reqd.respond_with_*: response already complete"
let schedule_trailers (t : t) new_trailers =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) -> assert false
| Closed _ -> failwith "h2.Reqd.schedule_trailers: stream already closed"
| Reserved _ -> failwith "h2.Reqd.schedule_trailers: response not started"
| Active ((Open (FullHeaders | ActiveMessage _) | HalfClosed _), stream) ->
(match stream.response_state with
| Streaming { response; response_body; trailers = old_trailers } ->
if old_trailers <> Headers.empty
then failwith "h2.Reqd.schedule_trailers: trailers already scheduled";
stream.response_state <-
Streaming { response; response_body; trailers = new_trailers }
| _ ->
failwith
"h2.Reqd.schedule_trailers: can only send trailers in Streaming mode")
let unsafe_respond_with_data (t : t) response data =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) -> assert false
| Active ((Open (FullHeaders | ActiveMessage _) | HalfClosed _), stream) ->
send_fixed_response t stream response data
| Reserved (request_info, stream) ->
send_fixed_response t stream response data;
(* From RFC7540§8.1:
* reserved (local): [...] In this state, only the following transitions
* are possible: The endpoint can send a HEADERS frame. This causes the
* stream to open in a "half-closed (remote)" state. *)
Writer.flush t.writer (fun _reason ->
(* TODO(anmonteiro): different if closed? *)
t.state <- Active (HalfClosed request_info, stream))
| Closed _ -> assert false
let respond_with_string (t : t) response str =
match t.error_code with
| No_error -> unsafe_respond_with_data t response (`String str)
| _ ->
failwith
"h2.Reqd.respond_with_string: invalid state, currently handling error"
let respond_with_bigstring (t : t) response bstr =
match t.error_code with
| No_error -> unsafe_respond_with_data t response (`Bigstring bstr)
| _ ->
failwith
"h2.Reqd.respond_with_bigstring: invalid state, currently handling error"
let send_streaming_response ~flush_headers_immediately (t : t) s response =
let wait_for_first_flush = not flush_headers_immediately in
match s.response_state with
| Waiting ->
let frame_info =
Writer.make_frame_info ~max_frame_size:t.max_frame_size t.id
in
let response_body_buffer = Bigstringaf.create s.body_buffer_size in
let response_body =
Body.Writer.create response_body_buffer ~writer:t.writer
in
Writer.write_response_headers t.writer s.encoder frame_info response;
if wait_for_first_flush then Writer.yield t.writer;
s.response_state <-
Streaming { response; response_body; trailers = Headers.empty };
Writer.wakeup t.writer;
response_body
| Streaming _ ->
failwith "h2.Reqd.respond_with_streaming: response already started"
| Fixed _ | Complete _ ->
failwith "h2.Reqd.respond_with_streaming: response already complete"
let unsafe_respond_with_streaming (t : t) ~flush_headers_immediately response =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) -> assert false
| Active ((Open (FullHeaders | ActiveMessage _) | HalfClosed _), stream) ->
send_streaming_response ~flush_headers_immediately t stream response
| Reserved (request_info, stream) ->
let response_body =
send_streaming_response ~flush_headers_immediately t stream response
in
(* From RFC7540§8.1:
* reserved (local): [...] In this state, only the following transitions
* are possible: The endpoint can send a HEADERS frame. This causes the
* stream to open in a "half-closed (remote)" state. *)
Writer.flush t.writer (fun _reason ->
(* TODO(anmonteiro): different if closed? *)
t.state <- Active (HalfClosed request_info, stream));
response_body
| Closed _ -> assert false
let respond_with_streaming (t : t) ?(flush_headers_immediately = false) response
=
match t.error_code with
| No_error ->
unsafe_respond_with_streaming ~flush_headers_immediately t response
| _ ->
failwith
"h2.Reqd.respond_with_streaming: invalid state, currently handling error"
let start_push_stream (t : t) s request =
match s.create_push_stream t.id with
| Ok promised_reqd ->
let frame_info =
Writer.make_frame_info ~max_frame_size:t.max_frame_size t.id
in
Writer.write_push_promise
t.writer
s.encoder
frame_info
~promised_id:promised_reqd.id
request;
let { encoder; body_buffer_size; create_push_stream; _ } = s in
(* From RFC7540§8.2:
* Promised requests [...] MUST NOT include a request body. *)
let request_info = create_active_request request Body.Reader.empty in
let active_stream =
create_active_stream encoder body_buffer_size create_push_stream
in
(* From RFC7540§8.2.1:
* Sending a PUSH_PROMISE frame creates a new stream and puts the stream
* into the "reserved (local)" state for the server and the "reserved
* (remote)" state for the client.
*
* Note: we do this before flushing the writer because request handlers
* might immediately call one of the `respond_with` functions and expect
* the stream to be in the `Reserved` state. *)
promised_reqd.state <- Reserved (request_info, active_stream);
Writer.wakeup t.writer;
Ok promised_reqd
| Error e ->
Error (e :> [ `Push_disabled | `Stream_cant_push | `Stream_ids_exhausted ])
(* TODO: We could easily allow the priority of the PUSH request to be
* configurable. We should allow users of this API to define the weight (maybe
* not strictly), dependency on the current Reqd, and exclusivity *)
let unsafe_push (t : t) request =
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) -> assert false
| Active ((Open (FullHeaders | ActiveMessage _) | HalfClosed _), stream) ->
start_push_stream t stream request
(* Already checked in `push` *)
| Reserved _ | Closed _ -> assert false
let push (t : t) request =
match t.error_code with
| No_error ->
if Stream_identifier.is_pushed t.id
then
(* From RFC7540§6.6:
* PUSH_PROMISE frames MUST only be sent on a peer-initiated stream that
* is in either the "open" or "half-closed (remote)" state. *)
Error `Stream_cant_push
else unsafe_push t request
| _ -> failwith "h2.Reqd.push: invalid state, currently handling error"
let _report_error ?request (t : t) s (error : error) error_code =
match s.response_state, t.error_code with
| Waiting, No_error ->
t.error_code <- Stream.error_to_code error error_code;
let status =
match (error :> [ error | Status.standard ]) with
| `Exn _ -> `Internal_server_error
| #Status.standard as status -> status
in
t.error_handler ?request error (fun headers ->
let response = Response.create ~headers status in
unsafe_respond_with_streaming ~flush_headers_immediately:true t response)
| Streaming { response_body; _ }, No_error ->
Body.Writer.close response_body;
t.error_code <- Stream.error_to_code error error_code;
Stream.reset_stream t error_code
| Fixed _, No_error ->
(* Still need to send an RST_STREAM frame. Set t.error_code with
* `error_code` and `flush_response_body` below will reset the stream after
* flushing any remaining body bytes. *)
t.error_code <- Stream.error_to_code error error_code;
Stream.reset_stream t error_code
| (Waiting | Fixed _ | Streaming _), Exn _ ->
(* XXX(seliopou): Decide what to do in this unlikely case. There is an
* outstanding call to the [error_handler], but an intervening exception
* has been reported as well. *)
failwith "h2.Reqd.report_exn: NYI"
| (Waiting | Streaming _ | Fixed _ | Complete _), _ -> ()
let report_error (t : t) exn error_code =
match t.state with
| Idle | Reserved _ | Active (Open (WaitingForPeer | PartialHeaders _), _) ->
assert false
| Active (Open FullHeaders, stream) -> _report_error t stream exn error_code
| Active
( ( Open (ActiveMessage { request; request_body; _ })
| HalfClosed { request; request_body; _ } )
, stream ) ->
Body.Reader.close request_body;
_report_error t stream ~request exn error_code
| Closed _ -> ()
let report_exn t exn = report_error t (`Exn exn) Error_code.InternalError
let try_with t f : (unit, exn) result =
try
f ();
Ok ()
with
| exn ->
report_exn t exn;
Error exn
let error_code = Stream.error_code
(* Private API, not exposed to the user through h2.mli *)
let requires_output (t : t) =
match t.state with
| Idle -> false
| Reserved _ -> true
| Active (Open (WaitingForPeer | PartialHeaders _), _) -> false
| Active
( (Open (FullHeaders | ActiveMessage _) | HalfClosed _)
, { response_state; _ } ) ->
(* 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. *)
(match response_state with
| Complete _ -> false
| Fixed { iovec = { len; _ }; _ } -> len > 0
| Streaming _ -> true
| Waiting -> true)
| Closed _ -> false
let flush_request_body (t : t) =
match t.state with
| Active
( ( Open (ActiveMessage { request_body; _ })
| HalfClosed { request_body; _ } )
, _ ) ->
if Body.Reader.has_pending_output request_body
then (
try Body.Reader.execute_read request_body with exn -> report_exn t exn)
| _ -> ()
let write_buffer_data writer ~off ~len frame_info buffer =
match buffer with
| `String str -> Writer.write_data writer ~off ~len frame_info str
| `Bigstring bstr -> Writer.schedule_data writer ~off ~len frame_info bstr
let close_stream (t : t) =
match t.error_code with
| No_error ->
(match t.state with
| Active (Open (FullHeaders | ActiveMessage _), _) ->
(* 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). *)
Stream.reset_stream t Error_code.NoError
| Active (HalfClosed _, _) ->
Writer.flush t.writer (fun _reason -> Stream.finish_stream t Finished)
| _ -> assert false)
| Exn _ -> Stream.reset_stream t InternalError
| Other { code; _ } -> Stream.reset_stream t code
let flush_response_body (t : t) ~max_bytes =
match t.state with
| Active ((Open _ | HalfClosed _), stream) ->
(match stream.response_state with
| Streaming { response; response_body; trailers } ->
if Body.Writer.has_pending_output response_body && max_bytes > 0
then
Body.Writer.transfer_to_writer
response_body
t.writer
~max_frame_size:t.max_frame_size
~max_bytes
t.id
else if Body.Writer.is_closed response_body
then (
(* no pending output and closed, we can finalize the message and close
the stream *)
let frame_info =
Writer.make_frame_info
~max_frame_size:t.max_frame_size
~flags:Flags.(set_end_stream default_flags)
t.id
in
match trailers with
| _ :: _ ->
Writer.write_response_trailers
t.writer
stream.encoder
frame_info
trailers;
close_stream t;
stream.response_state <- Complete response;
0
| [] ->
(* From RFC7540§6.9.1:
* Frames with zero length with the END_STREAM flag set (that is,
* an empty DATA frame) MAY be sent if there is no available space
* in either flow-control window. *)
Writer.schedule_data t.writer frame_info ~len:0 Bigstringaf.empty;
close_stream t;
stream.response_state <- Complete response;
0)
else (* no pending output but Body is still open *)
0
| Fixed ({ iovec = { buffer; off; len } as iovec; _ } as r)
when max_bytes > 0 ->
let is_partial_flush = max_bytes < len in
let frame_info =
let flags =
if is_partial_flush
then Flags.default_flags
else Flags.(set_end_stream default_flags)
in
Writer.make_frame_info ~max_frame_size:t.max_frame_size ~flags t.id
in
let len_to_write = if is_partial_flush then max_bytes else len in
write_buffer_data t.writer ~off ~len:len_to_write frame_info buffer;
r.iovec <- Httpun_types.IOVec.shift iovec len_to_write;
if not is_partial_flush then close_stream t;
len_to_write
| Fixed _ | Waiting | Complete _ -> 0)
| _ -> 0
let deliver_trailer_headers (t : t) headers =
match t.state with
| Active (Open (PartialHeaders _ | FullHeaders), _) -> assert false
| Active ((Open (ActiveMessage _) | HalfClosed _), stream) ->
(* TODO: call the schedule_trailers callback *)
stream.trailers <- Some headers
| _ -> assert false