Skip to content

Commit 443e1cd

Browse files
committed
feat: add --marionette-host for remote/tunneled Firefox
Adds a --marionette-host CLI parameter (and MARIONETTE_HOST env var) that controls which host geckodriver connects to for Marionette and where the BiDi WebSocket connection is directed. When connecting to a remote or tunneled Firefox (e.g., via SSH port forwarding), the Marionette and BiDi ports are on a different host than localhost. This parameter lets geckodriver reach the tunnel endpoint while preserving the original Host header for Firefox's WebSocket allowHosts check.
1 parent ae0f07a commit 443e1cd

3 files changed

Lines changed: 22 additions & 6 deletions

File tree

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ export const cliOptions = {
110110
description: 'Marionette port to connect to when using --connect-existing (default: 2828)',
111111
default: Number(process.env.MARIONETTE_PORT ?? '2828'),
112112
},
113+
marionetteHost: {
114+
type: 'string',
115+
description: 'Marionette host to connect to when using --connect-existing (default: 127.0.0.1). Also used as the BiDi WebSocket connect address when different from 127.0.0.1.',
116+
default: process.env.MARIONETTE_HOST ?? '127.0.0.1',
117+
},
113118
env: {
114119
type: 'array',
115120
description:

src/firefox/core.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class GeckodriverHttpDriver implements IDriver {
140140
this.webSocketUrl = webSocketUrl;
141141
}
142142

143-
static async connect(marionettePort: number): Promise<GeckodriverHttpDriver> {
143+
static async connect(marionettePort: number, marionetteHost = '127.0.0.1'): Promise<GeckodriverHttpDriver> {
144144
// Find geckodriver binary via selenium-manager
145145
const path = await import('node:path');
146146
const { execFileSync } = await import('node:child_process');
@@ -179,7 +179,7 @@ class GeckodriverHttpDriver implements IDriver {
179179
// Use --port=0 to let the OS assign a free port atomically (geckodriver ≥0.34.0)
180180
const gd = spawn(
181181
geckodriverPath,
182-
['--connect-existing', '--marionette-port', String(marionettePort), '--port', '0'],
182+
['--connect-existing', '--marionette-host', marionetteHost, '--marionette-port', String(marionettePort), '--port', '0'],
183183
{ stdio: ['ignore', 'pipe', 'pipe'] }
184184
);
185185

@@ -223,7 +223,14 @@ class GeckodriverHttpDriver implements IDriver {
223223
throw new Error(`Failed to create session: ${JSON.stringify(json)}`);
224224
}
225225

226-
const wsUrl = json.value.capabilities.webSocketUrl as string | undefined;
226+
let wsUrl = json.value.capabilities.webSocketUrl as string | undefined;
227+
logDebug(`Session capabilities webSocketUrl: ${wsUrl ?? 'not present'}, marionetteHost: ${marionetteHost}`);
228+
if (wsUrl && marionetteHost !== '127.0.0.1') {
229+
// Rewrite the URL to connect through the remote host / tunnel.
230+
const parsed = new URL(wsUrl);
231+
parsed.hostname = marionetteHost;
232+
wsUrl = parsed.toString();
233+
}
227234
if (wsUrl) {
228235
logDebug(`BiDi WebSocket URL: ${wsUrl}`);
229236
} else {
@@ -476,9 +483,11 @@ class GeckodriverHttpDriver implements IDriver {
476483
const ws = new WebSocket(this.webSocketUrl);
477484
await new Promise<void>((resolve, reject) => {
478485
ws.on('open', resolve);
479-
ws.on('error', reject);
486+
ws.on('error', (e: any) => {
487+
const msg = e?.message || e?.error?.message || e?.error || e?.type || JSON.stringify(e) || String(e);
488+
reject(new Error(`BiDi WS to ${this.webSocketUrl}: ${msg}`));
489+
});
480490
});
481-
logDebug('BiDi WebSocket connected');
482491

483492
let cmdId = 0;
484493
const subscribe = async (event: string, contexts?: string[]): Promise<void> => {
@@ -578,7 +587,8 @@ export class FirefoxCore {
578587
// We bypass selenium-webdriver because its BiDi auto-upgrade hangs
579588
// when used with geckodriver's --connect-existing mode.
580589
const port = this.options.marionettePort ?? 2828;
581-
this.driver = await GeckodriverHttpDriver.connect(port);
590+
const host = this.options.marionetteHost ?? '127.0.0.1';
591+
this.driver = await GeckodriverHttpDriver.connect(port, host);
582592
} else {
583593
// Set up output file for capturing Firefox stdout/stderr
584594
if (this.options.logFile) {

src/firefox/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface FirefoxLaunchOptions {
6060
acceptInsecureCerts?: boolean | undefined;
6161
connectExisting?: boolean | undefined;
6262
marionettePort?: number | undefined;
63+
marionetteHost?: string | undefined;
6364
env?: Record<string, string> | undefined;
6465
logFile?: string | undefined;
6566
/** Firefox preferences to set at startup via moz:firefoxOptions */

0 commit comments

Comments
 (0)