camoufox 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 00e2999e211a2e3ee480426bf3c9eed1a8c0b8760ece361bc2f1fde28db92cad
4
+ data.tar.gz: b10e48835271556ee652882db960a95087d3a896b2c6fe009b434c0ceb820acf
5
+ SHA512:
6
+ metadata.gz: d32e355e981ed307fcd1793cab79952cb3512708d1cbeb868e1db90cf60d21e847611b4ade10c9c440f2aee2640ac56af67ea95c25420ef1768dddfe32b266bb
7
+ data.tar.gz: 939da8ab954c0df051fb00c678a2147ada31fc16447f23a69dc75943a79a50a638d919bf4c85e9f9d1e9f824008d956fd39410ba0b2d43f9d5a5a7826a2baa17
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+ - Fix native launch options so the provided `headless` flag is respected by Playwright.
5
+
6
+ ## 0.1.0
7
+ - Initial Ruby bridge around the Camoufox Python package (legacy).
8
+ - Native rewrite in progress: mirror `pythonlib/camoufox` structure, remove the Ruby Playwright
9
+ client dependency, provide stubbed launch options via C++, add an experimental server launcher, and
10
+ reintroduce a `SyncAPI` helper that drives Playwright through a Node bridge.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Camoufox contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Camoufox Ruby
2
+
3
+ Camoufox Ruby is a work-in-progress native port of the
4
+ [Camoufox](https://github.com/daijro/camoufox) toolkit. The project is undergoing a full rewrite to
5
+ mirror the package layout of the reference Python implementation (`pythonlib/camoufox`) while keeping
6
+ all logic inside the Ruby gem.
7
+
8
+ > **Status:** Everything is stubbed. The gem exposes the same module/file structure as the Python
9
+ > package, but most methods simply return placeholder data or emit warnings. Real fingerprint
10
+ > generation, binary management, networking, and Web API spoofing are still to come.
11
+
12
+ ## Installation
13
+
14
+ Add the gem directly from the repository while the native rewrite is underway:
15
+
16
+ ```ruby
17
+ gem "camoufox"
18
+ ```
19
+
20
+ ### Prerequisites
21
+
22
+ - Ruby ≥ 3.0 (development targets 3.4.2)
23
+ - Node.js and `npm` (required by Playwright)
24
+
25
+ ### Install steps
26
+
27
+ ```bash
28
+ git clone https://github.com/GaetanJuvin/camoufox-ruby.git
29
+ cd camoufox-ruby
30
+ bundle install
31
+ rake compile
32
+ npx install playwright
33
+ npx playwright install firefox
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```ruby
39
+ require "camoufox"
40
+
41
+ driver_dir = ENV.fetch("CAMOUFOX_PLAYWRIGHT_DRIVER_DIR", File.expand_path("node_modules/playwright", __dir__))
42
+
43
+ Camoufox.configure do |config|
44
+ config.playwright_driver_dir = driver_dir
45
+ config.node_path = ENV["CAMOUFOX_NODE_PATH"] if ENV["CAMOUFOX_NODE_PATH"]
46
+ end
47
+
48
+ Camoufox::SyncAPI::Camoufox.open(headless: true) do |browser|
49
+ page = browser.new_page
50
+ page.goto("https://example.com")
51
+ puts page.title
52
+ end
53
+ ```
54
+
55
+ Behind the scenes the Ruby port encodes the Camoufox launch options, hands them to a small Node.js
56
+ bridge, and lets Playwright do the heavy lifting. You must supply a Playwright driver bundle or
57
+ installation so the Node script can `require('playwright')`.
58
+
59
+ ### Launching the Playwright server (experimental)
60
+
61
+ To mirror the Python helper that spins up a Playwright websocket endpoint, the Ruby port can invoke
62
+ Playwright's Node driver directly (no `playwright-ruby-client` dependency). You must provide the
63
+ location of a Playwright driver bundle that contains `lib/browserServerImpl.js`.
64
+
65
+ ```bash
66
+ export CAMOUFOX_PLAYWRIGHT_DRIVER_DIR=/path/to/playwright/driver/package
67
+ export CAMOUFOX_NODE_PATH=/path/to/node # optional, defaults to `node`
68
+ bundle exec ruby run.rb server
69
+ ```
70
+
71
+ The command prints the websocket endpoint and keeps the process alive, matching the Python
72
+ behaviour. Until the native mapper is complete, the underlying launch options remain stubbed.
73
+
74
+ ## Module layout
75
+
76
+ The Ruby sources now mirror the structure of `pythonlib/camoufox`:
77
+
78
+ ```
79
+ lib/camoufox/
80
+ ├── __init__ (lib/camoufox.rb)
81
+ ├── __main__.rb
82
+ ├── __version__.rb
83
+ ├── addons.rb
84
+ ├── async_api.rb
85
+ ├── browserforge.yml
86
+ ├── exceptions.rb
87
+ ├── fingerprints.rb
88
+ ├── fonts.json
89
+ ├── ip.rb
90
+ ├── locale.rb
91
+ ├── pkgman.rb
92
+ ├── server.rb
93
+ ├── sync_api.rb
94
+ ├── utils.rb
95
+ ├── virtdisplay.rb
96
+ ├── warnings.rb
97
+ └── webgl/
98
+ ```
99
+
100
+ Each file defines the corresponding Ruby module, currently implemented as lightweight stubs so that
101
+ call sites can be wired up without crashing.
102
+
103
+ ## CLI
104
+
105
+ The `bin/camoufox` executable is available, but commands only return informative placeholder
106
+ messages until the native implementation lands.
107
+
108
+ ```bash
109
+ camoufox version # => "Camoufox native stub v0.0.1"
110
+ ```
111
+
112
+ The repository also includes a helper script, `run.rb`, with convenience commands:
113
+
114
+ ```bash
115
+ bundle exec ruby run.rb # show stub details and launch options
116
+ bundle exec ruby run.rb launch-options --locale en-US --headful
117
+ bundle exec ruby run.rb browse --url https://example.com
118
+ bundle exec ruby run.rb server
119
+
120
+ # or run the sample script directly
121
+ bundle exec ruby examples/sync_playwright.rb
122
+ ```
123
+
124
+ ## Configuration
125
+
126
+ `Camoufox.configure` exposes a tiny configuration object that is growing alongside the native port.
127
+ Today it supports the basic directories and the Playwright driver configuration:
128
+
129
+ ```ruby
130
+ Camoufox.configure do |config|
131
+ config.data_dir = "/tmp/camoufox-data"
132
+ config.node_path = "/usr/local/bin/node"
133
+ config.playwright_driver_dir = "/opt/playwright-driver/package"
134
+ end
135
+ ```
136
+
137
+ Environment overrides:
138
+
139
+ - `CAMOUFOX_DATA_DIR` – override where Camoufox assets are stored (planned use)
140
+ - `CAMOUFOX_CACHE_DIR` – override the cache directory (planned use)
141
+ - `CAMOUFOX_NODE_PATH` – path to the Node.js binary used when spawning the Playwright server (defaults to `node`)
142
+ - `CAMOUFOX_PLAYWRIGHT_DRIVER_DIR` – directory containing `lib/browserServerImpl.js` (defaults to `node_modules/playwright` if present)
143
+ - `CAMOUFOX_PLAYWRIGHT_JS_REQUIRE` – optional module identifier passed to Node's `require()` when
144
+ running the synchronous Playwright bridge (defaults to `playwright`)
145
+
146
+ ## Testing
147
+
148
+ Specs intentionally exercise only the pieces that exist today. Run them after compiling the native
149
+ extension:
150
+
151
+ ```bash
152
+ ~/.rbenv/versions/3.4.2/bin/ruby -S bundle exec rspec
153
+ ```
154
+
155
+ ## Contributing
156
+
157
+ See `docs/native_port.md` for the roadmap toward feature parity with the Python Camoufox package.
158
+
159
+ ## License
160
+
161
+ MIT – see `LICENSE` for details.
data/bin/camoufox ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/camoufox"
5
+
6
+ if ARGV.empty?
7
+ warn "Usage: camoufox <command> [args]"
8
+ exit 1
9
+ end
10
+
11
+ command = ARGV.shift
12
+
13
+ begin
14
+ output = Camoufox::CLI.run(command, ARGV)
15
+ print(output) unless output.nil? || output.empty?
16
+ rescue Camoufox::MissingNativeExtension => e
17
+ warn e.message
18
+ exit 127
19
+ end
@@ -0,0 +1,96 @@
1
+ # Camoufox Native Port Plan
2
+
3
+ This document tracks the work needed to replace the Python bridge with a native C++ implementation
4
+ that exposes the same surface area to the Ruby gem. The goal is to eventually match the behaviour of
5
+ the original Python package (`camoufox`) without shelling out to Python.
6
+
7
+ ## Target surface area
8
+
9
+ The Ruby gem currently relies on the Python package for the following capabilities:
10
+
11
+ 1. **Launch options** – `camoufox.launch_options` assembles the configuration blob passed to the
12
+ Camoufox Firefox binary. This touches nearly every helper in `pythonlib/camoufox/utils.py`.
13
+ 2. **Binary management** – `camoufox fetch/remove/path/version` download the Firefox bundle from
14
+ GitHub releases, manage add-ons, fonts, and GeoIP data (`pkgman.py`, `addons.py`, `locale.py`).
15
+ 3. **Warnings & validation** – type checking, config validation, leak warnings, logging.
16
+ 4. **Virtual display and Playwright helpers** – optional Xvfb integration (Linux), environment
17
+ variable setup for the executable, proxy/geolocation interactions.
18
+
19
+ ## Major components to port
20
+
21
+ | Python module | Responsibility | Native port considerations |
22
+ | ------------- | -------------- | --------------------------- |
23
+ | `utils.py` | Fingerprint generation, fonts, WebGL spoofing, config assembly, env var packing | Requires BrowserForge fingerprints DB, WebGL dataset, numpy-like utilities, UA parsing |
24
+ | `fingerprints.py` | Integrates BrowserForge logic | Need a native fingerprint generator and data loader |
25
+ | `pkgman.py` | Download/validate Camoufox binaries from GitHub, version constraints | Re-implement HTTP client, ZIP handling, progress reporting |
26
+ | `addons.py` | Default addon management, path validation | Port addon discovery + download logic |
27
+ | `locale.py` | GeoIP integration, locale selection | Needs MMDB parsing or a replacement library |
28
+ | `webgl/` | Pre-generated WebGL fingerprints | Convert dataset to native-friendly format |
29
+ | `virtdisplay.py` | Manages Xvfb | Detect platform, spawn background process |
30
+ | `cli` commands | fetch/remove/test/version | Recreate CLI front-end in Ruby or C++ |
31
+
32
+ ## Architectural direction
33
+
34
+ 1. **C++ shared library (`libcamoufox_native`)**
35
+ - Exposes a minimal API surface to Ruby (initially stubbed, later feature-complete).
36
+ - Organised into modules mirroring the Python package (fingerprint, config, pkgman, addons, geo).
37
+ - Uses modern C++ (C++20) with libraries for HTTP (e.g., libcurl), JSON (nlohmann/json), YAML
38
+ (yaml-cpp), ZIP handling (libzip/minizip), and MMDB (libmaxminddb).
39
+
40
+ 2. **Ruby extension**
41
+ - Built via `extconf.rb` / `mkmf` (or CMake + rake) compiling against the shared library.
42
+ - Provides Ruby-friendly wrappers around the C++ API (converts to/from `VALUE`).
43
+
44
+ 3. **Data assets**
45
+ - Ship fingerprint/WebGL datasets as JSON/YAML alongside the gem.
46
+ - Provide tooling to update assets from upstream Camoufox/BrowserForge releases.
47
+
48
+ 4. **CLI integration**
49
+ - Re-implement the `camoufox` CLI commands purely in Ruby, calling into the native library for
50
+ heavy work.
51
+ - Provide an optional Node.js bridge for Playwright interactions while retaining the ability to
52
+ talk directly to the Playwright driver (no Ruby gem dependency).
53
+
54
+ ## Milestones
55
+
56
+ 1. **Bootstrap**
57
+ - Scaffold `ext/camoufox_native` with a shared library exposing `launch_options` (stubbed).
58
+ - Replace `Camoufox::PythonBridge` with `Camoufox::NativeBridge` calling the extension.
59
+ - Ensure existing specs pass using stubbed data.
60
+
61
+ 2. **Binary manager parity**
62
+ - Port GitHub release fetching + ZIP extraction.
63
+ - Implement `fetch/remove/path/version` in C++.
64
+
65
+ 3. **Fingerprint generation**
66
+ - Port BrowserForge fingerprint logic (initially with static samples, then full generator).
67
+ - Implement WebGL + fonts spoofing pipeline.
68
+
69
+ 4. **Advanced features**
70
+ - GeoIP integration, proxy warnings, virtual display support, leak warnings.
71
+
72
+ 5. **Testing & QA**
73
+ - Build parity test suite comparing native output with Python reference data.
74
+ - Automate cross-platform builds.
75
+
76
+ ## Immediate next steps
77
+
78
+ - Define the data model for fingerprints and decide how to ingest BrowserForge datasets natively.
79
+ - Choose third-party libraries (if any) for HTTP, ZIP, MMDB, JSON, and YAML handling; prototype
80
+ minimal integrations.
81
+ - Design a stable C API surface for the C++ library so Ruby (and potentially other languages) can
82
+ interact with it without tight coupling to implementation details.
83
+ - Expand the Node bridge to support more Playwright commands (multi-page workflows, screenshotting,
84
+ etc.) once the native mapper delivers real launch data.
85
+
86
+ ## Open questions
87
+
88
+ - How closely do we need to match BrowserForge’s fingerprint distribution? Reuse the dataset or
89
+ re-implement the generation algorithm?
90
+ - Should the native component expose a stable C API (for reuse in other languages) or remain
91
+ Ruby-specific for now?
92
+ - Packaging strategy for large assets (fonts, WebGL datasets) in a gem-friendly way.
93
+ - Publishing pipeline for https://github.com/GaetanJuvin/camoufox-ruby (tests, builds, release
94
+ artifacts).
95
+
96
+ This document will evolve as the native implementation grows. Contributions welcome.
@@ -0,0 +1,70 @@
1
+ #include <ruby.h>
2
+ #include <cstring>
3
+
4
+ namespace {
5
+
6
+ VALUE build_stub_launch_options(VALUE rb_options) {
7
+ VALUE result = rb_hash_new();
8
+
9
+ VALUE executable_path = rb_str_new_cstr("/usr/local/bin/camoufox");
10
+ rb_hash_aset(result, ID2SYM(rb_intern("executable_path")), executable_path);
11
+
12
+ VALUE args = rb_ary_new();
13
+ rb_hash_aset(result, ID2SYM(rb_intern("args")), args);
14
+
15
+ VALUE env = rb_hash_new();
16
+ rb_hash_aset(env, rb_str_new_cstr("CAMOU_CONFIG_1"), rb_str_new_cstr("{}"));
17
+ rb_hash_aset(result, ID2SYM(rb_intern("env")), env);
18
+
19
+ ID headless_id = rb_intern("headless");
20
+ VALUE headless_key = ID2SYM(headless_id);
21
+ VALUE headless_value = rb_hash_lookup(rb_options, headless_key);
22
+
23
+ if (NIL_P(headless_value)) {
24
+ headless_value = Qfalse;
25
+ } else {
26
+ headless_value = RTEST(headless_value) ? Qtrue : Qfalse;
27
+ }
28
+
29
+ rb_hash_aset(result, headless_key, headless_value);
30
+
31
+ return result;
32
+ }
33
+
34
+ VALUE build_cli_response(const char* command) {
35
+ if (strcmp(command, "path") == 0) {
36
+ return rb_str_new_cstr("/usr/local/share/camoufox\n");
37
+ }
38
+ if (strcmp(command, "version") == 0) {
39
+ return rb_str_new_cstr("Camoufox native stub v0.0.1\n");
40
+ }
41
+ if (strcmp(command, "fetch") == 0) {
42
+ return rb_str_new_cstr("Fetch command is not yet implemented in the native port.\n");
43
+ }
44
+ if (strcmp(command, "remove") == 0) {
45
+ return rb_str_new_cstr("Remove command is not yet implemented in the native port.\n");
46
+ }
47
+ return rb_str_new_cstr("Unknown command.\n");
48
+ }
49
+
50
+ VALUE native_launch_options(VALUE self, VALUE rb_options) {
51
+ return build_stub_launch_options(rb_options);
52
+ }
53
+
54
+ VALUE native_cli(int argc, VALUE* argv, VALUE self) {
55
+ if (argc < 1) {
56
+ rb_raise(rb_eArgError, "command required");
57
+ }
58
+ VALUE command_val = argv[0];
59
+ Check_Type(command_val, T_STRING);
60
+ const char* command = StringValueCStr(command_val);
61
+ return build_cli_response(command);
62
+ }
63
+
64
+ } // namespace
65
+
66
+ extern "C" void Init_camoufox_native() {
67
+ VALUE camoufox_module = rb_define_module("CamoufoxNative");
68
+ rb_define_module_function(camoufox_module, "launch_options", RUBY_METHOD_FUNC(native_launch_options), 1);
69
+ rb_define_module_function(camoufox_module, "run_cli", RUBY_METHOD_FUNC(native_cli), -1);
70
+ }
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+
5
+ extension_name = "camoufox_native"
6
+
7
+ # Enable C++ compilation
8
+ $CXXFLAGS << " -std=c++20 -Wall -Wextra"
9
+ $CFLAGS << " -std=c99"
10
+ $LDFLAGS << " "
11
+
12
+ create_makefile(extension_name)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module CLI
5
+ module_function
6
+
7
+ def run(command, args = [], env: {})
8
+ warn("[camoufox] Native CLI does not yet honour environment overrides") if env && !env.empty?
9
+ NativeBridge.run_cli(command, args)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Addons
5
+ DefaultAddons = [].freeze
6
+
7
+ module_function
8
+
9
+ def default_addons
10
+ DefaultAddons
11
+ end
12
+
13
+ def maybe_download_addons(_addons)
14
+ warn("[camoufox] addon management is not yet implemented in the native port")
15
+ end
16
+
17
+ def confirm_paths(_addons)
18
+ true
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module AsyncAPI
5
+ module_function
6
+
7
+ def new_browser(**launch_kwargs)
8
+ Utils.launch_options(**launch_kwargs)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ # Placeholder browserforge dataset for native Camoufox port.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ class Configuration
5
+ attr_accessor :data_dir, :cache_dir, :node_path, :playwright_driver_dir
6
+
7
+ def initialize
8
+ reset_defaults
9
+ end
10
+
11
+ def reset_defaults
12
+ @data_dir = ENV['CAMOUFOX_DATA_DIR']
13
+ @cache_dir = ENV['CAMOUFOX_CACHE_DIR']
14
+ @node_path = ENV['CAMOUFOX_NODE_PATH'] || 'node'
15
+ @playwright_driver_dir = ENV['CAMOUFOX_PLAYWRIGHT_DRIVER_DIR']
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ class Error < StandardError; end
5
+
6
+ class MissingNativeExtension < Error; end
7
+
8
+ class MissingPlaywrightDriver < Error; end
9
+
10
+ class NodeExecutionFailed < Error
11
+ attr_reader :status
12
+
13
+ def initialize(message, status)
14
+ super(message)
15
+ @status = status
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Fingerprints
5
+ module_function
6
+
7
+ def generate(_options = {})
8
+ warn("[camoufox] fingerprint generation is not yet implemented in the native port")
9
+ {}
10
+ end
11
+
12
+ def from_browserforge(_fingerprint, _ff_version)
13
+ warn("[camoufox] BrowserForge integration is not yet implemented")
14
+ {}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ {
2
+ "stub": []
3
+ }
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module IP
5
+ module_function
6
+
7
+ def public_ip(_proxy = nil)
8
+ warn("[camoufox] public IP resolution is not yet implemented")
9
+ "0.0.0.0"
10
+ end
11
+
12
+ def valid_ipv4(_value)
13
+ false
14
+ end
15
+
16
+ def valid_ipv6(_value)
17
+ false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ // Adapted from the Camoufox Python implementation
2
+ // Launches the Playwright browser server by invoking the undocumented launchServer API.
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ function resolveBrowserServerImpl() {
8
+ const cwd = process.cwd();
9
+ const candidate = path.join(cwd, 'lib', 'browserServerImpl.js');
10
+ try {
11
+ // eslint-disable-next-line global-require, import/no-dynamic-require
12
+ return require(candidate);
13
+ } catch (error) {
14
+ console.error('Unable to load Playwright browserServerImpl.js from', candidate);
15
+ console.error('Set CAMOUFOX_PLAYWRIGHT_DRIVER_DIR to the Playwright driver directory.');
16
+ process.exit(1);
17
+ }
18
+ }
19
+
20
+ const { BrowserServerLauncherImpl } = resolveBrowserServerImpl();
21
+
22
+ function collectData() {
23
+ return new Promise((resolve) => {
24
+ let data = '';
25
+ process.stdin.setEncoding('utf8');
26
+
27
+ process.stdin.on('data', (chunk) => {
28
+ data += chunk;
29
+ });
30
+
31
+ process.stdin.on('end', () => {
32
+ const buffer = Buffer.from(data, 'base64');
33
+ resolve(JSON.parse(buffer.toString()));
34
+ });
35
+ });
36
+ }
37
+
38
+ collectData()
39
+ .then((options) => {
40
+ if (options.executablePath && !fs.existsSync(options.executablePath)) {
41
+ console.warn(`camoufox: executable ${options.executablePath} not found, falling back to Playwright default`);
42
+ delete options.executablePath;
43
+ }
44
+
45
+ console.time('Server launched');
46
+ console.info('Launching server...');
47
+
48
+ const server = new BrowserServerLauncherImpl('firefox');
49
+
50
+ server
51
+ .launchServer(options)
52
+ .then((browserServer) => {
53
+ console.timeEnd('Server launched');
54
+ console.log('Websocket endpoint:\x1b[93m', browserServer.wsEndpoint(), '\x1b[0m');
55
+ process.stdin.resume();
56
+ })
57
+ .catch((error) => {
58
+ console.error('Error launching server:', error.message);
59
+ process.exit(1);
60
+ });
61
+ })
62
+ .catch((error) => {
63
+ console.error('Error collecting data:', error.message);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Locale
5
+ module_function
6
+
7
+ def handle_locales(_locales, config)
8
+ config
9
+ end
10
+
11
+ def geoip_allowed?
12
+ false
13
+ end
14
+
15
+ def download_mmdb
16
+ warn("[camoufox] GeoIP database download is not yet implemented")
17
+ end
18
+
19
+ def remove_mmdb
20
+ warn("[camoufox] GeoIP cleanup is not yet implemented")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module NativeBridge
5
+ module_function
6
+
7
+ def ensure_loaded!
8
+ return if defined?(@loaded) && @loaded
9
+
10
+ require 'camoufox_native'
11
+ @loaded = true
12
+ rescue LoadError => e
13
+ raise MissingNativeExtension, "camoufox_native extension is not available: #{e.message}"
14
+ end
15
+
16
+ def launch_options(**kwargs)
17
+ ensure_loaded!
18
+ CamoufoxNative.launch_options(kwargs)
19
+ end
20
+
21
+ def run_cli(command, args = [])
22
+ ensure_loaded!
23
+ CamoufoxNative.run_cli(command.to_s)
24
+ end
25
+
26
+ def available?
27
+ ensure_loaded!
28
+ true
29
+ rescue MissingNativeExtension
30
+ false
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Pkgman
5
+ InstallInfo = Struct.new(:path, :version, keyword_init: true)
6
+
7
+ module_function
8
+
9
+ def fetch_latest
10
+ warn("[camoufox] binary fetch is not yet implemented in the native port")
11
+ InstallInfo.new(path: "/usr/local/share/camoufox", version: "0.0.0")
12
+ end
13
+
14
+ def install
15
+ fetch_latest
16
+ end
17
+
18
+ def remove
19
+ warn("[camoufox] binary removal is not yet implemented")
20
+ false
21
+ end
22
+
23
+ def install_dir
24
+ "/usr/local/share/camoufox"
25
+ end
26
+
27
+ def version_string
28
+ "0.0.0"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+ require "open3"
6
+
7
+ module Camoufox
8
+ module Server
9
+ module_function
10
+
11
+ def launch(**kwargs)
12
+ launch_config = Utils.launch_options(**kwargs).to_h
13
+ payload = Base64.strict_encode64(JSON.generate(Utils.camelize_hash(launch_config)))
14
+
15
+ driver_dir = Camoufox.configuration.playwright_driver_dir
16
+ raise MissingPlaywrightDriver, missing_driver_message unless driver_dir && Dir.exist?(driver_dir)
17
+
18
+ node_path = Camoufox.configuration.node_path || 'node'
19
+ script_path = File.expand_path("launchServer.js", __dir__)
20
+
21
+ run_node_script(node_path, script_path, driver_dir, payload)
22
+ end
23
+
24
+ def missing_driver_message
25
+ 'Set CAMOUFOX_PLAYWRIGHT_DRIVER_DIR to the Playwright driver directory (contains lib/browserServerImpl.js)'
26
+ end
27
+
28
+ def run_node_script(node_path, script_path, working_dir, payload)
29
+ env = { 'CAMOUFOX_PLAYWRIGHT_DRIVER_DIR' => working_dir }
30
+ Open3.popen2e(env, node_path, script_path, chdir: working_dir) do |stdin, stdout_err, wait_thr|
31
+ stdin.write(payload)
32
+ stdin.close
33
+
34
+ stdout_err.each { |line| puts line }
35
+
36
+ status = wait_thr.value
37
+ return if status.success?
38
+
39
+ raise NodeExecutionFailed.new("Playwright server exited with status #{status.exitstatus}", status)
40
+ end
41
+ rescue Errno::ENOENT => e
42
+ raise NodeExecutionFailed.new("Failed to execute #{node_path}: #{e.message}", nil)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+ require "open3"
6
+
7
+ module Camoufox
8
+ module SyncAPI
9
+ class Camoufox
10
+ def self.open(**kwargs)
11
+ browser = new(**kwargs)
12
+ return browser unless block_given?
13
+
14
+ begin
15
+ yield browser
16
+ ensure
17
+ browser.close
18
+ end
19
+ end
20
+
21
+ def initialize(**kwargs)
22
+ @launch_options = Utils.launch_options(**kwargs).to_h
23
+ end
24
+
25
+ def new_page
26
+ Page.new(@launch_options)
27
+ end
28
+
29
+ def close
30
+ # Nothing to cleanup yet – placeholder for future native resources.
31
+ nil
32
+ end
33
+ end
34
+
35
+ class Page
36
+ attr_reader :title, :content
37
+
38
+ def initialize(launch_options)
39
+ @launch_options = launch_options
40
+ @title = nil
41
+ @content = nil
42
+ end
43
+
44
+ def goto(url)
45
+ result = NodeRunner.visit(@launch_options, url)
46
+ @title = result['title']
47
+ @content = result['content']&.to_s
48
+ self
49
+ end
50
+ end
51
+
52
+ module NodeRunner
53
+ module_function
54
+
55
+ def visit(launch_options, url)
56
+ node_path = ::Camoufox.configuration.node_path || 'node'
57
+ script_path = File.expand_path('visit.js', __dir__)
58
+
59
+ payload = Base64.strict_encode64(
60
+ JSON.generate(
61
+ options: Utils.camelize_hash(launch_options),
62
+ url: url,
63
+ ),
64
+ )
65
+
66
+ env = {}
67
+ if (driver_dir = ::Camoufox.configuration.playwright_driver_dir)
68
+ env['NODE_PATH'] = [driver_dir, ENV['NODE_PATH']].compact.join(File::PATH_SEPARATOR)
69
+ env['CAMOUFOX_PLAYWRIGHT_DRIVER_DIR'] = driver_dir
70
+ end
71
+
72
+ stdout, stderr, status = Open3.capture3(env, node_path, script_path, stdin_data: payload)
73
+
74
+ unless status.success?
75
+ message = stderr.empty? ? stdout : stderr
76
+ raise NodeExecutionFailed.new("Playwright visit failed: #{message.strip}", status)
77
+ end
78
+
79
+ JSON.parse(stdout)
80
+ rescue Errno::ENOENT => e
81
+ raise NodeExecutionFailed.new("Failed to execute #{node_path}: #{e.message}", nil)
82
+ rescue JSON::ParserError => e
83
+ raise NodeExecutionFailed.new("Invalid response from Playwright visit: #{e.message}", nil)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Utils
5
+ module_function
6
+
7
+ def launch_options(**kwargs)
8
+ LaunchOptions.new(NativeBridge.launch_options(**kwargs))
9
+ end
10
+
11
+ def camel_case(key)
12
+ segments = key.to_s.split('_')
13
+ return key.to_s if segments.length < 2
14
+
15
+ [segments.first, *segments[1..].map { |segment| segment[0].to_s.upcase + segment[1..].to_s }].join
16
+ end
17
+
18
+ def camelize_hash(hash)
19
+ camelize(hash)
20
+ end
21
+
22
+ def camelize(value)
23
+ case value
24
+ when Hash
25
+ value.each_with_object({}) do |(k, v), acc|
26
+ acc[camel_case(k)] = camelize(v)
27
+ end
28
+ when Array
29
+ value.map { |element| camelize(element) }
30
+ else
31
+ value
32
+ end
33
+ end
34
+ end
35
+
36
+ class LaunchOptions
37
+ attr_reader :raw
38
+
39
+ def initialize(raw_hash)
40
+ @raw = symbolize_top_level(raw_hash)
41
+ end
42
+
43
+ def to_h
44
+ raw.dup
45
+ end
46
+
47
+ private
48
+
49
+ def symbolize_top_level(hash)
50
+ hash.each_with_object({}) do |(key, value), acc|
51
+ sym_key = key.respond_to?(:to_sym) ? key.to_sym : key
52
+ acc[sym_key] = deep_dup(value)
53
+ end
54
+ end
55
+
56
+ def deep_dup(value)
57
+ case value
58
+ when Hash
59
+ value.each_with_object({}) do |(k, v), acc|
60
+ acc[k] = deep_dup(v)
61
+ end
62
+ when Array
63
+ value.map { |element| deep_dup(element) }
64
+ else
65
+ value
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Virtdisplay
5
+ module_function
6
+
7
+ def start(_debug: false)
8
+ warn("[camoufox] virtual display is not yet implemented")
9
+ nil
10
+ end
11
+
12
+ def stop
13
+ nil
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ function readStdinAsBase64() {
5
+ return new Promise((resolve, reject) => {
6
+ const chunks = [];
7
+ process.stdin.setEncoding('utf8');
8
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
9
+ process.stdin.on('end', () => resolve(chunks.join('')));
10
+ process.stdin.on('error', (error) => reject(error));
11
+ });
12
+ }
13
+
14
+ function loadPlaywright() {
15
+ const override = process.env.CAMOUFOX_PLAYWRIGHT_JS_REQUIRE;
16
+ if (override) {
17
+ return require(override);
18
+ }
19
+
20
+ try {
21
+ return require('playwright');
22
+ } catch (error) {
23
+ // fall through
24
+ }
25
+
26
+ const driverDir = process.env.CAMOUFOX_PLAYWRIGHT_DRIVER_DIR;
27
+ if (driverDir) {
28
+ try {
29
+ return require(path.join(driverDir, 'package'));
30
+ } catch (error) {
31
+ // fall through
32
+ }
33
+ }
34
+
35
+ console.error('Unable to require Playwright. Install the `playwright` npm package or set CAMOUFOX_PLAYWRIGHT_JS_REQUIRE.');
36
+ process.exit(1);
37
+ }
38
+
39
+ async function main() {
40
+ const payloadB64 = await readStdinAsBase64();
41
+ const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString());
42
+ const { options, url } = payload;
43
+
44
+ if (options.executablePath && !fs.existsSync(options.executablePath)) {
45
+ console.warn(`camoufox: executable ${options.executablePath} not found, falling back to Playwright default`);
46
+ delete options.executablePath;
47
+ }
48
+
49
+ const playwright = loadPlaywright();
50
+ const browserType = playwright.firefox;
51
+ if (!browserType) {
52
+ console.error('Playwright module does not expose `firefox`.');
53
+ process.exit(1);
54
+ }
55
+
56
+ const browser = await browserType.launch(options);
57
+ const page = await browser.newPage();
58
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
59
+ try {
60
+ await page.waitForLoadState('networkidle', { timeout: 15000 });
61
+ } catch (error) {
62
+ console.warn(`camoufox: waitForLoadState(networkidle) warning: ${error.message || error}`);
63
+ }
64
+
65
+ const [title, content] = await Promise.all([
66
+ page.title(),
67
+ page.content(),
68
+ ]);
69
+
70
+ console.log(JSON.stringify({ title, content }));
71
+ await browser.close();
72
+ }
73
+
74
+ main().catch((error) => {
75
+ console.error(error.message || error);
76
+ process.exit(1);
77
+ });
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camoufox
4
+ module Warnings
5
+ module_function
6
+
7
+ def warn(feature, message = nil)
8
+ note = message || "#{feature} is not yet implemented"
9
+ Kernel.warn("[camoufox] #{note}")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ # Placeholder warnings configuration for the Camoufox native port.
@@ -0,0 +1 @@
1
+ Placeholder WebGL dataset for the Camoufox native port.
data/lib/camoufox.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "camoufox/__version__"
4
+ require_relative "camoufox/exceptions"
5
+ require_relative "camoufox/configuration"
6
+ # Core helpers
7
+ require_relative "camoufox/utils"
8
+ require_relative "camoufox/addons"
9
+ require_relative "camoufox/fingerprints"
10
+ require_relative "camoufox/ip"
11
+ require_relative "camoufox/locale"
12
+ require_relative "camoufox/pkgman"
13
+ require_relative "camoufox/sync_api"
14
+ require_relative "camoufox/async_api"
15
+ require_relative "camoufox/server"
16
+ require_relative "camoufox/virtdisplay"
17
+ require_relative "camoufox/warnings"
18
+ require_relative "camoufox/native_bridge"
19
+ require_relative "camoufox/__main__"
20
+
21
+ module Camoufox
22
+ class << self
23
+ def configure
24
+ yield configuration
25
+ end
26
+
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def reset_configuration!
32
+ @configuration = Configuration.new
33
+ end
34
+
35
+ def launch_options(**kwargs)
36
+ Utils.launch_options(**kwargs)
37
+ end
38
+
39
+ def fetch(update_browserforge: false, env: {})
40
+ CLI.run("fetch", update_browserforge ? ["--browserforge"] : [], env: env)
41
+ end
42
+
43
+ def remove(env: {})
44
+ CLI.run("remove", [], env: env)
45
+ end
46
+
47
+ def path(env: {})
48
+ CLI.run("path", [], env: env).strip
49
+ end
50
+
51
+ def version(env: {})
52
+ CLI.run("version", [], env: env)
53
+ end
54
+ end
55
+ end
Binary file
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: camoufox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Camoufox contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-11-03 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.12'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.12'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.60'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.60'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake-compiler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.2'
68
+ description: Reimplements the pythonlib/camoufox package structure in Ruby with a
69
+ native extension stub while the full feature set is ported over.
70
+ email:
71
+ - opensource@camoufox.dev
72
+ executables:
73
+ - camoufox
74
+ extensions:
75
+ - ext/camoufox_native/extconf.rb
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - Gemfile
80
+ - LICENSE
81
+ - README.md
82
+ - bin/camoufox
83
+ - docs/native_port.md
84
+ - ext/camoufox_native/camoufox_native.cpp
85
+ - ext/camoufox_native/extconf.rb
86
+ - lib/camoufox.rb
87
+ - lib/camoufox/__main__.rb
88
+ - lib/camoufox/__version__.rb
89
+ - lib/camoufox/addons.rb
90
+ - lib/camoufox/async_api.rb
91
+ - lib/camoufox/browserforge.yml
92
+ - lib/camoufox/configuration.rb
93
+ - lib/camoufox/exceptions.rb
94
+ - lib/camoufox/fingerprints.rb
95
+ - lib/camoufox/fonts.json
96
+ - lib/camoufox/ip.rb
97
+ - lib/camoufox/launchServer.js
98
+ - lib/camoufox/locale.rb
99
+ - lib/camoufox/native_bridge.rb
100
+ - lib/camoufox/pkgman.rb
101
+ - lib/camoufox/server.rb
102
+ - lib/camoufox/sync_api.rb
103
+ - lib/camoufox/utils.rb
104
+ - lib/camoufox/virtdisplay.rb
105
+ - lib/camoufox/visit.js
106
+ - lib/camoufox/warnings.rb
107
+ - lib/camoufox/warnings.yml
108
+ - lib/camoufox/webgl/README.md
109
+ - lib/camoufox_native.bundle
110
+ homepage: https://github.com/daijro/camoufox
111
+ licenses:
112
+ - MIT
113
+ metadata:
114
+ homepage_uri: https://github.com/daijro/camoufox
115
+ source_code_uri: https://github.com/daijro/camoufox-ruby
116
+ changelog_uri: https://github.com/daijro/camoufox-ruby/blob/main/CHANGELOG.md
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubygems_version: 3.6.2
132
+ specification_version: 4
133
+ summary: Native rewrite of the Camoufox stealth Firefox toolkit
134
+ test_files: []