Compare commits

..

196 Commits

Author SHA1 Message Date
Anmol Sethi
93fb76e4a7 v3.7.3 2020-11-24 13:12:10 -05:00
Anmol Sethi
a1537d7138 Merge pull request #2358 from cdr/update-noti-45e1
vscode: Show notification when upgrade is available
2020-11-24 13:10:25 -05:00
Anmol Sethi
def81245a4 vscode: Check updates with absolute path
In case the window location path changes. Not entirely sure if it can
but best to be on the safe side.
2020-11-24 13:07:30 -05:00
Anmol Sethi
37c80c9bbd vscode: Add missing semicolons
See #2359
2020-11-24 12:48:22 -05:00
Anmol Sethi
be37821ab9 update.ts: Simplify comparison 2020-11-24 12:42:26 -05:00
Anmol Sethi
f74f1721e6 doc: Add note on upgrading into release notes and install.md
Closes #1652
Closes #2221
2020-11-24 12:42:26 -05:00
Anmol Sethi
fb63c0cd22 vscode: Show notification when upgrade is available
And link to the release notes.
2020-11-24 12:13:21 -05:00
Anmol Sethi
bb26d2edd3 Merge pull request #2357 from cdr/branding-0570
vscode: Customize welcome page for code-server
2020-11-24 11:58:31 -05:00
Anmol Sethi
303fe2bc4e vscode: Customize welcome page for code-server
- Title/subtitle are now code-server and VS Code version
- Added a list of code-server help links
2020-11-23 21:16:14 -05:00
Anmol Sethi
5a38ab95fe vscode: Disable go home button
See https://github.com/cdr/code-server/issues/2328
2020-11-23 21:16:14 -05:00
Anmol Sethi
19710ab144 vscode: Update product.json
The new fields are from vscodium and make the welcome page
documentation links work correctly.

I also renamed the distribution to "code-server" so that when you're
in a browser, it now says code-server instead of Code OSS.
2020-11-23 19:06:09 -05:00
Anmol Sethi
a018e30d6f Merge pull request #2348 from cdr/userdata
Use file system for user data
2020-11-23 13:57:55 -05:00
Asher
fb835838db Remove semver-umd link
This is included in the bundle now.
2020-11-20 15:35:18 -06:00
Asher
3d7fbec40f Use file system for settings and fix data home path
It's possible that using browser storage makes more sense with settings
sync, so we might want to revisit this once/if we get settings sync
working. As it currently is though, browser storage just causes jank.

The path was also missing a `User` at the end so I added that. This
might affect the Vim extension which would have been writing to the
wrong path previously but I don't believe it should affect anything
else since they would have been writing to browser storage.

- Fixes #2208
- Fixes #2231
- Fixes #2279
- Fixes #2274
2020-11-20 14:03:07 -06:00
Anmol Sethi
96170de191 Merge pull request #2342 from cdr/v3.7.2
v3.7.2
2020-11-19 18:22:45 -05:00
Anmol Sethi
2e2d03371f ci: Fix typo in release template 2020-11-19 18:03:12 -05:00
Anmol Sethi
a0db6723c1 v3.7.2 2020-11-19 17:28:22 -05:00
Asher
23ead21b1d Merge pull request #2340 from cdr/vscode-1.51.1
Update VS Code to 1.51.1
2020-11-19 15:52:40 -06:00
Asher
42390da097 Don't persist terminals for now 2020-11-19 15:51:37 -06:00
Asher
d0f6cbb02d Use resolverEnv to get exec path
This is the last unused variable in the create terminal payload.
2020-11-19 15:51:36 -06:00
Asher
fa59156a2a Implement remaining resolver methods 2020-11-19 15:51:35 -06:00
Asher
8ffe599796 Add notes on unimplemented terminal events 2020-11-19 15:51:34 -06:00
Asher
a6f8840009 Add timeout for disposing detached terminals 2020-11-19 15:51:33 -06:00
Asher
1feb30a7ff Send back workspace ID and name in terminal list
This makes it re-connect automatically.
2020-11-19 15:51:32 -06:00
Asher
182aca6490 Only replay terminals when detached 2020-11-19 15:51:31 -06:00
Asher
8311cf5657 Handle non-persistent terminals 2020-11-19 15:51:30 -06:00
Asher
4de2511162 Implement terminal replay event 2020-11-19 15:51:30 -06:00
Asher
3f7b91e2e2 Implement most of remote terminal service
It works, at least, but there are still some missing parts.
2020-11-19 15:51:29 -06:00
Asher
431137da45 Add new (unimplemented) terminal service 2020-11-19 15:51:28 -06:00
Asher
4d276b88c0 Add new logger service
The telemetry service depends on this now. I had to move it into
invokeFunction and use accessor.get otherwise getLogger on the service
was undefined.

I also had to move some the extension management service because it
depends on the moved telemetry service. I moved a few other services as
well to better match VS Code (sharedProcessMain.ts).

