quicsilver 0.1.0
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/.gitignore +12 -0
- data/.gitmodules +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +28 -0
- data/README.md +105 -0
- data/Rakefile +31 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/README.md +105 -0
- data/examples/minimal_http3_client.rb +89 -0
- data/examples/minimal_http3_server.rb +32 -0
- data/examples/rack_http3_server.rb +60 -0
- data/examples/setup_certs.sh +57 -0
- data/ext/quicsilver/extconf.rb +25 -0
- data/ext/quicsilver/quicsilver.c +807 -0
- data/lib/quicsilver/client.rb +191 -0
- data/lib/quicsilver/http3/request_encoder.rb +112 -0
- data/lib/quicsilver/http3/request_parser.rb +158 -0
- data/lib/quicsilver/http3/response_encoder.rb +73 -0
- data/lib/quicsilver/http3.rb +68 -0
- data/lib/quicsilver/listener_data.rb +29 -0
- data/lib/quicsilver/server.rb +258 -0
- data/lib/quicsilver/server_configuration.rb +49 -0
- data/lib/quicsilver/version.rb +3 -0
- data/lib/quicsilver.rb +22 -0
- data/quicsilver.gemspec +44 -0
- metadata +143 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include "msquic.h"
|
|
3
|
+
#include <pthread.h>
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
#include <string.h>
|
|
6
|
+
|
|
7
|
+
static VALUE mQuicsilver;
|
|
8
|
+
|
|
9
|
+
// Event queue for callbacks
|
|
10
|
+
typedef struct CallbackEvent {
|
|
11
|
+
char* event_type;
|
|
12
|
+
uint64_t stream_id;
|
|
13
|
+
char* data;
|
|
14
|
+
size_t data_len;
|
|
15
|
+
struct CallbackEvent* next;
|
|
16
|
+
} CallbackEvent;
|
|
17
|
+
|
|
18
|
+
static CallbackEvent* event_queue_head = NULL;
|
|
19
|
+
static CallbackEvent* event_queue_tail = NULL;
|
|
20
|
+
static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
21
|
+
|
|
22
|
+
// Global MSQUIC API table
|
|
23
|
+
static const QUIC_API_TABLE* MsQuic = NULL;
|
|
24
|
+
|
|
25
|
+
// Global registration handle
|
|
26
|
+
static HQUIC Registration = NULL;
|
|
27
|
+
|
|
28
|
+
// Registration configuration
|
|
29
|
+
static const QUIC_REGISTRATION_CONFIG RegConfig = { "quicsilver", QUIC_EXECUTION_PROFILE_LOW_LATENCY };
|
|
30
|
+
|
|
31
|
+
// Connection state tracking
|
|
32
|
+
typedef struct {
|
|
33
|
+
int connected;
|
|
34
|
+
int failed;
|
|
35
|
+
QUIC_STATUS error_status;
|
|
36
|
+
uint64_t error_code;
|
|
37
|
+
} ConnectionContext;
|
|
38
|
+
|
|
39
|
+
// Listener state tracking
|
|
40
|
+
typedef struct {
|
|
41
|
+
int started;
|
|
42
|
+
int stopped;
|
|
43
|
+
int failed;
|
|
44
|
+
QUIC_STATUS error_status;
|
|
45
|
+
HQUIC Configuration;
|
|
46
|
+
} ListenerContext;
|
|
47
|
+
|
|
48
|
+
// Stream state tracking
|
|
49
|
+
typedef struct {
|
|
50
|
+
int started;
|
|
51
|
+
int shutdown;
|
|
52
|
+
QUIC_STATUS error_status;
|
|
53
|
+
} StreamContext;
|
|
54
|
+
|
|
55
|
+
// Enqueue a callback event (thread-safe)
|
|
56
|
+
static void
|
|
57
|
+
enqueue_callback_event(const char* event_type, uint64_t stream_id, const char* data, size_t data_len)
|
|
58
|
+
{
|
|
59
|
+
CallbackEvent* event = (CallbackEvent*)malloc(sizeof(CallbackEvent));
|
|
60
|
+
if (event == NULL) return;
|
|
61
|
+
|
|
62
|
+
event->event_type = strdup(event_type);
|
|
63
|
+
event->stream_id = stream_id;
|
|
64
|
+
event->data = (char*)malloc(data_len);
|
|
65
|
+
if (event->data != NULL) {
|
|
66
|
+
memcpy(event->data, data, data_len);
|
|
67
|
+
}
|
|
68
|
+
event->data_len = data_len;
|
|
69
|
+
event->next = NULL;
|
|
70
|
+
|
|
71
|
+
pthread_mutex_lock(&queue_mutex);
|
|
72
|
+
if (event_queue_tail == NULL) {
|
|
73
|
+
event_queue_head = event_queue_tail = event;
|
|
74
|
+
} else {
|
|
75
|
+
event_queue_tail->next = event;
|
|
76
|
+
event_queue_tail = event;
|
|
77
|
+
}
|
|
78
|
+
pthread_mutex_unlock(&queue_mutex);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Process all pending callback events (called from Ruby)
|
|
82
|
+
static VALUE
|
|
83
|
+
quicsilver_process_events(VALUE self)
|
|
84
|
+
{
|
|
85
|
+
CallbackEvent* event;
|
|
86
|
+
VALUE server_class = rb_const_get(mQuicsilver, rb_intern("Server"));
|
|
87
|
+
int processed = 0;
|
|
88
|
+
|
|
89
|
+
while (1) {
|
|
90
|
+
pthread_mutex_lock(&queue_mutex);
|
|
91
|
+
event = event_queue_head;
|
|
92
|
+
if (event != NULL) {
|
|
93
|
+
event_queue_head = event->next;
|
|
94
|
+
if (event_queue_head == NULL) {
|
|
95
|
+
event_queue_tail = NULL;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
pthread_mutex_unlock(&queue_mutex);
|
|
99
|
+
|
|
100
|
+
if (event == NULL) break;
|
|
101
|
+
|
|
102
|
+
if (!NIL_P(server_class)) {
|
|
103
|
+
rb_funcall(server_class, rb_intern("handle_stream"), 3,
|
|
104
|
+
ULL2NUM(event->stream_id),
|
|
105
|
+
rb_str_new_cstr(event->event_type),
|
|
106
|
+
rb_str_new(event->data, event->data_len));
|
|
107
|
+
processed++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
free(event->event_type);
|
|
111
|
+
free(event->data);
|
|
112
|
+
free(event);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return INT2NUM(processed);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
QUIC_STATUS
|
|
119
|
+
StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
|
|
120
|
+
{
|
|
121
|
+
StreamContext* ctx = (StreamContext*)Context;
|
|
122
|
+
|
|
123
|
+
if (ctx == NULL) {
|
|
124
|
+
return QUIC_STATUS_SUCCESS;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
switch (Event->Type) {
|
|
128
|
+
case QUIC_STREAM_EVENT_RECEIVE:
|
|
129
|
+
// Client sent data - enqueue for Ruby processing
|
|
130
|
+
if (Event->RECEIVE.BufferCount > 0) {
|
|
131
|
+
const QUIC_BUFFER* buffer = &Event->RECEIVE.Buffers[0];
|
|
132
|
+
const char* event_type = (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) ? "RECEIVE_FIN" : "RECEIVE";
|
|
133
|
+
|
|
134
|
+
// Get the QUIC protocol stream ID (0, 4, 8, 12...)
|
|
135
|
+
uint64_t stream_id = 0;
|
|
136
|
+
uint32_t stream_id_len = sizeof(stream_id);
|
|
137
|
+
MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
|
|
138
|
+
|
|
139
|
+
// Pack stream handle pointer along with data for RECEIVE_FIN
|
|
140
|
+
if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) {
|
|
141
|
+
// Create combined buffer: [stream_handle(8 bytes)][data]
|
|
142
|
+
size_t total_len = sizeof(HQUIC) + buffer->Length;
|
|
143
|
+
char* combined = (char*)malloc(total_len);
|
|
144
|
+
if (combined != NULL) {
|
|
145
|
+
memcpy(combined, &Stream, sizeof(HQUIC));
|
|
146
|
+
memcpy(combined + sizeof(HQUIC), buffer->Buffer, buffer->Length);
|
|
147
|
+
enqueue_callback_event(event_type, stream_id, combined, total_len);
|
|
148
|
+
free(combined);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
enqueue_callback_event(event_type, stream_id, (const char*)buffer->Buffer, buffer->Length);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case QUIC_STREAM_EVENT_SEND_COMPLETE:
|
|
156
|
+
break;
|
|
157
|
+
case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
|
|
158
|
+
ctx->shutdown = 1;
|
|
159
|
+
break;
|
|
160
|
+
case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
|
|
161
|
+
break;
|
|
162
|
+
case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return QUIC_STATUS_SUCCESS;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Connection callback
|
|
170
|
+
static QUIC_STATUS QUIC_API
|
|
171
|
+
ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event)
|
|
172
|
+
{
|
|
173
|
+
ConnectionContext* ctx = (ConnectionContext*)Context;
|
|
174
|
+
HQUIC Stream;
|
|
175
|
+
StreamContext* stream_ctx;
|
|
176
|
+
|
|
177
|
+
if (ctx == NULL) {
|
|
178
|
+
return QUIC_STATUS_SUCCESS;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
switch (Event->Type) {
|
|
182
|
+
case QUIC_CONNECTION_EVENT_CONNECTED:
|
|
183
|
+
ctx->connected = 1;
|
|
184
|
+
ctx->failed = 0;
|
|
185
|
+
// Notify Ruby about new connection
|
|
186
|
+
enqueue_callback_event("CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC));
|
|
187
|
+
break;
|
|
188
|
+
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
|
|
189
|
+
ctx->connected = 0;
|
|
190
|
+
ctx->failed = 1;
|
|
191
|
+
ctx->error_status = Event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status;
|
|
192
|
+
ctx->error_code = Event->SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode;
|
|
193
|
+
break;
|
|
194
|
+
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
|
|
195
|
+
ctx->connected = 0;
|
|
196
|
+
ctx->failed = 1;
|
|
197
|
+
ctx->error_status = QUIC_STATUS_SUCCESS; // Peer initiated, not an error
|
|
198
|
+
ctx->error_code = Event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode;
|
|
199
|
+
break;
|
|
200
|
+
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
|
|
201
|
+
ctx->connected = 0;
|
|
202
|
+
break;
|
|
203
|
+
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
|
|
204
|
+
// Client opened a stream
|
|
205
|
+
Stream = Event->PEER_STREAM_STARTED.Stream;
|
|
206
|
+
stream_ctx = (StreamContext*)malloc(sizeof(StreamContext));
|
|
207
|
+
if (stream_ctx != NULL) {
|
|
208
|
+
stream_ctx->started = 1;
|
|
209
|
+
stream_ctx->shutdown = 0;
|
|
210
|
+
stream_ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
211
|
+
|
|
212
|
+
// Set the stream callback handler to handle data events
|
|
213
|
+
MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, stream_ctx);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return QUIC_STATUS_SUCCESS;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Listener callback to handle incoming connections
|
|
224
|
+
static QUIC_STATUS QUIC_API
|
|
225
|
+
ListenerCallback(HQUIC Listener, void* Context, QUIC_LISTENER_EVENT* Event)
|
|
226
|
+
{
|
|
227
|
+
ListenerContext* ctx = (ListenerContext*)Context;
|
|
228
|
+
ConnectionContext* conn_ctx;
|
|
229
|
+
|
|
230
|
+
if (ctx == NULL) {
|
|
231
|
+
return QUIC_STATUS_SUCCESS;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
switch (Event->Type) {
|
|
235
|
+
case QUIC_LISTENER_EVENT_NEW_CONNECTION:
|
|
236
|
+
// Create a connection context for the new connection
|
|
237
|
+
conn_ctx = (ConnectionContext*)malloc(sizeof(ConnectionContext));
|
|
238
|
+
if (conn_ctx != NULL) {
|
|
239
|
+
conn_ctx->connected = 0;
|
|
240
|
+
conn_ctx->failed = 0;
|
|
241
|
+
conn_ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
242
|
+
conn_ctx->error_code = 0;
|
|
243
|
+
|
|
244
|
+
// Set the connection callback
|
|
245
|
+
MsQuic->SetCallbackHandler(Event->NEW_CONNECTION.Connection, (void*)ConnectionCallback, conn_ctx);
|
|
246
|
+
|
|
247
|
+
// Accept the new connection with the server configuration
|
|
248
|
+
QUIC_STATUS Status = MsQuic->ConnectionSetConfiguration(Event->NEW_CONNECTION.Connection, ctx->Configuration);
|
|
249
|
+
if (QUIC_FAILED(Status)) {
|
|
250
|
+
free(conn_ctx);
|
|
251
|
+
return Status;
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// Reject the connection if we can't allocate context
|
|
255
|
+
return QUIC_STATUS_OUT_OF_MEMORY;
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case QUIC_LISTENER_EVENT_STOP_COMPLETE:
|
|
260
|
+
ctx->stopped = 1;
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
default:
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return QUIC_STATUS_SUCCESS;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Initialize MSQUIC
|
|
271
|
+
static VALUE
|
|
272
|
+
quicsilver_open(VALUE self)
|
|
273
|
+
{
|
|
274
|
+
QUIC_STATUS Status;
|
|
275
|
+
|
|
276
|
+
// Check if already initialized
|
|
277
|
+
if (MsQuic != NULL) {
|
|
278
|
+
return Qtrue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Open a handle to the library and get the API function table
|
|
282
|
+
if (QUIC_FAILED(Status = MsQuicOpenVersion(2, (const void**)&MsQuic))) {
|
|
283
|
+
rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
|
|
284
|
+
return Qfalse;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Create a registration for the app's connections
|
|
288
|
+
if (QUIC_FAILED(Status = MsQuic->RegistrationOpen(&RegConfig, &Registration))) {
|
|
289
|
+
rb_raise(rb_eRuntimeError, "RegistrationOpen failed, 0x%x!", Status);
|
|
290
|
+
MsQuicClose(MsQuic);
|
|
291
|
+
MsQuic = NULL;
|
|
292
|
+
return Qfalse;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return Qtrue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create a QUIC configuration (for client connections)
|
|
299
|
+
static VALUE
|
|
300
|
+
quicsilver_create_configuration(VALUE self, VALUE unsecure)
|
|
301
|
+
{
|
|
302
|
+
if (MsQuic == NULL) {
|
|
303
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
|
|
304
|
+
return Qnil;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
QUIC_STATUS Status;
|
|
308
|
+
HQUIC Configuration = NULL;
|
|
309
|
+
|
|
310
|
+
// Basic settings
|
|
311
|
+
QUIC_SETTINGS Settings = {0};
|
|
312
|
+
Settings.IdleTimeoutMs = 10000; // 10 second idle timeout to match server
|
|
313
|
+
Settings.IsSet.IdleTimeoutMs = TRUE;
|
|
314
|
+
|
|
315
|
+
// Simple ALPN for now - Ruby can customize this later
|
|
316
|
+
QUIC_BUFFER Alpn = { sizeof("h3") - 1, (uint8_t*)"h3" };
|
|
317
|
+
|
|
318
|
+
// Create configuration
|
|
319
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, &Alpn, 1, &Settings, sizeof(Settings), NULL, &Configuration))) {
|
|
320
|
+
rb_raise(rb_eRuntimeError, "ConfigurationOpen failed, 0x%x!", Status);
|
|
321
|
+
return Qnil;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Set up credentials
|
|
325
|
+
QUIC_CREDENTIAL_CONFIG CredConfig = {0};
|
|
326
|
+
CredConfig.Type = QUIC_CREDENTIAL_TYPE_NONE;
|
|
327
|
+
CredConfig.Flags = QUIC_CREDENTIAL_FLAG_CLIENT;
|
|
328
|
+
|
|
329
|
+
if (RTEST(unsecure)) {
|
|
330
|
+
CredConfig.Flags |= QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
|
|
334
|
+
rb_raise(rb_eRuntimeError, "ConfigurationLoadCredential failed, 0x%x!", Status);
|
|
335
|
+
MsQuic->ConfigurationClose(Configuration);
|
|
336
|
+
return Qnil;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Return the configuration handle as a Ruby integer (pointer)
|
|
340
|
+
return ULL2NUM((uintptr_t)Configuration);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create a QUIC server configuration
|
|
344
|
+
static VALUE
|
|
345
|
+
quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
|
|
346
|
+
{
|
|
347
|
+
if (MsQuic == NULL) {
|
|
348
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
|
|
349
|
+
return Qnil;
|
|
350
|
+
}
|
|
351
|
+
VALUE cert_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("cert_file")));
|
|
352
|
+
VALUE key_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("key_file")));
|
|
353
|
+
VALUE idle_timeout_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("idle_timeout")));
|
|
354
|
+
VALUE server_resumption_level_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("server_resumption_level")));
|
|
355
|
+
VALUE peer_bidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_bidi_stream_count")));
|
|
356
|
+
VALUE peer_unidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_unidi_stream_count")));
|
|
357
|
+
VALUE alpn_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("alpn")));
|
|
358
|
+
|
|
359
|
+
QUIC_STATUS Status;
|
|
360
|
+
HQUIC Configuration = NULL;
|
|
361
|
+
|
|
362
|
+
const char* cert_path = StringValueCStr(cert_file_val);
|
|
363
|
+
const char* key_path = StringValueCStr(key_file_val);
|
|
364
|
+
uint32_t idle_timeout_ms = NUM2INT(idle_timeout_val);
|
|
365
|
+
uint32_t server_resumption_level = NUM2INT(server_resumption_level_val);
|
|
366
|
+
uint32_t peer_bidi_stream_count = NUM2INT(peer_bidi_stream_count_val);
|
|
367
|
+
uint32_t peer_unidi_stream_count = NUM2INT(peer_unidi_stream_count_val);
|
|
368
|
+
const char* alpn_str = StringValueCStr(alpn_val);
|
|
369
|
+
|
|
370
|
+
QUIC_SETTINGS Settings = {0};
|
|
371
|
+
Settings.IdleTimeoutMs = idle_timeout_ms;
|
|
372
|
+
Settings.IsSet.IdleTimeoutMs = TRUE;
|
|
373
|
+
Settings.ServerResumptionLevel = server_resumption_level;
|
|
374
|
+
Settings.IsSet.ServerResumptionLevel = TRUE;
|
|
375
|
+
Settings.PeerBidiStreamCount = peer_bidi_stream_count;
|
|
376
|
+
Settings.IsSet.PeerBidiStreamCount = TRUE;
|
|
377
|
+
Settings.PeerUnidiStreamCount = peer_unidi_stream_count;
|
|
378
|
+
Settings.IsSet.PeerUnidiStreamCount = TRUE;
|
|
379
|
+
|
|
380
|
+
QUIC_BUFFER Alpn = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
|
|
381
|
+
|
|
382
|
+
// Create configuration
|
|
383
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, &Alpn, 1, &Settings, sizeof(Settings), NULL, &Configuration))) {
|
|
384
|
+
rb_raise(rb_eRuntimeError, "Server ConfigurationOpen failed, 0x%x!", Status);
|
|
385
|
+
return Qnil;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Set up server credentials with certificate files
|
|
389
|
+
QUIC_CREDENTIAL_CONFIG CredConfig = {0};
|
|
390
|
+
QUIC_CERTIFICATE_FILE CertFile = {0};
|
|
391
|
+
|
|
392
|
+
CertFile.CertificateFile = cert_path;
|
|
393
|
+
CertFile.PrivateKeyFile = key_path;
|
|
394
|
+
|
|
395
|
+
CredConfig.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE;
|
|
396
|
+
CredConfig.CertificateFile = &CertFile;
|
|
397
|
+
CredConfig.Flags = QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION;
|
|
398
|
+
|
|
399
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
|
|
400
|
+
rb_raise(rb_eRuntimeError, "Server ConfigurationLoadCredential failed, 0x%x!", Status);
|
|
401
|
+
MsQuic->ConfigurationClose(Configuration);
|
|
402
|
+
return Qnil;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Return the configuration handle as a Ruby integer (pointer)
|
|
406
|
+
return ULL2NUM((uintptr_t)Configuration);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Create a QUIC connection with context
|
|
410
|
+
static VALUE
|
|
411
|
+
quicsilver_create_connection(VALUE self)
|
|
412
|
+
{
|
|
413
|
+
if (MsQuic == NULL) {
|
|
414
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
|
|
415
|
+
return Qnil;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
QUIC_STATUS Status;
|
|
419
|
+
HQUIC Connection = NULL;
|
|
420
|
+
|
|
421
|
+
// Allocate and initialize connection context
|
|
422
|
+
ConnectionContext* ctx = (ConnectionContext*)malloc(sizeof(ConnectionContext));
|
|
423
|
+
if (ctx == NULL) {
|
|
424
|
+
rb_raise(rb_eRuntimeError, "Failed to allocate connection context");
|
|
425
|
+
return Qnil;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
ctx->connected = 0;
|
|
429
|
+
ctx->failed = 0;
|
|
430
|
+
ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
431
|
+
ctx->error_code = 0;
|
|
432
|
+
|
|
433
|
+
// Create connection with enhanced callback and context
|
|
434
|
+
if (QUIC_FAILED(Status = MsQuic->ConnectionOpen(Registration, ConnectionCallback, ctx, &Connection))) {
|
|
435
|
+
free(ctx);
|
|
436
|
+
rb_raise(rb_eRuntimeError, "ConnectionOpen failed, 0x%x!", Status);
|
|
437
|
+
return Qnil;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Return both the connection handle and context as an array
|
|
441
|
+
VALUE result = rb_ary_new2(2);
|
|
442
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)Connection));
|
|
443
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)ctx));
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Start a QUIC connection
|
|
448
|
+
static VALUE
|
|
449
|
+
quicsilver_start_connection(VALUE self, VALUE connection_handle, VALUE config_handle, VALUE hostname, VALUE port)
|
|
450
|
+
{
|
|
451
|
+
if (MsQuic == NULL) {
|
|
452
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
453
|
+
return Qfalse;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
457
|
+
HQUIC Configuration = (HQUIC)(uintptr_t)NUM2ULL(config_handle);
|
|
458
|
+
const char* Target = StringValueCStr(hostname);
|
|
459
|
+
uint16_t Port = (uint16_t)NUM2INT(port);
|
|
460
|
+
|
|
461
|
+
QUIC_STATUS Status;
|
|
462
|
+
if (QUIC_FAILED(Status = MsQuic->ConnectionStart(Connection, Configuration, QUIC_ADDRESS_FAMILY_UNSPEC, Target, Port))) {
|
|
463
|
+
rb_raise(rb_eRuntimeError, "ConnectionStart failed, 0x%x!", Status);
|
|
464
|
+
return Qfalse;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return Qtrue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Wait for connection to complete (connected or failed)
|
|
471
|
+
static VALUE
|
|
472
|
+
quicsilver_wait_for_connection(VALUE self, VALUE context_handle, VALUE timeout_ms)
|
|
473
|
+
{
|
|
474
|
+
ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
475
|
+
int timeout = NUM2INT(timeout_ms);
|
|
476
|
+
int elapsed = 0;
|
|
477
|
+
const int sleep_interval = 10; // 10ms
|
|
478
|
+
|
|
479
|
+
while (elapsed < timeout && !ctx->connected && !ctx->failed) {
|
|
480
|
+
usleep(sleep_interval * 1000); // Convert to microseconds
|
|
481
|
+
elapsed += sleep_interval;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (ctx->connected) {
|
|
485
|
+
return rb_hash_new();
|
|
486
|
+
} else if (ctx->failed) {
|
|
487
|
+
VALUE error_info = rb_hash_new();
|
|
488
|
+
rb_hash_aset(error_info, rb_str_new_cstr("error"), Qtrue);
|
|
489
|
+
rb_hash_aset(error_info, rb_str_new_cstr("status"), ULL2NUM(ctx->error_status));
|
|
490
|
+
rb_hash_aset(error_info, rb_str_new_cstr("code"), ULL2NUM(ctx->error_code));
|
|
491
|
+
|
|
492
|
+
return error_info;
|
|
493
|
+
} else {
|
|
494
|
+
VALUE timeout_info = rb_hash_new();
|
|
495
|
+
rb_hash_aset(timeout_info, rb_str_new_cstr("timeout"), Qtrue);
|
|
496
|
+
return timeout_info;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Check connection status
|
|
501
|
+
static VALUE
|
|
502
|
+
quicsilver_connection_status(VALUE self, VALUE context_handle)
|
|
503
|
+
{
|
|
504
|
+
ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
505
|
+
|
|
506
|
+
VALUE status = rb_hash_new();
|
|
507
|
+
rb_hash_aset(status, rb_str_new_cstr("connected"), ctx->connected ? Qtrue : Qfalse);
|
|
508
|
+
rb_hash_aset(status, rb_str_new_cstr("failed"), ctx->failed ? Qtrue : Qfalse);
|
|
509
|
+
|
|
510
|
+
if (ctx->failed) {
|
|
511
|
+
rb_hash_aset(status, rb_str_new_cstr("error_status"), ULL2NUM(ctx->error_status));
|
|
512
|
+
rb_hash_aset(status, rb_str_new_cstr("error_code"), ULL2NUM(ctx->error_code));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return status;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Close a QUIC connection and free context
|
|
519
|
+
static VALUE
|
|
520
|
+
quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
|
|
521
|
+
{
|
|
522
|
+
if (MsQuic == NULL) {
|
|
523
|
+
return Qnil;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Extract connection handle and context from array
|
|
527
|
+
VALUE connection_handle = rb_ary_entry(connection_data, 0);
|
|
528
|
+
VALUE context_handle = rb_ary_entry(connection_data, 1);
|
|
529
|
+
|
|
530
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
531
|
+
ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
532
|
+
|
|
533
|
+
// Only close if connection is valid
|
|
534
|
+
if (Connection != NULL) {
|
|
535
|
+
MsQuic->ConnectionClose(Connection);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Free context if valid
|
|
539
|
+
if (ctx != NULL) {
|
|
540
|
+
free(ctx);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return Qnil;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Close a QUIC configuration
|
|
547
|
+
static VALUE
|
|
548
|
+
quicsilver_close_configuration(VALUE self, VALUE config_handle)
|
|
549
|
+
{
|
|
550
|
+
if (MsQuic == NULL) {
|
|
551
|
+
return Qnil;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
HQUIC Configuration = (HQUIC)(uintptr_t)NUM2ULL(config_handle);
|
|
555
|
+
MsQuic->ConfigurationClose(Configuration);
|
|
556
|
+
return Qnil;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Close MSQUIC
|
|
560
|
+
static VALUE
|
|
561
|
+
quicsilver_close(VALUE self)
|
|
562
|
+
{
|
|
563
|
+
if (MsQuic != NULL) {
|
|
564
|
+
if (Registration != NULL) {
|
|
565
|
+
// This will block until all outstanding child objects have been closed
|
|
566
|
+
MsQuic->RegistrationClose(Registration);
|
|
567
|
+
Registration = NULL;
|
|
568
|
+
}
|
|
569
|
+
MsQuicClose(MsQuic);
|
|
570
|
+
MsQuic = NULL;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return Qnil;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Create a QUIC listener
|
|
577
|
+
static VALUE
|
|
578
|
+
quicsilver_create_listener(VALUE self, VALUE config_handle)
|
|
579
|
+
{
|
|
580
|
+
if (MsQuic == NULL) {
|
|
581
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
582
|
+
return Qnil;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
HQUIC Configuration = (HQUIC)(uintptr_t)NUM2ULL(config_handle);
|
|
586
|
+
HQUIC Listener = NULL;
|
|
587
|
+
QUIC_STATUS Status;
|
|
588
|
+
|
|
589
|
+
// Create listener context
|
|
590
|
+
ListenerContext* ctx = (ListenerContext*)malloc(sizeof(ListenerContext));
|
|
591
|
+
if (ctx == NULL) {
|
|
592
|
+
rb_raise(rb_eRuntimeError, "Failed to allocate listener context");
|
|
593
|
+
return Qnil;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
ctx->started = 0;
|
|
597
|
+
ctx->stopped = 0;
|
|
598
|
+
ctx->failed = 0;
|
|
599
|
+
ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
600
|
+
ctx->Configuration = Configuration;
|
|
601
|
+
|
|
602
|
+
// Create listener
|
|
603
|
+
if (QUIC_FAILED(Status = MsQuic->ListenerOpen(Registration, ListenerCallback, ctx, &Listener))) {
|
|
604
|
+
free(ctx);
|
|
605
|
+
rb_raise(rb_eRuntimeError, "ListenerOpen failed, 0x%x!", Status);
|
|
606
|
+
return Qnil;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Return listener handle and context
|
|
610
|
+
VALUE result = rb_ary_new2(2);
|
|
611
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)Listener));
|
|
612
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)ctx));
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Start listener on specific address and port
|
|
617
|
+
static VALUE
|
|
618
|
+
quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port)
|
|
619
|
+
{
|
|
620
|
+
if (MsQuic == NULL) {
|
|
621
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
622
|
+
return Qfalse;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
626
|
+
uint16_t Port = (uint16_t)NUM2INT(port);
|
|
627
|
+
|
|
628
|
+
// Setup address - properly initialize the entire structure
|
|
629
|
+
QUIC_ADDR Address;
|
|
630
|
+
memset(&Address, 0, sizeof(Address));
|
|
631
|
+
|
|
632
|
+
// Set up for localhost/any address
|
|
633
|
+
QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET);
|
|
634
|
+
QuicAddrSetPort(&Address, Port);
|
|
635
|
+
|
|
636
|
+
QUIC_STATUS Status;
|
|
637
|
+
|
|
638
|
+
// Create QUIC_BUFFER for the address
|
|
639
|
+
QUIC_BUFFER AlpnBuffer = { sizeof("h3") - 1, (uint8_t*)"h3" };
|
|
640
|
+
|
|
641
|
+
if (QUIC_FAILED(Status = MsQuic->ListenerStart(Listener, &AlpnBuffer, 1, &Address))) {
|
|
642
|
+
rb_raise(rb_eRuntimeError, "ListenerStart failed, 0x%x!", Status);
|
|
643
|
+
return Qfalse;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return Qtrue;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Stop listener
|
|
650
|
+
static VALUE
|
|
651
|
+
quicsilver_stop_listener(VALUE self, VALUE listener_handle)
|
|
652
|
+
{
|
|
653
|
+
if (MsQuic == NULL) {
|
|
654
|
+
return Qfalse;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
658
|
+
MsQuic->ListenerStop(Listener);
|
|
659
|
+
return Qtrue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Close listener
|
|
663
|
+
static VALUE
|
|
664
|
+
quicsilver_close_listener(VALUE self, VALUE listener_data)
|
|
665
|
+
{
|
|
666
|
+
if (MsQuic == NULL) {
|
|
667
|
+
return Qnil;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
VALUE listener_handle = rb_ary_entry(listener_data, 0);
|
|
671
|
+
VALUE context_handle = rb_ary_entry(listener_data, 1);
|
|
672
|
+
|
|
673
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
674
|
+
ListenerContext* ctx = (ListenerContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
675
|
+
|
|
676
|
+
MsQuic->ListenerClose(Listener);
|
|
677
|
+
|
|
678
|
+
if (ctx != NULL) {
|
|
679
|
+
free(ctx);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return Qnil;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Open a QUIC stream
|
|
686
|
+
static VALUE
|
|
687
|
+
quicsilver_open_stream(VALUE self, VALUE connection_handle, VALUE unidirectional)
|
|
688
|
+
{
|
|
689
|
+
if (MsQuic == NULL) {
|
|
690
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
691
|
+
return Qnil;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
695
|
+
HQUIC Stream = NULL;
|
|
696
|
+
|
|
697
|
+
StreamContext* ctx = (StreamContext*)malloc(sizeof(StreamContext));
|
|
698
|
+
if (ctx == NULL) {
|
|
699
|
+
rb_raise(rb_eRuntimeError, "Failed to allocate stream context");
|
|
700
|
+
return Qnil;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
ctx->started = 1;
|
|
704
|
+
ctx->shutdown = 0;
|
|
705
|
+
ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
706
|
+
|
|
707
|
+
// Use flag based on parameter
|
|
708
|
+
QUIC_STREAM_OPEN_FLAGS flags = RTEST(unidirectional)
|
|
709
|
+
? QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL
|
|
710
|
+
: QUIC_STREAM_OPEN_FLAG_NONE;
|
|
711
|
+
|
|
712
|
+
// Create stream
|
|
713
|
+
QUIC_STATUS Status = MsQuic->StreamOpen(Connection, flags, StreamCallback, ctx, &Stream);
|
|
714
|
+
if (QUIC_FAILED(Status)) {
|
|
715
|
+
free(ctx);
|
|
716
|
+
rb_raise(rb_eRuntimeError, "StreamOpen failed, 0x%x!", Status);
|
|
717
|
+
return Qnil;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Start the stream
|
|
721
|
+
Status = MsQuic->StreamStart(Stream, QUIC_STREAM_START_FLAG_NONE);
|
|
722
|
+
if (QUIC_FAILED(Status)) {
|
|
723
|
+
free(ctx);
|
|
724
|
+
MsQuic->StreamClose(Stream);
|
|
725
|
+
rb_raise(rb_eRuntimeError, "StreamStart failed, 0x%x!", Status);
|
|
726
|
+
return Qnil;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return ULL2NUM((uintptr_t)Stream);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Send data on a QUIC stream
|
|
733
|
+
static VALUE
|
|
734
|
+
quicsilver_send_stream(VALUE self, VALUE stream_handle, VALUE data, VALUE send_fin)
|
|
735
|
+
{
|
|
736
|
+
if (MsQuic == NULL) {
|
|
737
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
738
|
+
return Qnil;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
|
|
742
|
+
// Use StringValuePtr and RSTRING_LEN for binary data with null bytes
|
|
743
|
+
const char* data_str = RSTRING_PTR(data);
|
|
744
|
+
uint32_t data_len = (uint32_t)RSTRING_LEN(data);
|
|
745
|
+
|
|
746
|
+
void* SendBufferRaw = malloc(sizeof(QUIC_BUFFER) + data_len);
|
|
747
|
+
if (SendBufferRaw == NULL) {
|
|
748
|
+
rb_raise(rb_eRuntimeError, "SendBuffer allocation failed!");
|
|
749
|
+
return Qnil;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
QUIC_BUFFER* SendBuffer = (QUIC_BUFFER*)SendBufferRaw;
|
|
753
|
+
SendBuffer->Buffer = (uint8_t*)SendBufferRaw + sizeof(QUIC_BUFFER);
|
|
754
|
+
SendBuffer->Length = data_len;
|
|
755
|
+
|
|
756
|
+
memcpy(SendBuffer->Buffer, data_str, data_len);
|
|
757
|
+
|
|
758
|
+
// Use flag based on parameter (default to FIN for backwards compat)
|
|
759
|
+
QUIC_SEND_FLAGS flags = (NIL_P(send_fin) || RTEST(send_fin))
|
|
760
|
+
? QUIC_SEND_FLAG_FIN
|
|
761
|
+
: QUIC_SEND_FLAG_NONE;
|
|
762
|
+
|
|
763
|
+
QUIC_STATUS Status = MsQuic->StreamSend(Stream, SendBuffer, 1, flags, SendBufferRaw);
|
|
764
|
+
if (QUIC_FAILED(Status)) {
|
|
765
|
+
free(SendBufferRaw);
|
|
766
|
+
rb_raise(rb_eRuntimeError, "StreamSend failed, 0x%x!", Status);
|
|
767
|
+
return Qfalse;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return Qtrue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Initialize the extension
|
|
774
|
+
void
|
|
775
|
+
Init_quicsilver(void)
|
|
776
|
+
{
|
|
777
|
+
mQuicsilver = rb_define_module("Quicsilver");
|
|
778
|
+
|
|
779
|
+
// Core initialization
|
|
780
|
+
rb_define_singleton_method(mQuicsilver, "open_connection", quicsilver_open, 0);
|
|
781
|
+
rb_define_singleton_method(mQuicsilver, "close_connection", quicsilver_close, 0);
|
|
782
|
+
|
|
783
|
+
// Configuration management
|
|
784
|
+
rb_define_singleton_method(mQuicsilver, "create_configuration", quicsilver_create_configuration, 1);
|
|
785
|
+
rb_define_singleton_method(mQuicsilver, "create_server_configuration", quicsilver_create_server_configuration, 1);
|
|
786
|
+
rb_define_singleton_method(mQuicsilver, "close_configuration", quicsilver_close_configuration, 1);
|
|
787
|
+
|
|
788
|
+
// Connection management
|
|
789
|
+
rb_define_singleton_method(mQuicsilver, "create_connection", quicsilver_create_connection, 0);
|
|
790
|
+
rb_define_singleton_method(mQuicsilver, "start_connection", quicsilver_start_connection, 4);
|
|
791
|
+
rb_define_singleton_method(mQuicsilver, "wait_for_connection", quicsilver_wait_for_connection, 2);
|
|
792
|
+
rb_define_singleton_method(mQuicsilver, "connection_status", quicsilver_connection_status, 1);
|
|
793
|
+
rb_define_singleton_method(mQuicsilver, "close_connection_handle", quicsilver_close_connection_handle, 1);
|
|
794
|
+
|
|
795
|
+
// Listener management
|
|
796
|
+
rb_define_singleton_method(mQuicsilver, "create_listener", quicsilver_create_listener, 1);
|
|
797
|
+
rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 3);
|
|
798
|
+
rb_define_singleton_method(mQuicsilver, "stop_listener", quicsilver_stop_listener, 1);
|
|
799
|
+
rb_define_singleton_method(mQuicsilver, "close_listener", quicsilver_close_listener, 1);
|
|
800
|
+
|
|
801
|
+
// Stream management
|
|
802
|
+
rb_define_singleton_method(mQuicsilver, "open_stream", quicsilver_open_stream, 2);
|
|
803
|
+
rb_define_singleton_method(mQuicsilver, "send_stream", quicsilver_send_stream, 3);
|
|
804
|
+
|
|
805
|
+
// Event processing
|
|
806
|
+
rb_define_singleton_method(mQuicsilver, "process_events", quicsilver_process_events, 0);
|
|
807
|
+
}
|