Skip to content

Commit f0f5867

Browse files
kleberbaumnetsnek
authored andcommitted
Same-origin CORS proxy without allowlist, fix desktop CSS and font MIME types
- CORS proxy: inject /cors-proxy/ route into server-main.js (same-origin, no separate port needed). Remove allowlist so all external URLs are proxied. - Shim: rewrite all external https:// fetch requests to /cors-proxy/HOST/path - Desktop CSS: add <link> tag for workbench.desktop.main.css (Tailwind v4) since the HTML has workbench.css, not workbench.web.main.css - MIME types: add .ttf (font/ttf) and .woff2 (font/woff2) for icon fonts - localFilesystem: don't pretend files under .cursor/ exist (fixes createFile "already exists" errors for log files)
1 parent 3425908 commit f0f5867

File tree

2 files changed

+51
-39
lines changed

2 files changed

+51
-39
lines changed

patch-cursor-web.sh

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,19 @@ WB_HTML="$SERVDIR/out/vs/code/browser/workbench/workbench.html"
126126
python3 -c "
127127
html = open('$WB_HTML').read()
128128
129-
# Replace web CSS with desktop CSS
129+
# Add desktop CSS (contains Tailwind v4 runtime + Cursor-specific styles)
130+
# The HTML has workbench.css (not workbench.web.main.css), so replace doesn't work.
131+
# Instead, add a second <link> for the desktop CSS.
130132
html = html.replace(
131133
'workbench/workbench.web.main.css',
132134
'workbench/workbench.desktop.main.css'
133135
)
136+
desktop_css_link = '<link rel=\"stylesheet\" href=\"{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.desktop.main.css\">'
137+
if 'workbench.desktop.main.css' not in html:
138+
html = html.replace(
139+
'</head>',
140+
'\t' + desktop_css_link + '\n\t</head>'
141+
)
134142
135143
# Replace web workbench loader with desktop shim
136144
html = html.replace(
@@ -462,6 +470,9 @@ if old in js and 'style="display:none' not in js[js.find(old):js.find(old)+200]
462470
changed = True
463471
print(' update notification suppressed')
464472
473+
# 7t. Keep default Editor layout
474+
# (no change needed — M0.Editor is already the default)
475+
465476
if changed:
466477
open(f, 'w').write(js)
467478
PATCH_DESKTOP_EOF
@@ -495,14 +506,12 @@ import sys
495506
f = sys.argv[1]
496507
js = open(f).read()
497508
498-
# 8a. CSP connect-src: add CORS proxy origin
509+
# 8a. CSP connect-src: same-origin proxy, no external origin needed
499510
old = "connect-src 'self' ws: wss: https:;"
500-
new = "connect-src 'self' ws: wss: https: http://127.0.0.1:9080;"
501511
if old in js:
502-
js = js.replace(old, new, 1)
503-
print(' connect-src: added CORS proxy origin')
504-
elif new in js:
505-
print(' connect-src: already patched')
512+
print(' connect-src: already allows https: (no change needed)')
513+
elif 'connect-src' in js:
514+
print(' connect-src: non-standard, skipping')
506515
507516
# 8b. CSP font-src: add vscode-remote-resource
508517
old = "font-src 'self' blob:;"
@@ -532,14 +541,31 @@ if old in js:
532541
elif 'o="1.105.1";let r=ZS' in js:
533542
print(' U0() already patched')
534543
535-
# 8e. MIME type: add .wasm for WebAssembly
544+
# 8e. MIME types: add .wasm, .ttf, .woff2
536545
old = '".woff":"application/font-woff"'
537-
new = '".wasm":"application/wasm",".woff":"application/font-woff"'
546+
new = '".wasm":"application/wasm",".ttf":"font/ttf",".woff":"application/font-woff",".woff2":"font/woff2"'
538547
if old in js and '".wasm"' not in js:
539548
js = js.replace(old, new, 1)
540-
print(' MIME: added .wasm')
541-
elif '".wasm"' in js:
542-
print(' MIME: .wasm already present')
549+
print(' MIME: added .wasm, .ttf, .woff2')
550+
elif '".wasm"' in js and '".ttf"' not in js:
551+
old2 = '".wasm":"application/wasm",".woff":"application/font-woff"'
552+
new2 = '".wasm":"application/wasm",".ttf":"font/ttf",".woff":"application/font-woff",".woff2":"font/woff2"'
553+
if old2 in js:
554+
js = js.replace(old2, new2, 1)
555+
print(' MIME: added .ttf, .woff2')
556+
elif '".ttf"' in js:
557+
print(' MIME: .ttf/.woff2 already present')
558+
559+
# 8f0. Inject /cors-proxy/ route into server request handler (same-origin, no allowlist)
560+
# URL format: /cors-proxy/<target-host>/path → https://<target-host>/path
561+
# Must go BEFORE the GET-only method check since API calls use POST.
562+
old = 'handleRequest(e,n){if(e.method!=="GET")'
563+
cors_route = r'''handleRequest(e,n){let _u=new URL(e.url||"/",`http://${e.headers.host}`),_p=_u.pathname;if(this._serverBasePath!==void 0&&_p.startsWith(this._serverBasePath)){_p=_p.substring(this._serverBasePath.length);if(_p[0]!=="/")_p="/"+_p}if(_p.startsWith("/cors-proxy/")){const _cp=_p.substring(12),_si=_cp.indexOf("/"),_host=_si>0?_cp.substring(0,_si):_cp,_path=_si>0?_cp.substring(_si):"/";if(e.method==="OPTIONS"){n.writeHead(200,{"access-control-allow-origin":"*","access-control-allow-methods":"GET, POST, PUT, DELETE, OPTIONS","access-control-allow-headers":"*","access-control-max-age":"86400"});n.end();return}const _https=yH("https"),_chunks=[];e.on("data",_c=>_chunks.push(_c));e.on("end",()=>{const _body=Buffer.concat(_chunks),_fh={...e.headers};delete _fh["host"];delete _fh["origin"];delete _fh["referer"];delete _fh["connection"];delete _fh["accept-encoding"];_fh["host"]=_host;const _opts={hostname:_host,port:443,path:_path+(_u.search||""),method:e.method,headers:_fh},_pr=_https.request(_opts,_res=>{const _rh={..._res.headers};_rh["access-control-allow-origin"]="*";_rh["access-control-expose-headers"]="*";delete _rh["access-control-allow-credentials"];n.writeHead(_res.statusCode,_rh);_res.pipe(n)});_pr.on("error",_e=>{n.writeHead(502,{"Content-Type":"text/plain"});n.end("Proxy error: "+_e.message)});if(_body.length>0)_pr.write(_body);_pr.end()});return}if(e.method!=="GET")'''
564+
if old in js and '/cors-proxy/' not in js:
565+
js = js.replace(old, cors_route, 1)
566+
print(' CORS proxy route injected at /cors-proxy/')
567+
elif '/cors-proxy/' in js:
568+
print(' CORS proxy route already present')
543569
544570
open(f, 'w').write(js)
545571
PATCH_SERVER_EOF

workbench-desktop-shim.js

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,29 @@
22
* Cursor Web — desktop workbench in browser with IPC bridge
33
*--------------------------------------------------------*/
44

5-
// === CORS Proxy for Cursor API calls ===
6-
// The desktop workbench makes fetch/gRPC calls to api[2-5].cursor.sh and
7-
// various agent subdomains. All are blocked by CORS in the browser.
8-
// Intercept and route through local proxy.
5+
// === CORS Proxy — route ALL external fetch through same-origin /cors-proxy/ ===
6+
// No allowlist. URL: /cors-proxy/HOST/path → https://HOST/path
97
{
108
const _originalFetch = window.fetch;
11-
const PROXY_PORT = parseInt(new URLSearchParams(window.location.search).get('cors_port') || '9080', 10);
12-
// Match Cursor API endpoints + Statsig feature flag domains (CORS-blocked in browser)
13-
const _cursorApiRe = /https?:\/\/(?:[a-z0-9-]*\.?(?:api[2-5]\.cursor\.sh)|api\.statsigcdn\.com|featureassets\.org|prodregistryv2\.org|statsigapi\.net)/;
14-
// Rewrite vscode-remote:// URLs to local HTTP resource endpoint
9+
const _externalRe = /^https?:\/\/([^/]+)/;
1510
const _vsRemoteRe = /^vscode-remote:\/\/[^/]+(\/.*)$/;
1611
window.fetch = function(input, init) {
1712
let url = (input instanceof Request) ? input.url : String(input);
18-
// Handle vscode-remote:// scheme (extension resources loaded by desktop workbench)
1913
const rm = url.match(_vsRemoteRe);
2014
if (rm) {
2115
const resourcePath = rm[1];
2216
const rewritten = window.location.origin + '/vscode-remote-resource?path=' + encodeURIComponent(resourcePath);
2317
input = (input instanceof Request) ? new Request(rewritten, input) : rewritten;
2418
}
25-
const m = url.match(_cursorApiRe);
26-
if (m) {
27-
const originalHost = m[0].replace(/^https?:\/\//, '');
28-
const proxied = url.replace(_cursorApiRe, `http://127.0.0.1:${PROXY_PORT}`);
19+
const m = url.match(_externalRe);
20+
if (m && !url.startsWith(window.location.origin)) {
21+
const targetHost = m[1];
22+
const pathStart = url.indexOf('/', url.indexOf('://') + 3);
23+
const pathAndQuery = pathStart > 0 ? url.substring(pathStart) : '/';
24+
const proxied = window.location.origin + '/cors-proxy/' + targetHost + pathAndQuery;
2925
if (input instanceof Request) {
30-
const newInit = { method: input.method, headers: new Headers(input.headers), body: input.body, mode: 'cors', credentials: input.credentials, redirect: input.redirect, referrer: input.referrer, signal: input.signal };
31-
newInit.headers.set('X-Proxy-Host', originalHost);
32-
input = new Request(proxied, newInit);
26+
input = new Request(proxied, { method: input.method, headers: input.headers, body: input.body, credentials: input.credentials, redirect: input.redirect, referrer: input.referrer, signal: input.signal });
3327
} else {
34-
init = init || {};
35-
const headers = new Headers(init.headers || {});
36-
headers.set('X-Proxy-Host', originalHost);
37-
init = { ...init, headers };
3828
input = proxied;
3929
}
4030
}
@@ -271,13 +261,9 @@ function handleLocalFilesystem(method, arg) {
271261
path.endsWith('/cache')) {
272262
return { type: 2, ctime: Date.now(), mtime: Date.now(), size: 0 };
273263
}
274-
// Paths under .cursor/ — return dir stat for dirs, file stat for files
275-
if (isUnderCursor) {
276-
if (hasExtension) {
277-
// Files under .cursor (logs, config, db) — return empty file stat
278-
return { type: 1, ctime: Date.now(), mtime: Date.now(), size: 0 };
279-
}
280-
// Subdirectories
264+
// Paths under .cursor/ — only pretend subdirectories exist.
265+
// Files should be FileNotFound so createFile can create them.
266+
if (isUnderCursor && !hasExtension) {
281267
return { type: 2, ctime: Date.now(), mtime: Date.now(), size: 0 };
282268
}
283269
// Everything else: not found

0 commit comments

Comments
 (0)