| author | |
| committer | |
| log | 134294230a08d531afd0a6d823ae3046b1699b0f |
| tree | 771bbcec0a45f293bba1ae3dd8b8fea4ddeb0df9 |
| parent | 96533b1289f210b415e12d4cf5bbac466279c2e5 |
| signature | Commit is signed but in an unrecognized format. |
5 files changed, 733 insertions(+), 318 deletions(-)
lib/std/http.zig+4-5| ... | @@ -1,6 +1,10 @@ | ... | @@ -1,6 +1,10 @@ |
| 1 | pub const Client = @import("http/Client.zig"); | 1 | pub const Client = @import("http/Client.zig"); |
| 2 | pub const Server = @import("http/Server.zig"); | 2 | pub const Server = @import("http/Server.zig"); |
| 3 | pub const protocol = @import("http/protocol.zig"); | 3 | pub const protocol = @import("http/protocol.zig"); |
| 4 | const headers = @import("http/Headers.zig"); | ||
| 5 | |||
| 6 | pub const Headers = headers.Headers; | ||
| 7 | pub const Header = headers.HeaderEntry; | ||
| 4 | 8 | ||
| 5 | pub const Version = enum { | 9 | pub const Version = enum { |
| 6 | @"HTTP/1.0", | 10 | @"HTTP/1.0", |
| ... | @@ -265,11 +269,6 @@ pub const Connection = enum { | ... | @@ -265,11 +269,6 @@ pub const Connection = enum { |
| 265 | close, | 269 | close, |
| 266 | }; | 270 | }; |
| 267 | 271 | ||
| 268 | pub const Header = struct { | ||
| 269 | name: []const u8, | ||
| 270 | value: []const u8, | ||
| 271 | }; | ||
| 272 | |||
| 273 | const std = @import("std.zig"); | 272 | const std = @import("std.zig"); |
| 274 | 273 | ||
| 275 | test { | 274 | test { |
lib/std/http/Client.zig+190-166| ... | @@ -348,140 +348,125 @@ pub const Compression = union(enum) { | ... | @@ -348,140 +348,125 @@ pub const Compression = union(enum) { |
| 348 | 348 | ||
| 349 | /// A HTTP response originating from a server. | 349 | /// A HTTP response originating from a server. |
| 350 | pub const Response = struct { | 350 | pub const Response = struct { |
| 351 | pub const Headers = struct { | 351 | pub const ParseError = Allocator.Error || error{ |
| 352 | status: http.Status, | 352 | ShortHttpStatusLine, |
| 353 | version: http.Version, | 353 | BadHttpVersion, |
| 354 | location: ?[]const u8 = null, | 354 | HttpHeadersInvalid, |
| 355 | content_length: ?u64 = null, | 355 | HttpHeaderContinuationsUnsupported, |
| 356 | transfer_encoding: ?http.TransferEncoding = null, | 356 | HttpTransferEncodingUnsupported, |
| 357 | transfer_compression: ?http.ContentEncoding = null, | 357 | HttpConnectionHeaderUnsupported, |
| 358 | connection: http.Connection = .close, | 358 | InvalidContentLength, |
| 359 | upgrade: ?[]const u8 = null, | 359 | CompressionNotSupported, |
| 360 | 360 | }; | |
| 361 | pub const ParseError = error{ | ||
| 362 | ShortHttpStatusLine, | ||
| 363 | BadHttpVersion, | ||
| 364 | HttpHeadersInvalid, | ||
| 365 | HttpHeaderContinuationsUnsupported, | ||
| 366 | HttpTransferEncodingUnsupported, | ||
| 367 | HttpConnectionHeaderUnsupported, | ||
| 368 | InvalidContentLength, | ||
| 369 | CompressionNotSupported, | ||
| 370 | }; | ||
| 371 | |||
| 372 | pub fn parse(bytes: []const u8) ParseError!Headers { | ||
| 373 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | ||
| 374 | |||
| 375 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | ||
| 376 | if (first_line.len < 12) | ||
| 377 | return error.ShortHttpStatusLine; | ||
| 378 | |||
| 379 | const version: http.Version = switch (int64(first_line[0..8])) { | ||
| 380 | int64("HTTP/1.0") => .@"HTTP/1.0", | ||
| 381 | int64("HTTP/1.1") => .@"HTTP/1.1", | ||
| 382 | else => return error.BadHttpVersion, | ||
| 383 | }; | ||
| 384 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | ||
| 385 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); | ||
| 386 | |||
| 387 | var headers: Headers = .{ | ||
| 388 | .version = version, | ||
| 389 | .status = status, | ||
| 390 | }; | ||
| 391 | |||
| 392 | while (it.next()) |line| { | ||
| 393 | if (line.len == 0) return error.HttpHeadersInvalid; | ||
| 394 | switch (line[0]) { | ||
| 395 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | ||
| 396 | else => {}, | ||
| 397 | } | ||
| 398 | 361 | ||
| 399 | var line_it = mem.tokenize(u8, line, ": "); | 362 | pub fn parse(res: *Response, bytes: []const u8) ParseError!void { |
| 400 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | 363 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| 401 | const header_value = line_it.rest(); | ||
| 402 | if (std.ascii.eqlIgnoreCase(header_name, "location")) { | ||
| 403 | if (headers.location != null) return error.HttpHeadersInvalid; | ||
| 404 | headers.location = header_value; | ||
| 405 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | ||
| 406 | if (headers.content_length != null) return error.HttpHeadersInvalid; | ||
| 407 | headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | ||
| 408 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | ||
| 409 | // Transfer-Encoding: second, first | ||
| 410 | // Transfer-Encoding: deflate, chunked | ||
| 411 | var iter = mem.splitBackwards(u8, header_value, ","); | ||
| 412 | |||
| 413 | if (iter.next()) |first| { | ||
| 414 | const trimmed = mem.trim(u8, first, " "); | ||
| 415 | |||
| 416 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | ||
| 417 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | ||
| 418 | headers.transfer_encoding = te; | ||
| 419 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | ||
| 420 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 421 | headers.transfer_compression = ce; | ||
| 422 | } else { | ||
| 423 | return error.HttpTransferEncodingUnsupported; | ||
| 424 | } | ||
| 425 | } | ||
| 426 | 364 | ||
| 427 | if (iter.next()) |second| { | 365 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 428 | if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | 366 | if (first_line.len < 12) |
| 367 | return error.ShortHttpStatusLine; | ||
| 429 | 368 | ||
| 430 | const trimmed = mem.trim(u8, second, " "); | 369 | const version: http.Version = switch (int64(first_line[0..8])) { |
| 370 | int64("HTTP/1.0") => .@"HTTP/1.0", | ||
| 371 | int64("HTTP/1.1") => .@"HTTP/1.1", | ||
| 372 | else => return error.BadHttpVersion, | ||
| 373 | }; | ||
| 374 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | ||
| 375 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); | ||
| 376 | const reason = mem.trimLeft(u8, first_line[12..], " "); | ||
| 377 | |||
| 378 | res.version = version; | ||
| 379 | res.status = status; | ||
| 380 | res.reason = reason; | ||
| 381 | |||
| 382 | while (it.next()) |line| { | ||
| 383 | if (line.len == 0) return error.HttpHeadersInvalid; | ||
| 384 | switch (line[0]) { | ||
| 385 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | ||
| 386 | else => {}, | ||
| 387 | } | ||
| 431 | 388 | ||
| 432 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | 389 | var line_it = mem.tokenize(u8, line, ": "); |
| 433 | headers.transfer_compression = ce; | 390 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; |
| 434 | } else { | 391 | const header_value = line_it.rest(); |
| 435 | return error.HttpTransferEncodingUnsupported; | 392 | |
| 436 | } | 393 | try res.headers.append(header_name, header_value); |
| 394 | |||
| 395 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | ||
| 396 | if (res.content_length != null) return error.HttpHeadersInvalid; | ||
| 397 | res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | ||
| 398 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | ||
| 399 | // Transfer-Encoding: second, first | ||
| 400 | // Transfer-Encoding: deflate, chunked | ||
| 401 | var iter = mem.splitBackwards(u8, header_value, ","); | ||
| 402 | |||
| 403 | if (iter.next()) |first| { | ||
| 404 | const trimmed = mem.trim(u8, first, " "); | ||
| 405 | |||
| 406 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | ||
| 407 | if (res.transfer_encoding != null) return error.HttpHeadersInvalid; | ||
| 408 | res.transfer_encoding = te; | ||
| 409 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | ||
| 410 | if (res.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 411 | res.transfer_compression = ce; | ||
| 412 | } else { | ||
| 413 | return error.HttpTransferEncodingUnsupported; | ||
| 437 | } | 414 | } |
| 415 | } | ||
| 438 | 416 | ||
| 439 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | 417 | if (iter.next()) |second| { |
| 440 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | 418 | if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported; |
| 441 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 442 | 419 | ||
| 443 | const trimmed = mem.trim(u8, header_value, " "); | 420 | const trimmed = mem.trim(u8, second, " "); |
| 444 | 421 | ||
| 445 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | 422 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 446 | headers.transfer_compression = ce; | 423 | res.transfer_compression = ce; |
| 447 | } else { | 424 | } else { |
| 448 | return error.HttpTransferEncodingUnsupported; | 425 | return error.HttpTransferEncodingUnsupported; |
| 449 | } | 426 | } |
| 450 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | ||
| 451 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | ||
| 452 | headers.connection = .keep_alive; | ||
| 453 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | ||
| 454 | headers.connection = .close; | ||
| 455 | } else { | ||
| 456 | return error.HttpConnectionHeaderUnsupported; | ||
| 457 | } | ||
| 458 | } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) { | ||
| 459 | headers.upgrade = header_value; | ||
| 460 | } | 427 | } |
| 461 | } | ||
| 462 | 428 | ||
| 463 | return headers; | 429 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; |
| 464 | } | 430 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { |
| 431 | if (res.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 465 | 432 | ||
| 466 | inline fn int64(array: *const [8]u8) u64 { | 433 | const trimmed = mem.trim(u8, header_value, " "); |
| 467 | return @bitCast(u64, array.*); | ||
| 468 | } | ||
| 469 | 434 | ||
| 470 | fn parseInt3(nnn: @Vector(3, u8)) u10 { | 435 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 471 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | 436 | res.transfer_compression = ce; |
| 472 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | 437 | } else { |
| 473 | return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); | 438 | return error.HttpTransferEncodingUnsupported; |
| 439 | } | ||
| 440 | } | ||
| 474 | } | 441 | } |
| 442 | } | ||
| 475 | 443 | ||
| 476 | test parseInt3 { | 444 | inline fn int64(array: *const [8]u8) u64 { |
| 477 | const expectEqual = testing.expectEqual; | 445 | return @bitCast(u64, array.*); |
| 478 | try expectEqual(@as(u10, 0), parseInt3("000".*)); | 446 | } |
| 479 | try expectEqual(@as(u10, 418), parseInt3("418".*)); | ||
| 480 | try expectEqual(@as(u10, 999), parseInt3("999".*)); | ||
| 481 | } | ||
| 482 | }; | ||
| 483 | 447 | ||
| 484 | headers: Headers = undefined, | 448 | fn parseInt3(nnn: @Vector(3, u8)) u10 { |
| 449 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | ||
| 450 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | ||
| 451 | return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); | ||
| 452 | } | ||
| 453 | |||
| 454 | test parseInt3 { | ||
| 455 | const expectEqual = testing.expectEqual; | ||
| 456 | try expectEqual(@as(u10, 0), parseInt3("000".*)); | ||
| 457 | try expectEqual(@as(u10, 418), parseInt3("418".*)); | ||
| 458 | try expectEqual(@as(u10, 999), parseInt3("999".*)); | ||
| 459 | } | ||
| 460 | |||
| 461 | version: http.Version, | ||
| 462 | status: http.Status, | ||
| 463 | reason: []const u8, | ||
| 464 | |||
| 465 | content_length: ?u64 = null, | ||
| 466 | transfer_encoding: ?http.TransferEncoding = null, | ||
| 467 | transfer_compression: ?http.ContentEncoding = null, | ||
| 468 | |||
| 469 | headers: http.Headers, | ||
| 485 | parser: proto.HeadersParser, | 470 | parser: proto.HeadersParser, |
| 486 | compression: Compression = .none, | 471 | compression: Compression = .none, |
| 487 | skip: bool = false, | 472 | skip: bool = false, |
| ... | @@ -491,22 +476,14 @@ pub const Response = struct { | ... | @@ -491,22 +476,14 @@ pub const Response = struct { |
| 491 | /// | 476 | /// |
| 492 | /// Order of operations: request[ -> write -> finish] -> do -> read | 477 | /// Order of operations: request[ -> write -> finish] -> do -> read |
| 493 | pub const Request = struct { | 478 | pub const Request = struct { |
| 494 | pub const Headers = struct { | ||
| 495 | version: http.Version = .@"HTTP/1.1", | ||
| 496 | method: http.Method = .GET, | ||
| 497 | user_agent: []const u8 = "zig (std.http)", | ||
| 498 | connection: http.Connection = .keep_alive, | ||
| 499 | transfer_encoding: RequestTransfer = .none, | ||
| 500 | |||
| 501 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | ||
| 502 | }; | ||
| 503 | |||
| 504 | uri: Uri, | 479 | uri: Uri, |
| 505 | client: *Client, | 480 | client: *Client, |
| 506 | connection: *ConnectionPool.Node, | 481 | connection: *ConnectionPool.Node, |
| 507 | /// These are stored in Request so that they are available when following | 482 | |
| 508 | /// redirects. | 483 | method: http.Method, |
| 509 | headers: Headers, | 484 | version: http.Version = .@"HTTP/1.1", |
| 485 | headers: http.Headers, | ||
| 486 | transfer_encoding: RequestTransfer = .none, | ||
| 510 | 487 | ||
| 511 | redirects_left: u32, | 488 | redirects_left: u32, |
| 512 | handle_redirects: bool, | 489 | handle_redirects: bool, |
| ... | @@ -526,6 +503,7 @@ pub const Request = struct { | ... | @@ -526,6 +503,7 @@ pub const Request = struct { |
| 526 | } | 503 | } |
| 527 | 504 | ||
| 528 | if (req.response.parser.header_bytes_owned) { | 505 | if (req.response.parser.header_bytes_owned) { |
| 506 | req.response.headers.deinit(); | ||
| 529 | req.response.parser.header_bytes.deinit(req.client.allocator); | 507 | req.response.parser.header_bytes.deinit(req.client.allocator); |
| 530 | } | 508 | } |
| 531 | 509 | ||
| ... | @@ -540,14 +518,14 @@ pub const Request = struct { | ... | @@ -540,14 +518,14 @@ pub const Request = struct { |
| 540 | req.* = undefined; | 518 | req.* = undefined; |
| 541 | } | 519 | } |
| 542 | 520 | ||
| 543 | pub fn start(req: *Request, uri: Uri, headers: Headers) !void { | 521 | pub fn start(req: *Request, uri: Uri) !void { |
| 544 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); | 522 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| 545 | const w = buffered.writer(); | 523 | const w = buffered.writer(); |
| 546 | 524 | ||
| 547 | try w.writeAll(@tagName(headers.method)); | 525 | try w.writeAll(@tagName(req.method)); |
| 548 | try w.writeByte(' '); | 526 | try w.writeByte(' '); |
| 549 | 527 | ||
| 550 | if (req.headers.method == .CONNECT) { | 528 | if (req.method == .CONNECT) { |
| 551 | try w.writeAll(uri.host.?); | 529 | try w.writeAll(uri.host.?); |
| 552 | try w.writeByte(':'); | 530 | try w.writeByte(':'); |
| 553 | try w.print("{}", .{uri.port.?}); | 531 | try w.print("{}", .{uri.port.?}); |
| ... | @@ -559,33 +537,62 @@ pub const Request = struct { | ... | @@ -559,33 +537,62 @@ pub const Request = struct { |
| 559 | } | 537 | } |
| 560 | 538 | ||
| 561 | try w.writeByte(' '); | 539 | try w.writeByte(' '); |
| 562 | try w.writeAll(@tagName(headers.version)); | 540 | try w.writeAll(@tagName(req.version)); |
| 563 | try w.writeAll("\r\nHost: "); | 541 | try w.writeAll("\r\n"); |
| 564 | try w.writeAll(uri.host.?); | 542 | |
| 565 | try w.writeAll("\r\nUser-Agent: "); | 543 | if (!req.headers.contains("host")) { |
| 566 | try w.writeAll(headers.user_agent); | 544 | try w.writeAll("Host: "); |
| 567 | if (headers.connection == .close) { | 545 | try w.writeAll(uri.host.?); |
| 568 | try w.writeAll("\r\nConnection: close"); | 546 | try w.writeAll("\r\n"); |
| 569 | } else { | ||
| 570 | try w.writeAll("\r\nConnection: keep-alive"); | ||
| 571 | } | 547 | } |
| 572 | try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); | ||
| 573 | try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers. | ||
| 574 | 548 | ||
| 575 | switch (headers.transfer_encoding) { | 549 | if (!req.headers.contains("user-agent")) { |
| 576 | .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), | 550 | try w.writeAll("User-Agent: zig/"); |
| 577 | .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), | 551 | try w.writeAll(@import("builtin").zig_version_string); |
| 578 | .none => {}, | 552 | try w.writeAll(" (std.http)\r\n"); |
| 579 | } | 553 | } |
| 580 | 554 | ||
| 581 | for (headers.custom) |header| { | 555 | if (!req.headers.contains("connection")) { |
| 582 | try w.writeAll("\r\n"); | 556 | try w.writeAll("Connection: keep-alive\r\n"); |
| 583 | try w.writeAll(header.name); | ||
| 584 | try w.writeAll(": "); | ||
| 585 | try w.writeAll(header.value); | ||
| 586 | } | 557 | } |
| 587 | 558 | ||
| 588 | try w.writeAll("\r\n\r\n"); | 559 | if (!req.headers.contains("accept-encoding")) { |
| 560 | try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n"); | ||
| 561 | } | ||
| 562 | |||
| 563 | if (!req.headers.contains("te")) { | ||
| 564 | try w.writeAll("TE: gzip, deflate, trailers\r\n"); | ||
| 565 | } | ||
| 566 | |||
| 567 | const has_transfer_encoding = req.headers.contains("transfer-encoding"); | ||
| 568 | const has_content_length = req.headers.contains("content-length"); | ||
| 569 | |||
| 570 | if (!has_transfer_encoding and !has_content_length) { | ||
| 571 | switch (req.transfer_encoding) { | ||
| 572 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), | ||
| 573 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), | ||
| 574 | .none => {}, | ||
| 575 | } | ||
| 576 | } else { | ||
| 577 | if (has_content_length) { | ||
| 578 | const content_length = try std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10); | ||
| 579 | |||
| 580 | req.transfer_encoding = .{ .content_length = content_length }; | ||
| 581 | } else if (has_transfer_encoding) { | ||
| 582 | const transfer_encoding = req.headers.getFirstValue("content-length").?; | ||
| 583 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | ||
| 584 | req.transfer_encoding = .chunked; | ||
| 585 | } else { | ||
| 586 | return error.UnsupportedTransferEncoding; | ||
| 587 | } | ||
| 588 | } else { | ||
| 589 | req.transfer_encoding = .none; | ||
| 590 | } | ||
| 591 | } | ||
| 592 | |||
| 593 | try w.print("{}", .{req.headers}); | ||
| 594 | |||
| 595 | try w.writeAll("\r\n"); | ||
| 589 | 596 | ||
| 590 | try buffered.flush(); | 597 | try buffered.flush(); |
| 591 | } | 598 | } |
| ... | @@ -611,7 +618,7 @@ pub const Request = struct { | ... | @@ -611,7 +618,7 @@ pub const Request = struct { |
| 611 | return index; | 618 | return index; |
| 612 | } | 619 | } |
| 613 | 620 | ||
| 614 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; | 621 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; |
| 615 | 622 | ||
| 616 | /// Waits for a response from the server and parses any headers that are sent. | 623 | /// Waits for a response from the server and parses any headers that are sent. |
| 617 | /// This function will block until the final response is received. | 624 | /// This function will block until the final response is received. |
| ... | @@ -629,33 +636,39 @@ pub const Request = struct { | ... | @@ -629,33 +636,39 @@ pub const Request = struct { |
| 629 | if (req.response.parser.state.isContent()) break; | 636 | if (req.response.parser.state.isContent()) break; |
| 630 | } | 637 | } |
| 631 | 638 | ||
| 632 | req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); | 639 | req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false }; |
| 640 | try req.response.parse(req.response.parser.header_bytes.items); | ||
| 633 | 641 | ||
| 634 | if (req.response.headers.status == .switching_protocols) { | 642 | if (req.response.status == .switching_protocols) { |
| 635 | req.connection.data.closing = false; | 643 | req.connection.data.closing = false; |
| 636 | req.response.parser.done = true; | 644 | req.response.parser.done = true; |
| 637 | } | 645 | } |
| 638 | 646 | ||
| 639 | if (req.headers.method == .CONNECT and req.response.headers.status == .ok) { | 647 | if (req.method == .CONNECT and req.response.status == .ok) { |
| 640 | req.connection.data.closing = false; | 648 | req.connection.data.closing = false; |
| 641 | req.connection.data.proxied = true; | 649 | req.connection.data.proxied = true; |
| 642 | req.response.parser.done = true; | 650 | req.response.parser.done = true; |
| 643 | } | 651 | } |
| 644 | 652 | ||
| 645 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { | 653 | const req_connection = req.headers.getFirstValue("connection"); |
| 654 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | ||
| 655 | |||
| 656 | const res_connection = req.response.headers.getFirstValue("connection"); | ||
| 657 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | ||
| 658 | if (req_keepalive and res_keepalive) { | ||
| 646 | req.connection.data.closing = false; | 659 | req.connection.data.closing = false; |
| 647 | } else { | 660 | } else { |
| 648 | req.connection.data.closing = true; | 661 | req.connection.data.closing = true; |
| 649 | } | 662 | } |
| 650 | 663 | ||
| 651 | if (req.response.headers.transfer_encoding) |te| { | 664 | if (req.response.transfer_encoding) |te| { |
| 652 | switch (te) { | 665 | switch (te) { |
| 653 | .chunked => { | 666 | .chunked => { |
| 654 | req.response.parser.next_chunk_length = 0; | 667 | req.response.parser.next_chunk_length = 0; |
| 655 | req.response.parser.state = .chunk_head_size; | 668 | req.response.parser.state = .chunk_head_size; |
| 656 | }, | 669 | }, |
| 657 | } | 670 | } |
| 658 | } else if (req.response.headers.content_length) |cl| { | 671 | } else if (req.response.content_length) |cl| { |
| 659 | req.response.parser.next_chunk_length = cl; | 672 | req.response.parser.next_chunk_length = cl; |
| 660 | 673 | ||
| 661 | if (cl == 0) req.response.parser.done = true; | 674 | if (cl == 0) req.response.parser.done = true; |
| ... | @@ -663,7 +676,7 @@ pub const Request = struct { | ... | @@ -663,7 +676,7 @@ pub const Request = struct { |
| 663 | req.response.parser.done = true; | 676 | req.response.parser.done = true; |
| 664 | } | 677 | } |
| 665 | 678 | ||
| 666 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) { | 679 | if (req.response.status.class() == .redirect and req.handle_redirects) { |
| 667 | req.response.skip = true; | 680 | req.response.skip = true; |
| 668 | 681 | ||
| 669 | const empty = @as([*]u8, undefined)[0..0]; | 682 | const empty = @as([*]u8, undefined)[0..0]; |
| ... | @@ -671,7 +684,7 @@ pub const Request = struct { | ... | @@ -671,7 +684,7 @@ pub const Request = struct { |
| 671 | 684 | ||
| 672 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; | 685 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 673 | 686 | ||
| 674 | const location = req.response.headers.location orelse | 687 | const location = req.response.headers.getFirstValue("location") orelse |
| 675 | return error.HttpRedirectMissingLocation; | 688 | return error.HttpRedirectMissingLocation; |
| 676 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); | 689 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); |
| 677 | 690 | ||
| ... | @@ -683,6 +696,8 @@ pub const Request = struct { | ... | @@ -683,6 +696,8 @@ pub const Request = struct { |
| 683 | req.arena = new_arena; | 696 | req.arena = new_arena; |
| 684 | 697 | ||
| 685 | const new_req = try req.client.request(resolved_url, req.headers, .{ | 698 | const new_req = try req.client.request(resolved_url, req.headers, .{ |
| 699 | .method = req.method, | ||
| 700 | .version = req.version, | ||
| 686 | .max_redirects = req.redirects_left - 1, | 701 | .max_redirects = req.redirects_left - 1, |
| 687 | .header_strategy = if (req.response.parser.header_bytes_owned) .{ | 702 | .header_strategy = if (req.response.parser.header_bytes_owned) .{ |
| 688 | .dynamic = req.response.parser.max_header_bytes, | 703 | .dynamic = req.response.parser.max_header_bytes, |
| ... | @@ -695,7 +710,7 @@ pub const Request = struct { | ... | @@ -695,7 +710,7 @@ pub const Request = struct { |
| 695 | } else { | 710 | } else { |
| 696 | req.response.skip = false; | 711 | req.response.skip = false; |
| 697 | if (!req.response.parser.done) { | 712 | if (!req.response.parser.done) { |
| 698 | if (req.response.headers.transfer_compression) |tc| switch (tc) { | 713 | if (req.response.transfer_compression) |tc| switch (tc) { |
| 699 | .compress => return error.CompressionNotSupported, | 714 | .compress => return error.CompressionNotSupported, |
| 700 | .deflate => req.response.compression = .{ | 715 | .deflate => req.response.compression = .{ |
| 701 | .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, | 716 | .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, |
| ... | @@ -789,7 +804,7 @@ pub const Request = struct { | ... | @@ -789,7 +804,7 @@ pub const Request = struct { |
| 789 | 804 | ||
| 790 | /// Finish the body of a request. This notifies the server that you have no more data to send. | 805 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 791 | pub fn finish(req: *Request) FinishError!void { | 806 | pub fn finish(req: *Request) FinishError!void { |
| 792 | switch (req.headers.transfer_encoding) { | 807 | switch (req.transfer_encoding) { |
| 793 | .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| { | 808 | .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| { |
| 794 | req.client.last_error = .{ .write = err }; | 809 | req.client.last_error = .{ .write = err }; |
| 795 | return error.WriteFailed; | 810 | return error.WriteFailed; |
| ... | @@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio | ... | @@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 908 | } | 923 | } |
| 909 | } | 924 | } |
| 910 | 925 | ||
| 911 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || BufferedConnection.WriteError || error{ | 926 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || std.fmt.ParseIntError || BufferedConnection.WriteError || error{ |
| 912 | UnsupportedUrlScheme, | 927 | UnsupportedUrlScheme, |
| 913 | UriMissingHost, | 928 | UriMissingHost, |
| 914 | 929 | ||
| 915 | CertificateBundleLoadFailure, | 930 | CertificateBundleLoadFailure, |
| 931 | UnsupportedTransferEncoding, | ||
| 916 | }; | 932 | }; |
| 917 | 933 | ||
| 918 | pub const Options = struct { | 934 | pub const Options = struct { |
| 935 | method: http.Method = .GET, | ||
| 936 | version: http.Version = .@"HTTP/1.1", | ||
| 937 | |||
| 919 | handle_redirects: bool = true, | 938 | handle_redirects: bool = true, |
| 920 | max_redirects: u32 = 3, | 939 | max_redirects: u32 = 3, |
| 921 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, | 940 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, |
| ... | @@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ | ... | @@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ |
| 946 | 965 | ||
| 947 | /// Form and send a http request to a server. | 966 | /// Form and send a http request to a server. |
| 948 | /// This function is threadsafe. | 967 | /// This function is threadsafe. |
| 949 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { | 968 | pub fn request(client: *Client, uri: Uri, headers: http.Headers, options: Options) RequestError!Request { |
| 950 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; | 969 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 951 | 970 | ||
| 952 | const port: u16 = uri.port orelse switch (protocol) { | 971 | const port: u16 = uri.port orelse switch (protocol) { |
| ... | @@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt | ... | @@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 973 | .client = client, | 992 | .client = client, |
| 974 | .connection = conn, | 993 | .connection = conn, |
| 975 | .headers = headers, | 994 | .headers = headers, |
| 995 | .method = options.method, | ||
| 996 | .version = options.version, | ||
| 976 | .redirects_left = options.max_redirects, | 997 | .redirects_left = options.max_redirects, |
| 977 | .handle_redirects = options.handle_redirects, | 998 | .handle_redirects = options.handle_redirects, |
| 978 | .response = .{ | 999 | .response = .{ |
| 1000 | .status = undefined, | ||
| 1001 | .version = undefined, | ||
| 1002 | .headers = undefined, | ||
| 979 | .parser = switch (options.header_strategy) { | 1003 | .parser = switch (options.header_strategy) { |
| 980 | .dynamic => |max| proto.HeadersParser.initDynamic(max), | 1004 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 981 | .static => |buf| proto.HeadersParser.initStatic(buf), | 1005 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| ... | @@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt | ... | @@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 987 | 1011 | ||
| 988 | req.arena = std.heap.ArenaAllocator.init(client.allocator); | 1012 | req.arena = std.heap.ArenaAllocator.init(client.allocator); |
| 989 | 1013 | ||
| 990 | try req.start(uri, headers); | 1014 | try req.start(uri); |
| 991 | 1015 | ||
| 992 | return req; | 1016 | return req; |
| 993 | } | 1017 | } |
lib/std/http/Headers.zig created+386| ... | @@ -0,0 +1,386 @@ | ||
| 1 | const std = @import("../std.zig"); | ||
| 2 | |||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | |||
| 5 | const testing = std.testing; | ||
| 6 | const ascii = std.ascii; | ||
| 7 | const assert = std.debug.assert; | ||
| 8 | |||
| 9 | pub const HeaderList = std.ArrayListUnmanaged(HeaderEntry); | ||
| 10 | pub const HeaderIndexList = std.ArrayListUnmanaged(usize); | ||
| 11 | pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage); | ||
| 12 | |||
| 13 | pub const CaseInsensitiveStringContext = struct { | ||
| 14 | pub fn hash(self: @This(), s: []const u8) u64 { | ||
| 15 | _ = self; | ||
| 16 | var buf: [64]u8 = undefined; | ||
| 17 | var i: u8 = 0; | ||
| 18 | |||
| 19 | var h = std.hash.Wyhash.init(0); | ||
| 20 | while (i < s.len) : (i += 64) { | ||
| 21 | const left = @min(64, s.len - i); | ||
| 22 | const ret = ascii.lowerString(buf[0..], s[i..][0..left]); | ||
| 23 | h.update(ret); | ||
| 24 | } | ||
| 25 | |||
| 26 | return h.final(); | ||
| 27 | } | ||
| 28 | |||
| 29 | pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { | ||
| 30 | _ = self; | ||
| 31 | return ascii.eqlIgnoreCase(a, b); | ||
| 32 | } | ||
| 33 | }; | ||
| 34 | |||
| 35 | pub const HeaderEntry = struct { | ||
| 36 | name: []const u8, | ||
| 37 | value: []const u8, | ||
| 38 | |||
| 39 | pub fn modify(entry: *HeaderEntry, allocator: Allocator, new_value: []const u8) !void { | ||
| 40 | if (entry.value.len <= new_value.len) { | ||
| 41 | std.mem.copy(u8, @constCast(entry.value), new_value); | ||
| 42 | } else { | ||
| 43 | allocator.free(entry.value); | ||
| 44 | |||
| 45 | entry.value = try allocator.dupe(u8, new_value); | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | fn lessThan(ctx: void, a: HeaderEntry, b: HeaderEntry) bool { | ||
| 50 | _ = ctx; | ||
| 51 | if (a.name.ptr == b.name.ptr) return false; | ||
| 52 | |||
| 53 | return ascii.lessThanIgnoreCase(a.name, b.name); | ||
| 54 | } | ||
| 55 | }; | ||
| 56 | |||
| 57 | pub const Headers = struct { | ||
| 58 | allocator: Allocator, | ||
| 59 | list: HeaderList = .{}, | ||
| 60 | index: HeaderIndex = .{}, | ||
| 61 | |||
| 62 | /// When this is false, names and values will not be duplicated. | ||
| 63 | /// Use with caution. | ||
| 64 | owned: bool = true, | ||
| 65 | |||
| 66 | pub fn init(allocator: Allocator) Headers { | ||
| 67 | return .{ .allocator = allocator }; | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn deinit(headers: *Headers) void { | ||
| 71 | var it = headers.index.iterator(); | ||
| 72 | while (it.next()) |entry| { | ||
| 73 | entry.value_ptr.deinit(headers.allocator); | ||
| 74 | |||
| 75 | if (headers.owned) headers.allocator.free(entry.key_ptr.*); | ||
| 76 | } | ||
| 77 | |||
| 78 | for (headers.list.items) |entry| { | ||
| 79 | if (headers.owned) headers.allocator.free(entry.value); | ||
| 80 | } | ||
| 81 | |||
| 82 | headers.index.deinit(headers.allocator); | ||
| 83 | headers.list.deinit(headers.allocator); | ||
| 84 | |||
| 85 | headers.* = undefined; | ||
| 86 | } | ||
| 87 | |||
| 88 | /// Appends a header to the list. Both name and value are copied. | ||
| 89 | pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void { | ||
| 90 | const n = headers.list.items.len; | ||
| 91 | |||
| 92 | const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value; | ||
| 93 | errdefer if (headers.owned) headers.allocator.free(value_duped); | ||
| 94 | |||
| 95 | var entry = HeaderEntry{ .name = undefined, .value = value_duped }; | ||
| 96 | |||
| 97 | if (headers.index.getEntry(name)) |kv| { | ||
| 98 | entry.name = kv.key_ptr.*; | ||
| 99 | try kv.value_ptr.append(headers.allocator, n); | ||
| 100 | } else { | ||
| 101 | const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name; | ||
| 102 | errdefer if (headers.owned) headers.allocator.free(name_duped); | ||
| 103 | |||
| 104 | entry.name = name_duped; | ||
| 105 | |||
| 106 | var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1); | ||
| 107 | errdefer new_index.deinit(headers.allocator); | ||
| 108 | |||
| 109 | new_index.appendAssumeCapacity(n); | ||
| 110 | try headers.index.put(headers.allocator, name_duped, new_index); | ||
| 111 | } | ||
| 112 | |||
| 113 | try headers.list.append(headers.allocator, entry); | ||
| 114 | } | ||
| 115 | |||
| 116 | pub fn contains(headers: Headers, name: []const u8) bool { | ||
| 117 | return headers.index.contains(name); | ||
| 118 | } | ||
| 119 | |||
| 120 | pub fn delete(headers: *Headers, name: []const u8) bool { | ||
| 121 | if (headers.index.fetchRemove(name)) |kv| { | ||
| 122 | var index = kv.value; | ||
| 123 | |||
| 124 | // iterate backwards | ||
| 125 | var i = index.items.len; | ||
| 126 | while (i > 0) { | ||
| 127 | i -= 1; | ||
| 128 | const data_index = index.items[i]; | ||
| 129 | const removed = headers.list.orderedRemove(data_index); | ||
| 130 | |||
| 131 | assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted | ||
| 132 | if (headers.owned) headers.allocator.free(removed.value); | ||
| 133 | } | ||
| 134 | |||
| 135 | if (headers.owned) headers.allocator.free(kv.key); | ||
| 136 | index.deinit(headers.allocator); | ||
| 137 | headers.rebuildIndex(); | ||
| 138 | |||
| 139 | return true; | ||
| 140 | } else { | ||
| 141 | return false; | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | /// Returns the index of the first occurrence of a header with the given name. | ||
| 146 | pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize { | ||
| 147 | const index = headers.index.get(name) orelse return null; | ||
| 148 | |||
| 149 | return index.items[0]; | ||
| 150 | } | ||
| 151 | |||
| 152 | /// Returns a list of indices containing headers with the given name. | ||
| 153 | pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize { | ||
| 154 | const index = headers.index.get(name) orelse return null; | ||
| 155 | |||
| 156 | return index.items; | ||
| 157 | } | ||
| 158 | |||
| 159 | /// Returns the entry of the first occurrence of a header with the given name. | ||
| 160 | pub fn getFirstEntry(headers: Headers, name: []const u8) ?HeaderEntry { | ||
| 161 | const first_index = headers.firstIndexOf(name) orelse return null; | ||
| 162 | |||
| 163 | return headers.list.items[first_index]; | ||
| 164 | } | ||
| 165 | |||
| 166 | /// Returns a slice containing each header with the given name. | ||
| 167 | /// The caller owns the returned slice, but NOT the values in the slice. | ||
| 168 | pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const HeaderEntry { | ||
| 169 | const indices = headers.getIndices(name) orelse return null; | ||
| 170 | |||
| 171 | const buf = try allocator.alloc(HeaderEntry, indices.len); | ||
| 172 | for (indices, 0..) |idx, n| { | ||
| 173 | buf[n] = headers.list.items[idx]; | ||
| 174 | } | ||
| 175 | |||
| 176 | return buf; | ||
| 177 | } | ||
| 178 | |||
| 179 | /// Returns the value in the entry of the first occurrence of a header with the given name. | ||
| 180 | pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 { | ||
| 181 | const first_index = headers.firstIndexOf(name) orelse return null; | ||
| 182 | |||
| 183 | return headers.list.items[first_index].value; | ||
| 184 | } | ||
| 185 | |||
| 186 | /// Returns a slice containing the value of each header with the given name. | ||
| 187 | /// The caller owns the returned slice, but NOT the values in the slice. | ||
| 188 | pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 { | ||
| 189 | const indices = headers.getIndices(name) orelse return null; | ||
| 190 | |||
| 191 | const buf = try allocator.alloc([]const u8, indices.len); | ||
| 192 | for (indices, 0..) |idx, n| { | ||
| 193 | buf[n] = headers.list.items[idx].value; | ||
| 194 | } | ||
| 195 | |||
| 196 | return buf; | ||
| 197 | } | ||
| 198 | |||
| 199 | fn rebuildIndex(headers: *Headers) void { | ||
| 200 | // clear out the indexes | ||
| 201 | var it = headers.index.iterator(); | ||
| 202 | while (it.next()) |entry| { | ||
| 203 | entry.value_ptr.shrinkRetainingCapacity(0); | ||
| 204 | } | ||
| 205 | |||
| 206 | // fill up indexes again; we know capacity is fine from before | ||
| 207 | for (headers.list.items, 0..) |entry, i| { | ||
| 208 | headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i); | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | /// Sorts the headers in lexicographical order. | ||
| 213 | pub fn sort(headers: *Headers) void { | ||
| 214 | std.sort.sort(HeaderEntry, headers.list.items, {}, HeaderEntry.lessThan); | ||
| 215 | headers.rebuildIndex(); | ||
| 216 | } | ||
| 217 | |||
| 218 | /// Writes the headers to the given stream. | ||
| 219 | pub fn format( | ||
| 220 | headers: Headers, | ||
| 221 | comptime fmt: []const u8, | ||
| 222 | options: std.fmt.FormatOptions, | ||
| 223 | out_stream: anytype, | ||
| 224 | ) !void { | ||
| 225 | _ = fmt; | ||
| 226 | _ = options; | ||
| 227 | |||
| 228 | for (headers.list.items) |entry| { | ||
| 229 | if (entry.value.len == 0) continue; | ||
| 230 | |||
| 231 | try out_stream.writeAll(entry.name); | ||
| 232 | try out_stream.writeAll(": "); | ||
| 233 | try out_stream.writeAll(entry.value); | ||
| 234 | try out_stream.writeAll("\r\n"); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | /// Writes all of the headers with the given name to the given stream, separated by commas. | ||
| 239 | /// | ||
| 240 | /// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2 | ||
| 241 | pub fn formatCommaSeparated( | ||
| 242 | headers: Headers, | ||
| 243 | name: []const u8, | ||
| 244 | out_stream: anytype, | ||
| 245 | ) !void { | ||
| 246 | const indices = headers.getIndices(name) orelse return; | ||
| 247 | |||
| 248 | try out_stream.writeAll(name); | ||
| 249 | try out_stream.writeAll(": "); | ||
| 250 | |||
| 251 | for (indices, 0..) |idx, n| { | ||
| 252 | if (n != 0) try out_stream.writeAll(", "); | ||
| 253 | try out_stream.writeAll(headers.list.items[idx].value); | ||
| 254 | } | ||
| 255 | |||
| 256 | try out_stream.writeAll("\r\n"); | ||
| 257 | } | ||
| 258 | }; | ||
| 259 | |||
| 260 | test "Headers.append" { | ||
| 261 | var h = Headers{ .allocator = std.testing.allocator }; | ||
| 262 | defer h.deinit(); | ||
| 263 | |||
| 264 | try h.append("foo", "bar"); | ||
| 265 | try h.append("hello", "world"); | ||
| 266 | |||
| 267 | try testing.expect(h.contains("Foo")); | ||
| 268 | try testing.expect(!h.contains("Bar")); | ||
| 269 | } | ||
| 270 | |||
| 271 | test "Headers.delete" { | ||
| 272 | var h = Headers{ .allocator = std.testing.allocator }; | ||
| 273 | defer h.deinit(); | ||
| 274 | |||
| 275 | try h.append("foo", "bar"); | ||
| 276 | try h.append("hello", "world"); | ||
| 277 | |||
| 278 | try testing.expect(h.contains("Foo")); | ||
| 279 | |||
| 280 | _ = h.delete("Foo"); | ||
| 281 | |||
| 282 | try testing.expect(!h.contains("foo")); | ||
| 283 | } | ||
| 284 | |||
| 285 | test "Headers consistency" { | ||
| 286 | var h = Headers{ .allocator = std.testing.allocator }; | ||
| 287 | defer h.deinit(); | ||
| 288 | |||
| 289 | try h.append("foo", "bar"); | ||
| 290 | try h.append("hello", "world"); | ||
| 291 | _ = h.delete("Foo"); | ||
| 292 | |||
| 293 | try h.append("foo", "bar"); | ||
| 294 | try h.append("bar", "world"); | ||
| 295 | try h.append("foo", "baz"); | ||
| 296 | try h.append("baz", "hello"); | ||
| 297 | |||
| 298 | try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello")); | ||
| 299 | try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo")); | ||
| 300 | try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar")); | ||
| 301 | try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz")); | ||
| 302 | try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog")); | ||
| 303 | |||
| 304 | try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?); | ||
| 305 | try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?); | ||
| 306 | try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?); | ||
| 307 | try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?); | ||
| 308 | try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog")); | ||
| 309 | |||
| 310 | try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value); | ||
| 311 | try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value); | ||
| 312 | try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value); | ||
| 313 | try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value); | ||
| 314 | |||
| 315 | const hello_entries = (try h.getEntries(testing.allocator, "hello")).?; | ||
| 316 | defer testing.allocator.free(hello_entries); | ||
| 317 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | ||
| 318 | .{ .name = "hello", .value = "world" }, | ||
| 319 | }), hello_entries); | ||
| 320 | |||
| 321 | const foo_entries = (try h.getEntries(testing.allocator, "foo")).?; | ||
| 322 | defer testing.allocator.free(foo_entries); | ||
| 323 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | ||
| 324 | .{ .name = "foo", .value = "bar" }, | ||
| 325 | .{ .name = "foo", .value = "baz" }, | ||
| 326 | }), foo_entries); | ||
| 327 | |||
| 328 | const bar_entries = (try h.getEntries(testing.allocator, "bar")).?; | ||
| 329 | defer testing.allocator.free(bar_entries); | ||
| 330 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | ||
| 331 | .{ .name = "bar", .value = "world" }, | ||
| 332 | }), bar_entries); | ||
| 333 | |||
| 334 | const baz_entries = (try h.getEntries(testing.allocator, "baz")).?; | ||
| 335 | defer testing.allocator.free(baz_entries); | ||
| 336 | try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ | ||
| 337 | .{ .name = "baz", .value = "hello" }, | ||
| 338 | }), baz_entries); | ||
| 339 | |||
| 340 | const pog_entries = (try h.getEntries(testing.allocator, "pog")); | ||
| 341 | try testing.expectEqual(@as(?[]const HeaderEntry, null), pog_entries); | ||
| 342 | |||
| 343 | try testing.expectEqualStrings("world", h.getFirstValue("hello").?); | ||
| 344 | try testing.expectEqualStrings("bar", h.getFirstValue("foo").?); | ||
| 345 | try testing.expectEqualStrings("world", h.getFirstValue("bar").?); | ||
| 346 | try testing.expectEqualStrings("hello", h.getFirstValue("baz").?); | ||
| 347 | try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog")); | ||
| 348 | |||
| 349 | const hello_values = (try h.getValues(testing.allocator, "hello")).?; | ||
| 350 | defer testing.allocator.free(hello_values); | ||
| 351 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values); | ||
| 352 | |||
| 353 | const foo_values = (try h.getValues(testing.allocator, "foo")).?; | ||
| 354 | defer testing.allocator.free(foo_values); | ||
| 355 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values); | ||
| 356 | |||
| 357 | const bar_values = (try h.getValues(testing.allocator, "bar")).?; | ||
| 358 | defer testing.allocator.free(bar_values); | ||
| 359 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values); | ||
| 360 | |||
| 361 | const baz_values = (try h.getValues(testing.allocator, "baz")).?; | ||
| 362 | defer testing.allocator.free(baz_values); | ||
| 363 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values); | ||
| 364 | |||
| 365 | const pog_values = (try h.getValues(testing.allocator, "pog")); | ||
| 366 | try testing.expectEqual(@as(?[]const []const u8, null), pog_values); | ||
| 367 | |||
| 368 | h.sort(); | ||
| 369 | |||
| 370 | try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?); | ||
| 371 | try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?); | ||
| 372 | try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?); | ||
| 373 | try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?); | ||
| 374 | |||
| 375 | const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h}); | ||
| 376 | defer testing.allocator.free(formatted_values); | ||
| 377 | |||
| 378 | try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values); | ||
| 379 | |||
| 380 | var buf: [128]u8 = undefined; | ||
| 381 | var fbs = std.io.fixedBufferStream(&buf); | ||
| 382 | const writer = fbs.writer(); | ||
| 383 | |||
| 384 | try h.formatCommaSeparated("foo", writer); | ||
| 385 | try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten()); | ||
| 386 | } | ||
lib/std/http/Server.zig+149-146| ... | @@ -157,134 +157,120 @@ pub const BufferedConnection = struct { | ... | @@ -157,134 +157,120 @@ pub const BufferedConnection = struct { |
| 157 | 157 | ||
| 158 | /// A HTTP request originating from a client. | 158 | /// A HTTP request originating from a client. |
| 159 | pub const Request = struct { | 159 | pub const Request = struct { |
| 160 | pub const Headers = struct { | 160 | pub const ParseError = Allocator.Error || error{ |
| 161 | method: http.Method, | 161 | ShortHttpStatusLine, |
| 162 | target: []const u8, | 162 | BadHttpVersion, |
| 163 | version: http.Version, | 163 | UnknownHttpMethod, |
| 164 | content_length: ?u64 = null, | 164 | HttpHeadersInvalid, |
| 165 | transfer_encoding: ?http.TransferEncoding = null, | 165 | HttpHeaderContinuationsUnsupported, |
| 166 | transfer_compression: ?http.ContentEncoding = null, | 166 | HttpTransferEncodingUnsupported, |
| 167 | connection: http.Connection = .close, | 167 | HttpConnectionHeaderUnsupported, |
| 168 | host: ?[]const u8 = null, | 168 | InvalidCharacter, |
| 169 | 169 | }; | |
| 170 | pub const ParseError = error{ | ||
| 171 | ShortHttpStatusLine, | ||
| 172 | BadHttpVersion, | ||
| 173 | UnknownHttpMethod, | ||
| 174 | HttpHeadersInvalid, | ||
| 175 | HttpHeaderContinuationsUnsupported, | ||
| 176 | HttpTransferEncodingUnsupported, | ||
| 177 | HttpConnectionHeaderUnsupported, | ||
| 178 | InvalidCharacter, | ||
| 179 | }; | ||
| 180 | |||
| 181 | pub fn parse(bytes: []const u8) !Headers { | ||
| 182 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | ||
| 183 | |||
| 184 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | ||
| 185 | if (first_line.len < 10) | ||
| 186 | return error.ShortHttpStatusLine; | ||
| 187 | |||
| 188 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | ||
| 189 | const method_str = first_line[0..method_end]; | ||
| 190 | const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; | ||
| 191 | 170 | ||
| 192 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; | 171 | pub fn parse(req: *Request, bytes: []const u8) !void { |
| 193 | if (version_start == method_end) return error.HttpHeadersInvalid; | 172 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| 194 | 173 | ||
| 195 | const version_str = first_line[version_start + 1 ..]; | 174 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 196 | if (version_str.len != 8) return error.HttpHeadersInvalid; | 175 | if (first_line.len < 10) |
| 197 | const version: http.Version = switch (int64(version_str[0..8])) { | 176 | return error.ShortHttpStatusLine; |
| 198 | int64("HTTP/1.0") => .@"HTTP/1.0", | ||
| 199 | int64("HTTP/1.1") => .@"HTTP/1.1", | ||
| 200 | else => return error.BadHttpVersion, | ||
| 201 | }; | ||
| 202 | 177 | ||
| 203 | const target = first_line[method_end + 1 .. version_start]; | 178 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; |
| 179 | const method_str = first_line[0..method_end]; | ||
| 180 | const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; | ||
| 204 | 181 | ||
| 205 | var headers: Headers = .{ | 182 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; |
| 206 | .method = method, | 183 | if (version_start == method_end) return error.HttpHeadersInvalid; |
| 207 | .target = target, | ||
| 208 | .version = version, | ||
| 209 | }; | ||
| 210 | 184 | ||
| 211 | while (it.next()) |line| { | 185 | const version_str = first_line[version_start + 1 ..]; |
| 212 | if (line.len == 0) return error.HttpHeadersInvalid; | 186 | if (version_str.len != 8) return error.HttpHeadersInvalid; |
| 213 | switch (line[0]) { | 187 | const version: http.Version = switch (int64(version_str[0..8])) { |
| 214 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | 188 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 215 | else => {}, | 189 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 216 | } | 190 | else => return error.BadHttpVersion, |
| 191 | }; | ||
| 217 | 192 | ||
| 218 | var line_it = mem.tokenize(u8, line, ": "); | 193 | const target = first_line[method_end + 1 .. version_start]; |
| 219 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; | ||
| 220 | const header_value = line_it.rest(); | ||
| 221 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | ||
| 222 | if (headers.content_length != null) return error.HttpHeadersInvalid; | ||
| 223 | headers.content_length = try std.fmt.parseInt(u64, header_value, 10); | ||
| 224 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | ||
| 225 | // Transfer-Encoding: second, first | ||
| 226 | // Transfer-Encoding: deflate, chunked | ||
| 227 | var iter = mem.splitBackwards(u8, header_value, ","); | ||
| 228 | |||
| 229 | if (iter.next()) |first| { | ||
| 230 | const trimmed = mem.trim(u8, first, " "); | ||
| 231 | |||
| 232 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | ||
| 233 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | ||
| 234 | headers.transfer_encoding = te; | ||
| 235 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | ||
| 236 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 237 | headers.transfer_compression = ce; | ||
| 238 | } else { | ||
| 239 | return error.HttpTransferEncodingUnsupported; | ||
| 240 | } | ||
| 241 | } | ||
| 242 | 194 | ||
| 243 | if (iter.next()) |second| { | 195 | req.method = method; |
| 244 | if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | 196 | req.target = target; |
| 197 | req.version = version; | ||
| 245 | 198 | ||
| 246 | const trimmed = mem.trim(u8, second, " "); | 199 | while (it.next()) |line| { |
| 200 | if (line.len == 0) return error.HttpHeadersInvalid; | ||
| 201 | switch (line[0]) { | ||
| 202 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | ||
| 203 | else => {}, | ||
| 204 | } | ||
| 247 | 205 | ||
| 248 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | 206 | var line_it = mem.tokenize(u8, line, ": "); |
| 249 | headers.transfer_compression = ce; | 207 | const header_name = line_it.next() orelse return error.HttpHeadersInvalid; |
| 250 | } else { | 208 | const header_value = line_it.rest(); |
| 251 | return error.HttpTransferEncodingUnsupported; | 209 | |
| 252 | } | 210 | try req.headers.append(header_name, header_value); |
| 211 | |||
| 212 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | ||
| 213 | if (req.content_length != null) return error.HttpHeadersInvalid; | ||
| 214 | req.content_length = try std.fmt.parseInt(u64, header_value, 10); | ||
| 215 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | ||
| 216 | // Transfer-Encoding: second, first | ||
| 217 | // Transfer-Encoding: deflate, chunked | ||
| 218 | var iter = mem.splitBackwards(u8, header_value, ","); | ||
| 219 | |||
| 220 | if (iter.next()) |first| { | ||
| 221 | const trimmed = mem.trim(u8, first, " "); | ||
| 222 | |||
| 223 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | ||
| 224 | if (req.transfer_encoding != null) return error.HttpHeadersInvalid; | ||
| 225 | req.transfer_encoding = te; | ||
| 226 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | ||
| 227 | if (req.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 228 | req.transfer_compression = ce; | ||
| 229 | } else { | ||
| 230 | return error.HttpTransferEncodingUnsupported; | ||
| 253 | } | 231 | } |
| 232 | } | ||
| 254 | 233 | ||
| 255 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | 234 | if (iter.next()) |second| { |
| 256 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | 235 | if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported; |
| 257 | if (headers.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 258 | 236 | ||
| 259 | const trimmed = mem.trim(u8, header_value, " "); | 237 | const trimmed = mem.trim(u8, second, " "); |
| 260 | 238 | ||
| 261 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | 239 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 262 | headers.transfer_compression = ce; | 240 | req.transfer_compression = ce; |
| 263 | } else { | 241 | } else { |
| 264 | return error.HttpTransferEncodingUnsupported; | 242 | return error.HttpTransferEncodingUnsupported; |
| 265 | } | 243 | } |
| 266 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | ||
| 267 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | ||
| 268 | headers.connection = .keep_alive; | ||
| 269 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | ||
| 270 | headers.connection = .close; | ||
| 271 | } else { | ||
| 272 | return error.HttpConnectionHeaderUnsupported; | ||
| 273 | } | ||
| 274 | } else if (std.ascii.eqlIgnoreCase(header_name, "host")) { | ||
| 275 | headers.host = header_value; | ||
| 276 | } | 244 | } |
| 277 | } | ||
| 278 | 245 | ||
| 279 | return headers; | 246 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; |
| 280 | } | 247 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { |
| 248 | if (req.transfer_compression != null) return error.HttpHeadersInvalid; | ||
| 281 | 249 | ||
| 282 | inline fn int64(array: *const [8]u8) u64 { | 250 | const trimmed = mem.trim(u8, header_value, " "); |
| 283 | return @bitCast(u64, array.*); | 251 | |
| 252 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | ||
| 253 | req.transfer_compression = ce; | ||
| 254 | } else { | ||
| 255 | return error.HttpTransferEncodingUnsupported; | ||
| 256 | } | ||
| 257 | } | ||
| 284 | } | 258 | } |
| 285 | }; | 259 | } |
| 260 | |||
| 261 | inline fn int64(array: *const [8]u8) u64 { | ||
| 262 | return @bitCast(u64, array.*); | ||
| 263 | } | ||
| 286 | 264 | ||
| 287 | headers: Headers = undefined, | 265 | method: http.Method, |
| 266 | target: []const u8, | ||
| 267 | version: http.Version, | ||
| 268 | |||
| 269 | content_length: ?u64 = null, | ||
| 270 | transfer_encoding: ?http.TransferEncoding = null, | ||
| 271 | transfer_compression: ?http.ContentEncoding = null, | ||
| 272 | |||
| 273 | headers: http.Headers = undefined, | ||
| 288 | parser: proto.HeadersParser, | 274 | parser: proto.HeadersParser, |
| 289 | compression: Compression = .none, | 275 | compression: Compression = .none, |
| 290 | }; | 276 | }; |
| ... | @@ -295,23 +281,17 @@ pub const Request = struct { | ... | @@ -295,23 +281,17 @@ pub const Request = struct { |
| 295 | /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] | 281 | /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] |
| 296 | /// \ -> read / | 282 | /// \ -> read / |
| 297 | pub const Response = struct { | 283 | pub const Response = struct { |
| 298 | pub const Headers = struct { | 284 | version: http.Version = .@"HTTP/1.1", |
| 299 | version: http.Version = .@"HTTP/1.1", | 285 | status: http.Status = .ok, |
| 300 | status: http.Status = .ok, | 286 | reason: ?[]const u8 = null, |
| 301 | reason: ?[]const u8 = null, | ||
| 302 | 287 | ||
| 303 | server: ?[]const u8 = "zig (std.http)", | 288 | transfer_encoding: ResponseTransfer = .none, |
| 304 | connection: http.Connection = .keep_alive, | ||
| 305 | transfer_encoding: RequestTransfer = .none, | ||
| 306 | |||
| 307 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | ||
| 308 | }; | ||
| 309 | 289 | ||
| 310 | server: *Server, | 290 | server: *Server, |
| 311 | address: net.Address, | 291 | address: net.Address, |
| 312 | connection: BufferedConnection, | 292 | connection: BufferedConnection, |
| 313 | 293 | ||
| 314 | headers: Headers = .{}, | 294 | headers: http.Headers, |
| 315 | request: Request, | 295 | request: Request, |
| 316 | 296 | ||
| 317 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. | 297 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. |
| ... | @@ -346,41 +326,54 @@ pub const Response = struct { | ... | @@ -346,41 +326,54 @@ pub const Response = struct { |
| 346 | var buffered = std.io.bufferedWriter(res.connection.writer()); | 326 | var buffered = std.io.bufferedWriter(res.connection.writer()); |
| 347 | const w = buffered.writer(); | 327 | const w = buffered.writer(); |
| 348 | 328 | ||
| 349 | try w.writeAll(@tagName(res.headers.version)); | 329 | try w.writeAll(@tagName(res.version)); |
| 350 | try w.writeByte(' '); | 330 | try w.writeByte(' '); |
| 351 | try w.print("{d}", .{@enumToInt(res.headers.status)}); | 331 | try w.print("{d}", .{@enumToInt(res.status)}); |
| 352 | try w.writeByte(' '); | 332 | try w.writeByte(' '); |
| 353 | if (res.headers.reason) |reason| { | 333 | if (res.reason) |reason| { |
| 354 | try w.writeAll(reason); | 334 | try w.writeAll(reason); |
| 355 | } else if (res.headers.status.phrase()) |phrase| { | 335 | } else if (res.status.phrase()) |phrase| { |
| 356 | try w.writeAll(phrase); | 336 | try w.writeAll(phrase); |
| 357 | } | 337 | } |
| 338 | try w.writeAll("\r\n"); | ||
| 358 | 339 | ||
| 359 | if (res.headers.server) |server| { | 340 | if (!res.headers.contains("server")) { |
| 360 | try w.writeAll("\r\nServer: "); | 341 | try w.writeAll("Server: zig (std.http)\r\n"); |
| 361 | try w.writeAll(server); | ||
| 362 | } | 342 | } |
| 363 | 343 | ||
| 364 | if (res.headers.connection == .close) { | 344 | if (!res.headers.contains("connection")) { |
| 365 | try w.writeAll("\r\nConnection: close"); | 345 | try w.writeAll("Connection: keep-alive\r\n"); |
| 366 | } else { | ||
| 367 | try w.writeAll("\r\nConnection: keep-alive"); | ||
| 368 | } | 346 | } |
| 369 | 347 | ||
| 370 | switch (res.headers.transfer_encoding) { | 348 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); |
| 371 | .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), | 349 | const has_content_length = res.headers.contains("content-length"); |
| 372 | .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), | ||
| 373 | .none => {}, | ||
| 374 | } | ||
| 375 | 350 | ||
| 376 | for (res.headers.custom) |header| { | 351 | if (!has_transfer_encoding and !has_content_length) { |
| 377 | try w.writeAll("\r\n"); | 352 | switch (res.transfer_encoding) { |
| 378 | try w.writeAll(header.name); | 353 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), |
| 379 | try w.writeAll(": "); | 354 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), |
| 380 | try w.writeAll(header.value); | 355 | .none => {}, |
| 356 | } | ||
| 357 | } else { | ||
| 358 | if (has_content_length) { | ||
| 359 | const content_length = try std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10); | ||
| 360 | |||
| 361 | res.transfer_encoding = .{ .content_length = content_length }; | ||
| 362 | } else if (has_transfer_encoding) { | ||
| 363 | const transfer_encoding = res.headers.getFirstValue("content-length").?; | ||
| 364 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | ||
| 365 | res.transfer_encoding = .chunked; | ||
| 366 | } else { | ||
| 367 | return error.UnsupportedTransferEncoding; | ||
| 368 | } | ||
| 369 | } else { | ||
| 370 | res.transfer_encoding = .none; | ||
| 371 | } | ||
| 381 | } | 372 | } |
| 382 | 373 | ||
| 383 | try w.writeAll("\r\n\r\n"); | 374 | try w.print("{}", .{res.headers}); |
| 375 | |||
| 376 | try w.writeAll("\r\n"); | ||
| 384 | 377 | ||
| 385 | try buffered.flush(); | 378 | try buffered.flush(); |
| 386 | } | 379 | } |
| ... | @@ -419,22 +412,28 @@ pub const Response = struct { | ... | @@ -419,22 +412,28 @@ pub const Response = struct { |
| 419 | if (res.request.parser.state.isContent()) break; | 412 | if (res.request.parser.state.isContent()) break; |
| 420 | } | 413 | } |
| 421 | 414 | ||
| 422 | res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items); | 415 | res.request.headers = .{ .allocator = res.server.allocator, .owned = true }; |
| 416 | try res.request.parse(res.request.parser.header_bytes.items); | ||
| 423 | 417 | ||
| 424 | if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) { | 418 | const res_connection = res.headers.getFirstValue("connection"); |
| 419 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | ||
| 420 | |||
| 421 | const req_connection = res.request.headers.getFirstValue("connection"); | ||
| 422 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | ||
| 423 | if (res_keepalive and req_keepalive) { | ||
| 425 | res.connection.conn.closing = false; | 424 | res.connection.conn.closing = false; |
| 426 | } else { | 425 | } else { |
| 427 | res.connection.conn.closing = true; | 426 | res.connection.conn.closing = true; |
| 428 | } | 427 | } |
| 429 | 428 | ||
| 430 | if (res.request.headers.transfer_encoding) |te| { | 429 | if (res.request.transfer_encoding) |te| { |
| 431 | switch (te) { | 430 | switch (te) { |
| 432 | .chunked => { | 431 | .chunked => { |
| 433 | res.request.parser.next_chunk_length = 0; | 432 | res.request.parser.next_chunk_length = 0; |
| 434 | res.request.parser.state = .chunk_head_size; | 433 | res.request.parser.state = .chunk_head_size; |
| 435 | }, | 434 | }, |
| 436 | } | 435 | } |
| 437 | } else if (res.request.headers.content_length) |cl| { | 436 | } else if (res.request.content_length) |cl| { |
| 438 | res.request.parser.next_chunk_length = cl; | 437 | res.request.parser.next_chunk_length = cl; |
| 439 | 438 | ||
| 440 | if (cl == 0) res.request.parser.done = true; | 439 | if (cl == 0) res.request.parser.done = true; |
| ... | @@ -443,7 +442,7 @@ pub const Response = struct { | ... | @@ -443,7 +442,7 @@ pub const Response = struct { |
| 443 | } | 442 | } |
| 444 | 443 | ||
| 445 | if (!res.request.parser.done) { | 444 | if (!res.request.parser.done) { |
| 446 | if (res.request.headers.transfer_compression) |tc| switch (tc) { | 445 | if (res.request.transfer_compression) |tc| switch (tc) { |
| 447 | .compress => return error.CompressionNotSupported, | 446 | .compress => return error.CompressionNotSupported, |
| 448 | .deflate => res.request.compression = .{ | 447 | .deflate => res.request.compression = .{ |
| 449 | .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()), | 448 | .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()), |
| ... | @@ -495,7 +494,7 @@ pub const Response = struct { | ... | @@ -495,7 +494,7 @@ pub const Response = struct { |
| 495 | 494 | ||
| 496 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. | 495 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 497 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { | 496 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { |
| 498 | switch (res.headers.transfer_encoding) { | 497 | switch (res.transfer_encoding) { |
| 499 | .chunked => { | 498 | .chunked => { |
| 500 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); | 499 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| 501 | try res.connection.writeAll(bytes); | 500 | try res.connection.writeAll(bytes); |
| ... | @@ -525,7 +524,7 @@ pub const Response = struct { | ... | @@ -525,7 +524,7 @@ pub const Response = struct { |
| 525 | }; | 524 | }; |
| 526 | 525 | ||
| 527 | /// The mode of transport for responses. | 526 | /// The mode of transport for responses. |
| 528 | pub const RequestTransfer = union(enum) { | 527 | pub const ResponseTransfer = union(enum) { |
| 529 | content_length: u64, | 528 | content_length: u64, |
| 530 | chunked: void, | 529 | chunked: void, |
| 531 | none: void, | 530 | none: void, |
| ... | @@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { | ... | @@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { |
| 588 | .stream = in.stream, | 587 | .stream = in.stream, |
| 589 | .protocol = .plain, | 588 | .protocol = .plain, |
| 590 | } }, | 589 | } }, |
| 590 | .headers = .{ .allocator = server.allocator }, | ||
| 591 | .request = .{ | 591 | .request = .{ |
| 592 | .version = undefined, | ||
| 593 | .method = undefined, | ||
| 594 | .target = undefined, | ||
| 592 | .parser = switch (options) { | 595 | .parser = switch (options) { |
| 593 | .dynamic => |max| proto.HeadersParser.initDynamic(max), | 596 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 594 | .static => |buf| proto.HeadersParser.initStatic(buf), | 597 | .static => |buf| proto.HeadersParser.initStatic(buf), |
src/Package.zig+4-1| ... | @@ -479,7 +479,10 @@ fn fetchAndUnpack( | ... | @@ -479,7 +479,10 @@ fn fetchAndUnpack( |
| 479 | }; | 479 | }; |
| 480 | defer tmp_directory.closeAndFree(gpa); | 480 | defer tmp_directory.closeAndFree(gpa); |
| 481 | 481 | ||
| 482 | var req = try http_client.request(uri, .{}, .{}); | 482 | var h = std.http.Headers{ .allocator = gpa }; |
| 483 | defer h.deinit(); | ||
| 484 | |||
| 485 | var req = try http_client.request(uri, h, .{ .method = .GET }); | ||
| 483 | defer req.deinit(); | 486 | defer req.deinit(); |
| 484 | 487 | ||
| 485 | try req.do(); | 488 | try req.do(); |