| ... | ... | @@ -1,613 +1,805 @@ |
| 1 | | connection: Connection, |
| 2 | | /// This value is determined by Server when sending headers to the client, and |
| 3 | | /// then used to determine the return value of `reset`. |
| 4 | | connection_keep_alive: bool, |
| 1 | //! Blocking HTTP server implementation. |
| 2 | |
| 3 | connection: net.Server.Connection, |
| 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. |
| 7 | state: State, |
| 8 | /// User-provided buffer that must outlive this Server. |
| 9 | /// Used to store the client's entire HTTP header. |
| 10 | read_buffer: []u8, |
| 11 | /// Amount of available data inside read_buffer. |
| 12 | read_buffer_len: usize, |
| 13 | /// Index into `read_buffer` of the first byte of the next HTTP request. |
| 14 | next_request_start: usize, |
| 5 | 15 | |
| 6 | | /// The HTTP request that this response is responding to. |
| 7 | | /// |
| 8 | | /// This field is only valid after calling `wait`. |
| 9 | | request: Request, |
| 10 | | |
| 11 | | state: State = .first, |
| 16 | pub const State = enum { |
| 17 | /// The connection is available to be used for the first time, or reused. |
| 18 | ready, |
| 19 | /// An error occurred in `receiveHead`. |
| 20 | receiving_head, |
| 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 | }; |
| 12 | 30 | |
| 13 | 31 | /// Initialize an HTTP server that can respond to multiple requests on the same |
| 14 | 32 | /// connection. |
| 15 | | /// The returned `Server` is ready for `reset` or `wait` to be called. |
| 16 | | pub fn init(connection: std.net.Server.Connection, options: Server.Request.InitOptions) Server { |
| 33 | /// The returned `Server` is ready for `readRequest` to be called. |
| 34 | pub fn init(connection: net.Server.Connection, read_buffer: []u8) Server { |
| 17 | 35 | return .{ |
| 18 | | .connection = .{ |
| 19 | | .stream = connection.stream, |
| 20 | | .read_buf = undefined, |
| 21 | | .read_start = 0, |
| 22 | | .read_end = 0, |
| 23 | | }, |
| 24 | | .connection_keep_alive = false, |
| 25 | | .request = Server.Request.init(options), |
| 36 | .connection = connection, |
| 37 | .state = .ready, |
| 38 | .read_buffer = read_buffer, |
| 39 | .read_buffer_len = 0, |
| 40 | .next_request_start = 0, |
| 26 | 41 | }; |
| 27 | 42 | } |
| 28 | 43 | |
| 29 | | pub const State = enum { |
| 30 | | first, |
| 31 | | start, |
| 32 | | waited, |
| 33 | | responded, |
| 34 | | finished, |
| 35 | | }; |
| 36 | | |
| 37 | | pub const ResetState = enum { reset, closing }; |
| 38 | | |
| 39 | | pub const Connection = @import("Server/Connection.zig"); |
| 40 | | |
| 41 | | /// The mode of transport for responses. |
| 42 | | pub const ResponseTransfer = union(enum) { |
| 43 | | content_length: u64, |
| 44 | | chunked: void, |
| 45 | | none: void, |
| 46 | | }; |
| 47 | | |
| 48 | | /// The decompressor for request messages. |
| 49 | | pub 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, |
| 44 | pub const ReceiveHeadError = error{ |
| 45 | /// Client sent too many bytes of HTTP headers. |
| 46 | /// The HTTP specification suggests to respond with a 431 status code |
| 47 | /// before closing the connection. |
| 48 | HttpHeadersOversize, |
| 49 | /// Client sent headers that did not conform to the HTTP protocol. |
| 50 | HttpHeadersInvalid, |
| 51 | /// A low level I/O error occurred trying to read the headers. |
| 52 | HttpHeadersUnreadable, |
| 60 | 53 | }; |
| 61 | 54 | |
| 62 | | /// A HTTP request originating from a client. |
| 63 | | pub const Request = struct { |
| 64 | | method: http.Method, |
| 65 | | target: []const u8, |
| 66 | | version: http.Version, |
| 67 | | expect: ?[]const u8, |
| 68 | | content_type: ?[]const u8, |
| 69 | | content_length: ?u64, |
| 70 | | transfer_encoding: http.TransferEncoding, |
| 71 | | transfer_compression: http.ContentEncoding, |
| 72 | | keep_alive: bool, |
| 73 | | parser: proto.HeadersParser, |
| 74 | | compression: Compression, |
| 75 | | |
| 76 | | pub const InitOptions = struct { |
| 77 | | /// Externally-owned memory used to store the client's entire HTTP header. |
| 78 | | /// `error.HttpHeadersOversize` is returned from read() when a |
| 79 | | /// client sends too many bytes of HTTP headers. |
| 80 | | client_header_buffer: []u8, |
| 81 | | }; |
| 55 | /// The header bytes reference the read buffer that Server was initialized with |
| 56 | /// and remain alive until the next call to receiveHead. |
| 57 | pub fn receiveHead(s: *Server) ReceiveHeadError!Request { |
| 58 | assert(s.state == .ready); |
| 59 | s.state = .received_head; |
| 60 | errdefer s.state = .receiving_head; |
| 61 | |
| 62 | // In case of a reused connection, move the next request's bytes to the |
| 63 | // beginning of the buffer. |
| 64 | if (s.next_request_start > 0) { |
| 65 | if (s.read_buffer_len > s.next_request_start) { |
| 66 | const leftover = s.read_buffer[s.next_request_start..s.read_buffer_len]; |
| 67 | const dest = s.read_buffer[0..leftover.len]; |
| 68 | if (leftover.len <= s.next_request_start) { |
| 69 | @memcpy(dest, leftover); |
| 70 | } else { |
| 71 | mem.copyBackwards(u8, dest, leftover); |
| 72 | } |
| 73 | s.read_buffer_len = leftover.len; |
| 74 | } |
| 75 | s.next_request_start = 0; |
| 76 | } |
| 82 | 77 | |
| 83 | | pub fn init(options: InitOptions) Request { |
| 84 | | return .{ |
| 85 | | .method = undefined, |
| 86 | | .target = undefined, |
| 87 | | .version = undefined, |
| 88 | | .expect = null, |
| 89 | | .content_type = null, |
| 90 | | .content_length = null, |
| 91 | | .transfer_encoding = .none, |
| 92 | | .transfer_compression = .identity, |
| 93 | | .keep_alive = false, |
| 94 | | .parser = proto.HeadersParser.init(options.client_header_buffer), |
| 95 | | .compression = .none, |
| 78 | var hp: http.HeadParser = .{}; |
| 79 | while (true) { |
| 80 | const buf = s.read_buffer[s.read_buffer_len..]; |
| 81 | if (buf.len == 0) |
| 82 | return error.HttpHeadersOversize; |
| 83 | const read_n = s.connection.stream.read(buf) catch |
| 84 | return error.HttpHeadersUnreadable; |
| 85 | s.read_buffer_len += read_n; |
| 86 | const bytes = buf[0..read_n]; |
| 87 | const end = hp.feed(bytes); |
| 88 | if (hp.state == .finished) return .{ |
| 89 | .server = s, |
| 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 | } |
| 98 | 97 | |
| 99 | | pub const ParseError = error{ |
| 100 | | UnknownHttpMethod, |
| 101 | | HttpHeadersInvalid, |
| 102 | | HttpHeaderContinuationsUnsupported, |
| 103 | | HttpTransferEncodingUnsupported, |
| 104 | | HttpConnectionHeaderUnsupported, |
| 105 | | InvalidContentLength, |
| 106 | | CompressionUnsupported, |
| 98 | pub const Request = struct { |
| 99 | server: *Server, |
| 100 | /// Index into Server's read_buffer. |
| 101 | head_end: usize, |
| 102 | head: Head, |
| 103 | reader_state: union { |
| 104 | remaining_content_length: u64, |
| 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 | }; |
| 108 | 117 | |
| 109 | | pub fn parse(req: *Request, bytes: []const u8) ParseError!void { |
| 110 | | var it = mem.splitSequence(u8, bytes, "\r\n"); |
| 111 | | |
| 112 | | const first_line = it.next().?; |
| 113 | | if (first_line.len < 10) |
| 114 | | return error.HttpHeadersInvalid; |
| 115 | | |
| 116 | | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse |
| 117 | | return error.HttpHeadersInvalid; |
| 118 | | if (method_end > 24) return error.HttpHeadersInvalid; |
| 119 | | |
| 120 | | const method_str = first_line[0..method_end]; |
| 121 | | const method: http.Method = @enumFromInt(http.Method.parse(method_str)); |
| 122 | | |
| 123 | | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse |
| 124 | | return error.HttpHeadersInvalid; |
| 125 | | if (version_start == method_end) return error.HttpHeadersInvalid; |
| 126 | | |
| 127 | | const version_str = first_line[version_start + 1 ..]; |
| 128 | | if (version_str.len != 8) return error.HttpHeadersInvalid; |
| 129 | | const version: http.Version = switch (int64(version_str[0..8])) { |
| 130 | | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 131 | | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 132 | | else => return error.HttpHeadersInvalid, |
| 118 | pub const Head = struct { |
| 119 | method: http.Method, |
| 120 | target: []const u8, |
| 121 | version: http.Version, |
| 122 | expect: ?[]const u8, |
| 123 | content_type: ?[]const u8, |
| 124 | content_length: ?u64, |
| 125 | transfer_encoding: http.TransferEncoding, |
| 126 | transfer_compression: http.ContentEncoding, |
| 127 | keep_alive: bool, |
| 128 | compression: Compression, |
| 129 | |
| 130 | pub const ParseError = error{ |
| 131 | UnknownHttpMethod, |
| 132 | HttpHeadersInvalid, |
| 133 | HttpHeaderContinuationsUnsupported, |
| 134 | HttpTransferEncodingUnsupported, |
| 135 | HttpConnectionHeaderUnsupported, |
| 136 | InvalidContentLength, |
| 137 | CompressionUnsupported, |
| 138 | MissingFinalNewline, |
| 133 | 139 | }; |
| 134 | 140 | |
| 135 | | const target = first_line[method_end + 1 .. version_start]; |
| 136 | | |
| 137 | | req.method = method; |
| 138 | | req.target = target; |
| 139 | | req.version = version; |
| 140 | | |
| 141 | | while (it.next()) |line| { |
| 142 | | if (line.len == 0) return; |
| 143 | | switch (line[0]) { |
| 144 | | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, |
| 145 | | else => {}, |
| 146 | | } |
| 147 | | |
| 148 | | var line_it = mem.splitSequence(u8, line, ": "); |
| 149 | | const header_name = line_it.next().?; |
| 150 | | const header_value = line_it.rest(); |
| 151 | | if (header_value.len == 0) return error.HttpHeadersInvalid; |
| 152 | | |
| 153 | | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { |
| 154 | | req.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); |
| 155 | | } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) { |
| 156 | | req.expect = header_value; |
| 157 | | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { |
| 158 | | req.content_type = header_value; |
| 159 | | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 160 | | if (req.content_length != null) return error.HttpHeadersInvalid; |
| 161 | | req.content_length = std.fmt.parseInt(u64, header_value, 10) catch |
| 162 | | return error.InvalidContentLength; |
| 163 | | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { |
| 164 | | if (req.transfer_compression != .identity) return error.HttpHeadersInvalid; |
| 165 | | |
| 166 | | const trimmed = mem.trim(u8, header_value, " "); |
| 167 | | |
| 168 | | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 169 | | req.transfer_compression = ce; |
| 170 | | } else { |
| 171 | | return error.HttpTransferEncodingUnsupported; |
| 141 | pub fn parse(bytes: []const u8) ParseError!Head { |
| 142 | var it = mem.splitSequence(u8, bytes, "\r\n"); |
| 143 | |
| 144 | const first_line = it.next().?; |
| 145 | if (first_line.len < 10) |
| 146 | return error.HttpHeadersInvalid; |
| 147 | |
| 148 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse |
| 149 | return error.HttpHeadersInvalid; |
| 150 | if (method_end > 24) return error.HttpHeadersInvalid; |
| 151 | |
| 152 | const method_str = first_line[0..method_end]; |
| 153 | const method: http.Method = @enumFromInt(http.Method.parse(method_str)); |
| 154 | |
| 155 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse |
| 156 | return error.HttpHeadersInvalid; |
| 157 | if (version_start == method_end) return error.HttpHeadersInvalid; |
| 158 | |
| 159 | const version_str = first_line[version_start + 1 ..]; |
| 160 | if (version_str.len != 8) return error.HttpHeadersInvalid; |
| 161 | const version: http.Version = switch (int64(version_str[0..8])) { |
| 162 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 163 | int64("HTTP/1.1") => .@"HTTP/1.1", |
| 164 | else => return error.HttpHeadersInvalid, |
| 165 | }; |
| 166 | |
| 167 | const target = first_line[method_end + 1 .. version_start]; |
| 168 | |
| 169 | var head: Head = .{ |
| 170 | .method = method, |
| 171 | .target = target, |
| 172 | .version = version, |
| 173 | .expect = null, |
| 174 | .content_type = null, |
| 175 | .content_length = null, |
| 176 | .transfer_encoding = .none, |
| 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, ','); |
| 177 | 188 | |
| 178 | | const first = iter.first(); |
| 179 | | const trimmed_first = mem.trim(u8, first, " "); |
| 180 | | |
| 181 | | var next: ?[]const u8 = first; |
| 182 | | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { |
| 183 | | if (req.transfer_encoding != .none) |
| 184 | | return error.HttpHeadersInvalid; // we already have a transfer encoding |
| 185 | | req.transfer_encoding = transfer; |
| 186 | | |
| 187 | | next = iter.next(); |
| 188 | | } |
| 189 | | |
| 190 | | if (next) |second| { |
| 191 | | const trimmed_second = mem.trim(u8, second, " "); |
| 192 | | |
| 193 | | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { |
| 194 | | if (req.transfer_compression != .identity) |
| 195 | | return error.HttpHeadersInvalid; // double compression is not supported |
| 196 | | req.transfer_compression = transfer; |
| 189 | var line_it = mem.splitSequence(u8, line, ": "); |
| 190 | const header_name = line_it.next().?; |
| 191 | const header_value = line_it.rest(); |
| 192 | if (header_value.len == 0) return error.HttpHeadersInvalid; |
| 193 | |
| 194 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { |
| 195 | head.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); |
| 196 | } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) { |
| 197 | head.expect = header_value; |
| 198 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { |
| 199 | head.content_type = header_value; |
| 200 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 201 | if (head.content_length != null) return error.HttpHeadersInvalid; |
| 202 | head.content_length = std.fmt.parseInt(u64, header_value, 10) catch |
| 203 | return error.InvalidContentLength; |
| 204 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { |
| 205 | if (head.transfer_compression != .identity) return error.HttpHeadersInvalid; |
| 206 | |
| 207 | const trimmed = mem.trim(u8, header_value, " "); |
| 208 | |
| 209 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 210 | head.transfer_compression = ce; |
| 197 | 211 | } else { |
| 198 | 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, ','); |
| 201 | 218 | |
| 202 | | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; |
| 203 | | } |
| 204 | | } |
| 205 | | return error.HttpHeadersInvalid; // missing empty line |
| 206 | | } |
| 219 | const first = iter.first(); |
| 220 | const trimmed_first = mem.trim(u8, first, " "); |
| 207 | 221 | |
| 208 | | inline fn int64(array: *const [8]u8) u64 { |
| 209 | | return @bitCast(array.*); |
| 210 | | } |
| 211 | | }; |
| 212 | | |
| 213 | | /// Reset this response to its initial state. This must be called before |
| 214 | | /// handling a second request on the same connection. |
| 215 | | pub 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 | | } |
| 222 | var next: ?[]const u8 = first; |
| 223 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { |
| 224 | if (head.transfer_encoding != .none) |
| 225 | return error.HttpHeadersInvalid; // we already have a transfer encoding |
| 226 | head.transfer_encoding = transfer; |
| 226 | 227 | |
| 227 | | res.state = .start; |
| 228 | | res.request = Request.init(.{ |
| 229 | | .client_header_buffer = res.request.parser.header_bytes_buffer, |
| 230 | | }); |
| 228 | next = iter.next(); |
| 229 | } |
| 231 | 230 | |
| 232 | | return if (res.connection_keep_alive) .reset else .closing; |
| 233 | | } |
| 231 | if (next) |second| { |
| 232 | const trimmed_second = mem.trim(u8, second, " "); |
| 234 | 233 | |
| 235 | | pub 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 | } |
| 236 | 242 | |
| 237 | | pub const SendOptions = struct { |
| 238 | | version: http.Version = .@"HTTP/1.1", |
| 239 | | status: http.Status = .ok, |
| 240 | | reason: ?[]const u8 = null, |
| 241 | | keep_alive: bool = true, |
| 242 | | extra_headers: []const http.Header = &.{}, |
| 243 | | content: []const u8, |
| 244 | | }; |
| 243 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; |
| 244 | } |
| 245 | } |
| 246 | return error.MissingFinalNewline; |
| 247 | } |
| 245 | 248 | |
| 246 | | /// Send an entire HTTP response to the client, including headers and body. |
| 247 | | /// Automatically handles HEAD requests by omitting the body. |
| 248 | | /// Uses the "content-length" header. |
| 249 | | /// Asserts status is not `continue`. |
| 250 | | /// Asserts there are at most 25 extra_headers. |
| 251 | | pub 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 | | } |
| 249 | inline fn int64(array: *const [8]u8) u64 { |
| 250 | return @bitCast(array.*); |
| 251 | } |
| 252 | }; |
| 263 | 253 | |
| 264 | | s.connection_keep_alive = options.keep_alive and s.request.keep_alive; |
| 265 | | const keep_alive_line = if (s.connection_keep_alive) |
| 266 | | "connection: keep-alive\r\n" |
| 267 | | else |
| 268 | | ""; |
| 269 | | const phrase = options.reason orelse options.status.phrase() orelse ""; |
| 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, |
| 254 | pub const RespondOptions = struct { |
| 255 | version: http.Version = .@"HTTP/1.1", |
| 256 | status: http.Status = .ok, |
| 257 | reason: ?[]const u8 = null, |
| 258 | keep_alive: bool = true, |
| 259 | extra_headers: []const http.Header = &.{}, |
| 290 | 260 | }; |
| 291 | | iovecs_len += 1; |
| 292 | 261 | |
| 293 | | for (options.extra_headers) |header| { |
| 294 | | iovecs[iovecs_len] = .{ |
| 295 | | .iov_base = header.name.ptr, |
| 296 | | .iov_len = header.name.len, |
| 297 | | }; |
| 298 | | iovecs_len += 1; |
| 262 | /// Send an entire HTTP response to the client, including headers and body. |
| 263 | /// |
| 264 | /// Automatically handles HEAD requests by omitting the body. |
| 265 | /// Uses the "content-length" header unless `content` is empty in which |
| 266 | /// case it omits the content-length header. |
| 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; |
| 299 | 301 | |
| 300 | 302 | iovecs[iovecs_len] = .{ |
| 301 | | .iov_base = ": ", |
| 302 | | .iov_len = 2, |
| 303 | .iov_base = h.items.ptr, |
| 304 | .iov_len = h.items.len, |
| 303 | 305 | }; |
| 304 | 306 | iovecs_len += 1; |
| 305 | 307 | |
| 306 | | iovecs[iovecs_len] = .{ |
| 307 | | .iov_base = header.value.ptr, |
| 308 | | .iov_len = header.value.len, |
| 309 | | }; |
| 310 | | iovecs_len += 1; |
| 308 | for (options.extra_headers) |header| { |
| 309 | iovecs[iovecs_len] = .{ |
| 310 | .iov_base = header.name.ptr, |
| 311 | .iov_len = header.name.len, |
| 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 | } |
| 311 | 333 | |
| 312 | 334 | iovecs[iovecs_len] = .{ |
| 313 | 335 | .iov_base = "\r\n", |
| 314 | 336 | .iov_len = 2, |
| 315 | 337 | }; |
| 316 | 338 | iovecs_len += 1; |
| 317 | | } |
| 318 | 339 | |
| 319 | | iovecs[iovecs_len] = .{ |
| 320 | | .iov_base = "\r\n", |
| 321 | | .iov_len = 2, |
| 322 | | }; |
| 323 | | iovecs_len += 1; |
| 340 | if (request.head.method != .HEAD and content.len > 0) { |
| 341 | iovecs[iovecs_len] = .{ |
| 342 | .iov_base = content.ptr, |
| 343 | .iov_len = content.len, |
| 344 | }; |
| 345 | iovecs_len += 1; |
| 346 | } |
| 324 | 347 | |
| 325 | | if (s.request.method != .HEAD) { |
| 326 | | iovecs[iovecs_len] = .{ |
| 327 | | .iov_base = options.content.ptr, |
| 328 | | .iov_len = options.content.len, |
| 329 | | }; |
| 330 | | iovecs_len += 1; |
| 348 | try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]); |
| 331 | 349 | } |
| 332 | 350 | |
| 333 | | return s.connection.stream.writevAll(iovecs[0..iovecs_len]); |
| 334 | | } |
| 335 | | |
| 336 | | pub const Response = struct { |
| 337 | | transfer_encoding: ResponseTransfer, |
| 338 | | }; |
| 339 | | |
| 340 | | pub const SendError = Connection.WriteError || error{ |
| 341 | | UnsupportedTransferEncoding, |
| 342 | | InvalidContentLength, |
| 343 | | }; |
| 344 | | |
| 345 | | /// Send the HTTP response headers to the client. |
| 346 | | pub fn send(res: *Server) SendError!void { |
| 347 | | switch (res.state) { |
| 348 | | .waited => res.state = .responded, |
| 349 | | .first, .start, .responded, .finished => unreachable, |
| 350 | | } |
| 351 | pub const RespondStreamingOptions = struct { |
| 352 | /// An externally managed slice of memory used to batch bytes before |
| 353 | /// sending. `respondStreaming` asserts this is large enough to store |
| 354 | /// the full HTTP response head. |
| 355 | /// |
| 356 | /// Must outlive the returned Response. |
| 357 | send_buffer: []u8, |
| 358 | /// If provided, the response will use the content-length header; |
| 359 | /// otherwise it will use transfer-encoding: chunked. |
| 360 | content_length: ?u64 = null, |
| 361 | /// Options that are shared with the `respond` method. |
| 362 | respond_options: RespondOptions = .{}, |
| 363 | }; |
| 351 | 364 | |
| 352 | | var buffered = std.io.bufferedWriter(res.connection.writer()); |
| 353 | | const w = buffered.writer(); |
| 354 | | |
| 355 | | try w.writeAll(@tagName(res.version)); |
| 356 | | try w.writeByte(' '); |
| 357 | | try w.print("{d}", .{@intFromEnum(res.status)}); |
| 358 | | try w.writeByte(' '); |
| 359 | | if (res.reason) |reason| { |
| 360 | | try w.writeAll(reason); |
| 361 | | } else if (res.status.phrase()) |phrase| { |
| 362 | | try w.writeAll(phrase); |
| 363 | | } |
| 364 | | try w.writeAll("\r\n"); |
| 365 | | |
| 366 | | if (res.status == .@"continue") { |
| 367 | | res.state = .waited; // we still need to send another request after this |
| 368 | | } else { |
| 369 | | res.connection_keep_alive = res.keep_alive and res.request.keep_alive; |
| 370 | | if (res.connection_keep_alive) { |
| 371 | | try w.writeAll("connection: keep-alive\r\n"); |
| 365 | /// The header is buffered but not sent until Response.flush is called. |
| 366 | /// |
| 367 | /// If the request contains a body and the connection is to be reused, |
| 368 | /// discards the request body, leaving the Server in the `ready` state. If |
| 369 | /// this discarding fails, the connection is marked as not to be reused and |
| 370 | /// no error is surfaced. |
| 371 | /// |
| 372 | /// HEAD requests are handled transparently by setting a flag on the |
| 373 | /// returned Response to omit the body. However it may be worth noticing |
| 374 | /// that flag and skipping any expensive work that would otherwise need to |
| 375 | /// be done to satisfy the request. |
| 376 | /// |
| 377 | /// Asserts `send_buffer` is large enough to store the entire response header. |
| 378 | /// Asserts status is not `continue`. |
| 379 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response { |
| 380 | const o = options.respond_options; |
| 381 | assert(o.status != .@"continue"); |
| 382 | |
| 383 | const keep_alive = request.discardBody(o.keep_alive); |
| 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 | 395 | } else { |
| 373 | | try w.writeAll("connection: close\r\n"); |
| 396 | h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"); |
| 374 | 397 | } |
| 375 | 398 | |
| 376 | | switch (res.transfer_encoding) { |
| 377 | | .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), |
| 378 | | .content_length => |content_length| try w.print("content-length: {d}\r\n", .{content_length}), |
| 379 | | .none => {}, |
| 399 | for (o.extra_headers) |header| { |
| 400 | h.appendSliceAssumeCapacity(header.name); |
| 401 | h.appendSliceAssumeCapacity(": "); |
| 402 | h.appendSliceAssumeCapacity(header.value); |
| 403 | h.appendSliceAssumeCapacity("\r\n"); |
| 380 | 404 | } |
| 381 | 405 | |
| 382 | | for (res.extra_headers) |header| { |
| 383 | | try w.print("{s}: {s}\r\n", .{ header.name, header.value }); |
| 384 | | } |
| 385 | | } |
| 406 | h.appendSliceAssumeCapacity("\r\n"); |
| 386 | 407 | |
| 387 | | if (res.request.method == .HEAD) { |
| 388 | | res.transfer_encoding = .none; |
| 408 | return .{ |
| 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 | } |
| 390 | 418 | |
| 391 | | try w.writeAll("\r\n"); |
| 392 | | |
| 393 | | try buffered.flush(); |
| 394 | | } |
| 419 | pub const ReadError = net.Stream.ReadError; |
| 395 | 420 | |
| 396 | | const 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); |
| 397 | 425 | |
| 398 | | const TransferReader = std.io.Reader(*Server, TransferReadError, transferRead); |
| 399 | | |
| 400 | | fn transferReader(res: *Server) TransferReader { |
| 401 | | return .{ .context = res }; |
| 402 | | } |
| 426 | const remaining_content_length = &request.reader_state.remaining_content_length; |
| 403 | 427 | |
| 404 | | fn transferRead(res: *Server, buf: []u8) TransferReadError!usize { |
| 405 | | if (res.request.parser.done) return 0; |
| 428 | if (remaining_content_length.* == 0) { |
| 429 | s.state = .ready; |
| 430 | return 0; |
| 431 | } |
| 406 | 432 | |
| 407 | | var index: usize = 0; |
| 408 | | while (index == 0) { |
| 409 | | const amt = try res.request.parser.read(&res.connection, buf[index..], false); |
| 410 | | if (amt == 0 and res.request.parser.done) break; |
| 411 | | index += amt; |
| 433 | const available_bytes = s.read_buffer_len - request.head_end; |
| 434 | if (available_bytes == 0) |
| 435 | s.read_buffer_len += try s.connection.stream.read(s.read_buffer[request.head_end..]); |
| 436 | |
| 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 | } |
| 413 | 445 | |
| 414 | | return index; |
| 415 | | } |
| 416 | | |
| 417 | | pub const WaitError = Connection.ReadError || |
| 418 | | proto.HeadersParser.CheckCompleteHeadError || Request.ParseError || |
| 419 | | error{CompressionUnsupported}; |
| 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. |
| 438 | | pub fn wait(res: *Server) WaitError!void { |
| 439 | | switch (res.state) { |
| 440 | | .first, .start => res.state = .waited, |
| 441 | | .waited, .responded, .finished => unreachable, |
| 446 | fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize { |
| 447 | const request: *Request = @constCast(@alignCast(@ptrCast(context))); |
| 448 | const s = request.server; |
| 449 | assert(s.state == .receiving_body); |
| 450 | _ = buffer; |
| 451 | @panic("TODO"); |
| 442 | 452 | } |
| 443 | 453 | |
| 444 | | while (true) { |
| 445 | | try res.connection.fill(); |
| 454 | pub const ReadAllError = ReadError || error{HttpBodyOversize}; |
| 446 | 455 | |
| 447 | | const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek()); |
| 448 | | res.connection.drop(@intCast(nchecked)); |
| 456 | pub fn reader(request: *Request) std.io.AnyReader { |
| 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 | } |
| 449 | 476 | |
| 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 | }; |
| 452 | 527 | |
| 453 | | try res.request.parse(res.request.parser.get()); |
| 528 | pub 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 | } |
| 454 | 561 | |
| 455 | | switch (res.request.transfer_encoding) { |
| 456 | | .none => { |
| 457 | | if (res.request.content_length) |len| { |
| 458 | | res.request.parser.next_chunk_length = len; |
| 562 | pub const EndChunkedOptions = struct { |
| 563 | trailers: []const http.Header = &.{}, |
| 564 | }; |
| 459 | 565 | |
| 460 | | if (len == 0) res.request.parser.done = true; |
| 461 | | } else { |
| 462 | | res.request.parser.done = true; |
| 463 | | } |
| 464 | | }, |
| 465 | | .chunked => { |
| 466 | | res.request.parser.next_chunk_length = 0; |
| 467 | | res.request.parser.state = .chunk_head_size; |
| 468 | | }, |
| 566 | /// Asserts that the Response is using transfer-encoding: chunked. |
| 567 | /// Writes the end-of-stream message and any optional trailers, then |
| 568 | /// flushes the stream to the system. |
| 569 | /// When request method is HEAD, does not write anything to the stream. |
| 570 | /// Asserts there are at most 25 trailers. |
| 571 | pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void { |
| 572 | assert(r.content_length == null); |
| 573 | if (r.elide_body) return; |
| 574 | try flush_chunked(r, options.trailers); |
| 575 | r.* = undefined; |
| 469 | 576 | } |
| 470 | 577 | |
| 471 | | if (!res.request.parser.done) { |
| 472 | | switch (res.request.transfer_compression) { |
| 473 | | .identity => res.request.compression = .none, |
| 474 | | .compress, .@"x-compress" => return error.CompressionUnsupported, |
| 475 | | .deflate => res.request.compression = .{ |
| 476 | | .deflate = std.compress.zlib.decompressor(res.transferReader()), |
| 477 | | }, |
| 478 | | .gzip, .@"x-gzip" => res.request.compression = .{ |
| 479 | | .gzip = std.compress.gzip.decompressor(res.transferReader()), |
| 480 | | }, |
| 481 | | .zstd => { |
| 482 | | // https://github.com/ziglang/zig/issues/18937 |
| 483 | | return error.CompressionUnsupported; |
| 484 | | }, |
| 578 | /// If using content-length, asserts that writing these bytes to the client |
| 579 | /// would not exceed the content-length value sent in the HTTP header. |
| 580 | /// May return 0, which does not indicate end of stream. The caller decides |
| 581 | /// when the end of stream occurs by calling `end`. |
| 582 | pub fn write(r: *Response, bytes: []const u8) WriteError!usize { |
| 583 | if (r.content_length != null) { |
| 584 | return write_cl(r, bytes); |
| 585 | } else { |
| 586 | return write_chunked(r, bytes); |
| 485 | 587 | } |
| 486 | 588 | } |
| 487 | | } |
| 488 | 589 | |
| 489 | | pub 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 | } |
| 490 | 597 | |
| 491 | | pub 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 | } |
| 492 | 620 | |
| 493 | | pub fn reader(res: *Server) Reader { |
| 494 | | return .{ .context = res }; |
| 495 | | } |
| 621 | // It didn't even make it through the existing buffer, let |
| 622 | // alone the new bytes provided. |
| 623 | r.send_buffer_start += n; |
| 624 | return 0; |
| 625 | } |
| 496 | 626 | |
| 497 | | /// Reads data from the response body. Must be called after `wait`. |
| 498 | | pub fn read(res: *Server, buffer: []u8) ReadError!usize { |
| 499 | | switch (res.state) { |
| 500 | | .waited, .responded, .finished => {}, |
| 501 | | .first, .start => unreachable, |
| 627 | // All bytes can be stored in the remaining space of the buffer. |
| 628 | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); |
| 629 | r.send_buffer_end += bytes.len; |
| 630 | len.* -= bytes.len; |
| 631 | return bytes.len; |
| 502 | 632 | } |
| 503 | 633 | |
| 504 | | const out_index = switch (res.request.compression) { |
| 505 | | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, |
| 506 | | .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, |
| 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(); |
| 634 | fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize { |
| 635 | const r: *Response = @constCast(@alignCast(@ptrCast(context))); |
| 636 | assert(r.content_length == null); |
| 514 | 637 | |
| 515 | | while (!res.request.parser.state.isContent()) { // read trailing headers |
| 516 | | try res.connection.fill(); |
| 638 | if (r.elide_body) |
| 639 | return bytes.len; |
| 517 | 640 | |
| 518 | | const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek()); |
| 519 | | res.connection.drop(@intCast(nchecked)); |
| 641 | if (bytes.len + r.send_buffer_end > r.send_buffer.len) { |
| 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 | } |
| 521 | 677 | |
| 522 | | if (has_trail) { |
| 523 | | // The response headers before the trailers are already |
| 524 | | // guaranteed to be valid, so they will always be parsed again |
| 525 | | // and cannot return an error. |
| 526 | | // This will *only* fail for a malformed trailer. |
| 527 | | res.request.parse(res.request.parser.get()) catch return error.InvalidTrailers; |
| 678 | // All bytes can be stored in the remaining space of the buffer. |
| 679 | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); |
| 680 | r.send_buffer_end += bytes.len; |
| 681 | r.chunk_len += bytes.len; |
| 682 | return bytes.len; |
| 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 | } |
| 530 | 693 | |
| 531 | | return out_index; |
| 532 | | } |
| 694 | /// Sends all buffered data to the client. |
| 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 | } |
| 533 | 703 | |
| 534 | | /// Reads data from the response body. Must be called after `wait`. |
| 535 | | pub fn readAll(res: *Server, buffer: []u8) !usize { |
| 536 | | var index: usize = 0; |
| 537 | | while (index < buffer.len) { |
| 538 | | const amt = try read(res, buffer[index..]); |
| 539 | | if (amt == 0) break; |
| 540 | | index += amt; |
| 704 | fn flush_cl(r: *Response) WriteError!void { |
| 705 | assert(r.content_length != null); |
| 706 | try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]); |
| 707 | r.send_buffer_start = 0; |
| 708 | r.send_buffer_end = 0; |
| 541 | 709 | } |
| 542 | | return index; |
| 543 | | } |
| 544 | 710 | |
| 545 | | pub 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; |
| 546 | 718 | |
| 547 | | pub 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; |
| 548 | 721 | |
| 549 | | pub fn writer(res: *Server) Writer { |
| 550 | | return .{ .context = res }; |
| 551 | | } |
| 722 | iovecs[iovecs_len] = .{ |
| 723 | .iov_base = r.send_buffer.ptr + r.send_buffer_start, |
| 724 | .iov_len = send_buffer_len - r.chunk_len, |
| 725 | }; |
| 726 | iovecs_len += 1; |
| 552 | 727 | |
| 553 | | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 554 | | /// Must be called after `send` and before `finish`. |
| 555 | | pub fn write(res: *Server, bytes: []const u8) WriteError!usize { |
| 556 | | switch (res.state) { |
| 557 | | .responded => {}, |
| 558 | | .first, .waited, .start, .finished => unreachable, |
| 559 | | } |
| 728 | iovecs[iovecs_len] = .{ |
| 729 | .iov_base = chunk_header.ptr, |
| 730 | .iov_len = chunk_header.len, |
| 731 | }; |
| 732 | iovecs_len += 1; |
| 560 | 733 | |
| 561 | | switch (res.transfer_encoding) { |
| 562 | | .chunked => { |
| 563 | | if (bytes.len > 0) { |
| 564 | | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| 565 | | try res.connection.writeAll(bytes); |
| 566 | | try res.connection.writeAll("\r\n"); |
| 567 | | } |
| 734 | iovecs[iovecs_len] = .{ |
| 735 | .iov_base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len, |
| 736 | .iov_len = r.chunk_len, |
| 737 | }; |
| 738 | iovecs_len += 1; |
| 568 | 739 | |
| 569 | | return bytes.len; |
| 570 | | }, |
| 571 | | .content_length => |*len| { |
| 572 | | if (len.* < bytes.len) return error.MessageTooLong; |
| 573 | | |
| 574 | | const amt = try res.connection.write(bytes); |
| 575 | | len.* -= amt; |
| 576 | | return amt; |
| 577 | | }, |
| 578 | | .none => return error.NotWriteable, |
| 579 | | } |
| 580 | | } |
| 740 | if (end_trailers) |trailers| { |
| 741 | if (r.chunk_len > 0) { |
| 742 | iovecs[iovecs_len] = .{ |
| 743 | .iov_base = "\r\n0\r\n", |
| 744 | .iov_len = 5, |
| 745 | }; |
| 746 | iovecs_len += 1; |
| 747 | } |
| 581 | 748 | |
| 582 | | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 583 | | /// Must be called after `send` and before `finish`. |
| 584 | | pub fn writeAll(req: *Server, bytes: []const u8) WriteError!void { |
| 585 | | var index: usize = 0; |
| 586 | | while (index < bytes.len) { |
| 587 | | index += try write(req, bytes[index..]); |
| 588 | | } |
| 589 | | } |
| 749 | for (trailers) |trailer| { |
| 750 | iovecs[iovecs_len] = .{ |
| 751 | .iov_base = trailer.name.ptr, |
| 752 | .iov_len = trailer.name.len, |
| 753 | }; |
| 754 | iovecs_len += 1; |
| 755 | |
| 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 | } |
| 590 | 774 | |
| 591 | | pub 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 | } |
| 592 | 787 | |
| 593 | | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 594 | | /// Must be called after `send`. |
| 595 | | pub fn finish(res: *Server) FinishError!void { |
| 596 | | switch (res.state) { |
| 597 | | .responded => res.state = .finished, |
| 598 | | .first, .waited, .start, .finished => unreachable, |
| 788 | try r.stream.writevAll(iovecs[0..iovecs_len]); |
| 789 | r.send_buffer_start = 0; |
| 790 | r.send_buffer_end = 0; |
| 791 | r.chunk_len = 0; |
| 599 | 792 | } |
| 600 | 793 | |
| 601 | | switch (res.transfer_encoding) { |
| 602 | | .chunked => try res.connection.writeAll("0\r\n\r\n"), |
| 603 | | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 604 | | .none => {}, |
| 794 | pub fn writer(r: *Response) std.io.AnyWriter { |
| 795 | return .{ |
| 796 | .writeFn = if (r.content_length != null) write_cl else write_chunked, |
| 797 | .context = r, |
| 798 | }; |
| 605 | 799 | } |
| 606 | | } |
| 800 | }; |
| 607 | 801 | |
| 608 | | const builtin = @import("builtin"); |
| 609 | 802 | const std = @import("../std.zig"); |
| 610 | | const testing = std.testing; |
| 611 | 803 | const http = std.http; |
| 612 | 804 | const mem = std.mem; |
| 613 | 805 | const net = std.net; |
| ... | ... | @@ -615,4 +807,3 @@ const Uri = std.Uri; |
| 615 | 807 | const assert = std.debug.assert; |
| 616 | 808 | |
| 617 | 809 | const Server = @This(); |
| 618 | | const proto = @import("protocol.zig"); |