| author | |
| committer | |
| log | b723296e1fa65b73a43b0790bdddcbfcea7d656d |
| tree | 33173696d644f655374e134b73df3d694e4a1a28 |
| parent | 832f6d8f7f7f7b10b86b109a8b26bb5eacc8d13e |
4 files changed, 141 insertions(+), 13 deletions(-)
lib/std/http/Client.zig+50-3| ... | ... | @@ -1,4 +1,8 @@ |
| 1 | //! Connecting and opening requests are threadsafe. Individual requests are not. | |
| 1 | //! HTTP(S) Client implementation. | |
| 2 | //! | |
| 3 | //! Connections are opened in a thread-safe manner, but individual Requests are not. | |
| 4 | //! | |
| 5 | //! TLS support may be disabled via `std.options.http_disable_tls`. | |
| 2 | 6 | |
| 3 | 7 | const std = @import("../std.zig"); |
| 4 | 8 | const builtin = @import("builtin"); |
| ... | ... | @@ -157,6 +161,9 @@ pub const ConnectionPool = struct { |
| 157 | 161 | pool.free_size = new_size; |
| 158 | 162 | } |
| 159 | 163 | |
| 164 | /// Frees the connection pool and closes all connections within. This function is threadsafe. | |
| 165 | /// | |
| 166 | /// All future operations on the connection pool will deadlock. | |
| 160 | 167 | pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void { |
| 161 | 168 | pool.mutex.lock(); |
| 162 | 169 | |
| ... | ... | @@ -191,11 +198,19 @@ pub const Connection = struct { |
| 191 | 198 | /// undefined unless protocol is tls. |
| 192 | 199 | tls_client: if (!disable_tls) *std.crypto.tls.Client else void, |
| 193 | 200 | |
| 201 | /// The protocol that this connection is using. | |
| 194 | 202 | protocol: Protocol, |
| 203 | ||
| 204 | /// The host that this connection is connected to. | |
| 195 | 205 | host: []u8, |
| 206 | ||
| 207 | /// The port that this connection is connected to. | |
| 196 | 208 | port: u16, |
| 197 | 209 | |
| 210 | /// Whether this connection is proxied and is not directly connected. | |
| 198 | 211 | proxied: bool = false, |
| 212 | ||
| 213 | /// Whether this connection is closing when we're done with it. | |
| 199 | 214 | closing: bool = false, |
| 200 | 215 | |
| 201 | 216 | read_start: BufferSize = 0, |
| ... | ... | @@ -232,6 +247,7 @@ pub const Connection = struct { |
| 232 | 247 | }; |
| 233 | 248 | } |
| 234 | 249 | |
| 250 | /// Refills the read buffer with data from the connection. | |
| 235 | 251 | pub fn fill(conn: *Connection) ReadError!void { |
| 236 | 252 | if (conn.read_end != conn.read_start) return; |
| 237 | 253 | |
| ... | ... | @@ -244,14 +260,17 @@ pub const Connection = struct { |
| 244 | 260 | conn.read_end = @intCast(nread); |
| 245 | 261 | } |
| 246 | 262 | |
| 263 | /// Returns the current slice of buffered data. | |
| 247 | 264 | pub fn peek(conn: *Connection) []const u8 { |
| 248 | 265 | return conn.read_buf[conn.read_start..conn.read_end]; |
| 249 | 266 | } |
| 250 | 267 | |
| 268 | /// Discards the given number of bytes from the read buffer. | |
| 251 | 269 | pub fn drop(conn: *Connection, num: BufferSize) void { |
| 252 | 270 | conn.read_start += num; |
| 253 | 271 | } |
| 254 | 272 | |
| 273 | /// Reads data from the connection into the given buffer. | |
| 255 | 274 | pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { |
| 256 | 275 | const available_read = conn.read_end - conn.read_start; |
| 257 | 276 | const available_buffer = buffer.len; |
| ... | ... | @@ -318,6 +337,7 @@ pub const Connection = struct { |
| 318 | 337 | }; |
| 319 | 338 | } |
| 320 | 339 | |
| 340 | /// Writes the given buffer to the connection. | |
| 321 | 341 | pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize { |
| 322 | 342 | if (conn.write_end + buffer.len > conn.write_buf.len) { |
| 323 | 343 | try conn.flush(); |
| ... | ... | @@ -334,6 +354,7 @@ pub const Connection = struct { |
| 334 | 354 | return buffer.len; |
| 335 | 355 | } |
| 336 | 356 | |
| 357 | /// Flushes the write buffer to the connection. | |
| 337 | 358 | pub fn flush(conn: *Connection) WriteError!void { |
| 338 | 359 | if (conn.write_end == 0) return; |
| 339 | 360 | |
| ... | ... | @@ -352,6 +373,7 @@ pub const Connection = struct { |
| 352 | 373 | return Writer{ .context = conn }; |
| 353 | 374 | } |
| 354 | 375 | |
| 376 | /// Closes the connection. | |
| 355 | 377 | pub fn close(conn: *Connection, allocator: Allocator) void { |
| 356 | 378 | if (conn.protocol == .tls) { |
| 357 | 379 | if (disable_tls) unreachable; |
| ... | ... | @@ -502,8 +524,13 @@ pub const Response = struct { |
| 502 | 524 | try expectEqual(@as(u10, 999), parseInt3("999")); |
| 503 | 525 | } |
| 504 | 526 | |
| 527 | /// The HTTP version this response is using. | |
| 505 | 528 | version: http.Version, |
| 529 | ||
| 530 | /// The status code of the response. | |
| 506 | 531 | status: http.Status, |
| 532 | ||
| 533 | /// The reason phrase of the response. | |
| 507 | 534 | reason: []const u8, |
| 508 | 535 | |
| 509 | 536 | /// If present, the number of bytes in the response body. |
| ... | ... | @@ -528,22 +555,36 @@ pub const Response = struct { |
| 528 | 555 | /// |
| 529 | 556 | /// Order of operations: open -> send[ -> write -> finish] -> wait -> read |
| 530 | 557 | pub const Request = struct { |
| 558 | /// The uri that this request is being sent to. | |
| 531 | 559 | uri: Uri, |
| 560 | ||
| 561 | /// The client that this request was created from. | |
| 532 | 562 | client: *Client, |
| 533 | /// is null when this connection is released | |
| 563 | ||
| 564 | /// Underlying connection to the server. This is null when the connection is released. | |
| 534 | 565 | connection: ?*Connection, |
| 535 | 566 | |
| 536 | 567 | method: http.Method, |
| 537 | 568 | version: http.Version = .@"HTTP/1.1", |
| 569 | ||
| 570 | /// The list of HTTP request headers. | |
| 538 | 571 | headers: http.Headers, |
| 539 | 572 | |
| 540 | 573 | /// The transfer encoding of the request body. |
| 541 | 574 | transfer_encoding: RequestTransfer = .none, |
| 542 | 575 | |
| 576 | /// The redirect quota left for this request. | |
| 543 | 577 | redirects_left: u32, |
| 578 | ||
| 579 | /// Whether the request should follow redirects. | |
| 544 | 580 | handle_redirects: bool, |
| 581 | ||
| 582 | /// Whether the request should handle a 100-continue response before sending the request body. | |
| 545 | 583 | handle_continue: bool, |
| 546 | 584 | |
| 585 | /// The response associated with this request. | |
| 586 | /// | |
| 587 | /// This field is undefined until `wait` is called. | |
| 547 | 588 | response: Response, |
| 548 | 589 | |
| 549 | 590 | /// Used as a allocator for resolving redirects locations. |
| ... | ... | @@ -993,6 +1034,7 @@ pub const Request = struct { |
| 993 | 1034 | } |
| 994 | 1035 | }; |
| 995 | 1036 | |
| 1037 | /// A HTTP proxy server. | |
| 996 | 1038 | pub const Proxy = struct { |
| 997 | 1039 | allocator: Allocator, |
| 998 | 1040 | headers: http.Headers, |
| ... | ... | @@ -1144,6 +1186,7 @@ pub fn loadDefaultProxies(client: *Client) !void { |
| 1144 | 1186 | pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; |
| 1145 | 1187 | |
| 1146 | 1188 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 1189 | /// | |
| 1147 | 1190 | /// This function is threadsafe. |
| 1148 | 1191 | pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection { |
| 1149 | 1192 | if (client.connection_pool.findConnection(.{ |
| ... | ... | @@ -1203,6 +1246,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec |
| 1203 | 1246 | pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError; |
| 1204 | 1247 | |
| 1205 | 1248 | /// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open. |
| 1249 | /// | |
| 1206 | 1250 | /// This function is threadsafe. |
| 1207 | 1251 | pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection { |
| 1208 | 1252 | if (!net.has_unix_sockets) return error.Unsupported; |
| ... | ... | @@ -1237,6 +1281,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti |
| 1237 | 1281 | } |
| 1238 | 1282 | |
| 1239 | 1283 | /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open. |
| 1284 | /// | |
| 1240 | 1285 | /// This function is threadsafe. |
| 1241 | 1286 | pub fn connectTunnel( |
| 1242 | 1287 | client: *Client, |
| ... | ... | @@ -1318,7 +1363,6 @@ const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, Conn |
| 1318 | 1363 | pub const ConnectError = ConnectErrorPartial || RequestError; |
| 1319 | 1364 | |
| 1320 | 1365 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 1321 | /// | |
| 1322 | 1366 | /// If a proxy is configured for the client, then the proxy will be used to connect to the host. |
| 1323 | 1367 | /// |
| 1324 | 1368 | /// This function is threadsafe. |
| ... | ... | @@ -1375,7 +1419,10 @@ pub const RequestOptions = struct { |
| 1375 | 1419 | /// request, then the request *will* deadlock. |
| 1376 | 1420 | handle_continue: bool = true, |
| 1377 | 1421 | |
| 1422 | /// Automatically follow redirects. This will only follow redirects for repeatable requests (ie. with no payload or the server has acknowledged the payload) | |
| 1378 | 1423 | handle_redirects: bool = true, |
| 1424 | ||
| 1425 | /// How many redirects to follow before returning an error. | |
| 1379 | 1426 | max_redirects: u32 = 3, |
| 1380 | 1427 | header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 }, |
| 1381 | 1428 |
lib/std/http/Headers.zig+12-1| ... | ... | @@ -35,6 +35,7 @@ pub const CaseInsensitiveStringContext = struct { |
| 35 | 35 | } |
| 36 | 36 | }; |
| 37 | 37 | |
| 38 | /// A single HTTP header field. | |
| 38 | 39 | pub const Field = struct { |
| 39 | 40 | name: []const u8, |
| 40 | 41 | value: []const u8, |
| ... | ... | @@ -47,6 +48,7 @@ pub const Field = struct { |
| 47 | 48 | } |
| 48 | 49 | }; |
| 49 | 50 | |
| 51 | /// A list of HTTP header fields. | |
| 50 | 52 | pub const Headers = struct { |
| 51 | 53 | allocator: Allocator, |
| 52 | 54 | list: HeaderList = .{}, |
| ... | ... | @@ -56,10 +58,12 @@ pub const Headers = struct { |
| 56 | 58 | /// Use with caution. |
| 57 | 59 | owned: bool = true, |
| 58 | 60 | |
| 61 | /// Initialize an empty list of headers. | |
| 59 | 62 | pub fn init(allocator: Allocator) Headers { |
| 60 | 63 | return .{ .allocator = allocator }; |
| 61 | 64 | } |
| 62 | 65 | |
| 66 | /// Initialize a pre-populated list of headers from a list of fields. | |
| 63 | 67 | pub fn initList(allocator: Allocator, list: []const Field) !Headers { |
| 64 | 68 | var new = Headers.init(allocator); |
| 65 | 69 | |
| ... | ... | @@ -72,6 +76,9 @@ pub const Headers = struct { |
| 72 | 76 | return new; |
| 73 | 77 | } |
| 74 | 78 | |
| 79 | /// Deallocate all memory associated with the headers. | |
| 80 | /// | |
| 81 | /// If the `owned` field is false, this will not free the names and values of the headers. | |
| 75 | 82 | pub fn deinit(headers: *Headers) void { |
| 76 | 83 | headers.deallocateIndexListsAndFields(); |
| 77 | 84 | headers.index.deinit(headers.allocator); |
| ... | ... | @@ -80,7 +87,9 @@ pub const Headers = struct { |
| 80 | 87 | headers.* = undefined; |
| 81 | 88 | } |
| 82 | 89 | |
| 83 | /// Appends a header to the list. Both name and value are copied. | |
| 90 | /// Appends a header to the list. | |
| 91 | /// | |
| 92 | /// If the `owned` field is true, both name and value will be copied. | |
| 84 | 93 | pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void { |
| 85 | 94 | const n = headers.list.items.len; |
| 86 | 95 | |
| ... | ... | @@ -108,6 +117,7 @@ pub const Headers = struct { |
| 108 | 117 | try headers.list.append(headers.allocator, entry); |
| 109 | 118 | } |
| 110 | 119 | |
| 120 | /// Returns true if this list of headers contains the given name. | |
| 111 | 121 | pub fn contains(headers: Headers, name: []const u8) bool { |
| 112 | 122 | return headers.index.contains(name); |
| 113 | 123 | } |
| ... | ... | @@ -285,6 +295,7 @@ pub const Headers = struct { |
| 285 | 295 | headers.list.clearRetainingCapacity(); |
| 286 | 296 | } |
| 287 | 297 | |
| 298 | /// Creates a copy of the headers using the provided allocator. | |
| 288 | 299 | pub fn clone(headers: Headers, allocator: Allocator) !Headers { |
| 289 | 300 | var new = Headers.init(allocator); |
| 290 | 301 |
lib/std/http/Server.zig+78-8| ... | ... | @@ -1,3 +1,44 @@ |
| 1 | //! HTTP Server implementation. | |
| 2 | //! | |
| 3 | //! This server assumes *all* clients are well behaved and standard compliant; it can and will deadlock if a client holds a connection open without sending a request. | |
| 4 | //! | |
| 5 | //! Example usage: | |
| 6 | //! | |
| 7 | //! ```zig | |
| 8 | //! var server = Server.init(.{ .reuse_address = true }); | |
| 9 | //! defer server.deinit(); | |
| 10 | //! | |
| 11 | //! try server.listen(bind_addr); | |
| 12 | //! | |
| 13 | //! while (true) { | |
| 14 | //! var res = try server.accept(.{ .allocator = gpa }); | |
| 15 | //! defer res.deinit(); | |
| 16 | //! | |
| 17 | //! while (res.reset() != .closing) { | |
| 18 | //! res.wait() catch |err| switch (err) { | |
| 19 | //! error.HttpHeadersInvalid => break, | |
| 20 | //! error.HttpHeadersExceededSizeLimit => { | |
| 21 | //! res.status = .request_header_fields_too_large; | |
| 22 | //! res.send() catch break; | |
| 23 | //! break; | |
| 24 | //! }, | |
| 25 | //! else => { | |
| 26 | //! res.status = .bad_request; | |
| 27 | //! res.send() catch break; | |
| 28 | //! break; | |
| 29 | //! }, | |
| 30 | //! } | |
| 31 | //! | |
| 32 | //! res.status = .ok; | |
| 33 | //! res.transfer_encoding = .chunked; | |
| 34 | //! | |
| 35 | //! try res.send(); | |
| 36 | //! try res.writeAll("Hello, World!\n"); | |
| 37 | //! try res.finish(); | |
| 38 | //! } | |
| 39 | //! } | |
| 40 | //! ``` | |
| 41 | ||
| 1 | 42 | const std = @import("../std.zig"); |
| 2 | 43 | const testing = std.testing; |
| 3 | 44 | const http = std.http; |
| ... | ... | @@ -10,8 +51,7 @@ const assert = std.debug.assert; |
| 10 | 51 | const Server = @This(); |
| 11 | 52 | const proto = @import("protocol.zig"); |
| 12 | 53 | |
| 13 | allocator: Allocator, | |
| 14 | ||
| 54 | /// The underlying server socket. | |
| 15 | 55 | socket: net.StreamServer, |
| 16 | 56 | |
| 17 | 57 | /// An interface to a plain connection. |
| ... | ... | @@ -269,8 +309,13 @@ pub const Request = struct { |
| 269 | 309 | return @as(u64, @bitCast(array.*)); |
| 270 | 310 | } |
| 271 | 311 | |
| 312 | /// The HTTP request method. | |
| 272 | 313 | method: http.Method, |
| 314 | ||
| 315 | /// The HTTP request target. | |
| 273 | 316 | target: []const u8, |
| 317 | ||
| 318 | /// The HTTP version of this request. | |
| 274 | 319 | version: http.Version, |
| 275 | 320 | |
| 276 | 321 | /// The length of the request body, if known. |
| ... | ... | @@ -282,16 +327,21 @@ pub const Request = struct { |
| 282 | 327 | /// The compression of the request body, or .identity (no compression) if not present. |
| 283 | 328 | transfer_compression: http.ContentEncoding = .identity, |
| 284 | 329 | |
| 330 | /// The list of HTTP request headers | |
| 285 | 331 | headers: http.Headers, |
| 332 | ||
| 286 | 333 | parser: proto.HeadersParser, |
| 287 | 334 | compression: Compression = .none, |
| 288 | 335 | }; |
| 289 | 336 | |
| 290 | 337 | /// A HTTP response waiting to be sent. |
| 291 | 338 | /// |
| 292 | /// [/ <----------------------------------- \] | |
| 293 | /// Order of operations: accept -> wait -> send [ -> write -> finish][ -> reset /] | |
| 294 | /// \ -> read / | |
| 339 | /// Order of operations: | |
| 340 | /// ``` | |
| 341 | /// [/ <--------------------------------------- \] | |
| 342 | /// accept -> wait -> send [ -> write -> finish][ -> reset /] | |
| 343 | /// \ -> read / | |
| 344 | /// ``` | |
| 295 | 345 | pub const Response = struct { |
| 296 | 346 | version: http.Version = .@"HTTP/1.1", |
| 297 | 347 | status: http.Status = .ok, |
| ... | ... | @@ -299,11 +349,21 @@ pub const Response = struct { |
| 299 | 349 | |
| 300 | 350 | transfer_encoding: ResponseTransfer = .none, |
| 301 | 351 | |
| 352 | /// The allocator responsible for allocating memory for this response. | |
| 302 | 353 | allocator: Allocator, |
| 354 | ||
| 355 | /// The peer's address | |
| 303 | 356 | address: net.Address, |
| 357 | ||
| 358 | /// The underlying connection for this response. | |
| 304 | 359 | connection: Connection, |
| 305 | 360 | |
| 361 | /// The HTTP response headers | |
| 306 | 362 | headers: http.Headers, |
| 363 | ||
| 364 | /// The HTTP request that this response is responding to. | |
| 365 | /// | |
| 366 | /// This field is only valid after calling `wait`. | |
| 307 | 367 | request: Request, |
| 308 | 368 | |
| 309 | 369 | state: State = .first, |
| ... | ... | @@ -495,6 +555,17 @@ pub const Response = struct { |
| 495 | 555 | pub const WaitError = Connection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Request.ParseError || error{ CompressionInitializationFailed, CompressionNotSupported }; |
| 496 | 556 | |
| 497 | 557 | /// Wait for the client to send a complete request head. |
| 558 | /// | |
| 559 | /// For correct behavior, the following rules must be followed: | |
| 560 | /// | |
| 561 | /// * If this returns any error in `Connection.ReadError`, you MUST immediately close the connection by calling `deinit`. | |
| 562 | /// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close the connection by calling `deinit`. | |
| 563 | /// * If this returns `error.HttpHeadersExceededSizeLimit`, you MUST respond with a 431 status code and then call `deinit`. | |
| 564 | /// * If this returns any error in `Request.ParseError`, you MUST respond with a 400 status code and then call `deinit`. | |
| 565 | /// * If this returns any other error, you MUST respond with a 400 status code and then call `deinit`. | |
| 566 | /// * If the request has an Expect header containing 100-continue, you MUST either: | |
| 567 | /// * Respond with a 100 status code, then call `wait` again. | |
| 568 | /// * Respond with a 417 status code. | |
| 498 | 569 | pub fn wait(res: *Response) WaitError!void { |
| 499 | 570 | switch (res.state) { |
| 500 | 571 | .first, .start => res.state = .waited, |
| ... | ... | @@ -664,9 +735,8 @@ pub const Response = struct { |
| 664 | 735 | }; |
| 665 | 736 | |
| 666 | 737 | /// Create a new HTTP server. |
| 667 | pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server { | |
| 738 | pub fn init(options: net.StreamServer.Options) Server { | |
| 668 | 739 | return .{ |
| 669 | .allocator = allocator, | |
| 670 | 740 | .socket = net.StreamServer.init(options), |
| 671 | 741 | }; |
| 672 | 742 | } |
| ... | ... | @@ -748,7 +818,7 @@ test "HTTP server handles a chunked transfer coding request" { |
| 748 | 818 | const expect = std.testing.expect; |
| 749 | 819 | |
| 750 | 820 | const max_header_size = 8192; |
| 751 | var server = std.http.Server.init(allocator, .{ .reuse_address = true }); | |
| 821 | var server = std.http.Server.init(.{ .reuse_address = true }); | |
| 752 | 822 | defer server.deinit(); |
| 753 | 823 | |
| 754 | 824 | const address = try std.net.Address.parseIp("127.0.0.1", 0); |
test/standalone/http.zig+1-1| ... | ... | @@ -220,7 +220,7 @@ pub fn main() !void { |
| 220 | 220 | |
| 221 | 221 | defer _ = gpa_client.deinit(); |
| 222 | 222 | |
| 223 | server = Server.init(salloc, .{ .reuse_address = true }); | |
| 223 | server = Server.init(.{ .reuse_address = true }); | |
| 224 | 224 | |
| 225 | 225 | const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable; |
| 226 | 226 | try server.listen(addr); |