| author | |
| committer | |
| log | ef6d58ed3b03b2a5d08489daee0c232d5e113d55 |
| tree | 57026931ce164f75ece056989d224eaa58fc9901 |
| parent | 8250a925444254c6c0c84af3006d04b7102a3343 |
| signature |
3 files changed, 73 insertions(+), 30 deletions(-)
lib/std/http/Client.zig+19-9| ... | ... | @@ -15,8 +15,6 @@ const proto = @import("protocol.zig"); |
| 15 | 15 | pub const default_connection_pool_size = 32; |
| 16 | 16 | pub const connection_pool_size = std.options.http_connection_pool_size; |
| 17 | 17 | |
| 18 | /// Used for tcpConnectToHost and storing HTTP headers when an externally | |
| 19 | /// managed buffer is not provided. | |
| 20 | 18 | allocator: Allocator, |
| 21 | 19 | ca_bundle: std.crypto.Certificate.Bundle = .{}, |
| 22 | 20 | ca_bundle_mutex: std.Thread.Mutex = .{}, |
| ... | ... | @@ -24,8 +22,10 @@ ca_bundle_mutex: std.Thread.Mutex = .{}, |
| 24 | 22 | /// it will first rescan the system for root certificates. |
| 25 | 23 | next_https_rescan_certs: bool = true, |
| 26 | 24 | |
| 25 | /// The pool of connections that can be reused (and currently in use). | |
| 27 | 26 | connection_pool: ConnectionPool = .{}, |
| 28 | 27 | |
| 28 | /// The last error that occurred on this client. This is not threadsafe, do not expect it to be completely accurate. | |
| 29 | 29 | last_error: ?ExtraError = null, |
| 30 | 30 | |
| 31 | 31 | pub const ExtraError = union(enum) { |
| ... | ... | @@ -68,7 +68,9 @@ pub const ExtraError = union(enum) { |
| 68 | 68 | decompress: DecompressError, // error.ReadFailed |
| 69 | 69 | }; |
| 70 | 70 | |
| 71 | /// A set of linked lists of connections that can be reused. | |
| 71 | 72 | pub const ConnectionPool = struct { |
| 73 | /// The criteria for a connection to be considered a match. | |
| 72 | 74 | pub const Criteria = struct { |
| 73 | 75 | host: []const u8, |
| 74 | 76 | port: u16, |
| ... | ... | @@ -92,7 +94,9 @@ pub const ConnectionPool = struct { |
| 92 | 94 | pub const Node = Queue.Node; |
| 93 | 95 | |
| 94 | 96 | mutex: std.Thread.Mutex = .{}, |
| 97 | /// Open connections that are currently in use. | |
| 95 | 98 | used: Queue = .{}, |
| 99 | /// Open connections that are not currently in use. | |
| 96 | 100 | free: Queue = .{}, |
| 97 | 101 | free_len: usize = 0, |
| 98 | 102 | free_size: usize = connection_pool_size, |
| ... | ... | @@ -189,6 +193,7 @@ pub const ConnectionPool = struct { |
| 189 | 193 | } |
| 190 | 194 | }; |
| 191 | 195 | |
| 196 | /// An interface to either a plain or TLS connection. | |
| 192 | 197 | pub const Connection = struct { |
| 193 | 198 | stream: net.Stream, |
| 194 | 199 | /// undefined unless protocol is tls. |
| ... | ... | @@ -261,6 +266,7 @@ pub const Connection = struct { |
| 261 | 266 | } |
| 262 | 267 | }; |
| 263 | 268 | |
| 269 | /// A buffered (and peekable) Connection. | |
| 264 | 270 | pub const BufferedConnection = struct { |
| 265 | 271 | pub const buffer_size = 0x2000; |
| 266 | 272 | |
| ... | ... | @@ -344,12 +350,14 @@ pub const BufferedConnection = struct { |
| 344 | 350 | } |
| 345 | 351 | }; |
| 346 | 352 | |
| 353 | /// The mode of transport for requests. | |
| 347 | 354 | pub const RequestTransfer = union(enum) { |
| 348 | 355 | content_length: u64, |
| 349 | 356 | chunked: void, |
| 350 | 357 | none: void, |
| 351 | 358 | }; |
| 352 | 359 | |
| 360 | /// The decompressor for response messages. | |
| 353 | 361 | pub const Compression = union(enum) { |
| 354 | 362 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader); |
| 355 | 363 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader); |
| ... | ... | @@ -361,6 +369,7 @@ pub const Compression = union(enum) { |
| 361 | 369 | none: void, |
| 362 | 370 | }; |
| 363 | 371 | |
| 372 | /// A HTTP response originating from a server. | |
| 364 | 373 | pub const Response = struct { |
| 365 | 374 | pub const Headers = struct { |
| 366 | 375 | status: http.Status, |
| ... | ... | @@ -501,14 +510,9 @@ pub const Response = struct { |
| 501 | 510 | skip: bool = false, |
| 502 | 511 | }; |
| 503 | 512 | |
| 504 | /// A HTTP request. | |
| 513 | /// A HTTP request that has been sent. | |
| 505 | 514 | /// |
| 506 | /// Order of operations: | |
| 507 | /// - request | |
| 508 | /// - write | |
| 509 | /// - finish | |
| 510 | /// - do | |
| 511 | /// - read | |
| 515 | /// Order of operations: request[ -> write -> finish] -> do -> read | |
| 512 | 516 | pub const Request = struct { |
| 513 | 517 | pub const Headers = struct { |
| 514 | 518 | version: http.Version = .@"HTTP/1.1", |
| ... | ... | @@ -862,6 +866,8 @@ pub const Request = struct { |
| 862 | 866 | } |
| 863 | 867 | }; |
| 864 | 868 | |
| 869 | /// Release all associated resources with the client. | |
| 870 | /// TODO: currently leaks all request allocated data | |
| 865 | 871 | pub fn deinit(client: *Client) void { |
| 866 | 872 | client.connection_pool.deinit(client); |
| 867 | 873 | |
| ... | ... | @@ -871,6 +877,8 @@ pub fn deinit(client: *Client) void { |
| 871 | 877 | |
| 872 | 878 | pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed }; |
| 873 | 879 | |
| 880 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. | |
| 881 | /// This function is threadsafe. | |
| 874 | 882 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| 875 | 883 | if (client.connection_pool.findConnection(.{ |
| 876 | 884 | .host = host, |
| ... | ... | @@ -955,6 +963,8 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ |
| 955 | 963 | .{ "wss", .tls }, |
| 956 | 964 | }); |
| 957 | 965 | |
| 966 | /// Form and send a http request to a server. | |
| 967 | /// This function is threadsafe. | |
| 958 | 968 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { |
| 959 | 969 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 960 | 970 |
lib/std/http/Server.zig+22-7| ... | ... | @@ -14,10 +14,7 @@ allocator: Allocator, |
| 14 | 14 | |
| 15 | 15 | socket: net.StreamServer, |
| 16 | 16 | |
| 17 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader); | |
| 18 | pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader); | |
| 19 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{}); | |
| 20 | ||
| 17 | /// An interface to either a plain or TLS connection. | |
| 21 | 18 | pub const Connection = struct { |
| 22 | 19 | stream: net.Stream, |
| 23 | 20 | protocol: Protocol, |
| ... | ... | @@ -74,6 +71,7 @@ pub const Connection = struct { |
| 74 | 71 | } |
| 75 | 72 | }; |
| 76 | 73 | |
| 74 | /// A buffered (and peekable) Connection. | |
| 77 | 75 | pub const BufferedConnection = struct { |
| 78 | 76 | pub const buffer_size = 0x2000; |
| 79 | 77 | |
| ... | ... | @@ -157,6 +155,7 @@ pub const BufferedConnection = struct { |
| 157 | 155 | } |
| 158 | 156 | }; |
| 159 | 157 | |
| 158 | /// A HTTP request originating from a client. | |
| 160 | 159 | pub const Request = struct { |
| 161 | 160 | pub const Headers = struct { |
| 162 | 161 | method: http.Method, |
| ... | ... | @@ -290,6 +289,11 @@ pub const Request = struct { |
| 290 | 289 | compression: Compression = .none, |
| 291 | 290 | }; |
| 292 | 291 | |
| 292 | /// A HTTP response waiting to be sent. | |
| 293 | /// | |
| 294 | /// [/ <----------------------------------- \] | |
| 295 | /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] | |
| 296 | /// \ -> read / | |
| 293 | 297 | pub const Response = struct { |
| 294 | 298 | pub const Headers = struct { |
| 295 | 299 | version: http.Version = .@"HTTP/1.1", |
| ... | ... | @@ -310,6 +314,7 @@ pub const Response = struct { |
| 310 | 314 | headers: Headers = .{}, |
| 311 | 315 | request: Request, |
| 312 | 316 | |
| 317 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. | |
| 313 | 318 | pub fn reset(res: *Response) void { |
| 314 | 319 | switch (res.request.compression) { |
| 315 | 320 | .none => {}, |
| ... | ... | @@ -336,7 +341,8 @@ pub const Response = struct { |
| 336 | 341 | } |
| 337 | 342 | } |
| 338 | 343 | |
| 339 | pub fn sendResponseHead(res: *Response) !void { | |
| 344 | /// Send the response headers. | |
| 345 | pub fn do(res: *Response) !void { | |
| 340 | 346 | var buffered = std.io.bufferedWriter(res.connection.writer()); |
| 341 | 347 | const w = buffered.writer(); |
| 342 | 348 | |
| ... | ... | @@ -402,7 +408,8 @@ pub const Response = struct { |
| 402 | 408 | |
| 403 | 409 | pub const WaitForCompleteHeadError = BufferedConnection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Request.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported}; |
| 404 | 410 | |
| 405 | pub fn waitForCompleteHead(res: *Response) !void { | |
| 411 | /// Wait for the client to send a complete request head. | |
| 412 | pub fn wait(res: *Response) !void { | |
| 406 | 413 | while (true) { |
| 407 | 414 | try res.connection.fill(); |
| 408 | 415 | |
| ... | ... | @@ -451,7 +458,7 @@ pub const Response = struct { |
| 451 | 458 | } |
| 452 | 459 | } |
| 453 | 460 | |
| 454 | pub const ReadError = DeflateDecompressor.Error || GzipDecompressor.Error || ZstdDecompressor.Error || WaitForCompleteHeadError; | |
| 461 | pub const ReadError = Compression.DeflateDecompressor.Error || Compression.GzipDecompressor.Error || Compression.ZstdDecompressor.Error || WaitForCompleteHeadError; | |
| 455 | 462 | |
| 456 | 463 | pub const Reader = std.io.Reader(*Response, ReadError, read); |
| 457 | 464 | |
| ... | ... | @@ -517,13 +524,19 @@ pub const Response = struct { |
| 517 | 524 | } |
| 518 | 525 | }; |
| 519 | 526 | |
| 527 | /// The mode of transport for responses. | |
| 520 | 528 | pub const RequestTransfer = union(enum) { |
| 521 | 529 | content_length: u64, |
| 522 | 530 | chunked: void, |
| 523 | 531 | none: void, |
| 524 | 532 | }; |
| 525 | 533 | |
| 534 | /// The decompressor for request messages. | |
| 526 | 535 | pub const Compression = union(enum) { |
| 536 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader); | |
| 537 | pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader); | |
| 538 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{}); | |
| 539 | ||
| 527 | 540 | deflate: DeflateDecompressor, |
| 528 | 541 | gzip: GzipDecompressor, |
| 529 | 542 | zstd: ZstdDecompressor, |
| ... | ... | @@ -543,6 +556,7 @@ pub fn deinit(server: *Server) void { |
| 543 | 556 | |
| 544 | 557 | pub const ListenError = std.os.SocketError || std.os.BindError || std.os.ListenError || std.os.SetSockOptError || std.os.GetSockNameError; |
| 545 | 558 | |
| 559 | /// Start the HTTP server listening on the given address. | |
| 546 | 560 | pub fn listen(server: *Server, address: net.Address) !void { |
| 547 | 561 | try server.socket.listen(address); |
| 548 | 562 | } |
| ... | ... | @@ -562,6 +576,7 @@ pub const HeaderStrategy = union(enum) { |
| 562 | 576 | static: []u8, |
| 563 | 577 | }; |
| 564 | 578 | |
| 579 | /// Accept a new connection and allocate a Response for it. | |
| 565 | 580 | pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { |
| 566 | 581 | const in = try server.socket.accept(); |
| 567 | 582 |
lib/std/http/protocol.zig+32-14| ... | ... | @@ -21,6 +21,7 @@ pub const State = enum { |
| 21 | 21 | chunk_data_suffix, |
| 22 | 22 | chunk_data_suffix_r, |
| 23 | 23 | |
| 24 | /// Returns true if the parser is in a content state (ie. not waiting for more headers). | |
| 24 | 25 | pub fn isContent(self: State) bool { |
| 25 | 26 | return switch (self) { |
| 26 | 27 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false, |
| ... | ... | @@ -31,7 +32,7 @@ pub const State = enum { |
| 31 | 32 | |
| 32 | 33 | pub const HeadersParser = struct { |
| 33 | 34 | state: State = .start, |
| 34 | /// Wether or not `header_bytes` is allocated or was provided as a fixed buffer. | |
| 35 | /// Whether or not `header_bytes` is allocated or was provided as a fixed buffer. | |
| 35 | 36 | header_bytes_owned: bool, |
| 36 | 37 | /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`. |
| 37 | 38 | /// Pointers into this buffer are not stable until after a message is complete. |
| ... | ... | @@ -39,10 +40,11 @@ pub const HeadersParser = struct { |
| 39 | 40 | /// The maximum allowed size of `header_bytes`. |
| 40 | 41 | max_header_bytes: usize, |
| 41 | 42 | next_chunk_length: u64 = 0, |
| 42 | /// Wether this parser is done parsing a complete message. | |
| 43 | /// A message is only done when the entire payload has been read | |
| 43 | /// Whether this parser is done parsing a complete message. | |
| 44 | /// A message is only done when the entire payload has been read. | |
| 44 | 45 | done: bool = false, |
| 45 | 46 | |
| 47 | /// Initializes the parser with a dynamically growing header buffer of up to `max` bytes. | |
| 46 | 48 | pub fn initDynamic(max: usize) HeadersParser { |
| 47 | 49 | return .{ |
| 48 | 50 | .header_bytes = .{}, |
| ... | ... | @@ -51,6 +53,7 @@ pub const HeadersParser = struct { |
| 51 | 53 | }; |
| 52 | 54 | } |
| 53 | 55 | |
| 56 | /// Initializes the parser with a provided buffer `buf`. | |
| 54 | 57 | pub fn initStatic(buf: []u8) HeadersParser { |
| 55 | 58 | return .{ |
| 56 | 59 | .header_bytes = .{ .items = buf[0..0], .capacity = buf.len }, |
| ... | ... | @@ -59,7 +62,11 @@ pub const HeadersParser = struct { |
| 59 | 62 | }; |
| 60 | 63 | } |
| 61 | 64 | |
| 65 | /// Completely resets the parser to it's initial state. | |
| 66 | /// This must be called after a message is complete. | |
| 62 | 67 | pub fn reset(r: *HeadersParser) void { |
| 68 | assert(r.done); // The message must be completely read before reset, otherwise the parser is in an invalid state. | |
| 69 | ||
| 63 | 70 | r.header_bytes.clearRetainingCapacity(); |
| 64 | 71 | |
| 65 | 72 | r.* = .{ |
| ... | ... | @@ -69,13 +76,14 @@ pub const HeadersParser = struct { |
| 69 | 76 | }; |
| 70 | 77 | } |
| 71 | 78 | |
| 72 | /// Returns how many bytes are part of HTTP headers. Always less than or | |
| 73 | /// equal to bytes.len. If the amount returned is less than bytes.len, it | |
| 74 | /// means the headers ended and the first byte after the double \r\n\r\n is | |
| 75 | /// located at `bytes[result]`. | |
| 79 | /// Returns the number of bytes consumed by headers. This is always less than or equal to `bytes.len`. | |
| 80 | /// You should check `r.state.isContent()` after this to check if the headers are done. | |
| 81 | /// | |
| 82 | /// If the amount returned is less than `bytes.len`, you may assume that the parser is in a content state and the | |
| 83 | /// first byte of content is located at `bytes[result]`. | |
| 76 | 84 | pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 { |
| 77 | const vector_len = 16; | |
| 78 | const len = @truncate(u32, bytes.len); | |
| 85 | const vector_len: comptime_int = comptime std.simd.suggestVectorSize(u8) orelse 8; | |
| 86 | const len = @intCast(u32, bytes.len); | |
| 79 | 87 | var index: u32 = 0; |
| 80 | 88 | |
| 81 | 89 | while (true) { |
| ... | ... | @@ -390,8 +398,13 @@ pub const HeadersParser = struct { |
| 390 | 398 | } |
| 391 | 399 | } |
| 392 | 400 | |
| 401 | /// Returns the number of bytes consumed by the chunk size. This is always less than or equal to `bytes.len`. | |
| 402 | /// You should check `r.state == .chunk_data` after this to check if the chunk size has been fully parsed. | |
| 403 | /// | |
| 404 | /// If the amount returned is less than `bytes.len`, you may assume that the parser is in the `chunk_data` state | |
| 405 | /// and that the first byte of the chunk is at `bytes[result]`. | |
| 393 | 406 | pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 { |
| 394 | const len = @truncate(u32, bytes.len); | |
| 407 | const len = @intCast(u32, bytes.len); | |
| 395 | 408 | |
| 396 | 409 | for (bytes[0..], 0..) |c, i| { |
| 397 | 410 | const index = @intCast(u32, i); |
| ... | ... | @@ -471,8 +484,10 @@ pub const HeadersParser = struct { |
| 471 | 484 | |
| 472 | 485 | pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit}; |
| 473 | 486 | |
| 474 | /// Pumps `in` bytes into the parser. Returns the number of bytes consumed. This function will return 0 if the parser | |
| 475 | /// is not in a state to parse more headers. | |
| 487 | /// Pushes `in` into the parser. Returns the number of bytes consumed by the header. Any header bytes are appended | |
| 488 | /// to the `header_bytes` buffer. | |
| 489 | /// | |
| 490 | /// This function only uses `allocator` if `r.header_bytes_owned` is true, and may be undefined otherwise. | |
| 476 | 491 | pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 { |
| 477 | 492 | if (r.state.isContent()) return 0; |
| 478 | 493 | |
| ... | ... | @@ -493,8 +508,11 @@ pub const HeadersParser = struct { |
| 493 | 508 | HttpChunkInvalid, |
| 494 | 509 | }; |
| 495 | 510 | |
| 496 | /// Reads the body of the message into `buffer`. If `skip` is true, the buffer will be unused and the body will be | |
| 497 | /// skipped. Returns the number of bytes placed in the buffer. | |
| 511 | /// Reads the body of the message into `buffer`. Returns the number of bytes placed in the buffer. | |
| 512 | /// | |
| 513 | /// If `skip` is true, the buffer will be unused and the body will be skipped. | |
| 514 | /// | |
| 515 | /// See `std.http.Client.BufferedConnection for an example of `bconn`. | |
| 498 | 516 | pub fn read(r: *HeadersParser, bconn: anytype, buffer: []u8, skip: bool) !usize { |
| 499 | 517 | assert(r.state.isContent()); |
| 500 | 518 | if (r.done) return 0; |