I swapped some this.services.get with accessor.get since that seems to
be the correct method although for these other services either method
seems to work.
2020-11-19 15:51:27 -06:00
Asher
e28c9ab287 Update VS Code to 1.51.1 2020-11-19 15:51:23 -06:00
Anmol Sethi
b540737b10 Merge pull request #2339 from cdr/ios-input-3cf7
login.css: Disable webkit appearance for input elements
2020-11-19 11:31:07 -05:00
Asher
4380356e0c Merge pull request #2334 from cdr/wrappers
Separate process wrappers and pass arguments
2020-11-19 10:28:54 -06:00
Asher
72caafe8b0 Fix service worker not loading (#2335)
I removed this under the impression the default was to allow it anywhere
but that's not the case. Since the service worker was already registered
in my browser I never got the error during testing.
2020-11-19 10:18:15 -06:00
Asher
08b9e9ad1f Merge pull request #2336 from cdr/webview-404
Fix 404 webviews and tar endpoint
2020-11-19 10:14:54 -06:00
Anmol Sethi
2dc7863ec3 login.css: Disable webkit appearance for input elements
Not sure why Safari does these things...

Closes #2247
2020-11-19 10:43:26 -05:00
Anmol Sethi
30100caf0c Revert "login.css: Fix button styling on iOS"
This reverts commit f79bb210ec.
2020-11-19 10:41:37 -05:00
Jacky
f79bb210ec login.css: Fix button styling on iOS 2020-11-19 10:37:51 -05:00
Asher
182791319a Fix tar authentication
It was checking the request path but for tars the path is in the query
variable so the request path is irrelevant.
2020-11-18 17:15:53 -06:00
Asher
624cd9d44f Fix webview 404s
An extra slash caused a 404 (was /webview//vscode-resource).
2020-11-18 17:10:53 -06:00
Asher
95ef6dbf2f Remove unused wrapper options
Also move our memory default to the beginning of NODE_OPTIONS so it can
be overidden. The version of the flag with dashes seems to be the more
correct one now so use that instead of underscores.

Related: #2113.
2020-11-18 13:23:06 -06:00
Asher
016daf2fdd Parse arguments once
Fixes #2316.
2020-11-18 13:01:46 -06:00
Asher
247c4ec776 Move onMessage so it can be used in the wrappers 2020-11-18 12:28:43 -06:00
Asher
d55e06936b Split child and parent wrappers
I think having them combined and relying on if statements was getting
confusing especially if we want to add additional messages with
different payloads (which will soon be the case).
2020-11-18 12:28:42 -06:00
Asher
2a3608df53 Skip heartbeat on /healthz endpoint (#2333)
I managed to lose this in the rewrite.

Fixes #2327.
2020-11-18 12:19:08 -06:00
piousdeer
c6062c3d0a Fix log message (#2331) 2020-11-18 10:41:32 -06:00
Anmol Sethi
9ff535eddc Merge pull request #2312 from cdr/v3.7.1
v3.7.1
2020-11-16 18:14:15 -05:00
Anmol Sethi
2bf91ff6a6 v3.7.1 2020-11-16 17:18:12 -05:00
Anmol Sethi
ccc519ecbd ci: Pin nfpm to v1.9.0
Closes #2310
2020-11-16 16:57:04 -05:00
Anmol Sethi
40e1f066ff ci: Improve release template (#2311) 2020-11-16 16:56:53 -05:00
Anmol Sethi
ac09aa6ea8 doc/ipad.md: Fix TOC 2020-11-16 15:40:28 -05:00
Anmol Sethi
f5e3dca3b9 Merge pull request #2309 from cdr/v3.7.0
v3.7.0
2020-11-16 15:33:17 -05:00
Anmol Sethi
f64599b94d ci: Update standalone build test
ms-toolsai.jupyter is now a dependency of ms-python and is installed
along with it.
2020-11-16 14:40:06 -05:00
Anmol Sethi
9917da068a v3.7.0 2020-11-16 11:11:50 -05:00
Anmol Sethi
8bf1bf2c9f helm: Use upgrade --install everywhere
See @sreya's review
2020-11-13 18:45:13 -05:00
Anmol Sethi
79e8f3dfdb ci: Only use helm kubeval if installed 2020-11-13 18:44:29 -05:00
Anmol Sethi
a37572d92d ci: Disable no-unused-vars for function args
See previous commit for failure introduced.
2020-11-13 18:44:28 -05:00
Anmol Sethi
40a7c11ce3 node/routes: Fix error handling
We should always send HTML if the user agent expects it.

If they do not, they should clearly indicate such via the Accept header.

Closes #2297
2020-11-13 18:44:28 -05:00
Anmol Sethi
7afa689285 Merge pull request #2303 from cdr/helm-db7f
Move helm from root and fix stuff in README
2020-11-13 18:40:03 -05:00
Anmol Sethi
f4d48bc880 ci: Remove helm validation action in favour of helm kubeval directly 2020-11-13 18:38:58 -05:00
Anmol Sethi
9af3671c05 helm: Add link in install.md 2020-11-13 18:38:58 -05:00
Anmol Sethi
248c2adb2e helm: Fix README examples
Not sure where --name came from? Maybe an older version of helm.

Ah, it's from v2.16.7
2020-11-13 18:38:58 -05:00
Anmol Sethi
52ea32f4a7 helm: Move chart into ci/helm-chart 2020-11-13 17:27:52 -05:00
Anmol Sethi
affa64c89c Merge pull request #2048 from Matthew-Beckett/feature/helm3
Update Kubernetes Helm Chart
2020-11-13 16:39:13 -05:00
Asher
5e603056fd Merge pull request #2238 from cdr/code-asher/ch1385 2020-11-12 14:04:04 -06:00
Asher
9889f30224 Remove unused ts-expect-error from VS Code
I'm not sure why other builds are passing with this still in.
2020-11-12 12:30:41 -06:00
Asher
96995b78d1 Update cert flag test 2020-11-12 12:29:41 -06:00
Asher
6f14b8b8dd Add separate handler for error
Feels like it parallels better with the other handlers.
2020-11-12 12:07:45 -06:00
Asher
b73ea2fea2 Unbind message handler itself after getting message
Also switch `once` to `on` since we `off` them later anyway so no point
in making Node do it twice.
2020-11-12 12:03:28 -06:00
Asher
e1702a1d21 Merge branch master into code-asher/ch1385 2020-11-12 11:52:02 -06:00
Asher
5499a3d125 Use baseUrl when redirecting from domain proxy
This will make the route more robust since it'll work under more than
just the root.
2020-11-12 11:23:52 -06:00
Asher
31b67062b0 Remove <type> from onMessage
Turns out that while Typescript can't infer the callback return type
from it, Typescript can do the opposite and infer it from the callback
return type.
2020-11-12 11:17:45 -06:00
Asher
72931edcf0 Fix cleanup after getting message from vscode 2020-11-12 11:16:21 -06:00
Asher
79478eb89f Clarify some points around the cookie domain
Also add a check that the domain has a dot. This covers the localhost
case as well, so remove that.
2020-11-10 18:53:38 -06:00
Asher
4574593664 Refactor vscode init to use async
Hopefully is a bit easier to read.
2020-11-10 18:21:20 -06:00
Asher
71850e312b Avoid setting ?to=/
That's the default so it's extra visual noise.
2020-11-10 18:14:18 -06:00
Asher
b8340a2ae9 Close sockets correctly 2020-11-10 17:55:04 -06:00
Asher
f706039a9d Re-add TLS socket proxy 2020-11-10 17:55:03 -06:00
Asher
de4949571c Document getFirstPath better 2020-11-10 17:02:39 -06:00
Asher
0a01338edd Deduplicate child process message dance 2020-11-10 16:36:46 -06:00
Matthew Beckett
aa7415a479 Update CODEOWNERS file 2020-11-10 20:37:14 +00:00
Matthew Beckett
10799aa1ec Bump chart app version 2020-11-10 20:10:50 +00:00
Matthew Beckett
0e39bb9f2c Fix trailing line breaks 2020-11-10 20:09:48 +00:00
Matthew Beckett
03aa7709ca Add maintainers 2020-11-10 20:09:13 +00:00
Matthew Beckett
77c2a72cf8 Bump version and update README 2020-11-10 20:07:38 +00:00
Matthew Beckett
f3d7d3f616 Merge branch 'master' into feature/helm3 2020-11-10 19:08:53 +00:00
Ben Potter
da6000b96f Add Slack link to issue template (#2282) 2020-11-10 14:00:05 -05:00
Anmol Sethi
d969a5bd6b Merge pull request #2252 from cdr/plugin-5d60
Plugin API to add more applications to code-server
2020-11-06 14:49:00 -05:00
Anmol Sethi
fe399ff0fe Fix formatting 2020-11-06 14:47:08 -05:00
Anmol Sethi
277211c4ce plugin: Make init and applications callbacks optional 2020-11-06 14:47:08 -05:00
Anmol Sethi
9d39c53c99 plugin: Give test-plugin some html to test overlay 2020-11-06 14:47:08 -05:00
Anmol Sethi
197a09f0c1 plugin: Test endpoints via supertest
Unfortunately we can't use node-mocks-http to test a express.Router
that has async routes. See https://github.com/howardabrams/node-mocks-http/issues/225

router will just return undefined if the executing handler is async and
so the test will have no way to wait for it to complete. Thus, we have
to use supertest which starts an actual HTTP server in the background
and uses a HTTP client to send requests.
2020-11-06 10:13:01 -05:00
Anmol Sethi
9453f891df plugin.ts: Fix usage of routerPath in mount 2020-11-06 10:13:01 -05:00
Anmol Sethi
14f408a837 plugin: Plugin modules now export a single top level identifier
Makes typing much easier. Addresse's Will's last comment.
2020-11-06 10:13:01 -05:00
Anmol Sethi
8a8159c683 plugin: More review fixes
Next commit will address Will's comments about the typings being weird.
2020-11-06 10:13:01 -05:00
Anmol Sethi
706bc23f04 plugin: Fixes for CI 2020-11-06 10:13:01 -05:00
Anmol Sethi
af73b96313 routes/apps.ts: Add example output 2020-11-06 10:12:47 -05:00
Anmol Sethi
2a13d003d3 plugin.ts: Add homepageURL to plugin and application 2020-11-06 10:12:47 -05:00
Anmol Sethi
687094802e plugin.ts: Make application endpoint paths absolute 2020-11-06 10:12:46 -05:00
Anmol Sethi
139a28e0ea plugin.ts: Describe private counterpart functions
Addresses Will's comments.
2020-11-06 10:12:46 -05:00
Anmol Sethi
e03bbe3149 routes/apps.ts: Implement /api/applications endpoint 2020-11-06 10:12:46 -05:00
Anmol Sethi
afff86ae9c plugin.ts: Adjust to implement pluginapi.d.ts correctly 2020-11-06 10:12:46 -05:00
Anmol Sethi
fed545e67d plugin.d.ts -> pluginapi.d.ts
More clear.
2020-11-06 10:12:46 -05:00
Anmol Sethi
6638daf6f0 plugin.d.ts: Add explicit path field and adjust types to reflect
See my discussion with Will in the PR.
2020-11-06 10:12:46 -05:00
Anmol Sethi
8d3a7721fe plugin.d.ts: Document plugin priority correctly 2020-11-06 10:12:46 -05:00
Anmol Sethi
75e52a3774 plugin.ts: Fixes for @code-asher 2020-11-06 10:12:46 -05:00
Anmol Sethi
f4d7f00033 plugin.ts: Fixes for @wbobeirne 2020-11-06 10:12:46 -05:00
Anmol Sethi
ef971009d9 plugin.test.ts: Make it clear iconPath is a path 2020-11-06 10:12:46 -05:00
Anmol Sethi
30d2962e21 src/node/plugin.ts: Warn on duplicate plugin and only load first 2020-11-06 10:12:46 -05:00
Anmol Sethi
82e8a00a0d Fix CI 2020-11-06 10:12:46 -05:00
Anmol Sethi
bea185b8b2 plugin: Add basic loading test
Will work on testing overlay next.
2020-11-06 10:12:46 -05:00
Anmol Sethi
e08a55d44a src/node/plugin.ts: Implement new plugin API 2020-11-06 10:12:45 -05:00
Anmol Sethi
481df70622 ci/dev/test.sh: Pass through args 2020-11-06 10:11:57 -05:00
Anmol Sethi
aa2cfa2c17 typings/plugin.d.ts: Create 2020-11-06 10:11:57 -05:00
Asher
959497067c Document HttpError
Also type the status.
2020-11-05 17:07:58 -06:00
Asher
f7076247f9 Move domain proxy to routes
This matches better with the other routes.

Also add a missing authentication check to the path proxy web socket.
2020-11-05 17:07:32 -06:00
Asher
f6c4434191 Tweak proxy fallthrough behavior
It will now redirect all HTML requests. Also it avoids req.accepts since
that's always truthy.
2020-11-05 16:49:30 -06:00
Asher
cb991a9143 Handle errors for JSON requests
Previously it would have just given them the error HTML.
2020-11-05 15:19:56 -06:00
Asher
3f1750cf83 Fix destroying response in update again
I added another reject that doesn't destroy the response.
2020-11-05 15:08:10 -06:00
Asher
7b2752a62c Move websocket routes into a separate app
This is mostly so we don't have to do any wacky patching but it also
makes it so we don't have to keep checking if the request is a web
socket request every time we add middleware.
2020-11-05 15:08:09 -06:00
Asher
9e09c1f92b Upgrade to Express 5
Now async routes are handled!
2020-11-05 15:08:08 -06:00
Asher
8252c372af Provide a way to tell when event handlers are finished
This lets us actually wait for disposal before a graceful exit.
2020-11-05 15:08:07 -06:00
Asher
396af23842 Kill VS Code when process exits
This is to ensure it doesn't hang around.
2020-11-04 17:07:41 -06:00
Asher
34225e2bdf Use ensureAuthenticated as middleware 2020-11-04 17:07:40 -06:00
Asher
476379a77e Fix cookie domain
Had double Domain=
2020-11-04 17:07:39 -06:00
Asher
210fc049c4 Document VS Code endpoints 2020-11-04 17:07:39 -06:00
Asher
e5c8e0aad1 Remove useless || 2020-11-04 17:07:38 -06:00
Asher
c5ce365482 Use query variable to force update check 2020-11-04 17:07:37 -06:00
Asher
a653b93ce2 Include protocol on printed address
This makes it clickable from the terminal.
2020-11-04 17:07:36 -06:00
Asher
e2c35facdb Remove invalid comment on maybeProxy
It no longer handles authentication.
2020-11-04 17:07:35 -06:00
Asher
75b93f9dc5 Fix bind address priority
Broke when converting to a loop.
2020-11-04 17:07:24 -06:00
Jon Ayers
1eebde56ab Specify that Coder Alpha requires v3.6.2 (#2270) 2020-11-03 18:03:28 -06:00
Asher
e27188c2f9 Merge pull request #2268 from cdr/update-alpha
Update README Alpha section to remove reference to typeform
2020-11-03 17:33:02 -06:00
Jon Ayers
ddbac8dd78 Update README Alpha section to remove reference to typeform 2020-11-03 23:31:08 +00:00
Asher
8066da12fe Remove unused Locals interface 2020-11-03 15:37:22 -06:00
Asher
03e0013112 Unbind error/exit events once handshakes resolve 2020-11-03 15:27:21 -06:00
Asher
e243f6e369 Return early when forking to reduce indentation 2020-11-03 15:27:21 -06:00
Asher
c10450c4c5 Move isFile into util
That allows its use in entry.ts as well.
2020-11-03 15:27:20 -06:00
Asher
c72c53f64d Fix not being able to dispose vscode after failed disposal 2020-11-03 14:36:27 -06:00
Asher
f4e5855318 Simplify update request 2020-11-03 14:35:23 -06:00
Asher
3a074fd844 Skip unnecessary auth type check when using --link 2020-11-03 14:30:34 -06:00
Asher
8a9e61defb Use Addr interface everywhere and loop over arg sources 2020-11-03 14:28:49 -06:00
Asher
1067507c41 Proxy to 0.0.0.0 instead of localhost 2020-11-03 14:28:48 -06:00
Anmol Sethi
f9e0990594 Merge pull request #2255 from cdr/self-signed-3b2c
Fix self signed certificate for iPad
2020-11-03 14:04:47 -05:00
Anmol Sethi
c07296cce0 docs: Add known issues to iPad docs and add more links to iPad docs
Closes #1816
2020-11-03 14:04:19 -05:00
Anmol Sethi
31306f7fdd docs: Add iPad self signed certificate documentation
Closes #1816
Closes #1566
2020-11-03 14:04:19 -05:00
Matthew Beckett
7affce5801 Merge branch 'v3.6.2' into feature/helm3 2020-11-03 12:28:29 +00:00
Asher
37b87dd2b8 Merge pull request #2264 from cdr/v3.6.2
v3.6.2
2020-11-02 18:07:53 -06:00
Asher
9bde62fbd6 v3.6.2 2020-11-02 17:17:25 -06:00
Asher
6fbbb1047f fmt 2020-11-02 17:17:09 -06:00
Asher
e07a591745 Catch cloud agent download failure
- See #2251 and #2229.
2020-11-02 16:48:25 -06:00
Asher
676c7bf915 Merge pull request #2250 from cdr/disconnects
Experimental initial connection fix
2020-11-02 16:46:14 -06:00
Asher
9ad7d0b7a3 Fix potential 500 when loading in parallel 2020-10-30 16:16:46 -05:00
Asher
07e7c38ea2 Immediately pause web socket
This will buffer any data sent to it until something is ready to listen
on it.
2020-10-30 16:16:46 -05:00
Asher
0b9af6ef67 Initiate connection handshake from server
This way the connection can be initiated by either side. It looks like
sometimes the initial message from the client is lost (it never makes it
into the onControlMessage callback) but I'm still not sure why or if
that is preventable.

Also added a timeout on the server end to clean things up in case the
client never responds.
2020-10-30 16:16:45 -05:00
Asher
c63dc3a1ea Add more logging around connections 2020-10-30 16:16:44 -05:00
Anmol Sethi
a1b61d1659 src/node/util.ts: Mark generated certificates as CA
Required for access under iPad.
2020-10-30 13:36:53 -04:00
Anmol Sethi
bae28727bd src/node/cli.ts: Add --cert-host to configure generated certificate hostname 2020-10-30 13:36:53 -04:00
Anmol Sethi
8b85006996 src/node/util.ts: Make certificate generation "modern"
Now we add a subject alt name, set extendedKeyUsage and use the
correct certificate extension.

The above allow it to be properly trusted by iOS.

See https://support.apple.com/en-us/HT210176

*.cert isn't a real extension for certificates, *.crt is correct
for it to be recognized by e.g. keychain or when importing as a profile
into iOS.

Updates #1566

I've been able to successfully connect from my iPad Pro now to my
code-server instance with a self signed certificate! Next commit
will be docs.
2020-10-30 13:36:53 -04:00
Anmol Sethi
10b3028196 util: Generate self signed certificate into data directory
Closes #1778
2020-10-30 13:36:53 -04:00
Katie Horne
860c99e3b8 Docs copyedits: README.md + CONTRIBUTING.md (#2242)
* Edit README

* Edit CONTRIBUTING

* Format CONTRIBUTING.MD

* Incorporate feedback

* Revert movement of CONTRIBUTING.MD and format
2020-10-29 17:21:47 -04:00
Asher
f2f1fee6f1 Short-circuit heartbeat when alive 2020-10-27 17:48:37 -05:00
Asher
504d89638b Fix open line being printed when open fails
Opening the URL can fail if the user doesn't have something appropriate
installed to handle it.
2020-10-27 17:43:11 -05:00
Asher
dc177ab505 Unambiguify address replacement
Co-authored-by: Teffen Ellis <TeffenEllis@users.noreply.github.com>
2020-10-27 17:38:54 -05:00
Asher
cde94d5ed4 Remove redundant serverAddress check
We now guarantee there is an address.
2020-10-27 17:35:42 -05:00
Asher
305348f0ac Improve proxy fallthrough logic
- Use accept header.
- Match /login and /login/ exactly.
- Match /static/ (trailing slash).
- Use req.path. Same result but feels more accurate to me.
2020-10-27 17:31:37 -05:00
Asher
6ab6cb4f07 Fix error handler types 2020-10-27 17:20:13 -05:00
Asher
6422a8d74b Fix webview resource path 2020-10-27 17:17:05 -05:00
Asher
257d9a4fa4 Make authentication work with sub-domain proxy 2020-10-26 17:56:14 -05:00
Asher
112eda4605 Convert routes to Express 2020-10-26 17:56:13 -05:00
Asher
4b6cbacbad Add file for global constants 2020-10-26 17:56:12 -05:00
Asher
71dc5c7542 Switch to Express
It doesn't do anything yet.
2020-10-26 17:56:11 -05:00
Asher
7e1e9d1249 Merge pull request #2205 from cdr/v3.6.1
v3.6.1
2020-10-23 17:04:33 -05:00
Asher
8e93e28162 Strip config file password from debug log 2020-10-20 16:18:28 -05:00
Asher
9f25cc6d5d Move providers from app to routes 2020-10-20 16:18:27 -05:00
Asher
6000e389bc Add Express as a dep and regenerate lockfile
The Express types were throwing errors but regenerating the lockfile
resolved them.
2020-10-20 16:18:26 -05:00
Asher
2928d362fa Move heart and AuthType out of http
This file is going to get blasted in favor of Express.
2020-10-20 16:18:24 -05:00
Asher
dcb303a437 Move argument defaults into setDefaults 2020-10-20 16:15:13 -05:00
Matthew Beckett
d7cba30c6a Merge branch 'master' into feature/helm3 2020-10-13 14:29:43 +01:00
Matthew Beckett
30fafc8937 Merge pull request #15 from hammady/feature/helm3
Add support for hostPath volumes
2020-10-13 14:28:48 +01:00
Hossam Hammady
83465a2f4f Simplify data volume mount 2020-10-12 12:50:43 +00:00
Hossam Hammady
b4fd47b5af Add support for hostPath volumes 2020-10-12 11:59:53 +00:00
Matthew Beckett
3570ff796d Merge pull request #14 from hammady/feature/helm3
Merge hammady/code-server/feature/helm3 into Matthew-Beckett/code-server/feature/helm3
2020-10-12 12:02:40 +01:00
Hossam Hammady
fd241d555b Fix indentation for extra mounts 2020-10-12 10:49:21 +00:00
Hossam Hammady
d323f4f75b Add missing existingClaim in values.yaml comments 2020-10-12 10:46:52 +00:00
Hossam Hammady
40b1efa142 Add echo command in NOTES to get password 2020-10-12 10:45:26 +00:00
Matthew Beckett
e858d11279 Merge branch 'master' into feature/helm3 2020-09-03 15:02:25 +01:00
Matthew Beckett
96a78c98d1 Add checkout of repo 2020-09-02 22:26:06 +01:00
Matthew Beckett
70b73d7cb9 Add kubernetes version environment variable 2020-09-02 22:24:24 +01:00
Matthew Beckett
8fe7986d0d Add kubeval workflow 2020-09-02 22:19:56 +01:00
Matthew Beckett
559d05bb7b Update readme and service port 2020-09-02 22:02:37 +01:00
Matthew Beckett
341cb342b2 Patch helm chart to stable with v3.5.0 2020-09-02 21:53:52 +01:00
Matthew Beckett
34f8c77a03 Change service default to ClusterIP and add helmignore 2020-09-02 21:05:57 +01:00
Matthew Beckett
d33df75662 Add updated Helm chart 2020-09-02 21:00:12 +01:00
Matthew Beckett
85d5858b1d Remove Kubernetes native manifests 2020-09-02 20:43:57 +01:00
Matthew Beckett
1b6ddb66f0 Re-commit 0f1bbc 2020-09-02 20:41:58 +01:00
84 changed files with 5446 additions and 2632 deletions

View File

@@ -19,6 +19,9 @@ extends:
- prettier/@typescript-eslint # Remove conflicts again.
rules:
# Sometimes you need to add args to implement a function signature even
# if they are unused.
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }]
# For overloads.
no-dupe-class-members: off
"@typescript-eslint/no-use-before-define": off
@@ -31,6 +34,8 @@ rules:
import/order:
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
no-async-promise-executor: off
# This isn't a real module, just types, which apparently doesn't resolve.
import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }]
settings:
# Does not work with CommonJS unfortunately.

2
.github/CODEOWNERS vendored
View File

@@ -1 +1,3 @@
* @code-asher @nhooyr
ci/helm-chart @Matthew-Beckett @alexgorbatchev

View File

@@ -2,4 +2,7 @@ blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
about: Ask the community for help
about: Ask the community for help on our GitHub Discussions board
- name: Chat
about: Need immediate help or just want to talk? Hop in our Slack
url: https://cdr.co/join-community

View File

@@ -6,72 +6,64 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a
## Highlights
- **Code everywhere**
- Code on your Chromebook, tablet, and laptop with a consistent development environment.
- Develop on a Linux machine and pick up from any device with a web browser.
- **Server-powered**
- Take advantage of large cloud servers to speed up tests, compilations, downloads, and more.
- Preserve battery life when you're on the go as all intensive tasks run on your server.
- Make use of a spare computer you have lying around and turn it into a full development environment.
- Code on any device with a consistent development environment
- Use cloud servers to speed up tests, compilations, downloads, and more
- Preserve battery life when you're on the go; all intensive tasks run on your server
## Getting Started
For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md).
There are two ways to get started:
### Quick Install
1. Using the [install script](./install.sh), which automates most of the process. The script uses the system package manager (if possible)
2. Manually installing code-server; see [Installation](./doc/install.md) for instructions applicable to most use cases
We have a [script](./install.sh) to install code-server for Linux, macOS and FreeBSD.
It tries to use the system package manager if possible.
First run to print out the install process:
If you choose to use the install script, you can preview what occurs during the install process:
```bash
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
```
Now to actually install:
To install, run:
```bash
curl -fsSL https://code-server.dev/install.sh | sh
```
The install script will print out how to run and start using code-server.
When done, the install script prints out instructions for running and starting code-server.
### Manual Install
Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md).
We also have an in-depth [setup and configuration](./doc/guide.md) guide.
### Alpha Program 🐣
We're working on a cloud platform to make deploying and managing code-server easier. If you don't want to worry about
We're working on a cloud platform that makes deploying and managing code-server easier.
Consider updating to the latest version and running code-server with our experimental flag `--link` if you don't want to worry about
- TLS
- Authentication
- Port Forwarding
consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W).
```bash
$ code-server --link
Proxying code-server to Coder Cloud, you can access your IDE at https://valmar-jon.cdr.co
```
## FAQ
See [./doc/FAQ.md](./doc/FAQ.md).
## Contributing
## Want to help?
See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md).
See [CONTRIBUTING](./doc/CONTRIBUTING.md) for details.
## Hiring
We ([@cdr](https://github.com/cdr)) are looking for engineers to help maintain
code-server, innovate on open source and streamline dev workflows.
We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain
code-server](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8), innovate on open source, and streamline dev workflows.
Our main office is in Austin, Texas. Remote is ok as long as
you're in North America or Europe.
Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested.
We're also hiring someone specifically to help maintain code-server.
See the listing [here](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8).
Please get in [touch](mailto:jobs@coder.com) with your resume/GitHub if interested.
## For Organizations

View File

@@ -17,6 +17,8 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
1. Update the version of code-server and make a PR.
1. Update in `package.json`
2. Update in [./doc/install.md](../doc/install.md)
3. Update in [./ci/helm-chart/README.md](../ci/helm-chart/README.md)
- Remember to update the chart version as well on top of appVersion in `Chart.yaml`.
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
1. You do not have to wait for these.
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with

View File

@@ -67,7 +67,7 @@ EOF
bundle_vscode() {
mkdir -p "$VSCODE_OUT_PATH"
rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH"
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out"
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out"
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
if [ "$KEEP_MODULES" = 0 ]; then

View File

@@ -25,8 +25,11 @@ main() {
esac
OS="$(uname | tr '[:upper:]' '[:lower:]')"
curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent
chmod +x ./lib/coder-cloud-agent
if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent; then
chmod +x ./lib/coder-cloud-agent
else
echo "Failed to download cloud agent; --link will not work"
fi
if ! vscode_yarn; then
echo "You may not have the required dependencies to build the native modules."

View File

@@ -15,7 +15,17 @@ v$VERSION
VS Code v$(vscode_version)
- Summarize changes here with references to issues
Upgrading is as easy as installing the new version over the old one. code-server
maintains all user data in \`~/.local/share/code-server\` so that it is preserved in between
installations.
## New Features
- ⭐ Summarize new features here with references to issues
## Bug Fixes
- ⭐ Summarize bug fixes here with references to issues
Cheers! 🍻
EOF
}

View File

@@ -15,7 +15,8 @@ main() {
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
local installed_extensions
installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
if [[ $installed_extensions != "ms-python.python" ]]; then
# We use grep as ms-python.python may have dependency extensions that change.
if ! echo "$installed_extensions" | grep -q "ms-python.python"; then
echo "Unexpected output from listing extensions:"
echo "$installed_extensions"
exit 1

View File

@@ -19,13 +19,16 @@ main() {
"*.yaml"
"*.yml"
)
prettier --write --loglevel=warn $(git ls-files "${prettierExts[@]}")
prettier --write --loglevel=warn $(
git ls-files "${prettierExts[@]}" | grep -v 'helm-chart'
)
doctoc --title '# FAQ' doc/FAQ.md > /dev/null
doctoc --title '# Setup Guide' doc/guide.md > /dev/null
doctoc --title '# Install' doc/install.md > /dev/null
doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null
doctoc --title '# Contributing' doc/CONTRIBUTING.md > /dev/null
doctoc --title '# iPad' doc/ipad.md > /dev/null
if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
echo "Files need generation or are formatted incorrectly:"

View File

@@ -8,6 +8,9 @@ main() {
stylelint $(git ls-files "*.css")
tsc --noEmit
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
if command -v helm && helm kubeval --help > /dev/null; then
helm kubeval ci/helm-chart
fi
}
main "$@"

View File

@@ -4,7 +4,10 @@ set -euo pipefail
main() {
cd "$(dirname "$0")/../.."
mocha -r ts-node/register ./test/*.test.ts
cd test/test-plugin
make -s out/index.js
cd "$OLDPWD"
mocha -r ts-node/register ./test/*.test.ts "$@"
}
main "$@"

File diff suppressed because it is too large Load Diff

23
ci/helm-chart/.helmignore Normal file
View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

23
ci/helm-chart/Chart.yaml Normal file
View File

@@ -0,0 +1,23 @@
apiVersion: v2
name: code-server
description: A Helm chart for cdr/code-server
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 3.7.3

117
ci/helm-chart/README.md Normal file
View File

@@ -0,0 +1,117 @@
# code-server
![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.3](https://img.shields.io/badge/AppVersion-3.7.3-informational?style=flat-square)
[code-server](https://github.com/cdr/code-server) code-server is VS Code running
on a remote server, accessible through the browser.
This chart is community maintained by [@Matthew-Beckett](https://github.com/Matthew-Beckett) and [@alexgorbatchev](https://github.com/alexgorbatchev)
## TL;DR;
```console
$ git clone https://github.com/cdr/code-server
$ cd code-server
$ helm upgrade --install code-server ci/helm-chart
```
## Introduction
This chart bootstraps a code-server deployment on a
[Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh)
package manager.
## Prerequisites
- Kubernetes 1.6+
## Installing the Chart
To install the chart with the release name `code-server`:
```console
$ git clone https://github.com/cdr/code-server
$ cd code-server
$ helm upgrade --install code-server ci/helm-chart
```
The command deploys code-server on the Kubernetes cluster in the default
configuration. The [configuration](#configuration) section lists the parameters
that can be configured during installation.
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `code-server` deployment:
```console
$ helm delete code-server
```
The command removes all the Kubernetes components associated with the chart and
deletes the release.
## Configuration
The following table lists the configurable parameters of the code-server chart
and their default values.
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| extraArgs | list | `[]` | |
| extraConfigmapMounts | list | `[]` | |
| extraContainers | string | `""` | |
| extraSecretMounts | list | `[]` | |
| extraVars | list | `[]` | |
| extraVolumeMounts | list | `[]` | |
| fullnameOverride | string | `""` | |
| hostnameOverride | string | `""` | |
| image.pullPolicy | string | `"Always"` | |
| image.repository | string | `"codercom/code-server"` | |
| image.tag | string | `"3.7.3"` | |
| imagePullSecrets | list | `[]` | |
| ingress.enabled | bool | `false` | |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| persistence.accessMode | string | `"ReadWriteOnce"` | |
| persistence.annotations | object | `{}` | |
| persistence.enabled | bool | `true` | |
| persistence.size | string | `"1Gi"` | |
| podAnnotations | object | `{}` | |
| podSecurityContext | object | `{}` | |
| replicaCount | int | `1` | |
| resources | object | `{}` | |
| securityContext.enabled | bool | `true` | |
| securityContext.fsGroup | int | `1000` | |
| securityContext.runAsUser | int | `1000` | |
| service.port | int | `8443` | |
| service.type | string | `"ClusterIP"` | |
| serviceAccount.create | bool | `true` | |
| serviceAccount.name | string | `nil` | |
| tolerations | list | `[]` | |
| volumePermissions.enabled | bool | `true` | |
| volumePermissions.securityContext.runAsUser | int | `0` | |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm
install`. For example,
```console
$ helm upgrade --install code-server \
ci/helm-chart \
--set persistence.enabled=false
```
The above command sets the the persistence storage to false.
Alternatively, a YAML file that specifies the values for the above parameters
can be provided while installing the chart. For example,
```console
$ helm upgrade --install code-server ci/helm-chart -f values.yaml
```
> **Tip**: You can use the default [values.yaml](values.yaml)

View File

@@ -0,0 +1,25 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "code-server.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "code-server.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "code-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80
{{- end }}
Administrator credentials:
Password: echo $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode)

View File

@@ -0,0 +1,63 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "code-server.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "code-server.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "code-server.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "code-server.labels" -}}
helm.sh/chart: {{ include "code-server.chart" . }}
{{ include "code-server.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "code-server.selectorLabels" -}}
app.kubernetes.io/name: {{ include "code-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "code-server.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "code-server.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,152 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "code-server.fullname" . }}
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
helm.sh/chart: {{ include "code-server.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
{{- if .Values.hostnameOverride }}
hostname: {{ .Values.hostnameOverride }}
{{- end }}
{{- if .Values.securityContext.enabled }}
securityContext:
fsGroup: {{ .Values.securityContext.fsGroup }}
{{- end }}
{{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }}
initContainers:
- name: init-chmod-data
image: busybox:latest
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} /home/coder
securityContext:
runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }}
volumeMounts:
- name: data
mountPath: /home/coder
{{- end }}
containers:
{{- if .Values.extraContainers }}
{{ toYaml .Values.extraContainers | indent 8}}
{{- end }}
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.securityContext.enabled }}
securityContext:
runAsUser: {{ .Values.securityContext.runAsUser }}
{{- end }}
env:
{{- if .Values.extraVars }}
{{ toYaml .Values.extraVars | indent 10 }}
{{- end }}
- name: PASSWORD
valueFrom:
secretKeyRef:
{{- if .Values.existingSecret }}
name: {{ .Values.existingSecret }}
{{- else }}
name: {{ template "code-server.fullname" . }}
{{- end }}
key: password
{{- if .Values.extraArgs }}
args:
{{ toYaml .Values.extraArgs | indent 10 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /home/coder
{{- range .Values.extraConfigmapMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }}
{{- end }}
{{- range .Values.extraSecretMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
readOnly: {{ .readOnly }}
{{- end }}
{{- range .Values.extraVolumeMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ template "code-server.serviceAccountName" . }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
{{- if not .Values.persistence.hostPath }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "code-server.fullname" .) }}
{{- else }}
hostPath:
path: {{ .Values.persistence.hostPath }}
type: Directory
{{- end -}}
{{- else }}
emptyDir: {}
{{- end -}}
{{- range .Values.extraSecretMounts }}
- name: {{ .name }}
secret:
secretName: {{ .secretName }}
defaultMode: {{ .defaultMode }}
{{- end }}
{{- range .Values.extraVolumeMounts }}
- name: {{ .name }}
{{- if .existingClaim }}
persistentVolumeClaim:
claimName: {{ .existingClaim }}
{{- else }}
hostPath:
path: {{ .hostPath }}
type: Directory
{{- end }}
{{- end }}

View File

@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "code-server.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "code-server.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ . }}
backend:
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,29 @@
{{- if and (and .Values.persistence.enabled (not .Values.persistence.existingClaim)) (not .Values.persistence.hostPath) }}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ include "code-server.fullname" . }}
namespace: {{ .Release.Namespace }}
{{- with .Values.persistence.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
helm.sh/chart: {{ include "code-server.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
accessModes:
- {{ .Values.persistence.accessMode | quote }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- if .Values.persistence.storageClass }}
{{- if (eq "-" .Values.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: "{{ .Values.persistence.storageClass }}"
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "code-server.fullname" . }}
annotations:
"helm.sh/hook": "pre-install"
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
helm.sh/chart: {{ include "code-server.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
type: Opaque
data:
{{ if .Values.password }}
password: "{{ .Values.password | b64enc }}"
{{ else }}
password: "{{ randAlphaNum 24 | b64enc }}"
{{ end }}

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "code-server.fullname" . }}
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
helm.sh/chart: {{ include "code-server.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "code-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}

View File

@@ -0,0 +1,11 @@
{{- if or .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
helm.sh/chart: {{ include "code-server.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
name: {{ template "code-server.serviceAccountName" . }}
{{- end -}}

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "code-server.fullname" . }}-test-connection"
labels:
app.kubernetes.io/name: {{ include "code-server.name" . }}
helm.sh/chart: {{ include "code-server.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

163
ci/helm-chart/values.yaml Normal file
View File

@@ -0,0 +1,163 @@
# Default values for code-server.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: codercom/code-server
tag: '3.7.3'
pullPolicy: Always
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
hostnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
#annotations:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
#hosts:
# - host: code-server.example.loc
# paths:
# - /
#tls:
# - secretName: code-server
# hosts:
# - code-server.example.loc
# Optional additional arguments
extraArgs: []
# - --allow-http
# - --no-auth
# Optional additional environment variables
extraVars: []
# - name: DISABLE_TELEMETRY
# value: true
##
## Init containers parameters:
## volumePermissions: Change the owner of the persist volume mountpoint to RunAsUser:fsGroup
##
volumePermissions:
enabled: true
securityContext:
runAsUser: 0
## Pod Security Context
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
##
securityContext:
enabled: true
fsGroup: 1000
runAsUser: 1000
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 1000Mi
nodeSelector: {}
tolerations: []
affinity: {}
## Persist data to a persistent volume
persistence:
enabled: true
## code-server data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
accessMode: ReadWriteOnce
size: 10Gi
annotations: {}
# existingClaim: ""
# hostPath: /data
serviceAccount:
create: true
name:
## Enable an Specify container in extraContainers.
## This is meant to allow adding code-server dependencies, like docker-dind.
extraContainers: |
#- name: docker-dind
# image: docker:19.03-dind
# imagePullPolicy: IfNotPresent
# resources:
# requests:
# cpu: 250m
# memory: 256M
# securityContext:
# privileged: true
# procMount: Default
# env:
# - name: DOCKER_TLS_CERTDIR
# value: ""
# - name: DOCKER_DRIVER
# value: "overlay2"
## Additional code-server secret mounts
extraSecretMounts: []
# - name: secret-files
# mountPath: /etc/secrets
# secretName: code-server-secret-files
# readOnly: true
## Additional code-server volume mounts
extraVolumeMounts: []
# - name: extra-volume
# mountPath: /mnt/volume
# readOnly: true
# existingClaim: volume-claim
# hostPath: ""
extraConfigmapMounts: []
# - name: certs-configmap
# mountPath: /etc/code-server/ssl/
# subPath: certificates.crt # (optional)
# configMap: certs-configmap
# readOnly: true

View File

@@ -27,6 +27,6 @@ ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
# Install Go dependencies
ENV GO111MODULE=on
RUN go get mvdan.cc/sh/v3/cmd/shfmt
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0
RUN curl -fsSL https://get.docker.com | sh

View File

@@ -43,6 +43,12 @@ ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
# Install Go dependencies
ENV GO111MODULE=on
RUN go get mvdan.cc/sh/v3/cmd/shfmt
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0
RUN VERSION="$(curl -fsSL https://storage.googleapis.com/kubernetes-release/release/stable.txt)" && \
curl -fsSL "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl" > /usr/local/bin/kubectl \
&& chmod +x /usr/local/bin/kubectl
RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
RUN helm plugin install https://github.com/instrumenta/helm-kubeval
RUN curl -fsSL https://get.docker.com | sh

View File

@@ -8,6 +8,7 @@
- [Build](#build)
- [Structure](#structure)
- [VS Code Patch](#vs-code-patch)
- [Currently Known Issues](#currently-known-issues)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -15,24 +16,26 @@
## Pull Requests
Please link to the issue each PR solves.
If there is no existing issue, please first create one unless the fix is minor.
Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue
you'd like to address unless the proposed fix is minor.
Please make sure the base of your PR is the master branch. We keep the GitHub
default branch the latest release branch to avoid confusion as the
documentation is on GitHub and we don't want users to see docs on unreleased
features.
In your Pull Requests (PR), link to the issue that the PR solves.
Please ensure that the base of your PR is the **master** branch. (Note: The default
GitHub branch is the latest release branch, though you should point all of your changes to be merged into
master).
## Requirements
Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
The prerequisites for contributing to code-server are almost the same as those for
[VS Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
There are several differences, however. You must:
Differences:
- Use Node.js version 12.x (or greater)
- Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed
- We require a minimum of node v12 but later versions should work.
- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
- We use [jq](https://stedolan.github.io/jq/) to build code-server releases.
- The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all our dependencies.
The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all
of the dependencies code-server uses.
## Development Workflow
@@ -40,10 +43,10 @@ Differences:
yarn
yarn vscode
yarn watch
# Visit http://localhost:8080 once the build completed.
# Visit http://localhost:8080 once the build is completed.
```
To develop inside of an isolated docker container:
To develop inside an isolated Docker container:
```shell
./ci/dev/image/run.sh yarn
@@ -53,12 +56,12 @@ To develop inside of an isolated docker container:
`yarn watch` will live reload changes to the source.
If changes are made to the patch and you've built previously you must manually
reset VS Code then run `yarn vscode:patch`.
If you introduce changes to the patch and you've previously built, you
must (1) manually reset VS Code and (2) run `yarn vscode:patch`.
## Build
You can build with:
You can build using:
```shell
./ci/dev/image/run.sh ./ci/steps/release.sh
@@ -66,22 +69,22 @@ You can build with:
Run your build with:
```
```shell
cd release
yarn --production
# Runs the built JavaScript with Node.
node .
```
Build release packages (make sure you run `./ci/steps/release.sh` first):
Build the release packages (make sure that you run `./ci/steps/release.sh` first):
```
```shell
IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh
# The standalone release is in ./release-standalone
# .deb, .rpm and the standalone archive are in ./release-packages
```
The `release.sh` script is the equivalent of:
The `release.sh` script is equal to running:
```shell
yarn
@@ -91,73 +94,69 @@ yarn build:vscode
yarn release
```
And `release-packages.sh` is:
And `release-packages.sh` is equal to:
```
```shell
yarn release:standalone
yarn test:standalone-release
yarn package
```
For a faster release build you can also run:
For a faster release build, you can run instead:
```
```shell
KEEP_MODULES=1 ./ci/steps/release.sh
node ./release
```
## Structure
The `code-server` script serves an HTTP API to login and start a remote VS Code process.
The `code-server` script serves an HTTP API for login and starting a remote VS Code process.
The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in
[./src/node/app](./src/node/app).
Most of the meaty parts are in our VS Code patch which is described next.
Most of the meaty parts are in the VS Code patch, which we described next.
### VS Code Patch
Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase
into a frontend and server. The frontend consisted of all UI code and the server ran
the extensions and exposed an API to the frontend for file access and everything else
that the UI needed.
In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end
and a server. The front-end consisted of all UI code, while the server ran the extensions
and exposed an API to the front-end for file access and all UI needs.
This worked but eventually Microsoft added support to VS Code to run it in the web.
They have open sourced the frontend but have kept the server closed source.
So in interest of piggy backing off their work, v2 and beyond use the VS Code
web frontend and fill in the server. This is contained in our
Over time, Microsoft added support to VS Code to run it on the web. They have made
the front-end open source, but not the server. As such, code-server v2 (and later) uses
the VS Code front-end and implements the server. You can find this in
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
Other notable changes in our patch include:
- Add our own build file which includes our code and VS Code's web code.
- Allow multiple extension directories (both user and built-in).
- Modify the loader, websocket, webview, service worker, and asset requests to
use the URL of the page as a base (and TLS if necessary for the websocket).
- Send client-side telemetry through the server.
- Allow modification of the display language.
- Make it possible for us to load code on the client.
- Make extensions work in the browser.
- Make it possible to install extensions of any kind.
- Fix getting permanently disconnected when you sleep or hibernate for a while.
- Add connection type to web socket query parameters.
- Adding our build file, which includes our code and VS Code's web code
- Allowing multiple extension directories (both user and built-in)
- Modifying the loader, websocket, webview, service worker, and asset requests to
use the URL of the page as a base (and TLS, if necessary for the websocket)
- Sending client-side telemetry through the server
- Allowing modification of the display language
- Making it possible for us to load code on the client
- Making extensions work in the browser
- Making it possible to install extensions of any kind
- Fixing issue with getting disconnected when your machine sleeps or hibernates
- Adding connection type to web socket query parameters
Some known issues presently:
- Creating custom VS Code extensions and debugging them doesn't work.
- Extension profiling and tips are currently disabled.
As the web portion of VS Code matures, we'll be able to shrink and maybe even entirely
eliminate our patch. In the meantime, however, upgrading the VS Code version requires
ensuring that the patch still applies and has the intended effects.
To generate a new patch run `yarn vscode:diff`.
**note**: We have extension docs on the CI and build system at [./ci/README.md](../ci/README.md)
If functionality doesn't depend on code from VS Code then it should be moved
into code-server otherwise it should be in the patch.
In the future we'd like to run VS Code unit tests against our builds to ensure features
As the web portion of VS Code matures, we'll be able to shrink and possibly
eliminate our patch. In the meantime, upgrading the VS Code version requires
us to ensure that the patch is applied and works as intended. In the future,
we'd like to run VS Code unit tests against our builds to ensure that features
work as expected.
To generate a new patch, run `yarn vscode:diff`
**Note**: We have [extension docs](../ci/README.md) on the CI and build system.
If the functionality you're working on does NOT depend on code from VS Code, please
move it out and into code-server.
### Currently Known Issues
- Creating custom VS Code extensions and debugging them doesn't work
- Extension profiling and tips are currently disabled

View File

@@ -3,6 +3,7 @@
# FAQ
- [Questions?](#questions)
- [iPad Status?](#ipad-status)
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
@@ -21,7 +22,6 @@
- [Heartbeat File](#heartbeat-file)
- [Healthz endpoint](#healthz-endpoint)
- [How does the config file work?](#how-does-the-config-file-work)
- [Blank screen on iPad?](#blank-screen-on-ipad)
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
- [Differences compared to Theia?](#differences-compared-to-theia)
@@ -33,6 +33,10 @@
Please file all questions and support requests at https://github.com/cdr/code-server/discussions.
## iPad Status?
Please see [./ipad.md](./ipad.md).
## How can I reuse my VS Code configuration?
The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works.
@@ -144,6 +148,9 @@ For HTTPS, you can use a self signed certificate by passing in just `--cert` or
pass in an existing certificate by providing the path to `--cert` and the path to
the key with `--cert-key`.
The self signed certificate will be generated into
`~/.local/share/code-server/self-signed.crt`.
If `code-server` has been passed a certificate it will also respond to HTTPS
requests and will redirect all HTTP requests to HTTPS.
@@ -279,15 +286,6 @@ The `--config` flag or `$CODE_SERVER_CONFIG` can be used to change the config fi
The default location also respects `$XDG_CONFIG_HOME`.
## Blank screen on iPad?
Unfortunately at the moment self signed certificates cause a blank screen on iPadOS
There does seem to be a way to get it to work if you create your own CA and create a
certificate using the CA and then import the CA onto your iPad.
See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434).
## Isn't an install script piped into sh insecure?
Please give

View File

@@ -251,8 +251,7 @@ Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
### Self Signed Certificate
**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
have to use [Let's Encrypt](#lets-encrypt) instead. See the [FAQ](./FAQ.md#blank-screen-on-ipad).
**note:** Self signed certificates do not work with iPad normally. See [./ipad.md](./ipad.md) for details.
Recommended reading: https://security.stackexchange.com/a/8112.

View File

@@ -2,6 +2,7 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Install
- [Upgrading](#upgrading)
- [install.sh](#installsh)
- [Flags](#flags)
- [Detection Reference](#detection-reference)
@@ -12,12 +13,19 @@
- [macOS](#macos)
- [Standalone Releases](#standalone-releases)
- [Docker](#docker)
- [helm](#helm)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
This document demonstrates how to install `code-server` on
various distros and operating systems.
## Upgrading
When upgrading you can just install the new version over the old one. code-server
maintains all user data in `~/.local/share/code-server` so that it is preserved in between
installations.
## install.sh
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
@@ -79,8 +87,8 @@ commands presented in the rest of this document.
## Debian, Ubuntu
```bash
curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server_3.6.1_amd64.deb
sudo dpkg -i code-server_3.6.1_amd64.deb
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server_3.7.3_amd64.deb
sudo dpkg -i code-server_3.7.3_amd64.deb
sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
```
@@ -88,8 +96,8 @@ sudo systemctl enable --now code-server@$USER
## Fedora, CentOS, RHEL, SUSE
```bash
curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-amd64.rpm
sudo rpm -i code-server-3.6.1-amd64.rpm
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-amd64.rpm
sudo rpm -i code-server-3.7.3-amd64.rpm
sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
```
@@ -158,10 +166,10 @@ Here is an example script for installing and using a standalone `code-server` re
```bash
mkdir -p ~/.local/lib ~/.local/bin
curl -fL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-linux-amd64.tar.gz \
curl -fL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-linux-amd64.tar.gz \
| tar -C ~/.local/lib -xz
mv ~/.local/lib/code-server-3.6.1-linux-amd64 ~/.local/lib/code-server-3.6.1
ln -s ~/.local/lib/code-server-3.6.1/bin/code-server ~/.local/bin/code-server
mv ~/.local/lib/code-server-3.7.3-linux-amd64 ~/.local/lib/code-server-3.7.3
ln -s ~/.local/lib/code-server-3.7.3/bin/code-server ~/.local/bin/code-server
PATH="~/.local/bin:$PATH"
code-server
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
@@ -192,3 +200,7 @@ Our official image supports `amd64` and `arm64`.
For `arm32` support there is a popular community maintained alternative:
https://hub.docker.com/r/linuxserver/code-server
## helm
See [the chart](../ci/helm-chart).

53
doc/ipad.md Normal file
View File

@@ -0,0 +1,53 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# iPad
- [Known Issues](#known-issues)
- [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Known Issues
- Getting self signed certificates certificates to work is involved, see below.
- Keyboard may disappear sometimes [#1313](https://github.com/cdr/code-server/issues/1313), [#979](https://github.com/cdr/code-server/issues/979)
- Trackpad scrolling does not work [#1455](https://github.com/cdr/code-server/issues/1455)
- See [issues tagged with the iPad label](https://github.com/cdr/code-server/issues?q=is%3Aopen+is%3Aissue+label%3AiPad) for more.
## How to access code-server with a self signed certificate on iPad?
Accessing a self signed certificate on iPad isn't as easy as accepting through all
the security warnings. Safari will prevent WebSocket connections unless the certificate
is installed as a profile on the device.
The below assumes you are using the self signed certificate that code-server
generates for you. If not, that's fine but you'll have to make sure your certificate
abides by the following guidelines from Apple: https://support.apple.com/en-us/HT210176
**note**: Another undocumented requirement we noticed is that the certificate has to have `basicConstraints=CA:true`.
The following instructions assume you have code-server installed and running
with a self signed certificate. If not, please first go through [./guide.md](./guide.md)!
**warning**: Your iPad must access code-server via a domain name. It could be local
DNS like `mymacbookpro.local` but it must be a domain name. Otherwise Safari will
refuse to allow WebSockets to connect.
1. Your certificate **must** have a subject alt name that matches the hostname
at which you will access code-server from your iPad. You can pass this to code-server
so that it generates the certificate correctly with `--cert-host`.
2. Share your self signed certificate with the iPad.
- code-server will print the location of the certificate it has generated in the logs.
```
[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt
```
- You can mail it to yourself or if you have a Mac, it's easiest to just Airdrop to the iPad.
3. When opening the `*.crt` file, you'll be prompted to go into settings to install.
4. Go to `Settings -> General -> Profile`, select the profile and then hit `Install`.
- It should say the profile is verified.
5. Go to `Settings -> About -> Certificate Trust Settings` and enable full trust for
the certificate.
6. Now you can access code-server! 🍻

View File

@@ -1,7 +1,7 @@
{
"name": "code-server",
"license": "MIT",
"version": "3.6.1",
"version": "3.7.3",
"description": "Run VS Code on a remote server.",
"homepage": "https://github.com/cdr/code-server",
"bugs": {
@@ -30,6 +30,9 @@
},
"main": "out/node/entry.js",
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.8",
"@types/fs-extra": "^8.0.1",
"@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.3",
@@ -40,11 +43,12 @@
"@types/safe-compare": "^1.1.0",
"@types/semver": "^7.1.0",
"@types/split2": "^2.1.6",
"@types/supertest": "^2.0.10",
"@types/tar-fs": "^2.0.0",
"@types/tar-stream": "^2.1.0",
"@types/ws": "^7.2.6",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"doctoc": "^1.4.0",
"eslint": "^7.7.0",
"eslint-config-prettier": "^6.0.0",
@@ -56,6 +60,7 @@
"prettier": "^2.0.5",
"stylelint": "^13.0.0",
"stylelint-config-recommended": "^3.0.0",
"supertest": "^6.0.1",
"ts-node": "^9.0.0",
"typescript": "4.0.2"
},
@@ -66,13 +71,17 @@
},
"dependencies": {
"@coder/logger": "1.1.16",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"env-paths": "^2.2.0",
"express": "^5.0.0-alpha.8",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.0",
"httpolyglot": "^0.1.2",
"js-yaml": "^3.13.1",
"limiter": "^1.1.5",
"pem": "^1.14.2",
"qs": "6.7.0",
"rotating-file-stream": "^2.1.1",
"safe-buffer": "^5.1.1",
"safe-compare": "^1.1.4",

View File

@@ -37,3 +37,7 @@ body {
.login-form > .field > .submit {
margin-left: 20px;
}
input {
-webkit-appearance: none;
}

View File

@@ -32,11 +32,6 @@
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Prefetch to avoid waterfall -->
<!-- PROD_ONLY
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
END_PROD_ONLY -->
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>

View File

@@ -41,7 +41,6 @@ try {
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
"xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`,
"tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`,
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,

View File

@@ -1,4 +1,10 @@
import { Callback } from "./types"
import { logger } from "@coder/logger"
/**
* Event emitter callback. Called with the emitted value and a promise that
* resolves when all emitters have finished.
*/
export type Callback<T, R = void | Promise<void>> = (t: T, p: Promise<void>) => R
export interface Disposable {
dispose(): void
@@ -32,8 +38,21 @@ export class Emitter<T> {
/**
* Emit an event with a value.
*/
public emit(value: T): void {
this.listeners.forEach((cb) => cb(value))
public async emit(value: T): Promise<void> {
let resolve: () => void
const promise = new Promise<void>((r) => (resolve = r))
await Promise.all(
this.listeners.map(async (cb) => {
try {
await cb(value, promise)
} catch (error) {
logger.error(error.message)
}
}),
)
resolve!()
}
public dispose(): void {

View File

@@ -8,8 +8,12 @@ export enum HttpCode {
ServerError = 500,
}
/**
* Represents an error with a message and an HTTP status code. This code will be
* used in the HTTP response.
*/
export class HttpError extends Error {
public constructor(message: string, public readonly code: number, public readonly details?: object) {
public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) {
super(message)
this.name = this.constructor.name
}

View File

@@ -1 +0,0 @@
export type Callback<T, R = void> = (t: T) => R

61
src/node/app.ts Normal file
View File

@@ -0,0 +1,61 @@
import { logger } from "@coder/logger"
import express, { Express } from "express"
import { promises as fs } from "fs"
import http from "http"
import * as httpolyglot from "httpolyglot"
import { DefaultedArgs } from "./cli"
import { handleUpgrade } from "./wsRouter"
/**
* Create an Express app and an HTTP/S server to serve it.
*/
export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => {
const app = express()
const server = args.cert
? httpolyglot.createServer(
{
cert: args.cert && (await fs.readFile(args.cert.value)),
key: args["cert-key"] && (await fs.readFile(args["cert-key"])),
},
app,
)
: http.createServer(app)
await new Promise<http.Server>(async (resolve, reject) => {
server.on("error", reject)
if (args.socket) {
try {
await fs.unlink(args.socket)
} catch (error) {
if (error.code !== "ENOENT") {
logger.error(error.message)
}
}
server.listen(args.socket, resolve)
} else {
// [] is the correct format when using :: but Node errors with them.
server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve)
}
})
const wsApp = express()
handleUpgrade(wsApp, server)
return [app, wsApp, server]
}
/**
* Get the address of a server as a string (protocol *is* included) while
* ensuring there is one (will throw if there isn't).
*/
export const ensureAddress = (server: http.Server): string => {
const addr = server.address()
if (!addr) {
throw new Error("server has no address")
}
if (typeof addr !== "string") {
return `http://${addr.address}:${addr.port}`
}
return addr
}

View File

@@ -1,21 +0,0 @@
import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http"
/**
* Check the heartbeat.
*/
export class HealthHttpProvider extends HttpProvider {
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
super(options)
}
public async handleRequest(): Promise<HttpResponse> {
return {
cache: false,
mime: "application/json",
content: {
status: this.heart.alive() ? "alive" : "expired",
lastHeartbeat: this.heart.lastHeartbeat,
},
}
}
}

View File

@@ -1,144 +0,0 @@
import * as http from "http"
import * as limiter from "limiter"
import * as querystring from "querystring"
import { HttpCode, HttpError } from "../../common/http"
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { hash, humanPath } from "../util"
interface LoginPayload {
password?: string
/**
* Since we must set a cookie with an absolute path, we need to know the full
* base path.
*/
base?: string
}
/**
* Login HTTP provider.
*/
export class LoginHttpProvider extends HttpProvider {
public constructor(
options: HttpProviderOptions,
private readonly configFile: string,
private readonly envPassword: boolean,
) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
case "/":
switch (request.method) {
case "POST":
this.ensureMethod(request, ["GET", "POST"])
return this.tryLogin(route, request)
default:
this.ensureMethod(request)
if (this.authenticated(request)) {
return {
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
query: { to: undefined },
}
}
return this.getRoot(route)
}
}
throw new HttpError("Not found", HttpCode.NotFound)
}
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.`
if (this.envPassword) {
passwordMsg = "Password was set from $PASSWORD."
}
response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
return this.replaceTemplates(route, response)
}
private readonly limiter = new RateLimiter()
/**
* Try logging in. On failure, show the login page with an error.
*/
private async tryLogin(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
// Already authenticated via cookies?
const providedPassword = this.authenticated(request)
if (providedPassword) {
return { code: HttpCode.Ok }
}
try {
if (!this.limiter.try()) {
throw new Error("Login rate limited!")
}
const data = await this.getData(request)
const payload = data ? querystring.parse(data) : {}
return await this.login(payload, route, request)
} catch (error) {
return this.getRoot(route, error)
}
}
/**
* Return a cookie if the user is authenticated otherwise throw an error.
*/
private async login(payload: LoginPayload, route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
const password = this.authenticated(request, {
key: typeof payload.password === "string" ? [hash(payload.password)] : undefined,
})
if (password) {
return {
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
query: { to: undefined },
cookie:
typeof password === "string"
? {
key: "key",
value: password,
path: payload.base,
}
: undefined,
}
}
// Only log if it was an actual login attempt.
if (payload && payload.password) {
console.error(
"Failed login attempt",
JSON.stringify({
xForwardedFor: request.headers["x-forwarded-for"],
remoteAddress: request.connection.remoteAddress,
userAgent: request.headers["user-agent"],
timestamp: Math.floor(new Date().getTime() / 1000),
}),
)
throw new Error("Incorrect password")
}
throw new Error("Missing password")
}
}
// RateLimiter wraps around the limiter library for logins.
// It allows 2 logins every minute and 12 logins every hour.
class RateLimiter {
private readonly minuteLimiter = new limiter.RateLimiter(2, "minute")
private readonly hourLimiter = new limiter.RateLimiter(12, "hour")
public try(): boolean {
if (this.minuteLimiter.tryRemoveTokens(1)) {
return true
}
return this.hourLimiter.tryRemoveTokens(1)
}
}

View File

@@ -1,43 +0,0 @@
import * as http from "http"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
/**
* Proxy HTTP provider.
*/
export class ProxyHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (!this.authenticated(request)) {
if (this.isRoot(route)) {
return { redirect: "/login", query: { to: route.fullPath } }
}
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
// Ensure there is a trailing slash so relative paths work correctly.
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
return {
redirect: `${route.fullPath}/`,
}
}
const port = route.base.replace(/^\//, "")
return {
proxy: {
strip: `${route.providerBase}/${port}`,
port,
},
}
}
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
this.ensureAuthenticated(request)
const port = route.base.replace(/^\//, "")
return {
proxy: {
strip: `${route.providerBase}/${port}`,
port,
},
}
}
}

View File

@@ -1,73 +0,0 @@
import { field, logger } from "@coder/logger"
import * as http from "http"
import * as path from "path"
import { Readable } from "stream"
import * as tarFs from "tar-fs"
import * as zlib from "zlib"
import { HttpProvider, HttpResponse, Route } from "../http"
import { pathToFsPath } from "../util"
/**
* Static file HTTP provider. Static requests do not require authentication if
* the resource is in the application's directory except requests to serve a
* directory as a tar which always requires authentication.
*/
export class StaticHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureMethod(request)
if (typeof route.query.tar === "string") {
this.ensureAuthenticated(request)
return this.getTarredResource(request, pathToFsPath(route.query.tar))
}
const response = await this.getReplacedResource(request, route)
if (!this.isDev) {
response.cache = true
}
return response
}
/**
* Return a resource with variables replaced where necessary.
*/
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
// The first part is always the commit (for caching purposes).
const split = route.requestPath.split("/").slice(1)
const resourcePath = path.resolve("/", ...split)
// Make sure it's in code-server or a plugin.
const validPaths = [this.rootPath, process.env.PLUGIN_DIR]
if (!validPaths.find((p) => p && resourcePath.startsWith(p))) {
this.ensureAuthenticated(request)
}
switch (split[split.length - 1]) {
case "manifest.json": {
const response = await this.getUtf8Resource(resourcePath)
return this.replaceTemplates(route, response)
}
}
return this.getResource(resourcePath)
}
/**
* Tar up and stream a directory.
*/
private async getTarredResource(request: http.IncomingMessage, ...parts: string[]): Promise<HttpResponse> {
const filePath = path.join(...parts)
let stream: Readable = tarFs.pack(filePath)
const headers: http.OutgoingHttpHeaders = {}
if (request.headers["accept-encoding"] && request.headers["accept-encoding"].includes("gzip")) {
logger.debug("gzipping tar", field("filePath", filePath))
const compress = zlib.createGzip()
stream.pipe(compress)
stream.on("error", (error) => compress.destroy(error))
stream.on("close", () => compress.end())
stream = compress
headers["content-encoding"] = "gzip"
}
return { stream, filePath, mime: "application/x-tar", cache: true, headers }
}
}

View File

@@ -1,237 +0,0 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as crypto from "crypto"
import * as fs from "fs-extra"
import * as http from "http"
import * as net from "net"
import * as path from "path"
import {
CodeServerMessage,
Options,
StartPath,
VscodeMessage,
VscodeOptions,
WorkbenchOptions,
} from "../../../lib/vscode/src/vs/server/ipc"
import { HttpCode, HttpError } from "../../common/http"
import { arrayify, generateUuid } from "../../common/util"
import { Args } from "../cli"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings } from "../settings"
import { pathToFsPath } from "../util"
export class VscodeHttpProvider extends HttpProvider {
private readonly serverRootPath: string
private readonly vsRootPath: string
private _vscode?: Promise<cp.ChildProcess>
public constructor(options: HttpProviderOptions, private readonly args: Args) {
super(options)
this.vsRootPath = path.resolve(this.rootPath, "lib/vscode")
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
}
public get running(): boolean {
return !!this._vscode
}
public async dispose(): Promise<void> {
if (this._vscode) {
const vscode = await this._vscode
vscode.removeAllListeners()
this._vscode = undefined
vscode.kill()
}
}
private async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
const id = generateUuid()
const vscode = await this.fork()
logger.debug("setting up vs code...")
return new Promise<WorkbenchOptions>((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("got message from vs code", field("message", message))
return message.type === "options" && message.id === id
? resolve(message.options)
: reject(new Error("Unexpected response during initialization"))
})
vscode.once("error", reject)
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
this.send({ type: "init", id, options }, vscode)
})
}
private fork(): Promise<cp.ChildProcess> {
if (!this._vscode) {
logger.debug("forking vs code...")
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
vscode.on("error", (error) => {
logger.error(error.message)
this._vscode = undefined
})
vscode.on("exit", (code) => {
logger.error(`VS Code exited unexpectedly with code ${code}`)
this._vscode = undefined
})
this._vscode = new Promise((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("got message from vs code", field("message", message))
return message.type === "ready"
? resolve(vscode)
: reject(new Error("Unexpected response waiting for ready response"))
})
vscode.once("error", reject)
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
})
}
return this._vscode
}
public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise<void> {
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
// VS Code expects a raw socket. It will handle all the web socket frames.
// We just need to handle the initial upgrade.
// This magic value is specified by the websocket spec.
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const reply = crypto
.createHash("sha1")
.update(request.headers["sec-websocket-key"] + magic)
.digest("base64")
socket.write(
[
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${reply}`,
].join("\r\n") + "\r\n\r\n",
)
const vscode = await this._vscode
this.send({ type: "socket", query: route.query }, vscode, socket)
}
private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
if (!vscode || vscode.killed) {
throw new Error("vscode is not running")
}
vscode.send(message, socket)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureMethod(request)
switch (route.base) {
case "/":
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: route.providerBase } }
}
try {
return await this.getRoot(request, route)
} catch (error) {
const message = `<div>VS Code failed to load.</div> ${
this.isDev
? `<div>It might not have finished compiling.</div>` +
`Check for <code>Finished <span class="success">compilation</span></code> in the output.`
: ""
} <br><br>${error}`
return this.getErrorRoot(route, "VS Code failed to load", "500", message)
}
}
this.ensureAuthenticated(request)
switch (route.base) {
case "/resource":
case "/vscode-remote-resource":
if (typeof route.query.path === "string") {
return this.getResource(pathToFsPath(route.query.path))
}
break
case "/webview":
if (/^\/vscode-resource/.test(route.requestPath)) {
return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
}
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath)
}
throw new HttpError("Not found", HttpCode.NotFound)
}
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
const remoteAuthority = request.headers.host as string
const { lastVisited } = await settings.read()
const startPath = await this.getFirstPath([
{ url: route.query.workspace, workspace: true },
{ url: route.query.folder, workspace: false },
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
lastVisited,
])
const [response, options] = await Promise.all([
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
this.initialize({
args: this.args,
remoteAuthority,
startPath,
}),
])
settings.write({
lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited
query: route.query,
})
if (!this.isDev) {
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
}
options.productConfiguration.codeServerVersion = require("../../../package.json").version
response.content = response.content
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
return this.replaceTemplates<Options>(route, response, {
disableTelemetry: !!this.args["disable-telemetry"],
})
}
/**
* Choose the first non-empty path.
*/
private async getFirstPath(
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
): Promise<StartPath | undefined> {
const isFile = async (path: string): Promise<boolean> => {
try {
const stat = await fs.stat(path)
return stat.isFile()
} catch (error) {
logger.warn(error.message)
return false
}
}
for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i]
const url = arrayify(startPath && startPath.url).find((p) => !!p)
if (startPath && url) {
return {
url,
// The only time `workspace` is undefined is for the command-line
// argument, in which case it's a path (not a URL) so we can stat it
// without having to parse it.
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
}
}
}
return undefined
}
}

View File

@@ -4,8 +4,12 @@ import yaml from "js-yaml"
import * as os from "os"
import * as path from "path"
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
import { AuthType } from "./http"
import { canConnect, generatePassword, humanPath, paths } from "./util"
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
export enum AuthType {
Password = "password",
None = "none",
}
export class Optional<T> {
public constructor(public readonly value?: T) {}
@@ -22,33 +26,34 @@ export enum LogLevel {
export class OptionalString extends Optional<string> {}
export interface Args extends VsArgs {
readonly config?: string
readonly auth?: AuthType
readonly password?: string
readonly cert?: OptionalString
readonly "cert-key"?: string
readonly "disable-telemetry"?: boolean
readonly help?: boolean
readonly host?: string
readonly json?: boolean
config?: string
auth?: AuthType
password?: string
cert?: OptionalString
"cert-host"?: string
"cert-key"?: string
"disable-telemetry"?: boolean
help?: boolean
host?: string
json?: boolean
log?: LogLevel
readonly open?: boolean
readonly port?: number
readonly "bind-addr"?: string
readonly socket?: string
readonly version?: boolean
readonly force?: boolean
readonly "list-extensions"?: boolean
readonly "install-extension"?: string[]
readonly "show-versions"?: boolean
readonly "uninstall-extension"?: string[]
readonly "proxy-domain"?: string[]
readonly locale?: string
readonly _: string[]
readonly "reuse-window"?: boolean
readonly "new-window"?: boolean
open?: boolean
port?: number
"bind-addr"?: string
socket?: string
version?: boolean
force?: boolean
"list-extensions"?: boolean
"install-extension"?: string[]
"show-versions"?: boolean
"uninstall-extension"?: string[]
"proxy-domain"?: string[]
locale?: string
_: string[]
"reuse-window"?: boolean
"new-window"?: boolean
readonly link?: OptionalString
link?: OptionalString
}
interface Option<T> {
@@ -101,7 +106,11 @@ const options: Options<Required<Args>> = {
cert: {
type: OptionalString,
path: true,
description: "Path to certificate. Generated if no path is provided.",
description: "Path to certificate. A self signed certificate is generated if none is provided.",
},
"cert-host": {
type: "string",
description: "Hostname to use when generating a self signed certificate.",
},
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
@@ -325,13 +334,37 @@ export const parse = (
args._.push(arg)
}
logger.debug("parsed command line", field("args", args))
// If a cert was provided a key must also be provided.
if (args.cert && args.cert.value && !args["cert-key"]) {
throw new Error("--cert-key is missing")
}
logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })])
return args
}
export async function setDefaults(args: Args): Promise<Args> {
args = { ...args }
export interface DefaultedArgs extends ConfigArgs {
auth: AuthType
cert?: {
value: string
}
host: string
port: number
"proxy-domain": string[]
verbose: boolean
usingEnvPassword: boolean
"extensions-dir": string
"user-data-dir": string
}
/**
* Take CLI and config arguments (optional) and return a single set of arguments
* with the defaults set. Arguments from the CLI are prioritized over config
* arguments.
*/
export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise<DefaultedArgs> {
const args = Object.assign({}, configArgs || {}, cliArgs)
if (!args["user-data-dir"]) {
await copyOldMacOSDataDir()
@@ -381,7 +414,49 @@ export async function setDefaults(args: Args): Promise<Args> {
break
}
return args
// Default to using a password.
if (!args.auth) {
args.auth = AuthType.Password
}
const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs)
args.host = addr.host
args.port = addr.port
// If we're being exposed to the cloud, we listen on a random address and
// disable auth.
if (args.link) {
args.host = "localhost"
args.port = 0
args.socket = undefined
args.cert = undefined
args.auth = AuthType.None
}
if (args.cert && !args.cert.value) {
const { cert, certKey } = await generateCertificate(args["cert-host"] || "localhost")
args.cert = {
value: cert,
}
args["cert-key"] = certKey
}
const usingEnvPassword = !!process.env.PASSWORD
if (process.env.PASSWORD) {
args.password = process.env.PASSWORD
}
// Ensure it's not readable by child processes.
delete process.env.PASSWORD
// Filter duplicate proxy domains and remove any leading `*.`.
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
args["proxy-domain"] = Array.from(proxyDomains)
return {
...args,
usingEnvPassword,
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
}
async function defaultConfigFile(): Promise<string> {
@@ -392,12 +467,16 @@ cert: false
`
}
interface ConfigArgs extends Args {
config: string
}
/**
* Reads the code-server yaml config file and returns it as Args.
*
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
*/
export async function readConfigFile(configPath?: string): Promise<Args> {
export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
if (!configPath) {
configPath = process.env.CODE_SERVER_CONFIG
if (!configPath) {
@@ -435,12 +514,15 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
}
}
function parseBindAddr(bindAddr: string): [string, number] {
function parseBindAddr(bindAddr: string): Addr {
const u = new URL(`http://${bindAddr}`)
// With the http scheme 80 will be dropped so assume it's 80 if missing. This
// means --bind-addr <addr> without a port will default to 80 as well and not
// the code-server default.
return [u.hostname, u.port ? parseInt(u.port, 10) : 80]
return {
host: u.hostname,
// With the http scheme 80 will be dropped so assume it's 80 if missing.
// This means --bind-addr <addr> without a port will default to 80 as well
// and not the code-server default.
port: u.port ? parseInt(u.port, 10) : 80,
}
}
interface Addr {
@@ -451,7 +533,7 @@ interface Addr {
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
addr = { ...addr }
if (args["bind-addr"]) {
;[addr.host, addr.port] = parseBindAddr(args["bind-addr"])
addr = parseBindAddr(args["bind-addr"])
}
if (args.host) {
addr.host = args.host
@@ -466,16 +548,17 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr {
return addr
}
export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] {
function bindAddrFromAllSources(...argsConfig: Args[]): Addr {
let addr: Addr = {
host: "localhost",
port: 8080,
}
addr = bindAddrFromArgs(addr, configArgs)
addr = bindAddrFromArgs(addr, cliArgs)
for (const args of argsConfig) {
addr = bindAddrFromArgs(addr, args)
}
return [addr.host, addr.port]
return addr
}
async function copyOldMacOSDataDir(): Promise<void> {

13
src/node/constants.ts Normal file
View File

@@ -0,0 +1,13 @@
import { logger } from "@coder/logger"
import * as path from "path"
let pkg: { version?: string; commit?: string } = {}
try {
pkg = require("../../package.json")
} catch (error) {
logger.warn(error.message)
}
export const version = pkg.version || "development"
export const commit = pkg.commit || "development"
export const rootPath = path.resolve(__dirname, "../..")

View File

@@ -1,19 +1,13 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import { promises as fs } from "fs"
import http from "http"
import * as path from "path"
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
import { plural } from "../common/util"
import { HealthHttpProvider } from "./app/health"
import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static"
import { UpdateHttpProvider } from "./app/update"
import { VscodeHttpProvider } from "./app/vscode"
import { createApp, ensureAddress } from "./app"
import {
Args,
bindAddrFromAllSources,
AuthType,
DefaultedArgs,
optionDescriptions,
parse,
readConfigFile,
@@ -22,22 +16,12 @@ import {
shouldRunVsCodeCli,
} from "./cli"
import { coderCloudBind } from "./coder-cloud"
import { AuthType, HttpServer, HttpServerOptions } from "./http"
import { loadPlugins } from "./plugin"
import { generateCertificate, hash, humanPath, open } from "./util"
import { ipcMain, WrapperProcess } from "./wrapper"
import { commit, version } from "./constants"
import { register } from "./routes"
import { humanPath, isFile, open } from "./util"
import { isChild, wrapper } from "./wrapper"
let pkg: { version?: string; commit?: string } = {}
try {
pkg = require("../../package.json")
} catch (error) {
logger.warn(error.message)
}
const version = pkg.version || "development"
const commit = pkg.commit || "development"
export const runVsCodeCli = (args: Args): void => {
export const runVsCodeCli = (args: DefaultedArgs): void => {
logger.debug("forking vs code cli...")
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
env: {
@@ -61,7 +45,7 @@ export const runVsCodeCli = (args: Args): void => {
vscode.on("exit", (code) => process.exit(code || 0))
}
export const openInExistingInstance = async (args: Args, socketPath: string): Promise<void> => {
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
type: "open",
folderURIs: [],
@@ -70,21 +54,12 @@ export const openInExistingInstance = async (args: Args, socketPath: string): Pr
forceNewWindow: args["new-window"],
}
const isDir = async (path: string): Promise<boolean> => {
try {
const st = await fs.stat(path)
return st.isDirectory()
} catch (error) {
return false
}
}
for (let i = 0; i < args._.length; i++) {
const fp = path.resolve(args._[i])
if (await isDir(fp)) {
pipeArgs.folderURIs.push(fp)
} else {
if (await isFile(fp)) {
pipeArgs.fileURIs.push(fp)
} else {
pipeArgs.folderURIs.push(fp)
}
}
@@ -117,144 +92,84 @@ export const openInExistingInstance = async (args: Args, socketPath: string): Pr
vscode.end()
}
const main = async (args: Args, configArgs: Args): Promise<void> => {
if (args.link) {
// If we're being exposed to the cloud, we listen on a random address and disable auth.
args = {
...args,
host: "localhost",
port: 0,
auth: AuthType.None,
socket: undefined,
cert: undefined,
}
logger.info("link: disabling auth and listening on random localhost port for cloud agent")
}
if (!args.auth) {
args = {
...args,
auth: AuthType.Password,
}
}
const main = async (args: DefaultedArgs): Promise<void> => {
logger.info(`code-server ${version} ${commit}`)
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
const envPassword = !!process.env.PASSWORD
const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password)
if (args.auth === AuthType.Password && !password) {
if (args.auth === AuthType.Password && !args.password) {
throw new Error("Please pass in a password via the config file or $PASSWORD")
}
const [host, port] = bindAddrFromAllSources(args, configArgs)
// Spawn the main HTTP server.
const options: HttpServerOptions = {
auth: args.auth,
commit,
host: host,
// The hash does not add any actual security but we do it for obfuscation purposes.
password: password ? hash(password) : undefined,
port: port,
proxyDomains: args["proxy-domain"],
socket: args.socket,
...(args.cert && !args.cert.value
? await generateCertificate()
: {
cert: args.cert && args.cert.value,
certKey: args["cert-key"],
}),
}
const [app, wsApp, server] = await createApp(args)
const serverAddress = ensureAddress(server)
await register(app, wsApp, server, args)
if (options.cert && !options.certKey) {
throw new Error("--cert-key is missing")
}
const httpServer = new HttpServer(options)
httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args)
httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
await loadPlugins(httpServer, args)
ipcMain.onDispose(() => {
httpServer.dispose().then((errors) => {
errors.forEach((error) => logger.error(error.message))
})
})
logger.info(`code-server ${version} ${commit}`)
logger.info(`Using config file ${humanPath(args.config)}`)
const serverAddress = await httpServer.listen()
logger.info(`HTTP server listening on ${serverAddress}`)
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
if (args.auth === AuthType.Password) {
if (envPassword) {
logger.info(" - Authentication is enabled")
if (args.usingEnvPassword) {
logger.info(" - Using password from $PASSWORD")
} else {
logger.info(` - Using password from ${humanPath(args.config)}`)
}
logger.info(" - To disable use `--auth none`")
} else {
logger.info(" - No authentication")
logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
}
delete process.env.PASSWORD
if (httpServer.protocol === "https") {
logger.info(
args.cert && args.cert.value
? ` - Using provided certificate and key for HTTPS`
: ` - Using generated certificate and key for HTTPS`,
)
if (args.cert) {
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
} else {
logger.info(" - Not serving HTTPS")
}
if (httpServer.proxyDomains.size > 0) {
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
}
if (serverAddress && !options.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
await open(openAddress).catch((error: Error) => {
logger.error("Failed to open", field("address", openAddress), field("error", error))
})
logger.info(`Opened ${openAddress}`)
if (args["proxy-domain"].length > 0) {
logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`)
args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`))
}
if (args.link) {
try {
await coderCloudBind(serverAddress!, args.link.value)
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
logger.info(" - Connected to cloud agent")
} catch (err) {
logger.error(err.message)
ipcMain.exit(1)
wrapper.exit(1)
}
}
if (!args.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace("://0.0.0.0", "://localhost")
try {
await open(openAddress)
logger.info(`Opened ${openAddress}`)
} catch (error) {
logger.error("Failed to open", field("address", openAddress), field("error", error))
}
}
}
async function entry(): Promise<void> {
const cliArgs = parse(process.argv.slice(2))
const configArgs = await readConfigFile(cliArgs.config)
// This prioritizes the flags set in args over the ones in the config file.
let args = Object.assign(configArgs, cliArgs)
args = await setDefaults(args)
// There's no need to check flags like --help or to spawn in an existing
// instance for the child process because these would have already happened in
// the parent and the child wouldn't have been spawned.
if (ipcMain.isChild) {
await ipcMain.handshake()
ipcMain.preventExit()
return main(args, configArgs)
// the parent and the child wouldn't have been spawned. We also get the
// arguments from the parent so we don't have to parse twice and to account
// for environment manipulation (like how PASSWORD gets removed to avoid
// leaking to child processes).
if (isChild(wrapper)) {
const args = await wrapper.handshake()
wrapper.preventExit()
return main(args)
}
const cliArgs = parse(process.argv.slice(2))
const configArgs = await readConfigFile(cliArgs.config)
const args = await setDefaults(cliArgs, configArgs)
if (args.help) {
console.log("code-server", version, commit)
console.log("")
@@ -289,11 +204,10 @@ async function entry(): Promise<void> {
return openInExistingInstance(args, socketPath)
}
const wrapper = new WrapperProcess(require("../../package.json").version)
return wrapper.start()
return wrapper.start(args)
}
entry().catch((error) => {
logger.error(error.message)
ipcMain.exit(error)
wrapper.exit(error)
})

48
src/node/heart.ts Normal file
View File

@@ -0,0 +1,48 @@
import { logger } from "@coder/logger"
import { promises as fs } from "fs"
/**
* Provides a heartbeat using a local file to indicate activity.
*/
export class Heart {
private heartbeatTimer?: NodeJS.Timeout
private heartbeatInterval = 60000
public lastHeartbeat = 0
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
public alive(): boolean {
const now = Date.now()
return now - this.lastHeartbeat < this.heartbeatInterval
}
/**
* Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there is
* activity. Failures are logged as warnings.
*/
public beat(): void {
if (this.alive()) {
return
}
logger.trace("heartbeat")
fs.writeFile(this.heartbeatPath, "").catch((error) => {
logger.warn(error.message)
})
this.lastHeartbeat = Date.now()
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
this.heartbeatTimer = setTimeout(() => {
this.isActive()
.then((active) => {
if (active) {
this.beat()
}
})
.catch((error) => {
logger.warn(error.message)
})
}, this.heartbeatInterval)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,92 +1,249 @@
import { field, logger } from "@coder/logger"
import { Logger, field } from "@coder/logger"
import * as express from "express"
import * as fs from "fs"
import * as path from "path"
import * as util from "util"
import { Args } from "./cli"
import { HttpServer } from "./http"
import { paths } from "./util"
import * as semver from "semver"
import * as pluginapi from "../../typings/pluginapi"
import { version } from "./constants"
import * as util from "./util"
const fsp = fs.promises
/* eslint-disable @typescript-eslint/no-var-requires */
interface Plugin extends pluginapi.Plugin {
/**
* These fields are populated from the plugin's package.json
* and now guaranteed to exist.
*/
name: string
version: string
export type Activate = (httpServer: HttpServer, args: Args) => void
/**
* path to the node module on the disk.
*/
modulePath: string
}
/**
* Plugins must implement this interface.
*/
export interface Plugin {
activate: Activate
interface Application extends pluginapi.Application {
/*
* Clone of the above without functions.
*/
plugin: Omit<Plugin, "init" | "router" | "applications">
}
/**
* Intercept imports so we can inject code-server when the plugin tries to
* import it.
* PluginAPI implements the plugin API described in typings/pluginapi.d.ts
* Please see that file for details.
*/
const originalLoad = require("module")._load
// eslint-disable-next-line @typescript-eslint/no-explicit-any
require("module")._load = function (request: string, parent: object, isMain: boolean): any {
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
}
export class PluginAPI {
private readonly plugins = new Map<string, Plugin>()
private readonly logger: Logger
/**
* Load a plugin and run its activation function.
*/
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
try {
const plugin: Plugin = require(pluginPath)
plugin.activate(httpServer, args)
const packageJson = require(path.join(pluginPath, "package.json"))
logger.debug(
"Loaded plugin",
field("name", packageJson.name || path.basename(pluginPath)),
field("path", pluginPath),
field("version", packageJson.version || "n/a"),
)
} catch (error) {
logger.error(error.message)
public constructor(
logger: Logger,
/**
* These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively.
*/
private readonly csPlugin = "",
private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`,
) {
this.logger = logger.named("pluginapi")
}
}
/**
* Load all plugins in the specified directory.
*/
const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise<void> => {
try {
const files = await util.promisify(fs.readdir)(pluginDir, {
withFileTypes: true,
})
await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args)))
} catch (error) {
if (error.code !== "ENOENT") {
logger.warn(error.message)
/**
* applications grabs the full list of applications from
* all loaded plugins.
*/
public async applications(): Promise<Application[]> {
const apps = new Array<Application>()
for (const [, p] of this.plugins) {
if (!p.applications) {
continue
}
const pluginApps = await p.applications()
// Add plugin key to each app.
apps.push(
...pluginApps.map((app) => {
app = { ...app, path: path.join(p.routerPath, app.path || "") }
app = { ...app, iconPath: path.join(app.path || "", app.iconPath) }
return {
...app,
plugin: {
name: p.name,
version: p.version,
modulePath: p.modulePath,
displayName: p.displayName,
description: p.description,
routerPath: p.routerPath,
homepageURL: p.homepageURL,
},
}
}),
)
}
return apps
}
/**
* mount mounts all plugin routers onto r.
*/
public mount(r: express.Router): void {
for (const [, p] of this.plugins) {
if (!p.router) {
continue
}
r.use(`${p.routerPath}`, p.router())
}
}
/**
* loadPlugins loads all plugins based on this.csPlugin,
* this.csPluginPath and the built in plugins.
*/
public async loadPlugins(): Promise<void> {
for (const dir of this.csPlugin.split(":")) {
if (!dir) {
continue
}
await this.loadPlugin(dir)
}
for (const dir of this.csPluginPath.split(":")) {
if (!dir) {
continue
}
await this._loadPlugins(dir)
}
// Built-in plugins.
await this._loadPlugins(path.join(__dirname, "../../plugins"))
}
/**
* _loadPlugins is the counterpart to loadPlugins.
*
* It differs in that it loads all plugins in a single
* directory whereas loadPlugins uses all available directories
* as documented.
*/
private async _loadPlugins(dir: string): Promise<void> {
try {
const entries = await fsp.readdir(dir, { withFileTypes: true })
for (const ent of entries) {
if (!ent.isDirectory()) {
continue
}
await this.loadPlugin(path.join(dir, ent.name))
}
} catch (err) {
if (err.code !== "ENOENT") {
this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`)
}
}
}
private async loadPlugin(dir: string): Promise<void> {
try {
const str = await fsp.readFile(path.join(dir, "package.json"), {
encoding: "utf8",
})
const packageJSON: PackageJSON = JSON.parse(str)
for (const [, p] of this.plugins) {
if (p.name === packageJSON.name) {
this.logger.warn(
`ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`,
)
return
}
}
const p = this._loadPlugin(dir, packageJSON)
this.plugins.set(p.name, p)
} catch (err) {
if (err.code !== "ENOENT") {
this.logger.warn(`failed to load plugin: ${err.stack}`)
}
}
}
/**
* _loadPlugin is the counterpart to loadPlugin and actually
* loads the plugin now that we know there is no duplicate
* and that the package.json has been read.
*/
private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin {
dir = path.resolve(dir)
const logger = this.logger.named(packageJSON.name)
logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON))
if (!packageJSON.name) {
throw new Error("plugin package.json missing name")
}
if (!packageJSON.version) {
throw new Error("plugin package.json missing version")
}
if (!packageJSON.engines || !packageJSON.engines["code-server"]) {
throw new Error(`plugin package.json missing code-server range like:
"engines": {
"code-server": "^3.7.0"
}
`)
}
if (!semver.satisfies(version, packageJSON.engines["code-server"])) {
throw new Error(
`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`,
)
}
const pluginModule = require(dir)
if (!pluginModule.plugin) {
throw new Error("plugin module does not export a plugin")
}
const p = {
name: packageJSON.name,
version: packageJSON.version,
modulePath: dir,
...pluginModule.plugin,
} as Plugin
if (!p.displayName) {
throw new Error("plugin missing displayName")
}
if (!p.description) {
throw new Error("plugin missing description")
}
if (!p.routerPath) {
throw new Error("plugin missing router path")
}
if (!p.routerPath.startsWith("/") || p.routerPath.length < 2) {
throw new Error(`plugin router path ${q(p.routerPath)}: invalid`)
}
if (!p.homepageURL) {
throw new Error("plugin missing homepage")
}
p.init({
logger: logger,
})
logger.debug("loaded")
return p
}
}
/**
* Load all plugins from the `plugins` directory, directories specified by
* `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by
* `CS_PLUGIN` (also colon-separated).
*/
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins`
const plugin = process.env.CS_PLUGIN || ""
await Promise.all([
// Built-in plugins.
_loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args),
// User-added plugins.
...pluginPath
.split(":")
.filter((p) => !!p)
.map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)),
// Individual plugins so you don't have to symlink or move them into a
// directory specifically for plugins. This lets you load plugins that are
// on the same level as other directories that are not plugins (if you tried
// to use CS_PLUGIN_PATH code-server would try to load those other
// directories as plugins). Intended for development.
...plugin
.split(":")
.filter((p) => !!p)
.map((dir) => loadPlugin(path.resolve(dir), httpServer, args)),
])
interface PackageJSON {
name: string
version: string
engines: {
"code-server": string
}
}
function q(s: string | undefined): string {
if (s === undefined) {
s = "undefined"
}
return JSON.stringify(s)
}

16
src/node/proxy.ts Normal file
View File

@@ -0,0 +1,16 @@
import proxyServer from "http-proxy"
import { HttpCode } from "../common/http"
export const proxy = proxyServer.createProxyServer({})
proxy.on("error", (error, _, res) => {
res.writeHead(HttpCode.ServerError)
res.end(error.message)
})
// Intercept the response to rewrite absolute redirects against the base path.
proxy.on("proxyRes", (res, req) => {
if (res.headers.location && res.headers.location.startsWith("/") && (req as any).base) {
res.headers.location = (req as any).base + res.headers.location
}
})

17
src/node/routes/apps.ts Normal file
View File

@@ -0,0 +1,17 @@
import * as express from "express"
import { PluginAPI } from "../plugin"
/**
* Implements the /api/applications endpoint
*
* See typings/pluginapi.d.ts for details.
*/
export function router(papi: PluginAPI): express.Router {
const router = express.Router()
router.get("/", async (req, res) => {
res.json(await papi.applications())
})
return router
}

View File

@@ -0,0 +1,89 @@
import { Request, Router } from "express"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { authenticated, ensureAuthenticated, redirect } from "../http"
import { proxy } from "../proxy"
import { Router as WsRouter } from "../wsRouter"
export const router = Router()
/**
* Return the port if the request should be proxied. Anything that ends in a
* proxy domain and has a *single* subdomain should be proxied. Anything else
* should return `undefined` and will be handled as normal.
*
* For example if `coder.com` is specified `8080.coder.com` will be proxied
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
*/
const maybeProxy = (req: Request): string | undefined => {
// Split into parts.
const host = req.headers.host || ""
const idx = host.indexOf(":")
const domain = idx !== -1 ? host.substring(0, idx) : host
const parts = domain.split(".")
// There must be an exact match.
const port = parts.shift()
const proxyDomain = parts.join(".")
if (!port || !req.args["proxy-domain"].includes(proxyDomain)) {
return undefined
}
return port
}
router.all("*", (req, res, next) => {
const port = maybeProxy(req)
if (!port) {
return next()
}
// Must be authenticated to use the proxy.
if (!authenticated(req)) {
// Let the assets through since they're used on the login page.
if (req.path.startsWith("/static/") && req.method === "GET") {
return next()
}
// Assume anything that explicitly accepts text/html is a user browsing a
// page (as opposed to an xhr request). Don't use `req.accepts()` since
// *every* request that I've seen (in Firefox and Chromium at least)
// includes `*/*` making it always truthy. Even for css/javascript.
if (req.headers.accept && req.headers.accept.includes("text/html")) {
// Let the login through.
if (/\/login\/?/.test(req.path)) {
return next()
}
// Redirect all other pages to the login.
const to = normalize(`${req.baseUrl}${req.path}`)
return redirect(req, res, "login", {
to: to !== "/" ? to : undefined,
})
}
// Everything else gets an unauthorized message.
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
proxy.web(req, res, {
ignorePath: true,
target: `http://0.0.0.0:${port}${req.originalUrl}`,
})
})
export const wsRouter = WsRouter()
wsRouter.ws("*", (req, _, next) => {
const port = maybeProxy(req)
if (!port) {
return next()
}
// Must be authenticated to use the proxy.
ensureAuthenticated(req)
proxy.ws(req, req.ws, req.head, {
ignorePath: true,
target: `http://0.0.0.0:${port}${req.originalUrl}`,
})
})

10
src/node/routes/health.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Router } from "express"
export const router = Router()
router.get("/", (req, res) => {
res.json({
status: req.heart.alive() ? "alive" : "expired",
lastHeartbeat: req.heart.lastHeartbeat,
})
})

170
src/node/routes/index.ts Normal file
View File

@@ -0,0 +1,170 @@
import { logger } from "@coder/logger"
import bodyParser from "body-parser"
import cookieParser from "cookie-parser"
import * as express from "express"
import { promises as fs } from "fs"
import http from "http"
import * as path from "path"
import * as tls from "tls"
import { HttpCode, HttpError } from "../../common/http"
import { plural } from "../../common/util"
import { AuthType, DefaultedArgs } from "../cli"
import { rootPath } from "../constants"
import { Heart } from "../heart"
import { replaceTemplates } from "../http"
import { PluginAPI } from "../plugin"
import { getMediaMime, paths } from "../util"
import { WebsocketRequest } from "../wsRouter"
import * as apps from "./apps"
import * as domainProxy from "./domainProxy"
import * as health from "./health"
import * as login from "./login"
import * as proxy from "./pathProxy"
// static is a reserved keyword.
import * as _static from "./static"
import * as update from "./update"
import * as vscode from "./vscode"
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
export interface Request {
args: DefaultedArgs
heart: Heart
}
}
}
/**
* Register all routes and middleware.
*/
export const register = async (
app: express.Express,
wsApp: express.Express,
server: http.Server,
args: DefaultedArgs,
): Promise<void> => {
const heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
return new Promise((resolve, reject) => {
server.getConnections((error, count) => {
if (error) {
return reject(error)
}
logger.trace(plural(count, `${count} active connection`))
resolve(count > 0)
})
})
})
app.disable("x-powered-by")
wsApp.disable("x-powered-by")
app.use(cookieParser())
wsApp.use(cookieParser())
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
const common: express.RequestHandler = (req, _, next) => {
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
// it look like code-server is always in use.
if (!/^\/healthz\/?$/.test(req.url)) {
heart.beat()
}
// Add common variables routes can use.
req.args = args
req.heart = heart
next()
}
app.use(common)
wsApp.use(common)
app.use(async (req, res, next) => {
// If we're handling TLS ensure all requests are redirected to HTTPS.
// TODO: This does *NOT* work if you have a base path since to specify the
// protocol we need to specify the whole path.
if (args.cert && !(req.connection as tls.TLSSocket).encrypted) {
return res.redirect(`https://${req.headers.host}${req.originalUrl}`)
}
// Return robots.txt.
if (req.originalUrl === "/robots.txt") {
const resourcePath = path.resolve(rootPath, "src/browser/robots.txt")
res.set("Content-Type", getMediaMime(resourcePath))
return res.send(await fs.readFile(resourcePath))
}
next()
})
app.use("/", domainProxy.router)
wsApp.use("/", domainProxy.wsRouter.router)
app.use("/", vscode.router)
wsApp.use("/", vscode.wsRouter.router)
app.use("/vscode", vscode.router)
wsApp.use("/vscode", vscode.wsRouter.router)
app.use("/healthz", health.router)
if (args.auth === AuthType.Password) {
app.use("/login", login.router)
}
app.use("/proxy", proxy.router)
wsApp.use("/proxy", proxy.wsRouter.router)
app.use("/static", _static.router)
app.use("/update", update.router)
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
await papi.loadPlugins()
papi.mount(app)
app.use("/api/applications", apps.router(papi))
app.use(() => {
throw new HttpError("Not Found", HttpCode.NotFound)
})
const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
if (err.code === "ENOENT" || err.code === "EISDIR") {
err.status = HttpCode.NotFound
}
const status = err.status ?? err.statusCode ?? 500
res.status(status)
// Assume anything that explicitly accepts text/html is a user browsing a
// page (as opposed to an xhr request). Don't use `req.accepts()` since
// *every* request that I've seen (in Firefox and Chromium at least)
// includes `*/*` making it always truthy. Even for css/javascript.
if (req.headers.accept && req.headers.accept.includes("text/html")) {
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
res.set("Content-Type", getMediaMime(resourcePath))
const content = await fs.readFile(resourcePath, "utf8")
res.send(
replaceTemplates(req, content)
.replace(/{{ERROR_TITLE}}/g, status)
.replace(/{{ERROR_HEADER}}/g, status)
.replace(/{{ERROR_BODY}}/g, err.message),
)
} else {
res.json({
error: err.message,
...(err.details || {}),
})
}
}
app.use(errorHandler)
const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => {
logger.error(`${err.message} ${err.stack}`)
;(req as WebsocketRequest).ws.end()
}
wsApp.use(wsErrorHandler)
}

95
src/node/routes/login.ts Normal file
View File

@@ -0,0 +1,95 @@
import { Router, Request } from "express"
import { promises as fs } from "fs"
import { RateLimiter as Limiter } from "limiter"
import * as path from "path"
import safeCompare from "safe-compare"
import { rootPath } from "../constants"
import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http"
import { hash, humanPath } from "../util"
enum Cookie {
Key = "key",
}
// RateLimiter wraps around the limiter library for logins.
// It allows 2 logins every minute and 12 logins every hour.
class RateLimiter {
private readonly minuteLimiter = new Limiter(2, "minute")
private readonly hourLimiter = new Limiter(12, "hour")
public try(): boolean {
if (this.minuteLimiter.tryRemoveTokens(1)) {
return true
}
return this.hourLimiter.tryRemoveTokens(1)
}
}
const getRoot = async (req: Request, error?: Error): Promise<string> => {
const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8")
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
if (req.args.usingEnvPassword) {
passwordMsg = "Password was set from $PASSWORD."
}
return replaceTemplates(
req,
content
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : ""),
)
}
const limiter = new RateLimiter()
export const router = Router()
router.use((req, res, next) => {
const to = (typeof req.query.to === "string" && req.query.to) || "/"
if (authenticated(req)) {
return redirect(req, res, to, { to: undefined })
}
next()
})
router.get("/", async (req, res) => {
res.send(await getRoot(req))
})
router.post("/", async (req, res) => {
try {
if (!limiter.try()) {
throw new Error("Login rate limited!")
}
if (!req.body.password) {
throw new Error("Missing password")
}
if (req.args.password && safeCompare(req.body.password, req.args.password)) {
// The hash does not add any actual security but we do it for
// obfuscation purposes (and as a side effect it handles escaping).
res.cookie(Cookie.Key, hash(req.body.password), {
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
path: req.body.base || "/",
sameSite: "lax",
})
const to = (typeof req.query.to === "string" && req.query.to) || "/"
return redirect(req, res, to, { to: undefined })
}
console.error(
"Failed login attempt",
JSON.stringify({
xForwardedFor: req.headers["x-forwarded-for"],
remoteAddress: req.connection.remoteAddress,
userAgent: req.headers["user-agent"],
timestamp: Math.floor(new Date().getTime() / 1000),
}),
)
throw new Error("Incorrect password")
} catch (error) {
res.send(await getRoot(req, error))
}
})

View File

@@ -0,0 +1,47 @@
import { Request, Router } from "express"
import qs from "qs"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { authenticated, ensureAuthenticated, redirect } from "../http"
import { proxy } from "../proxy"
import { Router as WsRouter } from "../wsRouter"
export const router = Router()
const getProxyTarget = (req: Request, rewrite: boolean): string => {
if (rewrite) {
const query = qs.stringify(req.query)
return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}`
}
return `http://0.0.0.0:${req.params.port}/${req.originalUrl}`
}
router.all("/(:port)(/*)?", (req, res) => {
if (!authenticated(req)) {
// If visiting the root (/:port only) redirect to the login page.
if (!req.params[0] || req.params[0] === "/") {
const to = normalize(`${req.baseUrl}${req.path}`)
return redirect(req, res, "login", {
to: to !== "/" ? to : undefined,
})
}
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
// Absolute redirects need to be based on the subpath when rewriting.
;(req as any).base = `${req.baseUrl}/${req.params.port}`
proxy.web(req, res, {
ignorePath: true,
target: getProxyTarget(req, true),
})
})
export const wsRouter = WsRouter()
wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => {
proxy.ws(req, req.ws, req.head, {
ignorePath: true,
target: getProxyTarget(req, true),
})
})

69
src/node/routes/static.ts Normal file
View File

@@ -0,0 +1,69 @@
import { field, logger } from "@coder/logger"
import { Router } from "express"
import { promises as fs } from "fs"
import * as path from "path"
import { Readable } from "stream"
import * as tarFs from "tar-fs"
import * as zlib from "zlib"
import { HttpCode, HttpError } from "../../common/http"
import { rootPath } from "../constants"
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
import { getMediaMime, pathToFsPath } from "../util"
export const router = Router()
// The commit is for caching.
router.get("/(:commit)(/*)?", async (req, res) => {
// Used by VS Code to load extensions into the web worker.
const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar
if (typeof tar === "string") {
ensureAuthenticated(req)
let stream: Readable = tarFs.pack(pathToFsPath(tar))
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
logger.debug("gzipping tar", field("path", tar))
const compress = zlib.createGzip()
stream.pipe(compress)
stream.on("error", (error) => compress.destroy(error))
stream.on("close", () => compress.end())
stream = compress
res.header("content-encoding", "gzip")
}
res.set("Content-Type", "application/x-tar")
stream.on("close", () => res.end())
return stream.pipe(res)
}
// If not a tar use the remainder of the path to load the resource.
if (!req.params[0]) {
throw new HttpError("Not Found", HttpCode.NotFound)
}
const resourcePath = path.resolve(req.params[0])
// Make sure it's in code-server if you aren't authenticated. This lets
// unauthenticated users load the login assets.
if (!resourcePath.startsWith(rootPath) && !authenticated(req)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
// Don't cache during development. - can also be used if you want to make a
// static request without caching.
if (req.params.commit !== "development" && req.params.commit !== "-") {
res.header("Cache-Control", "public, max-age=31536000")
}
// Without this the default is to use the directory the script loaded from.
if (req.headers["service-worker"]) {
res.header("service-worker-allowed", "/")
}
res.set("Content-Type", getMediaMime(resourcePath))
if (resourcePath.endsWith("manifest.json")) {
const content = await fs.readFile(resourcePath, "utf8")
return res.send(replaceTemplates(req, content))
}
const content = await fs.readFile(resourcePath)
return res.send(content)
})

18
src/node/routes/update.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Router } from "express"
import { version } from "../constants"
import { ensureAuthenticated } from "../http"
import { UpdateProvider } from "../update"
export const router = Router()
const provider = new UpdateProvider()
router.get("/check", ensureAuthenticated, async (req, res) => {
const update = await provider.getUpdate(req.query.force === "true")
res.json({
checked: update.checked,
latest: update.version,
current: version,
isLatest: provider.isLatestVersion(update),
})
})

105
src/node/routes/vscode.ts Normal file
View File

@@ -0,0 +1,105 @@
import * as crypto from "crypto"
import { Router } from "express"
import { promises as fs } from "fs"
import * as path from "path"
import { commit, rootPath, version } from "../constants"
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
import { getMediaMime, pathToFsPath } from "../util"
import { VscodeProvider } from "../vscode"
import { Router as WsRouter } from "../wsRouter"
export const router = Router()
const vscode = new VscodeProvider()
router.get("/", async (req, res) => {
if (!authenticated(req)) {
return redirect(req, res, "login", {
// req.baseUrl can be blank if already at the root.
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
})
}
const [content, options] = await Promise.all([
await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"),
(async () => {
try {
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
} catch (error) {
const devMessage = commit === "development" ? "It might not have finished compiling." : ""
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
}
})(),
])
options.productConfiguration.codeServerVersion = version
res.send(
replaceTemplates(
req,
// Uncomment prod blocks if not in development. TODO: Would this be
// better as a build step? Or maintain two HTML files again?
commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
{
disableTelemetry: !!req.args["disable-telemetry"],
},
)
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`),
)
})
/**
* TODO: Might currently be unused.
*/
router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => {
if (typeof req.query.path === "string") {
res.set("Content-Type", getMediaMime(req.query.path))
res.send(await fs.readFile(pathToFsPath(req.query.path)))
}
})
/**
* Used by VS Code to load files.
*/
router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => {
if (typeof req.query.path === "string") {
res.set("Content-Type", getMediaMime(req.query.path))
res.send(await fs.readFile(pathToFsPath(req.query.path)))
}
})
/**
* VS Code webviews use these paths to load files and to load webview assets
* like HTML and JavaScript.
*/
router.get("/webview/*", ensureAuthenticated, async (req, res) => {
res.set("Content-Type", getMediaMime(req.path))
if (/^vscode-resource/.test(req.params[0])) {
return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, "")))
}
return res.send(
await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])),
)
})
export const wsRouter = WsRouter()
wsRouter.ws("/", ensureAuthenticated, async (req) => {
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const reply = crypto
.createHash("sha1")
.update(req.headers["sec-websocket-key"] + magic)
.digest("base64")
req.ws.write(
[
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${reply}`,
].join("\r\n") + "\r\n\r\n",
)
await vscode.sendWebsocket(req.ws, req.query)
})

View File

@@ -1,7 +1,7 @@
import { logger } from "@coder/logger"
import { Query } from "express-serve-static-core"
import * as fs from "fs-extra"
import * as path from "path"
import { Route } from "./http"
import { paths } from "./util"
export type Settings = { [key: string]: Settings | string | boolean | number }
@@ -58,7 +58,7 @@ export interface CoderSettings extends UpdateSettings {
url: string
workspace: boolean
}
query: Route["query"]
query: Query
}
/**

View File

@@ -1,12 +1,10 @@
import { field, logger } from "@coder/logger"
import * as http from "http"
import * as https from "https"
import * as path from "path"
import * as semver from "semver"
import * as url from "url"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
import { version } from "./constants"
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
export interface Update {
checked: number
@@ -18,15 +16,13 @@ export interface LatestResponse {
}
/**
* HTTP provider for checking updates (does not download/install them).
* Provide update information.
*/
export class UpdateHttpProvider extends HttpProvider {
export class UpdateProvider {
private update?: Promise<Update>
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
public constructor(
options: HttpProviderOptions,
public readonly enabled: boolean,
/**
* The URL for getting the latest version of code-server. Should return JSON
* that fulfills `LatestResponse`.
@@ -37,37 +33,7 @@ export class UpdateHttpProvider extends HttpProvider {
* settings will be used.
*/
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureAuthenticated(request)
this.ensureMethod(request)
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
if (!this.enabled) {
throw new Error("update checks are disabled")
}
switch (route.base) {
case "/check":
case "/": {
const update = await this.getUpdate(route.base === "/check")
return {
content: {
...update,
isLatest: this.isLatestVersion(update),
},
}
}
}
throw new HttpError("Not found", HttpCode.NotFound)
}
) {}
/**
* Query for and return the latest update.
@@ -89,7 +55,7 @@ export class UpdateHttpProvider extends HttpProvider {
if (!update || update.checked + this.updateInterval < now) {
const buffer = await this.request(this.latestUrl)
const data = JSON.parse(buffer.toString()) as LatestResponse
update = { checked: now, version: data.name }
update = { checked: now, version: data.name.replace(/^v/, "") }
await this.settings.write({ update })
}
logger.debug("got latest version", field("latest", update.version))
@@ -103,18 +69,13 @@ export class UpdateHttpProvider extends HttpProvider {
}
}
public get currentVersion(): string {
return require(path.resolve(__dirname, "../../../package.json")).version
}
/**
* Return true if the currently installed version is the latest.
*/
public isLatestVersion(latest: Update): boolean {
const version = this.currentVersion
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
try {
return latest.version === version || semver.lt(latest.version, version)
return semver.lte(latest.version, version)
} catch (error) {
return true
}
@@ -144,24 +105,22 @@ export class UpdateHttpProvider extends HttpProvider {
logger.debug("Making request", field("uri", uri))
const httpx = uri.startsWith("https") ? https : http
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
if (
response.statusCode &&
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location
) {
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
}
if (response.statusCode >= 300) {
++redirects
response.destroy()
if (redirects > maxRedirects) {
return reject(new Error("reached max redirects"))
}
response.destroy()
if (!response.headers.location) {
return reject(new Error("received redirect with no location header"))
}
return request(url.resolve(uri, response.headers.location))
}
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
}
resolve(response)
})
client.on("error", reject)

View File

@@ -54,25 +54,45 @@ export function humanPath(p?: string): string {
return p.replace(os.homedir(), "~")
}
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
const paths = {
cert: path.join(tmpdir, "self-signed.cert"),
certKey: path.join(tmpdir, "self-signed.key"),
}
const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)])
export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => {
const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`)
const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`)
const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)])
if (!checks[0] || !checks[1]) {
// Require on demand so openssl isn't required if you aren't going to
// generate certificates.
const pem = require("pem") as typeof import("pem")
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
pem.createCertificate({ selfSigned: true }, (error, result) => {
return error ? reject(error) : resolve(result)
})
pem.createCertificate(
{
selfSigned: true,
commonName: hostname,
config: `
[req]
req_extensions = v3_req
[ v3_req ]
basicConstraints = CA:true
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${hostname}
`,
},
(error, result) => {
return error ? reject(error) : resolve(result)
},
)
})
await fs.mkdirp(tmpdir)
await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)])
await fs.mkdirp(paths.data)
await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)])
}
return {
cert: certPath,
certKey: certKeyPath,
}
return paths
}
export const generatePassword = async (length = 24): Promise<string> => {
@@ -261,3 +281,12 @@ export function canConnect(path: string): Promise<boolean> {
})
})
}
export const isFile = async (path: string): Promise<boolean> => {
try {
const stat = await fs.stat(path)
return stat.isFile()
} catch (error) {
return false
}
}

