mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
Compare commits
1 Commits
beta
...
window-deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58ca323918 |
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -28,7 +28,3 @@
|
|||||||
[submodule "vendor/zig-async-io"]
|
[submodule "vendor/zig-async-io"]
|
||||||
path = vendor/zig-async-io
|
path = vendor/zig-async-io
|
||||||
url = git@github.com:lightpanda-io/zig-async-io.git
|
url = git@github.com:lightpanda-io/zig-async-io.git
|
||||||
[submodule "vendor/websocket.zig"]
|
|
||||||
path = vendor/websocket.zig
|
|
||||||
url = git@github.com:lightpanda-io/websocket.zig.git
|
|
||||||
branch = lightpanda
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ The following files are licensed under MIT:
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/http/Client.zig
|
src/http/Client.zig
|
||||||
src/polyfill/fetch.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The following directories and their subdirectories are licensed under their
|
The following directories and their subdirectories are licensed under their
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -2,9 +2,7 @@
|
|||||||
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">Lightpanda Browser</h1>
|
<h1 align="center">Lightpanda</h1>
|
||||||
|
|
||||||
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<br />
|
<br />
|
||||||
@@ -18,10 +16,10 @@ Lightpanda is the open-source browser made for headless usage:
|
|||||||
|
|
||||||
Fast scraping and web automation with minimal memory footprint:
|
Fast scraping and web automation with minimal memory footprint:
|
||||||
|
|
||||||
- Ultra-low memory footprint (9x less than Chrome)
|
- Ultra-low memory footprint (12x less than Chrome)
|
||||||
- Blazingly fast execution (11x faster than Chrome) & instant startup
|
- Blazingly fast & instant startup (64x faster than Chrome)
|
||||||
|
|
||||||
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
|
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
|
||||||
|
|
||||||
See [benchmark details](https://github.com/lightpanda-io/demo).
|
See [benchmark details](https://github.com/lightpanda-io/demo).
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ Back in the good old times, grabbing a webpage was as easy as making an HTTP req
|
|||||||
|
|
||||||
### Chrome is not the right tool
|
### Chrome is not the right tool
|
||||||
|
|
||||||
So if we need Javascript, why not use a real web browser. Let’s take a huge desktop application, hack it, and run it on the server, right? Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
So if we need Javascript, why not use a real web browser. Let’s take a huge desktop application, hack it, and run it on the server, right? Hundreds of instance of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||||
|
|
||||||
- Heavy on RAM and CPU, expensive to run
|
- Heavy on RAM and CPU, expensive to run
|
||||||
- Hard to package, deploy and maintain at scale
|
- Hard to package, deploy and maintain at scale
|
||||||
@@ -48,34 +46,34 @@ If we want both Javascript and performance, for a real headless browser, we need
|
|||||||
|
|
||||||
- Not based on Chromium, Blink or WebKit
|
- Not based on Chromium, Blink or WebKit
|
||||||
- Low-level system programming language (Zig) with optimisations in mind
|
- Low-level system programming language (Zig) with optimisations in mind
|
||||||
- Opinionated, without graphical rendering
|
- Opinionated, no rendering
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Lightpanda is still a work in progress and is currently at a Beta stage.
|
Lightpanda is still a work in progress and is currently at the Alpha stage.
|
||||||
|
|
||||||
Here are the key features we have implemented:
|
Here are the key features we want to implement before releasing a Beta version:
|
||||||
|
|
||||||
- [x] HTTP loader
|
- [x] Loader
|
||||||
- [x] HTML parser and DOM tree
|
- [x] HTML parser and DOM tree
|
||||||
- [x] Javascript support (v8)
|
- [x] Javascript support
|
||||||
- [x] Basic DOM APIs
|
- [x] Basic DOM APIs
|
||||||
- [x] Ajax
|
- [x] Ajax
|
||||||
- [x] XHR API
|
- [x] XHR API
|
||||||
- [x] Fetch API
|
- [ ] Fetch API
|
||||||
- [x] DOM dump
|
- [x] DOM dump
|
||||||
- [x] Basic CDP/websockets server
|
- [ ] Basic CDP server
|
||||||
|
|
||||||
|
We will not provide binary versions until we reach at least the Beta stage.
|
||||||
|
|
||||||
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
|
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
|
||||||
|
|
||||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
We do provide [nighly builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for Linux x86_64 and MacOS aarch64.
|
|
||||||
|
|
||||||
## Build from sources
|
## Build from sources
|
||||||
|
|
||||||
|
We do not provide yet binary versions of Lightpanda, you have to compile it from source.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
|
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
|
||||||
|
|||||||
@@ -168,11 +168,6 @@ fn common(
|
|||||||
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
|
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
|
||||||
});
|
});
|
||||||
step.root_module.addImport("tls", tlsmod);
|
step.root_module.addImport("tls", tlsmod);
|
||||||
|
|
||||||
const wsmod = b.addModule("websocket", .{
|
|
||||||
.root_source_file = b.path("vendor/websocket.zig/src/websocket.zig"),
|
|
||||||
});
|
|
||||||
step.root_module.addImport("websocket", wsmod);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ const FetchResult = @import("../http/Client.zig").Client.FetchResult;
|
|||||||
const UserContext = @import("../user_context.zig").UserContext;
|
const UserContext = @import("../user_context.zig").UserContext;
|
||||||
const HttpClient = @import("asyncio").Client;
|
const HttpClient = @import("asyncio").Client;
|
||||||
|
|
||||||
const polyfill = @import("../polyfill/polyfill.zig");
|
|
||||||
|
|
||||||
const log = std.log.scoped(.browser);
|
const log = std.log.scoped(.browser);
|
||||||
|
|
||||||
// Browser is an instance of the browser.
|
// Browser is an instance of the browser.
|
||||||
@@ -357,9 +355,6 @@ pub const Page = struct {
|
|||||||
log.debug("start js env", .{});
|
log.debug("start js env", .{});
|
||||||
try self.session.env.start();
|
try self.session.env.start();
|
||||||
|
|
||||||
// load polyfills
|
|
||||||
try polyfill.load(alloc, self.session.env);
|
|
||||||
|
|
||||||
// inspector
|
// inspector
|
||||||
if (self.session.inspector) |inspector| {
|
if (self.session.inspector) |inspector| {
|
||||||
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
|
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ pub fn sendEvent(
|
|||||||
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
|
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
|
||||||
|
|
||||||
const event_msg = try stringify(alloc, resp);
|
const event_msg = try stringify(alloc, resp);
|
||||||
try ctx.send(event_msg);
|
try server.sendAsync(ctx, event_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ fn navigate(
|
|||||||
.loaderId = ctx.state.loaderID,
|
.loaderId = ctx.state.loaderID,
|
||||||
};
|
};
|
||||||
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
|
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
|
||||||
try ctx.send(res);
|
try server.sendAsync(ctx, res);
|
||||||
|
|
||||||
// TODO: at this point do we need async the following actions to be async?
|
// TODO: at this point do we need async the following actions to be async?
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ fn disposeBrowserContext(
|
|||||||
|
|
||||||
// output
|
// output
|
||||||
const res = try result(alloc, input.id, null, .{}, null);
|
const res = try result(alloc, input.id, null, .{}, null);
|
||||||
try ctx.send(res);
|
try server.sendAsync(ctx, res);
|
||||||
|
|
||||||
return error.DisposeBrowserContext;
|
return error.DisposeBrowserContext;
|
||||||
}
|
}
|
||||||
@@ -378,7 +378,7 @@ fn closeTarget(
|
|||||||
success: bool = true,
|
success: bool = true,
|
||||||
};
|
};
|
||||||
const res = try result(alloc, input.id, Resp, Resp{}, null);
|
const res = try result(alloc, input.id, Resp, Resp{}, null);
|
||||||
try ctx.send(res);
|
try server.sendAsync(ctx, res);
|
||||||
|
|
||||||
// Inspector.detached event
|
// Inspector.detached event
|
||||||
const InspectorDetached = struct {
|
const InspectorDetached = struct {
|
||||||
|
|||||||
@@ -1,95 +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 ws = @import("websocket");
|
|
||||||
const Msg = @import("msg.zig").Msg;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.handler);
|
|
||||||
|
|
||||||
pub const Stream = struct {
|
|
||||||
addr: std.net.Address,
|
|
||||||
socket: std.posix.socket_t = undefined,
|
|
||||||
|
|
||||||
ws_host: []const u8,
|
|
||||||
ws_port: u16,
|
|
||||||
ws_conn: *ws.Conn = undefined,
|
|
||||||
|
|
||||||
fn connectCDP(self: *Stream) !void {
|
|
||||||
const flags: u32 = std.posix.SOCK.STREAM;
|
|
||||||
const proto = blk: {
|
|
||||||
if (self.addr.any.family == std.posix.AF.UNIX) break :blk @as(u32, 0);
|
|
||||||
break :blk std.posix.IPPROTO.TCP;
|
|
||||||
};
|
|
||||||
const socket = try std.posix.socket(self.addr.any.family, flags, proto);
|
|
||||||
|
|
||||||
try std.posix.connect(
|
|
||||||
socket,
|
|
||||||
&self.addr.any,
|
|
||||||
self.addr.getOsSockLen(),
|
|
||||||
);
|
|
||||||
log.debug("connected to Stream server", .{});
|
|
||||||
self.socket = socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn closeCDP(self: *const Stream) void {
|
|
||||||
const close_msg: []const u8 = .{ 5, 0 } ++ "close";
|
|
||||||
self.recv(close_msg) catch |err| {
|
|
||||||
log.err("stream close error: {any}", .{err});
|
|
||||||
};
|
|
||||||
std.posix.close(self.socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start(self: *Stream, ws_conn: *ws.Conn) !void {
|
|
||||||
try self.connectCDP();
|
|
||||||
self.ws_conn = ws_conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recv(self: *const Stream, data: []const u8) !void {
|
|
||||||
var pos: usize = 0;
|
|
||||||
while (pos < data.len) {
|
|
||||||
const len = try std.posix.write(self.socket, data[pos..]);
|
|
||||||
pos += len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send(self: *const Stream, data: []const u8) !void {
|
|
||||||
return self.ws_conn.write(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Handler = struct {
|
|
||||||
stream: *Stream,
|
|
||||||
|
|
||||||
pub fn init(_: ws.Handshake, ws_conn: *ws.Conn, stream: *Stream) !Handler {
|
|
||||||
try stream.start(ws_conn);
|
|
||||||
return .{ .stream = stream };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(self: *Handler) void {
|
|
||||||
self.stream.closeCDP();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clientMessage(self: *Handler, data: []const u8) !void {
|
|
||||||
var header: [2]u8 = undefined;
|
|
||||||
Msg.setSize(data.len, &header);
|
|
||||||
try self.stream.recv(&header);
|
|
||||||
try self.stream.recv(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -26,7 +26,6 @@ const checkCases = jsruntime.test_utils.checkCases;
|
|||||||
|
|
||||||
const Element = @import("../dom/element.zig").Element;
|
const Element = @import("../dom/element.zig").Element;
|
||||||
const URL = @import("../url/url.zig").URL;
|
const URL = @import("../url/url.zig").URL;
|
||||||
const Node = @import("../dom/node.zig").Node;
|
|
||||||
|
|
||||||
// HTMLElement interfaces
|
// HTMLElement interfaces
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -118,25 +117,6 @@ pub const HTMLElement = struct {
|
|||||||
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
|
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
|
|
||||||
const n = @as(*parser.Node, @ptrCast(e));
|
|
||||||
return try parser.nodeTextContent(n) orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
|
|
||||||
const n = @as(*parser.Node, @ptrCast(e));
|
|
||||||
|
|
||||||
// create text node.
|
|
||||||
const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
|
|
||||||
const t = try parser.documentCreateTextNode(doc, s);
|
|
||||||
|
|
||||||
// remove existing children.
|
|
||||||
try Node.removeChildren(n);
|
|
||||||
|
|
||||||
// attach the text node.
|
|
||||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deprecated HTMLElements in Chrome (2023/03/15)
|
// Deprecated HTMLElements in Chrome (2023/03/15)
|
||||||
@@ -1088,12 +1068,4 @@ pub fn testExecFn(
|
|||||||
.{ .src = "script.async", .ex = "false" },
|
.{ .src = "script.async", .ex = "false" },
|
||||||
};
|
};
|
||||||
try checkCases(js_env, &script);
|
try checkCases(js_env, &script);
|
||||||
|
|
||||||
var innertext = [_]Case{
|
|
||||||
.{ .src = "const backup = document.getElementById('content')", .ex = "undefined" },
|
|
||||||
.{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" },
|
|
||||||
.{ .src = "document.getElementById('content').innerText", .ex = "foo" },
|
|
||||||
.{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" },
|
|
||||||
};
|
|
||||||
try checkCases(js_env, &innertext);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|||||||
|
|
||||||
const storage = @import("../storage/storage.zig");
|
const storage = @import("../storage/storage.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.window);
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||||
pub const Window = struct {
|
pub const Window = struct {
|
||||||
@@ -66,6 +68,10 @@ pub const Window = struct {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _debug(_: *Window, str: []const u8) void {
|
||||||
|
log.debug("{s}", .{str});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_self(self: *Window) *Window {
|
pub fn get_self(self: *Window) *Window {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
197
src/main.zig
197
src/main.zig
@@ -17,15 +17,13 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const posix = std.posix;
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const jsruntime = @import("jsruntime");
|
const jsruntime = @import("jsruntime");
|
||||||
const websocket = @import("websocket");
|
|
||||||
|
|
||||||
const Browser = @import("browser/browser.zig").Browser;
|
const Browser = @import("browser/browser.zig").Browser;
|
||||||
const server = @import("server.zig");
|
const server = @import("server.zig");
|
||||||
const handler = @import("handler.zig");
|
|
||||||
const MaxSize = @import("msg.zig").MaxSize;
|
|
||||||
|
|
||||||
const parser = @import("netsurf");
|
const parser = @import("netsurf");
|
||||||
const apiweb = @import("apiweb.zig");
|
const apiweb = @import("apiweb.zig");
|
||||||
@@ -34,12 +32,103 @@ pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
|||||||
pub const UserContext = apiweb.UserContext;
|
pub const UserContext = apiweb.UserContext;
|
||||||
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
||||||
|
|
||||||
// Simple blocking websocket connection model
|
|
||||||
// ie. 1 thread per ws connection without thread pool and epoll/kqueue
|
|
||||||
pub const websocket_blocking = true;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.cli);
|
const log = std.log.scoped(.cli);
|
||||||
|
|
||||||
|
// Inspired by std.net.StreamServer in Zig < 0.12
|
||||||
|
pub const StreamServer = struct {
|
||||||
|
/// Copied from `Options` on `init`.
|
||||||
|
kernel_backlog: u31,
|
||||||
|
reuse_address: bool,
|
||||||
|
reuse_port: bool,
|
||||||
|
nonblocking: bool,
|
||||||
|
|
||||||
|
/// `undefined` until `listen` returns successfully.
|
||||||
|
listen_address: std.net.Address,
|
||||||
|
|
||||||
|
sockfd: ?posix.socket_t,
|
||||||
|
|
||||||
|
pub const Options = struct {
|
||||||
|
/// How many connections the kernel will accept on the application's behalf.
|
||||||
|
/// If more than this many connections pool in the kernel, clients will start
|
||||||
|
/// seeing "Connection refused".
|
||||||
|
kernel_backlog: u31 = 128,
|
||||||
|
|
||||||
|
/// Enable SO.REUSEADDR on the socket.
|
||||||
|
reuse_address: bool = false,
|
||||||
|
|
||||||
|
/// Enable SO.REUSEPORT on the socket.
|
||||||
|
reuse_port: bool = false,
|
||||||
|
|
||||||
|
/// Non-blocking mode.
|
||||||
|
nonblocking: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// After this call succeeds, resources have been acquired and must
|
||||||
|
/// be released with `deinit`.
|
||||||
|
pub fn init(options: Options) StreamServer {
|
||||||
|
return StreamServer{
|
||||||
|
.sockfd = null,
|
||||||
|
.kernel_backlog = options.kernel_backlog,
|
||||||
|
.reuse_address = options.reuse_address,
|
||||||
|
.reuse_port = options.reuse_port,
|
||||||
|
.nonblocking = options.nonblocking,
|
||||||
|
.listen_address = undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release all resources. The `StreamServer` memory becomes `undefined`.
|
||||||
|
pub fn deinit(self: *StreamServer) void {
|
||||||
|
self.close();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setSockOpt(fd: posix.socket_t, level: i32, option: u32, value: c_int) !void {
|
||||||
|
try posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen(self: *StreamServer, address: std.net.Address) !void {
|
||||||
|
const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
|
||||||
|
var use_sock_flags: u32 = sock_flags;
|
||||||
|
if (self.nonblocking) use_sock_flags |= posix.SOCK.NONBLOCK;
|
||||||
|
const proto = if (address.any.family == posix.AF.UNIX) @as(u32, 0) else posix.IPPROTO.TCP;
|
||||||
|
|
||||||
|
const sockfd = try posix.socket(address.any.family, use_sock_flags, proto);
|
||||||
|
self.sockfd = sockfd;
|
||||||
|
errdefer {
|
||||||
|
posix.close(sockfd);
|
||||||
|
self.sockfd = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// socket options
|
||||||
|
if (self.reuse_address) {
|
||||||
|
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
|
||||||
|
}
|
||||||
|
if (@hasDecl(posix.SO, "REUSEPORT") and self.reuse_port) {
|
||||||
|
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1);
|
||||||
|
}
|
||||||
|
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
|
||||||
|
// WARNING: disable Nagle's alogrithm to avoid latency issues
|
||||||
|
try setSockOpt(sockfd, posix.IPPROTO.TCP, posix.TCP.NODELAY, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var socklen = address.getOsSockLen();
|
||||||
|
try posix.bind(sockfd, &address.any, socklen);
|
||||||
|
try posix.listen(sockfd, self.kernel_backlog);
|
||||||
|
try posix.getsockname(sockfd, &self.listen_address.any, &socklen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop listening. It is still necessary to call `deinit` after stopping listening.
|
||||||
|
/// Calling `deinit` will automatically call `close`. It is safe to call `close` when
|
||||||
|
/// not listening.
|
||||||
|
pub fn close(self: *StreamServer) void {
|
||||||
|
if (self.sockfd) |fd| {
|
||||||
|
posix.close(fd);
|
||||||
|
self.sockfd = null;
|
||||||
|
self.listen_address = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const usage =
|
const usage =
|
||||||
\\usage: {s} [options] [URL]
|
\\usage: {s} [options] [URL]
|
||||||
\\
|
\\
|
||||||
@@ -50,7 +139,7 @@ const usage =
|
|||||||
\\
|
\\
|
||||||
\\ -h, --help Print this help message and exit.
|
\\ -h, --help Print this help message and exit.
|
||||||
\\ --host Host of the CDP server (default "127.0.0.1")
|
\\ --host Host of the CDP server (default "127.0.0.1")
|
||||||
\\ --port Port of the CDP server (default "9222")
|
\\ --port Port of the CDP server (default "3245")
|
||||||
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
||||||
\\ --dump Dump document in stdout (fetch mode only)
|
\\ --dump Dump document in stdout (fetch mode only)
|
||||||
\\
|
\\
|
||||||
@@ -81,11 +170,10 @@ const CliMode = union(CliModeTag) {
|
|||||||
host: []const u8 = Host,
|
host: []const u8 = Host,
|
||||||
port: u16 = Port,
|
port: u16 = Port,
|
||||||
timeout: u8 = Timeout,
|
timeout: u8 = Timeout,
|
||||||
tcp: bool = false, // undocumented TCP mode
|
|
||||||
|
|
||||||
// default options
|
// default options
|
||||||
const Host = "127.0.0.1";
|
const Host = "127.0.0.1";
|
||||||
const Port = 9222;
|
const Port = 3245;
|
||||||
const Timeout = 3; // in seconds
|
const Timeout = 3; // in seconds
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,10 +235,6 @@ const CliMode = union(CliModeTag) {
|
|||||||
return printUsageExit(execname, 1);
|
return printUsageExit(execname, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (std.mem.eql(u8, "--tcp", opt)) {
|
|
||||||
_server.tcp = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unknown option
|
// unknown option
|
||||||
if (std.mem.startsWith(u8, opt, "--")) {
|
if (std.mem.startsWith(u8, opt, "--")) {
|
||||||
@@ -233,70 +317,33 @@ pub fn main() !void {
|
|||||||
defer cli_mode.deinit();
|
defer cli_mode.deinit();
|
||||||
|
|
||||||
switch (cli_mode) {
|
switch (cli_mode) {
|
||||||
.server => |opts| {
|
.server => |mode| {
|
||||||
|
|
||||||
// Stream server
|
// server
|
||||||
const addr = blk: {
|
var srv = StreamServer.init(.{
|
||||||
if (opts.tcp) {
|
.reuse_address = true,
|
||||||
break :blk opts.addr;
|
.reuse_port = true,
|
||||||
} else {
|
.nonblocking = true,
|
||||||
const unix_path = "/tmp/lightpanda";
|
});
|
||||||
std.fs.deleteFileAbsolute(unix_path) catch {}; // file could not exists
|
defer srv.deinit();
|
||||||
break :blk try std.net.Address.initUnix(unix_path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const socket = server.listen(addr) catch |err| {
|
|
||||||
log.err("Server listen error: {any}\n", .{err});
|
|
||||||
return printUsageExit(opts.execname, 1);
|
|
||||||
};
|
|
||||||
defer std.posix.close(socket);
|
|
||||||
log.debug("Server opts: listening internally on {any}...", .{addr});
|
|
||||||
|
|
||||||
const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
|
srv.listen(mode.addr) catch |err| {
|
||||||
|
log.err("address (host:port) {any}\n", .{err});
|
||||||
|
return printUsageExit(mode.execname, 1);
|
||||||
|
};
|
||||||
|
defer srv.close();
|
||||||
|
log.info("Server mode: listening on {s}:{d}...", .{ mode.host, mode.port });
|
||||||
|
|
||||||
// loop
|
// loop
|
||||||
var loop = try jsruntime.Loop.init(alloc);
|
var loop = try jsruntime.Loop.init(alloc);
|
||||||
defer loop.deinit();
|
defer loop.deinit();
|
||||||
|
|
||||||
// TCP server mode
|
// listen
|
||||||
if (opts.tcp) {
|
try server.listen(alloc, &loop, srv.sockfd.?, std.time.ns_per_s * @as(u64, mode.timeout));
|
||||||
return server.handle(alloc, &loop, socket, null, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// start stream server in separate thread
|
|
||||||
var stream = handler.Stream{
|
|
||||||
.ws_host = opts.host,
|
|
||||||
.ws_port = opts.port,
|
|
||||||
.addr = addr,
|
|
||||||
};
|
|
||||||
const cdp_thread = try std.Thread.spawn(
|
|
||||||
.{ .allocator = alloc },
|
|
||||||
server.handle,
|
|
||||||
.{ alloc, &loop, socket, &stream, timeout },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Websocket server
|
|
||||||
var ws = try websocket.Server(handler.Handler).init(alloc, .{
|
|
||||||
.port = opts.port,
|
|
||||||
.address = opts.host,
|
|
||||||
.max_message_size = MaxSize + 14, // overhead websocket
|
|
||||||
.max_conn = 1,
|
|
||||||
.handshake = .{
|
|
||||||
.timeout = 3,
|
|
||||||
.max_size = 1024,
|
|
||||||
// since we aren't using hanshake.headers
|
|
||||||
// we can set this to 0 to save a few bytes.
|
|
||||||
.max_headers = 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
defer ws.deinit();
|
|
||||||
|
|
||||||
try ws.listen(&stream);
|
|
||||||
cdp_thread.join();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
.fetch => |opts| {
|
.fetch => |mode| {
|
||||||
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
|
log.debug("Fetch mode: url {s}, dump {any}", .{ mode.url, mode.dump });
|
||||||
|
|
||||||
// vm
|
// vm
|
||||||
const vm = jsruntime.VM.init();
|
const vm = jsruntime.VM.init();
|
||||||
@@ -314,21 +361,21 @@ pub fn main() !void {
|
|||||||
// page
|
// page
|
||||||
const page = try browser.session.createPage();
|
const page = try browser.session.createPage();
|
||||||
|
|
||||||
_ = page.navigate(opts.url, null) catch |err| switch (err) {
|
_ = page.navigate(mode.url, null) catch |err| switch (err) {
|
||||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||||
log.err("'{s}' is not a valid URL ({any})\n", .{ opts.url, err });
|
log.err("'{s}' is not a valid URL ({any})\n", .{ mode.url, err });
|
||||||
return printUsageExit(opts.execname, 1);
|
return printUsageExit(mode.execname, 1);
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
log.err("'{s}' fetching error ({any})s\n", .{ opts.url, err });
|
log.err("'{s}' fetching error ({any})s\n", .{ mode.url, err });
|
||||||
return printUsageExit(opts.execname, 1);
|
return printUsageExit(mode.execname, 1);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try page.wait();
|
try page.wait();
|
||||||
|
|
||||||
// dump
|
// dump
|
||||||
if (opts.dump) {
|
if (mode.dump) {
|
||||||
try page.dump(std.io.getStdOut());
|
try page.dump(std.io.getStdOut());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
107
src/msg.zig
107
src/msg.zig
@@ -18,47 +18,44 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub const MsgSize = 16 * 1204; // 16KB
|
/// MsgBuffer returns messages from a raw text read stream,
|
||||||
pub const HeaderSize = 2;
|
/// according to the following format `<msg_size>:<msg>`.
|
||||||
pub const MaxSize = HeaderSize + MsgSize;
|
|
||||||
|
|
||||||
pub const Msg = struct {
|
|
||||||
pub fn getSize(data: []const u8) usize {
|
|
||||||
return std.mem.readInt(u16, data[0..HeaderSize], .little);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setSize(len: usize, header: *[2]u8) void {
|
|
||||||
std.mem.writeInt(u16, header, @intCast(len), .little);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Buffer returns messages from a raw text read stream,
|
|
||||||
/// with the message size being encoded on the 2 first bytes (little endian)
|
|
||||||
/// It handles both:
|
/// It handles both:
|
||||||
/// - combined messages in one read
|
/// - combined messages in one read
|
||||||
/// - single message in several reads (multipart)
|
/// - single message in several reads (multipart)
|
||||||
/// It's safe (and a good practice) to reuse the same Buffer
|
/// It's safe (and a good practice) to reuse the same MsgBuffer
|
||||||
/// on several reads of the same stream.
|
/// on several reads of the same stream.
|
||||||
pub const Buffer = struct {
|
pub const MsgBuffer = struct {
|
||||||
buf: []u8,
|
|
||||||
size: usize = 0,
|
size: usize = 0,
|
||||||
|
buf: []u8,
|
||||||
pos: usize = 0,
|
pos: usize = 0,
|
||||||
|
|
||||||
fn isFinished(self: *const Buffer) bool {
|
const MaxSize = 1024 * 1024; // 1MB
|
||||||
|
|
||||||
|
pub fn init(alloc: std.mem.Allocator, size: usize) std.mem.Allocator.Error!MsgBuffer {
|
||||||
|
const buf = try alloc.alloc(u8, size);
|
||||||
|
return .{ .buf = buf };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: MsgBuffer, alloc: std.mem.Allocator) void {
|
||||||
|
alloc.free(self.buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isFinished(self: *MsgBuffer) bool {
|
||||||
return self.pos >= self.size;
|
return self.pos >= self.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isEmpty(self: *const Buffer) bool {
|
fn isEmpty(self: MsgBuffer) bool {
|
||||||
return self.size == 0 and self.pos == 0;
|
return self.size == 0 and self.pos == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(self: *Buffer) void {
|
fn reset(self: *MsgBuffer) void {
|
||||||
self.size = 0;
|
self.size = 0;
|
||||||
self.pos = 0;
|
self.pos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// read input
|
// read input
|
||||||
pub fn read(self: *Buffer, input: []const u8) !struct {
|
pub fn read(self: *MsgBuffer, alloc: std.mem.Allocator, input: []const u8) !struct {
|
||||||
msg: []const u8,
|
msg: []const u8,
|
||||||
left: []const u8,
|
left: []const u8,
|
||||||
} {
|
} {
|
||||||
@@ -67,9 +64,11 @@ pub const Buffer = struct {
|
|||||||
// msg size
|
// msg size
|
||||||
var msg_size: usize = undefined;
|
var msg_size: usize = undefined;
|
||||||
if (self.isEmpty()) {
|
if (self.isEmpty()) {
|
||||||
// decode msg size header
|
// parse msg size metadata
|
||||||
msg_size = Msg.getSize(_input);
|
const size_pos = std.mem.indexOfScalar(u8, _input, ':') orelse return error.InputWithoutSize;
|
||||||
_input = _input[HeaderSize..];
|
const size_str = _input[0..size_pos];
|
||||||
|
msg_size = try std.fmt.parseInt(u32, size_str, 10);
|
||||||
|
_input = _input[size_pos + 1 ..];
|
||||||
} else {
|
} else {
|
||||||
msg_size = self.size;
|
msg_size = self.size;
|
||||||
}
|
}
|
||||||
@@ -78,7 +77,7 @@ pub const Buffer = struct {
|
|||||||
const is_multipart = !self.isEmpty() or _input.len < msg_size;
|
const is_multipart = !self.isEmpty() or _input.len < msg_size;
|
||||||
if (is_multipart) {
|
if (is_multipart) {
|
||||||
|
|
||||||
// set msg size on empty Buffer
|
// set msg size on empty MsgBuffer
|
||||||
if (self.isEmpty()) {
|
if (self.isEmpty()) {
|
||||||
self.size = msg_size;
|
self.size = msg_size;
|
||||||
}
|
}
|
||||||
@@ -91,11 +90,19 @@ pub const Buffer = struct {
|
|||||||
return error.MsgTooBig;
|
return error.MsgTooBig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy the current input into Buffer
|
// check if the current input can fit in MsgBuffer
|
||||||
// NOTE: we could use @memcpy but it's not Thread-safe (alias problem)
|
if (new_pos > self.buf.len) {
|
||||||
// see https://www.openmymind.net/Zigs-memcpy-copyForwards-and-copyBackwards/
|
// we want to realloc at least:
|
||||||
// Intead we just use std.mem.copyForwards
|
// - a size big enough to fit the entire input (ie. new_pos)
|
||||||
std.mem.copyForwards(u8, self.buf[self.pos..new_pos], _input[0..]);
|
// - a size big enough (ie. current msg size + starting buffer size)
|
||||||
|
// to avoid multiple reallocation
|
||||||
|
const new_size = @max(self.buf.len + self.size, new_pos);
|
||||||
|
// resize the MsgBuffer to fit
|
||||||
|
self.buf = try alloc.realloc(self.buf, new_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the current input into MsgBuffer
|
||||||
|
@memcpy(self.buf[self.pos..new_pos], _input[0..]);
|
||||||
|
|
||||||
// set the new cursor position
|
// set the new cursor position
|
||||||
self.pos = new_pos;
|
self.pos = new_pos;
|
||||||
@@ -113,45 +120,47 @@ pub const Buffer = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test "Buffer" {
|
fn doTest(nb: *u8) void {
|
||||||
|
nb.* += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "MsgBuffer" {
|
||||||
const Case = struct {
|
const Case = struct {
|
||||||
input: []const u8,
|
input: []const u8,
|
||||||
nb: u8,
|
nb: u8,
|
||||||
};
|
};
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
const cases = [_]Case{
|
const cases = [_]Case{
|
||||||
// simple
|
// simple
|
||||||
.{ .input = .{ 2, 0 } ++ "ok", .nb = 1 },
|
.{ .input = "2:ok", .nb = 1 },
|
||||||
// combined
|
// combined
|
||||||
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 3, 0 } ++ "foo", .nb = 2 },
|
.{ .input = "2:ok3:foo7:bar2:ok", .nb = 3 }, // "bar2:ok" is a message, no need to escape "2:" here
|
||||||
// multipart
|
// multipart
|
||||||
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
|
.{ .input = "9:multi", .nb = 0 },
|
||||||
.{ .input = "part", .nb = 1 },
|
.{ .input = "part", .nb = 1 },
|
||||||
// multipart & combined
|
// multipart & combined
|
||||||
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
|
.{ .input = "9:multi", .nb = 0 },
|
||||||
.{ .input = "part" ++ .{ 2, 0 } ++ "ok", .nb = 2 },
|
.{ .input = "part2:ok", .nb = 2 },
|
||||||
// multipart & combined with other multipart
|
// multipart & combined with other multipart
|
||||||
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
|
.{ .input = "9:multi", .nb = 0 },
|
||||||
.{ .input = "part" ++ .{ 8, 0 } ++ "co", .nb = 1 },
|
.{ .input = "part8:co", .nb = 1 },
|
||||||
.{ .input = "mbined", .nb = 1 },
|
.{ .input = "mbined", .nb = 1 },
|
||||||
// several multipart
|
// several multipart
|
||||||
.{ .input = .{ 23, 0 } ++ "multi", .nb = 0 },
|
.{ .input = "23:multi", .nb = 0 },
|
||||||
.{ .input = "several", .nb = 0 },
|
.{ .input = "several", .nb = 0 },
|
||||||
.{ .input = "complex", .nb = 0 },
|
.{ .input = "complex", .nb = 0 },
|
||||||
.{ .input = "part", .nb = 1 },
|
.{ .input = "part", .nb = 1 },
|
||||||
// combined & multipart
|
// combined & multipart
|
||||||
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 9, 0 } ++ "multi", .nb = 1 },
|
.{ .input = "2:ok9:multi", .nb = 1 },
|
||||||
.{ .input = "part", .nb = 1 },
|
.{ .input = "part", .nb = 1 },
|
||||||
};
|
};
|
||||||
|
var msg_buf = try MsgBuffer.init(alloc, 10);
|
||||||
var b: [MaxSize]u8 = undefined;
|
defer msg_buf.deinit(alloc);
|
||||||
var buf = Buffer{ .buf = &b };
|
|
||||||
|
|
||||||
for (cases) |case| {
|
for (cases) |case| {
|
||||||
var nb: u8 = 0;
|
var nb: u8 = 0;
|
||||||
var input = case.input;
|
var input: []const u8 = case.input;
|
||||||
while (input.len > 0) {
|
while (input.len > 0) {
|
||||||
const parts = buf.read(input) catch |err| {
|
const parts = msg_buf.read(alloc, input) catch |err| {
|
||||||
if (err == error.MsgMultipart) break; // go to the next case input
|
if (err == error.MsgMultipart) break; // go to the next case input
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,671 +0,0 @@
|
|||||||
// fetch.js code comes from
|
|
||||||
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
|
|
||||||
//
|
|
||||||
// The original code source is available in MIT license.
|
|
||||||
//
|
|
||||||
// The script comes from the built version from npm.
|
|
||||||
// You can get the package with the command:
|
|
||||||
//
|
|
||||||
// wget $(npm view whatwg-fetch dist.tarball)
|
|
||||||
//
|
|
||||||
// The source is the content of `package/dist/fetch.umd.js` file.
|
|
||||||
(function (global, factory) {
|
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
||||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
||||||
(factory((global.WHATWGFetch = {})));
|
|
||||||
}(this, (function (exports) { 'use strict';
|
|
||||||
|
|
||||||
/* eslint-disable no-prototype-builtins */
|
|
||||||
var g =
|
|
||||||
(typeof globalThis !== 'undefined' && globalThis) ||
|
|
||||||
(typeof self !== 'undefined' && self) ||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
(typeof global !== 'undefined' && global) ||
|
|
||||||
{};
|
|
||||||
|
|
||||||
var support = {
|
|
||||||
searchParams: 'URLSearchParams' in g,
|
|
||||||
iterable: 'Symbol' in g && 'iterator' in Symbol,
|
|
||||||
blob:
|
|
||||||
'FileReader' in g &&
|
|
||||||
'Blob' in g &&
|
|
||||||
(function() {
|
|
||||||
try {
|
|
||||||
new Blob();
|
|
||||||
return true
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
formData: 'FormData' in g,
|
|
||||||
|
|
||||||
// Arraybuffer is available but xhr doesn't implement it for now.
|
|
||||||
// arrayBuffer: 'ArrayBuffer' in g
|
|
||||||
arrayBuffer: false
|
|
||||||
};
|
|
||||||
|
|
||||||
function isDataView(obj) {
|
|
||||||
return obj && DataView.prototype.isPrototypeOf(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (support.arrayBuffer) {
|
|
||||||
var viewClasses = [
|
|
||||||
'[object Int8Array]',
|
|
||||||
'[object Uint8Array]',
|
|
||||||
'[object Uint8ClampedArray]',
|
|
||||||
'[object Int16Array]',
|
|
||||||
'[object Uint16Array]',
|
|
||||||
'[object Int32Array]',
|
|
||||||
'[object Uint32Array]',
|
|
||||||
'[object Float32Array]',
|
|
||||||
'[object Float64Array]'
|
|
||||||
];
|
|
||||||
|
|
||||||
var isArrayBufferView =
|
|
||||||
ArrayBuffer.isView ||
|
|
||||||
function(obj) {
|
|
||||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeName(name) {
|
|
||||||
if (typeof name !== 'string') {
|
|
||||||
name = String(name);
|
|
||||||
}
|
|
||||||
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
|
|
||||||
throw new TypeError('Invalid character in header field name: "' + name + '"')
|
|
||||||
}
|
|
||||||
return name.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeValue(value) {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
value = String(value);
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a destructive iterator for the value list
|
|
||||||
function iteratorFor(items) {
|
|
||||||
var iterator = {
|
|
||||||
next: function() {
|
|
||||||
var value = items.shift();
|
|
||||||
return {done: value === undefined, value: value}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.iterable) {
|
|
||||||
iterator[Symbol.iterator] = function() {
|
|
||||||
return iterator
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
function Headers(headers) {
|
|
||||||
this.map = {};
|
|
||||||
|
|
||||||
if (headers instanceof Headers) {
|
|
||||||
headers.forEach(function(value, name) {
|
|
||||||
this.append(name, value);
|
|
||||||
}, this);
|
|
||||||
} else if (Array.isArray(headers)) {
|
|
||||||
headers.forEach(function(header) {
|
|
||||||
if (header.length != 2) {
|
|
||||||
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
|
|
||||||
}
|
|
||||||
this.append(header[0], header[1]);
|
|
||||||
}, this);
|
|
||||||
} else if (headers) {
|
|
||||||
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
|
||||||
this.append(name, headers[name]);
|
|
||||||
}, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Headers.prototype.append = function(name, value) {
|
|
||||||
name = normalizeName(name);
|
|
||||||
value = normalizeValue(value);
|
|
||||||
var oldValue = this.map[name];
|
|
||||||
this.map[name] = oldValue ? oldValue + ', ' + value : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype['delete'] = function(name) {
|
|
||||||
delete this.map[normalizeName(name)];
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.get = function(name) {
|
|
||||||
name = normalizeName(name);
|
|
||||||
return this.has(name) ? this.map[name] : null
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.has = function(name) {
|
|
||||||
return this.map.hasOwnProperty(normalizeName(name))
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.set = function(name, value) {
|
|
||||||
this.map[normalizeName(name)] = normalizeValue(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.forEach = function(callback, thisArg) {
|
|
||||||
for (var name in this.map) {
|
|
||||||
if (this.map.hasOwnProperty(name)) {
|
|
||||||
callback.call(thisArg, this.map[name], name, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.keys = function() {
|
|
||||||
var items = [];
|
|
||||||
this.forEach(function(value, name) {
|
|
||||||
items.push(name);
|
|
||||||
});
|
|
||||||
return iteratorFor(items)
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.values = function() {
|
|
||||||
var items = [];
|
|
||||||
this.forEach(function(value) {
|
|
||||||
items.push(value);
|
|
||||||
});
|
|
||||||
return iteratorFor(items)
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.entries = function() {
|
|
||||||
var items = [];
|
|
||||||
this.forEach(function(value, name) {
|
|
||||||
items.push([name, value]);
|
|
||||||
});
|
|
||||||
return iteratorFor(items)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.iterable) {
|
|
||||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumed(body) {
|
|
||||||
if (body._noBody) return
|
|
||||||
if (body.bodyUsed) {
|
|
||||||
return Promise.reject(new TypeError('Already read'))
|
|
||||||
}
|
|
||||||
body.bodyUsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileReaderReady(reader) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
reader.onload = function() {
|
|
||||||
resolve(reader.result);
|
|
||||||
};
|
|
||||||
reader.onerror = function() {
|
|
||||||
reject(reader.error);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBlobAsArrayBuffer(blob) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
var promise = fileReaderReady(reader);
|
|
||||||
reader.readAsArrayBuffer(blob);
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBlobAsText(blob) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
var promise = fileReaderReady(reader);
|
|
||||||
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
|
|
||||||
var encoding = match ? match[1] : 'utf-8';
|
|
||||||
reader.readAsText(blob, encoding);
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
function readArrayBufferAsText(buf) {
|
|
||||||
var view = new Uint8Array(buf);
|
|
||||||
var chars = new Array(view.length);
|
|
||||||
|
|
||||||
for (var i = 0; i < view.length; i++) {
|
|
||||||
chars[i] = String.fromCharCode(view[i]);
|
|
||||||
}
|
|
||||||
return chars.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function bufferClone(buf) {
|
|
||||||
if (buf.slice) {
|
|
||||||
return buf.slice(0)
|
|
||||||
} else {
|
|
||||||
var view = new Uint8Array(buf.byteLength);
|
|
||||||
view.set(new Uint8Array(buf));
|
|
||||||
return view.buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Body() {
|
|
||||||
this.bodyUsed = false;
|
|
||||||
|
|
||||||
this._initBody = function(body) {
|
|
||||||
/*
|
|
||||||
fetch-mock wraps the Response object in an ES6 Proxy to
|
|
||||||
provide useful test harness features such as flush. However, on
|
|
||||||
ES5 browsers without fetch or Proxy support pollyfills must be used;
|
|
||||||
the proxy-pollyfill is unable to proxy an attribute unless it exists
|
|
||||||
on the object before the Proxy is created. This change ensures
|
|
||||||
Response.bodyUsed exists on the instance, while maintaining the
|
|
||||||
semantic of setting Request.bodyUsed in the constructor before
|
|
||||||
_initBody is called.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-self-assign
|
|
||||||
this.bodyUsed = this.bodyUsed;
|
|
||||||
this._bodyInit = body;
|
|
||||||
if (!body) {
|
|
||||||
this._noBody = true;
|
|
||||||
this._bodyText = '';
|
|
||||||
} else if (typeof body === 'string') {
|
|
||||||
this._bodyText = body;
|
|
||||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
|
||||||
this._bodyBlob = body;
|
|
||||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
|
||||||
this._bodyFormData = body;
|
|
||||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
|
||||||
this._bodyText = body.toString();
|
|
||||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
|
||||||
this._bodyArrayBuffer = bufferClone(body.buffer);
|
|
||||||
// IE 10-11 can't handle a DataView body.
|
|
||||||
this._bodyInit = new Blob([this._bodyArrayBuffer]);
|
|
||||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
|
||||||
this._bodyArrayBuffer = bufferClone(body);
|
|
||||||
} else {
|
|
||||||
this._bodyText = body = Object.prototype.toString.call(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.headers.get('content-type')) {
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
this.headers.set('content-type', 'text/plain;charset=UTF-8');
|
|
||||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
|
||||||
this.headers.set('content-type', this._bodyBlob.type);
|
|
||||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
|
||||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.blob) {
|
|
||||||
this.blob = function() {
|
|
||||||
var rejected = consumed(this);
|
|
||||||
if (rejected) {
|
|
||||||
return rejected
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._bodyBlob) {
|
|
||||||
return Promise.resolve(this._bodyBlob)
|
|
||||||
} else if (this._bodyArrayBuffer) {
|
|
||||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
|
|
||||||
} else if (this._bodyFormData) {
|
|
||||||
throw new Error('could not read FormData body as blob')
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(new Blob([this._bodyText]))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.arrayBuffer = function() {
|
|
||||||
if (this._bodyArrayBuffer) {
|
|
||||||
var isConsumed = consumed(this);
|
|
||||||
if (isConsumed) {
|
|
||||||
return isConsumed
|
|
||||||
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
|
||||||
return Promise.resolve(
|
|
||||||
this._bodyArrayBuffer.buffer.slice(
|
|
||||||
this._bodyArrayBuffer.byteOffset,
|
|
||||||
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(this._bodyArrayBuffer)
|
|
||||||
}
|
|
||||||
} else if (support.blob) {
|
|
||||||
return this.blob().then(readBlobAsArrayBuffer)
|
|
||||||
} else {
|
|
||||||
throw new Error('could not read as ArrayBuffer')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.text = function() {
|
|
||||||
var rejected = consumed(this);
|
|
||||||
if (rejected) {
|
|
||||||
return rejected
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._bodyBlob) {
|
|
||||||
return readBlobAsText(this._bodyBlob)
|
|
||||||
} else if (this._bodyArrayBuffer) {
|
|
||||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
|
|
||||||
} else if (this._bodyFormData) {
|
|
||||||
throw new Error('could not read FormData body as text')
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(this._bodyText)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.formData) {
|
|
||||||
this.formData = function() {
|
|
||||||
return this.text().then(decode)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.json = function() {
|
|
||||||
return this.text().then(JSON.parse)
|
|
||||||
};
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP methods whose capitalization should be normalized
|
|
||||||
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
|
|
||||||
|
|
||||||
function normalizeMethod(method) {
|
|
||||||
var upcased = method.toUpperCase();
|
|
||||||
return methods.indexOf(upcased) > -1 ? upcased : method
|
|
||||||
}
|
|
||||||
|
|
||||||
function Request(input, options) {
|
|
||||||
if (!(this instanceof Request)) {
|
|
||||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
|
||||||
}
|
|
||||||
|
|
||||||
options = options || {};
|
|
||||||
var body = options.body;
|
|
||||||
|
|
||||||
if (input instanceof Request) {
|
|
||||||
if (input.bodyUsed) {
|
|
||||||
throw new TypeError('Already read')
|
|
||||||
}
|
|
||||||
this.url = input.url;
|
|
||||||
this.credentials = input.credentials;
|
|
||||||
if (!options.headers) {
|
|
||||||
this.headers = new Headers(input.headers);
|
|
||||||
}
|
|
||||||
this.method = input.method;
|
|
||||||
this.mode = input.mode;
|
|
||||||
this.signal = input.signal;
|
|
||||||
if (!body && input._bodyInit != null) {
|
|
||||||
body = input._bodyInit;
|
|
||||||
input.bodyUsed = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.url = String(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.credentials = options.credentials || this.credentials || 'same-origin';
|
|
||||||
if (options.headers || !this.headers) {
|
|
||||||
this.headers = new Headers(options.headers);
|
|
||||||
}
|
|
||||||
this.method = normalizeMethod(options.method || this.method || 'GET');
|
|
||||||
this.mode = options.mode || this.mode || null;
|
|
||||||
this.signal = options.signal || this.signal || (function () {
|
|
||||||
if ('AbortController' in g) {
|
|
||||||
var ctrl = new AbortController();
|
|
||||||
return ctrl.signal;
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
this.referrer = null;
|
|
||||||
|
|
||||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
|
||||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
|
||||||
}
|
|
||||||
this._initBody(body);
|
|
||||||
|
|
||||||
if (this.method === 'GET' || this.method === 'HEAD') {
|
|
||||||
if (options.cache === 'no-store' || options.cache === 'no-cache') {
|
|
||||||
// Search for a '_' parameter in the query string
|
|
||||||
var reParamSearch = /([?&])_=[^&]*/;
|
|
||||||
if (reParamSearch.test(this.url)) {
|
|
||||||
// If it already exists then set the value with the current time
|
|
||||||
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
|
|
||||||
} else {
|
|
||||||
// Otherwise add a new '_' parameter to the end with the current time
|
|
||||||
var reQueryString = /\?/;
|
|
||||||
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Request.prototype.clone = function() {
|
|
||||||
return new Request(this, {body: this._bodyInit})
|
|
||||||
};
|
|
||||||
|
|
||||||
function decode(body) {
|
|
||||||
var form = new FormData();
|
|
||||||
body
|
|
||||||
.trim()
|
|
||||||
.split('&')
|
|
||||||
.forEach(function(bytes) {
|
|
||||||
if (bytes) {
|
|
||||||
var split = bytes.split('=');
|
|
||||||
var name = split.shift().replace(/\+/g, ' ');
|
|
||||||
var value = split.join('=').replace(/\+/g, ' ');
|
|
||||||
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return form
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHeaders(rawHeaders) {
|
|
||||||
var headers = new Headers();
|
|
||||||
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
|
|
||||||
// https://tools.ietf.org/html/rfc7230#section-3.2
|
|
||||||
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
|
|
||||||
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
|
|
||||||
// https://github.com/github/fetch/issues/748
|
|
||||||
// https://github.com/zloirock/core-js/issues/751
|
|
||||||
preProcessedHeaders
|
|
||||||
.split('\r')
|
|
||||||
.map(function(header) {
|
|
||||||
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
|
|
||||||
})
|
|
||||||
.forEach(function(line) {
|
|
||||||
var parts = line.split(':');
|
|
||||||
var key = parts.shift().trim();
|
|
||||||
if (key) {
|
|
||||||
var value = parts.join(':').trim();
|
|
||||||
try {
|
|
||||||
headers.append(key, value);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Response ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
Body.call(Request.prototype);
|
|
||||||
|
|
||||||
function Response(bodyInit, options) {
|
|
||||||
if (!(this instanceof Response)) {
|
|
||||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
|
||||||
}
|
|
||||||
if (!options) {
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.type = 'default';
|
|
||||||
this.status = options.status === undefined ? 200 : options.status;
|
|
||||||
if (this.status < 200 || this.status > 599) {
|
|
||||||
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
|
|
||||||
}
|
|
||||||
this.ok = this.status >= 200 && this.status < 300;
|
|
||||||
this.statusText = options.statusText === undefined ? '' : '' + options.statusText;
|
|
||||||
this.headers = new Headers(options.headers);
|
|
||||||
this.url = options.url || '';
|
|
||||||
this._initBody(bodyInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
Body.call(Response.prototype);
|
|
||||||
|
|
||||||
Response.prototype.clone = function() {
|
|
||||||
return new Response(this._bodyInit, {
|
|
||||||
status: this.status,
|
|
||||||
statusText: this.statusText,
|
|
||||||
headers: new Headers(this.headers),
|
|
||||||
url: this.url
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
Response.error = function() {
|
|
||||||
var response = new Response(null, {status: 200, statusText: ''});
|
|
||||||
response.ok = false;
|
|
||||||
response.status = 0;
|
|
||||||
response.type = 'error';
|
|
||||||
return response
|
|
||||||
};
|
|
||||||
|
|
||||||
var redirectStatuses = [301, 302, 303, 307, 308];
|
|
||||||
|
|
||||||
Response.redirect = function(url, status) {
|
|
||||||
if (redirectStatuses.indexOf(status) === -1) {
|
|
||||||
throw new RangeError('Invalid status code')
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(null, {status: status, headers: {location: url}})
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.DOMException = g.DOMException;
|
|
||||||
try {
|
|
||||||
new exports.DOMException();
|
|
||||||
} catch (err) {
|
|
||||||
exports.DOMException = function(message, name) {
|
|
||||||
this.message = message;
|
|
||||||
this.name = name;
|
|
||||||
var error = Error(message);
|
|
||||||
this.stack = error.stack;
|
|
||||||
};
|
|
||||||
exports.DOMException.prototype = Object.create(Error.prototype);
|
|
||||||
exports.DOMException.prototype.constructor = exports.DOMException;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetch(input, init) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var request = new Request(input, init);
|
|
||||||
|
|
||||||
if (request.signal && request.signal.aborted) {
|
|
||||||
return reject(new exports.DOMException('Aborted', 'AbortError'))
|
|
||||||
}
|
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
function abortXhr() {
|
|
||||||
xhr.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.onload = function() {
|
|
||||||
var options = {
|
|
||||||
statusText: xhr.statusText,
|
|
||||||
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
|
|
||||||
};
|
|
||||||
// This check if specifically for when a user fetches a file locally from the file system
|
|
||||||
// Only if the status is out of a normal range
|
|
||||||
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
|
|
||||||
options.status = 200;
|
|
||||||
} else {
|
|
||||||
options.status = xhr.status;
|
|
||||||
}
|
|
||||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
|
|
||||||
var body = 'response' in xhr ? xhr.response : xhr.responseText;
|
|
||||||
setTimeout(function() {
|
|
||||||
resolve(new Response(body, options));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
reject(new TypeError('Network request failed'));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.ontimeout = function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
reject(new TypeError('Network request timed out'));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onabort = function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
reject(new exports.DOMException('Aborted', 'AbortError'));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
function fixUrl(url) {
|
|
||||||
try {
|
|
||||||
return url === '' && g.location.href ? g.location.href : url
|
|
||||||
} catch (e) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.open(request.method, fixUrl(request.url), true);
|
|
||||||
|
|
||||||
if (request.credentials === 'include') {
|
|
||||||
xhr.withCredentials = true;
|
|
||||||
} else if (request.credentials === 'omit') {
|
|
||||||
xhr.withCredentials = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('responseType' in xhr) {
|
|
||||||
if (support.blob) {
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
} else if (
|
|
||||||
support.arrayBuffer
|
|
||||||
) {
|
|
||||||
xhr.responseType = 'arraybuffer';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
|
||||||
var names = [];
|
|
||||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
|
||||||
names.push(normalizeName(name));
|
|
||||||
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
|
|
||||||
});
|
|
||||||
request.headers.forEach(function(value, name) {
|
|
||||||
if (names.indexOf(name) === -1) {
|
|
||||||
xhr.setRequestHeader(name, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
request.headers.forEach(function(value, name) {
|
|
||||||
xhr.setRequestHeader(name, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.signal) {
|
|
||||||
request.signal.addEventListener('abort', abortXhr);
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
// DONE (success or failure)
|
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
request.signal.removeEventListener('abort', abortXhr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch.polyfill = true;
|
|
||||||
|
|
||||||
if (!g.fetch) {
|
|
||||||
g.fetch = fetch;
|
|
||||||
g.Headers = Headers;
|
|
||||||
g.Request = Request;
|
|
||||||
g.Response = Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.Headers = Headers;
|
|
||||||
exports.Request = Request;
|
|
||||||
exports.Response = Response;
|
|
||||||
exports.fetch = fetch;
|
|
||||||
|
|
||||||
Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
|
|
||||||
})));
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const jsruntime = @import("jsruntime");
|
|
||||||
const Case = jsruntime.test_utils.Case;
|
|
||||||
const checkCases = jsruntime.test_utils.checkCases;
|
|
||||||
|
|
||||||
// fetch.js code comes from
|
|
||||||
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
|
|
||||||
//
|
|
||||||
// The original code source is available in MIT license.
|
|
||||||
//
|
|
||||||
// The script comes from the built version from npm.
|
|
||||||
// You can get the package with the command:
|
|
||||||
//
|
|
||||||
// wget $(npm view whatwg-fetch dist.tarball)
|
|
||||||
//
|
|
||||||
// The source is the content of `package/dist/fetch.umd.js` file.
|
|
||||||
pub const source = @embedFile("fetch.js");
|
|
||||||
|
|
||||||
pub fn testExecFn(
|
|
||||||
alloc: std.mem.Allocator,
|
|
||||||
js_env: *jsruntime.Env,
|
|
||||||
) anyerror!void {
|
|
||||||
try @import("polyfill.zig").load(alloc, js_env.*);
|
|
||||||
|
|
||||||
var fetch = [_]Case{
|
|
||||||
.{
|
|
||||||
.src =
|
|
||||||
\\var ok = false;
|
|
||||||
\\const request = new Request("https://httpbin.io/json");
|
|
||||||
\\fetch(request)
|
|
||||||
\\ .then((response) => { ok = response.ok; });
|
|
||||||
\\false;
|
|
||||||
,
|
|
||||||
.ex = "false",
|
|
||||||
},
|
|
||||||
// all events have been resolved.
|
|
||||||
.{ .src = "ok", .ex = "true" },
|
|
||||||
};
|
|
||||||
try checkCases(js_env, &fetch);
|
|
||||||
|
|
||||||
var fetch2 = [_]Case{
|
|
||||||
.{
|
|
||||||
.src =
|
|
||||||
\\var ok2 = false;
|
|
||||||
\\const request2 = new Request("https://httpbin.io/json");
|
|
||||||
\\(async function () { resp = await fetch(request2); ok2 = resp.ok; }());
|
|
||||||
\\false;
|
|
||||||
,
|
|
||||||
.ex = "false",
|
|
||||||
},
|
|
||||||
// all events have been resolved.
|
|
||||||
.{ .src = "ok2", .ex = "true" },
|
|
||||||
};
|
|
||||||
try checkCases(js_env, &fetch2);
|
|
||||||
}
|
|
||||||
@@ -1,56 +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 builtin = @import("builtin");
|
|
||||||
|
|
||||||
const jsruntime = @import("jsruntime");
|
|
||||||
const Env = jsruntime.Env;
|
|
||||||
|
|
||||||
const fetch = @import("fetch.zig").fetch_polyfill;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.polyfill);
|
|
||||||
|
|
||||||
const modules = [_]struct {
|
|
||||||
name: []const u8,
|
|
||||||
source: []const u8,
|
|
||||||
}{
|
|
||||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn load(alloc: std.mem.Allocator, env: Env) !void {
|
|
||||||
var try_catch: jsruntime.TryCatch = undefined;
|
|
||||||
try_catch.init(env);
|
|
||||||
defer try_catch.deinit();
|
|
||||||
|
|
||||||
for (modules) |m| {
|
|
||||||
const res = env.exec(m.source, m.name) catch {
|
|
||||||
if (try try_catch.err(alloc, env)) |msg| {
|
|
||||||
defer alloc.free(msg);
|
|
||||||
log.err("load {s}: {s}", .{ m.name, msg });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
|
||||||
const msg = try res.toString(alloc, env);
|
|
||||||
defer alloc.free(msg);
|
|
||||||
log.debug("load {s}: {s}", .{ m.name, msg });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -136,7 +136,6 @@ fn testsAllExecFn(
|
|||||||
URLTestExecFn,
|
URLTestExecFn,
|
||||||
HTMLElementTestExecFn,
|
HTMLElementTestExecFn,
|
||||||
MutationObserverTestExecFn,
|
MutationObserverTestExecFn,
|
||||||
@import("polyfill/fetch.zig").testExecFn,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline for (testFns) |testFn| {
|
inline for (testFns) |testFn| {
|
||||||
|
|||||||
108
src/server.zig
108
src/server.zig
@@ -19,8 +19,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const Stream = @import("handler.zig").Stream;
|
|
||||||
|
|
||||||
const jsruntime = @import("jsruntime");
|
const jsruntime = @import("jsruntime");
|
||||||
const Completion = jsruntime.IO.Completion;
|
const Completion = jsruntime.IO.Completion;
|
||||||
const AcceptError = jsruntime.IO.AcceptError;
|
const AcceptError = jsruntime.IO.AcceptError;
|
||||||
@@ -30,8 +28,7 @@ const CloseError = jsruntime.IO.CloseError;
|
|||||||
const CancelError = jsruntime.IO.CancelError;
|
const CancelError = jsruntime.IO.CancelError;
|
||||||
const TimeoutError = jsruntime.IO.TimeoutError;
|
const TimeoutError = jsruntime.IO.TimeoutError;
|
||||||
|
|
||||||
const MsgBuffer = @import("msg.zig").Buffer;
|
const MsgBuffer = @import("msg.zig").MsgBuffer;
|
||||||
const MaxSize = @import("msg.zig").MaxSize;
|
|
||||||
const Browser = @import("browser/browser.zig").Browser;
|
const Browser = @import("browser/browser.zig").Browser;
|
||||||
const cdp = @import("cdp/cdp.zig");
|
const cdp = @import("cdp/cdp.zig");
|
||||||
|
|
||||||
@@ -52,7 +49,6 @@ const MaxStdOutSize = 512; // ensure debug msg are not too long
|
|||||||
|
|
||||||
pub const Ctx = struct {
|
pub const Ctx = struct {
|
||||||
loop: *jsruntime.Loop,
|
loop: *jsruntime.Loop,
|
||||||
stream: ?*Stream,
|
|
||||||
|
|
||||||
// internal fields
|
// internal fields
|
||||||
accept_socket: std.posix.socket_t,
|
accept_socket: std.posix.socket_t,
|
||||||
@@ -121,8 +117,8 @@ pub const Ctx = struct {
|
|||||||
std.debug.assert(completion == self.conn_completion);
|
std.debug.assert(completion == self.conn_completion);
|
||||||
|
|
||||||
const size = result catch |err| {
|
const size = result catch |err| {
|
||||||
if (self.isClosed() and err == error.FileDescriptorInvalid) {
|
if (err == error.Canceled) {
|
||||||
log.debug("read has been canceled", .{});
|
log.debug("read canceled", .{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.err("read error: {any}", .{err});
|
log.err("read error: {any}", .{err});
|
||||||
@@ -162,7 +158,7 @@ pub const Ctx = struct {
|
|||||||
// read and execute input
|
// read and execute input
|
||||||
var input: []const u8 = self.read_buf[0..size];
|
var input: []const u8 = self.read_buf[0..size];
|
||||||
while (input.len > 0) {
|
while (input.len > 0) {
|
||||||
const parts = self.msg_buf.read(input) catch |err| {
|
const parts = self.msg_buf.read(self.alloc(), input) catch |err| {
|
||||||
if (err == error.MsgMultipart) {
|
if (err == error.MsgMultipart) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -203,7 +199,7 @@ pub const Ctx = struct {
|
|||||||
if (now.since(self.last_active.?) > self.timeout) {
|
if (now.since(self.last_active.?) > self.timeout) {
|
||||||
// close current connection
|
// close current connection
|
||||||
log.debug("conn timeout, closing...", .{});
|
log.debug("conn timeout, closing...", .{});
|
||||||
self.close();
|
self.cancelAndClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +213,19 @@ pub const Ctx = struct {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cancelCbk(self: *Ctx, completion: *Completion, result: CancelError!void) void {
|
||||||
|
std.debug.assert(completion == self.accept_completion);
|
||||||
|
|
||||||
|
_ = result catch |err| {
|
||||||
|
log.err("cancel error: {any}", .{err});
|
||||||
|
self.err = err;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log.debug("cancel done", .{});
|
||||||
|
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
// ---------
|
// ---------
|
||||||
|
|
||||||
@@ -253,7 +262,7 @@ pub const Ctx = struct {
|
|||||||
if (std.mem.eql(u8, cmd, "close")) {
|
if (std.mem.eql(u8, cmd, "close")) {
|
||||||
// close connection
|
// close connection
|
||||||
log.info("close cmd, closing conn...", .{});
|
log.info("close cmd, closing conn...", .{});
|
||||||
self.close();
|
self.cancelAndClose();
|
||||||
return error.Closed;
|
return error.Closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,27 +283,30 @@ pub const Ctx = struct {
|
|||||||
|
|
||||||
// send result
|
// send result
|
||||||
if (!std.mem.eql(u8, res, "")) {
|
if (!std.mem.eql(u8, res, "")) {
|
||||||
return self.send(res);
|
return sendAsync(self, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(self: *Ctx, msg: []const u8) !void {
|
fn cancelAndClose(self: *Ctx) void {
|
||||||
if (self.stream) |stream| {
|
if (isLinux) { // cancel is only available on Linux
|
||||||
// if we have a stream connection, just write on it
|
self.loop.io.cancel(
|
||||||
defer self.alloc().free(msg);
|
*Ctx,
|
||||||
try stream.send(msg);
|
self,
|
||||||
|
Ctx.cancelCbk,
|
||||||
|
self.accept_completion,
|
||||||
|
self.conn_completion,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// otherwise write asynchronously on the socket connection
|
self.close();
|
||||||
return sendAsync(self, msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(self: *Ctx) void {
|
fn close(self: *Ctx) void {
|
||||||
|
std.posix.close(self.conn_socket);
|
||||||
|
|
||||||
// conn is closed
|
// conn is closed
|
||||||
self.last_active = null;
|
|
||||||
std.posix.close(self.conn_socket);
|
|
||||||
log.debug("connection closed", .{});
|
log.debug("connection closed", .{});
|
||||||
|
self.last_active = null;
|
||||||
|
|
||||||
// restart a new browser session in case of re-connect
|
// restart a new browser session in case of re-connect
|
||||||
if (!self.sessionNew) {
|
if (!self.sessionNew) {
|
||||||
@@ -350,7 +362,7 @@ pub const Ctx = struct {
|
|||||||
.{ msg_open, cdp.ContextSessionID },
|
.{ msg_open, cdp.ContextSessionID },
|
||||||
);
|
);
|
||||||
|
|
||||||
try ctx.send(s);
|
try sendAsync(ctx, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
|
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
|
||||||
@@ -410,17 +422,16 @@ const Send = struct {
|
|||||||
|
|
||||||
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
|
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
|
||||||
const sd = try Send.init(ctx, msg);
|
const sd = try Send.init(ctx, msg);
|
||||||
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, sd.msg);
|
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listener and handler
|
// Listen
|
||||||
// --------------------
|
// ------
|
||||||
|
|
||||||
pub fn handle(
|
pub fn listen(
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
loop: *jsruntime.Loop,
|
loop: *jsruntime.Loop,
|
||||||
server_socket: std.posix.socket_t,
|
server_socket: std.posix.socket_t,
|
||||||
stream: ?*Stream,
|
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
) anyerror!void {
|
) anyerror!void {
|
||||||
|
|
||||||
@@ -435,8 +446,8 @@ pub fn handle(
|
|||||||
|
|
||||||
// create buffers
|
// create buffers
|
||||||
var read_buf: [BufReadSize]u8 = undefined;
|
var read_buf: [BufReadSize]u8 = undefined;
|
||||||
var buf: [MaxSize]u8 = undefined;
|
var msg_buf = try MsgBuffer.init(loop.alloc, BufReadSize * 256); // 256KB
|
||||||
var msg_buf = MsgBuffer{ .buf = &buf };
|
defer msg_buf.deinit(loop.alloc);
|
||||||
|
|
||||||
// create I/O completions
|
// create I/O completions
|
||||||
var accept_completion: Completion = undefined;
|
var accept_completion: Completion = undefined;
|
||||||
@@ -447,7 +458,6 @@ pub fn handle(
|
|||||||
// for accepting connections and receving messages
|
// for accepting connections and receving messages
|
||||||
var ctx = Ctx{
|
var ctx = Ctx{
|
||||||
.loop = loop,
|
.loop = loop,
|
||||||
.stream = stream,
|
|
||||||
.browser = &browser,
|
.browser = &browser,
|
||||||
.sessionNew = true,
|
.sessionNew = true,
|
||||||
.read_buf = &read_buf,
|
.read_buf = &read_buf,
|
||||||
@@ -487,43 +497,3 @@ pub fn handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setSockOpt(fd: std.posix.socket_t, level: i32, option: u32, value: c_int) !void {
|
|
||||||
try std.posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isUnixSocket(addr: std.net.Address) bool {
|
|
||||||
return addr.any.family == std.posix.AF.UNIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn listen(address: std.net.Address) !std.posix.socket_t {
|
|
||||||
|
|
||||||
// create socket
|
|
||||||
const flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC | std.posix.SOCK.NONBLOCK;
|
|
||||||
const proto = if (isUnixSocket(address)) @as(u32, 0) else std.posix.IPPROTO.TCP;
|
|
||||||
const sockfd = try std.posix.socket(address.any.family, flags, proto);
|
|
||||||
errdefer std.posix.close(sockfd);
|
|
||||||
|
|
||||||
// socket options
|
|
||||||
if (@hasDecl(std.posix.SO, "REUSEPORT")) {
|
|
||||||
try setSockOpt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEPORT, 1);
|
|
||||||
} else {
|
|
||||||
try setSockOpt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, 1);
|
|
||||||
}
|
|
||||||
if (!isUnixSocket(address)) {
|
|
||||||
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
|
|
||||||
// WARNING: disable Nagle's alogrithm to avoid latency issues
|
|
||||||
try setSockOpt(sockfd, std.posix.IPPROTO.TCP, std.posix.TCP.NODELAY, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bind & listen
|
|
||||||
var socklen = address.getOsSockLen();
|
|
||||||
try std.posix.bind(sockfd, &address.any, socklen);
|
|
||||||
const kernel_backlog = 1; // default value is 128. Here we just want 1 connection
|
|
||||||
try std.posix.listen(sockfd, kernel_backlog);
|
|
||||||
var listen_address: std.net.Address = undefined;
|
|
||||||
try std.posix.getsockname(sockfd, &listen_address.any, &socklen);
|
|
||||||
|
|
||||||
return sockfd;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ const Client = @import("asyncio").Client;
|
|||||||
const Types = @import("../main_wpt.zig").Types;
|
const Types = @import("../main_wpt.zig").Types;
|
||||||
const UserContext = @import("../main_wpt.zig").UserContext;
|
const UserContext = @import("../main_wpt.zig").UserContext;
|
||||||
|
|
||||||
const polyfill = @import("../polyfill/polyfill.zig");
|
|
||||||
|
|
||||||
// runWPT parses the given HTML file, starts a js env and run the first script
|
// runWPT parses the given HTML file, starts a js env and run the first script
|
||||||
// tags containing javascript sources.
|
// tags containing javascript sources.
|
||||||
// It loads first the js libs files.
|
// It loads first the js libs files.
|
||||||
@@ -76,9 +74,6 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
|||||||
try js_env.start();
|
try js_env.start();
|
||||||
defer js_env.stop();
|
defer js_env.stop();
|
||||||
|
|
||||||
// load polyfills
|
|
||||||
try polyfill.load(alloc, js_env);
|
|
||||||
|
|
||||||
// display console logs
|
// display console logs
|
||||||
defer {
|
defer {
|
||||||
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
|
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
|
||||||
|
|||||||
@@ -756,10 +756,8 @@ pub const XMLHttpRequest = struct {
|
|||||||
// https://xhr.spec.whatwg.org/#the-response-attribute
|
// https://xhr.spec.whatwg.org/#the-response-attribute
|
||||||
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
|
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
|
||||||
if (self.response_type == .Empty or self.response_type == .Text) {
|
if (self.response_type == .Empty or self.response_type == .Text) {
|
||||||
if (self.state == LOADING or self.state == DONE) {
|
if (self.state == LOADING or self.state == DONE) return .{ .Text = "" };
|
||||||
return .{ .Text = try self.get_responseText() };
|
return .{ .Text = try self.get_responseText() };
|
||||||
}
|
|
||||||
return .{ .Text = "" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fastpath if response is previously parsed.
|
// fastpath if response is previously parsed.
|
||||||
@@ -776,7 +774,6 @@ pub const XMLHttpRequest = struct {
|
|||||||
// response object to a new ArrayBuffer object representing this’s
|
// response object to a new ArrayBuffer object representing this’s
|
||||||
// received bytes. If this throws an exception, then set this’s
|
// received bytes. If this throws an exception, then set this’s
|
||||||
// response object to failure and return null.
|
// response object to failure and return null.
|
||||||
log.err("response type ArrayBuffer not implemented", .{});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,7 +782,6 @@ pub const XMLHttpRequest = struct {
|
|||||||
// response object to a new Blob object representing this’s
|
// response object to a new Blob object representing this’s
|
||||||
// received bytes with type set to the result of get a final MIME
|
// received bytes with type set to the result of get a final MIME
|
||||||
// type for this.
|
// type for this.
|
||||||
log.err("response type Blob not implemented", .{});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +944,7 @@ pub fn testExecFn(
|
|||||||
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
|
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
|
||||||
.{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
|
.{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
|
||||||
.{ .src = "req.responseText.length > 64", .ex = "true" },
|
.{ .src = "req.responseText.length > 64", .ex = "true" },
|
||||||
.{ .src = "req.response.length == req.responseText.length", .ex = "true" },
|
.{ .src = "req.response", .ex = "" },
|
||||||
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
|
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
|
||||||
};
|
};
|
||||||
try checkCases(js_env, &send);
|
try checkCases(js_env, &send);
|
||||||
|
|||||||
1
vendor/websocket.zig
vendored
1
vendor/websocket.zig
vendored
Submodule vendor/websocket.zig deleted from 1b49626c78
2
vendor/zig-async-io
vendored
2
vendor/zig-async-io
vendored
Submodule vendor/zig-async-io updated: 570f436c72...ed7ae07d1c
Reference in New Issue
Block a user