authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-20 03:30:51-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-23 02:37:11-07:00
log6395ba852a88f0e0b2a2f0659f1daf9d08e90157
tree9cc1a8b6dce2ccb049667a008c596ab8bcf86e14
parent9129fb28dcc3c94a709cefd8040f41b9125693ee

std.http.Server: rework the API entirely

Mainly, this removes the poorly named `wait`, `send`, `finish` functions, which all operated on the same "Response" object, which was actually being used as the request. Now, it looks like this: 1. std.net.Server.accept() gives you a std.net.Server.Connection 2. std.http.Server.init() with the connection 3. Server.receiveHead() gives you a Request 4. Request.reader() gives you a body reader 5. Request.respond() is a one-shot, or Request.respondStreaming() creates a Response 6. Response.writer() gives you a body writer 7. Response.end() finishes the response; Response.endChunked() allows passing response trailers. In other words, the type system now guides the API user down the correct path. receiveHead allows extra bytes to be read into the read buffer, and then will reuse those bytes for the body or the next request upon connection reuse. respond(), the one-shot function, will send the entire response in one syscall. Streaming response bodies no longer wastefully wraps every call to write with a chunk header and trailer; instead it only sends the HTTP chunk wrapper when flushing. This means the user can still control when it happens but it also does not add unnecessary chunks. Empirically, in my example project that uses this API, the usage code is significantly less noisy, it has less error handling while handling errors more correctly, it's more obvious what is happening, and it is syscall-optimal. Additionally: * Uncouple std.http.HeadParser from protocol.zig * Delete std.Server.Connection; use std.net.Server.Connection instead. - The API user supplies the read buffer when initializing the http.Server, and it is used for the HTTP head as well as a buffer for reading the body into. * Replace and document the State enum. No longer is there both "start" and "first".

6 files changed, 1131 insertions(+), 1006 deletions(-)

