Skip to content

Commit 454eb2a

Browse files
authored
Merge pull request rancher-sandbox#8286 from mook-as/rdx/disable-cors
RDX: Bypass CORS
2 parents 8cbd964 + fd1daec commit 454eb2a

8 files changed

Lines changed: 124 additions & 39 deletions

File tree

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ Object.assign(module.exports.rules, {
238238

239239
// destructuring: don't error if `a` is reassigned, but `b` is never reassigned
240240
'prefer-const': ['error', { destructuring: 'all' }],
241+
242+
// This one assumes all callbacks have errors in the first argument, which isn't likely.
243+
'n/no-callback-literal': 'off',
241244
});
242245
module.exports.rules['key-spacing'][1].align.mode = 'strict';
243246

.github/actions/spelling/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ poweroff
658658
PQgrl
659659
prakhar
660660
prebuilds
661+
preflights
661662
Privs
662663
PROCARGS
663664
procnet

background.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,12 @@ mainEvents.handle('settings-fetch', () => {
183183
Electron.protocol.registerSchemesAsPrivileged([{ scheme: 'app' }, {
184184
scheme: 'x-rd-extension',
185185
privileges: {
186-
standard: true,
187-
supportFetchAPI: true,
186+
standard: true,
187+
secure: true,
188+
bypassCSP: true,
189+
allowServiceWorkers: true,
190+
supportFetchAPI: true,
191+
corsEnabled: true,
188192
},
189193
}]);
190194

pkg/rancher-desktop/main/extensions/extensions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ export class ExtensionImpl implements Extension {
287287
console.error(`Ignoring error running ${ this.id } post-install script: ${ ex }`);
288288
}
289289

290+
// Since we now run extensions in a separate session, register the protocol handler there.
291+
const encodedId = Buffer.from(this.id).toString('hex');
292+
293+
await mainEvents.invoke('extensions/register-protocol', `persist:rdx-${ encodedId }`);
294+
290295
console.debug(`Install ${ this.id }: install complete.`);
291296

292297
return true;

pkg/rancher-desktop/main/mainEvents.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ interface MainEventNames {
130130
*/
131131
'extensions/shutdown'(): Promise<void>;
132132

133+
/**
134+
* Register the extension protocol handler in the given webContents partition.
135+
* @param partition The partition name; likely "persist:rdx-..."
136+
*/
137+
'extensions/register-protocol'(partition: string): Promise<void>;
138+
133139
/**
134140
* Emitted on application quit, used to shut down any integrations. This
135141
* requires feedback from the handler to know when all tasks are complete.

pkg/rancher-desktop/main/networking/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,15 @@ export default async function setupNetworking() {
6262
pluginDevUrls.some(x => url.startsWith(x))
6363
) {
6464
event.preventDefault();
65-
// eslint-disable-next-line n/no-callback-literal
65+
6666
callback(true);
6767

6868
return;
6969
}
7070

7171
if (dashboardUrls.some(x => url.startsWith(x)) && 'dashboard' in windowMapping) {
7272
event.preventDefault();
73-
// eslint-disable-next-line n/no-callback-literal
73+
7474
callback(true);
7575

7676
return;
@@ -87,7 +87,7 @@ export default async function setupNetworking() {
8787
// an attacker generating a cert with the same serial.
8888
if (cert === certificate.data.replace(/\r/g, '')) {
8989
console.log(`Accepting system certificate for ${ certificate.subjectName } (${ certificate.fingerprint })`);
90-
// eslint-disable-next-line n/no-callback-literal
90+
9191
callback(true);
9292

9393
return;
@@ -100,7 +100,6 @@ export default async function setupNetworking() {
100100

101101
console.log(`Not handling certificate error ${ error } for ${ url }`);
102102

103-
// eslint-disable-next-line n/no-callback-literal
104103
callback(false);
105104
});
106105

pkg/rancher-desktop/utils/protocols.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import path from 'path';
22
import { URL, pathToFileURL } from 'url';
33

4-
import { app, protocol, net } from 'electron';
4+
import Electron from 'electron';
55

6+
import mainEvents from '@pkg/main/mainEvents';
67
import { isDevBuild } from '@pkg/utils/environment';
78
import Latch from '@pkg/utils/latch';
89
import Logging from '@pkg/utils/logging';
@@ -20,8 +21,8 @@ function redirectedUrl(relPath: string) {
2021
if (isDevBuild) {
2122
return `http://localhost:8888${ relPath }`;
2223
}
23-
if (app.isPackaged) {
24-
return path.join(app.getAppPath(), 'dist', 'app', relPath);
24+
if (Electron.app.isPackaged) {
25+
return path.join(Electron.app.getAppPath(), 'dist', 'app', relPath);
2526
}
2627

2728
// Unpackaged non-dev build; this normally means E2E tests, where
@@ -41,17 +42,17 @@ export const protocolsRegistered = Latch();
4142
* production environments.
4243
*/
4344
function setupAppProtocolHandler() {
44-
protocol.handle(
45+
Electron.protocol.handle(
4546
'app',
4647
(request) => {
4748
const relPath = new URL(request.url).pathname;
4849
const redirectUrl = redirectedUrl(relPath);
4950

5051
if (isDevBuild) {
51-
return net.fetch(redirectUrl);
52+
return Electron.net.fetch(redirectUrl);
5253
}
5354

54-
return net.fetch(pathToFileURL(redirectUrl).toString());
55+
return Electron.net.fetch(pathToFileURL(redirectUrl).toString());
5556
});
5657
}
5758

@@ -62,26 +63,36 @@ function setupAppProtocolHandler() {
6263
* x-rd-extension://<extension id>/...
6364
* Where the extension id is the extension image id, hex encoded (to avoid
6465
* issues with slashes). Base64 was not available in Vue.
66+
* @param partition The Electron session partition name; if unset, set it up for
67+
* the default session.
6568
*/
66-
function setupExtensionProtocolHandler() {
67-
protocol.handle(
68-
'x-rd-extension',
69-
(request) => {
70-
const url = new URL(request.url);
71-
// Re-encoding the extension ID here also ensures it doesn't contain any
72-
// directory traversal etc. issues.
73-
const extensionID = Buffer.from(url.hostname, 'hex').toString('base64url');
74-
const resourcePath = path.normalize(url.pathname);
75-
const filepath = path.join(paths.extensionRoot, extensionID, resourcePath);
69+
function setupExtensionProtocolHandler(partition?: string): Promise<void> {
70+
const scheme = 'x-rd-extension';
71+
const session = partition ? Electron.session.fromPartition(partition) : Electron.session.defaultSession;
7672

77-
return net.fetch(pathToFileURL(filepath).toString());
78-
});
73+
if (!session.protocol.isProtocolHandled(scheme)) {
74+
session.protocol.handle(
75+
scheme,
76+
(request) => {
77+
const url = new URL(request.url);
78+
// Re-encoding the extension ID here also ensures it doesn't contain any
79+
// directory traversal etc. issues.
80+
const extensionID = Buffer.from(url.hostname, 'hex').toString('base64url');
81+
const resourcePath = path.normalize(url.pathname);
82+
const filepath = path.join(paths.extensionRoot, extensionID, resourcePath);
83+
84+
return Electron.net.fetch(pathToFileURL(filepath).toString());
85+
});
86+
}
87+
88+
return Promise.resolve();
7989
}
8090

8191
export function setupProtocolHandlers() {
8292
try {
8393
setupAppProtocolHandler();
8494
setupExtensionProtocolHandler();
95+
mainEvents.handle('extensions/register-protocol', setupExtensionProtocolHandler);
8596

8697
protocolsRegistered.resolve();
8798
} catch (ex) {

pkg/rancher-desktop/window/index.ts

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export function getWindow(name: string): Electron.BrowserWindow | null {
5252
return (name in windowMapping) ? BrowserWindow.fromId(windowMapping[name]) : null;
5353
}
5454

55+
function isInternalURL(url: string) {
56+
return url.startsWith(`${ webRoot }/`) || url.startsWith('x-rd-extension://');
57+
}
58+
5559
/**
5660
* Open a given window; if it is already open, focus it.
5761
* @param name The window identifier; this controls window re-use.
@@ -65,10 +69,6 @@ export function createWindow(name: string, url: string, options: Electron.Browse
6569
return window;
6670
}
6771

68-
const isInternalURL = (url: string) => {
69-
return url.startsWith(`${ webRoot }/`) || url.startsWith('x-rd-extension://');
70-
};
71-
7272
window = new BrowserWindow(options);
7373
window.webContents.on('console-message', (event, level, message, line, sourceId) => {
7474
const levelNum = level === 0 ? 0 : level === 1 ? 1 : level === 2 ? 2 : level === 3 ? 3 : 0;
@@ -197,7 +197,7 @@ let lastOpenExtension: { id: string, relPath: string } | undefined;
197197
/**
198198
* Attaches a browser view to the main window
199199
*/
200-
const createView = () => {
200+
function createView() {
201201
const mainWindow = getWindow('main');
202202
const hostInfo = {
203203
arch: process.arch,
@@ -208,15 +208,71 @@ const createView = () => {
208208
throw new Error('Failed to get main window, cannot create view');
209209
}
210210

211-
view = new WebContentsView({
212-
webPreferences: {
213-
nodeIntegration: false,
214-
contextIsolation: true,
215-
preload: path.join(paths.resources, 'preload.js'),
216-
sandbox: true,
217-
additionalArguments: [JSON.stringify(hostInfo)],
218-
},
219-
});
211+
const webPreferences: Electron.WebPreferences = {
212+
nodeIntegration: false,
213+
contextIsolation: true,
214+
preload: path.join(paths.resources, 'preload.js'),
215+
sandbox: true,
216+
additionalArguments: [JSON.stringify(hostInfo)],
217+
};
218+
219+
if (currentExtension?.id) {
220+
webPreferences.partition = `persist:rdx-${ currentExtension.id }`;
221+
const webRequest = Electron.session.fromPartition(webPreferences.partition).webRequest;
222+
223+
webRequest.onBeforeSendHeaders((details, callback) => {
224+
const source = details.webContents?.getURL() ?? '';
225+
const requestHeaders = { ...details.requestHeaders };
226+
227+
if (isInternalURL(source)) {
228+
// If the request is coming from the extension, remove the Origin: header
229+
// because it has x-rd-extension:// nonsense (relative to the server).
230+
delete requestHeaders.Origin;
231+
}
232+
callback({ requestHeaders });
233+
});
234+
235+
webRequest.onHeadersReceived((details, callback) => {
236+
const sourceURL = details.webContents?.getURL() ?? '';
237+
238+
if (!isInternalURL(sourceURL)) {
239+
// Do not rewrite requests from outside the extension (e.g. iframe).
240+
callback({});
241+
242+
return;
243+
}
244+
245+
// Insert (or overwrite) CORS headers to pretend this was allowed. While
246+
// ideally we can just disable `webSecurity` instead, that seems to break
247+
// the preload script (which breaks the extension APIs).
248+
const responseHeaders: Record<string, string|string[]> = { ...details.responseHeaders };
249+
// HTTP headers use case-insensitive comparison; but accents should count
250+
// as different characters (even though it should be ASCII only).
251+
const { compare } = new Intl.Collator('en', { sensitivity: 'accent' });
252+
const overwriteHeaders = [
253+
'Access-Control-Allow-Headers',
254+
'Access-Control-Allow-Methods',
255+
'Access-Control-Allow-Origin',
256+
];
257+
258+
for (const header of overwriteHeaders) {
259+
const match = Object.keys(responseHeaders).find(k => compare(header, k) === 0);
260+
261+
responseHeaders[match ?? header] = '*';
262+
}
263+
264+
if (details.method !== 'OPTIONS') {
265+
// For any request that's not a CORS preflight, just overwrite the headers.
266+
callback({ responseHeaders });
267+
} else {
268+
// For CORS preflights, also change the status code.
269+
const prefix = /\s+/.exec(details.statusLine)?.shift() ?? 'HTTP/1.1';
270+
271+
callback({ responseHeaders, statusLine: `${ prefix } 204 No Content` });
272+
}
273+
});
274+
}
275+
view = new WebContentsView({ webPreferences });
220276
mainWindow.contentView.addChildView(view);
221277
mainWindow.contentView.addListener('bounds-changed', () => {
222278
setImmediate(() => mainWindow.webContents.send('extensions/getContentArea'));
@@ -225,7 +281,7 @@ const createView = () => {
225281
const backgroundColor = nativeTheme.shouldUseDarkColors ? '#202c33' : '#f4f4f6';
226282

227283
view.setBackgroundColor(backgroundColor);
228-
};
284+
}
229285

230286
/**
231287
* Updates the browser view size and position

0 commit comments

Comments
 (0)