| ... | @@ -1,613 +1,805 @@ | ... | @@ -1,613 +1,805 @@ |
| 1 | connection: Connection, | 1 | //! Blocking HTTP server implementation. |
| 2 | /// This value is determined by Server when sending headers to the client, and | 2 | |
| 3 | /// then used to determine the return value of `reset`. | 3 | connection: net.Server.Connection, |
| 4 | connection_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. |
| | 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. | 16 | pub 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, |
| 9 | request: Request, | 19 | /// An error occurred in `receiveHead`. |
| 10 | | 20 | receiving_head, |
| 11 | state: 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 | }; |
| 12 | | 30 | |
| 13 | /// Initialize an HTTP server that can respond to multiple requests on the same | 31 | /// 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. |
| 16 | pub fn init(connection: std.net.Server.Connection, options: Server.Request.InitOptions) Server { | 34 | pub 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 | } |
| 28 | | 43 | |
| 29 | pub const State = enum { | 44 | pub 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, |
| 36 | | 51 | /// A low level I/O error occurred trying to read the headers. |
| 37 | pub const ResetState = enum { reset, closing }; | 52 | HttpHeadersUnreadable, |
| 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, | | |
| 60 | }; | 53 | }; |
| 61 | | 54 | |
| 62 | /// A HTTP request originating from a client. | 55 | /// The header bytes reference the read buffer that Server was initialized with |
| 63 | pub const Request = struct { | 56 | /// and remain alive until the next call to receiveHead. |
| 64 | method: http.Method, | 57 | pub 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]; |
| 75 | | 68 | 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 a | 71 | 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 | } |
| 82 | | 77 | |
| 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 | } |
| 98 | | 97 | |
| 99 | pub const ParseError = error{ | 98 | pub 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 | }; |
| 108 | | 117 | |
| 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, |
| 111 | | 120 | 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, |
| 115 | | 124 | content_length: ?u64, |
| 116 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse | 125 | 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, |
| 119 | | 128 | 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{ |
| 122 | | 131 | UnknownHttpMethod, |
| 123 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse | 132 | HttpHeadersInvalid, |
| 124 | return error.HttpHeadersInvalid; | 133 | HttpHeaderContinuationsUnsupported, |
| 125 | if (version_start == method_end) return error.HttpHeadersInvalid; | 134 | HttpTransferEncodingUnsupported, |
| 126 | | 135 | 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 | }; |
| 134 | | 140 | |
| 135 | const target = first_line[method_end + 1 .. version_start]; | 141 | pub fn parse(bytes: []const u8) ParseError!Head { |
| 136 | | 142 | 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) |
| 140 | | 146 | 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]; |
| 147 | | 153 | 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; |
| 152 | | 158 | |
| 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) catch | 167 | 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, |
| 165 | | 171 | .target = target, |
| 166 | const trimmed = mem.trim(u8, header_value, " "); | 172 | .version = version, |
| 167 | | 173 | .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, ','); | | |
| 177 | | 188 | |
| 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().?; |
| 180 | | 191 | 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 encoding | 195 | head.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); |
| 185 | req.transfer_encoding = transfer; | 196 | } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) { |
| 186 | | 197 | 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; |
| 189 | | 200 | } 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 |
| 192 | | 203 | 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 supported | 206 | |
| 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, ','); |
| 201 | | 218 | |
| 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 | } | | |
| 207 | | 221 | |
| 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 |
| 212 | | 226 | 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. | | |
| 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 | } | | |
| 226 | | 227 | |
| 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 | }); | | |
| 231 | | 230 | |
| 232 | return if (res.connection_keep_alive) .reset else .closing; | 231 | if (next) |second| { |
| 233 | } | 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 { | 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 | }; | | |
| 245 | | 248 | |
| 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. | | |
| 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 | } | | |
| 263 | | 253 | |
| 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 | else | 257 | 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; | | |
| 292 | | 261 | |
| 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; |
| 299 | | 301 | |
| 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; |
| 305 | | 307 | |
| 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 | } |
| 311 | | 333 | |
| 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 | } | | |
| 318 | | 339 | |
| 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 | } |
| 324 | | 347 | |
| 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 | } |
| 332 | | 350 | |
| 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 |
| 335 | | 353 | /// sending. `respondStreaming` asserts this is large enough to store |
| 336 | pub const Response = struct { | 354 | /// the full HTTP response head. |
| 337 | transfer_encoding: ResponseTransfer, | 355 | /// |
| 338 | }; | 356 | /// Must outlive the returned Response. |
| 339 | | 357 | send_buffer: []u8, |
| 340 | pub 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. |
| 344 | | 362 | respond_options: RespondOptions = .{}, |
| 345 | /// Send the HTTP response headers to the client. | 363 | }; |
| 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 | | 364 | |
| 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 | /// |
| 354 | | 367 | /// 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. |
| 365 | | 378 | /// 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 this | 380 | 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 | } |
| 375 | | 398 | |
| 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 | } |
| 381 | | 405 | |
| 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 | } | | |
| 386 | | 407 | |
| 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 | } |
| 390 | | 418 | |
| 391 | try w.writeAll("\r\n"); | 419 | pub const ReadError = net.Stream.ReadError; |
| 392 | | | |
| 393 | try buffered.flush(); | | |
| 394 | } | | |
| 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); | 426 | const remaining_content_length = &request.reader_state.remaining_content_length; |
| 399 | | | |
| 400 | fn transferReader(res: *Server) TransferReader { | | |
| 401 | return .{ .context = res }; | | |
| 402 | } | | |
| 403 | | 427 | |
| 404 | fn 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 | } |
| 406 | | 432 | |
| 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 | } |
| 413 | | 445 | |
| 414 | return index; | 446 | fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize { |
| 415 | } | 447 | const request: *Request = @constCast(@alignCast(@ptrCast(context))); |
| 416 | | 448 | const s = request.server; |
| 417 | pub 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. | | |
| 438 | pub fn wait(res: *Server) WaitError!void { | | |
| 439 | switch (res.state) { | | |
| 440 | .first, .start => res.state = .waited, | | |
| 441 | .waited, .responded, .finished => unreachable, | | |
| 442 | } | 452 | } |
| 443 | | 453 | |
| 444 | while (true) { | 454 | pub const ReadAllError = ReadError || error{HttpBodyOversize}; |
| 445 | try res.connection.fill(); | | |
| 446 | | 455 | |
| 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 | } |
| 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) { | 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; | | |
| 459 | | 565 | |
| 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 | } |
| 470 | | 577 | |
| 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 | } | | |
| 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 { | 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 | } |
| 496 | | 626 | |
| 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. |
| 498 | pub 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 | } |
| 503 | | 633 | |
| 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(); | | |
| 514 | | 637 | |
| 515 | while (!res.request.parser.state.isContent()) { // read trailing headers | 638 | if (r.elide_body) |
| 516 | try res.connection.fill(); | 639 | return bytes.len; |
| 517 | | 640 | |
| 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 | } |
| 521 | | 677 | |
| 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 already | 679 | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); |
| 524 | // guaranteed to be valid, so they will always be parsed again | 680 | 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 | } |
| 530 | | 693 | |
| 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 | } |
| 533 | | 703 | |
| 534 | /// Reads data from the response body. Must be called after `wait`. | 704 | fn flush_cl(r: *Response) WriteError!void { |
| 535 | pub 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 | } | | |
| 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 { | 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; |
| 552 | | 727 | |
| 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, |
| 555 | pub 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 | } | | |
| 560 | | 733 | |
| 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 | } | | |
| 568 | | 739 | |
| 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", |
| 573 | | 744 | .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 | } | | |
| 581 | | 748 | |
| 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] = .{ |
| 584 | pub 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 | } |
| 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. | 788 | try r.stream.writevAll(iovecs[0..iovecs_len]); |
| 594 | /// Must be called after `send`. | 789 | r.send_buffer_start = 0; |
| 595 | pub 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 | } |
| 600 | | 793 | |
| 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 | }; |
| 607 | | 801 | |
| 608 | const builtin = @import("builtin"); | | |
| 609 | const std = @import("../std.zig"); | 802 | const std = @import("../std.zig"); |
| 610 | const testing = std.testing; | | |
| 611 | const http = std.http; | 803 | const http = std.http; |
| 612 | const mem = std.mem; | 804 | const mem = std.mem; |
| 613 | const net = std.net; | 805 | const net = std.net; |
| ... | @@ -615,4 +807,3 @@ const Uri = std.Uri; | ... | @@ -615,4 +807,3 @@ const Uri = std.Uri; |
| 615 | const assert = std.debug.assert; | 807 | const assert = std.debug.assert; |
| 616 | | 808 | |
| 617 | const Server = @This(); | 809 | const Server = @This(); |
| 618 | const proto = @import("protocol.zig"); | | |