Skip to content

Commit 718af04

Browse files
authored
Merge pull request microsoft#141572 from microsoft/sandy081/extensionResourceHandler
Handle extension resources on server
2 parents e7d99f3 + 6c48bde commit 718af04

File tree

3 files changed

+115
-3
lines changed

3 files changed

+115
-3
lines changed

src/vs/base/common/network.ts

+4
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ class RemoteAuthoritiesImpl {
130130
this._connectionTokens[authority] = connectionToken;
131131
}
132132

133+
getPreferredWebSchema(): 'http' | 'https' {
134+
return this._preferredWebSchema;
135+
}
136+
133137
rewrite(uri: URI): URI {
134138
if (this._delegate) {
135139
return this._delegate(uri);

src/vs/server/node/webClientServer.ts

+99-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import { FileAccess, connectionTokenCookieName, connectionTokenQueryName } from
1919
import { generateUuid } from 'vs/base/common/uuid';
2020
import { IProductService } from 'vs/platform/product/common/productService';
2121
import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
22+
import { asText, IRequestService } from 'vs/platform/request/common/request';
23+
import { IHeaders } from 'vs/base/parts/request/common/request';
24+
import { CancellationToken } from 'vs/base/common/cancellation';
25+
import { URI } from 'vs/base/common/uri';
26+
import { streamToBuffer } from 'vs/base/common/buffer';
27+
import { IProductConfiguration } from 'vs/base/common/product';
28+
import { isString } from 'vs/base/common/types';
2229

2330
const textMimeType = {
2431
'.html': 'text/html',
@@ -73,12 +80,17 @@ const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath);
7380

7481
export class WebClientServer {
7582

83+
private readonly _webExtensionResourceUrlTemplate: URI | undefined;
84+
7685
constructor(
7786
private readonly _connectionToken: ServerConnectionToken,
7887
@IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService,
7988
@ILogService private readonly _logService: ILogService,
80-
@IProductService private readonly _productService: IProductService
81-
) { }
89+
@IRequestService private readonly _requestService: IRequestService,
90+
@IProductService private readonly _productService: IProductService,
91+
) {
92+
this._webExtensionResourceUrlTemplate = this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate) : undefined;
93+
}
8294

8395
/**
8496
* Handle web resources (i.e. only needed by the web client).
@@ -102,6 +114,10 @@ export class WebClientServer {
102114
// callback support
103115
return this._handleCallback(res);
104116
}
117+
if (/^\/web-extension-resource\//.test(pathname)) {
118+
// extension resource support
119+
return this._handleWebExtensionResource(req, res, parsedUrl);
120+
}
105121

106122
return serveError(req, res, 404, 'Not found.');
107123
} catch (error) {
@@ -130,6 +146,77 @@ export class WebClientServer {
130146
return serveFile(this._logService, req, res, filePath, headers);
131147
}
132148

149+
private _getResourceURLTemplateAuthority(uri: URI): string | undefined {
150+
const index = uri.authority.indexOf('.');
151+
return index !== -1 ? uri.authority.substring(index + 1) : undefined;
152+
}
153+
154+
/**
155+
* Handle extension resources
156+
*/
157+
private async _handleWebExtensionResource(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
158+
if (!this._webExtensionResourceUrlTemplate) {
159+
return serveError(req, res, 500, 'No extension gallery service configured.');
160+
}
161+
162+
// Strip `/web-extension-resource/` from the path
163+
const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20)
164+
const path = normalize(normalizedPathname.substr('/web-extension-resource/'.length));
165+
const uri = URI.parse(path).with({
166+
scheme: this._webExtensionResourceUrlTemplate.scheme,
167+
authority: path.substring(0, path.indexOf('/')),
168+
path: path.substring(path.indexOf('/') + 1)
169+
});
170+
171+
if (this._getResourceURLTemplateAuthority(this._webExtensionResourceUrlTemplate) !== this._getResourceURLTemplateAuthority(uri)) {
172+
return serveError(req, res, 403, 'Request Forbidden');
173+
}
174+
175+
const headers: IHeaders = {};
176+
const setRequestHeader = (header: string) => {
177+
const value = req.headers[header];
178+
if (value && (isString(value) || value[0])) {
179+
headers[header] = isString(value) ? value : value[0];
180+
} else if (header !== header.toLowerCase()) {
181+
setRequestHeader(header.toLowerCase());
182+
}
183+
};
184+
setRequestHeader('X-Client-Name');
185+
setRequestHeader('X-Client-Version');
186+
setRequestHeader('X-Machine-Id');
187+
setRequestHeader('X-Client-Commit');
188+
189+
const context = await this._requestService.request({
190+
type: 'GET',
191+
url: uri.toString(true),
192+
headers
193+
}, CancellationToken.None);
194+
195+
const status = context.res.statusCode || 500;
196+
if (status !== 200) {
197+
let text: string | null = null;
198+
try {
199+
text = await asText(context);
200+
} catch (error) {/* Ignore */ }
201+
return serveError(req, res, status, text || `Request failed with status ${status}`);
202+
}
203+
204+
const responseHeaders: Record<string, string> = Object.create(null);
205+
const setResponseHeader = (header: string) => {
206+
const value = context.res.headers[header];
207+
if (value) {
208+
responseHeaders[header] = value;
209+
} else if (header !== header.toLowerCase()) {
210+
setResponseHeader(header.toLowerCase());
211+
}
212+
};
213+
setResponseHeader('Cache-Control');
214+
setResponseHeader('Content-Type');
215+
res.writeHead(200, responseHeaders);
216+
const buffer = await streamToBuffer(context.stream);
217+
return res.end(buffer.buffer);
218+
}
219+
133220
/**
134221
* Handle HTTP requests for /
135222
*/
@@ -191,6 +278,16 @@ export class WebClientServer {
191278
_wrapWebWorkerExtHostInIframe,
192279
developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined },
193280
settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined,
281+
productConfiguration: <Partial<IProductConfiguration>>{
282+
extensionsGallery: this._webExtensionResourceUrlTemplate ? {
283+
...this._productService.extensionsGallery,
284+
'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({
285+
scheme: 'http',
286+
authority: remoteAuthority,
287+
path: `web-extension-resource/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}`
288+
}).toString(true)
289+
} : undefined
290+
}
194291
})))
195292
.replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '');
196293

