protocol-http3 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52450e9925c14c39e98b9e9a762a4f0ef69c92a1ff6c7f7898eb8d4e1c60c1e7
4
- data.tar.gz: a18affd137347ef514d858c68df6be51f35683e82d13eb28abd0b89b05d55c0a
3
+ metadata.gz: 2026dd714a98de999332e2aa5eb59944d2f42ee40ec34f1e90ce320c124a41b0
4
+ data.tar.gz: 25bd20d83b78acd2c7d9eb00436630a6b25e67928fa609e2df5659949a2f6c1e
5
5
  SHA512:
6
- metadata.gz: 252b5eb914682b2797eab8b6b5c14e76efdb6f53ef7a2e527cc9358dbce83fd7953ffb7fe99e8d08ae6537f00ffb0d83dbf2aeb244bec372606e6cc98e47ae04
7
- data.tar.gz: 6715a25f8dda241f3cd0029bfe8aa897524c30418723b5d249f71d2e0ae4e987e6656f213a0f49ebfb91f90d516eedaa1c3c1702b2be33b410998ad8a008b2dd
6
+ metadata.gz: 10a05532e4bc989f3a4750e1d27eadf1cc745e3b3e6177b3f5cd35de598b096dc869058c62878b8ff45d526d3d4917bf1648d23ae1210b2df8b280c9a32418c7
7
+ data.tar.gz: 0ce529f6b7f18644c4b4ed655c682ca0aea12c5e482bed759f8745b922861f86da3300b162946fe42263bee4aa1bb0b7c6ed15a806cda69c709db9400e49e574
checksums.yaml.gz.sig CHANGED
Binary file
data/ext/rakefile.rb CHANGED
@@ -3,10 +3,15 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
+ require "rbconfig"
7
+ require "rubygems"
8
+
9
+ TEAPOT = [RbConfig.ruby, Gem.bin_path("teapot", "teapot")]
10
+
6
11
  task :default do
7
12
  ruby_library_directory = ENV.fetch("RUBYLIBDIR"){ENV.fetch("RUBYARCHDIR")}
8
13
  build_environment = {"RUBYLIBDIR" => ruby_library_directory}
9
14
 
10
- sh build_environment, "teapot", "fetch"
11
- sh build_environment, "teapot", "scheduler-ruby-library", "Ruby/Protocol/HTTP3"
15
+ sh build_environment, *TEAPOT, "fetch"
16
+ sh build_environment, *TEAPOT, "scheduler-ruby-library", "Ruby/Protocol/HTTP3"
12
17
  end
@@ -9,7 +9,7 @@ build-cmake:
9
9
  :commit: 6558fdad5bbb475f0fa5584ff773de8f12ff232e
10
10
  :branch: master
11
11
  protocol-http3:
12
- :commit: d9e1c8bc9cb301d430fb48c3aec3b938a6446277
12
+ :commit: b7d3d4d1b1d6f6c089d2ec9986631bcfe2c373cf
13
13
  :branch: main
14
14
  ruby:
15
15
  :commit: 685cdd5a421dae7749f368d08add9aa9709ad043
@@ -48,7 +48,7 @@ executor-lldb:
48
48
  :commit: 193ba0b15c532d2745b0f0bfb05cd9581e869555
49
49
  :branch: master
50
50
  protocol-quic:
51
- :commit: 1328f66a91ba7b3e544304dde8ba7bc93d3462ec
51
+ :commit: 5d069ac54f446f24dc95003543a2acaeef807e70
52
52
  :branch: main
53
53
  scheduler:
54
54
  :commit: 4d05a1631ac749106d268bda07425bf6ce0476dc
@@ -80,3 +80,12 @@ coroutine-arm64:
80
80
  scheduler-ruby:
81
81
  :commit: 94e524e64912081aaa5a8cb54bad7ad73f01a51a
82
82
  :branch: main
83
+ platform-linux:
84
+ :commit: c3c5cbf90d26d23c6137def781cd997ddca5e51a
85
+ :branch: master
86
+ build-linux:
87
+ :commit: cbc8c52c2ca2fc75761e545c83429ef34e4cd4fe
88
+ :branch: master
89
+ coroutine-amd64:
90
+ :commit: 27aee56aa1414094584e7b2d8b74e289ecad4c86
91
+ :branch: master
@@ -1,17 +1,15 @@
1
1
  #include "Client.hpp"
2
+ #include "Stream.hpp"
2
3
 
3
4
  #include "../QUIC/Bindings.hpp"
4
5
 
5
- #include <Protocol/HTTP3/Stream.hpp>
6
-
7
6
  #include <array>
8
- #include <unordered_map>
7
+ #include <stdexcept>
9
8
  #include <vector>
10
9
 
11
10
  VALUE Ruby_Protocol_HTTP3_Client = Qnil;
12
11
 
