raptor 0.10.0 → 0.11.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/CHANGELOG.md +14 -0
- data/README.md +9 -9
- data/docs/raptor-vs-puma.md +45 -34
- data/ext/raptor_http/raptor_http.c +28 -3
- data/lib/raptor/cli.rb +2 -2
- data/lib/raptor/http.rb +1 -0
- data/lib/raptor/http1.rb +71 -24
- data/lib/raptor/http2.rb +46 -3
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/http.rbs +2 -0
- data/sig/generated/raptor/http1.rbs +18 -5
- data/sig/generated/raptor/http2.rbs +16 -0
- 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: be4ff0cf1729c188fab8261b749c2a279616bd565dbad81d18f5a3e75311b734
|
|
4
|
+
data.tar.gz: 0acc2ece57f69207a34696df792e45bf52bfce22bb7617c5ceb3113223c175f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 787fabc9128f3c4e16a394dc5634ff8435916b60b68ff3f6d569b0db561f07af0c61ab5d17e7440301c850d87fbbe1f0dc3691274b1c060d1cbbd6f7bc84bfbf
|
|
7
|
+
data.tar.gz: 26d154b6c0abbc63986262e1212e526bd9e4e492d06f9a86e723e73fd85afd142550a15324b55ad91b575633db2d0231f262aebb818f198e2de4c96c9afa8666
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-07-12
|
|
4
|
+
|
|
5
|
+
- Yield subsequent HTTP/1.1 keep-alive requests to the thread pool only under saturation
|
|
6
|
+
- Reuse a per-thread HTTP/1.1 parser across requests
|
|
7
|
+
- Skip a per-response allocation when formatting response headers
|
|
8
|
+
- Reject HTTP/2 streams with malformed pseudo-headers with `RST_STREAM`
|
|
9
|
+
- Populate `PATH_INFO` from absolute-form request targets
|
|
10
|
+
- Consume the trailer section after the chunked-body terminator
|
|
11
|
+
- Reject request headers larger than 112KB with 400
|
|
12
|
+
- Reject chunk sizes containing non-hex characters with 400
|
|
13
|
+
- Advance the parser's `nread` past the request header terminator
|
|
14
|
+
- Detect chunked `Transfer-Encoding` case-insensitively
|
|
15
|
+
- Reject HTTP/1.1 requests without a valid `Host` header with 400
|
|
16
|
+
|
|
3
17
|
## [0.10.0] - 2026-07-07
|
|
4
18
|
|
|
5
19
|
- Memoize the server port string for the Rack env
|
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 4 -t 3 hello_world.ru
|
|
39
39
|
[Raptor 76577|Main|Main] Cluster initializing:
|
|
40
|
-
[Raptor 76577|Main|Main] ├─ Version: 0.
|
|
40
|
+
[Raptor 76577|Main|Main] ├─ Version: 0.11.0
|
|
41
41
|
[Raptor 76577|Main|Main] ├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
42
42
|
[Raptor 76577|Main|Main] ├─ Environment: development
|
|
43
43
|
[Raptor 76577|Main|Main] ├─ Master PID: 76577
|
|
@@ -187,20 +187,20 @@ Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_
|
|
|
187
187
|
|
|
188
188
|
## (Micro) Benchmarks
|
|
189
189
|
|
|
190
|
-
Raptor 0.
|
|
190
|
+
Raptor 0.11.0 vs Puma 8.0.2, median of 3 runs across two workload profiles: IO-bound (sleep for a random 1-10ms then
|
|
191
191
|
return small JSON) and CPU-bound (serialise a JSON array of 20-200 items).
|
|
192
192
|
|
|
193
193
|
| Protocol | Workload | Raptor | Puma | +/- vs Puma |
|
|
194
194
|
| --------------------- | -------- | ------------ | ------------ | ----------- |
|
|
195
|
-
| HTTP/1.1 | IO |
|
|
196
|
-
| HTTP/1.1 | CPU |
|
|
197
|
-
| HTTP/1.1 (keep-alive) | IO |
|
|
198
|
-
| HTTP/1.1 (keep-alive) | CPU | 27.
|
|
199
|
-
| HTTP/2 | IO | 0.
|
|
200
|
-
| HTTP/2 | CPU |
|
|
195
|
+
| HTTP/1.1 | IO | 2.05k req/s | 1.94k req/s | +5.7% |
|
|
196
|
+
| HTTP/1.1 | CPU | 13.66k req/s | 9.21k req/s | +48.3% |
|
|
197
|
+
| HTTP/1.1 (keep-alive) | IO | 2.08k req/s | 1.95k req/s | +6.6% |
|
|
198
|
+
| HTTP/1.1 (keep-alive) | CPU | 27.85k req/s | 27.47k req/s | +1.4% |
|
|
199
|
+
| HTTP/2 | IO | 0.96k req/s | N/A | - |
|
|
200
|
+
| HTTP/2 | CPU | 28.92k req/s | N/A | - |
|
|
201
201
|
|
|
202
202
|
> ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [aarch64-linux]
|
|
203
|
-
> 4 workers, 3 threads,
|
|
203
|
+
> 4 workers, 3 threads, 48 concurrent connections
|
|
204
204
|
|
|
205
205
|
See [bin/benchmark](bin/benchmark) for more details.
|
|
206
206
|
|
data/docs/raptor-vs-puma.md
CHANGED
|
@@ -12,7 +12,7 @@ This document walks through both servers at systems-design depth. By the end you
|
|
|
12
12
|
|
|
13
13
|
## The shape of the benchmark
|
|
14
14
|
|
|
15
|
-
The Raptor README carries the [current head-to-head numbers](../README.md#micro-benchmarks) against the latest Puma release, run on the same hardware with the same Rack app, 4 workers, 3 threads,
|
|
15
|
+
The Raptor README carries the [current head-to-head numbers](../README.md#micro-benchmarks) against the latest Puma release, run on the same hardware with the same Rack app, 4 workers, 3 threads, 48 concurrent connections, on a recent Ruby with YJIT enabled. Two workload profiles are measured: **IO-bound** (each request sleeps for a random 1-10ms then returns a small JSON response) and **CPU-bound** (each request serialises a JSON array of 20-200 items). Rather than pin specific numbers into this document (they drift with Ruby versions and hardware), the shape of the result is what matters.
|
|
16
16
|
|
|
17
17
|
- On IO-bound work, both servers land near parity across all protocols; throughput is bounded by the per-request sleep and the benchmark client's concurrency, not by anything the server does.
|
|
18
18
|
- On CPU-bound HTTP/1.1 without keep-alive, Raptor holds a meaningful lead; per-request parsing and response-writing overhead is a real fraction of the work.
|
|
@@ -84,7 +84,7 @@ Puma stores its reactor timeouts in a Ruby array of `Client` objects. New client
|
|
|
84
84
|
|
|
85
85
|
Cross-thread waking is done through a pipe. When work needs to enter the reactor from another thread (a Client being registered), a byte is written to a pipe that the selector is also watching; the selector wakes, drains the input queue, and gets on with it.
|
|
86
86
|
|
|
87
|
-
###
|
|
87
|
+
### HTTP/1.1 request lifecycle
|
|
88
88
|
|
|
89
89
|
Putting the pieces together, a request through Puma looks like this:
|
|
90
90
|
|
|
@@ -104,16 +104,17 @@ Response writing is done from the app thread. Puma sets `TCP_CORK` on Linux, wri
|
|
|
104
104
|
```mermaid
|
|
105
105
|
flowchart TB
|
|
106
106
|
subgraph Master["Master process, cluster mode"]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
MM["Master loop<br/>Reads worker pipes for<br/>per-worker vitals<br/>Handles signals"]
|
|
108
|
+
MM -->|"fork"| W1["Worker 0"]
|
|
109
|
+
MM -->|"fork"| W2["Worker 1"]
|
|
110
|
+
MM -->|"fork"| W3["Worker N"]
|
|
111
|
+
MM -.->|"SIGUSR2<br/>exec with argv"| MM
|
|
111
112
|
end
|
|
112
113
|
|
|
113
|
-
subgraph Worker["Puma worker process"]
|
|
114
|
-
|
|
114
|
+
subgraph Worker["Puma worker process, one of N"]
|
|
115
|
+
SRV["Server thread<br/>IO.select on listeners<br/>accept_nonblock"]
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
RCT["Reactor thread<br/>NIO::Selector<br/>sorted timeout array"]
|
|
117
118
|
|
|
118
119
|
subgraph TP["Thread pool, Mutex + CondVar"]
|
|
119
120
|
T1["Thread 1"]
|
|
@@ -121,37 +122,45 @@ flowchart TB
|
|
|
121
122
|
T3["Thread N"]
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
STA["Stat thread<br/>writes to worker pipe<br/>on worker_check_interval"]
|
|
126
|
+
|
|
124
127
|
C1{"Complete<br/>request<br/>on accept?"}
|
|
125
128
|
C2{"Complete<br/>request<br/>after read?"}
|
|
126
129
|
KA{"Keep-alive?"}
|
|
127
130
|
BB{"Back-to-back<br/>data in buffer?"}
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
SRV -->|"push new socket"| TP
|
|
130
133
|
TP -->|"eagerly_finish"| C1
|
|
131
134
|
C1 -->|"complete, app.call"| APP["Rack app"]
|
|
132
|
-
C1 -->|"incomplete, register"|
|
|
133
|
-
|
|
135
|
+
C1 -->|"incomplete, register"| RCT
|
|
136
|
+
RCT -->|"socket readable"| C2
|
|
134
137
|
C2 -->|"complete, push"| TP
|
|
135
|
-
C2 -->|"incomplete, keep waiting"|
|
|
136
|
-
|
|
138
|
+
C2 -->|"incomplete, keep waiting"| RCT
|
|
139
|
+
RCT -->|"timeout"| TO["write 408, close"]
|
|
137
140
|
APP -->|"write response"| KA
|
|
138
|
-
KA -->|"no"|
|
|
141
|
+
KA -->|"no"| CLS["close socket"]
|
|
139
142
|
KA -->|"yes"| BB
|
|
140
143
|
BB -->|"yes, reprocess"| TP
|
|
141
|
-
BB -->|"no, back to reactor"|
|
|
144
|
+
BB -->|"no, back to reactor"| RCT
|
|
145
|
+
|
|
146
|
+
STA -.->|"writes ping"| PIPE[("worker→master pipe")]
|
|
142
147
|
end
|
|
143
148
|
|
|
149
|
+
MM -.->|"reads ping"| PIPE
|
|
150
|
+
|
|
144
151
|
classDef accept fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
|
|
145
152
|
classDef reactor fill:#fecdd3,stroke:#e11d48,color:#881337
|
|
146
153
|
classDef pool fill:#fef3c7,stroke:#d97706,color:#78350f
|
|
147
154
|
classDef app fill:#d1fae5,stroke:#059669,color:#064e3b
|
|
148
|
-
|
|
149
|
-
class
|
|
155
|
+
classDef storage fill:#e5e7eb,stroke:#6b7280,color:#374151
|
|
156
|
+
class SRV accept
|
|
157
|
+
class RCT reactor
|
|
150
158
|
class TP pool
|
|
151
159
|
class APP app
|
|
160
|
+
class PIPE storage
|
|
152
161
|
```
|
|
153
162
|
|
|
154
|
-
|
|
163
|
+
Notably, the parser runs inside an app thread. Puma's concurrency model is "many app threads, each doing one full request (parse plus app plus write) at a time". Under MRI's GVL this is genuine concurrency but not parallelism; at any given moment at most one of those threads is executing Ruby, and the others are either blocked on I/O (which releases the GVL and lets another thread proceed) or waiting their turn. Puma leans on the fact that most Rack apps spend most of their time blocked on the database or an external API, where threads do effectively overlap.
|
|
155
164
|
|
|
156
165
|
## Part II: Raptor
|
|
157
166
|
|
|
@@ -175,7 +184,7 @@ Master-to-worker communication does not use pipes. Every worker writes its stats
|
|
|
175
184
|
|
|
176
185
|
On Linux, each worker pins itself to a distinct CPU via `sched_setaffinity` when the worker count fits within the process's allowed CPU set, so it stays on one core and its L1/L2 caches stay warm. When workers outnumber available CPUs the pin is skipped and the kernel scheduler manages placement.
|
|
177
186
|
|
|
178
|
-
###
|
|
187
|
+
### Threading model
|
|
179
188
|
|
|
180
189
|
This is where Raptor diverges dramatically. Inside a worker there are five kinds of concurrent activity:
|
|
181
190
|
|
|
@@ -188,11 +197,13 @@ This is where Raptor diverges dramatically. Inside a worker there are five kinds
|
|
|
188
197
|
|
|
189
198
|
That is a lot of moving parts. Let us go through why.
|
|
190
199
|
|
|
191
|
-
**Why Ractors for parsing.** Ractors are Ruby's answer to true parallelism. Multiple Ractors can execute Ruby code simultaneously on different OS threads, each with its own GVL. But Ractors are heavily restricted
|
|
200
|
+
**Why Ractors for parsing.** Ractors are Ruby's answer to true parallelism. Multiple Ractors can execute Ruby code simultaneously on different OS threads, each with its own GVL. But Ractors are heavily restricted. They can only share frozen data, they cannot access most global mutable state, and code inside a Ractor cannot use most existing gems (which universally assume shared-state semantics).
|
|
192
201
|
|
|
193
202
|
For a web server, this restriction turns out to be almost exactly right for HTTP parsing. Parsing a request is CPU-bound (tokenising bytes, uppercasing header names, decoding chunked bodies), it does not need to touch any global state, and it produces a result (a hash) that can be safely frozen and handed off. The native HTTP/1 parser (`raptor_http.c`) is declared `rb_ext_ractor_safe(true)`: it holds no per-parser Ruby state in the extension itself, and it writes only into the caller-supplied env hash. Same for the HTTP/2 parser plus HPACK.
|
|
194
203
|
|
|
195
|
-
The
|
|
204
|
+
The HTTP/1 parser also pre-interns the ~40 most common header keys (`HTTP_HOST`, `HTTP_USER_AGENT`, the `HTTP_ACCEPT_*` family, `CONTENT_LENGTH`, `HTTP_X_FORWARDED_*`, the `HTTP_SEC_FETCH_*` client hints, and so on) once at load time. During parsing, a `memcmp` lookup against that table returns the shared frozen `VALUE` for known keys and falls back to `rb_enc_interned_str` for the rest. Every request's env hash therefore reuses the same String object for its header names, which both skips per-request allocation and lets Ruby's hash lookup use the interned key's cached hash code.
|
|
205
|
+
|
|
206
|
+
The upshot is that while your Rack app runs on regular threads under the GVL (so your app does not need to be Ractor-safe), the protocol-level work runs in parallel across Ractors. Under heavy load with lots of small requests, the GVL contention that would otherwise dominate parsing simply is not there.
|
|
196
207
|
|
|
197
208
|
**How the Ractor pool actually works.** Raptor uses the `ractor-pool` gem, which is another one of my libraries. The pool has one coordinator Ractor and M pipeline Ractors. When a pipeline Ractor is idle, it sends itself back to the coordinator via `coordinator.send(Ractor.current, move: true)`. When work arrives at the coordinator, it either forwards it to a waiting Ractor (if any) or queues it. This coordinator-dispatch pattern guarantees that no Ractor sits idle while there is work. Results flow back through a shared `Ractor::Port` (a many-to-one channel added in recent Ruby versions and stable in 4.0) to a Ruby-side collector thread. If `M == 1` the coordinator is skipped and work goes straight to the single pipeline Ractor; this is the default because a single Ractor already parses in parallel with the app threads and adds enough headroom for typical workloads.
|
|
198
209
|
|
|
@@ -341,7 +352,7 @@ flowchart TB
|
|
|
341
352
|
ATP -->|"app.call + write"| KA
|
|
342
353
|
KA -->|"no, close"| CLS["close socket"]
|
|
343
354
|
KA -->|"yes"| EAG
|
|
344
|
-
EAG
|
|
355
|
+
EAG -.->|"bytes within 1ms, parse+dispatch on same thread"| ATP
|
|
345
356
|
EAG -->|"no bytes, reactor.persist"| RCT
|
|
346
357
|
RCT -->|"timeout expired"| TO["write 408, close"]
|
|
347
358
|
|
|
@@ -364,7 +375,7 @@ flowchart TB
|
|
|
364
375
|
class SHM storage
|
|
365
376
|
```
|
|
366
377
|
|
|
367
|
-
The critical structural difference from Puma
|
|
378
|
+
The critical structural difference from Puma is that parsing is not on the app thread. It is on the Ractor pool. The app thread only does the Rack call and the response write. This decouples the two costs and lets Ruby actually use more than one CPU for the protocol work.
|
|
368
379
|
|
|
369
380
|
## Part III: Head to head
|
|
370
381
|
|
|
@@ -398,11 +409,11 @@ Under moderate load these look equivalent. Under high concurrency (many threads
|
|
|
398
409
|
|
|
399
410
|
**Puma.** After a response, if the connection is keep-alive and there are already buffered bytes for the next request (`has_back_to_back_requests?`) and there is a spare app thread, loop inline. Otherwise, if `eagerly_finish` (a single non-blocking read attempt) returns true, either loop inline (if spare threads) or hand back to the thread pool (`@thread_pool << client`). Otherwise, back to the reactor with `@persistent_timeout`.
|
|
400
411
|
|
|
401
|
-
**Raptor.** After a response, the app thread does `socket.wait_readable(0.001)`, waiting up to 1ms for bytes. If bytes arrive,
|
|
412
|
+
**Raptor.** After a response, the app thread does `socket.wait_readable(0.001)`, waiting up to 1ms for bytes. If bytes arrive, it parses the next request inline. If the thread pool queue is at least as deep as the pool, the parsed request is handed back to the pool so other threads share the load; otherwise the same thread dispatches it inline. If no bytes arrive, `reactor.persist` and return.
|
|
402
413
|
|
|
403
|
-
The difference is subtle but the numbers show it up. Real-world clients pipelining requests often send the next request 100-500 microseconds after the previous response completes. Puma's `eagerly_finish` catches the case where bytes are already in the socket buffer; Raptor's `wait_readable(1ms)` catches both the "already buffered" case and the "arriving very shortly" case. And Raptor
|
|
414
|
+
The difference is subtle but the numbers show it up. Real-world clients pipelining requests often send the next request 100-500 microseconds after the previous response completes. Puma's `eagerly_finish` catches the case where bytes are already in the socket buffer; Raptor's `wait_readable(1ms)` catches both the "already buffered" case and the "arriving very shortly" case. And Raptor always parses the next request on the response-writing thread, so the parse itself never crosses a thread boundary.
|
|
404
415
|
|
|
405
|
-
This is where most of Raptor's keep-alive edge comes from.
|
|
416
|
+
This is where most of Raptor's keep-alive edge comes from. Subsequent requests are always parsed and dispatched without a reactor round-trip: the response-writing thread either continues serving that connection inline (when the pool queue is shallower than the pool) or hands the parsed request back to the pool for another thread to pick up (when it is not). Puma's app threads do the same when they can, but the coordination overhead between thread pool and reactor for the transitions costs meaningfully more.
|
|
406
417
|
|
|
407
418
|
### Backpressure
|
|
408
419
|
|
|
@@ -434,6 +445,10 @@ On the HTTP/1.1 path, Raptor has a small `writev(2)` C extension (`Raptor::Vecto
|
|
|
434
445
|
|
|
435
446
|
HTTP/1.1 responses also reuse a per-thread String buffer for the status line and headers rather than allocating one per response. The buffer grows once to fit the largest response the thread has served and stays that size afterwards, so subsequent responses on that thread skip the allocation entirely.
|
|
436
447
|
|
|
448
|
+
Chunked responses with an array or file body accumulate chunk-framed writes into the response buffer up to 512KB before flushing to the socket, collapsing what would be N `write` syscalls for an N-chunk body into a handful. Enumerable bodies (SSE, long-polling, per-line log tailing) still flush every chunk, so streaming semantics are preserved.
|
|
449
|
+
|
|
450
|
+
Around the response boundary, HTTP/1.1 also amortises the common per-request allocations. A frozen Rack env template with the static keys (`rack.version`, `rack.hijack?`, `SCRIPT_NAME`, `QUERY_STRING`, `SERVER_SOFTWARE`) is duped per request, so the app-thread env build only writes the dynamic keys. Response header serialisation appends directly onto the response buffer instead of allocating intermediate `Array` wraps and `flat_map` products for each header value.
|
|
451
|
+
|
|
437
452
|
### Keep-alive request by request
|
|
438
453
|
|
|
439
454
|
Because the keep-alive fast path is where Raptor's throughput edge shows up, here is each server processing three back-to-back requests on the same connection. Drawn separately so the participant columns stay wide enough to read. Compare the number of boundaries each request has to cross.
|
|
@@ -476,16 +491,12 @@ sequenceDiagram
|
|
|
476
491
|
autonumber
|
|
477
492
|
participant Client
|
|
478
493
|
participant RS as Server thread
|
|
479
|
-
participant RR as Reactor thread
|
|
480
|
-
participant RC as Pipeline Ractor
|
|
481
494
|
participant RP as App thread
|
|
482
495
|
|
|
483
496
|
Note over Client,RP: Request 1, initial
|
|
484
497
|
Client->>RS: SYN, request bytes
|
|
485
|
-
RS->>
|
|
486
|
-
|
|
487
|
-
RC->>RC: parse on own Ractor GVL
|
|
488
|
-
RC->>RP: dispatch complete request
|
|
498
|
+
RS->>RS: eager_accept, read + parse inline
|
|
499
|
+
RS->>RP: push proc to thread pool
|
|
489
500
|
RP->>Client: response 1
|
|
490
501
|
|
|
491
502
|
Note over Client,RP: Request 2, pipelined a few hundred us later
|
|
@@ -508,7 +519,7 @@ Puma has to bounce Request 3 through the reactor and back through the mutex-prot
|
|
|
508
519
|
|
|
509
520
|
### IO-bound work, close to parity
|
|
510
521
|
|
|
511
|
-
On the IO-bound benchmark profile, each request sleeps for a random
|
|
522
|
+
On the IO-bound benchmark profile, each request sleeps for a random 1-10ms then returns a small JSON payload. Throughput is bounded by the per-request sleep and the benchmark client's 48-connection concurrency; both servers push those requests through their pipelines faster than the sleep completes, so what shows up in the numbers is the workload cost itself, not the server cost. Raptor and Puma land within a few percent of each other in both keep-alive modes because there is nothing to differentiate.
|
|
512
523
|
|
|
513
524
|
Real applications that spend most of their time waiting on a database or an upstream service look like this. The choice between Raptor and Puma there is not throughput-driven.
|
|
514
525
|
|
|
@@ -121,6 +121,21 @@ static inline void upcase_header_char(char *c) {
|
|
|
121
121
|
*c = '_';
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
static int contains_chunked(const char *value, long len) {
|
|
125
|
+
static const char chunked[] = "chunked";
|
|
126
|
+
static const long chunked_len = sizeof(chunked) - 1;
|
|
127
|
+
|
|
128
|
+
if (len < chunked_len) return 0;
|
|
129
|
+
for (long start = 0; start + chunked_len <= len; start++) {
|
|
130
|
+
long i;
|
|
131
|
+
for (i = 0; i < chunked_len; i++) {
|
|
132
|
+
if ((char)tolower((unsigned char)value[start + i]) != chunked[i]) break;
|
|
133
|
+
}
|
|
134
|
+
if (i == chunked_len) return 1;
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
static const int raptor_parser_start = 1;
|
|
125
140
|
static const int raptor_parser_first_final = 46;
|
|
126
141
|
static const int raptor_parser_error = 0;
|
|
@@ -392,7 +407,7 @@ tr26:
|
|
|
392
407
|
parser->flags |= FLAG_HAS_BODY;
|
|
393
408
|
} else if (needs_http_prefix && parser->field_len == 17 &&
|
|
394
409
|
memcmp(field_ptr, "TRANSFER_ENCODING", 17) == 0) {
|
|
395
|
-
if (
|
|
410
|
+
if (contains_chunked(value_ptr, value_real_len)) {
|
|
396
411
|
parser->flags |= FLAG_CHUNKED | FLAG_HAS_BODY;
|
|
397
412
|
parser->content_len = 0;
|
|
398
413
|
}
|
|
@@ -452,7 +467,7 @@ tr29:
|
|
|
452
467
|
parser->flags |= FLAG_HAS_BODY;
|
|
453
468
|
} else if (needs_http_prefix && parser->field_len == 17 &&
|
|
454
469
|
memcmp(field_ptr, "TRANSFER_ENCODING", 17) == 0) {
|
|
455
|
-
if (
|
|
470
|
+
if (contains_chunked(value_ptr, value_real_len)) {
|
|
456
471
|
parser->flags |= FLAG_CHUNKED | FLAG_HAS_BODY;
|
|
457
472
|
parser->content_len = 0;
|
|
458
473
|
}
|
|
@@ -1160,7 +1175,10 @@ case 45:
|
|
|
1160
1175
|
|
|
1161
1176
|
done:
|
|
1162
1177
|
parser->cs = cs;
|
|
1163
|
-
parser->nread = p - buffer;
|
|
1178
|
+
parser->nread = (parser->flags & FLAG_FINISHED) ? parser->body_start : (size_t)(p - buffer);
|
|
1179
|
+
|
|
1180
|
+
if (parser->nread > MAX_HEADER_LENGTH)
|
|
1181
|
+
rb_raise(eHttpParserError, "request header too long");
|
|
1164
1182
|
|
|
1165
1183
|
assert(p <= pe && "buffer overflow after parsing execute");
|
|
1166
1184
|
assert(parser->nread <= len && "nread longer than length");
|
|
@@ -1267,6 +1285,12 @@ static VALUE parser_has_body_p(VALUE self) {
|
|
|
1267
1285
|
return raptor_parser_has_body(parser) ? Qtrue : Qfalse;
|
|
1268
1286
|
}
|
|
1269
1287
|
|
|
1288
|
+
static VALUE parser_chunked_p(VALUE self) {
|
|
1289
|
+
raptor_parser *parser;
|
|
1290
|
+
TypedData_Get_Struct(self, raptor_parser, &parser_type, parser);
|
|
1291
|
+
return raptor_parser_is_chunked(parser) ? Qtrue : Qfalse;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1270
1294
|
static VALUE parser_content_length(VALUE self) {
|
|
1271
1295
|
raptor_parser *parser;
|
|
1272
1296
|
TypedData_Get_Struct(self, raptor_parser, &parser_type, parser);
|
|
@@ -1322,6 +1346,7 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
|
|
|
1322
1346
|
rb_define_method(cHttpParser, "execute", parser_execute, 3);
|
|
1323
1347
|
rb_define_method(cHttpParser, "finished?", parser_finished_p, 0);
|
|
1324
1348
|
rb_define_method(cHttpParser, "has_body?", parser_has_body_p, 0);
|
|
1349
|
+
rb_define_method(cHttpParser, "chunked?", parser_chunked_p, 0);
|
|
1325
1350
|
rb_define_method(cHttpParser, "content_length", parser_content_length, 0);
|
|
1326
1351
|
rb_define_method(cHttpParser, "nread", parser_nread, 0);
|
|
1327
1352
|
rb_define_method(cHttpParser, "reset", parser_reset, 0);
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -187,9 +187,9 @@ module Raptor
|
|
|
187
187
|
#
|
|
188
188
|
# @rbs (Array[String] argv) -> String?
|
|
189
189
|
def extract_config_path(argv)
|
|
190
|
-
argv.each_with_index do |arg,
|
|
190
|
+
argv.each_with_index do |arg, index|
|
|
191
191
|
case arg
|
|
192
|
-
when "-c", "--config" then return argv[
|
|
192
|
+
when "-c", "--config" then return argv[index + 1]
|
|
193
193
|
when /\A--config=(.*)\z/, /\A-c(.+)\z/ then return Regexp.last_match(1)
|
|
194
194
|
end
|
|
195
195
|
end
|
data/lib/raptor/http.rb
CHANGED
data/lib/raptor/http1.rb
CHANGED
|
@@ -58,6 +58,22 @@ module Raptor
|
|
|
58
58
|
|
|
59
59
|
ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
|
|
60
60
|
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
|
61
|
+
CHUNK_SIZE_REGEX = /\A[0-9A-Fa-f]+\z/
|
|
62
|
+
|
|
63
|
+
# Returns true when an HTTP/1.1 request lacks a valid `Host` header per
|
|
64
|
+
# RFC 9112 section 3.2, where a valid value is a non-empty single-value
|
|
65
|
+
# line.
|
|
66
|
+
#
|
|
67
|
+
# @param env [Hash] the Rack environment after header parsing
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
#
|
|
70
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
71
|
+
def self.invalid_host?(env)
|
|
72
|
+
return false unless env[Rack::SERVER_PROTOCOL] == HTTP_11
|
|
73
|
+
|
|
74
|
+
http_host = env[Rack::HTTP_HOST]
|
|
75
|
+
!http_host || http_host.empty? || http_host.include?(",")
|
|
76
|
+
end
|
|
61
77
|
|
|
62
78
|
# Returns true when the message framing shows a request-smuggling vector
|
|
63
79
|
# per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
|
|
@@ -89,9 +105,10 @@ module Raptor
|
|
|
89
105
|
# Decodes a chunked transfer-encoded body buffer.
|
|
90
106
|
#
|
|
91
107
|
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
92
|
-
# terminating zero-length chunk
|
|
93
|
-
# size would exceed `max_size`, `:malformed`
|
|
94
|
-
#
|
|
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.
|
|
95
112
|
#
|
|
96
113
|
# @param buffer [String] the raw body buffer to decode
|
|
97
114
|
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
@@ -107,8 +124,24 @@ module Raptor
|
|
|
107
124
|
crlf = buffer.index("\r\n", offset)
|
|
108
125
|
return [decoded, :incomplete] unless crlf
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
|
|
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 == 0
|
|
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
|
+
|
|
112
145
|
return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size
|
|
113
146
|
|
|
114
147
|
overhead += (crlf - offset) + 4
|
|
@@ -240,7 +273,8 @@ module Raptor
|
|
|
240
273
|
buffer << socket.read_nonblock(socket.pending)
|
|
241
274
|
end
|
|
242
275
|
|
|
243
|
-
parser = HttpParser.new
|
|
276
|
+
parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
|
|
277
|
+
parser.reset
|
|
244
278
|
env = @env_template.dup
|
|
245
279
|
nread = begin
|
|
246
280
|
parser.execute(env, buffer, 0)
|
|
@@ -254,7 +288,7 @@ module Raptor
|
|
|
254
288
|
if !parser.finished?
|
|
255
289
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
256
290
|
return
|
|
257
|
-
elsif Http1.request_smuggling?(env)
|
|
291
|
+
elsif Http1.invalid_host?(env) || Http1.request_smuggling?(env)
|
|
258
292
|
reject_malformed(socket)
|
|
259
293
|
return
|
|
260
294
|
elsif parser.has_body?
|
|
@@ -265,7 +299,7 @@ module Raptor
|
|
|
265
299
|
|
|
266
300
|
body = buffer.byteslice(nread..-1) || ""
|
|
267
301
|
|
|
268
|
-
if
|
|
302
|
+
if parser.chunked?
|
|
269
303
|
body, chunked_state = Http1.decode_chunked(body, @max_body_size)
|
|
270
304
|
case chunked_state
|
|
271
305
|
when :complete
|
|
@@ -322,14 +356,14 @@ module Raptor
|
|
|
322
356
|
parse_data[:parse_count] += 1
|
|
323
357
|
|
|
324
358
|
message = if parser.finished?
|
|
325
|
-
if Raptor::Http1.request_smuggling?(env)
|
|
359
|
+
if Raptor::Http1.invalid_host?(env) || Raptor::Http1.request_smuggling?(env)
|
|
326
360
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
327
361
|
elsif parser.has_body?
|
|
328
362
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
329
363
|
|
|
330
364
|
if max_body_size && parser.content_length > max_body_size
|
|
331
365
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
332
|
-
elsif
|
|
366
|
+
elsif parser.chunked?
|
|
333
367
|
decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)
|
|
334
368
|
|
|
335
369
|
case chunked_state
|
|
@@ -571,7 +605,8 @@ module Raptor
|
|
|
571
605
|
buffer << socket.read_nonblock(socket.pending)
|
|
572
606
|
end
|
|
573
607
|
|
|
574
|
-
parser = HttpParser.new
|
|
608
|
+
parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
|
|
609
|
+
parser.reset
|
|
575
610
|
env = @env_template.dup
|
|
576
611
|
nread = begin
|
|
577
612
|
parser.execute(env, buffer, 0)
|
|
@@ -588,8 +623,7 @@ module Raptor
|
|
|
588
623
|
elsif parser.has_body?
|
|
589
624
|
body = buffer.byteslice(nread..-1) || ""
|
|
590
625
|
|
|
591
|
-
chunked
|
|
592
|
-
if chunked || parser.content_length > body.bytesize
|
|
626
|
+
if parser.chunked? || parser.content_length > body.bytesize
|
|
593
627
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
594
628
|
return
|
|
595
629
|
end
|
|
@@ -597,7 +631,7 @@ module Raptor
|
|
|
597
631
|
|
|
598
632
|
request_count += 1
|
|
599
633
|
|
|
600
|
-
if thread_pool.queue_size
|
|
634
|
+
if thread_pool.queue_size >= thread_pool.size
|
|
601
635
|
thread_pool << proc do
|
|
602
636
|
process_client(
|
|
603
637
|
socket,
|
|
@@ -718,8 +752,23 @@ module Raptor
|
|
|
718
752
|
send_early_hints(socket, hints) rescue nil
|
|
719
753
|
end
|
|
720
754
|
|
|
721
|
-
|
|
722
|
-
|
|
755
|
+
unless env.key?(Rack::PATH_INFO)
|
|
756
|
+
request_uri = env[Http::REQUEST_URI]
|
|
757
|
+
scheme_end = request_uri&.index("://")
|
|
758
|
+
if scheme_end
|
|
759
|
+
authority_end = request_uri.index("/", scheme_end + 3) || request_uri.bytesize
|
|
760
|
+
path_and_query = request_uri.byteslice(authority_end..-1) || ""
|
|
761
|
+
if query_delim = path_and_query.index("?")
|
|
762
|
+
env[Rack::PATH_INFO] = query_delim.zero? ? "/" : path_and_query.byteslice(0, query_delim)
|
|
763
|
+
env[Rack::QUERY_STRING] = path_and_query.byteslice(query_delim + 1..-1)
|
|
764
|
+
else
|
|
765
|
+
env[Rack::PATH_INFO] = path_and_query.empty? ? "/" : path_and_query
|
|
766
|
+
end
|
|
767
|
+
else
|
|
768
|
+
env[Rack::PATH_INFO] = ""
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
|
|
723
772
|
if (content_length = parse_data[:content_length]).positive?
|
|
724
773
|
env[Http::CONTENT_LENGTH] = content_length.to_s
|
|
725
774
|
end
|
|
@@ -976,7 +1025,7 @@ module Raptor
|
|
|
976
1025
|
#
|
|
977
1026
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, ^(TCPSocket) -> void response_hijack) -> void
|
|
978
1027
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
979
|
-
response
|
|
1028
|
+
format_headers(response, headers)
|
|
980
1029
|
response << "\r\n"
|
|
981
1030
|
socket_write(socket, response)
|
|
982
1031
|
uncork_socket(socket)
|
|
@@ -1001,7 +1050,7 @@ module Raptor
|
|
|
1001
1050
|
headers[Rack::CONTENT_LENGTH] = "0" unless headers.key?(Rack::CONTENT_LENGTH) || headers.key?(Rack::TRANSFER_ENCODING)
|
|
1002
1051
|
end
|
|
1003
1052
|
|
|
1004
|
-
response
|
|
1053
|
+
format_headers(response, headers)
|
|
1005
1054
|
response << "\r\n"
|
|
1006
1055
|
socket_write(socket, response)
|
|
1007
1056
|
end
|
|
@@ -1023,7 +1072,7 @@ module Raptor
|
|
|
1023
1072
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
|
|
1024
1073
|
def write_full_response(socket, response, headers, body, http_version)
|
|
1025
1074
|
if body.respond_to?(:call)
|
|
1026
|
-
response
|
|
1075
|
+
format_headers(response, headers)
|
|
1027
1076
|
response << "\r\n"
|
|
1028
1077
|
socket_write(socket, response)
|
|
1029
1078
|
uncork_socket(socket)
|
|
@@ -1049,7 +1098,7 @@ module Raptor
|
|
|
1049
1098
|
headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED
|
|
1050
1099
|
end
|
|
1051
1100
|
|
|
1052
|
-
response
|
|
1101
|
+
format_headers(response, headers)
|
|
1053
1102
|
response << "\r\n"
|
|
1054
1103
|
|
|
1055
1104
|
if body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
|
|
@@ -1253,9 +1302,8 @@ module Raptor
|
|
|
1253
1302
|
# @param headers [Hash] normalized response headers
|
|
1254
1303
|
# @return [String] formatted header lines, each ending with CRLF
|
|
1255
1304
|
#
|
|
1256
|
-
# @rbs (Hash[String, String | Array[String]] headers) ->
|
|
1257
|
-
def format_headers(headers)
|
|
1258
|
-
result = +""
|
|
1305
|
+
# @rbs (String result, Hash[String, String | Array[String]] headers) -> void
|
|
1306
|
+
def format_headers(result, headers)
|
|
1259
1307
|
headers.each do |name, value|
|
|
1260
1308
|
next if illegal_header_key?(name)
|
|
1261
1309
|
|
|
@@ -1265,7 +1313,6 @@ module Raptor
|
|
|
1265
1313
|
append_header_value(result, name, value)
|
|
1266
1314
|
end
|
|
1267
1315
|
end
|
|
1268
|
-
result
|
|
1269
1316
|
end
|
|
1270
1317
|
|
|
1271
1318
|
# Appends one or more `name: value` header lines to `result`. Newline-
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -247,6 +247,39 @@ module Raptor
|
|
|
247
247
|
RACK_HEADER_PREFIX = "rack."
|
|
248
248
|
HOP_BY_HOP_HEADERS = ["connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"].freeze
|
|
249
249
|
|
|
250
|
+
REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
|
|
251
|
+
REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
|
|
252
|
+
|
|
253
|
+
# Returns true when a decoded header block violates the HTTP/2 pseudo-header
|
|
254
|
+
# rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate
|
|
255
|
+
# pseudo-header, a pseudo-header appearing after any regular header, or a
|
|
256
|
+
# required pseudo-header (`:method`, `:scheme`, `:path`) missing on
|
|
257
|
+
# non-`CONNECT` requests.
|
|
258
|
+
#
|
|
259
|
+
# @param headers [Array<Array(String, String)>] decoded header pairs
|
|
260
|
+
# @return [Boolean]
|
|
261
|
+
#
|
|
262
|
+
# @rbs (Array[[String, String]] headers) -> bool
|
|
263
|
+
def self.invalid_pseudo_headers?(headers)
|
|
264
|
+
seen_pseudo = {}
|
|
265
|
+
seen_regular = false
|
|
266
|
+
|
|
267
|
+
headers.each do |name, _value|
|
|
268
|
+
if name.start_with?(":")
|
|
269
|
+
return true if seen_regular
|
|
270
|
+
return true unless REQUEST_PSEUDO_HEADERS.include?(name)
|
|
271
|
+
return true if seen_pseudo[name]
|
|
272
|
+
seen_pseudo[name] = true
|
|
273
|
+
else
|
|
274
|
+
seen_regular = true
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
|
|
279
|
+
|
|
280
|
+
REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
|
|
281
|
+
end
|
|
282
|
+
|
|
250
283
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
251
284
|
# @rbs @server_port: Integer
|
|
252
285
|
# @rbs @write_timeout: Integer
|
|
@@ -370,7 +403,12 @@ module Raptor
|
|
|
370
403
|
|
|
371
404
|
if (frame[:flags] & FLAG_END_HEADERS) != 0
|
|
372
405
|
decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
|
|
373
|
-
|
|
406
|
+
if invalid_pseudo_headers?(decoded_headers)
|
|
407
|
+
streams.delete(stream_id)
|
|
408
|
+
outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
|
|
409
|
+
else
|
|
410
|
+
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
|
|
411
|
+
end
|
|
374
412
|
else
|
|
375
413
|
pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
|
|
376
414
|
end
|
|
@@ -386,7 +424,12 @@ module Raptor
|
|
|
386
424
|
if (frame[:flags] & FLAG_END_HEADERS) != 0
|
|
387
425
|
stream_id = pending_headers[:stream_id]
|
|
388
426
|
decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table)
|
|
389
|
-
|
|
427
|
+
if invalid_pseudo_headers?(decoded_headers)
|
|
428
|
+
streams.delete(stream_id)
|
|
429
|
+
outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
|
|
430
|
+
else
|
|
431
|
+
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
|
|
432
|
+
end
|
|
390
433
|
pending_headers = nil
|
|
391
434
|
end
|
|
392
435
|
|
|
@@ -688,7 +731,7 @@ module Raptor
|
|
|
688
731
|
next if HOP_BY_HOP_HEADERS.include?(lowered)
|
|
689
732
|
|
|
690
733
|
if value.is_a?(Array)
|
|
691
|
-
value.each { |
|
|
734
|
+
value.each { |entry| header_pairs << [lowered, entry.to_s] }
|
|
692
735
|
else
|
|
693
736
|
header_pairs << [lowered, value.to_s]
|
|
694
737
|
end
|
data/lib/raptor/version.rb
CHANGED
|
@@ -64,6 +64,18 @@ module Raptor
|
|
|
64
64
|
|
|
65
65
|
ILLEGAL_HEADER_VALUE_REGEX: ::Regexp
|
|
66
66
|
|
|
67
|
+
CHUNK_SIZE_REGEX: ::Regexp
|
|
68
|
+
|
|
69
|
+
# Returns true when an HTTP/1.1 request lacks a valid `Host` header per
|
|
70
|
+
# RFC 9112 section 3.2, where a valid value is a non-empty single-value
|
|
71
|
+
# line.
|
|
72
|
+
#
|
|
73
|
+
# @param env [Hash] the Rack environment after header parsing
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
#
|
|
76
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
77
|
+
def self.invalid_host?: (Hash[String, untyped] env) -> bool
|
|
78
|
+
|
|
67
79
|
# Returns true when the message framing shows a request-smuggling vector
|
|
68
80
|
# per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
|
|
69
81
|
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
@@ -79,9 +91,10 @@ module Raptor
|
|
|
79
91
|
# Decodes a chunked transfer-encoded body buffer.
|
|
80
92
|
#
|
|
81
93
|
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
82
|
-
# terminating zero-length chunk
|
|
83
|
-
# size would exceed `max_size`, `:malformed`
|
|
84
|
-
#
|
|
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.
|
|
85
98
|
#
|
|
86
99
|
# @param buffer [String] the raw body buffer to decode
|
|
87
100
|
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
@@ -579,8 +592,8 @@ module Raptor
|
|
|
579
592
|
# @param headers [Hash] normalized response headers
|
|
580
593
|
# @return [String] formatted header lines, each ending with CRLF
|
|
581
594
|
#
|
|
582
|
-
# @rbs (Hash[String, String | Array[String]] headers) ->
|
|
583
|
-
def format_headers: (Hash[String, String | Array[String]] headers) ->
|
|
595
|
+
# @rbs (String result, Hash[String, String | Array[String]] headers) -> void
|
|
596
|
+
def format_headers: (String result, Hash[String, String | Array[String]] headers) -> void
|
|
584
597
|
|
|
585
598
|
# Appends one or more `name: value` header lines to `result`. Newline-
|
|
586
599
|
# separated values are emitted as separate lines; empty values and values
|
|
@@ -145,6 +145,22 @@ module Raptor
|
|
|
145
145
|
|
|
146
146
|
HOP_BY_HOP_HEADERS: untyped
|
|
147
147
|
|
|
148
|
+
REQUEST_PSEUDO_HEADERS: untyped
|
|
149
|
+
|
|
150
|
+
REQUIRED_REQUEST_PSEUDO_HEADERS: untyped
|
|
151
|
+
|
|
152
|
+
# Returns true when a decoded header block violates the HTTP/2 pseudo-header
|
|
153
|
+
# rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate
|
|
154
|
+
# pseudo-header, a pseudo-header appearing after any regular header, or a
|
|
155
|
+
# required pseudo-header (`:method`, `:scheme`, `:path`) missing on
|
|
156
|
+
# non-`CONNECT` requests.
|
|
157
|
+
#
|
|
158
|
+
# @param headers [Array<Array(String, String)>] decoded header pairs
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
#
|
|
161
|
+
# @rbs (Array[[String, String]] headers) -> bool
|
|
162
|
+
def self.invalid_pseudo_headers?: (Array[[ String, String ]] headers) -> bool
|
|
163
|
+
|
|
148
164
|
@initial_settings_frame: String
|
|
149
165
|
|
|
150
166
|
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|