src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { getServiceMachineId } from 'vs/platform/externalServices/common/service
1616
import { IStorageService } from 'vs/platform/storage/common/storage';
1717
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
1818
import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
19+
import { RemoteAuthorities } from 'vs/base/common/network';
20+
21+
export const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource';
1922

2023
export const IExtensionResourceLoaderService = createDecorator<IExtensionResourceLoaderService>('extensionResourceLoaderService');
2124

@@ -68,7 +71,8 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi
6871

6972
public getExtensionGalleryResourceURL(galleryExtension: { publisher: string, name: string, version: string }, path?: string): URI | undefined {
7073
if (this._extensionGalleryResourceUrlTemplate) {
71-
return URI.parse(format2(this._extensionGalleryResourceUrlTemplate, { publisher: galleryExtension.publisher, name: galleryExtension.name, version: galleryExtension.version, path: 'extension' }));
74+
const uri = URI.parse(format2(this._extensionGalleryResourceUrlTemplate, { publisher: galleryExtension.publisher, name: galleryExtension.name, version: galleryExtension.version, path: 'extension' }));
75+
return this._isWebExtensionResourceEndPoint(uri) ? uri.with({ scheme: RemoteAuthorities.getPreferredWebSchema() }) : uri;
7276
}
7377
return undefined;
7478
}
@@ -103,8 +107,15 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi
103107
}
104108

105109
private _getExtensionGalleryAuthority(uri: URI): string | undefined {
110+
if (this._isWebExtensionResourceEndPoint(uri)) {
111+
return uri.authority;
112+
}
106113
const index = uri.authority.indexOf('.');
107114
return index !== -1 ? uri.authority.substring(index + 1) : undefined;
108115
}
109116

117+
protected _isWebExtensionResourceEndPoint(uri: URI): boolean {
118+
return uri.path.startsWith(`/${WEB_EXTENSION_RESOURCE_END_POINT}/`);
119+
}
120+
110121
}

0 commit comments

Comments
 (0)