@@ -14,203 +14,186 @@ import { SocketProxyProvider } from "../socket"
14
14
import { isFile , loadAMDModule } from "../util"
15
15
import { Router as WsRouter } from "../wsRouter"
16
16
17
+ export const router = express . Router ( )
18
+
19
+ export const wsRouter = WsRouter ( )
20
+
17
21
/**
18
- * This is the API of Code's web client server. code-server delegates requests
19
- * to Code here.
22
+ * The API of VS Code's web client server. code-server delegates requests to VS
23
+ * Code here.
24
+ *
25
+ * @see ../../../lib/vscode/src/vs/server/node/server.main.ts:72
20
26
*/
21
- export interface IServerAPI {
27
+ export interface IVSCodeServerAPI {
22
28
handleRequest ( req : http . IncomingMessage , res : http . ServerResponse ) : Promise < void >
23
29
handleUpgrade ( req : http . IncomingMessage , socket : net . Socket ) : void
24
30
handleServerError ( err : Error ) : void
25
31
dispose ( ) : void
26
32
}
27
33
28
- // Types for ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
29
- export type CreateServer = ( address : string | net . AddressInfo | null , args : CodeArgs ) => Promise < IServerAPI >
30
-
31
- export class CodeServerRouteWrapper {
32
- /** Assigned in `ensureCodeServerLoaded` */
33
- private _codeServerMain ! : IServerAPI
34
- private _wsRouterWrapper = WsRouter ( )
35
- private _socketProxyProvider = new SocketProxyProvider ( )
36
- public router = express . Router ( )
37
- private mintKeyPromise : Promise < Buffer > | undefined
34
+ // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
35
+ export type CreateServer = ( address : string | net . AddressInfo | null , args : CodeArgs ) => Promise < IVSCodeServerAPI >
38
36
39
- public get wsRouter ( ) {
40
- return this . _wsRouterWrapper . router
41
- }
37
+ // The VS Code server is dynamically loaded in when a request is made to this
38
+ // router by `ensureCodeServerLoaded`.
39
+ let vscodeServer : IVSCodeServerAPI | undefined
42
40
43
- //#region Route Handlers
44
-
45
- private manifest : express . Handler = async ( req , res , next ) => {
46
- const appName = req . args [ "app-name" ] || "code-server"
47
- res . writeHead ( 200 , { "Content-Type" : "application/manifest+json" } )
48
-
49
- return res . end (
50
- replaceTemplates (
51
- req ,
52
- JSON . stringify (
53
- {
54
- name : appName ,
55
- short_name : appName ,
56
- start_url : "." ,
57
- display : "fullscreen" ,
58
- display_override : [ "window-controls-overlay" ] ,
59
- description : "Run Code on a remote server." ,
60
- icons : [ 192 , 512 ] . map ( ( size ) => ( {
61
- src : `{{BASE}}/_static/src/browser/media/pwa-icon-${ size } .png` ,
62
- type : "image/png" ,
63
- sizes : `${ size } x${ size } ` ,
64
- } ) ) ,
65
- } ,
66
- null ,
67
- 2 ,
68
- ) ,
69
- ) ,
70
- )
41
+ /**
42
+ * Ensure the VS Code server is loaded.
43
+ */
44
+ export const ensureVSCodeLoaded = async (
45
+ req : express . Request ,
46
+ _ : express . Response ,
47
+ next : express . NextFunction ,
48
+ ) : Promise < void > => {
49
+ if ( vscodeServer ) {
50
+ return next ( )
71
51
}
72
-
73
- private mintKey : express . Handler = async ( req , res , next ) => {
74
- if ( ! this . mintKeyPromise ) {
75
- this . mintKeyPromise = new Promise ( async ( resolve ) => {
76
- const keyPath = path . join ( req . args [ "user-data-dir" ] , "serve-web-key-half" )
77
- logger . debug ( `Reading server web key half from ${ keyPath } ` )
78
- try {
79
- resolve ( await fs . readFile ( keyPath ) )
80
- return
81
- } catch ( error : any ) {
82
- if ( error . code !== "ENOENT" ) {
83
- logError ( logger , `read ${ keyPath } ` , error )
84
- }
85
- }
86
- // VS Code wants 256 bits.
87
- const key = crypto . randomBytes ( 32 )
88
- try {
89
- await fs . writeFile ( keyPath , key )
90
- } catch ( error : any ) {
91
- logError ( logger , `write ${ keyPath } ` , error )
92
- }
93
- resolve ( key )
94
- } )
52
+ // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
53
+ const createVSServer = await loadAMDModule < CreateServer > ( "vs/server/node/server.main" , "createServer" )
54
+ try {
55
+ vscodeServer = await createVSServer ( null , {
56
+ ...( await toCodeArgs ( req . args ) ) ,
57
+ "without-connection-token" : true ,
58
+ } )
59
+ } catch ( error ) {
60
+ logError ( logger , "CodeServerRouteWrapper" , error )
61
+ if ( isDevMode ) {
62
+ return next ( new Error ( ( error instanceof Error ? error . message : error ) + " (VS Code may still be compiling)" ) )
95
63
}
96
- const key = await this . mintKeyPromise
97
- res . end ( key )
64
+ return next ( error )
98
65
}
66
+ return next ( )
67
+ }
99
68
100
- private $root : express . Handler = async ( req , res , next ) => {
101
- const isAuthenticated = await authenticated ( req )
102
- const NO_FOLDER_OR_WORKSPACE_QUERY = ! req . query . folder && ! req . query . workspace
103
- // Ew means the workspace was closed so clear the last folder/workspace.
104
- const FOLDER_OR_WORKSPACE_WAS_CLOSED = req . query . ew
105
-
106
- if ( ! isAuthenticated ) {
107
- const to = self ( req )
108
- return redirect ( req , res , "login" , {
109
- to : to !== "/" ? to : undefined ,
110
- } )
111
- }
112
-
113
- if ( NO_FOLDER_OR_WORKSPACE_QUERY && ! FOLDER_OR_WORKSPACE_WAS_CLOSED ) {
114
- const settings = await req . settings . read ( )
115
- const lastOpened = settings . query || { }
116
- // This flag disables the last opened behavior
117
- const IGNORE_LAST_OPENED = req . args [ "ignore-last-opened" ]
118
- const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened . folder || lastOpened . workspace
119
- const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req . args . _ . length > 0
120
- const to = self ( req )
121
-
122
- let folder = undefined
123
- let workspace = undefined
124
-
125
- // Redirect to the last folder/workspace if nothing else is opened.
126
- if ( HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && ! IGNORE_LAST_OPENED ) {
127
- folder = lastOpened . folder
128
- workspace = lastOpened . workspace
129
- } else if ( HAS_FOLDER_OR_WORKSPACE_FROM_CLI ) {
130
- const lastEntry = path . resolve ( req . args . _ [ req . args . _ . length - 1 ] )
131
- const entryIsFile = await isFile ( lastEntry )
132
- const IS_WORKSPACE_FILE = entryIsFile && path . extname ( lastEntry ) === ".code-workspace"
133
-
134
- if ( IS_WORKSPACE_FILE ) {
135
- workspace = lastEntry
136
- } else if ( ! entryIsFile ) {
137
- folder = lastEntry
138
- }
139
- }
140
-
141
- if ( folder || workspace ) {
142
- return redirect ( req , res , to , {
143
- folder,
144
- workspace,
145
- } )
146
- }
147
- }
148
-
149
- // Store the query parameters so we can use them on the next load. This
150
- // also allows users to create functionality around query parameters.
151
- await req . settings . write ( { query : req . query } )
152
-
153
- next ( )
154
- }
155
-
156
- private $proxyRequest : express . Handler = async ( req , res , next ) => {
157
- this . _codeServerMain . handleRequest ( req , res )
158
- }
159
-
160
- private $proxyWebsocket = async ( req : WebsocketRequest ) => {
161
- const wrappedSocket = await this . _socketProxyProvider . createProxy ( req . ws )
162
- // This should actually accept a duplex stream but it seems Code has not
163
- // been updated to match the Node 16 types so cast for now. There does not
164
- // appear to be any code specific to sockets so this should be fine.
165
- this . _codeServerMain . handleUpgrade ( req , wrappedSocket as net . Socket )
166
-
167
- req . ws . resume ( )
69
+ router . get ( "/" , ensureVSCodeLoaded , async ( req , res , next ) => {
70
+ const isAuthenticated = await authenticated ( req )
71
+ const NO_FOLDER_OR_WORKSPACE_QUERY = ! req . query . folder && ! req . query . workspace
72
+ // Ew means the workspace was closed so clear the last folder/workspace.
73
+ const FOLDER_OR_WORKSPACE_WAS_CLOSED = req . query . ew
74
+
75
+ if ( ! isAuthenticated ) {
76
+ const to = self ( req )
77
+ return redirect ( req , res , "login" , {
78
+ to : to !== "/" ? to : undefined ,
79
+ } )
168
80
}
169
81
170
- //#endregion
171
-
172
- /**
173
- * Fetches a code server instance asynchronously to avoid an initial memory overhead.
174
- */
175
- private ensureCodeServerLoaded : express . Handler = async ( req , _res , next ) => {
176
- if ( this . _codeServerMain ) {
177
- // Already loaded...
178
- return next ( )
82
+ if ( NO_FOLDER_OR_WORKSPACE_QUERY && ! FOLDER_OR_WORKSPACE_WAS_CLOSED ) {
83
+ const settings = await req . settings . read ( )
84
+ const lastOpened = settings . query || { }
85
+ // This flag disables the last opened behavior
86
+ const IGNORE_LAST_OPENED = req . args [ "ignore-last-opened" ]
87
+ const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened . folder || lastOpened . workspace
88
+ const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req . args . _ . length > 0
89
+ const to = self ( req )
90
+
91
+ let folder = undefined
92
+ let workspace = undefined
93
+
94
+ // Redirect to the last folder/workspace if nothing else is opened.
95
+ if ( HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && ! IGNORE_LAST_OPENED ) {
96
+ folder = lastOpened . folder
97
+ workspace = lastOpened . workspace
98
+ } else if ( HAS_FOLDER_OR_WORKSPACE_FROM_CLI ) {
99
+ const lastEntry = path . resolve ( req . args . _ [ req . args . _ . length - 1 ] )
100
+ const entryIsFile = await isFile ( lastEntry )
101
+ const IS_WORKSPACE_FILE = entryIsFile && path . extname ( lastEntry ) === ".code-workspace"
102
+
103
+ if ( IS_WORKSPACE_FILE ) {
104
+ workspace = lastEntry
105
+ } else if ( ! entryIsFile ) {
106
+ folder = lastEntry
107
+ }
179
108
}
180
109
181
- // Create the server...
182
-
183
- const { args } = req
184
-
185
- // See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
186
- const createVSServer = await loadAMDModule < CreateServer > ( "vs/server/node/server.main" , "createServer" )
187
-
188
- try {
189
- this . _codeServerMain = await createVSServer ( null , {
190
- ...( await toCodeArgs ( args ) ) ,
191
- "without-connection-token" : true ,
110
+ if ( folder || workspace ) {
111
+ return redirect ( req , res , to , {
112
+ folder,
113
+ workspace,
192
114
} )
193
- } catch ( error ) {
194
- logError ( logger , "CodeServerRouteWrapper" , error )
195
- if ( isDevMode ) {
196
- return next ( new Error ( ( error instanceof Error ? error . message : error ) + " (VS Code may still be compiling)" ) )
197
- }
198
- return next ( error )
199
115
}
200
-
201
- return next ( )
202
- }
203
-
204
- constructor ( ) {
205
- this . router . get ( "/" , this . ensureCodeServerLoaded , this . $root )
206
- this . router . get ( "/manifest.json" , this . manifest )
207
- this . router . post ( "/mint-key" , this . mintKey )
208
- this . router . all ( / .* / , ensureAuthenticated , this . ensureCodeServerLoaded , this . $proxyRequest )
209
- this . _wsRouterWrapper . ws ( / .* / , ensureOrigin , ensureAuthenticated , this . ensureCodeServerLoaded , this . $proxyWebsocket )
210
116
}
211
117
212
- dispose ( ) {
213
- this . _codeServerMain ?. dispose ( )
214
- this . _socketProxyProvider . stop ( )
118
+ // Store the query parameters so we can use them on the next load. This
119
+ // also allows users to create functionality around query parameters.
120
+ await req . settings . write ( { query : req . query } )
121
+
122
+ next ( )
123
+ } )
124
+
125
+ router . get ( "/manifest.json" , async ( req , res ) => {
126
+ const appName = req . args [ "app-name" ] || "code-server"
127
+ res . writeHead ( 200 , { "Content-Type" : "application/manifest+json" } )
128
+
129
+ return res . end (
130
+ replaceTemplates (
131
+ req ,
132
+ JSON . stringify (
133
+ {
134
+ name : appName ,
135
+ short_name : appName ,
136
+ start_url : "." ,
137
+ display : "fullscreen" ,
138
+ display_override : [ "window-controls-overlay" ] ,
139
+ description : "Run Code on a remote server." ,
140
+ icons : [ 192 , 512 ] . map ( ( size ) => ( {
141
+ src : `{{BASE}}/_static/src/browser/media/pwa-icon-${ size } .png` ,
142
+ type : "image/png" ,
143
+ sizes : `${ size } x${ size } ` ,
144
+ } ) ) ,
145
+ } ,
146
+ null ,
147
+ 2 ,
148
+ ) ,
149
+ ) ,
150
+ )
151
+ } )
152
+
153
+ let mintKeyPromise : Promise < Buffer > | undefined
154
+ router . post ( "/mint-key" , async ( req , res ) => {
155
+ if ( ! mintKeyPromise ) {
156
+ mintKeyPromise = new Promise ( async ( resolve ) => {
157
+ const keyPath = path . join ( req . args [ "user-data-dir" ] , "serve-web-key-half" )
158
+ logger . debug ( `Reading server web key half from ${ keyPath } ` )
159
+ try {
160
+ resolve ( await fs . readFile ( keyPath ) )
161
+ return
162
+ } catch ( error : any ) {
163
+ if ( error . code !== "ENOENT" ) {
164
+ logError ( logger , `read ${ keyPath } ` , error )
165
+ }
166
+ }
167
+ // VS Code wants 256 bits.
168
+ const key = crypto . randomBytes ( 32 )
169
+ try {
170
+ await fs . writeFile ( keyPath , key )
171
+ } catch ( error : any ) {
172
+ logError ( logger , `write ${ keyPath } ` , error )
173
+ }
174
+ resolve ( key )
175
+ } )
215
176
}
177
+ const key = await mintKeyPromise
178
+ res . end ( key )
179
+ } )
180
+
181
+ router . all ( / .* / , ensureAuthenticated , ensureVSCodeLoaded , async ( req , res ) => {
182
+ vscodeServer ! . handleRequest ( req , res )
183
+ } )
184
+
185
+ const socketProxyProvider = new SocketProxyProvider ( )
186
+ wsRouter . ws ( / .* / , ensureOrigin , ensureAuthenticated , ensureVSCodeLoaded , async ( req : WebsocketRequest ) => {
187
+ const wrappedSocket = await socketProxyProvider . createProxy ( req . ws )
188
+ // This should actually accept a duplex stream but it seems Code has not
189
+ // been updated to match the Node 16 types so cast for now. There does not
190
+ // appear to be any code specific to sockets so this should be fine.
191
+ vscodeServer ! . handleUpgrade ( req , wrappedSocket as net . Socket )
192
+
193
+ req . ws . resume ( )
194
+ } )
195
+
196
+ export function dispose ( ) {
197
+ vscodeServer ?. dispose ( )
198
+ socketProxyProvider . stop ( )
216
199
}
0 commit comments