Skip to content

Commit 34f55ad

Browse files
author
iamhyc
committed
feat: init local proxy based webview
1 parent 5f0890b commit 34f55ad

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

src/core/projectManagerProvider.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ProjectTagsResponseSchema } from '../api/base';
44
import { GlobalStateManager } from '../utils/globalStateManager';
55
import { VirtualFileSystem, parseUri } from './remoteFileSystemProvider';
66
import { LocalReplicaSCMProvider } from '../scm/localReplicaSCM';
7+
import { PhantomWebview } from '../utils/phantomWebview';
78

89
class DataItem extends vscode.TreeItem {
910
constructor(
@@ -200,6 +201,25 @@ export class ProjectManagerProvider implements vscode.TreeDataProvider<DataItem>
200201

201202
loginServer(server: ServerItem) {
202203
const loginMethods:Record<string, ()=>void> = {
204+
// eslint-disable-next-line @typescript-eslint/naming-convention
205+
'Login with Webview': () => {
206+
const webview = new PhantomWebview(server.api.url);
207+
webview.onCookieUpdated((cookies) => {
208+
// 'overleaf_session2', 'sharelatex.sid'
209+
if (cookies['overleaf_session2'] || cookies['sharelatex.sid']) {
210+
const cookie = Object.entries(cookies).map(([key,value]) => `${key}=${value}`).join('; ');
211+
GlobalStateManager.loginServer(this.context, server.api, server.name, {cookies:cookie})
212+
.then(success => {
213+
if (success) {
214+
this.refresh();
215+
webview.dispose();
216+
} else {
217+
vscode.window.showErrorMessage( vscode.l10n.t('Login failed.') );
218+
}
219+
});
220+
}
221+
});
222+
},
203223
// eslint-disable-next-line @typescript-eslint/naming-convention
204224
'Login with Password': () => {
205225
vscode.window.showInputBox({'placeHolder': vscode.l10n.t('Email')})

src/utils/phantomWebview.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import * as vscode from 'vscode';
2+
import * as http from 'http';
3+
import * as https from 'https';
4+
import * as url from 'url';
5+
6+
interface Cookies {
7+
[key: string]: string;
8+
}
9+
const cookieBroadcast = new vscode.EventEmitter<Cookies>();
10+
11+
class ProxyServer {
12+
private cookies: Cookies = {};
13+
private server: http.Server;
14+
15+
constructor(
16+
private readonly parent: CORSProxy,
17+
readonly targetUrl: url.URL,
18+
private readonly agent: http.Agent | https.Agent,
19+
) {
20+
targetUrl.port = targetUrl.port || (targetUrl.protocol === 'https:' ? '443' : '80');
21+
this.server = http.createServer((req, res) => this.proxyRequest(req, res));
22+
}
23+
24+
get proxyAddress() {
25+
return this.server.address();
26+
}
27+
28+
start(callback?:() => void) {
29+
this.server.listen(0, 'localhost', callback);
30+
}
31+
32+
close() {
33+
this.server.close();
34+
}
35+
36+
private proxyRequest(req: http.IncomingMessage, res: http.ServerResponse) {
37+
// update the request headers with the cookies
38+
const cookie = Object.entries(this.cookies).map(([key,value]) => `${key}=${value}`).join('; ');
39+
req.headers.cookie = cookie;
40+
// update the request host with the target host
41+
req.headers.host = this.targetUrl.host;
42+
req.headers.origin = this.targetUrl.origin;
43+
req.headers.referer = this.targetUrl.origin + req.url;
44+
// remove the `sec-fetch-*` headers
45+
delete req.headers['sec-fetch-mode'];
46+
delete req.headers['sec-fetch-site'];
47+
delete req.headers['sec-fetch-dest'];
48+
// proxy the request
49+
const options = {
50+
hostname: this.targetUrl.hostname,
51+
port: this.targetUrl.port,
52+
path: req.url,
53+
method: req.method,
54+
headers: req.headers,
55+
agent: this.agent,
56+
};
57+
const proxy = this.targetUrl.protocol === 'https:' ? https.request(options) : http.request(options);
58+
req.pipe(proxy);
59+
60+
proxy.on('response', (proxyRes) => {
61+
// Record the cookies
62+
if (proxyRes.headers['set-cookie']) {
63+
proxyRes.headers['set-cookie'].forEach((cookie) => {
64+
const [keyValue, ...rest] = cookie.split(';');
65+
const [_key, _value] = keyValue.split('=');
66+
const [key, value] = [_key.trim(), _value.trim()];
67+
// Notify the cookie update
68+
if ( req.method==='GET' && req.url?.endsWith('/project') ) {
69+
cookieBroadcast.fire({ [key]: value });
70+
}
71+
this.cookies[key] = value;
72+
});
73+
}
74+
// Remove CORS related restrictions
75+
delete proxyRes.headers['content-security-policy'];
76+
delete proxyRes.headers['cross-origin-opener-policy'];
77+
delete proxyRes.headers['cross-origin-resource-policy'],
78+
delete proxyRes.headers['referrer-policy'];
79+
delete proxyRes.headers['strict-transport-security'];
80+
delete proxyRes.headers['x-content-type-options'];
81+
delete proxyRes.headers['x-download-options'];
82+
delete proxyRes.headers['x-frame-options'];
83+
delete proxyRes.headers['x-permitted-cross-domain-policies'];
84+
delete proxyRes.headers['x-served-by'];
85+
delete proxyRes.headers['x-xss-protection'];
86+
// Copy the response headers
87+
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
88+
// Notify parent with 302 redirection
89+
if (proxyRes.statusCode === 302 && proxyRes.headers['location']?.startsWith('http')) {
90+
proxyRes = this.parent.updateProxyServer(proxyRes);
91+
}
92+
// Pipe the response data
93+
proxyRes.pipe(res);
94+
});
95+
96+
proxy.on('error', (err) => {
97+
console.error(`Error on proxy request: ${err.message}`);
98+
res.writeHead(500);
99+
res.end();
100+
});
101+
}
102+
}
103+
104+
class CORSProxy {
105+
rootServer: ProxyServer;
106+
private proxyAgent: http.Agent | https.Agent;
107+
private proxyServers: { [key: string]: ProxyServer } = {};
108+
109+
constructor(
110+
private readonly targetUrl: url.URL,
111+
) {
112+
this.proxyAgent = this.targetUrl.protocol === 'https:' ? new https.Agent({ keepAlive: true }) : new http.Agent({ keepAlive: true });
113+
this.rootServer = new ProxyServer(this, this.targetUrl, this.proxyAgent);
114+
this.proxyServers[this.targetUrl.origin] = this.rootServer;
115+
}
116+
117+
updateProxyServer(proxyRes: http.IncomingMessage) {
118+
const location = proxyRes.headers['location'];
119+
const locationUrl = new url.URL( location! );
120+
if (location) {
121+
// Create a new proxy server for the redirection
122+
if (this.proxyServers[locationUrl.origin]===undefined) {
123+
const proxyServer = new ProxyServer(this, locationUrl, this.proxyAgent);
124+
this.proxyServers[locationUrl.origin] = proxyServer;
125+
proxyServer.start();
126+
}
127+
// Update the redirection location origin
128+
const proxyServer = this.proxyServers[locationUrl.origin];
129+
const proxyAddress = proxyServer.proxyAddress as any;
130+
proxyRes.headers['location'] = location.replace(locationUrl.origin, `http://${proxyAddress.address}:${proxyAddress.port}`);
131+
}
132+
return proxyRes;
133+
}
134+
135+
close() {
136+
Object.values(this.proxyServers).forEach((server) => server.close());
137+
this.proxyAgent.destroy();
138+
}
139+
}
140+
141+
export class PhantomWebview extends vscode.Disposable {
142+
private targetUrl: url.URL;
143+
private proxy: CORSProxy;
144+
145+
private panel?: vscode.WebviewPanel;
146+
147+
constructor(targetUrl: string) {
148+
super(() => this.dispose());
149+
this.targetUrl = new url.URL(targetUrl);
150+
// Create the root proxy server
151+
this.proxy = new CORSProxy(this.targetUrl);
152+
this.proxy.rootServer.start(() => {
153+
this.panel = this.createWebviewPanel();
154+
this.panel.onDidDispose(() => this.dispose());
155+
});
156+
}
157+
158+
dispose() {
159+
// Close the webview panel
160+
this.panel?.dispose();
161+
this.panel = undefined;
162+
// Close the root proxy server
163+
this.proxy.close();
164+
}
165+
166+
onCookieUpdated(listener: (cookies: Cookies) => any, thisArgs?: any, disposables?: vscode.Disposable[]) {
167+
return cookieBroadcast.event(listener, thisArgs, disposables);
168+
}
169+
170+
private createWebviewPanel() {
171+
const proxyAddress = this.proxy.rootServer.proxyAddress as any;
172+
const proxyUrl = `http://${proxyAddress.address}:${proxyAddress.port}`;
173+
const panel = vscode.window.createWebviewPanel('phantom', this.targetUrl.hostname, vscode.ViewColumn.One, {
174+
enableScripts: true,
175+
retainContextWhenHidden: false,
176+
});
177+
panel.webview.html = `<!DOCTYPE html>
178+
<html>
179+
<head>
180+
<title>Phantom Webview</title>
181+
<style>
182+
html, body, iframe {
183+
margin: 0;
184+
padding: 0;
185+
width: 100%;
186+
height: 100%;
187+
overflow: hidden;
188+
border: none;
189+
}
190+
</style>
191+
</head>
192+
<body>
193+
<iframe src=${proxyUrl}></iframe>
194+
</body>
195+
</html>
196+
`;
197+
return panel;
198+
}
199+
200+
}

0 commit comments

Comments
 (0)