mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 06:33:29 +00:00
replace zig-async-io and std.http.Client with a custom HTTP client
This commit is contained in:
@@ -10,7 +10,6 @@ The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||
The following files are licensed under MIT:
|
||||
|
||||
```
|
||||
src/http/Client.zig
|
||||
src/polyfill/fetch.js
|
||||
```
|
||||
|
||||
|
||||
13
build.zig
13
build.zig
@@ -177,6 +177,9 @@ fn common(
|
||||
options: jsruntime.Options,
|
||||
) !void {
|
||||
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(
|
||||
b,
|
||||
options,
|
||||
@@ -189,15 +192,7 @@ fn common(
|
||||
netsurf.addImport("jsruntime", jsruntimemod);
|
||||
step.root_module.addImport("netsurf", netsurf);
|
||||
|
||||
const asyncio = b.addModule("asyncio", .{
|
||||
.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);
|
||||
step.root_module.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
|
||||
12
build.zig.zon
Normal file
12
build.zig.zon
Normal 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"
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -24,7 +24,6 @@ const Allocator = std.mem.Allocator;
|
||||
const Types = @import("root").Types;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const Loader = @import("loader.zig").Loader;
|
||||
const Dump = @import("dump.zig");
|
||||
const Mime = @import("mime.zig").Mime;
|
||||
|
||||
@@ -44,10 +43,8 @@ const Location = @import("../html/location.zig").Location;
|
||||
|
||||
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 HttpClient = @import("asyncio").Client;
|
||||
|
||||
const polyfill = @import("../polyfill/polyfill.zig");
|
||||
|
||||
@@ -63,20 +60,21 @@ pub const Browser = struct {
|
||||
app: *App,
|
||||
session: ?*Session,
|
||||
allocator: Allocator,
|
||||
http_client: *HttpClient,
|
||||
http_client: *http.Client,
|
||||
session_pool: SessionPool,
|
||||
page_arena: std.heap.ArenaAllocator,
|
||||
|
||||
const SessionPool = std.heap.MemoryPool(Session);
|
||||
|
||||
pub fn init(app: *App) Browser {
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
return .{
|
||||
.app = app,
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.http_client = @ptrCast(&app.http_client),
|
||||
.http_client = &app.http_client,
|
||||
.session_pool = SessionPool.init(allocator),
|
||||
.http_client = try http.Client.init(allocator, 5),
|
||||
.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.
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
// TODO handle proxy
|
||||
loader: Loader,
|
||||
|
||||
env: Env,
|
||||
inspector: jsruntime.Inspector,
|
||||
|
||||
@@ -132,6 +127,7 @@ pub const Session = struct {
|
||||
// TODO move the shed to the browser?
|
||||
storage_shed: storage.Shed,
|
||||
page: ?Page = null,
|
||||
http_client: *http.Client,
|
||||
|
||||
jstypes: [Types.len]usize = undefined,
|
||||
|
||||
@@ -143,7 +139,7 @@ pub const Session = struct {
|
||||
.env = undefined,
|
||||
.browser = browser,
|
||||
.inspector = undefined,
|
||||
.loader = Loader.init(allocator),
|
||||
.http_client = &browser.http_client,
|
||||
.storage_shed = storage.Shed.init(allocator),
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.window = Window.create(null, .{ .agent = user_agent }),
|
||||
@@ -181,7 +177,6 @@ pub const Session = struct {
|
||||
}
|
||||
self.env.deinit();
|
||||
self.arena.deinit();
|
||||
self.loader.deinit();
|
||||
self.storage_shed.deinit();
|
||||
}
|
||||
|
||||
@@ -370,32 +365,14 @@ pub const Page = struct {
|
||||
} });
|
||||
|
||||
// load the data
|
||||
var resp = try self.session.loader.get(arena, self.uri);
|
||||
defer resp.deinit();
|
||||
var request = try self.session.http_client.request(.GET, self.uri);
|
||||
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) });
|
||||
|
||||
// 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 {
|
||||
const ct = response.header.get("content-type") orelse {
|
||||
// no content type in HTTP headers.
|
||||
// TODO try to sniff mime type from the body.
|
||||
log.info("no content-type HTTP header", .{});
|
||||
@@ -404,14 +381,18 @@ pub const Page = struct {
|
||||
|
||||
log.debug("header content-type: {s}", .{ct});
|
||||
var mime = try Mime.parse(arena, ct);
|
||||
defer mime.deinit();
|
||||
|
||||
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 {
|
||||
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.
|
||||
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.
|
||||
try session.env.setUserContext(.{
|
||||
.document = html_doc,
|
||||
.httpClient = self.session.browser.http_client,
|
||||
.http_client = self.session.browser.http_client,
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
var fetchres = try self.session.loader.get(arena, u);
|
||||
defer fetchres.deinit();
|
||||
var request = try self.session.http_client.request(.GET, u);
|
||||
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 (resp.status != .ok) {
|
||||
if (response.header.status != 200) {
|
||||
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
|
||||
const body = try fetchres.req.reader().readAllAlloc(arena, 16 * 1024 * 1024);
|
||||
|
||||
// check no body
|
||||
if (body.len == 0) {
|
||||
if (arr.items.len == 0) {
|
||||
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 {
|
||||
const arena = self.arena;
|
||||
const body = try self.fetchData(arena, s.src, null);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -73,13 +73,13 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
pub const Browser = TypeProvider.Browser;
|
||||
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;
|
||||
return .{
|
||||
.client = client,
|
||||
.allocator = allocator,
|
||||
.browser_context = null,
|
||||
.browser = Browser.init(app),
|
||||
.browser = try Browser.init(app),
|
||||
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const Browser = struct {
|
||||
session: ?*Session = null,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
pub fn init(app: *App) Browser {
|
||||
pub fn init(app: *App) !Browser {
|
||||
return .{
|
||||
.arena = std.heap.ArenaAllocator.init(app.allocator),
|
||||
};
|
||||
|
||||
1776
src/http/Client.zig
1776
src/http/Client.zig
File diff suppressed because it is too large
Load Diff
1350
src/http/client.zig
Normal file
1350
src/http/client.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,6 @@ const storage = @import("storage/storage.zig");
|
||||
const url = @import("url/url.zig");
|
||||
const URL = url.URL;
|
||||
const urlquery = @import("url/query.zig");
|
||||
const Client = @import("asyncio").Client;
|
||||
const Location = @import("html/location.zig").Location;
|
||||
|
||||
const documentTestExecFn = @import("dom/document.zig").testExecFn;
|
||||
@@ -89,12 +88,12 @@ fn testExecFn(
|
||||
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
|
||||
var cli = Client{ .allocator = alloc };
|
||||
defer cli.deinit();
|
||||
var http_client = try @import("http/client.zig").Client.init(alloc, 5);
|
||||
defer http_client.deinit();
|
||||
|
||||
try js_env.setUserContext(.{
|
||||
.document = doc,
|
||||
.httpClient = &cli,
|
||||
.http_client = &http_client,
|
||||
});
|
||||
|
||||
// alias global as self and window
|
||||
@@ -220,6 +219,11 @@ pub fn main() !void {
|
||||
if (run == .all or run == .unit) {
|
||||
std.debug.print("\n", .{});
|
||||
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();
|
||||
defer parser.deinit();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
var err: c.hubbub_error = undefined;
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = try reader.read(&buffer);
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
|
||||
// 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);
|
||||
const TI = @typeInfo(@TypeOf(reader));
|
||||
if (TI == .pointer and @hasDecl(TI.pointer.child, "next")) {
|
||||
while (try reader.next()) |data| {
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, data.ptr, data.len);
|
||||
try parserErr(err);
|
||||
}
|
||||
} else {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = try reader.read(&buffer);
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
|
||||
// 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);
|
||||
try parserErr(err);
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ pub const Client = struct {
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ pub fn expectEqual(expected: anytype, actual: anytype) !void {
|
||||
return;
|
||||
},
|
||||
.optional => {
|
||||
if (actual == null) {
|
||||
return std.testing.expectEqual(null, expected);
|
||||
if (@typeInfo(@TypeOf(expected)) == .null) {
|
||||
return std.testing.expectEqual(null, actual);
|
||||
}
|
||||
return expectEqual(expected, actual.?);
|
||||
},
|
||||
@@ -141,3 +141,36 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
|
||||
pub fn app(_: anytype) *App {
|
||||
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)
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn main() !void {
|
||||
|
||||
const http_thread = blk: {
|
||||
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;
|
||||
};
|
||||
defer http_thread.join();
|
||||
@@ -323,12 +323,18 @@ fn isUnnamed(t: std.builtin.TestFn) bool {
|
||||
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 });
|
||||
defer listener.deinit();
|
||||
|
||||
var read_buffer: [1024]u8 = undefined;
|
||||
ACCEPT: while (true) {
|
||||
defer _ = arena.reset(.{ .retain_with_limit = 1024 });
|
||||
const aa = arena.allocator();
|
||||
|
||||
var conn = try listener.accept();
|
||||
defer conn.stream.close();
|
||||
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;
|
||||
if (std.mem.eql(u8, path, "/loader")) {
|
||||
try writeResponse(&request, .{
|
||||
.body = "Hello!",
|
||||
try request.respond("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 {
|
||||
std.testing.refAllDecls(@import("url/query.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("css/css.zig"));
|
||||
std.testing.refAllDecls(@import("css/libdom_test.zig"));
|
||||
std.testing.refAllDecls(@import("css/match_test.zig"));
|
||||
std.testing.refAllDecls(@import("css/parser.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/cookie.zig"));
|
||||
std.testing.refAllDecls(@import("iterator/iterator.zig"));
|
||||
@@ -388,4 +399,5 @@ test {
|
||||
std.testing.refAllDecls(@import("log.zig"));
|
||||
std.testing.refAllDecls(@import("datetime.zig"));
|
||||
std.testing.refAllDecls(@import("telemetry/telemetry.zig"));
|
||||
std.testing.refAllDecls(@import("http/client.zig"));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
const Client = @import("asyncio").Client;
|
||||
const Client = @import("http/client.zig").Client;
|
||||
|
||||
pub const UserContext = struct {
|
||||
document: *parser.DocumentHTML,
|
||||
httpClient: *Client,
|
||||
http_client: *Client,
|
||||
};
|
||||
|
||||
238
src/xhr/xhr.zig
238
src/xhr/xhr.zig
@@ -31,7 +31,7 @@ const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEven
|
||||
const Mime = @import("../browser/mime.zig").Mime;
|
||||
|
||||
const Loop = jsruntime.Loop;
|
||||
const Client = @import("asyncio").Client;
|
||||
const http = @import("../http/client.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
@@ -95,14 +95,12 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
|
||||
pub const XMLHttpRequest = struct {
|
||||
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
|
||||
alloc: std.mem.Allocator,
|
||||
cli: *Client,
|
||||
io: Client.IO,
|
||||
client: *http.Client,
|
||||
request: ?http.Request = null,
|
||||
|
||||
priv_state: PrivState = .new,
|
||||
req: ?Client.Request = null,
|
||||
ctx: ?Client.Ctx = null,
|
||||
|
||||
method: std.http.Method,
|
||||
method: http.Request.Method,
|
||||
state: State,
|
||||
url: ?[]const u8,
|
||||
uri: std.Uri,
|
||||
@@ -125,7 +123,7 @@ pub const XMLHttpRequest = struct {
|
||||
|
||||
withCredentials: bool = false,
|
||||
// TODO: response readonly attribute any response;
|
||||
response_bytes: ?[]const u8 = null,
|
||||
response_bytes: std.ArrayListUnmanaged(u8) = .{},
|
||||
response_type: ResponseType = .Empty,
|
||||
response_headers: Headers,
|
||||
|
||||
@@ -133,7 +131,7 @@ pub const XMLHttpRequest = struct {
|
||||
// use 16KB for headers buffer size.
|
||||
response_header_buffer: [1024 * 16]u8 = undefined,
|
||||
|
||||
response_status: u10 = 0,
|
||||
response_status: u16 = 0,
|
||||
|
||||
// TODO uncomment this field causes casting issue with
|
||||
// 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 {
|
||||
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) {
|
||||
@@ -290,17 +281,16 @@ pub const XMLHttpRequest = struct {
|
||||
|
||||
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 .{
|
||||
.alloc = alloc,
|
||||
.headers = Headers.init(alloc),
|
||||
.response_headers = Headers.init(alloc),
|
||||
.io = Client.IO.init(loop),
|
||||
.method = undefined,
|
||||
.url = null,
|
||||
.uri = undefined,
|
||||
.state = .unsent,
|
||||
.cli = userctx.httpClient,
|
||||
.client = userctx.http_client,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,7 +301,6 @@ pub const XMLHttpRequest = struct {
|
||||
if (self.payload) |v| alloc.free(v);
|
||||
self.payload = null;
|
||||
|
||||
if (self.response_bytes) |v| alloc.free(v);
|
||||
if (self.response_obj) |v| v.deinit();
|
||||
|
||||
self.response_obj = null;
|
||||
@@ -329,12 +318,6 @@ pub const XMLHttpRequest = struct {
|
||||
self.send_flag = false;
|
||||
|
||||
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 {
|
||||
@@ -449,7 +432,7 @@ pub const XMLHttpRequest = struct {
|
||||
}
|
||||
|
||||
const methods = [_]struct {
|
||||
tag: std.http.Method,
|
||||
tag: http.Request.Method,
|
||||
name: []const u8,
|
||||
}{
|
||||
.{ .tag = .DELETE, .name = "DELETE" },
|
||||
@@ -461,7 +444,7 @@ pub const XMLHttpRequest = struct {
|
||||
};
|
||||
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| {
|
||||
if (std.ascii.eqlIgnoreCase(method.name, m)) {
|
||||
return method.tag;
|
||||
@@ -485,7 +468,7 @@ pub const XMLHttpRequest = struct {
|
||||
}
|
||||
|
||||
// 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.send_flag) return DOMError.InvalidState;
|
||||
|
||||
@@ -515,153 +498,77 @@ pub const XMLHttpRequest = struct {
|
||||
|
||||
self.priv_state = .open;
|
||||
|
||||
self.req = try self.cli.create(self.method, self.uri, .{
|
||||
.server_header_buffer = &self.response_header_buffer,
|
||||
.extra_headers = self.headers.all(),
|
||||
});
|
||||
errdefer {
|
||||
self.req.?.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
self.request = try self.client.request(self.method, self.uri);
|
||||
|
||||
self.ctx = try Client.Ctx.init(&self.io, &self.req.?);
|
||||
errdefer {
|
||||
self.ctx.?.deinit();
|
||||
self.ctx = null;
|
||||
}
|
||||
self.ctx.?.userData = self;
|
||||
var request = &self.request.?;
|
||||
errdefer request.deinit();
|
||||
|
||||
try self.cli.async_open(
|
||||
self.method,
|
||||
self.uri,
|
||||
.{ .server_header_buffer = &self.response_header_buffer },
|
||||
&self.ctx.?,
|
||||
onRequestConnect,
|
||||
);
|
||||
for (self.headers.list.items) |hdr| {
|
||||
try request.addHeader(hdr.name, hdr.value, .{});
|
||||
}
|
||||
request.body = self.payload;
|
||||
try request.sendAsync(loop, self, .{});
|
||||
}
|
||||
|
||||
fn onRequestWait(ctx: *Client.Ctx, res: anyerror!void) !void {
|
||||
var self = selfCtx(ctx);
|
||||
res catch |err| return self.onErr(err);
|
||||
pub fn onHttpResponse(self: *XMLHttpRequest, progress_: http.Error!http.Progress) !void {
|
||||
const progress = progress_ catch |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;
|
||||
var it = self.req.?.response.iterateHeaders();
|
||||
self.response_headers.load(&it) catch |e| return self.onErr(e);
|
||||
self.priv_state = .done;
|
||||
|
||||
// extract a mime type from headers.
|
||||
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
|
||||
self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e);
|
||||
for (header.headers.items) |hdr| {
|
||||
try self.response_headers.append(hdr.name, hdr.value);
|
||||
}
|
||||
|
||||
// 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;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
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;
|
||||
// TODO handle override mime type
|
||||
self.state = .headers_received;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event progress.
|
||||
self.dispatchProgressEvent("progress", .{
|
||||
.loaded = loaded,
|
||||
.total = total,
|
||||
});
|
||||
}
|
||||
self.response_bytes = buf.items;
|
||||
self.send_flag = false;
|
||||
self.response_status = header.status;
|
||||
|
||||
// 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.dispatchEvt("readystatechange");
|
||||
|
||||
// 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.
|
||||
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
|
||||
|
||||
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));
|
||||
self.dispatchProgressEvent("loadend", .{ .loaded = total_len, .total = total_len });
|
||||
}
|
||||
|
||||
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
|
||||
@@ -675,12 +582,6 @@ pub const XMLHttpRequest = struct {
|
||||
self.dispatchProgressEvent("loadend", .{});
|
||||
|
||||
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 {
|
||||
@@ -803,7 +704,7 @@ pub const XMLHttpRequest = struct {
|
||||
}
|
||||
|
||||
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
|
||||
// on this’s received bytes. If that threw an exception, then return
|
||||
@@ -841,7 +742,7 @@ pub const XMLHttpRequest = struct {
|
||||
};
|
||||
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 {
|
||||
self.response_obj = .{ .Failure = true };
|
||||
return;
|
||||
@@ -862,7 +763,7 @@ pub const XMLHttpRequest = struct {
|
||||
const p = std.json.parseFromSlice(
|
||||
JSONValue,
|
||||
alloc,
|
||||
self.response_bytes.?,
|
||||
self.response_bytes.items,
|
||||
.{},
|
||||
) catch |e| {
|
||||
log.err("parse JSON: {}", .{e});
|
||||
@@ -875,8 +776,7 @@ pub const XMLHttpRequest = struct {
|
||||
|
||||
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
|
||||
if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState;
|
||||
|
||||
return if (self.response_bytes) |v| v else "";
|
||||
return self.response_bytes.items;
|
||||
}
|
||||
|
||||
pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 {
|
||||
|
||||
1
vendor/tls.zig
vendored
1
vendor/tls.zig
vendored
Submodule vendor/tls.zig deleted from 7eb35dabf8
Reference in New Issue
Block a user