package main import ( "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "unicode/utf8" json "github.com/goccy/go-json" "github.com/mileusna/useragent" ) func main() { configPath, remainingArgs := parseArgs(os.Args[1:]) // Parse args arguments configMap := readAndParseConfig(configPath) // Read and parse the config file userAgentOS := determineUserAgentOS(configMap) // Determine the user agent OS // OS specific font config updateFonts(configMap, userAgentOS) setEnvironmentVariables(configMap, userAgentOS) // Run the Camoufox executable execName := getExecutableName() runCamoufox(execName, remainingArgs) } func parseArgs(args []string) (string, []string) { 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) } } } // If no config data is provided, fallback to an empty object if configPath == "" { configPath = "{}" } return configPath, remainingArgs } func readAndParseConfig(configInput string) map[string]interface{} { var configData []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 != nil { fmt.Printf("Error reading config file: %v\n", err) os.Exit(1) } } else { // Assume it's inline JSON configData = []byte(configInput) } var configMap map[string]interface{} if err := json.Unmarshal(configData, &configMap); err != nil { fmt.Printf("Invalid JSON in config: %v\n", err) os.Exit(1) } return configMap } func determineUserAgentOS(configMap map[string]interface{}) string { defaultOS := normalizeOS(runtime.GOOS) if ua, ok := configMap["navigator.userAgent"].(string); ok { parsedUA := useragent.Parse(ua) if parsedUA.OS != "" { return normalizeOS(parsedUA.OS) } } return defaultOS } func normalizeOS(osName string) string { osName = strings.ToLower(osName) switch { case osName == "darwin" || strings.Contains(osName, "mac"): return "macos" case strings.Contains(osName, "win"): return "windows" default: return "linux" } } func updateFonts(configMap map[string]interface{}, userAgentOS string) { fonts, ok := configMap["fonts"].([]interface{}) if !ok { fonts = []interface{}{} } existingFonts := make(map[string]bool) for _, font := range fonts { if f, ok := font.(string); ok { existingFonts[f] = true } } for _, font := range FontsByOS[userAgentOS] { if !existingFonts[font] { fonts = append(fonts, font) } } configMap["fonts"] = fonts } func setEnvironmentVariables(configMap map[string]interface{}, userAgentOS string) { updatedConfigData, err := json.Marshal(configMap) if err != nil { fmt.Printf("Error updating config: %v\n", err) os.Exit(1) } // Validate utf8 if !utf8.Valid(updatedConfigData) { fmt.Println("Config is not valid UTF-8") os.Exit(1) } // Split the config into chunks of 2047 characters if the OS is Windows, // otherwise split into 32767 characters var chunkSize int if normalizeOS(runtime.GOOS) == "windows" { chunkSize = 2047 } else { chunkSize = 32767 } configStr := string(updatedConfigData) for i := 0; i < len(configStr); i += chunkSize { end := i + chunkSize if end > len(configStr) { end = len(configStr) } chunk := configStr[i:end] envName := fmt.Sprintf("CAMOU_CONFIG_%d", (i/chunkSize)+1) if err := os.Setenv(envName, chunk); err != nil { fmt.Printf("Error setting %s: %v\n", envName, err) os.Exit(1) } } if normalizeOS(runtime.GOOS) == "linux" { fontconfigPath := filepath.Join("fontconfig", userAgentOS) os.Setenv("FONTCONFIG_PATH", fontconfigPath) } } func getExecutableName() string { switch normalizeOS(runtime.GOOS) { case "linux": return "./camoufox-bin" case "macos": return "./Camoufox.app" case "windows": return "./camoufox.exe" default: // This should never be reached due to the check in normalizeOS return "" } } func runCamoufox(execName string, args []string) { cmd := exec.Command(execName, args...) // Create pipes for stdout and stderr stdout, err := cmd.StdoutPipe() if err != nil { fmt.Printf("Error creating stdout pipe: %v\n", err) os.Exit(1) } stderr, err := cmd.StderrPipe() if err != nil { fmt.Printf("Error creating stderr pipe: %v\n", err) os.Exit(1) } // Start the command if err := cmd.Start(); err != nil { fmt.Printf("Error starting %s: %v\n", execName, err) os.Exit(1) } // Create a channel to signal when we're done copying output done := make(chan bool) // Copy stdout and stderr to the console go func() { io.Copy(os.Stdout, stdout) done <- true }() go func() { io.Copy(os.Stderr, stderr) done <- true }() // Wait for both stdout and stderr to finish <-done <-done // Wait for the command to finish if err := cmd.Wait(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { os.Exit(exitErr.ExitCode()) } else { fmt.Printf("Error running %s: %v\n", execName, err) os.Exit(1) } } }