Skip to content

Commit 56291ed

Browse files
committed
- Check if URL is pointing to the right service
- Set selective request and response headers - Update the scheme on the client
1 parent fb6144b commit 56291ed

File tree

3 files changed

+66
-11
lines changed

3 files changed

+66
-11
lines changed

src/vs/base/common/network.ts

Lines changed: 4 additions & 0 deletions
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

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +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 { IRequestService } from 'vs/platform/request/common/request';
22+
import { asText, IRequestService } from 'vs/platform/request/common/request';
2323
import { IHeaders } from 'vs/base/parts/request/common/request';
2424
import { CancellationToken } from 'vs/base/common/cancellation';
2525
import { URI } from 'vs/base/common/uri';
2626
import { streamToBuffer } from 'vs/base/common/buffer';
2727
import { IProductConfiguration } from 'vs/base/common/product';
28+
import { isString } from 'vs/base/common/types';
2829

2930
const textMimeType = {
3031
'.html': 'text/html',
@@ -139,34 +140,73 @@ export class WebClientServer {
139140
return serveFile(this._logService, req, res, filePath, headers);
140141
}
141142

143+
private _getResourceURLTemplateAuthority(uri: URI): string | undefined {
144+
const index = uri.authority.indexOf('.');
145+
return index !== -1 ? uri.authority.substring(index + 1) : undefined;
146+
}
147+
142148
/**
143149
* Handle extension resources
144150
*/
145151
private async _handleWebExtensionResource(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
152+
if (!this._productService.extensionsGallery?.resourceUrlTemplate) {
153+
return serveError(req, res, 500, 'No extension gallery service configured.');
154+
}
155+
146156
// Strip `/web-extension-resource/` from the path
147157
const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20)
148158
const path = normalize(normalizedPathname.substr('/web-extension-resource/'.length));
149-
150-
const url = URI.parse(path).with({
159+
const uri = URI.parse(path).with({
151160
scheme: this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate).scheme : 'https',
152161
authority: path.substring(0, path.indexOf('/')),
153162
path: path.substring(path.indexOf('/') + 1)
154-
}).toString(true);
163+
});
164+
165+
if (this._getResourceURLTemplateAuthority(URI.parse(this._productService.extensionsGallery.resourceUrlTemplate)) !== this._getResourceURLTemplateAuthority(uri)) {
166+
return serveError(req, res, 403, 'Request Forbidden');
167+
}
155168

156169
const headers: IHeaders = {};
157-
for (const header of req.rawHeaders) {
158-
if (req.headers[header]) {
159-
headers[header] = req.headers[header]![0];
170+
const seRequestHeader = (header: string) => {
171+
const value = req.headers[header];
172+
if (value && (isString(value) || value[0])) {
173+
headers[header] = isString(value) ? value : value[0];
174+
} else if (header !== header.toLowerCase()) {
175+
seRequestHeader(header.toLowerCase());
160176
}
161-
}
177+
};
178+
seRequestHeader('X-Client-Name');
179+
seRequestHeader('X-Client-Version');
180+
seRequestHeader('X-Machine-Id');
181+
seRequestHeader('X-Client-Commit');
162182

163183
const context = await this._requestService.request({
164184
type: 'GET',
165-
url,
185+
url: uri.toString(true),
166186
headers
167187
}, CancellationToken.None);
168188

169-
res.writeHead(context.res.statusCode || 500, context.res.headers);
189+
const status = context.res.statusCode || 500;
190+
if (status !== 200) {
191+
let text: string | null = null;
192+
try {
193+
text = await asText(context);
194+
} catch (error) {/* Ignore */ }
195+
return serveError(req, res, status, text || `Request failed with status ${status}`);
196+
}
197+
198+
const responseHeaders: Record<string, string> = Object.create(null);
199+
const seResponseHeader = (header: string) => {
200+
const value = context.res.headers[header];
201+
if (value) {
202+
responseHeaders[header] = value;
203+
} else if (header !== header.toLowerCase()) {
204+
seResponseHeader(header.toLowerCase());
205+
}
206+
};
207+
seResponseHeader('Cache-Control');
208+
seResponseHeader('Content-Type');
209+
res.writeHead(200, responseHeaders);
170210
const buffer = await streamToBuffer(context.stream);
171211
return res.end(buffer.buffer);
172212
}

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

Lines changed: 12 additions & 1 deletion
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)