164
src/node/vscode.ts Normal file
View File

@@ -0,0 +1,164 @@
import { logger } from "@coder/logger"
import * as cp from "child_process"
import * as net from "net"
import * as path from "path"
import * as ipc from "../../lib/vscode/src/vs/server/ipc"
import { arrayify, generateUuid } from "../common/util"
import { rootPath } from "./constants"
import { settings } from "./settings"
import { SocketProxyProvider } from "./socket"
import { isFile } from "./util"
import { onMessage, wrapper } from "./wrapper"
export class VscodeProvider {
public readonly serverRootPath: string
public readonly vsRootPath: string
private _vscode?: Promise<cp.ChildProcess>
private readonly socketProvider = new SocketProxyProvider()
public constructor() {
this.vsRootPath = path.resolve(rootPath, "lib/vscode")
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
wrapper.onDispose(() => this.dispose())
}
public async dispose(): Promise<void> {
this.socketProvider.stop()
if (this._vscode) {
const vscode = await this._vscode
vscode.removeAllListeners()
vscode.kill()
this._vscode = undefined
}
}
public async initialize(
options: Omit<ipc.VscodeOptions, "startPath">,
query: ipc.Query,
): Promise<ipc.WorkbenchOptions> {
const { lastVisited } = await settings.read()
const startPath = await this.getFirstPath([
{ url: query.workspace, workspace: true },
{ url: query.folder, workspace: false },
options.args._ && options.args._.length > 0
? { url: path.resolve(options.args._[options.args._.length - 1]) }
: undefined,
lastVisited,
])
settings.write({
lastVisited: startPath,
query,
})
const id = generateUuid()
const vscode = await this.fork()
logger.debug("setting up vs code...")
this.send(
{
type: "init",
id,
options: {
...options,
startPath,
},
},
vscode,
)
const message = await onMessage<ipc.VscodeMessage, ipc.OptionsMessage>(
vscode,
(message): message is ipc.OptionsMessage => {
// There can be parallel initializations so wait for the right ID.
return message.type === "options" && message.id === id
},
)
return message.options
}
private fork(): Promise<cp.ChildProcess> {
if (this._vscode) {
return this._vscode
}
logger.debug("forking vs code...")
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
const dispose = () => {
vscode.removeAllListeners()
vscode.kill()
this._vscode = undefined
}
vscode.on("error", (error: Error) => {
logger.error(error.message)
if (error.stack) {
logger.debug(error.stack)
}
dispose()
})
vscode.on("exit", (code) => {
logger.error(`VS Code exited unexpectedly with code ${code}`)
dispose()
})
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
return message.type === "ready"
}).then(() => vscode)
return this._vscode
}
/**
* VS Code expects a raw socket. It will handle all the web socket frames.
*/
public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise<void> {
const vscode = await this._vscode
// TLS sockets cannot be transferred to child processes so we need an
// in-between. Non-TLS sockets will be returned as-is.
const socketProxy = await this.socketProvider.createProxy(socket)
this.send({ type: "socket", query }, vscode, socketProxy)
}
private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
if (!vscode || vscode.killed) {
throw new Error("vscode is not running")
}
vscode.send(message, socket)
}
/**
* Choose the first non-empty path from the provided array.
*
* Each array item consists of `url` and an optional `workspace` boolean that
* indicates whether that url is for a workspace.
*
* `url` can be a fully qualified URL or just the path portion.
*
* `url` can also be a query object to make it easier to pass in query
* variables directly but anything that isn't a string or string array is not
* valid and will be ignored.
*/
private async getFirstPath(
startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>,
): Promise<ipc.StartPath | undefined> {
for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i]
const url = arrayify(startPath && startPath.url).find((p) => !!p)
if (startPath && url && typeof url === "string") {
return {
url,
// The only time `workspace` is undefined is for the command-line
// argument, in which case it's a path (not a URL) so we can stat it
// without having to parse it.
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
}
}
}
return undefined
}
}

