@@ -19,6 +19,13 @@ import { FileAccess, connectionTokenCookieName, connectionTokenQueryName } from
19
19
import { generateUuid } from 'vs/base/common/uuid' ;
20
20
import { IProductService } from 'vs/platform/product/common/productService' ;
21
21
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' ;
22
29
23
30
const textMimeType = {
24
31
'.html' : 'text/html' ,
@@ -73,12 +80,17 @@ const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath);
73
80
74
81
export class WebClientServer {
75
82
83
+ private readonly _webExtensionResourceUrlTemplate : URI | undefined ;
84
+
76
85
constructor (
77
86
private readonly _connectionToken : ServerConnectionToken ,
78
87
@IServerEnvironmentService private readonly _environmentService : IServerEnvironmentService ,
79
88
@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
+ }
82
94
83
95
/**
84
96
* Handle web resources (i.e. only needed by the web client).
@@ -102,6 +114,10 @@ export class WebClientServer {
102
114
// callback support
103
115
return this . _handleCallback ( res ) ;
104
116
}
117
+ if ( / ^ \/ w e b - e x t e n s i o n - r e s o u r c e \/ / . test ( pathname ) ) {
118
+ // extension resource support
119
+ return this . _handleWebExtensionResource ( req , res , parsedUrl ) ;
120
+ }
105
121
106
122
return serveError ( req , res , 404 , 'Not found.' ) ;
107
123
} catch ( error ) {
@@ -130,6 +146,77 @@ export class WebClientServer {
130
146
return serveFile ( this . _logService , req , res , filePath , headers ) ;
131
147
}
132
148
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
+
133
220
/**
134
221
* Handle HTTP requests for /
135
222
*/
@@ -191,6 +278,16 @@ export class WebClientServer {
191
278
_wrapWebWorkerExtHostInIframe,
192
279
developmentOptions : { enableSmokeTestDriver : this . _environmentService . driverHandle === 'web' ? true : undefined } ,
193
280
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
+ }
194
291
} ) ) )
195
292
. replace ( '{{WORKBENCH_AUTH_SESSION}}' , ( ) => authSessionInfo ? escapeAttribute ( JSON . stringify ( authSessionInfo ) ) : '' ) ;
196
293
0 commit comments