diff --git a/README.md b/README.md index 465468f..231df66 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Camoufox aims to be a minimalistic browser for robust fingerprint injection & an ## Features - Fingerprint injection (override properties of `navigator`, `window`, `screen`, etc) ✅ -- Patches to avoid Playwright detection ✅ -- Custom Playwright Juggler implementation with minimal leaks ✅ +- Patches to avoid bot detection ✅ +- Custom Playwright Juggler implementation for the latest Firefox ✅ - Font spoofing & anti-fingerprinting ✅ - Patches from LibreWolf & Ghostery to remove Mozilla services ✅ - Optimized for memory and speed ✅ @@ -34,6 +34,7 @@ Camoufox is built on top of Firefox/Juggler instead of Chromium because: ### What's planned? +- Continue research on potential leaks - Built in TLS fingerprinting protection using [Hazetunnel](https://github.com/daijro/hazetunnel) - Create integration tests - Chromium port (long-term) @@ -198,6 +199,25 @@ Spoofing document.body has been implemented, but it is more advicable to set `wi +
+ +HTTP Headers + + +Camoufox can override the following network headers: + +| Property | Status | +| ----------------------- | ------ | +| headers.User-Agent | ✅ | +| headers.Accept-Language | ✅ | +| headers.Accept-Encoding | ✅ | + +**Notes:** + +- If `headers.User-Agent` is not set, it will fall back to `navigator.userAgent`. + +
+
@@ -214,6 +234,14 @@ Example: Camoufox will automatically download and use the latest uBlock Origin with custom privacy/adblock filters, and B.P.C. by default to help with scraping. +You can also exclude default addons with the `--exclude-addons` flag: + +```bash +./launcher --exclude-addons '["uBO", "BPC"]' +``` + +
+
@@ -285,6 +313,7 @@ Miscellaneous (WebGl spoofing, battery status, etc) - Added B.P.C. - Addons are not allowed to open tabs - Addons are automatically enabled in Private Browsing mode +- Addons are automatically pinned to the toolbar ## Stealth Performance @@ -319,7 +348,7 @@ Camoufox performs well against every major WAF I've tested. (Test sites from [Bo | [**BrowserScan**](https://browserscan.net/) | ✔️ | | [**Bet365**](https://www.bet365.com/#/AC/B1/C1/D1002/E79147586/G40/) | ✔️ | -Camoufox does **not** fully support injecting Chromium fingerprints. Some websites (such as Cloudflare [Interstitial](https://nopecha.com/demo/cloudflare)) look for the Gecko webdriver underneath. +Camoufox does **not** fully support injecting Chromium fingerprints. Some WAFs (such as [Interstitial](https://nopecha.com/demo/cloudflare)) look for the Gecko webdriver underneath. --- @@ -334,11 +363,11 @@ graph TD FFSRC[Firefox Source] -->|make fetch| REPO subgraph REPO[Camoufox Repository] - MASKING[Camoufox masking module] - PATCHES[Debloat/optimizations] + PATCHES[Fingerprint masking patches] ADDONS[uBlock & B.P.C.] - SYSTEM_FONTS[Win, Mac, Linux System fonts] - JUGGLER[Playwright Juggler] + DEBLOAT[Debloat/optimizations] + SYSTEM_FONTS[Win, Mac, Linux fonts] + JUGGLER[Patched Juggler] end subgraph Local diff --git a/launcher/constants.go b/launcher/constants.go index 1b3f2c2..58ac57e 100644 --- a/launcher/constants.go +++ b/launcher/constants.go @@ -5,6 +5,12 @@ import ( "strings" ) +// Default addons to extract to /addons +var DefaultAddons = map[string]string{ + "uBO": "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi", + "BPC": "https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/raw?file=bypass_paywalls_clean-latest.xpi", +} + // Exclude lines from output var ExclusionRules = []string{ // Ignore search related warnings @@ -17,6 +23,7 @@ var ExclusionRules = []string{ "^console\\.error:\\ \\(\\{\\}\\)$", "^console\\.error:\\ \"Could\\ not\\ record\\ event:\\ \"\\ \\(\\{\\}\\)$", "^\\s*?$", + "Rejected by Camoufox\\.$", // Ignore missing urlbar provider errors "^JavaScript\\ error:\\ resource:///modules/UrlbarProvider", } diff --git a/launcher/runner.go b/launcher/exec.go similarity index 89% rename from launcher/runner.go rename to launcher/exec.go index ab64dbc..98c9bca 100644 --- a/launcher/runner.go +++ b/launcher/exec.go @@ -4,14 +4,12 @@ import ( "bufio" "fmt" "io" - "net" "os" "os/exec" "os/signal" "path/filepath" "runtime" "syscall" - "time" ) func getExecutableName() string { @@ -68,23 +66,6 @@ func filterOutput(r io.Reader, w io.Writer) { } } -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 diff --git a/launcher/addon.go b/launcher/load-addons.go similarity index 82% rename from launcher/addon.go rename to launcher/load-addons.go index 9692750..c8450e1 100644 --- a/launcher/addon.go +++ b/launcher/load-addons.go @@ -8,8 +8,10 @@ import ( "os" "strconv" "strings" + "time" ) +// Gets the debug port from the args, or creates a new one if not provided func getDebugPort(args *[]string) int { debugPort := parseArgs("-start-debugger-server", "", args, false) @@ -30,8 +32,8 @@ func getDebugPort(args *[]string) int { return debugPortInt } +// Confirm paths are valid 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) @@ -40,8 +42,8 @@ func confirmPaths(paths []string) { } } +// Generate an open port func getOpenPort() int { - // Generate an open port ln, err := net.Listen("tcp", ":0") // listen on a random port if err != nil { return 0 @@ -52,6 +54,24 @@ func getOpenPort() int { return addr.Port } +// Waits for the server to start, then loads the addons +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 { + go loadFirefoxAddon(debugPortInt, addon) + } +} + // Firefox addon loader // Ported from this Nodejs implementation: // https://github.com/microsoft/playwright/issues/7297#issuecomment-1211763085 diff --git a/launcher/main.go b/launcher/main.go index 069d5de..8706858 100644 --- a/launcher/main.go +++ b/launcher/main.go @@ -17,11 +17,16 @@ func main() { configPath := parseArgs("--config", "{}", &args, true) addons := parseArgs("--addons", "[]", &args, true) + excludeAddons := parseArgs("--exclude-addons", "[]", &args, true) + + //*** PARSE CONFIG ***// // Read and parse the config file var configMap map[string]interface{} parseJson(configPath, &configMap) + //*** PARSE ADDONS ***// + // If addons are passed, handle them var addonsList []string parseJson(addons, &addonsList) @@ -29,10 +34,21 @@ func main() { // Confirm addon paths are valid confirmPaths(addonsList) - userAgentOS := determineUserAgentOS(configMap) // Determine the user agent OS + // Add the default addons, excluding the ones specified in --exclude-addons + var excludeAddonsList []string + parseJson(excludeAddons, &excludeAddonsList) - // OS specific font config + addDefaultAddons(excludeAddonsList, &addonsList) + + //*** FONTS ***// + + // Determine the target OS + userAgentOS := determineUserAgentOS(configMap) + // Add OS specific fonts updateFonts(configMap, userAgentOS) + + //*** LAUNCH ***// + setEnvironmentVariables(configMap, userAgentOS) // Run the Camoufox executable @@ -44,6 +60,7 @@ func main() { runCamoufox(execName, args, addonsList) } +// Parses & removes an argument from the args list func parseArgs(param string, defaultValue string, args *[]string, removeFromArgs bool) string { for i := 0; i < len(*args); i++ { if (*args)[i] != param { @@ -62,6 +79,7 @@ func parseArgs(param string, defaultValue string, args *[]string, removeFromArgs return defaultValue } +// Parses a JSON string or file into a map func parseJson(argv string, target interface{}) { // Unmarshal the config input into a map var data []byte @@ -84,6 +102,7 @@ func parseJson(argv string, target interface{}) { } } +// Determines the target OS from the user agent string if provided func determineUserAgentOS(configMap map[string]interface{}) string { // Determine the OS from the user agent string if provided defaultOS := normalizeOS(runtime.GOOS) @@ -96,8 +115,8 @@ func determineUserAgentOS(configMap map[string]interface{}) string { return defaultOS } +// Get the OS name as {macos, windows, linux} func normalizeOS(osName string) string { - // Get the OS name as {macos, windows, linux} osName = strings.ToLower(osName) switch { case osName == "darwin" || strings.Contains(osName, "mac"): @@ -109,8 +128,8 @@ func normalizeOS(osName string) string { } } +// Add fonts associated with the OS to the config map func updateFonts(configMap map[string]interface{}, userAgentOS string) { - // Add fonts associated with the OS to the config map fonts, ok := configMap["fonts"].([]interface{}) if !ok { fonts = []interface{}{} @@ -129,8 +148,8 @@ func updateFonts(configMap map[string]interface{}, userAgentOS string) { configMap["fonts"] = fonts } +// Update the config map with the fonts and environment variables func setEnvironmentVariables(configMap map[string]interface{}, userAgentOS string) { - // Update the config map with the fonts and environment variables updatedConfigData, err := json.Marshal(configMap) if err != nil { fmt.Printf("Error updating config: %v\n", err) diff --git a/launcher/procgroup_unix.go b/launcher/procgroup-unix.go similarity index 100% rename from launcher/procgroup_unix.go rename to launcher/procgroup-unix.go diff --git a/launcher/procgroup_win.go b/launcher/procgroup-win.go similarity index 100% rename from launcher/procgroup_win.go rename to launcher/procgroup-win.go diff --git a/launcher/xpi-dl.go b/launcher/xpi-dl.go new file mode 100644 index 0000000..30c2233 --- /dev/null +++ b/launcher/xpi-dl.go @@ -0,0 +1,158 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +// Downloads and extracts the default addons +func addDefaultAddons(excludeList []string, addonsList *[]string) { + // Build a map from DefaultAddons, excluding keys found in excludeAddonsList + addonsMap := make(map[string]string) + for name, url := range DefaultAddons { + if len(excludeList) == 0 || !contains(excludeList, name) { + addonsMap[name] = url + } + } + + // Download if not already downloaded + maybeDownloadAddons(addonsMap, addonsList) +} + +// Downloads and extracts the addon +func downloadAndExtract(url, extractPath string) error { + // Create a temporary file to store the downloaded zip + tempFile, err := os.CreateTemp("", "camoufox-addon-*.zip") + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + defer os.Remove(tempFile.Name()) // Clean up the temp file when done + + // Download the zip file + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to download addon: %w", err) + } + defer resp.Body.Close() + + // Write the body to the temp file + _, err = io.Copy(tempFile, resp.Body) + if err != nil { + return fmt.Errorf("failed to write addon to temp file: %w", err) + } + + // Close the file before unzipping + tempFile.Close() + + // Extract the zip file + err = unzip(tempFile.Name(), extractPath) + if err != nil { + return fmt.Errorf("failed to extract addon: %w", err) + } + + return nil +} + +// Extracts the zip file +func unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + continue + } + + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + outFile.Close() + return err + } + + _, err = io.Copy(outFile, rc) + outFile.Close() + rc.Close() + + if err != nil { + return err + } + } + return nil +} + +// Checks if a slice contains a string +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +// Returns the absolute path to the target addon location +func getAddonPath(addonName string) (string, error) { + execPath, err := os.Executable() + if err != nil { + fmt.Printf("Error getting executable path: %v\n", err) + return "", err + } + execDir := filepath.Dir(execPath) + + addonPath := filepath.Join(execDir, "addons", addonName) + return addonPath, nil +} + +// Downloads and extracts the addons +func maybeDownloadAddons(addons map[string]string, addonsList *[]string) { + for addonName, url := range addons { + // Get the addon path + addonPath, err := getAddonPath(addonName) + if err != nil { + fmt.Printf("Error getting addon path: %v\n", err) + continue + } + + // Check if the addon is already extracted + if _, err := os.Stat(addonPath); !os.IsNotExist(err) { + // Add the existing addon path to addonsList + *addonsList = append(*addonsList, addonPath) + continue + } + + // Addon doesn't exist, create directory and download + err = os.MkdirAll(addonPath, 0755) + if err != nil { + fmt.Printf("Failed to create directory for %s: %v\n", addonName, err) + continue + } + + err = downloadAndExtract(url, addonPath) + if err != nil { + fmt.Printf("Failed to download and extract %s: %v\n", addonName, err) + } else { + fmt.Printf("Successfully downloaded and extracted %s\n", addonName) + // Add the new addon directory path to addonsList + *addonsList = append(*addonsList, addonPath) + } + } +} diff --git a/settings/distribution/policies.json b/settings/distribution/policies.json index 0134edf..d076824 100644 --- a/settings/distribution/policies.json +++ b/settings/distribution/policies.json @@ -35,10 +35,6 @@ "Exceptions": ["https://localhost/*"] }, "Extensions": { - "Install": [ - "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi", - "https://github.com/bpc-clone/bpc_updates/releases/download/latest/bypass_paywalls_clean-latest.xpi" - ], "Uninstall": [ "google@search.mozilla.org", "bing@search.mozilla.org",