websocket
0

Configure Feed

Select the types of activity you want to include in your feed.

websocket.zig / src / server / handshake.zig
48 kB 1239 lines
1const std = @import("std"); 2 3const libc = std.c; 4const posix = std.posix; 5 6// 0.16: std.Thread.sleep removed, use libc nanosleep 7fn sleep(ns: u64) void { 8 const secs = ns / std.time.ns_per_s; 9 const nsecs = ns % std.time.ns_per_s; 10 var ts: libc.timespec = .{ 11 .sec = @intCast(secs), 12 .nsec = @intCast(nsecs), 13 }; 14 _ = libc.nanosleep(&ts, null); 15} 16const ascii = std.ascii; 17const Allocator = std.mem.Allocator; 18const websocket = @import("../websocket.zig"); 19 20const M = @This(); 21 22const SpecialHeader = enum { 23 none, 24 upgrade, 25 connection, 26 @"sec-websocket-key", 27 @"sec-websocket-version", 28 @"sec-websocket-extensions", 29}; 30 31pub const Handshake = struct { 32 url: []const u8, 33 key: []const u8, 34 method: []const u8, 35 headers: *KeyValue, 36 res_headers: *KeyValue, 37 raw_header: []const u8, 38 compression: ?Compression, 39 40 pub const Pool = M.Pool; 41 42 pub const Compression = struct { 43 client_no_context_takeover: bool, 44 server_no_context_takeover: bool, 45 }; 46 47 /// Incrementally parse an HTTP/1.1 request from `state.buf[0..state.len]`. 48 /// 49 /// Return values: 50 /// - `null` — more data needed to make a decision 51 /// - `Some(Handshake)` — valid RFC 6455 upgrade request 52 /// - `error.MissingHeaders` — well-formed HTTP/1.1 request that is NOT a 53 /// websocket upgrade. Held back until the request body (per 54 /// Content-Length) has fully arrived, so the worker's `httpFallback` 55 /// sees a complete request rather than a truncated one. 56 /// - other named errors — request is malformed beyond recovery; the 57 /// worker responds 400. 58 /// 59 /// Streaming: end-of-headers is detected by `std.http.HeadParser`, a 60 /// stdlib SIMD state machine that handles every TCP-fragmentation 61 /// boundary correctly (including splits *inside* CRLF and CRLFCRLF 62 /// sequences). The parser feeds only new bytes per call, so repeated 63 /// calls as the buffer grows are O(new_bytes), not O(buf). 64 /// 65 /// RFC compliance: 66 /// - whitespace between header name and ':' is rejected per 67 /// RFC 7230 §3.2 (request smuggling vector) 68 /// - simultaneous Transfer-Encoding and Content-Length is rejected 69 /// per RFC 7230 §3.3 (request smuggling vector) 70 /// - obsolete line folding (RFC 7230 §3.2.4) is not supported; 71 /// a folded continuation line will surface as InvalidHeader 72 /// - request bodies with Transfer-Encoding only (no Content-Length) 73 /// are passed through to httpFallback without the worker 74 /// waiting for body completeness — chunked decoding is the 75 /// fallback handler's responsibility, not this parser's. 76 /// 77 /// Crash discipline (per Zig zen "runtime crashes are better than bugs"): 78 /// four invariant-protected paths use `unreachable` so a contract 79 /// violation surfaces as a debuggable panic rather than a silent 80 /// misclassification of valid input as malformed. all other error 81 /// paths correspond to genuinely malformed external input. 82 pub fn parse(state: *State) !?Handshake { 83 // Phase 1: streaming detection of end-of-headers via std.http.HeadParser. 84 // feed only NEW bytes since last call so the SIMD scan is amortized. 85 const newly_arrived = state.buf[state.head_fed..state.len]; 86 state.head_fed += state.head_parser.feed(newly_arrived); 87 if (state.head_parser.state != .finished) return null; 88 89 // Phase 2: parse the bounded request. 90 // body_start = head_fed = position of first body byte (just past CRLFCRLF). 91 // Structure: 92 // REQUEST_LINE \r\n 93 // HEADER_LINE \r\n (zero or more) 94 // \r\n (blank line) 95 // [body] 96 const body_start = state.head_fed; 97 // HeadParser .finished means it consumed at least the 4-byte CRLFCRLF. 98 if (body_start < 4) unreachable; 99 100 // Any buffer of length ≥4 ending in CRLFCRLF contains at least one CRLF. 101 const first_crlf = std.mem.indexOf(u8, state.buf[0..body_start], "\r\n") orelse unreachable; 102 const request_line = state.buf[0..first_crlf]; 103 104 if (!ascii.endsWithIgnoreCase(request_line, "http/1.1")) { 105 return error.InvalidProtocol; 106 } 107 108 // headers accumulator. parse may be called multiple times on the 109 // same state (e.g. while waiting for body bytes), so reset to keep 110 // re-runs idempotent. 111 var headers = &state.req_headers; 112 headers.len = 0; 113 114 var key: []const u8 = ""; 115 var required_headers: u8 = 0; 116 var compression: ?Handshake.Compression = null; 117 118 // Errors that signal "this is not a websocket upgrade, dispatch via 119 // httpFallback" are *deferred* rather than returned inline. We need 120 // to finish parsing the headers regardless so the body-completeness 121 // check below sees `Content-Length`. Otherwise a fragmented POST 122 // (headers in one TCP read, body in the next) whose request also 123 // contains `Connection: keep-alive` or `Upgrade: ...` (non-websocket) 124 // would dispatch with a truncated body — handlers would observe 125 // body.len == 0 even though Content-Length was non-zero. 126 const DeferredFallbackErr = error{ InvalidUpgrade, InvalidConnection }; 127 var deferred_fallback_err: ?DeferredFallbackErr = null; 128 129 // header lines occupy [first_crlf + 2 .. body_start - 2], excluding 130 // the blank line's CRLF. each line ends in CRLF (RFC 7230 §3.2.4 131 // line folding deprecated; we don't support it). 132 var rest = state.buf[first_crlf + 2 .. body_start - 2]; 133 while (rest.len > 0) { 134 // rest is bounded by HeadParser's CRLFCRLF detection: every 135 // line in it ends in CRLF, so indexOf must find one. 136 const crlf = std.mem.indexOf(u8, rest, "\r\n") orelse unreachable; 137 const line = rest[0..crlf]; 138 rest = rest[crlf + 2 ..]; 139 140 // HeadParser stops at the FIRST empty line, so an empty line 141 // mid-iteration would mean rest was constructed wrong. 142 if (line.len == 0) unreachable; 143 144 // Leading whitespace = obsolete line folding (RFC 7230 §3.2.4) 145 // OR malformed header. Both are rejected. 146 if (ascii.isWhitespace(line[0])) return error.InvalidHeader; 147 148 const colon = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidHeader; 149 if (colon == 0) return error.InvalidHeader; // empty name 150 151 // RFC 7230 §3.2: "No whitespace is allowed between the header 152 // field-name and colon. Servers MUST reject ... with a 400." 153 // This is a request-smuggling defense, not stylistic. 154 if (ascii.isWhitespace(line[colon - 1])) return error.WhitespaceBeforeColon; 155 156 const name = toLower(line[0..colon]); 157 const value = std.mem.trim(u8, line[colon + 1 ..], &ascii.whitespace); 158 159 headers.add(name, value); 160 switch (std.meta.stringToEnum(SpecialHeader, name) orelse .none) { 161 .upgrade => { 162 if (!ascii.eqlIgnoreCase("websocket", value)) { 163 // Defer — keep parsing to find Content-Length. 164 if (deferred_fallback_err == null) deferred_fallback_err = error.InvalidUpgrade; 165 } else { 166 required_headers |= 1; 167 } 168 }, 169 .connection => { 170 // Connection: keep-alive, Upgrade — the spec allows multiple tokens 171 if (std.ascii.indexOfIgnoreCase(value, "upgrade") == null) { 172 // Defer — keep parsing to find Content-Length. 173 if (deferred_fallback_err == null) deferred_fallback_err = error.InvalidConnection; 174 } else { 175 required_headers |= 4; 176 } 177 }, 178 .@"sec-websocket-key" => { 179 key = value; 180 required_headers |= 8; 181 }, 182 .@"sec-websocket-version" => { 183 if (value.len != 2 or value[0] != '1' or value[1] != '3') { 184 return error.InvalidVersion; 185 } 186 required_headers |= 2; 187 }, 188 .@"sec-websocket-extensions" => compression = try parseExtension(value), 189 .none => {}, 190 } 191 } 192 193 // RFC 7230 §3.3: simultaneous Transfer-Encoding and Content-Length is 194 // a request-smuggling vector. The two headers can disagree about 195 // body length, letting an attacker hide a second request inside the 196 // first. Reject before any body-completeness logic runs. 197 if (headers.get("transfer-encoding") != null and headers.get("content-length") != null) { 198 return error.AmbiguousBodyLength; 199 } 200 201 // Single body-completeness gate covering BOTH `MissingHeaders` (no 202 // websocket headers at all) and `InvalidConnection`/`InvalidUpgrade` 203 // (looks like a normal HTTP/1.1 request with `Connection: keep-alive` 204 // or some non-websocket Upgrade token). Both paths feed httpFallback 205 // and both need a complete body in the buffer. 206 if (deferred_fallback_err != null or required_headers != 15) { 207 if (headers.get("content-length")) |cl_str| { 208 const content_length = std.fmt.parseInt(usize, cl_str, 10) catch { 209 return deferred_fallback_err orelse error.MissingHeaders; 210 }; 211 const body_available = state.len - body_start; 212 if (body_available < content_length) { 213 // body not yet complete — ask caller for more data 214 return null; 215 } 216 } 217 return deferred_fallback_err orelse error.MissingHeaders; 218 } 219 220 // request line: METHOD <space> URL <space> HTTP/1.1 221 const sp = std.mem.indexOfScalar(u8, request_line, ' ') orelse return error.InvalidRequestLine; 222 const method = request_line[0..sp]; 223 // url is between method-end and " HTTP/1.1" suffix (9 trailing chars) 224 if (request_line.len < sp + 1 + 9) return error.InvalidRequestLine; 225 const url = std.mem.trim(u8, request_line[sp + 1 .. request_line.len - 9], &ascii.whitespace); 226 227 return .{ 228 .key = key, 229 .url = url, 230 .method = method, 231 .headers = headers, 232 .compression = compression, 233 .res_headers = &state.res_headers, 234 // raw_header preserves original byte-exact slice. matches the 235 // pre-rewrite contract: starts after the request line's CRLF 236 // and ends just past the last header's CRLF (i.e. excludes 237 // the blank line's CRLF). callers re-emit this verbatim. 238 .raw_header = state.buf[first_crlf + 2 .. body_start - 2], 239 }; 240 } 241 242 pub fn createReply(key: []const u8, headers_: ?*KeyValue, compression: bool, buf: []u8) ![]const u8 { 243 return createReplyWithCompression(key, headers_, if (compression) .{ 244 .server_no_context_takeover = true, 245 .client_no_context_takeover = true, 246 } else null, buf); 247 } 248 249 pub fn createReplyNegotiated(key: []const u8, headers_: ?*KeyValue, compression: ?Compression, buf: []u8) ![]const u8 { 250 return createReplyWithCompression(key, headers_, compression, buf); 251 } 252 253 fn createReplyWithCompression(key: []const u8, headers_: ?*KeyValue, compression: ?Compression, buf: []u8) ![]const u8 { 254 const HEADER = 255 "HTTP/1.1 101 Switching Protocols\r\n" ++ 256 "Upgrade: websocket\r\n" ++ 257 "Connection: upgrade\r\n" ++ 258 "Sec-Websocket-Accept: "; 259 260 @memcpy(buf[0..HEADER.len], HEADER); 261 var pos = HEADER.len; 262 263 { 264 var h: [20]u8 = undefined; 265 var hasher = std.crypto.hash.Sha1.init(.{}); 266 hasher.update(key); 267 // websocket spec always used this value 268 hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 269 hasher.final(&h); 270 271 const end = pos + 28; 272 _ = std.base64.standard.Encoder.encode(buf[pos..end], h[0..]); 273 pos = end; 274 } 275 276 if (compression) |negotiated| { 277 const extension = "\r\nSec-WebSocket-Extensions: permessage-deflate"; 278 @memcpy(buf[pos..][0..extension.len], extension); 279 pos += extension.len; 280 if (negotiated.server_no_context_takeover) { 281 const param = "; server_no_context_takeover"; 282 @memcpy(buf[pos..][0..param.len], param); 283 pos += param.len; 284 } 285 if (negotiated.client_no_context_takeover) { 286 const param = "; client_no_context_takeover"; 287 @memcpy(buf[pos..][0..param.len], param); 288 pos += param.len; 289 } 290 } 291 292 if (headers_) |headers| { 293 for (headers.keys[0..headers.len], headers.values[0..headers.len]) |k, v| { 294 pos += (try std.fmt.bufPrint(buf[pos..], "\r\n{s}: {s}", .{ k, v })).len; 295 } 296 } 297 298 const end = pos + 4; 299 @memcpy(buf[pos..end], "\r\n\r\n"); 300 return buf[0..end]; 301 } 302 303 pub fn parseExtension(value: []const u8) !?Handshake.Compression { 304 var offers = std.mem.splitScalar(u8, value, ','); 305 while (offers.next()) |offer| { 306 var params = std.mem.splitScalar(u8, offer, ';'); 307 if (!std.mem.eql(u8, std.mem.trim(u8, params.first(), &ascii.whitespace), "permessage-deflate")) 308 continue; 309 310 var client_no_context_takeover = false; 311 var server_no_context_takeover = false; 312 var valid = true; 313 while (params.next()) |param_| { 314 const param = std.mem.trim(u8, param_, &ascii.whitespace); 315 if (std.mem.eql(u8, param, "client_no_context_takeover")) { 316 client_no_context_takeover = true; 317 } else if (std.mem.eql(u8, param, "server_no_context_takeover")) { 318 server_no_context_takeover = true; 319 } else if (std.mem.eql(u8, param, "client_max_window_bits") or 320 std.mem.startsWith(u8, param, "client_max_window_bits=")) 321 { 322 // A larger decoder window can read a stream produced with 323 // any smaller client window. 324 } else if (!std.mem.eql(u8, param, "server_max_window_bits=15")) { 325 valid = false; 326 break; 327 } 328 } 329 if (!valid) continue; 330 return .{ 331 .client_no_context_takeover = client_no_context_takeover, 332 .server_no_context_takeover = server_no_context_takeover, 333 }; 334 } 335 return null; 336 } 337 338 // This is what we're pooling 339 pub const State = struct { 340 // length of data we have in buf 341 len: usize = 0, 342 343 // a buffer to read data into 344 buf: []u8, 345 346 // streaming end-of-headers detector. advances across multiple 347 // parse() calls as new bytes arrive. survives splits inside CRLF 348 // sequences (the recurring class of TCP-fragmentation bugs). 349 head_parser: std.http.HeadParser = .{}, 350 351 // monotonically tracks how many bytes have been fed into 352 // head_parser. on each parse() call we feed only the new bytes 353 // [head_fed..len] so the SIMD scan is amortized. 354 head_fed: usize = 0, 355 356 // Headers from the request 357 req_headers: KeyValue, 358 359 // Headers that we want to send in the response 360 res_headers: KeyValue, 361 362 pool: *M.Pool, 363 364 fn init(pool: *M.Pool) !State { 365 const allocator = pool.allocator; 366 const buf = try allocator.alloc(u8, pool.buffer_size); 367 errdefer allocator.free(buf); 368 369 const req_headers = try Handshake.KeyValue.init(allocator, pool.max_req_headers); 370 errdefer req_headers.deinit(allocator); 371 372 const res_headers = try Handshake.KeyValue.init(allocator, pool.max_res_headers); 373 errdefer res_headers.deinit(allocator); 374 375 return .{ 376 .buf = buf, 377 .pool = pool, 378 .req_headers = req_headers, 379 .res_headers = res_headers, 380 }; 381 } 382 383 fn deinit(self: *State) void { 384 const allocator = self.pool.allocator; 385 allocator.free(self.buf); 386 self.req_headers.deinit(allocator); 387 self.res_headers.deinit(allocator); 388 } 389 390 pub fn release(self: *State) void { 391 self.len = 0; 392 self.head_parser = .{}; 393 self.head_fed = 0; 394 self.req_headers.len = 0; 395 self.res_headers.len = 0; 396 self.pool.release(self); 397 } 398 }; 399 400 pub const KeyValue = struct { 401 len: usize, 402 keys: [][]const u8, 403 values: [][]const u8, 404 405 fn init(allocator: Allocator, max: usize) !KeyValue { 406 const keys = try allocator.alloc([]const u8, max); 407 errdefer allocator.free(keys); 408 409 const values = try allocator.alloc([]const u8, max); 410 errdefer allocator.free(values); 411 412 return .{ 413 .len = 0, 414 .keys = keys, 415 .values = values, 416 }; 417 } 418 419 fn deinit(self: *const KeyValue, allocator: Allocator) void { 420 allocator.free(self.keys); 421 allocator.free(self.values); 422 } 423 424 pub fn add(self: *KeyValue, key: []const u8, value: []const u8) void { 425 const len = self.len; 426 var keys = self.keys; 427 if (len == keys.len) { 428 return; 429 } 430 431 keys[len] = key; 432 self.values[len] = value; 433 self.len = len + 1; 434 } 435 436 pub fn get(self: *const KeyValue, needle: []const u8) ?[]const u8 { 437 const keys = self.keys[0..self.len]; 438 loop: for (keys, 0..) |key, i| { 439 // This is largely a reminder to myself that std.mem.eql isn't 440 // particularly fast. Here we at least avoid the 1 extra ptr 441 // equality check that std.mem.eql does, but we could do better 442 // TODO: monitor https://github.com/ziglang/zig/issues/8689 443 if (needle.len != key.len) { 444 continue; 445 } 446 for (needle, key) |n, k| { 447 if (n != k) { 448 continue :loop; 449 } 450 } 451 return self.values[i]; 452 } 453 454 return null; 455 } 456 457 pub fn iterator(self: *const KeyValue) Iterator { 458 const len = self.len; 459 return .{ 460 .pos = 0, 461 .keys = self.keys[0..len], 462 .values = self.values[0..len], 463 }; 464 } 465 466 pub const Iterator = struct { 467 pos: usize, 468 keys: [][]const u8, 469 values: [][]const u8, 470 471 const KV = struct { 472 key: []const u8, 473 value: []const u8, 474 }; 475 476 pub fn next(self: *Iterator) ?KV { 477 const pos = self.pos; 478 if (pos == self.keys.len) { 479 return null; 480 } 481 482 self.pos = pos + 1; 483 return .{ 484 .key = self.keys[pos], 485 .value = self.values[pos], 486 }; 487 } 488 }; 489 }; 490}; 491 492pub const Pool = struct { 493 mutex: std.Io.Mutex, 494 io: std.Io, 495 available: usize, 496 allocator: Allocator, 497 buffer_size: usize, 498 max_req_headers: usize, 499 max_res_headers: usize, 500 states: []*Handshake.State, 501 502 pub fn init(allocator: Allocator, count: usize, buffer_size: usize, max_req_headers: usize, max_res_headers: usize) !*Pool { 503 const states = try allocator.alloc(*Handshake.State, count); 504 errdefer allocator.free(states); 505 506 const pool = try allocator.create(Pool); 507 errdefer allocator.destroy(pool); 508 509 pool.* = .{ 510 .mutex = .init, 511 .io = std.Options.debug_io, 512 .states = states, 513 .allocator = allocator, 514 .available = count, 515 .buffer_size = buffer_size, 516 .max_req_headers = max_req_headers, 517 .max_res_headers = max_res_headers, 518 }; 519 520 for (0..count) |i| { 521 const state = try allocator.create(Handshake.State); 522 errdefer allocator.destroy(state); 523 524 state.* = try Handshake.State.init(pool); 525 states[i] = state; 526 } 527 528 return pool; 529 } 530 531 pub fn deinit(self: *Pool) void { 532 const allocator = self.allocator; 533 for (self.states) |s| { 534 s.deinit(); 535 allocator.destroy(s); 536 } 537 allocator.free(self.states); 538 allocator.destroy(self); 539 } 540 541 pub fn acquire(self: *Pool) !*Handshake.State { 542 const states = self.states; 543 const io = self.io; 544 545 self.mutex.lockUncancelable(io); 546 const available = self.available; 547 if (available == 0) { 548 // dont hold the lock over factory 549 self.mutex.unlock(io); 550 551 const allocator = self.allocator; 552 const state = try allocator.create(Handshake.State); 553 errdefer allocator.destroy(state); 554 state.* = try Handshake.State.init(self); 555 return state; 556 } 557 const index = available - 1; 558 const state = states[index]; 559 self.available = index; 560 self.mutex.unlock(io); 561 return state; 562 } 563 564 fn release(self: *Pool, state: *Handshake.State) void { 565 var states = self.states; 566 const io = self.io; 567 568 self.mutex.lockUncancelable(io); 569 const available = self.available; 570 if (available == states.len) { 571 self.mutex.unlock(io); 572 state.deinit(); 573 self.allocator.destroy(state); 574 return; 575 } 576 states[available] = state; 577 self.available = available + 1; 578 self.mutex.unlock(io); 579 } 580}; 581 582fn toLower(str: []u8) []u8 { 583 for (str, 0..) |c, i| { 584 str[i] = ascii.toLower(c); 585 } 586 return str; 587} 588 589const t = @import("../t.zig"); 590test "handshake: parse" { 591 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 592 defer pool.deinit(); 593 594 { 595 var state = try pool.acquire(); 596 defer state.release(); 597 598 try t.expectEqual(null, try testHandshake("", state)); 599 try t.expectEqual(null, try testHandshake("GET", state)); 600 try t.expectEqual(null, try testHandshake("GET 1 HTTP/1.0\r", state)); 601 try t.expectEqual(null, try testHandshake("GET 1 HTTP/1.0\r\n", state)); 602 603 try t.expectError(error.InvalidProtocol, testHandshake("GET / HTTP/1.0\r\n\r\n", state)); 604 try t.expectError(error.MissingHeaders, testHandshake("GET / HTTP/1.1\r\n\r\n", state)); 605 try t.expectError(error.MissingHeaders, testHandshake("GET / HTTP/1.1\r\nConnection: upgrade\r\n\r\n", state)); 606 try t.expectError(error.MissingHeaders, testHandshake("GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\n\r\n", state)); 607 try t.expectError(error.MissingHeaders, testHandshake("GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\n\r\n", state)); 608 } 609 610 { 611 var state = try pool.acquire(); 612 defer state.release(); 613 614 const h = (try testHandshake("GET /test?a=1 HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\nsec-websocket-key: 9000!\r\nCustom: Header-Value\r\n\r\n", state)).?; 615 try t.expectString("9000!", h.key); 616 try t.expectString("GET", h.method); 617 try t.expectString("/test?a=1", h.url); 618 try t.expectString("connection: upgrade\r\nupgrade: websocket\r\nsec-websocket-version:13\r\nsec-websocket-key: 9000!\r\ncustom: Header-Value\r\n", h.raw_header); 619 try t.expectString("Header-Value", h.headers.get("custom").?); 620 621 var it = h.headers.iterator(); 622 { 623 const kv = it.next().?; 624 try t.expectString("connection", kv.key); 625 try t.expectString("upgrade", kv.value); 626 } 627 628 { 629 const kv = it.next().?; 630 try t.expectString("upgrade", kv.key); 631 try t.expectString("websocket", kv.value); 632 } 633 634 { 635 const kv = it.next().?; 636 try t.expectString("sec-websocket-version", kv.key); 637 try t.expectString("13", kv.value); 638 } 639 640 { 641 const kv = it.next().?; 642 try t.expectString("sec-websocket-key", kv.key); 643 try t.expectString("9000!", kv.value); 644 } 645 646 { 647 const kv = it.next().?; 648 try t.expectString("custom", kv.key); 649 try t.expectString("Header-Value", kv.value); 650 } 651 652 try t.expectEqual(null, it.next()); 653 } 654} 655 656test "handshake: parse — every byte-split of a valid upgrade is equivalent" { 657 // Regression net for the recurring class of TCP-fragmentation bugs. 658 // For a known-good request, feeding it 1-byte-at-a-time, 2-bytes-at-a-time, 659 // etc. must produce the same final outcome as feeding it whole. 660 // This catches any future parser change that loses idempotence under 661 // partial reads — including splits inside CRLF or CRLFCRLF sequences. 662 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 663 defer pool.deinit(); 664 665 const req = "GET /chat HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\nsec-websocket-key: abc\r\n\r\n"; 666 667 var chunk: usize = 1; 668 while (chunk <= req.len) : (chunk += 1) { 669 var state = try pool.acquire(); 670 defer state.release(); 671 672 const h = (try testHandshakeIncremental(req, chunk, state)) orelse { 673 std.debug.print("chunk={d}: parse returned null instead of Handshake\n", .{chunk}); 674 return error.UnexpectedNull; 675 }; 676 try t.expectString("abc", h.key); 677 try t.expectString("GET", h.method); 678 try t.expectString("/chat", h.url); 679 } 680} 681 682test "handshake: parse — split inside CRLF and CRLFCRLF sequences" { 683 // Pin specific torture-cases that have caused production outages: 684 // - split between '\r' and '\n' of the request-line CRLF 685 // - split between request-line's CRLF and the next header's first byte 686 // - split inside the final CRLFCRLF (each of the 4 internal positions) 687 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 688 defer pool.deinit(); 689 690 const req = "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\nsec-websocket-key: k\r\n\r\n"; 691 692 // Try every two-chunk split. Each split must produce the same handshake. 693 var split: usize = 1; 694 while (split < req.len) : (split += 1) { 695 var state = try pool.acquire(); 696 defer state.release(); 697 698 state.head_parser = .{}; 699 state.head_fed = 0; 700 state.req_headers.len = 0; 701 state.len = 0; 702 703 // first chunk 704 @memcpy(state.buf[0..split], req[0..split]); 705 state.len = split; 706 const r1 = try Handshake.parse(state); 707 try t.expectEqual(null, r1); 708 709 // second chunk 710 @memcpy(state.buf[split..req.len], req[split..]); 711 state.len = req.len; 712 const h = (try Handshake.parse(state)).?; 713 try t.expectString("k", h.key); 714 } 715} 716 717test "handshake: parse — malformed inputs return errors, never panic" { 718 // Adversarial / malformed inputs should map to named errors. There 719 // must be no `unreachable` reachable from any input; every parse 720 // exits via either Some(handshake), null, or a named error. 721 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 722 defer pool.deinit(); 723 724 const Case = struct { input: []const u8, expected: anyerror }; 725 const cases = [_]Case{ 726 // request line missing space → InvalidRequestLine after protocol check 727 .{ .input = "GETONLY\r\n\r\n", .expected = error.InvalidProtocol }, 728 .{ .input = "GETONLY HTTP/1.1\r\n\r\n", .expected = error.MissingHeaders }, 729 // request line ends mid-protocol 730 .{ .input = "GET / HTTP/1\r\n\r\n", .expected = error.InvalidProtocol }, 731 // header line with no colon 732 .{ .input = "GET / HTTP/1.1\r\nNoColonHere\r\n\r\n", .expected = error.InvalidHeader }, 733 // header with empty name 734 .{ .input = "GET / HTTP/1.1\r\n: value\r\n\r\n", .expected = error.InvalidHeader }, 735 // upgrade present but not "websocket" 736 .{ .input = "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: nope\r\n\r\n", .expected = error.InvalidUpgrade }, 737 // version not 13 738 .{ .input = "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version: 12\r\n\r\n", .expected = error.InvalidVersion }, 739 // connection without "upgrade" token 740 .{ .input = "GET / HTTP/1.1\r\nConnection: keep-alive\r\nUpgrade: websocket\r\n\r\n", .expected = error.InvalidConnection }, 741 }; 742 743 for (cases) |c| { 744 var state = try pool.acquire(); 745 defer state.release(); 746 try t.expectError(c.expected, testHandshake(c.input, state)); 747 } 748} 749 750test "handshake: parse — POST body byte-split equivalence" { 751 // The headline bug from b45400c: POST with body split across reads 752 // must hold null until the full body has arrived, then return 753 // MissingHeaders so the worker dispatches httpFallback with the 754 // complete body. Testing every possible split position guards 755 // against any future regression in the body-completeness path. 756 var pool = try Pool.init(t.allocator, 1, 1024, 10, 1); 757 defer pool.deinit(); 758 759 const req = "POST /xrpc/com.atproto.sync.requestCrawl HTTP/1.1\r\nHost: relay\r\nContent-Type: application/json\r\nContent-Length: 27\r\n\r\n{\"hostname\":\"pds.test.com\"}"; 760 761 var split: usize = 1; 762 while (split < req.len) : (split += 1) { 763 var state = try pool.acquire(); 764 defer state.release(); 765 766 state.head_parser = .{}; 767 state.head_fed = 0; 768 state.req_headers.len = 0; 769 state.len = 0; 770 771 @memcpy(state.buf[0..split], req[0..split]); 772 state.len = split; 773 // first call: must NOT commit to MissingHeaders before body arrives 774 const r1 = Handshake.parse(state) catch |e| blk: { 775 // legitimate early errors (bad request line) are fine 776 if (e != error.InvalidProtocol and e != error.InvalidRequestLine) return e; 777 break :blk @as(?Handshake, null); 778 }; 779 try t.expectEqual(null, r1); 780 781 @memcpy(state.buf[split..req.len], req[split..]); 782 state.len = req.len; 783 try t.expectError(error.MissingHeaders, Handshake.parse(state)); 784 } 785} 786 787test "handshake: parse — RFC 7230 §3.2 rejects whitespace before colon" { 788 // "No whitespace is allowed between the header field-name and colon. 789 // Servers must reject ... with a 400." This is a request-smuggling 790 // defense, not stylistic. 791 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 792 defer pool.deinit(); 793 794 const cases = [_][]const u8{ 795 // single space before colon 796 "GET / HTTP/1.1\r\nHost : x\r\n\r\n", 797 // tab before colon 798 "GET / HTTP/1.1\r\nHost\t: x\r\n\r\n", 799 // also catches it on a non-required header 800 "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version: 13\r\nsec-websocket-key: x\r\nFoo : bar\r\n\r\n", 801 }; 802 803 for (cases) |c| { 804 var state = try pool.acquire(); 805 defer state.release(); 806 try t.expectError(error.WhitespaceBeforeColon, testHandshake(c, state)); 807 } 808} 809 810test "handshake: parse — RFC 7230 §3.3 rejects Transfer-Encoding + Content-Length" { 811 // Combination is a request-smuggling vector. RFC says either reject or 812 // remove Content-Length before forwarding; we choose explicit rejection 813 // because we can't safely forward what we don't trust. 814 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 815 defer pool.deinit(); 816 817 { 818 var state = try pool.acquire(); 819 defer state.release(); 820 try t.expectError( 821 error.AmbiguousBodyLength, 822 testHandshake( 823 "POST /x HTTP/1.1\r\nHost: r\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nhello", 824 state, 825 ), 826 ); 827 } 828 829 { 830 // also rejects when both appear on a (would-be) WS upgrade 831 var state = try pool.acquire(); 832 defer state.release(); 833 try t.expectError( 834 error.AmbiguousBodyLength, 835 testHandshake( 836 "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version: 13\r\nsec-websocket-key: x\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n", 837 state, 838 ), 839 ); 840 } 841} 842 843test "handshake: parse — rejects obsolete line folding (RFC 7230 §3.2.4)" { 844 // "A sender MUST NOT generate a message that includes line folding." 845 // We surface a folded continuation as InvalidHeader rather than try 846 // to reconstruct the obs-fold semantics. 847 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 848 defer pool.deinit(); 849 850 var state = try pool.acquire(); 851 defer state.release(); 852 // continuation line starts with whitespace — would be folded value of "Host" 853 try t.expectError( 854 error.InvalidHeader, 855 testHandshake("GET / HTTP/1.1\r\nHost: r\r\n continued\r\n\r\n", state), 856 ); 857} 858 859test "handshake: parse — idempotent under repeated calls without new bytes" { 860 // Calling parse twice in a row with the same state.len must produce 861 // the same answer. Otherwise the worker can race with itself when 862 // it polls before another byte arrives. 863 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 864 defer pool.deinit(); 865 866 var state = try pool.acquire(); 867 defer state.release(); 868 869 const req = "GET /a HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\nsec-websocket-key: x\r\n\r\n"; 870 @memcpy(state.buf[0..req.len], req); 871 state.len = req.len; 872 873 const h1 = (try Handshake.parse(state)).?; 874 const h2 = (try Handshake.parse(state)).?; 875 try t.expectString(h1.key, h2.key); 876 try t.expectString(h1.method, h2.method); 877 try t.expectString(h1.url, h2.url); 878} 879 880test "handshake: parse POST with body signals MissingHeaders (for httpFallback)" { 881 var pool = try Pool.init(t.allocator, 1, 512, 10, 1); 882 defer pool.deinit(); 883 884 { 885 // POST whose body has arrived in the same read as the headers. 886 // Must surface MissingHeaders (so the worker dispatches to httpFallback) 887 // — NOT return null (which would loop forever waiting for more data). 888 var state = try pool.acquire(); 889 defer state.release(); 890 try t.expectError( 891 error.MissingHeaders, 892 testHandshake( 893 "POST /xrpc/com.atproto.sync.requestCrawl HTTP/1.1\r\nHost: relay\r\nContent-Type: application/json\r\nContent-Length: 27\r\n\r\n{\"hostname\":\"pds.test.com\"}", 894 state, 895 ), 896 ); 897 } 898 899 { 900 // POST whose body has NOT fully arrived yet — parse must return null 901 // so the worker reads more data before dispatching the fallback. 902 var state = try pool.acquire(); 903 defer state.release(); 904 try t.expectEqual( 905 null, 906 try testHandshake( 907 "POST /x HTTP/1.1\r\nHost: r\r\nContent-Length: 100\r\n\r\n{\"only\":\"part\"}", 908 state, 909 ), 910 ); 911 } 912 913 { 914 // POST with no Content-Length and headers terminated — surface 915 // MissingHeaders immediately, no body expected. 916 var state = try pool.acquire(); 917 defer state.release(); 918 try t.expectError( 919 error.MissingHeaders, 920 testHandshake("POST /x HTTP/1.1\r\nHost: r\r\n\r\n", state), 921 ); 922 } 923} 924 925test "handshake: Connection: keep-alive + partial body returns null (not InvalidConnection)" { 926 // Regression: an httpx-style POST sends `Connection: keep-alive` (no 927 // Upgrade token). Previously, parse returned `error.InvalidConnection` 928 // the moment it saw that header — even if the body hadn't fully arrived 929 // yet — and the worker dispatched httpFallback with a truncated body. 930 // Handlers observed body.len == 0 for a `Content-Length: 179` POST, 931 // returned 400, and the prefect client crashed the worker run. 932 // 933 // The fix defers `InvalidConnection`/`InvalidUpgrade` past the 934 // body-completeness check so we ask for more data first. 935 936 var pool = try Pool.init(t.allocator, 1, 1024, 10, 1); 937 defer pool.deinit(); 938 939 { 940 // Headers only (no body yet) — parse must return null so the worker 941 // reads more. It must NOT return error.InvalidConnection. 942 var state = try pool.acquire(); 943 defer state.release(); 944 try t.expectEqual( 945 null, 946 try testHandshake( 947 "POST /api/flow_runs/x/labels HTTP/1.1\r\n" ++ 948 "Host: localhost\r\n" ++ 949 "Connection: keep-alive\r\n" ++ 950 "Content-Type: application/json\r\n" ++ 951 "Content-Length: 50\r\n" ++ 952 "\r\n", 953 state, 954 ), 955 ); 956 } 957 958 { 959 // Same request with body now present — parse must surface 960 // InvalidConnection (so the worker dispatches httpFallback with the 961 // full body now in state.buf). 962 var state = try pool.acquire(); 963 defer state.release(); 964 try t.expectError( 965 error.InvalidConnection, 966 testHandshake( 967 "POST /api/flow_runs/x/labels HTTP/1.1\r\n" ++ 968 "Host: localhost\r\n" ++ 969 "Connection: keep-alive\r\n" ++ 970 "Content-Type: application/json\r\n" ++ 971 "Content-Length: 50\r\n" ++ 972 "\r\n" ++ 973 "{\"prefect.flow-run.smoke\":\"\",\"prefect.worker\":\"w\"}", 974 state, 975 ), 976 ); 977 } 978 979 { 980 // Non-websocket Upgrade token (e.g., Upgrade: h2c) on a POST — same 981 // pattern, this time triggering the InvalidUpgrade defer path. 982 var state = try pool.acquire(); 983 defer state.release(); 984 try t.expectEqual( 985 null, 986 try testHandshake( 987 "POST /x HTTP/1.1\r\n" ++ 988 "Host: r\r\n" ++ 989 "Upgrade: h2c\r\n" ++ 990 "Connection: upgrade\r\n" ++ 991 "Content-Length: 20\r\n" ++ 992 "\r\n", 993 state, 994 ), 995 ); 996 } 997} 998 999test "handshake: reply" { 1000 var buf: [512]u8 = undefined; 1001 1002 { 1003 // no compression 1004 const expected = 1005 "HTTP/1.1 101 Switching Protocols\r\n" ++ 1006 "Upgrade: websocket\r\n" ++ 1007 "Connection: upgrade\r\n" ++ 1008 "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n"; 1009 try t.expectString(expected, try Handshake.createReply("this is my key", null, false, &buf)); 1010 } 1011 1012 // Compatibility for http.zig and other integrations which own the HTTP 1013 // upgrade and pass the historical boolean negotiation flag. 1014 { 1015 const expected = 1016 "HTTP/1.1 101 Switching Protocols\r\n" ++ 1017 "Upgrade: websocket\r\n" ++ 1018 "Connection: upgrade\r\n" ++ 1019 "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n" ++ 1020 "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n\r\n"; 1021 try t.expectString(expected, try Handshake.createReply("this is my key", null, true, &buf)); 1022 } 1023 1024 { 1025 // compression 1026 const expected = 1027 "HTTP/1.1 101 Switching Protocols\r\n" ++ 1028 "Upgrade: websocket\r\n" ++ 1029 "Connection: upgrade\r\n" ++ 1030 "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n" ++ 1031 "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n\r\n"; 1032 try t.expectString(expected, try Handshake.createReplyNegotiated("this is my key", null, .{ 1033 .server_no_context_takeover = true, 1034 .client_no_context_takeover = true, 1035 }, &buf)); 1036 } 1037 1038 // With custom headers 1039 var res_headers = try Handshake.KeyValue.init(t.allocator, 2); 1040 defer res_headers.deinit(t.allocator); 1041 res_headers.add("Set-Cookie", "Yummy!"); 1042 1043 { 1044 // no compression 1045 const expected = 1046 "HTTP/1.1 101 Switching Protocols\r\n" ++ 1047 "Upgrade: websocket\r\n" ++ 1048 "Connection: upgrade\r\n" ++ 1049 "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n" ++ 1050 "Set-Cookie: Yummy!\r\n\r\n"; 1051 try t.expectString(expected, try Handshake.createReply("this is my key", &res_headers, false, &buf)); 1052 } 1053 1054 { 1055 // compression 1056 const expected = 1057 "HTTP/1.1 101 Switching Protocols\r\n" ++ 1058 "Upgrade: websocket\r\n" ++ 1059 "Connection: upgrade\r\n" ++ 1060 "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n" ++ 1061 "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n" ++ 1062 "Set-Cookie: Yummy!\r\n\r\n"; 1063 try t.expectString(expected, try Handshake.createReplyNegotiated("this is my key", &res_headers, .{ 1064 .server_no_context_takeover = true, 1065 .client_no_context_takeover = true, 1066 }, &buf)); 1067 } 1068} 1069 1070test "handshake: permessage-deflate offer selection" { 1071 const context = (try Handshake.parseExtension( 1072 "permessage-deflate; client_max_window_bits", 1073 )).?; 1074 try t.expectEqual(false, context.client_no_context_takeover); 1075 try t.expectEqual(false, context.server_no_context_takeover); 1076 1077 const reset = (try Handshake.parseExtension( 1078 "permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=15", 1079 )).?; 1080 try t.expectEqual(true, reset.client_no_context_takeover); 1081 try t.expectEqual(true, reset.server_no_context_takeover); 1082 1083 try t.expectEqual(null, try Handshake.parseExtension( 1084 "permessage-deflate; server_max_window_bits=12", 1085 )); 1086 try t.expectEqual(null, try Handshake.parseExtension( 1087 "permessage-deflate; made_up_parameter=yes", 1088 )); 1089 try std.testing.expect((try Handshake.parseExtension( 1090 "x-webkit-deflate-frame, permessage-deflate; client_max_window_bits=12", 1091 )) != null); 1092} 1093 1094test "KeyValue: get" { 1095 const allocator = t.allocator; 1096 var kv = try Handshake.KeyValue.init(allocator, 2); 1097 defer kv.deinit(t.allocator); 1098 1099 var key = "content-type".*; 1100 kv.add(&key, "application/json"); 1101 1102 try t.expectString("application/json", kv.get("content-type").?); 1103 1104 kv.len = 0; 1105 try t.expectEqual(null, kv.get("content-type")); 1106 kv.add(&key, "application/json2"); 1107 try t.expectString("application/json2", kv.get("content-type").?); 1108} 1109 1110test "KeyValue: ignores beyond max" { 1111 var kv = try Handshake.KeyValue.init(t.allocator, 2); 1112 defer kv.deinit(t.allocator); 1113 1114 var n1 = "content-length".*; 1115 kv.add(&n1, "cl"); 1116 1117 var n2 = "host".*; 1118 kv.add(&n2, "www"); 1119 1120 var n3 = "authorization".*; 1121 kv.add(&n3, "hack"); 1122 1123 try t.expectString("cl", kv.get("content-length").?); 1124 try t.expectString("www", kv.get("host").?); 1125 try t.expectEqual(null, kv.get("authorization")); 1126} 1127 1128test "pool: acquire and release" { 1129 // not 100% sure this is testing exactly what I want, but it's ....something ? 1130 var p = try Pool.init(t.allocator, 2, 10, 3, 1); 1131 defer p.deinit(); 1132 1133 var hs1a = p.acquire() catch unreachable; 1134 var hs2a = p.acquire() catch unreachable; 1135 var hs3a = p.acquire() catch unreachable; // this should be dynamically generated 1136 1137 try t.expectEqual(false, &hs1a.buf[0] == &hs2a.buf[0]); 1138 try t.expectEqual(false, &hs2a.buf[0] == &hs3a.buf[0]); 1139 try t.expectEqual(10, hs1a.buf.len); 1140 try t.expectEqual(10, hs2a.buf.len); 1141 try t.expectEqual(10, hs3a.buf.len); 1142 try t.expectEqual(0, hs1a.req_headers.len); 1143 try t.expectEqual(0, hs2a.req_headers.len); 1144 try t.expectEqual(0, hs3a.req_headers.len); 1145 try t.expectEqual(3, hs1a.req_headers.keys.len); 1146 try t.expectEqual(3, hs2a.req_headers.keys.len); 1147 try t.expectEqual(3, hs3a.req_headers.keys.len); 1148 1149 p.release(hs1a); 1150 1151 var hs1b = p.acquire() catch unreachable; 1152 try t.expectEqual(true, &hs1a.buf[0] == &hs1b.buf[0]); 1153 1154 p.release(hs3a); 1155 p.release(hs2a); 1156 p.release(hs1b); 1157} 1158 1159test "Handshake.Pool: threadsafety" { 1160 var p = try Pool.init(t.allocator, 4, 10, 2, 2); 1161 defer p.deinit(); 1162 1163 for (p.states) |hs| { 1164 hs.buf[0] = 0; 1165 } 1166 1167 const t1 = try std.Thread.spawn(.{}, testPool, .{p}); 1168 const t2 = try std.Thread.spawn(.{}, testPool, .{p}); 1169 const t3 = try std.Thread.spawn(.{}, testPool, .{p}); 1170 const t4 = try std.Thread.spawn(.{}, testPool, .{p}); 1171 1172 t1.join(); 1173 t2.join(); 1174 t3.join(); 1175 t4.join(); 1176} 1177 1178fn testPool(p: *Pool) void { 1179 var r = t.getRandom(); 1180 const random = r.random(); 1181 1182 for (0..5000) |_| { 1183 var hs = p.acquire() catch unreachable; 1184 std.debug.assert(hs.buf[0] == 0); 1185 hs.buf[0] = 255; 1186 sleep(random.uintAtMost(u32, 100000)); 1187 hs.buf[0] = 0; 1188 p.release(hs); 1189 } 1190} 1191 1192fn testHandshake(request: []const u8, state: *Handshake.State) !?Handshake { 1193 // simulate a fresh socket: clear streaming parse state before pushing 1194 // a new request. existing tests reuse one State across many distinct 1195 // requests for convenience, which would otherwise wedge head_parser 1196 // in whatever state the previous request left it in. 1197 state.head_parser = .{}; 1198 state.head_fed = 0; 1199 state.req_headers.len = 0; 1200 @memcpy(state.buf[0..request.len], request); 1201 state.len = request.len; 1202 return Handshake.parse(state); 1203} 1204 1205/// Feed the request `chunk_size` bytes at a time, asserting that all 1206/// non-final calls return null. Returns the result of the final parse. 1207/// This is the core regression-test mechanism for TCP-split bugs: 1208/// any parser that's correct under all-at-once must also be correct 1209/// under every possible byte-split arrival pattern. 1210fn testHandshakeIncremental(request: []const u8, chunk_size: usize, state: *Handshake.State) !?Handshake { 1211 state.head_parser = .{}; 1212 state.head_fed = 0; 1213 state.req_headers.len = 0; 1214 state.len = 0; 1215 1216 var fed: usize = 0; 1217 while (fed < request.len) { 1218 const take = @min(chunk_size, request.len - fed); 1219 @memcpy(state.buf[state.len .. state.len + take], request[fed .. fed + take]); 1220 state.len += take; 1221 fed += take; 1222 if (fed < request.len) { 1223 // not the last chunk yet — parser must not commit either way 1224 const r = Handshake.parse(state) catch |e| { 1225 // an early error is allowed only if the request is 1226 // detectably malformed regardless of remaining bytes 1227 // (e.g. wrong HTTP version in the request line). 1228 return e; 1229 }; 1230 if (r != null) { 1231 // committing to success before the request is fully delivered 1232 // would be a bug — at minimum it would race with body bytes 1233 // for non-upgrade requests. 1234 return error.PrematureSuccess; 1235 } 1236 } 1237 } 1238 return Handshake.parse(state); 1239}