mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Merge branch 'main' into crypto_get_random_values_fix
This commit is contained in:
11
src/app.zig
11
src/app.zig
@@ -3,7 +3,8 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Loop = @import("runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("http/client.zig").Client;
|
||||
const http = @import("http/client.zig");
|
||||
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const Notification = @import("notification.zig").Notification;
|
||||
|
||||
@@ -14,7 +15,7 @@ pub const App = struct {
|
||||
config: Config,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
http_client: HttpClient,
|
||||
http_client: http.Client,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
@@ -29,6 +30,8 @@ pub const App = struct {
|
||||
run_mode: RunMode,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?std.Uri = null,
|
||||
proxy_type: ?http.ProxyType = null,
|
||||
proxy_auth: ?http.ProxyAuth = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
@@ -52,9 +55,11 @@ pub const App = struct {
|
||||
.telemetry = undefined,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.http_client = try HttpClient.init(allocator, .{
|
||||
.http_client = try http.Client.init(allocator, .{
|
||||
.max_concurrent = 3,
|
||||
.http_proxy = config.http_proxy,
|
||||
.proxy_type = config.proxy_type,
|
||||
.proxy_auth = config.proxy_auth,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
}),
|
||||
.config = config,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const parser = @import("netsurf.zig");
|
||||
const DataSet = @import("html/DataSet.zig");
|
||||
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
@@ -36,6 +37,7 @@ onerror: ?Env.Function = null,
|
||||
|
||||
// for HTMLElement
|
||||
style: CSSStyleDeclaration = .empty,
|
||||
dataset: ?DataSet = null,
|
||||
|
||||
// for html/document
|
||||
ready_state: ReadyState = .loading,
|
||||
|
||||
@@ -29,6 +29,8 @@ pub const Interfaces = .{
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSS
|
||||
pub const Css = struct {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _supports(_: *Css, _: []const u8, _: ?[]const u8) bool {
|
||||
// TODO: Actually respond with which CSS features we support.
|
||||
return true;
|
||||
|
||||
100
src/browser/html/DataSet.zig
Normal file
100
src/browser/html/DataSet.zig
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const DataSet = @This();
|
||||
|
||||
element: *parser.Element,
|
||||
|
||||
const GetResult = union(enum) {
|
||||
value: []const u8,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !GetResult {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
||||
return .{ .value = value };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
|
||||
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
try parser.elementSetAttribute(self.element, normalized_name, value);
|
||||
}
|
||||
|
||||
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool, page: *Page) !void {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
try parser.elementRemoveAttribute(self.element, normalized_name);
|
||||
}
|
||||
|
||||
fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
|
||||
var upper_count: usize = 0;
|
||||
for (name) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
upper_count += 1;
|
||||
}
|
||||
}
|
||||
// for every upper-case letter, we'll probably need a dash before it
|
||||
// and we need the 'data-' prefix
|
||||
var normalized = try allocator.alloc(u8, name.len + upper_count + 5);
|
||||
|
||||
@memcpy(normalized[0..5], "data-");
|
||||
if (upper_count == 0) {
|
||||
@memcpy(normalized[5..], name);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
var pos: usize = 5;
|
||||
for (name) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
normalized[pos] = '-';
|
||||
pos += 1;
|
||||
normalized[pos] = c + 32;
|
||||
} else {
|
||||
normalized[pos] = c;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let el1 = document.createElement('div')", null },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "el1.dataset.x = '123'", "123" },
|
||||
.{ "delete el1.dataset.x", "true" },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "delete el1.dataset.other", "true" }, // yes, this is right
|
||||
|
||||
.{ "let ds1 = el1.dataset", null },
|
||||
.{ "ds1.helloWorld = 'yes'", null },
|
||||
.{ "el1.getAttribute('data-hello-world')", "yes" },
|
||||
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
|
||||
.{ "ds1.thisWillWork", "positive" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -27,6 +27,7 @@ const urlStitch = @import("../../url.zig").URL.stitch;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const DataSet = @import("DataSet.zig");
|
||||
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
@@ -122,6 +123,15 @@ pub const HTMLElement = struct {
|
||||
return &state.style;
|
||||
}
|
||||
|
||||
pub fn get_dataset(e: *parser.ElementHTML, page: *Page) !*DataSet {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(e));
|
||||
if (state.dataset) |*ds| {
|
||||
return ds;
|
||||
}
|
||||
state.dataset = DataSet{ .element = @ptrCast(e) };
|
||||
return &state.dataset.?;
|
||||
}
|
||||
|
||||
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
|
||||
const n = @as(*parser.Node, @ptrCast(e));
|
||||
return try parser.nodeTextContent(n) orelse "";
|
||||
@@ -1561,6 +1571,13 @@ test "Browser.HTML.Element" {
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.Element.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{ .{ "let div = document.getElementById('x')", null }, .{ "div.dataset.nope", "undefined" }, .{ "div.dataset.power", "over 9000" }, .{ "div.dataset.empty", "" }, .{ "div.dataset.someLongKey", "ok" }, .{ "delete div.dataset.power", "true" }, .{ "div.dataset.power", "undefined" } }, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.HtmlInputElement.properties" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
|
||||
defer runner.deinit();
|
||||
|
||||
@@ -36,6 +36,7 @@ pub const Interfaces = .{
|
||||
History,
|
||||
Location,
|
||||
MediaQueryList,
|
||||
@import("DataSet.zig"),
|
||||
@import("screen.zig").Interfaces,
|
||||
@import("error_event.zig").ErrorEvent,
|
||||
};
|
||||
|
||||
@@ -41,6 +41,39 @@ const BUFFER_LEN = 32 * 1024;
|
||||
|
||||
const MAX_HEADER_LINE_LEN = 4096;
|
||||
|
||||
pub const ProxyType = enum {
|
||||
forward,
|
||||
connect,
|
||||
};
|
||||
|
||||
pub const ProxyAuth = union(enum) {
|
||||
basic: struct { user_pass: []const u8 },
|
||||
bearer: struct { token: []const u8 },
|
||||
|
||||
pub fn header_value(self: ProxyAuth, allocator: Allocator) ![]const u8 {
|
||||
switch (self) {
|
||||
.basic => |*auth| {
|
||||
if (std.mem.indexOfScalar(u8, auth.user_pass, ':') == null) return error.InvalidProxyAuth;
|
||||
|
||||
const prefix = "Basic ";
|
||||
var encoder = std.base64.standard.Encoder;
|
||||
const size = encoder.calcSize(auth.user_pass.len);
|
||||
var buffer = try allocator.alloc(u8, size + prefix.len);
|
||||
@memcpy(buffer[0..prefix.len], prefix);
|
||||
_ = std.base64.standard.Encoder.encode(buffer[prefix.len..], auth.user_pass);
|
||||
return buffer;
|
||||
},
|
||||
.bearer => |*auth| {
|
||||
const prefix = "Bearer ";
|
||||
var buffer = try allocator.alloc(u8, auth.token.len + prefix.len);
|
||||
@memcpy(buffer[0..prefix.len], prefix);
|
||||
@memcpy(buffer[prefix.len..], auth.token);
|
||||
return buffer;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Thread-safe. Holds our root certificate, connection pool and state pool
|
||||
// Used to create Requests.
|
||||
pub const Client = struct {
|
||||
@@ -48,6 +81,8 @@ pub const Client = struct {
|
||||
allocator: Allocator,
|
||||
state_pool: StatePool,
|
||||
http_proxy: ?Uri,
|
||||
proxy_type: ?ProxyType,
|
||||
proxy_auth: ?[]const u8, // Basic <user:pass; base64> or Bearer <token>
|
||||
root_ca: tls.config.CertBundle,
|
||||
tls_verify_host: bool = true,
|
||||
connection_manager: ConnectionManager,
|
||||
@@ -56,6 +91,8 @@ pub const Client = struct {
|
||||
const Opts = struct {
|
||||
max_concurrent: usize = 3,
|
||||
http_proxy: ?std.Uri = null,
|
||||
proxy_type: ?ProxyType = null,
|
||||
proxy_auth: ?ProxyAuth = null,
|
||||
tls_verify_host: bool = true,
|
||||
max_idle_connection: usize = 10,
|
||||
};
|
||||
@@ -64,10 +101,10 @@ pub const Client = struct {
|
||||
var root_ca: tls.config.CertBundle = if (builtin.is_test) .{} else try tls.config.CertBundle.fromSystem(allocator);
|
||||
errdefer root_ca.deinit(allocator);
|
||||
|
||||
const state_pool = try StatePool.init(allocator, opts.max_concurrent);
|
||||
var state_pool = try StatePool.init(allocator, opts.max_concurrent);
|
||||
errdefer state_pool.deinit(allocator);
|
||||
|
||||
const connection_manager = ConnectionManager.init(allocator, opts.max_idle_connection);
|
||||
var connection_manager = ConnectionManager.init(allocator, opts.max_idle_connection);
|
||||
errdefer connection_manager.deinit();
|
||||
|
||||
return .{
|
||||
@@ -76,6 +113,8 @@ pub const Client = struct {
|
||||
.allocator = allocator,
|
||||
.state_pool = state_pool,
|
||||
.http_proxy = opts.http_proxy,
|
||||
.proxy_type = if (opts.http_proxy == null) null else (opts.proxy_type orelse .connect),
|
||||
.proxy_auth = if (opts.proxy_auth) |*auth| try auth.header_value(allocator) else null,
|
||||
.tls_verify_host = opts.tls_verify_host,
|
||||
.connection_manager = connection_manager,
|
||||
.request_pool = std.heap.MemoryPool(Request).init(allocator),
|
||||
@@ -90,6 +129,10 @@ pub const Client = struct {
|
||||
self.state_pool.deinit(allocator);
|
||||
self.connection_manager.deinit();
|
||||
self.request_pool.deinit();
|
||||
|
||||
if (self.proxy_auth) |auth| {
|
||||
allocator.free(auth);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request(self: *Client, method: Request.Method, uri: *const Uri) !*Request {
|
||||
@@ -186,6 +229,16 @@ pub const Client = struct {
|
||||
pub fn freeSlotCount(self: *Client) usize {
|
||||
return self.state_pool.freeSlotCount();
|
||||
}
|
||||
|
||||
fn isConnectProxy(self: *const Client) bool {
|
||||
const proxy_type = self.proxy_type orelse return false;
|
||||
return proxy_type == .connect;
|
||||
}
|
||||
|
||||
fn isSimpleProxy(self: *const Client) bool {
|
||||
const proxy_type = self.proxy_type orelse return false;
|
||||
return proxy_type == .forward;
|
||||
}
|
||||
};
|
||||
|
||||
const RequestOpts = struct {
|
||||
@@ -330,6 +383,7 @@ pub const Request = struct {
|
||||
_keepalive: bool,
|
||||
|
||||
// extracted from request_uri
|
||||
_request_port: u16,
|
||||
_request_host: []const u8,
|
||||
|
||||
// extracted from connect_uri
|
||||
@@ -420,6 +474,7 @@ pub const Request = struct {
|
||||
._connect_host = decomposed.connect_host,
|
||||
._connect_port = decomposed.connect_port,
|
||||
._request_host = decomposed.request_host,
|
||||
._request_port = decomposed.request_port,
|
||||
._state = state,
|
||||
._client = client,
|
||||
._aborter = null,
|
||||
@@ -455,6 +510,7 @@ pub const Request = struct {
|
||||
connect_port: u16,
|
||||
connect_host: []const u8,
|
||||
connect_uri: *const std.Uri,
|
||||
request_port: u16,
|
||||
request_host: []const u8,
|
||||
};
|
||||
fn decomposeURL(client: *const Client, uri: *const Uri) !DecomposedURL {
|
||||
@@ -470,8 +526,10 @@ pub const Request = struct {
|
||||
connect_host = proxy.host.?.percent_encoded;
|
||||
}
|
||||
|
||||
const is_connect_proxy = client.isConnectProxy();
|
||||
|
||||
var secure: bool = undefined;
|
||||
const scheme = connect_uri.scheme;
|
||||
const scheme = if (is_connect_proxy) uri.scheme else connect_uri.scheme;
|
||||
if (std.ascii.eqlIgnoreCase(scheme, "https")) {
|
||||
secure = true;
|
||||
} else if (std.ascii.eqlIgnoreCase(scheme, "http")) {
|
||||
@@ -479,13 +537,15 @@ pub const Request = struct {
|
||||
} else {
|
||||
return error.UnsupportedUriScheme;
|
||||
}
|
||||
const connect_port: u16 = connect_uri.port orelse if (secure) 443 else 80;
|
||||
const request_port: u16 = uri.port orelse if (secure) 443 else 80;
|
||||
const connect_port: u16 = connect_uri.port orelse (if (is_connect_proxy) 80 else request_port);
|
||||
|
||||
return .{
|
||||
.secure = secure,
|
||||
.connect_port = connect_port,
|
||||
.connect_host = connect_host,
|
||||
.connect_uri = connect_uri,
|
||||
.request_port = request_port,
|
||||
.request_host = request_host,
|
||||
};
|
||||
}
|
||||
@@ -595,13 +655,18 @@ pub const Request = struct {
|
||||
};
|
||||
self._connection = connection;
|
||||
|
||||
const is_connect_proxy = self._client.isConnectProxy();
|
||||
if (is_connect_proxy) {
|
||||
try SyncHandler.connect(self);
|
||||
}
|
||||
|
||||
if (self._secure) {
|
||||
self._connection.?.tls = .{
|
||||
.blocking = try tls.client(std.net.Stream{ .handle = socket }, .{
|
||||
.host = self._connect_host,
|
||||
.host = if (is_connect_proxy) self._request_host else self._connect_host,
|
||||
.root_ca = self._client.root_ca,
|
||||
.insecure_skip_verify = self._tls_verify_host == false,
|
||||
// .key_log_callback = tls.config.key_log.callback,
|
||||
.key_log_callback = tls.config.key_log.callback,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -682,7 +747,7 @@ pub const Request = struct {
|
||||
if (self._secure) {
|
||||
connection.tls = .{
|
||||
.nonblocking = try tls.nb.Client().init(self._client.allocator, .{
|
||||
.host = self._connect_host,
|
||||
.host = if (self._client.isConnectProxy()) self._request_host else self._connect_host,
|
||||
.root_ca = self._client.root_ca,
|
||||
.insecure_skip_verify = self._tls_verify_host == false,
|
||||
// .key_log_callback = tls.config.key_log.callback,
|
||||
@@ -733,6 +798,13 @@ pub const Request = struct {
|
||||
|
||||
try self.headers.append(arena, .{ .name = "User-Agent", .value = "Lightpanda/1.0" });
|
||||
try self.headers.append(arena, .{ .name = "Accept", .value = "*/*" });
|
||||
|
||||
if (self._client.isSimpleProxy()) {
|
||||
if (self._client.proxy_auth) |proxy_auth| {
|
||||
try self.headers.append(arena, .{ .name = "Proxy-Authorization", .value = proxy_auth });
|
||||
}
|
||||
}
|
||||
|
||||
self.requestStarting();
|
||||
}
|
||||
|
||||
@@ -831,7 +903,7 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
fn buildHeader(self: *Request) ![]const u8 {
|
||||
const proxied = self.connect_uri != self.request_uri;
|
||||
const proxied = self._client.isSimpleProxy();
|
||||
|
||||
const buf = self._state.header_buf;
|
||||
var fbs = std.io.fixedBufferStream(buf);
|
||||
@@ -851,6 +923,22 @@ pub const Request = struct {
|
||||
return buf[0..fbs.pos];
|
||||
}
|
||||
|
||||
fn buildConnectHeader(self: *Request) ![]const u8 {
|
||||
const buf = self._state.header_buf;
|
||||
var fbs = std.io.fixedBufferStream(buf);
|
||||
var writer = fbs.writer();
|
||||
|
||||
try writer.print("CONNECT {s}:{d} HTTP/1.1\r\n", .{ self._request_host, self._request_port });
|
||||
try writer.print("Host: {s}:{d}\r\n", .{ self._request_host, self._request_port });
|
||||
|
||||
if (self._client.proxy_auth) |proxy_auth| {
|
||||
try writer.print("Proxy-Authorization: {s}\r\n", .{proxy_auth});
|
||||
}
|
||||
|
||||
_ = try writer.write("\r\n");
|
||||
return buf[0..fbs.pos];
|
||||
}
|
||||
|
||||
fn requestStarting(self: *Request) void {
|
||||
const notification = self.notification orelse return;
|
||||
if (self._notified_start) {
|
||||
@@ -895,6 +983,15 @@ pub const Request = struct {
|
||||
.headers = response.headers.items,
|
||||
});
|
||||
}
|
||||
|
||||
fn shouldProxyConnect(self: *const Request) bool {
|
||||
// if the connection comes from a keepalive pool, than we already
|
||||
// made a CONNECT request
|
||||
if (self._connection_from_keepalive) {
|
||||
return false;
|
||||
}
|
||||
return self._client.isConnectProxy();
|
||||
}
|
||||
};
|
||||
|
||||
// Handles asynchronous requests
|
||||
@@ -958,6 +1055,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
const SendQueue = std.DoublyLinkedList([]const u8);
|
||||
|
||||
const SendState = enum {
|
||||
connect,
|
||||
handshake,
|
||||
header,
|
||||
body,
|
||||
@@ -986,7 +1084,19 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
if (self.shutdown) {
|
||||
return self.maybeShutdown();
|
||||
}
|
||||
|
||||
result catch |err| return self.handleError("Connection failed", err);
|
||||
|
||||
if (self.request.shouldProxyConnect()) {
|
||||
self.state = .connect;
|
||||
const header = self.request.buildConnectHeader() catch |err| {
|
||||
return self.handleError("Failed to build CONNECT header", err);
|
||||
};
|
||||
self.send(header);
|
||||
self.receive();
|
||||
return;
|
||||
}
|
||||
|
||||
self.conn.connected() catch |err| {
|
||||
self.handleError("connected handler error", err);
|
||||
};
|
||||
@@ -1056,6 +1166,12 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.state == .connect) {
|
||||
// We're in a proxy CONNECT flow. There's nothing for us to
|
||||
// do except for wait for the response.
|
||||
return;
|
||||
}
|
||||
|
||||
self.conn.sent() catch |err| {
|
||||
self.handleError("send handling", err);
|
||||
};
|
||||
@@ -1099,7 +1215,27 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
return self.handleError("Connection closed", error.ConnectionResetByPeer);
|
||||
}
|
||||
|
||||
const status = self.conn.received(self.read_buf[0 .. self.read_pos + n]) catch |err| {
|
||||
const data = self.read_buf[0 .. self.read_pos + n];
|
||||
|
||||
if (self.state == .connect) {
|
||||
const success = self.reader.connectResponse(data) catch |err| {
|
||||
return self.handleError("Invalid CONNECT response", err);
|
||||
};
|
||||
|
||||
if (!success) {
|
||||
self.receive();
|
||||
} else {
|
||||
// CONNECT was successful, resume our normal flow
|
||||
self.state = .handshake;
|
||||
self.reader = self.request.newReader();
|
||||
self.conn.connected() catch |err| {
|
||||
self.handleError("connected handler error", err);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const status = self.conn.received(data) catch |err| {
|
||||
if (err == error.TlsAlertCloseNotify and self.state == .handshake and self.maybeRetryRequest()) {
|
||||
return;
|
||||
}
|
||||
@@ -1438,7 +1574,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
const handler = self.handler;
|
||||
switch (self.protocol) {
|
||||
.plain => switch (handler.state) {
|
||||
.handshake => unreachable,
|
||||
.handshake, .connect => unreachable,
|
||||
.header => {
|
||||
handler.state = .body;
|
||||
if (handler.request.body) |body| {
|
||||
@@ -1455,6 +1591,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
return;
|
||||
}
|
||||
switch (handler.state) {
|
||||
.connect => unreachable,
|
||||
.handshake => return self.sendSecureHeader(tls_client),
|
||||
.header => {
|
||||
handler.state = .body;
|
||||
@@ -1589,6 +1726,37 @@ const SyncHandler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately, this is called from the Request doSendSync since we need
|
||||
// to do this before setting up our TLS connection.
|
||||
fn connect(request: *Request) !void {
|
||||
const socket = request._connection.?.socket;
|
||||
|
||||
const header = try request.buildConnectHeader();
|
||||
try Conn.writeAll(socket, header);
|
||||
|
||||
var pos: usize = 0;
|
||||
var reader = request.newReader();
|
||||
var read_buf = request._state.read_buf;
|
||||
|
||||
while (true) {
|
||||
// we would never 'maybeRetryOrErr' on a CONNECT request, because
|
||||
// we only send CONNECT requests on newly established connections
|
||||
// and maybeRetryOrErr is only for connections that might have been
|
||||
// closed while being kept-alive
|
||||
const n = try posix.read(socket, read_buf[pos..]);
|
||||
if (n == 0) {
|
||||
return error.ConnectionResetByPeer;
|
||||
}
|
||||
pos += n;
|
||||
if (try reader.connectResponse(read_buf[0..pos])) {
|
||||
// returns true if we have a successful connect response
|
||||
return;
|
||||
}
|
||||
|
||||
// we don't have enough data yet.
|
||||
}
|
||||
}
|
||||
|
||||
fn maybeRetryOrErr(self: *SyncHandler, err: anyerror) !Response {
|
||||
var request = self.request;
|
||||
|
||||
@@ -1828,6 +1996,26 @@ const Reader = struct {
|
||||
return .{ .use_get = use_get, .location = location };
|
||||
}
|
||||
|
||||
fn connectResponse(self: *Reader, data: []u8) !bool {
|
||||
const result = try self.process(data);
|
||||
if (self.header_done == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.done == false) {
|
||||
// CONNECT responses should not have a body. If the header is
|
||||
// done, then the entire response should be done.
|
||||
return error.InvalidConnectResponse;
|
||||
}
|
||||
|
||||
const status = self.response.status;
|
||||
if (status < 200 or status > 299) {
|
||||
return error.InvalidConnectResponseStatus;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn process(self: *Reader, data: []u8) ProcessError!Result {
|
||||
if (self.body_reader) |*br| {
|
||||
const ok, const result = try br.process(data);
|
||||
@@ -2790,14 +2978,14 @@ test "HttpClient Reader: fuzz" {
|
||||
}
|
||||
|
||||
test "HttpClient: invalid url" {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
const uri = try Uri.parse("http:///");
|
||||
try testing.expectError(error.UriMissingHost, client.request(.GET, &uri));
|
||||
}
|
||||
|
||||
test "HttpClient: sync connect error" {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("HTTP://127.0.0.1:9920");
|
||||
@@ -2809,7 +2997,7 @@ test "HttpClient: sync connect error" {
|
||||
|
||||
test "HttpClient: sync no body" {
|
||||
for (0..2) |i| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/simple");
|
||||
@@ -2831,7 +3019,7 @@ test "HttpClient: sync no body" {
|
||||
|
||||
test "HttpClient: sync tls no body" {
|
||||
for (0..1) |_| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("https://127.0.0.1:9581/http_client/simple");
|
||||
@@ -2850,7 +3038,7 @@ test "HttpClient: sync tls no body" {
|
||||
|
||||
test "HttpClient: sync with body" {
|
||||
for (0..2) |i| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo");
|
||||
@@ -2873,9 +3061,76 @@ test "HttpClient: sync with body" {
|
||||
}
|
||||
}
|
||||
|
||||
test "HttpClient: sync with body proxy CONNECT" {
|
||||
for (0..2) |i| {
|
||||
const proxy_uri = try Uri.parse("http://127.0.0.1:9582/");
|
||||
var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri });
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo");
|
||||
var req = try client.request(.GET, &uri);
|
||||
defer req.deinit();
|
||||
|
||||
var res = try req.sendSync(.{});
|
||||
|
||||
if (i == 0) {
|
||||
try testing.expectEqual("over 9000!", try res.peek());
|
||||
}
|
||||
try testing.expectEqual("over 9000!", try res.next());
|
||||
try testing.expectEqual(201, res.header.status);
|
||||
try testing.expectEqual(6, res.header.count());
|
||||
try testing.expectEqual("Close", res.header.get("connection"));
|
||||
try testing.expectEqual("10", res.header.get("content-length"));
|
||||
try testing.expectEqual("127.0.0.1", res.header.get("_host"));
|
||||
try testing.expectEqual("Lightpanda/1.0", res.header.get("_user-agent"));
|
||||
try testing.expectEqual("*/*", res.header.get("_accept"));
|
||||
// Proxy headers
|
||||
try testing.expectEqual("127.0.0.1:9582", res.header.get("__host"));
|
||||
}
|
||||
}
|
||||
|
||||
test "HttpClient: basic authentication CONNECT" {
|
||||
const proxy_uri = try Uri.parse("http://127.0.0.1:9582/");
|
||||
var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .basic = .{ .user_pass = "user:pass" } } });
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo");
|
||||
var req = try client.request(.GET, &uri);
|
||||
defer req.deinit();
|
||||
|
||||
var res = try req.sendSync(.{});
|
||||
|
||||
try testing.expectEqual(201, res.header.status);
|
||||
// Destination headers
|
||||
try testing.expectEqual(null, res.header.get("_authorization"));
|
||||
try testing.expectEqual(null, res.header.get("_proxy-authorization"));
|
||||
// Proxy headers
|
||||
try testing.expectEqual(null, res.header.get("__authorization"));
|
||||
try testing.expectEqual("Basic dXNlcjpwYXNz", res.header.get("__proxy-authorization"));
|
||||
}
|
||||
test "HttpClient: bearer authentication CONNECT" {
|
||||
const proxy_uri = try Uri.parse("http://127.0.0.1:9582/");
|
||||
var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri, .proxy_auth = .{ .bearer = .{ .token = "fruitsalad" } } });
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo");
|
||||
var req = try client.request(.GET, &uri);
|
||||
defer req.deinit();
|
||||
|
||||
var res = try req.sendSync(.{});
|
||||
|
||||
try testing.expectEqual(201, res.header.status);
|
||||
// Destination headers
|
||||
try testing.expectEqual(null, res.header.get("_authorization"));
|
||||
try testing.expectEqual(null, res.header.get("_proxy-authorization"));
|
||||
// Proxy headers
|
||||
try testing.expectEqual(null, res.header.get("__authorization"));
|
||||
try testing.expectEqual("Bearer fruitsalad", res.header.get("__proxy-authorization"));
|
||||
}
|
||||
|
||||
test "HttpClient: sync with gzip body" {
|
||||
for (0..2) |i| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/gzip");
|
||||
@@ -2897,7 +3152,7 @@ test "HttpClient: sync tls with body" {
|
||||
defer arr.deinit(testing.allocator);
|
||||
try arr.ensureTotalCapacity(testing.allocator, 20);
|
||||
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
for (0..5) |_| {
|
||||
defer arr.clearRetainingCapacity();
|
||||
@@ -2927,7 +3182,7 @@ test "HttpClient: sync redirect from TLS to Plaintext" {
|
||||
|
||||
for (0..5) |_| {
|
||||
defer arr.clearRetainingCapacity();
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("https://127.0.0.1:9581/http_client/redirect/insecure");
|
||||
@@ -2957,7 +3212,7 @@ test "HttpClient: sync redirect plaintext to TLS" {
|
||||
|
||||
for (0..5) |_| {
|
||||
defer arr.clearRetainingCapacity();
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect/secure");
|
||||
@@ -2978,7 +3233,7 @@ test "HttpClient: sync redirect plaintext to TLS" {
|
||||
}
|
||||
|
||||
test "HttpClient: sync GET redirect" {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect");
|
||||
@@ -3024,7 +3279,7 @@ test "HttpClient: async connect error" {
|
||||
};
|
||||
|
||||
var reset: Thread.ResetEvent = .{};
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = Handler{
|
||||
@@ -3056,7 +3311,7 @@ test "HttpClient: async connect error" {
|
||||
test "HttpClient: async no body" {
|
||||
defer testing.reset();
|
||||
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3075,7 +3330,7 @@ test "HttpClient: async no body" {
|
||||
test "HttpClient: async with body" {
|
||||
defer testing.reset();
|
||||
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3100,7 +3355,7 @@ test "HttpClient: async with body" {
|
||||
test "HttpClient: async with gzip body" {
|
||||
defer testing.reset();
|
||||
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3123,7 +3378,7 @@ test "HttpClient: async with gzip body" {
|
||||
test "HttpClient: async redirect" {
|
||||
defer testing.reset();
|
||||
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3153,7 +3408,7 @@ test "HttpClient: async redirect" {
|
||||
|
||||
test "HttpClient: async tls no body" {
|
||||
defer testing.reset();
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
for (0..5) |_| {
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3178,7 +3433,7 @@ test "HttpClient: async tls no body" {
|
||||
test "HttpClient: async tls with body" {
|
||||
defer testing.reset();
|
||||
for (0..5) |_| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3202,7 +3457,7 @@ test "HttpClient: async tls with body" {
|
||||
test "HttpClient: async redirect from TLS to Plaintext" {
|
||||
defer testing.reset();
|
||||
for (0..1) |_| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3228,7 +3483,7 @@ test "HttpClient: async redirect from TLS to Plaintext" {
|
||||
test "HttpClient: async redirect plaintext to TLS" {
|
||||
defer testing.reset();
|
||||
for (0..5) |_| {
|
||||
var client = try testClient();
|
||||
var client = try testClient(.{});
|
||||
defer client.deinit();
|
||||
|
||||
var handler = try CaptureHandler.init();
|
||||
@@ -3441,6 +3696,8 @@ fn testReader(state: *State, res: *TestResponse, data: []const u8) !void {
|
||||
return error.NeverDone;
|
||||
}
|
||||
|
||||
fn testClient() !Client {
|
||||
return try Client.init(testing.allocator, .{ .max_concurrent = 1 });
|
||||
fn testClient(opts: Client.Opts) !Client {
|
||||
var o = opts;
|
||||
o.max_concurrent = 1;
|
||||
return try Client.init(testing.allocator, o);
|
||||
}
|
||||
|
||||
194
src/main.zig
194
src/main.zig
@@ -23,6 +23,7 @@ const Allocator = std.mem.Allocator;
|
||||
const log = @import("log.zig");
|
||||
const server = @import("server.zig");
|
||||
const App = @import("app.zig").App;
|
||||
const http = @import("http/client.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
const Browser = @import("browser/browser.zig").Browser;
|
||||
|
||||
@@ -83,6 +84,8 @@ fn run(alloc: Allocator) !void {
|
||||
var app = try App.init(alloc, .{
|
||||
.run_mode = args.mode,
|
||||
.http_proxy = args.httpProxy(),
|
||||
.proxy_type = args.proxyType(),
|
||||
.proxy_auth = args.proxyAuth(),
|
||||
.tls_verify_host = args.tlsVerifyHost(),
|
||||
});
|
||||
defer app.deinit();
|
||||
@@ -155,6 +158,20 @@ const Command = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn proxyType(self: *const Command) ?http.ProxyType {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.proxy_type,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn proxyAuth(self: *const Command) ?http.ProxyAuth {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.proxy_auth,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn logLevel(self: *const Command) ?log.Level {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_level,
|
||||
@@ -198,6 +215,8 @@ const Command = struct {
|
||||
|
||||
const Common = struct {
|
||||
http_proxy: ?std.Uri = null,
|
||||
proxy_type: ?http.ProxyType = null,
|
||||
proxy_auth: ?http.ProxyAuth = null,
|
||||
tls_verify_host: bool = true,
|
||||
log_level: ?log.Level = null,
|
||||
log_format: ?log.Format = null,
|
||||
@@ -216,6 +235,21 @@ const Command = struct {
|
||||
\\--http_proxy The HTTP proxy to use for all HTTP requests.
|
||||
\\ Defaults to none.
|
||||
\\
|
||||
\\--proxy_type The type of proxy: connect, forward.
|
||||
\\ 'connect' creates a tunnel through the proxy via
|
||||
\\ and initial CONNECT request.
|
||||
\\ 'forward' sends the full URL in the request target
|
||||
\\ and expects the proxy to MITM the request.
|
||||
\\ Defaults to connect when --http_proxy is set.
|
||||
\\
|
||||
\\--proxy_bearer_token
|
||||
\\ The token to send for bearer authentication with the proxy
|
||||
\\ Proxy-Authorization: Bearer <token>
|
||||
\\
|
||||
\\--proxy_basic_auth
|
||||
\\ The user:password to send for basic authentication with the proxy
|
||||
\\ Proxy-Authorization: Basic <base64(user:password)>
|
||||
\\
|
||||
\\--log_level The log level: debug, info, warn, error or fatal.
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
||||
@@ -456,6 +490,47 @@ fn parseCommonArg(
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.http_proxy = try std.Uri.parse(try allocator.dupe(u8, str));
|
||||
if (common.http_proxy.?.host == null) {
|
||||
log.fatal(.app, "invalid http proxy", .{ .arg = "--http_proxy", .hint = "missing scheme?" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--proxy_type", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_type" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_type = std.meta.stringToEnum(http.ProxyType, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--proxy_type", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
||||
if (common.proxy_auth != null) {
|
||||
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_bearer_token" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_auth = .{ .bearer = .{ .token = str } };
|
||||
return true;
|
||||
}
|
||||
if (std.mem.eql(u8, "--proxy_basic_auth", opt)) {
|
||||
if (common.proxy_auth != null) {
|
||||
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_basic_auth" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_basic_auth" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_auth = .{ .basic = .{ .user_pass = str } };
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -573,58 +648,81 @@ fn serveHTTP(address: std.net.Address) !void {
|
||||
var conn = try listener.accept();
|
||||
defer conn.stream.close();
|
||||
var http_server = std.http.Server.init(conn, &read_buffer);
|
||||
|
||||
var request = http_server.receiveHead() catch |err| switch (err) {
|
||||
error.HttpConnectionClosing => continue :ACCEPT,
|
||||
else => {
|
||||
std.debug.print("Test HTTP Server error: {}\n", .{err});
|
||||
return err;
|
||||
},
|
||||
};
|
||||
|
||||
const path = request.head.target;
|
||||
if (std.mem.eql(u8, path, "/loader")) {
|
||||
try request.respond("Hello!", .{
|
||||
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/simple")) {
|
||||
try request.respond("", .{
|
||||
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/redirect")) {
|
||||
try request.respond("", .{
|
||||
.status = .moved_permanently,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Connection", .value = "close" },
|
||||
.{ .name = "LOCATION", .value = "../http_client/echo" },
|
||||
var connect_headers: std.ArrayListUnmanaged(std.http.Header) = .{};
|
||||
REQUEST: while (true) {
|
||||
var request = http_server.receiveHead() catch |err| switch (err) {
|
||||
error.HttpConnectionClosing => continue :ACCEPT,
|
||||
else => {
|
||||
std.debug.print("Test HTTP Server error: {}\n", .{err});
|
||||
return err;
|
||||
},
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) {
|
||||
try request.respond("", .{
|
||||
.status = .moved_permanently,
|
||||
.extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" } },
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/gzip")) {
|
||||
const body = &.{ 0x1f, 0x8b, 0x08, 0x08, 0x01, 0xc6, 0x19, 0x68, 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x00, 0x73, 0x54, 0xc8, 0x4b, 0x2d, 0x57, 0x48, 0x2a, 0xca, 0x2f, 0x2f, 0x4e, 0x2d, 0x52, 0x48, 0x2a, 0xcd, 0xcc, 0x29, 0x51, 0x48, 0xcb, 0x2f, 0x52, 0xc8, 0x4d, 0x4c, 0xce, 0xc8, 0xcc, 0x4b, 0x2d, 0xe6, 0x02, 0x00, 0xe7, 0xc3, 0x4b, 0x27, 0x21, 0x00, 0x00, 0x00 };
|
||||
try request.respond(body, .{
|
||||
.extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "Content-Encoding", .value = "gzip" } },
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/echo")) {
|
||||
var headers: std.ArrayListUnmanaged(std.http.Header) = .{};
|
||||
};
|
||||
|
||||
var it = request.iterateHeaders();
|
||||
while (it.next()) |hdr| {
|
||||
try headers.append(aa, .{
|
||||
.name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}),
|
||||
.value = hdr.value,
|
||||
if (request.head.method == .CONNECT) {
|
||||
try request.respond("", .{ .status = .ok });
|
||||
|
||||
// Proxy headers and destination headers are separated in the case of a CONNECT proxy
|
||||
// We store the CONNECT headers, then continue with the request for the destination
|
||||
var it = request.iterateHeaders();
|
||||
while (it.next()) |hdr| {
|
||||
try connect_headers.append(aa, .{
|
||||
.name = try std.fmt.allocPrint(aa, "__{s}", .{hdr.name}),
|
||||
.value = try aa.dupe(u8, hdr.value),
|
||||
});
|
||||
}
|
||||
continue :REQUEST;
|
||||
}
|
||||
|
||||
const path = request.head.target;
|
||||
if (std.mem.eql(u8, path, "/loader")) {
|
||||
try request.respond("Hello!", .{
|
||||
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/simple")) {
|
||||
try request.respond("", .{
|
||||
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/redirect")) {
|
||||
try request.respond("", .{
|
||||
.status = .moved_permanently,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Connection", .value = "close" },
|
||||
.{ .name = "LOCATION", .value = "../http_client/echo" },
|
||||
},
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) {
|
||||
try request.respond("", .{
|
||||
.status = .moved_permanently,
|
||||
.extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" } },
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/gzip")) {
|
||||
const body = &.{ 0x1f, 0x8b, 0x08, 0x08, 0x01, 0xc6, 0x19, 0x68, 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x00, 0x73, 0x54, 0xc8, 0x4b, 0x2d, 0x57, 0x48, 0x2a, 0xca, 0x2f, 0x2f, 0x4e, 0x2d, 0x52, 0x48, 0x2a, 0xcd, 0xcc, 0x29, 0x51, 0x48, 0xcb, 0x2f, 0x52, 0xc8, 0x4d, 0x4c, 0xce, 0xc8, 0xcc, 0x4b, 0x2d, 0xe6, 0x02, 0x00, 0xe7, 0xc3, 0x4b, 0x27, 0x21, 0x00, 0x00, 0x00 };
|
||||
try request.respond(body, .{
|
||||
.extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "Content-Encoding", .value = "gzip" } },
|
||||
});
|
||||
} else if (std.mem.eql(u8, path, "/http_client/echo")) {
|
||||
var headers: std.ArrayListUnmanaged(std.http.Header) = .{};
|
||||
|
||||
var it = request.iterateHeaders();
|
||||
while (it.next()) |hdr| {
|
||||
try headers.append(aa, .{
|
||||
.name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}),
|
||||
.value = hdr.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (connect_headers.items.len > 0) {
|
||||
try headers.appendSlice(aa, connect_headers.items);
|
||||
connect_headers.clearRetainingCapacity();
|
||||
}
|
||||
try headers.append(aa, .{ .name = "Connection", .value = "Close" });
|
||||
|
||||
try request.respond("over 9000!", .{
|
||||
.status = .created,
|
||||
.extra_headers = headers.items,
|
||||
});
|
||||
}
|
||||
try headers.append(aa, .{ .name = "Connection", .value = "Close" });
|
||||
|
||||
try request.respond("over 9000!", .{
|
||||
.status = .created,
|
||||
.extra_headers = headers.items,
|
||||
});
|
||||
continue :ACCEPT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1936,7 +1936,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
}
|
||||
|
||||
generateIndexer(Struct, template_proto);
|
||||
generateNamedIndexer(Struct, template_proto);
|
||||
generateNamedIndexer(Struct, template.getInstanceTemplate());
|
||||
generateUndetectable(Struct, template.getInstanceTemplate());
|
||||
}
|
||||
|
||||
@@ -2121,7 +2121,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
|
||||
var configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
.getter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
@@ -2143,13 +2144,37 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||
};
|
||||
|
||||
// If you're trying to implement setter, read:
|
||||
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
|
||||
// The issue I had was
|
||||
// (a) where to attache it: does it go ont he instance_template
|
||||
// instead of the prototype?
|
||||
// (b) defining the getter or query to respond with the
|
||||
// PropertyAttribute to indicate if the property can be set
|
||||
if (@hasDecl(Struct, "named_set")) {
|
||||
configuration.setter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller(Self, State).init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_set");
|
||||
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback;
|
||||
}
|
||||
|
||||
if (@hasDecl(Struct, "named_delete")) {
|
||||
configuration.deleter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller(Self, State).init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_delete");
|
||||
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback;
|
||||
}
|
||||
template_proto.setNamedProperty(configuration, null);
|
||||
}
|
||||
|
||||
@@ -2651,37 +2676,63 @@ fn Caller(comptime E: type, comptime State: type) type {
|
||||
}
|
||||
|
||||
fn getNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const js_context = self.js_context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
const NamedGet = @TypeOf(func);
|
||||
if (@typeInfo(NamedGet).@"fn".return_type == null) {
|
||||
@compileError(named_function.full_name ++ " must have a return type");
|
||||
}
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args: ParamterTypes(NamedGet) = undefined;
|
||||
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
|
||||
switch (arg_fields.len) {
|
||||
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
|
||||
3, 4 => {
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
if (comptime arg_fields.len == 4) {
|
||||
comptime assertIsStateArg(Struct, named_function, 3);
|
||||
@field(args, "3") = js_context.state;
|
||||
}
|
||||
},
|
||||
else => @compileError(named_function.full_name ++ " has too many parmaters"),
|
||||
}
|
||||
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
info.getReturnValue().set(try js_context.zigValueToJs(res));
|
||||
info.getReturnValue().set(try self.js_context.zigValueToJs(res));
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
fn setNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const js_context = self.js_context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 4, info);
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = try js_context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
|
||||
@field(args, "3") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
return namedSetOrDeleteCall(res, has_value);
|
||||
}
|
||||
|
||||
fn deleteNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
return namedSetOrDeleteCall(res, has_value);
|
||||
}
|
||||
|
||||
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
|
||||
if (@typeInfo(@TypeOf(res)) == .error_union) {
|
||||
_ = try res;
|
||||
}
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,10 @@ pub fn expectEqual(expected: anytype, actual: anytype) !void {
|
||||
if (@typeInfo(@TypeOf(expected)) == .null) {
|
||||
return std.testing.expectEqual(null, actual);
|
||||
}
|
||||
return expectEqual(expected, actual.?);
|
||||
if (actual) |_actual| {
|
||||
return expectEqual(expected, _actual);
|
||||
}
|
||||
return std.testing.expectEqual(expected, null);
|
||||
},
|
||||
.@"union" => |union_info| {
|
||||
if (union_info.tag_type == null) {
|
||||
|
||||
Reference in New Issue
Block a user