http3 0.0.3
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 +7 -0
- data/.clang-format +1 -0
- data/.vscode/extensions.json +6 -0
- data/.vscode/settings.json +10 -0
- data/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE-DEPENDENCIES.txt +66 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +20 -0
- data/ext/http3/extconf.rb +48 -0
- data/ext/http3/nghttp3.c +347 -0
- data/ext/http3/nghttp3.h +105 -0
- data/ext/http3/nghttp3_callbacks.c +621 -0
- data/ext/http3/nghttp3_connection.c +1303 -0
- data/ext/http3/nghttp3_nv.c +87 -0
- data/ext/http3/nghttp3_qpack.c +656 -0
- data/ext/http3/nghttp3_settings.c +188 -0
- data/ext/http3/session.c +1017 -0
- data/lib/http3/rack_adapter.rb +162 -0
- data/lib/http3/request.rb +9 -0
- data/lib/http3/response.rb +21 -0
- data/lib/http3/server.rb +357 -0
- data/lib/http3/session.rb +193 -0
- data/lib/http3/version.rb +5 -0
- data/lib/http3.rb +16 -0
- data/sig/http3/nghttp3/callbacks.rbs +32 -0
- data/sig/http3/nghttp3/connection.rbs +101 -0
- data/sig/http3/nghttp3/error.rbs +48 -0
- data/sig/http3/nghttp3/info.rbs +9 -0
- data/sig/http3/nghttp3/nv.rbs +11 -0
- data/sig/http3/nghttp3/qpack.rbs +48 -0
- data/sig/http3/nghttp3/settings.rbs +27 -0
- data/sig/http3/rack_adapter.rbs +18 -0
- data/sig/http3/request.rbs +11 -0
- data/sig/http3/response.rbs +30 -0
- data/sig/http3/server.rbs +32 -0
- data/sig/http3/session.rbs +52 -0
- data/sig/http3.rbs +66 -0
- metadata +140 -0
data/ext/http3/session.c
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
#include "nghttp3.h"
|
|
2
|
+
|
|
3
|
+
#include <errno.h>
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
#include <stddef.h>
|
|
6
|
+
#include <stdlib.h>
|
|
7
|
+
#include <string.h>
|
|
8
|
+
#include <sys/random.h>
|
|
9
|
+
#include <unistd.h>
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* HTTP3::Session, the C part: the glue between the quic gem's stream handler
|
|
13
|
+
* (quic/ext.h) and an nghttp3 server connection. quic calls the handler
|
|
14
|
+
* functions below from inside QUIC::Connection#read_pkt / #write_pkt; they
|
|
15
|
+
* feed nghttp3, whose callbacks hand the parts of each request to the Ruby
|
|
16
|
+
* part (lib/http3/session.rb) and take response bodies from what Ruby
|
|
17
|
+
* queued with #push_chunk or #attach_file.
|
|
18
|
+
*
|
|
19
|
+
* Neither the handler functions nor the nghttp3 callbacks may raise. Ruby is
|
|
20
|
+
* only called under rb_protect, and an exception waits in pending_exc for
|
|
21
|
+
* #take_error. Memory a callback allocates is malloc()ed, not xmalloc()ed,
|
|
22
|
+
* since ruby_xmalloc raises NoMemoryError on failure.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* How much of a file body read_data reads at a time. */
|
|
26
|
+
#define H3_FILE_CHUNK_SIZE (16 * 1024)
|
|
27
|
+
|
|
28
|
+
VALUE rb_cHTTP3Session;
|
|
29
|
+
|
|
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;
|
|
32
|
+
|
|
33
|
+
/* What h3_session.field_sizes holds for a request whose header section has
|
|
34
|
+
* gone over max_field_section_size. */
|
|
35
|
+
#define H3_FIELD_SECTION_OVER UINT64_MAX
|
|
36
|
+
|
|
37
|
+
/* The size RFC 9114 section 4.2.2 counts a field at: its name and value
|
|
38
|
+
* plus 32 bytes. */
|
|
39
|
+
#define H3_FIELD_OVERHEAD 32
|
|
40
|
+
|
|
41
|
+
/* The largest value a QUIC variable-length integer, such as a SETTINGS
|
|
42
|
+
* value, can hold. */
|
|
43
|
+
#define H3_VARINT_MAX ((1ULL << 62) - 1)
|
|
44
|
+
|
|
45
|
+
_Static_assert(sizeof(quic_ext_vec) == sizeof(nghttp3_vec),
|
|
46
|
+
"quic_ext_vec must have the layout of nghttp3_vec");
|
|
47
|
+
_Static_assert(offsetof(quic_ext_vec, base) == offsetof(nghttp3_vec, base),
|
|
48
|
+
"quic_ext_vec.base must line up with nghttp3_vec.base");
|
|
49
|
+
_Static_assert(offsetof(quic_ext_vec, len) == offsetof(nghttp3_vec, len),
|
|
50
|
+
"quic_ext_vec.len must line up with nghttp3_vec.len");
|
|
51
|
+
|
|
52
|
+
/* A piece of a response body handed to nghttp3, kept until the peer has
|
|
53
|
+
* acknowledged it. str is the frozen String it points into, pinned by dmark;
|
|
54
|
+
* for a piece read from a file, str is Qnil and base is malloc()ed. */
|
|
55
|
+
typedef struct {
|
|
56
|
+
const uint8_t *base;
|
|
57
|
+
size_t len;
|
|
58
|
+
VALUE str;
|
|
59
|
+
} h3_chunk;
|
|
60
|
+
|
|
61
|
+
/* The body of one response. */
|
|
62
|
+
typedef struct {
|
|
63
|
+
int64_t id;
|
|
64
|
+
h3_chunk *chunks; /* in the order queued; acknowledged ones leave the front */
|
|
65
|
+
size_t nchunks, cap;
|
|
66
|
+
size_t handed; /* how many of chunks read_data has handed to nghttp3 */
|
|
67
|
+
uint64_t unsent; /* bytes queued but not handed to nghttp3 yet */
|
|
68
|
+
uint64_t acked; /* bytes the peer has acknowledged */
|
|
69
|
+
uint64_t shifted; /* bytes of the chunks already dropped */
|
|
70
|
+
int fd; /* a file body (#attach_file) not read in full yet, or -1 */
|
|
71
|
+
off_t file_off, file_end;
|
|
72
|
+
bool eof; /* nothing follows the queued chunks */
|
|
73
|
+
bool blocked; /* read_data returned NGHTTP3_ERR_WOULDBLOCK */
|
|
74
|
+
} h3_stream;
|
|
75
|
+
|
|
76
|
+
typedef struct {
|
|
77
|
+
VALUE self; /* the object wrapping this struct; dcompact follows it */
|
|
78
|
+
VALUE quic_conn; /* the QUIC::Connection::Server */
|
|
79
|
+
quic_ext_conn *qc;
|
|
80
|
+
nghttp3_conn *h3; /* made on handshake_completed */
|
|
81
|
+
st_table *streams; /* stream_id => h3_stream * */
|
|
82
|
+
VALUE pending_exc; /* an exception the Ruby part raised in a callback */
|
|
83
|
+
VALUE closed_ids; /* ids of the streams nghttp3 closed, for #take_closed */
|
|
84
|
+
/* The largest header section a request may have. nghttp3 only advertises
|
|
85
|
+
* the limit in SETTINGS; it is enforced here. */
|
|
86
|
+
uint64_t max_field_section_size;
|
|
87
|
+
/* stream_id => size of the header section received so far, while the
|
|
88
|
+
* headers of that request come in */
|
|
89
|
+
st_table *field_sizes;
|
|
90
|
+
} h3_session;
|
|
91
|
+
|
|
92
|
+
/* Streams */
|
|
93
|
+
|
|
94
|
+
static void h3_chunk_release(h3_chunk *chunk) {
|
|
95
|
+
if (NIL_P(chunk->str)) {
|
|
96
|
+
free((void *)chunk->base);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static void h3_stream_free(h3_stream *st) {
|
|
101
|
+
for (size_t i = 0; i < st->nchunks; i++) {
|
|
102
|
+
h3_chunk_release(&st->chunks[i]);
|
|
103
|
+
}
|
|
104
|
+
free(st->chunks);
|
|
105
|
+
if (st->fd >= 0) {
|
|
106
|
+
close(st->fd);
|
|
107
|
+
}
|
|
108
|
+
xfree(st);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static h3_stream *h3_stream_find(h3_session *s, int64_t id) {
|
|
112
|
+
st_data_t value;
|
|
113
|
+
return st_lookup(s->streams, (st_data_t)id, &value) ? (h3_stream *)value
|
|
114
|
+
: NULL;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Queue a chunk. Returns -1 when the list cannot grow. */
|
|
118
|
+
static int h3_stream_push(h3_stream *st, const uint8_t *base, size_t len,
|
|
119
|
+
VALUE str) {
|
|
120
|
+
if (st->nchunks == st->cap) {
|
|
121
|
+
size_t cap = st->cap ? st->cap * 2 : 8;
|
|
122
|
+
h3_chunk *chunks = realloc(st->chunks, cap * sizeof(*chunks));
|
|
123
|
+
if (chunks == NULL) {
|
|
124
|
+
return -1;
|
|
125
|
+
}
|
|
126
|
+
st->chunks = chunks;
|
|
127
|
+
st->cap = cap;
|
|
128
|
+
}
|
|
129
|
+
st->chunks[st->nchunks++] = (h3_chunk){base, len, str};
|
|
130
|
+
st->unsent += len;
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The whole file body has been read: let go of its descriptor now rather
|
|
135
|
+
* than when the stream closes, which waits for the peer to acknowledge
|
|
136
|
+
* every byte. */
|
|
137
|
+
static void h3_stream_end_file(h3_stream *st) {
|
|
138
|
+
close(st->fd);
|
|
139
|
+
st->fd = -1;
|
|
140
|
+
st->eof = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Drop the chunks at the front the peer has acknowledged to their end. A
|
|
144
|
+
* chunk acknowledged only in part stays. */
|
|
145
|
+
static void h3_stream_acked(h3_stream *st, uint64_t n) {
|
|
146
|
+
size_t drop = 0;
|
|
147
|
+
st->acked += n;
|
|
148
|
+
while (drop < st->handed && st->shifted + st->chunks[drop].len <= st->acked) {
|
|
149
|
+
st->shifted += st->chunks[drop].len;
|
|
150
|
+
h3_chunk_release(&st->chunks[drop]);
|
|
151
|
+
drop++;
|
|
152
|
+
}
|
|
153
|
+
if (drop == 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
memmove(st->chunks, st->chunks + drop,
|
|
157
|
+
(st->nchunks - drop) * sizeof(*st->chunks));
|
|
158
|
+
st->nchunks -= drop;
|
|
159
|
+
st->handed -= drop;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Calling the Ruby part */
|
|
163
|
+
|
|
164
|
+
static int h3_protect(h3_session *s, VALUE (*body)(VALUE), VALUE arg) {
|
|
165
|
+
int state = 0;
|
|
166
|
+
rb_protect(body, arg, &state);
|
|
167
|
+
if (state == 0) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (NIL_P(s->pending_exc)) {
|
|
171
|
+
s->pending_exc = rb_errinfo();
|
|
172
|
+
}
|
|
173
|
+
rb_set_errinfo(Qnil);
|
|
174
|
+
return NGHTTP3_ERR_CALLBACK_FAILURE;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
typedef struct {
|
|
178
|
+
h3_session *s;
|
|
179
|
+
ID mid;
|
|
180
|
+
int64_t id;
|
|
181
|
+
int nbytes;
|
|
182
|
+
nghttp3_vec bytes[2];
|
|
183
|
+
} h3_call;
|
|
184
|
+
|
|
185
|
+
static VALUE h3_call_body(VALUE arg) {
|
|
186
|
+
const h3_call *call = (const h3_call *)arg;
|
|
187
|
+
VALUE argv[3];
|
|
188
|
+
argv[0] = LL2NUM(call->id);
|
|
189
|
+
for (int i = 0; i < call->nbytes; i++) {
|
|
190
|
+
argv[1 + i] =
|
|
191
|
+
rb_str_new((const char *)call->bytes[i].base, call->bytes[i].len);
|
|
192
|
+
}
|
|
193
|
+
rb_funcallv(call->s->self, call->mid, 1 + call->nbytes, argv);
|
|
194
|
+
return Qnil;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* self.mid(id, *bytes as Strings) under rb_protect. */
|
|
198
|
+
static int h3_call_ruby(h3_session *s, ID mid, int64_t id, int nbytes,
|
|
199
|
+
const nghttp3_vec *bytes) {
|
|
200
|
+
h3_call call = {s, mid, id, nbytes, {{0}}};
|
|
201
|
+
for (int i = 0; i < nbytes; i++) {
|
|
202
|
+
call.bytes[i] = bytes[i];
|
|
203
|
+
}
|
|
204
|
+
return h3_protect(s, h3_call_body, (VALUE)&call);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
typedef struct {
|
|
208
|
+
h3_session *s;
|
|
209
|
+
int64_t id;
|
|
210
|
+
} h3_closed_arg;
|
|
211
|
+
|
|
212
|
+
static VALUE h3_closed_body(VALUE arg) {
|
|
213
|
+
const h3_closed_arg *a = (const h3_closed_arg *)arg;
|
|
214
|
+
st_data_t key = (st_data_t)a->id;
|
|
215
|
+
st_delete(a->s->field_sizes, &key, NULL);
|
|
216
|
+
rb_ary_push(a->s->closed_ids, LL2NUM(a->id));
|
|
217
|
+
return Qnil;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Header sections. The bookkeeping in field_sizes may allocate, so it runs
|
|
221
|
+
* under rb_protect together with the call into Ruby. */
|
|
222
|
+
|
|
223
|
+
typedef struct {
|
|
224
|
+
h3_session *s;
|
|
225
|
+
int64_t id;
|
|
226
|
+
nghttp3_vec name, value;
|
|
227
|
+
} h3_field_arg;
|
|
228
|
+
|
|
229
|
+
static VALUE h3_begin_headers_body(VALUE arg) {
|
|
230
|
+
const h3_field_arg *a = (const h3_field_arg *)arg;
|
|
231
|
+
st_insert(a->s->field_sizes, (st_data_t)a->id, 0);
|
|
232
|
+
rb_funcall(a->s->self, id_on_begin_headers, 1, LL2NUM(a->id));
|
|
233
|
+
return Qnil;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* Pass the field on, unless the section has grown past
|
|
237
|
+
* max_field_section_size: then tell Ruby once, so that it can answer 431,
|
|
238
|
+
* and drop that field and every later one. */
|
|
239
|
+
static VALUE h3_recv_header_body(VALUE arg) {
|
|
240
|
+
const h3_field_arg *a = (const h3_field_arg *)arg;
|
|
241
|
+
h3_session *s = a->s;
|
|
242
|
+
st_data_t size = 0;
|
|
243
|
+
st_lookup(s->field_sizes, (st_data_t)a->id, &size);
|
|
244
|
+
if ((uint64_t)size == H3_FIELD_SECTION_OVER) {
|
|
245
|
+
return Qnil;
|
|
246
|
+
}
|
|
247
|
+
uint64_t total =
|
|
248
|
+
(uint64_t)size + a->name.len + a->value.len + H3_FIELD_OVERHEAD;
|
|
249
|
+
if (total > s->max_field_section_size) {
|
|
250
|
+
st_insert(s->field_sizes, (st_data_t)a->id,
|
|
251
|
+
(st_data_t)H3_FIELD_SECTION_OVER);
|
|
252
|
+
rb_funcall(s->self, id_on_fields_too_large, 1, LL2NUM(a->id));
|
|
253
|
+
return Qnil;
|
|
254
|
+
}
|
|
255
|
+
st_insert(s->field_sizes, (st_data_t)a->id, (st_data_t)total);
|
|
256
|
+
rb_funcall(s->self, id_on_header, 3, LL2NUM(a->id),
|
|
257
|
+
rb_str_new((const char *)a->name.base, a->name.len),
|
|
258
|
+
rb_str_new((const char *)a->value.base, a->value.len));
|
|
259
|
+
return Qnil;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
static VALUE h3_end_headers_body(VALUE arg) {
|
|
263
|
+
const h3_field_arg *a = (const h3_field_arg *)arg;
|
|
264
|
+
st_data_t key = (st_data_t)a->id;
|
|
265
|
+
st_delete(a->s->field_sizes, &key, NULL);
|
|
266
|
+
rb_funcall(a->s->self, id_on_end_headers, 1, LL2NUM(a->id));
|
|
267
|
+
return Qnil;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* nghttp3 callbacks (nghttp3 -> glue) */
|
|
271
|
+
|
|
272
|
+
static int h3_acked_stream_data_cb(nghttp3_conn *conn, int64_t id,
|
|
273
|
+
uint64_t datalen, void *user_data,
|
|
274
|
+
void *stream_user_data) {
|
|
275
|
+
h3_stream *st = h3_stream_find(user_data, id);
|
|
276
|
+
if (st != NULL) {
|
|
277
|
+
h3_stream_acked(st, datalen);
|
|
278
|
+
}
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
static int h3_stream_close_cb(nghttp3_conn *conn, int64_t id,
|
|
283
|
+
uint64_t app_error_code, void *user_data,
|
|
284
|
+
void *stream_user_data) {
|
|
285
|
+
h3_session *s = user_data;
|
|
286
|
+
st_data_t key = (st_data_t)id;
|
|
287
|
+
st_data_t value;
|
|
288
|
+
if (st_delete(s->streams, &key, &value)) {
|
|
289
|
+
h3_stream_free((h3_stream *)value);
|
|
290
|
+
}
|
|
291
|
+
h3_closed_arg arg = {s, id};
|
|
292
|
+
return h3_protect(s, h3_closed_body, (VALUE)&arg);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* The body goes to Ruby, which takes all of it, so the peer gets the flow
|
|
296
|
+
* control credit back at once. */
|
|
297
|
+
static int h3_recv_data_cb(nghttp3_conn *conn, int64_t id, const uint8_t *data,
|
|
298
|
+
size_t datalen, void *user_data,
|
|
299
|
+
void *stream_user_data) {
|
|
300
|
+
h3_session *s = user_data;
|
|
301
|
+
nghttp3_vec bytes = {(uint8_t *)data, datalen};
|
|
302
|
+
int rv = h3_call_ruby(s, id_on_data, id, 1, &bytes);
|
|
303
|
+
if (rv != 0) {
|
|
304
|
+
return rv;
|
|
305
|
+
}
|
|
306
|
+
return http3_quic_api->consume(s->qc, id, datalen) == 0
|
|
307
|
+
? 0
|
|
308
|
+
: NGHTTP3_ERR_CALLBACK_FAILURE;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static int h3_deferred_consume_cb(nghttp3_conn *conn, int64_t id,
|
|
312
|
+
size_t consumed, void *user_data,
|
|
313
|
+
void *stream_user_data) {
|
|
314
|
+
h3_session *s = user_data;
|
|
315
|
+
return http3_quic_api->consume(s->qc, id, consumed) == 0
|
|
316
|
+
? 0
|
|
317
|
+
: NGHTTP3_ERR_CALLBACK_FAILURE;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static int h3_begin_headers_cb(nghttp3_conn *conn, int64_t id, void *user_data,
|
|
321
|
+
void *stream_user_data) {
|
|
322
|
+
h3_field_arg arg = {user_data, id};
|
|
323
|
+
return h3_protect(user_data, h3_begin_headers_body, (VALUE)&arg);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
static int h3_recv_header_cb(nghttp3_conn *conn, int64_t id, int32_t token,
|
|
327
|
+
nghttp3_rcbuf *name, nghttp3_rcbuf *value,
|
|
328
|
+
uint8_t flags, void *user_data,
|
|
329
|
+
void *stream_user_data) {
|
|
330
|
+
h3_field_arg arg = {user_data, id, nghttp3_rcbuf_get_buf(name),
|
|
331
|
+
nghttp3_rcbuf_get_buf(value)};
|
|
332
|
+
return h3_protect(user_data, h3_recv_header_body, (VALUE)&arg);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
static int h3_end_headers_cb(nghttp3_conn *conn, int64_t id, int fin,
|
|
336
|
+
void *user_data, void *stream_user_data) {
|
|
337
|
+
h3_field_arg arg = {user_data, id};
|
|
338
|
+
return h3_protect(user_data, h3_end_headers_body, (VALUE)&arg);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
static int h3_end_stream_cb(nghttp3_conn *conn, int64_t id, void *user_data,
|
|
342
|
+
void *stream_user_data) {
|
|
343
|
+
return h3_call_ruby(user_data, id_on_end_stream, id, 0, NULL);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* nghttp3 asks the QUIC layer to send STOP_SENDING, or RESET_STREAM. */
|
|
347
|
+
static int h3_stop_sending_cb(nghttp3_conn *conn, int64_t id,
|
|
348
|
+
uint64_t app_error_code, void *user_data,
|
|
349
|
+
void *stream_user_data) {
|
|
350
|
+
h3_session *s = user_data;
|
|
351
|
+
return http3_quic_api->shutdown_stream_read(s->qc, id, app_error_code) == 0
|
|
352
|
+
? 0
|
|
353
|
+
: NGHTTP3_ERR_CALLBACK_FAILURE;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
static int h3_reset_stream_cb(nghttp3_conn *conn, int64_t id,
|
|
357
|
+
uint64_t app_error_code, void *user_data,
|
|
358
|
+
void *stream_user_data) {
|
|
359
|
+
h3_session *s = user_data;
|
|
360
|
+
return http3_quic_api->shutdown_stream_write(s->qc, id, app_error_code) == 0
|
|
361
|
+
? 0
|
|
362
|
+
: NGHTTP3_ERR_CALLBACK_FAILURE;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* Unpredictable bytes for nghttp3's hardening against misbehaving peers.
|
|
366
|
+
* Should getrandom fail other than by EINTR, the rest stays as it was:
|
|
367
|
+
* nghttp3 only loses some unpredictability. */
|
|
368
|
+
static void h3_rand_cb(uint8_t *dest, size_t destlen) {
|
|
369
|
+
while (destlen > 0) {
|
|
370
|
+
ssize_t n = getrandom(dest, destlen, 0);
|
|
371
|
+
if (n < 0) {
|
|
372
|
+
if (errno == EINTR) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
dest += n;
|
|
378
|
+
destlen -= (size_t)n;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* The body reader. A file body is read 16 KiB at a time as nghttp3 asks;
|
|
383
|
+
* queued chunks are handed over as they are, without copying. */
|
|
384
|
+
static nghttp3_ssize h3_read_data_cb(nghttp3_conn *conn, int64_t id,
|
|
385
|
+
nghttp3_vec *vec, size_t veccnt,
|
|
386
|
+
uint32_t *pflags, void *user_data,
|
|
387
|
+
void *stream_user_data) {
|
|
388
|
+
h3_stream *st = h3_stream_find(user_data, id);
|
|
389
|
+
if (st == NULL) {
|
|
390
|
+
*pflags |= NGHTTP3_DATA_FLAG_EOF;
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (st->fd >= 0 && st->handed == st->nchunks && st->file_off < st->file_end) {
|
|
395
|
+
size_t want = (size_t)(st->file_end - st->file_off);
|
|
396
|
+
if (want > H3_FILE_CHUNK_SIZE) {
|
|
397
|
+
want = H3_FILE_CHUNK_SIZE;
|
|
398
|
+
}
|
|
399
|
+
uint8_t *buf = malloc(want);
|
|
400
|
+
if (buf == NULL) {
|
|
401
|
+
return NGHTTP3_ERR_NOMEM;
|
|
402
|
+
}
|
|
403
|
+
ssize_t n = pread(st->fd, buf, want, st->file_off);
|
|
404
|
+
if (n <= 0 || h3_stream_push(st, buf, (size_t)n, Qnil) != 0) {
|
|
405
|
+
/* A read error, or the file shrank under us: the response cannot be
|
|
406
|
+
* completed. */
|
|
407
|
+
free(buf);
|
|
408
|
+
return NGHTTP3_ERR_CALLBACK_FAILURE;
|
|
409
|
+
}
|
|
410
|
+
st->file_off += n;
|
|
411
|
+
if (st->file_off >= st->file_end) {
|
|
412
|
+
h3_stream_end_file(st);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
size_t n = 0;
|
|
417
|
+
while (st->handed < st->nchunks && n < veccnt) {
|
|
418
|
+
const h3_chunk *chunk = &st->chunks[st->handed];
|
|
419
|
+
vec[n].base = (uint8_t *)chunk->base;
|
|
420
|
+
vec[n].len = chunk->len;
|
|
421
|
+
st->unsent -= chunk->len;
|
|
422
|
+
st->handed++;
|
|
423
|
+
n++;
|
|
424
|
+
}
|
|
425
|
+
if (st->eof && st->handed == st->nchunks) {
|
|
426
|
+
*pflags |= NGHTTP3_DATA_FLAG_EOF;
|
|
427
|
+
}
|
|
428
|
+
if (n > 0 || st->eof) {
|
|
429
|
+
return (nghttp3_ssize)n;
|
|
430
|
+
}
|
|
431
|
+
st->blocked = true;
|
|
432
|
+
return NGHTTP3_ERR_WOULDBLOCK;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
static const nghttp3_data_reader h3_data_reader = {.read_data =
|
|
436
|
+
h3_read_data_cb};
|
|
437
|
+
|
|
438
|
+
/* Stream handler (quic -> glue). ud is the h3_session. */
|
|
439
|
+
|
|
440
|
+
/* Make the nghttp3 connection and its three unidirectional streams, as
|
|
441
|
+
* ngtcp2's examples/server.cc does once the handshake is done. */
|
|
442
|
+
static int h3_handshake_completed(void *ud) {
|
|
443
|
+
h3_session *s = ud;
|
|
444
|
+
if (s->h3 != NULL) {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
nghttp3_callbacks callbacks = {
|
|
449
|
+
.acked_stream_data = h3_acked_stream_data_cb,
|
|
450
|
+
.stream_close = h3_stream_close_cb,
|
|
451
|
+
.recv_data = h3_recv_data_cb,
|
|
452
|
+
.deferred_consume = h3_deferred_consume_cb,
|
|
453
|
+
.begin_headers = h3_begin_headers_cb,
|
|
454
|
+
.recv_header = h3_recv_header_cb,
|
|
455
|
+
.end_headers = h3_end_headers_cb,
|
|
456
|
+
.stop_sending = h3_stop_sending_cb,
|
|
457
|
+
.end_stream = h3_end_stream_cb,
|
|
458
|
+
.reset_stream = h3_reset_stream_cb,
|
|
459
|
+
.rand = h3_rand_cb,
|
|
460
|
+
};
|
|
461
|
+
nghttp3_settings settings;
|
|
462
|
+
nghttp3_settings_default(&settings);
|
|
463
|
+
settings.qpack_max_dtable_capacity = 4096;
|
|
464
|
+
settings.qpack_blocked_streams = 100;
|
|
465
|
+
settings.max_field_section_size = s->max_field_section_size;
|
|
466
|
+
|
|
467
|
+
if (nghttp3_conn_server_new(&s->h3, &callbacks, &settings, NULL, s) != 0) {
|
|
468
|
+
s->h3 = NULL;
|
|
469
|
+
return -1;
|
|
470
|
+
}
|
|
471
|
+
nghttp3_conn_set_max_client_streams_bidi(
|
|
472
|
+
s->h3, http3_quic_api->local_max_streams_bidi(s->qc));
|
|
473
|
+
|
|
474
|
+
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 ||
|
|
478
|
+
nghttp3_conn_bind_control_stream(s->h3, ctrl) != 0 ||
|
|
479
|
+
nghttp3_conn_bind_qpack_streams(s->h3, enc, dec) != 0) {
|
|
480
|
+
return -1;
|
|
481
|
+
}
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
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
|
+
static int h3_recv_stream_data(void *ud, int64_t id, const uint8_t *data,
|
|
494
|
+
size_t len, int fin, uint64_t ts,
|
|
495
|
+
size_t *consumed) {
|
|
496
|
+
h3_session *s = ud;
|
|
497
|
+
if (s->h3 == NULL) {
|
|
498
|
+
/* Nothing can arrive on a stream before the handshake; drop it. */
|
|
499
|
+
*consumed = len;
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
nghttp3_ssize rv = nghttp3_conn_read_stream2(s->h3, id, data, len, fin, ts);
|
|
503
|
+
if (rv < 0) {
|
|
504
|
+
return h3_fail(s, rv);
|
|
505
|
+
}
|
|
506
|
+
*consumed = (size_t)rv;
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
static int h3_next_stream_data(void *ud, int64_t *id, int *fin,
|
|
511
|
+
quic_ext_vec *vecs, size_t cap, size_t *nvecs) {
|
|
512
|
+
h3_session *s = ud;
|
|
513
|
+
*id = -1;
|
|
514
|
+
*fin = 0;
|
|
515
|
+
*nvecs = 0;
|
|
516
|
+
if (s->h3 == NULL) {
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
nghttp3_ssize n =
|
|
520
|
+
nghttp3_conn_writev_stream(s->h3, id, fin, (nghttp3_vec *)vecs, cap);
|
|
521
|
+
if (n < 0) {
|
|
522
|
+
return h3_fail(s, n);
|
|
523
|
+
}
|
|
524
|
+
*nvecs = (size_t)n;
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
static int h3_stream_data_written(void *ud, int64_t id, size_t n) {
|
|
529
|
+
h3_session *s = ud;
|
|
530
|
+
if (s->h3 == NULL || id < 0) {
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
int rv = nghttp3_conn_add_write_offset(s->h3, id, n);
|
|
534
|
+
return rv == 0 ? 0 : h3_fail(s, rv);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
static int h3_stream_blocked(void *ud, int64_t id) {
|
|
538
|
+
h3_session *s = ud;
|
|
539
|
+
if (s->h3 != NULL) {
|
|
540
|
+
nghttp3_conn_block_stream(s->h3, id);
|
|
541
|
+
}
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
static int h3_stream_shut_write(void *ud, int64_t id) {
|
|
546
|
+
h3_session *s = ud;
|
|
547
|
+
if (s->h3 != NULL) {
|
|
548
|
+
nghttp3_conn_shutdown_stream_write(s->h3, id);
|
|
549
|
+
}
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
static int h3_acked_stream_data(void *ud, int64_t id, uint64_t n) {
|
|
554
|
+
h3_session *s = ud;
|
|
555
|
+
if (s->h3 == NULL) {
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
int rv = nghttp3_conn_add_ack_offset(s->h3, id, n);
|
|
559
|
+
return rv == 0 ? 0 : h3_fail(s, rv);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
static int h3_extend_max_stream_data(void *ud, int64_t id) {
|
|
563
|
+
h3_session *s = ud;
|
|
564
|
+
if (s->h3 == NULL) {
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
int rv = nghttp3_conn_unblock_stream(s->h3, id);
|
|
568
|
+
return rv == 0 ? 0 : h3_fail(s, rv);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
static int h3_extend_max_remote_streams_bidi(void *ud, uint64_t max) {
|
|
572
|
+
h3_session *s = ud;
|
|
573
|
+
if (s->h3 != NULL) {
|
|
574
|
+
nghttp3_conn_set_max_client_streams_bidi(s->h3, max);
|
|
575
|
+
}
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
static int h3_shutdown_stream_read(h3_session *s, int64_t id) {
|
|
580
|
+
if (s->h3 == NULL) {
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
int rv = nghttp3_conn_shutdown_stream_read(s->h3, id);
|
|
584
|
+
return rv == 0 ? 0 : h3_fail(s, rv);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
static int h3_stream_stop_sending(void *ud, int64_t id,
|
|
588
|
+
uint64_t app_error_code) {
|
|
589
|
+
return h3_shutdown_stream_read(ud, id);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
static int h3_stream_reset(void *ud, int64_t id, uint64_t final_size,
|
|
593
|
+
uint64_t app_error_code) {
|
|
594
|
+
return h3_shutdown_stream_read(ud, id);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
static int h3_stream_close(void *ud, int64_t id, int has_code,
|
|
598
|
+
uint64_t app_error_code) {
|
|
599
|
+
h3_session *s = ud;
|
|
600
|
+
if (s->h3 == NULL) {
|
|
601
|
+
return 0;
|
|
602
|
+
}
|
|
603
|
+
int rv = nghttp3_conn_close_stream(
|
|
604
|
+
s->h3, id, has_code ? app_error_code : NGHTTP3_H3_NO_ERROR);
|
|
605
|
+
if (rv == 0 || rv == NGHTTP3_ERR_STREAM_NOT_FOUND) {
|
|
606
|
+
return 0;
|
|
607
|
+
}
|
|
608
|
+
return h3_fail(s, rv);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
static const quic_ext_stream_handler h3_handler = {
|
|
612
|
+
.size = sizeof(quic_ext_stream_handler),
|
|
613
|
+
.recv_stream_data = h3_recv_stream_data,
|
|
614
|
+
.next_stream_data = h3_next_stream_data,
|
|
615
|
+
.stream_data_written = h3_stream_data_written,
|
|
616
|
+
.stream_blocked = h3_stream_blocked,
|
|
617
|
+
.stream_shut_write = h3_stream_shut_write,
|
|
618
|
+
.acked_stream_data = h3_acked_stream_data,
|
|
619
|
+
.extend_max_stream_data = h3_extend_max_stream_data,
|
|
620
|
+
.extend_max_remote_streams_bidi = h3_extend_max_remote_streams_bidi,
|
|
621
|
+
.stream_stop_sending = h3_stream_stop_sending,
|
|
622
|
+
.stream_reset = h3_stream_reset,
|
|
623
|
+
.stream_close = h3_stream_close,
|
|
624
|
+
.handshake_completed = h3_handshake_completed,
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
/* The Ruby object */
|
|
628
|
+
|
|
629
|
+
static int h3_mark_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
|
|
630
|
+
const h3_stream *st = (const h3_stream *)value;
|
|
631
|
+
/* rb_gc_mark, not rb_gc_mark_movable: nghttp3 holds pointers into these
|
|
632
|
+
* Strings. */
|
|
633
|
+
for (size_t i = 0; i < st->nchunks; i++) {
|
|
634
|
+
rb_gc_mark(st->chunks[i].str);
|
|
635
|
+
}
|
|
636
|
+
return ST_CONTINUE;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
static void h3_session_mark(void *ptr) {
|
|
640
|
+
h3_session *s = ptr;
|
|
641
|
+
rb_gc_mark(s->quic_conn);
|
|
642
|
+
rb_gc_mark(s->pending_exc);
|
|
643
|
+
rb_gc_mark(s->closed_ids);
|
|
644
|
+
if (s->streams != NULL) {
|
|
645
|
+
st_foreach(s->streams, h3_mark_stream_i, 0);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
static int h3_free_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
|
|
650
|
+
h3_stream_free((h3_stream *)value);
|
|
651
|
+
return ST_CONTINUE;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* Never touches qc: the QUIC connection may be gone already. */
|
|
655
|
+
static void h3_session_free(void *ptr) {
|
|
656
|
+
h3_session *s = ptr;
|
|
657
|
+
if (s->h3 != NULL) {
|
|
658
|
+
nghttp3_conn_del(s->h3);
|
|
659
|
+
}
|
|
660
|
+
if (s->streams != NULL) {
|
|
661
|
+
st_foreach(s->streams, h3_free_stream_i, 0);
|
|
662
|
+
st_free_table(s->streams);
|
|
663
|
+
}
|
|
664
|
+
if (s->field_sizes != NULL) {
|
|
665
|
+
st_free_table(s->field_sizes);
|
|
666
|
+
}
|
|
667
|
+
xfree(s);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
static size_t h3_session_memsize(const void *ptr) {
|
|
671
|
+
const h3_session *s = ptr;
|
|
672
|
+
return sizeof(*s) + (s->streams != NULL ? st_memsize(s->streams) : 0);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/* self is the object wrapping this struct, so it is not marked, but
|
|
676
|
+
* GC.compact may still move it. Everything else is pinned by dmark. */
|
|
677
|
+
static void h3_session_compact(void *ptr) {
|
|
678
|
+
h3_session *s = ptr;
|
|
679
|
+
s->self = rb_gc_location(s->self);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
static const rb_data_type_t h3_session_data_type = {
|
|
683
|
+
.wrap_struct_name = "HTTP3::Session",
|
|
684
|
+
.function =
|
|
685
|
+
{
|
|
686
|
+
.dmark = h3_session_mark,
|
|
687
|
+
.dfree = h3_session_free,
|
|
688
|
+
.dsize = h3_session_memsize,
|
|
689
|
+
.dcompact = h3_session_compact,
|
|
690
|
+
},
|
|
691
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
static h3_session *h3_session_get(VALUE self) {
|
|
695
|
+
h3_session *s;
|
|
696
|
+
TypedData_Get_Struct(self, h3_session, &h3_session_data_type, s);
|
|
697
|
+
return s;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
static h3_session *h3_session_get_open(VALUE self) {
|
|
701
|
+
h3_session *s = h3_session_get(self);
|
|
702
|
+
if (s->h3 == NULL) {
|
|
703
|
+
rb_raise(rb_eNghttp3InvalidStateError,
|
|
704
|
+
"the HTTP/3 connection is not set up yet (handshake pending)");
|
|
705
|
+
}
|
|
706
|
+
return s;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
static h3_stream *h3_session_stream(VALUE self, VALUE rb_id) {
|
|
710
|
+
h3_session *s = h3_session_get(self);
|
|
711
|
+
h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
|
|
712
|
+
if (st == NULL) {
|
|
713
|
+
rb_raise(rb_eNghttp3StreamNotFoundError,
|
|
714
|
+
"no response body is open on stream %" PRIsVALUE, rb_id);
|
|
715
|
+
}
|
|
716
|
+
return st;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* Let nghttp3 ask for the body again after read_data said it would block. */
|
|
720
|
+
static void h3_stream_wake(h3_session *s, h3_stream *st) {
|
|
721
|
+
if (!st->blocked || s->h3 == NULL) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
st->blocked = false;
|
|
725
|
+
int rv = nghttp3_conn_resume_stream(s->h3, st->id);
|
|
726
|
+
if (rv != 0) {
|
|
727
|
+
nghttp3_rb_raise(rv, "Failed to resume stream");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/*
|
|
732
|
+
* call-seq:
|
|
733
|
+
* HTTP3::Session._attach(quic_conn, max_field_section_size) -> Session
|
|
734
|
+
*
|
|
735
|
+
* Take over the streams of quic_conn, a QUIC::Connection::Server no stream
|
|
736
|
+
* has been opened on yet. The nghttp3 connection is made when the
|
|
737
|
+
* handshake completes (at once if it already has). A request whose header
|
|
738
|
+
* section (counted as RFC 9114 section 4.2.2 does) exceeds
|
|
739
|
+
* max_field_section_size gets #on_fields_too_large instead of the rest of
|
|
740
|
+
* its headers.
|
|
741
|
+
*/
|
|
742
|
+
static VALUE rb_http3_session_s_attach(VALUE klass, VALUE quic_conn,
|
|
743
|
+
VALUE max_field_section_size) {
|
|
744
|
+
uint64_t max_fields = NUM2ULL(max_field_section_size);
|
|
745
|
+
if (max_fields == 0 || max_fields > H3_VARINT_MAX) {
|
|
746
|
+
rb_raise(rb_eArgError, "max_field_section_size must be 1..2**62-1");
|
|
747
|
+
}
|
|
748
|
+
quic_ext_conn *qc = http3_quic_api->conn_get(quic_conn);
|
|
749
|
+
|
|
750
|
+
h3_session *s;
|
|
751
|
+
VALUE self =
|
|
752
|
+
TypedData_Make_Struct(klass, h3_session, &h3_session_data_type, s);
|
|
753
|
+
s->self = self;
|
|
754
|
+
s->quic_conn = quic_conn;
|
|
755
|
+
s->qc = qc;
|
|
756
|
+
s->pending_exc = Qnil;
|
|
757
|
+
s->closed_ids = Qnil;
|
|
758
|
+
s->max_field_section_size = max_fields;
|
|
759
|
+
s->streams = st_init_numtable();
|
|
760
|
+
s->field_sizes = st_init_numtable();
|
|
761
|
+
s->closed_ids = rb_ary_new();
|
|
762
|
+
|
|
763
|
+
int rv = http3_quic_api->set_stream_handler(qc, &h3_handler, s, self);
|
|
764
|
+
if (rv != 0) {
|
|
765
|
+
rb_raise(rb_eNghttp3InvalidStateError,
|
|
766
|
+
"cannot attach to the QUIC connection (set_stream_handler: %d)",
|
|
767
|
+
rv);
|
|
768
|
+
}
|
|
769
|
+
if (RTEST(rb_funcall(quic_conn, rb_intern("handshake_completed?"), 0)) &&
|
|
770
|
+
h3_handshake_completed(s) != 0) {
|
|
771
|
+
rb_raise(rb_eNghttp3FatalError, "cannot set up the HTTP/3 connection");
|
|
772
|
+
}
|
|
773
|
+
return self;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/*
|
|
777
|
+
* call-seq:
|
|
778
|
+
* session.submit_response(stream_id, headers, has_body) -> self
|
|
779
|
+
*
|
|
780
|
+
* headers is an Array of [name, value] Strings, ":status" first. With
|
|
781
|
+
* has_body, the body follows through #push_chunk / #push_eof or
|
|
782
|
+
* #attach_file.
|
|
783
|
+
*/
|
|
784
|
+
static VALUE rb_http3_session_submit_response(VALUE self, VALUE rb_id,
|
|
785
|
+
VALUE headers, VALUE has_body) {
|
|
786
|
+
h3_session *s = h3_session_get_open(self);
|
|
787
|
+
int64_t id = NUM2LL(rb_id);
|
|
788
|
+
Check_Type(headers, T_ARRAY);
|
|
789
|
+
|
|
790
|
+
long nvlen = RARRAY_LEN(headers);
|
|
791
|
+
VALUE tmp;
|
|
792
|
+
nghttp3_nv *nva = ALLOCV_N(nghttp3_nv, tmp, nvlen);
|
|
793
|
+
for (long i = 0; i < nvlen; i++) {
|
|
794
|
+
VALUE pair = rb_check_array_type(RARRAY_AREF(headers, i));
|
|
795
|
+
if (NIL_P(pair) || RARRAY_LEN(pair) != 2) {
|
|
796
|
+
rb_raise(rb_eArgError, "headers must be [name, value] pairs");
|
|
797
|
+
}
|
|
798
|
+
VALUE name = RARRAY_AREF(pair, 0);
|
|
799
|
+
VALUE value = RARRAY_AREF(pair, 1);
|
|
800
|
+
StringValue(name);
|
|
801
|
+
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};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
h3_stream *st = NULL;
|
|
810
|
+
if (RTEST(has_body) && h3_stream_find(s, id) == NULL) {
|
|
811
|
+
st = ZALLOC(h3_stream);
|
|
812
|
+
st->id = id;
|
|
813
|
+
st->fd = -1;
|
|
814
|
+
st_insert(s->streams, (st_data_t)id, (st_data_t)st);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* nghttp3 copies the header names and values. */
|
|
818
|
+
int rv = nghttp3_conn_submit_response(
|
|
819
|
+
s->h3, id, nva, (size_t)nvlen, RTEST(has_body) ? &h3_data_reader : NULL);
|
|
820
|
+
ALLOCV_END(tmp);
|
|
821
|
+
RB_GC_GUARD(headers);
|
|
822
|
+
if (rv != 0) {
|
|
823
|
+
if (st != NULL) {
|
|
824
|
+
st_data_t key = (st_data_t)id;
|
|
825
|
+
st_delete(s->streams, &key, NULL);
|
|
826
|
+
h3_stream_free(st);
|
|
827
|
+
}
|
|
828
|
+
nghttp3_rb_raise(rv, "Failed to submit response");
|
|
829
|
+
}
|
|
830
|
+
return self;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/*
|
|
834
|
+
* call-seq:
|
|
835
|
+
* session.push_chunk(stream_id, str) -> self
|
|
836
|
+
*
|
|
837
|
+
* Queue a frozen copy of str as the next piece of the response body.
|
|
838
|
+
*/
|
|
839
|
+
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);
|
|
842
|
+
StringValue(str);
|
|
843
|
+
if (st->eof) {
|
|
844
|
+
rb_raise(rb_eNghttp3InvalidStateError, "the body has ended");
|
|
845
|
+
}
|
|
846
|
+
if (RSTRING_LEN(str) == 0) {
|
|
847
|
+
return self;
|
|
848
|
+
}
|
|
849
|
+
VALUE copy = rb_str_new_frozen(str);
|
|
850
|
+
if (h3_stream_push(st, (const uint8_t *)RSTRING_PTR(copy),
|
|
851
|
+
(size_t)RSTRING_LEN(copy), copy) != 0) {
|
|
852
|
+
rb_raise(rb_eNoMemError, "failed to queue a body chunk");
|
|
853
|
+
}
|
|
854
|
+
RB_GC_GUARD(copy);
|
|
855
|
+
h3_stream_wake(s, st);
|
|
856
|
+
return self;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/*
|
|
860
|
+
* call-seq:
|
|
861
|
+
* session.push_eof(stream_id) -> self
|
|
862
|
+
*
|
|
863
|
+
* Mark the end of the response body.
|
|
864
|
+
*/
|
|
865
|
+
static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
|
|
866
|
+
h3_session *s = h3_session_get(self);
|
|
867
|
+
h3_stream *st = h3_session_stream(self, rb_id);
|
|
868
|
+
st->eof = true;
|
|
869
|
+
h3_stream_wake(s, st);
|
|
870
|
+
return self;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/*
|
|
874
|
+
* call-seq:
|
|
875
|
+
* session.attach_file(stream_id, io, offset, length) -> self
|
|
876
|
+
*
|
|
877
|
+
* Send length bytes of io from offset as the whole response body. The
|
|
878
|
+
* descriptor is duplicated, so io may be closed afterwards. The duplicate
|
|
879
|
+
* is close-on-exec, as Ruby's own descriptors are, so that a process the
|
|
880
|
+
* application spawns does not inherit it, and is closed as soon as the
|
|
881
|
+
* whole length has been read.
|
|
882
|
+
*/
|
|
883
|
+
static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
|
|
884
|
+
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
|
+
}
|
|
890
|
+
off_t off = NUM2OFFT(offset);
|
|
891
|
+
off_t len = NUM2OFFT(length);
|
|
892
|
+
if (off < 0 || len < 0) {
|
|
893
|
+
rb_raise(rb_eArgError, "offset and length must not be negative");
|
|
894
|
+
}
|
|
895
|
+
int fd = rb_cloexec_dup(NUM2INT(rb_funcall(io, rb_intern("fileno"), 0)));
|
|
896
|
+
if (fd < 0) {
|
|
897
|
+
rb_sys_fail("dup");
|
|
898
|
+
}
|
|
899
|
+
rb_update_max_fd(fd);
|
|
900
|
+
st->fd = fd;
|
|
901
|
+
st->file_off = off;
|
|
902
|
+
st->file_end = off + len;
|
|
903
|
+
if (len == 0) {
|
|
904
|
+
h3_stream_end_file(st);
|
|
905
|
+
}
|
|
906
|
+
h3_stream_wake(s, st);
|
|
907
|
+
return self;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/*
|
|
911
|
+
* call-seq:
|
|
912
|
+
* session.unsent_bytes(stream_id) -> Integer
|
|
913
|
+
*
|
|
914
|
+
* Bytes queued with #push_chunk that nghttp3 has not taken yet; 0 for a
|
|
915
|
+
* stream without a body.
|
|
916
|
+
*/
|
|
917
|
+
static VALUE rb_http3_session_unsent_bytes(VALUE self, VALUE rb_id) {
|
|
918
|
+
h3_stream *st = h3_stream_find(h3_session_get(self), NUM2LL(rb_id));
|
|
919
|
+
return ULL2NUM(st == NULL ? 0 : st->unsent);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/*
|
|
923
|
+
* call-seq:
|
|
924
|
+
* session.take_closed -> Array
|
|
925
|
+
*
|
|
926
|
+
* The ids of the streams nghttp3 closed since the last call.
|
|
927
|
+
*/
|
|
928
|
+
static VALUE rb_http3_session_take_closed(VALUE self) {
|
|
929
|
+
h3_session *s = h3_session_get(self);
|
|
930
|
+
VALUE closed = s->closed_ids;
|
|
931
|
+
s->closed_ids = rb_ary_new();
|
|
932
|
+
return closed;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/*
|
|
936
|
+
* call-seq:
|
|
937
|
+
* session.take_error -> Exception or nil
|
|
938
|
+
*
|
|
939
|
+
* The exception the Ruby part raised inside a callback, if any. The
|
|
940
|
+
* connection was closed because of it.
|
|
941
|
+
*/
|
|
942
|
+
static VALUE rb_http3_session_take_error(VALUE self) {
|
|
943
|
+
h3_session *s = h3_session_get(self);
|
|
944
|
+
VALUE exc = s->pending_exc;
|
|
945
|
+
s->pending_exc = Qnil;
|
|
946
|
+
return exc;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/*
|
|
950
|
+
* call-seq:
|
|
951
|
+
* session.shutdown_notice -> self
|
|
952
|
+
*
|
|
953
|
+
* Send GOAWAY: the client may finish its requests, but no new one will be
|
|
954
|
+
* served.
|
|
955
|
+
*/
|
|
956
|
+
static VALUE rb_http3_session_shutdown_notice(VALUE self) {
|
|
957
|
+
h3_session *s = h3_session_get(self);
|
|
958
|
+
if (s->h3 == NULL) {
|
|
959
|
+
return self;
|
|
960
|
+
}
|
|
961
|
+
int rv = nghttp3_conn_submit_shutdown_notice(s->h3);
|
|
962
|
+
if (rv != 0) {
|
|
963
|
+
nghttp3_rb_raise(rv, "Failed to submit shutdown notice");
|
|
964
|
+
}
|
|
965
|
+
return self;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/*
|
|
969
|
+
* call-seq:
|
|
970
|
+
* session.abort_stream(stream_id, app_error_code) -> self
|
|
971
|
+
*
|
|
972
|
+
* Stop the stream in both directions with app_error_code (for instance
|
|
973
|
+
* H3_INTERNAL_ERROR after the response headers went out).
|
|
974
|
+
*/
|
|
975
|
+
static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
|
|
976
|
+
VALUE rb_code) {
|
|
977
|
+
h3_session *s = h3_session_get(self);
|
|
978
|
+
int64_t id = NUM2LL(rb_id);
|
|
979
|
+
uint64_t code = NUM2ULL(rb_code);
|
|
980
|
+
if (s->h3 != NULL) {
|
|
981
|
+
nghttp3_conn_shutdown_stream_write(s->h3, id);
|
|
982
|
+
}
|
|
983
|
+
http3_quic_api->shutdown_stream_write(s->qc, id, code);
|
|
984
|
+
http3_quic_api->shutdown_stream_read(s->qc, id, code);
|
|
985
|
+
return self;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
void Init_http3_session(VALUE rb_mHTTP3) {
|
|
989
|
+
id_on_begin_headers = rb_intern("on_begin_headers");
|
|
990
|
+
id_on_header = rb_intern("on_header");
|
|
991
|
+
id_on_end_headers = rb_intern("on_end_headers");
|
|
992
|
+
id_on_data = rb_intern("on_data");
|
|
993
|
+
id_on_end_stream = rb_intern("on_end_stream");
|
|
994
|
+
id_on_fields_too_large = rb_intern("on_fields_too_large");
|
|
995
|
+
|
|
996
|
+
rb_cHTTP3Session = rb_define_class_under(rb_mHTTP3, "Session", rb_cObject);
|
|
997
|
+
rb_undef_alloc_func(rb_cHTTP3Session);
|
|
998
|
+
rb_define_singleton_method(rb_cHTTP3Session, "_attach",
|
|
999
|
+
rb_http3_session_s_attach, 2);
|
|
1000
|
+
rb_define_method(rb_cHTTP3Session, "submit_response",
|
|
1001
|
+
rb_http3_session_submit_response, 3);
|
|
1002
|
+
rb_define_method(rb_cHTTP3Session, "push_chunk", rb_http3_session_push_chunk,
|
|
1003
|
+
2);
|
|
1004
|
+
rb_define_method(rb_cHTTP3Session, "push_eof", rb_http3_session_push_eof, 1);
|
|
1005
|
+
rb_define_method(rb_cHTTP3Session, "attach_file",
|
|
1006
|
+
rb_http3_session_attach_file, 4);
|
|
1007
|
+
rb_define_method(rb_cHTTP3Session, "unsent_bytes",
|
|
1008
|
+
rb_http3_session_unsent_bytes, 1);
|
|
1009
|
+
rb_define_method(rb_cHTTP3Session, "take_closed",
|
|
1010
|
+
rb_http3_session_take_closed, 0);
|
|
1011
|
+
rb_define_method(rb_cHTTP3Session, "take_error", rb_http3_session_take_error,
|
|
1012
|
+
0);
|
|
1013
|
+
rb_define_method(rb_cHTTP3Session, "shutdown_notice",
|
|
1014
|
+
rb_http3_session_shutdown_notice, 0);
|
|
1015
|
+
rb_define_method(rb_cHTTP3Session, "abort_stream",
|
|
1016
|
+
rb_http3_session_abort_stream, 2);
|
|
1017
|
+
}
|