struct Request [src]

Fields

server: *Server
head: HeadPointers in this struct are invalidated when the request body stream is initialized.
head_buffer: []const u8
respond_err: ?RespondError = null

Members

Source

pub const Request = struct { server: *Server, /// Pointers in this struct are invalidated when the request body stream is /// initialized. head: Head, head_buffer: []const u8, respond_err: ?RespondError = null, pub const RespondError = error{ /// The request contained an `expect` header with an unrecognized value. HttpExpectationFailed, }; pub const Head = struct { method: http.Method, target: []const u8, version: http.Version, expect: ?[]const u8, content_type: ?[]const u8, content_length: ?u64, transfer_encoding: http.TransferEncoding, transfer_compression: http.ContentEncoding, keep_alive: bool, pub const ParseError = error{ UnknownHttpMethod, HttpHeadersInvalid, HttpHeaderContinuationsUnsupported, HttpTransferEncodingUnsupported, HttpConnectionHeaderUnsupported, InvalidContentLength, CompressionUnsupported, MissingFinalNewline, }; pub fn parse(bytes: []const u8) ParseError!Head { var it = mem.splitSequence(u8, bytes, "\r\n"); const first_line = it.next().?; if (first_line.len < 10) return error.HttpHeadersInvalid; const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; const method = std.meta.stringToEnum(http.Method, first_line[0..method_end]) orelse return error.UnknownHttpMethod; const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; if (version_start == method_end) return error.HttpHeadersInvalid; const version_str = first_line[version_start + 1 ..]; if (version_str.len != 8) return error.HttpHeadersInvalid; const version: http.Version = switch (int64(version_str[0..8])) { int64("HTTP/1.0") => .@"HTTP/1.0", int64("HTTP/1.1") => .@"HTTP/1.1", else => return error.HttpHeadersInvalid, }; const target = first_line[method_end + 1 .. version_start]; var head: Head = .{ .method = method, .target = target, .version = version, .expect = null, .content_type = null, .content_length = null, .transfer_encoding = .none, .transfer_compression = .identity, .keep_alive = switch (version) { .@"HTTP/1.0" => false, .@"HTTP/1.1" => true, }, }; while (it.next()) |line| { if (line.len == 0) return head; switch (line[0]) { ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, else => {}, } var line_it = mem.splitScalar(u8, line, ':'); const header_name = line_it.next().?; const header_value = mem.trim(u8, line_it.rest(), " \t"); if (header_name.len == 0) return error.HttpHeadersInvalid; if (std.ascii.eqlIgnoreCase(header_name, "connection")) { head.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) { head.expect = header_value; } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { head.content_type = header_value; } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { if (head.content_length != null) return error.HttpHeadersInvalid; head.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { if (head.transfer_compression != .identity) return error.HttpHeadersInvalid; const trimmed = mem.trim(u8, header_value, " "); if (http.ContentEncoding.fromString(trimmed)) |ce| { head.transfer_compression = ce; } else { return error.HttpTransferEncodingUnsupported; } } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { // Transfer-Encoding: second, first // Transfer-Encoding: deflate, chunked var iter = mem.splitBackwardsScalar(u8, header_value, ','); const first = iter.first(); const trimmed_first = mem.trim(u8, first, " "); var next: ?[]const u8 = first; if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { if (head.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding head.transfer_encoding = transfer; next = iter.next(); } if (next) |second| { const trimmed_second = mem.trim(u8, second, " "); if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { if (head.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported head.transfer_compression = transfer; } else { return error.HttpTransferEncodingUnsupported; } } if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; } } return error.MissingFinalNewline; } test parse { const request_bytes = "GET /hi HTTP/1.0\r\n" ++ "content-tYpe: text/plain\r\n" ++ "content-Length:10\r\n" ++ "expeCt: 100-continue \r\n" ++ "TRansfer-encoding:\tdeflate, chunked \r\n" ++ "connectioN:\t keep-alive \r\n\r\n"; const req = try parse(request_bytes); try testing.expectEqual(.GET, req.method); try testing.expectEqual(.@"HTTP/1.0", req.version); try testing.expectEqualStrings("/hi", req.target); try testing.expectEqualStrings("text/plain", req.content_type.?); try testing.expectEqualStrings("100-continue", req.expect.?); try testing.expectEqual(true, req.keep_alive); try testing.expectEqual(10, req.content_length.?); try testing.expectEqual(.chunked, req.transfer_encoding); try testing.expectEqual(.deflate, req.transfer_compression); } inline fn int64(array: *const [8]u8) u64 { return @bitCast(array.*); } /// Help the programmer avoid bugs by calling this when the string /// memory of `Head` becomes invalidated. fn invalidateStrings(h: *Head) void { h.target = undefined; if (h.expect) |*s| s.* = undefined; if (h.content_type) |*s| s.* = undefined; } }; pub fn iterateHeaders(r: *const Request) http.HeaderIterator { assert(r.server.reader.state == .received_head); return http.HeaderIterator.init(r.head_buffer); } test iterateHeaders { const request_bytes = "GET /hi HTTP/1.0\r\n" ++ "content-tYpe: text/plain\r\n" ++ "content-Length:10\r\n" ++ "expeCt: 100-continue \r\n" ++ "TRansfer-encoding:\tdeflate, chunked \r\n" ++ "connectioN:\t keep-alive \r\n\r\n"; var server: Server = .{ .reader = .{ .in = undefined, .state = .received_head, .interface = undefined, .max_head_len = 4096, }, .out = undefined, }; var request: Request = .{ .server = &server, .head = undefined, .head_buffer = @constCast(request_bytes), }; var it = request.iterateHeaders(); { const header = it.next().?; try testing.expectEqualStrings("content-tYpe", header.name); try testing.expectEqualStrings("text/plain", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("content-Length", header.name); try testing.expectEqualStrings("10", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("expeCt", header.name); try testing.expectEqualStrings("100-continue", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("TRansfer-encoding", header.name); try testing.expectEqualStrings("deflate, chunked", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("connectioN", header.name); try testing.expectEqualStrings("keep-alive", header.value); try testing.expect(!it.is_trailer); } try testing.expectEqual(null, it.next()); } pub const RespondOptions = struct { version: http.Version = .@"HTTP/1.1", status: http.Status = .ok, reason: ?[]const u8 = null, keep_alive: bool = true, extra_headers: []const http.Header = &.{}, transfer_encoding: ?http.TransferEncoding = null, }; /// Send an entire HTTP response to the client, including headers and body. /// /// Automatically handles HEAD requests by omitting the body. /// /// Unless `transfer_encoding` is specified, uses the "content-length" /// header. /// /// If the request contains a body and the connection is to be reused, /// discards the request body, leaving the Server in the `ready` state. If /// this discarding fails, the connection is marked as not to be reused and /// no error is surfaced. /// /// Asserts status is not `continue`. /// Asserts that "\r\n" does not occur in any header name or value. pub fn respond( request: *Request, content: []const u8, options: RespondOptions, ) ExpectContinueError!void { try respondUnflushed(request, content, options); try request.server.out.flush(); } pub fn respondUnflushed( request: *Request, content: []const u8, options: RespondOptions, ) ExpectContinueError!void { assert(options.status != .@"continue"); if (std.debug.runtime_safety) { for (options.extra_headers) |header| { assert(header.name.len != 0); assert(std.mem.indexOfScalar(u8, header.name, ':') == null); assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null); assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); } } try writeExpectContinue(request); const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none; const server_keep_alive = !transfer_encoding_none and options.keep_alive; const keep_alive = request.discardBody(server_keep_alive); const phrase = options.reason orelse options.status.phrase() orelse ""; const out = request.server.out; try out.print("{s} {d} {s}\r\n", .{ @tagName(options.version), @intFromEnum(options.status), phrase, }); switch (options.version) { .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), } if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { .none => {}, .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), } else { try out.print("content-length: {d}\r\n", .{content.len}); } for (options.extra_headers) |header| { var vecs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; try out.writeVecAll(&vecs); } try out.writeAll("\r\n"); if (request.head.method != .HEAD) { const is_chunked = (options.transfer_encoding orelse .none) == .chunked; if (is_chunked) { if (content.len > 0) try out.print("{x}\r\n{s}\r\n", .{ content.len, content }); try out.writeAll("0\r\n\r\n"); } else if (content.len > 0) { try out.writeAll(content); } } } pub const RespondStreamingOptions = struct { /// If provided, the response will use the content-length header; /// otherwise it will use transfer-encoding: chunked. content_length: ?u64 = null, /// Options that are shared with the `respond` method. respond_options: RespondOptions = .{}, }; /// The header is not guaranteed to be sent until `BodyWriter.flush` or /// `BodyWriter.end` is called. /// /// If the request contains a body and the connection is to be reused, /// discards the request body, leaving the Server in the `ready` state. If /// this discarding fails, the connection is marked as not to be reused and /// no error is surfaced. /// /// HEAD requests are handled transparently by setting the /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing /// the response stream to omit the body. However, it may be worth noticing /// that flag and skipping any expensive work that would otherwise need to /// be done to satisfy the request. /// /// Asserts status is not `continue`. pub fn respondStreaming( request: *Request, buffer: []u8, options: RespondStreamingOptions, ) ExpectContinueError!http.BodyWriter { try writeExpectContinue(request); const o = options.respond_options; assert(o.status != .@"continue"); const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; const server_keep_alive = !transfer_encoding_none and o.keep_alive; const keep_alive = request.discardBody(server_keep_alive); const phrase = o.reason orelse o.status.phrase() orelse ""; const out = request.server.out; try out.print("{s} {d} {s}\r\n", .{ @tagName(o.version), @intFromEnum(o.status), phrase, }); switch (o.version) { .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), } if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), .none => {}, } else if (options.content_length) |len| { try out.print("content-length: {d}\r\n", .{len}); } else { try out.writeAll("transfer-encoding: chunked\r\n"); } for (o.extra_headers) |header| { assert(header.name.len != 0); var bufs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; try out.writeVecAll(&bufs); } try out.writeAll("\r\n"); const elide_body = request.head.method == .HEAD; const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) { .chunked => .init_chunked, .none => .none, } else if (options.content_length) |len| .{ .content_length = len, } else .init_chunked; return if (elide_body) .{ .http_protocol_output = request.server.out, .state = state, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.elidingDrain, .sendFile = http.BodyWriter.elidingSendFile, }, }, } else .{ .http_protocol_output = request.server.out, .state = state, .writer = .{ .buffer = buffer, .vtable = switch (state) { .none => &.{ .drain = http.BodyWriter.noneDrain, .sendFile = http.BodyWriter.noneSendFile, }, .content_length => &.{ .drain = http.BodyWriter.contentLengthDrain, .sendFile = http.BodyWriter.contentLengthSendFile, }, .chunk_len => &.{ .drain = http.BodyWriter.chunkedDrain, .sendFile = http.BodyWriter.chunkedSendFile, }, .end => unreachable, }, }, }; } pub const UpgradeRequest = union(enum) { websocket: ?[]const u8, other: []const u8, none, }; /// Does not invalidate `request.head`. pub fn upgradeRequested(request: *const Request) UpgradeRequest { switch (request.head.version) { .@"HTTP/1.0" => return .none, .@"HTTP/1.1" => if (request.head.method != .GET) return .none, } var sec_websocket_key: ?[]const u8 = null; var upgrade_name: ?[]const u8 = null; var it = request.iterateHeaders(); while (it.next()) |header| { if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) { sec_websocket_key = header.value; } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) { upgrade_name = header.value; } } const name = upgrade_name orelse return .none; if (std.ascii.eqlIgnoreCase(name, "websocket")) return .{ .websocket = sec_websocket_key }; return .{ .other = name }; } pub const WebSocketOptions = struct { /// The value from `UpgradeRequest.websocket` (sec-websocket-key header value). key: []const u8, reason: ?[]const u8 = null, extra_headers: []const http.Header = &.{}, }; /// The header is not guaranteed to be sent until `WebSocket.flush` is /// called on the returned struct. pub fn respondWebSocket(request: *Request, options: WebSocketOptions) ExpectContinueError!WebSocket { if (request.head.expect != null) return error.HttpExpectationFailed; const out = request.server.out; const version: http.Version = .@"HTTP/1.1"; const status: http.Status = .switching_protocols; const phrase = options.reason orelse status.phrase() orelse ""; assert(request.head.version == version); assert(request.head.method == .GET); var sha1 = std.crypto.hash.Sha1.init(.{}); sha1.update(options.key); sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined; sha1.final(&digest); try out.print("{s} {d} {s}\r\n", .{ @tagName(version), @intFromEnum(status), phrase }); try out.writeAll("connection: upgrade\r\nupgrade: websocket\r\nsec-websocket-accept: "); const base64_digest = try out.writableArray(28); assert(std.base64.standard.Encoder.encode(base64_digest, &digest).len == base64_digest.len); try out.writeAll("\r\n"); for (options.extra_headers) |header| { assert(header.name.len != 0); var bufs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; try out.writeVecAll(&bufs); } try out.writeAll("\r\n"); return .{ .input = request.server.reader.in, .output = request.server.out, .key = options.key, }; } /// In the case that the request contains "expect: 100-continue", this /// function writes the continuation header, which means it can fail with a /// write error. After sending the continuation header, it sets the /// request's expect field to `null`. /// /// Asserts that this function is only called once. /// /// See `readerExpectNone` for an infallible alternative that cannot write /// to the server output stream. pub fn readerExpectContinue(request: *Request, buffer: []u8) ExpectContinueError!*Reader { const flush = request.head.expect != null; try writeExpectContinue(request); if (flush) try request.server.out.flush(); return readerExpectNone(request, buffer); } /// Asserts the expect header is `null`. The caller must handle the /// expectation manually and then set the value to `null` prior to calling /// this function. /// /// Asserts that this function is only called once. /// /// Invalidates the string memory inside `Head`. pub fn readerExpectNone(request: *Request, buffer: []u8) *Reader { assert(request.server.reader.state == .received_head); assert(request.head.expect == null); request.head.invalidateStrings(); if (!request.head.method.requestHasBody()) return .ending; return request.server.reader.bodyReader(buffer, request.head.transfer_encoding, request.head.content_length); } pub const ExpectContinueError = error{ /// Failed to write "HTTP/1.1 100 Continue\r\n\r\n" to the stream. WriteFailed, /// The client sent an expect HTTP header value other than /// "100-continue". HttpExpectationFailed, }; pub fn writeExpectContinue(request: *Request) ExpectContinueError!void { const expect = request.head.expect orelse return; if (!mem.eql(u8, expect, "100-continue")) return error.HttpExpectationFailed; try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); request.head.expect = null; } /// Returns whether the connection should remain persistent. /// /// If it would fail, it instead sets the Server state to receiving body /// and returns false. fn discardBody(request: *Request, keep_alive: bool) bool { // Prepare to receive another request on the same connection. // There are two factors to consider: // * Any body the client sent must be discarded. // * The Server's read_buffer may already have some bytes in it from // whatever came after the head, which may be the next HTTP request // or the request body. // If the connection won't be kept alive, then none of this matters // because the connection will be severed after the response is sent. const r = &request.server.reader; if (keep_alive and request.head.keep_alive) switch (r.state) { .received_head => { if (request.head.method.requestHasBody()) { assert(request.head.transfer_encoding != .none or request.head.content_length != null); const reader_interface = request.readerExpectContinue(&.{}) catch return false; _ = reader_interface.discardRemaining() catch return false; assert(r.state == .ready); } else { r.state = .ready; } return true; }, .body_remaining_content_length, .body_remaining_chunk_len, .body_none, .ready => return true, else => unreachable, }; // Avoid clobbering the state in case a reading stream already exists. switch (r.state) { .received_head => r.state = .closing, else => {}, } return false; } }