| author | |
| committer | |
| log | 08bdaf3bd650ec7682f1424c3644fc3b762ccf27 |
| tree | 733f2b571bfbe39c3c78d1548a27c5bc7104476c |
| parent | fde05b10b3c29b914e4d2ef034dcd8a78800ef6e |
| signature |
* extract http protocol into protocol.zig, as it is shared between client and server
* coalesce Request and Response back into Client.zig, they don't contain
any large chunks of code anymore
* http.Server is implemented as basic as possible, a simple example below:
```zig
fn handler(res: *Server.Response) !void {
while (true) {
defer res.reset();
try res.waitForCompleteHead();
res.headers.transfer_encoding = .{ .content_length = 14 };
res.headers.connection = res.request.headers.connection;
try res.sendResponseHead();
_ = try res.write("Hello, World!\n");
if (res.connection.closing) break;
}
}
pub fn main() !void {
var server = Server.init(std.heap.page_allocator, .{ .reuse_address = true });
defer server.deinit();
try server.listen(try net.Address.parseIp("127.0.0.1", 8080));
while (true) {
const res = try server.accept(.{ .dynamic = 8192 });
const thread = try std.Thread.spawn(.{}, handler, .{res});
thread.detach();
}
}
```6 files changed, 1626 insertions(+), 740 deletions(-)
lib/std/http.zig+2| ... | ... | @@ -1,4 +1,6 @@ |
| 1 | 1 | pub const Client = @import("http/Client.zig"); |
| 2 | pub const Server = @import("http/Server.zig"); | |
| 3 | pub const protocol = @import("http/protocol.zig"); | |
| 2 | 4 | |
| 3 | 5 | pub const Version = enum { |
| 4 | 6 | @"HTTP/1.0", |
lib/std/http/Client.zig+414-24| ... | ... | @@ -1,23 +1,19 @@ |
| 1 | //! TODO: send connection: keep-alive and LRU cache a configurable number of | |
| 2 | //! open connections to skip DNS and TLS handshake for subsequent requests. | |
| 3 | //! | |
| 4 | //! This API is *not* thread safe. | |
| 1 | //! Connecting and opening requests are threadsafe. Individual requests are not. | |
| 5 | 2 | |
| 6 | 3 | const std = @import("../std.zig"); |
| 7 | const mem = std.mem; | |
| 8 | const assert = std.debug.assert; | |
| 4 | const testing = std.testing; | |
| 9 | 5 | const http = std.http; |
| 6 | const mem = std.mem; | |
| 10 | 7 | const net = std.net; |
| 11 | const Client = @This(); | |
| 12 | 8 | const Uri = std.Uri; |
| 13 | const Allocator = std.mem.Allocator; | |
| 14 | const testing = std.testing; | |
| 9 | const Allocator = mem.Allocator; | |
| 10 | const assert = std.debug.assert; | |
| 15 | 11 | |
| 16 | pub const Request = @import("Client/Request.zig"); | |
| 17 | pub const Response = @import("Client/Response.zig"); | |
| 12 | const Client = @This(); | |
| 13 | const proto = @import("protocol.zig"); | |
| 18 | 14 | |
| 19 | 15 | pub const default_connection_pool_size = 32; |
| 20 | const connection_pool_size = std.options.http_connection_pool_size; | |
| 16 | pub const connection_pool_size = std.options.http_connection_pool_size; | |
| 21 | 17 | |
| 22 | 18 | /// Used for tcpConnectToHost and storing HTTP headers when an externally |
| 23 | 19 | /// managed buffer is not provided. |
| ... | ... | @@ -43,7 +39,7 @@ pub const ConnectionPool = struct { |
| 43 | 39 | used: Queue = .{}, |
| 44 | 40 | free: Queue = .{}, |
| 45 | 41 | free_len: usize = 0, |
| 46 | free_size: usize = default_connection_pool_size, | |
| 42 | free_size: usize = connection_pool_size, | |
| 47 | 43 | |
| 48 | 44 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. |
| 49 | 45 | /// If no connection is found, null is returned. |
| ... | ... | @@ -55,7 +51,7 @@ pub const ConnectionPool = struct { |
| 55 | 51 | while (next) |node| : (next = node.prev) { |
| 56 | 52 | if ((node.data.protocol == .tls) != criteria.is_tls) continue; |
| 57 | 53 | if (node.data.port != criteria.port) continue; |
| 58 | if (std.mem.eql(u8, node.data.host, criteria.host)) continue; | |
| 54 | if (mem.eql(u8, node.data.host, criteria.host)) continue; | |
| 59 | 55 | |
| 60 | 56 | pool.acquireUnsafe(node); |
| 61 | 57 | return node; |
| ... | ... | @@ -137,9 +133,9 @@ pub const ConnectionPool = struct { |
| 137 | 133 | } |
| 138 | 134 | }; |
| 139 | 135 | |
| 140 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw); | |
| 141 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw); | |
| 142 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.ReaderRaw, .{}); | |
| 136 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader); | |
| 137 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader); | |
| 138 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{}); | |
| 143 | 139 | |
| 144 | 140 | pub const Connection = struct { |
| 145 | 141 | stream: net.Stream, |
| ... | ... | @@ -220,6 +216,379 @@ pub const Connection = struct { |
| 220 | 216 | } |
| 221 | 217 | }; |
| 222 | 218 | |
| 219 | pub const RequestTransfer = union(enum) { | |
| 220 | content_length: u64, | |
| 221 | chunked: void, | |
| 222 | none: void, | |
| 223 | }; | |
| 224 | ||
| 225 | pub const Compression = union(enum) { | |
| 226 | deflate: DeflateDecompressor, | |
| 227 | gzip: GzipDecompressor, | |
| 228 | zstd: ZstdDecompressor, | |
| 229 | none: void, | |
| 230 | }; | |
| 231 | ||
| 232 | pub const Response = struct { | |
| 233 | pub const Headers = struct { | |
| 234 | status: http.Status, | |
| 235 | version: http.Version, | |
| 236 | location: ?[]const u8 = null, | |
| 237 | content_length: ?u64 = null, | |
| 238 | transfer_encoding: ?http.TransferEncoding = null, | |
| 239 | transfer_compression: ?http.ContentEncoding = null, | |
| 240 | connection: http.Connection = .close, | |
| 241 | upgrade: ?[]const u8 = null, | |
| 242 | ||
| 243 | pub const ParseError = error{ | |
| 244 | ShortHttpStatusLine, | |
| 245 | BadHttpVersion, | |
| 246 | HttpHeadersInvalid, | |
| 247 | HttpHeaderContinuationsUnsupported, | |
| 248 | HttpTransferEncodingUnsupported, | |
| 249 | HttpConnectionHeaderUnsupported, | |
| 250 | InvalidCharacter, | |
| 251 | }; | |
| 252 | ||
| 253 | pub fn parse(bytes: []const u8) !Headers { | |
| 254 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | |
| 255 | ||
| 256 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | |
| 257 | if (first_line.len < 12) | |
| 258 | return error.ShortHttpStatusLine; | |
| 259 | ||
| 260 | const version: http.Version = switch (int64(first_line[0..8])) { | |
| 261 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 262 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 263 | else => return error.BadHttpVersion, | |
| 264 | }; | |
| 265 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | |
| 266 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); | |
| 267 | ||
| 268 | var headers: Headers = .{ | |
| 269 | .version = version, | |
| 270 | .status = status, | |
| 271 | }; | |
| 272 | ||
| 273 | while (it.next()) |line| { | |
| 274 | if (line.len == 0) return error.HttpHeadersInvalid; | |
| 275 | switch (line[0]) { | |
| 276 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 277 | else => {}, | |
| 278 | } | |
| 279 | ||
| 280 | var line_it = mem.tokenize(u8, line, ": "); | |
| 281 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | |
| 282 | const header_value = line_it.rest(); | |
| 283 | if (std.ascii.eqlIgnoreCase(header_name, "location")) { | |
| 284 | if (headers.location != null) return error.HttpHeadersInvalid; | |
| 285 | headers.location = header_value; | |
| 286 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 287 | if (headers.content_length != null) return error.HttpHeadersInvalid; | |
| 288 | headers.content_length = try std.fmt.parseInt(u64, header_value, 10); | |
| 289 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 290 | // Transfer-Encoding: second, first | |
| 291 | // Transfer-Encoding: deflate, chunked | |
| 292 | var iter = mem.splitBackwards(u8, header_value, ","); | |
| 293 | ||
| 294 | if (iter.next()) |first| { | |
| 295 | const trimmed = mem.trim(u8, first, " "); | |
| 296 | ||
| 297 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | |
| 298 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | |
| 299 | headers.transfer_encoding = te; | |
| 300 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 301 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 302 | headers.transfer_compression = ce; | |
| 303 | } else { | |
| 304 | return error.HttpTransferEncodingUnsupported; | |
| 305 | } | |
| 306 | } | |
| 307 | ||
| 308 | if (iter.next()) |second| { | |
| 309 | if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | |
| 310 | ||
| 311 | const trimmed = mem.trim(u8, second, " "); | |
| 312 | ||
| 313 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 314 | headers.transfer_compression = ce; | |
| 315 | } else { | |
| 316 | return error.HttpTransferEncodingUnsupported; | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 321 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 322 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 323 | ||
| 324 | const trimmed = mem.trim(u8, header_value, " "); | |
| 325 | ||
| 326 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 327 | headers.transfer_compression = ce; | |
| 328 | } else { | |
| 329 | return error.HttpTransferEncodingUnsupported; | |
| 330 | } | |
| 331 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 332 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | |
| 333 | headers.connection = .keep_alive; | |
| 334 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | |
| 335 | headers.connection = .close; | |
| 336 | } else { | |
| 337 | return error.HttpConnectionHeaderUnsupported; | |
| 338 | } | |
| 339 | } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) { | |
| 340 | headers.upgrade = header_value; | |
| 341 | } | |
| 342 | } | |
| 343 | ||
| 344 | return headers; | |
| 345 | } | |
| 346 | ||
| 347 | inline fn int64(array: *const [8]u8) u64 { | |
| 348 | return @bitCast(u64, array.*); | |
| 349 | } | |
| 350 | ||
| 351 | fn parseInt3(nnn: @Vector(3, u8)) u10 { | |
| 352 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 353 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 354 | return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); | |
| 355 | } | |
| 356 | ||
| 357 | test parseInt3 { | |
| 358 | const expectEqual = testing.expectEqual; | |
| 359 | try expectEqual(@as(u10, 0), parseInt3("000".*)); | |
| 360 | try expectEqual(@as(u10, 418), parseInt3("418".*)); | |
| 361 | try expectEqual(@as(u10, 999), parseInt3("999".*)); | |
| 362 | } | |
| 363 | }; | |
| 364 | ||
| 365 | headers: Headers = undefined, | |
| 366 | parser: proto.HeadersParser, | |
| 367 | compression: Compression = .none, | |
| 368 | skip: bool = false, | |
| 369 | }; | |
| 370 | ||
| 371 | pub const Request = struct { | |
| 372 | pub const Headers = struct { | |
| 373 | version: http.Version = .@"HTTP/1.1", | |
| 374 | method: http.Method = .GET, | |
| 375 | user_agent: []const u8 = "zig (std.http)", | |
| 376 | connection: http.Connection = .keep_alive, | |
| 377 | transfer_encoding: RequestTransfer = .none, | |
| 378 | ||
| 379 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | |
| 380 | }; | |
| 381 | ||
| 382 | uri: Uri, | |
| 383 | client: *Client, | |
| 384 | connection: *ConnectionPool.Node, | |
| 385 | /// These are stored in Request so that they are available when following | |
| 386 | /// redirects. | |
| 387 | headers: Headers, | |
| 388 | ||
| 389 | redirects_left: u32, | |
| 390 | handle_redirects: bool, | |
| 391 | ||
| 392 | response: Response, | |
| 393 | ||
| 394 | /// Used as a allocator for resolving redirects locations. | |
| 395 | arena: std.heap.ArenaAllocator, | |
| 396 | ||
| 397 | /// Frees all resources associated with the request. | |
| 398 | pub fn deinit(req: *Request) void { | |
| 399 | switch (req.response.compression) { | |
| 400 | .none => {}, | |
| 401 | .deflate => |*deflate| deflate.deinit(), | |
| 402 | .gzip => |*gzip| gzip.deinit(), | |
| 403 | .zstd => |*zstd| zstd.deinit(), | |
| 404 | } | |
| 405 | ||
| 406 | if (req.response.parser.header_bytes_owned) { | |
| 407 | req.response.parser.header_bytes.deinit(req.client.allocator); | |
| 408 | } | |
| 409 | ||
| 410 | if (!req.response.parser.done) { | |
| 411 | // If the response wasn't fully read, then we need to close the connection. | |
| 412 | req.connection.data.closing = true; | |
| 413 | req.client.connection_pool.release(req.client, req.connection); | |
| 414 | } | |
| 415 | ||
| 416 | req.arena.deinit(); | |
| 417 | req.* = undefined; | |
| 418 | } | |
| 419 | ||
| 420 | pub const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; | |
| 421 | ||
| 422 | pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); | |
| 423 | ||
| 424 | pub fn transferReader(req: *Request) TransferReader { | |
| 425 | return .{ .context = req }; | |
| 426 | } | |
| 427 | ||
| 428 | pub fn transferRead(req: *Request, buf: []u8) TransferReadError!usize { | |
| 429 | if (req.response.parser.isComplete()) return 0; | |
| 430 | ||
| 431 | var index: usize = 0; | |
| 432 | while (index == 0) { | |
| 433 | const amt = try req.response.parser.read(req.connection.data.reader(), buf[index..], req.response.skip); | |
| 434 | if (amt == 0 and req.response.parser.isComplete()) break; | |
| 435 | index += amt; | |
| 436 | } | |
| 437 | ||
| 438 | return index; | |
| 439 | } | |
| 440 | ||
| 441 | pub const WaitForCompleteHeadError = Connection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Response.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported}; | |
| 442 | ||
| 443 | pub fn waitForCompleteHead(req: *Request) !void { | |
| 444 | try req.response.parser.waitForCompleteHead(req.connection.data.reader(), req.client.allocator); | |
| 445 | ||
| 446 | req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); | |
| 447 | ||
| 448 | if (req.response.headers.status == .switching_protocols) { | |
| 449 | req.connection.data.closing = false; | |
| 450 | req.response.parser.done = true; | |
| 451 | } | |
| 452 | ||
| 453 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { | |
| 454 | req.connection.data.closing = false; | |
| 455 | } else { | |
| 456 | req.connection.data.closing = true; | |
| 457 | } | |
| 458 | ||
| 459 | if (req.response.headers.transfer_encoding) |te| { | |
| 460 | switch (te) { | |
| 461 | .chunked => { | |
| 462 | req.response.parser.next_chunk_length = 0; | |
| 463 | req.response.parser.state = .chunk_head_size; | |
| 464 | }, | |
| 465 | } | |
| 466 | } else if (req.response.headers.content_length) |cl| { | |
| 467 | req.response.parser.next_chunk_length = cl; | |
| 468 | ||
| 469 | if (cl == 0) req.response.parser.done = true; | |
| 470 | } else { | |
| 471 | req.response.parser.done = true; | |
| 472 | } | |
| 473 | ||
| 474 | if (!req.response.parser.done) { | |
| 475 | if (req.response.headers.transfer_compression) |tc| switch (tc) { | |
| 476 | .compress => return error.CompressionNotSupported, | |
| 477 | .deflate => req.response.compression = .{ | |
| 478 | .deflate = try std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()), | |
| 479 | }, | |
| 480 | .gzip => req.response.compression = .{ | |
| 481 | .gzip = try std.compress.gzip.decompress(req.client.allocator, req.transferReader()), | |
| 482 | }, | |
| 483 | .zstd => req.response.compression = .{ | |
| 484 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), | |
| 485 | }, | |
| 486 | }; | |
| 487 | } | |
| 488 | ||
| 489 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) req.response.skip = true; | |
| 490 | } | |
| 491 | ||
| 492 | pub const ReadError = RequestError || Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, InvalidFormat, InvalidPort, UnexpectedCharacter }; | |
| 493 | ||
| 494 | pub const Reader = std.io.Reader(*Request, ReadError, read); | |
| 495 | ||
| 496 | pub fn reader(req: *Request) Reader { | |
| 497 | return .{ .context = req }; | |
| 498 | } | |
| 499 | ||
| 500 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { | |
| 501 | while (true) { | |
| 502 | if (!req.response.parser.state.isContent()) try req.waitForCompleteHead(); | |
| 503 | ||
| 504 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { | |
| 505 | assert(try req.transferRead(buffer) == 0); | |
| 506 | ||
| 507 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; | |
| 508 | ||
| 509 | const location = req.response.headers.location orelse | |
| 510 | return error.HttpRedirectMissingLocation; | |
| 511 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); | |
| 512 | ||
| 513 | var new_arena = std.heap.ArenaAllocator.init(req.client.allocator); | |
| 514 | const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator()); | |
| 515 | errdefer new_arena.deinit(); | |
| 516 | ||
| 517 | req.arena.deinit(); | |
| 518 | req.arena = new_arena; | |
| 519 | ||
| 520 | const new_req = try req.client.request(resolved_url, req.headers, .{ | |
| 521 | .max_redirects = req.redirects_left - 1, | |
| 522 | .header_strategy = if (req.response.parser.header_bytes_owned) .{ | |
| 523 | .dynamic = req.response.parser.max_header_bytes, | |
| 524 | } else .{ | |
| 525 | .static = req.response.parser.header_bytes.items.ptr[0..req.response.parser.max_header_bytes], | |
| 526 | }, | |
| 527 | }); | |
| 528 | req.deinit(); | |
| 529 | req.* = new_req; | |
| 530 | } else { | |
| 531 | break; | |
| 532 | } | |
| 533 | } | |
| 534 | ||
| 535 | return switch (req.response.compression) { | |
| 536 | .deflate => |*deflate| try deflate.read(buffer), | |
| 537 | .gzip => |*gzip| try gzip.read(buffer), | |
| 538 | .zstd => |*zstd| try zstd.read(buffer), | |
| 539 | else => try req.transferRead(buffer), | |
| 540 | }; | |
| 541 | } | |
| 542 | ||
| 543 | pub fn readAll(req: *Request, buffer: []u8) !usize { | |
| 544 | var index: usize = 0; | |
| 545 | while (index < buffer.len) { | |
| 546 | const amt = try read(req, buffer[index..]); | |
| 547 | if (amt == 0) break; | |
| 548 | index += amt; | |
| 549 | } | |
| 550 | return index; | |
| 551 | } | |
| 552 | ||
| 553 | pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong }; | |
| 554 | ||
| 555 | pub const Writer = std.io.Writer(*Request, WriteError, write); | |
| 556 | ||
| 557 | pub fn writer(req: *Request) Writer { | |
| 558 | return .{ .context = req }; | |
| 559 | } | |
| 560 | ||
| 561 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. | |
| 562 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { | |
| 563 | switch (req.headers.transfer_encoding) { | |
| 564 | .chunked => { | |
| 565 | try req.connection.data.writer().print("{x}\r\n", .{bytes.len}); | |
| 566 | try req.connection.data.writeAll(bytes); | |
| 567 | try req.connection.data.writeAll("\r\n"); | |
| 568 | ||
| 569 | return bytes.len; | |
| 570 | }, | |
| 571 | .content_length => |*len| { | |
| 572 | if (len.* < bytes.len) return error.MessageTooLong; | |
| 573 | ||
| 574 | const amt = try req.connection.data.write(bytes); | |
| 575 | len.* -= amt; | |
| 576 | return amt; | |
| 577 | }, | |
| 578 | .none => return error.NotWriteable, | |
| 579 | } | |
| 580 | } | |
| 581 | ||
| 582 | /// Finish the body of a request. This notifies the server that you have no more data to send. | |
| 583 | pub fn finish(req: *Request) !void { | |
| 584 | switch (req.headers.transfer_encoding) { | |
| 585 | .chunked => try req.connection.data.writeAll("0\r\n"), | |
| 586 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | |
| 587 | .none => {}, | |
| 588 | } | |
| 589 | } | |
| 590 | }; | |
| 591 | ||
| 223 | 592 | pub fn deinit(client: *Client) void { |
| 224 | 593 | client.connection_pool.deinit(client); |
| 225 | 594 | |
| ... | ... | @@ -227,7 +596,7 @@ pub fn deinit(client: *Client) void { |
| 227 | 596 | client.* = undefined; |
| 228 | 597 | } |
| 229 | 598 | |
| 230 | pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream); | |
| 599 | pub const ConnectError = Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream); | |
| 231 | 600 | |
| 232 | 601 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| 233 | 602 | if (client.connection_pool.findConnection(.{ |
| ... | ... | @@ -276,7 +645,26 @@ pub const RequestError = ConnectError || Connection.WriteError || error{ |
| 276 | 645 | EndOfStream, |
| 277 | 646 | }; |
| 278 | 647 | |
| 279 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Request.Options) RequestError!Request { | |
| 648 | pub const Options = struct { | |
| 649 | handle_redirects: bool = true, | |
| 650 | max_redirects: u32 = 3, | |
| 651 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, | |
| 652 | ||
| 653 | pub const HeaderStrategy = union(enum) { | |
| 654 | /// In this case, the client's Allocator will be used to store the | |
| 655 | /// entire HTTP header. This value is the maximum total size of | |
| 656 | /// HTTP headers allowed, otherwise | |
| 657 | /// error.HttpHeadersExceededSizeLimit is returned from read(). | |
| 658 | dynamic: usize, | |
| 659 | /// This is used to store the entire HTTP header. If the HTTP | |
| 660 | /// header is too big to fit, `error.HttpHeadersExceededSizeLimit` | |
| 661 | /// is returned from read(). When this is used, `error.OutOfMemory` | |
| 662 | /// cannot be returned from `read()`. | |
| 663 | static: []u8, | |
| 664 | }; | |
| 665 | }; | |
| 666 | ||
| 667 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { | |
| 280 | 668 | const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http")) |
| 281 | 669 | .plain |
| 282 | 670 | else if (mem.eql(u8, uri.scheme, "https")) |
| ... | ... | @@ -304,14 +692,15 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 304 | 692 | var req: Request = .{ |
| 305 | 693 | .uri = uri, |
| 306 | 694 | .client = client, |
| 307 | .headers = headers, | |
| 308 | 695 | .connection = try client.connect(host, port, protocol), |
| 696 | .headers = headers, | |
| 309 | 697 | .redirects_left = options.max_redirects, |
| 310 | 698 | .handle_redirects = options.handle_redirects, |
| 311 | .compression_init = false, | |
| 312 | .response = switch (options.header_strategy) { | |
| 313 | .dynamic => |max| Response.initDynamic(max), | |
| 314 | .static => |buf| Response.initStatic(buf), | |
| 699 | .response = .{ | |
| 700 | .parser = switch (options.header_strategy) { | |
| 701 | .dynamic => |max| proto.HeadersParser.initDynamic(max), | |
| 702 | .static => |buf| proto.HeadersParser.initStatic(buf), | |
| 703 | }, | |
| 315 | 704 | }, |
| 316 | 705 | .arena = undefined, |
| 317 | 706 | }; |
| ... | ... | @@ -358,6 +747,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 358 | 747 | try writer.writeAll("\r\nConnection: keep-alive"); |
| 359 | 748 | } |
| 360 | 749 | try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); |
| 750 | try writer.writeAll("\r\nTE: trailers, gzip, deflate"); | |
| 361 | 751 | |
| 362 | 752 | switch (headers.transfer_encoding) { |
| 363 | 753 | .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"), |
lib/std/http/Client/Request.zig deleted-482| ... | ... | @@ -1,482 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const http = std.http; | |
| 3 | const Uri = std.Uri; | |
| 4 | const mem = std.mem; | |
| 5 | const assert = std.debug.assert; | |
| 6 | ||
| 7 | const Client = @import("../Client.zig"); | |
| 8 | const Connection = Client.Connection; | |
| 9 | const ConnectionNode = Client.ConnectionPool.Node; | |
| 10 | const Response = @import("Response.zig"); | |
| 11 | ||
| 12 | const Request = @This(); | |
| 13 | ||
| 14 | const read_buffer_size = 8192; | |
| 15 | const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size); | |
| 16 | ||
| 17 | uri: Uri, | |
| 18 | client: *Client, | |
| 19 | connection: *ConnectionNode, | |
| 20 | response: Response, | |
| 21 | /// These are stored in Request so that they are available when following | |
| 22 | /// redirects. | |
| 23 | headers: Headers, | |
| 24 | ||
| 25 | redirects_left: u32, | |
| 26 | handle_redirects: bool, | |
| 27 | compression_init: bool, | |
| 28 | ||
| 29 | /// Used as a allocator for resolving redirects locations. | |
| 30 | arena: std.heap.ArenaAllocator, | |
| 31 | ||
| 32 | /// Read buffer for the connection. This is used to pull in large amounts of data from the connection even if the user asks for a small amount. This can probably be removed with careful planning. | |
| 33 | read_buffer: [read_buffer_size]u8 = undefined, | |
| 34 | read_buffer_start: ReadBufferIndex = 0, | |
| 35 | read_buffer_len: ReadBufferIndex = 0, | |
| 36 | ||
| 37 | pub const RequestTransfer = union(enum) { | |
| 38 | content_length: u64, | |
| 39 | chunked: void, | |
| 40 | none: void, | |
| 41 | }; | |
| 42 | ||
| 43 | pub const Headers = struct { | |
| 44 | version: http.Version = .@"HTTP/1.1", | |
| 45 | method: http.Method = .GET, | |
| 46 | user_agent: []const u8 = "zig (std.http)", | |
| 47 | connection: http.Connection = .keep_alive, | |
| 48 | transfer_encoding: RequestTransfer = .none, | |
| 49 | ||
| 50 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | |
| 51 | }; | |
| 52 | ||
| 53 | pub const Options = struct { | |
| 54 | handle_redirects: bool = true, | |
| 55 | max_redirects: u32 = 3, | |
| 56 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, | |
| 57 | ||
| 58 | pub const HeaderStrategy = union(enum) { | |
| 59 | /// In this case, the client's Allocator will be used to store the | |
| 60 | /// entire HTTP header. This value is the maximum total size of | |
| 61 | /// HTTP headers allowed, otherwise | |
| 62 | /// error.HttpHeadersExceededSizeLimit is returned from read(). | |
| 63 | dynamic: usize, | |
| 64 | /// This is used to store the entire HTTP header. If the HTTP | |
| 65 | /// header is too big to fit, `error.HttpHeadersExceededSizeLimit` | |
| 66 | /// is returned from read(). When this is used, `error.OutOfMemory` | |
| 67 | /// cannot be returned from `read()`. | |
| 68 | static: []u8, | |
| 69 | }; | |
| 70 | }; | |
| 71 | ||
| 72 | /// Frees all resources associated with the request. | |
| 73 | pub fn deinit(req: *Request) void { | |
| 74 | switch (req.response.compression) { | |
| 75 | .none => {}, | |
| 76 | .deflate => |*deflate| deflate.deinit(), | |
| 77 | .gzip => |*gzip| gzip.deinit(), | |
| 78 | .zstd => |*zstd| zstd.deinit(), | |
| 79 | } | |
| 80 | ||
| 81 | if (req.response.header_bytes_owned) { | |
| 82 | req.response.header_bytes.deinit(req.client.allocator); | |
| 83 | } | |
| 84 | ||
| 85 | if (!req.response.done) { | |
| 86 | // If the response wasn't fully read, then we need to close the connection. | |
| 87 | req.connection.data.closing = true; | |
| 88 | req.client.connection_pool.release(req.client, req.connection); | |
| 89 | } | |
| 90 | ||
| 91 | req.arena.deinit(); | |
| 92 | req.* = undefined; | |
| 93 | } | |
| 94 | ||
| 95 | pub const ReadRawError = Connection.ReadError || Uri.ParseError || Client.RequestError || error{ | |
| 96 | UnexpectedEndOfStream, | |
| 97 | TooManyHttpRedirects, | |
| 98 | HttpRedirectMissingLocation, | |
| 99 | HttpHeadersInvalid, | |
| 100 | }; | |
| 101 | ||
| 102 | pub const ReaderRaw = std.io.Reader(*Request, ReadRawError, readRaw); | |
| 103 | ||
| 104 | /// Read from the underlying stream, without decompressing or parsing the headers. Must be called | |
| 105 | /// after waitForCompleteHead() has returned successfully. | |
| 106 | pub fn readRaw(req: *Request, buffer: []u8) ReadRawError!usize { | |
| 107 | assert(req.response.state.isContent()); | |
| 108 | ||
| 109 | var index: usize = 0; | |
| 110 | while (index == 0) { | |
| 111 | const amt = try req.readRawAdvanced(buffer[index..]); | |
| 112 | if (amt == 0 and req.response.done) break; | |
| 113 | index += amt; | |
| 114 | } | |
| 115 | ||
| 116 | return index; | |
| 117 | } | |
| 118 | ||
| 119 | fn checkForCompleteHead(req: *Request, buffer: []u8) !usize { | |
| 120 | switch (req.response.state) { | |
| 121 | .invalid => unreachable, | |
| 122 | .start, .seen_r, .seen_rn, .seen_rnr => {}, | |
| 123 | else => return 0, // No more headers to read. | |
| 124 | } | |
| 125 | ||
| 126 | const i = req.response.findHeadersEnd(buffer[0..]); | |
| 127 | if (req.response.state == .invalid) return error.HttpHeadersInvalid; | |
| 128 | ||
| 129 | const headers_data = buffer[0..i]; | |
| 130 | if (req.response.header_bytes.items.len + headers_data.len > req.response.max_header_bytes) { | |
| 131 | return error.HttpHeadersExceededSizeLimit; | |
| 132 | } | |
| 133 | try req.response.header_bytes.appendSlice(req.client.allocator, headers_data); | |
| 134 | ||
| 135 | if (req.response.state == .finished) { | |
| 136 | req.response.headers = try Response.Headers.parse(req.response.header_bytes.items); | |
| 137 | ||
| 138 | if (req.response.headers.upgrade) |_| { | |
| 139 | req.connection.data.closing = false; | |
| 140 | req.response.done = true; | |
| 141 | return i; | |
| 142 | } | |
| 143 | ||
| 144 | if (req.response.headers.connection == .keep_alive) { | |
| 145 | req.connection.data.closing = false; | |
| 146 | } else { | |
| 147 | req.connection.data.closing = true; | |
| 148 | } | |
| 149 | ||
| 150 | if (req.response.headers.transfer_encoding) |transfer_encoding| { | |
| 151 | switch (transfer_encoding) { | |
| 152 | .chunked => { | |
| 153 | req.response.next_chunk_length = 0; | |
| 154 | req.response.state = .chunk_size; | |
| 155 | }, | |
| 156 | } | |
| 157 | } else if (req.response.headers.content_length) |content_length| { | |
| 158 | req.response.next_chunk_length = content_length; | |
| 159 | ||
| 160 | if (content_length == 0) req.response.done = true; | |
| 161 | } else { | |
| 162 | req.response.done = true; | |
| 163 | } | |
| 164 | ||
| 165 | return i; | |
| 166 | } | |
| 167 | ||
| 168 | return 0; | |
| 169 | } | |
| 170 | ||
| 171 | pub const WaitForCompleteHeadError = ReadRawError || error{ | |
| 172 | UnexpectedEndOfStream, | |
| 173 | ||
| 174 | HttpHeadersExceededSizeLimit, | |
| 175 | ShortHttpStatusLine, | |
| 176 | BadHttpVersion, | |
| 177 | HttpHeaderContinuationsUnsupported, | |
| 178 | HttpTransferEncodingUnsupported, | |
| 179 | HttpConnectionHeaderUnsupported, | |
| 180 | }; | |
| 181 | ||
| 182 | /// Reads a complete response head. Any leftover data is stored in the request. This function is idempotent. | |
| 183 | pub fn waitForCompleteHead(req: *Request) WaitForCompleteHeadError!void { | |
| 184 | if (req.response.state.isContent()) return; | |
| 185 | ||
| 186 | while (true) { | |
| 187 | const nread = try req.connection.data.read(req.read_buffer[0..]); | |
| 188 | const amt = try checkForCompleteHead(req, req.read_buffer[0..nread]); | |
| 189 | ||
| 190 | if (amt != 0) { | |
| 191 | req.read_buffer_start = @intCast(ReadBufferIndex, amt); | |
| 192 | req.read_buffer_len = @intCast(ReadBufferIndex, nread); | |
| 193 | return; | |
| 194 | } else if (nread == 0) { | |
| 195 | return error.UnexpectedEndOfStream; | |
| 196 | } | |
| 197 | } | |
| 198 | } | |
| 199 | ||
| 200 | /// This one can return 0 without meaning EOF. | |
| 201 | fn readRawAdvanced(req: *Request, buffer: []u8) !usize { | |
| 202 | assert(req.response.state.isContent()); | |
| 203 | if (req.response.done) return 0; | |
| 204 | ||
| 205 | // var in: []const u8 = undefined; | |
| 206 | if (req.read_buffer_start == req.read_buffer_len) { | |
| 207 | const nread = try req.connection.data.read(req.read_buffer[0..]); | |
| 208 | if (nread == 0) return error.UnexpectedEndOfStream; | |
| 209 | ||
| 210 | req.read_buffer_start = 0; | |
| 211 | req.read_buffer_len = @intCast(ReadBufferIndex, nread); | |
| 212 | } | |
| 213 | ||
| 214 | var out_index: usize = 0; | |
| 215 | while (true) { | |
| 216 | switch (req.response.state) { | |
| 217 | .invalid, .start, .seen_r, .seen_rn, .seen_rnr => unreachable, | |
| 218 | .finished => { | |
| 219 | // TODO https://github.com/ziglang/zig/issues/14039 | |
| 220 | const buf_avail = req.read_buffer_len - req.read_buffer_start; | |
| 221 | const data_avail = req.response.next_chunk_length; | |
| 222 | const out_avail = buffer.len; | |
| 223 | ||
| 224 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { | |
| 225 | const can_read = @intCast(usize, @min(buf_avail, data_avail)); | |
| 226 | req.response.next_chunk_length -= can_read; | |
| 227 | ||
| 228 | if (req.response.next_chunk_length == 0) { | |
| 229 | req.client.connection_pool.release(req.client, req.connection); | |
| 230 | req.connection = undefined; | |
| 231 | req.response.done = true; | |
| 232 | } | |
| 233 | ||
| 234 | return 0; // skip over as much data as possible | |
| 235 | } | |
| 236 | ||
| 237 | const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail)); | |
| 238 | req.response.next_chunk_length -= can_read; | |
| 239 | ||
| 240 | mem.copy(u8, buffer[0..], req.read_buffer[req.read_buffer_start..][0..can_read]); | |
| 241 | req.read_buffer_start += @intCast(ReadBufferIndex, can_read); | |
| 242 | ||
| 243 | if (req.response.next_chunk_length == 0) { | |
| 244 | req.client.connection_pool.release(req.client, req.connection); | |
| 245 | req.connection = undefined; | |
| 246 | req.response.done = true; | |
| 247 | } | |
| 248 | ||
| 249 | return can_read; | |
| 250 | }, | |
| 251 | .chunk_size_prefix_r => switch (req.read_buffer_len - req.read_buffer_start) { | |
| 252 | 0 => return out_index, | |
| 253 | 1 => switch (req.read_buffer[req.read_buffer_start]) { | |
| 254 | '\r' => { | |
| 255 | req.response.state = .chunk_size_prefix_n; | |
| 256 | return out_index; | |
| 257 | }, | |
| 258 | else => { | |
| 259 | req.response.state = .invalid; | |
| 260 | return error.HttpHeadersInvalid; | |
| 261 | }, | |
| 262 | }, | |
| 263 | else => switch (int16(req.read_buffer[req.read_buffer_start..][0..2])) { | |
| 264 | int16("\r\n") => { | |
| 265 | req.read_buffer_start += 2; | |
| 266 | req.response.state = .chunk_size; | |
| 267 | continue; | |
| 268 | }, | |
| 269 | else => { | |
| 270 | req.response.state = .invalid; | |
| 271 | return error.HttpHeadersInvalid; | |
| 272 | }, | |
| 273 | }, | |
| 274 | }, | |
| 275 | .chunk_size_prefix_n => switch (req.read_buffer_len - req.read_buffer_start) { | |
| 276 | 0 => return out_index, | |
| 277 | else => switch (req.read_buffer[req.read_buffer_start]) { | |
| 278 | '\n' => { | |
| 279 | req.read_buffer_start += 1; | |
| 280 | req.response.state = .chunk_size; | |
| 281 | continue; | |
| 282 | }, | |
| 283 | else => { | |
| 284 | req.response.state = .invalid; | |
| 285 | return error.HttpHeadersInvalid; | |
| 286 | }, | |
| 287 | }, | |
| 288 | }, | |
| 289 | .chunk_size, .chunk_r => { | |
| 290 | const i = req.response.findChunkedLen(req.read_buffer[req.read_buffer_start..req.read_buffer_len]); | |
| 291 | switch (req.response.state) { | |
| 292 | .invalid => return error.HttpHeadersInvalid, | |
| 293 | .chunk_data => { | |
| 294 | if (req.response.next_chunk_length == 0) { | |
| 295 | req.response.done = true; | |
| 296 | req.client.connection_pool.release(req.client, req.connection); | |
| 297 | req.connection = undefined; | |
| 298 | ||
| 299 | return out_index; | |
| 300 | } | |
| 301 | ||
| 302 | req.read_buffer_start += @intCast(ReadBufferIndex, i); | |
| 303 | continue; | |
| 304 | }, | |
| 305 | .chunk_size => return out_index, | |
| 306 | else => unreachable, | |
| 307 | } | |
| 308 | }, | |
| 309 | .chunk_data => { | |
| 310 | // TODO https://github.com/ziglang/zig/issues/14039 | |
| 311 | const buf_avail = req.read_buffer_len - req.read_buffer_start; | |
| 312 | const data_avail = req.response.next_chunk_length; | |
| 313 | const out_avail = buffer.len - out_index; | |
| 314 | ||
| 315 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { | |
| 316 | const can_read = @intCast(usize, @min(buf_avail, data_avail)); | |
| 317 | req.response.next_chunk_length -= can_read; | |
| 318 | ||
| 319 | if (req.response.next_chunk_length == 0) { | |
| 320 | req.client.connection_pool.release(req.client, req.connection); | |
| 321 | req.connection = undefined; | |
| 322 | req.response.done = true; | |
| 323 | continue; | |
| 324 | } | |
| 325 | ||
| 326 | return 0; // skip over as much data as possible | |
| 327 | } | |
| 328 | ||
| 329 | const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail)); | |
| 330 | req.response.next_chunk_length -= can_read; | |
| 331 | ||
| 332 | mem.copy(u8, buffer[out_index..], req.read_buffer[req.read_buffer_start..][0..can_read]); | |
| 333 | req.read_buffer_start += @intCast(ReadBufferIndex, can_read); | |
| 334 | out_index += can_read; | |
| 335 | ||
| 336 | if (req.response.next_chunk_length == 0) { | |
| 337 | req.response.state = .chunk_size_prefix_r; | |
| 338 | ||
| 339 | continue; | |
| 340 | } | |
| 341 | ||
| 342 | return out_index; | |
| 343 | }, | |
| 344 | } | |
| 345 | } | |
| 346 | } | |
| 347 | ||
| 348 | pub const ReadError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize, CompressionNotSupported }; | |
| 349 | ||
| 350 | pub const Reader = std.io.Reader(*Request, ReadError, read); | |
| 351 | ||
| 352 | pub fn reader(req: *Request) Reader { | |
| 353 | return .{ .context = req }; | |
| 354 | } | |
| 355 | ||
| 356 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { | |
| 357 | while (true) { | |
| 358 | if (!req.response.state.isContent()) try req.waitForCompleteHead(); | |
| 359 | ||
| 360 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { | |
| 361 | assert(try req.readRaw(buffer) == 0); | |
| 362 | ||
| 363 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; | |
| 364 | ||
| 365 | const location = req.response.headers.location orelse | |
| 366 | return error.HttpRedirectMissingLocation; | |
| 367 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); | |
| 368 | ||
| 369 | var new_arena = std.heap.ArenaAllocator.init(req.client.allocator); | |
| 370 | const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator()); | |
| 371 | errdefer new_arena.deinit(); | |
| 372 | ||
| 373 | req.arena.deinit(); | |
| 374 | req.arena = new_arena; | |
| 375 | ||
| 376 | const new_req = try req.client.request(resolved_url, req.headers, .{ | |
| 377 | .max_redirects = req.redirects_left - 1, | |
| 378 | .header_strategy = if (req.response.header_bytes_owned) .{ | |
| 379 | .dynamic = req.response.max_header_bytes, | |
| 380 | } else .{ | |
| 381 | .static = req.response.header_bytes.unusedCapacitySlice(), | |
| 382 | }, | |
| 383 | }); | |
| 384 | req.deinit(); | |
| 385 | req.* = new_req; | |
| 386 | } else { | |
| 387 | break; | |
| 388 | } | |
| 389 | } | |
| 390 | ||
| 391 | if (req.response.compression == .none) { | |
| 392 | if (req.response.headers.transfer_compression) |compression| { | |
| 393 | switch (compression) { | |
| 394 | .compress => return error.CompressionNotSupported, | |
| 395 | .deflate => req.response.compression = .{ | |
| 396 | .deflate = try std.compress.zlib.zlibStream(req.client.allocator, ReaderRaw{ .context = req }), | |
| 397 | }, | |
| 398 | .gzip => req.response.compression = .{ | |
| 399 | .gzip = try std.compress.gzip.decompress(req.client.allocator, ReaderRaw{ .context = req }), | |
| 400 | }, | |
| 401 | .zstd => req.response.compression = .{ | |
| 402 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, ReaderRaw{ .context = req }), | |
| 403 | }, | |
| 404 | } | |
| 405 | } | |
| 406 | } | |
| 407 | ||
| 408 | return switch (req.response.compression) { | |
| 409 | .deflate => |*deflate| try deflate.read(buffer), | |
| 410 | .gzip => |*gzip| try gzip.read(buffer), | |
| 411 | .zstd => |*zstd| try zstd.read(buffer), | |
| 412 | else => try req.readRaw(buffer), | |
| 413 | }; | |
| 414 | } | |
| 415 | ||
| 416 | pub fn readAll(req: *Request, buffer: []u8) !usize { | |
| 417 | var index: usize = 0; | |
| 418 | while (index < buffer.len) { | |
| 419 | const amt = try read(req, buffer[index..]); | |
| 420 | if (amt == 0) break; | |
| 421 | index += amt; | |
| 422 | } | |
| 423 | return index; | |
| 424 | } | |
| 425 | ||
| 426 | pub const WriteError = Connection.WriteError || error{MessageTooLong}; | |
| 427 | ||
| 428 | pub const Writer = std.io.Writer(*Request, WriteError, write); | |
| 429 | ||
| 430 | pub fn writer(req: *Request) Writer { | |
| 431 | return .{ .context = req }; | |
| 432 | } | |
| 433 | ||
| 434 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. | |
| 435 | pub fn write(req: *Request, bytes: []const u8) !usize { | |
| 436 | switch (req.headers.transfer_encoding) { | |
| 437 | .chunked => { | |
| 438 | try req.connection.data.writer().print("{x}\r\n", .{bytes.len}); | |
| 439 | try req.connection.data.writeAll(bytes); | |
| 440 | try req.connection.data.writeAll("\r\n"); | |
| 441 | ||
| 442 | return bytes.len; | |
| 443 | }, | |
| 444 | .content_length => |*len| { | |
| 445 | if (len.* < bytes.len) return error.MessageTooLong; | |
| 446 | ||
| 447 | const amt = try req.connection.data.write(bytes); | |
| 448 | len.* -= amt; | |
| 449 | return amt; | |
| 450 | }, | |
| 451 | .none => return error.NotWriteable, | |
| 452 | } | |
| 453 | } | |
| 454 | ||
| 455 | /// Finish the body of a request. This notifies the server that you have no more data to send. | |
| 456 | pub fn finish(req: *Request) !void { | |
| 457 | switch (req.headers.transfer_encoding) { | |
| 458 | .chunked => try req.connection.data.writeAll("0\r\n"), | |
| 459 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | |
| 460 | .none => {}, | |
| 461 | } | |
| 462 | } | |
| 463 | ||
| 464 | inline fn int16(array: *const [2]u8) u16 { | |
| 465 | return @bitCast(u16, array.*); | |
| 466 | } | |
| 467 | ||
| 468 | inline fn int32(array: *const [4]u8) u32 { | |
| 469 | return @bitCast(u32, array.*); | |
| 470 | } | |
| 471 | ||
| 472 | inline fn int64(array: *const [8]u8) u64 { | |
| 473 | return @bitCast(u64, array.*); | |
| 474 | } | |
| 475 | ||
| 476 | test { | |
| 477 | const builtin = @import("builtin"); | |
| 478 | ||
| 479 | if (builtin.os.tag == .wasi) return error.SkipZigTest; | |
| 480 | ||
| 481 | _ = Response; | |
| 482 | } |
lib/std/http/Client/Response.zig+1-234| ... | ... | @@ -4,6 +4,7 @@ const mem = std.mem; |
| 4 | 4 | const testing = std.testing; |
| 5 | 5 | const assert = std.debug.assert; |
| 6 | 6 | |
| 7 | const protocol = @import("../protocol.zig"); | |
| 7 | 8 | const Client = @import("../Client.zig"); |
| 8 | 9 | const Response = @This(); |
| 9 | 10 | |
| ... | ... | @@ -169,14 +170,6 @@ pub const Headers = struct { |
| 169 | 170 | } |
| 170 | 171 | }; |
| 171 | 172 | |
| 172 | inline fn int16(array: *const [2]u8) u16 { | |
| 173 | return @bitCast(u16, array.*); | |
| 174 | } | |
| 175 | ||
| 176 | inline fn int32(array: *const [4]u8) u32 { | |
| 177 | return @bitCast(u32, array.*); | |
| 178 | } | |
| 179 | ||
| 180 | 173 | inline fn int64(array: *const [8]u8) u64 { |
| 181 | 174 | return @bitCast(u64, array.*); |
| 182 | 175 | } |
| ... | ... | @@ -226,232 +219,6 @@ pub fn initStatic(buf: []u8) Response { |
| 226 | 219 | }; |
| 227 | 220 | } |
| 228 | 221 | |
| 229 | /// Returns how many bytes are part of HTTP headers. Always less than or | |
| 230 | /// equal to bytes.len. If the amount returned is less than bytes.len, it | |
| 231 | /// means the headers ended and the first byte after the double \r\n\r\n is | |
| 232 | /// located at `bytes[result]`. | |
| 233 | pub fn findHeadersEnd(r: *Response, bytes: []const u8) usize { | |
| 234 | var index: usize = 0; | |
| 235 | ||
| 236 | // TODO: https://github.com/ziglang/zig/issues/8220 | |
| 237 | state: while (true) { | |
| 238 | switch (r.state) { | |
| 239 | .invalid => unreachable, | |
| 240 | .finished => unreachable, | |
| 241 | .start => while (true) { | |
| 242 | switch (bytes.len - index) { | |
| 243 | 0 => return index, | |
| 244 | 1 => { | |
| 245 | if (bytes[index] == '\r') | |
| 246 | r.state = .seen_r; | |
| 247 | return index + 1; | |
| 248 | }, | |
| 249 | 2 => { | |
| 250 | if (int16(bytes[index..][0..2]) == int16("\r\n")) { | |
| 251 | r.state = .seen_rn; | |
| 252 | } else if (bytes[index + 1] == '\r') { | |
| 253 | r.state = .seen_r; | |
| 254 | } | |
| 255 | return index + 2; | |
| 256 | }, | |
| 257 | 3 => { | |
| 258 | if (int16(bytes[index..][0..2]) == int16("\r\n") and | |
| 259 | bytes[index + 2] == '\r') | |
| 260 | { | |
| 261 | r.state = .seen_rnr; | |
| 262 | } else if (int16(bytes[index + 1 ..][0..2]) == int16("\r\n")) { | |
| 263 | r.state = .seen_rn; | |
| 264 | } else if (bytes[index + 2] == '\r') { | |
| 265 | r.state = .seen_r; | |
| 266 | } | |
| 267 | return index + 3; | |
| 268 | }, | |
| 269 | 4...15 => { | |
| 270 | if (int32(bytes[index..][0..4]) == int32("\r\n\r\n")) { | |
| 271 | r.state = .finished; | |
| 272 | return index + 4; | |
| 273 | } else if (int16(bytes[index + 1 ..][0..2]) == int16("\r\n") and | |
| 274 | bytes[index + 3] == '\r') | |
| 275 | { | |
| 276 | r.state = .seen_rnr; | |
| 277 | index += 4; | |
| 278 | continue :state; | |
| 279 | } else if (int16(bytes[index + 2 ..][0..2]) == int16("\r\n")) { | |
| 280 | r.state = .seen_rn; | |
| 281 | index += 4; | |
| 282 | continue :state; | |
| 283 | } else if (bytes[index + 3] == '\r') { | |
| 284 | r.state = .seen_r; | |
| 285 | index += 4; | |
| 286 | continue :state; | |
| 287 | } | |
| 288 | index += 4; | |
| 289 | continue; | |
| 290 | }, | |
| 291 | else => { | |
| 292 | const chunk = bytes[index..][0..16]; | |
| 293 | const v: @Vector(16, u8) = chunk.*; | |
| 294 | const matches_r = v == @splat(16, @as(u8, '\r')); | |
| 295 | const iota = std.simd.iota(u8, 16); | |
| 296 | const default = @splat(16, @as(u8, 16)); | |
| 297 | const sub_index = @reduce(.Min, @select(u8, matches_r, iota, default)); | |
| 298 | switch (sub_index) { | |
| 299 | 0...12 => { | |
| 300 | index += sub_index + 4; | |
| 301 | if (int32(chunk[sub_index..][0..4]) == int32("\r\n\r\n")) { | |
| 302 | r.state = .finished; | |
| 303 | return index; | |
| 304 | } | |
| 305 | continue; | |
| 306 | }, | |
| 307 | 13 => { | |
| 308 | index += 16; | |
| 309 | if (int16(chunk[14..][0..2]) == int16("\n\r")) { | |
| 310 | r.state = .seen_rnr; | |
| 311 | continue :state; | |
| 312 | } | |
| 313 | continue; | |
| 314 | }, | |
| 315 | 14 => { | |
| 316 | index += 16; | |
| 317 | if (chunk[15] == '\n') { | |
| 318 | r.state = .seen_rn; | |
| 319 | continue :state; | |
| 320 | } | |
| 321 | continue; | |
| 322 | }, | |
| 323 | 15 => { | |
| 324 | r.state = .seen_r; | |
| 325 | index += 16; | |
| 326 | continue :state; | |
| 327 | }, | |
| 328 | 16 => { | |
| 329 | index += 16; | |
| 330 | continue; | |
| 331 | }, | |
| 332 | else => unreachable, | |
| 333 | } | |
| 334 | }, | |
| 335 | } | |
| 336 | }, | |
| 337 | ||
| 338 | .seen_r => switch (bytes.len - index) { | |
| 339 | 0 => return index, | |
| 340 | 1 => { | |
| 341 | switch (bytes[index]) { | |
| 342 | '\n' => r.state = .seen_rn, | |
| 343 | '\r' => r.state = .seen_r, | |
| 344 | else => r.state = .start, | |
| 345 | } | |
| 346 | return index + 1; | |
| 347 | }, | |
| 348 | 2 => { | |
| 349 | if (int16(bytes[index..][0..2]) == int16("\n\r")) { | |
| 350 | r.state = .seen_rnr; | |
| 351 | return index + 2; | |
| 352 | } | |
| 353 | r.state = .start; | |
| 354 | return index + 2; | |
| 355 | }, | |
| 356 | else => { | |
| 357 | if (int16(bytes[index..][0..2]) == int16("\n\r") and | |
| 358 | bytes[index + 2] == '\n') | |
| 359 | { | |
| 360 | r.state = .finished; | |
| 361 | return index + 3; | |
| 362 | } | |
| 363 | index += 3; | |
| 364 | r.state = .start; | |
| 365 | continue :state; | |
| 366 | }, | |
| 367 | }, | |
| 368 | .seen_rn => switch (bytes.len - index) { | |
| 369 | 0 => return index, | |
| 370 | 1 => { | |
| 371 | switch (bytes[index]) { | |
| 372 | '\r' => r.state = .seen_rnr, | |
| 373 | else => r.state = .start, | |
| 374 | } | |
| 375 | return index + 1; | |
| 376 | }, | |
| 377 | else => { | |
| 378 | if (int16(bytes[index..][0..2]) == int16("\r\n")) { | |
| 379 | r.state = .finished; | |
| 380 | return index + 2; | |
| 381 | } | |
| 382 | index += 2; | |
| 383 | r.state = .start; | |
| 384 | continue :state; | |
| 385 | }, | |
| 386 | }, | |
| 387 | .seen_rnr => switch (bytes.len - index) { | |
| 388 | 0 => return index, | |
| 389 | else => { | |
| 390 | if (bytes[index] == '\n') { | |
| 391 | r.state = .finished; | |
| 392 | return index + 1; | |
| 393 | } | |
| 394 | index += 1; | |
| 395 | r.state = .start; | |
| 396 | continue :state; | |
| 397 | }, | |
| 398 | }, | |
| 399 | .chunk_size_prefix_r => unreachable, | |
| 400 | .chunk_size_prefix_n => unreachable, | |
| 401 | .chunk_size => unreachable, | |
| 402 | .chunk_r => unreachable, | |
| 403 | .chunk_data => unreachable, | |
| 404 | } | |
| 405 | ||
| 406 | return index; | |
| 407 | } | |
| 408 | } | |
| 409 | ||
| 410 | pub fn findChunkedLen(r: *Response, bytes: []const u8) usize { | |
| 411 | var i: usize = 0; | |
| 412 | if (r.state == .chunk_size) { | |
| 413 | while (i < bytes.len) : (i += 1) { | |
| 414 | const digit = switch (bytes[i]) { | |
| 415 | '0'...'9' => |b| b - '0', | |
| 416 | 'A'...'Z' => |b| b - 'A' + 10, | |
| 417 | 'a'...'z' => |b| b - 'a' + 10, | |
| 418 | '\r' => { | |
| 419 | r.state = .chunk_r; | |
| 420 | i += 1; | |
| 421 | break; | |
| 422 | }, | |
| 423 | else => { | |
| 424 | r.state = .invalid; | |
| 425 | return i; | |
| 426 | }, | |
| 427 | }; | |
| 428 | const mul = @mulWithOverflow(r.next_chunk_length, 16); | |
| 429 | if (mul[1] != 0) { | |
| 430 | r.state = .invalid; | |
| 431 | return i; | |
| 432 | } | |
| 433 | const add = @addWithOverflow(mul[0], digit); | |
| 434 | if (add[1] != 0) { | |
| 435 | r.state = .invalid; | |
| 436 | return i; | |
| 437 | } | |
| 438 | r.next_chunk_length = add[0]; | |
| 439 | } else { | |
| 440 | return i; | |
| 441 | } | |
| 442 | } | |
| 443 | assert(r.state == .chunk_r); | |
| 444 | if (i == bytes.len) return i; | |
| 445 | ||
| 446 | if (bytes[i] == '\n') { | |
| 447 | r.state = .chunk_data; | |
| 448 | return i + 1; | |
| 449 | } else { | |
| 450 | r.state = .invalid; | |
| 451 | return i; | |
| 452 | } | |
| 453 | } | |
| 454 | ||
| 455 | 222 | fn parseInt3(nnn: @Vector(3, u8)) u10 { |
| 456 | 223 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; |
| 457 | 224 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; |
lib/std/http/Server.zig created+495| ... | ... | @@ -0,0 +1,495 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const testing = std.testing; | |
| 3 | const http = std.http; | |
| 4 | const mem = std.mem; | |
| 5 | const net = std.net; | |
| 6 | const Uri = std.Uri; | |
| 7 | const Allocator = mem.Allocator; | |
| 8 | const assert = std.debug.assert; | |
| 9 | ||
| 10 | const Server = @This(); | |
| 11 | const proto = @import("protocol.zig"); | |
| 12 | ||
| 13 | allocator: Allocator, | |
| 14 | ||
| 15 | socket: net.StreamServer, | |
| 16 | ||
| 17 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader); | |
| 18 | pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader); | |
| 19 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{}); | |
| 20 | ||
| 21 | pub const Connection = struct { | |
| 22 | stream: net.Stream, | |
| 23 | protocol: Protocol, | |
| 24 | ||
| 25 | closing: bool = true, | |
| 26 | ||
| 27 | pub const Protocol = enum { plain }; | |
| 28 | ||
| 29 | pub fn read(conn: *Connection, buffer: []u8) !usize { | |
| 30 | switch (conn.protocol) { | |
| 31 | .plain => return conn.stream.read(buffer), | |
| 32 | // .tls => return conn.tls_client.read(conn.stream, buffer), | |
| 33 | } | |
| 34 | } | |
| 35 | ||
| 36 | pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize { | |
| 37 | switch (conn.protocol) { | |
| 38 | .plain => return conn.stream.readAtLeast(buffer, len), | |
| 39 | // .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len), | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | pub const ReadError = net.Stream.ReadError; | |
| 44 | ||
| 45 | pub const Reader = std.io.Reader(*Connection, ReadError, read); | |
| 46 | ||
| 47 | pub fn reader(conn: *Connection) Reader { | |
| 48 | return Reader{ .context = conn }; | |
| 49 | } | |
| 50 | ||
| 51 | pub fn writeAll(conn: *Connection, buffer: []const u8) !void { | |
| 52 | switch (conn.protocol) { | |
| 53 | .plain => return conn.stream.writeAll(buffer), | |
| 54 | // .tls => return conn.tls_client.writeAll(conn.stream, buffer), | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | pub fn write(conn: *Connection, buffer: []const u8) !usize { | |
| 59 | switch (conn.protocol) { | |
| 60 | .plain => return conn.stream.write(buffer), | |
| 61 | // .tls => return conn.tls_client.write(conn.stream, buffer), | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | pub const WriteError = net.Stream.WriteError || error{}; | |
| 66 | pub const Writer = std.io.Writer(*Connection, WriteError, write); | |
| 67 | ||
| 68 | pub fn writer(conn: *Connection) Writer { | |
| 69 | return Writer{ .context = conn }; | |
| 70 | } | |
| 71 | ||
| 72 | pub fn close(conn: *Connection) void { | |
| 73 | conn.stream.close(); | |
| 74 | } | |
| 75 | }; | |
| 76 | ||
| 77 | pub const Request = struct { | |
| 78 | pub const Headers = struct { | |
| 79 | method: http.Method, | |
| 80 | target: []const u8, | |
| 81 | version: http.Version, | |
| 82 | content_length: ?u64 = null, | |
| 83 | transfer_encoding: ?http.TransferEncoding = null, | |
| 84 | transfer_compression: ?http.ContentEncoding = null, | |
| 85 | connection: http.Connection = .close, | |
| 86 | host: ?[]const u8 = null, | |
| 87 | ||
| 88 | pub const ParseError = error{ | |
| 89 | ShortHttpStatusLine, | |
| 90 | BadHttpVersion, | |
| 91 | UnknownHttpMethod, | |
| 92 | HttpHeadersInvalid, | |
| 93 | HttpHeaderContinuationsUnsupported, | |
| 94 | HttpTransferEncodingUnsupported, | |
| 95 | HttpConnectionHeaderUnsupported, | |
| 96 | InvalidCharacter, | |
| 97 | }; | |
| 98 | ||
| 99 | pub fn parse(bytes: []const u8) !Headers { | |
| 100 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | |
| 101 | ||
| 102 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | |
| 103 | if (first_line.len < 10) | |
| 104 | return error.ShortHttpStatusLine; | |
| 105 | ||
| 106 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | |
| 107 | const method_str = first_line[0..method_end]; | |
| 108 | const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; | |
| 109 | ||
| 110 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | |
| 111 | if (version_start == method_end) return error.HttpHeadersInvalid; | |
| 112 | ||
| 113 | const version_str = first_line[version_start + 1 ..]; | |
| 114 | if (version_str.len != 8) return error.HttpHeadersInvalid; | |
| 115 | const version: http.Version = switch (int64(version_str[0..8])) { | |
| 116 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 117 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 118 | else => return error.BadHttpVersion, | |
| 119 | }; | |
| 120 | ||
| 121 | const target = first_line[method_end + 1 .. version_start]; | |
| 122 | ||
| 123 | var headers: Headers = .{ | |
| 124 | .method = method, | |
| 125 | .target = target, | |
| 126 | .version = version, | |
| 127 | }; | |
| 128 | ||
| 129 | while (it.next()) |line| { | |
| 130 | if (line.len == 0) return error.HttpHeadersInvalid; | |
| 131 | switch (line[0]) { | |
| 132 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 133 | else => {}, | |
| 134 | } | |
| 135 | ||
| 136 | var line_it = mem.tokenize(u8, line, ": "); | |
| 137 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | |
| 138 | const header_value = line_it.rest(); | |
| 139 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 140 | if (headers.content_length != null) return error.HttpHeadersInvalid; | |
| 141 | headers.content_length = try std.fmt.parseInt(u64, header_value, 10); | |
| 142 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 143 | // Transfer-Encoding: second, first | |
| 144 | // Transfer-Encoding: deflate, chunked | |
| 145 | var iter = mem.splitBackwards(u8, header_value, ","); | |
| 146 | ||
| 147 | if (iter.next()) |first| { | |
| 148 | const trimmed = mem.trim(u8, first, " "); | |
| 149 | ||
| 150 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | |
| 151 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | |
| 152 | headers.transfer_encoding = te; | |
| 153 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 154 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 155 | headers.transfer_compression = ce; | |
| 156 | } else { | |
| 157 | return error.HttpTransferEncodingUnsupported; | |
| 158 | } | |
| 159 | } | |
| 160 | ||
| 161 | if (iter.next()) |second| { | |
| 162 | if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | |
| 163 | ||
| 164 | const trimmed = mem.trim(u8, second, " "); | |
| 165 | ||
| 166 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 167 | headers.transfer_compression = ce; | |
| 168 | } else { | |
| 169 | return error.HttpTransferEncodingUnsupported; | |
| 170 | } | |
| 171 | } | |
| 172 | ||
| 173 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 174 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 175 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | |
| 176 | ||
| 177 | const trimmed = mem.trim(u8, header_value, " "); | |
| 178 | ||
| 179 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 180 | headers.transfer_compression = ce; | |
| 181 | } else { | |
| 182 | return error.HttpTransferEncodingUnsupported; | |
| 183 | } | |
| 184 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 185 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | |
| 186 | headers.connection = .keep_alive; | |
| 187 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | |
| 188 | headers.connection = .close; | |
| 189 | } else { | |
| 190 | return error.HttpConnectionHeaderUnsupported; | |
| 191 | } | |
| 192 | } else if (std.ascii.eqlIgnoreCase(header_name, "host")) { | |
| 193 | headers.host = header_value; | |
| 194 | } | |
| 195 | } | |
| 196 | ||
| 197 | return headers; | |
| 198 | } | |
| 199 | ||
| 200 | inline fn int64(array: *const [8]u8) u64 { | |
| 201 | return @bitCast(u64, array.*); | |
| 202 | } | |
| 203 | }; | |
| 204 | ||
| 205 | headers: Headers = undefined, | |
| 206 | parser: proto.HeadersParser, | |
| 207 | compression: Compression = .none, | |
| 208 | }; | |
| 209 | ||
| 210 | pub const Response = struct { | |
| 211 | pub const Headers = struct { | |
| 212 | version: http.Version = .@"HTTP/1.1", | |
| 213 | status: http.Status = .ok, | |
| 214 | reason: ?[]const u8 = null, | |
| 215 | ||
| 216 | server: ?[]const u8 = "zig (std.http)", | |
| 217 | connection: http.Connection = .keep_alive, | |
| 218 | transfer_encoding: RequestTransfer = .none, | |
| 219 | ||
| 220 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | |
| 221 | }; | |
| 222 | ||
| 223 | server: *Server, | |
| 224 | address: net.Address, | |
| 225 | connection: Connection, | |
| 226 | ||
| 227 | headers: Headers = .{}, | |
| 228 | request: Request, | |
| 229 | ||
| 230 | pub fn reset(res: *Response) void { | |
| 231 | switch (res.request.compression) { | |
| 232 | .none => {}, | |
| 233 | .deflate => |*deflate| deflate.deinit(), | |
| 234 | .gzip => |*gzip| gzip.deinit(), | |
| 235 | .zstd => |*zstd| zstd.deinit(), | |
| 236 | } | |
| 237 | ||
| 238 | if (!res.request.parser.done) { | |
| 239 | // If the response wasn't fully read, then we need to close the connection. | |
| 240 | res.connection.closing = true; | |
| 241 | } | |
| 242 | ||
| 243 | if (res.connection.closing) { | |
| 244 | res.connection.close(); | |
| 245 | ||
| 246 | if (res.request.parser.header_bytes_owned) { | |
| 247 | res.request.parser.header_bytes.deinit(res.server.allocator); | |
| 248 | } | |
| 249 | ||
| 250 | res.* = undefined; | |
| 251 | } else { | |
| 252 | res.request.parser.reset(); | |
| 253 | } | |
| 254 | } | |
| 255 | ||
| 256 | pub fn sendResponseHead(res: *Response) !void { | |
| 257 | var buffered = std.io.bufferedWriter(res.connection.writer()); | |
| 258 | const w = buffered.writer(); | |
| 259 | ||
| 260 | try w.writeAll(@tagName(res.headers.version)); | |
| 261 | try w.writeByte(' '); | |
| 262 | try w.print("{d}", .{@enumToInt(res.headers.status)}); | |
| 263 | try w.writeByte(' '); | |
| 264 | if (res.headers.reason) |reason| { | |
| 265 | try w.writeAll(reason); | |
| 266 | } else if (res.headers.status.phrase()) |phrase| { | |
| 267 | try w.writeAll(phrase); | |
| 268 | } | |
| 269 | ||
| 270 | if (res.headers.server) |server| { | |
| 271 | try w.writeAll("\r\nServer: "); | |
| 272 | try w.writeAll(server); | |
| 273 | } | |
| 274 | ||
| 275 | if (res.headers.connection == .close) { | |
| 276 | try w.writeAll("\r\nConnection: close"); | |
| 277 | } else { | |
| 278 | try w.writeAll("\r\nConnection: keep-alive"); | |
| 279 | } | |
| 280 | ||
| 281 | switch (res.headers.transfer_encoding) { | |
| 282 | .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), | |
| 283 | .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), | |
| 284 | .none => {}, | |
| 285 | } | |
| 286 | ||
| 287 | for (res.headers.custom) |header| { | |
| 288 | try w.writeAll("\r\n"); | |
| 289 | try w.writeAll(header.name); | |
| 290 | try w.writeAll(": "); | |
| 291 | try w.writeAll(header.value); | |
| 292 | } | |
| 293 | ||
| 294 | try w.writeAll("\r\n\r\n"); | |
| 295 | ||
| 296 | try buffered.flush(); | |
| 297 | } | |
| 298 | ||
| 299 | pub const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; | |
| 300 | ||
| 301 | pub const TransferReader = std.io.Reader(*Response, TransferReadError, transferRead); | |
| 302 | ||
| 303 | pub fn transferReader(res: *Response) TransferReader { | |
| 304 | return .{ .context = res }; | |
| 305 | } | |
| 306 | ||
| 307 | pub fn transferRead(res: *Response, buf: []u8) TransferReadError!usize { | |
| 308 | if (res.request.parser.isComplete()) return 0; | |
| 309 | ||
| 310 | var index: usize = 0; | |
| 311 | while (index == 0) { | |
| 312 | const amt = try res.request.parser.read(res.connection.reader(), buf[index..], false); | |
| 313 | if (amt == 0 and res.request.parser.isComplete()) break; | |
| 314 | index += amt; | |
| 315 | } | |
| 316 | ||
| 317 | return index; | |
| 318 | } | |
| 319 | ||
| 320 | pub const WaitForCompleteHeadError = Connection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Request.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported}; | |
| 321 | ||
| 322 | pub fn waitForCompleteHead(res: *Response) !void { | |
| 323 | try res.request.parser.waitForCompleteHead(res.connection.reader(), res.server.allocator); | |
| 324 | ||
| 325 | res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items); | |
| 326 | ||
| 327 | if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) { | |
| 328 | res.connection.closing = false; | |
| 329 | } else { | |
| 330 | res.connection.closing = true; | |
| 331 | } | |
| 332 | ||
| 333 | if (res.request.headers.transfer_encoding) |te| { | |
| 334 | switch (te) { | |
| 335 | .chunked => { | |
| 336 | res.request.parser.next_chunk_length = 0; | |
| 337 | res.request.parser.state = .chunk_head_size; | |
| 338 | }, | |
| 339 | } | |
| 340 | } else if (res.request.headers.content_length) |cl| { | |
| 341 | res.request.parser.next_chunk_length = cl; | |
| 342 | ||
| 343 | if (cl == 0) res.request.parser.done = true; | |
| 344 | } else { | |
| 345 | res.request.parser.done = true; | |
| 346 | } | |
| 347 | ||
| 348 | if (!res.request.parser.done) { | |
| 349 | if (res.request.headers.transfer_compression) |tc| switch (tc) { | |
| 350 | .compress => return error.CompressionNotSupported, | |
| 351 | .deflate => res.request.compression = .{ | |
| 352 | .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()), | |
| 353 | }, | |
| 354 | .gzip => res.request.compression = .{ | |
| 355 | .gzip = try std.compress.gzip.decompress(res.server.allocator, res.transferReader()), | |
| 356 | }, | |
| 357 | .zstd => res.request.compression = .{ | |
| 358 | .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()), | |
| 359 | }, | |
| 360 | }; | |
| 361 | } | |
| 362 | } | |
| 363 | ||
| 364 | pub const ReadError = DeflateDecompressor.Error || GzipDecompressor.Error || ZstdDecompressor.Error || WaitForCompleteHeadError; | |
| 365 | ||
| 366 | pub const Reader = std.io.Reader(*Response, ReadError, read); | |
| 367 | ||
| 368 | pub fn reader(res: *Response) Reader { | |
| 369 | return .{ .context = res }; | |
| 370 | } | |
| 371 | ||
| 372 | pub fn read(res: *Response, buffer: []u8) ReadError!usize { | |
| 373 | return switch (res.request.compression) { | |
| 374 | .deflate => |*deflate| try deflate.read(buffer), | |
| 375 | .gzip => |*gzip| try gzip.read(buffer), | |
| 376 | .zstd => |*zstd| try zstd.read(buffer), | |
| 377 | else => try res.transferRead(buffer), | |
| 378 | }; | |
| 379 | } | |
| 380 | ||
| 381 | pub fn readAll(res: *Response, buffer: []u8) !usize { | |
| 382 | var index: usize = 0; | |
| 383 | while (index < buffer.len) { | |
| 384 | const amt = try read(res, buffer[index..]); | |
| 385 | if (amt == 0) break; | |
| 386 | index += amt; | |
| 387 | } | |
| 388 | return index; | |
| 389 | } | |
| 390 | ||
| 391 | pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong }; | |
| 392 | ||
| 393 | pub const Writer = std.io.Writer(*Response, WriteError, write); | |
| 394 | ||
| 395 | pub fn writer(res: *Response) Writer { | |
| 396 | return .{ .context = res }; | |
| 397 | } | |
| 398 | ||
| 399 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. | |
| 400 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { | |
| 401 | switch (res.headers.transfer_encoding) { | |
| 402 | .chunked => { | |
| 403 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); | |
| 404 | try res.connection.writeAll(bytes); | |
| 405 | try res.connection.writeAll("\r\n"); | |
| 406 | ||
| 407 | return bytes.len; | |
| 408 | }, | |
| 409 | .content_length => |*len| { | |
| 410 | if (len.* < bytes.len) return error.MessageTooLong; | |
| 411 | ||
| 412 | const amt = try res.connection.write(bytes); | |
| 413 | len.* -= amt; | |
| 414 | return amt; | |
| 415 | }, | |
| 416 | .none => return error.NotWriteable, | |
| 417 | } | |
| 418 | } | |
| 419 | ||
| 420 | /// Finish the body of a request. This notifies the server that you have no more data to send. | |
| 421 | pub fn finish(res: *Response) !void { | |
| 422 | switch (res.headers.transfer_encoding) { | |
| 423 | .chunked => try res.connection.writeAll("0\r\n"), | |
| 424 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | |
| 425 | .none => {}, | |
| 426 | } | |
| 427 | } | |
| 428 | }; | |
| 429 | ||
| 430 | pub const RequestTransfer = union(enum) { | |
| 431 | content_length: u64, | |
| 432 | chunked: void, | |
| 433 | none: void, | |
| 434 | }; | |
| 435 | ||
| 436 | pub const Compression = union(enum) { | |
| 437 | deflate: DeflateDecompressor, | |
| 438 | gzip: GzipDecompressor, | |
| 439 | zstd: ZstdDecompressor, | |
| 440 | none: void, | |
| 441 | }; | |
| 442 | ||
| 443 | pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server { | |
| 444 | return .{ | |
| 445 | .allocator = allocator, | |
| 446 | .socket = net.StreamServer.init(options), | |
| 447 | }; | |
| 448 | } | |
| 449 | ||
| 450 | pub fn deinit(server: *Server) void { | |
| 451 | server.socket.deinit(); | |
| 452 | } | |
| 453 | ||
| 454 | pub const ListenError = std.os.SocketError || std.os.BindError || std.os.ListenError || std.os.SetSockOptError || std.os.GetSockNameError; | |
| 455 | ||
| 456 | pub fn listen(server: *Server, address: net.Address) !void { | |
| 457 | try server.socket.listen(address); | |
| 458 | } | |
| 459 | ||
| 460 | pub const AcceptError = net.StreamServer.AcceptError || Allocator.Error; | |
| 461 | ||
| 462 | pub const HeaderStrategy = union(enum) { | |
| 463 | /// In this case, the client's Allocator will be used to store the | |
| 464 | /// entire HTTP header. This value is the maximum total size of | |
| 465 | /// HTTP headers allowed, otherwise | |
| 466 | /// error.HttpHeadersExceededSizeLimit is returned from read(). | |
| 467 | dynamic: usize, | |
| 468 | /// This is used to store the entire HTTP header. If the HTTP | |
| 469 | /// header is too big to fit, `error.HttpHeadersExceededSizeLimit` | |
| 470 | /// is returned from read(). When this is used, `error.OutOfMemory` | |
| 471 | /// cannot be returned from `read()`. | |
| 472 | static: []u8, | |
| 473 | }; | |
| 474 | ||
| 475 | pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { | |
| 476 | const in = try server.socket.accept(); | |
| 477 | ||
| 478 | const res = try server.allocator.create(Response); | |
| 479 | res.* = .{ | |
| 480 | .server = server, | |
| 481 | .address = in.address, | |
| 482 | .connection = .{ | |
| 483 | .stream = in.stream, | |
| 484 | .protocol = .plain, | |
| 485 | }, | |
| 486 | .request = .{ | |
| 487 | .parser = switch (options) { | |
| 488 | .dynamic => |max| proto.HeadersParser.initDynamic(max), | |
| 489 | .static => |buf| proto.HeadersParser.initStatic(buf), | |
| 490 | }, | |
| 491 | }, | |
| 492 | }; | |
| 493 | ||
| 494 | return res; | |
| 495 | } |
lib/std/http/protocol.zig created+714| ... | ... | @@ -0,0 +1,714 @@ |
| 1 | const std = @import("std"); | |
| 2 | const testing = std.testing; | |
| 3 | const mem = std.mem; | |
| 4 | ||
| 5 | const assert = std.debug.assert; | |
| 6 | ||
| 7 | pub const State = enum { | |
| 8 | /// Begin header parsing states. | |
| 9 | invalid, | |
| 10 | start, | |
| 11 | seen_n, | |
| 12 | seen_r, | |
| 13 | seen_rn, | |
| 14 | seen_rnr, | |
| 15 | finished, | |
| 16 | /// Begin transfer-encoding: chunked parsing states. | |
| 17 | chunk_head_size, | |
| 18 | chunk_head_ext, | |
| 19 | chunk_head_r, | |
| 20 | chunk_data, | |
| 21 | chunk_data_suffix, | |
| 22 | chunk_data_suffix_r, | |
| 23 | ||
| 24 | pub fn isContent(self: State) bool { | |
| 25 | return switch (self) { | |
| 26 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false, | |
| 27 | .finished, .chunk_head_size, .chunk_head_ext, .chunk_head_r, .chunk_data, .chunk_data_suffix, .chunk_data_suffix_r => true, | |
| 28 | }; | |
| 29 | } | |
| 30 | }; | |
| 31 | ||
| 32 | const read_buffer_size = 0x4000; | |
| 33 | const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size); | |
| 34 | ||
| 35 | pub const HeadersParser = struct { | |
| 36 | state: State = .start, | |
| 37 | /// Wether or not `header_bytes` is allocated or was provided as a fixed buffer. | |
| 38 | header_bytes_owned: bool, | |
| 39 | /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`. | |
| 40 | /// Pointers into this buffer are not stable until after a message is complete. | |
| 41 | header_bytes: std.ArrayListUnmanaged(u8), | |
| 42 | /// The maximum allowed size of `header_bytes`. | |
| 43 | max_header_bytes: usize, | |
| 44 | next_chunk_length: u64 = 0, | |
| 45 | /// Wether this parser is done parsing a complete message. | |
| 46 | /// A message is only done when the entire payload has been read | |
| 47 | done: bool = false, | |
| 48 | ||
| 49 | read_buffer: [read_buffer_size]u8 = undefined, | |
| 50 | read_buffer_start: ReadBufferIndex = 0, | |
| 51 | read_buffer_len: ReadBufferIndex = 0, | |
| 52 | ||
| 53 | pub fn initDynamic(max: usize) HeadersParser { | |
| 54 | return .{ | |
| 55 | .header_bytes = .{}, | |
| 56 | .max_header_bytes = max, | |
| 57 | .header_bytes_owned = true, | |
| 58 | }; | |
| 59 | } | |
| 60 | ||
| 61 | pub fn initStatic(buf: []u8) HeadersParser { | |
| 62 | return .{ | |
| 63 | .header_bytes = .{ .items = buf[0..0], .capacity = buf.len }, | |
| 64 | .max_header_bytes = buf.len, | |
| 65 | .header_bytes_owned = false, | |
| 66 | }; | |
| 67 | } | |
| 68 | ||
| 69 | pub fn reset(r: *HeadersParser) void { | |
| 70 | r.header_bytes.clearRetainingCapacity(); | |
| 71 | ||
| 72 | r.* = .{ | |
| 73 | .header_bytes = r.header_bytes, | |
| 74 | .max_header_bytes = r.max_header_bytes, | |
| 75 | .header_bytes_owned = r.header_bytes_owned, | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | /// Returns how many bytes are part of HTTP headers. Always less than or | |
| 80 | /// equal to bytes.len. If the amount returned is less than bytes.len, it | |
| 81 | /// means the headers ended and the first byte after the double \r\n\r\n is | |
| 82 | /// located at `bytes[result]`. | |
| 83 | pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 { | |
| 84 | const vector_len = 16; | |
| 85 | const len = @truncate(u32, bytes.len); | |
| 86 | var index: u32 = 0; | |
| 87 | ||
| 88 | while (true) { | |
| 89 | switch (r.state) { | |
| 90 | .invalid => unreachable, | |
| 91 | .finished => return index, | |
| 92 | .start => switch (len - index) { | |
| 93 | 0 => return index, | |
| 94 | 1 => { | |
| 95 | switch (bytes[index]) { | |
| 96 | '\r' => r.state = .seen_r, | |
| 97 | '\n' => r.state = .seen_n, | |
| 98 | else => {}, | |
| 99 | } | |
| 100 | ||
| 101 | return index + 1; | |
| 102 | }, | |
| 103 | 2 => { | |
| 104 | const b16 = int16(bytes[index..][0..2]); | |
| 105 | const b8 = intShift(u8, b16); | |
| 106 | ||
| 107 | switch (b8) { | |
| 108 | '\r' => r.state = .seen_r, | |
| 109 | '\n' => r.state = .seen_n, | |
| 110 | else => {}, | |
| 111 | } | |
| 112 | ||
| 113 | switch (b16) { | |
| 114 | int16("\r\n") => r.state = .seen_rn, | |
| 115 | int16("\n\n") => r.state = .finished, | |
| 116 | else => {}, | |
| 117 | } | |
| 118 | ||
| 119 | return index + 2; | |
| 120 | }, | |
| 121 | 3 => { | |
| 122 | const b24 = int24(bytes[index..][0..3]); | |
| 123 | const b16 = intShift(u16, b24); | |
| 124 | const b8 = intShift(u8, b24); | |
| 125 | ||
| 126 | switch (b8) { | |
| 127 | '\r' => r.state = .seen_r, | |
| 128 | '\n' => r.state = .seen_n, | |
| 129 | else => {}, | |
| 130 | } | |
| 131 | ||
| 132 | switch (b16) { | |
| 133 | int16("\r\n") => r.state = .seen_rn, | |
| 134 | int16("\n\n") => r.state = .finished, | |
| 135 | else => {}, | |
| 136 | } | |
| 137 | ||
| 138 | switch (b24) { | |
| 139 | int24("\r\n\r") => r.state = .seen_rnr, | |
| 140 | else => {}, | |
| 141 | } | |
| 142 | ||
| 143 | return index + 3; | |
| 144 | }, | |
| 145 | 4...vector_len - 1 => { | |
| 146 | const b32 = int32(bytes[index..][0..4]); | |
| 147 | const b24 = intShift(u24, b32); | |
| 148 | const b16 = intShift(u16, b32); | |
| 149 | const b8 = intShift(u8, b32); | |
| 150 | ||
| 151 | switch (b8) { | |
| 152 | '\r' => r.state = .seen_r, | |
| 153 | '\n' => r.state = .seen_n, | |
| 154 | else => {}, | |
| 155 | } | |
| 156 | ||
| 157 | switch (b16) { | |
| 158 | int16("\r\n") => r.state = .seen_rn, | |
| 159 | int16("\n\n") => r.state = .finished, | |
| 160 | else => {}, | |
| 161 | } | |
| 162 | ||
| 163 | switch (b24) { | |
| 164 | int24("\r\n\r") => r.state = .seen_rnr, | |
| 165 | else => {}, | |
| 166 | } | |
| 167 | ||
| 168 | switch (b32) { | |
| 169 | int32("\r\n\r\n") => r.state = .finished, | |
| 170 | else => {}, | |
| 171 | } | |
| 172 | ||
| 173 | index += 4; | |
| 174 | continue; | |
| 175 | }, | |
| 176 | else => { | |
| 177 | const Vector = @Vector(vector_len, u8); | |
| 178 | // const BoolVector = @Vector(vector_len, bool); | |
| 179 | const BitVector = @Vector(vector_len, u1); | |
| 180 | const SizeVector = @Vector(vector_len, u8); | |
| 181 | ||
| 182 | const chunk = bytes[index..][0..vector_len]; | |
| 183 | const v: Vector = chunk.*; | |
| 184 | const matches_r = @bitCast(BitVector, v == @splat(vector_len, @as(u8, '\r'))); | |
| 185 | const matches_n = @bitCast(BitVector, v == @splat(vector_len, @as(u8, '\n'))); | |
| 186 | const matches_or: SizeVector = matches_r | matches_n; | |
| 187 | ||
| 188 | const matches = @reduce(.Add, matches_or); | |
| 189 | switch (matches) { | |
| 190 | 0 => {}, | |
| 191 | 1 => switch (chunk[vector_len - 1]) { | |
| 192 | '\r' => r.state = .seen_r, | |
| 193 | '\n' => r.state = .seen_n, | |
| 194 | else => {}, | |
| 195 | }, | |
| 196 | 2 => { | |
| 197 | const b16 = int16(chunk[vector_len - 2 ..][0..2]); | |
| 198 | const b8 = intShift(u8, b16); | |
| 199 | ||
| 200 | switch (b8) { | |
| 201 | '\r' => r.state = .seen_r, | |
| 202 | '\n' => r.state = .seen_n, | |
| 203 | else => {}, | |
| 204 | } | |
| 205 | ||
| 206 | switch (b16) { | |
| 207 | int16("\r\n") => r.state = .seen_rn, | |
| 208 | int16("\n\n") => r.state = .finished, | |
| 209 | else => {}, | |
| 210 | } | |
| 211 | }, | |
| 212 | 3 => { | |
| 213 | const b24 = int24(chunk[vector_len - 3 ..][0..3]); | |
| 214 | const b16 = intShift(u16, b24); | |
| 215 | const b8 = intShift(u8, b24); | |
| 216 | ||
| 217 | switch (b8) { | |
| 218 | '\r' => r.state = .seen_r, | |
| 219 | '\n' => r.state = .seen_n, | |
| 220 | else => {}, | |
| 221 | } | |
| 222 | ||
| 223 | switch (b16) { | |
| 224 | int16("\r\n") => r.state = .seen_rn, | |
| 225 | int16("\n\n") => r.state = .finished, | |
| 226 | else => {}, | |
| 227 | } | |
| 228 | ||
| 229 | switch (b24) { | |
| 230 | int24("\r\n\r") => r.state = .seen_rnr, | |
| 231 | else => {}, | |
| 232 | } | |
| 233 | }, | |
| 234 | 4...vector_len - 1 => { | |
| 235 | for (0..vector_len - 4) |i_usize| { | |
| 236 | const i = @truncate(u32, i_usize); | |
| 237 | ||
| 238 | const b32 = int32(chunk[i..][0..4]); | |
| 239 | const b16 = intShift(u16, b32); | |
| 240 | ||
| 241 | if (b32 == int32("\r\n\r\n")) { | |
| 242 | r.state = .finished; | |
| 243 | return index + i + 4; | |
| 244 | } else if (b16 == int16("\n\n")) { | |
| 245 | r.state = .finished; | |
| 246 | return index + i + 2; | |
| 247 | } | |
| 248 | } | |
| 249 | }, | |
| 250 | else => unreachable, | |
| 251 | } | |
| 252 | ||
| 253 | index += vector_len; | |
| 254 | continue; | |
| 255 | }, | |
| 256 | }, | |
| 257 | .seen_n => switch (len - index) { | |
| 258 | 0 => return index, | |
| 259 | else => { | |
| 260 | switch (bytes[index]) { | |
| 261 | '\n' => r.state = .finished, | |
| 262 | else => r.state = .start, | |
| 263 | } | |
| 264 | ||
| 265 | index += 1; | |
| 266 | continue; | |
| 267 | }, | |
| 268 | }, | |
| 269 | .seen_r => switch (len - index) { | |
| 270 | 0 => return index, | |
| 271 | 1 => { | |
| 272 | switch (bytes[index]) { | |
| 273 | '\n' => r.state = .seen_rn, | |
| 274 | '\r' => r.state = .seen_r, | |
| 275 | else => r.state = .start, | |
| 276 | } | |
| 277 | ||
| 278 | return index + 1; | |
| 279 | }, | |
| 280 | 2 => { | |
| 281 | const b16 = int16(bytes[index..][0..2]); | |
| 282 | const b8 = intShift(u8, b16); | |
| 283 | ||
| 284 | switch (b8) { | |
| 285 | '\r' => r.state = .seen_r, | |
| 286 | '\n' => r.state = .seen_rn, | |
| 287 | else => r.state = .start, | |
| 288 | } | |
| 289 | ||
| 290 | switch (b16) { | |
| 291 | int16("\r\n") => r.state = .seen_rn, | |
| 292 | int16("\n\n") => r.state = .finished, | |
| 293 | else => {}, | |
| 294 | } | |
| 295 | ||
| 296 | return index + 2; | |
| 297 | }, | |
| 298 | else => { | |
| 299 | const b24 = int24(bytes[index..][0..3]); | |
| 300 | const b16 = intShift(u16, b24); | |
| 301 | const b8 = intShift(u8, b24); | |
| 302 | ||
| 303 | switch (b8) { | |
| 304 | '\r' => r.state = .seen_r, | |
| 305 | '\n' => r.state = .seen_n, | |
| 306 | else => r.state = .start, | |
| 307 | } | |
| 308 | ||
| 309 | switch (b16) { | |
| 310 | int16("\r\n") => r.state = .seen_rn, | |
| 311 | int16("\n\n") => r.state = .finished, | |
| 312 | else => {}, | |
| 313 | } | |
| 314 | ||
| 315 | switch (b24) { | |
| 316 | int24("\n\r\n") => r.state = .finished, | |
| 317 | else => {}, | |
| 318 | } | |
| 319 | ||
| 320 | index += 3; | |
| 321 | continue; | |
| 322 | }, | |
| 323 | }, | |
| 324 | .seen_rn => switch (len - index) { | |
| 325 | 0 => return index, | |
| 326 | 1 => { | |
| 327 | switch (bytes[index]) { | |
| 328 | '\r' => r.state = .seen_rnr, | |
| 329 | '\n' => r.state = .seen_n, | |
| 330 | else => r.state = .start, | |
| 331 | } | |
| 332 | ||
| 333 | return index + 1; | |
| 334 | }, | |
| 335 | else => { | |
| 336 | const b16 = int16(bytes[index..][0..2]); | |
| 337 | const b8 = intShift(u8, b16); | |
| 338 | ||
| 339 | switch (b8) { | |
| 340 | '\r' => r.state = .seen_rnr, | |
| 341 | '\n' => r.state = .seen_n, | |
| 342 | else => r.state = .start, | |
| 343 | } | |
| 344 | ||
| 345 | switch (b16) { | |
| 346 | int16("\r\n") => r.state = .finished, | |
| 347 | int16("\n\n") => r.state = .finished, | |
| 348 | else => {}, | |
| 349 | } | |
| 350 | ||
| 351 | index += 2; | |
| 352 | continue; | |
| 353 | }, | |
| 354 | }, | |
| 355 | .seen_rnr => switch (len - index) { | |
| 356 | 0 => return index, | |
| 357 | else => { | |
| 358 | switch (bytes[index]) { | |
| 359 | '\n' => r.state = .finished, | |
| 360 | else => r.state = .start, | |
| 361 | } | |
| 362 | ||
| 363 | index += 1; | |
| 364 | continue; | |
| 365 | }, | |
| 366 | }, | |
| 367 | .chunk_head_size => unreachable, | |
| 368 | .chunk_head_ext => unreachable, | |
| 369 | .chunk_head_r => unreachable, | |
| 370 | .chunk_data => unreachable, | |
| 371 | .chunk_data_suffix => unreachable, | |
| 372 | .chunk_data_suffix_r => unreachable, | |
| 373 | } | |
| 374 | ||
| 375 | return index; | |
| 376 | } | |
| 377 | } | |
| 378 | ||
| 379 | pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 { | |
| 380 | const len = @truncate(u32, bytes.len); | |
| 381 | ||
| 382 | for (bytes[0..], 0..) |c, i| { | |
| 383 | const index = @intCast(u32, i); | |
| 384 | switch (r.state) { | |
| 385 | .chunk_data_suffix => switch (c) { | |
| 386 | '\r' => r.state = .chunk_data_suffix_r, | |
| 387 | '\n' => r.state = .chunk_head_size, | |
| 388 | else => { | |
| 389 | r.state = .invalid; | |
| 390 | return index; | |
| 391 | }, | |
| 392 | }, | |
| 393 | .chunk_data_suffix_r => switch (c) { | |
| 394 | '\n' => r.state = .chunk_head_size, | |
| 395 | else => { | |
| 396 | r.state = .invalid; | |
| 397 | return index; | |
| 398 | }, | |
| 399 | }, | |
| 400 | .chunk_head_size => { | |
| 401 | const digit = switch (c) { | |
| 402 | '0'...'9' => |b| b - '0', | |
| 403 | 'A'...'Z' => |b| b - 'A' + 10, | |
| 404 | 'a'...'z' => |b| b - 'a' + 10, | |
| 405 | '\r' => { | |
| 406 | r.state = .chunk_head_r; | |
| 407 | continue; | |
| 408 | }, | |
| 409 | '\n' => { | |
| 410 | r.state = .chunk_data; | |
| 411 | return index + 1; | |
| 412 | }, | |
| 413 | else => { | |
| 414 | r.state = .chunk_head_ext; | |
| 415 | continue; | |
| 416 | }, | |
| 417 | }; | |
| 418 | ||
| 419 | const new_len = r.next_chunk_length *% 16 +% digit; | |
| 420 | if (new_len <= r.next_chunk_length and r.next_chunk_length != 0) { | |
| 421 | r.state = .invalid; | |
| 422 | return index; | |
| 423 | } | |
| 424 | ||
| 425 | r.next_chunk_length = new_len; | |
| 426 | }, | |
| 427 | .chunk_head_ext => switch (c) { | |
| 428 | '\r' => r.state = .chunk_head_r, | |
| 429 | '\n' => { | |
| 430 | r.state = .chunk_data; | |
| 431 | return index + 1; | |
| 432 | }, | |
| 433 | else => continue, | |
| 434 | }, | |
| 435 | .chunk_head_r => switch (c) { | |
| 436 | '\n' => { | |
| 437 | r.state = .chunk_data; | |
| 438 | return index + 1; | |
| 439 | }, | |
| 440 | else => { | |
| 441 | r.state = .invalid; | |
| 442 | return index; | |
| 443 | }, | |
| 444 | }, | |
| 445 | else => unreachable, | |
| 446 | } | |
| 447 | } | |
| 448 | ||
| 449 | return len; | |
| 450 | } | |
| 451 | ||
| 452 | /// Returns whether or not the parser has finished parsing a complete message. A message is only complete after the | |
| 453 | /// entire body has been read and any trailing headers have been parsed. | |
| 454 | pub fn isComplete(r: *HeadersParser) bool { | |
| 455 | return r.done and r.state == .finished; | |
| 456 | } | |
| 457 | ||
| 458 | pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit}; | |
| 459 | ||
| 460 | /// Pumps `in` bytes into the parser. Returns the number of bytes consumed. This function will return 0 if the parser | |
| 461 | /// is not in a state to parse more headers. | |
| 462 | pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 { | |
| 463 | if (r.state.isContent()) return 0; | |
| 464 | ||
| 465 | const i = r.findHeadersEnd(in); | |
| 466 | const data = in[0..i]; | |
| 467 | if (r.header_bytes.items.len + data.len > r.max_header_bytes) { | |
| 468 | return error.HttpHeadersExceededSizeLimit; | |
| 469 | } else { | |
| 470 | if (r.header_bytes_owned) try r.header_bytes.ensureUnusedCapacity(allocator, data.len); | |
| 471 | ||
| 472 | r.header_bytes.appendSliceAssumeCapacity(data); | |
| 473 | } | |
| 474 | ||
| 475 | return i; | |
| 476 | } | |
| 477 | ||
| 478 | /// Set of errors that `waitForCompleteHead` can throw except any errors inherited by `reader` | |
| 479 | pub const WaitForCompleteHeadError = CheckCompleteHeadError || error{UnexpectedEndOfStream}; | |
| 480 | ||
| 481 | /// Waits for the complete head to be available. This function will continue trying to read until the head is complete | |
| 482 | /// or an error occurs. | |
| 483 | pub fn waitForCompleteHead(r: *HeadersParser, reader: anytype, allocator: std.mem.Allocator) !void { | |
| 484 | if (r.state.isContent()) return; | |
| 485 | ||
| 486 | while (true) { | |
| 487 | if (r.read_buffer_start == r.read_buffer_len) { | |
| 488 | const nread = try reader.read(r.read_buffer[0..]); | |
| 489 | if (nread == 0) return error.UnexpectedEndOfStream; | |
| 490 | ||
| 491 | r.read_buffer_start = 0; | |
| 492 | r.read_buffer_len = @intCast(ReadBufferIndex, nread); | |
| 493 | } | |
| 494 | ||
| 495 | const amt = try r.checkCompleteHead(allocator, r.read_buffer[r.read_buffer_start..r.read_buffer_len]); | |
| 496 | r.read_buffer_start += @intCast(ReadBufferIndex, amt); | |
| 497 | ||
| 498 | if (amt != 0) return; | |
| 499 | } | |
| 500 | } | |
| 501 | ||
| 502 | pub const ReadError = error{ | |
| 503 | UnexpectedEndOfStream, | |
| 504 | HttpHeadersExceededSizeLimit, | |
| 505 | HttpChunkInvalid, | |
| 506 | }; | |
| 507 | ||
| 508 | /// Reads the body of the message into `buffer`. If `skip` is true, the buffer will be unused and the body will be | |
| 509 | /// skipped. Returns the number of bytes placed in the buffer. | |
| 510 | pub fn read(r: *HeadersParser, reader: anytype, buffer: []u8, skip: bool) !usize { | |
| 511 | assert(r.state.isContent()); | |
| 512 | if (r.done) return 0; | |
| 513 | ||
| 514 | if (r.read_buffer_start == r.read_buffer_len) { | |
| 515 | const nread = try reader.read(r.read_buffer[0..]); | |
| 516 | if (nread == 0) return error.UnexpectedEndOfStream; | |
| 517 | ||
| 518 | r.read_buffer_start = 0; | |
| 519 | r.read_buffer_len = @intCast(ReadBufferIndex, nread); | |
| 520 | } | |
| 521 | ||
| 522 | var out_index: usize = 0; | |
| 523 | while (true) { | |
| 524 | switch (r.state) { | |
| 525 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => unreachable, | |
| 526 | .finished => { | |
| 527 | const buf_avail = r.read_buffer_len - r.read_buffer_start; | |
| 528 | const data_avail = r.next_chunk_length; | |
| 529 | const out_avail = buffer.len; | |
| 530 | ||
| 531 | // TODO https://github.com/ziglang/zig/issues/14039 | |
| 532 | const read_available = @intCast(usize, @min(buf_avail, data_avail)); | |
| 533 | if (skip) { | |
| 534 | r.next_chunk_length -= read_available; | |
| 535 | r.read_buffer_start += @intCast(ReadBufferIndex, read_available); | |
| 536 | } else { | |
| 537 | const can_read = @min(read_available, out_avail); | |
| 538 | r.next_chunk_length -= can_read; | |
| 539 | ||
| 540 | mem.copy(u8, buffer[out_index..], r.read_buffer[r.read_buffer_start..][0..can_read]); | |
| 541 | r.read_buffer_start += @intCast(ReadBufferIndex, can_read); | |
| 542 | out_index += can_read; | |
| 543 | } | |
| 544 | ||
| 545 | if (r.next_chunk_length == 0) r.done = true; | |
| 546 | ||
| 547 | return out_index; | |
| 548 | }, | |
| 549 | .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { | |
| 550 | const i = r.findChunkedLen(r.read_buffer[r.read_buffer_start..r.read_buffer_len]); | |
| 551 | r.read_buffer_start += @intCast(ReadBufferIndex, i); | |
| 552 | ||
| 553 | switch (r.state) { | |
| 554 | .invalid => return error.HttpChunkInvalid, | |
| 555 | .chunk_data => if (r.next_chunk_length == 0) { | |
| 556 | // The trailer section is formatted identically to the header section. | |
| 557 | r.state = .seen_rn; | |
| 558 | r.done = true; | |
| 559 | ||
| 560 | return out_index; | |
| 561 | }, | |
| 562 | else => return out_index, | |
| 563 | } | |
| 564 | ||
| 565 | continue; | |
| 566 | }, | |
| 567 | .chunk_data => { | |
| 568 | const buf_avail = r.read_buffer_len - r.read_buffer_start; | |
| 569 | const data_avail = r.next_chunk_length; | |
| 570 | const out_avail = buffer.len; | |
| 571 | ||
| 572 | // TODO https://github.com/ziglang/zig/issues/14039 | |
| 573 | const read_available = @intCast(usize, @min(buf_avail, data_avail)); | |
| 574 | if (skip) { | |
| 575 | r.next_chunk_length -= read_available; | |
| 576 | r.read_buffer_start += @intCast(ReadBufferIndex, read_available); | |
| 577 | } else { | |
| 578 | const can_read = @min(read_available, out_avail); | |
| 579 | r.next_chunk_length -= can_read; | |
| 580 | ||
| 581 | mem.copy(u8, buffer[out_index..], r.read_buffer[r.read_buffer_start..][0..can_read]); | |
| 582 | r.read_buffer_start += @intCast(ReadBufferIndex, can_read); | |
| 583 | out_index += can_read; | |
| 584 | } | |
| 585 | ||
| 586 | if (r.next_chunk_length == 0) { | |
| 587 | r.state = .chunk_data_suffix; | |
| 588 | continue; | |
| 589 | } | |
| 590 | ||
| 591 | return out_index; | |
| 592 | }, | |
| 593 | } | |
| 594 | } | |
| 595 | } | |
| 596 | }; | |
| 597 | ||
| 598 | inline fn int16(array: *const [2]u8) u16 { | |
| 599 | return @bitCast(u16, array.*); | |
| 600 | } | |
| 601 | ||
| 602 | inline fn int24(array: *const [3]u8) u24 { | |
| 603 | return @bitCast(u24, array.*); | |
| 604 | } | |
| 605 | ||
| 606 | inline fn int32(array: *const [4]u8) u32 { | |
| 607 | return @bitCast(u32, array.*); | |
| 608 | } | |
| 609 | ||
| 610 | inline fn intShift(comptime T: type, x: anytype) T { | |
| 611 | switch (@import("builtin").cpu.arch.endian()) { | |
| 612 | .Little => return @truncate(T, x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T))), | |
| 613 | .Big => return @truncate(T, x), | |
| 614 | } | |
| 615 | } | |
| 616 | ||
| 617 | test "HeadersParser.findHeadersEnd" { | |
| 618 | var r: HeadersParser = undefined; | |
| 619 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello"; | |
| 620 | ||
| 621 | for (0..36) |i| { | |
| 622 | r = HeadersParser.initDynamic(0); | |
| 623 | try std.testing.expectEqual(@intCast(u32, i), r.findHeadersEnd(data[0..i])); | |
| 624 | try std.testing.expectEqual(@intCast(u32, 35 - i), r.findHeadersEnd(data[i..])); | |
| 625 | } | |
| 626 | } | |
| 627 | ||
| 628 | test "HeadersParser.findChunkedLen" { | |
| 629 | var r: HeadersParser = undefined; | |
| 630 | const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n"; | |
| 631 | ||
| 632 | r = HeadersParser.initDynamic(0); | |
| 633 | r.state = .chunk_head_size; | |
| 634 | r.next_chunk_length = 0; | |
| 635 | ||
| 636 | const first = r.findChunkedLen(data[0..]); | |
| 637 | try testing.expectEqual(@as(u32, 4), first); | |
| 638 | try testing.expectEqual(@as(u64, 0xff), r.next_chunk_length); | |
| 639 | try testing.expectEqual(State.chunk_data, r.state); | |
| 640 | r.state = .chunk_head_size; | |
| 641 | r.next_chunk_length = 0; | |
| 642 | ||
| 643 | const second = r.findChunkedLen(data[first..]); | |
| 644 | try testing.expectEqual(@as(u32, 13), second); | |
| 645 | try testing.expectEqual(@as(u64, 0xf0f000), r.next_chunk_length); | |
| 646 | try testing.expectEqual(State.chunk_data, r.state); | |
| 647 | r.state = .chunk_head_size; | |
| 648 | r.next_chunk_length = 0; | |
| 649 | ||
| 650 | const third = r.findChunkedLen(data[first + second ..]); | |
| 651 | try testing.expectEqual(@as(u32, 3), third); | |
| 652 | try testing.expectEqual(@as(u64, 0), r.next_chunk_length); | |
| 653 | try testing.expectEqual(State.chunk_data, r.state); | |
| 654 | r.state = .chunk_head_size; | |
| 655 | r.next_chunk_length = 0; | |
| 656 | ||
| 657 | const fourth = r.findChunkedLen(data[first + second + third ..]); | |
| 658 | try testing.expectEqual(@as(u32, 16), fourth); | |
| 659 | try testing.expectEqual(@as(u64, 0xffffffffffffffff), r.next_chunk_length); | |
| 660 | try testing.expectEqual(State.invalid, r.state); | |
| 661 | } | |
| 662 | ||
| 663 | test "HeadersParser.read length" { | |
| 664 | var r = HeadersParser.initDynamic(256); | |
| 665 | defer r.header_bytes.deinit(std.testing.allocator); | |
| 666 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello"; | |
| 667 | var fbs = std.io.fixedBufferStream(data); | |
| 668 | ||
| 669 | try r.waitForCompleteHead(fbs.reader(), std.testing.allocator); | |
| 670 | var buf: [8]u8 = undefined; | |
| 671 | ||
| 672 | r.next_chunk_length = 5; | |
| 673 | const len = try r.read(fbs.reader(), &buf, false); | |
| 674 | try std.testing.expectEqual(@as(usize, 5), len); | |
| 675 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | |
| 676 | ||
| 677 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.header_bytes.items); | |
| 678 | } | |
| 679 | ||
| 680 | test "HeadersParser.read chunked" { | |
| 681 | var r = HeadersParser.initDynamic(256); | |
| 682 | defer r.header_bytes.deinit(std.testing.allocator); | |
| 683 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n"; | |
| 684 | var fbs = std.io.fixedBufferStream(data); | |
| 685 | ||
| 686 | try r.waitForCompleteHead(fbs.reader(), std.testing.allocator); | |
| 687 | var buf: [8]u8 = undefined; | |
| 688 | ||
| 689 | r.state = .chunk_head_size; | |
| 690 | const len = try r.read(fbs.reader(), &buf, false); | |
| 691 | try std.testing.expectEqual(@as(usize, 5), len); | |
| 692 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | |
| 693 | ||
| 694 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.header_bytes.items); | |
| 695 | } | |
| 696 | ||
| 697 | test "HeadersParser.read chunked trailer" { | |
| 698 | var r = HeadersParser.initDynamic(256); | |
| 699 | defer r.header_bytes.deinit(std.testing.allocator); | |
| 700 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n"; | |
| 701 | var fbs = std.io.fixedBufferStream(data); | |
| 702 | ||
| 703 | try r.waitForCompleteHead(fbs.reader(), std.testing.allocator); | |
| 704 | var buf: [8]u8 = undefined; | |
| 705 | ||
| 706 | r.state = .chunk_head_size; | |
| 707 | const len = try r.read(fbs.reader(), &buf, false); | |
| 708 | try std.testing.expectEqual(@as(usize, 5), len); | |
| 709 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | |
| 710 | ||
| 711 | try r.waitForCompleteHead(fbs.reader(), std.testing.allocator); | |
| 712 | ||
| 713 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.header_bytes.items); | |
| 714 | } |