mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-02 10:47:15 -04:00
rework Connection.upgrade with header_parser
This commit is contained in:
@@ -28,6 +28,7 @@ const ArenaPool = @import("../ArenaPool.zig");
|
||||
|
||||
const WS = @import("../network/WS.zig");
|
||||
const sys_net = @import("../sys/net.zig");
|
||||
const header_parser = @import("../network/header_parser.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const posix = std.posix;
|
||||
@@ -418,59 +419,96 @@ fn pushCdp(self: *Connection, bytes: []const u8) !bool {
|
||||
}
|
||||
|
||||
pub fn upgrade(self: *Connection, request: []u8) !void {
|
||||
// our caller already confirmed that we have a trailing \r\n\r\n
|
||||
const request_line_end = std.mem.indexOfScalar(u8, request, '\r') orelse unreachable;
|
||||
const request_line = request[0..request_line_end];
|
||||
|
||||
if (!std.ascii.endsWithIgnoreCase(request_line, "http/1.1")) {
|
||||
var cursor = header_parser.Cursor{
|
||||
.idx = request.ptr,
|
||||
.start = request.ptr,
|
||||
.end = request.ptr + request.len,
|
||||
};
|
||||
// A malformed request line maps to a 400 in processHttpRequest.
|
||||
header_parser.validateWebSocketRequestLine(&cursor) catch {
|
||||
return error.InvalidProtocol;
|
||||
}
|
||||
};
|
||||
|
||||
// we need to extract the sec-websocket-key value
|
||||
var key: []const u8 = "";
|
||||
// We need to extract the `Sec-WebSocket-Key` value.
|
||||
var sec_websocket_key: []const u8 = "";
|
||||
// We need to make sure that we got all the necessary headers + values.
|
||||
const RequiredHeaders = packed struct(u8) {
|
||||
/// Upgrade: websocket
|
||||
upgrade: bool = false,
|
||||
sec_websocket_version: bool = false,
|
||||
/// Connection: upgrade
|
||||
connection: bool = false,
|
||||
sec_websocket_key: bool = false,
|
||||
__pad: u4 = 0,
|
||||
};
|
||||
|
||||
// we need to make sure that we got all the necessary headers + values
|
||||
var required_headers: u8 = 0;
|
||||
var required_headers = RequiredHeaders{};
|
||||
// We reuse this to parse headers that're required.
|
||||
var header: header_parser.Header = undefined;
|
||||
while (true) {
|
||||
if (cursor.reachedEnd()) {
|
||||
return error.InvalidRequest;
|
||||
}
|
||||
|
||||
// can't std.mem.split because it forces the iterated value to be const
|
||||
// (we could @constCast...)
|
||||
// Check if headers part has finished.
|
||||
switch (cursor.char()) {
|
||||
'\n' => {
|
||||
// End of headers.
|
||||
cursor.advance(1);
|
||||
break;
|
||||
},
|
||||
'\r' => {
|
||||
// We need an LF too.
|
||||
if (!cursor.hasLength(2) or !cursor.peek2('\r', '\n')) {
|
||||
return error.InvalidRequest;
|
||||
}
|
||||
// End of headers.
|
||||
cursor.advance(2);
|
||||
break;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
var buf = request[request_line_end + 2 ..];
|
||||
// A malformed header maps to a 400 in processHttpRequest.
|
||||
header.parse(&cursor) catch {
|
||||
return error.InvalidRequest;
|
||||
};
|
||||
const key = header.key;
|
||||
const value = header.value;
|
||||
|
||||
while (buf.len > 4) {
|
||||
const index = std.mem.indexOfScalar(u8, buf, '\r') orelse unreachable;
|
||||
const separator = std.mem.indexOfScalar(u8, buf[0..index], ':') orelse return error.InvalidRequest;
|
||||
|
||||
const name = std.mem.trim(u8, toLower(buf[0..separator]), &std.ascii.whitespace);
|
||||
const value = std.mem.trim(u8, buf[(separator + 1)..index], &std.ascii.whitespace);
|
||||
|
||||
if (std.mem.eql(u8, name, "upgrade")) {
|
||||
// Header names are case-insensitive; `Header.parse` keeps their
|
||||
// original casing.
|
||||
if (std.ascii.eqlIgnoreCase(key, "upgrade")) {
|
||||
if (!std.ascii.eqlIgnoreCase("websocket", value)) {
|
||||
return error.InvalidUpgradeHeader;
|
||||
}
|
||||
required_headers |= 1;
|
||||
} else if (std.mem.eql(u8, name, "sec-websocket-version")) {
|
||||
required_headers.upgrade = true;
|
||||
} else if (std.ascii.eqlIgnoreCase(key, "sec-websocket-version")) {
|
||||
if (value.len != 2 or value[0] != '1' or value[1] != '3') {
|
||||
return error.InvalidVersionHeader;
|
||||
}
|
||||
required_headers |= 2;
|
||||
} else if (std.mem.eql(u8, name, "connection")) {
|
||||
required_headers.sec_websocket_version = true;
|
||||
} else if (std.ascii.eqlIgnoreCase(key, "connection")) {
|
||||
// find if connection header has upgrade in it, example header:
|
||||
// Connection: keep-alive, Upgrade
|
||||
if (std.ascii.indexOfIgnoreCase(value, "upgrade") == null) {
|
||||
return error.InvalidConnectionHeader;
|
||||
}
|
||||
required_headers |= 4;
|
||||
} else if (std.mem.eql(u8, name, "sec-websocket-key")) {
|
||||
key = value;
|
||||
required_headers |= 8;
|
||||
required_headers.connection = true;
|
||||
} else if (std.ascii.eqlIgnoreCase(key, "sec-websocket-key")) {
|
||||
sec_websocket_key = value;
|
||||
required_headers.sec_websocket_key = true;
|
||||
}
|
||||
|
||||
const next = index + 2;
|
||||
buf = buf[next..];
|
||||
}
|
||||
|
||||
if (required_headers != 15) {
|
||||
// Check if we've received all related headers.
|
||||
const satisfied = @as(u8, @bitCast(required_headers)) == @as(u8, @bitCast(RequiredHeaders{
|
||||
.upgrade = true,
|
||||
.sec_websocket_version = true,
|
||||
.connection = true,
|
||||
.sec_websocket_key = true,
|
||||
}));
|
||||
if (!satisfied) {
|
||||
return error.MissingHeaders;
|
||||
}
|
||||
|
||||
@@ -498,7 +536,7 @@ pub fn upgrade(self: *Connection, request: []u8) !void {
|
||||
const key_pos = res.len - 32;
|
||||
var h: [20]u8 = undefined;
|
||||
var hasher = std.crypto.hash.Sha1.init(.{});
|
||||
hasher.update(key);
|
||||
hasher.update(sec_websocket_key);
|
||||
// websocket spec always used this value
|
||||
hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
hasher.final(&h);
|
||||
@@ -583,11 +621,3 @@ fn websocketHeader(buf: []u8, op_code: WS.OpCode, payload_len: usize) []const u8
|
||||
buf[9] = @intCast(len & 0xFF);
|
||||
return buf[0..10];
|
||||
}
|
||||
|
||||
// In-place string lowercase
|
||||
fn toLower(str: []u8) []u8 {
|
||||
for (str, 0..) |ch, i| {
|
||||
str[i] = std.ascii.toLower(ch);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
@@ -231,6 +231,66 @@ pub const Header = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Validates WebSocket initialization requests.
|
||||
/// Currently does not validate paths.
|
||||
pub fn validateWebSocketRequestLine(cursor: *Cursor) !void {
|
||||
// GET / HTTP/1.1\n
|
||||
const min_request_len = 0xf;
|
||||
if (cursor.hasLength(min_request_len) == false) {
|
||||
return error.Incomplete;
|
||||
}
|
||||
|
||||
// WS requests can only be send w/ GET method.
|
||||
if (!cursor.peek4('G', 'E', 'T', ' ')) {
|
||||
return error.Invalid;
|
||||
}
|
||||
cursor.advance(4);
|
||||
|
||||
const path_start = cursor.current();
|
||||
// Find the first space.
|
||||
while (cursor.end - cursor.current() > 0 and cursor.char() != ' ') : (cursor.advance(1)) {}
|
||||
const path_end = cursor.current();
|
||||
// 0 length path.
|
||||
if (path_start == path_end) {
|
||||
return error.Invalid;
|
||||
}
|
||||
const path = path_start[0 .. path_end - path_start];
|
||||
_ = path;
|
||||
|
||||
// Skip past the delimiting space(s); the scan above guarantees we're on
|
||||
// a space or at the end, and recipients may parse on whitespace
|
||||
// boundaries (RFC 9112 §3).
|
||||
while (cursor.end - cursor.current() > 0 and cursor.char() == ' ') : (cursor.advance(1)) {}
|
||||
|
||||
// HTTP/1.1(\r)\n
|
||||
if (cursor.hasLength(9) == false) {
|
||||
return error.Incomplete;
|
||||
}
|
||||
// Make sure we got HTTP/1.1.
|
||||
if (cursor.asInteger(u64) != @as(u64, @bitCast(@as([]const u8, "HTTP/1.1")[0..8].*))) {
|
||||
return error.Invalid;
|
||||
}
|
||||
cursor.advance(8);
|
||||
|
||||
// Trailing (CR)LF.
|
||||
switch (cursor.char()) {
|
||||
'\n' => cursor.advance(1),
|
||||
'\r' => {
|
||||
// We need an LF too.
|
||||
if (!cursor.hasLength(2)) {
|
||||
return error.Incomplete;
|
||||
}
|
||||
if (!cursor.peek2('\r', '\n')) {
|
||||
@branchHint(.unlikely);
|
||||
return error.Invalid;
|
||||
}
|
||||
cursor.advance(2);
|
||||
},
|
||||
// Any other character is invalid.
|
||||
else => return error.Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
pub const Disposition = struct {
|
||||
name: ?[]const u8 = null,
|
||||
filename: ?[]const u8 = null,
|
||||
@@ -343,6 +403,12 @@ pub const Cursor = struct {
|
||||
return cursor.asInteger(u16) == @as(u16, @bitCast([2]u8{ c0, c1 }));
|
||||
}
|
||||
|
||||
/// Peek the current and next 3 characters but don't advance.
|
||||
/// SAFETY: This function doesn't check if out of bounds reachable.
|
||||
pub fn peek4(cursor: *const Cursor, c0: u8, c1: u8, c2: u8, c3: u8) bool {
|
||||
return cursor.asInteger(u32) == @as(u32, @bitCast([4]u8{ c0, c1, c2, c3 }));
|
||||
}
|
||||
|
||||
/// Moves the cursor until no leading spaces there are.
|
||||
pub fn skipSpaces(cursor: *Cursor) void {
|
||||
while (cursor.end - cursor.current() > 0 and (cursor.char() == ' ' or cursor.char() == '\t')) : (cursor.advance(1)) {}
|
||||
|
||||
Reference in New Issue
Block a user