http3 0.0.3 → 0.0.5
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 +92 -0
- data/LICENSE-DEPENDENCIES.txt +2 -2
- data/README.md +124 -8
- data/ext/http3/extconf.rb +8 -5
- data/ext/http3/nghttp3.c +20 -1
- data/ext/http3/nghttp3.h +38 -3
- data/ext/http3/nghttp3_callbacks.c +8 -10
- data/ext/http3/nghttp3_connection.c +378 -206
- data/ext/http3/nghttp3_nv.c +53 -20
- data/ext/http3/nghttp3_qpack.c +66 -22
- data/ext/http3/session.c +429 -55
- data/lib/http3/body_pipe.rb +160 -0
- data/lib/http3/body_stream.rb +120 -0
- data/lib/http3/rack_adapter.rb +220 -38
- data/lib/http3/request.rb +4 -2
- data/lib/http3/server.rb +551 -41
- data/lib/http3/session.rb +614 -66
- data/lib/http3/version.rb +1 -1
- data/lib/http3/worker_pool.rb +71 -0
- data/lib/http3.rb +3 -0
- data/sig/http3/body_pipe.rbs +45 -0
- data/sig/http3/body_stream.rbs +41 -0
- 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 +29 -4
- data/sig/http3/request.rbs +5 -3
- data/sig/http3/response.rbs +2 -2
- data/sig/http3/server.rbs +14 -1
- data/sig/http3/session.rbs +136 -13
- data/sig/http3/worker_pool.rbs +19 -0
- data/sig/http3.rbs +3 -0
- metadata +9 -6
- data/.clang-format +0 -1
- data/.vscode/extensions.json +0 -6
- data/.vscode/settings.json +0 -10
data/ext/http3/session.c
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <stdlib.h>
|
|
7
7
|
#include <string.h>
|
|
8
8
|
#include <sys/random.h>
|
|
9
|
+
#include <time.h>
|
|
9
10
|
#include <unistd.h>
|
|
10
11
|
|
|
11
12
|
/*
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
VALUE rb_cHTTP3Session;
|
|
29
30
|
|
|
30
31
|
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;
|
|
32
|
+
id_on_end_stream, id_on_fields_too_large, id_on_reset, id_on_stop_sending;
|
|
32
33
|
|
|
33
34
|
/* What h3_session.field_sizes holds for a request whose header section has
|
|
34
35
|
* gone over max_field_section_size. */
|
|
@@ -65,19 +66,28 @@ typedef struct {
|
|
|
65
66
|
size_t nchunks, cap;
|
|
66
67
|
size_t handed; /* how many of chunks read_data has handed to nghttp3 */
|
|
67
68
|
uint64_t unsent; /* bytes queued but not handed to nghttp3 yet */
|
|
69
|
+
uint64_t queued; /* bytes ever queued, the pieces read from a file too */
|
|
68
70
|
uint64_t acked; /* bytes the peer has acknowledged */
|
|
69
71
|
uint64_t shifted; /* bytes of the chunks already dropped */
|
|
70
72
|
int fd; /* a file body (#attach_file) not read in full yet, or -1 */
|
|
71
73
|
off_t file_off, file_end;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/* When the body began to wait on the peer, or last moved: the peer
|
|
75
|
+
* acknowledged some of it, or widened the stream's flow control window
|
|
76
|
+
* (CLOCK_MONOTONIC seconds; #response_stalled_since) */
|
|
77
|
+
double since;
|
|
78
|
+
bool fc_blocked; /* the peer's flow control window holds it back */
|
|
79
|
+
bool eof; /* nothing follows the queued chunks */
|
|
80
|
+
bool blocked; /* read_data returned NGHTTP3_ERR_WOULDBLOCK */
|
|
81
|
+
bool aborted; /* #abort_stream stopped it: no more of the body is sent */
|
|
74
82
|
} h3_stream;
|
|
75
83
|
|
|
76
84
|
typedef struct {
|
|
77
85
|
VALUE self; /* the object wrapping this struct; dcompact follows it */
|
|
78
86
|
VALUE quic_conn; /* the QUIC::Connection::Server */
|
|
79
87
|
quic_ext_conn *qc;
|
|
80
|
-
nghttp3_conn *h3; /* made on handshake_completed */
|
|
88
|
+
nghttp3_conn *h3; /* made on handshake_completed, deleted by h3_fail */
|
|
89
|
+
bool failed; /* h3_fail has been called: h3 is gone for good */
|
|
90
|
+
int in_callback; /* how deep the Ruby part is in h3_protect */
|
|
81
91
|
st_table *streams; /* stream_id => h3_stream * */
|
|
82
92
|
VALUE pending_exc; /* an exception the Ruby part raised in a callback */
|
|
83
93
|
VALUE closed_ids; /* ids of the streams nghttp3 closed, for #take_closed */
|
|
@@ -87,8 +97,18 @@ typedef struct {
|
|
|
87
97
|
/* stream_id => size of the header section received so far, while the
|
|
88
98
|
* headers of that request come in */
|
|
89
99
|
st_table *field_sizes;
|
|
100
|
+
/* When the peer last acknowledged some response body, on any stream
|
|
101
|
+
* (CLOCK_MONOTONIC seconds) */
|
|
102
|
+
double acked_at;
|
|
90
103
|
} h3_session;
|
|
91
104
|
|
|
105
|
+
/* CLOCK_MONOTONIC in seconds, as Process.clock_gettime gives it. */
|
|
106
|
+
static double h3_now(void) {
|
|
107
|
+
struct timespec ts;
|
|
108
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
109
|
+
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
/* Streams */
|
|
93
113
|
|
|
94
114
|
static void h3_chunk_release(h3_chunk *chunk) {
|
|
@@ -128,9 +148,20 @@ static int h3_stream_push(h3_stream *st, const uint8_t *base, size_t len,
|
|
|
128
148
|
}
|
|
129
149
|
st->chunks[st->nchunks++] = (h3_chunk){base, len, str};
|
|
130
150
|
st->unsent += len;
|
|
151
|
+
st->queued += len;
|
|
131
152
|
return 0;
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
/* Bytes of the body the peer has not acknowledged: queued, the rest of a
|
|
156
|
+
* file not read yet, or sent; 0 once the stream has been aborted. */
|
|
157
|
+
static uint64_t h3_stream_unacked(const h3_stream *st) {
|
|
158
|
+
if (st->aborted) {
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
uint64_t rest = st->fd >= 0 ? (uint64_t)(st->file_end - st->file_off) : 0;
|
|
162
|
+
return st->queued - st->acked + rest;
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
/* The whole file body has been read: let go of its descriptor now rather
|
|
135
166
|
* than when the stream closes, which waits for the peer to acknowledge
|
|
136
167
|
* every byte. */
|
|
@@ -140,6 +171,24 @@ static void h3_stream_end_file(h3_stream *st) {
|
|
|
140
171
|
st->eof = true;
|
|
141
172
|
}
|
|
142
173
|
|
|
174
|
+
/* The stream has been aborted, and no more of its body will be sent: let go
|
|
175
|
+
* at once of what nghttp3 has not taken, and of the file's descriptor,
|
|
176
|
+
* rather than when the stream closes, which waits for the peer to
|
|
177
|
+
* acknowledge the reset. The chunks nghttp3 has taken stay until then,
|
|
178
|
+
* since it still points into them. */
|
|
179
|
+
static void h3_stream_abort(h3_stream *st) {
|
|
180
|
+
for (size_t i = st->handed; i < st->nchunks; i++) {
|
|
181
|
+
h3_chunk_release(&st->chunks[i]);
|
|
182
|
+
}
|
|
183
|
+
st->nchunks = st->handed;
|
|
184
|
+
st->unsent = 0;
|
|
185
|
+
if (st->fd >= 0) {
|
|
186
|
+
close(st->fd);
|
|
187
|
+
st->fd = -1;
|
|
188
|
+
}
|
|
189
|
+
st->aborted = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
143
192
|
/* Drop the chunks at the front the peer has acknowledged to their end. A
|
|
144
193
|
* chunk acknowledged only in part stays. */
|
|
145
194
|
static void h3_stream_acked(h3_stream *st, uint64_t n) {
|
|
@@ -163,7 +212,9 @@ static void h3_stream_acked(h3_stream *st, uint64_t n) {
|
|
|
163
212
|
|
|
164
213
|
static int h3_protect(h3_session *s, VALUE (*body)(VALUE), VALUE arg) {
|
|
165
214
|
int state = 0;
|
|
215
|
+
s->in_callback++;
|
|
166
216
|
rb_protect(body, arg, &state);
|
|
217
|
+
s->in_callback--;
|
|
167
218
|
if (state == 0) {
|
|
168
219
|
return 0;
|
|
169
220
|
}
|
|
@@ -272,7 +323,14 @@ static VALUE h3_end_headers_body(VALUE arg) {
|
|
|
272
323
|
static int h3_acked_stream_data_cb(nghttp3_conn *conn, int64_t id,
|
|
273
324
|
uint64_t datalen, void *user_data,
|
|
274
325
|
void *stream_user_data) {
|
|
275
|
-
|
|
326
|
+
h3_session *s = user_data;
|
|
327
|
+
h3_stream *st = h3_stream_find(s, id);
|
|
328
|
+
if (datalen > 0) {
|
|
329
|
+
s->acked_at = h3_now();
|
|
330
|
+
if (st != NULL) {
|
|
331
|
+
st->since = s->acked_at;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
276
334
|
if (st != NULL) {
|
|
277
335
|
h3_stream_acked(st, datalen);
|
|
278
336
|
}
|
|
@@ -437,10 +495,43 @@ static const nghttp3_data_reader h3_data_reader = {.read_data =
|
|
|
437
495
|
|
|
438
496
|
/* Stream handler (quic -> glue). ud is the h3_session. */
|
|
439
497
|
|
|
498
|
+
/* The HTTP/3 connection has failed: the QUIC connection is to close with
|
|
499
|
+
* app_error_code, and the nghttp3 connection goes, since nghttp3 allows
|
|
500
|
+
* nothing but nghttp3_conn_del once it has returned an error. From then on
|
|
501
|
+
* the handler fails every read and write, so that the QUIC connection
|
|
502
|
+
* closes even when the failure came from a call the Ruby part made. Never
|
|
503
|
+
* called while nghttp3 is running. */
|
|
504
|
+
static int h3_fail_with(h3_session *s, uint64_t app_error_code) {
|
|
505
|
+
http3_quic_api->set_application_error(s->qc, app_error_code);
|
|
506
|
+
if (s->h3 != NULL) {
|
|
507
|
+
nghttp3_conn_del(s->h3);
|
|
508
|
+
s->h3 = NULL;
|
|
509
|
+
}
|
|
510
|
+
s->failed = true;
|
|
511
|
+
return -1;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* A failure inside nghttp3, with the HTTP/3 error code it stands for. */
|
|
515
|
+
static int h3_fail(h3_session *s, int64_t rv) {
|
|
516
|
+
return h3_fail_with(s, nghttp3_err_infer_quic_app_error_code((int)rv));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* Raise for rv, which a call the Ruby part made into nghttp3 returned. A
|
|
520
|
+
* fatal error fails the connection first. */
|
|
521
|
+
static void h3_raise(h3_session *s, int rv, const char *message) {
|
|
522
|
+
if (nghttp3_err_is_fatal(rv) || rv == NGHTTP3_ERR_QPACK_FATAL) {
|
|
523
|
+
h3_fail(s, rv);
|
|
524
|
+
}
|
|
525
|
+
nghttp3_rb_raise(rv, "%s", message);
|
|
526
|
+
}
|
|
527
|
+
|
|
440
528
|
/* Make the nghttp3 connection and its three unidirectional streams, as
|
|
441
529
|
* ngtcp2's examples/server.cc does once the handshake is done. */
|
|
442
530
|
static int h3_handshake_completed(void *ud) {
|
|
443
531
|
h3_session *s = ud;
|
|
532
|
+
if (s->failed) {
|
|
533
|
+
return -1;
|
|
534
|
+
}
|
|
444
535
|
if (s->h3 != NULL) {
|
|
445
536
|
return 0;
|
|
446
537
|
}
|
|
@@ -466,34 +557,39 @@ static int h3_handshake_completed(void *ud) {
|
|
|
466
557
|
|
|
467
558
|
if (nghttp3_conn_server_new(&s->h3, &callbacks, &settings, NULL, s) != 0) {
|
|
468
559
|
s->h3 = NULL;
|
|
469
|
-
return
|
|
560
|
+
return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
|
|
470
561
|
}
|
|
471
|
-
|
|
472
|
-
|
|
562
|
+
uint64_t max_streams = http3_quic_api->local_max_streams_bidi(s->qc);
|
|
563
|
+
nghttp3_conn_set_max_client_streams_bidi(s->h3, max_streams);
|
|
564
|
+
/* Every request #stop_reading gives up on leaves a Stream Cancellation on
|
|
565
|
+
* the QPACK decoder stream, and nghttp3 fails the connection once that
|
|
566
|
+
* stream holds more than max(this, 100) * 20 bytes. */
|
|
567
|
+
nghttp3_conn_set_max_concurrent_streams(s->h3, (size_t)max_streams);
|
|
473
568
|
|
|
474
569
|
int64_t ctrl, enc, dec;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
http3_quic_api->open_uni_stream(s->qc, &
|
|
570
|
+
int rv = 0;
|
|
571
|
+
if ((rv = http3_quic_api->open_uni_stream(s->qc, &ctrl)) != 0 ||
|
|
572
|
+
(rv = http3_quic_api->open_uni_stream(s->qc, &enc)) != 0 ||
|
|
573
|
+
(rv = http3_quic_api->open_uni_stream(s->qc, &dec)) != 0 ||
|
|
478
574
|
nghttp3_conn_bind_control_stream(s->h3, ctrl) != 0 ||
|
|
479
575
|
nghttp3_conn_bind_qpack_streams(s->h3, enc, dec) != 0) {
|
|
480
|
-
|
|
576
|
+
/* nghttp3 asserts the streams are bound before most calls, so the half
|
|
577
|
+
* made connection must go. A client has to let the server open three
|
|
578
|
+
* unidirectional streams (RFC 9114 section 6.2). */
|
|
579
|
+
return h3_fail_with(s, rv == QUIC_EXT_ERR_STREAM_ID_BLOCKED
|
|
580
|
+
? NGHTTP3_H3_GENERAL_PROTOCOL_ERROR
|
|
581
|
+
: NGHTTP3_H3_INTERNAL_ERROR);
|
|
481
582
|
}
|
|
482
583
|
return 0;
|
|
483
584
|
}
|
|
484
585
|
|
|
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
586
|
static int h3_recv_stream_data(void *ud, int64_t id, const uint8_t *data,
|
|
494
587
|
size_t len, int fin, uint64_t ts,
|
|
495
588
|
size_t *consumed) {
|
|
496
589
|
h3_session *s = ud;
|
|
590
|
+
if (s->failed) {
|
|
591
|
+
return -1;
|
|
592
|
+
}
|
|
497
593
|
if (s->h3 == NULL) {
|
|
498
594
|
/* Nothing can arrive on a stream before the handshake; drop it. */
|
|
499
595
|
*consumed = len;
|
|
@@ -513,6 +609,9 @@ static int h3_next_stream_data(void *ud, int64_t *id, int *fin,
|
|
|
513
609
|
*id = -1;
|
|
514
610
|
*fin = 0;
|
|
515
611
|
*nvecs = 0;
|
|
612
|
+
if (s->failed) {
|
|
613
|
+
return -1;
|
|
614
|
+
}
|
|
516
615
|
if (s->h3 == NULL) {
|
|
517
616
|
return 0;
|
|
518
617
|
}
|
|
@@ -534,14 +633,22 @@ static int h3_stream_data_written(void *ud, int64_t id, size_t n) {
|
|
|
534
633
|
return rv == 0 ? 0 : h3_fail(s, rv);
|
|
535
634
|
}
|
|
536
635
|
|
|
636
|
+
/* The peer's flow control window for the stream is used up. */
|
|
537
637
|
static int h3_stream_blocked(void *ud, int64_t id) {
|
|
538
638
|
h3_session *s = ud;
|
|
639
|
+
h3_stream *st = h3_stream_find(s, id);
|
|
640
|
+
if (st != NULL) {
|
|
641
|
+
st->fc_blocked = true;
|
|
642
|
+
}
|
|
539
643
|
if (s->h3 != NULL) {
|
|
540
644
|
nghttp3_conn_block_stream(s->h3, id);
|
|
541
645
|
}
|
|
542
646
|
return 0;
|
|
543
647
|
}
|
|
544
648
|
|
|
649
|
+
/* quic found our side of the stream shut when next_stream_data offered it:
|
|
650
|
+
* nghttp3 sends no more of it. recv_stop_sending and #abort_stream tell
|
|
651
|
+
* nghttp3 first, so this is what is left for a stream shut otherwise. */
|
|
545
652
|
static int h3_stream_shut_write(void *ud, int64_t id) {
|
|
546
653
|
h3_session *s = ud;
|
|
547
654
|
if (s->h3 != NULL) {
|
|
@@ -559,8 +666,15 @@ static int h3_acked_stream_data(void *ud, int64_t id, uint64_t n) {
|
|
|
559
666
|
return rv == 0 ? 0 : h3_fail(s, rv);
|
|
560
667
|
}
|
|
561
668
|
|
|
669
|
+
/* The peer widened the stream's flow control window: it has read some of
|
|
670
|
+
* the body. */
|
|
562
671
|
static int h3_extend_max_stream_data(void *ud, int64_t id) {
|
|
563
672
|
h3_session *s = ud;
|
|
673
|
+
h3_stream *st = h3_stream_find(s, id);
|
|
674
|
+
if (st != NULL) {
|
|
675
|
+
st->fc_blocked = false;
|
|
676
|
+
st->since = h3_now();
|
|
677
|
+
}
|
|
564
678
|
if (s->h3 == NULL) {
|
|
565
679
|
return 0;
|
|
566
680
|
}
|
|
@@ -589,9 +703,61 @@ static int h3_stream_stop_sending(void *ud, int64_t id,
|
|
|
589
703
|
return h3_shutdown_stream_read(ud, id);
|
|
590
704
|
}
|
|
591
705
|
|
|
706
|
+
/* The client reset its side of the stream. The Ruby part answers a request
|
|
707
|
+
* cut short by it (#on_reset). */
|
|
592
708
|
static int h3_stream_reset(void *ud, int64_t id, uint64_t final_size,
|
|
593
709
|
uint64_t app_error_code) {
|
|
594
|
-
|
|
710
|
+
h3_session *s = ud;
|
|
711
|
+
int rv = h3_shutdown_stream_read(s, id);
|
|
712
|
+
if (rv != 0 || s->h3 == NULL) {
|
|
713
|
+
return rv;
|
|
714
|
+
}
|
|
715
|
+
return h3_call_ruby(s, id_on_reset, id, 0, NULL);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* The client asked for no more of what goes out on the stream
|
|
719
|
+
* (STOP_SENDING); ngtcp2 resets our side of it once this returns. Of a
|
|
720
|
+
* response, what nghttp3 has not taken is let go at once, and nghttp3
|
|
721
|
+
* sends no more. The rest of the request is not read either: RFC 9114
|
|
722
|
+
* section 4.1.1 has a cancelled request stopped in every direction still
|
|
723
|
+
* open. That is done here, as #stop_reading does it, rather than left to
|
|
724
|
+
* the Ruby part, so that nothing more of the request comes in, from the
|
|
725
|
+
* packets read after this one too, even on a stream the Ruby part has not
|
|
726
|
+
* heard of yet, since STOP_SENDING may come before the request. quic sends
|
|
727
|
+
* nothing if all of the request has arrived. The Ruby part then drops the
|
|
728
|
+
* request (#on_stop_sending).
|
|
729
|
+
*
|
|
730
|
+
* Our unidirectional streams are the control and QPACK streams, which the
|
|
731
|
+
* client must not ask us to close, and whose closing is a connection error
|
|
732
|
+
* of type H3_CLOSED_CRITICAL_STREAM (RFC 9114 section 6.2.1, RFC 9204
|
|
733
|
+
* section 4.2): the connection fails at once, rather than when the stream
|
|
734
|
+
* has closed. The client's own unidirectional streams never get here:
|
|
735
|
+
* ngtcp2 refuses STOP_SENDING on a stream we only receive on. */
|
|
736
|
+
static int h3_recv_stop_sending(void *ud, int64_t id, uint64_t app_error_code) {
|
|
737
|
+
h3_session *s = ud;
|
|
738
|
+
if (s->h3 == NULL) {
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
if (id & 0x2) {
|
|
742
|
+
return h3_fail_with(s, NGHTTP3_H3_CLOSED_CRITICAL_STREAM);
|
|
743
|
+
}
|
|
744
|
+
h3_stream *st = h3_stream_find(s, id);
|
|
745
|
+
if (st != NULL) {
|
|
746
|
+
h3_stream_abort(st);
|
|
747
|
+
}
|
|
748
|
+
nghttp3_conn_shutdown_stream_write(s->h3, id);
|
|
749
|
+
int rv = h3_shutdown_stream_read(s, id);
|
|
750
|
+
if (rv != 0) {
|
|
751
|
+
return rv;
|
|
752
|
+
}
|
|
753
|
+
if (http3_quic_api->shutdown_stream_read(s->qc, id,
|
|
754
|
+
NGHTTP3_H3_REQUEST_CANCELLED) != 0) {
|
|
755
|
+
return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
|
|
756
|
+
}
|
|
757
|
+
/* No end_headers will come to drop the count of a section cut short. */
|
|
758
|
+
st_data_t key = (st_data_t)id;
|
|
759
|
+
st_delete(s->field_sizes, &key, NULL);
|
|
760
|
+
return h3_call_ruby(s, id_on_stop_sending, id, 0, NULL);
|
|
595
761
|
}
|
|
596
762
|
|
|
597
763
|
static int h3_stream_close(void *ud, int64_t id, int has_code,
|
|
@@ -608,8 +774,16 @@ static int h3_stream_close(void *ud, int64_t id, int has_code,
|
|
|
608
774
|
return h3_fail(s, rv);
|
|
609
775
|
}
|
|
610
776
|
|
|
777
|
+
/* The size of a handler filled in up to member m. The size tells quic.so
|
|
778
|
+
* which members the handler has (quic/ext.h): those it fills in, whatever
|
|
779
|
+
* quic/ext.h it is built against. */
|
|
780
|
+
#define H3_HANDLER_SIZE_TO(m) \
|
|
781
|
+
(offsetof(quic_ext_stream_handler, m) + \
|
|
782
|
+
sizeof(((quic_ext_stream_handler *)0)->m))
|
|
783
|
+
|
|
784
|
+
/* An ABI 1.1 handler: recv_stop_sending came with quic 0.0.6. */
|
|
611
785
|
static const quic_ext_stream_handler h3_handler = {
|
|
612
|
-
.size =
|
|
786
|
+
.size = H3_HANDLER_SIZE_TO(recv_stop_sending),
|
|
613
787
|
.recv_stream_data = h3_recv_stream_data,
|
|
614
788
|
.next_stream_data = h3_next_stream_data,
|
|
615
789
|
.stream_data_written = h3_stream_data_written,
|
|
@@ -622,6 +796,7 @@ static const quic_ext_stream_handler h3_handler = {
|
|
|
622
796
|
.stream_reset = h3_stream_reset,
|
|
623
797
|
.stream_close = h3_stream_close,
|
|
624
798
|
.handshake_completed = h3_handshake_completed,
|
|
799
|
+
.recv_stop_sending = h3_recv_stop_sending,
|
|
625
800
|
};
|
|
626
801
|
|
|
627
802
|
/* The Ruby object */
|
|
@@ -651,6 +826,11 @@ static int h3_free_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
|
|
|
651
826
|
return ST_CONTINUE;
|
|
652
827
|
}
|
|
653
828
|
|
|
829
|
+
static int h3_abort_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
|
|
830
|
+
h3_stream_abort((h3_stream *)value);
|
|
831
|
+
return ST_CONTINUE;
|
|
832
|
+
}
|
|
833
|
+
|
|
654
834
|
/* Never touches qc: the QUIC connection may be gone already. */
|
|
655
835
|
static void h3_session_free(void *ptr) {
|
|
656
836
|
h3_session *s = ptr;
|
|
@@ -697,8 +877,27 @@ static h3_session *h3_session_get(VALUE self) {
|
|
|
697
877
|
return s;
|
|
698
878
|
}
|
|
699
879
|
|
|
700
|
-
|
|
880
|
+
/* The session, for a method that may call into nghttp3 or the QUIC
|
|
881
|
+
* connection, or fail the connection. The Ruby part's callbacks run inside
|
|
882
|
+
* nghttp3 or inside the QUIC connection's packet handling, where none of
|
|
883
|
+
* that may happen, so such a method raises InvalidStateError there. */
|
|
884
|
+
static h3_session *h3_session_enter(VALUE self) {
|
|
701
885
|
h3_session *s = h3_session_get(self);
|
|
886
|
+
if (s->in_callback > 0) {
|
|
887
|
+
rb_raise(rb_eNghttp3InvalidStateError,
|
|
888
|
+
"#%" PRIsVALUE
|
|
889
|
+
" cannot be called from inside a callback of this session",
|
|
890
|
+
rb_id2str(rb_frame_this_func()));
|
|
891
|
+
}
|
|
892
|
+
return s;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* h3_session_enter, for a method that needs the nghttp3 connection. */
|
|
896
|
+
static h3_session *h3_session_get_open(VALUE self) {
|
|
897
|
+
h3_session *s = h3_session_enter(self);
|
|
898
|
+
if (s->failed) {
|
|
899
|
+
rb_raise(rb_eNghttp3InvalidStateError, "the HTTP/3 connection has failed");
|
|
900
|
+
}
|
|
702
901
|
if (s->h3 == NULL) {
|
|
703
902
|
rb_raise(rb_eNghttp3InvalidStateError,
|
|
704
903
|
"the HTTP/3 connection is not set up yet (handshake pending)");
|
|
@@ -706,12 +905,26 @@ static h3_session *h3_session_get_open(VALUE self) {
|
|
|
706
905
|
return s;
|
|
707
906
|
}
|
|
708
907
|
|
|
908
|
+
NORETURN(static void h3_raise_no_body(VALUE rb_id));
|
|
909
|
+
static void h3_raise_no_body(VALUE rb_id) {
|
|
910
|
+
rb_raise(rb_eNghttp3StreamNotFoundError,
|
|
911
|
+
"no response body is open on stream %" PRIsVALUE, rb_id);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
NORETURN(static void h3_raise_aborted(VALUE rb_id));
|
|
915
|
+
static void h3_raise_aborted(VALUE rb_id) {
|
|
916
|
+
rb_raise(rb_eNghttp3InvalidStateError,
|
|
917
|
+
"stream %" PRIsVALUE " has been aborted", rb_id);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/* The body of stream rb_id. Ruby code that runs once it is found may make
|
|
921
|
+
* the connection go on and close the stream, so convert everything else
|
|
922
|
+
* first. */
|
|
709
923
|
static h3_stream *h3_session_stream(VALUE self, VALUE rb_id) {
|
|
710
924
|
h3_session *s = h3_session_get(self);
|
|
711
925
|
h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
|
|
712
926
|
if (st == NULL) {
|
|
713
|
-
|
|
714
|
-
"no response body is open on stream %" PRIsVALUE, rb_id);
|
|
927
|
+
h3_raise_no_body(rb_id);
|
|
715
928
|
}
|
|
716
929
|
return st;
|
|
717
930
|
}
|
|
@@ -724,7 +937,7 @@ static void h3_stream_wake(h3_session *s, h3_stream *st) {
|
|
|
724
937
|
st->blocked = false;
|
|
725
938
|
int rv = nghttp3_conn_resume_stream(s->h3, st->id);
|
|
726
939
|
if (rv != 0) {
|
|
727
|
-
|
|
940
|
+
h3_raise(s, rv, "Failed to resume stream");
|
|
728
941
|
}
|
|
729
942
|
}
|
|
730
943
|
|
|
@@ -783,15 +996,19 @@ static VALUE rb_http3_session_s_attach(VALUE klass, VALUE quic_conn,
|
|
|
783
996
|
*/
|
|
784
997
|
static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
|
|
785
998
|
VALUE headers, VALUE has_body) {
|
|
786
|
-
h3_session *s = h3_session_get_open(self);
|
|
787
999
|
int64_t id = NUM2LL(rb_id);
|
|
788
1000
|
Check_Type(headers, T_ARRAY);
|
|
789
1001
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1002
|
+
/* Every field is converted before a pointer into any is taken: to_ary and
|
|
1003
|
+
* to_str may run Ruby code, which could change or drop the Strings taken
|
|
1004
|
+
* so far, or start a GC that moves them. It goes over a copy of headers,
|
|
1005
|
+
* which that code cannot shrink under it, and keeps the Strings in
|
|
1006
|
+
* strings, two per field. */
|
|
1007
|
+
VALUE fields = rb_ary_dup(headers);
|
|
1008
|
+
long nvlen = RARRAY_LEN(fields);
|
|
1009
|
+
VALUE strings = rb_ary_new_capa(nvlen * 2);
|
|
793
1010
|
for (long i = 0; i < nvlen; i++) {
|
|
794
|
-
VALUE pair = rb_check_array_type(RARRAY_AREF(
|
|
1011
|
+
VALUE pair = rb_check_array_type(RARRAY_AREF(fields, i));
|
|
795
1012
|
if (NIL_P(pair) || RARRAY_LEN(pair) != 2) {
|
|
796
1013
|
rb_raise(rb_eArgError, "headers must be [name, value] pairs");
|
|
797
1014
|
}
|
|
@@ -799,13 +1016,16 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
|
|
|
799
1016
|
VALUE value = RARRAY_AREF(pair, 1);
|
|
800
1017
|
StringValue(name);
|
|
801
1018
|
StringValue(value);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
.namelen = (size_t)RSTRING_LEN(name),
|
|
805
|
-
.valuelen = (size_t)RSTRING_LEN(value),
|
|
806
|
-
.flags = NGHTTP3_NV_FLAG_NONE};
|
|
1019
|
+
rb_ary_push(strings, name);
|
|
1020
|
+
rb_ary_push(strings, value);
|
|
807
1021
|
}
|
|
808
1022
|
|
|
1023
|
+
/* Then the connection, which that code may have failed, and every
|
|
1024
|
+
* allocation; then the pointers, and nghttp3 copies the names and values
|
|
1025
|
+
* before anything else can run. */
|
|
1026
|
+
h3_session *s = h3_session_get_open(self);
|
|
1027
|
+
VALUE tmp;
|
|
1028
|
+
nghttp3_nv *nva = ALLOCV_N(nghttp3_nv, tmp, nvlen);
|
|
809
1029
|
h3_stream *st = NULL;
|
|
810
1030
|
if (RTEST(has_body) && h3_stream_find(s, id) == NULL) {
|
|
811
1031
|
st = ZALLOC(h3_stream);
|
|
@@ -813,40 +1033,101 @@ static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
|
|
|
813
1033
|
st->fd = -1;
|
|
814
1034
|
st_insert(s->streams, (st_data_t)id, (st_data_t)st);
|
|
815
1035
|
}
|
|
1036
|
+
for (long i = 0; i < nvlen; i++) {
|
|
1037
|
+
VALUE name = RARRAY_AREF(strings, 2 * i);
|
|
1038
|
+
VALUE value = RARRAY_AREF(strings, 2 * i + 1);
|
|
1039
|
+
nva[i] = (nghttp3_nv){.name = (const uint8_t *)RSTRING_PTR(name),
|
|
1040
|
+
.value = (const uint8_t *)RSTRING_PTR(value),
|
|
1041
|
+
.namelen = (size_t)RSTRING_LEN(name),
|
|
1042
|
+
.valuelen = (size_t)RSTRING_LEN(value),
|
|
1043
|
+
.flags = NGHTTP3_NV_FLAG_NONE};
|
|
1044
|
+
}
|
|
816
1045
|
|
|
817
|
-
/* nghttp3 copies the header names and values. */
|
|
818
1046
|
int rv = nghttp3_conn_submit_response(
|
|
819
1047
|
s->h3, id, nva, (size_t)nvlen, RTEST(has_body) ? &h3_data_reader : NULL);
|
|
820
1048
|
ALLOCV_END(tmp);
|
|
821
|
-
RB_GC_GUARD(
|
|
1049
|
+
RB_GC_GUARD(strings);
|
|
822
1050
|
if (rv != 0) {
|
|
823
1051
|
if (st != NULL) {
|
|
824
1052
|
st_data_t key = (st_data_t)id;
|
|
825
1053
|
st_delete(s->streams, &key, NULL);
|
|
826
1054
|
h3_stream_free(st);
|
|
827
1055
|
}
|
|
828
|
-
|
|
1056
|
+
h3_raise(s, rv, "Failed to submit response");
|
|
829
1057
|
}
|
|
830
1058
|
return self;
|
|
831
1059
|
}
|
|
832
1060
|
|
|
1061
|
+
/* rb_id as the id of a request stream: 0..2**62-1 and client-initiated
|
|
1062
|
+
* bidirectional. nghttp3 asserts the range. */
|
|
1063
|
+
static int64_t h3_request_stream_id(VALUE rb_id) {
|
|
1064
|
+
int64_t id = NUM2LL(rb_id);
|
|
1065
|
+
if (id < 0 || (uint64_t)id > H3_VARINT_MAX) {
|
|
1066
|
+
rb_raise(rb_eArgError, "stream_id must be 0..2**62-1 (got %" PRIsVALUE ")",
|
|
1067
|
+
rb_id);
|
|
1068
|
+
}
|
|
1069
|
+
if ((id & 3) != 0) {
|
|
1070
|
+
rb_raise(rb_eArgError,
|
|
1071
|
+
"stream_id %" PRIsVALUE
|
|
1072
|
+
" is not a client-initiated bidirectional stream",
|
|
1073
|
+
rb_id);
|
|
1074
|
+
}
|
|
1075
|
+
return id;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/*
|
|
1079
|
+
* call-seq:
|
|
1080
|
+
* session.stop_reading(stream_id) -> self
|
|
1081
|
+
*
|
|
1082
|
+
* Read no more of the request on stream_id, which is being answered
|
|
1083
|
+
* before all of it has arrived: nghttp3 drops what still comes, and the
|
|
1084
|
+
* client gets STOP_SENDING with H3_NO_ERROR. RFC 9114 section 4.1 lets a
|
|
1085
|
+
* server send a complete response before the request is complete and
|
|
1086
|
+
* then ask the client to stop sending it, with H3_NO_ERROR.
|
|
1087
|
+
*/
|
|
1088
|
+
static VALUE rb_http3_session_stop_reading(VALUE self, VALUE rb_id) {
|
|
1089
|
+
int64_t id = h3_request_stream_id(rb_id);
|
|
1090
|
+
h3_session *s = h3_session_get_open(self);
|
|
1091
|
+
int rv = nghttp3_conn_shutdown_stream_read(s->h3, id);
|
|
1092
|
+
if (rv != 0) {
|
|
1093
|
+
h3_raise(s, rv, "Failed to stop reading the stream");
|
|
1094
|
+
}
|
|
1095
|
+
if (http3_quic_api->shutdown_stream_read(s->qc, id, NGHTTP3_H3_NO_ERROR) !=
|
|
1096
|
+
0) {
|
|
1097
|
+
rb_raise(rb_eNghttp3Error, "cannot send STOP_SENDING on stream %" PRIsVALUE,
|
|
1098
|
+
rb_id);
|
|
1099
|
+
}
|
|
1100
|
+
/* No end_headers will come to drop the count of a section cut short. */
|
|
1101
|
+
st_data_t key = (st_data_t)id;
|
|
1102
|
+
st_delete(s->field_sizes, &key, NULL);
|
|
1103
|
+
return self;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
833
1106
|
/*
|
|
834
1107
|
* call-seq:
|
|
835
1108
|
* session.push_chunk(stream_id, str) -> self
|
|
836
1109
|
*
|
|
837
1110
|
* Queue a frozen copy of str as the next piece of the response body.
|
|
1111
|
+
* Raises InvalidStateError once the stream has been aborted.
|
|
838
1112
|
*/
|
|
839
1113
|
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);
|
|
1114
|
+
h3_session *s = h3_session_enter(self);
|
|
842
1115
|
StringValue(str);
|
|
1116
|
+
VALUE copy = rb_str_new_frozen(str);
|
|
1117
|
+
h3_stream *st = h3_session_stream(self, rb_id);
|
|
1118
|
+
if (st->aborted) {
|
|
1119
|
+
h3_raise_aborted(rb_id);
|
|
1120
|
+
}
|
|
843
1121
|
if (st->eof) {
|
|
844
1122
|
rb_raise(rb_eNghttp3InvalidStateError, "the body has ended");
|
|
845
1123
|
}
|
|
846
|
-
if (RSTRING_LEN(
|
|
1124
|
+
if (RSTRING_LEN(copy) == 0) {
|
|
847
1125
|
return self;
|
|
848
1126
|
}
|
|
849
|
-
|
|
1127
|
+
if (h3_stream_unacked(st) == 0) {
|
|
1128
|
+
/* The body waits on the peer from now. */
|
|
1129
|
+
st->since = h3_now();
|
|
1130
|
+
}
|
|
850
1131
|
if (h3_stream_push(st, (const uint8_t *)RSTRING_PTR(copy),
|
|
851
1132
|
(size_t)RSTRING_LEN(copy), copy) != 0) {
|
|
852
1133
|
rb_raise(rb_eNoMemError, "failed to queue a body chunk");
|
|
@@ -860,11 +1141,15 @@ static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
|
|
|
860
1141
|
* call-seq:
|
|
861
1142
|
* session.push_eof(stream_id) -> self
|
|
862
1143
|
*
|
|
863
|
-
* Mark the end of the response body.
|
|
1144
|
+
* Mark the end of the response body. Raises InvalidStateError once the
|
|
1145
|
+
* stream has been aborted.
|
|
864
1146
|
*/
|
|
865
1147
|
static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
|
|
866
|
-
h3_session *s =
|
|
1148
|
+
h3_session *s = h3_session_enter(self);
|
|
867
1149
|
h3_stream *st = h3_session_stream(self, rb_id);
|
|
1150
|
+
if (st->aborted) {
|
|
1151
|
+
h3_raise_aborted(rb_id);
|
|
1152
|
+
}
|
|
868
1153
|
st->eof = true;
|
|
869
1154
|
h3_stream_wake(s, st);
|
|
870
1155
|
return self;
|
|
@@ -878,15 +1163,14 @@ static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
|
|
|
878
1163
|
* descriptor is duplicated, so io may be closed afterwards. The duplicate
|
|
879
1164
|
* is close-on-exec, as Ruby's own descriptors are, so that a process the
|
|
880
1165
|
* application spawns does not inherit it, and is closed as soon as the
|
|
881
|
-
* whole length has been read.
|
|
1166
|
+
* whole length has been read. Raises InvalidStateError once the stream has
|
|
1167
|
+
* been aborted.
|
|
882
1168
|
*/
|
|
883
1169
|
static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
|
|
884
1170
|
VALUE offset, VALUE length) {
|
|
885
|
-
h3_session *s =
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
|
|
889
|
-
}
|
|
1171
|
+
h3_session *s = h3_session_enter(self);
|
|
1172
|
+
/* The arguments first, as #push_chunk does: io.fileno is Ruby code. */
|
|
1173
|
+
int64_t id = NUM2LL(rb_id);
|
|
890
1174
|
off_t off = NUM2OFFT(offset);
|
|
891
1175
|
off_t len = NUM2OFFT(length);
|
|
892
1176
|
if (off < 0 || len < 0) {
|
|
@@ -897,9 +1181,23 @@ static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
|
|
|
897
1181
|
rb_sys_fail("dup");
|
|
898
1182
|
}
|
|
899
1183
|
rb_update_max_fd(fd);
|
|
1184
|
+
h3_stream *st = h3_stream_find(s, id);
|
|
1185
|
+
if (st == NULL) {
|
|
1186
|
+
close(fd);
|
|
1187
|
+
h3_raise_no_body(rb_id);
|
|
1188
|
+
}
|
|
1189
|
+
if (st->aborted) {
|
|
1190
|
+
close(fd);
|
|
1191
|
+
h3_raise_aborted(rb_id);
|
|
1192
|
+
}
|
|
1193
|
+
if (st->fd >= 0 || st->eof || st->nchunks > 0) {
|
|
1194
|
+
close(fd);
|
|
1195
|
+
rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
|
|
1196
|
+
}
|
|
900
1197
|
st->fd = fd;
|
|
901
1198
|
st->file_off = off;
|
|
902
1199
|
st->file_end = off + len;
|
|
1200
|
+
st->since = h3_now();
|
|
903
1201
|
if (len == 0) {
|
|
904
1202
|
h3_stream_end_file(st);
|
|
905
1203
|
}
|
|
@@ -919,6 +1217,36 @@ static VALUE rb_http3_session_unsent_bytes(VALUE self, VALUE rb_id) {
|
|
|
919
1217
|
return ULL2NUM(st == NULL ? 0 : st->unsent);
|
|
920
1218
|
}
|
|
921
1219
|
|
|
1220
|
+
/*
|
|
1221
|
+
* call-seq:
|
|
1222
|
+
* session.response_stalled_since(stream_id) -> Float or nil
|
|
1223
|
+
*
|
|
1224
|
+
* Since when (CLOCK_MONOTONIC seconds, as Process.clock_gettime gives it)
|
|
1225
|
+
* the response body on stream_id has waited on the client: some of it has
|
|
1226
|
+
* not been acknowledged, and nothing has moved since. nil while none of it
|
|
1227
|
+
* waits, for a stream without a body, and once the stream has been
|
|
1228
|
+
* aborted.
|
|
1229
|
+
*
|
|
1230
|
+
* Moving is the client acknowledging some of the body, or widening the
|
|
1231
|
+
* stream's flow control window. While that window does not hold the body
|
|
1232
|
+
* back, an acknowledgement of any response body on the connection counts
|
|
1233
|
+
* too: nghttp3 sends the responses of one priority one at a time, so a
|
|
1234
|
+
* response waiting its turn behind another the client is reading gets no
|
|
1235
|
+
* acknowledgements of its own.
|
|
1236
|
+
*/
|
|
1237
|
+
static VALUE rb_http3_session_response_stalled_since(VALUE self, VALUE rb_id) {
|
|
1238
|
+
h3_session *s = h3_session_get(self);
|
|
1239
|
+
h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
|
|
1240
|
+
if (st == NULL || h3_stream_unacked(st) == 0) {
|
|
1241
|
+
return Qnil;
|
|
1242
|
+
}
|
|
1243
|
+
double since = st->since;
|
|
1244
|
+
if (!st->fc_blocked && s->acked_at > since) {
|
|
1245
|
+
since = s->acked_at;
|
|
1246
|
+
}
|
|
1247
|
+
return DBL2NUM(since);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
922
1250
|
/*
|
|
923
1251
|
* call-seq:
|
|
924
1252
|
* session.take_closed -> Array
|
|
@@ -954,13 +1282,13 @@ static VALUE rb_http3_session_take_error(VALUE self) {
|
|
|
954
1282
|
* served.
|
|
955
1283
|
*/
|
|
956
1284
|
static VALUE rb_http3_session_shutdown_notice(VALUE self) {
|
|
957
|
-
h3_session *s =
|
|
1285
|
+
h3_session *s = h3_session_enter(self);
|
|
958
1286
|
if (s->h3 == NULL) {
|
|
959
1287
|
return self;
|
|
960
1288
|
}
|
|
961
1289
|
int rv = nghttp3_conn_submit_shutdown_notice(s->h3);
|
|
962
1290
|
if (rv != 0) {
|
|
963
|
-
|
|
1291
|
+
h3_raise(s, rv, "Failed to submit shutdown notice");
|
|
964
1292
|
}
|
|
965
1293
|
return self;
|
|
966
1294
|
}
|
|
@@ -970,13 +1298,21 @@ static VALUE rb_http3_session_shutdown_notice(VALUE self) {
|
|
|
970
1298
|
* session.abort_stream(stream_id, app_error_code) -> self
|
|
971
1299
|
*
|
|
972
1300
|
* Stop the stream in both directions with app_error_code (for instance
|
|
973
|
-
* H3_INTERNAL_ERROR after the response headers went out).
|
|
1301
|
+
* H3_INTERNAL_ERROR after the response headers went out). What of the
|
|
1302
|
+
* response body nghttp3 has not taken is let go at once, so that
|
|
1303
|
+
* #unsent_bytes no longer counts it, and a file body's descriptor is
|
|
1304
|
+
* closed; #push_chunk, #push_eof and #attach_file raise InvalidStateError
|
|
1305
|
+
* from then on.
|
|
974
1306
|
*/
|
|
975
1307
|
static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
|
|
976
1308
|
VALUE rb_code) {
|
|
977
|
-
h3_session *s =
|
|
1309
|
+
h3_session *s = h3_session_enter(self);
|
|
978
1310
|
int64_t id = NUM2LL(rb_id);
|
|
979
1311
|
uint64_t code = NUM2ULL(rb_code);
|
|
1312
|
+
h3_stream *st = h3_stream_find(s, id);
|
|
1313
|
+
if (st != NULL) {
|
|
1314
|
+
h3_stream_abort(st);
|
|
1315
|
+
}
|
|
980
1316
|
if (s->h3 != NULL) {
|
|
981
1317
|
nghttp3_conn_shutdown_stream_write(s->h3, id);
|
|
982
1318
|
}
|
|
@@ -985,6 +1321,36 @@ static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
|
|
|
985
1321
|
return self;
|
|
986
1322
|
}
|
|
987
1323
|
|
|
1324
|
+
/*
|
|
1325
|
+
* call-seq:
|
|
1326
|
+
* session.release_connection -> self (private; #release_all calls it)
|
|
1327
|
+
*
|
|
1328
|
+
* Let go of the HTTP/3 connection now, rather than when the Session is
|
|
1329
|
+
* garbage collected: the nghttp3 connection is deleted, and every response
|
|
1330
|
+
* body aborted as #abort_stream aborts one, though no stream is reset, so
|
|
1331
|
+
* that what nghttp3 had not taken of it is let go of and a file body's
|
|
1332
|
+
* descriptor closed. What nghttp3 had taken stays until the Session is
|
|
1333
|
+
* freed, even once the stream closes, since without nghttp3 nothing tells
|
|
1334
|
+
* of that: ngtcp2 may still point into it, to send it again.
|
|
1335
|
+
*
|
|
1336
|
+
* For a QUIC connection that has gone. One still driven goes on as after a
|
|
1337
|
+
* failure (h3_fail_with), with H3_NO_ERROR; without nghttp3, it sends
|
|
1338
|
+
* nothing more of a body let go of, its FIN included. A connection that
|
|
1339
|
+
* failed keeps the error it failed with. From then on #submit_response and
|
|
1340
|
+
* #stop_reading raise InvalidStateError, as on a failed connection, and
|
|
1341
|
+
* #push_chunk, #push_eof and #attach_file as on an aborted stream. Raises
|
|
1342
|
+
* InvalidStateError inside a callback of the Session, where nghttp3 may be
|
|
1343
|
+
* running. Calling it again does nothing.
|
|
1344
|
+
*/
|
|
1345
|
+
static VALUE rb_http3_session_release_connection(VALUE self) {
|
|
1346
|
+
h3_session *s = h3_session_enter(self);
|
|
1347
|
+
if (!s->failed) {
|
|
1348
|
+
h3_fail_with(s, NGHTTP3_H3_NO_ERROR);
|
|
1349
|
+
}
|
|
1350
|
+
st_foreach(s->streams, h3_abort_stream_i, 0);
|
|
1351
|
+
return self;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
988
1354
|
void Init_http3_session(VALUE rb_mHTTP3) {
|
|
989
1355
|
id_on_begin_headers = rb_intern("on_begin_headers");
|
|
990
1356
|
id_on_header = rb_intern("on_header");
|
|
@@ -992,6 +1358,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
|
|
|
992
1358
|
id_on_data = rb_intern("on_data");
|
|
993
1359
|
id_on_end_stream = rb_intern("on_end_stream");
|
|
994
1360
|
id_on_fields_too_large = rb_intern("on_fields_too_large");
|
|
1361
|
+
id_on_reset = rb_intern("on_reset");
|
|
1362
|
+
id_on_stop_sending = rb_intern("on_stop_sending");
|
|
995
1363
|
|
|
996
1364
|
rb_cHTTP3Session = rb_define_class_under(rb_mHTTP3, "Session", rb_cObject);
|
|
997
1365
|
rb_undef_alloc_func(rb_cHTTP3Session);
|
|
@@ -999,6 +1367,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
|
|
|
999
1367
|
rb_http3_session_s_attach, 2);
|
|
1000
1368
|
rb_define_method(rb_cHTTP3Session, "submit_response",
|
|
1001
1369
|
rb_http3_session_submit_response, 3);
|
|
1370
|
+
rb_define_method(rb_cHTTP3Session, "stop_reading",
|
|
1371
|
+
rb_http3_session_stop_reading, 1);
|
|
1002
1372
|
rb_define_method(rb_cHTTP3Session, "push_chunk", rb_http3_session_push_chunk,
|
|
1003
1373
|
2);
|
|
1004
1374
|
rb_define_method(rb_cHTTP3Session, "push_eof", rb_http3_session_push_eof, 1);
|
|
@@ -1006,6 +1376,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
|
|
|
1006
1376
|
rb_http3_session_attach_file, 4);
|
|
1007
1377
|
rb_define_method(rb_cHTTP3Session, "unsent_bytes",
|
|
1008
1378
|
rb_http3_session_unsent_bytes, 1);
|
|
1379
|
+
rb_define_method(rb_cHTTP3Session, "response_stalled_since",
|
|
1380
|
+
rb_http3_session_response_stalled_since, 1);
|
|
1009
1381
|
rb_define_method(rb_cHTTP3Session, "take_closed",
|
|
1010
1382
|
rb_http3_session_take_closed, 0);
|
|
1011
1383
|
rb_define_method(rb_cHTTP3Session, "take_error", rb_http3_session_take_error,
|
|
@@ -1014,4 +1386,6 @@ void Init_http3_session(VALUE rb_mHTTP3) {
|
|
|
1014
1386
|
rb_http3_session_shutdown_notice, 0);
|
|
1015
1387
|
rb_define_method(rb_cHTTP3Session, "abort_stream",
|
|
1016
1388
|
rb_http3_session_abort_stream, 2);
|
|
1389
|
+
rb_define_private_method(rb_cHTTP3Session, "release_connection",
|
|
1390
|
+
rb_http3_session_release_connection, 0);
|
|
1017
1391
|
}
|