From 077f6acf478eefaee059c928f07fe12fd9c67f74 Mon Sep 17 00:00:00 2001 From: daijro Date: Thu, 8 Aug 2024 04:32:54 -0500 Subject: [PATCH] Launcher: Add --addon option to CLI --- launcher/addon.go | 137 ++++++++++++++++++++++++++++++++++++++++++ launcher/main.go | 69 ++++++++++----------- launcher/runner.go | 33 +++++++++- settings/camoufox.cfg | 15 ++++- 4 files changed, 217 insertions(+), 37 deletions(-) create mode 100644 launcher/addon.go diff --git a/launcher/addon.go b/launcher/addon.go new file mode 100644 index 0000000..9692750 --- /dev/null +++ b/launcher/addon.go @@ -0,0 +1,137 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "strconv" + "strings" +) + +func getDebugPort(args *[]string) int { + debugPort := parseArgs("-start-debugger-server", "", args, false) + + var debugPortInt int + var err error + if debugPort == "" { + // Create new debugger server port + debugPortInt = getOpenPort() + // Add -start-debugger-server {debugPort} to args + *args = append(*args, "-start-debugger-server", strconv.Itoa(debugPortInt)) + } else { + debugPortInt, err = strconv.Atoi(debugPort) + if err != nil { + fmt.Printf("Error parsing debug port. Must be an integer: %v\n", err) + os.Exit(1) + } + } + return debugPortInt +} + +func confirmPaths(paths []string) { + // Confirm paths are valid + for _, path := range paths { + if _, err := os.Stat(path); err != nil { + fmt.Printf("Error: %s is not a valid addon path.\n", path) + os.Exit(1) + } + } +} + +func getOpenPort() int { + // Generate an open port + ln, err := net.Listen("tcp", ":0") // listen on a random port + if err != nil { + return 0 + } + defer ln.Close() + + addr := ln.Addr().(*net.TCPAddr) // type assert to *net.TCPAddr to get the Port + return addr.Port +} + +// Firefox addon loader +// Ported from this Nodejs implementation: +// https://github.com/microsoft/playwright/issues/7297#issuecomment-1211763085 +func loadFirefoxAddon(port int, addonPath string) bool { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", "localhost", port)) + if err != nil { + return false + } + defer conn.Close() + + success := false + + send := func(data map[string]string) error { + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + _, err = fmt.Fprintf(conn, "%d:%s", len(jsonData), jsonData) + return err + } + + err = send(map[string]string{ + "to": "root", + "type": "getRoot", + }) + if err != nil { + return false + } + + onMessage := func(message map[string]interface{}) bool { + if addonsActor, ok := message["addonsActor"].(string); ok { + err := send(map[string]string{ + "to": addonsActor, + "type": "installTemporaryAddon", + "addonPath": addonPath, + }) + if err != nil { + return true + } + } + + if _, ok := message["addon"]; ok { + success = true + return true + } + + if _, ok := message["error"]; ok { + return true + } + + return false + } + + reader := bufio.NewReader(conn) + for { + lengthStr, err := reader.ReadString(':') + if err != nil { + break + } + length, err := strconv.Atoi(strings.TrimSuffix(lengthStr, ":")) + if err != nil { + break + } + + jsonData := make([]byte, length) + _, err = reader.Read(jsonData) + if err != nil { + break + } + + var message map[string]interface{} + err = json.Unmarshal(jsonData, &message) + if err != nil { + break + } + + if onMessage(message) { + break + } + } + + return success +} diff --git a/launcher/main.go b/launcher/main.go index d045475..069d5de 100644 --- a/launcher/main.go +++ b/launcher/main.go @@ -13,9 +13,21 @@ import ( ) func main() { - configPath, remainingArgs := parseArgs(os.Args[1:]) // Parse args arguments + args := os.Args[1:] - configMap := readAndParseConfig(configPath) // Read and parse the config file + configPath := parseArgs("--config", "{}", &args, true) + addons := parseArgs("--addons", "[]", &args, true) + + // Read and parse the config file + var configMap map[string]interface{} + parseJson(configPath, &configMap) + + // If addons are passed, handle them + var addonsList []string + parseJson(addons, &addonsList) + + // Confirm addon paths are valid + confirmPaths(addonsList) userAgentOS := determineUserAgentOS(configMap) // Determine the user agent OS @@ -29,58 +41,47 @@ func main() { fmt.Printf("Error setting executable permissions: %v\n", err) os.Exit(1) } - runCamoufox(execName, remainingArgs) + runCamoufox(execName, args, addonsList) } -func parseArgs(args []string) (string, []string) { - // Parse the arguments - var configPath string - var remainingArgs []string - - for i := 0; i < len(args); i++ { - if args[i] == "--config" { - if i+1 < len(args) { - configPath = args[i+1] - remainingArgs = append(args[:i], args[i+2:]...) - break - } else { - fmt.Println("Error: --config flag requires a value") - os.Exit(1) - } +func parseArgs(param string, defaultValue string, args *[]string, removeFromArgs bool) string { + for i := 0; i < len(*args); i++ { + if (*args)[i] != param { + continue } + if i+1 < len(*args) { + value := (*args)[i+1] + if removeFromArgs { + *args = append((*args)[:i], (*args)[i+2:]...) + } + return value + } + fmt.Printf("Error: %s flag requires a value\n", param) + os.Exit(1) } - - // If no config data is provided, fallback to an empty object - if configPath == "" { - configPath = "{}" - } - - return configPath, remainingArgs + return defaultValue } -func readAndParseConfig(configInput string) map[string]interface{} { +func parseJson(argv string, target interface{}) { // Unmarshal the config input into a map - var configData []byte + var data []byte // Check if the input is a file path or inline JSON - if _, err := os.Stat(configInput); err == nil { - configData, err = os.ReadFile(configInput) + if _, err := os.Stat(argv); err == nil { + data, err = os.ReadFile(argv) if err != nil { fmt.Printf("Error reading config file: %v\n", err) os.Exit(1) } } else { // Assume it's inline JSON - configData = []byte(configInput) + data = []byte(argv) } - var configMap map[string]interface{} - if err := json.Unmarshal(configData, &configMap); err != nil { + if err := json.Unmarshal(data, target); err != nil { fmt.Printf("Invalid JSON in config: %v\n", err) os.Exit(1) } - - return configMap } func determineUserAgentOS(configMap map[string]interface{}) string { diff --git a/launcher/runner.go b/launcher/runner.go index 74735aa..ab64dbc 100644 --- a/launcher/runner.go +++ b/launcher/runner.go @@ -4,12 +4,14 @@ import ( "bufio" "fmt" "io" + "net" "os" "os/exec" "os/signal" "path/filepath" "runtime" "syscall" + "time" ) func getExecutableName() string { @@ -66,7 +68,32 @@ func filterOutput(r io.Reader, w io.Writer) { } } -func runCamoufox(execName string, args []string) { +func tryLoadAddons(debugPortInt int, addonsList []string) { + // Wait for the server to be open + for { + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", debugPortInt)) + if err == nil { + conn.Close() + break + } + time.Sleep(10 * time.Millisecond) + } + + // Load addons + for _, addon := range addonsList { + loadFirefoxAddon(debugPortInt, addon) + } +} + +// Run Camoufox +func runCamoufox(execName string, args []string, addonsList []string) { + // If addons are specified, get the debug port + var debugPortInt int + if len(addonsList) > 0 { + debugPortInt = getDebugPort(&args) + } + + // Print args cmd := exec.Command(execName, args...) setProcessGroupID(cmd) @@ -91,6 +118,10 @@ func runCamoufox(execName string, args []string) { os.Exit(1) } + if len(addonsList) > 0 { + go tryLoadAddons(debugPortInt, addonsList) + } + // Channel to signal when the subprocess has finished subprocessDone := make(chan struct{}) diff --git a/settings/camoufox.cfg b/settings/camoufox.cfg index c354614..c274529 100644 --- a/settings/camoufox.cfg +++ b/settings/camoufox.cfg @@ -1,3 +1,16 @@ +// Camoufox functionality + +pref("gfx.bundled-fonts.activate", 1); + +pref("devtools.debugger.remote-enabled", true); +pref("devtools.debugger.prompt-connection", false); +pref("privacy.userContext.enabled", true); + +pref("browser.sessionstore.max_resumed_crashes", 0); +pref("browser.sessionstore.restore_on_demand", false); +pref("browser.sessionstore.restore_tabs_lazily", false); + + // Debloat and speed up Camoufox. // Debloat (from Peskyfox) @@ -285,7 +298,6 @@ pref("privacy.partition.network_state", false); // Disable network seperations pref("accessibility.force_disabled", 1); pref("browser.sessionstore.max_tabs_undo", 0); pref("browser.sessionstore.max_windows_undo", 0); -pref("browser.sessionstore.resume_from_crash", false); pref("browser.sessionstore.resuming_after_os_restart", false); pref("browser.sessionstore.resume_session_once", false); pref("browser.sessionstore.upgradeBackup.maxUpgradeBackups", 0); @@ -406,7 +418,6 @@ pref("userChrome.icon.global_menu", true); pref("userChrome.icon.global_menubar", true); pref("userChrome.icon.1-25px_stroke", true); -pref("gfx.bundled-fonts.activate", 1); // ================================================================= // THESE ARE THE PROPERTIES THAT MUST BE ENABLED FOR JUGGLER TO WORK