| 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,4 +1,6 @@ |
| 1 | pub const Client = @import("http/Client.zig"); | 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 | pub const Version = enum { | 5 | pub const Version = enum { |
| 4 | @"HTTP/1.0", | 6 | @"HTTP/1.0", |
lib/std/http/Client.zig+414-24| ... | @@ -1,23 +1,19 @@ | ... | @@ -1,23 +1,19 @@ |
| 1 | //! TODO: send connection: keep-alive and LRU cache a configurable number of | 1 | //! Connecting and opening requests are threadsafe. Individual requests are not. |
| 2 | //! open connections to skip DNS and TLS handshake for subsequent requests. | ||
| 3 | //! | ||
| 4 | //! This API is *not* thread safe. | ||
| 5 | 2 | ||
| 6 | const std = @import("../std.zig"); | 3 | const std = @import("../std.zig"); |
| 7 | const mem = std.mem; | 4 | const testing = std.testing; |
| 8 | const assert = std.debug.assert; | ||
| 9 | const http = std.http; | 5 | const http = std.http; |
| 6 | const mem = std.mem; | ||
| 10 | const net = std.net; | 7 | const net = std.net; |
| 11 | const Client = @This(); | ||
| 12 | const Uri = std.Uri; | 8 | const Uri = std.Uri; |
| 13 | const Allocator = std.mem.Allocator; | 9 | const Allocator = mem.Allocator; |
| 14 | const testing = std.testing; | 10 | const assert = std.debug.assert; |
| 15 | 11 | ||
| 16 | pub const Request = @import("Client/Request.zig"); | 12 | const Client = @This(); |
| 17 | pub const Response = @import("Client/Response.zig"); | 13 | const proto = @import("protocol.zig"); |
| 18 | 14 | ||
| 19 | pub const default_connection_pool_size = 32; | 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 | /// Used for tcpConnectToHost and storing HTTP headers when an externally | 18 | /// Used for tcpConnectToHost and storing HTTP headers when an externally |
| 23 | /// managed buffer is not provided. | 19 | /// managed buffer is not provided. |
| ... | @@ -43,7 +39,7 @@ pub const ConnectionPool = struct { | ... | @@ -43,7 +39,7 @@ pub const ConnectionPool = struct { |
| 43 | used: Queue = .{}, | 39 | used: Queue = .{}, |
| 44 | free: Queue = .{}, | 40 | free: Queue = .{}, |
| 45 | free_len: usize = 0, | 41 | free_len: usize = 0, |
| 46 | free_size: usize = default_connection_pool_size, | 42 | free_size: usize = connection_pool_size, |
| 47 | 43 | ||
| 48 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. | 44 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. |
| 49 | /// If no connection is found, null is returned. | 45 | /// If no connection is found, null is returned. |
| ... | @@ -55,7 +51,7 @@ pub const ConnectionPool = struct { | ... | @@ -55,7 +51,7 @@ pub const ConnectionPool = struct { |
| 55 | while (next) |node| : (next = node.prev) { | 51 | while (next) |node| : (next = node.prev) { |
| 56 | if ((node.data.protocol == .tls) != criteria.is_tls) continue; | 52 | if ((node.data.protocol == .tls) != criteria.is_tls) continue; |
| 57 | if (node.data.port != criteria.port) continue; | 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 | pool.acquireUnsafe(node); | 56 | pool.acquireUnsafe(node); |
| 61 | return node; | 57 | return node; |
| ... | @@ -137,9 +133,9 @@ pub const ConnectionPool = struct { | ... | @@ -137,9 +133,9 @@ pub const ConnectionPool = struct { |
| 137 | } | 133 | } |
| 138 | }; | 134 | }; |
| 139 | 135 | ||
| 140 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw); | 136 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader); |
| 141 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw); | 137 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader); |
| 142 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.ReaderRaw, .{}); | 138 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{}); |
| 143 | 139 | ||
| 144 | pub const Connection = struct { | 140 | pub const Connection = struct { |
| 145 | stream: net.Stream, | 141 | stream: net.Stream, |
| ... | @@ -220,6 +216,379 @@ pub const Connection = struct { | ... | @@ -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 | pub fn deinit(client: *Client) void { | 592 | pub fn deinit(client: *Client) void { |
| 224 | client.connection_pool.deinit(client); | 593 | client.connection_pool.deinit(client); |
| 225 | 594 | ||
| ... | @@ -227,7 +596,7 @@ pub fn deinit(client: *Client) void { | ... | @@ -227,7 +596,7 @@ pub fn deinit(client: *Client) void { |
| 227 | client.* = undefined; | 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 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { | 601 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| 233 | if (client.connection_pool.findConnection(.{ | 602 | if (client.connection_pool.findConnection(.{ |
| ... | @@ -276,7 +645,26 @@ pub const RequestError = ConnectError || Connection.WriteError || error{ | ... | @@ -276,7 +645,26 @@ pub const RequestError = ConnectError || Connection.WriteError || error{ |
| 276 | EndOfStream, | 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 | const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http")) | 668 | const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http")) |
| 281 | .plain | 669 | .plain |
| 282 | else if (mem.eql(u8, uri.scheme, "https")) | 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,14 +692,15 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 304 | var req: Request = .{ | 692 | var req: Request = .{ |
| 305 | .uri = uri, | 693 | .uri = uri, |
| 306 | .client = client, | 694 | .client = client, |
| 307 | .headers = headers, | ||
| 308 | .connection = try client.connect(host, port, protocol), | 695 | .connection = try client.connect(host, port, protocol), |
| 696 | .headers = headers, | ||
| 309 | .redirects_left = options.max_redirects, | 697 | .redirects_left = options.max_redirects, |
| 310 | .handle_redirects = options.handle_redirects, | 698 | .handle_redirects = options.handle_redirects, |
| 311 | .compression_init = false, | 699 | .response = .{ |
| 312 | .response = switch (options.header_strategy) { | 700 | .parser = switch (options.header_strategy) { |
| 313 | .dynamic => |max| Response.initDynamic(max), | 701 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 314 | .static => |buf| Response.initStatic(buf), | 702 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| 703 | }, | ||
| 315 | }, | 704 | }, |
| 316 | .arena = undefined, | 705 | .arena = undefined, |
| 317 | }; | 706 | }; |
| ... | @@ -358,6 +747,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req | ... | @@ -358,6 +747,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 358 | try writer.writeAll("\r\nConnection: keep-alive"); | 747 | try writer.writeAll("\r\nConnection: keep-alive"); |
| 359 | } | 748 | } |
| 360 | try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); | 749 | try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); |
| 750 | try writer.writeAll("\r\nTE: trailers, gzip, deflate"); | ||
| 361 | 751 | ||
| 362 | switch (headers.transfer_encoding) { | 752 | switch (headers.transfer_encoding) { |
| 363 | .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"), | 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,6 +4,7 @@ const mem = std.mem; |
| 4 | const testing = std.testing; | 4 | const testing = std.testing; |
| 5 | const assert = std.debug.assert; | 5 | const assert = std.debug.assert; |
| 6 | 6 | ||
| 7 | const protocol = @import("../protocol.zig"); | ||
| 7 | const Client = @import("../Client.zig"); | 8 | const Client = @import("../Client.zig"); |
| 8 | const Response = @This(); | 9 | const Response = @This(); |
| 9 | 10 | ||
| ... | @@ -169,14 +170,6 @@ pub const Headers = struct { | ... | @@ -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 | inline fn int64(array: *const [8]u8) u64 { | 173 | inline fn int64(array: *const [8]u8) u64 { |
| 181 | return @bitCast(u64, array.*); | 174 | return @bitCast(u64, array.*); |
| 182 | } | 175 | } |
| ... | @@ -226,232 +219,6 @@ pub fn initStatic(buf: []u8) Response { | ... | @@ -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 | fn parseInt3(nnn: @Vector(3, u8)) u10 { | 222 | fn parseInt3(nnn: @Vector(3, u8)) u10 { |
| 456 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | 223 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; |
| 457 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | 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 | } | ||