// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // 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 . const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const log = @import("log.zig"); const server = @import("server.zig"); const App = @import("app.zig").App; const Http = @import("http/Http.zig"); const Platform = @import("runtime/js.zig").Platform; const Browser = @import("browser/browser.zig").Browser; const build_config = @import("build_config"); const parser = @import("browser/netsurf.zig"); pub fn main() !void { // allocator // - in Debug mode we use the General Purpose Allocator to detect memory leaks // - in Release mode we use the c allocator var gpa: std.heap.DebugAllocator(.{}) = .init; const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; defer if (builtin.mode == .Debug) { if (gpa.detectLeaks()) std.posix.exit(1); }; run(alloc) catch |err| { // If explicit filters were set, they won't be valid anymore because // the args_arena is gone. We need to set it to something that's not // invalid. (We should just move the args_arena up to main) log.opts.filter_scopes = &.{}; log.fatal(.app, "exit", .{ .err = err }); std.posix.exit(1); }; } fn run(alloc: Allocator) !void { var args_arena = std.heap.ArenaAllocator.init(alloc); defer args_arena.deinit(); const args = try parseArgs(args_arena.allocator()); switch (args.mode) { .help => { args.printUsageAndExit(args.mode.help); return std.process.cleanExit(); }, .version => { std.debug.print("{s}\n", .{build_config.git_commit}); return std.process.cleanExit(); }, else => {}, } if (args.logLevel()) |ll| { log.opts.level = ll; } if (args.logFormat()) |lf| { log.opts.format = lf; } if (args.logFilterScopes()) |lfs| { log.opts.filter_scopes = lfs; } const platform = try Platform.init(); defer platform.deinit(); var app = try App.init(alloc, .{ .run_mode = args.mode, .platform = &platform, .http_proxy = args.httpProxy(), .proxy_type = args.proxyType(), .proxy_auth = args.proxyAuth(), .tls_verify_host = args.tlsVerifyHost(), }); defer app.deinit(); app.telemetry.record(.{ .run = {} }); switch (args.mode) { .serve => |opts| { log.debug(.app, "startup", .{ .mode = "serve" }); const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); return args.printUsageAndExit(false); }; const timeout = std.time.ns_per_s * @as(u64, opts.timeout); server.run(app, address, timeout) catch |err| { log.fatal(.app, "server run error", .{ .err = err }); return err; }; }, .fetch => |opts| { const url = opts.url; log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = url }); // browser var browser = try Browser.init(app); defer browser.deinit(); var session = try browser.newSession(); // page const page = try session.createPage(); _ = page.navigate(url, .{}) catch |err| switch (err) { error.UnsupportedUriScheme, error.UriMissingHost => { log.fatal(.app, "invalid fetch URL", .{ .err = err, .url = url }); return args.printUsageAndExit(false); }, else => { log.fatal(.app, "fetch error", .{ .err = err, .url = url }); return err; }, }; page.wait(5); // 5 seconds // dump if (opts.dump) { try page.dump(.{ .page = page, .with_base = opts.withbase, .exclude_scripts = opts.noscript, }, std.io.getStdOut()); } }, else => unreachable, } } const Command = struct { mode: Mode, exec_name: []const u8, fn tlsVerifyHost(self: *const Command) bool { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.tls_verify_host, else => unreachable, }; } fn httpProxy(self: *const Command) ?std.Uri { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.http_proxy, else => unreachable, }; } fn proxyType(self: *const Command) ?Http.ProxyType { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.proxy_type, else => unreachable, }; } fn proxyAuth(self: *const Command) ?Http.ProxyAuth { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.proxy_auth, else => unreachable, }; } fn logLevel(self: *const Command) ?log.Level { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.log_level, else => unreachable, }; } fn logFormat(self: *const Command) ?log.Format { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.log_format, else => unreachable, }; } fn logFilterScopes(self: *const Command) ?[]const log.Scope { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.log_filter_scopes, else => unreachable, }; } const Mode = union(App.RunMode) { help: bool, // false when being printed because of an error fetch: Fetch, serve: Serve, version: void, }; const Serve = struct { host: []const u8, port: u16, timeout: u16, common: Common, }; const Fetch = struct { url: []const u8, dump: bool = false, common: Common, noscript: bool = false, withbase: bool = false, }; const Common = struct { http_proxy: ?std.Uri = null, proxy_type: ?Http.ProxyType = null, proxy_auth: ?Http.ProxyAuth = null, tls_verify_host: bool = true, log_level: ?log.Level = null, log_format: ?log.Format = null, log_filter_scopes: ?[]log.Scope = null, }; fn printUsageAndExit(self: *const Command, success: bool) void { const common_options = \\ \\--insecure_disable_tls_host_verification \\ Disables host verification on all HTTP requests. \\ This is an advanced option which should only be \\ set if you understand and accept the risk of \\ disabling host verification. \\ \\--http_proxy The HTTP proxy to use for all HTTP requests. \\ Defaults to none. \\ \\--proxy_type The type of proxy: connect, forward. \\ 'connect' creates a tunnel through the proxy via \\ and initial CONNECT request. \\ 'forward' sends the full URL in the request target \\ and expects the proxy to MITM the request. \\ Defaults to connect when --http_proxy is set. \\ \\--proxy_bearer_token \\ The token to send for bearer authentication with the proxy \\ Proxy-Authorization: Bearer \\ \\--proxy_basic_auth \\ The user:password to send for basic authentication with the proxy \\ Proxy-Authorization: Basic \\ \\--log_level The log level: debug, info, warn, error or fatal. \\ Defaults to ++ (if (builtin.mode == .Debug) " info." else "warn.") ++ \\ \\ \\--log_format The log format: pretty or logfmt. \\ Defaults to ++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++ \\ \\ ; const usage = \\usage: {s} command [options] [URL] \\ \\Command can be either 'fetch', 'serve' or 'help' \\ \\fetch command \\Fetches the specified URL \\Example: {s} fetch --dump https://lightpanda.io/ \\ \\Options: \\--dump Dumps document to stdout. \\ Defaults to false. \\--noscript Exclude