| author | |
| committer | |
| log | 71c228fe6572b9f3b30e82035bf8fd7e2b1dd29d |
| tree | bc260df4dd4ac6befa2f05d54a39dd8b43548907 |
| parent | d71a43ec2c28a53a3e1d9bcb538707eca00a6fc0 |
| signature |
8 files changed, 546 insertions(+), 134 deletions(-)
lib/std/Uri.zig+6-3| ... | ... | @@ -216,6 +216,7 @@ pub fn format( |
| 216 | 216 | |
| 217 | 217 | const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null; |
| 218 | 218 | const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0; |
| 219 | const needs_fragment = comptime std.mem.indexOf(u8, fmt, "#") != null; | |
| 219 | 220 | |
| 220 | 221 | if (needs_absolute) { |
| 221 | 222 | try writer.writeAll(uri.scheme); |
| ... | ... | @@ -253,9 +254,11 @@ pub fn format( |
| 253 | 254 | try Uri.writeEscapedQuery(writer, q); |
| 254 | 255 | } |
| 255 | 256 | |
| 256 | if (uri.fragment) |f| { | |
| 257 | try writer.writeAll("#"); | |
| 258 | try Uri.writeEscapedQuery(writer, f); | |
| 257 | if (needs_fragment) { | |
| 258 | if (uri.fragment) |f| { | |
| 259 | try writer.writeAll("#"); | |
| 260 | try Uri.writeEscapedQuery(writer, f); | |
| 261 | } | |
| 259 | 262 | } |
| 260 | 263 | } |
| 261 | 264 | } |
lib/std/http.zig-1| ... | ... | @@ -275,5 +275,4 @@ test { |
| 275 | 275 | _ = Client; |
| 276 | 276 | _ = Method; |
| 277 | 277 | _ = Status; |
| 278 | _ = @import("http/test.zig"); | |
| 279 | 278 | } |
lib/std/http/Client.zig+9-7| ... | ... | @@ -264,7 +264,7 @@ pub const BufferedConnection = struct { |
| 264 | 264 | const nread = try bconn.conn.read(bconn.buf[0..]); |
| 265 | 265 | if (nread == 0) return error.EndOfStream; |
| 266 | 266 | bconn.start = 0; |
| 267 | bconn.end = @truncate(u16, nread); | |
| 267 | bconn.end = @intCast(u16, nread); | |
| 268 | 268 | } |
| 269 | 269 | |
| 270 | 270 | pub fn peek(bconn: *BufferedConnection) []const u8 { |
| ... | ... | @@ -282,7 +282,7 @@ pub const BufferedConnection = struct { |
| 282 | 282 | const left = buffer.len - out_index; |
| 283 | 283 | |
| 284 | 284 | if (available > 0) { |
| 285 | const can_read = @truncate(u16, @min(available, left)); | |
| 285 | const can_read = @intCast(u16, @min(available, left)); | |
| 286 | 286 | |
| 287 | 287 | @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]); |
| 288 | 288 | out_index += can_read; |
| ... | ... | @@ -355,8 +355,6 @@ pub const Compression = union(enum) { |
| 355 | 355 | /// A HTTP response originating from a server. |
| 356 | 356 | pub const Response = struct { |
| 357 | 357 | pub const ParseError = Allocator.Error || error{ |
| 358 | ShortHttpStatusLine, | |
| 359 | BadHttpVersion, | |
| 360 | 358 | HttpHeadersInvalid, |
| 361 | 359 | HttpHeaderContinuationsUnsupported, |
| 362 | 360 | HttpTransferEncodingUnsupported, |
| ... | ... | @@ -370,12 +368,12 @@ pub const Response = struct { |
| 370 | 368 | |
| 371 | 369 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 372 | 370 | if (first_line.len < 12) |
| 373 | return error.ShortHttpStatusLine; | |
| 371 | return error.HttpHeadersInvalid; | |
| 374 | 372 | |
| 375 | 373 | const version: http.Version = switch (int64(first_line[0..8])) { |
| 376 | 374 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 377 | 375 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 378 | else => return error.BadHttpVersion, | |
| 376 | else => return error.HttpHeadersInvalid, | |
| 379 | 377 | }; |
| 380 | 378 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; |
| 381 | 379 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); |
| ... | ... | @@ -695,7 +693,6 @@ pub const Request = struct { |
| 695 | 693 | |
| 696 | 694 | if (req.method == .CONNECT and req.response.status == .ok) { |
| 697 | 695 | req.connection.data.closing = false; |
| 698 | req.connection.data.proxied = true; | |
| 699 | 696 | req.response.parser.done = true; |
| 700 | 697 | } |
| 701 | 698 | |
| ... | ... | @@ -725,6 +722,11 @@ pub const Request = struct { |
| 725 | 722 | req.response.parser.done = true; |
| 726 | 723 | } |
| 727 | 724 | |
| 725 | // HEAD requests have no body | |
| 726 | if (req.method == .HEAD) { | |
| 727 | req.response.parser.done = true; | |
| 728 | } | |
| 729 | ||
| 728 | 730 | if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) { |
| 729 | 731 | req.response.skip = true; |
| 730 | 732 |
lib/std/http/Headers.zig-11| ... | ... | @@ -36,17 +36,6 @@ pub const Field = struct { |
| 36 | 36 | name: []const u8, |
| 37 | 37 | value: []const u8, |
| 38 | 38 | |
| 39 | pub fn modify(entry: *Field, allocator: Allocator, new_value: []const u8) !void { | |
| 40 | if (entry.value.len <= new_value.len) { | |
| 41 | // TODO: eliminate this use of `@constCast`. | |
| 42 | @memcpy(@constCast(entry.value)[0..new_value.len], new_value); | |
| 43 | } else { | |
| 44 | allocator.free(entry.value); | |
| 45 | ||
| 46 | entry.value = try allocator.dupe(u8, new_value); | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | 39 | fn lessThan(ctx: void, a: Field, b: Field) bool { |
| 51 | 40 | _ = ctx; |
| 52 | 41 | if (a.name.ptr == b.name.ptr) return false; |
lib/std/http/Server.zig+95-40| ... | ... | @@ -108,7 +108,7 @@ pub const BufferedConnection = struct { |
| 108 | 108 | const nread = try bconn.conn.read(bconn.buf[0..]); |
| 109 | 109 | if (nread == 0) return error.EndOfStream; |
| 110 | 110 | bconn.start = 0; |
| 111 | bconn.end = @truncate(u16, nread); | |
| 111 | bconn.end = @intCast(u16, nread); | |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | 114 | pub fn peek(bconn: *BufferedConnection) []const u8 { |
| ... | ... | @@ -126,7 +126,7 @@ pub const BufferedConnection = struct { |
| 126 | 126 | const left = buffer.len - out_index; |
| 127 | 127 | |
| 128 | 128 | if (available > 0) { |
| 129 | const can_read = @truncate(u16, @min(available, left)); | |
| 129 | const can_read = @intCast(u16, @min(available, left)); | |
| 130 | 130 | |
| 131 | 131 | @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]); |
| 132 | 132 | out_index += can_read; |
| ... | ... | @@ -199,8 +199,6 @@ pub const Compression = union(enum) { |
| 199 | 199 | /// A HTTP request originating from a client. |
| 200 | 200 | pub const Request = struct { |
| 201 | 201 | pub const ParseError = Allocator.Error || error{ |
| 202 | ShortHttpStatusLine, | |
| 203 | BadHttpVersion, | |
| 204 | 202 | UnknownHttpMethod, |
| 205 | 203 | HttpHeadersInvalid, |
| 206 | 204 | HttpHeaderContinuationsUnsupported, |
| ... | ... | @@ -215,7 +213,7 @@ pub const Request = struct { |
| 215 | 213 | |
| 216 | 214 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 217 | 215 | if (first_line.len < 10) |
| 218 | return error.ShortHttpStatusLine; | |
| 216 | return error.HttpHeadersInvalid; | |
| 219 | 217 | |
| 220 | 218 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; |
| 221 | 219 | const method_str = first_line[0..method_end]; |
| ... | ... | @@ -229,7 +227,7 @@ pub const Request = struct { |
| 229 | 227 | const version: http.Version = switch (int64(version_str[0..8])) { |
| 230 | 228 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 231 | 229 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 232 | else => return error.BadHttpVersion, | |
| 230 | else => return error.HttpHeadersInvalid, | |
| 233 | 231 | }; |
| 234 | 232 | |
| 235 | 233 | const target = first_line[method_end + 1 .. version_start]; |
| ... | ... | @@ -312,7 +310,7 @@ pub const Request = struct { |
| 312 | 310 | transfer_encoding: ?http.TransferEncoding = null, |
| 313 | 311 | transfer_compression: ?http.ContentEncoding = null, |
| 314 | 312 | |
| 315 | headers: http.Headers = undefined, | |
| 313 | headers: http.Headers, | |
| 316 | 314 | parser: proto.HeadersParser, |
| 317 | 315 | compression: Compression = .none, |
| 318 | 316 | }; |
| ... | ... | @@ -336,42 +334,92 @@ pub const Response = struct { |
| 336 | 334 | headers: http.Headers, |
| 337 | 335 | request: Request, |
| 338 | 336 | |
| 337 | state: State = .first, | |
| 338 | ||
| 339 | const State = enum { | |
| 340 | first, | |
| 341 | start, | |
| 342 | waited, | |
| 343 | responded, | |
| 344 | finished, | |
| 345 | }; | |
| 346 | ||
| 339 | 347 | pub fn deinit(res: *Response) void { |
| 340 | res.server.allocator.destroy(res); | |
| 341 | } | |
| 348 | res.connection.close(); | |
| 342 | 349 | |
| 343 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. | |
| 344 | pub fn reset(res: *Response) void { | |
| 345 | res.request.headers.deinit(); | |
| 346 | 350 | res.headers.deinit(); |
| 351 | res.request.headers.deinit(); | |
| 347 | 352 | |
| 348 | switch (res.request.compression) { | |
| 349 | .none => {}, | |
| 350 | .deflate => |*deflate| deflate.deinit(), | |
| 351 | .gzip => |*gzip| gzip.deinit(), | |
| 352 | .zstd => |*zstd| zstd.deinit(), | |
| 353 | if (res.request.parser.header_bytes_owned) { | |
| 354 | res.request.parser.header_bytes.deinit(res.server.allocator); | |
| 355 | } | |
| 356 | } | |
| 357 | ||
| 358 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. | |
| 359 | pub fn reset(res: *Response) bool { | |
| 360 | if (res.state == .first) { | |
| 361 | res.state = .start; | |
| 362 | return true; | |
| 353 | 363 | } |
| 354 | 364 | |
| 355 | 365 | if (!res.request.parser.done) { |
| 356 | 366 | // If the response wasn't fully read, then we need to close the connection. |
| 357 | 367 | res.connection.conn.closing = true; |
| 368 | return false; | |
| 358 | 369 | } |
| 359 | 370 | |
| 360 | if (res.connection.conn.closing) { | |
| 361 | res.connection.close(); | |
| 371 | // A connection is only keep-alive if the Connection header is present and it's value is not "close". | |
| 372 | // The server and client must both agree | |
| 373 | const res_connection = res.headers.getFirstValue("connection"); | |
| 374 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | |
| 362 | 375 | |
| 363 | if (res.request.parser.header_bytes_owned) { | |
| 364 | res.request.parser.header_bytes.deinit(res.server.allocator); | |
| 365 | } | |
| 376 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 377 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 378 | if (res_keepalive and req_keepalive) { | |
| 379 | res.connection.conn.closing = false; | |
| 366 | 380 | } else { |
| 367 | res.request.parser.reset(); | |
| 381 | res.connection.conn.closing = true; | |
| 382 | } | |
| 383 | ||
| 384 | switch (res.request.compression) { | |
| 385 | .none => {}, | |
| 386 | .deflate => |*deflate| deflate.deinit(), | |
| 387 | .gzip => |*gzip| gzip.deinit(), | |
| 388 | .zstd => |*zstd| zstd.deinit(), | |
| 368 | 389 | } |
| 390 | ||
| 391 | res.state = .start; | |
| 392 | res.version = .@"HTTP/1.1"; | |
| 393 | res.status = .ok; | |
| 394 | res.reason = null; | |
| 395 | ||
| 396 | res.transfer_encoding = .none; | |
| 397 | ||
| 398 | res.headers.clearRetainingCapacity(); | |
| 399 | ||
| 400 | res.request.headers.clearRetainingCapacity(); | |
| 401 | res.request.parser.reset(); | |
| 402 | ||
| 403 | res.request = Request{ | |
| 404 | .version = undefined, | |
| 405 | .method = undefined, | |
| 406 | .target = undefined, | |
| 407 | .headers = res.request.headers, | |
| 408 | .parser = res.request.parser, | |
| 409 | }; | |
| 410 | ||
| 411 | return !res.connection.conn.closing; | |
| 369 | 412 | } |
| 370 | 413 | |
| 371 | 414 | pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength }; |
| 372 | 415 | |
| 373 | 416 | /// Send the response headers. |
| 374 | 417 | pub fn do(res: *Response) !void { |
| 418 | switch (res.state) { | |
| 419 | .waited => res.state = .responded, | |
| 420 | .first, .start, .responded, .finished => unreachable, | |
| 421 | } | |
| 422 | ||
| 375 | 423 | var buffered = std.io.bufferedWriter(res.connection.writer()); |
| 376 | 424 | const w = buffered.writer(); |
| 377 | 425 | |
| ... | ... | @@ -452,6 +500,11 @@ pub const Response = struct { |
| 452 | 500 | |
| 453 | 501 | /// Wait for the client to send a complete request head. |
| 454 | 502 | pub fn wait(res: *Response) WaitError!void { |
| 503 | switch (res.state) { | |
| 504 | .first, .start => res.state = .waited, | |
| 505 | .waited, .responded, .finished => unreachable, | |
| 506 | } | |
| 507 | ||
| 455 | 508 | while (true) { |
| 456 | 509 | try res.connection.fill(); |
| 457 | 510 | |
| ... | ... | @@ -464,17 +517,6 @@ pub const Response = struct { |
| 464 | 517 | res.request.headers = .{ .allocator = res.server.allocator, .owned = true }; |
| 465 | 518 | try res.request.parse(res.request.parser.header_bytes.items); |
| 466 | 519 | |
| 467 | const res_connection = res.headers.getFirstValue("connection"); | |
| 468 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | |
| 469 | ||
| 470 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 471 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 472 | if (res_keepalive and req_keepalive) { | |
| 473 | res.connection.conn.closing = false; | |
| 474 | } else { | |
| 475 | res.connection.conn.closing = true; | |
| 476 | } | |
| 477 | ||
| 478 | 520 | if (res.request.transfer_encoding) |te| { |
| 479 | 521 | switch (te) { |
| 480 | 522 | .chunked => { |
| ... | ... | @@ -515,6 +557,11 @@ pub const Response = struct { |
| 515 | 557 | } |
| 516 | 558 | |
| 517 | 559 | pub fn read(res: *Response, buffer: []u8) ReadError!usize { |
| 560 | switch (res.state) { | |
| 561 | .waited, .responded, .finished => {}, | |
| 562 | .first, .start => unreachable, | |
| 563 | } | |
| 564 | ||
| 518 | 565 | const out_index = switch (res.request.compression) { |
| 519 | 566 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, |
| 520 | 567 | .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, |
| ... | ... | @@ -564,6 +611,11 @@ pub const Response = struct { |
| 564 | 611 | |
| 565 | 612 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 566 | 613 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { |
| 614 | switch (res.state) { | |
| 615 | .responded => {}, | |
| 616 | .first, .waited, .start, .finished => unreachable, | |
| 617 | } | |
| 618 | ||
| 567 | 619 | switch (res.transfer_encoding) { |
| 568 | 620 | .chunked => { |
| 569 | 621 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| ... | ... | @@ -583,7 +635,7 @@ pub const Response = struct { |
| 583 | 635 | } |
| 584 | 636 | } |
| 585 | 637 | |
| 586 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | |
| 638 | pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void { | |
| 587 | 639 | var index: usize = 0; |
| 588 | 640 | while (index < bytes.len) { |
| 589 | 641 | index += try write(req, bytes[index..]); |
| ... | ... | @@ -594,6 +646,11 @@ pub const Response = struct { |
| 594 | 646 | |
| 595 | 647 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 596 | 648 | pub fn finish(res: *Response) FinishError!void { |
| 649 | switch (res.state) { | |
| 650 | .responded => res.state = .finished, | |
| 651 | .first, .waited, .start, .finished => unreachable, | |
| 652 | } | |
| 653 | ||
| 597 | 654 | switch (res.transfer_encoding) { |
| 598 | 655 | .chunked => try res.connection.writeAll("0\r\n\r\n"), |
| 599 | 656 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| ... | ... | @@ -636,11 +693,10 @@ pub const HeaderStrategy = union(enum) { |
| 636 | 693 | }; |
| 637 | 694 | |
| 638 | 695 | /// Accept a new connection and allocate a Response for it. |
| 639 | pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { | |
| 696 | pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!Response { | |
| 640 | 697 | const in = try server.socket.accept(); |
| 641 | 698 | |
| 642 | const res = try server.allocator.create(Response); | |
| 643 | res.* = .{ | |
| 699 | return Response{ | |
| 644 | 700 | .server = server, |
| 645 | 701 | .address = in.address, |
| 646 | 702 | .connection = .{ .conn = .{ |
| ... | ... | @@ -652,14 +708,13 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { |
| 652 | 708 | .version = undefined, |
| 653 | 709 | .method = undefined, |
| 654 | 710 | .target = undefined, |
| 711 | .headers = .{ .allocator = server.allocator, .owned = false }, | |
| 655 | 712 | .parser = switch (options) { |
| 656 | 713 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 657 | 714 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| 658 | 715 | }, |
| 659 | 716 | }, |
| 660 | 717 | }; |
| 661 | ||
| 662 | return res; | |
| 663 | 718 | } |
| 664 | 719 | |
| 665 | 720 | test "HTTP server handles a chunked transfer coding request" { |
lib/std/http/test.zig deleted-72| ... | ... | @@ -1,72 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const expect = std.testing.expect; | |
| 3 | ||
| 4 | test "client requests server" { | |
| 5 | const builtin = @import("builtin"); | |
| 6 | ||
| 7 | // This test requires spawning threads. | |
| 8 | if (builtin.single_threaded) { | |
| 9 | return error.SkipZigTest; | |
| 10 | } | |
| 11 | ||
| 12 | const native_endian = comptime builtin.cpu.arch.endian(); | |
| 13 | if (builtin.zig_backend == .stage2_llvm and native_endian == .Big) { | |
| 14 | // https://github.com/ziglang/zig/issues/13782 | |
| 15 | return error.SkipZigTest; | |
| 16 | } | |
| 17 | ||
| 18 | if (builtin.os.tag == .wasi) return error.SkipZigTest; | |
| 19 | ||
| 20 | const allocator = std.testing.allocator; | |
| 21 | ||
| 22 | const max_header_size = 8192; | |
| 23 | var server = std.http.Server.init(allocator, .{ .reuse_address = true }); | |
| 24 | defer server.deinit(); | |
| 25 | ||
| 26 | const address = try std.net.Address.parseIp("127.0.0.1", 0); | |
| 27 | try server.listen(address); | |
| 28 | const server_port = server.socket.listen_address.in.getPort(); | |
| 29 | ||
| 30 | const server_thread = try std.Thread.spawn(.{}, (struct { | |
| 31 | fn apply(s: *std.http.Server) !void { | |
| 32 | const res = try s.accept(.{ .dynamic = max_header_size }); | |
| 33 | defer res.deinit(); | |
| 34 | defer res.reset(); | |
| 35 | try res.wait(); | |
| 36 | ||
| 37 | const server_body: []const u8 = "message from server!\n"; | |
| 38 | res.transfer_encoding = .{ .content_length = server_body.len }; | |
| 39 | try res.headers.append("content-type", "text/plain"); | |
| 40 | try res.headers.append("connection", "close"); | |
| 41 | try res.do(); | |
| 42 | ||
| 43 | var buf: [128]u8 = undefined; | |
| 44 | const n = try res.readAll(&buf); | |
| 45 | try expect(std.mem.eql(u8, buf[0..n], "Hello, World!\n")); | |
| 46 | _ = try res.writer().writeAll(server_body); | |
| 47 | try res.finish(); | |
| 48 | } | |
| 49 | }).apply, .{&server}); | |
| 50 | ||
| 51 | var uri_buf: [22]u8 = undefined; | |
| 52 | const uri = try std.Uri.parse(try std.fmt.bufPrint(&uri_buf, "http://127.0.0.1:{d}", .{server_port})); | |
| 53 | var client = std.http.Client{ .allocator = allocator }; | |
| 54 | defer client.deinit(); | |
| 55 | var client_headers = std.http.Headers{ .allocator = allocator }; | |
| 56 | defer client_headers.deinit(); | |
| 57 | var client_req = try client.request(.POST, uri, client_headers, .{}); | |
| 58 | defer client_req.deinit(); | |
| 59 | ||
| 60 | client_req.transfer_encoding = .{ .content_length = 14 }; // this will be checked to ensure you sent exactly 14 bytes | |
| 61 | try client_req.start(); // this sends the request | |
| 62 | try client_req.writeAll("Hello, "); | |
| 63 | try client_req.writeAll("World!\n"); | |
| 64 | try client_req.finish(); | |
| 65 | try client_req.wait(); // this waits for a response | |
| 66 | ||
| 67 | const body = try client_req.reader().readAllAlloc(allocator, 8192 * 1024); | |
| 68 | defer allocator.free(body); | |
| 69 | try expect(std.mem.eql(u8, body, "message from server!\n")); | |
| 70 | ||
| 71 | server_thread.join(); | |
| 72 | } |
test/standalone.zig+4| ... | ... | @@ -55,6 +55,10 @@ pub const simple_cases = [_]SimpleCase{ |
| 55 | 55 | .os_filter = .windows, |
| 56 | 56 | .link_libc = true, |
| 57 | 57 | }, |
| 58 | .{ | |
| 59 | .src_path = "test/standalone/http.zig", | |
| 60 | .all_modes = true, | |
| 61 | }, | |
| 58 | 62 | |
| 59 | 63 | // Ensure the development tools are buildable. Alphabetically sorted. |
| 60 | 64 | // No need to build `tools/spirv/grammar.zig`. |
test/standalone/http.zig created+432| ... | ... | @@ -0,0 +1,432 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | const http = std.http; | |
| 4 | const Server = http.Server; | |
| 5 | const Client = http.Client; | |
| 6 | ||
| 7 | const mem = std.mem; | |
| 8 | const testing = std.testing; | |
| 9 | ||
| 10 | const max_header_size = 8192; | |
| 11 | ||
| 12 | var gpa_server = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 13 | var gpa_client = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 14 | ||
| 15 | const salloc = gpa_server.allocator(); | |
| 16 | const calloc = gpa_client.allocator(); | |
| 17 | ||
| 18 | fn handleRequest(res: *Server.Response) !void { | |
| 19 | const log = std.log.scoped(.server); | |
| 20 | ||
| 21 | log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); | |
| 22 | ||
| 23 | const body = try res.reader().readAllAlloc(salloc, 8192); | |
| 24 | defer salloc.free(body); | |
| 25 | ||
| 26 | if (res.request.headers.contains("connection")) { | |
| 27 | try res.headers.append("connection", "keep-alive"); | |
| 28 | } | |
| 29 | ||
| 30 | if (mem.startsWith(u8, res.request.target, "/get")) { | |
| 31 | if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) { | |
| 32 | res.transfer_encoding = .chunked; | |
| 33 | } else { | |
| 34 | res.transfer_encoding = .{ .content_length = 14 }; | |
| 35 | } | |
| 36 | ||
| 37 | try res.headers.append("content-type", "text/plain"); | |
| 38 | ||
| 39 | try res.do(); | |
| 40 | if (res.request.method != .HEAD) { | |
| 41 | try res.writeAll("Hello, "); | |
| 42 | try res.writeAll("World!\n"); | |
| 43 | try res.finish(); | |
| 44 | } | |
| 45 | } else if (mem.eql(u8, res.request.target, "/echo-content")) { | |
| 46 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 47 | try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); | |
| 48 | ||
| 49 | if (res.request.headers.contains("transfer-encoding")) { | |
| 50 | try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?); | |
| 51 | res.transfer_encoding = .chunked; | |
| 52 | } else { | |
| 53 | res.transfer_encoding = .{ .content_length = 14 }; | |
| 54 | try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?); | |
| 55 | } | |
| 56 | ||
| 57 | try res.do(); | |
| 58 | try res.writeAll("Hello, "); | |
| 59 | try res.writeAll("World!\n"); | |
| 60 | try res.finish(); | |
| 61 | } else if (mem.eql(u8, res.request.target, "/trailer")) { | |
| 62 | res.transfer_encoding = .chunked; | |
| 63 | ||
| 64 | try res.do(); | |
| 65 | try res.writeAll("Hello, "); | |
| 66 | try res.writeAll("World!\n"); | |
| 67 | // try res.finish(); | |
| 68 | try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n"); | |
| 69 | } else if (mem.eql(u8, res.request.target, "/redirect/1")) { | |
| 70 | res.transfer_encoding = .chunked; | |
| 71 | ||
| 72 | res.status = .found; | |
| 73 | try res.headers.append("location", "../../get"); | |
| 74 | ||
| 75 | try res.do(); | |
| 76 | try res.writeAll("Hello, "); | |
| 77 | try res.writeAll("Redirected!\n"); | |
| 78 | try res.finish(); | |
| 79 | } else if (mem.eql(u8, res.request.target, "/redirect/2")) { | |
| 80 | res.transfer_encoding = .chunked; | |
| 81 | ||
| 82 | res.status = .found; | |
| 83 | try res.headers.append("location", "/redirect/1"); | |
| 84 | ||
| 85 | try res.do(); | |
| 86 | try res.writeAll("Hello, "); | |
| 87 | try res.writeAll("Redirected!\n"); | |
| 88 | try res.finish(); | |
| 89 | } else if (mem.eql(u8, res.request.target, "/redirect/3")) { | |
| 90 | res.transfer_encoding = .chunked; | |
| 91 | ||
| 92 | const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{res.server.socket.listen_address.getPort()}); | |
| 93 | defer salloc.free(location); | |
| 94 | ||
| 95 | res.status = .found; | |
| 96 | try res.headers.append("location", location); | |
| 97 | ||
| 98 | try res.do(); | |
| 99 | try res.writeAll("Hello, "); | |
| 100 | try res.writeAll("Redirected!\n"); | |
| 101 | try res.finish(); | |
| 102 | } else if (mem.eql(u8, res.request.target, "/redirect/4")) { | |
| 103 | res.transfer_encoding = .chunked; | |
| 104 | ||
| 105 | res.status = .found; | |
| 106 | try res.headers.append("location", "/redirect/3"); | |
| 107 | ||
| 108 | try res.do(); | |
| 109 | try res.writeAll("Hello, "); | |
| 110 | try res.writeAll("Redirected!\n"); | |
| 111 | try res.finish(); | |
| 112 | } else { | |
| 113 | res.status = .not_found; | |
| 114 | try res.do(); | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | var handle_new_requests = true; | |
| 119 | ||
| 120 | fn runServer(srv: *Server) !void { | |
| 121 | outer: while (handle_new_requests) { | |
| 122 | var res = try srv.accept(.{ .dynamic = max_header_size }); | |
| 123 | defer res.deinit(); | |
| 124 | ||
| 125 | while (res.reset()) { | |
| 126 | res.wait() catch |err| switch (err) { | |
| 127 | error.HttpHeadersInvalid => continue :outer, | |
| 128 | error.EndOfStream => continue, | |
| 129 | else => return err, | |
| 130 | }; | |
| 131 | ||
| 132 | try handleRequest(&res); | |
| 133 | } | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | fn serverThread(srv: *Server) void { | |
| 138 | defer srv.deinit(); | |
| 139 | defer _ = gpa_server.deinit(); | |
| 140 | ||
| 141 | runServer(srv) catch |err| { | |
| 142 | std.debug.print("server error: {}\n", .{err}); | |
| 143 | ||
| 144 | if (@errorReturnTrace()) |trace| { | |
| 145 | std.debug.dumpStackTrace(trace.*); | |
| 146 | } | |
| 147 | ||
| 148 | _ = gpa_server.deinit(); | |
| 149 | std.os.exit(1); | |
| 150 | }; | |
| 151 | } | |
| 152 | ||
| 153 | fn killServer(addr: std.net.Address) void { | |
| 154 | handle_new_requests = false; | |
| 155 | ||
| 156 | const conn = std.net.tcpConnectToAddress(addr) catch return; | |
| 157 | conn.close(); | |
| 158 | } | |
| 159 | ||
| 160 | pub fn main() !void { | |
| 161 | const log = std.log.scoped(.client); | |
| 162 | ||
| 163 | defer _ = gpa_client.deinit(); | |
| 164 | ||
| 165 | var server = Server.init(salloc, .{ .reuse_address = true }); | |
| 166 | ||
| 167 | const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable; | |
| 168 | try server.listen(addr); | |
| 169 | ||
| 170 | const port = server.socket.listen_address.getPort(); | |
| 171 | ||
| 172 | const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server}); | |
| 173 | ||
| 174 | var client = Client{ .allocator = calloc }; | |
| 175 | ||
| 176 | defer client.deinit(); | |
| 177 | ||
| 178 | { // read content-length response | |
| 179 | var h = http.Headers{ .allocator = calloc }; | |
| 180 | defer h.deinit(); | |
| 181 | ||
| 182 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | |
| 183 | defer calloc.free(location); | |
| 184 | const uri = try std.Uri.parse(location); | |
| 185 | ||
| 186 | log.info("{s}", .{location}); | |
| 187 | var req = try client.request(.GET, uri, h, .{}); | |
| 188 | defer req.deinit(); | |
| 189 | ||
| 190 | try req.start(); | |
| 191 | try req.wait(); | |
| 192 | ||
| 193 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 194 | defer calloc.free(body); | |
| 195 | ||
| 196 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 197 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 198 | } | |
| 199 | ||
| 200 | { // send head request and not read chunked | |
| 201 | var h = http.Headers{ .allocator = calloc }; | |
| 202 | defer h.deinit(); | |
| 203 | ||
| 204 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | |
| 205 | defer calloc.free(location); | |
| 206 | const uri = try std.Uri.parse(location); | |
| 207 | ||
| 208 | log.info("{s}", .{location}); | |
| 209 | var req = try client.request(.HEAD, uri, h, .{}); | |
| 210 | defer req.deinit(); | |
| 211 | ||
| 212 | try req.start(); | |
| 213 | try req.wait(); | |
| 214 | ||
| 215 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 216 | defer calloc.free(body); | |
| 217 | ||
| 218 | try testing.expectEqualStrings("", body); | |
| 219 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 220 | try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?); | |
| 221 | } | |
| 222 | ||
| 223 | { // read chunked response | |
| 224 | var h = http.Headers{ .allocator = calloc }; | |
| 225 | defer h.deinit(); | |
| 226 | ||
| 227 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); | |
| 228 | defer calloc.free(location); | |
| 229 | const uri = try std.Uri.parse(location); | |
| 230 | ||
| 231 | log.info("{s}", .{location}); | |
| 232 | var req = try client.request(.GET, uri, h, .{}); | |
| 233 | defer req.deinit(); | |
| 234 | ||
| 235 | try req.start(); | |
| 236 | try req.wait(); | |
| 237 | ||
| 238 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 239 | defer calloc.free(body); | |
| 240 | ||
| 241 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 242 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 243 | } | |
| 244 | ||
| 245 | { // send head request and not read chunked | |
| 246 | var h = http.Headers{ .allocator = calloc }; | |
| 247 | defer h.deinit(); | |
| 248 | ||
| 249 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); | |
| 250 | defer calloc.free(location); | |
| 251 | const uri = try std.Uri.parse(location); | |
| 252 | ||
| 253 | log.info("{s}", .{location}); | |
| 254 | var req = try client.request(.HEAD, uri, h, .{}); | |
| 255 | defer req.deinit(); | |
| 256 | ||
| 257 | try req.start(); | |
| 258 | try req.wait(); | |
| 259 | ||
| 260 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 261 | defer calloc.free(body); | |
| 262 | ||
| 263 | try testing.expectEqualStrings("", body); | |
| 264 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 265 | try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?); | |
| 266 | } | |
| 267 | ||
| 268 | { // check trailing headers | |
| 269 | var h = http.Headers{ .allocator = calloc }; | |
| 270 | defer h.deinit(); | |
| 271 | ||
| 272 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port}); | |
| 273 | defer calloc.free(location); | |
| 274 | const uri = try std.Uri.parse(location); | |
| 275 | ||
| 276 | log.info("{s}", .{location}); | |
| 277 | var req = try client.request(.GET, uri, h, .{}); | |
| 278 | defer req.deinit(); | |
| 279 | ||
| 280 | try req.start(); | |
| 281 | try req.wait(); | |
| 282 | ||
| 283 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 284 | defer calloc.free(body); | |
| 285 | ||
| 286 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 287 | try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?); | |
| 288 | } | |
| 289 | ||
| 290 | { // send content-length request | |
| 291 | var h = http.Headers{ .allocator = calloc }; | |
| 292 | defer h.deinit(); | |
| 293 | ||
| 294 | try h.append("content-type", "text/plain"); | |
| 295 | ||
| 296 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 297 | defer calloc.free(location); | |
| 298 | const uri = try std.Uri.parse(location); | |
| 299 | ||
| 300 | log.info("{s}", .{location}); | |
| 301 | var req = try client.request(.POST, uri, h, .{}); | |
| 302 | defer req.deinit(); | |
| 303 | ||
| 304 | req.transfer_encoding = .{ .content_length = 14 }; | |
| 305 | ||
| 306 | try req.start(); | |
| 307 | try req.writeAll("Hello, "); | |
| 308 | try req.writeAll("World!\n"); | |
| 309 | try req.finish(); | |
| 310 | ||
| 311 | try req.wait(); | |
| 312 | ||
| 313 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 314 | defer calloc.free(body); | |
| 315 | ||
| 316 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 317 | } | |
| 318 | ||
| 319 | { // send chunked request | |
| 320 | var h = http.Headers{ .allocator = calloc }; | |
| 321 | defer h.deinit(); | |
| 322 | ||
| 323 | try h.append("content-type", "text/plain"); | |
| 324 | ||
| 325 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 326 | defer calloc.free(location); | |
| 327 | const uri = try std.Uri.parse(location); | |
| 328 | ||
| 329 | log.info("{s}", .{location}); | |
| 330 | var req = try client.request(.POST, uri, h, .{}); | |
| 331 | defer req.deinit(); | |
| 332 | ||
| 333 | req.transfer_encoding = .chunked; | |
| 334 | ||
| 335 | try req.start(); | |
| 336 | try req.writeAll("Hello, "); | |
| 337 | try req.writeAll("World!\n"); | |
| 338 | try req.finish(); | |
| 339 | ||
| 340 | try req.wait(); | |
| 341 | ||
| 342 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 343 | defer calloc.free(body); | |
| 344 | ||
| 345 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 346 | } | |
| 347 | ||
| 348 | { // relative redirect | |
| 349 | var h = http.Headers{ .allocator = calloc }; | |
| 350 | defer h.deinit(); | |
| 351 | ||
| 352 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port}); | |
| 353 | defer calloc.free(location); | |
| 354 | const uri = try std.Uri.parse(location); | |
| 355 | ||
| 356 | log.info("{s}", .{location}); | |
| 357 | var req = try client.request(.GET, uri, h, .{}); | |
| 358 | defer req.deinit(); | |
| 359 | ||
| 360 | try req.start(); | |
| 361 | try req.wait(); | |
| 362 | ||
| 363 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 364 | defer calloc.free(body); | |
| 365 | ||
| 366 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 367 | } | |
| 368 | ||
| 369 | { // redirect from root | |
| 370 | var h = http.Headers{ .allocator = calloc }; | |
| 371 | defer h.deinit(); | |
| 372 | ||
| 373 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port}); | |
| 374 | defer calloc.free(location); | |
| 375 | const uri = try std.Uri.parse(location); | |
| 376 | ||
| 377 | log.info("{s}", .{location}); | |
| 378 | var req = try client.request(.GET, uri, h, .{}); | |
| 379 | defer req.deinit(); | |
| 380 | ||
| 381 | try req.start(); | |
| 382 | try req.wait(); | |
| 383 | ||
| 384 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 385 | defer calloc.free(body); | |
| 386 | ||
| 387 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 388 | } | |
| 389 | ||
| 390 | { // absolute redirect | |
| 391 | var h = http.Headers{ .allocator = calloc }; | |
| 392 | defer h.deinit(); | |
| 393 | ||
| 394 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port}); | |
| 395 | defer calloc.free(location); | |
| 396 | const uri = try std.Uri.parse(location); | |
| 397 | ||
| 398 | log.info("{s}", .{location}); | |
| 399 | var req = try client.request(.GET, uri, h, .{}); | |
| 400 | defer req.deinit(); | |
| 401 | ||
| 402 | try req.start(); | |
| 403 | try req.wait(); | |
| 404 | ||
| 405 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 406 | defer calloc.free(body); | |
| 407 | ||
| 408 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 409 | } | |
| 410 | ||
| 411 | { // too many redirects | |
| 412 | var h = http.Headers{ .allocator = calloc }; | |
| 413 | defer h.deinit(); | |
| 414 | ||
| 415 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port}); | |
| 416 | defer calloc.free(location); | |
| 417 | const uri = try std.Uri.parse(location); | |
| 418 | ||
| 419 | log.info("{s}", .{location}); | |
| 420 | var req = try client.request(.GET, uri, h, .{}); | |
| 421 | defer req.deinit(); | |
| 422 | ||
| 423 | try req.start(); | |
| 424 | req.wait() catch |err| switch (err) { | |
| 425 | error.TooManyHttpRedirects => {}, | |
| 426 | else => return err, | |
| 427 | }; | |
| 428 | } | |
| 429 | ||
| 430 | killServer(server.socket.listen_address); | |
| 431 | server_thread.join(); | |
| 432 | } |