nghttp3 0.0.1
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 +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +20 -0
- data/ext/nghttp3/extconf.rb +18 -0
- data/ext/nghttp3/nghttp3.c +300 -0
- data/ext/nghttp3/nghttp3.h +65 -0
- data/ext/nghttp3/nghttp3_callbacks.c +713 -0
- data/ext/nghttp3/nghttp3_connection.c +1070 -0
- data/ext/nghttp3/nghttp3_nv.c +87 -0
- data/ext/nghttp3/nghttp3_qpack.c +680 -0
- data/ext/nghttp3/nghttp3_settings.c +188 -0
- data/lib/nghttp3/client.rb +236 -0
- data/lib/nghttp3/headers.rb +113 -0
- data/lib/nghttp3/request.rb +147 -0
- data/lib/nghttp3/response.rb +126 -0
- data/lib/nghttp3/server.rb +253 -0
- data/lib/nghttp3/stream_manager.rb +116 -0
- data/lib/nghttp3/version.rb +5 -0
- data/lib/nghttp3.rb +16 -0
- data/sig/nghttp3/callbacks.rbs +30 -0
- data/sig/nghttp3/client.rbs +38 -0
- data/sig/nghttp3/connection.rbs +85 -0
- data/sig/nghttp3/error.rbs +46 -0
- data/sig/nghttp3/headers.rbs +37 -0
- data/sig/nghttp3/info.rbs +7 -0
- data/sig/nghttp3/nv.rbs +9 -0
- data/sig/nghttp3/qpack.rbs +46 -0
- data/sig/nghttp3/request.rbs +35 -0
- data/sig/nghttp3/response.rbs +34 -0
- data/sig/nghttp3/server.rbs +33 -0
- data/sig/nghttp3/settings.rbs +25 -0
- data/sig/nghttp3/stream_manager.rbs +26 -0
- data/sig/nghttp3.rbs +64 -0
- metadata +83 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#include "nghttp3.h"
|
|
2
|
+
|
|
3
|
+
VALUE rb_cNghttp3Settings;
|
|
4
|
+
|
|
5
|
+
typedef struct {
|
|
6
|
+
nghttp3_settings settings;
|
|
7
|
+
} SettingsObj;
|
|
8
|
+
|
|
9
|
+
static void settings_free(void *ptr) { xfree(ptr); }
|
|
10
|
+
|
|
11
|
+
static size_t settings_memsize(const void *ptr) { return sizeof(SettingsObj); }
|
|
12
|
+
|
|
13
|
+
const rb_data_type_t settings_data_type = {
|
|
14
|
+
.wrap_struct_name = "nghttp3_settings_rb",
|
|
15
|
+
.function =
|
|
16
|
+
{
|
|
17
|
+
.dmark = NULL,
|
|
18
|
+
.dfree = settings_free,
|
|
19
|
+
.dsize = settings_memsize,
|
|
20
|
+
},
|
|
21
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static VALUE settings_alloc(VALUE klass) {
|
|
25
|
+
SettingsObj *obj;
|
|
26
|
+
VALUE self =
|
|
27
|
+
TypedData_Make_Struct(klass, SettingsObj, &settings_data_type, obj);
|
|
28
|
+
memset(&obj->settings, 0, sizeof(nghttp3_settings));
|
|
29
|
+
return self;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Returns a pointer to the underlying nghttp3_settings structure.
|
|
34
|
+
*/
|
|
35
|
+
nghttp3_settings *nghttp3_rb_get_settings(VALUE rb_settings) {
|
|
36
|
+
SettingsObj *obj;
|
|
37
|
+
TypedData_Get_Struct(rb_settings, SettingsObj, &settings_data_type, obj);
|
|
38
|
+
return &obj->settings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* call-seq:
|
|
43
|
+
* Settings.new -> Settings
|
|
44
|
+
*
|
|
45
|
+
* Creates a new Settings object with all values set to zero.
|
|
46
|
+
*/
|
|
47
|
+
static VALUE rb_nghttp3_settings_initialize(VALUE self) { return self; }
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* call-seq:
|
|
51
|
+
* Settings.default -> Settings
|
|
52
|
+
*
|
|
53
|
+
* Creates a new Settings object with default values from nghttp3.
|
|
54
|
+
*/
|
|
55
|
+
static VALUE rb_nghttp3_settings_default(VALUE klass) {
|
|
56
|
+
VALUE self = settings_alloc(klass);
|
|
57
|
+
SettingsObj *obj;
|
|
58
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
59
|
+
nghttp3_settings_default(&obj->settings);
|
|
60
|
+
return self;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Settings getters */
|
|
64
|
+
static VALUE rb_nghttp3_settings_get_max_field_section_size(VALUE self) {
|
|
65
|
+
SettingsObj *obj;
|
|
66
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
67
|
+
return ULL2NUM(obj->settings.max_field_section_size);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static VALUE rb_nghttp3_settings_get_qpack_max_dtable_capacity(VALUE self) {
|
|
71
|
+
SettingsObj *obj;
|
|
72
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
73
|
+
return SIZET2NUM(obj->settings.qpack_max_dtable_capacity);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static VALUE
|
|
77
|
+
rb_nghttp3_settings_get_qpack_encoder_max_dtable_capacity(VALUE self) {
|
|
78
|
+
SettingsObj *obj;
|
|
79
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
80
|
+
return SIZET2NUM(obj->settings.qpack_encoder_max_dtable_capacity);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static VALUE rb_nghttp3_settings_get_qpack_blocked_streams(VALUE self) {
|
|
84
|
+
SettingsObj *obj;
|
|
85
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
86
|
+
return SIZET2NUM(obj->settings.qpack_blocked_streams);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static VALUE rb_nghttp3_settings_get_enable_connect_protocol(VALUE self) {
|
|
90
|
+
SettingsObj *obj;
|
|
91
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
92
|
+
return obj->settings.enable_connect_protocol ? Qtrue : Qfalse;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static VALUE rb_nghttp3_settings_get_h3_datagram(VALUE self) {
|
|
96
|
+
SettingsObj *obj;
|
|
97
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
98
|
+
return obj->settings.h3_datagram ? Qtrue : Qfalse;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Settings setters */
|
|
102
|
+
static VALUE rb_nghttp3_settings_set_max_field_section_size(VALUE self,
|
|
103
|
+
VALUE val) {
|
|
104
|
+
SettingsObj *obj;
|
|
105
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
106
|
+
obj->settings.max_field_section_size = NUM2ULL(val);
|
|
107
|
+
return val;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static VALUE rb_nghttp3_settings_set_qpack_max_dtable_capacity(VALUE self,
|
|
111
|
+
VALUE val) {
|
|
112
|
+
SettingsObj *obj;
|
|
113
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
114
|
+
obj->settings.qpack_max_dtable_capacity = NUM2SIZET(val);
|
|
115
|
+
return val;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static VALUE
|
|
119
|
+
rb_nghttp3_settings_set_qpack_encoder_max_dtable_capacity(VALUE self,
|
|
120
|
+
VALUE val) {
|
|
121
|
+
SettingsObj *obj;
|
|
122
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
123
|
+
obj->settings.qpack_encoder_max_dtable_capacity = NUM2SIZET(val);
|
|
124
|
+
return val;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static VALUE rb_nghttp3_settings_set_qpack_blocked_streams(VALUE self,
|
|
128
|
+
VALUE val) {
|
|
129
|
+
SettingsObj *obj;
|
|
130
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
131
|
+
obj->settings.qpack_blocked_streams = NUM2SIZET(val);
|
|
132
|
+
return val;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static VALUE rb_nghttp3_settings_set_enable_connect_protocol(VALUE self,
|
|
136
|
+
VALUE val) {
|
|
137
|
+
SettingsObj *obj;
|
|
138
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
139
|
+
obj->settings.enable_connect_protocol = RTEST(val) ? 1 : 0;
|
|
140
|
+
return val;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static VALUE rb_nghttp3_settings_set_h3_datagram(VALUE self, VALUE val) {
|
|
144
|
+
SettingsObj *obj;
|
|
145
|
+
TypedData_Get_Struct(self, SettingsObj, &settings_data_type, obj);
|
|
146
|
+
obj->settings.h3_datagram = RTEST(val) ? 1 : 0;
|
|
147
|
+
return val;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void Init_nghttp3_settings(void) {
|
|
151
|
+
rb_cNghttp3Settings =
|
|
152
|
+
rb_define_class_under(rb_mNghttp3, "Settings", rb_cObject);
|
|
153
|
+
rb_define_alloc_func(rb_cNghttp3Settings, settings_alloc);
|
|
154
|
+
rb_define_method(rb_cNghttp3Settings, "initialize",
|
|
155
|
+
rb_nghttp3_settings_initialize, 0);
|
|
156
|
+
rb_define_singleton_method(rb_cNghttp3Settings, "default",
|
|
157
|
+
rb_nghttp3_settings_default, 0);
|
|
158
|
+
|
|
159
|
+
/* Settings getters */
|
|
160
|
+
rb_define_method(rb_cNghttp3Settings, "max_field_section_size",
|
|
161
|
+
rb_nghttp3_settings_get_max_field_section_size, 0);
|
|
162
|
+
rb_define_method(rb_cNghttp3Settings, "qpack_max_dtable_capacity",
|
|
163
|
+
rb_nghttp3_settings_get_qpack_max_dtable_capacity, 0);
|
|
164
|
+
rb_define_method(rb_cNghttp3Settings, "qpack_encoder_max_dtable_capacity",
|
|
165
|
+
rb_nghttp3_settings_get_qpack_encoder_max_dtable_capacity,
|
|
166
|
+
0);
|
|
167
|
+
rb_define_method(rb_cNghttp3Settings, "qpack_blocked_streams",
|
|
168
|
+
rb_nghttp3_settings_get_qpack_blocked_streams, 0);
|
|
169
|
+
rb_define_method(rb_cNghttp3Settings, "enable_connect_protocol",
|
|
170
|
+
rb_nghttp3_settings_get_enable_connect_protocol, 0);
|
|
171
|
+
rb_define_method(rb_cNghttp3Settings, "h3_datagram",
|
|
172
|
+
rb_nghttp3_settings_get_h3_datagram, 0);
|
|
173
|
+
|
|
174
|
+
/* Settings setters */
|
|
175
|
+
rb_define_method(rb_cNghttp3Settings, "max_field_section_size=",
|
|
176
|
+
rb_nghttp3_settings_set_max_field_section_size, 1);
|
|
177
|
+
rb_define_method(rb_cNghttp3Settings, "qpack_max_dtable_capacity=",
|
|
178
|
+
rb_nghttp3_settings_set_qpack_max_dtable_capacity, 1);
|
|
179
|
+
rb_define_method(rb_cNghttp3Settings, "qpack_encoder_max_dtable_capacity=",
|
|
180
|
+
rb_nghttp3_settings_set_qpack_encoder_max_dtable_capacity,
|
|
181
|
+
1);
|
|
182
|
+
rb_define_method(rb_cNghttp3Settings, "qpack_blocked_streams=",
|
|
183
|
+
rb_nghttp3_settings_set_qpack_blocked_streams, 1);
|
|
184
|
+
rb_define_method(rb_cNghttp3Settings, "enable_connect_protocol=",
|
|
185
|
+
rb_nghttp3_settings_set_enable_connect_protocol, 1);
|
|
186
|
+
rb_define_method(rb_cNghttp3Settings,
|
|
187
|
+
"h3_datagram=", rb_nghttp3_settings_set_h3_datagram, 1);
|
|
188
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nghttp3
|
|
4
|
+
# High-level HTTP/3 client
|
|
5
|
+
#
|
|
6
|
+
# Wraps the low-level Connection API with automatic stream management,
|
|
7
|
+
# convenient request methods, and response tracking.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# client = Nghttp3::Client.new
|
|
11
|
+
# client.bind_streams(control: 2, qpack_encoder: 6, qpack_decoder: 10)
|
|
12
|
+
#
|
|
13
|
+
# stream_id = client.get("https://example.com/path")
|
|
14
|
+
#
|
|
15
|
+
# # Pump data to QUIC layer
|
|
16
|
+
# client.pump_writes do |stream_id, data, fin|
|
|
17
|
+
# quic.write(stream_id, data, fin)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # Feed data from QUIC layer
|
|
21
|
+
# client.read_stream(stream_id, received_data, fin: false)
|
|
22
|
+
#
|
|
23
|
+
# # Get response when ready
|
|
24
|
+
# response = client.responses[stream_id]
|
|
25
|
+
class Client
|
|
26
|
+
# @return [Connection] the underlying low-level connection
|
|
27
|
+
attr_reader :connection
|
|
28
|
+
|
|
29
|
+
# @return [Settings] the settings used for this client
|
|
30
|
+
attr_reader :settings
|
|
31
|
+
|
|
32
|
+
# @return [Hash{Integer => Response}] responses by stream ID
|
|
33
|
+
attr_reader :responses
|
|
34
|
+
|
|
35
|
+
# @return [Hash{Integer => Request}] pending requests by stream ID
|
|
36
|
+
attr_reader :pending_requests
|
|
37
|
+
|
|
38
|
+
# Create a new HTTP/3 client
|
|
39
|
+
# @param settings [Settings, nil] settings to use (defaults to Settings.default)
|
|
40
|
+
def initialize(settings: nil)
|
|
41
|
+
@settings = settings || Settings.default
|
|
42
|
+
@callbacks = setup_callbacks
|
|
43
|
+
@connection = Connection.client_new(@settings, @callbacks)
|
|
44
|
+
@stream_manager = StreamManager.new(is_server: false)
|
|
45
|
+
@pending_requests = {}
|
|
46
|
+
@responses = {}
|
|
47
|
+
@streams_bound = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Bind control and QPACK streams
|
|
51
|
+
#
|
|
52
|
+
# Must be called before submitting requests. The stream IDs should be
|
|
53
|
+
# unidirectional streams opened by the QUIC layer.
|
|
54
|
+
#
|
|
55
|
+
# @param control [Integer] control stream ID
|
|
56
|
+
# @param qpack_encoder [Integer] QPACK encoder stream ID
|
|
57
|
+
# @param qpack_decoder [Integer] QPACK decoder stream ID
|
|
58
|
+
# @return [self]
|
|
59
|
+
def bind_streams(control:, qpack_encoder:, qpack_decoder:)
|
|
60
|
+
@connection.bind_control_stream(control)
|
|
61
|
+
@connection.bind_qpack_streams(qpack_encoder, qpack_decoder)
|
|
62
|
+
@stream_manager.register_stream(control, type: :uni)
|
|
63
|
+
@stream_manager.register_stream(qpack_encoder, type: :uni)
|
|
64
|
+
@stream_manager.register_stream(qpack_decoder, type: :uni)
|
|
65
|
+
@streams_bound = true
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if streams are bound
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def streams_bound?
|
|
72
|
+
@streams_bound
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Submit a request
|
|
76
|
+
# @param request [Request] the request to submit
|
|
77
|
+
# @return [Integer] the stream ID for this request
|
|
78
|
+
def submit(request)
|
|
79
|
+
raise InvalidStateError, "Streams not bound. Call bind_streams first." unless @streams_bound
|
|
80
|
+
|
|
81
|
+
stream_id = @stream_manager.allocate_bidi_stream_id
|
|
82
|
+
@pending_requests[stream_id] = request
|
|
83
|
+
@responses[stream_id] = Response.new(stream_id: stream_id)
|
|
84
|
+
|
|
85
|
+
@connection.submit_request(stream_id, request.to_nv_array, body: request.body)
|
|
86
|
+
stream_id
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Convenience method for GET request
|
|
90
|
+
# @param url [String] URL to request
|
|
91
|
+
# @param headers [Hash] additional headers
|
|
92
|
+
# @return [Integer] stream ID
|
|
93
|
+
def get(url, headers: {})
|
|
94
|
+
submit(Request.get(url, headers: headers))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Convenience method for POST request
|
|
98
|
+
# @param url [String] URL to request
|
|
99
|
+
# @param body [String, nil] request body
|
|
100
|
+
# @param headers [Hash] additional headers
|
|
101
|
+
# @return [Integer] stream ID
|
|
102
|
+
def post(url, body: nil, headers: {})
|
|
103
|
+
submit(Request.post(url, body: body, headers: headers))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Convenience method for PUT request
|
|
107
|
+
# @param url [String] URL to request
|
|
108
|
+
# @param body [String, nil] request body
|
|
109
|
+
# @param headers [Hash] additional headers
|
|
110
|
+
# @return [Integer] stream ID
|
|
111
|
+
def put(url, body: nil, headers: {})
|
|
112
|
+
submit(Request.put(url, body: body, headers: headers))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Convenience method for DELETE request
|
|
116
|
+
# @param url [String] URL to request
|
|
117
|
+
# @param headers [Hash] additional headers
|
|
118
|
+
# @return [Integer] stream ID
|
|
119
|
+
def delete(url, headers: {})
|
|
120
|
+
submit(Request.delete(url, headers: headers))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Convenience method for HEAD request
|
|
124
|
+
# @param url [String] URL to request
|
|
125
|
+
# @param headers [Hash] additional headers
|
|
126
|
+
# @return [Integer] stream ID
|
|
127
|
+
def head(url, headers: {})
|
|
128
|
+
submit(Request.head(url, headers: headers))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Pump pending writes to the QUIC layer
|
|
132
|
+
#
|
|
133
|
+
# @yield [stream_id, data, fin] for each pending write
|
|
134
|
+
# @yieldparam stream_id [Integer] the stream ID
|
|
135
|
+
# @yieldparam data [String] the data to write
|
|
136
|
+
# @yieldparam fin [Boolean] true if this is the final data for the stream
|
|
137
|
+
# @yieldreturn [Integer] number of bytes accepted by QUIC layer
|
|
138
|
+
# @return [self]
|
|
139
|
+
def pump_writes
|
|
140
|
+
while (result = @connection.writev_stream)
|
|
141
|
+
stream_id = result[:stream_id]
|
|
142
|
+
data = result[:data]
|
|
143
|
+
fin = result[:fin]
|
|
144
|
+
|
|
145
|
+
bytes_written = yield(stream_id, data, fin) if block_given?
|
|
146
|
+
bytes_written ||= data.bytesize
|
|
147
|
+
|
|
148
|
+
@connection.add_write_offset(stream_id, bytes_written)
|
|
149
|
+
end
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Read data from QUIC layer into HTTP/3 connection
|
|
154
|
+
# @param stream_id [Integer] stream ID
|
|
155
|
+
# @param data [String] received data
|
|
156
|
+
# @param fin [Boolean] true if this is the final data for the stream
|
|
157
|
+
# @return [Integer] number of bytes consumed
|
|
158
|
+
def read_stream(stream_id, data, fin: false)
|
|
159
|
+
@connection.read_stream(stream_id, data, fin: fin)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Notify that bytes have been acknowledged by the peer
|
|
163
|
+
# @param stream_id [Integer] stream ID
|
|
164
|
+
# @param n [Integer] number of bytes acknowledged
|
|
165
|
+
# @return [self]
|
|
166
|
+
def add_ack_offset(stream_id, n)
|
|
167
|
+
@connection.add_ack_offset(stream_id, n)
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Close the client connection
|
|
172
|
+
# @return [nil]
|
|
173
|
+
def close
|
|
174
|
+
@connection.close
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Check if the connection is closed
|
|
178
|
+
# @return [Boolean]
|
|
179
|
+
def closed?
|
|
180
|
+
@connection.closed?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def setup_callbacks
|
|
186
|
+
Callbacks.new
|
|
187
|
+
.on_begin_headers { |stream_id| on_begin_headers(stream_id) }
|
|
188
|
+
.on_recv_header { |stream_id, name, value, flags| on_recv_header(stream_id, name, value, flags) }
|
|
189
|
+
.on_end_headers { |stream_id, fin| on_end_headers(stream_id, fin) }
|
|
190
|
+
.on_recv_data { |stream_id, data| on_recv_data(stream_id, data) }
|
|
191
|
+
.on_end_stream { |stream_id| on_end_stream(stream_id) }
|
|
192
|
+
.on_stream_close { |stream_id, app_error_code| on_stream_close(stream_id, app_error_code) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def on_begin_headers(stream_id)
|
|
196
|
+
# Response headers starting
|
|
197
|
+
@responses[stream_id] ||= Response.new(stream_id: stream_id)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def on_recv_header(stream_id, name, value, _flags)
|
|
201
|
+
response = @responses[stream_id]
|
|
202
|
+
return unless response
|
|
203
|
+
|
|
204
|
+
if name == ":status"
|
|
205
|
+
response.status = value.to_i
|
|
206
|
+
else
|
|
207
|
+
response.headers[name] = value
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def on_end_headers(stream_id, _fin)
|
|
212
|
+
# Headers complete
|
|
213
|
+
response = @responses[stream_id]
|
|
214
|
+
response&.write_headers
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def on_recv_data(stream_id, data)
|
|
218
|
+
response = @responses[stream_id]
|
|
219
|
+
response&.append_body(data)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def on_end_stream(stream_id)
|
|
223
|
+
response = @responses[stream_id]
|
|
224
|
+
response&.finish
|
|
225
|
+
@pending_requests.delete(stream_id)
|
|
226
|
+
@stream_manager.close_stream(stream_id)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def on_stream_close(stream_id, _app_error_code)
|
|
230
|
+
response = @responses[stream_id]
|
|
231
|
+
response&.finish
|
|
232
|
+
@pending_requests.delete(stream_id)
|
|
233
|
+
@stream_manager.close_stream(stream_id)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nghttp3
|
|
4
|
+
# A case-insensitive header collection for HTTP/3
|
|
5
|
+
#
|
|
6
|
+
# Header names are automatically normalized to lowercase.
|
|
7
|
+
# Supports multiple values for the same header name.
|
|
8
|
+
class Headers
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
def initialize(hash = {})
|
|
12
|
+
@headers = {}
|
|
13
|
+
hash&.each { |k, v| self[k] = v }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get header value by name (case-insensitive)
|
|
17
|
+
# @param name [String, Symbol] header name
|
|
18
|
+
# @return [String, nil] header value or nil if not found
|
|
19
|
+
def [](name)
|
|
20
|
+
@headers[normalize_name(name)]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Set header value (case-insensitive)
|
|
24
|
+
# @param name [String, Symbol] header name
|
|
25
|
+
# @param value [String, #to_s] header value
|
|
26
|
+
# @return [String] the value
|
|
27
|
+
def []=(name, value)
|
|
28
|
+
@headers[normalize_name(name)] = value.to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Delete a header
|
|
32
|
+
# @param name [String, Symbol] header name
|
|
33
|
+
# @return [String, nil] deleted value or nil
|
|
34
|
+
def delete(name)
|
|
35
|
+
@headers.delete(normalize_name(name))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if header exists
|
|
39
|
+
# @param name [String, Symbol] header name
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def key?(name)
|
|
42
|
+
@headers.key?(normalize_name(name))
|
|
43
|
+
end
|
|
44
|
+
alias_method :has_key?, :key?
|
|
45
|
+
alias_method :include?, :key?
|
|
46
|
+
|
|
47
|
+
# Iterate over all headers
|
|
48
|
+
# @yield [name, value] each header name-value pair
|
|
49
|
+
def each(&block)
|
|
50
|
+
return enum_for(:each) unless block_given?
|
|
51
|
+
@headers.each(&block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Merge another hash into this headers collection
|
|
55
|
+
# @param other [Hash, Headers] headers to merge
|
|
56
|
+
# @return [self]
|
|
57
|
+
def merge!(other)
|
|
58
|
+
other.each { |k, v| self[k] = v }
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Return a new Headers with merged values
|
|
63
|
+
# @param other [Hash, Headers] headers to merge
|
|
64
|
+
# @return [Headers] new Headers instance
|
|
65
|
+
def merge(other)
|
|
66
|
+
dup.merge!(other)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Convert to Hash
|
|
70
|
+
# @return [Hash{String => String}]
|
|
71
|
+
def to_h
|
|
72
|
+
@headers.dup
|
|
73
|
+
end
|
|
74
|
+
alias_method :to_hash, :to_h
|
|
75
|
+
|
|
76
|
+
# Number of headers
|
|
77
|
+
# @return [Integer]
|
|
78
|
+
def size
|
|
79
|
+
@headers.size
|
|
80
|
+
end
|
|
81
|
+
alias_method :length, :size
|
|
82
|
+
|
|
83
|
+
# Check if empty
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def empty?
|
|
86
|
+
@headers.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Remove all headers
|
|
90
|
+
# @return [self]
|
|
91
|
+
def clear
|
|
92
|
+
@headers.clear
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Duplicate the headers collection
|
|
97
|
+
# @return [Headers]
|
|
98
|
+
def dup
|
|
99
|
+
self.class.new(@headers)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# String representation
|
|
103
|
+
def inspect
|
|
104
|
+
"#<#{self.class} #{@headers.inspect}>"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def normalize_name(name)
|
|
110
|
+
name.to_s.downcase
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Nghttp3
|
|
6
|
+
# An immutable HTTP/3 request object
|
|
7
|
+
#
|
|
8
|
+
# Represents an HTTP request with method, path, headers, and optional body.
|
|
9
|
+
# Can be created directly or via convenience factory methods.
|
|
10
|
+
class Request
|
|
11
|
+
# @return [String] HTTP method (GET, POST, etc.)
|
|
12
|
+
attr_reader :method
|
|
13
|
+
|
|
14
|
+
# @return [String] URL scheme (https, http)
|
|
15
|
+
attr_reader :scheme
|
|
16
|
+
|
|
17
|
+
# @return [String, nil] authority (host:port)
|
|
18
|
+
attr_reader :authority
|
|
19
|
+
|
|
20
|
+
# @return [String] request path
|
|
21
|
+
attr_reader :path
|
|
22
|
+
|
|
23
|
+
# @return [Headers] request headers
|
|
24
|
+
attr_reader :headers
|
|
25
|
+
|
|
26
|
+
# @return [String, nil] request body
|
|
27
|
+
attr_reader :body
|
|
28
|
+
|
|
29
|
+
# Create a new request
|
|
30
|
+
# @param method [String, Symbol] HTTP method
|
|
31
|
+
# @param path [String] request path
|
|
32
|
+
# @param scheme [String] URL scheme (default: "https")
|
|
33
|
+
# @param authority [String, nil] authority (host:port)
|
|
34
|
+
# @param headers [Hash, Headers] additional headers
|
|
35
|
+
# @param body [String, nil] request body
|
|
36
|
+
def initialize(method:, path:, scheme: "https", authority: nil, headers: {}, body: nil)
|
|
37
|
+
@method = method.to_s.upcase.freeze
|
|
38
|
+
@scheme = scheme.to_s.freeze
|
|
39
|
+
@authority = authority&.to_s&.freeze
|
|
40
|
+
@path = path.to_s.freeze
|
|
41
|
+
@headers = headers.is_a?(Headers) ? headers : Headers.new(headers)
|
|
42
|
+
@body = body&.dup&.freeze
|
|
43
|
+
freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Convert to NV array for low-level Connection API
|
|
47
|
+
# @return [Array<NV>] array of NV objects for pseudo-headers and headers
|
|
48
|
+
def to_nv_array
|
|
49
|
+
nvs = []
|
|
50
|
+
nvs << NV.new(":method", @method)
|
|
51
|
+
nvs << NV.new(":scheme", @scheme)
|
|
52
|
+
nvs << NV.new(":authority", @authority) if @authority
|
|
53
|
+
nvs << NV.new(":path", @path)
|
|
54
|
+
@headers.each { |name, value| nvs << NV.new(name, value) }
|
|
55
|
+
nvs
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if request has a body
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def body?
|
|
61
|
+
!@body.nil? && !@body.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# String representation
|
|
65
|
+
def inspect
|
|
66
|
+
"#<#{self.class} #{@method} #{@scheme}://#{@authority}#{@path}>"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# Create a GET request
|
|
71
|
+
# @param url [String] full URL
|
|
72
|
+
# @param headers [Hash] additional headers
|
|
73
|
+
# @return [Request]
|
|
74
|
+
def get(url, headers: {})
|
|
75
|
+
build_from_url("GET", url, headers: headers)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Create a POST request
|
|
79
|
+
# @param url [String] full URL
|
|
80
|
+
# @param body [String, nil] request body
|
|
81
|
+
# @param headers [Hash] additional headers
|
|
82
|
+
# @return [Request]
|
|
83
|
+
def post(url, body: nil, headers: {})
|
|
84
|
+
build_from_url("POST", url, body: body, headers: headers)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Create a PUT request
|
|
88
|
+
# @param url [String] full URL
|
|
89
|
+
# @param body [String, nil] request body
|
|
90
|
+
# @param headers [Hash] additional headers
|
|
91
|
+
# @return [Request]
|
|
92
|
+
def put(url, body: nil, headers: {})
|
|
93
|
+
build_from_url("PUT", url, body: body, headers: headers)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Create a DELETE request
|
|
97
|
+
# @param url [String] full URL
|
|
98
|
+
# @param headers [Hash] additional headers
|
|
99
|
+
# @return [Request]
|
|
100
|
+
def delete(url, headers: {})
|
|
101
|
+
build_from_url("DELETE", url, headers: headers)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Create a HEAD request
|
|
105
|
+
# @param url [String] full URL
|
|
106
|
+
# @param headers [Hash] additional headers
|
|
107
|
+
# @return [Request]
|
|
108
|
+
def head(url, headers: {})
|
|
109
|
+
build_from_url("HEAD", url, headers: headers)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Create a PATCH request
|
|
113
|
+
# @param url [String] full URL
|
|
114
|
+
# @param body [String, nil] request body
|
|
115
|
+
# @param headers [Hash] additional headers
|
|
116
|
+
# @return [Request]
|
|
117
|
+
def patch(url, body: nil, headers: {})
|
|
118
|
+
build_from_url("PATCH", url, body: body, headers: headers)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Create an OPTIONS request
|
|
122
|
+
# @param url [String] full URL
|
|
123
|
+
# @param headers [Hash] additional headers
|
|
124
|
+
# @return [Request]
|
|
125
|
+
def options(url, headers: {})
|
|
126
|
+
build_from_url("OPTIONS", url, headers: headers)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def build_from_url(method, url, body: nil, headers: {})
|
|
132
|
+
uri = URI.parse(url)
|
|
133
|
+
authority = (uri.port == uri.default_port) ? uri.host : "#{uri.host}:#{uri.port}"
|
|
134
|
+
path = uri.request_uri.empty? ? "/" : uri.request_uri
|
|
135
|
+
|
|
136
|
+
new(
|
|
137
|
+
method: method,
|
|
138
|
+
scheme: uri.scheme || "https",
|
|
139
|
+
authority: authority,
|
|
140
|
+
path: path,
|
|
141
|
+
headers: headers,
|
|
142
|
+
body: body
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|