hyperion-rb 1.6.1 → 2.10.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 +4 -4
- data/CHANGELOG.md +4570 -0
- data/README.md +212 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +452 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +368 -9
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/* ----------------------------------------------------------------------
|
|
2
|
+
* Hyperion::WebSocket::CFrame — RFC 6455 frame ser/de in C.
|
|
3
|
+
*
|
|
4
|
+
* Public surface (defined as singleton methods on Hyperion::WebSocket::CFrame):
|
|
5
|
+
*
|
|
6
|
+
* CFrame.unmask(payload, mask_key) -> String
|
|
7
|
+
* Input: binary String `payload`, 4-byte binary String `mask_key`.
|
|
8
|
+
* Output: a freshly-allocated binary String of `payload.bytesize` bytes,
|
|
9
|
+
* XOR-unmasked with `mask_key` per RFC 6455 §5.3.
|
|
10
|
+
* Implementation: word-at-a-time XOR with the 4-byte key smeared into a
|
|
11
|
+
* uint32_t, then a 0..3-byte tail. GVL is released for payloads larger
|
|
12
|
+
* than 64 KiB so other fibers / threads can run while we crunch.
|
|
13
|
+
*
|
|
14
|
+
* CFrame.parse(buf, offset = 0) ->
|
|
15
|
+
* [fin, opcode, payload_len, masked, mask_key, payload_offset,
|
|
16
|
+
* frame_total_len, rsv1]
|
|
17
|
+
* OR :incomplete OR :error
|
|
18
|
+
* Non-copying parser. Returns metadata only; the caller still owns
|
|
19
|
+
* `buf` and uses `payload_offset` + `frame_total_len` to slice or to
|
|
20
|
+
* advance to the next frame. `mask_key` is `nil` when `masked == false`.
|
|
21
|
+
* Returns `:incomplete` if `buf[offset..]` does not yet hold a full
|
|
22
|
+
* header. Returns `:error` for malformed frames (RSV2/RSV3 bits set,
|
|
23
|
+
* unknown opcode, control frame > 125 bytes, fragmented control
|
|
24
|
+
* frame, 64-bit length with high bit set, or RSV1 set on a control
|
|
25
|
+
* frame). RSV1 is preserved as the 8th tuple slot; the Ruby façade
|
|
26
|
+
* decides whether to treat it as a permessage-deflate marker (when
|
|
27
|
+
* the extension was negotiated) or as a protocol error (when it was
|
|
28
|
+
* not).
|
|
29
|
+
*
|
|
30
|
+
* CFrame.build(opcode, payload, fin: true, mask: false, mask_key: nil,
|
|
31
|
+
* rsv1: false)
|
|
32
|
+
* -> String
|
|
33
|
+
* Builds a serialized frame ready for `socket.write`. Server frames are
|
|
34
|
+
* unmasked (`mask: false`, the default) per §5.1. Client frames must
|
|
35
|
+
* pass `mask: true` and a 4-byte `mask_key`. Control frames (close 0x8,
|
|
36
|
+
* ping 0x9, pong 0xA) MUST have `payload.bytesize <= 125` and MUST have
|
|
37
|
+
* `fin: true` and `rsv1: false` — this helper raises ArgumentError
|
|
38
|
+
* otherwise. Pass `rsv1: true` only on a text/binary frame whose
|
|
39
|
+
* payload is permessage-deflate compressed.
|
|
40
|
+
*
|
|
41
|
+
* Why C?
|
|
42
|
+
* The dominant CPU cost on the receive path is XOR-unmasking the
|
|
43
|
+
* payload. A tight uint32 loop in C is ~5–10× faster than Ruby's
|
|
44
|
+
* `unpack1('N*')` + manual XOR for typical 1 KiB–1 MiB messages.
|
|
45
|
+
* RFC 6455 framing itself is ~200 lines of C with no dependencies, so
|
|
46
|
+
* we keep parser+builder+unmask in one translation unit.
|
|
47
|
+
* ---------------------------------------------------------------------- */
|
|
48
|
+
|
|
49
|
+
#include <ruby.h>
|
|
50
|
+
#include <ruby/thread.h>
|
|
51
|
+
#include <ruby/encoding.h>
|
|
52
|
+
|
|
53
|
+
#include <stdint.h>
|
|
54
|
+
#include <stdlib.h>
|
|
55
|
+
#include <string.h>
|
|
56
|
+
|
|
57
|
+
/* Threshold above which CFrame.unmask releases the GVL. Below this,
|
|
58
|
+
* the GVL-release ceremony itself costs more than the XOR work. */
|
|
59
|
+
#define HYP_WS_GVL_RELEASE_THRESHOLD (64 * 1024)
|
|
60
|
+
|
|
61
|
+
/* Control-frame payload cap per RFC 6455 §5.5. */
|
|
62
|
+
#define HYP_WS_CONTROL_MAX_PAYLOAD 125
|
|
63
|
+
|
|
64
|
+
static VALUE rb_mHyperion;
|
|
65
|
+
static VALUE rb_mHyperionWebSocket;
|
|
66
|
+
static VALUE rb_cCFrame;
|
|
67
|
+
|
|
68
|
+
static VALUE sym_incomplete;
|
|
69
|
+
static VALUE sym_error;
|
|
70
|
+
|
|
71
|
+
/* ------------------------------------------------------------------ */
|
|
72
|
+
/* Unmask */
|
|
73
|
+
/* ------------------------------------------------------------------ */
|
|
74
|
+
|
|
75
|
+
typedef struct {
|
|
76
|
+
const uint8_t *src;
|
|
77
|
+
uint8_t *dst;
|
|
78
|
+
size_t len;
|
|
79
|
+
uint32_t key32; /* host-order uint32 with the 4 mask bytes smeared */
|
|
80
|
+
uint8_t key[4];
|
|
81
|
+
} unmask_args_t;
|
|
82
|
+
|
|
83
|
+
/* Core XOR loop. Word-at-a-time for the body, byte-by-byte for the
|
|
84
|
+
* 0..3-byte tail. We deliberately avoid SIMD intrinsics — they don't
|
|
85
|
+
* portably outperform a unaligned uint32 read+xor on the codegen path
|
|
86
|
+
* GCC/Clang produce for this inner loop, and they'd hurt portability. */
|
|
87
|
+
static void hyp_ws_xor_inplace(unmask_args_t *a) {
|
|
88
|
+
size_t len = a->len;
|
|
89
|
+
size_t words = len / 4;
|
|
90
|
+
size_t tail = len & 0x3;
|
|
91
|
+
|
|
92
|
+
/* Use memcpy() to avoid strict-aliasing UB on the unaligned uint32
|
|
93
|
+
* read. -O2 collapses the memcpy into a single mov on x86_64 and
|
|
94
|
+
* arm64. */
|
|
95
|
+
for (size_t i = 0; i < words; i++) {
|
|
96
|
+
uint32_t v;
|
|
97
|
+
memcpy(&v, a->src + i * 4, 4);
|
|
98
|
+
v ^= a->key32;
|
|
99
|
+
memcpy(a->dst + i * 4, &v, 4);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Tail: index into the original 4-byte key by absolute byte offset
|
|
103
|
+
* mod 4, per RFC 6455 §5.3. */
|
|
104
|
+
size_t base = words * 4;
|
|
105
|
+
for (size_t i = 0; i < tail; i++) {
|
|
106
|
+
a->dst[base + i] = a->src[base + i] ^ a->key[(base + i) & 0x3];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static void *hyp_ws_xor_blocking(void *raw) {
|
|
111
|
+
hyp_ws_xor_inplace((unmask_args_t *)raw);
|
|
112
|
+
return NULL;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static VALUE rb_cframe_unmask(VALUE self, VALUE rb_payload, VALUE rb_key) {
|
|
116
|
+
(void)self;
|
|
117
|
+
|
|
118
|
+
Check_Type(rb_payload, T_STRING);
|
|
119
|
+
Check_Type(rb_key, T_STRING);
|
|
120
|
+
|
|
121
|
+
long key_len = RSTRING_LEN(rb_key);
|
|
122
|
+
if (key_len != 4) {
|
|
123
|
+
rb_raise(rb_eArgError,
|
|
124
|
+
"Hyperion::WebSocket::CFrame.unmask: mask_key must be exactly "
|
|
125
|
+
"4 bytes (got %ld)", key_len);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
long payload_len = RSTRING_LEN(rb_payload);
|
|
129
|
+
/* Empty payload — return an empty binary String of the right encoding. */
|
|
130
|
+
if (payload_len == 0) {
|
|
131
|
+
VALUE empty = rb_str_new(NULL, 0);
|
|
132
|
+
rb_enc_associate(empty, rb_ascii8bit_encoding());
|
|
133
|
+
return empty;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
VALUE out = rb_str_new(NULL, payload_len);
|
|
137
|
+
rb_enc_associate(out, rb_ascii8bit_encoding());
|
|
138
|
+
|
|
139
|
+
unmask_args_t args;
|
|
140
|
+
args.src = (const uint8_t *)RSTRING_PTR(rb_payload);
|
|
141
|
+
args.dst = (uint8_t *)RSTRING_PTR(out);
|
|
142
|
+
args.len = (size_t)payload_len;
|
|
143
|
+
memcpy(args.key, RSTRING_PTR(rb_key), 4);
|
|
144
|
+
/* Smear the 4 mask bytes into a host-order uint32. memcpy() handles
|
|
145
|
+
* platform endianness — we treat the key as a stream of 4 bytes that
|
|
146
|
+
* we want to apply at offsets {0,1,2,3,4,5,...} ≡ {0,1,2,3,0,1,2,3,...},
|
|
147
|
+
* and reading 4 bytes at a time with memcpy preserves that pattern
|
|
148
|
+
* independent of endianness. */
|
|
149
|
+
memcpy(&args.key32, args.key, 4);
|
|
150
|
+
|
|
151
|
+
if ((size_t)payload_len > HYP_WS_GVL_RELEASE_THRESHOLD) {
|
|
152
|
+
rb_thread_call_without_gvl(hyp_ws_xor_blocking, &args, RUBY_UBF_IO, NULL);
|
|
153
|
+
} else {
|
|
154
|
+
hyp_ws_xor_inplace(&args);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Keep `rb_payload` and `out` alive across the GVL release. */
|
|
158
|
+
RB_GC_GUARD(rb_payload);
|
|
159
|
+
RB_GC_GUARD(rb_key);
|
|
160
|
+
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ------------------------------------------------------------------ */
|
|
165
|
+
/* Parse */
|
|
166
|
+
/* ------------------------------------------------------------------ */
|
|
167
|
+
|
|
168
|
+
/* Returns 1 if opcode is a control frame (0x8 close, 0x9 ping, 0xA pong),
|
|
169
|
+
* 0 otherwise. Per §5.5 control opcodes are 0x8..0xF; 0xB..0xF are
|
|
170
|
+
* reserved and rejected by the unknown-opcode check. */
|
|
171
|
+
static inline int hyp_ws_is_control(uint8_t opcode) {
|
|
172
|
+
return opcode >= 0x8;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static inline int hyp_ws_is_known_opcode(uint8_t opcode) {
|
|
176
|
+
/* 0x0 continuation, 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong. */
|
|
177
|
+
return opcode == 0x0 || opcode == 0x1 || opcode == 0x2 ||
|
|
178
|
+
opcode == 0x8 || opcode == 0x9 || opcode == 0xA;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static VALUE rb_cframe_parse(int argc, VALUE *argv, VALUE self) {
|
|
182
|
+
(void)self;
|
|
183
|
+
|
|
184
|
+
VALUE rb_buf, rb_offset;
|
|
185
|
+
rb_scan_args(argc, argv, "11", &rb_buf, &rb_offset);
|
|
186
|
+
|
|
187
|
+
Check_Type(rb_buf, T_STRING);
|
|
188
|
+
long offset = NIL_P(rb_offset) ? 0 : NUM2LONG(rb_offset);
|
|
189
|
+
if (offset < 0) {
|
|
190
|
+
rb_raise(rb_eArgError, "offset must be >= 0 (got %ld)", offset);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
long buf_len = RSTRING_LEN(rb_buf);
|
|
194
|
+
if (offset > buf_len) {
|
|
195
|
+
return sym_incomplete;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
long avail = buf_len - offset;
|
|
199
|
+
if (avail < 2) {
|
|
200
|
+
return sym_incomplete;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const uint8_t *p = (const uint8_t *)RSTRING_PTR(rb_buf) + offset;
|
|
204
|
+
uint8_t b0 = p[0];
|
|
205
|
+
uint8_t b1 = p[1];
|
|
206
|
+
|
|
207
|
+
int fin = (b0 & 0x80) != 0;
|
|
208
|
+
int rsv1 = (b0 & 0x40) != 0;
|
|
209
|
+
int rsv2 = (b0 & 0x20) != 0;
|
|
210
|
+
int rsv3 = (b0 & 0x10) != 0;
|
|
211
|
+
uint8_t opcode = b0 & 0x0F;
|
|
212
|
+
int masked = (b1 & 0x80) != 0;
|
|
213
|
+
uint8_t len7 = b1 & 0x7F;
|
|
214
|
+
|
|
215
|
+
/* RSV2/RSV3 are still reserved with no negotiated semantics; reject.
|
|
216
|
+
* RSV1 is the permessage-deflate marker (RFC 7692 §6) — allow it to
|
|
217
|
+
* pass through here so the Ruby façade can decide what to do based
|
|
218
|
+
* on whether the extension was negotiated for this connection. The
|
|
219
|
+
* Connection wrapper closes 1002 if it sees RSV1 without a
|
|
220
|
+
* negotiated extension. RSV1 on a control frame is always a
|
|
221
|
+
* protocol error per RFC 7692 §6.1 ("control frames MUST NOT be
|
|
222
|
+
* compressed"); we trap that one case below. */
|
|
223
|
+
if (rsv2 || rsv3) {
|
|
224
|
+
return sym_error;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!hyp_ws_is_known_opcode(opcode)) {
|
|
228
|
+
return sym_error;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* §5.5 — control frames MUST have FIN=1 and len <= 125. RFC 7692
|
|
232
|
+
* §6.1 — control frames MUST NOT have RSV1 set. */
|
|
233
|
+
if (hyp_ws_is_control(opcode)) {
|
|
234
|
+
if (!fin) {
|
|
235
|
+
return sym_error;
|
|
236
|
+
}
|
|
237
|
+
if (len7 > HYP_WS_CONTROL_MAX_PAYLOAD) {
|
|
238
|
+
return sym_error;
|
|
239
|
+
}
|
|
240
|
+
if (rsv1) {
|
|
241
|
+
return sym_error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
long header_len = 2;
|
|
246
|
+
uint64_t payload_len64 = 0;
|
|
247
|
+
|
|
248
|
+
if (len7 < 126) {
|
|
249
|
+
payload_len64 = len7;
|
|
250
|
+
} else if (len7 == 126) {
|
|
251
|
+
if (avail < header_len + 2) return sym_incomplete;
|
|
252
|
+
payload_len64 = ((uint64_t)p[2] << 8) | (uint64_t)p[3];
|
|
253
|
+
header_len += 2;
|
|
254
|
+
} else { /* len7 == 127 */
|
|
255
|
+
if (avail < header_len + 8) return sym_incomplete;
|
|
256
|
+
/* Network byte order; high bit MUST be 0 per RFC 6455 §5.2. */
|
|
257
|
+
if (p[2] & 0x80) {
|
|
258
|
+
return sym_error;
|
|
259
|
+
}
|
|
260
|
+
payload_len64 =
|
|
261
|
+
((uint64_t)p[2] << 56) | ((uint64_t)p[3] << 48) |
|
|
262
|
+
((uint64_t)p[4] << 40) | ((uint64_t)p[5] << 32) |
|
|
263
|
+
((uint64_t)p[6] << 24) | ((uint64_t)p[7] << 16) |
|
|
264
|
+
((uint64_t)p[8] << 8) | (uint64_t)p[9];
|
|
265
|
+
header_len += 8;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
VALUE rb_mask_key = Qnil;
|
|
269
|
+
if (masked) {
|
|
270
|
+
if (avail < header_len + 4) return sym_incomplete;
|
|
271
|
+
rb_mask_key = rb_str_new((const char *)(p + header_len), 4);
|
|
272
|
+
rb_enc_associate(rb_mask_key, rb_ascii8bit_encoding());
|
|
273
|
+
header_len += 4;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* Bound payload_len64: a frame_total_len that overflows a Ruby
|
|
277
|
+
* Integer is fine (Bignum), but we still want to make sure the
|
|
278
|
+
* header_len + payload_len arithmetic below doesn't overflow size_t
|
|
279
|
+
* on 32-bit hosts. On 64-bit Linux/Darwin this is academic; the
|
|
280
|
+
* cap is 2^63-1 and Ruby Strings can't be larger than LONG_MAX
|
|
281
|
+
* anyway. */
|
|
282
|
+
if (payload_len64 > (uint64_t)LONG_MAX - (uint64_t)header_len) {
|
|
283
|
+
return sym_error;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* Incomplete payload — the caller should buffer more bytes. */
|
|
287
|
+
long payload_offset_abs = offset + header_len;
|
|
288
|
+
long frame_total_len = header_len + (long)payload_len64;
|
|
289
|
+
if (avail < frame_total_len) {
|
|
290
|
+
return sym_incomplete;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
VALUE result = rb_ary_new_capa(8);
|
|
294
|
+
rb_ary_push(result, fin ? Qtrue : Qfalse);
|
|
295
|
+
rb_ary_push(result, INT2FIX(opcode));
|
|
296
|
+
rb_ary_push(result, ULL2NUM(payload_len64));
|
|
297
|
+
rb_ary_push(result, masked ? Qtrue : Qfalse);
|
|
298
|
+
rb_ary_push(result, rb_mask_key);
|
|
299
|
+
rb_ary_push(result, LONG2NUM(payload_offset_abs));
|
|
300
|
+
rb_ary_push(result, LONG2NUM(frame_total_len));
|
|
301
|
+
rb_ary_push(result, rsv1 ? Qtrue : Qfalse);
|
|
302
|
+
|
|
303
|
+
RB_GC_GUARD(rb_buf);
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ------------------------------------------------------------------ */
|
|
308
|
+
/* Build */
|
|
309
|
+
/* ------------------------------------------------------------------ */
|
|
310
|
+
|
|
311
|
+
static ID id_kw_fin;
|
|
312
|
+
static ID id_kw_mask;
|
|
313
|
+
static ID id_kw_mask_key;
|
|
314
|
+
static ID id_kw_rsv1;
|
|
315
|
+
|
|
316
|
+
static VALUE rb_cframe_build(int argc, VALUE *argv, VALUE self) {
|
|
317
|
+
(void)self;
|
|
318
|
+
|
|
319
|
+
VALUE rb_opcode, rb_payload, rb_kwargs;
|
|
320
|
+
rb_scan_args(argc, argv, "20:", &rb_opcode, &rb_payload, &rb_kwargs);
|
|
321
|
+
|
|
322
|
+
long opcode_l = NUM2LONG(rb_opcode);
|
|
323
|
+
if (opcode_l < 0 || opcode_l > 0xF) {
|
|
324
|
+
rb_raise(rb_eArgError, "opcode must be 0..15 (got %ld)", opcode_l);
|
|
325
|
+
}
|
|
326
|
+
uint8_t opcode = (uint8_t)opcode_l;
|
|
327
|
+
if (!hyp_ws_is_known_opcode(opcode)) {
|
|
328
|
+
rb_raise(rb_eArgError, "unknown opcode 0x%x", (unsigned)opcode);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
Check_Type(rb_payload, T_STRING);
|
|
332
|
+
long payload_len = RSTRING_LEN(rb_payload);
|
|
333
|
+
|
|
334
|
+
int fin = 1;
|
|
335
|
+
int mask = 0;
|
|
336
|
+
int rsv1 = 0;
|
|
337
|
+
VALUE rb_mask_key = Qnil;
|
|
338
|
+
|
|
339
|
+
if (!NIL_P(rb_kwargs)) {
|
|
340
|
+
VALUE kw_vals[4] = { Qundef, Qundef, Qundef, Qundef };
|
|
341
|
+
ID kw_ids[4] = { id_kw_fin, id_kw_mask, id_kw_mask_key, id_kw_rsv1 };
|
|
342
|
+
rb_get_kwargs(rb_kwargs, kw_ids, 0, 4, kw_vals);
|
|
343
|
+
if (kw_vals[0] != Qundef) {
|
|
344
|
+
fin = RTEST(kw_vals[0]) ? 1 : 0;
|
|
345
|
+
}
|
|
346
|
+
if (kw_vals[1] != Qundef) {
|
|
347
|
+
mask = RTEST(kw_vals[1]) ? 1 : 0;
|
|
348
|
+
}
|
|
349
|
+
if (kw_vals[2] != Qundef) {
|
|
350
|
+
rb_mask_key = kw_vals[2];
|
|
351
|
+
}
|
|
352
|
+
if (kw_vals[3] != Qundef) {
|
|
353
|
+
rsv1 = RTEST(kw_vals[3]) ? 1 : 0;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* §5.5 control-frame validation. RFC 7692 §6.1 — control frames
|
|
358
|
+
* MUST NOT have RSV1 set. */
|
|
359
|
+
if (hyp_ws_is_control(opcode)) {
|
|
360
|
+
if (!fin) {
|
|
361
|
+
rb_raise(rb_eArgError,
|
|
362
|
+
"control frame (opcode 0x%x) MUST have fin=true",
|
|
363
|
+
(unsigned)opcode);
|
|
364
|
+
}
|
|
365
|
+
if (payload_len > HYP_WS_CONTROL_MAX_PAYLOAD) {
|
|
366
|
+
rb_raise(rb_eArgError,
|
|
367
|
+
"control frame (opcode 0x%x) payload %ld exceeds 125-byte cap",
|
|
368
|
+
(unsigned)opcode, payload_len);
|
|
369
|
+
}
|
|
370
|
+
if (rsv1) {
|
|
371
|
+
rb_raise(rb_eArgError,
|
|
372
|
+
"control frame (opcode 0x%x) MUST NOT have rsv1=true",
|
|
373
|
+
(unsigned)opcode);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (mask) {
|
|
378
|
+
if (NIL_P(rb_mask_key)) {
|
|
379
|
+
rb_raise(rb_eArgError, "mask: true requires a 4-byte mask_key");
|
|
380
|
+
}
|
|
381
|
+
Check_Type(rb_mask_key, T_STRING);
|
|
382
|
+
if (RSTRING_LEN(rb_mask_key) != 4) {
|
|
383
|
+
rb_raise(rb_eArgError, "mask_key must be exactly 4 bytes (got %ld)",
|
|
384
|
+
RSTRING_LEN(rb_mask_key));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* Compute header length. */
|
|
389
|
+
long header_len = 2;
|
|
390
|
+
if (payload_len < 126) {
|
|
391
|
+
/* 7-bit length encoded inline. */
|
|
392
|
+
} else if (payload_len <= 0xFFFF) {
|
|
393
|
+
header_len += 2;
|
|
394
|
+
} else {
|
|
395
|
+
header_len += 8;
|
|
396
|
+
}
|
|
397
|
+
if (mask) header_len += 4;
|
|
398
|
+
|
|
399
|
+
long frame_len = header_len + payload_len;
|
|
400
|
+
VALUE out = rb_str_new(NULL, frame_len);
|
|
401
|
+
rb_enc_associate(out, rb_ascii8bit_encoding());
|
|
402
|
+
uint8_t *q = (uint8_t *)RSTRING_PTR(out);
|
|
403
|
+
|
|
404
|
+
q[0] = (uint8_t)((fin ? 0x80 : 0x00) | (rsv1 ? 0x40 : 0x00) | (opcode & 0x0F));
|
|
405
|
+
uint8_t mask_bit = mask ? 0x80 : 0x00;
|
|
406
|
+
|
|
407
|
+
long body_offset;
|
|
408
|
+
if (payload_len < 126) {
|
|
409
|
+
q[1] = mask_bit | (uint8_t)payload_len;
|
|
410
|
+
body_offset = 2;
|
|
411
|
+
} else if (payload_len <= 0xFFFF) {
|
|
412
|
+
q[1] = mask_bit | 126;
|
|
413
|
+
q[2] = (uint8_t)((payload_len >> 8) & 0xFF);
|
|
414
|
+
q[3] = (uint8_t)(payload_len & 0xFF);
|
|
415
|
+
body_offset = 4;
|
|
416
|
+
} else {
|
|
417
|
+
q[1] = mask_bit | 127;
|
|
418
|
+
uint64_t pl = (uint64_t)payload_len;
|
|
419
|
+
q[2] = (uint8_t)((pl >> 56) & 0xFF);
|
|
420
|
+
q[3] = (uint8_t)((pl >> 48) & 0xFF);
|
|
421
|
+
q[4] = (uint8_t)((pl >> 40) & 0xFF);
|
|
422
|
+
q[5] = (uint8_t)((pl >> 32) & 0xFF);
|
|
423
|
+
q[6] = (uint8_t)((pl >> 24) & 0xFF);
|
|
424
|
+
q[7] = (uint8_t)((pl >> 16) & 0xFF);
|
|
425
|
+
q[8] = (uint8_t)((pl >> 8) & 0xFF);
|
|
426
|
+
q[9] = (uint8_t)(pl & 0xFF);
|
|
427
|
+
body_offset = 10;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (mask) {
|
|
431
|
+
memcpy(q + body_offset, RSTRING_PTR(rb_mask_key), 4);
|
|
432
|
+
body_offset += 4;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (payload_len > 0) {
|
|
436
|
+
if (mask) {
|
|
437
|
+
/* Reuse the unmask kernel — XOR is symmetric. */
|
|
438
|
+
unmask_args_t args;
|
|
439
|
+
args.src = (const uint8_t *)RSTRING_PTR(rb_payload);
|
|
440
|
+
args.dst = q + body_offset;
|
|
441
|
+
args.len = (size_t)payload_len;
|
|
442
|
+
memcpy(args.key, RSTRING_PTR(rb_mask_key), 4);
|
|
443
|
+
memcpy(&args.key32, args.key, 4);
|
|
444
|
+
if ((size_t)payload_len > HYP_WS_GVL_RELEASE_THRESHOLD) {
|
|
445
|
+
rb_thread_call_without_gvl(hyp_ws_xor_blocking, &args, RUBY_UBF_IO, NULL);
|
|
446
|
+
} else {
|
|
447
|
+
hyp_ws_xor_inplace(&args);
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
memcpy(q + body_offset, RSTRING_PTR(rb_payload), (size_t)payload_len);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
RB_GC_GUARD(rb_payload);
|
|
455
|
+
RB_GC_GUARD(rb_mask_key);
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* ------------------------------------------------------------------ */
|
|
460
|
+
/* Init */
|
|
461
|
+
/* ------------------------------------------------------------------ */
|
|
462
|
+
|
|
463
|
+
void Init_hyperion_websocket(void) {
|
|
464
|
+
rb_mHyperion = rb_const_get(rb_cObject, rb_intern("Hyperion"));
|
|
465
|
+
|
|
466
|
+
if (rb_const_defined(rb_mHyperion, rb_intern("WebSocket"))) {
|
|
467
|
+
rb_mHyperionWebSocket = rb_const_get(rb_mHyperion, rb_intern("WebSocket"));
|
|
468
|
+
} else {
|
|
469
|
+
rb_mHyperionWebSocket = rb_define_module_under(rb_mHyperion, "WebSocket");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
rb_cCFrame = rb_define_module_under(rb_mHyperionWebSocket, "CFrame");
|
|
473
|
+
|
|
474
|
+
rb_define_singleton_method(rb_cCFrame, "unmask", rb_cframe_unmask, 2);
|
|
475
|
+
rb_define_singleton_method(rb_cCFrame, "parse", rb_cframe_parse, -1);
|
|
476
|
+
rb_define_singleton_method(rb_cCFrame, "build", rb_cframe_build, -1);
|
|
477
|
+
|
|
478
|
+
sym_incomplete = ID2SYM(rb_intern("incomplete"));
|
|
479
|
+
sym_error = ID2SYM(rb_intern("error"));
|
|
480
|
+
rb_gc_register_mark_object(sym_incomplete);
|
|
481
|
+
rb_gc_register_mark_object(sym_error);
|
|
482
|
+
|
|
483
|
+
id_kw_fin = rb_intern("fin");
|
|
484
|
+
id_kw_mask = rb_intern("mask");
|
|
485
|
+
id_kw_mask_key = rb_intern("mask_key");
|
|
486
|
+
id_kw_rsv1 = rb_intern("rsv1");
|
|
487
|
+
|
|
488
|
+
/* Expose constants for the Ruby façade & specs. */
|
|
489
|
+
rb_define_const(rb_cCFrame, "GVL_RELEASE_THRESHOLD",
|
|
490
|
+
INT2NUM(HYP_WS_GVL_RELEASE_THRESHOLD));
|
|
491
|
+
rb_define_const(rb_cCFrame, "CONTROL_MAX_PAYLOAD",
|
|
492
|
+
INT2NUM(HYP_WS_CONTROL_MAX_PAYLOAD));
|
|
493
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 3
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "bitflags"
|
|
7
|
+
version = "1.3.2"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "hyperion_io_uring"
|
|
13
|
+
version = "2.3.0"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"io-uring",
|
|
16
|
+
"libc",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[package]]
|
|
20
|
+
name = "io-uring"
|
|
21
|
+
version = "0.6.4"
|
|
22
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
23
|
+
checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"bitflags",
|
|
26
|
+
"libc",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "libc"
|
|
31
|
+
version = "0.2.186"
|
|
32
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "hyperion_io_uring"
|
|
3
|
+
version = "2.3.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
description = "io_uring accept/read primitives for Hyperion (2.3-A, Linux 5.6+ only)"
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
name = "hyperion_io_uring"
|
|
11
|
+
|
|
12
|
+
# Linux-only dep. io-uring (https://docs.rs/io-uring) is the
|
|
13
|
+
# well-maintained safe Rust wrapper around liburing — owns the ring
|
|
14
|
+
# lifecycle, SQE/CQE submission, kernel-feature probing, and the dance
|
|
15
|
+
# of io_uring's evolving feature set across kernel versions. We pull
|
|
16
|
+
# it in only on Linux so the macOS dev build still cargo-checks
|
|
17
|
+
# cleanly; on Darwin the cdylib compiles down to stubbed
|
|
18
|
+
# `extern "C"` entry points that always return -ENOSYS, mirroring the
|
|
19
|
+
# kernel surface we'd see on Linux < 5.6.
|
|
20
|
+
#
|
|
21
|
+
# 2.3-A keeps the surface intentionally small: ring lifecycle (new /
|
|
22
|
+
# close), one accept submission API, and a feature-gate hook used by
|
|
23
|
+
# `Hyperion::IOUring.supported?`. fix-A read_into hooks are wired but
|
|
24
|
+
# not exercised on the wire path yet; future 2.3-x rounds will plumb
|
|
25
|
+
# them once the accept-only path is field-validated.
|
|
26
|
+
[target.'cfg(target_os = "linux")'.dependencies]
|
|
27
|
+
io-uring = "0.6"
|
|
28
|
+
libc = "0.2"
|
|
29
|
+
|
|
30
|
+
[profile.release]
|
|
31
|
+
opt-level = 3
|
|
32
|
+
lto = true
|
|
33
|
+
codegen-units = 1
|
|
34
|
+
strip = true
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Build the hyperion_io_uring Rust extension (2.3-A).
|
|
4
|
+
#
|
|
5
|
+
# Mirrors `ext/hyperion_h2_codec/extconf.rb`: shells out to
|
|
6
|
+
# `cargo build --release`, drops the resulting cdylib into
|
|
7
|
+
# `lib/hyperion_io_uring/`, and writes a no-op Makefile so `gem
|
|
8
|
+
# install` succeeds.
|
|
9
|
+
#
|
|
10
|
+
# Cargo is OPTIONAL. If it's missing, the extconf writes a stub
|
|
11
|
+
# Makefile that prints a friendly note and exits cleanly — Hyperion
|
|
12
|
+
# still ships and `Hyperion::IOUring.supported?` returns false on
|
|
13
|
+
# both Darwin (no kernel support anyway) and Linux hosts that lack
|
|
14
|
+
# Rust. Operators who want the perf bump install Rust via `rustup`
|
|
15
|
+
# and `gem pristine hyperion-rb` to rebuild.
|
|
16
|
+
#
|
|
17
|
+
# Cross-platform notes:
|
|
18
|
+
# * Linux + GNU libc: cargo emits `libhyperion_io_uring.so`.
|
|
19
|
+
# * macOS: `libhyperion_io_uring.dylib`.
|
|
20
|
+
# * The Linux-only `io-uring` crate is gated via Cargo
|
|
21
|
+
# `target.'cfg(target_os = "linux")'`, so the macOS build
|
|
22
|
+
# compiles cleanly but every entry point returns -ENOSYS. The
|
|
23
|
+
# Ruby loader checks the OS first via `IOUring.supported?` and
|
|
24
|
+
# never reaches those stubs.
|
|
25
|
+
|
|
26
|
+
require 'mkmf'
|
|
27
|
+
require 'fileutils'
|
|
28
|
+
require 'rbconfig'
|
|
29
|
+
|
|
30
|
+
ext_dir = __dir__
|
|
31
|
+
crate_dir = ext_dir
|
|
32
|
+
target_dir = File.join(crate_dir, 'target', 'release')
|
|
33
|
+
gem_lib_dir = File.expand_path('../../lib/hyperion_io_uring', __dir__)
|
|
34
|
+
|
|
35
|
+
cargo_present = system('cargo --version > /dev/null 2>&1')
|
|
36
|
+
|
|
37
|
+
if cargo_present
|
|
38
|
+
warn '[hyperion_io_uring] cargo detected — building io_uring accept extension'
|
|
39
|
+
Dir.chdir(crate_dir) do
|
|
40
|
+
ok = system('cargo build --release')
|
|
41
|
+
unless ok
|
|
42
|
+
warn '[hyperion_io_uring] cargo build failed; falling back to epoll accept path'
|
|
43
|
+
cargo_present = false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
FileUtils.mkdir_p(gem_lib_dir)
|
|
49
|
+
|
|
50
|
+
if cargo_present
|
|
51
|
+
candidates = %w[libhyperion_io_uring.dylib libhyperion_io_uring.so]
|
|
52
|
+
found = candidates.find { |c| File.exist?(File.join(target_dir, c)) }
|
|
53
|
+
if found
|
|
54
|
+
src = File.join(target_dir, found)
|
|
55
|
+
dst = File.join(gem_lib_dir, found)
|
|
56
|
+
FileUtils.cp(src, dst)
|
|
57
|
+
warn "[hyperion_io_uring] installed #{dst}"
|
|
58
|
+
else
|
|
59
|
+
warn '[hyperion_io_uring] cargo finished but no cdylib artifact found; falling back'
|
|
60
|
+
cargo_present = false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Always emit a Makefile — gem install protocol expects one. The body
|
|
65
|
+
# is a no-op when cargo isn't present so `make` exits 0 and gem
|
|
66
|
+
# install completes.
|
|
67
|
+
File.open(File.join(ext_dir, 'Makefile'), 'w') do |f|
|
|
68
|
+
f.puts 'all:'
|
|
69
|
+
f.puts "\t@echo \"[hyperion_io_uring] no-op make (cargo handled the build)\""
|
|
70
|
+
f.puts 'clean:'
|
|
71
|
+
f.puts "\t@rm -rf target"
|
|
72
|
+
f.puts 'install:'
|
|
73
|
+
f.puts "\t@echo \"[hyperion_io_uring] no-op install (artifact already in lib/)\""
|
|
74
|
+
end
|