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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +66 -9
- data/ext/http3/nghttp3.c +20 -1
- data/ext/http3/nghttp3.h +35 -2
- data/ext/http3/nghttp3_connection.c +377 -205
- data/ext/http3/nghttp3_nv.c +53 -20
- data/ext/http3/nghttp3_qpack.c +64 -20
- data/ext/http3/session.c +197 -48
- data/lib/http3/rack_adapter.rb +141 -26
- data/lib/http3/server.rb +187 -17
- data/lib/http3/session.rb +211 -32
- data/lib/http3/version.rb +1 -1
- data/sig/http3/nghttp3/connection.rbs +43 -9
- data/sig/http3/nghttp3/nv.rbs +4 -0
- data/sig/http3/nghttp3/qpack.rbs +10 -3
- data/sig/http3/rack_adapter.rbs +17 -2
- data/sig/http3/server.rbs +6 -0
- data/sig/http3/session.rbs +52 -8
- data/sig/http3.rbs +3 -0
- metadata +3 -3
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
|
|
506
|
+
return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
|
|
470
507
|
}
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
http3_quic_api->open_uni_stream(s->qc, &
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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(
|
|
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
|
-
|
|
803
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
886
|
-
|
|
887
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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);
|
data/lib/http3/rack_adapter.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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" =>
|
|
60
|
-
"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
|
-
#
|
|
92
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|