// 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 App = @import("app.zig").App; const Server = @import("server.zig").Server; 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"); var _app: ?*App = null; var _server: ?Server = null; 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); }; } // Handle app shutdown gracefuly on signals. fn shutdown() void { const sigaction: std.posix.Sigaction = .{ .handler = .{ .handler = struct { pub fn handler(_: c_int) callconv(.c) void { // Shutdown service gracefuly. if (_server) |server| { server.deinit(); } if (_app) |app| { app.deinit(); } std.posix.exit(0); } }.handler, }, .mask = std.posix.empty_sigset, .flags = 0, }; // Exit the program on SIGINT signal. When running the browser in a Docker // container, sending a CTRL-C (SIGINT) signal is catched but doesn't exit // the program. Here we force exiting on SIGINT. std.posix.sigaction(std.posix.SIG.INT, &sigaction, null); std.posix.sigaction(std.posix.SIG.TERM, &sigaction, null); std.posix.sigaction(std.posix.SIG.QUIT, &sigaction, null); } 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(); // _app is global to handle graceful shutdown. _app = try App.init(alloc, .{ .run_mode = args.mode, .platform = &platform, .http_proxy = args.httpProxy(), .proxy_bearer_token = args.proxyBearerToken(), .tls_verify_host = args.tlsVerifyHost(), .http_timeout_ms = args.httpTimeout(), .http_connect_timeout_ms = args.httpConnectTiemout(), .http_max_host_open = args.httpMaxHostOpen(), .http_max_concurrent = args.httpMaxConcurrent(), }); const app = _app.?; 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); }; // _server is global to handle graceful shutdown. _server = try Server.init(app, address); const server = &_server.?; defer server.deinit(); server.run(address, opts.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; }, }; session.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) ?[:0]const u8 { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.http_proxy, else => unreachable, }; } fn proxyBearerToken(self: *const Command) ?[:0]const u8 { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.proxy_bearer_token, else => unreachable, }; } fn httpMaxConcurrent(self: *const Command) ?u8 { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.http_max_concurrent, else => unreachable, }; } fn httpMaxHostOpen(self: *const Command) ?u8 { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.http_max_host_open, else => unreachable, }; } fn httpConnectTiemout(self: *const Command) ?u31 { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.http_connect_timeout, else => unreachable, }; } fn httpTimeout(self: *const Command) ?u31 { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.http_timeout, 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 { proxy_bearer_token: ?[:0]const u8 = null, http_proxy: ?[:0]const u8 = null, http_max_concurrent: ?u8 = null, http_max_host_open: ?u8 = null, http_timeout: ?u31 = null, http_connect_timeout: ?u31 = 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 { // MAX_HELP_LEN| 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. \\ A username:password can be included for basic authentication. \\ Defaults to none. \\ \\--proxy_bearer_token \\ The to send for bearer authentication with the proxy \\ Proxy-Authorization: Bearer \\ \\--http_max_concurrent \\ The maximum number of concurrent HTTP requests. \\ Defaults to 10. \\ \\--http_max_host_open \\ The maximum number of open connection to a given host:port. \\ Defaults to 4. \\ \\--http_connect_timeout \\ The time, in milliseconds, for establishing an HTTP connection \\ before timing out. 0 means it never times out. \\ Defaults to 0. \\ \\--http_timeout \\ The maximum time, in milliseconds, the transfer is allowed \\ to complete. 0 means it never times out. \\ Defaults to 10000. \\ \\--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.") ++ \\ ; // MAX_HELP_LEN| 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