replace zig-async-io and std.http.Client with a custom HTTP client

This commit is contained in:
Karl Seguin
2025-03-14 19:40:56 +08:00
parent fd35724aa8
commit 2017d4785b
17 changed files with 1566 additions and 2145 deletions

View File

@@ -10,7 +10,6 @@ The default license for this project is [AGPL-3.0-only](LICENSE).
The following files are licensed under MIT: The following files are licensed under MIT:
``` ```
src/http/Client.zig
src/polyfill/fetch.js src/polyfill/fetch.js
``` ```

View File

@@ -177,6 +177,9 @@ fn common(
options: jsruntime.Options, options: jsruntime.Options,
) !void { ) !void {
const target = step.root_module.resolved_target.?; const target = step.root_module.resolved_target.?;
const optimize = step.root_module.optimize.?;
const dep_opts = .{ .target = target, .optimize = optimize };
const jsruntimemod = try jsruntime_pkgs.module( const jsruntimemod = try jsruntime_pkgs.module(
b, b,
options, options,
@@ -189,15 +192,7 @@ fn common(
netsurf.addImport("jsruntime", jsruntimemod); netsurf.addImport("jsruntime", jsruntimemod);
step.root_module.addImport("netsurf", netsurf); step.root_module.addImport("netsurf", netsurf);
const asyncio = b.addModule("asyncio", .{ step.root_module.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
});
step.root_module.addImport("asyncio", asyncio);
const tlsmod = b.addModule("tls", .{
.root_source_file = b.path("vendor/tls.zig/src/root.zig"),
});
step.root_module.addImport("tls", tlsmod);
} }
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module { fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {

12
build.zig.zon Normal file
View File

@@ -0,0 +1,12 @@
.{
.name = .browser,
.paths = .{""},
.version = "0.0.0",
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.tls = .{
.url = "https://github.com/karlseguin/tls.zig/archive/e39d40150f10464992da11352fb3955b3345272f.tar.gz",
.hash = "122039cd3abe387b69d23930bf12154c2c84fc894874e10129a1fc5e8ac75ca0ddc0"
},
},
}

View File

@@ -24,7 +24,6 @@ const Allocator = std.mem.Allocator;
const Types = @import("root").Types; const Types = @import("root").Types;
const parser = @import("netsurf"); const parser = @import("netsurf");
const Loader = @import("loader.zig").Loader;
const Dump = @import("dump.zig"); const Dump = @import("dump.zig");
const Mime = @import("mime.zig").Mime; const Mime = @import("mime.zig").Mime;
@@ -44,10 +43,8 @@ const Location = @import("../html/location.zig").Location;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
const FetchResult = @import("../http/Client.zig").Client.FetchResult; const http = @import("../http/client.zig");
const UserContext = @import("../user_context.zig").UserContext; const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("asyncio").Client;
const polyfill = @import("../polyfill/polyfill.zig"); const polyfill = @import("../polyfill/polyfill.zig");
@@ -63,20 +60,21 @@ pub const Browser = struct {
app: *App, app: *App,
session: ?*Session, session: ?*Session,
allocator: Allocator, allocator: Allocator,
http_client: *HttpClient, http_client: *http.Client,
session_pool: SessionPool, session_pool: SessionPool,
page_arena: std.heap.ArenaAllocator, page_arena: std.heap.ArenaAllocator,
const SessionPool = std.heap.MemoryPool(Session); const SessionPool = std.heap.MemoryPool(Session);
pub fn init(app: *App) Browser { pub fn init(app: *App) !Browser {
const allocator = app.allocator; const allocator = app.allocator;
return .{ return .{
.app = app, .app = app,
.session = null, .session = null,
.allocator = allocator, .allocator = allocator,
.http_client = @ptrCast(&app.http_client), .http_client = &app.http_client,
.session_pool = SessionPool.init(allocator), .session_pool = SessionPool.init(allocator),
.http_client = try http.Client.init(allocator, 5),
.page_arena = std.heap.ArenaAllocator.init(allocator), .page_arena = std.heap.ArenaAllocator.init(allocator),
}; };
} }
@@ -121,9 +119,6 @@ pub const Session = struct {
// all others Session deps use directly self.alloc and not the arena. // all others Session deps use directly self.alloc and not the arena.
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
// TODO handle proxy
loader: Loader,
env: Env, env: Env,
inspector: jsruntime.Inspector, inspector: jsruntime.Inspector,
@@ -132,6 +127,7 @@ pub const Session = struct {
// TODO move the shed to the browser? // TODO move the shed to the browser?
storage_shed: storage.Shed, storage_shed: storage.Shed,
page: ?Page = null, page: ?Page = null,
http_client: *http.Client,
jstypes: [Types.len]usize = undefined, jstypes: [Types.len]usize = undefined,
@@ -143,7 +139,7 @@ pub const Session = struct {
.env = undefined, .env = undefined,
.browser = browser, .browser = browser,
.inspector = undefined, .inspector = undefined,
.loader = Loader.init(allocator), .http_client = &browser.http_client,
.storage_shed = storage.Shed.init(allocator), .storage_shed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator),
.window = Window.create(null, .{ .agent = user_agent }), .window = Window.create(null, .{ .agent = user_agent }),
@@ -181,7 +177,6 @@ pub const Session = struct {
} }
self.env.deinit(); self.env.deinit();
self.arena.deinit(); self.arena.deinit();
self.loader.deinit();
self.storage_shed.deinit(); self.storage_shed.deinit();
} }
@@ -370,32 +365,14 @@ pub const Page = struct {
} }); } });
// load the data // load the data
var resp = try self.session.loader.get(arena, self.uri); var request = try self.session.http_client.request(.GET, self.uri);
defer resp.deinit(); defer request.deinit();
var response = try request.sendSync(.{});
const req = resp.req; const header = response.header;
log.info("GET {any} {d}", .{ self.uri, header.status });
log.info("GET {any} {d}", .{ self.uri, @intFromEnum(req.response.status) }); const ct = response.header.get("content-type") orelse {
// TODO handle redirection
log.debug("{?} {d} {s}", .{
req.response.version,
@intFromEnum(req.response.status),
req.response.reason,
// TODO log headers
});
// TODO handle charset
// https://html.spec.whatwg.org/#content-type
var it = req.response.iterateHeaders();
var ct_: ?[]const u8 = null;
while (true) {
const h = it.next() orelse break;
if (std.ascii.eqlIgnoreCase(h.name, "Content-Type")) {
ct_ = try arena.dupe(u8, h.value);
}
}
const ct = ct_ orelse {
// no content type in HTTP headers. // no content type in HTTP headers.
// TODO try to sniff mime type from the body. // TODO try to sniff mime type from the body.
log.info("no content-type HTTP header", .{}); log.info("no content-type HTTP header", .{});
@@ -404,14 +381,18 @@ pub const Page = struct {
log.debug("header content-type: {s}", .{ct}); log.debug("header content-type: {s}", .{ct});
var mime = try Mime.parse(arena, ct); var mime = try Mime.parse(arena, ct);
defer mime.deinit();
if (mime.isHTML()) { if (mime.isHTML()) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", aux_data); try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data);
} else { } else {
log.info("non-HTML document: {s}", .{ct}); log.info("non-HTML document: {s}", .{ct});
var arr: std.ArrayListUnmanaged(u8) = .{};
while (try response.next()) |data| {
try arr.appendSlice(arena, try arena.dupe(u8, data));
}
// save the body into the page. // save the body into the page.
self.raw_data = try req.reader().readAllAlloc(arena, 16 * 1024 * 1024); self.raw_data = arr.items;
} }
} }
@@ -453,7 +434,7 @@ pub const Page = struct {
// replace the user context document with the new one. // replace the user context document with the new one.
try session.env.setUserContext(.{ try session.env.setUserContext(.{
.document = html_doc, .document = html_doc,
.httpClient = self.session.browser.http_client, .http_client = self.session.browser.http_client,
}); });
// browse the DOM tree to retrieve scripts // browse the DOM tree to retrieve scripts
@@ -628,30 +609,32 @@ pub const Page = struct {
const u = try std.Uri.resolve_inplace(self.uri, res_src, &b); const u = try std.Uri.resolve_inplace(self.uri, res_src, &b);
var fetchres = try self.session.loader.get(arena, u); var request = try self.session.http_client.request(.GET, u);
defer fetchres.deinit(); defer request.deinit();
var response = try request.sendSync(.{});
const resp = fetchres.req.response; log.info("fetch {any}: {d}", .{ u, response.header.status });
log.info("fetch {any}: {d}", .{ u, resp.status }); if (response.header.status != 200) {
if (resp.status != .ok) {
return FetchError.BadStatusCode; return FetchError.BadStatusCode;
} }
var arr: std.ArrayListUnmanaged(u8) = .{};
while (try response.next()) |data| {
try arr.appendSlice(arena, try arena.dupe(u8, data));
}
// TODO check content-type // TODO check content-type
const body = try fetchres.req.reader().readAllAlloc(arena, 16 * 1024 * 1024);
// check no body // check no body
if (body.len == 0) { if (arr.items.len == 0) {
return FetchError.NoBody; return FetchError.NoBody;
} }
return body; return arr.items;
} }
// fetchScript senf a GET request to the src and execute the script
// received.
fn fetchScript(self: *const Page, s: *const Script) !void { fn fetchScript(self: *const Page, s: *const Script) !void {
const arena = self.arena; const arena = self.arena;
const body = try self.fetchData(arena, s.src, null); const body = try self.fetchData(arena, s.src, null);

View File

@@ -1,97 +0,0 @@
// 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 Client = @import("../http/Client.zig");
const user_agent = @import("browser.zig").user_agent;
pub const Loader = struct {
client: Client,
// use 64KB for headers buffer size.
server_header_buffer: [1024 * 64]u8 = undefined,
pub const Response = struct {
alloc: std.mem.Allocator,
req: *Client.Request,
pub fn deinit(self: *Response) void {
self.req.deinit();
self.alloc.destroy(self.req);
}
};
pub fn init(alloc: std.mem.Allocator) Loader {
return Loader{
.client = Client{
.allocator = alloc,
},
};
}
pub fn deinit(self: *Loader) void {
self.client.deinit();
}
// see
// https://ziglang.org/documentation/master/std/#A;std:http.Client.fetch
// for reference.
// The caller is responsible for calling `deinit()` on the `Response`.
pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
var resp = Response{
.alloc = alloc,
.req = try alloc.create(Client.Request),
};
errdefer alloc.destroy(resp.req);
resp.req.* = try self.client.open(.GET, uri, .{
.headers = .{
.user_agent = .{ .override = user_agent },
},
.extra_headers = &.{
.{ .name = "Accept", .value = "*/*" },
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
},
.server_header_buffer = &self.server_header_buffer,
});
errdefer resp.req.deinit();
try resp.req.send();
try resp.req.finish();
try resp.req.wait();
return resp;
}
};
test "loader: get" {
const alloc = std.testing.allocator;
var loader = Loader.init(alloc);
defer loader.deinit();
const uri = try std.Uri.parse("http://localhost:9582/loader");
var result = try loader.get(alloc, uri);
defer result.deinit();
try std.testing.expectEqual(.ok, result.req.response.status);
var res: [128]u8 = undefined;
const size = try result.req.readAll(&res);
try std.testing.expectEqual(6, size);
try std.testing.expectEqualStrings("Hello!", res[0..6]);
}

View File

@@ -73,13 +73,13 @@ pub fn CDPT(comptime TypeProvider: type) type {
pub const Browser = TypeProvider.Browser; pub const Browser = TypeProvider.Browser;
pub const Session = TypeProvider.Session; pub const Session = TypeProvider.Session;
pub fn init(app: *App, client: TypeProvider.Client) Self { pub fn init(app: *App, client: TypeProvider.Client) !Self {
const allocator = app.allocator; const allocator = app.allocator;
return .{ return .{
.client = client, .client = client,
.allocator = allocator, .allocator = allocator,
.browser_context = null, .browser_context = null,
.browser = Browser.init(app), .browser = try Browser.init(app),
.message_arena = std.heap.ArenaAllocator.init(allocator), .message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator), .browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
}; };

View File

@@ -17,7 +17,7 @@ const Browser = struct {
session: ?*Session = null, session: ?*Session = null,
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
pub fn init(app: *App) Browser { pub fn init(app: *App) !Browser {
return .{ return .{
.arena = std.heap.ArenaAllocator.init(app.allocator), .arena = std.heap.ArenaAllocator.init(app.allocator),
}; };

File diff suppressed because it is too large Load Diff

1350
src/http/client.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,6 @@ const storage = @import("storage/storage.zig");
const url = @import("url/url.zig"); const url = @import("url/url.zig");
const URL = url.URL; const URL = url.URL;
const urlquery = @import("url/query.zig"); const urlquery = @import("url/query.zig");
const Client = @import("asyncio").Client;
const Location = @import("html/location.zig").Location; const Location = @import("html/location.zig").Location;
const documentTestExecFn = @import("dom/document.zig").testExecFn; const documentTestExecFn = @import("dom/document.zig").testExecFn;
@@ -89,12 +88,12 @@ fn testExecFn(
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)}); std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
}; };
var cli = Client{ .allocator = alloc }; var http_client = try @import("http/client.zig").Client.init(alloc, 5);
defer cli.deinit(); defer http_client.deinit();
try js_env.setUserContext(.{ try js_env.setUserContext(.{
.document = doc, .document = doc,
.httpClient = &cli, .http_client = &http_client,
}); });
// alias global as self and window // alias global as self and window
@@ -220,6 +219,11 @@ pub fn main() !void {
if (run == .all or run == .unit) { if (run == .all or run == .unit) {
std.debug.print("\n", .{}); std.debug.print("\n", .{});
for (builtin.test_functions) |test_fn| { for (builtin.test_functions) |test_fn| {
if (std.mem.startsWith(u8, test_fn.name, "http.client.test")) {
// covered by unit test, needs a dummy server started, which
// main_test doesn't do.
continue;
}
try parser.init(); try parser.init();
defer parser.deinit(); defer parser.deinit();

View File

@@ -2181,21 +2181,28 @@ fn parseParams(enc: ?[:0]const u8) c.dom_hubbub_parser_params {
fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void { fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void {
var err: c.hubbub_error = undefined; var err: c.hubbub_error = undefined;
var buffer: [1024]u8 = undefined; const TI = @typeInfo(@TypeOf(reader));
var ln = buffer.len; if (TI == .pointer and @hasDecl(TI.pointer.child, "next")) {
while (ln > 0) { while (try reader.next()) |data| {
ln = try reader.read(&buffer); err = c.dom_hubbub_parser_parse_chunk(parser, data.ptr, data.len);
err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln); try parserErr(err);
// TODO handle encoding change error return. }
// When the HTML contains a META tag with a different encoding than the } else {
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is var buffer: [1024]u8 = undefined;
// returned. var ln = buffer.len;
// In this case, we must restart the parsing with the new detected while (ln > 0) {
// encoding. The detected encoding is stored in the document and we can ln = try reader.read(&buffer);
// get it with documentGetInputEncoding(). err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
try parserErr(err); // TODO handle encoding change error return.
// When the HTML contains a META tag with a different encoding than the
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is
// returned.
// In this case, we must restart the parsing with the new detected
// encoding. The detected encoding is stored in the document and we can
// get it with documentGetInputEncoding().
try parserErr(err);
}
} }
err = c.dom_hubbub_parser_completed(parser); err = c.dom_hubbub_parser_completed(parser);
try parserErr(err); try parserErr(err);
} }

View File

@@ -445,7 +445,7 @@ pub const Client = struct {
}; };
self.mode = .websocket; self.mode = .websocket;
self.cdp = CDP.init(self.server.app, self); self.cdp = try CDP.init(self.server.app, self);
return self.send(arena, response); return self.send(arena, response);
} }

View File

@@ -30,8 +30,8 @@ pub fn expectEqual(expected: anytype, actual: anytype) !void {
return; return;
}, },
.optional => { .optional => {
if (actual == null) { if (@typeInfo(@TypeOf(expected)) == .null) {
return std.testing.expectEqual(null, expected); return std.testing.expectEqual(null, actual);
} }
return expectEqual(expected, actual.?); return expectEqual(expected, actual.?);
}, },
@@ -141,3 +141,36 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
pub fn app(_: anytype) *App { pub fn app(_: anytype) *App {
return App.init(allocator, .serve) catch unreachable; return App.init(allocator, .serve) catch unreachable;
} }
pub const Random = struct {
var instance: ?std.Random.DefaultPrng = null;
pub fn fill(buf: []u8) void {
var r = random();
r.bytes(buf);
}
pub fn fillAtLeast(buf: []u8, min: usize) []u8 {
var r = random();
const l = r.intRangeAtMost(usize, min, buf.len);
r.bytes(buf[0..l]);
return buf;
}
pub fn intRange(comptime T: type, min: T, max: T) T {
var r = random();
return r.intRangeAtMost(T, min, max);
}
pub fn random() std.Random {
if (instance == null) {
var seed: u64 = undefined;
std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
instance = std.Random.DefaultPrng.init(seed);
// instance = std.Random.DefaultPrng.init(0);
}
return instance.?.random();
}
};
>>>>>>> eaccbd0 (replace zig-async-io and std.http.Client with a custom HTTP client)

View File

@@ -60,7 +60,7 @@ pub fn main() !void {
const http_thread = blk: { const http_thread = blk: {
const address = try std.net.Address.parseIp("127.0.0.1", 9582); const address = try std.net.Address.parseIp("127.0.0.1", 9582);
const thread = try std.Thread.spawn(.{}, serveHTTP, .{address}); const thread = try std.Thread.spawn(.{}, serveHTTP, .{ allocator, address });
break :blk thread; break :blk thread;
}; };
defer http_thread.join(); defer http_thread.join();
@@ -323,12 +323,18 @@ fn isUnnamed(t: std.builtin.TestFn) bool {
return true; return true;
} }
fn serveHTTP(address: std.net.Address) !void { fn serveHTTP(allocator: Allocator, address: std.net.Address) !void {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var listener = try address.listen(.{ .reuse_address = true }); var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit(); defer listener.deinit();
var read_buffer: [1024]u8 = undefined; var read_buffer: [1024]u8 = undefined;
ACCEPT: while (true) { ACCEPT: while (true) {
defer _ = arena.reset(.{ .retain_with_limit = 1024 });
const aa = arena.allocator();
var conn = try listener.accept(); var conn = try listener.accept();
defer conn.stream.close(); defer conn.stream.close();
var server = std.http.Server.init(conn, &read_buffer); var server = std.http.Server.init(conn, &read_buffer);
@@ -344,8 +350,23 @@ fn serveHTTP(address: std.net.Address) !void {
const path = request.head.target; const path = request.head.target;
if (std.mem.eql(u8, path, "/loader")) { if (std.mem.eql(u8, path, "/loader")) {
try writeResponse(&request, .{ try request.respond("Hello!", .{});
.body = "Hello!", } else if (std.mem.eql(u8, path, "/http_client/simple")) {
try request.respond("", .{});
} else if (std.mem.eql(u8, path, "/http_client/body")) {
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,
});
}
try request.respond("over 9000!", .{
.status = .created,
.extra_headers = headers.items,
}); });
} }
} }
@@ -360,26 +381,16 @@ fn serveCDP(app: *App, address: std.net.Address) !void {
}; };
} }
const Response = struct {
body: []const u8 = "",
status: std.http.Status = .ok,
};
fn writeResponse(req: *std.http.Server.Request, res: Response) !void {
try req.respond(res.body, .{ .status = res.status });
}
test { test {
std.testing.refAllDecls(@import("url/query.zig")); std.testing.refAllDecls(@import("url/query.zig"));
std.testing.refAllDecls(@import("browser/dump.zig")); std.testing.refAllDecls(@import("browser/dump.zig"));
std.testing.refAllDecls(@import("browser/loader.zig"));
std.testing.refAllDecls(@import("browser/mime.zig")); std.testing.refAllDecls(@import("browser/mime.zig"));
std.testing.refAllDecls(@import("css/css.zig")); std.testing.refAllDecls(@import("css/css.zig"));
std.testing.refAllDecls(@import("css/libdom_test.zig")); std.testing.refAllDecls(@import("css/libdom_test.zig"));
std.testing.refAllDecls(@import("css/match_test.zig")); std.testing.refAllDecls(@import("css/match_test.zig"));
std.testing.refAllDecls(@import("css/parser.zig")); std.testing.refAllDecls(@import("css/parser.zig"));
std.testing.refAllDecls(@import("generate.zig")); std.testing.refAllDecls(@import("generate.zig"));
std.testing.refAllDecls(@import("http/Client.zig")); std.testing.refAllDecls(@import("http/client.zig"));
std.testing.refAllDecls(@import("storage/storage.zig")); std.testing.refAllDecls(@import("storage/storage.zig"));
std.testing.refAllDecls(@import("storage/cookie.zig")); std.testing.refAllDecls(@import("storage/cookie.zig"));
std.testing.refAllDecls(@import("iterator/iterator.zig")); std.testing.refAllDecls(@import("iterator/iterator.zig"));
@@ -388,4 +399,5 @@ test {
std.testing.refAllDecls(@import("log.zig")); std.testing.refAllDecls(@import("log.zig"));
std.testing.refAllDecls(@import("datetime.zig")); std.testing.refAllDecls(@import("datetime.zig"));
std.testing.refAllDecls(@import("telemetry/telemetry.zig")); std.testing.refAllDecls(@import("telemetry/telemetry.zig"));
std.testing.refAllDecls(@import("http/client.zig"));
} }

View File

@@ -1,8 +1,8 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("netsurf");
const Client = @import("asyncio").Client; const Client = @import("http/client.zig").Client;
pub const UserContext = struct { pub const UserContext = struct {
document: *parser.DocumentHTML, document: *parser.DocumentHTML,
httpClient: *Client, http_client: *Client,
}; };

View File

@@ -31,7 +31,7 @@ const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEven
const Mime = @import("../browser/mime.zig").Mime; const Mime = @import("../browser/mime.zig").Mime;
const Loop = jsruntime.Loop; const Loop = jsruntime.Loop;
const Client = @import("asyncio").Client; const http = @import("../http/client.zig");
const parser = @import("netsurf"); const parser = @import("netsurf");
@@ -95,14 +95,12 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
pub const XMLHttpRequest = struct { pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
cli: *Client, client: *http.Client,
io: Client.IO, request: ?http.Request = null,
priv_state: PrivState = .new, priv_state: PrivState = .new,
req: ?Client.Request = null,
ctx: ?Client.Ctx = null,
method: std.http.Method, method: http.Request.Method,
state: State, state: State,
url: ?[]const u8, url: ?[]const u8,
uri: std.Uri, uri: std.Uri,
@@ -125,7 +123,7 @@ pub const XMLHttpRequest = struct {
withCredentials: bool = false, withCredentials: bool = false,
// TODO: response readonly attribute any response; // TODO: response readonly attribute any response;
response_bytes: ?[]const u8 = null, response_bytes: std.ArrayListUnmanaged(u8) = .{},
response_type: ResponseType = .Empty, response_type: ResponseType = .Empty,
response_headers: Headers, response_headers: Headers,
@@ -133,7 +131,7 @@ pub const XMLHttpRequest = struct {
// use 16KB for headers buffer size. // use 16KB for headers buffer size.
response_header_buffer: [1024 * 16]u8 = undefined, response_header_buffer: [1024 * 16]u8 = undefined,
response_status: u10 = 0, response_status: u16 = 0,
// TODO uncomment this field causes casting issue with // TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but // XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
@@ -246,13 +244,6 @@ pub const XMLHttpRequest = struct {
fn all(self: Headers) []std.http.Header { fn all(self: Headers) []std.http.Header {
return self.list.items; return self.list.items;
} }
fn load(self: *Headers, it: *std.http.HeaderIterator) !void {
while (true) {
const h = it.next() orelse break;
_ = try self.append(h.name, h.value);
}
}
}; };
const Response = union(ResponseType) { const Response = union(ResponseType) {
@@ -290,17 +281,16 @@ pub const XMLHttpRequest = struct {
const min_delay: u64 = 50000000; // 50ms const min_delay: u64 = 50000000; // 50ms
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop, userctx: UserContext) !XMLHttpRequest { pub fn constructor(alloc: std.mem.Allocator, userctx: UserContext) !XMLHttpRequest {
return .{ return .{
.alloc = alloc, .alloc = alloc,
.headers = Headers.init(alloc), .headers = Headers.init(alloc),
.response_headers = Headers.init(alloc), .response_headers = Headers.init(alloc),
.io = Client.IO.init(loop),
.method = undefined, .method = undefined,
.url = null, .url = null,
.uri = undefined, .uri = undefined,
.state = .unsent, .state = .unsent,
.cli = userctx.httpClient, .client = userctx.http_client,
}; };
} }
@@ -311,7 +301,6 @@ pub const XMLHttpRequest = struct {
if (self.payload) |v| alloc.free(v); if (self.payload) |v| alloc.free(v);
self.payload = null; self.payload = null;
if (self.response_bytes) |v| alloc.free(v);
if (self.response_obj) |v| v.deinit(); if (self.response_obj) |v| v.deinit();
self.response_obj = null; self.response_obj = null;
@@ -329,12 +318,6 @@ pub const XMLHttpRequest = struct {
self.send_flag = false; self.send_flag = false;
self.priv_state = .new; self.priv_state = .new;
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
} }
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
@@ -449,7 +432,7 @@ pub const XMLHttpRequest = struct {
} }
const methods = [_]struct { const methods = [_]struct {
tag: std.http.Method, tag: http.Request.Method,
name: []const u8, name: []const u8,
}{ }{
.{ .tag = .DELETE, .name = "DELETE" }, .{ .tag = .DELETE, .name = "DELETE" },
@@ -461,7 +444,7 @@ pub const XMLHttpRequest = struct {
}; };
const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" }; const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" };
pub fn validMethod(m: []const u8) DOMError!std.http.Method { pub fn validMethod(m: []const u8) DOMError!http.Request.Method {
for (methods) |method| { for (methods) |method| {
if (std.ascii.eqlIgnoreCase(method.name, m)) { if (std.ascii.eqlIgnoreCase(method.name, m)) {
return method.tag; return method.tag;
@@ -485,7 +468,7 @@ pub const XMLHttpRequest = struct {
} }
// TODO body can be either a XMLHttpRequestBodyInit or a document // TODO body can be either a XMLHttpRequestBodyInit or a document
pub fn _send(self: *XMLHttpRequest, alloc: std.mem.Allocator, body: ?[]const u8) !void { pub fn _send(self: *XMLHttpRequest, loop: *Loop, alloc: std.mem.Allocator, body: ?[]const u8) !void {
if (self.state != .opened) return DOMError.InvalidState; if (self.state != .opened) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState; if (self.send_flag) return DOMError.InvalidState;
@@ -515,153 +498,77 @@ pub const XMLHttpRequest = struct {
self.priv_state = .open; self.priv_state = .open;
self.req = try self.cli.create(self.method, self.uri, .{ self.request = try self.client.request(self.method, self.uri);
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
});
errdefer {
self.req.?.deinit();
self.req = null;
}
self.ctx = try Client.Ctx.init(&self.io, &self.req.?); var request = &self.request.?;
errdefer { errdefer request.deinit();
self.ctx.?.deinit();
self.ctx = null;
}
self.ctx.?.userData = self;
try self.cli.async_open( for (self.headers.list.items) |hdr| {
self.method, try request.addHeader(hdr.name, hdr.value, .{});
self.uri, }
.{ .server_header_buffer = &self.response_header_buffer }, request.body = self.payload;
&self.ctx.?, try request.sendAsync(loop, self, .{});
onRequestConnect,
);
} }
fn onRequestWait(ctx: *Client.Ctx, res: anyerror!void) !void { pub fn onHttpResponse(self: *XMLHttpRequest, progress_: http.Error!http.Progress) !void {
var self = selfCtx(ctx); const progress = progress_ catch |err| {
res catch |err| return self.onErr(err); self.onErr(err);
return err;
};
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status }); if (progress.first) {
const header = progress.header;
log.info("{any} {any} {d}", .{ self.method, self.uri, header.status });
self.priv_state = .done; self.priv_state = .done;
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
// extract a mime type from headers. for (header.headers.items) |hdr| {
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml"; try self.response_headers.append(hdr.name, hdr.value);
self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e); }
// TODO handle override mime type // extract a mime type from headers.
const ct = header.get("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e);
self.state = .headers_received; // TODO handle override mime type
self.dispatchEvt("readystatechange"); self.state = .headers_received;
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
// TODO read async
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = .loading;
self.dispatchEvt("readystatechange"); self.dispatchEvt("readystatechange");
// dispatch a progress event progress. self.response_status = header.status;
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
// TODO correct total
self.dispatchProgressEvent("loadstart", .{ .loaded = 0, .total = 0 });
self.state = .loading;
}
const data = progress.data orelse return;
const buf = &self.response_bytes;
try buf.appendSlice(self.alloc, data);
const total_len = buf.items.len;
// TODO: don't dispatch this more than once every 50ms
// dispatch a progress event progress.
self.dispatchEvt("readystatechange");
self.dispatchProgressEvent("progress", .{
.total = buf.items.len,
.loaded = buf.items.len,
});
if (progress.done == false) {
return;
}
self.send_flag = false;
self.state = .done; self.state = .done;
self.dispatchEvt("readystatechange"); self.dispatchEvt("readystatechange");
// dispatch a progress event load. // dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total }); self.dispatchProgressEvent("load", .{ .loaded = total_len, .total = total_len });
// dispatch a progress event loadend. // dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total }); self.dispatchProgressEvent("loadend", .{ .loaded = total_len, .total = total_len });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
fn onRequestFinish(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .wait;
return ctx.req.async_wait(ctx, onRequestWait) catch |e| return self.onErr(e);
}
fn onRequestSend(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
if (self.payload) |payload| {
self.priv_state = .write;
return ctx.req.async_writeAll(payload, ctx, onRequestWrite) catch |e| return self.onErr(e);
}
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestWrite(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestConnect(ctx: *Client.Ctx, res: anyerror!void) anyerror!void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
return ctx.req.async_send(ctx, onRequestSend) catch |err| return self.onErr(err);
}
fn selfCtx(ctx: *Client.Ctx) *XMLHttpRequest {
return @ptrCast(@alignCast(ctx.userData));
} }
fn onErr(self: *XMLHttpRequest, err: anyerror) void { fn onErr(self: *XMLHttpRequest, err: anyerror) void {
@@ -675,12 +582,6 @@ pub const XMLHttpRequest = struct {
self.dispatchProgressEvent("loadend", .{}); self.dispatchProgressEvent("loadend", .{});
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err }); log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
} }
pub fn _abort(self: *XMLHttpRequest) void { pub fn _abort(self: *XMLHttpRequest) void {
@@ -803,7 +704,7 @@ pub const XMLHttpRequest = struct {
} }
if (self.response_type == .JSON) { if (self.response_type == .JSON) {
if (self.response_bytes == null) return null; if (self.response_bytes.items.len == 0) return null;
// TODO Let jsonObject be the result of running parse JSON from bytes // TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return // on thiss received bytes. If that threw an exception, then return
@@ -841,7 +742,7 @@ pub const XMLHttpRequest = struct {
}; };
defer alloc.free(ccharset); defer alloc.free(ccharset);
var fbs = std.io.fixedBufferStream(self.response_bytes.?); var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
self.response_obj = .{ .Failure = true }; self.response_obj = .{ .Failure = true };
return; return;
@@ -862,7 +763,7 @@ pub const XMLHttpRequest = struct {
const p = std.json.parseFromSlice( const p = std.json.parseFromSlice(
JSONValue, JSONValue,
alloc, alloc,
self.response_bytes.?, self.response_bytes.items,
.{}, .{},
) catch |e| { ) catch |e| {
log.err("parse JSON: {}", .{e}); log.err("parse JSON: {}", .{e});
@@ -875,8 +776,7 @@ pub const XMLHttpRequest = struct {
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState; if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState;
return self.response_bytes.items;
return if (self.response_bytes) |v| v else "";
} }
pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 { pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 {

1
vendor/tls.zig vendored

Submodule vendor/tls.zig deleted from 7eb35dabf8