| author | |
| committer | |
| log | 3bb3d39fb4158ba4b811bcae7e7a897febf07e17 |
| tree | 51d408085e1e9c60003c02960e509e3498807ab5 |
| parent | 5569e6b49d9b421d35e3175df36eb9fe7e4e8084 |
| parent | 9017d758b96c1f296249670dffb774a598bc8598 |
| signature |
std.http: more http fixes, add standalone http server test8 files changed, 771 insertions(+), 183 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+53-27| ... | ... | @@ -71,7 +71,7 @@ pub const ConnectionPool = struct { |
| 71 | 71 | while (next) |node| : (next = node.prev) { |
| 72 | 72 | if ((node.data.buffered.conn.protocol == .tls) != criteria.is_tls) continue; |
| 73 | 73 | if (node.data.port != criteria.port) continue; |
| 74 | if (mem.eql(u8, node.data.host, criteria.host)) continue; | |
| 74 | if (!mem.eql(u8, node.data.host, criteria.host)) continue; | |
| 75 | 75 | |
| 76 | 76 | pool.acquireUnsafe(node); |
| 77 | 77 | return node; |
| ... | ... | @@ -251,47 +251,50 @@ pub const Connection = struct { |
| 251 | 251 | |
| 252 | 252 | /// A buffered (and peekable) Connection. |
| 253 | 253 | pub const BufferedConnection = struct { |
| 254 | pub const buffer_size = 0x2000; | |
| 254 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; | |
| 255 | 255 | |
| 256 | 256 | conn: Connection, |
| 257 | buf: [buffer_size]u8 = undefined, | |
| 258 | start: u16 = 0, | |
| 259 | end: u16 = 0, | |
| 257 | read_buf: [buffer_size]u8 = undefined, | |
| 258 | read_start: u16 = 0, | |
| 259 | read_end: u16 = 0, | |
| 260 | ||
| 261 | write_buf: [buffer_size]u8 = undefined, | |
| 262 | write_end: u16 = 0, | |
| 260 | 263 | |
| 261 | 264 | pub fn fill(bconn: *BufferedConnection) ReadError!void { |
| 262 | if (bconn.end != bconn.start) return; | |
| 265 | if (bconn.read_end != bconn.read_start) return; | |
| 263 | 266 | |
| 264 | const nread = try bconn.conn.read(bconn.buf[0..]); | |
| 267 | const nread = try bconn.conn.read(bconn.read_buf[0..]); | |
| 265 | 268 | if (nread == 0) return error.EndOfStream; |
| 266 | bconn.start = 0; | |
| 267 | bconn.end = @truncate(u16, nread); | |
| 269 | bconn.read_start = 0; | |
| 270 | bconn.read_end = @intCast(u16, nread); | |
| 268 | 271 | } |
| 269 | 272 | |
| 270 | 273 | pub fn peek(bconn: *BufferedConnection) []const u8 { |
| 271 | return bconn.buf[bconn.start..bconn.end]; | |
| 274 | return bconn.read_buf[bconn.read_start..bconn.read_end]; | |
| 272 | 275 | } |
| 273 | 276 | |
| 274 | 277 | pub fn clear(bconn: *BufferedConnection, num: u16) void { |
| 275 | bconn.start += num; | |
| 278 | bconn.read_start += num; | |
| 276 | 279 | } |
| 277 | 280 | |
| 278 | 281 | pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize { |
| 279 | 282 | var out_index: u16 = 0; |
| 280 | 283 | while (out_index < len) { |
| 281 | const available = bconn.end - bconn.start; | |
| 284 | const available = bconn.read_end - bconn.read_start; | |
| 282 | 285 | const left = buffer.len - out_index; |
| 283 | 286 | |
| 284 | 287 | if (available > 0) { |
| 285 | const can_read = @truncate(u16, @min(available, left)); | |
| 288 | const can_read = @intCast(u16, @min(available, left)); | |
| 286 | 289 | |
| 287 | @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]); | |
| 290 | @memcpy(buffer[out_index..][0..can_read], bconn.read_buf[bconn.read_start..][0..can_read]); | |
| 288 | 291 | out_index += can_read; |
| 289 | bconn.start += can_read; | |
| 292 | bconn.read_start += can_read; | |
| 290 | 293 | |
| 291 | 294 | continue; |
| 292 | 295 | } |
| 293 | 296 | |
| 294 | if (left > bconn.buf.len) { | |
| 297 | if (left > bconn.read_buf.len) { | |
| 295 | 298 | // skip the buffer if the output is large enough |
| 296 | 299 | return bconn.conn.read(buffer[out_index..]); |
| 297 | 300 | } |
| ... | ... | @@ -314,11 +317,30 @@ pub const BufferedConnection = struct { |
| 314 | 317 | } |
| 315 | 318 | |
| 316 | 319 | pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void { |
| 317 | return bconn.conn.writeAll(buffer); | |
| 320 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { | |
| 321 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | |
| 322 | bconn.write_end += @intCast(u16, buffer.len); | |
| 323 | } else { | |
| 324 | try bconn.flush(); | |
| 325 | try bconn.conn.writeAll(buffer); | |
| 326 | } | |
| 318 | 327 | } |
| 319 | 328 | |
| 320 | 329 | pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize { |
| 321 | return bconn.conn.write(buffer); | |
| 330 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { | |
| 331 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | |
| 332 | bconn.write_end += @intCast(u16, buffer.len); | |
| 333 | ||
| 334 | return buffer.len; | |
| 335 | } else { | |
| 336 | try bconn.flush(); | |
| 337 | return try bconn.conn.write(buffer); | |
| 338 | } | |
| 339 | } | |
| 340 | ||
| 341 | pub fn flush(bconn: *BufferedConnection) WriteError!void { | |
| 342 | defer bconn.write_end = 0; | |
| 343 | return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]); | |
| 322 | 344 | } |
| 323 | 345 | |
| 324 | 346 | pub const WriteError = Connection.WriteError; |
| ... | ... | @@ -355,8 +377,6 @@ pub const Compression = union(enum) { |
| 355 | 377 | /// A HTTP response originating from a server. |
| 356 | 378 | pub const Response = struct { |
| 357 | 379 | pub const ParseError = Allocator.Error || error{ |
| 358 | ShortHttpStatusLine, | |
| 359 | BadHttpVersion, | |
| 360 | 380 | HttpHeadersInvalid, |
| 361 | 381 | HttpHeaderContinuationsUnsupported, |
| 362 | 382 | HttpTransferEncodingUnsupported, |
| ... | ... | @@ -370,12 +390,12 @@ pub const Response = struct { |
| 370 | 390 | |
| 371 | 391 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 372 | 392 | if (first_line.len < 12) |
| 373 | return error.ShortHttpStatusLine; | |
| 393 | return error.HttpHeadersInvalid; | |
| 374 | 394 | |
| 375 | 395 | const version: http.Version = switch (int64(first_line[0..8])) { |
| 376 | 396 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 377 | 397 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 378 | else => return error.BadHttpVersion, | |
| 398 | else => return error.HttpHeadersInvalid, | |
| 379 | 399 | }; |
| 380 | 400 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; |
| 381 | 401 | const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); |
| ... | ... | @@ -569,8 +589,7 @@ pub const Request = struct { |
| 569 | 589 | |
| 570 | 590 | /// Send the request to the server. |
| 571 | 591 | pub fn start(req: *Request) StartError!void { |
| 572 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); | |
| 573 | const w = buffered.writer(); | |
| 592 | const w = req.connection.data.buffered.writer(); | |
| 574 | 593 | |
| 575 | 594 | try w.writeAll(@tagName(req.method)); |
| 576 | 595 | try w.writeByte(' '); |
| ... | ... | @@ -644,7 +663,7 @@ pub const Request = struct { |
| 644 | 663 | |
| 645 | 664 | try w.writeAll("\r\n"); |
| 646 | 665 | |
| 647 | try buffered.flush(); | |
| 666 | try req.connection.data.buffered.flush(); | |
| 648 | 667 | } |
| 649 | 668 | |
| 650 | 669 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; |
| ... | ... | @@ -695,16 +714,16 @@ pub const Request = struct { |
| 695 | 714 | |
| 696 | 715 | if (req.method == .CONNECT and req.response.status == .ok) { |
| 697 | 716 | req.connection.data.closing = false; |
| 698 | req.connection.data.proxied = true; | |
| 699 | 717 | req.response.parser.done = true; |
| 700 | 718 | } |
| 701 | 719 | |
| 720 | // we default to using keep-alive if not provided | |
| 702 | 721 | const req_connection = req.headers.getFirstValue("connection"); |
| 703 | 722 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); |
| 704 | 723 | |
| 705 | 724 | const res_connection = req.response.headers.getFirstValue("connection"); |
| 706 | 725 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); |
| 707 | if (req_keepalive and res_keepalive) { | |
| 726 | if (res_keepalive and (req_keepalive or req_connection == null)) { | |
| 708 | 727 | req.connection.data.closing = false; |
| 709 | 728 | } else { |
| 710 | 729 | req.connection.data.closing = true; |
| ... | ... | @@ -725,6 +744,11 @@ pub const Request = struct { |
| 725 | 744 | req.response.parser.done = true; |
| 726 | 745 | } |
| 727 | 746 | |
| 747 | // HEAD requests have no body | |
| 748 | if (req.method == .HEAD) { | |
| 749 | req.response.parser.done = true; | |
| 750 | } | |
| 751 | ||
| 728 | 752 | if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) { |
| 729 | 753 | req.response.skip = true; |
| 730 | 754 | |
| ... | ... | @@ -866,6 +890,8 @@ pub const Request = struct { |
| 866 | 890 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 867 | 891 | .none => {}, |
| 868 | 892 | } |
| 893 | ||
| 894 | try req.connection.data.buffered.flush(); | |
| 869 | 895 | } |
| 870 | 896 | }; |
| 871 | 897 |
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+167-69| ... | ... | @@ -95,47 +95,50 @@ pub const Connection = struct { |
| 95 | 95 | |
| 96 | 96 | /// A buffered (and peekable) Connection. |
| 97 | 97 | pub const BufferedConnection = struct { |
| 98 | pub const buffer_size = 0x2000; | |
| 98 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; | |
| 99 | 99 | |
| 100 | 100 | conn: Connection, |
| 101 | buf: [buffer_size]u8 = undefined, | |
| 102 | start: u16 = 0, | |
| 103 | end: u16 = 0, | |
| 101 | read_buf: [buffer_size]u8 = undefined, | |
| 102 | read_start: u16 = 0, | |
| 103 | read_end: u16 = 0, | |
| 104 | ||
| 105 | write_buf: [buffer_size]u8 = undefined, | |
| 106 | write_end: u16 = 0, | |
| 104 | 107 | |
| 105 | 108 | pub fn fill(bconn: *BufferedConnection) ReadError!void { |
| 106 | if (bconn.end != bconn.start) return; | |
| 109 | if (bconn.read_end != bconn.read_start) return; | |
| 107 | 110 | |
| 108 | const nread = try bconn.conn.read(bconn.buf[0..]); | |
| 111 | const nread = try bconn.conn.read(bconn.read_buf[0..]); | |
| 109 | 112 | if (nread == 0) return error.EndOfStream; |
| 110 | bconn.start = 0; | |
| 111 | bconn.end = @truncate(u16, nread); | |
| 113 | bconn.read_start = 0; | |
| 114 | bconn.read_end = @intCast(u16, nread); | |
| 112 | 115 | } |
| 113 | 116 | |
| 114 | 117 | pub fn peek(bconn: *BufferedConnection) []const u8 { |
| 115 | return bconn.buf[bconn.start..bconn.end]; | |
| 118 | return bconn.read_buf[bconn.read_start..bconn.read_end]; | |
| 116 | 119 | } |
| 117 | 120 | |
| 118 | 121 | pub fn clear(bconn: *BufferedConnection, num: u16) void { |
| 119 | bconn.start += num; | |
| 122 | bconn.read_start += num; | |
| 120 | 123 | } |
| 121 | 124 | |
| 122 | 125 | pub fn readAtLeast(bconn: *BufferedConnection, buffer: []u8, len: usize) ReadError!usize { |
| 123 | 126 | var out_index: u16 = 0; |
| 124 | 127 | while (out_index < len) { |
| 125 | const available = bconn.end - bconn.start; | |
| 128 | const available = bconn.read_end - bconn.read_start; | |
| 126 | 129 | const left = buffer.len - out_index; |
| 127 | 130 | |
| 128 | 131 | if (available > 0) { |
| 129 | const can_read = @truncate(u16, @min(available, left)); | |
| 132 | const can_read = @intCast(u16, @min(available, left)); | |
| 130 | 133 | |
| 131 | @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]); | |
| 134 | @memcpy(buffer[out_index..][0..can_read], bconn.read_buf[bconn.read_start..][0..can_read]); | |
| 132 | 135 | out_index += can_read; |
| 133 | bconn.start += can_read; | |
| 136 | bconn.read_start += can_read; | |
| 134 | 137 | |
| 135 | 138 | continue; |
| 136 | 139 | } |
| 137 | 140 | |
| 138 | if (left > bconn.buf.len) { | |
| 141 | if (left > bconn.read_buf.len) { | |
| 139 | 142 | // skip the buffer if the output is large enough |
| 140 | 143 | return bconn.conn.read(buffer[out_index..]); |
| 141 | 144 | } |
| ... | ... | @@ -158,11 +161,30 @@ pub const BufferedConnection = struct { |
| 158 | 161 | } |
| 159 | 162 | |
| 160 | 163 | pub fn writeAll(bconn: *BufferedConnection, buffer: []const u8) WriteError!void { |
| 161 | return bconn.conn.writeAll(buffer); | |
| 164 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { | |
| 165 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | |
| 166 | bconn.write_end += @intCast(u16, buffer.len); | |
| 167 | } else { | |
| 168 | try bconn.flush(); | |
| 169 | try bconn.conn.writeAll(buffer); | |
| 170 | } | |
| 162 | 171 | } |
| 163 | 172 | |
| 164 | 173 | pub fn write(bconn: *BufferedConnection, buffer: []const u8) WriteError!usize { |
| 165 | return bconn.conn.write(buffer); | |
| 174 | if (bconn.write_buf.len - bconn.write_end >= buffer.len) { | |
| 175 | @memcpy(bconn.write_buf[bconn.write_end..][0..buffer.len], buffer); | |
| 176 | bconn.write_end += @intCast(u16, buffer.len); | |
| 177 | ||
| 178 | return buffer.len; | |
| 179 | } else { | |
| 180 | try bconn.flush(); | |
| 181 | return try bconn.conn.write(buffer); | |
| 182 | } | |
| 183 | } | |
| 184 | ||
| 185 | pub fn flush(bconn: *BufferedConnection) WriteError!void { | |
| 186 | defer bconn.write_end = 0; | |
| 187 | return bconn.conn.writeAll(bconn.write_buf[0..bconn.write_end]); | |
| 166 | 188 | } |
| 167 | 189 | |
| 168 | 190 | pub const WriteError = Connection.WriteError; |
| ... | ... | @@ -199,8 +221,6 @@ pub const Compression = union(enum) { |
| 199 | 221 | /// A HTTP request originating from a client. |
| 200 | 222 | pub const Request = struct { |
| 201 | 223 | pub const ParseError = Allocator.Error || error{ |
| 202 | ShortHttpStatusLine, | |
| 203 | BadHttpVersion, | |
| 204 | 224 | UnknownHttpMethod, |
| 205 | 225 | HttpHeadersInvalid, |
| 206 | 226 | HttpHeaderContinuationsUnsupported, |
| ... | ... | @@ -215,7 +235,7 @@ pub const Request = struct { |
| 215 | 235 | |
| 216 | 236 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 217 | 237 | if (first_line.len < 10) |
| 218 | return error.ShortHttpStatusLine; | |
| 238 | return error.HttpHeadersInvalid; | |
| 219 | 239 | |
| 220 | 240 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; |
| 221 | 241 | const method_str = first_line[0..method_end]; |
| ... | ... | @@ -229,7 +249,7 @@ pub const Request = struct { |
| 229 | 249 | const version: http.Version = switch (int64(version_str[0..8])) { |
| 230 | 250 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 231 | 251 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 232 | else => return error.BadHttpVersion, | |
| 252 | else => return error.HttpHeadersInvalid, | |
| 233 | 253 | }; |
| 234 | 254 | |
| 235 | 255 | const target = first_line[method_end + 1 .. version_start]; |
| ... | ... | @@ -312,7 +332,7 @@ pub const Request = struct { |
| 312 | 332 | transfer_encoding: ?http.TransferEncoding = null, |
| 313 | 333 | transfer_compression: ?http.ContentEncoding = null, |
| 314 | 334 | |
| 315 | headers: http.Headers = undefined, | |
| 335 | headers: http.Headers, | |
| 316 | 336 | parser: proto.HeadersParser, |
| 317 | 337 | compression: Compression = .none, |
| 318 | 338 | }; |
| ... | ... | @@ -329,21 +349,63 @@ pub const Response = struct { |
| 329 | 349 | |
| 330 | 350 | transfer_encoding: ResponseTransfer = .none, |
| 331 | 351 | |
| 332 | server: *Server, | |
| 352 | allocator: Allocator, | |
| 333 | 353 | address: net.Address, |
| 334 | 354 | connection: BufferedConnection, |
| 335 | 355 | |
| 336 | 356 | headers: http.Headers, |
| 337 | 357 | request: Request, |
| 338 | 358 | |
| 359 | state: State = .first, | |
| 360 | ||
| 361 | const State = enum { | |
| 362 | first, | |
| 363 | start, | |
| 364 | waited, | |
| 365 | responded, | |
| 366 | finished, | |
| 367 | }; | |
| 368 | ||
| 339 | 369 | pub fn deinit(res: *Response) void { |
| 340 | res.server.allocator.destroy(res); | |
| 370 | res.connection.close(); | |
| 371 | ||
| 372 | res.headers.deinit(); | |
| 373 | res.request.headers.deinit(); | |
| 374 | ||
| 375 | if (res.request.parser.header_bytes_owned) { | |
| 376 | res.request.parser.header_bytes.deinit(res.allocator); | |
| 377 | } | |
| 341 | 378 | } |
| 342 | 379 | |
| 380 | pub const ResetState = enum { reset, closing }; | |
| 381 | ||
| 343 | 382 | /// 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 | res.headers.deinit(); | |
| 383 | pub fn reset(res: *Response) ResetState { | |
| 384 | if (res.state == .first) { | |
| 385 | res.state = .start; | |
| 386 | return .reset; | |
| 387 | } | |
| 388 | ||
| 389 | if (!res.request.parser.done) { | |
| 390 | // If the response wasn't fully read, then we need to close the connection. | |
| 391 | res.connection.conn.closing = true; | |
| 392 | return .closing; | |
| 393 | } | |
| 394 | ||
| 395 | // A connection is only keep-alive if the Connection header is present and it's value is not "close". | |
| 396 | // The server and client must both agree | |
| 397 | // | |
| 398 | // do() defaults to using keep-alive if the client requests it. | |
| 399 | const res_connection = res.headers.getFirstValue("connection"); | |
| 400 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | |
| 401 | ||
| 402 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 403 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 404 | if (req_keepalive and (res_keepalive or res_connection == null)) { | |
| 405 | res.connection.conn.closing = false; | |
| 406 | } else { | |
| 407 | res.connection.conn.closing = true; | |
| 408 | } | |
| 347 | 409 | |
| 348 | 410 | switch (res.request.compression) { |
| 349 | 411 | .none => {}, |
| ... | ... | @@ -352,19 +414,30 @@ pub const Response = struct { |
| 352 | 414 | .zstd => |*zstd| zstd.deinit(), |
| 353 | 415 | } |
| 354 | 416 | |
| 355 | if (!res.request.parser.done) { | |
| 356 | // If the response wasn't fully read, then we need to close the connection. | |
| 357 | res.connection.conn.closing = true; | |
| 358 | } | |
| 417 | res.state = .start; | |
| 418 | res.version = .@"HTTP/1.1"; | |
| 419 | res.status = .ok; | |
| 420 | res.reason = null; | |
| 359 | 421 | |
| 360 | if (res.connection.conn.closing) { | |
| 361 | res.connection.close(); | |
| 422 | res.transfer_encoding = .none; | |
| 362 | 423 | |
| 363 | if (res.request.parser.header_bytes_owned) { | |
| 364 | res.request.parser.header_bytes.deinit(res.server.allocator); | |
| 365 | } | |
| 424 | res.headers.clearRetainingCapacity(); | |
| 425 | ||
| 426 | res.request.headers.clearAndFree(); // FIXME: figure out why `clearRetainingCapacity` causes a leak in hash_map here | |
| 427 | res.request.parser.reset(); | |
| 428 | ||
| 429 | res.request = Request{ | |
| 430 | .version = undefined, | |
| 431 | .method = undefined, | |
| 432 | .target = undefined, | |
| 433 | .headers = res.request.headers, | |
| 434 | .parser = res.request.parser, | |
| 435 | }; | |
| 436 | ||
| 437 | if (res.connection.conn.closing) { | |
| 438 | return .closing; | |
| 366 | 439 | } else { |
| 367 | res.request.parser.reset(); | |
| 440 | return .reset; | |
| 368 | 441 | } |
| 369 | 442 | } |
| 370 | 443 | |
| ... | ... | @@ -372,8 +445,12 @@ pub const Response = struct { |
| 372 | 445 | |
| 373 | 446 | /// Send the response headers. |
| 374 | 447 | pub fn do(res: *Response) !void { |
| 375 | var buffered = std.io.bufferedWriter(res.connection.writer()); | |
| 376 | const w = buffered.writer(); | |
| 448 | switch (res.state) { | |
| 449 | .waited => res.state = .responded, | |
| 450 | .first, .start, .responded, .finished => unreachable, | |
| 451 | } | |
| 452 | ||
| 453 | const w = res.connection.writer(); | |
| 377 | 454 | |
| 378 | 455 | try w.writeAll(@tagName(res.version)); |
| 379 | 456 | try w.writeByte(' '); |
| ... | ... | @@ -391,7 +468,14 @@ pub const Response = struct { |
| 391 | 468 | } |
| 392 | 469 | |
| 393 | 470 | if (!res.headers.contains("connection")) { |
| 394 | try w.writeAll("Connection: keep-alive\r\n"); | |
| 471 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 472 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 473 | ||
| 474 | if (req_keepalive) { | |
| 475 | try w.writeAll("Connection: keep-alive\r\n"); | |
| 476 | } else { | |
| 477 | try w.writeAll("Connection: close\r\n"); | |
| 478 | } | |
| 395 | 479 | } |
| 396 | 480 | |
| 397 | 481 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); |
| ... | ... | @@ -424,7 +508,7 @@ pub const Response = struct { |
| 424 | 508 | |
| 425 | 509 | try w.writeAll("\r\n"); |
| 426 | 510 | |
| 427 | try buffered.flush(); | |
| 511 | try res.connection.flush(); | |
| 428 | 512 | } |
| 429 | 513 | |
| 430 | 514 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; |
| ... | ... | @@ -452,29 +536,23 @@ pub const Response = struct { |
| 452 | 536 | |
| 453 | 537 | /// Wait for the client to send a complete request head. |
| 454 | 538 | pub fn wait(res: *Response) WaitError!void { |
| 539 | switch (res.state) { | |
| 540 | .first, .start => res.state = .waited, | |
| 541 | .waited, .responded, .finished => unreachable, | |
| 542 | } | |
| 543 | ||
| 455 | 544 | while (true) { |
| 456 | 545 | try res.connection.fill(); |
| 457 | 546 | |
| 458 | const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek()); | |
| 547 | const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek()); | |
| 459 | 548 | res.connection.clear(@intCast(u16, nchecked)); |
| 460 | 549 | |
| 461 | 550 | if (res.request.parser.state.isContent()) break; |
| 462 | 551 | } |
| 463 | 552 | |
| 464 | res.request.headers = .{ .allocator = res.server.allocator, .owned = true }; | |
| 553 | res.request.headers = .{ .allocator = res.allocator, .owned = true }; | |
| 465 | 554 | try res.request.parse(res.request.parser.header_bytes.items); |
| 466 | 555 | |
| 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 | 556 | if (res.request.transfer_encoding) |te| { |
| 479 | 557 | switch (te) { |
| 480 | 558 | .chunked => { |
| ... | ... | @@ -494,13 +572,13 @@ pub const Response = struct { |
| 494 | 572 | if (res.request.transfer_compression) |tc| switch (tc) { |
| 495 | 573 | .compress => return error.CompressionNotSupported, |
| 496 | 574 | .deflate => res.request.compression = .{ |
| 497 | .deflate = std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | |
| 575 | .deflate = std.compress.zlib.zlibStream(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | |
| 498 | 576 | }, |
| 499 | 577 | .gzip => res.request.compression = .{ |
| 500 | .gzip = std.compress.gzip.decompress(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | |
| 578 | .gzip = std.compress.gzip.decompress(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | |
| 501 | 579 | }, |
| 502 | 580 | .zstd => res.request.compression = .{ |
| 503 | .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()), | |
| 581 | .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()), | |
| 504 | 582 | }, |
| 505 | 583 | }; |
| 506 | 584 | } |
| ... | ... | @@ -515,6 +593,11 @@ pub const Response = struct { |
| 515 | 593 | } |
| 516 | 594 | |
| 517 | 595 | pub fn read(res: *Response, buffer: []u8) ReadError!usize { |
| 596 | switch (res.state) { | |
| 597 | .waited, .responded, .finished => {}, | |
| 598 | .first, .start => unreachable, | |
| 599 | } | |
| 600 | ||
| 518 | 601 | const out_index = switch (res.request.compression) { |
| 519 | 602 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, |
| 520 | 603 | .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, |
| ... | ... | @@ -528,12 +611,12 @@ pub const Response = struct { |
| 528 | 611 | while (!res.request.parser.state.isContent()) { // read trailing headers |
| 529 | 612 | try res.connection.fill(); |
| 530 | 613 | |
| 531 | const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek()); | |
| 614 | const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek()); | |
| 532 | 615 | res.connection.clear(@intCast(u16, nchecked)); |
| 533 | 616 | } |
| 534 | 617 | |
| 535 | 618 | if (has_trail) { |
| 536 | res.request.headers = http.Headers{ .allocator = res.server.allocator, .owned = false }; | |
| 619 | res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false }; | |
| 537 | 620 | |
| 538 | 621 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. |
| 539 | 622 | // This will *only* fail for a malformed trailer. |
| ... | ... | @@ -564,6 +647,11 @@ pub const Response = struct { |
| 564 | 647 | |
| 565 | 648 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 566 | 649 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { |
| 650 | switch (res.state) { | |
| 651 | .responded => {}, | |
| 652 | .first, .waited, .start, .finished => unreachable, | |
| 653 | } | |
| 654 | ||
| 567 | 655 | switch (res.transfer_encoding) { |
| 568 | 656 | .chunked => { |
| 569 | 657 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| ... | ... | @@ -583,7 +671,7 @@ pub const Response = struct { |
| 583 | 671 | } |
| 584 | 672 | } |
| 585 | 673 | |
| 586 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | |
| 674 | pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void { | |
| 587 | 675 | var index: usize = 0; |
| 588 | 676 | while (index < bytes.len) { |
| 589 | 677 | index += try write(req, bytes[index..]); |
| ... | ... | @@ -594,11 +682,18 @@ pub const Response = struct { |
| 594 | 682 | |
| 595 | 683 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 596 | 684 | pub fn finish(res: *Response) FinishError!void { |
| 685 | switch (res.state) { | |
| 686 | .responded => res.state = .finished, | |
| 687 | .first, .waited, .start, .finished => unreachable, | |
| 688 | } | |
| 689 | ||
| 597 | 690 | switch (res.transfer_encoding) { |
| 598 | 691 | .chunked => try res.connection.writeAll("0\r\n\r\n"), |
| 599 | 692 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 600 | 693 | .none => {}, |
| 601 | 694 | } |
| 695 | ||
| 696 | try res.connection.flush(); | |
| 602 | 697 | } |
| 603 | 698 | }; |
| 604 | 699 | |
| ... | ... | @@ -635,31 +730,34 @@ pub const HeaderStrategy = union(enum) { |
| 635 | 730 | static: []u8, |
| 636 | 731 | }; |
| 637 | 732 | |
| 638 | /// Accept a new connection and allocate a Response for it. | |
| 639 | pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { | |
| 733 | pub const AcceptOptions = struct { | |
| 734 | allocator: Allocator, | |
| 735 | header_strategy: HeaderStrategy = .{ .dynamic = 8192 }, | |
| 736 | }; | |
| 737 | ||
| 738 | /// Accept a new connection. | |
| 739 | pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response { | |
| 640 | 740 | const in = try server.socket.accept(); |
| 641 | 741 | |
| 642 | const res = try server.allocator.create(Response); | |
| 643 | res.* = .{ | |
| 644 | .server = server, | |
| 742 | return Response{ | |
| 743 | .allocator = options.allocator, | |
| 645 | 744 | .address = in.address, |
| 646 | 745 | .connection = .{ .conn = .{ |
| 647 | 746 | .stream = in.stream, |
| 648 | 747 | .protocol = .plain, |
| 649 | 748 | } }, |
| 650 | .headers = .{ .allocator = server.allocator }, | |
| 749 | .headers = .{ .allocator = options.allocator }, | |
| 651 | 750 | .request = .{ |
| 652 | 751 | .version = undefined, |
| 653 | 752 | .method = undefined, |
| 654 | 753 | .target = undefined, |
| 655 | .parser = switch (options) { | |
| 754 | .headers = .{ .allocator = options.allocator, .owned = false }, | |
| 755 | .parser = switch (options.header_strategy) { | |
| 656 | 756 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 657 | 757 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| 658 | 758 | }, |
| 659 | 759 | }, |
| 660 | 760 | }; |
| 661 | ||
| 662 | return res; | |
| 663 | 761 | } |
| 664 | 762 | |
| 665 | 763 | 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+541| ... | ... | @@ -0,0 +1,541 @@ |
| 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(.{ .stack_trace_frames = 12 }){}; | |
| 13 | var gpa_client = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; | |
| 14 | ||
| 15 | const salloc = gpa_server.allocator(); | |
| 16 | const calloc = gpa_client.allocator(); | |
| 17 | ||
| 18 | var server: Server = undefined; | |
| 19 | ||
| 20 | fn handleRequest(res: *Server.Response) !void { | |
| 21 | const log = std.log.scoped(.server); | |
| 22 | ||
| 23 | log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); | |
| 24 | ||
| 25 | const body = try res.reader().readAllAlloc(salloc, 8192); | |
| 26 | defer salloc.free(body); | |
| 27 | ||
| 28 | if (res.request.headers.contains("connection")) { | |
| 29 | try res.headers.append("connection", "keep-alive"); | |
| 30 | } | |
| 31 | ||
| 32 | if (mem.startsWith(u8, res.request.target, "/get")) { | |
| 33 | if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) { | |
| 34 | res.transfer_encoding = .chunked; | |
| 35 | } else { | |
| 36 | res.transfer_encoding = .{ .content_length = 14 }; | |
| 37 | } | |
| 38 | ||
| 39 | try res.headers.append("content-type", "text/plain"); | |
| 40 | ||
| 41 | try res.do(); | |
| 42 | if (res.request.method != .HEAD) { | |
| 43 | try res.writeAll("Hello, "); | |
| 44 | try res.writeAll("World!\n"); | |
| 45 | try res.finish(); | |
| 46 | } | |
| 47 | } else if (mem.startsWith(u8, res.request.target, "/large")) { | |
| 48 | res.transfer_encoding = .{ .content_length = 14 * 1024 + 14 * 10 }; | |
| 49 | ||
| 50 | try res.do(); | |
| 51 | ||
| 52 | var i: u32 = 0; | |
| 53 | while (i < 5) : (i += 1) { | |
| 54 | try res.writeAll("Hello, World!\n"); | |
| 55 | } | |
| 56 | ||
| 57 | try res.writeAll("Hello, World!\n" ** 1024); | |
| 58 | ||
| 59 | i = 0; | |
| 60 | while (i < 5) : (i += 1) { | |
| 61 | try res.writeAll("Hello, World!\n"); | |
| 62 | } | |
| 63 | ||
| 64 | try res.finish(); | |
| 65 | } else if (mem.eql(u8, res.request.target, "/echo-content")) { | |
| 66 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 67 | try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); | |
| 68 | ||
| 69 | if (res.request.headers.contains("transfer-encoding")) { | |
| 70 | try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?); | |
| 71 | res.transfer_encoding = .chunked; | |
| 72 | } else { | |
| 73 | res.transfer_encoding = .{ .content_length = 14 }; | |
| 74 | try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?); | |
| 75 | } | |
| 76 | ||
| 77 | try res.do(); | |
| 78 | try res.writeAll("Hello, "); | |
| 79 | try res.writeAll("World!\n"); | |
| 80 | try res.finish(); | |
| 81 | } else if (mem.eql(u8, res.request.target, "/trailer")) { | |
| 82 | res.transfer_encoding = .chunked; | |
| 83 | ||
| 84 | try res.do(); | |
| 85 | try res.writeAll("Hello, "); | |
| 86 | try res.writeAll("World!\n"); | |
| 87 | // try res.finish(); | |
| 88 | try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n"); | |
| 89 | try res.connection.flush(); | |
| 90 | } else if (mem.eql(u8, res.request.target, "/redirect/1")) { | |
| 91 | res.transfer_encoding = .chunked; | |
| 92 | ||
| 93 | res.status = .found; | |
| 94 | try res.headers.append("location", "../../get"); | |
| 95 | ||
| 96 | try res.do(); | |
| 97 | try res.writeAll("Hello, "); | |
| 98 | try res.writeAll("Redirected!\n"); | |
| 99 | try res.finish(); | |
| 100 | } else if (mem.eql(u8, res.request.target, "/redirect/2")) { | |
| 101 | res.transfer_encoding = .chunked; | |
| 102 | ||
| 103 | res.status = .found; | |
| 104 | try res.headers.append("location", "/redirect/1"); | |
| 105 | ||
| 106 | try res.do(); | |
| 107 | try res.writeAll("Hello, "); | |
| 108 | try res.writeAll("Redirected!\n"); | |
| 109 | try res.finish(); | |
| 110 | } else if (mem.eql(u8, res.request.target, "/redirect/3")) { | |
| 111 | res.transfer_encoding = .chunked; | |
| 112 | ||
| 113 | const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{server.socket.listen_address.getPort()}); | |
| 114 | defer salloc.free(location); | |
| 115 | ||
| 116 | res.status = .found; | |
| 117 | try res.headers.append("location", location); | |
| 118 | ||
| 119 | try res.do(); | |
| 120 | try res.writeAll("Hello, "); | |
| 121 | try res.writeAll("Redirected!\n"); | |
| 122 | try res.finish(); | |
| 123 | } else if (mem.eql(u8, res.request.target, "/redirect/4")) { | |
| 124 | res.transfer_encoding = .chunked; | |
| 125 | ||
| 126 | res.status = .found; | |
| 127 | try res.headers.append("location", "/redirect/3"); | |
| 128 | ||
| 129 | try res.do(); | |
| 130 | try res.writeAll("Hello, "); | |
| 131 | try res.writeAll("Redirected!\n"); | |
| 132 | try res.finish(); | |
| 133 | } else { | |
| 134 | res.status = .not_found; | |
| 135 | try res.do(); | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | var handle_new_requests = true; | |
| 140 | ||
| 141 | fn runServer(srv: *Server) !void { | |
| 142 | outer: while (handle_new_requests) { | |
| 143 | var res = try srv.accept(.{ | |
| 144 | .allocator = salloc, | |
| 145 | .header_strategy = .{ .dynamic = max_header_size }, | |
| 146 | }); | |
| 147 | defer res.deinit(); | |
| 148 | ||
| 149 | while (res.reset() != .closing) { | |
| 150 | res.wait() catch |err| switch (err) { | |
| 151 | error.HttpHeadersInvalid => continue :outer, | |
| 152 | error.EndOfStream => continue, | |
| 153 | else => return err, | |
| 154 | }; | |
| 155 | ||
| 156 | try handleRequest(&res); | |
| 157 | } | |
| 158 | } | |
| 159 | } | |
| 160 | ||
| 161 | fn serverThread(srv: *Server) void { | |
| 162 | defer srv.deinit(); | |
| 163 | defer _ = gpa_server.deinit(); | |
| 164 | ||
| 165 | runServer(srv) catch |err| { | |
| 166 | std.debug.print("server error: {}\n", .{err}); | |
| 167 | ||
| 168 | if (@errorReturnTrace()) |trace| { | |
| 169 | std.debug.dumpStackTrace(trace.*); | |
| 170 | } | |
| 171 | ||
| 172 | _ = gpa_server.deinit(); | |
| 173 | std.os.exit(1); | |
| 174 | }; | |
| 175 | } | |
| 176 | ||
| 177 | fn killServer(addr: std.net.Address) void { | |
| 178 | handle_new_requests = false; | |
| 179 | ||
| 180 | const conn = std.net.tcpConnectToAddress(addr) catch return; | |
| 181 | conn.close(); | |
| 182 | } | |
| 183 | ||
| 184 | pub fn main() !void { | |
| 185 | const log = std.log.scoped(.client); | |
| 186 | ||
| 187 | defer _ = gpa_client.deinit(); | |
| 188 | ||
| 189 | server = Server.init(salloc, .{ .reuse_address = true }); | |
| 190 | ||
| 191 | const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable; | |
| 192 | try server.listen(addr); | |
| 193 | ||
| 194 | const port = server.socket.listen_address.getPort(); | |
| 195 | ||
| 196 | const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server}); | |
| 197 | ||
| 198 | var client = Client{ .allocator = calloc }; | |
| 199 | // defer client.deinit(); handled below | |
| 200 | ||
| 201 | { // read content-length response | |
| 202 | var h = http.Headers{ .allocator = calloc }; | |
| 203 | defer h.deinit(); | |
| 204 | ||
| 205 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | |
| 206 | defer calloc.free(location); | |
| 207 | const uri = try std.Uri.parse(location); | |
| 208 | ||
| 209 | log.info("{s}", .{location}); | |
| 210 | var req = try client.request(.GET, uri, h, .{}); | |
| 211 | defer req.deinit(); | |
| 212 | ||
| 213 | try req.start(); | |
| 214 | try req.wait(); | |
| 215 | ||
| 216 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 217 | defer calloc.free(body); | |
| 218 | ||
| 219 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 220 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 221 | } | |
| 222 | ||
| 223 | // connection has been kept alive | |
| 224 | try testing.expect(client.connection_pool.free_len == 1); | |
| 225 | ||
| 226 | { // read large content-length response | |
| 227 | var h = http.Headers{ .allocator = calloc }; | |
| 228 | defer h.deinit(); | |
| 229 | ||
| 230 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port}); | |
| 231 | defer calloc.free(location); | |
| 232 | const uri = try std.Uri.parse(location); | |
| 233 | ||
| 234 | log.info("{s}", .{location}); | |
| 235 | var req = try client.request(.GET, uri, h, .{}); | |
| 236 | defer req.deinit(); | |
| 237 | ||
| 238 | try req.start(); | |
| 239 | try req.wait(); | |
| 240 | ||
| 241 | const body = try req.reader().readAllAlloc(calloc, 8192 * 1024); | |
| 242 | defer calloc.free(body); | |
| 243 | ||
| 244 | try testing.expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); | |
| 245 | } | |
| 246 | ||
| 247 | // connection has been kept alive | |
| 248 | try testing.expect(client.connection_pool.free_len == 1); | |
| 249 | ||
| 250 | { // send head request and not read chunked | |
| 251 | var h = http.Headers{ .allocator = calloc }; | |
| 252 | defer h.deinit(); | |
| 253 | ||
| 254 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | |
| 255 | defer calloc.free(location); | |
| 256 | const uri = try std.Uri.parse(location); | |
| 257 | ||
| 258 | log.info("{s}", .{location}); | |
| 259 | var req = try client.request(.HEAD, uri, h, .{}); | |
| 260 | defer req.deinit(); | |
| 261 | ||
| 262 | try req.start(); | |
| 263 | try req.wait(); | |
| 264 | ||
| 265 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 266 | defer calloc.free(body); | |
| 267 | ||
| 268 | try testing.expectEqualStrings("", body); | |
| 269 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 270 | try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?); | |
| 271 | } | |
| 272 | ||
| 273 | // connection has been kept alive | |
| 274 | try testing.expect(client.connection_pool.free_len == 1); | |
| 275 | ||
| 276 | { // read chunked response | |
| 277 | var h = http.Headers{ .allocator = calloc }; | |
| 278 | defer h.deinit(); | |
| 279 | ||
| 280 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); | |
| 281 | defer calloc.free(location); | |
| 282 | const uri = try std.Uri.parse(location); | |
| 283 | ||
| 284 | log.info("{s}", .{location}); | |
| 285 | var req = try client.request(.GET, uri, h, .{}); | |
| 286 | defer req.deinit(); | |
| 287 | ||
| 288 | try req.start(); | |
| 289 | try req.wait(); | |
| 290 | ||
| 291 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 292 | defer calloc.free(body); | |
| 293 | ||
| 294 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 295 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 296 | } | |
| 297 | ||
| 298 | // connection has been kept alive | |
| 299 | try testing.expect(client.connection_pool.free_len == 1); | |
| 300 | ||
| 301 | { // send head request and not read chunked | |
| 302 | var h = http.Headers{ .allocator = calloc }; | |
| 303 | defer h.deinit(); | |
| 304 | ||
| 305 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); | |
| 306 | defer calloc.free(location); | |
| 307 | const uri = try std.Uri.parse(location); | |
| 308 | ||
| 309 | log.info("{s}", .{location}); | |
| 310 | var req = try client.request(.HEAD, uri, h, .{}); | |
| 311 | defer req.deinit(); | |
| 312 | ||
| 313 | try req.start(); | |
| 314 | try req.wait(); | |
| 315 | ||
| 316 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 317 | defer calloc.free(body); | |
| 318 | ||
| 319 | try testing.expectEqualStrings("", body); | |
| 320 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 321 | try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?); | |
| 322 | } | |
| 323 | ||
| 324 | // connection has been kept alive | |
| 325 | try testing.expect(client.connection_pool.free_len == 1); | |
| 326 | ||
| 327 | { // check trailing headers | |
| 328 | var h = http.Headers{ .allocator = calloc }; | |
| 329 | defer h.deinit(); | |
| 330 | ||
| 331 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port}); | |
| 332 | defer calloc.free(location); | |
| 333 | const uri = try std.Uri.parse(location); | |
| 334 | ||
| 335 | log.info("{s}", .{location}); | |
| 336 | var req = try client.request(.GET, uri, h, .{}); | |
| 337 | defer req.deinit(); | |
| 338 | ||
| 339 | try req.start(); | |
| 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 | try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?); | |
| 347 | } | |
| 348 | ||
| 349 | // connection has been kept alive | |
| 350 | try testing.expect(client.connection_pool.free_len == 1); | |
| 351 | ||
| 352 | { // send content-length request | |
| 353 | var h = http.Headers{ .allocator = calloc }; | |
| 354 | defer h.deinit(); | |
| 355 | ||
| 356 | try h.append("content-type", "text/plain"); | |
| 357 | ||
| 358 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 359 | defer calloc.free(location); | |
| 360 | const uri = try std.Uri.parse(location); | |
| 361 | ||
| 362 | log.info("{s}", .{location}); | |
| 363 | var req = try client.request(.POST, uri, h, .{}); | |
| 364 | defer req.deinit(); | |
| 365 | ||
| 366 | req.transfer_encoding = .{ .content_length = 14 }; | |
| 367 | ||
| 368 | try req.start(); | |
| 369 | try req.writeAll("Hello, "); | |
| 370 | try req.writeAll("World!\n"); | |
| 371 | try req.finish(); | |
| 372 | ||
| 373 | try req.wait(); | |
| 374 | ||
| 375 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 376 | defer calloc.free(body); | |
| 377 | ||
| 378 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 379 | } | |
| 380 | ||
| 381 | // connection has been kept alive | |
| 382 | try testing.expect(client.connection_pool.free_len == 1); | |
| 383 | ||
| 384 | { // read content-length response with connection close | |
| 385 | var h = http.Headers{ .allocator = calloc }; | |
| 386 | defer h.deinit(); | |
| 387 | ||
| 388 | try h.append("connection", "close"); | |
| 389 | ||
| 390 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | |
| 391 | defer calloc.free(location); | |
| 392 | const uri = try std.Uri.parse(location); | |
| 393 | ||
| 394 | log.info("{s}", .{location}); | |
| 395 | var req = try client.request(.GET, uri, h, .{}); | |
| 396 | defer req.deinit(); | |
| 397 | ||
| 398 | try req.start(); | |
| 399 | try req.wait(); | |
| 400 | ||
| 401 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 402 | defer calloc.free(body); | |
| 403 | ||
| 404 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 405 | try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?); | |
| 406 | } | |
| 407 | ||
| 408 | // connection has been closed | |
| 409 | try testing.expect(client.connection_pool.free_len == 0); | |
| 410 | ||
| 411 | { // send chunked request | |
| 412 | var h = http.Headers{ .allocator = calloc }; | |
| 413 | defer h.deinit(); | |
| 414 | ||
| 415 | try h.append("content-type", "text/plain"); | |
| 416 | ||
| 417 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); | |
| 418 | defer calloc.free(location); | |
| 419 | const uri = try std.Uri.parse(location); | |
| 420 | ||
| 421 | log.info("{s}", .{location}); | |
| 422 | var req = try client.request(.POST, uri, h, .{}); | |
| 423 | defer req.deinit(); | |
| 424 | ||
| 425 | req.transfer_encoding = .chunked; | |
| 426 | ||
| 427 | try req.start(); | |
| 428 | try req.writeAll("Hello, "); | |
| 429 | try req.writeAll("World!\n"); | |
| 430 | try req.finish(); | |
| 431 | ||
| 432 | try req.wait(); | |
| 433 | ||
| 434 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 435 | defer calloc.free(body); | |
| 436 | ||
| 437 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 438 | } | |
| 439 | ||
| 440 | // connection has been kept alive | |
| 441 | try testing.expect(client.connection_pool.free_len == 1); | |
| 442 | ||
| 443 | { // relative redirect | |
| 444 | var h = http.Headers{ .allocator = calloc }; | |
| 445 | defer h.deinit(); | |
| 446 | ||
| 447 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port}); | |
| 448 | defer calloc.free(location); | |
| 449 | const uri = try std.Uri.parse(location); | |
| 450 | ||
| 451 | log.info("{s}", .{location}); | |
| 452 | var req = try client.request(.GET, uri, h, .{}); | |
| 453 | defer req.deinit(); | |
| 454 | ||
| 455 | try req.start(); | |
| 456 | try req.wait(); | |
| 457 | ||
| 458 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 459 | defer calloc.free(body); | |
| 460 | ||
| 461 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 462 | } | |
| 463 | ||
| 464 | // connection has been kept alive | |
| 465 | try testing.expect(client.connection_pool.free_len == 1); | |
| 466 | ||
| 467 | { // redirect from root | |
| 468 | var h = http.Headers{ .allocator = calloc }; | |
| 469 | defer h.deinit(); | |
| 470 | ||
| 471 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port}); | |
| 472 | defer calloc.free(location); | |
| 473 | const uri = try std.Uri.parse(location); | |
| 474 | ||
| 475 | log.info("{s}", .{location}); | |
| 476 | var req = try client.request(.GET, uri, h, .{}); | |
| 477 | defer req.deinit(); | |
| 478 | ||
| 479 | try req.start(); | |
| 480 | try req.wait(); | |
| 481 | ||
| 482 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 483 | defer calloc.free(body); | |
| 484 | ||
| 485 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 486 | } | |
| 487 | ||
| 488 | // connection has been kept alive | |
| 489 | try testing.expect(client.connection_pool.free_len == 1); | |
| 490 | ||
| 491 | { // absolute redirect | |
| 492 | var h = http.Headers{ .allocator = calloc }; | |
| 493 | defer h.deinit(); | |
| 494 | ||
| 495 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port}); | |
| 496 | defer calloc.free(location); | |
| 497 | const uri = try std.Uri.parse(location); | |
| 498 | ||
| 499 | log.info("{s}", .{location}); | |
| 500 | var req = try client.request(.GET, uri, h, .{}); | |
| 501 | defer req.deinit(); | |
| 502 | ||
| 503 | try req.start(); | |
| 504 | try req.wait(); | |
| 505 | ||
| 506 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 507 | defer calloc.free(body); | |
| 508 | ||
| 509 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 510 | } | |
| 511 | ||
| 512 | // connection has been kept alive | |
| 513 | try testing.expect(client.connection_pool.free_len == 1); | |
| 514 | ||
| 515 | { // too many redirects | |
| 516 | var h = http.Headers{ .allocator = calloc }; | |
| 517 | defer h.deinit(); | |
| 518 | ||
| 519 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port}); | |
| 520 | defer calloc.free(location); | |
| 521 | const uri = try std.Uri.parse(location); | |
| 522 | ||
| 523 | log.info("{s}", .{location}); | |
| 524 | var req = try client.request(.GET, uri, h, .{}); | |
| 525 | defer req.deinit(); | |
| 526 | ||
| 527 | try req.start(); | |
| 528 | req.wait() catch |err| switch (err) { | |
| 529 | error.TooManyHttpRedirects => {}, | |
| 530 | else => return err, | |
| 531 | }; | |
| 532 | } | |
| 533 | ||
| 534 | // connection has been kept alive | |
| 535 | try testing.expect(client.connection_pool.free_len == 1); | |
| 536 | ||
| 537 | client.deinit(); | |
| 538 | ||
| 539 | killServer(server.socket.listen_address); | |
| 540 | server_thread.join(); | |
| 541 | } |