raptor 0.16.1 → 0.17.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 +4 -4
- data/.buildkite/pipeline.yml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +9 -9
- data/docs/brisrails-talk.md +1 -1
- data/docs/raptor-vs-puma.md +1 -1
- data/ext/raptor_bpf/reuseport_select.bpf.c +19 -28
- data/ext/raptor_http/raptor_http.c +213 -0
- data/ext/raptor_http2/raptor_http2.c +60 -0
- data/lib/raptor/http1.rb +37 -158
- data/lib/raptor/http2.rb +1 -16
- data/lib/raptor/reuseport_bpf.rb +20 -3
- data/lib/raptor/server.rb +5 -2
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/http1.rbs +18 -77
- data/sig/generated/raptor/http2.rbs +0 -4
- data/sig/generated/raptor/reuseport_bpf.rbs +12 -2
- data/sig/generated/raptor/server.rbs +3 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fbd24155ed44e8b8faf1f5b11a6078a34ab58bf43c290f3ffcf2a4a582c74d16
|
|
4
|
+
data.tar.gz: 7d6a0f38240c4ae3e723650fcd7a9888f2b59b69f3145018a918524565324972
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 811414a1f6a2469ef38c20935564ba204efdc5988437be5d0fa8a167d85a42204abe2a743115d090b24cf2cc8489e8dffdf57a93c1313936dcfa04a7d2213768
|
|
7
|
+
data.tar.gz: 29a62a959b22ca99c55eea39919d8fe3618fdb5061054718c38503c658c803d77a3a2b40cc9b8089da917faf4041569545073c24c468f6e8552f0d113ee0d469
|
data/.buildkite/pipeline.yml
CHANGED
|
@@ -41,7 +41,7 @@ steps:
|
|
|
41
41
|
soft_fail: true
|
|
42
42
|
image: "rubylang/ruby:master-dev"
|
|
43
43
|
commands:
|
|
44
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
44
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
45
45
|
- ruby -v
|
|
46
46
|
- bundle install
|
|
47
47
|
- bundle exec rake ci
|
|
@@ -52,7 +52,7 @@ steps:
|
|
|
52
52
|
env:
|
|
53
53
|
RUBY_YJIT_ENABLE: "1"
|
|
54
54
|
commands:
|
|
55
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
55
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
56
56
|
- ruby -v
|
|
57
57
|
- bundle install
|
|
58
58
|
- bundle exec rake ci
|
|
@@ -63,7 +63,7 @@ steps:
|
|
|
63
63
|
env:
|
|
64
64
|
RUBY_GC_STRESS: "1"
|
|
65
65
|
commands:
|
|
66
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
66
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
67
67
|
- ruby -v
|
|
68
68
|
- bundle install
|
|
69
69
|
- bundle exec rake ci
|
|
@@ -75,7 +75,7 @@ steps:
|
|
|
75
75
|
RUBY_YJIT_ENABLE: "1"
|
|
76
76
|
RUBY_GC_STRESS: "1"
|
|
77
77
|
commands:
|
|
78
|
-
- apt-get update && apt-get install -y libyaml-dev
|
|
78
|
+
- apt-get update && apt-get install -y libyaml-dev libssl-dev
|
|
79
79
|
- ruby -v
|
|
80
80
|
- bundle install
|
|
81
81
|
- bundle exec rake ci
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.17.0] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
- Reserve BPF-routed connections before accept
|
|
6
|
+
- Skip unnecessary HTTP/1.1 response work
|
|
7
|
+
- Assemble HTTP/2 response headers in C
|
|
8
|
+
- Emit HTTP/1.1 response headers and chunked bodies in C
|
|
9
|
+
- Decode chunked request bodies in C
|
|
10
|
+
|
|
3
11
|
## [0.16.1] - 2026-08-23
|
|
4
12
|
|
|
5
13
|
- Drop the stale `Ractors=NUM` option from the Rackup handler
|
data/README.md
CHANGED
|
@@ -37,7 +37,7 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
37
37
|
```
|
|
38
38
|
> bundle exec raptor -w 10 -t 3 hello_world.ru
|
|
39
39
|
[Raptor 83654|Main|Main] Cluster initializing:
|
|
40
|
-
[Raptor 83654|Main|Main] ├─ Version: 0.
|
|
40
|
+
[Raptor 83654|Main|Main] ├─ Version: 0.17.0
|
|
41
41
|
[Raptor 83654|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
|
|
42
42
|
[Raptor 83654|Main|Main] ├─ Environment: development
|
|
43
43
|
[Raptor 83654|Main|Main] ├─ Master PID: 83654
|
|
@@ -201,7 +201,7 @@ Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_
|
|
|
201
201
|
|
|
202
202
|
## (Micro) Benchmarks
|
|
203
203
|
|
|
204
|
-
Raptor 0.
|
|
204
|
+
Raptor 0.17.0 vs Puma 8.0.2 vs Falcon 0.57.0 across two workload profiles. **IO-bound** is a GET endpoint that
|
|
205
205
|
interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes several DB or
|
|
206
206
|
cache calls. **CPU-bound** is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of JSON item
|
|
207
207
|
building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path that does
|
|
@@ -210,16 +210,16 @@ most of its work in Ruby with a few near-zero-cost cache hits.
|
|
|
210
210
|
Each cell reports the median throughput and median p95 latency independently across 5 runs, so the two numbers in a row
|
|
211
211
|
may come from different runs. Every run starts a fresh server process so the samples are independent of each other;
|
|
212
212
|
state accumulated in a previous run cannot bias the next. Across the whole table, the widest spread
|
|
213
|
-
((max - min) / 2 / median) between runs of a single cell was ±
|
|
213
|
+
((max - min) / 2 / median) between runs of a single cell was ±22.4% for throughput and ±37.4% for p95.
|
|
214
214
|
|
|
215
215
|
| Protocol | Workload | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
|
|
216
216
|
| --------------------- | -------- | ------------ | ---------- | ----------- | --------- | ------------- | ------------ | ------------ | ---------- | --------------- | ------------- |
|
|
217
|
-
| HTTP/1.1 | IO | 3.
|
|
218
|
-
| HTTP/1.1 | CPU | 8.
|
|
219
|
-
| HTTP/1.1 (keep-alive) | IO |
|
|
220
|
-
| HTTP/1.1 (keep-alive) | CPU | 8.
|
|
221
|
-
| HTTP/2 | IO | 0.
|
|
222
|
-
| HTTP/2 | CPU | 6.
|
|
217
|
+
| HTTP/1.1 | IO | 3.35k req/s | 59.60 ms | 1.51k req/s | 126.70 ms | 121.9% higher | 53.0% lower | 12.19k req/s | 14.10 ms | 72.6% lower | 322.7% higher |
|
|
218
|
+
| HTTP/1.1 | CPU | 8.31k req/s | 25.00 ms | 8.63k req/s | 21.10 ms | 3.7% lower | 18.5% higher | 6.57k req/s | 27.40 ms | 26.4% higher | 8.8% lower |
|
|
219
|
+
| HTTP/1.1 (keep-alive) | IO | 3.22k req/s | 46.90 ms | 1.47k req/s | 113.80 ms | 119.9% higher | 58.8% lower | 6.23k req/s | 28.20 ms | 48.2% lower | 66.3% higher |
|
|
220
|
+
| HTTP/1.1 (keep-alive) | CPU | 8.30k req/s | 23.30 ms | 8.41k req/s | 22.10 ms | 1.3% lower | 5.4% higher | 6.95k req/s | 32.30 ms | 19.5% higher | 27.9% lower |
|
|
221
|
+
| HTTP/2 | IO | 0.98k req/s | 196.28 ms | N/A | N/A | - | - | 6.55k req/s | 27.71 ms | 85.1% lower | 608.4% higher |
|
|
222
|
+
| HTTP/2 | CPU | 6.23k req/s | 33.11 ms | N/A | N/A | - | - | 6.92k req/s | 42.94 ms | 10.0% lower | 22.9% lower |
|
|
223
223
|
|
|
224
224
|
> ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [aarch64-linux]
|
|
225
225
|
> 10 worker processes; Raptor and Puma run 3 threads per worker, Falcon runs unbounded fibers per worker;
|
data/docs/brisrails-talk.md
CHANGED
|
@@ -37,7 +37,7 @@ _[luma.com/t3l24s8h](https://luma.com/t3l24s8h)_
|
|
|
37
37
|
- <big>I built a Ruby web server over the last few months</big>
|
|
38
38
|
- <big>It's called Raptor</big>
|
|
39
39
|
- <big>It runs Rack apps, like Puma and Falcon do</big>
|
|
40
|
-
- <big>It's built around
|
|
40
|
+
- <big>It's built around a Ractor-parallel HTTP parser</big>
|
|
41
41
|
- <big>It's fast enough that the numbers are interesting</big>
|
|
42
42
|
- <big>It's not production-ready. Nobody's Rails app should be behind it yet.</big>
|
|
43
43
|
|
data/docs/raptor-vs-puma.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Raptor vs Puma: A Design Comparison
|
|
2
2
|
|
|
3
|
-
Raptor is a Ruby web server designed around
|
|
3
|
+
Raptor is a Ruby web server designed around a Ractor-parallel HTTP parser. On CPU-bound HTTP/1.1 it holds a real edge over the latest Puma release on the same hardware regardless of keep-alive; on IO-heavy work fibers still win, and Falcon is the server to compare to there rather than Raptor or Puma. Raptor also speaks HTTP/2 natively, which Puma does not. This document is a systems-design walkthrough of how it manages both.
|
|
4
4
|
|
|
5
5
|
## Why this document exists
|
|
6
6
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
#include <bpf/bpf_helpers.h>
|
|
3
3
|
|
|
4
4
|
#define MAX_WORKERS 64
|
|
5
|
-
#define LOAD_TIE_TOLERANCE 1
|
|
6
5
|
|
|
7
6
|
// Per-worker listening sockets, keyed by worker index.
|
|
8
7
|
struct {
|
|
@@ -20,14 +19,12 @@ struct {
|
|
|
20
19
|
__uint(max_entries, MAX_WORKERS + 1);
|
|
21
20
|
} loads SEC(".maps");
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// instead of clustering on whichever worker most recently reported the
|
|
28
|
-
// lowest load.
|
|
22
|
+
// Uses the power-of-two-choices strategy and reserves the selected worker's
|
|
23
|
+
// slot before returning. Sampling two hash-selected workers avoids
|
|
24
|
+
// concentrating variable-cost requests, while the reservation prevents a
|
|
25
|
+
// burst from repeatedly selecting the same reported load.
|
|
29
26
|
SEC("sk_reuseport")
|
|
30
|
-
int
|
|
27
|
+
int select_less_loaded(struct sk_reuseport_md *ctx) {
|
|
31
28
|
__u32 count_key = 0;
|
|
32
29
|
__u32 *count_ptr = bpf_map_lookup_elem(&loads, &count_key);
|
|
33
30
|
if (!count_ptr || *count_ptr == 0) {
|
|
@@ -38,29 +35,23 @@ int select_least_loaded(struct sk_reuseport_md *ctx) {
|
|
|
38
35
|
num_workers = MAX_WORKERS;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
__u32
|
|
42
|
-
__u32
|
|
43
|
-
__u32
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!load_ptr) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (*load_ptr < min_load) {
|
|
55
|
-
min_load = *load_ptr;
|
|
56
|
-
min_idx = worker_idx;
|
|
57
|
-
}
|
|
58
|
-
if (*load_ptr > max_load) {
|
|
59
|
-
max_load = *load_ptr;
|
|
38
|
+
__u32 chosen_idx = ctx->hash % num_workers;
|
|
39
|
+
__u32 chosen_key = chosen_idx + 1;
|
|
40
|
+
__u32 *chosen_load = bpf_map_lookup_elem(&loads, &chosen_key);
|
|
41
|
+
if (num_workers > 1) {
|
|
42
|
+
__u32 alternate_idx = (chosen_idx + 1 + ((ctx->hash >> 16) % (num_workers - 1))) % num_workers;
|
|
43
|
+
__u32 alternate_key = alternate_idx + 1;
|
|
44
|
+
__u32 *alternate_load = bpf_map_lookup_elem(&loads, &alternate_key);
|
|
45
|
+
if (chosen_load && alternate_load && *alternate_load < *chosen_load) {
|
|
46
|
+
chosen_idx = alternate_idx;
|
|
47
|
+
chosen_load = alternate_load;
|
|
60
48
|
}
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
|
|
51
|
+
if (chosen_load) {
|
|
52
|
+
__sync_fetch_and_add(chosen_load, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
64
55
|
bpf_sk_select_reuseport(ctx, &socks, &chosen_idx, 0);
|
|
65
56
|
return SK_PASS;
|
|
66
57
|
}
|
|
@@ -37,6 +37,8 @@ typedef struct raptor_parser {
|
|
|
37
37
|
#define FLAG_HAS_BODY 0x2
|
|
38
38
|
#define FLAG_FINISHED 0x4
|
|
39
39
|
|
|
40
|
+
#define MAX_CHUNK_OVERHEAD (16 * 1024)
|
|
41
|
+
|
|
40
42
|
static VALUE eHttpParserError;
|
|
41
43
|
static VALUE global_request_method;
|
|
42
44
|
static VALUE global_request_uri;
|
|
@@ -44,6 +46,10 @@ static VALUE global_query_string;
|
|
|
44
46
|
static VALUE global_server_protocol;
|
|
45
47
|
static VALUE global_request_path;
|
|
46
48
|
static VALUE global_fragment;
|
|
49
|
+
static ID id_complete;
|
|
50
|
+
static ID id_incomplete;
|
|
51
|
+
static ID id_malformed;
|
|
52
|
+
static ID id_too_large;
|
|
47
53
|
|
|
48
54
|
struct common_field {
|
|
49
55
|
const char *name;
|
|
@@ -1316,6 +1322,200 @@ static VALUE parser_body(VALUE self) {
|
|
|
1316
1322
|
return parser->body;
|
|
1317
1323
|
}
|
|
1318
1324
|
|
|
1325
|
+
static const char *find_crlf(const char *buf, size_t len) {
|
|
1326
|
+
if (len < 2) return NULL;
|
|
1327
|
+
const char *end = buf + len - 1;
|
|
1328
|
+
for (const char *p = buf; p < end; p++) {
|
|
1329
|
+
if (*p == '\r' && *(p + 1) == '\n') return p;
|
|
1330
|
+
}
|
|
1331
|
+
return NULL;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
static VALUE parser_decode_chunked(int argc, VALUE *argv, VALUE self) {
|
|
1335
|
+
(void)self;
|
|
1336
|
+
VALUE buffer, max_size;
|
|
1337
|
+
rb_scan_args(argc, argv, "11", &buffer, &max_size);
|
|
1338
|
+
Check_Type(buffer, T_STRING);
|
|
1339
|
+
|
|
1340
|
+
const char *buf = RSTRING_PTR(buffer);
|
|
1341
|
+
size_t len = (size_t)RSTRING_LEN(buffer);
|
|
1342
|
+
size_t offset = 0;
|
|
1343
|
+
size_t overhead = 0;
|
|
1344
|
+
int has_max = !NIL_P(max_size);
|
|
1345
|
+
size_t max_bytes = has_max ? NUM2SIZET(max_size) : 0;
|
|
1346
|
+
|
|
1347
|
+
VALUE decoded = rb_str_buf_new((long)len);
|
|
1348
|
+
|
|
1349
|
+
while (offset < len) {
|
|
1350
|
+
const char *crlf = find_crlf(buf + offset, len - offset);
|
|
1351
|
+
if (!crlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1352
|
+
|
|
1353
|
+
size_t line_len = (size_t)(crlf - (buf + offset));
|
|
1354
|
+
size_t size_len = line_len;
|
|
1355
|
+
for (size_t i = 0; i < line_len; i++) {
|
|
1356
|
+
if (buf[offset + i] == ';') { size_len = i; break; }
|
|
1357
|
+
}
|
|
1358
|
+
if (size_len == 0) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1359
|
+
|
|
1360
|
+
uint64_t chunk_size = 0;
|
|
1361
|
+
for (size_t i = 0; i < size_len; i++) {
|
|
1362
|
+
char c = buf[offset + i];
|
|
1363
|
+
uint64_t digit;
|
|
1364
|
+
if (c >= '0' && c <= '9') digit = (uint64_t)(c - '0');
|
|
1365
|
+
else if (c >= 'a' && c <= 'f') digit = (uint64_t)(c - 'a' + 10);
|
|
1366
|
+
else if (c >= 'A' && c <= 'F') digit = (uint64_t)(c - 'A' + 10);
|
|
1367
|
+
else return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1368
|
+
if (chunk_size > (UINT64_MAX >> 4)) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1369
|
+
chunk_size = (chunk_size << 4) | digit;
|
|
1370
|
+
}
|
|
1371
|
+
if (chunk_size > (uint64_t)(SIZE_MAX / 2)) return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1372
|
+
|
|
1373
|
+
size_t crlf_offset = offset + line_len;
|
|
1374
|
+
|
|
1375
|
+
if (chunk_size == 0) {
|
|
1376
|
+
size_t trailer_offset = crlf_offset + 2;
|
|
1377
|
+
while (1) {
|
|
1378
|
+
if (trailer_offset >= len) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1379
|
+
const char *tcrlf = find_crlf(buf + trailer_offset, len - trailer_offset);
|
|
1380
|
+
if (!tcrlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1381
|
+
if (tcrlf == buf + trailer_offset) return rb_ary_new_from_args(2, decoded, ID2SYM(id_complete));
|
|
1382
|
+
trailer_offset = (size_t)(tcrlf - buf) + 2;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
size_t decoded_len = (size_t)RSTRING_LEN(decoded);
|
|
1387
|
+
if (has_max && (decoded_len + (size_t)chunk_size) > max_bytes) {
|
|
1388
|
+
return rb_ary_new_from_args(2, decoded, ID2SYM(id_too_large));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
overhead += line_len + 4;
|
|
1392
|
+
if (overhead > (decoded_len + (size_t)chunk_size + MAX_CHUNK_OVERHEAD)) {
|
|
1393
|
+
return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
size_t data_offset = crlf_offset + 2;
|
|
1397
|
+
size_t available = len > data_offset ? len - data_offset : 0;
|
|
1398
|
+
size_t bytes_to_copy = (size_t)chunk_size < available ? (size_t)chunk_size : available;
|
|
1399
|
+
rb_str_cat(decoded, buf + data_offset, (long)bytes_to_copy);
|
|
1400
|
+
|
|
1401
|
+
offset = data_offset + (size_t)chunk_size + 2;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
static uint8_t illegal_header_key_bitmap[32];
|
|
1408
|
+
static uint8_t illegal_header_value_bitmap[32];
|
|
1409
|
+
|
|
1410
|
+
static void bitmap_set(uint8_t *bitmap, unsigned char byte) {
|
|
1411
|
+
bitmap[byte >> 3] |= (uint8_t)(1u << (byte & 7));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
static int contains_illegal_byte(const uint8_t *bitmap, const char *str, size_t len) {
|
|
1415
|
+
for (size_t i = 0; i < len; i++) {
|
|
1416
|
+
unsigned char byte = (unsigned char)str[i];
|
|
1417
|
+
if ((bitmap[byte >> 3] >> (byte & 7)) & 1) return 1;
|
|
1418
|
+
}
|
|
1419
|
+
return 0;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
static VALUE parser_illegal_header_key_p(VALUE self, VALUE key) {
|
|
1423
|
+
(void)self;
|
|
1424
|
+
Check_Type(key, T_STRING);
|
|
1425
|
+
return contains_illegal_byte(illegal_header_key_bitmap, RSTRING_PTR(key), (size_t)RSTRING_LEN(key)) ? Qtrue : Qfalse;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
static VALUE parser_illegal_header_value_p(VALUE self, VALUE value) {
|
|
1429
|
+
(void)self;
|
|
1430
|
+
Check_Type(value, T_STRING);
|
|
1431
|
+
return contains_illegal_byte(illegal_header_value_bitmap, RSTRING_PTR(value), (size_t)RSTRING_LEN(value)) ? Qtrue : Qfalse;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
static void emit_header_line(VALUE buffer, const char *name, long name_len,
|
|
1435
|
+
const char *value, long value_len) {
|
|
1436
|
+
if (value_len == 0) return;
|
|
1437
|
+
if (contains_illegal_byte(illegal_header_value_bitmap, value, (size_t)value_len)) return;
|
|
1438
|
+
rb_str_cat(buffer, name, name_len);
|
|
1439
|
+
rb_str_cat(buffer, ": ", 2);
|
|
1440
|
+
rb_str_cat(buffer, value, value_len);
|
|
1441
|
+
rb_str_cat(buffer, "\r\n", 2);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
static void emit_header(VALUE buffer, const char *name, long name_len, VALUE value) {
|
|
1445
|
+
VALUE str = RB_TYPE_P(value, T_STRING) ? value : rb_funcall(value, rb_intern("to_s"), 0);
|
|
1446
|
+
const char *ptr = RSTRING_PTR(str);
|
|
1447
|
+
long len = RSTRING_LEN(str);
|
|
1448
|
+
if (len == 0) return;
|
|
1449
|
+
|
|
1450
|
+
if (!memchr(ptr, '\n', (size_t)len)) {
|
|
1451
|
+
emit_header_line(buffer, name, name_len, ptr, len);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
long line_start = 0;
|
|
1456
|
+
for (long i = 0; i < len; i++) {
|
|
1457
|
+
if (ptr[i] == '\n') {
|
|
1458
|
+
emit_header_line(buffer, name, name_len, ptr + line_start, i - line_start);
|
|
1459
|
+
line_start = i + 1;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (line_start < len) {
|
|
1463
|
+
emit_header_line(buffer, name, name_len, ptr + line_start, len - line_start);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
static int format_header_iter(VALUE key, VALUE value, VALUE data) {
|
|
1468
|
+
VALUE buffer = (VALUE)data;
|
|
1469
|
+
if (!RB_TYPE_P(key, T_STRING)) return ST_CONTINUE;
|
|
1470
|
+
const char *name = RSTRING_PTR(key);
|
|
1471
|
+
long name_len = RSTRING_LEN(key);
|
|
1472
|
+
if (contains_illegal_byte(illegal_header_key_bitmap, name, (size_t)name_len)) return ST_CONTINUE;
|
|
1473
|
+
|
|
1474
|
+
if (RB_TYPE_P(value, T_ARRAY)) {
|
|
1475
|
+
long entries = RARRAY_LEN(value);
|
|
1476
|
+
for (long i = 0; i < entries; i++) {
|
|
1477
|
+
emit_header(buffer, name, name_len, RARRAY_AREF(value, i));
|
|
1478
|
+
}
|
|
1479
|
+
} else {
|
|
1480
|
+
emit_header(buffer, name, name_len, value);
|
|
1481
|
+
}
|
|
1482
|
+
return ST_CONTINUE;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
static VALUE parser_format_headers(VALUE self, VALUE buffer, VALUE headers) {
|
|
1486
|
+
(void)self;
|
|
1487
|
+
Check_Type(buffer, T_STRING);
|
|
1488
|
+
Check_Type(headers, T_HASH);
|
|
1489
|
+
rb_hash_foreach(headers, format_header_iter, buffer);
|
|
1490
|
+
return buffer;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
static VALUE parser_chunked_encode(VALUE self, VALUE buffer, VALUE chunk) {
|
|
1494
|
+
(void)self;
|
|
1495
|
+
Check_Type(buffer, T_STRING);
|
|
1496
|
+
Check_Type(chunk, T_STRING);
|
|
1497
|
+
long chunk_len = RSTRING_LEN(chunk);
|
|
1498
|
+
if (chunk_len == 0) return buffer;
|
|
1499
|
+
|
|
1500
|
+
char hex_buf[17];
|
|
1501
|
+
int hex_len = snprintf(hex_buf, sizeof(hex_buf), "%lx", (unsigned long)chunk_len);
|
|
1502
|
+
rb_str_cat(buffer, hex_buf, hex_len);
|
|
1503
|
+
rb_str_cat(buffer, "\r\n", 2);
|
|
1504
|
+
rb_str_cat(buffer, RSTRING_PTR(chunk), chunk_len);
|
|
1505
|
+
rb_str_cat(buffer, "\r\n", 2);
|
|
1506
|
+
return buffer;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
static void init_illegal_header_bitmaps(void) {
|
|
1510
|
+
for (int byte = 0x00; byte <= 0x20; byte++) bitmap_set(illegal_header_key_bitmap, (unsigned char)byte);
|
|
1511
|
+
const char *key_specials = "()<>@,;:\\\"/[]?={}";
|
|
1512
|
+
for (const char *p = key_specials; *p; p++) bitmap_set(illegal_header_key_bitmap, (unsigned char)*p);
|
|
1513
|
+
bitmap_set(illegal_header_key_bitmap, 0x7F);
|
|
1514
|
+
|
|
1515
|
+
for (int byte = 0x00; byte <= 0x08; byte++) bitmap_set(illegal_header_value_bitmap, (unsigned char)byte);
|
|
1516
|
+
for (int byte = 0x0A; byte <= 0x1F; byte++) bitmap_set(illegal_header_value_bitmap, (unsigned char)byte);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1319
1519
|
RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
1320
1520
|
rb_ext_ractor_safe(true);
|
|
1321
1521
|
|
|
@@ -1337,6 +1537,13 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
|
1337
1537
|
global_request_path = rb_str_new2("PATH_INFO");
|
|
1338
1538
|
global_fragment = rb_str_new2("FRAGMENT");
|
|
1339
1539
|
|
|
1540
|
+
id_complete = rb_intern("complete");
|
|
1541
|
+
id_incomplete = rb_intern("incomplete");
|
|
1542
|
+
id_malformed = rb_intern("malformed");
|
|
1543
|
+
id_too_large = rb_intern("too_large");
|
|
1544
|
+
|
|
1545
|
+
init_illegal_header_bitmaps();
|
|
1546
|
+
|
|
1340
1547
|
for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
|
|
1341
1548
|
common_fields[i].interned = rb_enc_interned_str(common_fields[i].name, common_fields[i].len, rb_utf8_encoding());
|
|
1342
1549
|
rb_global_variable(&common_fields[i].interned);
|
|
@@ -1351,4 +1558,10 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
|
1351
1558
|
rb_define_method(cHttpParser, "nread", parser_nread, 0);
|
|
1352
1559
|
rb_define_method(cHttpParser, "reset", parser_reset, 0);
|
|
1353
1560
|
rb_define_method(cHttpParser, "body", parser_body, 0);
|
|
1561
|
+
rb_define_singleton_method(cHttpParser, "decode_chunked", parser_decode_chunked, -1);
|
|
1562
|
+
rb_define_singleton_method(cHttpParser, "illegal_header_key?", parser_illegal_header_key_p, 1);
|
|
1563
|
+
rb_define_singleton_method(cHttpParser, "illegal_header_value?", parser_illegal_header_value_p, 1);
|
|
1564
|
+
rb_define_singleton_method(cHttpParser, "format_headers", parser_format_headers, 2);
|
|
1565
|
+
rb_define_singleton_method(cHttpParser, "chunked_encode", parser_chunked_encode, 2);
|
|
1566
|
+
rb_define_const(cHttpParser, "MAX_CHUNK_OVERHEAD", INT2FIX(MAX_CHUNK_OVERHEAD));
|
|
1354
1567
|
}
|
|
@@ -599,6 +599,65 @@ static VALUE h2_encode_headers(VALUE self, VALUE headers) {
|
|
|
599
599
|
return hpack_encode_header_block(headers);
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
static const char *const HOP_BY_HOP_HEADERS[] = {
|
|
603
|
+
"connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"
|
|
604
|
+
};
|
|
605
|
+
#define NUM_HOP_BY_HOP_HEADERS 5
|
|
606
|
+
|
|
607
|
+
static int is_hop_by_hop(const char *name, long name_len) {
|
|
608
|
+
for (size_t i = 0; i < NUM_HOP_BY_HOP_HEADERS; i++) {
|
|
609
|
+
size_t hbh_len = strlen(HOP_BY_HOP_HEADERS[i]);
|
|
610
|
+
if ((long)hbh_len == name_len && memcmp(name, HOP_BY_HOP_HEADERS[i], hbh_len) == 0) return 1;
|
|
611
|
+
}
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
static VALUE ascii_lowercase_str(VALUE str) {
|
|
616
|
+
long len = RSTRING_LEN(str);
|
|
617
|
+
VALUE lower = rb_str_new(RSTRING_PTR(str), len);
|
|
618
|
+
char *ptr = RSTRING_PTR(lower);
|
|
619
|
+
for (long i = 0; i < len; i++) {
|
|
620
|
+
if (ptr[i] >= 'A' && ptr[i] <= 'Z') ptr[i] = (char)(ptr[i] | 0x20);
|
|
621
|
+
}
|
|
622
|
+
return lower;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
static int response_headers_iter(VALUE key, VALUE value, VALUE data) {
|
|
626
|
+
VALUE pairs = data;
|
|
627
|
+
if (!RB_TYPE_P(key, T_STRING)) return ST_CONTINUE;
|
|
628
|
+
|
|
629
|
+
VALUE lowered = ascii_lowercase_str(key);
|
|
630
|
+
const char *lname = RSTRING_PTR(lowered);
|
|
631
|
+
long lname_len = RSTRING_LEN(lowered);
|
|
632
|
+
|
|
633
|
+
if (lname_len >= 5 && memcmp(lname, "rack.", 5) == 0) return ST_CONTINUE;
|
|
634
|
+
if (is_hop_by_hop(lname, lname_len)) return ST_CONTINUE;
|
|
635
|
+
|
|
636
|
+
if (RB_TYPE_P(value, T_ARRAY)) {
|
|
637
|
+
long entries = RARRAY_LEN(value);
|
|
638
|
+
for (long i = 0; i < entries; i++) {
|
|
639
|
+
VALUE entry = RARRAY_AREF(value, i);
|
|
640
|
+
VALUE str = RB_TYPE_P(entry, T_STRING) ? entry : rb_obj_as_string(entry);
|
|
641
|
+
rb_ary_push(pairs, rb_ary_new_from_args(2, lowered, str));
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
VALUE str = RB_TYPE_P(value, T_STRING) ? value : rb_obj_as_string(value);
|
|
645
|
+
rb_ary_push(pairs, rb_ary_new_from_args(2, lowered, str));
|
|
646
|
+
}
|
|
647
|
+
return ST_CONTINUE;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
static VALUE h2_encode_response_headers(VALUE self, VALUE status, VALUE headers) {
|
|
651
|
+
(void)self;
|
|
652
|
+
Check_Type(headers, T_HASH);
|
|
653
|
+
|
|
654
|
+
VALUE pairs = rb_ary_new();
|
|
655
|
+
rb_ary_push(pairs, rb_ary_new_from_args(2, rb_str_new_lit(":status"), rb_obj_as_string(status)));
|
|
656
|
+
rb_hash_foreach(headers, response_headers_iter, pairs);
|
|
657
|
+
|
|
658
|
+
return hpack_encode_header_block(pairs);
|
|
659
|
+
}
|
|
660
|
+
|
|
602
661
|
static VALUE h2_parse_frame(VALUE self, VALUE buffer) {
|
|
603
662
|
(void)self;
|
|
604
663
|
Check_Type(buffer, T_STRING);
|
|
@@ -765,6 +824,7 @@ RUBY_FUNC_EXPORTED void Init_raptor_http2(void) {
|
|
|
765
824
|
rb_define_method(cHttp2Parser, "parse_frame", h2_parse_frame, 1);
|
|
766
825
|
rb_define_method(cHttp2Parser, "parse_headers", h2_parse_headers, 2);
|
|
767
826
|
rb_define_method(cHttp2Parser, "encode_headers", h2_encode_headers, 1);
|
|
827
|
+
rb_define_method(cHttp2Parser, "encode_response_headers", h2_encode_response_headers, 2);
|
|
768
828
|
rb_define_method(cHttp2Parser, "parse_settings", h2_parse_settings, 1);
|
|
769
829
|
rb_define_method(cHttp2Parser, "build_settings", h2_build_settings, 1);
|
|
770
830
|
rb_define_method(cHttp2Parser, "build_frame", h2_build_frame, 4);
|
data/lib/raptor/http1.rb
CHANGED
|
@@ -21,7 +21,6 @@ module Raptor
|
|
|
21
21
|
BODY_BUFFER_THRESHOLD = 256 * 1024
|
|
22
22
|
CHUNKED_WRITE_THRESHOLD = 512 * 1024
|
|
23
23
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
24
|
-
MAX_CHUNK_OVERHEAD = 16 * 1024
|
|
25
24
|
READ_BUFFER_SIZE = 64 * 1024
|
|
26
25
|
RESPONSE_BUFFER_CAPACITY = 4 * 1024
|
|
27
26
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
@@ -38,7 +37,6 @@ module Raptor
|
|
|
38
37
|
h[status] = "HTTP/1.1 #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
STATUS_WITH_NO_ENTITY_BODY = [204, 304, *100..199].freeze
|
|
42
40
|
CONTINUE_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
|
|
43
41
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
42
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
@@ -48,6 +46,7 @@ module Raptor
|
|
|
48
46
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
49
47
|
EXPECT_100_CONTINUE = "100-continue"
|
|
50
48
|
TRANSFER_ENCODING_CHUNKED = "chunked"
|
|
49
|
+
CHUNKED_TERMINATOR = "0\r\n\r\n"
|
|
51
50
|
|
|
52
51
|
HTTP_CONNECTION = "HTTP_CONNECTION"
|
|
53
52
|
HTTP_EXPECT = "HTTP_EXPECT"
|
|
@@ -56,10 +55,6 @@ module Raptor
|
|
|
56
55
|
RACK_HIJACKED = "rack.hijacked"
|
|
57
56
|
RACK_HIJACK_IO = "rack.hijack_io"
|
|
58
57
|
|
|
59
|
-
ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
|
|
60
|
-
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
|
61
|
-
CHUNK_SIZE_REGEX = /\A[0-9A-Fa-f]+\z/
|
|
62
|
-
|
|
63
58
|
# Returns true when an HTTP/1.1 request lacks a valid `Host` header per
|
|
64
59
|
# RFC 9112 section 3.2, where a valid value is a non-empty single-value
|
|
65
60
|
# line.
|
|
@@ -102,59 +97,6 @@ module Raptor
|
|
|
102
97
|
false
|
|
103
98
|
end
|
|
104
99
|
|
|
105
|
-
# Decodes a chunked transfer-encoded body buffer.
|
|
106
|
-
#
|
|
107
|
-
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
108
|
-
# terminating zero-length chunk and trailer section were fully consumed,
|
|
109
|
-
# `:too_large` when the decoded size would exceed `max_size`, `:malformed`
|
|
110
|
-
# when a chunk-size line is not valid hex or chunk framing overhead exceeds
|
|
111
|
-
# `MAX_CHUNK_OVERHEAD`, or `:incomplete` otherwise.
|
|
112
|
-
#
|
|
113
|
-
# @param buffer [String] the raw body buffer to decode
|
|
114
|
-
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
115
|
-
# @return [Array(String, Symbol)] decoded body and completion state
|
|
116
|
-
#
|
|
117
|
-
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
118
|
-
def self.decode_chunked(buffer, max_size = nil)
|
|
119
|
-
decoded = String.new
|
|
120
|
-
offset = 0
|
|
121
|
-
overhead = 0
|
|
122
|
-
|
|
123
|
-
while offset < buffer.bytesize
|
|
124
|
-
crlf = buffer.index("\r\n", offset)
|
|
125
|
-
return [decoded, :incomplete] unless crlf
|
|
126
|
-
|
|
127
|
-
size_line = buffer.byteslice(offset, crlf - offset)
|
|
128
|
-
semicolon = size_line.index(";")
|
|
129
|
-
size_part = semicolon ? size_line.byteslice(0, semicolon) : size_line
|
|
130
|
-
return [decoded, :malformed] unless size_part.match?(CHUNK_SIZE_REGEX)
|
|
131
|
-
|
|
132
|
-
chunk_size = size_part.to_i(16)
|
|
133
|
-
|
|
134
|
-
if chunk_size.zero?
|
|
135
|
-
trailer_offset = crlf + 2
|
|
136
|
-
loop do
|
|
137
|
-
trailer_crlf = buffer.index("\r\n", trailer_offset)
|
|
138
|
-
return [decoded, :incomplete] unless trailer_crlf
|
|
139
|
-
return [decoded, :complete] if trailer_crlf == trailer_offset
|
|
140
|
-
|
|
141
|
-
trailer_offset = trailer_crlf + 2
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size
|
|
146
|
-
|
|
147
|
-
overhead += (crlf - offset) + 4
|
|
148
|
-
return [decoded, :malformed] if overhead > (decoded.bytesize + chunk_size + MAX_CHUNK_OVERHEAD)
|
|
149
|
-
|
|
150
|
-
offset = crlf + 2
|
|
151
|
-
decoded << buffer.byteslice(offset, chunk_size)
|
|
152
|
-
offset += chunk_size + 2
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
[decoded, :incomplete]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
100
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
159
101
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
160
102
|
# plus populated `:env` and `:body`; malformed or oversized input flips
|
|
@@ -191,7 +133,7 @@ module Raptor
|
|
|
191
133
|
if max_body_size && parser.content_length > max_body_size
|
|
192
134
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
193
135
|
elsif parser.chunked?
|
|
194
|
-
decoded_body, chunked_state = decode_chunked(body_buffer, max_body_size)
|
|
136
|
+
decoded_body, chunked_state = HttpParser.decode_chunked(body_buffer, max_body_size)
|
|
195
137
|
|
|
196
138
|
case chunked_state
|
|
197
139
|
when :complete
|
|
@@ -479,7 +421,7 @@ module Raptor
|
|
|
479
421
|
if parser.chunked?
|
|
480
422
|
return :incomplete unless decode_chunked
|
|
481
423
|
|
|
482
|
-
body, chunked_state =
|
|
424
|
+
body, chunked_state = HttpParser.decode_chunked(body, @max_body_size)
|
|
483
425
|
case chunked_state
|
|
484
426
|
when :complete
|
|
485
427
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
@@ -581,7 +523,7 @@ module Raptor
|
|
|
581
523
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
582
524
|
streaming = body.respond_to?(:call) && !body.respond_to?(:each)
|
|
583
525
|
keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
|
|
584
|
-
response_size = response_size(headers, body)
|
|
526
|
+
response_size = response_size(headers, body) if @access_log_io && !hijacked
|
|
585
527
|
response_started = true
|
|
586
528
|
write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
|
|
587
529
|
end
|
|
@@ -919,11 +861,11 @@ module Raptor
|
|
|
919
861
|
|
|
920
862
|
response = +"#{HTTP_11} 103 Early Hints\r\n"
|
|
921
863
|
hints.each do |key, value|
|
|
922
|
-
next if illegal_header_key?(key)
|
|
864
|
+
next if HttpParser.illegal_header_key?(key)
|
|
923
865
|
|
|
924
866
|
values = value.is_a?(Array) ? value : [value]
|
|
925
867
|
values.each do |hint_value|
|
|
926
|
-
next if illegal_header_value?(hint_value.to_s)
|
|
868
|
+
next if HttpParser.illegal_header_value?(hint_value.to_s)
|
|
927
869
|
|
|
928
870
|
response << "#{key.downcase}: #{hint_value}\r\n"
|
|
929
871
|
end
|
|
@@ -948,27 +890,29 @@ module Raptor
|
|
|
948
890
|
validate_status(status)
|
|
949
891
|
response_hijack = headers.is_a?(Hash) ? headers.delete(Rack::RACK_HIJACK) : nil
|
|
950
892
|
headers = normalize_headers(headers)
|
|
951
|
-
|
|
893
|
+
no_entity_body = (status >= 100 && status < 200) || status == 204 || status == 304
|
|
894
|
+
validate_headers(headers, status, no_entity_body)
|
|
952
895
|
|
|
953
896
|
headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE
|
|
954
897
|
|
|
955
898
|
http_version = env[Rack::SERVER_PROTOCOL] == HTTP_11 ? HTTP_11 : HTTP_10
|
|
956
|
-
no_body = env[Rack::REQUEST_METHOD] == "HEAD" ||
|
|
899
|
+
no_body = env[Rack::REQUEST_METHOD] == "HEAD" || no_entity_body
|
|
957
900
|
|
|
958
901
|
response = build_status_line(http_version, status)
|
|
959
902
|
|
|
960
|
-
|
|
903
|
+
corked = !keep_alive
|
|
904
|
+
cork_socket(socket) if corked
|
|
961
905
|
|
|
962
906
|
if response_hijack
|
|
963
907
|
write_hijacked_response(socket, response, headers, response_hijack)
|
|
964
908
|
elsif no_body
|
|
965
|
-
write_no_body_response(socket, response, headers,
|
|
909
|
+
write_no_body_response(socket, response, headers, no_entity_body)
|
|
966
910
|
else
|
|
967
911
|
write_full_response(socket, response, headers, body, http_version)
|
|
968
912
|
end
|
|
969
913
|
ensure
|
|
970
914
|
body.close if body.respond_to?(:close)
|
|
971
|
-
uncork_socket(socket)
|
|
915
|
+
uncork_socket(socket) if corked
|
|
972
916
|
socket.flush rescue nil
|
|
973
917
|
end
|
|
974
918
|
|
|
@@ -1001,7 +945,7 @@ module Raptor
|
|
|
1001
945
|
headers.each do |key, value|
|
|
1002
946
|
raise TypeError, "header keys must be Strings" unless key.is_a?(String)
|
|
1003
947
|
|
|
1004
|
-
next if illegal_header_key?(key)
|
|
948
|
+
next if HttpParser.illegal_header_key?(key)
|
|
1005
949
|
|
|
1006
950
|
normalized_key = key.match?(/[A-Z]/) ? key.downcase : key
|
|
1007
951
|
next if normalized_key.start_with?(RACK_HEADER_PREFIX)
|
|
@@ -1017,12 +961,13 @@ module Raptor
|
|
|
1017
961
|
#
|
|
1018
962
|
# @param headers [Hash] normalized response headers
|
|
1019
963
|
# @param status [Integer] HTTP status code
|
|
964
|
+
# @param no_entity_body [Boolean] whether the status forbids an entity body
|
|
1020
965
|
# @return [void]
|
|
1021
966
|
# @raise [ArgumentError] if a forbidden header is present for the status
|
|
1022
967
|
#
|
|
1023
|
-
# @rbs (Hash[String, String | Array[String]] headers, Integer status) -> void
|
|
1024
|
-
def validate_headers(headers, status)
|
|
1025
|
-
if
|
|
968
|
+
# @rbs (Hash[String, String | Array[String]] headers, Integer status, bool no_entity_body) -> void
|
|
969
|
+
def validate_headers(headers, status, no_entity_body)
|
|
970
|
+
if no_entity_body
|
|
1026
971
|
raise ArgumentError, "content-type must not be present for status #{status}" if headers.key?(Rack::CONTENT_TYPE)
|
|
1027
972
|
|
|
1028
973
|
raise ArgumentError, "content-length must not be present for status #{status}" if headers.key?(Rack::CONTENT_LENGTH)
|
|
@@ -1055,7 +1000,7 @@ module Raptor
|
|
|
1055
1000
|
#
|
|
1056
1001
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, ^(TCPSocket) -> void response_hijack) -> void
|
|
1057
1002
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
1058
|
-
format_headers(response, headers)
|
|
1003
|
+
HttpParser.format_headers(response, headers)
|
|
1059
1004
|
response << "\r\n"
|
|
1060
1005
|
socket_write(socket, response)
|
|
1061
1006
|
uncork_socket(socket)
|
|
@@ -1069,16 +1014,16 @@ module Raptor
|
|
|
1069
1014
|
# @param socket [TCPSocket] the client socket
|
|
1070
1015
|
# @param response [String] the status line accumulated so far
|
|
1071
1016
|
# @param headers [Hash] normalized response headers
|
|
1072
|
-
# @param
|
|
1017
|
+
# @param no_entity_body [Boolean] whether the response status forbids an entity body
|
|
1073
1018
|
# @return [void]
|
|
1074
1019
|
#
|
|
1075
|
-
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers,
|
|
1076
|
-
def write_no_body_response(socket, response, headers,
|
|
1077
|
-
unless
|
|
1020
|
+
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, bool no_entity_body) -> void
|
|
1021
|
+
def write_no_body_response(socket, response, headers, no_entity_body)
|
|
1022
|
+
unless no_entity_body
|
|
1078
1023
|
headers[Rack::CONTENT_LENGTH] = "0" unless headers.key?(Rack::CONTENT_LENGTH) || headers.key?(Rack::TRANSFER_ENCODING)
|
|
1079
1024
|
end
|
|
1080
1025
|
|
|
1081
|
-
format_headers(response, headers)
|
|
1026
|
+
HttpParser.format_headers(response, headers)
|
|
1082
1027
|
response << "\r\n"
|
|
1083
1028
|
socket_write(socket, response)
|
|
1084
1029
|
end
|
|
@@ -1097,7 +1042,7 @@ module Raptor
|
|
|
1097
1042
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
|
|
1098
1043
|
def write_full_response(socket, response, headers, body, http_version)
|
|
1099
1044
|
if body.respond_to?(:call)
|
|
1100
|
-
format_headers(response, headers)
|
|
1045
|
+
HttpParser.format_headers(response, headers)
|
|
1101
1046
|
response << "\r\n"
|
|
1102
1047
|
socket_write(socket, response)
|
|
1103
1048
|
uncork_socket(socket)
|
|
@@ -1123,7 +1068,7 @@ module Raptor
|
|
|
1123
1068
|
headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED
|
|
1124
1069
|
end
|
|
1125
1070
|
|
|
1126
|
-
format_headers(response, headers)
|
|
1071
|
+
HttpParser.format_headers(response, headers)
|
|
1127
1072
|
response << "\r\n"
|
|
1128
1073
|
|
|
1129
1074
|
if body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
|
|
@@ -1172,13 +1117,13 @@ module Raptor
|
|
|
1172
1117
|
if use_chunked
|
|
1173
1118
|
buffer = response
|
|
1174
1119
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
1175
|
-
buffer
|
|
1120
|
+
HttpParser.chunked_encode(buffer, chunk)
|
|
1176
1121
|
if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
|
|
1177
1122
|
socket_write(socket, buffer)
|
|
1178
1123
|
buffer = +""
|
|
1179
1124
|
end
|
|
1180
1125
|
end
|
|
1181
|
-
buffer <<
|
|
1126
|
+
buffer << CHUNKED_TERMINATOR
|
|
1182
1127
|
socket_write(socket, buffer)
|
|
1183
1128
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
1184
1129
|
response << file.read(content_length)
|
|
@@ -1221,7 +1166,8 @@ module Raptor
|
|
|
1221
1166
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1222
1167
|
|
|
1223
1168
|
if use_chunked
|
|
1224
|
-
response
|
|
1169
|
+
HttpParser.chunked_encode(response, chunk)
|
|
1170
|
+
response << CHUNKED_TERMINATOR
|
|
1225
1171
|
socket_write(socket, response)
|
|
1226
1172
|
else
|
|
1227
1173
|
socket_writev(socket, [response, chunk])
|
|
@@ -1244,15 +1190,13 @@ module Raptor
|
|
|
1244
1190
|
body_array.each do |chunk|
|
|
1245
1191
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1246
1192
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
|
|
1193
|
+
HttpParser.chunked_encode(buffer, chunk)
|
|
1250
1194
|
if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
|
|
1251
1195
|
socket_write(socket, buffer)
|
|
1252
1196
|
buffer = +""
|
|
1253
1197
|
end
|
|
1254
1198
|
end
|
|
1255
|
-
buffer <<
|
|
1199
|
+
buffer << CHUNKED_TERMINATOR
|
|
1256
1200
|
socket_write(socket, buffer)
|
|
1257
1201
|
else
|
|
1258
1202
|
body_array.each do |chunk|
|
|
@@ -1275,14 +1219,15 @@ module Raptor
|
|
|
1275
1219
|
def write_enumerable_body(socket, response, body, use_chunked)
|
|
1276
1220
|
if use_chunked
|
|
1277
1221
|
socket_write(socket, response)
|
|
1222
|
+
buffer = +""
|
|
1278
1223
|
body.each do |chunk|
|
|
1279
1224
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1280
1225
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
socket_write(socket,
|
|
1226
|
+
buffer.clear
|
|
1227
|
+
HttpParser.chunked_encode(buffer, chunk)
|
|
1228
|
+
socket_write(socket, buffer) unless buffer.empty?
|
|
1284
1229
|
end
|
|
1285
|
-
socket_write(socket,
|
|
1230
|
+
socket_write(socket, CHUNKED_TERMINATOR)
|
|
1286
1231
|
else
|
|
1287
1232
|
body.each do |chunk|
|
|
1288
1233
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
@@ -1293,72 +1238,6 @@ module Raptor
|
|
|
1293
1238
|
end
|
|
1294
1239
|
end
|
|
1295
1240
|
|
|
1296
|
-
# Returns true if the header key contains characters illegal in HTTP headers.
|
|
1297
|
-
#
|
|
1298
|
-
# @param key [String] the header key to check
|
|
1299
|
-
# @return [Boolean] true if the key is illegal
|
|
1300
|
-
#
|
|
1301
|
-
# @rbs (String key) -> bool
|
|
1302
|
-
def illegal_header_key?(key)
|
|
1303
|
-
key.match?(ILLEGAL_HEADER_KEY_REGEX)
|
|
1304
|
-
end
|
|
1305
|
-
|
|
1306
|
-
# Returns true if the header value contains characters illegal in HTTP headers.
|
|
1307
|
-
#
|
|
1308
|
-
# @param value [String] the header value to check
|
|
1309
|
-
# @return [Boolean] true if the value is illegal
|
|
1310
|
-
#
|
|
1311
|
-
# @rbs (String value) -> bool
|
|
1312
|
-
def illegal_header_value?(value)
|
|
1313
|
-
value.match?(ILLEGAL_HEADER_VALUE_REGEX)
|
|
1314
|
-
end
|
|
1315
|
-
|
|
1316
|
-
# Appends normalised header lines to `result`. Skips entries with
|
|
1317
|
-
# illegal keys or values. Array values are written as separate lines.
|
|
1318
|
-
#
|
|
1319
|
-
# @param headers [Hash] normalized response headers
|
|
1320
|
-
# @return [String] formatted header lines, each ending with CRLF
|
|
1321
|
-
#
|
|
1322
|
-
# @rbs (String result, Hash[String, String | Array[String]] headers) -> void
|
|
1323
|
-
def format_headers(result, headers)
|
|
1324
|
-
headers.each do |name, value|
|
|
1325
|
-
next if illegal_header_key?(name)
|
|
1326
|
-
|
|
1327
|
-
if value.is_a?(Array)
|
|
1328
|
-
value.each { |entry| append_header_value(result, name, entry) }
|
|
1329
|
-
else
|
|
1330
|
-
append_header_value(result, name, value)
|
|
1331
|
-
end
|
|
1332
|
-
end
|
|
1333
|
-
end
|
|
1334
|
-
|
|
1335
|
-
# Appends one or more `name: value` header lines to `result`, splitting
|
|
1336
|
-
# newline-joined values across separate lines and skipping empty or
|
|
1337
|
-
# illegal values.
|
|
1338
|
-
#
|
|
1339
|
-
# @param result [String] the buffer to append to
|
|
1340
|
-
# @param name [String] the header name
|
|
1341
|
-
# @param value [Object] the header value (any object responding to `to_s`)
|
|
1342
|
-
# @return [void]
|
|
1343
|
-
#
|
|
1344
|
-
# @rbs (String result, String name, untyped value) -> void
|
|
1345
|
-
def append_header_value(result, name, value)
|
|
1346
|
-
string_value = value.is_a?(String) ? value : value.to_s
|
|
1347
|
-
return if string_value.empty?
|
|
1348
|
-
|
|
1349
|
-
if string_value.include?("\n")
|
|
1350
|
-
string_value.split("\n").each do |line|
|
|
1351
|
-
next if line.empty? || illegal_header_value?(line)
|
|
1352
|
-
|
|
1353
|
-
result << name << ": " << line << "\r\n"
|
|
1354
|
-
end
|
|
1355
|
-
else
|
|
1356
|
-
return if illegal_header_value?(string_value)
|
|
1357
|
-
|
|
1358
|
-
result << name << ": " << string_value << "\r\n"
|
|
1359
|
-
end
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
1241
|
# Calls every `rack.response_finished` callback in reverse
|
|
1363
1242
|
# registration order, rescuing any that raise.
|
|
1364
1243
|
#
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -229,8 +229,6 @@ module Raptor
|
|
|
229
229
|
MAX_FRAME_SIZE = 16_384
|
|
230
230
|
|
|
231
231
|
SERVER_PROTOCOL = "HTTP/2"
|
|
232
|
-
RACK_HEADER_PREFIX = "rack."
|
|
233
|
-
HOP_BY_HOP_HEADERS = ["connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"].freeze
|
|
234
232
|
|
|
235
233
|
REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
|
|
236
234
|
REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
|
|
@@ -766,20 +764,7 @@ module Raptor
|
|
|
766
764
|
def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
|
|
767
765
|
parser = Http2Parser.new
|
|
768
766
|
|
|
769
|
-
|
|
770
|
-
headers.each do |name, value|
|
|
771
|
-
lowered = name.downcase
|
|
772
|
-
next if lowered.start_with?(RACK_HEADER_PREFIX)
|
|
773
|
-
next if HOP_BY_HOP_HEADERS.include?(lowered)
|
|
774
|
-
|
|
775
|
-
if value.is_a?(Array)
|
|
776
|
-
value.each { |entry| header_pairs << [lowered, entry.to_s] }
|
|
777
|
-
else
|
|
778
|
-
header_pairs << [lowered, value.to_s]
|
|
779
|
-
end
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
encoded_headers = parser.encode_headers(header_pairs)
|
|
767
|
+
encoded_headers = parser.encode_response_headers(status, headers)
|
|
783
768
|
body_chunks = []
|
|
784
769
|
body_bytes = 0
|
|
785
770
|
body.each do |chunk|
|
data/lib/raptor/reuseport_bpf.rb
CHANGED
|
@@ -10,8 +10,8 @@ rescue LoadError
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
module Raptor
|
|
13
|
-
# Routes incoming connections across worker processes
|
|
14
|
-
# the
|
|
13
|
+
# Routes incoming connections across worker processes using two-choice
|
|
14
|
+
# load sampling, atomically reserving the selected slot before routing.
|
|
15
15
|
#
|
|
16
16
|
# Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
|
|
17
17
|
# BPF object file has been compiled. Falls back silently to standard
|
|
@@ -47,7 +47,7 @@ module Raptor
|
|
|
47
47
|
return false unless supported?
|
|
48
48
|
|
|
49
49
|
@object = LibBPFRuby::Object.new(BPF_OBJECT_PATH)
|
|
50
|
-
@program_fd = @object.program_fd("
|
|
50
|
+
@program_fd = @object.program_fd("select_less_loaded")
|
|
51
51
|
@socks_fd = @object.map_fd("socks")
|
|
52
52
|
@loads_fd = @object.map_fd("loads")
|
|
53
53
|
|
|
@@ -99,6 +99,7 @@ module Raptor
|
|
|
99
99
|
def self.enable_load_reporting(worker_index)
|
|
100
100
|
@load_key = [worker_index + 1].pack("L")
|
|
101
101
|
@load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
|
|
102
|
+
@accepted_load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
# Publishes this worker's current backlog to the BPF map.
|
|
@@ -115,6 +116,22 @@ module Raptor
|
|
|
115
116
|
LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
|
|
116
117
|
end
|
|
117
118
|
|
|
119
|
+
# Publishes an accept-side backlog estimate. This uses a separate value
|
|
120
|
+
# buffer because the server and load reporter call into the map from
|
|
121
|
+
# different threads.
|
|
122
|
+
#
|
|
123
|
+
# @param backlog [Integer] the estimated backlog after accepting a socket
|
|
124
|
+
# @return [void]
|
|
125
|
+
#
|
|
126
|
+
# @rbs (Integer backlog) -> void
|
|
127
|
+
def self.update_accepted_load(backlog)
|
|
128
|
+
@accepted_load_value.setbyte(0, backlog & 0xff)
|
|
129
|
+
@accepted_load_value.setbyte(1, (backlog >> 8) & 0xff)
|
|
130
|
+
@accepted_load_value.setbyte(2, (backlog >> 16) & 0xff)
|
|
131
|
+
@accepted_load_value.setbyte(3, (backlog >> 24) & 0xff)
|
|
132
|
+
LibBPFRuby.map_update(@loads_fd, @load_key, @accepted_load_value)
|
|
133
|
+
end
|
|
134
|
+
|
|
118
135
|
# Marks a worker's slot as unavailable by parking its load at the
|
|
119
136
|
# sentinel maximum so the BPF program's least-loaded pick skips it.
|
|
120
137
|
#
|
data/lib/raptor/server.rb
CHANGED
|
@@ -152,8 +152,9 @@ module Raptor
|
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
# Accepts a connection from the given listener
|
|
156
|
-
# the
|
|
155
|
+
# Accepts a connection from the given listener, immediately publishes
|
|
156
|
+
# the pending work to the BPF map, and dispatches the socket into the
|
|
157
|
+
# HTTP/1.1 or SSL/HTTP-2 pipeline.
|
|
157
158
|
#
|
|
158
159
|
# @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
|
|
159
160
|
# @return [Boolean] true if a connection was accepted, false if the listener had nothing to dispatch
|
|
@@ -172,6 +173,8 @@ module Raptor
|
|
|
172
173
|
remote_addr = DEFAULT_REMOTE_ADDR
|
|
173
174
|
end
|
|
174
175
|
|
|
176
|
+
ReuseportBPF.update_accepted_load(@reactor.backlog + 1) if @bpf_active
|
|
177
|
+
|
|
175
178
|
if listener.is_a?(Binder::SslListener)
|
|
176
179
|
@thread_pool << proc do
|
|
177
180
|
dispatch_ssl_connection(listener, tcp_client, remote_addr)
|
data/lib/raptor/version.rb
CHANGED
|
@@ -12,8 +12,6 @@ module Raptor
|
|
|
12
12
|
|
|
13
13
|
FILE_CHUNK_SIZE: untyped
|
|
14
14
|
|
|
15
|
-
MAX_CHUNK_OVERHEAD: untyped
|
|
16
|
-
|
|
17
15
|
READ_BUFFER_SIZE: untyped
|
|
18
16
|
|
|
19
17
|
RESPONSE_BUFFER_CAPACITY: untyped
|
|
@@ -30,8 +28,6 @@ module Raptor
|
|
|
30
28
|
|
|
31
29
|
STATUS_LINE_CACHE_11: untyped
|
|
32
30
|
|
|
33
|
-
STATUS_WITH_NO_ENTITY_BODY: untyped
|
|
34
|
-
|
|
35
31
|
CONTINUE_RESPONSE: ::String
|
|
36
32
|
|
|
37
33
|
BAD_REQUEST_RESPONSE: ::String
|
|
@@ -48,6 +44,8 @@ module Raptor
|
|
|
48
44
|
|
|
49
45
|
TRANSFER_ENCODING_CHUNKED: ::String
|
|
50
46
|
|
|
47
|
+
CHUNKED_TERMINATOR: ::String
|
|
48
|
+
|
|
51
49
|
HTTP_CONNECTION: ::String
|
|
52
50
|
|
|
53
51
|
HTTP_EXPECT: ::String
|
|
@@ -60,12 +58,6 @@ module Raptor
|
|
|
60
58
|
|
|
61
59
|
RACK_HIJACK_IO: ::String
|
|
62
60
|
|
|
63
|
-
ILLEGAL_HEADER_KEY_REGEX: ::Regexp
|
|
64
|
-
|
|
65
|
-
ILLEGAL_HEADER_VALUE_REGEX: ::Regexp
|
|
66
|
-
|
|
67
|
-
CHUNK_SIZE_REGEX: ::Regexp
|
|
68
|
-
|
|
69
61
|
# Returns true when an HTTP/1.1 request lacks a valid `Host` header per
|
|
70
62
|
# RFC 9112 section 3.2, where a valid value is a non-empty single-value
|
|
71
63
|
# line.
|
|
@@ -88,21 +80,6 @@ module Raptor
|
|
|
88
80
|
# @rbs (Hash[String, untyped] env) -> bool
|
|
89
81
|
def self.request_smuggling?: (Hash[String, untyped] env) -> bool
|
|
90
82
|
|
|
91
|
-
# Decodes a chunked transfer-encoded body buffer.
|
|
92
|
-
#
|
|
93
|
-
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
94
|
-
# terminating zero-length chunk and trailer section were fully consumed,
|
|
95
|
-
# `:too_large` when the decoded size would exceed `max_size`, `:malformed`
|
|
96
|
-
# when a chunk-size line is not valid hex or chunk framing overhead exceeds
|
|
97
|
-
# `MAX_CHUNK_OVERHEAD`, or `:incomplete` otherwise.
|
|
98
|
-
#
|
|
99
|
-
# @param buffer [String] the raw body buffer to decode
|
|
100
|
-
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
101
|
-
# @return [Array(String, Symbol)] decoded body and completion state
|
|
102
|
-
#
|
|
103
|
-
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
104
|
-
def self.decode_chunked: (String buffer, ?Integer? max_size) -> [ String, Symbol ]
|
|
105
|
-
|
|
106
83
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
107
84
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
108
85
|
# plus populated `:env` and `:body`; malformed or oversized input flips
|
|
@@ -117,27 +94,27 @@ module Raptor
|
|
|
117
94
|
# @rbs (Hash[Symbol, untyped] data, Hash[String, untyped] env_template, Integer? max_body_size) -> Hash[Symbol, untyped]
|
|
118
95
|
def self.parse: (Hash[Symbol, untyped] data, Hash[String, untyped] env_template, Integer? max_body_size) -> Hash[Symbol, untyped]
|
|
119
96
|
|
|
120
|
-
@
|
|
97
|
+
@app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
|
|
121
98
|
|
|
122
|
-
@
|
|
99
|
+
@server_port: Integer
|
|
123
100
|
|
|
124
|
-
@
|
|
101
|
+
@server_port_string: String
|
|
125
102
|
|
|
126
|
-
@
|
|
103
|
+
@write_timeout: Integer
|
|
127
104
|
|
|
128
|
-
@
|
|
105
|
+
@max_body_size: Integer?
|
|
129
106
|
|
|
130
107
|
@body_spool_threshold: Integer?
|
|
131
108
|
|
|
132
|
-
@
|
|
109
|
+
@max_keepalive_requests: Integer
|
|
133
110
|
|
|
134
|
-
@
|
|
111
|
+
@access_log_io: IO?
|
|
135
112
|
|
|
136
|
-
@
|
|
113
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
137
114
|
|
|
138
|
-
@
|
|
115
|
+
@running: AtomicBoolean
|
|
139
116
|
|
|
140
|
-
@
|
|
117
|
+
@env_template: Hash[String, untyped]
|
|
141
118
|
|
|
142
119
|
# Creates a new Http1 handler.
|
|
143
120
|
#
|
|
@@ -470,11 +447,12 @@ module Raptor
|
|
|
470
447
|
#
|
|
471
448
|
# @param headers [Hash] normalized response headers
|
|
472
449
|
# @param status [Integer] HTTP status code
|
|
450
|
+
# @param no_entity_body [Boolean] whether the status forbids an entity body
|
|
473
451
|
# @return [void]
|
|
474
452
|
# @raise [ArgumentError] if a forbidden header is present for the status
|
|
475
453
|
#
|
|
476
|
-
# @rbs (Hash[String, String | Array[String]] headers, Integer status) -> void
|
|
477
|
-
def validate_headers: (Hash[String, String | Array[String]] headers, Integer status) -> void
|
|
454
|
+
# @rbs (Hash[String, String | Array[String]] headers, Integer status, bool no_entity_body) -> void
|
|
455
|
+
def validate_headers: (Hash[String, String | Array[String]] headers, Integer status, bool no_entity_body) -> void
|
|
478
456
|
|
|
479
457
|
# Returns the HTTP status line for `status`.
|
|
480
458
|
#
|
|
@@ -504,11 +482,11 @@ module Raptor
|
|
|
504
482
|
# @param socket [TCPSocket] the client socket
|
|
505
483
|
# @param response [String] the status line accumulated so far
|
|
506
484
|
# @param headers [Hash] normalized response headers
|
|
507
|
-
# @param
|
|
485
|
+
# @param no_entity_body [Boolean] whether the response status forbids an entity body
|
|
508
486
|
# @return [void]
|
|
509
487
|
#
|
|
510
|
-
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers,
|
|
511
|
-
def write_no_body_response: (TCPSocket socket, String response, Hash[String, String | Array[String]] headers,
|
|
488
|
+
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, bool no_entity_body) -> void
|
|
489
|
+
def write_no_body_response: (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, bool no_entity_body) -> void
|
|
512
490
|
|
|
513
491
|
# Writes a complete response with a body. Emits a `Content-Length`
|
|
514
492
|
# when the total size is known upfront, otherwise chunked encoding on
|
|
@@ -592,43 +570,6 @@ module Raptor
|
|
|
592
570
|
# @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
|
|
593
571
|
def write_enumerable_body: (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
|
|
594
572
|
|
|
595
|
-
# Returns true if the header key contains characters illegal in HTTP headers.
|
|
596
|
-
#
|
|
597
|
-
# @param key [String] the header key to check
|
|
598
|
-
# @return [Boolean] true if the key is illegal
|
|
599
|
-
#
|
|
600
|
-
# @rbs (String key) -> bool
|
|
601
|
-
def illegal_header_key?: (String key) -> bool
|
|
602
|
-
|
|
603
|
-
# Returns true if the header value contains characters illegal in HTTP headers.
|
|
604
|
-
#
|
|
605
|
-
# @param value [String] the header value to check
|
|
606
|
-
# @return [Boolean] true if the value is illegal
|
|
607
|
-
#
|
|
608
|
-
# @rbs (String value) -> bool
|
|
609
|
-
def illegal_header_value?: (String value) -> bool
|
|
610
|
-
|
|
611
|
-
# Appends normalised header lines to `result`. Skips entries with
|
|
612
|
-
# illegal keys or values. Array values are written as separate lines.
|
|
613
|
-
#
|
|
614
|
-
# @param headers [Hash] normalized response headers
|
|
615
|
-
# @return [String] formatted header lines, each ending with CRLF
|
|
616
|
-
#
|
|
617
|
-
# @rbs (String result, Hash[String, String | Array[String]] headers) -> void
|
|
618
|
-
def format_headers: (String result, Hash[String, String | Array[String]] headers) -> void
|
|
619
|
-
|
|
620
|
-
# Appends one or more `name: value` header lines to `result`, splitting
|
|
621
|
-
# newline-joined values across separate lines and skipping empty or
|
|
622
|
-
# illegal values.
|
|
623
|
-
#
|
|
624
|
-
# @param result [String] the buffer to append to
|
|
625
|
-
# @param name [String] the header name
|
|
626
|
-
# @param value [Object] the header value (any object responding to `to_s`)
|
|
627
|
-
# @return [void]
|
|
628
|
-
#
|
|
629
|
-
# @rbs (String result, String name, untyped value) -> void
|
|
630
|
-
def append_header_value: (String result, String name, untyped value) -> void
|
|
631
|
-
|
|
632
573
|
# Calls every `rack.response_finished` callback in reverse
|
|
633
574
|
# registration order, rescuing any that raise.
|
|
634
575
|
#
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Generated from lib/raptor/reuseport_bpf.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
module Raptor
|
|
4
|
-
# Routes incoming connections across worker processes
|
|
5
|
-
# the
|
|
4
|
+
# Routes incoming connections across worker processes using two-choice
|
|
5
|
+
# load sampling, atomically reserving the selected slot before routing.
|
|
6
6
|
#
|
|
7
7
|
# Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
|
|
8
8
|
# BPF object file has been compiled. Falls back silently to standard
|
|
@@ -62,6 +62,16 @@ module Raptor
|
|
|
62
62
|
# @rbs (Integer backlog) -> void
|
|
63
63
|
def self.update_load: (Integer backlog) -> void
|
|
64
64
|
|
|
65
|
+
# Publishes an accept-side backlog estimate. This uses a separate value
|
|
66
|
+
# buffer because the server and load reporter call into the map from
|
|
67
|
+
# different threads.
|
|
68
|
+
#
|
|
69
|
+
# @param backlog [Integer] the estimated backlog after accepting a socket
|
|
70
|
+
# @return [void]
|
|
71
|
+
#
|
|
72
|
+
# @rbs (Integer backlog) -> void
|
|
73
|
+
def self.update_accepted_load: (Integer backlog) -> void
|
|
74
|
+
|
|
65
75
|
# Marks a worker's slot as unavailable by parking its load at the
|
|
66
76
|
# sentinel maximum so the BPF program's least-loaded pick skips it.
|
|
67
77
|
#
|
|
@@ -98,8 +98,9 @@ module Raptor
|
|
|
98
98
|
# @rbs () -> void
|
|
99
99
|
def drain_accept_queue: () -> void
|
|
100
100
|
|
|
101
|
-
# Accepts a connection from the given listener
|
|
102
|
-
# the
|
|
101
|
+
# Accepts a connection from the given listener, immediately publishes
|
|
102
|
+
# the pending work to the BPF map, and dispatches the socket into the
|
|
103
|
+
# HTTP/1.1 or SSL/HTTP-2 pipeline.
|
|
103
104
|
#
|
|
104
105
|
# @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
|
|
105
106
|
# @return [Boolean] true if a connection was accepted, false if the listener had nothing to dispatch
|