http3 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ext/http3/session.c CHANGED
@@ -28,7 +28,7 @@
28
28
  VALUE rb_cHTTP3Session;
29
29
 
30
30
  static ID id_on_begin_headers, id_on_header, id_on_end_headers, id_on_data,
31
- id_on_end_stream, id_on_fields_too_large;
31
+ id_on_end_stream, id_on_fields_too_large, id_on_reset;
32
32
 
33
33
  /* What h3_session.field_sizes holds for a request whose header section has
34
34
  * gone over max_field_section_size. */
@@ -77,7 +77,9 @@ typedef struct {
77
77
  VALUE self; /* the object wrapping this struct; dcompact follows it */
78
78
  VALUE quic_conn; /* the QUIC::Connection::Server */
79
79
  quic_ext_conn *qc;
80
- nghttp3_conn *h3; /* made on handshake_completed */
80
+ nghttp3_conn *h3; /* made on handshake_completed, deleted by h3_fail */
81
+ bool failed; /* h3_fail has been called: h3 is gone for good */
82
+ int in_callback; /* how deep the Ruby part is in h3_protect */
81
83
  st_table *streams; /* stream_id => h3_stream * */
82
84
  VALUE pending_exc; /* an exception the Ruby part raised in a callback */
83
85
  VALUE closed_ids; /* ids of the streams nghttp3 closed, for #take_closed */
@@ -163,7 +165,9 @@ static void h3_stream_acked(h3_stream *st, uint64_t n) {
163
165
 
164
166
  static int h3_protect(h3_session *s, VALUE (*body)(VALUE), VALUE arg) {
165
167
  int state = 0;
168
+ s->in_callback++;
166
169
  rb_protect(body, arg, &state);
170
+ s->in_callback--;
167
171
  if (state == 0) {
168
172
  return 0;
169
173
  }
@@ -437,10 +441,43 @@ static const nghttp3_data_reader h3_data_reader = {.read_data =
437
441
 
438
442
  /* Stream handler (quic -> glue). ud is the h3_session. */
439
443
 
444
+ /* The HTTP/3 connection has failed: the QUIC connection is to close with
445
+ * app_error_code, and the nghttp3 connection goes, since nghttp3 allows
446
+ * nothing but nghttp3_conn_del once it has returned an error. From then on
447
+ * the handler fails every read and write, so that the QUIC connection
448
+ * closes even when the failure came from a call the Ruby part made. Never
449
+ * called while nghttp3 is running. */
450
+ static int h3_fail_with(h3_session *s, uint64_t app_error_code) {
451
+ http3_quic_api->set_application_error(s->qc, app_error_code);
452
+ if (s->h3 != NULL) {
453
+ nghttp3_conn_del(s->h3);
454
+ s->h3 = NULL;
455
+ }
456
+ s->failed = true;
457
+ return -1;
458
+ }
459
+
460
+ /* A failure inside nghttp3, with the HTTP/3 error code it stands for. */
461
+ static int h3_fail(h3_session *s, int64_t rv) {
462
+ return h3_fail_with(s, nghttp3_err_infer_quic_app_error_code((int)rv));
463
+ }
464
+
465
+ /* Raise for rv, which a call the Ruby part made into nghttp3 returned. A
466
+ * fatal error fails the connection first. */
467
+ static void h3_raise(h3_session *s, int rv, const char *message) {
468
+ if (nghttp3_err_is_fatal(rv) || rv == NGHTTP3_ERR_QPACK_FATAL) {
469
+ h3_fail(s, rv);
470
+ }
471
+ nghttp3_rb_raise(rv, message);
472
+ }
473
+
440
474
  /* Make the nghttp3 connection and its three unidirectional streams, as
441
475
  * ngtcp2's examples/server.cc does once the handshake is done. */
442
476
  static int h3_handshake_completed(void *ud) {
443
477
  h3_session *s = ud;
478
+ if (s->failed) {
479
+ return -1;
480
+ }
444
481
  if (s->h3 != NULL) {
445
482
  return 0;
446
483
  }
@@ -466,34 +503,39 @@ static int h3_handshake_completed(void *ud) {
466
503
 
467
504
  if (nghttp3_conn_server_new(&s->h3, &callbacks, &settings, NULL, s) != 0) {
468
505
  s->h3 = NULL;
469
- return -1;
506
+ return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
470
507
  }
471
- nghttp3_conn_set_max_client_streams_bidi(
472
- s->h3, http3_quic_api->local_max_streams_bidi(s->qc));
508
+ uint64_t max_streams = http3_quic_api->local_max_streams_bidi(s->qc);
509
+ nghttp3_conn_set_max_client_streams_bidi(s->h3, max_streams);
510
+ /* Every request #stop_reading gives up on leaves a Stream Cancellation on
511
+ * the QPACK decoder stream, and nghttp3 fails the connection once that
512
+ * stream holds more than max(this, 100) * 20 bytes. */
513
+ nghttp3_conn_set_max_concurrent_streams(s->h3, (size_t)max_streams);
473
514
 
474
515
  int64_t ctrl, enc, dec;
475
- if (http3_quic_api->open_uni_stream(s->qc, &ctrl) != 0 ||
476
- http3_quic_api->open_uni_stream(s->qc, &enc) != 0 ||
477
- http3_quic_api->open_uni_stream(s->qc, &dec) != 0 ||
516
+ int rv = 0;
517
+ if ((rv = http3_quic_api->open_uni_stream(s->qc, &ctrl)) != 0 ||
518
+ (rv = http3_quic_api->open_uni_stream(s->qc, &enc)) != 0 ||
519
+ (rv = http3_quic_api->open_uni_stream(s->qc, &dec)) != 0 ||
478
520
  nghttp3_conn_bind_control_stream(s->h3, ctrl) != 0 ||
479
521
  nghttp3_conn_bind_qpack_streams(s->h3, enc, dec) != 0) {
480
- return -1;
522
+ /* nghttp3 asserts the streams are bound before most calls, so the half
523
+ * made connection must go. A client has to let the server open three
524
+ * unidirectional streams (RFC 9114 section 6.2). */
525
+ return h3_fail_with(s, rv == QUIC_EXT_ERR_STREAM_ID_BLOCKED
526
+ ? NGHTTP3_H3_GENERAL_PROTOCOL_ERROR
527
+ : NGHTTP3_H3_INTERNAL_ERROR);
481
528
  }
482
529
  return 0;
483
530
  }
484
531
 
485
- /* A failure inside nghttp3 closes the QUIC connection with the HTTP/3 error
486
- * code it stands for. */
487
- static int h3_fail(h3_session *s, int64_t rv) {
488
- http3_quic_api->set_application_error(
489
- s->qc, nghttp3_err_infer_quic_app_error_code((int)rv));
490
- return -1;
491
- }
492
-
493
532
  static int h3_recv_stream_data(void *ud, int64_t id, const uint8_t *data,
494
533
  size_t len, int fin, uint64_t ts,
495
534
  size_t *consumed) {
496
535
  h3_session *s = ud;
536
+ if (s->failed) {
537
+ return -1;
538
+ }
497
539
  if (s->h3 == NULL) {
498
540
  /* Nothing can arrive on a stream before the handshake; drop it. */
499
541
  *consumed = len;
@@ -513,6 +555,9 @@ static int h3_next_stream_data(void *ud, int64_t *id, int *fin,
513
555
  *id = -1;
514
556
  *fin = 0;
515
557
  *nvecs = 0;
558
+ if (s->failed) {
559
+ return -1;
560
+ }
516
561
  if (s->h3 == NULL) {
517
562
  return 0;
518
563
  }
@@ -589,9 +634,16 @@ static int h3_stream_stop_sending(void *ud, int64_t id,
589
634
  return h3_shutdown_stream_read(ud, id);
590
635
  }
591
636
 
637
+ /* The client reset its side of the stream. The Ruby part answers a request
638
+ * cut short by it (#on_reset). */
592
639
  static int h3_stream_reset(void *ud, int64_t id, uint64_t final_size,
593
640
  uint64_t app_error_code) {
594
- return h3_shutdown_stream_read(ud, id);
641
+ h3_session *s = ud;
642
+ int rv = h3_shutdown_stream_read(s, id);
643
+ if (rv != 0 || s->h3 == NULL) {
644
+ return rv;
645
+ }
646
+ return h3_call_ruby(s, id_on_reset, id, 0, NULL);
595
647
  }
596
648
 
597
649
  static int h3_stream_close(void *ud, int64_t id, int has_code,
@@ -697,8 +749,27 @@ static h3_session *h3_session_get(VALUE self) {
697
749
  return s;
698
750
  }
699
751
 
700
- static h3_session *h3_session_get_open(VALUE self) {
752
+ /* The session, for a method that may call into nghttp3 or the QUIC
753
+ * connection, or fail the connection. The Ruby part's callbacks run inside
754
+ * nghttp3 or inside the QUIC connection's packet handling, where none of
755
+ * that may happen, so such a method raises InvalidStateError there. */
756
+ static h3_session *h3_session_enter(VALUE self) {
701
757
  h3_session *s = h3_session_get(self);
758
+ if (s->in_callback > 0) {
759
+ rb_raise(rb_eNghttp3InvalidStateError,
760
+ "#%" PRIsVALUE
761
+ " cannot be called from inside a callback of this session",
762
+ rb_id2str(rb_frame_this_func()));
763
+ }
764
+ return s;
765
+ }
766
+
767
+ /* h3_session_enter, for a method that needs the nghttp3 connection. */
768
+ static h3_session *h3_session_get_open(VALUE self) {
769
+ h3_session *s = h3_session_enter(self);
770
+ if (s->failed) {
771
+ rb_raise(rb_eNghttp3InvalidStateError, "the HTTP/3 connection has failed");
772
+ }
702
773
  if (s->h3 == NULL) {
703
774
  rb_raise(rb_eNghttp3InvalidStateError,
704
775
  "the HTTP/3 connection is not set up yet (handshake pending)");
@@ -706,12 +777,20 @@ static h3_session *h3_session_get_open(VALUE self) {
706
777
  return s;
707
778
  }
708
779
 
780
+ NORETURN(static void h3_raise_no_body(VALUE rb_id));
781
+ static void h3_raise_no_body(VALUE rb_id) {
782
+ rb_raise(rb_eNghttp3StreamNotFoundError,
783
+ "no response body is open on stream %" PRIsVALUE, rb_id);
784
+ }
785
+
786
+ /* The body of stream rb_id. Ruby code that runs once it is found may make
787
+ * the connection go on and close the stream, so convert everything else
788
+ * first. */
709
789
  static h3_stream *h3_session_stream(VALUE self, VALUE rb_id) {
710
790
  h3_session *s = h3_session_get(self);
711
791
  h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
712
792
  if (st == NULL) {
713
- rb_raise(rb_eNghttp3StreamNotFoundError,
714
- "no response body is open on stream %" PRIsVALUE, rb_id);
793
+ h3_raise_no_body(rb_id);
715
794
  }
716
795
  return st;
717
796
  }
@@ -724,7 +803,7 @@ static void h3_stream_wake(h3_session *s, h3_stream *st) {
724
803
  st->blocked = false;
725
804
  int rv = nghttp3_conn_resume_stream(s->h3, st->id);
726
805
  if (rv != 0) {
727
- nghttp3_rb_raise(rv, "Failed to resume stream");
806
+ h3_raise(s, rv, "Failed to resume stream");
728
807
  }
729
808
  }
730
809
 
@@ -783,15 +862,19 @@ static VALUE rb_http3_session_s_attach(VALUE klass, VALUE quic_conn,
783
862
  */
784
863
  static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
785
864
  VALUE headers, VALUE has_body) {
786
- h3_session *s = h3_session_get_open(self);
787
865
  int64_t id = NUM2LL(rb_id);
788
866
  Check_Type(headers, T_ARRAY);
789
867
 
790
- long nvlen = RARRAY_LEN(headers);
791
- VALUE tmp;
792
- nghttp3_nv *nva = ALLOCV_N(nghttp3_nv, tmp, nvlen);
868
+ /* Every field is converted before a pointer into any is taken: to_ary and
869
+ * to_str may run Ruby code, which could change or drop the Strings taken
870
+ * so far, or start a GC that moves them. It goes over a copy of headers,
871
+ * which that code cannot shrink under it, and keeps the Strings in
872
+ * strings, two per field. */
873
+ VALUE fields = rb_ary_dup(headers);
874
+ long nvlen = RARRAY_LEN(fields);
875
+ VALUE strings = rb_ary_new_capa(nvlen * 2);
793
876
  for (long i = 0; i < nvlen; i++) {
794
- VALUE pair = rb_check_array_type(RARRAY_AREF(headers, i));
877
+ VALUE pair = rb_check_array_type(RARRAY_AREF(fields, i));
795
878
  if (NIL_P(pair) || RARRAY_LEN(pair) != 2) {
796
879
  rb_raise(rb_eArgError, "headers must be [name, value] pairs");
797
880
  }
@@ -799,13 +882,16 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
799
882
  VALUE value = RARRAY_AREF(pair, 1);
800
883
  StringValue(name);
801
884
  StringValue(value);
802
- nva[i] = (nghttp3_nv){.name = (const uint8_t *)RSTRING_PTR(name),
803
- .value = (const uint8_t *)RSTRING_PTR(value),
804
- .namelen = (size_t)RSTRING_LEN(name),
805
- .valuelen = (size_t)RSTRING_LEN(value),
806
- .flags = NGHTTP3_NV_FLAG_NONE};
885
+ rb_ary_push(strings, name);
886
+ rb_ary_push(strings, value);
807
887
  }
808
888
 
889
+ /* Then the connection, which that code may have failed, and every
890
+ * allocation; then the pointers, and nghttp3 copies the names and values
891
+ * before anything else can run. */
892
+ h3_session *s = h3_session_get_open(self);
893
+ VALUE tmp;
894
+ nghttp3_nv *nva = ALLOCV_N(nghttp3_nv, tmp, nvlen);
809
895
  h3_stream *st = NULL;
810
896
  if (RTEST(has_body) && h3_stream_find(s, id) == NULL) {
811
897
  st = ZALLOC(h3_stream);
@@ -813,23 +899,76 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
813
899
  st->fd = -1;
814
900
  st_insert(s->streams, (st_data_t)id, (st_data_t)st);
815
901
  }
902
+ for (long i = 0; i < nvlen; i++) {
903
+ VALUE name = RARRAY_AREF(strings, 2 * i);
904
+ VALUE value = RARRAY_AREF(strings, 2 * i + 1);
905
+ nva[i] = (nghttp3_nv){.name = (const uint8_t *)RSTRING_PTR(name),
906
+ .value = (const uint8_t *)RSTRING_PTR(value),
907
+ .namelen = (size_t)RSTRING_LEN(name),
908
+ .valuelen = (size_t)RSTRING_LEN(value),
909
+ .flags = NGHTTP3_NV_FLAG_NONE};
910
+ }
816
911
 
817
- /* nghttp3 copies the header names and values. */
818
912
  int rv = nghttp3_conn_submit_response(
819
913
  s->h3, id, nva, (size_t)nvlen, RTEST(has_body) ? &h3_data_reader : NULL);
820
914
  ALLOCV_END(tmp);
821
- RB_GC_GUARD(headers);
915
+ RB_GC_GUARD(strings);
822
916
  if (rv != 0) {
823
917
  if (st != NULL) {
824
918
  st_data_t key = (st_data_t)id;
825
919
  st_delete(s->streams, &key, NULL);
826
920
  h3_stream_free(st);
827
921
  }
828
- nghttp3_rb_raise(rv, "Failed to submit response");
922
+ h3_raise(s, rv, "Failed to submit response");
829
923
  }
830
924
  return self;
831
925
  }
832
926
 
927
+ /* rb_id as the id of a request stream: 0..2**62-1 and client-initiated
928
+ * bidirectional. nghttp3 asserts the range. */
929
+ static int64_t h3_request_stream_id(VALUE rb_id) {
930
+ int64_t id = NUM2LL(rb_id);
931
+ if (id < 0 || (uint64_t)id > H3_VARINT_MAX) {
932
+ rb_raise(rb_eArgError, "stream_id must be 0..2**62-1 (got %" PRIsVALUE ")",
933
+ rb_id);
934
+ }
935
+ if ((id & 3) != 0) {
936
+ rb_raise(rb_eArgError,
937
+ "stream_id %" PRIsVALUE
938
+ " is not a client-initiated bidirectional stream",
939
+ rb_id);
940
+ }
941
+ return id;
942
+ }
943
+
944
+ /*
945
+ * call-seq:
946
+ * session.stop_reading(stream_id) -> self
947
+ *
948
+ * Read no more of the request on stream_id, which is being answered
949
+ * before all of it has arrived: nghttp3 drops what still comes, and the
950
+ * client gets STOP_SENDING with H3_NO_ERROR. RFC 9114 section 4.1 lets a
951
+ * server send a complete response before the request is complete and
952
+ * then ask the client to stop sending it, with H3_NO_ERROR.
953
+ */
954
+ static VALUE rb_http3_session_stop_reading(VALUE self, VALUE rb_id) {
955
+ int64_t id = h3_request_stream_id(rb_id);
956
+ h3_session *s = h3_session_get_open(self);
957
+ int rv = nghttp3_conn_shutdown_stream_read(s->h3, id);
958
+ if (rv != 0) {
959
+ h3_raise(s, rv, "Failed to stop reading the stream");
960
+ }
961
+ if (http3_quic_api->shutdown_stream_read(s->qc, id, NGHTTP3_H3_NO_ERROR) !=
962
+ 0) {
963
+ rb_raise(rb_eNghttp3Error, "cannot send STOP_SENDING on stream %" PRIsVALUE,
964
+ rb_id);
965
+ }
966
+ /* No end_headers will come to drop the count of a section cut short. */
967
+ st_data_t key = (st_data_t)id;
968
+ st_delete(s->field_sizes, &key, NULL);
969
+ return self;
970
+ }
971
+
833
972
  /*
834
973
  * call-seq:
835
974
  * session.push_chunk(stream_id, str) -> self
@@ -837,16 +976,16 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
837
976
  * Queue a frozen copy of str as the next piece of the response body.
838
977
  */
839
978
  static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
840
- h3_session *s = h3_session_get(self);
841
- h3_stream *st = h3_session_stream(self, rb_id);
979
+ h3_session *s = h3_session_enter(self);
842
980
  StringValue(str);
981
+ VALUE copy = rb_str_new_frozen(str);
982
+ h3_stream *st = h3_session_stream(self, rb_id);
843
983
  if (st->eof) {
844
984
  rb_raise(rb_eNghttp3InvalidStateError, "the body has ended");
845
985
  }
846
- if (RSTRING_LEN(str) == 0) {
986
+ if (RSTRING_LEN(copy) == 0) {
847
987
  return self;
848
988
  }
849
- VALUE copy = rb_str_new_frozen(str);
850
989
  if (h3_stream_push(st, (const uint8_t *)RSTRING_PTR(copy),
851
990
  (size_t)RSTRING_LEN(copy), copy) != 0) {
852
991
  rb_raise(rb_eNoMemError, "failed to queue a body chunk");
@@ -863,7 +1002,7 @@ static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
863
1002
  * Mark the end of the response body.
864
1003
  */
865
1004
  static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
866
- h3_session *s = h3_session_get(self);
1005
+ h3_session *s = h3_session_enter(self);
867
1006
  h3_stream *st = h3_session_stream(self, rb_id);
868
1007
  st->eof = true;
869
1008
  h3_stream_wake(s, st);
@@ -882,11 +1021,9 @@ static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
882
1021
  */
883
1022
  static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
884
1023
  VALUE offset, VALUE length) {
885
- h3_session *s = h3_session_get(self);
886
- h3_stream *st = h3_session_stream(self, rb_id);
887
- if (st->fd >= 0 || st->eof || st->nchunks > 0) {
888
- rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
889
- }
1024
+ h3_session *s = h3_session_enter(self);
1025
+ /* The arguments first, as #push_chunk does: io.fileno is Ruby code. */
1026
+ int64_t id = NUM2LL(rb_id);
890
1027
  off_t off = NUM2OFFT(offset);
891
1028
  off_t len = NUM2OFFT(length);
892
1029
  if (off < 0 || len < 0) {
@@ -897,6 +1034,15 @@ static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
897
1034
  rb_sys_fail("dup");
898
1035
  }
899
1036
  rb_update_max_fd(fd);
1037
+ h3_stream *st = h3_stream_find(s, id);
1038
+ if (st == NULL) {
1039
+ close(fd);
1040
+ h3_raise_no_body(rb_id);
1041
+ }
1042
+ if (st->fd >= 0 || st->eof || st->nchunks > 0) {
1043
+ close(fd);
1044
+ rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
1045
+ }
900
1046
  st->fd = fd;
901
1047
  st->file_off = off;
902
1048
  st->file_end = off + len;
@@ -954,13 +1100,13 @@ static VALUE rb_http3_session_take_error(VALUE self) {
954
1100
  * served.
955
1101
  */
956
1102
  static VALUE rb_http3_session_shutdown_notice(VALUE self) {
957
- h3_session *s = h3_session_get(self);
1103
+ h3_session *s = h3_session_enter(self);
958
1104
  if (s->h3 == NULL) {
959
1105
  return self;
960
1106
  }
961
1107
  int rv = nghttp3_conn_submit_shutdown_notice(s->h3);
962
1108
  if (rv != 0) {
963
- nghttp3_rb_raise(rv, "Failed to submit shutdown notice");
1109
+ h3_raise(s, rv, "Failed to submit shutdown notice");
964
1110
  }
965
1111
  return self;
966
1112
  }
@@ -974,7 +1120,7 @@ static VALUE rb_http3_session_shutdown_notice(VALUE self) {
974
1120
  */
975
1121
  static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
976
1122
  VALUE rb_code) {
977
- h3_session *s = h3_session_get(self);
1123
+ h3_session *s = h3_session_enter(self);
978
1124
  int64_t id = NUM2LL(rb_id);
979
1125
  uint64_t code = NUM2ULL(rb_code);
980
1126
  if (s->h3 != NULL) {
@@ -992,6 +1138,7 @@ void Init_http3_session(VALUE rb_mHTTP3) {
992
1138
  id_on_data = rb_intern("on_data");
993
1139
  id_on_end_stream = rb_intern("on_end_stream");
994
1140
  id_on_fields_too_large = rb_intern("on_fields_too_large");
1141
+ id_on_reset = rb_intern("on_reset");
995
1142
 
996
1143
  rb_cHTTP3Session = rb_define_class_under(rb_mHTTP3, "Session", rb_cObject);
997
1144
  rb_undef_alloc_func(rb_cHTTP3Session);
@@ -999,6 +1146,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
999
1146
  rb_http3_session_s_attach, 2);
1000
1147
  rb_define_method(rb_cHTTP3Session, "submit_response",
1001
1148
  rb_http3_session_submit_response, 3);
1149
+ rb_define_method(rb_cHTTP3Session, "stop_reading",
1150
+ rb_http3_session_stop_reading, 1);
1002
1151
  rb_define_method(rb_cHTTP3Session, "push_chunk", rb_http3_session_push_chunk,
1003
1152
  2);
1004
1153
  rb_define_method(rb_cHTTP3Session, "push_eof", rb_http3_session_push_eof, 1);
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
+ require "uri"
4
5
 
5
6
  module HTTP3
6
7
  # Calls a Rack 3 application for an HTTP3::Request and turns what it
@@ -10,6 +11,18 @@ module HTTP3
10
11
  # server answers 500 instead.
11
12
  class InvalidResponse < StandardError; end
12
13
 
14
+ # The request cannot be put in a Rack env; it is answered with 400.
15
+ class BadRequest < StandardError; end
16
+ private_constant :BadRequest
17
+
18
+ # The schemes a Rack env may carry (Rack also allows ws and wss, which
19
+ # HTTP/3 has no use for).
20
+ SCHEMES = %w[http https].freeze
21
+
22
+ # An authority, "host" or "host:port" (RFC 3986 section 3.2), as Rack
23
+ # Lint checks HTTP_HOST: the port is digits, of any number, or none.
24
+ AUTHORITY = /\A(?<host>#{URI::RFC3986_Parser::HOST})(?::(?<port>\d*))?\z/
25
+
13
26
  # Connection-specific fields, which HTTP/3 forbids (RFC 9114 section
14
27
  # 4.2).
15
28
  CONNECTION_FIELDS = %w[connection keep-alive transfer-encoding upgrade proxy-connection].freeze
@@ -23,18 +36,53 @@ module HTTP3
23
36
 
24
37
  def initialize(app)
25
38
  @app = app
39
+ @refusals = Hash.new(0)
26
40
  end
27
41
 
28
- # remote is the client's address, an Addrinfo. Raises whatever the
42
+ # remote is the client's address, an Addrinfo. CONNECT is answered with
43
+ # 501, and a request the Rack env cannot carry with 400, without calling
44
+ # the application; #take_refusals counts them. Raises whatever the
29
45
  # application raises, and InvalidResponse for a response that cannot be
30
46
  # sent; the Rack body is closed in both cases.
31
47
  def call(request, remote:)
32
- status, headers, body = @app.call(env_for(request, remote))
48
+ # The server cannot open a tunnel (RFC 9114 section 4.4), and a
49
+ # CONNECT request has neither :scheme nor :path, so the env would be
50
+ # made up. The Session leaves nghttp3's enable_connect_protocol off,
51
+ # so nghttp3 refuses the :protocol of an extended CONNECT (RFC 9220).
52
+ return refuse(501) if request.headers.assoc(":method")&.last == "CONNECT"
53
+
54
+ env = env_for(request, remote)
55
+ rescue BadRequest
56
+ # Not logged: the client sent it, the server did not fail.
57
+ refuse(400)
58
+ else
59
+ status, headers, body = @app.call(env)
33
60
  response_for(request, status, headers, body)
34
61
  end
35
62
 
63
+ # The requests answered without the application since the last call,
64
+ # as {status => count}; the count starts over.
65
+ def take_refusals
66
+ refusals = @refusals
67
+ @refusals = Hash.new(0)
68
+ refusals
69
+ end
70
+
36
71
  private
37
72
 
73
+ def refuse(status)
74
+ @refusals[status] += 1
75
+ Response.new(status: status, headers: [], body: nil)
76
+ end
77
+
78
+ # Raises BadRequest for what Rack does not allow, or allows in a form
79
+ # the application might read differently from the client, in this
80
+ # order: a scheme other than http and https, a fragment in the path, an
81
+ # authority that is not one (RFC 3986 section 3.2), and an :authority
82
+ # and a host field that disagree (RFC 9114 section 4.3.1). nghttp3 has
83
+ # already refused a request without :scheme, :path, or both :authority
84
+ # and host (but CONNECT); the defaults are for calling the adapter
85
+ # directly.
38
86
  def env_for(request, remote)
39
87
  pseudo = {}
40
88
  fields = {}
@@ -46,18 +94,33 @@ module HTTP3
46
94
  end
47
95
  end
48
96
 
49
- path, query = pseudo.fetch(":path", "/").split("?", 2)
50
- scheme = pseudo.fetch(":scheme", "https")
51
- authority = pseudo[":authority"] || fields["host"]&.first || "localhost"
52
- server_name, server_port = split_authority(authority, scheme)
97
+ # nghttp3 takes either case of http and https; Rack wants them in
98
+ # lowercase.
99
+ scheme = pseudo.fetch(":scheme", "https").downcase
100
+ raise BadRequest, "scheme #{scheme} is not http or https" unless SCHEMES.include?(scheme)
101
+
102
+ # nghttp3 lets a fragment through; Rack forbids one.
103
+ target = pseudo.fetch(":path", "/")
104
+ raise BadRequest, "the path has a fragment" if target.include?("#")
105
+
106
+ path, query = target.split("?", 2)
107
+ host = fields["host"]&.first
108
+ authority = pseudo[":authority"] || host || "localhost"
109
+ match = AUTHORITY.match(authority) or raise BadRequest, "the authority is malformed"
110
+ if pseudo[":authority"] && host && host != pseudo[":authority"]
111
+ raise BadRequest, ":authority and host differ"
112
+ end
113
+
114
+ port = match[:port]
115
+ port = (scheme == "http") ? "80" : "443" if port.nil? || port.empty?
53
116
 
54
117
  env = {
55
118
  "REQUEST_METHOD" => pseudo.fetch(":method", "GET"),
56
119
  "SCRIPT_NAME" => "",
57
120
  "PATH_INFO" => path,
58
121
  "QUERY_STRING" => query || "",
59
- "SERVER_NAME" => server_name,
60
- "SERVER_PORT" => server_port,
122
+ "SERVER_NAME" => match[:host],
123
+ "SERVER_PORT" => port,
61
124
  "SERVER_PROTOCOL" => "HTTP/3",
62
125
  "REMOTE_ADDR" => remote.ip_address,
63
126
  "HTTP_HOST" => authority,
@@ -88,35 +151,45 @@ module HTTP3
88
151
  end
89
152
  end
90
153
 
91
- # "host", "host:port", "[v6]" or "[v6]:port"; the port defaults to the
92
- # scheme's.
93
- def split_authority(authority, scheme)
94
- default_port = (scheme == "http") ? "80" : "443"
95
- if (match = authority.match(/\A(\[[^\]]*\])(?::(\d+))?\z/))
96
- [match[1], match[2] || default_port]
97
- else
98
- host, port = authority.split(":", 2)
99
- [host, port || default_port]
100
- end
101
- end
102
-
103
- # The Rack body is closed here unless it goes on in the Response.
154
+ # The Rack body is closed here unless it goes on in the Response, or
155
+ # to_ary has closed it (body_for yields then).
104
156
  def response_for(request, status, headers, body)
105
157
  response = nil
106
158
  status = Integer(status)
159
+ # A 1xx is no final response, which is all HTTP/3 can send here (Rack
160
+ # Lint only asks for 100 or more).
161
+ unless (200..599).cover?(status)
162
+ raise InvalidResponse, "status #{status} is not a final status (200..599)"
163
+ end
107
164
  headers = header_pairs(headers)
165
+ # A 204 must not have one (RFC 9110 section 8.6); it is dropped as the
166
+ # connection-specific fields are.
167
+ headers = headers.reject { |pair| pair.first == "content-length" } if status == 204
168
+ length = content_length(headers)
108
169
 
109
170
  method = request.headers.assoc(":method")&.last
110
- response = if method == "HEAD" || status < 200 || status == 204 || status == 304
171
+ response = if method == "HEAD" || status == 204 || status == 304
172
+ # No body is sent, so content-length is not held against one.
111
173
  close_rack_body(body)
112
174
  Response.new(status: status, headers: headers, body: nil)
113
175
  else
114
- Response.new(status: status, headers: headers, body: body_for(body))
176
+ Response.new(status: status, headers: headers, body: body_for(body, length) { body = nil })
115
177
  end
116
178
  ensure
117
179
  close_rack_body(body) if response.nil?
118
180
  end
119
181
 
182
+ # The content-length among headers, as an Integer, or nil if there is
183
+ # none. The message leaves the value out, as header_pairs's do.
184
+ def content_length(headers)
185
+ values = headers.filter_map { |name, value| value if name == "content-length" }
186
+ return nil if values.empty?
187
+ raise InvalidResponse, "content-length is given more than once" if values.size > 1
188
+ raise InvalidResponse, "content-length is not a number" unless values.first.match?(/\A\d+\z/)
189
+
190
+ Integer(values.first, 10)
191
+ end
192
+
120
193
  # Rack 3 headers: lowercase names, each value a String or an Array of
121
194
  # them, one field per element. nghttp3 sends names and values as they
122
195
  # are, so a name that is not a token (":status", say) or a value with
@@ -141,20 +214,62 @@ module HTTP3
141
214
  pairs
142
215
  end
143
216
 
144
- def body_for(body)
217
+ # The body to send. One whose length is known before it is sent, a file
218
+ # or an Array, is held against length, the content-length, now; one
219
+ # read with each, as it is read (#counted).
220
+ def body_for(body, length)
145
221
  if body.respond_to?(:to_path)
146
222
  io = ::File.open(body.to_path, "rb")
223
+ if length && io.size != length
224
+ size = io.size
225
+ io.close
226
+ raise length_mismatch(size)
227
+ end
147
228
  Body::File.new(io: io, offset: 0, length: io.size, rack_body: body)
148
229
  elsif body.respond_to?(:to_ary)
149
230
  # Rack 3 has to_ary close the body itself.
150
- Body::Chunks.new(enum: body.to_ary.each, rack_body: nil)
231
+ chunks = body.to_ary
232
+ yield
233
+ # Checked before anything is sent, which Session#fill_bodies would
234
+ # only find out once the headers have gone.
235
+ chunks.each do |chunk|
236
+ raise InvalidResponse, "body chunk must be a String (got #{chunk.class})" unless chunk.is_a?(String)
237
+ end
238
+ size = chunks.sum(&:bytesize)
239
+ raise length_mismatch(size) if length && size != length
240
+
241
+ Body::Chunks.new(enum: chunks.each, rack_body: nil)
151
242
  elsif body.respond_to?(:each)
152
- Body::Chunks.new(enum: body.to_enum(:each), rack_body: body)
243
+ enum = length ? counted(body, length) : body.to_enum(:each)
244
+ Body::Chunks.new(enum: enum, rack_body: body)
153
245
  else
154
246
  raise InvalidResponse, "streaming bodies (#{body.class}) are not supported"
155
247
  end
156
248
  end
157
249
 
250
+ # The chunks of body's each, raising InvalidResponse from next once they
251
+ # pass length bytes, or end short of it. Session#fill_bodies aborts the
252
+ # stream then, since the headers have gone. A chunk that is not a String
253
+ # goes on to fill_bodies, which refuses it.
254
+ def counted(body, length)
255
+ Enumerator.new do |y|
256
+ size = 0
257
+ body.each do |chunk|
258
+ size += chunk.bytesize if chunk.is_a?(String)
259
+ if size > length
260
+ raise InvalidResponse, "the body is at least #{size} bytes but content-length says otherwise"
261
+ end
262
+
263
+ y << chunk
264
+ end
265
+ raise length_mismatch(size) if size < length
266
+ end
267
+ end
268
+
269
+ def length_mismatch(size)
270
+ InvalidResponse.new("the body is #{size} bytes but content-length says otherwise")
271
+ end
272
+
158
273
  def close_rack_body(body)
159
274
  body.close if body.respond_to?(:close)
160
275
  end