hyperion-rb 1.0.0.rc17

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.
@@ -0,0 +1,428 @@
1
+ #include <ruby.h>
2
+ #include <ruby/encoding.h>
3
+ #include <string.h>
4
+ #include "llhttp.h"
5
+
6
+ /* ----------------------------------------------------------------------
7
+ * Hyperion::CParser — C extension wrapping llhttp.
8
+ *
9
+ * Public surface matches Hyperion::Parser:
10
+ * parser.parse(buffer) -> [Request, end_offset]
11
+ *
12
+ * On parse error: raise Hyperion::ParseError.
13
+ * On unsupported: raise Hyperion::UnsupportedError.
14
+ * On success: returns [Request, end_offset] where end_offset is the
15
+ * number of bytes consumed from `buffer`.
16
+ *
17
+ * Implementation: each #parse call instantiates a fresh llhttp_t state
18
+ * on the stack; pooling comes in Phase 5. Callbacks accumulate fields
19
+ * into a parser_state_t struct. When llhttp signals message-complete,
20
+ * we build the Ruby Request and return.
21
+ * ---------------------------------------------------------------------- */
22
+
23
+ static VALUE rb_mHyperion;
24
+ static VALUE rb_cCParser;
25
+ static VALUE rb_cRequest;
26
+ static VALUE rb_eParseError;
27
+ static VALUE rb_eUnsupportedError;
28
+
29
+ static ID id_new;
30
+ static ID id_downcase;
31
+ static ID id_method_kw;
32
+ static ID id_path_kw;
33
+ static ID id_query_string_kw;
34
+ static ID id_http_version_kw;
35
+ static ID id_headers_kw;
36
+ static ID id_body_kw;
37
+
38
+ typedef struct {
39
+ /* Request line + headers */
40
+ VALUE method;
41
+ VALUE path;
42
+ VALUE query_string;
43
+ VALUE http_version;
44
+ VALUE headers; /* Hash, lowercase keys */
45
+ VALUE body; /* String */
46
+
47
+ /* Header parsing scratch */
48
+ VALUE current_header_name;
49
+ VALUE current_header_value;
50
+
51
+ /* Flags */
52
+ int message_complete;
53
+ int has_content_length;
54
+ int has_transfer_encoding;
55
+ int chunked_transfer_encoding;
56
+ int parse_error; /* 1 = parse, 2 = unsupported */
57
+ const char *error_message;
58
+ } parser_state_t;
59
+
60
+ static void state_init(parser_state_t *s) {
61
+ s->method = Qnil;
62
+ s->path = rb_str_new_cstr("");
63
+ s->query_string = rb_str_new_cstr("");
64
+ s->http_version = rb_str_new_cstr("HTTP/1.1");
65
+ s->headers = rb_hash_new();
66
+ s->body = rb_str_new_cstr("");
67
+ s->current_header_name = rb_str_new_cstr("");
68
+ s->current_header_value = rb_str_new_cstr("");
69
+ s->message_complete = 0;
70
+ s->has_content_length = 0;
71
+ s->has_transfer_encoding = 0;
72
+ s->chunked_transfer_encoding = 0;
73
+ s->parse_error = 0;
74
+ s->error_message = NULL;
75
+ }
76
+
77
+ /* Cap each individual field so we don't OOM on adversarial input. */
78
+ #define MAX_FIELD_BYTES (64 * 1024)
79
+ #define MAX_BODY_BYTES (16 * 1024 * 1024)
80
+
81
+ #define APPEND_OR_FAIL(dst, at, length, cap, who) do { \
82
+ if (RSTRING_LEN(dst) + (long)(length) > (long)(cap)) { \
83
+ s->parse_error = 1; \
84
+ s->error_message = (who " too large"); \
85
+ return -1; \
86
+ } \
87
+ rb_str_cat(dst, at, length); \
88
+ } while (0)
89
+
90
+ static void stash_pending_header(parser_state_t *s) {
91
+ if (RSTRING_LEN(s->current_header_name) > 0) {
92
+ VALUE downcased = rb_funcall(s->current_header_name, id_downcase, 0);
93
+ rb_hash_aset(s->headers, downcased, s->current_header_value);
94
+ s->current_header_name = rb_str_new_cstr("");
95
+ s->current_header_value = rb_str_new_cstr("");
96
+ }
97
+ }
98
+
99
+ static int on_url(llhttp_t *p, const char *at, size_t length) {
100
+ parser_state_t *s = (parser_state_t *)p->data;
101
+ APPEND_OR_FAIL(s->path, at, length, MAX_FIELD_BYTES, "url");
102
+ return 0;
103
+ }
104
+
105
+ static int on_url_complete(llhttp_t *p) {
106
+ parser_state_t *s = (parser_state_t *)p->data;
107
+ /* Split path?query. */
108
+ char *full = RSTRING_PTR(s->path);
109
+ long full_len = RSTRING_LEN(s->path);
110
+ long q_idx = -1;
111
+ for (long i = 0; i < full_len; i++) {
112
+ if (full[i] == '?') { q_idx = i; break; }
113
+ }
114
+ if (q_idx >= 0) {
115
+ s->query_string = rb_str_new(full + q_idx + 1, full_len - q_idx - 1);
116
+ rb_str_set_len(s->path, q_idx);
117
+ }
118
+ return 0;
119
+ }
120
+
121
+ static int on_method(llhttp_t *p, const char *at, size_t length) {
122
+ parser_state_t *s = (parser_state_t *)p->data;
123
+ if (NIL_P(s->method)) {
124
+ s->method = rb_str_new(at, length);
125
+ } else {
126
+ APPEND_OR_FAIL(s->method, at, length, 32, "method");
127
+ }
128
+ return 0;
129
+ }
130
+
131
+ static int on_version(llhttp_t *p, const char *at, size_t length) {
132
+ /* llhttp gives us "1.1"; we prepend "HTTP/" ourselves. */
133
+ parser_state_t *s = (parser_state_t *)p->data;
134
+ s->http_version = rb_str_new_cstr("HTTP/");
135
+ rb_str_cat(s->http_version, at, length);
136
+ return 0;
137
+ }
138
+
139
+ static int on_header_field(llhttp_t *p, const char *at, size_t length) {
140
+ parser_state_t *s = (parser_state_t *)p->data;
141
+ /* If current_header_value is non-empty, we just finished a header. */
142
+ if (RSTRING_LEN(s->current_header_value) > 0) {
143
+ stash_pending_header(s);
144
+ }
145
+ if (RSTRING_LEN(s->current_header_name) == 0) {
146
+ s->current_header_name = rb_str_new(at, length);
147
+ } else {
148
+ APPEND_OR_FAIL(s->current_header_name, at, length, MAX_FIELD_BYTES, "header name");
149
+ }
150
+ return 0;
151
+ }
152
+
153
+ static int on_header_value(llhttp_t *p, const char *at, size_t length) {
154
+ parser_state_t *s = (parser_state_t *)p->data;
155
+ if (RSTRING_LEN(s->current_header_value) == 0) {
156
+ s->current_header_value = rb_str_new(at, length);
157
+ } else {
158
+ APPEND_OR_FAIL(s->current_header_value, at, length, MAX_FIELD_BYTES, "header value");
159
+ }
160
+ return 0;
161
+ }
162
+
163
+ static int on_headers_complete(llhttp_t *p) {
164
+ parser_state_t *s = (parser_state_t *)p->data;
165
+ stash_pending_header(s);
166
+
167
+ /* Smuggling defense: both Content-Length and Transfer-Encoding present. */
168
+ VALUE cl_key = rb_str_new_cstr("content-length");
169
+ VALUE te_key = rb_str_new_cstr("transfer-encoding");
170
+ VALUE cl = rb_hash_aref(s->headers, cl_key);
171
+ VALUE te = rb_hash_aref(s->headers, te_key);
172
+ s->has_content_length = !NIL_P(cl);
173
+ s->has_transfer_encoding = !NIL_P(te);
174
+ if (s->has_content_length && s->has_transfer_encoding) {
175
+ s->parse_error = 1;
176
+ s->error_message = "both Content-Length and Transfer-Encoding present (smuggling defense)";
177
+ return -1;
178
+ }
179
+
180
+ /* Verify TE: only chunked (or comma-list ending in chunked) is supported. */
181
+ if (s->has_transfer_encoding) {
182
+ VALUE te_lower = rb_funcall(te, id_downcase, 0);
183
+ const char *te_str = RSTRING_PTR(te_lower);
184
+ long te_len = RSTRING_LEN(te_lower);
185
+ /* Trim trailing whitespace. */
186
+ while (te_len > 0 && (te_str[te_len - 1] == ' ' || te_str[te_len - 1] == '\t')) {
187
+ te_len--;
188
+ }
189
+ if (te_len < 7 || strncmp(te_str + te_len - 7, "chunked", 7) != 0) {
190
+ s->parse_error = 2;
191
+ s->error_message = "Transfer-Encoding not supported (only chunked)";
192
+ return -1;
193
+ }
194
+ s->chunked_transfer_encoding = 1;
195
+ }
196
+
197
+ return 0;
198
+ }
199
+
200
+ static int on_body(llhttp_t *p, const char *at, size_t length) {
201
+ parser_state_t *s = (parser_state_t *)p->data;
202
+ APPEND_OR_FAIL(s->body, at, length, MAX_BODY_BYTES, "body");
203
+ return 0;
204
+ }
205
+
206
+ static int on_message_complete(llhttp_t *p) {
207
+ parser_state_t *s = (parser_state_t *)p->data;
208
+ s->message_complete = 1;
209
+ /* Returning HPE_PAUSED halts llhttp_execute immediately at the message
210
+ * boundary; llhttp_get_error_pos then points to the next byte (start of
211
+ * the next pipelined request, if any). Without this, llhttp continues
212
+ * parsing the second message in-place, smearing method/path/etc. */
213
+ (void)p;
214
+ return HPE_PAUSED;
215
+ }
216
+
217
+ static llhttp_settings_t settings;
218
+
219
+ static void install_settings(void) {
220
+ llhttp_settings_init(&settings);
221
+ settings.on_url = on_url;
222
+ settings.on_url_complete = on_url_complete;
223
+ settings.on_method = on_method;
224
+ settings.on_version = on_version;
225
+ settings.on_header_field = on_header_field;
226
+ settings.on_header_value = on_header_value;
227
+ settings.on_headers_complete = on_headers_complete;
228
+ settings.on_body = on_body;
229
+ settings.on_message_complete = on_message_complete;
230
+ }
231
+
232
+ /* parse(buffer) -> [Request, end_offset]
233
+ *
234
+ * Parse one complete HTTP/1.1 request from `buffer`. If buffer doesn't yet
235
+ * contain a complete request, raise ParseError("incomplete"). For pipelined
236
+ * input, end_offset is the byte boundary of the first request — Connection
237
+ * carries the rest forward.
238
+ */
239
+ static VALUE cparser_parse(VALUE self, VALUE buffer) {
240
+ Check_Type(buffer, T_STRING);
241
+ (void)self;
242
+
243
+ parser_state_t s;
244
+ state_init(&s);
245
+
246
+ llhttp_t parser;
247
+ llhttp_init(&parser, HTTP_REQUEST, &settings);
248
+ parser.data = &s;
249
+
250
+ const char *data = RSTRING_PTR(buffer);
251
+ size_t len = (size_t)RSTRING_LEN(buffer);
252
+
253
+ enum llhttp_errno err = llhttp_execute(&parser, data, len);
254
+
255
+ /* Custom error flags (set inside callbacks) take precedence. */
256
+ if (s.parse_error == 2) {
257
+ rb_raise(rb_eUnsupportedError, "%s", s.error_message);
258
+ }
259
+ if (s.parse_error == 1) {
260
+ rb_raise(rb_eParseError, "%s", s.error_message);
261
+ }
262
+
263
+ if (err == HPE_PAUSED_UPGRADE) {
264
+ rb_raise(rb_eUnsupportedError, "Upgrade not supported");
265
+ }
266
+ if (err != HPE_OK && err != HPE_PAUSED) {
267
+ const char *reason = llhttp_get_error_reason(&parser);
268
+ rb_raise(rb_eParseError, "llhttp: %s",
269
+ (reason && *reason) ? reason : llhttp_errno_name(err));
270
+ }
271
+
272
+ if (!s.message_complete) {
273
+ rb_raise(rb_eParseError, "incomplete request");
274
+ }
275
+
276
+ /* Compute end_offset. We pause inside on_message_complete, so
277
+ * llhttp_get_error_pos returns the byte just after the message
278
+ * boundary — exactly the carry-over offset we want. */
279
+ size_t consumed;
280
+ if (err == HPE_PAUSED) {
281
+ const char *epos = llhttp_get_error_pos(&parser);
282
+ consumed = epos ? (size_t)(epos - data) : len;
283
+ } else {
284
+ consumed = len;
285
+ }
286
+
287
+ /* Build the Request. */
288
+ VALUE kwargs = rb_hash_new();
289
+ rb_hash_aset(kwargs, ID2SYM(id_method_kw), s.method);
290
+ rb_hash_aset(kwargs, ID2SYM(id_path_kw), s.path);
291
+ rb_hash_aset(kwargs, ID2SYM(id_query_string_kw), s.query_string);
292
+ rb_hash_aset(kwargs, ID2SYM(id_http_version_kw), s.http_version);
293
+ rb_hash_aset(kwargs, ID2SYM(id_headers_kw), s.headers);
294
+ rb_hash_aset(kwargs, ID2SYM(id_body_kw), s.body);
295
+
296
+ VALUE args[1] = { kwargs };
297
+ VALUE request = rb_funcallv_kw(rb_cRequest, id_new, 1, args, RB_PASS_KEYWORDS);
298
+
299
+ return rb_ary_new_from_args(2, request, ULONG2NUM((unsigned long)consumed));
300
+ }
301
+
302
+ /* Hyperion::CParser.build_response_head(status, reason, headers, body_size,
303
+ * keep_alive, date_str) -> String
304
+ *
305
+ * Builds the HTTP/1.1 response head:
306
+ * "HTTP/1.1 <status> <reason>\r\n"
307
+ * "<lowercased-key>: <value>\r\n" for each user header (except
308
+ * content-length / connection — we always set these from the framing
309
+ * args below, mirroring the rc16 Ruby behaviour where the normalized
310
+ * hash is overridden in place).
311
+ * "content-length: <body_size>\r\n"
312
+ * "connection: <close|keep-alive>\r\n"
313
+ * "date: <date_str>\r\n" (only if user headers didn't include 'date')
314
+ * "\r\n"
315
+ *
316
+ * Header values containing CR/LF raise ArgumentError (response-splitting
317
+ * guard). Bypasses Ruby Hash#each + per-line String#<< allocation; the
318
+ * status line, framing headers, and join slices live in C buffers.
319
+ */
320
+ static VALUE cbuild_response_head(VALUE self, VALUE rb_status, VALUE rb_reason,
321
+ VALUE rb_headers, VALUE rb_body_size,
322
+ VALUE rb_keep_alive, VALUE rb_date) {
323
+ (void)self;
324
+ Check_Type(rb_headers, T_HASH);
325
+ Check_Type(rb_reason, T_STRING);
326
+ Check_Type(rb_date, T_STRING);
327
+
328
+ int status = NUM2INT(rb_status);
329
+ long body_size = NUM2LONG(rb_body_size);
330
+ int keep_alive = RTEST(rb_keep_alive);
331
+
332
+ /* Most heads fit in 1 KiB; rb_str_cat grows on demand. */
333
+ VALUE buf = rb_str_buf_new(1024);
334
+
335
+ /* Status line: "HTTP/1.1 <status> <reason>\r\n" */
336
+ char status_line[48];
337
+ int n = snprintf(status_line, sizeof(status_line), "HTTP/1.1 %d ", status);
338
+ rb_str_cat(buf, status_line, n);
339
+ rb_str_cat(buf, RSTRING_PTR(rb_reason), RSTRING_LEN(rb_reason));
340
+ rb_str_cat(buf, "\r\n", 2);
341
+
342
+ /* Iterate user headers — lowercase key, validate value, skip framing. */
343
+ int has_date = 0;
344
+
345
+ VALUE keys = rb_funcall(rb_headers, rb_intern("keys"), 0);
346
+ long n_keys = RARRAY_LEN(keys);
347
+ for (long i = 0; i < n_keys; i++) {
348
+ VALUE k = rb_ary_entry(keys, i);
349
+ VALUE v = rb_hash_aref(rb_headers, k);
350
+
351
+ VALUE k_s = rb_obj_as_string(k);
352
+ VALUE v_s = rb_obj_as_string(v);
353
+ VALUE k_lower = rb_funcall(k_s, id_downcase, 0);
354
+
355
+ const char *k_ptr = RSTRING_PTR(k_lower);
356
+ long k_len = RSTRING_LEN(k_lower);
357
+ const char *v_ptr = RSTRING_PTR(v_s);
358
+ long v_len = RSTRING_LEN(v_s);
359
+
360
+ /* CRLF injection guard on value. */
361
+ for (long j = 0; j < v_len; j++) {
362
+ if (v_ptr[j] == '\r' || v_ptr[j] == '\n') {
363
+ rb_raise(rb_eArgError, "header %s contains CR/LF",
364
+ RSTRING_PTR(rb_inspect(k_lower)));
365
+ }
366
+ }
367
+
368
+ /* Drop user-supplied content-length / connection — we always set
369
+ * these unconditionally below (matches rc16 Ruby behaviour where
370
+ * the normalized hash overwrites in place). */
371
+ if (k_len == 14 && memcmp(k_ptr, "content-length", 14) == 0) continue;
372
+ if (k_len == 10 && memcmp(k_ptr, "connection", 10) == 0) continue;
373
+
374
+ if (k_len == 4 && memcmp(k_ptr, "date", 4) == 0) {
375
+ has_date = 1;
376
+ }
377
+
378
+ rb_str_cat(buf, k_ptr, k_len);
379
+ rb_str_cat(buf, ": ", 2);
380
+ rb_str_cat(buf, v_ptr, v_len);
381
+ rb_str_cat(buf, "\r\n", 2);
382
+ }
383
+
384
+ /* Framing headers — always emitted. */
385
+ char cl_buf[48];
386
+ n = snprintf(cl_buf, sizeof(cl_buf), "content-length: %ld\r\n", body_size);
387
+ rb_str_cat(buf, cl_buf, n);
388
+
389
+ if (keep_alive) {
390
+ rb_str_cat(buf, "connection: keep-alive\r\n", 24);
391
+ } else {
392
+ rb_str_cat(buf, "connection: close\r\n", 19);
393
+ }
394
+
395
+ if (!has_date) {
396
+ rb_str_cat(buf, "date: ", 6);
397
+ rb_str_cat(buf, RSTRING_PTR(rb_date), RSTRING_LEN(rb_date));
398
+ rb_str_cat(buf, "\r\n", 2);
399
+ }
400
+
401
+ /* End of head */
402
+ rb_str_cat(buf, "\r\n", 2);
403
+
404
+ return buf;
405
+ }
406
+
407
+ void Init_hyperion_http(void) {
408
+ install_settings();
409
+
410
+ rb_mHyperion = rb_const_get(rb_cObject, rb_intern("Hyperion"));
411
+ rb_cRequest = rb_const_get(rb_mHyperion, rb_intern("Request"));
412
+ rb_eParseError = rb_const_get(rb_mHyperion, rb_intern("ParseError"));
413
+ rb_eUnsupportedError = rb_const_get(rb_mHyperion, rb_intern("UnsupportedError"));
414
+
415
+ rb_cCParser = rb_define_class_under(rb_mHyperion, "CParser", rb_cObject);
416
+ rb_define_method(rb_cCParser, "parse", cparser_parse, 1);
417
+ rb_define_singleton_method(rb_cCParser, "build_response_head",
418
+ cbuild_response_head, 6);
419
+
420
+ id_new = rb_intern("new");
421
+ id_downcase = rb_intern("downcase");
422
+ id_method_kw = rb_intern("method");
423
+ id_path_kw = rb_intern("path");
424
+ id_query_string_kw = rb_intern("query_string");
425
+ id_http_version_kw = rb_intern("http_version");
426
+ id_headers_kw = rb_intern("headers");
427
+ id_body_kw = rb_intern("body");
428
+ }
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require_relative '../version'
5
+ require_relative '../pool'
6
+
7
+ module Hyperion
8
+ module Adapter
9
+ # NOTE: this is Hyperion::Adapter::Rack, not the Rack gem.
10
+ # Reference the Rack gem as ::Rack inside this module if needed.
11
+ module Rack
12
+ # Pre-frozen mapping for the 16 most common HTTP request headers.
13
+ # Skips the per-request `"HTTP_#{name.upcase.tr('-', '_')}"` allocation
14
+ # (5–15 string ops per request × N headers). Uncached header names fall
15
+ # back to the dynamic computation. Keys are lowercased to match the
16
+ # parser's normalisation.
17
+ HTTP_KEY_CACHE = {
18
+ 'host' => 'HTTP_HOST',
19
+ 'user-agent' => 'HTTP_USER_AGENT',
20
+ 'accept' => 'HTTP_ACCEPT',
21
+ 'accept-encoding' => 'HTTP_ACCEPT_ENCODING',
22
+ 'accept-language' => 'HTTP_ACCEPT_LANGUAGE',
23
+ 'connection' => 'HTTP_CONNECTION',
24
+ 'content-type' => 'HTTP_CONTENT_TYPE',
25
+ 'content-length' => 'HTTP_CONTENT_LENGTH',
26
+ 'cookie' => 'HTTP_COOKIE',
27
+ 'authorization' => 'HTTP_AUTHORIZATION',
28
+ 'cache-control' => 'HTTP_CACHE_CONTROL',
29
+ 'referer' => 'HTTP_REFERER',
30
+ 'origin' => 'HTTP_ORIGIN',
31
+ 'x-forwarded-for' => 'HTTP_X_FORWARDED_FOR',
32
+ 'x-forwarded-proto' => 'HTTP_X_FORWARDED_PROTO',
33
+ 'x-real-ip' => 'HTTP_X_REAL_IP'
34
+ }.freeze
35
+
36
+ ENV_POOL = Hyperion::Pool.new(
37
+ max_size: 256,
38
+ factory: -> { {} },
39
+ reset: ->(env) { env.clear }
40
+ )
41
+
42
+ INPUT_POOL = Hyperion::Pool.new(
43
+ max_size: 256,
44
+ factory: -> { StringIO.new },
45
+ reset: lambda { |io|
46
+ io.string = +''
47
+ io.rewind
48
+ }
49
+ )
50
+
51
+ class << self
52
+ def call(app, request)
53
+ env, input = build_env(request)
54
+ status, headers, body = app.call(env)
55
+ [status, headers, body]
56
+ rescue StandardError => e
57
+ Hyperion.metrics.increment(:app_errors)
58
+ Hyperion.logger.error do
59
+ {
60
+ message: 'app raised',
61
+ error: e.message,
62
+ error_class: e.class.name,
63
+ backtrace: (e.backtrace || []).first(20).join(' | ')
64
+ }
65
+ end
66
+ [500, { 'content-type' => 'text/plain' }, ['Internal Server Error']]
67
+ ensure
68
+ # Return env + input to pools after the response has been fully
69
+ # iterated by the writer. We can't release here because Rack body
70
+ # is iterated lazily — release happens after the writer.
71
+ # For Phase 5 simplicity we release synchronously since the writer
72
+ # buffers fully. Phase 7 (HTTP/2 streaming) will revisit.
73
+ ENV_POOL.release(env) if env
74
+ INPUT_POOL.release(input) if input
75
+ end
76
+
77
+ private
78
+
79
+ def build_env(request)
80
+ host_header = request.header('host') || ''
81
+ server_name, server_port = split_host(host_header)
82
+
83
+ env = ENV_POOL.acquire
84
+ input = INPUT_POOL.acquire
85
+ input.string = request.body
86
+ input.rewind
87
+
88
+ env['REQUEST_METHOD'] = request.method
89
+ env['PATH_INFO'] = request.path
90
+ env['QUERY_STRING'] = request.query_string
91
+ env['SERVER_NAME'] = server_name
92
+ env['SERVER_PORT'] = server_port
93
+ env['SERVER_PROTOCOL'] = request.http_version
94
+ env['HTTP_VERSION'] = request.http_version
95
+ env['SERVER_SOFTWARE'] = "Hyperion/#{Hyperion::VERSION}"
96
+ # Rack apps (Rack::Attack throttles, IpHelper.real_ip, audit logging)
97
+ # require REMOTE_ADDR. Fall back to localhost when no peer info is
98
+ # available — typically when a Request is constructed in specs
99
+ # without a backing socket.
100
+ env['REMOTE_ADDR'] = request.peer_address || '127.0.0.1'
101
+ env['rack.url_scheme'] = 'http'
102
+ env['rack.input'] = input
103
+ env['rack.errors'] = $stderr
104
+ env['rack.hijack?'] = false
105
+ env['rack.version'] = [3, 0]
106
+ env['rack.multithread'] = false
107
+ env['rack.multiprocess'] = false
108
+ env['rack.run_once'] = false
109
+ env['SCRIPT_NAME'] = ''
110
+
111
+ request.headers.each do |name, value|
112
+ key = HTTP_KEY_CACHE[name] || "HTTP_#{name.upcase.tr('-', '_')}"
113
+ env[key] = value
114
+ end
115
+
116
+ env['CONTENT_TYPE'] = env['HTTP_CONTENT_TYPE'] if env.key?('HTTP_CONTENT_TYPE')
117
+ env['CONTENT_LENGTH'] = env['HTTP_CONTENT_LENGTH'] if env.key?('HTTP_CONTENT_LENGTH')
118
+
119
+ [env, input]
120
+ end
121
+
122
+ def split_host(host_header)
123
+ return %w[localhost 80] if host_header.empty?
124
+
125
+ if host_header.start_with?('[')
126
+ close = host_header.index(']')
127
+ return [host_header, '80'] unless close
128
+
129
+ name = host_header[0..close]
130
+ rest = host_header[(close + 1)..]
131
+ port = rest&.start_with?(':') ? rest[1..] : '80'
132
+ [name, port.to_s.empty? ? '80' : port]
133
+ elsif host_header.include?(':')
134
+ name, port = host_header.split(':', 2)
135
+ [name, port]
136
+ else
137
+ [host_header, '80']
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hyperion::CParser — C extension wrapping Node's llhttp.
4
+ # Implements the same interface as Hyperion::Parser:
5
+ # parse(buffer) -> [Request, end_offset] | raise ParseError | raise UnsupportedError
6
+ #
7
+ # If the extension didn't compile (e.g. no C toolchain, JRuby), this require
8
+ # fails gracefully and Hyperion::Parser remains the only parser. Connection
9
+ # probes for `defined?(Hyperion::CParser)` to pick its default.
10
+ begin
11
+ require 'hyperion_http/hyperion_http'
12
+ rescue LoadError => e
13
+ Hyperion.logger.warn do
14
+ {
15
+ message: 'C parser not available — falling back to pure-Ruby parser',
16
+ error: e.message
17
+ }
18
+ end
19
+ end