13
12
  namespace Ruby::Protocol::HTTP3 {
14
-
15
13
  class Client final : public ::Protocol::QUIC::Client, public ::Protocol::HTTP3::Session {
16
14
  public:
17
15
  VALUE self;
@@ -21,7 +19,6 @@ namespace Ruby::Protocol::HTTP3 {
21
19
  VALUE _tls_context;
22
20
  VALUE _socket;
23
21
  VALUE _remote_address;
24
- std::unordered_map<::Protocol::QUIC::StreamID, VALUE> _streams;
25
22
 
26
23
  public:
27
24
  Client(VALUE self, VALUE configuration, VALUE tls_context, VALUE socket, VALUE remote_address, VALUE chosen_version) :
@@ -53,7 +50,19 @@ namespace Ruby::Protocol::HTTP3 {
53
50
 
54
51
  ::Protocol::QUIC::Stream * create_stream(::Protocol::QUIC::StreamID stream_id) override
55
52
  {
56
- return new ::Protocol::HTTP3::Stream(*this, *this, stream_id);
53
+ return new Stream(*this, *this, stream_id);
54
+ }
55
+
56
+ Stream * stream_for(::Protocol::QUIC::StreamID stream_id)
57
+ {
58
+ auto stream = reinterpret_cast<::Protocol::QUIC::Stream *>(ngtcp2_conn_get_stream_user_data(::Protocol::QUIC::Client::native_handle(), stream_id));
59
+ auto ruby_stream = dynamic_cast<Stream *>(stream);
60
+
61
+ if (!ruby_stream) {
62
+ throw std::runtime_error("Could not find HTTP/3 stream.");
63
+ }
64
+
65
+ return ruby_stream;
57
66
  }
58
67
 
59
68
  ::Protocol::QUIC::Connection::Status send_stream_data() override
@@ -65,7 +74,14 @@ namespace Ruby::Protocol::HTTP3 {
65
74
  ::Protocol::QUIC::StreamID stream_id = -1;
66
75
  bool is_final = false;
67
76
 
68
- auto vector_count = write_stream_data(stream_id, is_final, http_vectors.data(), http_vectors.size());
77
+ nghttp3_ssize vector_count = 0;
78
+
79
+ try {
80
+ vector_count = write_stream_data(stream_id, is_final, http_vectors.data(), http_vectors.size());
81
+ } catch (const std::system_error &) {
82
+ close_pending_streams();
83
+ return Status(NGTCP2_ERR_CALLBACK_FAILURE);
84
+ }
69
85
 
70
86
  if (stream_id < 0) {
71
87
  break;
@@ -98,12 +114,16 @@ namespace Ruby::Protocol::HTTP3 {
98
114
  return Status(result);
99
115
  }
100
116
 
101
- add_write_offset(stream_id, static_cast<std::size_t>(written_length));
102
-
103
117
  if (result > 0) {
104
118
  send_packet(path_storage.path, packet_info, packet.data(), result);
105
119
  }
106
120
 
121
+ if (written_length < 0) {
122
+ break;
123
+ }
124
+
125
+ add_write_offset(stream_id, static_cast<std::size_t>(written_length));
126
+
107
127
  if (result == 0 && written_length == 0) {
108
128
  break;
109
129
  }
@@ -112,6 +132,10 @@ namespace Ruby::Protocol::HTTP3 {
112
132
  return Status::OK;
113
133
  }
114
134
 
135
+ void close_pending_streams()
136
+ {
137
+ }
138
+
115
139
  void header_received(::Protocol::QUIC::StreamID stream_id, std::int32_t token, nghttp3_rcbuf *name, nghttp3_rcbuf *value, std::uint8_t flags, void *stream_data) override
116
140
  {
117
141
  (void)token;
@@ -144,6 +168,31 @@ namespace Ruby::Protocol::HTTP3 {
144
168
  }
145
169
  }
146
170
 
171
+ void stream_data_received(::Protocol::QUIC::StreamID stream_id, const std::uint8_t *data, std::size_t size, void *stream_data) override
172
+ {
173
+ (void)stream_data;
174
+
175
+ auto stream = stream_for(stream_id);
176
+ stream->receive_input(data, size);
177
+
178
+ if (rb_respond_to(self, rb_intern("data_received"))) {
179
+ rb_funcall(self, rb_intern("data_received"), 2, RB_LL2NUM(stream_id), stream->shift_input());
180
+ }
181
+ }
182
+
183
+ void stream_data_acknowledged(::Protocol::QUIC::StreamID stream_id, std::uint64_t size, void *stream_data) override
184
+ {
185
+ (void)stream_data;
186
+
187
+ stream_for(stream_id)->acknowledge_output(size);
188
+ }
189
+
190
+ void stream_closed(::Protocol::QUIC::StreamID stream_id, std::uint64_t error_code, void *stream_data) override
191
+ {
192
+ (void)error_code;
193
+ (void)stream_data;
194
+ }
195
+
147
196
  void settings_received(const nghttp3_proto_settings *settings) override
148
197
  {
149
198
  (void)settings;
@@ -157,6 +206,8 @@ namespace Ruby::Protocol::HTTP3 {
157
206
  {
158
207
  (void)stream_data;
159
208
 
209
+ stream_for(stream_id)->finish_input();
210
+
160
211
  if (rb_respond_to(self, rb_intern("stream_finished"))) {
161
212
  rb_funcall(self, rb_intern("stream_finished"), 1, RB_LL2NUM(stream_id));
162
213
  }
@@ -165,7 +216,6 @@ namespace Ruby::Protocol::HTTP3 {
165
216
  void disconnect() override
166
217
  {
167
218
  ::Protocol::QUIC::Client::disconnect();
168
- _streams.clear();
169
219
  }
170
220
 
171
221
  void mark()
@@ -176,9 +226,12 @@ namespace Ruby::Protocol::HTTP3 {
176
226
  rb_gc_mark_movable(_socket);
177
227
  rb_gc_mark_movable(_remote_address);
178
228
 
179
- for (auto & [stream_id, stream] : _streams) {
229
+ for (auto & [stream_id, stream] : ::Protocol::QUIC::Connection::_streams) {
180
230
  (void)stream_id;
181
- rb_gc_mark_movable(stream);
231
+
232
+ if (auto ruby_stream = dynamic_cast<Stream *>(stream)) {
233
+ ruby_stream->mark();
234
+ }
182
235
  }
183
236
  }
184
237
 
@@ -190,11 +243,57 @@ namespace Ruby::Protocol::HTTP3 {
190
243
  _socket = rb_gc_location(_socket);
191
244
  _remote_address = rb_gc_location(_remote_address);
192
245
 
193
- for (auto & [stream_id, stream] : _streams) {
246
+ for (auto & [stream_id, stream] : ::Protocol::QUIC::Connection::_streams) {
194
247
  (void)stream_id;
195
- stream = rb_gc_location(stream);
248
+
249
+ if (auto ruby_stream = dynamic_cast<Stream *>(stream)) {
250
+ ruby_stream->compact();
251
+ }
252
+ }
253
+ }
254
+
255
+ void submit_request_with_body(::Protocol::QUIC::StreamID stream_id, const nghttp3_nv *headers, std::size_t count, VALUE body)
256
+ {
257
+ auto stream = stream_for(stream_id);
258
+
259
+ if (NIL_P(body)) {
260
+ submit_request(stream_id, headers, count, nullptr, stream);
261
+ } else {
262
+ submit_request(stream_id, headers, count, stream->reader(), stream);
196
263
  }
197
264
  }
265
+
266
+ void append_body(::Protocol::QUIC::StreamID stream_id, VALUE chunk)
267
+ {
268
+ auto stream = stream_for(stream_id);
269
+ auto was_empty = !stream->output_pending();
270
+
271
+ stream->append_output(chunk);
272
+
273
+ if (was_empty) {
274
+ resume_stream(stream_id);
275
+ }
276
+ }
277
+
278
+ void finish_body(::Protocol::QUIC::StreamID stream_id)
279
+ {
280
+ auto stream = stream_for(stream_id);
281
+ auto was_empty = !stream->output_pending();
282
+
283
+ stream->finish_output();
284
+
285
+ if (was_empty) {
286
+ resume_stream(stream_id);
287
+ }
288
+ }
289
+
290
+ void reset_body(::Protocol::QUIC::StreamID stream_id, std::uint64_t error_code)
291
+ {
292
+ stream_for(stream_id)->reset_output();
293
+ shutdown_stream_write(stream_id);
294
+ close_stream(stream_id, error_code);
295
+ ngtcp2_conn_shutdown_stream_write(::Protocol::QUIC::Client::native_handle(), 0, stream_id, error_code);
296
+ }
198
297
  };
199
298
 
200
299
  }
@@ -307,6 +406,7 @@ static VALUE Ruby_Protocol_HTTP3_Client_receive(VALUE self, VALUE ruby_socket)
307
406
  switch (status) {
308
407
  case Protocol::QUIC::Connection::Status::OK:
309
408
  client->send_packets();
409
+
310
410
  return Qtrue;
311
411
  case Protocol::QUIC::Connection::Status::CLOSING:
312
412
  case Protocol::QUIC::Connection::Status::DRAINING:
@@ -316,8 +416,13 @@ static VALUE Ruby_Protocol_HTTP3_Client_receive(VALUE self, VALUE ruby_socket)
316
416
  }
317
417
  }
318
418
 
319
- static VALUE Ruby_Protocol_HTTP3_Client_submit_request(VALUE self, VALUE headers)
419
+ static VALUE Ruby_Protocol_HTTP3_Client_submit_request(int argc, VALUE *argv, VALUE self)
320
420
  {
421
+ VALUE headers;
422
+ VALUE body = Qnil;
423
+
424
+ rb_scan_args(argc, argv, "11", &headers, &body);
425
+
321
426
  auto client = Ruby_Protocol_HTTP3_Client_native_get(self);
322
427
  auto stream = client->open_bidirectional_stream();
323
428
  auto stream_id = stream->stream_id();
@@ -342,12 +447,51 @@ static VALUE Ruby_Protocol_HTTP3_Client_submit_request(VALUE self, VALUE headers
342
447
  });
343
448
  }
344
449
 
345
- client->submit_request(stream_id, native_headers.data(), native_headers.size());
450
+ client->submit_request_with_body(stream_id, native_headers.data(), native_headers.size(), body);
346
451
  client->send_packets();
347
452
 
348
453
  return RB_LL2NUM(stream_id);
349
454
  }
350
455
 
456
+ static VALUE Ruby_Protocol_HTTP3_Client_write_body_chunk(VALUE self, VALUE stream_id, VALUE chunk)
457
+ {
458
+ auto client = Ruby_Protocol_HTTP3_Client_native_get(self);
459
+
460
+ client->append_body(RB_NUM2LL(stream_id), chunk);
461
+
462
+ return Qnil;
463
+ }
464
+
465
+ static VALUE Ruby_Protocol_HTTP3_Client_read_body_chunk(VALUE self, VALUE stream_id)
466
+ {
467
+ auto client = Ruby_Protocol_HTTP3_Client_native_get(self);
468
+
469
+ return client->stream_for(RB_NUM2LL(stream_id))->shift_input();
470
+ }
471
+
472
+ static VALUE Ruby_Protocol_HTTP3_Client_finish_body(VALUE self, VALUE stream_id)
473
+ {
474
+ auto client = Ruby_Protocol_HTTP3_Client_native_get(self);
475
+
476
+ client->finish_body(RB_NUM2LL(stream_id));
477
+
478
+ return Qnil;
479
+ }
480
+
481
+ static VALUE Ruby_Protocol_HTTP3_Client_reset_body(int argc, VALUE *argv, VALUE self)
482
+ {
483
+ VALUE stream_id;
484
+ VALUE error_code;
485
+ rb_scan_args(argc, argv, "11", &stream_id, &error_code);
486
+
487
+ auto client = Ruby_Protocol_HTTP3_Client_native_get(self);
488
+ auto native_error_code = NIL_P(error_code) ? NGHTTP3_H3_INTERNAL_ERROR : RB_NUM2ULL(error_code);
489
+
490
+ client->reset_body(RB_NUM2LL(stream_id), native_error_code);
491
+
492
+ return Qnil;
493
+ }
494
+
351
495
  void Init_Ruby_Protocol_HTTP3_Client(VALUE Protocol_HTTP3)
352
496
  {
353
497
  Ruby_Protocol_HTTP3_Client = rb_define_class_under(Protocol_HTTP3, "Client", rb_cObject);
@@ -359,5 +503,9 @@ void Init_Ruby_Protocol_HTTP3_Client(VALUE Protocol_HTTP3)
359
503
  rb_define_method(Ruby_Protocol_HTTP3_Client, "close", Ruby_Protocol_HTTP3_Client_close, 0);
360
504
  rb_define_method(Ruby_Protocol_HTTP3_Client, "send_packets", Ruby_Protocol_HTTP3_Client_send_packets, 0);
361
505
  rb_define_method(Ruby_Protocol_HTTP3_Client, "receive", Ruby_Protocol_HTTP3_Client_receive, 1);
362
- rb_define_method(Ruby_Protocol_HTTP3_Client, "submit_request", Ruby_Protocol_HTTP3_Client_submit_request, 1);
506
+ rb_define_method(Ruby_Protocol_HTTP3_Client, "submit_request", RUBY_METHOD_FUNC(Ruby_Protocol_HTTP3_Client_submit_request), -1);
507
+ rb_define_method(Ruby_Protocol_HTTP3_Client, "write_body_chunk", Ruby_Protocol_HTTP3_Client_write_body_chunk, 2);
508
+ rb_define_method(Ruby_Protocol_HTTP3_Client, "read_body_chunk", Ruby_Protocol_HTTP3_Client_read_body_chunk, 1);
509
+ rb_define_method(Ruby_Protocol_HTTP3_Client, "finish_body", Ruby_Protocol_HTTP3_Client_finish_body, 1);
510
+ rb_define_method(Ruby_Protocol_HTTP3_Client, "reset_body", RUBY_METHOD_FUNC(Ruby_Protocol_HTTP3_Client_reset_body), -1);
363
511
  }
@@ -1,16 +1,17 @@
1
1
  #include "Server.hpp"
2
2
 
3
3
  #include "Dispatcher.hpp"
4
+ #include "Stream.hpp"
4
5
 
5
6
  #include "../QUIC/Bindings.hpp"
6
7
 
7
- #include <unordered_map>
8
+ #include <array>
9
+ #include <stdexcept>
8
10
  #include <vector>
9
11
 
10
12
  VALUE Ruby_Protocol_HTTP3_Server = Qnil;
11
13
 
12
14
  namespace Ruby::Protocol::HTTP3 {
13
-
14
15
  class Server : public ::Protocol::HTTP3::Server {
15
16
  public:
16
17
  VALUE self;
@@ -21,7 +22,6 @@ namespace Ruby::Protocol::HTTP3 {
21
22
  VALUE _tls_context;
22
23
  VALUE _socket;
23
24
  VALUE _remote_address;
24
- std::unordered_map<::Protocol::QUIC::StreamID, VALUE> _streams;
25
25
 
26
26
  public:
27
27
  Server(VALUE self, VALUE dispatcher, VALUE configuration, VALUE tls_context, VALUE socket, VALUE remote_address, VALUE packet_header, VALUE original_connection_id) :
@@ -40,6 +40,93 @@ namespace Ruby::Protocol::HTTP3 {
40
40
  {
41
41
  }
42
42
 
43
+ ::Protocol::QUIC::Stream * create_stream(::Protocol::QUIC::StreamID stream_id) override
44
+ {
45
+ return new Stream(*this, *this, stream_id);
46
+ }
47
+
48
+ Stream * stream_for(::Protocol::QUIC::StreamID stream_id)
49
+ {
50
+ auto stream = reinterpret_cast<::Protocol::QUIC::Stream *>(ngtcp2_conn_get_stream_user_data(::Protocol::QUIC::Server::native_handle(), stream_id));
51
+ auto ruby_stream = dynamic_cast<Stream *>(stream);
52
+
53
+ if (!ruby_stream) {
54
+ throw std::runtime_error("Could not find HTTP/3 stream.");
55
+ }
56
+
57
+ return ruby_stream;
58
+ }
59
+
60
+ ::Protocol::QUIC::Connection::Status send_stream_data() override
61
+ {
62
+ std::array<::Protocol::QUIC::Byte, 1024*64> packet;
63
+ std::array<nghttp3_vec, 16> http_vectors;
64
+
65
+ while (true) {
66
+ ::Protocol::QUIC::StreamID stream_id = -1;
67
+ bool is_final = false;
68
+ nghttp3_ssize vector_count = 0;
69
+
70
+ try {
71
+ vector_count = write_stream_data(stream_id, is_final, http_vectors.data(), http_vectors.size());
72
+ } catch (const std::system_error &) {
73
+ close_pending_streams();
74
+ return Status(NGTCP2_ERR_CALLBACK_FAILURE);
75
+ }
76
+
77
+ if (stream_id < 0) {
78
+ break;
79
+ }
80
+
81
+ std::vector<ngtcp2_vec> stream_vectors;
82
+ stream_vectors.reserve(static_cast<std::size_t>(vector_count));
83
+
84
+ for (nghttp3_ssize index = 0; index < vector_count; ++index) {
85
+ stream_vectors.push_back(ngtcp2_vec{
86
+ .base = http_vectors[index].base,
87
+ .len = http_vectors[index].len,
88
+ });
89
+ }
90
+
91
+ ngtcp2_path_storage path_storage;
92
+ ngtcp2_path_storage_zero(&path_storage);
93
+ ngtcp2_pkt_info packet_info;
94
+ ngtcp2_ssize written_length = 0;
95
+ ::Protocol::QUIC::StreamDataFlags flags = is_final ? NGTCP2_WRITE_STREAM_FLAG_FIN : 0;
96
+
97
+ auto result = ngtcp2_conn_writev_stream(::Protocol::QUIC::Server::native_handle(), &path_storage.path, &packet_info, packet.data(), packet.size(), &written_length, flags, stream_id, stream_vectors.data(), stream_vectors.size(), ::Protocol::QUIC::timestamp());
98
+
99
+ if (result == NGTCP2_ERR_STREAM_DATA_BLOCKED || result == NGTCP2_ERR_STREAM_SHUT_WR) {
100
+ block_stream(stream_id);
101
+ return Status(result);
102
+ }
103
+
104
+ if (result < 0) {
105
+ return Status(result);
106
+ }
107
+
108
+ if (result > 0) {
109
+ send_packet(path_storage.path, packet_info, packet.data(), result);
110
+ }
111
+
112
+ if (written_length < 0) {
113
+ break;
114
+ }
115
+
116
+ add_write_offset(stream_id, static_cast<std::size_t>(written_length));
117
+
118
+ if (result == 0 && written_length == 0) {
119
+ break;
120
+ }
121
+ }
122
+
123
+ return Status::OK;
124
+ }
125
+
126
+ void close_pending_streams()
127
+ {
128
+ }
129
+
43
130
  void header_received(::Protocol::QUIC::StreamID stream_id, std::int32_t token, nghttp3_rcbuf *name, nghttp3_rcbuf *value, std::uint8_t flags, void *stream_data) override
44
131
  {
45
132
  (void)token;
@@ -72,6 +159,26 @@ namespace Ruby::Protocol::HTTP3 {
72
159
  }
73
160
  }
74
161
 
162
+ void stream_data_received(::Protocol::QUIC::StreamID stream_id, const std::uint8_t *data, std::size_t size, void *stream_data) override
163
+ {
164
+ ::Protocol::HTTP3::Server::stream_data_received(stream_id, data, size, stream_data);
165
+
166
+ if (rb_respond_to(self, rb_intern("data_received"))) {
167
+ rb_funcall(self, rb_intern("data_received"), 2, RB_LL2NUM(stream_id), stream_for(stream_id)->shift_input());
168
+ }
169
+ }
170
+
171
+ void stream_data_acknowledged(::Protocol::QUIC::StreamID stream_id, std::uint64_t size, void *stream_data) override
172
+ {
173
+ ::Protocol::HTTP3::Server::stream_data_acknowledged(stream_id, size, stream_data);
174
+ }
175
+
176
+ void stream_closed(::Protocol::QUIC::StreamID stream_id, std::uint64_t error_code, void *stream_data) override
177
+ {
178
+ (void)error_code;
179
+ (void)stream_data;
180
+ }
181
+
75
182
  void settings_received(const nghttp3_proto_settings *settings) override
76
183
  {
77
184
  (void)settings;
@@ -83,7 +190,7 @@ namespace Ruby::Protocol::HTTP3 {
83
190
 
84
191
  void stream_finished(::Protocol::QUIC::StreamID stream_id, void *stream_data) override
85
192
  {
86
- (void)stream_data;
193
+ ::Protocol::HTTP3::Server::stream_finished(stream_id, stream_data);
87
194
 
88
195
  if (rb_respond_to(self, rb_intern("stream_finished"))) {
89
196
  rb_funcall(self, rb_intern("stream_finished"), 1, RB_LL2NUM(stream_id));
@@ -93,7 +200,6 @@ namespace Ruby::Protocol::HTTP3 {
93
200
  void disconnect() override
94
201
  {
95
202
  ::Protocol::HTTP3::Server::disconnect();
96
- _streams.clear();
97
203
  }
98
204
 
99
205
  void mark()
@@ -105,9 +211,12 @@ namespace Ruby::Protocol::HTTP3 {
105
211
  rb_gc_mark_movable(_socket);
106
212
  rb_gc_mark_movable(_remote_address);
107
213
 
108
- for (auto & [stream_id, stream] : _streams) {
214
+ for (auto & [stream_id, stream] : ::Protocol::QUIC::Connection::_streams) {
109
215
  (void)stream_id;
110
- rb_gc_mark_movable(stream);
216
+
217
+ if (auto ruby_stream = dynamic_cast<Stream *>(stream)) {
218
+ ruby_stream->mark();
219
+ }
111
220
  }
112
221
  }
113
222
 
@@ -120,11 +229,56 @@ namespace Ruby::Protocol::HTTP3 {
120
229
  _socket = rb_gc_location(_socket);
121
230
  _remote_address = rb_gc_location(_remote_address);
122
231
 
123
- for (auto & [stream_id, stream] : _streams) {
232
+ for (auto & [stream_id, stream] : ::Protocol::QUIC::Connection::_streams) {
124
233
  (void)stream_id;
125
- stream = rb_gc_location(stream);
234
+
235
+ if (auto ruby_stream = dynamic_cast<Stream *>(stream)) {
236
+ ruby_stream->compact();
237
+ }
238
+ }
239
+ }
240
+
241
+ void submit_response_with_body(::Protocol::QUIC::StreamID stream_id, const nghttp3_nv *headers, std::size_t count, VALUE body)
242
+ {
243
+ if (NIL_P(body)) {
244
+ submit_response(stream_id, headers, count);
245
+ } else {
246
+ auto stream = stream_for(stream_id);
247
+ submit_response(stream_id, headers, count, stream->reader(), stream);
126
248
  }
127
249
  }
250
+
251
+ void append_body(::Protocol::QUIC::StreamID stream_id, VALUE chunk)
252
+ {
253
+ auto stream = stream_for(stream_id);
254
+ auto was_empty = !stream->output_pending();
255
+
256
+ stream->append_output(chunk);
257
+
258
+ if (was_empty) {
259
+ resume_stream(stream_id);
260
+ }
261
+ }
262
+
263
+ void finish_body(::Protocol::QUIC::StreamID stream_id)
264
+ {
265
+ auto stream = stream_for(stream_id);
266
+ auto was_empty = !stream->output_pending();
267
+
268
+ stream->finish_output();
269
+
270
+ if (was_empty) {
271
+ resume_stream(stream_id);
272
+ }
273
+ }
274
+
275
+ void reset_body(::Protocol::QUIC::StreamID stream_id, std::uint64_t error_code)
276
+ {
277
+ stream_for(stream_id)->reset_output();
278
+ shutdown_stream_write(stream_id);
279
+ close_stream(stream_id, error_code);
280
+ ngtcp2_conn_shutdown_stream_write(::Protocol::QUIC::Server::native_handle(), 0, stream_id, error_code);
281
+ }
128
282
  };
129
283
 
130
284
  }
@@ -197,8 +351,14 @@ static VALUE Ruby_Protocol_HTTP3_Server_send_packets(VALUE self)
197
351
  return Qnil;
198
352
  }
199
353
 
200
- static VALUE Ruby_Protocol_HTTP3_Server_submit_response(VALUE self, VALUE stream_id, VALUE headers)
354
+ static VALUE Ruby_Protocol_HTTP3_Server_submit_response(int argc, VALUE *argv, VALUE self)
201
355
  {
356
+ VALUE stream_id;
357
+ VALUE headers;
358
+ VALUE body = Qnil;
359
+
360
+ rb_scan_args(argc, argv, "21", &stream_id, &headers, &body);
361
+
202
362
  auto server = Ruby_Protocol_HTTP3_Server_get(self);
203
363
  auto count = RARRAY_LEN(headers);
204
364
  std::vector<nghttp3_nv> native_headers;
@@ -221,12 +381,74 @@ static VALUE Ruby_Protocol_HTTP3_Server_submit_response(VALUE self, VALUE stream
221
381
  });
222
382
  }
223
383
 
224
- server->submit_response(RB_NUM2LL(stream_id), native_headers.data(), native_headers.size());
384
+ auto ruby_server = dynamic_cast<Ruby::Protocol::HTTP3::Server *>(server);
385
+
386
+ if (!ruby_server) {
387
+ rb_raise(rb_eRuntimeError, "Could not get HTTP/3 server.");
388
+ }
389
+
390
+ ruby_server->submit_response_with_body(RB_NUM2LL(stream_id), native_headers.data(), native_headers.size(), body);
225
391
  server->send_packets();
226
392
 
227
393
  return Qnil;
228
394
  }
229
395
 
396
+ static VALUE Ruby_Protocol_HTTP3_Server_write_body_chunk(VALUE self, VALUE stream_id, VALUE chunk)
397
+ {
398
+ auto server = dynamic_cast<Ruby::Protocol::HTTP3::Server *>(Ruby_Protocol_HTTP3_Server_get(self));
399
+
400
+ if (!server) {
401
+ rb_raise(rb_eRuntimeError, "Could not get HTTP/3 server.");
402
+ }
403
+
404
+ server->append_body(RB_NUM2LL(stream_id), chunk);
405
+
406
+ return Qnil;
407
+ }
408
+
409
+ static VALUE Ruby_Protocol_HTTP3_Server_read_body_chunk(VALUE self, VALUE stream_id)
410
+ {
411
+ auto server = dynamic_cast<Ruby::Protocol::HTTP3::Server *>(Ruby_Protocol_HTTP3_Server_get(self));
412
+
413
+ if (!server) {
414
+ rb_raise(rb_eRuntimeError, "Could not get HTTP/3 server.");
415
+ }
416
+
417
+ return server->stream_for(RB_NUM2LL(stream_id))->shift_input();
418
+ }
419
+
420
+ static VALUE Ruby_Protocol_HTTP3_Server_finish_body(VALUE self, VALUE stream_id)
421
+ {
422
+ auto server = dynamic_cast<Ruby::Protocol::HTTP3::Server *>(Ruby_Protocol_HTTP3_Server_get(self));
423
+
424
+ if (!server) {
425
+ rb_raise(rb_eRuntimeError, "Could not get HTTP/3 server.");
426
+ }
427
+
428
+ server->finish_body(RB_NUM2LL(stream_id));
429
+
430
+ return Qnil;
431
+ }
432
+
433
+ static VALUE Ruby_Protocol_HTTP3_Server_reset_body(int argc, VALUE *argv, VALUE self)
434
+ {
435
+ VALUE stream_id;
436
+ VALUE error_code;
437
+ rb_scan_args(argc, argv, "11", &stream_id, &error_code);
438
+
439
+ auto server = dynamic_cast<Ruby::Protocol::HTTP3::Server *>(Ruby_Protocol_HTTP3_Server_get(self));
440
+
441
+ if (!server) {
442
+ rb_raise(rb_eRuntimeError, "Could not get HTTP/3 server.");
443
+ }
444
+
445
+ auto native_error_code = NIL_P(error_code) ? NGHTTP3_H3_INTERNAL_ERROR : RB_NUM2ULL(error_code);
446
+
447
+ server->reset_body(RB_NUM2LL(stream_id), native_error_code);
448
+
449
+ return Qnil;
450
+ }
451
+
230
452
  void Init_Ruby_Protocol_HTTP3_Server(VALUE Protocol_HTTP3)
231
453
  {
232
454
  Ruby_Protocol_HTTP3_Server = rb_define_class_under(Protocol_HTTP3, "Server", rb_cObject);
@@ -235,5 +457,9 @@ void Init_Ruby_Protocol_HTTP3_Server(VALUE Protocol_HTTP3)
235
457
  rb_define_method(Ruby_Protocol_HTTP3_Server, "initialize", Ruby_Protocol_HTTP3_Server_initialize, 7);
236
458
 
237
459
  rb_define_method(Ruby_Protocol_HTTP3_Server, "send_packets", Ruby_Protocol_HTTP3_Server_send_packets, 0);
238
- rb_define_method(Ruby_Protocol_HTTP3_Server, "submit_response", Ruby_Protocol_HTTP3_Server_submit_response, 2);
460
+ rb_define_method(Ruby_Protocol_HTTP3_Server, "submit_response", RUBY_METHOD_FUNC(Ruby_Protocol_HTTP3_Server_submit_response), -1);
461
+ rb_define_method(Ruby_Protocol_HTTP3_Server, "write_body_chunk", Ruby_Protocol_HTTP3_Server_write_body_chunk, 2);
462
+ rb_define_method(Ruby_Protocol_HTTP3_Server, "read_body_chunk", Ruby_Protocol_HTTP3_Server_read_body_chunk, 1);
463
+ rb_define_method(Ruby_Protocol_HTTP3_Server, "finish_body", Ruby_Protocol_HTTP3_Server_finish_body, 1);
464
+ rb_define_method(Ruby_Protocol_HTTP3_Server, "reset_body", RUBY_METHOD_FUNC(Ruby_Protocol_HTTP3_Server_reset_body), -1);
239
465
  }
@@ -0,0 +1,155 @@
1
+ #include "Stream.hpp"
2
+
3
+ #include <stdexcept>
4
+
5
+ namespace Ruby::Protocol::HTTP3 {
6
+ Stream::Stream(::Protocol::HTTP3::Session & session, ::Protocol::QUIC::Connection & connection, ::Protocol::QUIC::StreamID stream_id) :
7
+ ::Protocol::HTTP3::Stream(session, connection, stream_id),
8
+ _reader{read_data}
9
+ {
10
+ }
11
+
12
+ Stream::~Stream()
13
+ {
14
+ }
15
+
16
+ void Stream::receive_input(const void *data, std::size_t size)
17
+ {
18
+ if (_input_finished) {
19
+ throw std::runtime_error("Cannot receive input after stream input has finished.");
20
+ }
21
+
22
+ _input_chunks.push_back(rb_str_new(static_cast<const char *>(data), size));
23
+ }
24
+
25
+ void Stream::finish_input()
26
+ {
27
+ _input_finished = true;
28
+ }
29
+
30
+ void Stream::acknowledge_output(std::size_t size)
31
+ {
32
+ _acknowledged_output += size;
33
+
34
+ while (!_retained_output_chunks.empty() && _acknowledged_output >= static_cast<std::size_t>(RSTRING_LEN(_retained_output_chunks.front()))) {
35
+ _acknowledged_output -= static_cast<std::size_t>(RSTRING_LEN(_retained_output_chunks.front()));
36
+ _retained_output_chunks.pop_front();
37
+ }
38
+ }
39
+
40
+ bool Stream::output_pending() const noexcept
41
+ {
42
+ return !_output_chunks.empty();
43
+ }
44
+
45
+ void Stream::append_output(VALUE chunk)
46
+ {
47
+ if (_output_finished) {
48
+ throw std::runtime_error("Cannot append output after stream output has finished.");
49
+ }
50
+
51
+ chunk = rb_str_to_str(chunk);
52
+ _output_chunks.push_back(rb_str_dup_frozen(chunk));
53
+ }
54
+
55
+ void Stream::finish_output()
56
+ {
57
+ _output_finished = true;
58
+ }
59
+
60
+ void Stream::reset_output()
61
+ {
62
+ _output_finished = true;
63
+ _output_chunks.clear();
64
+ clear_retained_output();
65
+ }
66
+
67
+ VALUE Stream::shift_input()
68
+ {
69
+ if (_input_chunks.empty()) {
70
+ return Qnil;
71
+ }
72
+
73
+ auto chunk = _input_chunks.front();
74
+ _input_chunks.pop_front();
75
+
76
+ return chunk;
77
+ }
78
+
79
+ void Stream::mark()
80
+ {
81
+ for (auto chunk : _input_chunks) {
82
+ rb_gc_mark_movable(chunk);
83
+ }
84
+
85
+ for (auto chunk : _output_chunks) {
86
+ rb_gc_mark_movable(chunk);
87
+ }
88
+
89
+ for (auto chunk : _retained_output_chunks) {
90
+ rb_gc_mark_movable(chunk);
91
+ }
92
+ }
93
+
94
+ void Stream::compact()
95
+ {
96
+ for (auto & chunk : _input_chunks) {
97
+ chunk = rb_gc_location(chunk);
98
+ }
99
+
100
+ for (auto & chunk : _output_chunks) {
101
+ chunk = rb_gc_location(chunk);
102
+ }
103
+
104
+ for (auto & chunk : _retained_output_chunks) {
105
+ chunk = rb_gc_location(chunk);
106
+ }
107
+ }
108
+
109
+ nghttp3_ssize Stream::read_data(nghttp3_conn *connection, std::int64_t stream_id, nghttp3_vec *vectors, std::size_t vector_count, std::uint32_t *flags, void *connection_data, void *stream_data)
110
+ {
111
+ (void)connection;
112
+ (void)stream_id;
113
+ (void)connection_data;
114
+
115
+ if (vector_count == 0) {
116
+ return 0;
117
+ }
118
+
119
+ auto stream = reinterpret_cast<Stream *>(stream_data);
120
+
121
+ if (!stream) {
122
+ *flags |= NGHTTP3_DATA_FLAG_EOF;
123
+ return 0;
124
+ }
125
+
126
+ if (stream->_output_chunks.empty()) {
127
+ if (!stream->_output_finished) {
128
+ return NGHTTP3_ERR_WOULDBLOCK;
129
+ }
130
+
131
+ *flags |= NGHTTP3_DATA_FLAG_EOF;
132
+ return 0;
133
+ }
134
+
135
+ stream->_retained_output_chunks.push_back(stream->_output_chunks.front());
136
+ stream->_output_chunks.pop_front();
137
+
138
+ auto chunk = stream->_retained_output_chunks.back();
139
+
140
+ vectors[0].base = reinterpret_cast<std::uint8_t *>(RSTRING_PTR(chunk));
141
+ vectors[0].len = RSTRING_LEN(chunk);
142
+
143
+ if (stream->_output_finished && stream->_output_chunks.empty()) {
144
+ *flags |= NGHTTP3_DATA_FLAG_EOF;
145
+ }
146
+
147
+ return 1;
148
+ }
149
+
150
+ void Stream::clear_retained_output()
151
+ {
152
+ _retained_output_chunks.clear();
153
+ _acknowledged_output = 0;
154
+ }
155
+ }
@@ -0,0 +1,47 @@
1
+ #pragma once
2
+
3
+ #include <ruby.h>
4
+
5
+ #include <Protocol/HTTP3/Stream.hpp>
6
+
7
+ #include <deque>
8
+
9
+ namespace Ruby::Protocol::HTTP3 {
10
+ class Stream : public ::Protocol::HTTP3::Stream {
11
+ public:
12
+ Stream(::Protocol::HTTP3::Session & session, ::Protocol::QUIC::Connection & connection, ::Protocol::QUIC::StreamID stream_id);
13
+ virtual ~Stream();
14
+
15
+ nghttp3_data_reader * reader() noexcept {return &_reader;}
16
+
17
+ void receive_input(const void *data, std::size_t size) override;
18
+ void finish_input() override;
19
+
20
+ void acknowledge_output(std::size_t size) override;
21
+
22
+ bool output_pending() const noexcept;
23
+ void append_output(VALUE chunk);
24
+ void finish_output();
25
+ void reset_output();
26
+
27
+ VALUE shift_input();
28
+
29
+ void mark();
30
+ void compact();
31
+
32
+ static nghttp3_ssize read_data(nghttp3_conn *connection, std::int64_t stream_id, nghttp3_vec *vectors, std::size_t vector_count, std::uint32_t *flags, void *connection_data, void *stream_data);
33
+
34
+ private:
35
+ nghttp3_data_reader _reader;
36
+
37
+ std::deque<VALUE> _input_chunks;
38
+ std::deque<VALUE> _output_chunks;
39
+ std::deque<VALUE> _retained_output_chunks;
40
+
41
+ std::size_t _acknowledged_output = 0;
42
+ bool _input_finished = false;
43
+ bool _output_finished = false;
44
+
45
+ void clear_retained_output();
46
+ };
47
+ }
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Protocol::HTTP3
7
+ # A single HTTP/3 request or response body stream.
8
+ class Stream
9
+ # Initialize the stream wrapper for the given connection and native stream identifier.
10
+ def initialize(connection, stream_id)
11
+ @connection = connection
12
+ @stream_id = stream_id
13
+ end
14
+
15
+ attr :connection
16
+ attr :stream_id
17
+
18
+ # Read the next available body chunk from the stream.
19
+ def read_chunk
20
+ @connection.__send__(:read_body_chunk, @stream_id)
21
+ end
22
+
23
+ # Write a body chunk to the stream.
24
+ def write_chunk(chunk)
25
+ @connection.__send__(:write_body_chunk, @stream_id, chunk)
26
+ @connection.send_packets
27
+ end
28
+
29
+ # Finish the stream after all body chunks have been written.
30
+ def finish
31
+ @connection.__send__(:finish_body, @stream_id)
32
+ @connection.send_packets
33
+ end
34
+
35
+ # Reset the stream with the optional HTTP/3 error code.
36
+ def reset(error_code = nil)
37
+ @connection.__send__(:reset_body, @stream_id, error_code)
38
+ @connection.send_packets
39
+ end
40
+
41
+ # Write all chunks from the given body to the stream.
42
+ def write_body(body)
43
+ error = nil
44
+
45
+ begin
46
+ while chunk = body.read
47
+ write_chunk(chunk)
48
+ end
49
+ rescue => error
50
+ reset
51
+ raise
52
+ ensure
53
+ body.close(error)
54
+ finish unless error
55
+ end
56
+ end
57
+ end
58
+ end
@@ -7,6 +7,6 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module HTTP3
10
- VERSION = "0.0.1"
10
+ VERSION = "0.0.2"
11
11
  end
12
12
  end
@@ -9,3 +9,39 @@ require_relative "http3/version"
9
9
 
10
10
  # Native extension:
11
11
  require "Ruby_Protocol_HTTP3"
12
+
13
+ require_relative "http3/stream"
14
+
15
+ module Protocol::HTTP3
16
+ # An HTTP/3 client connection.
17
+ class Client
18
+ alias native_submit_request submit_request
19
+
20
+ # Submit a request and return the stream used for the response body.
21
+ def submit_request(headers, body = nil)
22
+ Stream.new(self, native_submit_request(headers, body))
23
+ end
24
+
25
+ private :read_body_chunk
26
+ private :write_body_chunk
27
+ private :finish_body
28
+ private :reset_body
29
+ end
30
+
31
+ # An HTTP/3 server connection.
32
+ class Server
33
+ alias native_submit_response submit_response
34
+
35
+ # Submit a response for the given stream and return the stream used for the response body.
36
+ def submit_response(stream_id, headers, body = nil)
37
+ Stream.new(self, stream_id).tap do
38
+ native_submit_response(stream_id, headers, body)
39
+ end
40
+ end
41
+
42
+ private :read_body_chunk
43
+ private :write_body_chunk
44
+ private :finish_body
45
+ private :reset_body
46
+ end
47
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -38,20 +38,34 @@ cert_chain:
38
38
  -----END CERTIFICATE-----
39
39
  date: 1980-01-02 00:00:00.000000000 Z
40
40
  dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: protocol-http
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.62'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.62'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: protocol-quic
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: 0.0.5
61
+ version: 0.0.9
48
62
  type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: 0.0.5
68
+ version: 0.0.9
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: teapot
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -95,9 +109,12 @@ files:
95
109
  - ext/source/Ruby/Protocol/HTTP3/Dispatcher.hpp
96
110
  - ext/source/Ruby/Protocol/HTTP3/Server.cpp
97
111
  - ext/source/Ruby/Protocol/HTTP3/Server.hpp
112
+ - ext/source/Ruby/Protocol/HTTP3/Stream.cpp
113
+ - ext/source/Ruby/Protocol/HTTP3/Stream.hpp
98
114
  - ext/source/Ruby/Protocol/QUIC/Bindings.hpp
99
115
  - ext/teapot.rb
100
116
  - lib/protocol/http3.rb
117
+ - lib/protocol/http3/stream.rb
101
118
  - lib/protocol/http3/version.rb
102
119
  - license.md
103
120
  - readme.md
metadata.gz.sig CHANGED
Binary file