lib/std/http.zig+2
...@@ -3,6 +3,7 @@ const std = @import("std.zig");...@@ -3,6 +3,7 @@ const std = @import("std.zig");
3pub const Client = @import("http/Client.zig");3pub const Client = @import("http/Client.zig");
4pub const Server = @import("http/Server.zig");4pub const Server = @import("http/Server.zig");
5pub const protocol = @import("http/protocol.zig");5pub const protocol = @import("http/protocol.zig");
6pub const HeadParser = @import("http/HeadParser.zig");
67
7pub const Version = enum {8pub const Version = enum {
8 @"HTTP/1.0",9 @"HTTP/1.0",
...@@ -311,5 +312,6 @@ test {...@@ -311,5 +312,6 @@ test {
311 _ = Method;312 _ = Method;
312 _ = Server;313 _ = Server;
313 _ = Status;314 _ = Status;
315 _ = HeadParser;
314 _ = @import("http/test.zig");316 _ = @import("http/test.zig");
315}317}
lib/std/http/HeadParser.zig created+370
...@@ -0,0 +1,370 @@
1state: State = .start,
2
3pub const State = enum {
4 start,
5 seen_n,
6 seen_r,
7 seen_rn,
8 seen_rnr,
9 finished,
10};
11
12/// Returns the number of bytes consumed by headers. This is always less
13/// than or equal to `bytes.len`.
14///
15/// If the amount returned is less than `bytes.len`, the parser is in a
16/// content state and the first byte of content is located at
17/// `bytes[result]`.
18pub fn feed(p: *HeadParser, bytes: []const u8) usize {
19 const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
20 const len: u32 = @intCast(bytes.len);
21 var index: u32 = 0;
22
23 while (true) {
24 switch (p.state) {
25 .finished => return index,
26 .start => switch (len - index) {
27 0 => return index,
28 1 => {
29 switch (bytes[index]) {
30 '\r' => p.state = .seen_r,
31 '\n' => p.state = .seen_n,
32 else => {},
33 }
34
35 return index + 1;
36 },
37 2 => {
38 const b16 = int16(bytes[index..][0..2]);
39 const b8 = intShift(u8, b16);
40
41 switch (b8) {
42 '\r' => p.state = .seen_r,
43 '\n' => p.state = .seen_n,
44 else => {},
45 }
46
47 switch (b16) {
48 int16("\r\n") => p.state = .seen_rn,
49 int16("\n\n") => p.state = .finished,
50 else => {},
51 }
52
53 return index + 2;
54 },
55 3 => {
56 const b24 = int24(bytes[index..][0..3]);
57 const b16 = intShift(u16, b24);
58 const b8 = intShift(u8, b24);
59
60 switch (b8) {
61 '\r' => p.state = .seen_r,
62 '\n' => p.state = .seen_n,
63 else => {},
64 }
65
66 switch (b16) {
67 int16("\r\n") => p.state = .seen_rn,
68 int16("\n\n") => p.state = .finished,
69 else => {},
70 }
71
72 switch (b24) {
73 int24("\r\n\r") => p.state = .seen_rnr,
74 else => {},
75 }
76
77 return index + 3;
78 },
79 4...vector_len - 1 => {
80 const b32 = int32(bytes[index..][0..4]);
81 const b24 = intShift(u24, b32);
82 const b16 = intShift(u16, b32);
83 const b8 = intShift(u8, b32);
84
85 switch (b8) {
86 '\r' => p.state = .seen_r,
87 '\n' => p.state = .seen_n,
88 else => {},
89 }
90
91 switch (b16) {
92 int16("\r\n") => p.state = .seen_rn,
93 int16("\n\n") => p.state = .finished,
94 else => {},
95 }
96
97 switch (b24) {
98 int24("\r\n\r") => p.state = .seen_rnr,
99 else => {},
100 }
101
102 switch (b32) {
103 int32("\r\n\r\n") => p.state = .finished,
104 else => {},
105 }
106
107 index += 4;
108 continue;
109 },
110 else => {
111 const chunk = bytes[index..][0..vector_len];
112 const matches = if (use_vectors) matches: {
113 const Vector = @Vector(vector_len, u8);
114 // const BoolVector = @Vector(vector_len, bool);
115 const BitVector = @Vector(vector_len, u1);
116 const SizeVector = @Vector(vector_len, u8);
117
118 const v: Vector = chunk.*;
119 const matches_r: BitVector = @bitCast(v == @as(Vector, @splat('\r')));
120 const matches_n: BitVector = @bitCast(v == @as(Vector, @splat('\n')));
121 const matches_or: SizeVector = matches_r | matches_n;
122
123 break :matches @reduce(.Add, matches_or);
124 } else matches: {
125 var matches: u8 = 0;
126 for (chunk) |byte| switch (byte) {
127 '\r', '\n' => matches += 1,
128 else => {},
129 };
130 break :matches matches;
131 };
132 switch (matches) {
133 0 => {},
134 1 => switch (chunk[vector_len - 1]) {
135 '\r' => p.state = .seen_r,
136 '\n' => p.state = .seen_n,
137 else => {},
138 },
139 2 => {
140 const b16 = int16(chunk[vector_len - 2 ..][0..2]);
141 const b8 = intShift(u8, b16);
142
143 switch (b8) {
144 '\r' => p.state = .seen_r,
145 '\n' => p.state = .seen_n,
146 else => {},
147 }
148
149 switch (b16) {
150 int16("\r\n") => p.state = .seen_rn,
151 int16("\n\n") => p.state = .finished,
152 else => {},
153 }
154 },
155 3 => {
156 const b24 = int24(chunk[vector_len - 3 ..][0..3]);
157 const b16 = intShift(u16, b24);
158 const b8 = intShift(u8, b24);
159
160 switch (b8) {
161 '\r' => p.state = .seen_r,
162 '\n' => p.state = .seen_n,
163 else => {},
164 }
165
166 switch (b16) {
167 int16("\r\n") => p.state = .seen_rn,
168 int16("\n\n") => p.state = .finished,
169 else => {},
170 }
171
172 switch (b24) {
173 int24("\r\n\r") => p.state = .seen_rnr,
174 else => {},
175 }
176 },
177 4...vector_len => {
178 inline for (0..vector_len - 3) |i_usize| {
179 const i = @as(u32, @truncate(i_usize));
180
181 const b32 = int32(chunk[i..][0..4]);
182 const b16 = intShift(u16, b32);
183
184 if (b32 == int32("\r\n\r\n")) {
185 p.state = .finished;
186 return index + i + 4;
187 } else if (b16 == int16("\n\n")) {
188 p.state = .finished;
189 return index + i + 2;
190 }
191 }
192
193 const b24 = int24(chunk[vector_len - 3 ..][0..3]);
194 const b16 = intShift(u16, b24);
195 const b8 = intShift(u8, b24);
196
197 switch (b8) {
198 '\r' => p.state = .seen_r,
199 '\n' => p.state = .seen_n,
200 else => {},
201 }
202
203 switch (b16) {
204 int16("\r\n") => p.state = .seen_rn,
205 int16("\n\n") => p.state = .finished,
206 else => {},
207 }
208
209 switch (b24) {
210 int24("\r\n\r") => p.state = .seen_rnr,
211 else => {},
212 }
213 },
214 else => unreachable,
215 }
216
217 index += vector_len;
218 continue;
219 },
220 },
221 .seen_n => switch (len - index) {
222 0 => return index,
223 else => {
224 switch (bytes[index]) {
225 '\n' => p.state = .finished,
226 else => p.state = .start,
227 }
228
229 index += 1;
230 continue;
231 },
232 },
233 .seen_r => switch (len - index) {
234 0 => return index,
235 1 => {
236 switch (bytes[index]) {
237 '\n' => p.state = .seen_rn,
238 '\r' => p.state = .seen_r,
239 else => p.state = .start,
240 }
241
242 return index + 1;
243 },
244 2 => {
245 const b16 = int16(bytes[index..][0..2]);
246 const b8 = intShift(u8, b16);
247
248 switch (b8) {
249 '\r' => p.state = .seen_r,
250 '\n' => p.state = .seen_rn,
251 else => p.state = .start,
252 }
253
254 switch (b16) {
255 int16("\r\n") => p.state = .seen_rn,
256 int16("\n\r") => p.state = .seen_rnr,
257 int16("\n\n") => p.state = .finished,
258 else => {},
259 }
260
261 return index + 2;
262 },
263 else => {
264 const b24 = int24(bytes[index..][0..3]);
265 const b16 = intShift(u16, b24);
266 const b8 = intShift(u8, b24);
267
268 switch (b8) {
269 '\r' => p.state = .seen_r,
270 '\n' => p.state = .seen_n,
271 else => p.state = .start,
272 }
273
274 switch (b16) {
275 int16("\r\n") => p.state = .seen_rn,
276 int16("\n\n") => p.state = .finished,
277 else => {},
278 }
279
280 switch (b24) {
281 int24("\n\r\n") => p.state = .finished,
282 else => {},
283 }
284
285 index += 3;
286 continue;
287 },
288 },
289 .seen_rn => switch (len - index) {
290 0 => return index,
291 1 => {
292 switch (bytes[index]) {
293 '\r' => p.state = .seen_rnr,
294 '\n' => p.state = .seen_n,
295 else => p.state = .start,
296 }
297
298 return index + 1;
299 },
300 else => {
301 const b16 = int16(bytes[index..][0..2]);
302 const b8 = intShift(u8, b16);
303
304 switch (b8) {
305 '\r' => p.state = .seen_rnr,
306 '\n' => p.state = .seen_n,
307 else => p.state = .start,
308 }
309
310 switch (b16) {
311 int16("\r\n") => p.state = .finished,
312 int16("\n\n") => p.state = .finished,
313 else => {},
314 }
315
316 index += 2;
317 continue;
318 },
319 },
320 .seen_rnr => switch (len - index) {
321 0 => return index,
322 else => {
323 switch (bytes[index]) {
324 '\n' => p.state = .finished,
325 else => p.state = .start,
326 }
327
328 index += 1;
329 continue;
330 },
331 },
332 }
333
334 return index;
335 }
336}
337
338inline fn int16(array: *const [2]u8) u16 {
339 return @bitCast(array.*);
340}
341
342inline fn int24(array: *const [3]u8) u24 {
343 return @bitCast(array.*);
344}
345
346inline fn int32(array: *const [4]u8) u32 {
347 return @bitCast(array.*);
348}
349
350inline fn intShift(comptime T: type, x: anytype) T {
351 switch (@import("builtin").cpu.arch.endian()) {
352 .little => return @truncate(x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T))),
353 .big => return @truncate(x),
354 }
355}
356
357const HeadParser = @This();
358const std = @import("std");
359const use_vectors = builtin.zig_backend != .stage2_x86_64;
360const builtin = @import("builtin");
361
362test feed {
363 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
364
365 for (0..36) |i| {
366 var p: HeadParser = .{};
367 try std.testing.expectEqual(i, p.feed(data[0..i]));
368 try std.testing.expectEqual(35 - i, p.feed(data[i..]));
369 }
370}
lib/std/http/Server.zig+698-507
...@@ -1,613 +1,805 @@...@@ -1,613 +1,805 @@
1connection: Connection,1//! Blocking HTTP server implementation.
2/// This value is determined by Server when sending headers to the client, and2
3/// then used to determine the return value of `reset`.3connection: net.Server.Connection,
4connection_keep_alive: bool,4/// Keeps track of whether the Server is ready to accept a new request on the
5/// same connection, and makes invalid API usage cause assertion failures
6/// rather than HTTP protocol violations.
7state: State,
8/// User-provided buffer that must outlive this Server.
9/// Used to store the client's entire HTTP header.
10read_buffer: []u8,
11/// Amount of available data inside read_buffer.
12read_buffer_len: usize,
13/// Index into `read_buffer` of the first byte of the next HTTP request.
14next_request_start: usize,
515
6/// The HTTP request that this response is responding to.16pub const State = enum {
7///17 /// The connection is available to be used for the first time, or reused.
8/// This field is only valid after calling `wait`.18 ready,
9request: Request,19 /// An error occurred in `receiveHead`.
1020 receiving_head,
11state: State = .first,21 /// A Request object has been obtained and from there a Response can be
22 /// opened.
23 received_head,
24 /// The client is uploading something to this Server.
25 receiving_body,
26 /// The connection is eligible for another HTTP request, however the client
27 /// and server did not negotiate connection: keep-alive.
28 closing,
29};
1230
13/// Initialize an HTTP server that can respond to multiple requests on the same31/// Initialize an HTTP server that can respond to multiple requests on the same
14/// connection.32/// connection.
15/// The returned `Server` is ready for `reset` or `wait` to be called.33/// The returned `Server` is ready for `readRequest` to be called.
16pub fn init(connection: std.net.Server.Connection, options: Server.Request.InitOptions) Server {34pub fn init(connection: net.Server.Connection, read_buffer: []u8) Server {
17 return .{35 return .{
18 .connection = .{36 .connection = connection,
19 .stream = connection.stream,37 .state = .ready,
20 .read_buf = undefined,38 .read_buffer = read_buffer,
21 .read_start = 0,39 .read_buffer_len = 0,
22 .read_end = 0,40 .next_request_start = 0,
23 },
24 .connection_keep_alive = false,
25 .request = Server.Request.init(options),
26 };41 };
27}42}
2843
29pub const State = enum {44pub const ReceiveHeadError = error{
30 first,45 /// Client sent too many bytes of HTTP headers.
31 start,46 /// The HTTP specification suggests to respond with a 431 status code
32 waited,47 /// before closing the connection.
33 responded,48 HttpHeadersOversize,
34 finished,49 /// Client sent headers that did not conform to the HTTP protocol.
35};50 HttpHeadersInvalid,
3651 /// A low level I/O error occurred trying to read the headers.
37pub const ResetState = enum { reset, closing };52 HttpHeadersUnreadable,
38
39pub const Connection = @import("Server/Connection.zig");
40
41/// The mode of transport for responses.
42pub const ResponseTransfer = union(enum) {
43 content_length: u64,
44 chunked: void,
45 none: void,
46};
47
48/// The decompressor for request messages.
49pub const Compression = union(enum) {
50 pub const DeflateDecompressor = std.compress.zlib.Decompressor(Server.TransferReader);
51 pub const GzipDecompressor = std.compress.gzip.Decompressor(Server.TransferReader);
52 // https://github.com/ziglang/zig/issues/18937
53 //pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Server.TransferReader, .{});
54
55 deflate: DeflateDecompressor,
56 gzip: GzipDecompressor,
57 // https://github.com/ziglang/zig/issues/18937
58 //zstd: ZstdDecompressor,
59 none: void,
60};53};
6154
62/// A HTTP request originating from a client.55/// The header bytes reference the read buffer that Server was initialized with
63pub const Request = struct {56/// and remain alive until the next call to receiveHead.
64 method: http.Method,57pub fn receiveHead(s: *Server) ReceiveHeadError!Request {
65 target: []const u8,58 assert(s.state == .ready);
66 version: http.Version,59 s.state = .received_head;
67 expect: ?[]const u8,60 errdefer s.state = .receiving_head;
68 content_type: ?[]const u8,61
69 content_length: ?u64,62 // In case of a reused connection, move the next request's bytes to the
70 transfer_encoding: http.TransferEncoding,63 // beginning of the buffer.
71 transfer_compression: http.ContentEncoding,64 if (s.next_request_start > 0) {
72 keep_alive: bool,65 if (s.read_buffer_len > s.next_request_start) {
73 parser: proto.HeadersParser,66 const leftover = s.read_buffer[s.next_request_start..s.read_buffer_len];
74 compression: Compression,67 const dest = s.read_buffer[0..leftover.len];
7568 if (leftover.len <= s.next_request_start) {
76 pub const InitOptions = struct {69 @memcpy(dest, leftover);
77 /// Externally-owned memory used to store the client's entire HTTP header.70 } else {
78 /// `error.HttpHeadersOversize` is returned from read() when a71 mem.copyBackwards(u8, dest, leftover);
79 /// client sends too many bytes of HTTP headers.72 }
80 client_header_buffer: []u8,73 s.read_buffer_len = leftover.len;
81 };74 }
75 s.next_request_start = 0;
76 }
8277
83 pub fn init(options: InitOptions) Request {78 var hp: http.HeadParser = .{};
84 return .{79 while (true) {
85 .method = undefined,80 const buf = s.read_buffer[s.read_buffer_len..];
86 .target = undefined,81 if (buf.len == 0)
87 .version = undefined,82 return error.HttpHeadersOversize;
88 .expect = null,83 const read_n = s.connection.stream.read(buf) catch
89 .content_type = null,84 return error.HttpHeadersUnreadable;
90 .content_length = null,85 s.read_buffer_len += read_n;
91 .transfer_encoding = .none,86 const bytes = buf[0..read_n];
92 .transfer_compression = .identity,87 const end = hp.feed(bytes);
93 .keep_alive = false,88 if (hp.state == .finished) return .{
94 .parser = proto.HeadersParser.init(options.client_header_buffer),89 .server = s,
95 .compression = .none,90 .head_end = end,
91 .head = Request.Head.parse(s.read_buffer[0..end]) catch
92 return error.HttpHeadersInvalid,
93 .reader_state = undefined,
96 };94 };
97 }95 }
96}
9897
99 pub const ParseError = error{98pub const Request = struct {
100 UnknownHttpMethod,99 server: *Server,
101 HttpHeadersInvalid,100 /// Index into Server's read_buffer.
102 HttpHeaderContinuationsUnsupported,101 head_end: usize,
103 HttpTransferEncodingUnsupported,102 head: Head,
104 HttpConnectionHeaderUnsupported,103 reader_state: union {
105 InvalidContentLength,104 remaining_content_length: u64,
106 CompressionUnsupported,105 },
106
107 pub const Compression = union(enum) {
108 pub const DeflateDecompressor = std.compress.zlib.Decompressor(std.io.AnyReader);
109 pub const GzipDecompressor = std.compress.gzip.Decompressor(std.io.AnyReader);
110 pub const ZstdDecompressor = std.compress.zstd.Decompressor(std.io.AnyReader);
111
112 deflate: DeflateDecompressor,
113 gzip: GzipDecompressor,
114 zstd: ZstdDecompressor,
115 none: void,
107 };116 };
108117
109 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {118 pub const Head = struct {
110 var it = mem.splitSequence(u8, bytes, "\r\n");119 method: http.Method,
111120 target: []const u8,
112 const first_line = it.next().?;121 version: http.Version,
113 if (first_line.len < 10)122 expect: ?[]const u8,
114 return error.HttpHeadersInvalid;123 content_type: ?[]const u8,
115124 content_length: ?u64,
116 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse125 transfer_encoding: http.TransferEncoding,
117 return error.HttpHeadersInvalid;126 transfer_compression: http.ContentEncoding,
118 if (method_end > 24) return error.HttpHeadersInvalid;127 keep_alive: bool,
119128 compression: Compression,
120 const method_str = first_line[0..method_end];129
121 const method: http.Method = @enumFromInt(http.Method.parse(method_str));130 pub const ParseError = error{
122131 UnknownHttpMethod,
123 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse132 HttpHeadersInvalid,
124 return error.HttpHeadersInvalid;133 HttpHeaderContinuationsUnsupported,
125 if (version_start == method_end) return error.HttpHeadersInvalid;134 HttpTransferEncodingUnsupported,
126135 HttpConnectionHeaderUnsupported,
127 const version_str = first_line[version_start + 1 ..];136 InvalidContentLength,
128 if (version_str.len != 8) return error.HttpHeadersInvalid;137 CompressionUnsupported,
129 const version: http.Version = switch (int64(version_str[0..8])) {138 MissingFinalNewline,
130 int64("HTTP/1.0") => .@"HTTP/1.0",
131 int64("HTTP/1.1") => .@"HTTP/1.1",
132 else => return error.HttpHeadersInvalid,
133 };139 };
134140
135 const target = first_line[method_end + 1 .. version_start];141 pub fn parse(bytes: []const u8) ParseError!Head {
136142 var it = mem.splitSequence(u8, bytes, "\r\n");
137 req.method = method;143
138 req.target = target;144 const first_line = it.next().?;
139 req.version = version;145 if (first_line.len < 10)
140146 return error.HttpHeadersInvalid;
141 while (it.next()) |line| {147
142 if (line.len == 0) return;148 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse
143 switch (line[0]) {149 return error.HttpHeadersInvalid;
144 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,150 if (method_end > 24) return error.HttpHeadersInvalid;
145 else => {},151
146 }152 const method_str = first_line[0..method_end];
147153 const method: http.Method = @enumFromInt(http.Method.parse(method_str));
148 var line_it = mem.splitSequence(u8, line, ": ");154
149 const header_name = line_it.next().?;155 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse
150 const header_value = line_it.rest();156 return error.HttpHeadersInvalid;
151 if (header_value.len == 0) return error.HttpHeadersInvalid;157 if (version_start == method_end) return error.HttpHeadersInvalid;
152158
153 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {159 const version_str = first_line[version_start + 1 ..];
154 req.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");160 if (version_str.len != 8) return error.HttpHeadersInvalid;
155 } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) {161 const version: http.Version = switch (int64(version_str[0..8])) {
156 req.expect = header_value;162 int64("HTTP/1.0") => .@"HTTP/1.0",
157 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {163 int64("HTTP/1.1") => .@"HTTP/1.1",
158 req.content_type = header_value;164 else => return error.HttpHeadersInvalid,
159 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {165 };
160 if (req.content_length != null) return error.HttpHeadersInvalid;166
161 req.content_length = std.fmt.parseInt(u64, header_value, 10) catch167 const target = first_line[method_end + 1 .. version_start];
162 return error.InvalidContentLength;168
163 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {169 var head: Head = .{
164 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid;170 .method = method,
165171 .target = target,
166 const trimmed = mem.trim(u8, header_value, " ");172 .version = version,
167173 .expect = null,
168 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {174 .content_type = null,
169 req.transfer_compression = ce;175 .content_length = null,
170 } else {176 .transfer_encoding = .none,
171 return error.HttpTransferEncodingUnsupported;177 .transfer_compression = .identity,
178 .keep_alive = false,
179 .compression = .none,
180 };
181
182 while (it.next()) |line| {
183 if (line.len == 0) return head;
184 switch (line[0]) {
185 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
186 else => {},
172 }187 }
173 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
174 // Transfer-Encoding: second, first
175 // Transfer-Encoding: deflate, chunked
176 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
177188
178 const first = iter.first();189 var line_it = mem.splitSequence(u8, line, ": ");
179 const trimmed_first = mem.trim(u8, first, " ");190 const header_name = line_it.next().?;
180191 const header_value = line_it.rest();
181 var next: ?[]const u8 = first;192 if (header_value.len == 0) return error.HttpHeadersInvalid;
182 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {193
183 if (req.transfer_encoding != .none)194 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
184 return error.HttpHeadersInvalid; // we already have a transfer encoding195 head.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
185 req.transfer_encoding = transfer;196 } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) {
186197 head.expect = header_value;
187 next = iter.next();198 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
188 }199 head.content_type = header_value;
189200 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
190 if (next) |second| {201 if (head.content_length != null) return error.HttpHeadersInvalid;
191 const trimmed_second = mem.trim(u8, second, " ");202 head.content_length = std.fmt.parseInt(u64, header_value, 10) catch
192203 return error.InvalidContentLength;
193 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {204 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
194 if (req.transfer_compression != .identity)205 if (head.transfer_compression != .identity) return error.HttpHeadersInvalid;
195 return error.HttpHeadersInvalid; // double compression is not supported206
196 req.transfer_compression = transfer;207 const trimmed = mem.trim(u8, header_value, " ");
208
209 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
210 head.transfer_compression = ce;
197 } else {211 } else {
198 return error.HttpTransferEncodingUnsupported;212 return error.HttpTransferEncodingUnsupported;
199 }213 }
200 }214 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
215 // Transfer-Encoding: second, first
216 // Transfer-Encoding: deflate, chunked
217 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
201218
202 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;219 const first = iter.first();
203 }220 const trimmed_first = mem.trim(u8, first, " ");
204 }
205 return error.HttpHeadersInvalid; // missing empty line
206 }
207221
208 inline fn int64(array: *const [8]u8) u64 {222 var next: ?[]const u8 = first;
209 return @bitCast(array.*);223 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
210 }224 if (head.transfer_encoding != .none)
211};225 return error.HttpHeadersInvalid; // we already have a transfer encoding
212226 head.transfer_encoding = transfer;
213/// Reset this response to its initial state. This must be called before
214/// handling a second request on the same connection.
215pub fn reset(res: *Server) ResetState {
216 if (res.state == .first) {
217 res.state = .start;
218 return .reset;
219 }
220
221 if (!res.request.parser.done) {
222 // If the response wasn't fully read, then we need to close the connection.
223 res.connection_keep_alive = false;
224 return .closing;
225 }
226227
227 res.state = .start;228 next = iter.next();
228 res.request = Request.init(.{229 }
229 .client_header_buffer = res.request.parser.header_bytes_buffer,
230 });
231230
232 return if (res.connection_keep_alive) .reset else .closing;231 if (next) |second| {
233}232 const trimmed_second = mem.trim(u8, second, " ");
234233
235pub const SendAllError = std.net.Stream.WriteError;234 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {
235 if (head.transfer_compression != .identity)
236 return error.HttpHeadersInvalid; // double compression is not supported
237 head.transfer_compression = transfer;
238 } else {
239 return error.HttpTransferEncodingUnsupported;
240 }
241 }
236242
237pub const SendOptions = struct {243 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
238 version: http.Version = .@"HTTP/1.1",244 }
239 status: http.Status = .ok,245 }
240 reason: ?[]const u8 = null,246 return error.MissingFinalNewline;
241 keep_alive: bool = true,247 }
242 extra_headers: []const http.Header = &.{},
243 content: []const u8,
244};
245248
246/// Send an entire HTTP response to the client, including headers and body.249 inline fn int64(array: *const [8]u8) u64 {
247/// Automatically handles HEAD requests by omitting the body.250 return @bitCast(array.*);
248/// Uses the "content-length" header.251 }
249/// Asserts status is not `continue`.252 };
250/// Asserts there are at most 25 extra_headers.
251pub fn sendAll(s: *Server, options: SendOptions) SendAllError!void {
252 const max_extra_headers = 25;
253 assert(options.status != .@"continue");
254 assert(options.extra_headers.len <= max_extra_headers);
255
256 switch (s.state) {
257 .waited => s.state = .finished,
258 .first => unreachable, // Call reset() first.
259 .start => unreachable, // Call wait() first.
260 .responded => unreachable, // Cannot mix sendAll() with send().
261 .finished => unreachable, // Call reset() first.
262 }
263253
264 s.connection_keep_alive = options.keep_alive and s.request.keep_alive;254 pub const RespondOptions = struct {
265 const keep_alive_line = if (s.connection_keep_alive)255 version: http.Version = .@"HTTP/1.1",
266 "connection: keep-alive\r\n"256 status: http.Status = .ok,
267 else257 reason: ?[]const u8 = null,
268 "";258 keep_alive: bool = true,
269 const phrase = options.reason orelse options.status.phrase() orelse "";259 extra_headers: []const http.Header = &.{},
270
271 var first_buffer: [500]u8 = undefined;
272 const first_bytes = std.fmt.bufPrint(
273 &first_buffer,
274 "{s} {d} {s}\r\n{s}content-length: {d}\r\n",
275 .{
276 @tagName(options.version),
277 @intFromEnum(options.status),
278 phrase,
279 keep_alive_line,
280 options.content.len,
281 },
282 ) catch unreachable;
283
284 var iovecs: [max_extra_headers * 4 + 3]std.posix.iovec_const = undefined;
285 var iovecs_len: usize = 0;
286
287 iovecs[iovecs_len] = .{
288 .iov_base = first_bytes.ptr,
289 .iov_len = first_bytes.len,
290 };260 };
291 iovecs_len += 1;
292261
293 for (options.extra_headers) |header| {262 /// Send an entire HTTP response to the client, including headers and body.
294 iovecs[iovecs_len] = .{263 ///
295 .iov_base = header.name.ptr,264 /// Automatically handles HEAD requests by omitting the body.
296 .iov_len = header.name.len,265 /// Uses the "content-length" header unless `content` is empty in which
297 };266 /// case it omits the content-length header.
298 iovecs_len += 1;267 ///
268 /// If the request contains a body and the connection is to be reused,
269 /// discards the request body, leaving the Server in the `ready` state. If
270 /// this discarding fails, the connection is marked as not to be reused and
271 /// no error is surfaced.
272 ///
273 /// Asserts status is not `continue`.
274 /// Asserts there are at most 25 extra_headers.
275 pub fn respond(
276 request: *Request,
277 content: []const u8,
278 options: RespondOptions,
279 ) Response.WriteError!void {
280 const max_extra_headers = 25;
281 assert(options.status != .@"continue");
282 assert(options.extra_headers.len <= max_extra_headers);
283
284 const keep_alive = request.discardBody(options.keep_alive);
285
286 const phrase = options.reason orelse options.status.phrase() orelse "";
287
288 var first_buffer: [500]u8 = undefined;
289 var h = std.ArrayListUnmanaged(u8).initBuffer(&first_buffer);
290 h.writerAssumeCapacity().print("{s} {d} {s}\r\n", .{
291 @tagName(options.version), @intFromEnum(options.status), phrase,
292 }) catch |err| switch (err) {};
293 if (keep_alive)
294 h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
295 if (content.len > 0)
296 h.writerAssumeCapacity().print("content-length: {d}\r\n", .{content.len}) catch |err|
297 switch (err) {};
298
299 var iovecs: [max_extra_headers * 4 + 3]std.posix.iovec_const = undefined;
300 var iovecs_len: usize = 0;
299301
300 iovecs[iovecs_len] = .{302 iovecs[iovecs_len] = .{
301 .iov_base = ": ",303 .iov_base = h.items.ptr,
302 .iov_len = 2,304 .iov_len = h.items.len,
303 };305 };
304 iovecs_len += 1;306 iovecs_len += 1;
305307
306 iovecs[iovecs_len] = .{308 for (options.extra_headers) |header| {
307 .iov_base = header.value.ptr,309 iovecs[iovecs_len] = .{
308 .iov_len = header.value.len,310 .iov_base = header.name.ptr,
309 };311 .iov_len = header.name.len,
310 iovecs_len += 1;312 };
313 iovecs_len += 1;
314
315 iovecs[iovecs_len] = .{
316 .iov_base = ": ",
317 .iov_len = 2,
318 };
319 iovecs_len += 1;
320
321 iovecs[iovecs_len] = .{
322 .iov_base = header.value.ptr,
323 .iov_len = header.value.len,
324 };
325 iovecs_len += 1;
326
327 iovecs[iovecs_len] = .{
328 .iov_base = "\r\n",
329 .iov_len = 2,
330 };
331 iovecs_len += 1;
332 }
311333
312 iovecs[iovecs_len] = .{334 iovecs[iovecs_len] = .{
313 .iov_base = "\r\n",335 .iov_base = "\r\n",
314 .iov_len = 2,336 .iov_len = 2,
315 };337 };
316 iovecs_len += 1;338 iovecs_len += 1;
317 }
318339
319 iovecs[iovecs_len] = .{340 if (request.head.method != .HEAD and content.len > 0) {
320 .iov_base = "\r\n",341 iovecs[iovecs_len] = .{
321 .iov_len = 2,342 .iov_base = content.ptr,
322 };343 .iov_len = content.len,
323 iovecs_len += 1;344 };
345 iovecs_len += 1;
346 }
324347
325 if (s.request.method != .HEAD) {348 try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]);
326 iovecs[iovecs_len] = .{
327 .iov_base = options.content.ptr,
328 .iov_len = options.content.len,
329 };
330 iovecs_len += 1;
331 }349 }
332350
333 return s.connection.stream.writevAll(iovecs[0..iovecs_len]);351 pub const RespondStreamingOptions = struct {
334}352 /// An externally managed slice of memory used to batch bytes before
335353 /// sending. `respondStreaming` asserts this is large enough to store
336pub const Response = struct {354 /// the full HTTP response head.
337 transfer_encoding: ResponseTransfer,355 ///
338};356 /// Must outlive the returned Response.
339357 send_buffer: []u8,
340pub const SendError = Connection.WriteError || error{358 /// If provided, the response will use the content-length header;
341 UnsupportedTransferEncoding,359 /// otherwise it will use transfer-encoding: chunked.
342 InvalidContentLength,360 content_length: ?u64 = null,
343};361 /// Options that are shared with the `respond` method.
344362 respond_options: RespondOptions = .{},
345/// Send the HTTP response headers to the client.363 };
346pub fn send(res: *Server) SendError!void {
347 switch (res.state) {
348 .waited => res.state = .responded,
349 .first, .start, .responded, .finished => unreachable,
350 }
351364
352 var buffered = std.io.bufferedWriter(res.connection.writer());365 /// The header is buffered but not sent until Response.flush is called.
353 const w = buffered.writer();366 ///
354367 /// If the request contains a body and the connection is to be reused,
355 try w.writeAll(@tagName(res.version));368 /// discards the request body, leaving the Server in the `ready` state. If
356 try w.writeByte(' ');369 /// this discarding fails, the connection is marked as not to be reused and
357 try w.print("{d}", .{@intFromEnum(res.status)});370 /// no error is surfaced.
358 try w.writeByte(' ');371 ///
359 if (res.reason) |reason| {372 /// HEAD requests are handled transparently by setting a flag on the
360 try w.writeAll(reason);373 /// returned Response to omit the body. However it may be worth noticing
361 } else if (res.status.phrase()) |phrase| {374 /// that flag and skipping any expensive work that would otherwise need to
362 try w.writeAll(phrase);375 /// be done to satisfy the request.
363 }376 ///
364 try w.writeAll("\r\n");377 /// Asserts `send_buffer` is large enough to store the entire response header.
365378 /// Asserts status is not `continue`.
366 if (res.status == .@"continue") {379 pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response {
367 res.state = .waited; // we still need to send another request after this380 const o = options.respond_options;
368 } else {381 assert(o.status != .@"continue");
369 res.connection_keep_alive = res.keep_alive and res.request.keep_alive;382
370 if (res.connection_keep_alive) {383 const keep_alive = request.discardBody(o.keep_alive);
371 try w.writeAll("connection: keep-alive\r\n");384 const phrase = o.reason orelse o.status.phrase() orelse "";
385
386 var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer);
387
388 h.writerAssumeCapacity().print("{s} {d} {s}\r\n", .{
389 @tagName(o.version), @intFromEnum(o.status), phrase,
390 }) catch |err| switch (err) {};
391 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
392
393 if (options.content_length) |len| {
394 h.writerAssumeCapacity().print("content-length: {d}\r\n", .{len}) catch |err| switch (err) {};
372 } else {395 } else {
373 try w.writeAll("connection: close\r\n");396 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");
374 }397 }
375398
376 switch (res.transfer_encoding) {399 for (o.extra_headers) |header| {
377 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),400 h.appendSliceAssumeCapacity(header.name);
378 .content_length => |content_length| try w.print("content-length: {d}\r\n", .{content_length}),401 h.appendSliceAssumeCapacity(": ");
379 .none => {},402 h.appendSliceAssumeCapacity(header.value);
403 h.appendSliceAssumeCapacity("\r\n");
380 }404 }
381405
382 for (res.extra_headers) |header| {406 h.appendSliceAssumeCapacity("\r\n");
383 try w.print("{s}: {s}\r\n", .{ header.name, header.value });
384 }
385 }
386407
387 if (res.request.method == .HEAD) {408 return .{
388 res.transfer_encoding = .none;409 .stream = request.server.connection.stream,
410 .send_buffer = options.send_buffer,
411 .send_buffer_start = 0,
412 .send_buffer_end = h.items.len,
413 .content_length = options.content_length,
414 .elide_body = request.head.method == .HEAD,
415 .chunk_len = 0,
416 };
389 }417 }
390418
391 try w.writeAll("\r\n");419 pub const ReadError = net.Stream.ReadError;
392
393 try buffered.flush();
394}
395420
396const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;421 fn read_cl(context: *const anyopaque, buffer: []u8) ReadError!usize {
422 const request: *Request = @constCast(@alignCast(@ptrCast(context)));
423 const s = request.server;
424 assert(s.state == .receiving_body);
397425
398const TransferReader = std.io.Reader(*Server, TransferReadError, transferRead);426 const remaining_content_length = &request.reader_state.remaining_content_length;
399
400fn transferReader(res: *Server) TransferReader {
401 return .{ .context = res };
402}
403427
404fn transferRead(res: *Server, buf: []u8) TransferReadError!usize {428 if (remaining_content_length.* == 0) {
405 if (res.request.parser.done) return 0;429 s.state = .ready;
430 return 0;
431 }
406432
407 var index: usize = 0;433 const available_bytes = s.read_buffer_len - request.head_end;
408 while (index == 0) {434 if (available_bytes == 0)
409 const amt = try res.request.parser.read(&res.connection, buf[index..], false);435 s.read_buffer_len += try s.connection.stream.read(s.read_buffer[request.head_end..]);
410 if (amt == 0 and res.request.parser.done) break;436
411 index += amt;437 const available_buf = s.read_buffer[request.head_end..s.read_buffer_len];
438 const len = @min(remaining_content_length.*, available_buf.len, buffer.len);
439 @memcpy(buffer[0..len], available_buf[0..len]);
440 remaining_content_length.* -= len;
441 if (remaining_content_length.* == 0)
442 s.state = .ready;
443 return len;
412 }444 }
413445
414 return index;446 fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize {
415}447 const request: *Request = @constCast(@alignCast(@ptrCast(context)));
416448 const s = request.server;
417pub const WaitError = Connection.ReadError ||449 assert(s.state == .receiving_body);
418 proto.HeadersParser.CheckCompleteHeadError || Request.ParseError ||450 _ = buffer;
419 error{CompressionUnsupported};451 @panic("TODO");
420
421/// Wait for the client to send a complete request head.
422///
423/// For correct behavior, the following rules must be followed:
424///
425/// * If this returns any error in `Connection.ReadError`, you MUST
426/// immediately close the connection by calling `deinit`.
427/// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close
428/// the connection by calling `deinit`.
429/// * If this returns `error.HttpHeadersOversize`, you MUST
430/// respond with a 431 status code and then call `deinit`.
431/// * If this returns any error in `Request.ParseError`, you MUST respond
432/// with a 400 status code and then call `deinit`.
433/// * If this returns any other error, you MUST respond with a 400 status
434/// code and then call `deinit`.
435/// * If the request has an Expect header containing 100-continue, you MUST either:
436/// * Respond with a 100 status code, then call `wait` again.
437/// * Respond with a 417 status code.
438pub fn wait(res: *Server) WaitError!void {
439 switch (res.state) {
440 .first, .start => res.state = .waited,
441 .waited, .responded, .finished => unreachable,
442 }452 }
443453
444 while (true) {454 pub const ReadAllError = ReadError || error{HttpBodyOversize};
445 try res.connection.fill();
446455
447 const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek());456 pub fn reader(request: *Request) std.io.AnyReader {
448 res.connection.drop(@intCast(nchecked));457 const s = request.server;
458 assert(s.state == .received_head);
459 s.state = .receiving_body;
460 switch (request.head.transfer_encoding) {
461 .chunked => return .{
462 .readFn = read_chunked,
463 .context = request,
464 },
465 .none => {
466 request.reader_state = .{
467 .remaining_content_length = request.head.content_length orelse 0,
468 };
469 return .{
470 .readFn = read_cl,
471 .context = request,
472 };
473 },
474 }
475 }
449476
450 if (res.request.parser.state.isContent()) break;477 /// Returns whether the connection: keep-alive header should be sent to the client.
478 /// If it would fail, it instead sets the Server state to `receiving_body`
479 /// and returns false.
480 fn discardBody(request: *Request, keep_alive: bool) bool {
481 // Prepare to receive another request on the same connection.
482 // There are two factors to consider:
483 // * Any body the client sent must be discarded.
484 // * The Server's read_buffer may already have some bytes in it from
485 // whatever came after the head, which may be the next HTTP request
486 // or the request body.
487 // If the connection won't be kept alive, then none of this matters
488 // because the connection will be severed after the response is sent.
489 const s = request.server;
490 if (keep_alive and request.head.keep_alive) switch (s.state) {
491 .received_head => {
492 s.state = .receiving_body;
493 switch (request.head.transfer_encoding) {
494 .none => t: {
495 const len = request.head.content_length orelse break :t;
496 const head_end = request.head_end;
497 var total_body_discarded: usize = 0;
498 while (true) {
499 const available_bytes = s.read_buffer_len - head_end;
500 const remaining_len = len - total_body_discarded;
501 if (available_bytes >= remaining_len) {
502 s.next_request_start = head_end + remaining_len;
503 break :t;
504 }
505 total_body_discarded += available_bytes;
506 // Preserve request header memory until receiveHead is called.
507 const buf = s.read_buffer[head_end..];
508 const read_n = s.connection.stream.read(buf) catch return false;
509 s.read_buffer_len = head_end + read_n;
510 }
511 },
512 .chunked => {
513 @panic("TODO");
514 },
515 }
516 s.state = .ready;
517 return true;
518 },
519 .receiving_body, .ready => return true,
520 else => unreachable,
521 } else {
522 s.state = .closing;
523 return false;
524 }
451 }525 }
526};
452527
453 try res.request.parse(res.request.parser.get());528pub const Response = struct {
529 stream: net.Stream,
530 send_buffer: []u8,
531 /// Index of the first byte in `send_buffer`.
532 /// This is 0 unless a short write happens in `write`.
533 send_buffer_start: usize,
534 /// Index of the last byte + 1 in `send_buffer`.
535 send_buffer_end: usize,
536 /// `null` means transfer-encoding: chunked.
537 /// As a debugging utility, counts down to zero as bytes are written.
538 content_length: ?u64,
539 elide_body: bool,
540 /// Indicates how much of the end of the `send_buffer` corresponds to a
541 /// chunk. This amount of data will be wrapped by an HTTP chunk header.
542 chunk_len: usize,
543
544 pub const WriteError = net.Stream.WriteError;
545
546 /// When using content-length, asserts that the amount of data sent matches
547 /// the value sent in the header, then calls `flush`.
548 /// Otherwise, transfer-encoding: chunked is being used, and it writes the
549 /// end-of-stream message, then flushes the stream to the system.
550 /// When request method is HEAD, does not write anything to the stream.
551 pub fn end(r: *Response) WriteError!void {
552 if (r.content_length) |len| {
553 assert(len == 0); // Trips when end() called before all bytes written.
554 return flush_cl(r);
555 }
556 if (!r.elide_body) {
557 return flush_chunked(r, &.{});
558 }
559 r.* = undefined;
560 }
454561
455 switch (res.request.transfer_encoding) {562 pub const EndChunkedOptions = struct {
456 .none => {563 trailers: []const http.Header = &.{},
457 if (res.request.content_length) |len| {564 };
458 res.request.parser.next_chunk_length = len;
459565
460 if (len == 0) res.request.parser.done = true;566 /// Asserts that the Response is using transfer-encoding: chunked.
461 } else {567 /// Writes the end-of-stream message and any optional trailers, then
462 res.request.parser.done = true;568 /// flushes the stream to the system.
463 }569 /// When request method is HEAD, does not write anything to the stream.
464 },570 /// Asserts there are at most 25 trailers.
465 .chunked => {571 pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void {
466 res.request.parser.next_chunk_length = 0;572 assert(r.content_length == null);
467 res.request.parser.state = .chunk_head_size;573 if (r.elide_body) return;
468 },574 try flush_chunked(r, options.trailers);
575 r.* = undefined;
469 }576 }
470577
471 if (!res.request.parser.done) {578 /// If using content-length, asserts that writing these bytes to the client
472 switch (res.request.transfer_compression) {579 /// would not exceed the content-length value sent in the HTTP header.
473 .identity => res.request.compression = .none,580 /// May return 0, which does not indicate end of stream. The caller decides
474 .compress, .@"x-compress" => return error.CompressionUnsupported,581 /// when the end of stream occurs by calling `end`.
475 .deflate => res.request.compression = .{582 pub fn write(r: *Response, bytes: []const u8) WriteError!usize {
476 .deflate = std.compress.zlib.decompressor(res.transferReader()),583 if (r.content_length != null) {
477 },584 return write_cl(r, bytes);
478 .gzip, .@"x-gzip" => res.request.compression = .{585 } else {
479 .gzip = std.compress.gzip.decompressor(res.transferReader()),586 return write_chunked(r, bytes);
480 },
481 .zstd => {
482 // https://github.com/ziglang/zig/issues/18937
483 return error.CompressionUnsupported;
484 },
485 }587 }
486 }588 }
487}
488589
489pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers };590 fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize {
591 const r: *Response = @constCast(@alignCast(@ptrCast(context)));
592 const len = &r.content_length.?;
593 if (r.elide_body) {
594 len.* -= bytes.len;
595 return bytes.len;
596 }
490597
491pub const Reader = std.io.Reader(*Server, ReadError, read);598 if (bytes.len + r.send_buffer_end > r.send_buffer.len) {
599 const send_buffer_len = r.send_buffer_end - r.send_buffer_start;
600 var iovecs: [2]std.posix.iovec_const = .{
601 .{
602 .iov_base = r.send_buffer.ptr + r.send_buffer_start,
603 .iov_len = send_buffer_len,
604 },
605 .{
606 .iov_base = bytes.ptr,
607 .iov_len = bytes.len,
608 },
609 };
610 const n = try r.stream.writev(&iovecs);
611
612 if (n >= send_buffer_len) {
613 // It was enough to reset the buffer.
614 r.send_buffer_start = 0;
615 r.send_buffer_end = 0;
616 const bytes_n = n - send_buffer_len;
617 len.* -= bytes_n;
618 return bytes_n;
619 }
492620
493pub fn reader(res: *Server) Reader {621 // It didn't even make it through the existing buffer, let
494 return .{ .context = res };622 // alone the new bytes provided.
495}623 r.send_buffer_start += n;
624 return 0;
625 }
496626
497/// Reads data from the response body. Must be called after `wait`.627 // All bytes can be stored in the remaining space of the buffer.
498pub fn read(res: *Server, buffer: []u8) ReadError!usize {628 @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes);
499 switch (res.state) {629 r.send_buffer_end += bytes.len;
500 .waited, .responded, .finished => {},630 len.* -= bytes.len;
501 .first, .start => unreachable,631 return bytes.len;
502 }632 }
503633
504 const out_index = switch (res.request.compression) {634 fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize {
505 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,635 const r: *Response = @constCast(@alignCast(@ptrCast(context)));
506 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,636 assert(r.content_length == null);
507 // https://github.com/ziglang/zig/issues/18937
508 //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
509 else => try res.transferRead(buffer),
510 };
511
512 if (out_index == 0) {
513 const has_trail = !res.request.parser.state.isContent();
514637
515 while (!res.request.parser.state.isContent()) { // read trailing headers638 if (r.elide_body)
516 try res.connection.fill();639 return bytes.len;
517640
518 const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek());641 if (bytes.len + r.send_buffer_end > r.send_buffer.len) {
519 res.connection.drop(@intCast(nchecked));642 const send_buffer_len = r.send_buffer_end - r.send_buffer_start;
643 const chunk_len = r.chunk_len + bytes.len;
644 var header_buf: [18]u8 = undefined;
645 const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{chunk_len}) catch unreachable;
646
647 var iovecs: [5]std.posix.iovec_const = .{
648 .{
649 .iov_base = r.send_buffer.ptr + r.send_buffer_start,
650 .iov_len = send_buffer_len - r.chunk_len,
651 },
652 .{
653 .iov_base = chunk_header.ptr,
654 .iov_len = chunk_header.len,
655 },
656 .{
657 .iov_base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len,
658 .iov_len = r.chunk_len,
659 },
660 .{
661 .iov_base = bytes.ptr,
662 .iov_len = bytes.len,
663 },
664 .{
665 .iov_base = "\r\n",
666 .iov_len = 2,
667 },
668 };
669 // TODO make this writev instead of writevAll, which involves
670 // complicating the logic of this function.
671 try r.stream.writevAll(&iovecs);
672 r.send_buffer_start = 0;
673 r.send_buffer_end = 0;
674 r.chunk_len = 0;
675 return bytes.len;
520 }676 }
521677
522 if (has_trail) {678 // All bytes can be stored in the remaining space of the buffer.
523 // The response headers before the trailers are already679 @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes);
524 // guaranteed to be valid, so they will always be parsed again680 r.send_buffer_end += bytes.len;
525 // and cannot return an error.681 r.chunk_len += bytes.len;
526 // This will *only* fail for a malformed trailer.682 return bytes.len;
527 res.request.parse(res.request.parser.get()) catch return error.InvalidTrailers;683 }
684
685 /// If using content-length, asserts that writing these bytes to the client
686 /// would not exceed the content-length value sent in the HTTP header.
687 pub fn writeAll(r: *Response, bytes: []const u8) WriteError!void {
688 var index: usize = 0;
689 while (index < bytes.len) {
690 index += try write(r, bytes[index..]);
528 }691 }
529 }692 }
530693
531 return out_index;694 /// Sends all buffered data to the client.
532}695 /// This is redundant after calling `end`.
696 pub fn flush(r: *Response) WriteError!void {
697 if (r.content_length != null) {
698 return flush_cl(r);
699 } else {
700 return flush_chunked(r, null);
701 }
702 }
533703
534/// Reads data from the response body. Must be called after `wait`.704 fn flush_cl(r: *Response) WriteError!void {
535pub fn readAll(res: *Server, buffer: []u8) !usize {705 assert(r.content_length != null);
536 var index: usize = 0;706 try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]);
537 while (index < buffer.len) {707 r.send_buffer_start = 0;
538 const amt = try read(res, buffer[index..]);708 r.send_buffer_end = 0;
539 if (amt == 0) break;
540 index += amt;
541 }709 }
542 return index;
543}
544710
545pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong };711 fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void {
712 const max_trailers = 25;
713 if (end_trailers) |trailers| assert(trailers.len <= max_trailers);
714 assert(r.content_length == null);
715 const send_buffer_len = r.send_buffer_end - r.send_buffer_start;
716 var header_buf: [18]u8 = undefined;
717 const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{r.chunk_len}) catch unreachable;
546718
547pub const Writer = std.io.Writer(*Server, WriteError, write);719 var iovecs: [max_trailers * 4 + 5]std.posix.iovec_const = undefined;
720 var iovecs_len: usize = 0;
548721
549pub fn writer(res: *Server) Writer {722 iovecs[iovecs_len] = .{
550 return .{ .context = res };723 .iov_base = r.send_buffer.ptr + r.send_buffer_start,
551}724 .iov_len = send_buffer_len - r.chunk_len,
725 };
726 iovecs_len += 1;
552727
553/// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.728 iovecs[iovecs_len] = .{
554/// Must be called after `send` and before `finish`.729 .iov_base = chunk_header.ptr,
555pub fn write(res: *Server, bytes: []const u8) WriteError!usize {730 .iov_len = chunk_header.len,
556 switch (res.state) {731 };
557 .responded => {},732 iovecs_len += 1;
558 .first, .waited, .start, .finished => unreachable,
559 }
560733
561 switch (res.transfer_encoding) {734 iovecs[iovecs_len] = .{
562 .chunked => {735 .iov_base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len,
563 if (bytes.len > 0) {736 .iov_len = r.chunk_len,
564 try res.connection.writer().print("{x}\r\n", .{bytes.len});737 };
565 try res.connection.writeAll(bytes);738 iovecs_len += 1;
566 try res.connection.writeAll("\r\n");
567 }
568739
569 return bytes.len;740 if (end_trailers) |trailers| {
570 },741 if (r.chunk_len > 0) {
571 .content_length => |*len| {742 iovecs[iovecs_len] = .{
572 if (len.* < bytes.len) return error.MessageTooLong;743 .iov_base = "\r\n0\r\n",
573744 .iov_len = 5,
574 const amt = try res.connection.write(bytes);745 };
575 len.* -= amt;746 iovecs_len += 1;
576 return amt;747 }
577 },
578 .none => return error.NotWriteable,
579 }
580}
581748
582/// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.749 for (trailers) |trailer| {
583/// Must be called after `send` and before `finish`.750 iovecs[iovecs_len] = .{
584pub fn writeAll(req: *Server, bytes: []const u8) WriteError!void {751 .iov_base = trailer.name.ptr,
585 var index: usize = 0;752 .iov_len = trailer.name.len,
586 while (index < bytes.len) {753 };
587 index += try write(req, bytes[index..]);754 iovecs_len += 1;
588 }755
589}756 iovecs[iovecs_len] = .{
757 .iov_base = ": ",
758 .iov_len = 2,
759 };
760 iovecs_len += 1;
761
762 iovecs[iovecs_len] = .{
763 .iov_base = trailer.value.ptr,
764 .iov_len = trailer.value.len,
765 };
766 iovecs_len += 1;
767
768 iovecs[iovecs_len] = .{
769 .iov_base = "\r\n",
770 .iov_len = 2,
771 };
772 iovecs_len += 1;
773 }
590774
591pub const FinishError = Connection.WriteError || error{MessageNotCompleted};775 iovecs[iovecs_len] = .{
776 .iov_base = "\r\n",
777 .iov_len = 2,
778 };
779 iovecs_len += 1;
780 } else if (r.chunk_len > 0) {
781 iovecs[iovecs_len] = .{
782 .iov_base = "\r\n",
783 .iov_len = 2,
784 };
785 iovecs_len += 1;
786 }
592787
593/// Finish the body of a request. This notifies the server that you have no more data to send.788 try r.stream.writevAll(iovecs[0..iovecs_len]);
594/// Must be called after `send`.789 r.send_buffer_start = 0;
595pub fn finish(res: *Server) FinishError!void {790 r.send_buffer_end = 0;
596 switch (res.state) {791 r.chunk_len = 0;
597 .responded => res.state = .finished,
598 .first, .waited, .start, .finished => unreachable,
599 }792 }
600793
601 switch (res.transfer_encoding) {794 pub fn writer(r: *Response) std.io.AnyWriter {
602 .chunked => try res.connection.writeAll("0\r\n\r\n"),795 return .{
603 .content_length => |len| if (len != 0) return error.MessageNotCompleted,796 .writeFn = if (r.content_length != null) write_cl else write_chunked,
604 .none => {},797 .context = r,
798 };
605 }799 }
606}800};
607801
608const builtin = @import("builtin");
609const std = @import("../std.zig");802const std = @import("../std.zig");
610const testing = std.testing;
611const http = std.http;803const http = std.http;
612const mem = std.mem;804const mem = std.mem;
613const net = std.net;805const net = std.net;
...@@ -615,4 +807,3 @@ const Uri = std.Uri;...@@ -615,4 +807,3 @@ const Uri = std.Uri;
615const assert = std.debug.assert;807const assert = std.debug.assert;
616808
617const Server = @This();809const Server = @This();
618const proto = @import("protocol.zig");
lib/std/http/Server/Connection.zig deleted-119
...@@ -1,119 +0,0 @@
1stream: std.net.Stream,
2
3read_buf: [buffer_size]u8,
4read_start: u16,
5read_end: u16,
6
7pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
8
9pub fn rawReadAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
10 return conn.stream.readAtLeast(buffer, len) catch |err| {
11 switch (err) {
12 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
13 else => return error.UnexpectedReadFailure,
14 }
15 };
16}
17
18pub fn fill(conn: *Connection) ReadError!void {
19 if (conn.read_end != conn.read_start) return;
20
21 const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);
22 if (nread == 0) return error.EndOfStream;
23 conn.read_start = 0;
24 conn.read_end = @intCast(nread);
25}
26
27pub fn peek(conn: *Connection) []const u8 {
28 return conn.read_buf[conn.read_start..conn.read_end];
29}
30
31pub fn drop(conn: *Connection, num: u16) void {
32 conn.read_start += num;
33}
34
35pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
36 assert(len <= buffer.len);
37
38 var out_index: u16 = 0;
39 while (out_index < len) {
40 const available_read = conn.read_end - conn.read_start;
41 const available_buffer = buffer.len - out_index;
42
43 if (available_read > available_buffer) { // partially read buffered data
44 @memcpy(buffer[out_index..], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);
45 out_index += @as(u16, @intCast(available_buffer));
46 conn.read_start += @as(u16, @intCast(available_buffer));
47
48 break;
49 } else if (available_read > 0) { // fully read buffered data
50 @memcpy(buffer[out_index..][0..available_read], conn.read_buf[conn.read_start..conn.read_end]);
51 out_index += available_read;
52 conn.read_start += available_read;
53
54 if (out_index >= len) break;
55 }
56
57 const leftover_buffer = available_buffer - available_read;
58 const leftover_len = len - out_index;
59
60 if (leftover_buffer > conn.read_buf.len) {
61 // skip the buffer if the output is large enough
62 return conn.rawReadAtLeast(buffer[out_index..], leftover_len);
63 }
64
65 try conn.fill();
66 }
67
68 return out_index;
69}
70
71pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
72 return conn.readAtLeast(buffer, 1);
73}
74
75pub const ReadError = error{
76 ConnectionTimedOut,
77 ConnectionResetByPeer,
78 UnexpectedReadFailure,
79 EndOfStream,
80};
81
82pub const Reader = std.io.Reader(*Connection, ReadError, read);
83
84pub fn reader(conn: *Connection) Reader {
85 return .{ .context = conn };
86}
87
88pub fn writeAll(conn: *Connection, buffer: []const u8) WriteError!void {
89 return conn.stream.writeAll(buffer) catch |err| switch (err) {
90 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
91 else => return error.UnexpectedWriteFailure,
92 };
93}
94
95pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
96 return conn.stream.write(buffer) catch |err| switch (err) {
97 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
98 else => return error.UnexpectedWriteFailure,
99 };
100}
101
102pub const WriteError = error{
103 ConnectionResetByPeer,
104 UnexpectedWriteFailure,
105};
106
107pub const Writer = std.io.Writer(*Connection, WriteError, write);
108
109pub fn writer(conn: *Connection) Writer {
110 return .{ .context = conn };
111}
112
113pub fn close(conn: *Connection) void {
114 conn.stream.close();
115}
116
117const Connection = @This();
118const std = @import("../../std.zig");
119const assert = std.debug.assert;
lib/std/http/protocol.zig+21-343
...@@ -73,339 +73,28 @@ pub const HeadersParser = struct {...@@ -73,339 +73,28 @@ pub const HeadersParser = struct {
73 return hp.header_bytes_buffer[0..hp.header_bytes_len];73 return hp.header_bytes_buffer[0..hp.header_bytes_len];
74 }74 }
7575
76 /// Returns the number of bytes consumed by headers. This is always less
77 /// than or equal to `bytes.len`.
78 /// You should check `r.state.isContent()` after this to check if the
79 /// headers are done.
80 ///
81 /// If the amount returned is less than `bytes.len`, you may assume that
82 /// the parser is in a content state and the
83 /// first byte of content is located at `bytes[result]`.
84 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {76 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
85 const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);77 var hp: std.http.HeadParser = .{
86 const len: u32 = @intCast(bytes.len);78 .state = switch (r.state) {
87 var index: u32 = 0;79 .start => .start,
8880 .seen_n => .seen_n,
89 while (true) {81 .seen_r => .seen_r,
90 switch (r.state) {82 .seen_rn => .seen_rn,
91 .invalid => unreachable,83 .seen_rnr => .seen_rnr,
92 .finished => return index,84 .finished => .finished,
93 .start => switch (len - index) {85 else => unreachable,
94 0 => return index,86 },
95 1 => {87 };
96 switch (bytes[index]) {88 const result = hp.feed(bytes);
97 '\r' => r.state = .seen_r,89 r.state = switch (hp.state) {
98 '\n' => r.state = .seen_n,90 .start => .start,
99 else => {},91 .seen_n => .seen_n,
100 }92 .seen_r => .seen_r,
10193 .seen_rn => .seen_rn,
102 return index + 1;94 .seen_rnr => .seen_rnr,
103 },95 .finished => .finished,
104 2 => {96 };
105 const b16 = int16(bytes[index..][0..2]);97 return @intCast(result);
106 const b8 = intShift(u8, b16);
107
108 switch (b8) {
109 '\r' => r.state = .seen_r,
110 '\n' => r.state = .seen_n,
111 else => {},
112 }
113
114 switch (b16) {
115 int16("\r\n") => r.state = .seen_rn,
116 int16("\n\n") => r.state = .finished,
117 else => {},
118 }
119
120 return index + 2;
121 },
122 3 => {
123 const b24 = int24(bytes[index..][0..3]);
124 const b16 = intShift(u16, b24);
125 const b8 = intShift(u8, b24);
126
127 switch (b8) {
128 '\r' => r.state = .seen_r,
129 '\n' => r.state = .seen_n,
130 else => {},
131 }
132
133 switch (b16) {
134 int16("\r\n") => r.state = .seen_rn,
135 int16("\n\n") => r.state = .finished,
136 else => {},
137 }
138
139 switch (b24) {
140 int24("\r\n\r") => r.state = .seen_rnr,
141 else => {},
142 }
143
144 return index + 3;
145 },
146 4...vector_len - 1 => {
147 const b32 = int32(bytes[index..][0..4]);
148 const b24 = intShift(u24, b32);
149 const b16 = intShift(u16, b32);
150 const b8 = intShift(u8, b32);
151
152 switch (b8) {
153 '\r' => r.state = .seen_r,
154 '\n' => r.state = .seen_n,
155 else => {},
156 }
157
158 switch (b16) {
159 int16("\r\n") => r.state = .seen_rn,
160 int16("\n\n") => r.state = .finished,
161 else => {},
162 }
163
164 switch (b24) {
165 int24("\r\n\r") => r.state = .seen_rnr,
166 else => {},
167 }
168
169 switch (b32) {
170 int32("\r\n\r\n") => r.state = .finished,
171 else => {},
172 }
173
174 index += 4;
175 continue;
176 },
177 else => {
178 const chunk = bytes[index..][0..vector_len];
179 const matches = if (use_vectors) matches: {
180 const Vector = @Vector(vector_len, u8);
181 // const BoolVector = @Vector(vector_len, bool);
182 const BitVector = @Vector(vector_len, u1);
183 const SizeVector = @Vector(vector_len, u8);
184
185 const v: Vector = chunk.*;
186 const matches_r: BitVector = @bitCast(v == @as(Vector, @splat('\r')));
187 const matches_n: BitVector = @bitCast(v == @as(Vector, @splat('\n')));
188 const matches_or: SizeVector = matches_r | matches_n;
189
190 break :matches @reduce(.Add, matches_or);
191 } else matches: {
192 var matches: u8 = 0;
193 for (chunk) |byte| switch (byte) {
194 '\r', '\n' => matches += 1,
195 else => {},
196 };
197 break :matches matches;
198 };
199 switch (matches) {
200 0 => {},
201 1 => switch (chunk[vector_len - 1]) {
202 '\r' => r.state = .seen_r,
203 '\n' => r.state = .seen_n,
204 else => {},
205 },
206 2 => {
207 const b16 = int16(chunk[vector_len - 2 ..][0..2]);
208 const b8 = intShift(u8, b16);
209
210 switch (b8) {
211 '\r' => r.state = .seen_r,
212 '\n' => r.state = .seen_n,
213 else => {},
214 }
215
216 switch (b16) {
217 int16("\r\n") => r.state = .seen_rn,
218 int16("\n\n") => r.state = .finished,
219 else => {},
220 }
221 },
222 3 => {
223 const b24 = int24(chunk[vector_len - 3 ..][0..3]);
224 const b16 = intShift(u16, b24);
225 const b8 = intShift(u8, b24);
226
227 switch (b8) {
228 '\r' => r.state = .seen_r,
229 '\n' => r.state = .seen_n,
230 else => {},
231 }
232
233 switch (b16) {
234 int16("\r\n") => r.state = .seen_rn,
235 int16("\n\n") => r.state = .finished,
236 else => {},
237 }
238
239 switch (b24) {
240 int24("\r\n\r") => r.state = .seen_rnr,
241 else => {},
242 }
243 },
244 4...vector_len => {
245 inline for (0..vector_len - 3) |i_usize| {
246 const i = @as(u32, @truncate(i_usize));
247
248 const b32 = int32(chunk[i..][0..4]);
249 const b16 = intShift(u16, b32);
250
251 if (b32 == int32("\r\n\r\n")) {
252 r.state = .finished;
253 return index + i + 4;
254 } else if (b16 == int16("\n\n")) {
255 r.state = .finished;
256 return index + i + 2;
257 }
258 }
259
260 const b24 = int24(chunk[vector_len - 3 ..][0..3]);
261 const b16 = intShift(u16, b24);
262 const b8 = intShift(u8, b24);
263
264 switch (b8) {
265 '\r' => r.state = .seen_r,
266 '\n' => r.state = .seen_n,
267 else => {},
268 }
269
270 switch (b16) {
271 int16("\r\n") => r.state = .seen_rn,
272 int16("\n\n") => r.state = .finished,
273 else => {},
274 }
275
276 switch (b24) {
277 int24("\r\n\r") => r.state = .seen_rnr,
278 else => {},
279 }
280 },
281 else => unreachable,
282 }
283
284 index += vector_len;
285 continue;
286 },
287 },
288 .seen_n => switch (len - index) {
289 0 => return index,
290 else => {
291 switch (bytes[index]) {
292 '\n' => r.state = .finished,
293 else => r.state = .start,
294 }
295
296 index += 1;
297 continue;
298 },
299 },
300 .seen_r => switch (len - index) {
301 0 => return index,
302 1 => {
303 switch (bytes[index]) {
304 '\n' => r.state = .seen_rn,
305 '\r' => r.state = .seen_r,
306 else => r.state = .start,
307 }
308
309 return index + 1;
310 },
311 2 => {
312 const b16 = int16(bytes[index..][0..2]);
313 const b8 = intShift(u8, b16);
314
315 switch (b8) {
316 '\r' => r.state = .seen_r,
317 '\n' => r.state = .seen_rn,
318 else => r.state = .start,
319 }
320
321 switch (b16) {
322 int16("\r\n") => r.state = .seen_rn,
323 int16("\n\r") => r.state = .seen_rnr,
324 int16("\n\n") => r.state = .finished,
325 else => {},
326 }
327
328 return index + 2;
329 },
330 else => {
331 const b24 = int24(bytes[index..][0..3]);
332 const b16 = intShift(u16, b24);
333 const b8 = intShift(u8, b24);
334
335 switch (b8) {
336 '\r' => r.state = .seen_r,
337 '\n' => r.state = .seen_n,
338 else => r.state = .start,
339 }
340
341 switch (b16) {
342 int16("\r\n") => r.state = .seen_rn,
343 int16("\n\n") => r.state = .finished,
344 else => {},
345 }
346
347 switch (b24) {
348 int24("\n\r\n") => r.state = .finished,
349 else => {},
350 }
351
352 index += 3;
353 continue;
354 },
355 },
356 .seen_rn => switch (len - index) {
357 0 => return index,
358 1 => {
359 switch (bytes[index]) {
360 '\r' => r.state = .seen_rnr,
361 '\n' => r.state = .seen_n,
362 else => r.state = .start,
363 }
364
365 return index + 1;
366 },
367 else => {
368 const b16 = int16(bytes[index..][0..2]);
369 const b8 = intShift(u8, b16);
370
371 switch (b8) {
372 '\r' => r.state = .seen_rnr,
373 '\n' => r.state = .seen_n,
374 else => r.state = .start,
375 }
376
377 switch (b16) {
378 int16("\r\n") => r.state = .finished,
379 int16("\n\n") => r.state = .finished,
380 else => {},
381 }
382
383 index += 2;
384 continue;
385 },
386 },
387 .seen_rnr => switch (len - index) {
388 0 => return index,
389 else => {
390 switch (bytes[index]) {
391 '\n' => r.state = .finished,
392 else => r.state = .start,
393 }
394
395 index += 1;
396 continue;
397 },
398 },
399 .chunk_head_size => unreachable,
400 .chunk_head_ext => unreachable,
401 .chunk_head_r => unreachable,
402 .chunk_data => unreachable,
403 .chunk_data_suffix => unreachable,
404 .chunk_data_suffix_r => unreachable,
405 }
406
407 return index;
408 }
409 }98 }
41099
411 /// Returns the number of bytes consumed by the chunk size. This is always100 /// Returns the number of bytes consumed by the chunk size. This is always
...@@ -775,17 +464,6 @@ const MockBufferedConnection = struct {...@@ -775,17 +464,6 @@ const MockBufferedConnection = struct {
775 }464 }
776};465};
777466
778test "HeadersParser.findHeadersEnd" {
779 var r: HeadersParser = undefined;
780 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
781
782 for (0..36) |i| {
783 r = HeadersParser.init(&.{});
784 try std.testing.expectEqual(@as(u32, @intCast(i)), r.findHeadersEnd(data[0..i]));
785 try std.testing.expectEqual(@as(u32, @intCast(35 - i)), r.findHeadersEnd(data[i..]));
786 }
787}
788
789test "HeadersParser.findChunkedLen" {467test "HeadersParser.findChunkedLen" {
790 var r: HeadersParser = undefined;468 var r: HeadersParser = undefined;
791 const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";469 const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";
lib/std/http/test.zig+40-37
...@@ -69,31 +69,35 @@ test "trailers" {...@@ -69,31 +69,35 @@ test "trailers" {
69fn serverThread(http_server: *std.net.Server) anyerror!void {69fn serverThread(http_server: *std.net.Server) anyerror!void {
70 var header_buffer: [1024]u8 = undefined;70 var header_buffer: [1024]u8 = undefined;
71 var remaining: usize = 1;71 var remaining: usize = 1;
72 accept: while (remaining != 0) : (remaining -= 1) {72 while (remaining != 0) : (remaining -= 1) {
73 const conn = try http_server.accept();73 const conn = try http_server.accept();
74 defer conn.stream.close();74 defer conn.stream.close();
7575
76 var res = std.http.Server.init(conn, .{ .client_header_buffer = &header_buffer });76 var server = std.http.Server.init(conn, &header_buffer);
7777
78 res.wait() catch |err| switch (err) {78 try testing.expectEqual(.ready, server.state);
79 error.HttpHeadersInvalid => continue :accept,79 var request = try server.receiveHead();
80 error.EndOfStream => continue,80 try serve(&request);
81 else => return err,81 try testing.expectEqual(.ready, server.state);
82 };
83 try serve(&res);
84
85 try testing.expectEqual(.reset, res.reset());
86 }82 }
87}83}
8884
89fn serve(res: *std.http.Server) !void {85fn serve(request: *std.http.Server.Request) !void {
90 try testing.expectEqualStrings(res.request.target, "/trailer");86 try testing.expectEqualStrings(request.head.target, "/trailer");
91 res.transfer_encoding = .chunked;
9287
93 try res.send();88 var send_buffer: [1024]u8 = undefined;
94 try res.writeAll("Hello, ");89 var response = request.respondStreaming(.{
95 try res.writeAll("World!\n");90 .send_buffer = &send_buffer,
96 try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");91 });
92 try response.writeAll("Hello, ");
93 try response.flush();
94 try response.writeAll("World!\n");
95 try response.flush();
96 try response.endChunked(.{
97 .trailers = &.{
98 .{ .name = "X-Checksum", .value = "aaaa" },
99 },
100 });
97}101}
98102
99test "HTTP server handles a chunked transfer coding request" {103test "HTTP server handles a chunked transfer coding request" {
...@@ -116,34 +120,33 @@ test "HTTP server handles a chunked transfer coding request" {...@@ -116,34 +120,33 @@ test "HTTP server handles a chunked transfer coding request" {
116 const max_header_size = 8192;120 const max_header_size = 8192;
117121
118 const address = try std.net.Address.parseIp("127.0.0.1", 0);122 const address = try std.net.Address.parseIp("127.0.0.1", 0);
119 var server = try address.listen(.{ .reuse_address = true });123 var socket_server = try address.listen(.{ .reuse_address = true });
120 defer server.deinit();124 defer socket_server.deinit();
121 const server_port = server.listen_address.in.getPort();125 const server_port = socket_server.listen_address.in.getPort();
122126
123 const server_thread = try std.Thread.spawn(.{}, (struct {127 const server_thread = try std.Thread.spawn(.{}, (struct {
124 fn apply(s: *std.net.Server) !void {128 fn apply(net_server: *std.net.Server) !void {
125 var header_buffer: [max_header_size]u8 = undefined;129 var header_buffer: [max_header_size]u8 = undefined;
126 const conn = try s.accept();130 const conn = try net_server.accept();
127 defer conn.stream.close();131 defer conn.stream.close();
128 var res = std.http.Server.init(conn, .{ .client_header_buffer = &header_buffer });132
129 try res.wait();133 var server = std.http.Server.init(conn, &header_buffer);
130134 var request = try server.receiveHead();
131 try expect(res.request.transfer_encoding == .chunked);135
132 const server_body: []const u8 = "message from server!\n";136 try expect(request.head.transfer_encoding == .chunked);
133 res.transfer_encoding = .{ .content_length = server_body.len };
134 res.extra_headers = &.{
135 .{ .name = "content-type", .value = "text/plain" },
136 };
137 res.keep_alive = false;
138 try res.send();
139137
140 var buf: [128]u8 = undefined;138 var buf: [128]u8 = undefined;
141 const n = try res.readAll(&buf);139 const n = try request.reader().readAll(&buf);
142 try expect(std.mem.eql(u8, buf[0..n], "ABCD"));140 try expect(std.mem.eql(u8, buf[0..n], "ABCD"));
143 _ = try res.writer().writeAll(server_body);141
144 try res.finish();142 try request.respond("message from server!\n", .{
143 .extra_headers = &.{
144 .{ .name = "content-type", .value = "text/plain" },
145 },
146 .keep_alive = false,
147 });
145 }148 }
146 }).apply, .{&server});149 }).apply, .{&socket_server});
147150
148 const request_bytes =151 const request_bytes =
149 "POST / HTTP/1.1\r\n" ++152 "POST / HTTP/1.1\r\n" ++