open Test_common open H2 open H2__ module Server_connection_tests = struct open Server_connection module Read_operation = struct type t = [ `Read | `Close | `Error of Error.t ] let pp_hum fmt t = let str = match t with | `Read -> "Read" | `Error (Error.ConnectionError (e, msg)) -> Format.sprintf "ConnectionError: %ld %S" (Error_code.serialize e) msg | `Error (Error.StreamError (stream_id, e)) -> Format.sprintf "StreamError on %ld: %ld" stream_id (Error_code.serialize e) | `Close -> "Close" in Format.pp_print_string fmt str end module Write_operation = struct type t = [ `Write of Bigstringaf.t IOVec.t list | `Yield | `Close of int ] let iovecs_to_string iovecs = let len = IOVec.lengthv iovecs in let bytes = Bytes.create len in let dst_off = ref 0 in List.iter (fun { IOVec.buffer; off = src_off; len } -> Bigstringaf.unsafe_blit_to_bytes buffer ~src_off bytes ~dst_off:!dst_off ~len; dst_off := !dst_off + len) iovecs; Bytes.unsafe_to_string bytes let pp_hum fmt t = match t with | `Write iovecs -> Format.fprintf fmt "Write %S" (iovecs_to_string iovecs |> hex_of_string) | `Yield -> Format.pp_print_string fmt "Yield" | `Close len -> Format.fprintf fmt "Close %i" len let to_write_as_string t = match t with | `Write iovecs -> Some (iovecs_to_string iovecs) | `Close _ | `Yield -> None end let read_operation = Alcotest.of_pp Read_operation.pp_hum let write_operation = Alcotest.of_pp Write_operation.pp_hum let default_request_handler reqd = Reqd.respond_with_string reqd (Response.create `OK) "" let test_initial_reader_state () = let t = create default_request_handler in Alcotest.(check read_operation) "A new reader wants input" `Read (next_read_operation t) let test_reader_is_closed_after_eof () = let t = create default_request_handler in let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; Alcotest.(check read_operation) "Shutting down a reader closes it" `Close (next_read_operation t); let t = create default_request_handler in let c = read t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read with no input returns 0" 0 c; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; Alcotest.(check read_operation) "Shutting down a reader closes it" `Close (next_read_operation t) let preface ?(settings = []) () = let writer = Serialize.Writer.create 0x400 in Serialize.Writer.write_connection_preface writer settings; Faraday.serialize_to_string (Serialize.Writer.faraday writer) let empty_preface = preface () let handle_preface ?settings t = let preface = preface ?settings () in let preface_len = String.length preface in let preface = read t (bs_of_string preface) ~off:0 ~len:preface_len in Alcotest.(check int) "read preface returns preface length" preface_len preface; match next_write_operation t with | `Write iovecs -> let iovec_len = IOVec.lengthv iovecs in Alcotest.(check bool "Write more than 0" true (iovec_len > 0)); report_write_result t (`Ok iovec_len) | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing the \ connection preface." let create_and_handle_preface ?settings ?error_handler ?config request_handler = let t = create ?config ?error_handler request_handler in handle_preface ?settings t; t let read_string t str = let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in let c = read t input ~off:0 ~len in Alcotest.(check int) "read consumes all input" len c let request_to_string ?body request = let has_body = match body with None -> false | Some _ -> true in let hpack_encoder = Hpack.Encoder.create 4096 in let writer = Serialize.Writer.create 0x400 in let frame_info = Writer.make_frame_info ~flags: (if has_body then Flags.default_flags else Flags.(default_flags |> set_end_stream)) 1l in Serialize.Writer.write_request_headers writer hpack_encoder ~priority:Priority.default_priority frame_info request; Faraday.serialize_to_string (Serialize.Writer.faraday writer) let read_request ?body t request = let request_string = request_to_string ?body request in read_string t request_string let response_to_string t ?body response = let has_body = match body with None -> false | Some _ -> true in let writer = Serialize.Writer.create 0x400 in let frame_info = Writer.make_frame_info ~flags: (if has_body then Flags.default_flags else Flags.(default_flags |> set_end_stream)) 1l in Serialize.Writer.write_response_headers writer t.hpack_encoder frame_info response; (match body with | None -> () | Some body -> Serialize.Writer.write_data writer { frame_info with flags = Flags.(default_flags |> set_end_stream) } body); Faraday.serialize_to_string (Serialize.Writer.faraday writer) let write_string t ?(msg = "frames written") str = let len = String.length str in Alcotest.(check (option string)) msg (Some (str |> hex_of_string)) (next_write_operation t |> Write_operation.to_write_as_string |> Option.map hex_of_string); report_write_result t (`Ok len) let write_response t ?body response = let response_string = response_to_string t ?body response in write_string t ~msg:"Response written" response_string let write_eof t = report_write_result t `Closed let ready_to_read t = Alcotest.check read_operation "Reader wants to read" `Read (next_read_operation t) let writer_yields t = Alcotest.check write_operation "Writer yields" `Yield (next_write_operation t) let writer_closed ?(unread = 0) t = Alcotest.(check write_operation) "Next operation should be `Close" (`Close unread) (next_write_operation t) let error_handler ?request:_ error handle = let message = match error with | `Exn exn -> Printexc.to_string exn | (#Status.client_error | #Status.server_error) as error -> Status.to_string error in let body = handle Headers.empty in Body.Writer.write_string body message; Body.Writer.close body let test_reading_malformed_frame ?(is_failure = false) wire () = let t = create_and_handle_preface ~error_handler default_request_handler in let len = String.length wire in let bs = Bigstringaf.of_string ~off:0 ~len wire in let c = read_eof t bs ~off:0 ~len in if is_failure then ( Alcotest.(check int) "read_eof with bad input (triggers a parse error) reads the header" 10 c; match Reader.next t.reader with | `Error _ -> Alcotest.(check pass) "bad input triggers an `Error` in the parser state" () () | _ -> Alcotest.fail "expected parser to be in an error state") else ( Alcotest.(check int) "read_eof with invalid reads the whole frame" len c; Alcotest.check read_operation "Shutting down a reader closes it" `Close (Reader.next t.reader)) (* Well-formed HEADERS + CONTINUATION frames. *) let header_and_continuation_frames = let hpack_encoder = Hpack.Encoder.create 4096 in let headers = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.(set_end_stream default_flags) ; frame_type = Headers } ; frame_payload = Frame.Headers ( Priority.default_priority , encode_headers hpack_encoder Headers.( of_list [ ":method", "GET"; ":scheme", "https"; ":path", "/" ]) ) } in let continuation = { Frame.frame_header = { headers.frame_header with flags = Flags.(default_flags |> set_end_header) ; frame_type = Continuation } ; frame_payload = Frame.Continuation (encode_headers hpack_encoder Headers.(of_list [ "baz", "qux" ])) } in headers, continuation let test_send_frame_after_padded_frame () = let t = create_and_handle_preface ~error_handler default_request_handler in let headers, continuation = header_and_continuation_frames in let padding = Bigstringaf.of_string ~off:0 ~len:10 "0123456789" in let headers_wire = Test_common.serialize_frame ~padding headers in let headers_length = Bigstringaf.length headers_wire in let continuation_wire = Test_common.serialize_frame continuation in let continuation_length = Bigstringaf.length continuation_wire in let read_headers = read t ~off:0 ~len:headers_length headers_wire in Alcotest.(check int) "Read the entire first frame" headers_length read_headers; let read_continuation = read t ~off:0 ~len:continuation_length continuation_wire in Alcotest.(check int) "Read the entire second frame" continuation_length read_continuation let read_frames conn frames = List.iter (fun frame -> let frame_wire = Test_common.serialize_frame frame in let frame_length = Bigstringaf.length frame_wire in let read_frame = read conn ~off:0 ~len:frame_length frame_wire in Alcotest.(check int) "Read the entire frame" frame_length read_frame) frames let check_response conn = match next_write_operation conn with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in let frame = List.hd frames in Alcotest.(check int) "Next write operation is a HEADERS frame" (Frame.FrameType.serialize Headers) Frame.(frame.frame_header.frame_type |> FrameType.serialize); let iovec_len = IOVec.lengthv iovecs in report_write_result conn (`Ok iovec_len); writer_yields conn; ready_to_read conn | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers." let test_continuation_frame () = let t = create_and_handle_preface ~error_handler default_request_handler in let headers, continuation = header_and_continuation_frames in read_frames t [ headers; continuation ]; write_response t (Response.create `OK); let new_headers = { headers with frame_header = { headers.frame_header with stream_id = 3l ; flags = Flags.(set_end_header default_flags) } } in read_frames t [ new_headers ]; check_response t let test_continuation_frame_another_stream () = let t = create_and_handle_preface ~error_handler default_request_handler in let headers, continuation = header_and_continuation_frames in let continuation = { continuation (* continuation header on a different stream *) with frame_header = { continuation.frame_header with stream_id = 3l } } in read_frames t [ headers; continuation ]; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in let frame = List.hd frames in Alcotest.(check int) "Next write operation is a GOAWAY frame" (Frame.FrameType.serialize GoAway) Frame.(frame.frame_header.frame_type |> FrameType.serialize); let iovec_len = IOVec.lengthv iovecs in report_write_result t (`Ok iovec_len); writer_closed t; Alcotest.(check bool) "Connection is shutdown" true (is_closed t) | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing a \ malformed CONTINUATION frame." let test_read_frame_size_error () = let max_length = String.length (preface ()) in let config = { Config.default with read_buffer_size = max_length } in let hpack_encoder = Hpack.Encoder.create 4096 in let headers = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.default_flags ; frame_type = Headers } ; frame_payload = Frame.Headers ( Priority.default_priority , encode_headers hpack_encoder Headers.( of_list [ ":method", "GET" ; ":scheme", "https" ; ":path", "/" ; ( "some_really_long_header_name" , "some_really_long_header_value" ) ]) ) } in let frame_wire = Test_common.serialize_frame headers in let frame_length = Bigstringaf.length frame_wire in Alcotest.(check bool) "Frame payload is surely over the max length" true (frame_length > max_length); let t = create_and_handle_preface ~config ~error_handler default_request_handler in let read1 = read t ~off:0 ~len:max_length frame_wire in Alcotest.(check int) "only read the frame header" 9 read1; let read2 = read t ~off:9 ~len:(max_length - 9) frame_wire in Alcotest.(check int) "advances over fed input" (max_length - 9) read2; let read3 = read t ~off:(read1 + read2) ~len:(frame_length - max_length) frame_wire in Alcotest.(check int) "advances over more input" (frame_length - max_length) read3; Alcotest.check read_operation "Reader wants to read" `Read (Reader.next t.reader); (* #2 *) let t = create_and_handle_preface ~config ~error_handler default_request_handler in let read1 = read t ~off:0 ~len:max_length frame_wire in Alcotest.(check int) "only read the frame header" 9 read1; let read2 = read t ~off:9 ~len:(max_length - 9) frame_wire in Alcotest.(check int) "advances over fed input" (max_length - 9) read2; (* Read buffer advanced, contents are not the same anymore. *) let read3 = read_eof t ~off:(read1 + read2) ~len: (frame_length - max_length - (* random *) 5) frame_wire in Alcotest.(check int) "advances over more input" (frame_length - max_length - (* random *) 5) read3; Alcotest.check read_operation "There was a connection error of type FRAME_SIZE_ERROR" (`Error Error.(ConnectionError (FrameSizeError, ""))) (Reader.next t.reader) let test_read_frame_size_error_priority_frame () = let max_length = String.length empty_preface in let config = { Config.default with read_buffer_size = max_length } in let t = create_and_handle_preface ~config ~error_handler default_request_handler in let frame_header_wire = "000025020000000001" |> string_of_hex |> bs_of_string in let frame_payload_wire = "6365727461696E6C7900000000000000000000000000000000000000000000000000000000" |> string_of_hex |> bs_of_string in let frame_payload_length = Bigstringaf.length frame_payload_wire in Alcotest.(check bool) "Frame payload is surely over the max length" true (frame_payload_length > max_length); let read1 = read t ~off:0 ~len:9 frame_header_wire in Alcotest.(check int) "read only the frame header" 9 read1; Alcotest.check read_operation "There was a stream error of type FRAME_SIZE_ERROR" (`Error Error.(StreamError (1l, FrameSizeError))) (Reader.next t.reader); (* payload length declared in the frame header *) let bytes_to_skip = ref 0x25 in let read2 = read t ~off:0 ~len:max_length frame_payload_wire in Alcotest.(check int) "keeps advancing over bad input (payload length)" max_length read2; bytes_to_skip := !bytes_to_skip - read2; let read3 = read t ~off:read2 ~len:!bytes_to_skip frame_payload_wire in Alcotest.(check int) "Read last bytes of payload" !bytes_to_skip read3; bytes_to_skip := !bytes_to_skip - read3; Alcotest.(check int) "No more to read" 0 !bytes_to_skip; Alcotest.check read_operation "Stream Error is not reported again, reader wants to read now" `Read (Reader.next t.reader) let test_preface_read_with_more_frames () = let t = create ~error_handler default_request_handler in let headers, _ = header_and_continuation_frames in let frame_wire = Test_common.serialize_frame headers in let frame_length = Bigstringaf.length frame_wire in let preface_length = String.length empty_preface in let preface_and_headers = Bigstringaf.create (frame_length + preface_length) in let preface_headers_length = Bigstringaf.length preface_and_headers in Bigstringaf.blit_from_string empty_preface ~src_off:0 preface_and_headers ~dst_off:0 ~len:preface_length; Bigstringaf.blit frame_wire ~src_off:0 preface_and_headers ~dst_off:preface_length ~len:frame_length; let read_preface = read t preface_and_headers ~off:0 ~len:preface_headers_length in Alcotest.(check int) "read preface returns preface length" (preface_length + frame_length) read_preface; Alcotest.check read_operation "Reader wants to read" `Read (next_read_operation t) let test_settings_frame_unsigned () = let settings = { Frame.frame_header = { payload_length = 0 ; stream_id = 0l ; flags = Flags.default_flags ; frame_type = Settings } ; frame_payload = Frame.Settings Settings.[ InitialWindowSize (Int32.shift_left 1l 31) ] } in let frame_wire = Test_common.serialize_frame settings in match parse_frames_bigstring frame_wire with | [ { Frame.frame_payload = Settings [ Settings.InitialWindowSize v ]; _ } ] -> (* The protocol says it should read a uint32 here, but because the * largest value it accepts is 2^31 - 1 we work around that by checking * for negative numbers (that have overflown). We avoid adding a new * dependency this way, but if we ever want to support it at least we * have this test. *) Alcotest.(check int32) "Window Size value roundtrips in a signed fashion" (Int32.shift_left (-1l) 31) v | _ -> Alcotest.fail "Expected frame to parse successfully." let test_open_existing_stream () = let t = create_and_handle_preface ~error_handler default_request_handler in let priority = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.default_flags ; frame_type = Priority } ; frame_payload = Frame.Priority Priority.default_priority } in read_frames t [ priority ]; let open Scheduler in let (Connection root) = t.streams in let root_children = root.children |> PriorityQueue.to_list in Alcotest.(check (list int32)) "Stream has been added to the priority tree" [ 1l ] (List.map fst root_children); let (Stream { descriptor = old_reqd; _ }) = root_children |> List.hd |> snd in let headers, _ = header_and_continuation_frames in let headers = { headers with Frame.frame_header = { headers.frame_header with flags = Flags.(default_flags |> set_end_header |> set_end_stream) } } in read_frames t [ headers ]; let open Scheduler in let new_root_children = root.children |> PriorityQueue.to_list in let (Stream { descriptor; _ }) = new_root_children |> List.hd |> snd in Alcotest.(check (list int32)) "Priority tree still only contains one stream" [ 1l ] (new_root_children |> List.map fst); Alcotest.(check bool) "Reqd is the same" true (old_reqd == descriptor) let data_request_handler reqd = Reqd.respond_with_string reqd (Response.create `OK) "Some data" let test_dependent_stream () = let t = create_and_handle_preface ~error_handler data_request_handler in let hpack_encoder = Hpack.Encoder.create 4096 in let headers = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.(default_flags |> set_end_header |> set_end_stream) ; frame_type = Headers } ; frame_payload = Frame.Headers ( Priority.default_priority , encode_headers hpack_encoder Headers.( of_list [ ":method", "GET"; ":scheme", "https"; ":path", "/" ]) ) } in let second_headers = { Frame.frame_header = { headers.frame_header with stream_id = 3l } ; frame_payload = Frame.Headers ( { Priority.default_priority with stream_dependency = 1l } , encode_headers hpack_encoder Headers.( of_list [ ":method", "GET"; ":scheme", "https"; ":path", "/" ]) ) } in read_frames t [ headers; second_headers ]; let open Scheduler in let (Stream first_stream) = Scheduler.get_node t.streams 1l |> opt_exn in let first_stream_children = first_stream.children |> PriorityQueue.to_list in Alcotest.(check (list int32)) "Stream 3 has been added to the priority tree" [ 3l ] (List.map fst first_stream_children); match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "HEADERS frames are flushed immediately, along with DATA for stream 1" Frame.FrameType.(List.map serialize [ Headers; Headers; Data ]) (frames |> List.map (fun { Frame.frame_header; _ } -> Frame.(frame_header.frame_type |> FrameType.serialize))); let data_frame = List.nth frames 2 in Alcotest.(check int32) "The emitted DATA frame belongs to stream 1" 1l data_frame.frame_header.stream_id; Server_connection.report_write_result t (`Ok (IOVec.lengthv iovecs)); (match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "DATA for stream 3 is flushed next" Frame.FrameType.[ serialize Data ] (frames |> List.map (fun { Frame.frame_header; _ } -> Frame.(frame_header.frame_type |> FrameType.serialize))); let data_frame = List.hd frames in Alcotest.(check int32) "The emitted DATA frame belongs to stream 3" 3l data_frame.frame_header.stream_id | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers.") | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers." let server_push_request_handler reqd = let request = Request.create `GET ~scheme:"http" "/main.css" in let pushed_reqd = match Reqd.push reqd request with | Ok reqd -> reqd | Error _ -> Alcotest.fail "Expected `push` to succeed" in let response = Response.create `OK in (* Send the response for / *) Reqd.respond_with_string reqd response "Hello"; (* Send the response for /main.css *) Reqd.respond_with_string pushed_reqd response "Hello" let test_server_push () = let t = create_and_handle_preface ~error_handler server_push_request_handler in let request = Request.create ~scheme:"https" `GET "/" in (* This calls the request handler. We then expect to receive at least 3 * frames: 1 HEADERS frame for the response, a PUSH_PROMISE frame for the * pushed request and its respective HEADERS frame. *) read_request t request; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "Next write operation surfaces writes for the PUSH_PROMISE frame and \ HEADERS / DATA" List.( map (fun { Frame.frame_header; _ } -> Frame.FrameType.serialize frame_header.frame_type) frames) List.( map Frame.FrameType.serialize Frame.FrameType.[ PushPromise; Headers; Headers; Data ]); let (Stream pushed_stream) = opt_exn (Scheduler.get_node t.streams 2l) in Alcotest.(check int32) "Pushed stream has a stream dependency on the parent stream" 1l pushed_stream.priority.stream_dependency; let iovec_len = IOVec.lengthv iovecs in report_write_result t (`Ok iovec_len); (match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in let frame = List.hd frames in Alcotest.(check int) "Next write operation surfaces the last DATA frame" Frame.FrameType.(serialize frame.frame_header.frame_type) Frame.FrameType.(serialize Data); Alcotest.(check int32) "The last DATA frame is for the server-pushed stream" 2l frame.frame_header.stream_id; let iovec_len = IOVec.lengthv iovecs in report_write_result t (`Ok iovec_len); writer_yields t; ready_to_read t | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers.") | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers." (* data frame with invalid amount of padding *) let w = string_of_hex "00000400080000000104AAAAAA" (* data frame with frame size error *) let w2 = string_of_hex "0080000008000000020648656C6C6F2C20776F726C6421686F77647921" (* priority frame with (stream error of) frame size error - header declares 4 * bytes of payload, not yet totally available on the buffer. *) let w3 = string_of_hex "0000040200000000018000" (* Testing for https://github.com/inhabitedtype/angstrom/pull/166 *) let test_reading_just_header wire () = let t = create_and_handle_preface ~error_handler default_request_handler in let wire = bs_of_string wire in let wire_length = Bigstringaf.length wire in let c = read t wire ~off:0 ~len:wire_length in Alcotest.(check int) "read with invalid payload reads only the header" 9 c; Alcotest.check read_operation "Reader wants more input to advance and report the stream error" `Read (Reader.next t.reader); let t = create_and_handle_preface ~error_handler default_request_handler in let c = read_eof t wire ~off:0 ~len:wire_length in (* Difference between this test and the one above is `read_eof` vs `read` *) Alcotest.(check int) "read_eof with invalid payload reads only the header" 9 c; Alcotest.check read_operation "Shutting down a reader closes it" (`Error Error.(StreamError (1l, FrameSizeError))) (Reader.next t.reader) let test_connect () = let error_handler_called = ref false in let error_handler ?request:_ error handle = error_handler_called := true; Alcotest.(check bool) "request was malformed" true (error = `Bad_request); let body = handle Headers.empty in Body.Writer.write_string body ""; Body.Writer.close body in let t = create_and_handle_preface ~error_handler default_request_handler in let request = Request.create ~headers:Headers.(of_list [ ":authority", "foo.com:8080" ]) ~scheme:"https" `CONNECT "/" in read_request t request; write_response t (Response.create `OK); writer_yields t; Alcotest.(check bool) "error handler was not called" false !error_handler_called let test_connect_malformed () = let error_handler_called = ref false in let error_handler ?request:_ error handle = error_handler_called := true; Alcotest.(check bool) "request was malformed" true (error = `Bad_request); let body = handle Headers.empty in Body.Writer.write_string body ""; Body.Writer.close body in let t = create_and_handle_preface ~error_handler default_request_handler in (* CONNECT is malformed if it doesn't include the `:authority` * pseudo-header. Additionally, the `:scheme` and `:path` pseudo-headers * must be omitted, but we take care of that when serializing. See * RFC7540§8.3. *) let request = Request.create ~scheme:"https" `CONNECT "/" in read_request t request; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in let frame = List.hd frames in Alcotest.(check int) "Next write operation is a HEADERS frame" (Frame.FrameType.serialize Headers) Frame.(frame.frame_header.frame_type |> FrameType.serialize); let iovec_len = IOVec.lengthv iovecs in report_write_result t (`Ok iovec_len); Alcotest.(check bool) "error handler was called" true !error_handler_called | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers." let test_client_max_concurrent_streams () = (* From RFC7540§5.1.2: [...] clients specify the maximum number of * concurrent streams the server can initiate, and servers specify the * maximum number of concurrent streams the client can initiate. * * Note: in this test, the client is saying the server is not allowed to * initiate streams. The client, however, is. *) let t = create_and_handle_preface ~settings:[ MaxConcurrentStreams 0l ] ~error_handler default_request_handler in let request = Request.create ~scheme:"https" `GET "/" in read_request t request; write_response t (Response.create `OK) let streaming_handler ?(flush = false) response writes reqd = let request_body = Reqd.request_body reqd in Body.Reader.close request_body; let body = Reqd.respond_with_streaming ~flush_headers_immediately:flush reqd response in let rec write writes = match writes with | [] -> Body.Writer.close body | w :: ws -> Body.Writer.write_string body w; Body.Writer.flush body (function | `Closed -> assert false | `Written -> write ws) in write writes let test_empty_fixed_streaming_response () = let request = Request.create ~scheme:"http" `GET "/" in let response = Response.create `OK ~headers:(Headers.of_list [ "content-length", "0" ]) in let t = create_and_handle_preface ~error_handler (streaming_handler response []) in read_request t request; write_response t ~body:"" response; writer_yields t let test_h2c () = let settings_payload = Settings.[ EnablePush 0; MaxConcurrentStreams 2l ] in let f = Faraday.create 100 in Settings.write_settings_payload f settings_payload; let serialized_settings = Faraday.serialize_to_string f in let request_handler_called = ref false in match create_h2c ~meth:`GET ~target:"/" ~headers: (Httpun_types.Headers.of_list [ "Connection", "Upgrade, HTTP2-Settings" ; "Upgrade", "h2c" ; ( "HTTP2-Settings" , Base64.( encode_string ~alphabet:uri_safe_alphabet serialized_settings) ) ; "Host", "localhost" ]) (fun _ -> request_handler_called := true) with | Ok t -> Alcotest.(check bool) "Request handler called" true !request_handler_called; Alcotest.(check bool) "Connection settings were set as per the incoming settings" true (t.settings = { Settings.default with enable_push = false ; max_concurrent_streams = 2l }); (match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in let frame = List.hd frames in Alcotest.(check int) "Next write operation is a SETTINGS frame (server connection preface)" (Frame.FrameType.serialize Settings) Frame.(frame.frame_header.frame_type |> FrameType.serialize) | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers.") | Error msg -> Alcotest.fail msg let test_nonzero_content_length_no_data_frames () = let request = Request.create ~headers:(Headers.of_list [ "content-length", "1234" ]) ~scheme:"http" `GET "/" in let t = create_and_handle_preface ~error_handler default_request_handler in read_request t request; write_response t ?body:None (Response.create `OK); writer_yields t let test_unexpected_eof () = let t = create_and_handle_preface ~error_handler default_request_handler in let request = Request.create ~scheme:"http" `GET "/" in read_request t request; write_eof t; writer_closed t ~unread:10 let test_read_frame_size_error_unknown_frame () = (* Enough for a frame header *) let max_length = String.length (preface ()) in let config = { Config.default with read_buffer_size = max_length } in let t = create_and_handle_preface ~config ~error_handler default_request_handler in let frame = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.default_flags ; frame_type = Unknown 80 } ; frame_payload = Frame.Unknown (80, Bigstringaf.of_string ~off:0 ~len:40 (String.make 40 'a')) } in let frame_wire = Test_common.serialize_frame frame in let frame_length = Bigstringaf.length frame_wire in Alcotest.(check bool) "Frame payload is surely over the max length" true (frame_length > max_length); let read1 = read t ~off:0 ~len:max_length frame_wire in Alcotest.(check int) "only read the frame header" 9 read1; let read2 = read t ~off:9 ~len:(max_length - 9) frame_wire in Alcotest.(check int) "advances over fed input" (max_length - 9) read2; let read3 = read t ~off:(read1 + read2) ~len:(frame_length - max_length) frame_wire in Alcotest.(check int) "advances over more input" (frame_length - max_length) read3; Alcotest.check read_operation "Reader wants to read, unknown frame type is ignored" `Read (Reader.next t.reader) let test_reading_request_body () = let body_read_called = ref false in let body_eof_called = ref false in let request = Request.create ~scheme:"http" `GET "/" in let response = Response.create `OK ~headers:(Headers.of_list [ "content-length", "0" ]) in let request_handler reqd = let request_body = Reqd.request_body reqd in Body.Reader.schedule_read request_body ~on_eof:ignore ~on_read:(fun _bs ~off:_ ~len:_ -> body_read_called := true; Alcotest.(check bool) "Response body isn't closed (yet) when reading" false (Body.Reader.is_closed request_body); Body.Reader.schedule_read ~on_read:(fun _ ~off:_ ~len:_ -> Body.Reader.schedule_read ~on_read:(fun _ ~off:_ ~len:_ -> ()) ~on_eof:(fun () -> body_eof_called := true; Reqd.respond_with_string reqd response "") request_body) ~on_eof:ignore request_body) in let t = create_and_handle_preface ~error_handler request_handler in read_request ~body:"request body" t request; let data_frame = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.default_flags ; frame_type = Data } ; frame_payload = Frame.Data (Bigstringaf.of_string ~off:0 ~len:3 "foo") } in read_frames t [ data_frame ]; read_frames t [ { data_frame with frame_header = { data_frame.frame_header with flags = Flags.(default_flags |> set_end_stream) } } ]; let window_update_and_response_frames = next_write_operation t |> Write_operation.to_write_as_string |> Option.get in report_write_result t (`Ok (String.length window_update_and_response_frames)); writer_yields t; Alcotest.(check bool) "Response body read handler called" true !body_read_called; Alcotest.(check bool) "Response body EOF handler called" true !body_eof_called let test_rst_stream_frames () = let t = create_and_handle_preface default_request_handler in let request = Request.create ~scheme:"http" `GET "/" in read_request ~body:"request body" t request; let data_frame = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.default_flags ; frame_type = Data } ; frame_payload = Frame.Data (Bigstringaf.of_string ~off:0 ~len:3 "foo") } in let rst_stream = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.default_flags ; frame_type = RSTStream } ; frame_payload = Frame.RSTStream Error_code.ProtocolError } in read_frames t [ data_frame; rst_stream; rst_stream ]; let window_update_and_response_frames = next_write_operation t |> Write_operation.to_write_as_string |> Option.get in report_write_result t (`Ok (String.length window_update_and_response_frames)); writer_yields t let test_flow_control () = let body_read_called = ref false in let request = Request.create ~scheme:"http" `GET "/" in let request_handler reqd = let request_body = Reqd.request_body reqd in Body.Reader.schedule_read request_body ~on_eof:ignore ~on_read:(fun _bs ~off:_ ~len:_ -> body_read_called := true) in let t = create_and_handle_preface ~error_handler request_handler in read_request ~body:"request body" t request; let data_frame = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.(default_flags |> set_end_stream) ; frame_type = Data } ; frame_payload = Frame.Data (Bigstringaf.of_string ~off:0 ~len:3 "foo") } in read_frames t [ data_frame ]; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "Only writes are WINDOW_UPDATE frames" (List.map Frame.FrameType.serialize Frame.FrameType.[ WindowUpdate; WindowUpdate ]) (List.map (fun Frame.{ frame_header = { frame_type; _ }; _ } -> Frame.FrameType.serialize frame_type) frames); report_write_result t (`Ok (IOVec.lengthv iovecs)); Alcotest.(check bool) "Response handler called" true !body_read_called; writer_yields t | _ -> assert false let test_flow_control_can_send_empty_data_frame () = let request = Request.create ~scheme:"http" `GET "/" in let request_handler reqd = let response = Response.create `OK in let response_body = Reqd.respond_with_streaming reqd response in Body.Writer.write_string response_body "hello"; Body.Writer.flush response_body (function | `Closed -> assert false | `Written -> Body.Writer.close response_body) in let t = create_and_handle_preface ~settings:Settings.[ InitialWindowSize 5l ] ~error_handler request_handler in read_request ~body:"request body" t request; let data_frame = { Frame.frame_header = { payload_length = 0 ; stream_id = 1l ; flags = Flags.(default_flags |> set_end_stream) ; frame_type = Data } ; frame_payload = Frame.Data (Bigstringaf.of_string ~off:0 ~len:3 "foo") } in read_frames t [ data_frame ]; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "Response written" (List.map Frame.FrameType.serialize Frame.FrameType.[ Headers; Data ]) (List.map (fun Frame.{ frame_header = { frame_type; _ }; _ } -> Frame.FrameType.serialize frame_type) frames); report_write_result t (`Ok (IOVec.lengthv iovecs)); (match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "Final 0-length DATA frame is not subject to flow control" (List.map Frame.FrameType.serialize Frame.FrameType.[ Data ]) (List.map (fun Frame.{ frame_header = { frame_type; _ }; _ } -> Frame.FrameType.serialize frame_type) frames); report_write_result t (`Ok (IOVec.lengthv iovecs)); writer_yields t | _ -> assert false) | _ -> assert false let trailers_request_handler reqd = let response = Response.create `OK in (* Send the response for / *) let response_body = Reqd.respond_with_streaming reqd response in Body.Writer.write_string response_body "somedata"; Body.Writer.flush response_body (function | `Closed -> assert false | `Written -> Reqd.schedule_trailers reqd Headers.(add empty "foo" "bar"); Body.Writer.close response_body) let test_trailers () = let t = create ~error_handler trailers_request_handler in handle_preface t; let headers, _ = header_and_continuation_frames in let headers = { headers with Frame.frame_header = { headers.frame_header with flags = Flags.(default_flags |> set_end_header |> set_end_stream) } } in read_frames t [ headers ]; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in List.iter2 (fun { Frame.frame_header; _ } (label, frame_type, flags) -> Alcotest.(check int) ("Next write operation surfaces writes for " ^ label) (Frame.FrameType.serialize frame_header.frame_type) (Frame.FrameType.serialize frame_type); Alcotest.(check int) "Correct flags are used" frame_header.flags flags) frames Frame.FrameType. [ ("HEADERS", Headers, Flags.(set_end_header default_flags)) ; "DATA", Data, Flags.default_flags ]; let iovec_len = IOVec.lengthv iovecs in report_write_result t (`Ok iovec_len); (match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in let frame = List.hd frames in Alcotest.(check int) "Next write operation surfaces the trailers HEADERS frame" Frame.FrameType.(serialize Headers) Frame.FrameType.(serialize frame.frame_header.frame_type); Alcotest.(check int) "Last HEADERS frame has END_STREAM and END_HEADERS flag" frame.frame_header.flags Flags.(set_end_stream (set_end_header default_flags)); let iovec_len = IOVec.lengthv iovecs in report_write_result t (`Ok iovec_len); Alcotest.check write_operation "Writer yields" `Yield (next_write_operation t); Alcotest.check read_operation "Reader wants to read" `Read (next_read_operation t) | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers.") | _ -> Alcotest.fail "Expected state machine to issue a write operation after seeing \ headers." let test_reset_stream () = let request = Request.create ~scheme:"http" `GET "/" in let request_handler reqd = Reqd.respond_with_string reqd (Response.create `Internal_server_error) "An error occurred"; Reqd.report_exn reqd Not_found in let t = create_and_handle_preface ~error_handler request_handler in read_request ~body:"request body" t request; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "Doesn't send DATA frames after RST_STREAM frames" (List.map Frame.FrameType.serialize Frame.FrameType.[ Headers; RSTStream ]) (List.map (fun Frame.{ frame_header = { frame_type; _ }; _ } -> Frame.FrameType.serialize frame_type) frames); report_write_result t (`Ok (IOVec.lengthv iovecs)); writer_yields t | _ -> assert false let test_reset_stream_streaming_response () = (* CONNECT is malformed if it doesn't include the `:authority` * pseudo-header. Additionally, the `:scheme` and `:path` pseudo-headers * must be omitted, but we take care of that when serializing. See * RFC7540§8.3. *) let request = Request.create ~scheme:"https" `CONNECT "/" in let error_handler ?request:_ error handle = Alcotest.(check bool) "request was malformed" true (error = `Bad_request); let body = handle Headers.empty in Body.Writer.write_string body "hi"; Body.Writer.close body in let t = create_and_handle_preface ~error_handler default_request_handler in read_request ~body:"request body" t request; match next_write_operation t with | `Write iovecs -> let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in Alcotest.(check (list int)) "Doesn't send RST_STREAM frames if the error is `Bad_request" (List.map Frame.FrameType.serialize Frame.FrameType.[ Headers; Data ]) (List.map (fun Frame.{ frame_header = { frame_type; _ }; _ } -> Frame.FrameType.serialize frame_type) frames); report_write_result t (`Ok (IOVec.lengthv iovecs)) | _ -> assert false (* TODO: test graceful shutdown, allowing lower numbered streams to complete. *) let suite = [ "initial reader state", `Quick, test_initial_reader_state ; "shutdown reader closed", `Quick, test_reader_is_closed_after_eof ; "malformed frame", `Quick, test_reading_malformed_frame w ; ( "malformed frame" , `Quick , test_reading_malformed_frame ~is_failure:true w2 ) ; "malformed frame", `Quick, test_reading_just_header w3 ; ( "send frames after a padded frame" , `Quick , test_send_frame_after_padded_frame ) ; ( "continuation frame on the same stream (correct)" , `Quick , test_continuation_frame ) ; ( "continuation frame on another stream" , `Quick , test_continuation_frame_another_stream ) ; ( "frame size error (frame size exceeds `Config.read_buffer_size`)" , `Quick , test_read_frame_size_error ) ; ( "frame size error on a priority frame" , `Quick , test_read_frame_size_error_priority_frame ) ; ( "connection preface read with more frames" , `Quick , test_preface_read_with_more_frames ) ; ( "settings that exceeds the maximum allowed" , `Quick , test_settings_frame_unsigned ) ; "open existing stream", `Quick, test_open_existing_stream ; "dependent stream", `Quick, test_dependent_stream ; "server push", `Quick, test_server_push ; "CONNECT method", `Quick, test_connect ; "CONNECT method (malformed)", `Quick, test_connect_malformed ; ( "Client sends 0 max concurrent streams" , `Quick , test_client_max_concurrent_streams ) ; ( "empty fixed streaming response" , `Quick , test_empty_fixed_streaming_response ) ; "starting an h2c connection", `Quick, test_h2c ; ( "non-zero `content-length` and no DATA frames" , `Quick , test_nonzero_content_length_no_data_frames ) ; "premature remote close with pending bytes", `Quick, test_unexpected_eof ; ( "frame size error unknown frame" , `Quick , test_read_frame_size_error_unknown_frame ) ; ( "reading the request body as it arrives" , `Quick , test_reading_request_body ) ; "accepting multiple RST_STREAM frames", `Quick, test_rst_stream_frames ; "flow control", `Quick, test_flow_control ; ( "flow control -- can send empty data frame" , `Quick , test_flow_control_can_send_empty_data_frame ) ; "trailers", `Quick, test_trailers ; "reset stream before all DATA frames were sent", `Quick, test_reset_stream ; ( "reset stream before all DATA frames were sent (streaming response)" , `Quick , test_reset_stream_streaming_response ) ] end let () = Alcotest.run "ocaml-h2 unit tests" [ "server_connection", Server_connection_tests.suite ]