Skip to content

Commit 38660da

Browse files
committed
Fix launching Snap-based Firefox
1 parent aaac0db commit 38660da

File tree

5 files changed

+74
-29
lines changed

5 files changed

+74
-29
lines changed

src/browsers.ts

+7
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ export const getAvailableBrowsers = async (configPath: string) => {
8181
return (await getLauncher(configPath)).browsers;
8282
};
8383

84+
export const getBrowserDetails = async (configPath: string, variant: string): Promise<Browser | undefined> => {
85+
const browsers = await getAvailableBrowsers(configPath);
86+
87+
// Get the details for the first matching browsers that is installed:
88+
return browsers.find(b => b.name === variant);
89+
};
90+
8491
export { LaunchOptions };
8592

8693
export const launchBrowser = async (url: string, options: LaunchOptions, configPath: string) => {

src/interceptors/chromium-based-interceptors.ts

+5-13
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import { generateSPKIFingerprint } from 'mockttp';
44
import { HtkConfig } from '../config';
55

66
import {
7-
getAvailableBrowsers,
7+
getBrowserDetails,
88
launchBrowser,
99
BrowserInstance,
10-
Browser,
1110
LaunchOptions
1211
} from '../browsers';
1312
import { delay } from '../util/promise';
@@ -18,13 +17,6 @@ import { Interceptor } from '.';
1817
import { logError } from '../error-tracking';
1918
import { WEBEXTENSION_INSTALL } from '../webextension';
2019

21-
const getBrowserDetails = async (config: HtkConfig, variant: string): Promise<Browser | undefined> => {
22-
const browsers = await getAvailableBrowsers(config.configPath);
23-
24-
// Get the details for the first of these browsers that is installed.
25-
return _.find(browsers, b => b.name === variant);
26-
};
27-
2820
const getChromiumLaunchOptions = async (
2921
browser: string,
3022
config: HtkConfig,
@@ -91,7 +83,7 @@ abstract class FreshChromiumBasedInterceptor implements Interceptor {
9183
}
9284

9385
async isActivable() {
94-
const browserDetails = await getBrowserDetails(this.config, this.variantName)
86+
const browserDetails = await getBrowserDetails(this.config.configPath, this.variantName)
9587
return !!browserDetails;
9688
}
9789

@@ -101,7 +93,7 @@ abstract class FreshChromiumBasedInterceptor implements Interceptor {
10193
const hideWarningServer = new HideWarningServer(this.config);
10294
await hideWarningServer.start('https://amiusing.httptoolkit.tech');
10395

104-
const browserDetails = await getBrowserDetails(this.config, this.variantName);
96+
const browserDetails = await getBrowserDetails(this.config.configPath, this.variantName);
10597

10698
const browser = await launchBrowser(hideWarningServer.hideWarningUrl,
10799
await getChromiumLaunchOptions(
@@ -186,7 +178,7 @@ abstract class ExistingChromiumBasedInterceptor implements Interceptor {
186178
) { }
187179

188180
async browserDetails() {
189-
return getBrowserDetails(this.config, this.variantName);
181+
return getBrowserDetails(this.config.configPath, this.variantName);
190182
}
191183

192184
isActive(proxyPort: number | string) {
@@ -273,7 +265,7 @@ abstract class ExistingChromiumBasedInterceptor implements Interceptor {
273265
}
274266
}
275267

276-
const browserDetails = await getBrowserDetails(this.config, this.variantName);
268+
const browserDetails = await getBrowserDetails(this.config.configPath, this.variantName);
277269
const launchOptions = await getChromiumLaunchOptions(
278270
browserDetails ? browserDetails.name : this.variantName,
279271
this.config,

src/interceptors/fresh-firefox.ts

+21-15
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { APP_ROOT } from '../constants';
66
import { HtkConfig } from '../config';
77
import { logError } from '../error-tracking';
88

9-
import { getAvailableBrowsers, launchBrowser, BrowserInstance } from '../browsers';
109
import { delay } from '../util/promise';
1110
import { isErrorLike } from '../util/error';
11+
import { isSnap, getSnapConfigPath } from '../util/snap';
12+
13+
import { launchBrowser, BrowserInstance, getBrowserDetails } from '../browsers';
1214
import { readFile, canAccess, deleteFolder } from '../util/fs';
1315
import { windowsKill, spawnToResult } from '../util/process-management';
1416
import { MessageServer } from '../message-server';
@@ -73,23 +75,20 @@ export class FreshFirefox implements Interceptor {
7375

7476
constructor(private config: HtkConfig) { }
7577

76-
private readonly firefoxProfilePath = path.join(this.config.configPath, 'firefox-profile');
77-
7878
isActive(proxyPort: number | string) {
7979
return browsers[proxyPort] != null && !!browsers[proxyPort].pid;
8080
}
8181

8282
async isActivable() {
83-
const availableBrowsers = await getAvailableBrowsers(this.config.configPath);
84-
85-
const firefoxBrowser = _.find(availableBrowsers, { name: 'firefox' });
83+
const firefoxBrowser = await getBrowserDetails(this.config.configPath, 'firefox');
8684

8785
return !!firefoxBrowser && // Must have Firefox installed
8886
parseInt(firefoxBrowser.version.split('.')[0], 0) >= 58 && // Must use cert9.db
8987
await getCertutilCommand().then(() => true).catch(() => false) // Must have certutil available
9088
}
9189

9290
async startFirefox(
91+
profilePath: string,
9392
initialServer: MessageServer | CertCheckServer,
9493
proxyPort?: number,
9594
existingPrefs = {}
@@ -98,7 +97,7 @@ export class FreshFirefox implements Interceptor {
9897

9998
const browser = await launchBrowser(initialUrl, {
10099
browser: 'firefox',
101-
profile: this.firefoxProfilePath,
100+
profile: profilePath,
102101
proxy: proxyPort ? `127.0.0.1:${proxyPort}` : undefined,
103102
prefs: _.assign(
104103
existingPrefs,
@@ -184,7 +183,7 @@ export class FreshFirefox implements Interceptor {
184183
}
185184

186185
// Create the profile. We need to run FF to do its setup, then close it & edit more ourselves.
187-
async setupFirefoxProfile() {
186+
async setupFirefoxProfile(profilePath: string) {
188187
const messageServer = new MessageServer(
189188
this.config,
190189
`HTTP Toolkit is preparing a Firefox profile, please wait...`
@@ -193,15 +192,15 @@ export class FreshFirefox implements Interceptor {
193192

194193
let messageShown: Promise<void> | true = messageServer.waitForSuccess().catch(logError);
195194

196-
profileSetupBrowser = await this.startFirefox(messageServer);
195+
profileSetupBrowser = await this.startFirefox(profilePath, messageServer);
197196
profileSetupBrowser.process.once('close', (exitCode) => {
198197
console.log("Profile setup Firefox closed");
199198
messageServer.stop();
200199
profileSetupBrowser = undefined;
201200

202201
if (messageShown !== true) {
203202
logError(`Firefox profile setup failed with code ${exitCode}`);
204-
deleteFolder(this.firefoxProfilePath).catch(console.warn);
203+
deleteFolder(profilePath).catch(console.warn);
205204
}
206205
});
207206

@@ -222,7 +221,7 @@ export class FreshFirefox implements Interceptor {
222221
const certUtilResult = await spawnToResult(
223222
certutil.command, [
224223
'-A',
225-
'-d', `sql:${this.firefoxProfilePath}`,
224+
'-d', `sql:${profilePath}`,
226225
'-t', 'C,,',
227226
'-i', this.config.https.certPath,
228227
'-n', 'HTTP Toolkit'
@@ -240,7 +239,14 @@ export class FreshFirefox implements Interceptor {
240239
async activate(proxyPort: number) {
241240
if (this.isActive(proxyPort) || !!profileSetupBrowser) return;
242241

243-
const firefoxPrefsFile = path.join(this.firefoxProfilePath, 'prefs.js');
242+
const browserDetails = await getBrowserDetails(this.config.configPath, 'firefox');
243+
if (!browserDetails) throw new Error('Firefox could not be detected');
244+
245+
const profilePath = await isSnap(browserDetails.command)
246+
? path.join(getSnapConfigPath('firefox'), 'profile')
247+
: path.join(this.config.configPath, 'firefox-profile');
248+
249+
const firefoxPrefsFile = path.join(profilePath, 'prefs.js');
244250

245251
let existingPrefs: _.Dictionary<any> = {};
246252

@@ -250,7 +256,7 @@ export class FreshFirefox implements Interceptor {
250256
This helps avoid initial Firefox profile setup request noise, and tidies up some awkward UX where
251257
firefox likes to open extra welcome windows/tabs on first run.
252258
*/
253-
await this.setupFirefoxProfile();
259+
await this.setupFirefoxProfile(profilePath);
254260
}
255261

256262
// We need to preserve & reuse any existing preferences, to avoid issues
@@ -272,7 +278,7 @@ export class FreshFirefox implements Interceptor {
272278
const certCheckServer = new CertCheckServer(this.config);
273279
await certCheckServer.start("https://amiusing.httptoolkit.tech");
274280

275-
const browser = await this.startFirefox(certCheckServer, proxyPort, existingPrefs);
281+
const browser = await this.startFirefox(profilePath, certCheckServer, proxyPort, existingPrefs);
276282

277283
let certCheckSuccessful: boolean | undefined;
278284
certCheckServer.waitForSuccess().then(() => {
@@ -300,7 +306,7 @@ export class FreshFirefox implements Interceptor {
300306
? "failed"
301307
: "did not complete"
302308
} with FF exit code ${exitCode}`);
303-
deleteFolder(this.firefoxProfilePath).catch(console.warn);
309+
deleteFolder(profilePath).catch(console.warn);
304310
}
305311
});
306312

src/util/fs.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ export const deleteFolder = promisify(rimraf);
5656
export const ensureDirectoryExists = (path: string) =>
5757
checkAccess(path).catch(() => mkDir(path, { recursive: true }));
5858

59+
export const resolveCommandPath = (path: string): Promise<string | undefined> =>
60+
lookpath(path);
61+
5962
export const commandExists = (path: string): Promise<boolean> =>
60-
lookpath(path).then((result) => result !== undefined);
63+
resolveCommandPath(path).then((result) => result !== undefined);
6164

6265
export const createTmp = (options: tmp.Options = {}) => new Promise<{
6366
path: string,

src/util/snap.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import os = require('os');
2+
import path = require('path');
3+
4+
import fs = require('./fs');
5+
6+
export async function isSnap(bin: string) {
7+
if (os.platform() !== 'linux') return false;
8+
9+
const binPath = await fs.resolveCommandPath(bin);
10+
if (!binPath) {
11+
throw new Error(`Can't resolve command ${bin}`);
12+
}
13+
14+
// Most snaps directly run from the Snap bin folder:
15+
if (binPath.startsWith('/snap/bin/')) return true;
16+
17+
// Firefox is the only known example that doesn't - it uses a
18+
// wrapper script, so we just look for that:
19+
if (binPath === '/usr/bin/firefox') {
20+
const content = await fs.readFile(binPath);
21+
return content.includes('exec /snap/bin/firefox');
22+
}
23+
24+
return false;
25+
}
26+
27+
// For all Snaps, any data we want to inject needs to live inside the
28+
// Snap's data directory - we put it in a .httptoolkit folder.
29+
export const getSnapConfigPath = (appName: string) => {
30+
return path.join(
31+
os.homedir(),
32+
'snap',
33+
appName,
34+
'current',
35+
'.httptoolkit'
36+
);
37+
}

0 commit comments

Comments
 (0)