diff --git a/.github/workflows/e2e-integration-test.yml b/.github/workflows/e2e-integration-test.yml index 8999ce2ee..5a52a23cd 100644 --- a/.github/workflows/e2e-integration-test.yml +++ b/.github/workflows/e2e-integration-test.yml @@ -69,7 +69,7 @@ jobs: - name: run end to end integration tests continue-on-error: true run: | - ./lightpanda serve --experimental-features cors --http-cache-dir /tmp/lp-cache --http-proxy ${{ secrets.MASSIVE_PROXY_RESIDENTIAL_US }} --log-level error & echo $! > LPD.pid + ./lightpanda serve --http-cache-dir /tmp/lp-cache --http-proxy ${{ secrets.MASSIVE_PROXY_RESIDENTIAL_US }} --log-level error & echo $! > LPD.pid go run integration/main.go |tee result.log kill `cat LPD.pid` diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index f0c3f1810..9c06217a1 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -117,7 +117,7 @@ jobs: echo "value=$args" >> "$GITHUB_OUTPUT" - run: | - ./lightpanda serve --experimental-features cors --port 9222 ${{ steps.args.outputs.value }} & + ./lightpanda serve --port 9222 ${{ steps.args.outputs.value }} & - run: | go run runner/main.go @@ -163,7 +163,7 @@ jobs: - id: args name: build LP args run: | - args="--experimental-features cors --http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe" + args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe" args="$args --protocol cdp --protocol webdriver" [ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots" [ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem" @@ -227,7 +227,6 @@ jobs: exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}" ./lightpanda fetch --dump http://127.0.0.1:8989/ \ - --experimental-features cors \ --web-bot-auth-key-file /proc/self/fd/3 \ --web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \ --web-bot-auth-domain ${{ vars.WBA_DOMAIN }} @@ -251,7 +250,6 @@ jobs: exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}" ./lightpanda fetch --dump "http://127.0.0.1:8989/redirect?to=http://127.0.0.1:8990/" \ - --experimental-features cors \ --web-bot-auth-key-file /proc/self/fd/3 \ --web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \ --web-bot-auth-domain ${{ vars.WBA_DOMAIN }} @@ -430,7 +428,7 @@ jobs: - run: chmod a+x ./lightpanda - - run: ./lightpanda fetch --experimental-features cors "https://demo-browser.lightpanda.io/campfire-commerce/" + - run: ./lightpanda fetch "https://demo-browser.lightpanda.io/campfire-commerce/" agent-deterministic: name: agent deterministic replay @@ -483,7 +481,7 @@ jobs: jq --version - timeout 30 ./lightpanda mcp --experimental-features cors > mcp.out <<'JSONRPC' + timeout 30 ./lightpanda mcp > mcp.out <<'JSONRPC' {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1.0.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized"} {"jsonrpc":"2.0","id":2,"method":"tools/list"} diff --git a/README.md b/README.md index b315d50db..67309cdbe 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ Set `LIGHTPANDA_DISABLE_CORE_DUMP` (to any value) to suppress crash core dumps b Here are the key features we have implemented. For full details, see our [Web Platform Tests results](https://perf.lightpanda.io/wpt). -- [x] CORS (enable with `--experimental-features cors`) +- [x] CORS - [x] HTTP loader ([Libcurl](https://curl.se/libcurl/)) - [x] HTML parser ([html5ever](https://github.com/servo/html5ever)) - [x] DOM tree @@ -421,8 +421,7 @@ zig build -Dwpt_extensions run -- serve \ --load-resources iframe \ --load-resources image \ --load-resources worker \ - --load-resources stylesheet \ - --experimental-features cors + --load-resources stylesheet ``` Then you can start the wptrunner from the demo's clone dir: diff --git a/src/Config.zig b/src/Config.zig index e6b16d37e..0bb03abdb 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -239,10 +239,14 @@ pub const LoadResources = packed struct(u4) { }; pub const ExperimentalFeatures = packed struct(u2) { - cors: bool = false, + cors: bool = false, // ignored, kept only for backward compatibility. serviceworker: bool = false, }; +pub const DisabledFeatures = packed struct(u1) { + cors: bool = false, +}; + /// Common CLI args. const CommonOptions = .{ .{ .name = "obey_robots", .type = bool }, @@ -283,6 +287,7 @@ const CommonOptions = .{ .{ .name = "disable_subframes", .type = bool, .deprecated = "subframes are now disabled by default, use \"--load-resources iframe\" to enable" }, .{ .name = "disable_workers", .type = bool, .deprecated = "workers are now disabled by default, use \"--load-resources worker\" to enable" }, .{ .name = "enable_external_stylesheets", .type = bool, .deprecated = "use \"--load-resources stylesheet\" to enable" }, + .{ .name = "disable_features", .type = DisabledFeatures, .default = DisabledFeatures{} }, .{ .name = "experimental_features", .type = ExperimentalFeatures, .default = ExperimentalFeatures{} }, .{ .name = "load_resources", .type = LoadResources, .default = LoadResources{} }, .{ .name = "v8_flags_unsafe", .type = ?[]const u8 }, @@ -552,6 +557,13 @@ pub fn tlsVerifyHost(self: *const Config) bool { }; } +pub fn obeyCors(self: *const Config) bool { + return switch (self.mode) { + inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_features.cors == false, + else => unreachable, + }; +} + pub fn obeyRobots(self: *const Config) bool { return switch (self.mode) { inline .serve, .fetch, .mcp, .agent => |opts| opts.obey_robots, diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index edd99d786..c24748d6b 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -844,6 +844,10 @@ test "Env: Worker context " { const frame = try testing.createFrame(); defer testing.test_session.closeAllPages(); + // Navigate the frame first to avoid CORS blocking request for the worker. + try frame.navigate("http://localhost:9582/", .{}); + try testing.waitForFrame(); + const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", null, frame); var ls: js.Local.Scope = undefined; diff --git a/src/cli.zig b/src/cli.zig index 881644427..47d0414c2 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -766,6 +766,8 @@ pub fn Builder(comptime commands: anytype) type { break :blk command.options; }; + // toKebabCase walks every byte of every name. + @setEvalBranchQuota(50_000); iter_args: while (args.next()) |option_name| { inline for (options) |option| { const name = option.name; diff --git a/src/help.zon b/src/help.zon index 91f19b734..d00ecca73 100644 --- a/src/help.zon +++ b/src/help.zon @@ -409,14 +409,15 @@ \\ --cors-store-entry-limit \\ Maximum number of entries kept in the CorsStore. 0 means no limit. \\ Defaults to 1000. + \\ --disable-features + \\ Disable a feature that is on by default. Can be passed multiple times. + \\ Defaults to none disabled. + \\ Allowed values: + \\ cors Skip CORS checks on fetch/XHR requests. \\ --experimental-features \\ Enable an experimental, unstable feature. Can be passed multiple times. \\ Behavior may change or be removed without notice. \\ Defaults to none enabled. - \\ Allowed values: - \\ cors Obey CORS (cross-origin resource sharing) checks - \\ on fetch/XHR requests instead of allowing them - \\ unconditionally. \\ --load-resources \\ Sub-resource to actually request. Can be passed multiple times. \\ Defaults to requesting none of them. diff --git a/src/network/HttpClient.zig b/src/network/HttpClient.zig index decf406a4..0ba5cd4ae 100644 --- a/src/network/HttpClient.zig +++ b/src/network/HttpClient.zig @@ -244,7 +244,7 @@ pub fn init(self: *Client, app: *lp.App) !void { .serve_mode = config.mode == .serve, .obey_robots = config.obeyRobots(), .http_version = config.httpVersion(), - .obey_cors = config.experimentalFeatures().cors, + .obey_cors = config.obeyCors(), .robots = .{ .network = network, .single_flight = .init(allocator), diff --git a/src/testing.zig b/src/testing.zig index 72619afd3..604bb4935 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -635,9 +635,28 @@ var serve_counts = [_]struct { name: []const u8, count: u32 = 0 }{ .{ .name = "prescan_module" }, }; +fn origin(req: *std.http.Server.Request) ?[]const u8 { + var it = req.iterateHeaders(); + while (it.next()) |h| { + if (std.mem.eql(u8, "origin", h.name)) { + return h.value; + } + } + + return null; +} + fn testHTTPHandler(req: *std.http.Server.Request) !void { const path = req.head.target; + if (std.mem.eql(u8, path, "/")) { + return req.respond("", .{ + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "text/html; charset=utf-8" }, + }, + }); + } + if (std.mem.eql(u8, path, "/xhr")) { return req.respond("1234567890" ** 10, .{ .extra_headers = &.{ @@ -1155,6 +1174,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { return req.respond(html, .{ .extra_headers = &.{ .{ .name = "Content-Type", .value = "text/html; charset=utf-8" }, + .{ .name = "Access-Control-Allow-Origin", .value = origin(req) orelse "*" }, }, }); } @@ -1201,6 +1221,16 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { } if (std.mem.eql(u8, path, "/echo_headers")) { + if (req.head.method == .OPTIONS) { + return req.respond("", .{ + .extra_headers = &.{ + .{ .name = "Access-Control-Allow-Origin", .value = origin(req) orelse "*" }, + .{ .name = "Access-Control-Allow-Methods", .value = "GET" }, + .{ .name = "Access-Control-Allow-Headers", .value = "x-hop" }, + }, + }); + } + // Echo every request header back as "name: value" lines, so tests // can assert on the headers a request actually sent. var buf: [8192]u8 = undefined; @@ -1213,6 +1243,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { return req.respond(buf[0..pos], .{ .extra_headers = &.{ .{ .name = "Content-Type", .value = "text/plain; charset=utf-8" }, + .{ .name = "Access-Control-Allow-Origin", .value = origin(req) orelse "*" }, }, }); }