Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit a178617

Browse files
committed
Fix WebRTC bug in production installations only, due to config perms
When running locally, or running in an updated server environment, the webextension folder, including its config, is user writable. Unfortunately, when running in a fresh production install, that's not true: the folder is usually administrator/root-owned, and so cannot be modified by users, so our config injection mechanism didn't work. To fix this, we now 'install' the webextension on first use, copying it (~10 files total, 1MB) into a temp directory, which can then be modified, and deleted again when the app exits.
1 parent fee7f2b commit a178617

File tree

3 files changed

+64
-9
lines changed

3 files changed

+64
-9
lines changed

src/interceptors/chromium-based-interceptors.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { listRunningProcesses, windowsClose, waitForExit } from '../util/process
1616
import { HideWarningServer } from '../hide-warning-server';
1717
import { Interceptor } from '.';
1818
import { reportError } from '../error-tracking';
19-
import { WEBEXTENSION_PATH } from '../webextension';
19+
import { WEBEXTENSION_INSTALL } from '../webextension';
2020

2121
const getBrowserDetails = async (config: HtkConfig, variant: string): Promise<Browser | undefined> => {
2222
const browsers = await getAvailableBrowsers(config.configPath);
@@ -59,12 +59,12 @@ const getChromiumLaunchOptions = async (
5959
'--disable-background-networking',
6060
'--disable-component-update',
6161
'--check-for-update-interval=31536000', // Don't update for a year
62-
...(webExtensionEnabled
62+
...(webExtensionEnabled && WEBEXTENSION_INSTALL
6363
// Install HTTP Toolkit's extension, for advanced hook setup. Feature
6464
// flagged for now as it's still new & largely untested.
6565
? [
6666

67-
`--load-extension=${WEBEXTENSION_PATH}`
67+
`--load-extension=${WEBEXTENSION_INSTALL.path}`
6868
]
6969
: []
7070
)

src/util/fs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ export const writeFile = promisify(fs.writeFile);
1919
export const renameFile = promisify(fs.rename);
2020
export const copyFile = promisify(fs.copyFile);
2121

22+
export const copyRecursive = async (from: string, to: string) => {
23+
// fs.cp is only available in Node 16.7.0+
24+
if (!fs.cp) throw new Error("fs.cp not available");
25+
26+
return new Promise<void>((resolve, reject) => {
27+
fs.cp(from, to, { recursive: true }, (err) => {
28+
if (err) reject(err);
29+
else resolve();
30+
});
31+
});
32+
};
33+
2234
export const canAccess = (path: string) => checkAccess(path).then(() => true).catch(() => false);
2335

2436
// Takes a path, follows any links present (if possible) until we reach a non-link file. This

src/webextension.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
11
import * as path from 'path';
2+
import * as os from 'os';
3+
4+
import { deleteFile, mkDir, writeFile, copyRecursive, deleteFolder } from "./util/fs";
5+
import { addShutdownHandler } from './shutdown';
6+
27
import { OVERRIDES_DIR } from './interceptors/terminal/terminal-env-overrides';
38

4-
import { deleteFile, mkDir, writeFile } from "./util/fs";
9+
const WEBEXTENSION_BASE_PATH = path.join(OVERRIDES_DIR, 'webextension');
10+
11+
// We copy the WebExtension to a temp directory the first time it's activated, so that we can
12+
// modify the config folder within to easily inject config into the extension. Without this,
13+
// the extension is usually in the app install directory, which is often not user-writable.
14+
// We only make one copy for all sessions, but we later inject independent per-session
15+
// config files, so they can behave independently.
16+
export let WEBEXTENSION_INSTALL: {
17+
path: string;
18+
configPath: string;
19+
} | undefined;
20+
async function ensureWebExtensionInstalled() {
21+
if (WEBEXTENSION_INSTALL) return; // No-op after the first install
22+
else {
23+
const tmpDir = os.tmpdir();
524

6-
export const WEBEXTENSION_PATH = path.join(OVERRIDES_DIR, 'webextension');
7-
const WEBEXTENSION_CONFIG_PATH = path.join(WEBEXTENSION_PATH, 'config');
25+
const webExtensionPath = path.join(tmpDir, 'httptoolkit-webextension');
26+
const configPath = path.join(webExtensionPath, 'config');
27+
28+
await copyRecursive(WEBEXTENSION_BASE_PATH, webExtensionPath);
29+
await mkDir(configPath);
30+
31+
WEBEXTENSION_INSTALL = { path: webExtensionPath, configPath };
32+
console.log(`Webextension installed at ${WEBEXTENSION_INSTALL.path}`);
33+
}
34+
}
35+
36+
// On shutdown, we delete the webextension install again.
37+
addShutdownHandler(async () => {
38+
if (WEBEXTENSION_INSTALL) {
39+
console.log(`Uninstalling webextension from ${WEBEXTENSION_INSTALL.path}`);
40+
await deleteFolder(WEBEXTENSION_INSTALL.path);
41+
WEBEXTENSION_INSTALL = undefined;
42+
}
43+
});
844

945
interface WebExtensionConfig { // Should match config in the WebExtension itself
1046
mockRtc: {
@@ -17,9 +53,11 @@ const getConfigKey = (proxyPort: number) =>
1753
`127_0_0_1.${proxyPort}`; // Filename-safe proxy address
1854

1955
const getConfigPath = (proxyPort: number) =>
20-
path.join(WEBEXTENSION_CONFIG_PATH, getConfigKey(proxyPort));
56+
path.join(WEBEXTENSION_INSTALL!.configPath, getConfigKey(proxyPort));
2157

2258
export function clearWebExtensionConfig(httpProxyPort: number) {
59+
if (!WEBEXTENSION_INSTALL) return;
60+
2361
return deleteFile(getConfigPath(httpProxyPort))
2462
.catch(() => {}); // We ignore errors - nothing we can do, not very important.
2563
}
@@ -30,6 +68,8 @@ export async function updateWebExtensionConfig(
3068
webRTCEnabled: boolean
3169
) {
3270
if (webRTCEnabled) {
71+
await ensureWebExtensionInstalled();
72+
3373
const adminBaseUrl = `http://internal.httptoolkit.localhost:45456/session/${sessionId}`;
3474
await writeConfig(httpProxyPort, {
3575
mockRtc: {
@@ -38,11 +78,14 @@ export async function updateWebExtensionConfig(
3878
}
3979
});
4080
} else {
41-
await writeConfig(httpProxyPort, { mockRtc: false });
81+
if (WEBEXTENSION_INSTALL) {
82+
// If the extension is set up, but this specific session has it disabled, we
83+
// make the config explicitly disable it, just to be clear:
84+
await writeConfig(httpProxyPort, { mockRtc: false });
85+
}
4286
}
4387
}
4488

4589
async function writeConfig(proxyPort: number, config: WebExtensionConfig) {
46-
await mkDir(WEBEXTENSION_CONFIG_PATH).catch(() => {}); // Make sure the config dir exists
4790
return writeFile(getConfigPath(proxyPort), JSON.stringify(config));
4891
}

0 commit comments

Comments
 (0)