@@ -14,203 +14,186 @@ import { SocketProxyProvider } from "../socket"
1414import { isFile , loadAMDModule } from "../util"
1515import { Router as WsRouter } from "../wsRouter"
1616
17+ export const router = express . Router ( )
18+
19+ export const wsRouter = WsRouter ( )
20+
1721/**
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
2026 */
21- export interface IServerAPI {
27+ export interface IVSCodeServerAPI {
2228 handleRequest ( req : http . IncomingMessage , res : http . ServerResponse ) : Promise < void >
2329 handleUpgrade ( req : http . IncomingMessage , socket : net . Socket ) : void
2430 handleServerError ( err : Error ) : void
2531 dispose ( ) : void
2632}
2733
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 >
3836
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
4240
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 ( )
7151 }
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)" ) )
9563 }
96- const key = await this . mintKeyPromise
97- res . end ( key )
64+ return next ( error )
9865 }
66+ return next ( )
67+ }
9968
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+ } )
16880 }
16981
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+ }
179108 }
180109
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,
192114 } )
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 )
199115 }
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 )
210116 }
211117
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+ } )
215176 }
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 ( )
216199}
0 commit comments