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",