View File

@@ -1,11 +1,70 @@
import { field, logger } from "@coder/logger"
import { field, Logger, logger } from "@coder/logger"
import * as cp from "child_process"
import * as path from "path"
import * as rfs from "rotating-file-stream"
import { Emitter } from "../common/emitter"
import { DefaultedArgs } from "./cli"
import { paths } from "./util"
interface HandshakeMessage {
const timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
/**
* Listen to a single message from a process. Reject if the process errors,
* exits, or times out.
*
* `fn` is a function that determines whether the message is the one we're
* waiting for.
*/
export function onMessage<M, T extends M>(
proc: cp.ChildProcess | NodeJS.Process,
fn: (message: M) => message is T,
customLogger?: Logger,
): Promise<T> {
return new Promise((resolve, reject) => {
const cleanup = () => {
proc.off("error", onError)
proc.off("exit", onExit)
proc.off("message", onMessage)
clearTimeout(timeout)
}
const timeout = setTimeout(() => {
cleanup()
reject(new Error("timed out"))
}, timeoutInterval)
const onError = (error: Error) => {
cleanup()
reject(error)
}
const onExit = (code: number) => {
cleanup()
reject(new Error(`exited unexpectedly with code ${code}`))
}
const onMessage = (message: M) => {
;(customLogger || logger).trace("got message", field("message", message))
if (fn(message)) {
cleanup()
resolve(message)
}
}
proc.on("message", onMessage)
// NodeJS.Process doesn't have `error` but binding anyway shouldn't break
// anything. It does have `exit` but the types aren't working.
;(proc as cp.ChildProcess).on("error", onError)
;(proc as cp.ChildProcess).on("exit", onExit)
})
}
interface ParentHandshakeMessage {
type: "handshake"
args: DefaultedArgs
}
interface ChildHandshakeMessage {
type: "handshake"
}
@@ -14,9 +73,10 @@ interface RelaunchMessage {
version: string
}
export type Message = RelaunchMessage | HandshakeMessage
type ChildMessage = RelaunchMessage | ChildHandshakeMessage
type ParentMessage = ParentHandshakeMessage
export class ProcessError extends Error {
class ProcessError extends Error {
public constructor(message: string, public readonly code: number | undefined) {
super(message)
this.name = this.constructor.name
@@ -25,59 +85,55 @@ export class ProcessError extends Error {
}
/**
* Allows the wrapper and inner processes to communicate.
* Wrapper around a process that tries to gracefully exit when a process exits
* and provides a way to prevent `process.exit`.
*/
export class IpcMain {
private readonly _onMessage = new Emitter<Message>()
public readonly onMessage = this._onMessage.event
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
public readonly onDispose = this._onDispose.event
public readonly processExit: (code?: number) => never = process.exit
abstract class Process {
/**
* Emit this to trigger a graceful exit.
*/
protected readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
public constructor(private readonly parentPid?: number) {
/**
* Emitted when the process is about to be disposed.
*/
public readonly onDispose = this._onDispose.event
/**
* Uniquely named logger for the process.
*/
public abstract logger: Logger
public constructor() {
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
process.on("exit", () => this._onDispose.emit(undefined))
this.onDispose((signal) => {
this.onDispose((signal, wait) => {
// Remove listeners to avoid possibly triggering disposal again.
process.removeAllListeners()
// Let any other handlers run first then exit.
logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal))
setTimeout(() => this.exit(0), 0)
// Try waiting for other handlers to run first then exit.
this.logger.debug("disposing", field("code", signal))
wait.then(() => this.exit(0))
setTimeout(() => this.exit(0), 5000)
})
// Kill the inner process if the parent dies. This is for the case where the
// parent process is forcefully terminated and cannot clean up.
if (parentPid) {
setInterval(() => {
try {
// process.kill throws an exception if the process doesn't exist.
process.kill(parentPid, 0)
} catch (_) {
// Consider this an error since it should have been able to clean up
// the child process unless it was forcefully killed.
logger.error(`parent process ${parentPid} died`)
this._onDispose.emit(undefined)
}
}, 5000)
}
}
/**
* Ensure we control when the process exits.
* Ensure control over when the process exits.
*/
public preventExit(): void {
process.exit = function (code?: number) {
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
} as (code?: number) => never
;(process.exit as any) = (code?: number) => {
this.logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
}
}
public get isChild(): boolean {
return typeof this.parentPid !== "undefined"
}
private readonly processExit: (code?: number) => never = process.exit
/**
* Will always exit even if normal exit is being prevented.
*/
public exit(error?: number | ProcessError): never {
if (error && typeof error !== "number") {
this.processExit(typeof error.code === "number" ? error.code : 1)
@@ -85,48 +141,59 @@ export class IpcMain {
this.processExit(error)
}
}
}
public handshake(child?: cp.ChildProcess): Promise<void> {
return new Promise((resolve, reject) => {
const target = child || process
const onMessage = (message: Message): void => {
logger.debug(
`${child ? "wrapper" : "inner process"} ${process.pid} received message from ${
child ? child.pid : this.parentPid
}`,
field("message", message),
)
if (message.type === "handshake") {
target.removeListener("message", onMessage)
target.on("message", (msg) => this._onMessage.emit(msg))
// The wrapper responds once the inner process starts the handshake.
if (child) {
if (!target.send) {
throw new Error("child not spawned with IPC")
}
target.send({ type: "handshake" })
}
resolve()
}
/**
* Child process that will clean up after itself if the parent goes away and can
* perform a handshake with the parent and ask it to relaunch.
*/
class ChildProcess extends Process {
public logger = logger.named(`child:${process.pid}`)
public constructor(private readonly parentPid: number) {
super()
// Kill the inner process if the parent dies. This is for the case where the
// parent process is forcefully terminated and cannot clean up.
setInterval(() => {
try {
// process.kill throws an exception if the process doesn't exist.
process.kill(this.parentPid, 0)
} catch (_) {
// Consider this an error since it should have been able to clean up
// the child process unless it was forcefully killed.
this.logger.error(`parent process ${parentPid} died`)
this._onDispose.emit(undefined)
}
target.on("message", onMessage)
if (child) {
child.once("error", reject)
child.once("exit", (code) => {
reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined))
})
} else {
// The inner process initiates the handshake.
this.send({ type: "handshake" })
}
})
}, 5000)
}
/**
* Initiate the handshake and wait for a response from the parent.
*/
public async handshake(): Promise<DefaultedArgs> {
this.send({ type: "handshake" })
const message = await onMessage<ParentMessage, ParentHandshakeMessage>(
process,
(message): message is ParentHandshakeMessage => {
return message.type === "handshake"
},
this.logger,
)
return message.args
}
/**
* Notify the parent process that it should relaunch the child.
*/
public relaunch(version: string): void {
this.send({ type: "relaunch", version })
}
private send(message: Message): void {
/**
* Send a message to the parent.
*/
private send(message: ChildMessage): void {
if (!process.send) {
throw new Error("not spawned with IPC")
}
@@ -135,28 +202,31 @@ export class IpcMain {
}
/**
* Channel for communication between the child and parent processes.
* Parent process wrapper that spawns the child process and performs a handshake
* with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by
* the child. If the child otherwise exits the parent will also exit.
*/
export const ipcMain = new IpcMain(
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined,
)
export class ParentProcess extends Process {
public logger = logger.named(`parent:${process.pid}`)
export interface WrapperOptions {
maxMemory?: number
nodeOptions?: string
}
/**
* Provides a way to wrap a process for the purpose of updating the running
* instance.
*/
export class WrapperProcess {
private process?: cp.ChildProcess
private child?: cp.ChildProcess
private started?: Promise<void>
private readonly logStdoutStream: rfs.RotatingFileStream
private readonly logStderrStream: rfs.RotatingFileStream
public constructor(private currentVersion: string, private readonly options?: WrapperOptions) {
protected readonly _onChildMessage = new Emitter<ChildMessage>()
protected readonly onChildMessage = this._onChildMessage.event
private args?: DefaultedArgs
public constructor(private currentVersion: string) {
super()
process.on("SIGUSR1", async () => {
this.logger.info("Received SIGUSR1; hotswapping")
this.relaunch()
})
const opts = {
size: "10M",
maxFiles: 10,
@@ -164,19 +234,19 @@ export class WrapperProcess {
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
ipcMain.onDispose(() => {
this.onDispose(() => {
this.disposeChild()
})
ipcMain.onMessage((message) => {
this.onChildMessage((message) => {
switch (message.type) {
case "relaunch":
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
this.logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
this.currentVersion = message.version
this.relaunch()
break
default:
logger.error(`Unrecognized message ${message}`)
this.logger.error(`Unrecognized message ${message}`)
break
}
})
@@ -184,30 +254,26 @@ export class WrapperProcess {
private disposeChild(): void {
this.started = undefined
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
if (this.child) {
this.child.removeAllListeners()
this.child.kill()
}
}
private async relaunch(): Promise<void> {
this.disposeChild()
try {
await this.start()
this.started = this._start()
await this.started
} catch (error) {
logger.error(error.message)
ipcMain.exit(typeof error.code === "number" ? error.code : 1)
this.logger.error(error.message)
this.exit(typeof error.code === "number" ? error.code : 1)
}
}
public start(): Promise<void> {
// If we have a process then we've already bound this.
if (!this.process) {
process.on("SIGUSR1", async () => {
logger.info("Received SIGUSR1; hotswapping")
this.relaunch()
})
}
public start(args: DefaultedArgs): Promise<void> {
// Store for relaunches.
this.args = args
if (!this.started) {
this.started = this._start()
}
@@ -216,7 +282,7 @@ export class WrapperProcess {
private async _start(): Promise<void> {
const child = this.spawn()
this.process = child
this.child = child
// Log both to stdout and to the log directory.
if (child.stdout) {
@@ -228,45 +294,75 @@ export class WrapperProcess {
child.stderr.pipe(process.stderr)
}
logger.debug(`spawned inner process ${child.pid}`)
this.logger.debug(`spawned inner process ${child.pid}`)
await ipcMain.handshake(child)
await this.handshake(child)
child.once("exit", (code) => {
logger.debug(`inner process ${child.pid} exited unexpectedly`)
ipcMain.exit(code || 0)
this.logger.debug(`inner process ${child.pid} exited unexpectedly`)
this.exit(code || 0)
})
}
private spawn(): cp.ChildProcess {
// Flags to pass along to the Node binary.
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
nodeOptions += ` --max_old_space_size=${(this.options && this.options.maxMemory) || 2048}`
}
// Use spawn (instead of fork) to use the new binary in case it was updated.
return cp.spawn(process.argv[0], process.argv.slice(1), {
env: {
...process.env,
CODE_SERVER_PARENT_PID: process.pid.toString(),
NODE_OPTIONS: nodeOptions,
NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`,
},
stdio: ["ipc"],
})
}
/**
* Wait for a handshake from the child then reply.
*/
private async handshake(child: cp.ChildProcess): Promise<void> {
if (!this.args) {
throw new Error("started without args")
}
await onMessage<ChildMessage, ChildHandshakeMessage>(
child,
(message): message is ChildHandshakeMessage => {
return message.type === "handshake"
},
this.logger,
)
this.send(child, { type: "handshake", args: this.args })
}
/**
* Send a message to the child.
*/
private send(child: cp.ChildProcess, message: ParentMessage): void {
child.send(message)
}
}
/**
* Process wrapper.
*/
export const wrapper =
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
? new ChildProcess(parseInt(process.env.CODE_SERVER_PARENT_PID))
: new ParentProcess(require("../../package.json").version)
export function isChild(proc: ChildProcess | ParentProcess): proc is ChildProcess {
return proc instanceof ChildProcess
}
// It's possible that the pipe has closed (for example if you run code-server
// --version | head -1). Assume that means we're done.
if (!process.stdout.isTTY) {
process.stdout.on("error", () => ipcMain.exit())
process.stdout.on("error", () => wrapper.exit())
}
// Don't let uncaught exceptions crash the process.
process.on("uncaughtException", (error) => {
logger.error(`Uncaught exception: ${error.message}`)
wrapper.logger.error(`Uncaught exception: ${error.message}`)
if (typeof error.stack !== "undefined") {
logger.error(error.stack)
wrapper.logger.error(error.stack)
}
})

57
src/node/wsRouter.ts Normal file
View File

@@ -0,0 +1,57 @@
import * as express from "express"
import * as expressCore from "express-serve-static-core"
import * as http from "http"
import * as net from "net"
export const handleUpgrade = (app: express.Express, server: http.Server): void => {
server.on("upgrade", (req, socket, head) => {
socket.pause()
req.ws = socket
req.head = head
req._ws_handled = false
// Send the request off to be handled by Express.
;(app as any).handle(req, new http.ServerResponse(req), () => {
if (!req._ws_handled) {
socket.end("HTTP/1.1 404 Not Found\r\n\r\n")
}
})
})
}
export interface WebsocketRequest extends express.Request {
ws: net.Socket
head: Buffer
}
interface InternalWebsocketRequest extends WebsocketRequest {
_ws_handled: boolean
}
export type WebSocketHandler = (
req: WebsocketRequest,
res: express.Response,
next: express.NextFunction,
) => void | Promise<void>
export class WebsocketRouter {
public readonly router = express.Router()
public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
this.router.get(
route,
...handlers.map((handler) => {
const wrapped: express.Handler = (req, res, next) => {
;(req as InternalWebsocketRequest)._ws_handled = true
return handler(req as WebsocketRequest, res, next)
}
return wrapped
}),
)
}
}
export function Router(): WebsocketRouter {
return new WebsocketRouter()
}

View File

@@ -14,17 +14,23 @@ type Mutable<T> = {
describe("parser", () => {
beforeEach(() => {
delete process.env.LOG_LEVEL
delete process.env.PASSWORD
})
// The parser should not set any defaults so the caller can determine what
// values the user actually set. These are only set after explicitly calling
// `setDefaults`.
const defaults = {
auth: "password",
host: "localhost",
port: 8080,
"proxy-domain": [],
usingEnvPassword: false,
"extensions-dir": path.join(paths.data, "extensions"),
"user-data-dir": paths.data,
}
it("should set defaults", () => {
it("should parse nothing", () => {
assert.deepEqual(parse([]), { _: [] })
})
@@ -232,6 +238,71 @@ describe("parser", () => {
"proxy-domain": ["*.coder.com", "test.com"],
})
})
it("should enforce cert-key with cert value or otherwise generate one", async () => {
const args = parse(["--cert"])
assert.deepEqual(args, {
_: [],
cert: {
value: undefined,
},
})
assert.throws(() => parse(["--cert", "test"]), /--cert-key is missing/)
assert.deepEqual(await setDefaults(args), {
_: [],
...defaults,
cert: {
value: path.join(paths.data, "localhost.crt"),
},
"cert-key": path.join(paths.data, "localhost.key"),
})
})
it("should override with --link", async () => {
const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" "))
assert.deepEqual(await setDefaults(args), {
_: [],
...defaults,
auth: "none",
host: "localhost",
link: {
value: "test",
},
port: 0,
cert: undefined,
"cert-key": path.resolve("test"),
socket: undefined,
})
})
it("should use env var password", async () => {
process.env.PASSWORD = "test"
const args = parse([])
assert.deepEqual(args, {
_: [],
})
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
password: "test",
usingEnvPassword: true,
})
})
it("should filter proxy domains", async () => {
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
assert.deepEqual(args, {
_: [],
"proxy-domain": ["*.coder.com", "coder.com", "coder.org"],
})
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
"proxy-domain": ["coder.com", "coder.org"],
})
})
})
describe("cli", () => {

62
test/plugin.test.ts Normal file
View File

@@ -0,0 +1,62 @@
import { logger } from "@coder/logger"
import * as express from "express"
import * as fs from "fs"
import { describe } from "mocha"
import * as path from "path"
import * as supertest from "supertest"
import { PluginAPI } from "../src/node/plugin"
import * as apps from "../src/node/routes/apps"
const fsp = fs.promises
/**
* Use $LOG_LEVEL=debug to see debug logs.
*/
describe("plugin", () => {
let papi: PluginAPI
let app: express.Application
let agent: supertest.SuperAgentTest
before(async () => {
papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow")
await papi.loadPlugins()
app = express.default()
papi.mount(app)
app.use("/api/applications", apps.router(papi))
agent = supertest.agent(app)
})
it("/api/applications", async () => {
await agent.get("/api/applications").expect(200, [
{
name: "Test App",
version: "4.0.0",
description: "This app does XYZ.",
iconPath: "/test-plugin/test-app/icon.svg",
homepageURL: "https://example.com",
path: "/test-plugin/test-app",
plugin: {
name: "test-plugin",
version: "1.0.0",
modulePath: path.join(__dirname, "test-plugin"),
displayName: "Test Plugin",
description: "Plugin used in code-server tests.",
routerPath: "/test-plugin",
homepageURL: "https://example.com",
},
},
])
})
it("/test-plugin/test-app", async () => {
const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), {
encoding: "utf8",
})
await agent.get("/test-plugin/test-app").expect(200, indexHTML)
})
})

View File

@@ -45,7 +45,7 @@ describe("SocketProxyProvider", () => {
}
before(async () => {
const cert = await generateCertificate()
const cert = await generateCertificate("localhost")
const options = {
cert: fs.readFileSync(cert.cert),
key: fs.readFileSync(cert.certKey),

1
test/test-plugin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
out

View File

@@ -0,0 +1,6 @@
out/index.js: src/index.ts
# Typescript always emits, even on errors.
yarn build || rm out/index.js
node_modules: package.json yarn.lock
yarn

View File

@@ -0,0 +1,19 @@
{
"private": true,
"name": "test-plugin",
"version": "1.0.0",
"engines": {
"code-server": "^3.7.0"
},
"main": "out/index.js",
"devDependencies": {
"@types/express": "^4.17.8",
"typescript": "^4.0.5"
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"express": "^4.17.1"
}
}

View File

@@ -0,0 +1 @@
<svg width="121" height="131" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="9.612%" y1="66.482%" x2="89.899%" y2="33.523%" id="a"><stop stop-color="#FCEE39" offset="0%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="8.601%" y1="15.03%" x2="99.641%" y2="89.058%" id="b"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#F26F4E" offset="57%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="90.118%" y1="69.931%" x2="17.938%" y2="38.628%" id="c"><stop stop-color="#7C59A4" offset="0%"/><stop stop-color="#AF4C92" offset="38.52%"/><stop stop-color="#DC4183" offset="76.54%"/><stop stop-color="#ED3D7D" offset="95.7%"/></linearGradient><linearGradient x1="91.376%" y1="19.144%" x2="18.895%" y2="70.21%" id="d"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#EE4E72" offset="36.4%"/><stop stop-color="#ED3D7D" offset="100%"/></linearGradient></defs><g fill="none"><path d="M118.623 71.8c.9-.8 1.4-1.9 1.5-3.2.1-2.6-1.8-4.7-4.4-4.9-1.2-.1-2.4.4-3.3 1.1l-83.8 45.9c-1.9.8-3.6 2.2-4.7 4.1-2.9 4.8-1.3 11 3.6 13.9 3.4 2 7.5 1.8 10.7-.2.2-.2.5-.3.7-.5l78-54.8c.4-.3 1.5-1.1 1.7-1.4z" fill="url(#a)" transform="translate(-.023)"/><path d="M118.823 65.1l-63.8-62.6c-1.4-1.5-3.4-2.5-5.7-2.5-4.3 0-7.7 3.5-7.7 7.7 0 2.1.8 3.9 2.1 5.3.4.4.8.7 1.2 1l67.4 57.7c.8.7 1.8 1.2 3 1.3 2.6.1 4.7-1.8 4.9-4.4 0-1.3-.5-2.6-1.4-3.5z" fill="url(#b)" transform="translate(-.023)"/><path d="M57.123 59.5c-.1 0-39.4-31-40.2-31.5l-1.8-.9c-5.8-2.2-12.2.8-14.4 6.6-1.9 5.1.2 10.7 4.6 13.4.7.4 1.3.7 2 .9.4.2 45.4 18.8 45.4 18.8 1.8.8 3.9.3 5.1-1.2 1.5-1.9 1.2-4.6-.7-6.1z" fill="url(#c)" transform="translate(-.023)"/><path d="M49.323 0c-1.7 0-3.3.6-4.6 1.5l-39.8 26.8c-.1.1-.2.1-.2.2h-.1c-1.7 1.2-3.1 3-3.9 5.1-2.2 5.8.8 12.3 6.6 14.4 3.6 1.4 7.5.7 10.4-1.4.7-.5 1.3-1 1.8-1.6l34.6-31.2c1.8-1.4 3-3.6 3-6.1 0-4.2-3.5-7.7-7.8-7.7z" fill="url(#d)" transform="translate(-.023)"/><path fill="#000" d="M34.6 37.4h51v51h-51z"/><path fill="#FFF" d="M39 78.8h19.1V82H39zm-.2-28l1.5-1.4c.4.5.8.8 1.3.8.6 0 .9-.4.9-1.2v-5.3h2.3V49c0 1-.3 1.8-.8 2.3-.5.5-1.3.8-2.3.8-1.5.1-2.3-.5-2.9-1.3zm6.5-7H52v1.9h-4.4V47h4v1.8h-4v1.3h4.5v2h-6.7zm9.7 2h-2.5v-2h7.3v2h-2.5v6.3H55zM39 54h4.3c1 0 1.8.3 2.3.7.3.3.5.8.5 1.4 0 1-.5 1.5-1.3 1.9 1 .3 1.6.9 1.6 2 0 1.4-1.2 2.3-3.1 2.3H39V54zm4.8 2.6c0-.5-.4-.7-1-.7h-1.5v1.5h1.4c.7-.1 1.1-.3 1.1-.8zM43 59h-1.8v1.5H43c.7 0 1.1-.3 1.1-.8s-.4-.7-1.1-.7zm3.8-5h3.9c1.3 0 2.1.3 2.7.9.5.5.7 1.1.7 1.9 0 1.3-.7 2.1-1.7 2.6l2 2.9h-2.6l-1.7-2.5h-1v2.5h-2.3V54zm3.8 4c.8 0 1.2-.4 1.2-1 0-.7-.5-1-1.2-1h-1.5v2h1.5z"/><path d="M56.8 54H59l3.5 8.4H60l-.6-1.5h-3.2l-.6 1.5h-2.4l3.6-8.4zm2 5l-.9-2.3L57 59h1.8zm4-5h2.3v8.3h-2.3zm2.9 0h2.1l3.4 4.4V54h2.3v8.3h-2L68 57.8v4.6h-2.3zm8 7.1l1.3-1.5c.8.7 1.7 1 2.7 1 .6 0 1-.2 1-.6 0-.4-.3-.5-1.4-.8-1.8-.4-3.1-.9-3.1-2.6 0-1.5 1.2-2.7 3.2-2.7 1.4 0 2.5.4 3.4 1.1l-1.2 1.6c-.8-.5-1.6-.8-2.3-.8-.6 0-.8.2-.8.5 0 .4.3.5 1.4.8 1.9.4 3.1 1 3.1 2.6 0 1.7-1.3 2.7-3.4 2.7-1.5.1-2.9-.4-3.9-1.3z" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Test Plugin</title>
</head>
<body>
<p>Welcome to the test plugin!</p>
</body>
</html>

View File

@@ -0,0 +1,39 @@
import * as express from "express"
import * as fspath from "path"
import * as pluginapi from "../../../typings/pluginapi"
export const plugin: pluginapi.Plugin = {
displayName: "Test Plugin",
routerPath: "/test-plugin",
homepageURL: "https://example.com",
description: "Plugin used in code-server tests.",
init(config) {
config.logger.debug("test-plugin loaded!")
},
router() {
const r = express.Router()
r.get("/test-app", (req, res) => {
res.sendFile(fspath.resolve(__dirname, "../public/index.html"))
})
r.get("/goland/icon.svg", (req, res) => {
res.sendFile(fspath.resolve(__dirname, "../public/icon.svg"))
})
return r
},
applications() {
return [
{
name: "Test App",
version: "4.0.0",
iconPath: "/icon.svg",
path: "/test-app",
description: "This app does XYZ.",
homepageURL: "https://example.com",
},
]
},
}

View File

@@ -0,0 +1,69 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./out" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

435
test/test-plugin/yarn.lock Normal file
View File

@@ -0,0 +1,435 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/body-parser@*":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/connect@*":
version "3.4.33"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
dependencies:
"@types/node" "*"
"@types/express-serve-static-core@*":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084"
integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express@^4.17.8":
version "4.17.8"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a"
integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/mime@*":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
"@types/node@*":
version "14.14.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f"
integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==
"@types/qs@*":
version "6.9.5"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==
"@types/range-parser@*":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/serve-static@*":
version "1.13.6"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1"
integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA==
dependencies:
"@types/mime" "*"
"@types/node" "*"
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-types@~2.1.24:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
dependencies:
mime-db "1.44.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typescript@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@@ -2,13 +2,11 @@ import * as assert from "assert"
import * as fs from "fs-extra"
import * as http from "http"
import * as path from "path"
import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update"
import { AuthType } from "../src/node/http"
import { SettingsProvider, UpdateSettings } from "../src/node/settings"
import { LatestResponse, UpdateProvider } from "../src/node/update"
import { tmpdir } from "../src/node/util"
describe("update", () => {
return
describe.skip("update", () => {
let version = "1.0.0"
let spy: string[] = []
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
@@ -35,22 +33,14 @@ describe("update", () => {
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
const settings = new SettingsProvider<UpdateSettings>(jsonPath)
let _provider: UpdateHttpProvider | undefined
const provider = (): UpdateHttpProvider => {
let _provider: UpdateProvider | undefined
const provider = (): UpdateProvider => {
if (!_provider) {
const address = server.address()
if (!address || typeof address === "string" || !address.port) {
throw new Error("unexpected address")
}
_provider = new UpdateHttpProvider(
{
auth: AuthType.None,
commit: "test",
},
true,
`http://${address.address}:${address.port}/latest`,
settings,
)
_provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, settings)
}
return _provider
}
@@ -154,14 +144,10 @@ describe("update", () => {
})
it("should not reject if unable to fetch", async () => {
const options = {
auth: AuthType.None,
commit: "test",
}
let provider = new UpdateHttpProvider(options, true, "invalid", settings)
let provider = new UpdateProvider("invalid", settings)
await assert.doesNotReject(() => provider.getUpdate(true))
provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings)
provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings)
await assert.doesNotReject(() => provider.getUpdate(true))
})
})

View File

@@ -16,7 +16,8 @@
"tsBuildInfoFile": "./.cache/tsbuildinfo",
"incremental": true,
"rootDir": "./src",
"typeRoots": ["./node_modules/@types", "./typings"]
"typeRoots": ["./node_modules/@types", "./typings"],
"downlevelIteration": true
},
"include": ["./src/**/*.ts"]
}

189
typings/pluginapi.d.ts vendored Normal file
View File

@@ -0,0 +1,189 @@
/**
* This file describes the code-server plugin API for adding new applications.
*/
import { Logger } from "@coder/logger"
import * as express from "express"
/**
* Overlay
*
* The homepage of code-server will launch into VS Code. However, there will be an overlay
* button that when clicked, will show all available applications with their names,
* icons and provider plugins. When one clicks on an app's icon, they will be directed
* to <code-server-root>/<plugin-path>/<app-path> to access the application.
*/
/**
* Plugins
*
* Plugins are just node modules that contain a top level export "plugin" that implements
* the Plugin interface.
*
* 1. code-server uses $CS_PLUGIN to find plugins.
*
* e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load
* /tmp/will and /tmp/teffen as plugins.
*
* 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in
* $CS_PLUGIN_PATH with a package.json where the engine is code-server is
* a valid plugin.
*
* e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search
* /tmp/nhooyr and then /tmp/ash for plugins.
*
* CS_PLUGIN_PATH defaults to
* ~/.local/share/code-server/plugins:/usr/share/code-server/plugins
* if unset.
*
*
* 3. Built in plugins are loaded from __dirname/../plugins
*
* Plugins are required as soon as they are found and then initialized.
* See the Plugin interface for details.
*
* If two plugins are found with the exact same name, then code-server will
* use the first one and emit a warning.
*
*/
/**
* Programmability
*
* There is also a /api/applications endpoint to allow programmatic access to all
* available applications. It could be used to create a custom application dashboard
* for example. An important difference with the API is that all application paths
* will be absolute (i.e have the plugin path prepended) so that they may be used
* directly.
*
* Example output:
*
* [
* {
* "name": "Test App",
* "version": "4.0.0",
* "iconPath": "/test-plugin/test-app/icon.svg",
* "path": "/test-plugin/test-app",
* "description": "This app does XYZ.",
* "homepageURL": "https://example.com",
* "plugin": {
* "name": "test-plugin",
* "version": "1.0.0",
* "modulePath": "/Users/nhooyr/src/cdr/code-server/test/test-plugin",
* "displayName": "Test Plugin",
* "description": "Plugin used in code-server tests.",
* "routerPath": "/test-plugin",
* "homepageURL": "https://example.com"
* }
* }
* ]
*/
/**
* Your plugin module must have a top level export "plugin" that implements this interface.
*
* The plugin's router will be mounted at <code-sever-root>/<plugin-path>
*/
export interface Plugin {
/**
* name is used as the plugin's unique identifier.
* No two plugins may share the same name.
*
* Fetched from package.json.
*/
readonly name?: string
/**
* The version for the plugin in the overlay.
*
* Fetched from package.json.
*/
readonly version?: string
/**
* Name used in the overlay.
*/
readonly displayName: string
/**
* Used in overlay.
* Should be a full sentence describing the plugin.
*/
readonly description: string
/**
* The path at which the plugin router is to be registered.
*/
readonly routerPath: string
/**
* Link to plugin homepage.
*/
readonly homepageURL: string
/**
* init is called so that the plugin may initialize itself with the config.
*/
init(config: PluginConfig): void
/**
* Returns the plugin's router.
*
* Mounted at <code-sever-root>/<plugin-path>
*
* If not present, the plugin provides no routes.
*/
router?(): express.Router
/**
* code-server uses this to collect the list of applications that
* the plugin can currently provide.
* It is called when /api/applications is hit or the overlay needs to
* refresh the list of applications
*
* Ensure this is as fast as possible.
*
* If not present, the plugin provides no applications.
*/
applications?(): Application[] | Promise<Application[]>
}
/**
* PluginConfig contains the configuration required for initializing
* a plugin.
*/
export interface PluginConfig {
/**
* All plugin logs should be logged via this logger.
*/
readonly logger: Logger
}
/**
* Application represents a user accessible application.
*/
export interface Application {
readonly name: string
readonly version: string
/**
* When the user clicks on the icon in the overlay, they will be
* redirected to <code-server-root>/<plugin-path>/<app-path>
* where the application should be accessible.
*
* If undefined, then <code-server-root>/<plugin-path> is used.
*/
readonly path?: string
readonly description?: string
/**
* The path at which the icon for this application can be accessed.
* <code-server-root>/<plugin-path>/<app-path>/<icon-path>
*/
readonly iconPath: string
/**
* Link to application homepage.
*/
readonly homepageURL: string
}

1255
yarn.lock

File diff suppressed because it is too large Load Diff