Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f53784c5 | ||
|
|
7e1bb8fc96 | ||
|
|
ebe4d7ef29 | ||
|
|
f71d98f95c | ||
|
|
7fe475c1ef | ||
|
|
261af28f70 | ||
|
|
0713fa900b | ||
|
|
cc18175ce3 | ||
|
|
27f0f195a8 | ||
|
|
7282ebf436 | ||
|
|
c35d558352 | ||
|
|
8cb4e2c226 | ||
|
|
e5067ba2a9 | ||
|
|
fa0853dca6 | ||
|
|
a898dd34b9 | ||
|
|
4eb4375119 | ||
|
|
290c533c8e | ||
|
|
67e2a99df2 | ||
|
|
0ad7d93ea6 | ||
|
|
4cb8a32f4c | ||
|
|
833314aae8 | ||
|
|
5247878d93 | ||
|
|
ae65c83cbd | ||
|
|
eca4448877 | ||
|
|
93fb76e4a7 | ||
|
|
a1537d7138 | ||
|
|
def81245a4 | ||
|
|
37c80c9bbd | ||
|
|
be37821ab9 | ||
|
|
f74f1721e6 | ||
|
|
fb63c0cd22 | ||
|
|
bb26d2edd3 | ||
|
|
303fe2bc4e | ||
|
|
5a38ab95fe | ||
|
|
19710ab144 | ||
|
|
a018e30d6f | ||
|
|
fb835838db | ||
|
|
3d7fbec40f | ||
|
|
96170de191 | ||
|
|
2e2d03371f | ||
|
|
a0db6723c1 | ||
|
|
23ead21b1d | ||
|
|
42390da097 | ||
|
|
d0f6cbb02d | ||
|
|
fa59156a2a | ||
|
|
8ffe599796 | ||
|
|
a6f8840009 | ||
|
|
1feb30a7ff | ||
|
|
182aca6490 | ||
|
|
8311cf5657 | ||
|
|
4de2511162 | ||
|
|
3f7b91e2e2 | ||
|
|
431137da45 | ||
|
|
4d276b88c0 | ||
|
|
e28c9ab287 | ||
|
|
b540737b10 | ||
|
|
4380356e0c | ||
|
|
72caafe8b0 | ||
|
|
08b9e9ad1f | ||
|
|
2dc7863ec3 | ||
|
|
30100caf0c | ||
|
|
f79bb210ec | ||
|
|
182791319a | ||
|
|
624cd9d44f | ||
|
|
95ef6dbf2f | ||
|
|
016daf2fdd | ||
|
|
247c4ec776 | ||
|
|
d55e06936b | ||
|
|
2a3608df53 | ||
|
|
c6062c3d0a | ||
|
|
9ff535eddc | ||
|
|
2bf91ff6a6 | ||
|
|
ccc519ecbd | ||
|
|
40e1f066ff | ||
|
|
ac09aa6ea8 | ||
|
|
f5e3dca3b9 | ||
|
|
f64599b94d | ||
|
|
9917da068a | ||
|
|
8bf1bf2c9f | ||
|
|
79e8f3dfdb | ||
|
|
a37572d92d | ||
|
|
40a7c11ce3 | ||
|
|
7afa689285 | ||
|
|
f4d48bc880 | ||
|
|
9af3671c05 | ||
|
|
248c2adb2e | ||
|
|
52ea32f4a7 | ||
|
|
affa64c89c | ||
|
|
5e603056fd | ||
|
|
9889f30224 | ||
|
|
96995b78d1 | ||
|
|
6f14b8b8dd | ||
|
|
b73ea2fea2 | ||
|
|
e1702a1d21 | ||
|
|
5499a3d125 | ||
|
|
31b67062b0 | ||
|
|
72931edcf0 | ||
|
|
79478eb89f | ||
|
|
4574593664 | ||
|
|
71850e312b | ||
|
|
b8340a2ae9 | ||
|
|
f706039a9d | ||
|
|
de4949571c | ||
|
|
0a01338edd | ||
|
|
aa7415a479 | ||
|
|
10799aa1ec | ||
|
|
0e39bb9f2c | ||
|
|
03aa7709ca | ||
|
|
77c2a72cf8 | ||
|
|
f3d7d3f616 | ||
|
|
da6000b96f | ||
|
|
d969a5bd6b | ||
|
|
fe399ff0fe | ||
|
|
277211c4ce | ||
|
|
9d39c53c99 | ||
|
|
197a09f0c1 | ||
|
|
9453f891df | ||
|
|
14f408a837 | ||
|
|
8a8159c683 | ||
|
|
706bc23f04 | ||
|
|
af73b96313 | ||
|
|
2a13d003d3 | ||
|
|
687094802e | ||
|
|
139a28e0ea | ||
|
|
e03bbe3149 | ||
|
|
afff86ae9c | ||
|
|
fed545e67d | ||
|
|
6638daf6f0 | ||
|
|
8d3a7721fe | ||
|
|
75e52a3774 | ||
|
|
f4d7f00033 | ||
|
|
ef971009d9 | ||
|
|
30d2962e21 | ||
|
|
82e8a00a0d | ||
|
|
bea185b8b2 | ||
|
|
e08a55d44a | ||
|
|
481df70622 | ||
|
|
aa2cfa2c17 | ||
|
|
959497067c | ||
|
|
f7076247f9 | ||
|
|
f6c4434191 | ||
|
|
cb991a9143 | ||
|
|
3f1750cf83 | ||
|
|
7b2752a62c | ||
|
|
9e09c1f92b | ||
|
|
8252c372af | ||
|
|
396af23842 | ||
|
|
34225e2bdf | ||
|
|
476379a77e | ||
|
|
210fc049c4 | ||
|
|
e5c8e0aad1 | ||
|
|
c5ce365482 | ||
|
|
a653b93ce2 | ||
|
|
e2c35facdb | ||
|
|
75b93f9dc5 | ||
|
|
1eebde56ab | ||
|
|
e27188c2f9 | ||
|
|
ddbac8dd78 | ||
|
|
8066da12fe | ||
|
|
03e0013112 | ||
|
|
e243f6e369 | ||
|
|
c10450c4c5 | ||
|
|
c72c53f64d | ||
|
|
f4e5855318 | ||
|
|
3a074fd844 | ||
|
|
8a9e61defb | ||
|
|
1067507c41 | ||
|
|
f9e0990594 | ||
|
|
c07296cce0 | ||
|
|
31306f7fdd | ||
|
|
7affce5801 | ||
|
|
37b87dd2b8 | ||
|
|
9bde62fbd6 | ||
|
|
6fbbb1047f | ||
|
|
e07a591745 | ||
|
|
676c7bf915 | ||
|
|
9ad7d0b7a3 | ||
|
|
07e7c38ea2 | ||
|
|
0b9af6ef67 | ||
|
|
c63dc3a1ea | ||
|
|
a1b61d1659 | ||
|
|
bae28727bd | ||
|
|
8b85006996 | ||
|
|
10b3028196 | ||
|
|
860c99e3b8 | ||
|
|
f2f1fee6f1 | ||
|
|
504d89638b | ||
|
|
dc177ab505 | ||
|
|
cde94d5ed4 | ||
|
|
305348f0ac | ||
|
|
6ab6cb4f07 | ||
|
|
6422a8d74b | ||
|
|
257d9a4fa4 | ||
|
|
112eda4605 | ||
|
|
4b6cbacbad | ||
|
|
71dc5c7542 | ||
|
|
7e1e9d1249 | ||
|
|
62735da694 | ||
|
|
6cc1ee1b00 | ||
|
|
79443c14ff | ||
|
|
a0b7bf2180 | ||
|
|
30f3030530 | ||
|
|
759a78d9d8 | ||
|
|
7093f99a78 | ||
|
|
bca1bcfc03 | ||
|
|
4a3d2e5a94 | ||
|
|
14287df655 | ||
|
|
8e93e28162 | ||
|
|
9f25cc6d5d | ||
|
|
6000e389bc | ||
|
|
2928d362fa | ||
|
|
dcb303a437 | ||
|
|
daf204eeda | ||
|
|
f20f7ac166 | ||
|
|
a7c43a8eb6 | ||
|
|
30d05aeb4b | ||
|
|
07580e1fcb | ||
|
|
e3699cf258 | ||
|
|
2a22676d93 | ||
|
|
36b3183b75 | ||
|
|
d7cba30c6a | ||
|
|
30fafc8937 | ||
|
|
ec564091f1 | ||
|
|
83465a2f4f | ||
|
|
b4fd47b5af | ||
|
|
3570ff796d | ||
|
|
fd241d555b | ||
|
|
d323f4f75b | ||
|
|
40b1efa142 | ||
|
|
ea105a9290 | ||
|
|
e453d3107d | ||
|
|
a4a03c1492 | ||
|
|
d7ba9ae633 | ||
|
|
00383b79b9 | ||
|
|
c6ba12942c | ||
|
|
d7e3112625 | ||
|
|
26c735b434 | ||
|
|
466a04f874 | ||
|
|
e0769dc13a | ||
|
|
fe19391c03 | ||
|
|
021c084e43 | ||
|
|
1902296702 | ||
|
|
bb1bf88439 | ||
|
|
0a8e71c647 | ||
|
|
6bdaada689 | ||
|
|
811cf3364a | ||
|
|
64a6a460c8 | ||
|
|
1e4e72aa5b | ||
|
|
fcfb03382a | ||
|
|
d67bd3f604 | ||
|
|
2d1de749f4 | ||
|
|
c6c293d53a | ||
|
|
daa1c86fe0 | ||
|
|
9002f118c3 | ||
|
|
a5b6d080bd | ||
|
|
9ff37977a8 | ||
|
|
f5489cd3a0 | ||
|
|
c86d7398ab | ||
|
|
9f963c7e66 | ||
|
|
8063c79e44 | ||
|
|
febf4ead96 | ||
|
|
3e28ab85a0 | ||
|
|
85b0804be5 | ||
|
|
ebbcb8d6a7 | ||
|
|
df3089f3ad | ||
|
|
7cc16ceb3a | ||
|
|
bfe731f4f3 | ||
|
|
c4f1c053bf | ||
|
|
4b3c089630 | ||
|
|
1c16814a89 | ||
|
|
c3c24fe4d2 | ||
|
|
6e8248cf0c | ||
|
|
dd996d8f60 | ||
|
|
fae07e14fb | ||
|
|
c308ae0edd | ||
|
|
9035bfa871 | ||
|
|
22c4a7e10f | ||
|
|
607444c695 | ||
|
|
b22f3cb72f | ||
|
|
eacca7d692 | ||
|
|
0aa98279d6 | ||
|
|
55a7e8b56f | ||
|
|
916e24e109 | ||
|
|
c7c62daa67 | ||
|
|
579bb94a6c | ||
|
|
a44b4455f5 | ||
|
|
548a35c0ee | ||
|
|
402f5ebd77 | ||
|
|
c2ac126a50 | ||
|
|
b3811a67e0 | ||
|
|
ddda280df4 | ||
|
|
b415b7524f | ||
|
|
7a982555a8 | ||
|
|
e64b186527 | ||
|
|
11eaf0b470 | ||
|
|
8b5deac92b | ||
|
|
9d87c5328c | ||
|
|
cc5ed1eb57 | ||
|
|
e998dc1e82 | ||
|
|
ffe6a663aa | ||
|
|
938b460685 | ||
|
|
fef619aef8 | ||
|
|
0a2328c1f6 | ||
|
|
e44e574ce1 | ||
|
|
7991e09bbc | ||
|
|
9fb318cf15 | ||
|
|
4a250be79a | ||
|
|
3761f7bd51 | ||
|
|
ceceef1dae | ||
|
|
e858d11279 | ||
|
|
35a2d71b67 | ||
|
|
96a78c98d1 | ||
|
|
70b73d7cb9 | ||
|
|
8fe7986d0d | ||
|
|
559d05bb7b | ||
|
|
341cb342b2 | ||
|
|
34f8c77a03 | ||
|
|
d33df75662 | ||
|
|
85d5858b1d | ||
|
|
1b6ddb66f0 | ||
|
|
617cd38c71 | ||
|
|
75c8fdeed2 | ||
|
|
de41646fc4 | ||
|
|
882a2bfd5a |
@@ -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
|
||||
@@ -30,6 +33,9 @@ rules:
|
||||
eqeqeq: error
|
||||
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
@@ -1 +1,3 @@
|
||||
* @code-asher @nhooyr
|
||||
|
||||
ci/helm-chart @Matthew-Beckett @alexgorbatchev
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Documentation improvement
|
||||
about: Suggest a documentation improvement
|
||||
title: ""
|
||||
labels: "docs"
|
||||
assignees: ""
|
||||
---
|
||||
10
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/fmt.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/fmt.sh
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/lint.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/lint.sh
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/test.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/test.sh
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/release.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/release.sh
|
||||
- name: Upload npm package artifact
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
- name: Run ./ci/steps/build-docker-image.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/build-docker-image.sh
|
||||
- name: Upload release image
|
||||
|
||||
4
.github/workflows/publish.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/publish-npm.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/publish-npm.sh
|
||||
env:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/push-docker-manifest.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/push-docker-manifest.sh
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
@@ -11,3 +11,5 @@ release-images/
|
||||
node_modules
|
||||
node-*
|
||||
/plugins
|
||||
/lib/coder-cloud-agent
|
||||
.home
|
||||
|
||||
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "lib/vscode"]
|
||||
path = lib/vscode
|
||||
url = https://github.com/microsoft/vscode
|
||||
ignore = dirty
|
||||
|
||||
56
README.md
@@ -1,4 +1,4 @@
|
||||
# code-server
|
||||
# code-server · [](https://github.com/cdr/code-server/discussions) [](https://cdr.co/join-community) [](https://twitter.com/coderhq)
|
||||
|
||||
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser.
|
||||
|
||||
@@ -6,62 +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 runs 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
|
||||
We also have an in-depth [setup and configuration](./doc/guide.md) guide.
|
||||
|
||||
Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md).
|
||||
### Alpha Program 🐣
|
||||
|
||||
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
|
||||
|
||||
```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 a 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
|
||||
|
||||
|
||||
13
ci/README.md
@@ -17,14 +17,18 @@ 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
|
||||
the updated version.
|
||||
1. Summarize the major changes in the release notes and link to the relevant issues.
|
||||
4. Wait for the artifacts in step 2 to build.
|
||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact and
|
||||
upload them to the draft release.
|
||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
||||
- It will upload them to the draft release.
|
||||
6. Run some basic sanity tests on one of the released packages.
|
||||
- Especially make sure the terminal works fine.
|
||||
7. Make sure the github release tag is the commit with the artifacts. This is a bug in
|
||||
`hub` where uploading assets in step 5 will break the tag.
|
||||
8. Publish the release and merge the PR.
|
||||
@@ -36,7 +40,6 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
|
||||
10. Wait for the npm package to be published.
|
||||
11. Update the homebrew package.
|
||||
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated.
|
||||
12. Make sure to add a release without the `v` prefix for autoupdate from `3.2.0`.
|
||||
|
||||
## dev
|
||||
|
||||
@@ -64,6 +67,10 @@ This directory contains scripts used for the development of code-server.
|
||||
- [./ci/dev/watch.ts](./dev/watch.ts) (`yarn watch`)
|
||||
- Starts a process to build and launch code-server and restart on any code changes.
|
||||
- Example usage in [./doc/CONTRIBUTING.md](../doc/CONTRIBUTING.md).
|
||||
- [./ci/dev/gen_icons.sh](./ci/dev/gen_icons.sh) (`yarn icons`)
|
||||
- Generates the various icons from a single `.svg` favicon in
|
||||
`src/browser/media/favicon.svg`.
|
||||
- Requires [imagemagick](https://imagemagick.org/index.php)
|
||||
|
||||
## build
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ main() {
|
||||
chmod +x out/node/entry.js
|
||||
fi
|
||||
|
||||
if ! [ -f ./lib/coder-cloud-agent ]; then
|
||||
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
|
||||
fi
|
||||
|
||||
parcel build \
|
||||
--public-url "." \
|
||||
--out-dir dist \
|
||||
|
||||
@@ -11,15 +11,6 @@ main() {
|
||||
mkdir -p release-packages
|
||||
|
||||
release_archive
|
||||
# Will stop the auto update issues and allow people to upgrade their scripts
|
||||
# for the new release structure.
|
||||
if [[ $ARCH == "amd64" ]]; then
|
||||
if [[ $OS == "linux" ]]; then
|
||||
ARCH=x86_64 release_archive
|
||||
elif [[ $OS == "macos" ]]; then
|
||||
OS=darwin ARCH=x86_64 release_archive
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $OS == "linux" ]]; then
|
||||
release_nfpm
|
||||
@@ -30,12 +21,6 @@ release_archive() {
|
||||
local release_name="code-server-$VERSION-$OS-$ARCH"
|
||||
if [[ $OS == "linux" ]]; then
|
||||
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
|
||||
elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then
|
||||
# Just exists to make autoupdating from 3.2.0 work again.
|
||||
mv ./release-standalone "./$release_name"
|
||||
zip -r "release-packages/$release_name.zip" "./$release_name"
|
||||
mv "./$release_name" ./release-standalone
|
||||
return
|
||||
else
|
||||
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,10 @@ set -euo pipefail
|
||||
# MINIFY controls whether minified vscode is bundled.
|
||||
MINIFY="${MINIFY-true}"
|
||||
|
||||
# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install
|
||||
# to run first.
|
||||
KEEP_MODULES="${KEEP_MODULES-0}"
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
@@ -37,6 +41,7 @@ bundle_code_server() {
|
||||
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
||||
mkdir -p "$RELEASE_PATH/src/browser/pages"
|
||||
rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages"
|
||||
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
|
||||
|
||||
# Adds the commit to package.json
|
||||
jq --slurp '.[0] * .[1]' package.json <(
|
||||
@@ -51,15 +56,25 @@ EOF
|
||||
) > "$RELEASE_PATH/package.json"
|
||||
rsync yarn.lock "$RELEASE_PATH"
|
||||
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
|
||||
|
||||
if [ "$KEEP_MODULES" = 1 ]; then
|
||||
rsync node_modules/ "$RELEASE_PATH/node_modules"
|
||||
mkdir -p "$RELEASE_PATH/lib"
|
||||
rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib"
|
||||
fi
|
||||
}
|
||||
|
||||
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"
|
||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
||||
if [ "$KEEP_MODULES" = 0 ]; then
|
||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
||||
else
|
||||
rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules"
|
||||
fi
|
||||
rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions"
|
||||
rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions"
|
||||
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"
|
||||
|
||||
@@ -5,16 +5,7 @@ main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
rm -rf \
|
||||
out \
|
||||
release \
|
||||
release-standalone \
|
||||
release-packages \
|
||||
release-gcp \
|
||||
release-images \
|
||||
dist \
|
||||
.cache \
|
||||
node-*
|
||||
git clean -Xffd
|
||||
|
||||
pushd lib/vscode
|
||||
git clean -xffd
|
||||
|
||||
@@ -12,7 +12,7 @@ homepage: "https://github.com/cdr/code-server"
|
||||
license: "MIT"
|
||||
files:
|
||||
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
||||
./ci/build/code-server.service: /usr/lib/systemd/system/code-server.service
|
||||
./ci/build/code-server@.service: /usr/lib/systemd/system/code-server@.service
|
||||
# Only included for backwards compat with previous releases that shipped
|
||||
# the user service. See #1997
|
||||
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
||||
|
||||
@@ -24,6 +24,13 @@ main() {
|
||||
;;
|
||||
esac
|
||||
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
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."
|
||||
echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md"
|
||||
@@ -36,6 +43,13 @@ vscode_yarn() {
|
||||
yarn --production --frozen-lockfile
|
||||
cd extensions
|
||||
yarn --production --frozen-lockfile
|
||||
for ext in */; do
|
||||
ext="${ext%/}"
|
||||
echo "extensions/$ext: installing dependencies"
|
||||
cd "$ext"
|
||||
yarn --production --frozen-lockfile
|
||||
cd "$OLDPWD"
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -11,7 +11,7 @@ main() {
|
||||
source ./ci/lib.sh
|
||||
|
||||
download_artifact release-packages ./release-packages
|
||||
local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.zip,.deb,.rpm})
|
||||
local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.deb,.rpm})
|
||||
for i in "${!assets[@]}"; do
|
||||
assets[$i]="--attach=${assets[$i]}"
|
||||
done
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +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 != *"info Using config file ~/.config/code-server/config.yaml
|
||||
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
|
||||
|
||||
@@ -6,7 +6,7 @@ main() {
|
||||
|
||||
cd ./lib/vscode
|
||||
git add -A
|
||||
git diff HEAD > ../../ci/dev/vscode.patch
|
||||
git diff HEAD --full-index > ../../ci/dev/vscode.patch
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -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:"
|
||||
|
||||
21
ci/dev/gen_icons.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
main() {
|
||||
cd src/browser/media
|
||||
|
||||
# We need .ico for backwards compatibility.
|
||||
# The other two are the only icon sizes required by Chrome and
|
||||
# we use them for stuff like apple-touch-icon as well.
|
||||
# https://web.dev/add-manifest/
|
||||
#
|
||||
# This should be enough and we can always add more if there are problems.
|
||||
|
||||
# -background defaults to white but we want it transparent.
|
||||
# https://imagemagick.org/script/command-line-options.php#background
|
||||
convert -background transparent -resize 256x256 favicon.svg favicon.ico
|
||||
convert -background transparent -resize 192x192 favicon.svg pwa-icon-192.png
|
||||
convert -background transparent -resize 512x512 favicon.svg pwa-icon-512.png
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -4,14 +4,22 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "$0")/../../.."
|
||||
source ./ci/lib.sh
|
||||
mkdir -p .home
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
-v "$PWD:/src" \
|
||||
-e HOME="/src/.home" \
|
||||
-e USER="coder" \
|
||||
-e GITHUB_TOKEN \
|
||||
-e KEEP_MODULES \
|
||||
-e MINIFY \
|
||||
-w /src \
|
||||
-p 127.0.0.1:8080:8080 \
|
||||
"$(docker_build ./ci/images/debian8)" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e CI \
|
||||
"$(docker_build ./ci/images/"${IMAGE-debian10}")" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
@@ -7,10 +7,15 @@ main() {
|
||||
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
|
||||
stylelint $(git ls-files "*.css")
|
||||
tsc --noEmit
|
||||
# See comment in ./ci/image/debian8
|
||||
if [[ ! ${CI-} ]]; then
|
||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||
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
|
||||
|
||||
cd lib/vscode
|
||||
# Run this periodically in vanilla VS code to make sure we don't add any more warnings.
|
||||
yarn eslint --max-warnings=3
|
||||
cd "$OLDPWD"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
1639
ci/dev/vscode.patch
23
ci/helm-chart/.helmignore
Normal 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
@@ -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.3
|
||||
|
||||
# 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.4
|
||||
117
ci/helm-chart/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# code-server
|
||||
|
||||
  
|
||||
|
||||
[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.4"` | |
|
||||
| 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)
|
||||
25
ci/helm-chart/templates/NOTES.txt
Normal 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)
|
||||
63
ci/helm-chart/templates/_helpers.tpl
Normal 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 -}}
|
||||
152
ci/helm-chart/templates/deployment.yaml
Normal 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 }}
|
||||
41
ci/helm-chart/templates/ingress.yaml
Normal 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 }}
|
||||
29
ci/helm-chart/templates/pvc.yaml
Normal 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 }}
|
||||
18
ci/helm-chart/templates/secrets.yaml
Normal 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 }}
|
||||
19
ci/helm-chart/templates/service.yaml
Normal 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 }}
|
||||
11
ci/helm-chart/templates/serviceaccount.yaml
Normal 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 -}}
|
||||
18
ci/helm-chart/templates/tests/test-connection.yaml
Normal 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
@@ -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.4'
|
||||
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
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM centos:7
|
||||
|
||||
ARG NODE_VERSION=v12.18.3
|
||||
ARG NODE_VERSION=v12.18.4
|
||||
RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \
|
||||
curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \
|
||||
mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION"
|
||||
@@ -15,13 +15,18 @@ RUN npm config set python python2
|
||||
RUN yum install -y epel-release && yum install -y jq
|
||||
RUN yum install -y rsync
|
||||
|
||||
# Copied from ../debian8/Dockerfile
|
||||
# Install Go dependencies
|
||||
# Copied from ../debian10/Dockerfile
|
||||
# Install Go.
|
||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
||||
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
||||
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
|
||||
ENV GOPATH=/gopath
|
||||
# Ensures running this image as another user works.
|
||||
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:8
|
||||
FROM debian:10
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
@@ -24,30 +24,31 @@ RUN apt-get install -y build-essential \
|
||||
RUN apt-get install -y gettext-base
|
||||
|
||||
# Misc build dependencies.
|
||||
RUN apt-get install -y git rsync unzip
|
||||
|
||||
# We need latest jq from debian buster for date support.
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \
|
||||
dpkg -i libonig*.deb && \
|
||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i libjq*.deb && \
|
||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i jq*.deb && rm *.deb
|
||||
RUN apt-get install -y git rsync unzip jq
|
||||
|
||||
# Installs shellcheck.
|
||||
# Unfortunately coredumps on debian:8 so disabled for now.
|
||||
#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
||||
# tar -xJ && \
|
||||
# mv shellcheck*/shellcheck /usr/local/bin && \
|
||||
# rm -R shellcheck*
|
||||
RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
||||
tar -xJ && \
|
||||
mv shellcheck*/shellcheck /usr/local/bin && \
|
||||
rm -R shellcheck*
|
||||
|
||||
# Install Go dependencies
|
||||
# Install Go.
|
||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
||||
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
||||
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
|
||||
ENV GOPATH=/gopath
|
||||
# Ensures running this image as another user works.
|
||||
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
|
||||
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
|
||||
@@ -39,6 +39,10 @@ COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
|
||||
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
|
||||
|
||||
EXPOSE 8080
|
||||
USER coder
|
||||
# This way, if someone sets $DOCKER_USER, docker-exec will still work as
|
||||
# the uid will remain the same. note: only relevant if -u isn't passed to
|
||||
# docker-run.
|
||||
USER 1000
|
||||
ENV USER=coder
|
||||
WORKDIR /home/coder
|
||||
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# We do this first to ensure sudo works below when renaming the user.
|
||||
# Otherwise the current container UID may not exist in the passwd database.
|
||||
eval "$(fixuid -q)"
|
||||
|
||||
if [ "${DOCKER_USER-}" ]; then
|
||||
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
||||
sudo usermod --login "$DOCKER_USER" \
|
||||
--move-home --home "/home/$DOCKER_USER" \
|
||||
coder
|
||||
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
|
||||
# nor can we bind mount $HOME into a new home as that requires a privileged container.
|
||||
sudo usermod --login "$DOCKER_USER" coder
|
||||
sudo groupmod -n "$DOCKER_USER" coder
|
||||
|
||||
USER="$DOCKER_USER"
|
||||
|
||||
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
|
||||
sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml
|
||||
export HOME="/home/$DOCKER_USER"
|
||||
fi
|
||||
|
||||
# This isn't set by default.
|
||||
export USER="$(whoami)"
|
||||
dumb-init fixuid -q /usr/bin/code-server "$@"
|
||||
dumb-init /usr/bin/code-server "$@"
|
||||
|
||||
@@ -7,7 +7,7 @@ main() {
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# We do not `yarn vscode` to make fmt.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ main() {
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
# We need to fetch VS Code's deps for lint dependencies.
|
||||
yarn vscode
|
||||
|
||||
yarn lint
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
NODE_VERSION=v12.18.3
|
||||
NODE_VERSION=v12.18.4
|
||||
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
|
||||
curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz
|
||||
|
||||
@@ -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/debian8/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,48 +43,48 @@ 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/exec.sh yarn
|
||||
./ci/dev/image/exec.sh yarn vscode
|
||||
./ci/dev/image/exec.sh yarn watch
|
||||
./ci/dev/image/run.sh yarn
|
||||
./ci/dev/image/run.sh yarn vscode
|
||||
./ci/dev/image/run.sh yarn watch
|
||||
```
|
||||
|
||||
`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/steps/release.sh
|
||||
./ci/dev/image/run.sh ./ci/steps/release.sh
|
||||
```
|
||||
|
||||
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):
|
||||
|
||||
```
|
||||
./ci/dev/image/exec.sh ./ci/steps/release-packages.sh
|
||||
```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,66 +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 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
|
||||
|
||||
33
doc/FAQ.md
@@ -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)
|
||||
@@ -19,8 +20,8 @@
|
||||
- [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open)
|
||||
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
|
||||
- [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)
|
||||
@@ -32,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.
|
||||
@@ -143,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.
|
||||
|
||||
@@ -242,6 +250,20 @@ older than X minutes, kill `code-server`.
|
||||
|
||||
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
|
||||
|
||||
## Healthz endpoint
|
||||
|
||||
`code-server` exposes an endpoint at `/healthz` which can be used to check
|
||||
whether `code-server` is up without triggering a heartbeat. The response will
|
||||
include a status (`alive` or `expired`) and a timestamp for the last heartbeat
|
||||
(defaults to `0`). This endpoint does not require authentication.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "alive",
|
||||
"lastHeartbeat": 1599166210566
|
||||
}
|
||||
```
|
||||
|
||||
## How does the config file work?
|
||||
|
||||
When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks
|
||||
@@ -264,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.5.0/code-server_3.5.0_amd64.deb
|
||||
sudo dpkg -i code-server_3.5.0_amd64.deb
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.4/code-server_3.7.4_amd64.deb
|
||||
sudo dpkg -i code-server_3.7.4_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.5.0/code-server-3.5.0-amd64.rpm
|
||||
sudo rpm -i code-server-3.5.0-amd64.rpm
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.4/code-server-3.7.4-amd64.rpm
|
||||
sudo rpm -i code-server-3.7.4-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.5.0/code-server-3.5.0-linux-amd64.tar.gz \
|
||||
curl -fL https://github.com/cdr/code-server/releases/download/v3.7.4/code-server-3.7.4-linux-amd64.tar.gz \
|
||||
| tar -C ~/.local/lib -xz
|
||||
mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0
|
||||
ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server
|
||||
mv ~/.local/lib/code-server-3.7.4-linux-amd64 ~/.local/lib/code-server-3.7.4
|
||||
ln -s ~/.local/lib/code-server-3.7.4/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
|
||||
@@ -179,10 +187,11 @@ code-server
|
||||
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
|
||||
# outside the container.
|
||||
mkdir -p ~/.config
|
||||
docker run -it -p 127.0.0.1:8080:8080 \
|
||||
docker run -it --name code-server -p 127.0.0.1:8080:8080 \
|
||||
-v "$HOME/.config:/home/coder/.config" \
|
||||
-v "$PWD:/home/coder/project" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e "DOCKER_USER=$USER" \
|
||||
codercom/code-server:latest
|
||||
```
|
||||
|
||||
@@ -191,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
@@ -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! 🍻
|
||||
64
install.sh
@@ -17,27 +17,37 @@ usage() {
|
||||
Installs code-server for Linux, macOS and FreeBSD.
|
||||
It tries to use the system package manager if possible.
|
||||
After successful installation it explains how to start using code-server.
|
||||
|
||||
Pass in user@host to install code-server on user@host over ssh.
|
||||
The remote host must have internet access.
|
||||
${not_curl_usage-}
|
||||
Usage:
|
||||
|
||||
$arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local]
|
||||
$arg0 [--dry-run] [--version X.X.X] [--method detect] \
|
||||
[--prefix ~/.local] [--rsh ssh] [user@host]
|
||||
|
||||
--dry-run
|
||||
Echo the commands for the install process without running them.
|
||||
|
||||
--version X.X.X
|
||||
Install a specific version instead of the latest.
|
||||
|
||||
--method [detect | standalone]
|
||||
Choose the installation method. Defaults to detect.
|
||||
- detect detects the system package manager and tries to use it.
|
||||
Full reference on the process is further below.
|
||||
- standalone installs a standalone release archive into ~/.local
|
||||
Add ~/.local/bin to your \$PATH to use it.
|
||||
|
||||
--prefix <dir>
|
||||
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
||||
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
||||
and the binary symlinked into ~/.local/bin/code-server
|
||||
To install system wide pass ---prefix=/usr/local
|
||||
|
||||
--rsh <bin>
|
||||
Specifies the remote shell for remote installation. Defaults to ssh.
|
||||
|
||||
- For Debian, Ubuntu and Raspbian it will install the latest deb package.
|
||||
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
||||
- For Arch Linux it will install the AUR package.
|
||||
@@ -100,9 +110,19 @@ main() {
|
||||
METHOD \
|
||||
STANDALONE_INSTALL_PREFIX \
|
||||
VERSION \
|
||||
OPTIONAL
|
||||
OPTIONAL \
|
||||
ALL_FLAGS \
|
||||
RSH_ARGS \
|
||||
RSH
|
||||
|
||||
ALL_FLAGS=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-*)
|
||||
ALL_FLAGS="${ALL_FLAGS} $1"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
@@ -128,20 +148,45 @@ main() {
|
||||
--version=*)
|
||||
VERSION="$(parse_arg "$@")"
|
||||
;;
|
||||
--rsh)
|
||||
RSH="$(parse_arg "$@")"
|
||||
shift
|
||||
;;
|
||||
--rsh=*)
|
||||
RSH="$(parse_arg "$@")"
|
||||
;;
|
||||
-h | --h | -help | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
--)
|
||||
shift
|
||||
# We remove the -- added above.
|
||||
ALL_FLAGS="${ALL_FLAGS% --}"
|
||||
RSH_ARGS="$*"
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echoerr "Unknown flag $1"
|
||||
echoerr "Run with --help to see usage."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
RSH_ARGS="$*"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
shift
|
||||
done
|
||||
|
||||
if [ "${RSH_ARGS-}" ]; then
|
||||
RSH="${RSH-ssh}"
|
||||
echoh "Installing remotely with $RSH $RSH_ARGS"
|
||||
curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS"
|
||||
return
|
||||
fi
|
||||
|
||||
VERSION="${VERSION-$(echo_latest_version)}"
|
||||
METHOD="${METHOD-detect}"
|
||||
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
||||
@@ -446,7 +491,7 @@ arch() {
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
command -v "$@" > /dev/null
|
||||
}
|
||||
|
||||
sh_c() {
|
||||
@@ -500,4 +545,15 @@ humanpath() {
|
||||
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
|
||||
}
|
||||
|
||||
# We need to make sure we exit with a non zero exit if the command fails.
|
||||
# /bin/sh does not support -o pipefail unfortunately.
|
||||
prefix() {
|
||||
PREFIX="$1"
|
||||
shift
|
||||
fifo="$(mktemp -d)/fifo"
|
||||
mkfifo "$fifo"
|
||||
sed -e "s#^#$PREFIX: #" "$fifo" &
|
||||
"$@" > "$fifo" 2>&1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
20
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.5.0",
|
||||
"version": "3.7.4",
|
||||
"description": "Run VS Code on a remote server.",
|
||||
"homepage": "https://github.com/cdr/code-server",
|
||||
"bugs": {
|
||||
@@ -26,10 +26,14 @@
|
||||
"lint": "./ci/dev/lint.sh",
|
||||
"test": "./ci/dev/test.sh",
|
||||
"ci": "./ci/dev/ci.sh",
|
||||
"watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
|
||||
"watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts",
|
||||
"icons": "./ci/dev/gen_icons.sh"
|
||||
},
|
||||
"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",
|
||||
@@ -39,11 +43,13 @@
|
||||
"@types/pem": "^1.9.5",
|
||||
"@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",
|
||||
@@ -55,6 +61,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"
|
||||
},
|
||||
@@ -65,17 +72,22 @@
|
||||
},
|
||||
"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",
|
||||
"semver": "^7.1.3",
|
||||
"split2": "^3.2.2",
|
||||
"tar": "^6.0.1",
|
||||
"tar-fs": "^2.0.0",
|
||||
"ws": "^7.2.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 12 KiB |
1
src/browser/media/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 2250 2250" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M2029.18,672.912c-0,-249.515 -202.574,-452.089 -452.089,-452.089l-904.176,0c-249.515,0 -452.089,202.574 -452.089,452.089l0,904.176c0,249.515 202.574,452.089 452.089,452.089l904.176,-0c249.515,-0 452.089,-202.574 452.089,-452.089l-0,-904.176Z" style="fill:#fff;"/><path d="M1748.89,1058.72c-28.26,-0 -47.092,-16.57 -47.092,-50.58l0,-195.345c0,-124.707 -51.376,-193.601 -184.095,-193.601l-61.651,0l0,131.683l18.839,-0c52.23,-0 77.061,28.779 77.061,80.23l0,172.672c0,74.998 22.262,105.521 71.07,121.218c-48.808,14.827 -71.07,46.22 -71.07,121.218l0,128.197c0,35.753 0,70.636 -9.418,106.39c-9.418,33.14 -24.831,64.534 -46.237,91.567c-11.987,15.701 -25.688,28.78 -41.098,40.991l-0,17.44l61.647,-0c132.72,-0 184.097,-68.895 184.097,-193.601l-0,-195.345c-0,-34.883 17.975,-50.58 47.091,-50.58l35.108,0l-0,-131.684l-34.252,0l0,-0.87Z" style="fill-rule:nonzero;"/><path d="M1329.33,818.057l-190.087,-0c-4.282,-0 -7.705,-3.489 -7.705,-7.849l0,-14.824c0,-4.362 3.423,-7.849 7.705,-7.849l190.943,-0c4.28,-0 7.705,3.487 7.705,7.849l0,14.824c0,4.36 -4.282,7.849 -8.561,7.849Z" style="fill-rule:nonzero;"/><path d="M1361.87,1006.42l-138.711,-0c-4.282,-0 -7.708,-3.491 -7.708,-7.851l-0,-14.824c-0,-4.359 3.426,-7.849 7.708,-7.849l138.711,0c4.283,0 7.705,3.49 7.705,7.849l0,14.824c0,3.49 -3.422,7.851 -7.705,7.851Z" style="fill-rule:nonzero;"/><path d="M1416.67,912.236l-277.423,0c-4.282,0 -7.705,-3.487 -7.705,-7.848l0,-14.826c0,-4.36 3.423,-7.848 7.705,-7.848l276.567,0c4.282,0 7.707,3.488 7.707,7.848l0,14.826c0,3.488 -2.569,7.848 -6.851,7.848Z" style="fill-rule:nonzero;"/><path d="M919.188,860.762c18.837,0 37.676,1.745 55.657,6.105l-0,-35.757c-0,-50.58 25.687,-80.23 77.063,-80.23l18.837,-0l-0,-131.683l-61.651,0c-132.72,0 -184.093,68.894 -184.093,193.601l0,64.532c29.967,-10.463 61.651,-16.568 94.187,-16.568Z" style="fill-rule:nonzero;"/><path d="M1474.9,1335.15c-13.701,-110.754 -97.614,-203.194 -205.501,-224.124c-29.967,-6.103 -59.938,-6.978 -89.049,-1.744c-0.856,-0 -0.856,-0.873 -1.712,-0.873c-47.094,-100.288 -148.13,-166.566 -257.731,-166.566c-109.6,-0 -209.78,64.535 -257.731,164.823c-0.856,-0 -0.856,0.872 -1.712,0.872c-30.824,-3.49 -61.65,-1.747 -92.475,6.104c-106.174,26.16 -186.662,116.857 -201.218,226.738c-1.712,11.337 -2.569,22.673 -2.569,33.141c0,33.136 22.263,63.659 54.8,68.02c40.244,6.106 75.35,-25.29 74.494,-65.404c-0,-6.106 -0,-13.084 0.856,-19.187c6.85,-55.814 48.806,-102.904 103.605,-115.987c17.126,-4.361 34.251,-5.231 50.519,-2.614c52.232,6.977 103.606,-20.059 125.869,-67.149c16.27,-34.884 41.957,-65.409 76.207,-81.978c37.672,-18.314 80.487,-20.927 119.876,-6.974c41.097,14.824 71.921,46.218 90.76,85.461c19.693,38.374 29.111,65.407 71.069,70.64c17.124,2.614 65.074,1.743 83.056,0.871c35.106,-0 70.213,12.209 95.044,37.499c16.266,17.441 28.254,39.244 33.393,63.661c7.705,39.244 -1.713,78.486 -24.832,108.137c-16.27,20.93 -38.532,36.626 -63.363,43.604c-11.987,3.49 -23.975,4.36 -35.962,4.36l-189.232,0c-37.672,0 -67.642,-30.52 -67.642,-68.894l-0,-255.519c-0,-10.462 -8.561,-19.182 -18.837,-19.182l-26.544,-0c-52.233,0.87 -94.187,60.173 -94.187,122.96l-0,229.358c-0,68.021 53.942,122.961 120.731,122.961c0,0 297.119,-0.874 301.399,-0.874c68.499,-6.976 131.863,-42.73 174.673,-97.671c42.814,-53.196 62.507,-122.963 53.946,-194.47Z" style="fill-rule:nonzero;"/></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -6,31 +6,11 @@
|
||||
"background-color": "#fff",
|
||||
"description": "Run editors on a remote server.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png",
|
||||
"type": "image/png",
|
||||
"sizes": "384x384"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||
"type": "image/png",
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
@@ -11,9 +11,11 @@
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>{{ERROR_TITLE}} - code-server</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
|
||||
@@ -37,3 +37,7 @@ body {
|
||||
.login-form > .field > .submit {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server login</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
@@ -47,5 +49,5 @@
|
||||
</div>
|
||||
</body>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/login.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/login.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -9,11 +9,6 @@
|
||||
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="font-src 'self' data:; connect-src ws: wss: 'self' https:; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; manifest-src 'self'; img-src 'self' data: https:;"
|
||||
/>
|
||||
|
||||
<!-- Disable pinch zooming -->
|
||||
<meta
|
||||
name="viewport"
|
||||
@@ -29,19 +24,16 @@
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<!-- PROD_ONLY
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
|
||||
END_PROD_ONLY -->
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.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>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ try {
|
||||
}
|
||||
// FIXME: Only works if path separators are /.
|
||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||
fetch(`{{BASE}}/resource/?path=${encodeURIComponent(path)}`)
|
||||
fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
bundles[bundle] = json
|
||||
@@ -31,7 +31,8 @@ try {
|
||||
}
|
||||
|
||||
;(self.require as any) = {
|
||||
baseUrl: `${options.csStaticBase}/lib/vscode/out`,
|
||||
// Without the full URL VS Code will try to load file://.
|
||||
baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`,
|
||||
recordStats: true,
|
||||
paths: {
|
||||
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
||||
@@ -40,7 +41,7 @@ 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`,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ if ("serviceWorker" in navigator) {
|
||||
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
||||
navigator.serviceWorker
|
||||
.register(path, {
|
||||
scope: options.base || "/",
|
||||
scope: (options.base ?? "") + "/",
|
||||
})
|
||||
.then(() => {
|
||||
console.log("[Service Worker] registered")
|
||||
|
||||
2
src/browser/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type Callback<T, R = void> = (t: T) => R
|
||||
61
src/node/app.ts
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
300
src/node/cli.ts
@@ -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 { 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,31 +26,35 @@ 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
|
||||
"disable-update-check"?: 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
|
||||
|
||||
link?: OptionalString
|
||||
}
|
||||
|
||||
interface Option<T> {
|
||||
@@ -63,6 +71,11 @@ interface Option<T> {
|
||||
* Description of the option. Leave blank to hide the option.
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* If marked as beta, the option is not printed unless $CS_BETA is set.
|
||||
*/
|
||||
beta?: boolean
|
||||
}
|
||||
|
||||
type OptionType<T> = T extends boolean
|
||||
@@ -94,10 +107,20 @@ 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." },
|
||||
"disable-update-check": {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
|
||||
"then notifies you once every week that a new release is available.",
|
||||
},
|
||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||
json: { type: "boolean" },
|
||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||
@@ -130,7 +153,8 @@ const options: Options<Required<Args>> = {
|
||||
"install-extension": {
|
||||
type: "string[]",
|
||||
description:
|
||||
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
|
||||
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" +
|
||||
"To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
|
||||
},
|
||||
"enable-proposed-api": {
|
||||
type: "string[]",
|
||||
@@ -144,17 +168,29 @@ const options: Options<Required<Args>> = {
|
||||
"new-window": {
|
||||
type: "boolean",
|
||||
short: "n",
|
||||
description: "Force to open a new window. (use with open-in)",
|
||||
description: "Force to open a new window.",
|
||||
},
|
||||
"reuse-window": {
|
||||
type: "boolean",
|
||||
short: "r",
|
||||
description: "Force to open a file or folder in an already opened window. (use with open-in)",
|
||||
description: "Force to open a file or folder in an already opened window.",
|
||||
},
|
||||
|
||||
locale: { type: "string" },
|
||||
log: { type: LogLevel },
|
||||
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
|
||||
|
||||
link: {
|
||||
type: OptionalString,
|
||||
description: `
|
||||
Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like
|
||||
https://myname.coder-cloud.com at which you can easily access your code-server instance.
|
||||
Authorization is done via GitHub.
|
||||
This is presently beta and requires being accepted for testing.
|
||||
See https://github.com/cdr/code-server/discussions/2137
|
||||
`,
|
||||
beta: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const optionDescriptions = (): string[] => {
|
||||
@@ -166,12 +202,32 @@ export const optionDescriptions = (): string[] => {
|
||||
}),
|
||||
{ short: 0, long: 0 },
|
||||
)
|
||||
return entries.map(
|
||||
([k, v]) =>
|
||||
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
|
||||
widths.long - k.length,
|
||||
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
|
||||
)
|
||||
return entries
|
||||
.filter(([, v]) => {
|
||||
// If CS_BETA is set, we show beta options but if not, then we do not want
|
||||
// to show beta options.
|
||||
return process.env.CS_BETA || !v.beta
|
||||
})
|
||||
.map(([k, v]) => {
|
||||
const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${
|
||||
v.short ? `-${v.short}` : " "
|
||||
} --${k} `
|
||||
return (
|
||||
help +
|
||||
v.description
|
||||
?.trim()
|
||||
.split(/\n/)
|
||||
.map((line, i) => {
|
||||
line = line.trim()
|
||||
if (i === 0) {
|
||||
return " ".repeat(widths.long - k.length) + line
|
||||
}
|
||||
return " ".repeat(widths.long + widths.short + 6) + line
|
||||
})
|
||||
.join("\n") +
|
||||
(typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const parse = (
|
||||
@@ -285,7 +341,46 @@ 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 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()
|
||||
args["user-data-dir"] = paths.data
|
||||
}
|
||||
|
||||
if (!args["extensions-dir"]) {
|
||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
||||
}
|
||||
|
||||
// --verbose takes priority over --log and --log takes priority over the
|
||||
// environment variable.
|
||||
@@ -326,22 +421,49 @@ export const parse = (
|
||||
break
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
export async function setDefaults(args: Args): Promise<Args> {
|
||||
args = { ...args }
|
||||
|
||||
if (!args["user-data-dir"]) {
|
||||
await copyOldMacOSDataDir()
|
||||
args["user-data-dir"] = paths.data
|
||||
// Default to using a password.
|
||||
if (!args.auth) {
|
||||
args.auth = AuthType.Password
|
||||
}
|
||||
|
||||
if (!args["extensions-dir"]) {
|
||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
||||
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
|
||||
}
|
||||
|
||||
return args
|
||||
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> {
|
||||
@@ -352,12 +474,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) {
|
||||
@@ -370,10 +496,6 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
||||
}
|
||||
|
||||
if (!process.env.CODE_SERVER_PARENT_PID && !process.env.VSCODE_IPC_HOOK_CLI) {
|
||||
logger.info(`Using config file ${humanPath(configPath)}`)
|
||||
}
|
||||
|
||||
const configFile = await fs.readFile(configPath)
|
||||
const config = yaml.safeLoad(configFile.toString(), {
|
||||
filename: configPath,
|
||||
@@ -399,9 +521,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}`)
|
||||
return [u.hostname, parseInt(u.port, 10)]
|
||||
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 {
|
||||
@@ -412,7 +540,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
|
||||
@@ -427,16 +555,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> {
|
||||
@@ -453,3 +582,52 @@ async function copyOldMacOSDataDir(): Promise<void> {
|
||||
await fs.copy(oldDataDir, paths.data)
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldRunVsCodeCli = (args: Args): boolean => {
|
||||
return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if it looks like the user is trying to open a file or folder in an
|
||||
* existing instance. The arguments here should be the arguments the user
|
||||
* explicitly passed on the command line, not defaults or the configuration.
|
||||
*/
|
||||
export const shouldOpenInExistingInstance = async (args: Args): Promise<string | undefined> => {
|
||||
// Always use the existing instance if we're running from VS Code's terminal.
|
||||
if (process.env.VSCODE_IPC_HOOK_CLI) {
|
||||
return process.env.VSCODE_IPC_HOOK_CLI
|
||||
}
|
||||
|
||||
const readSocketPath = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// If these flags are set then assume the user is trying to open in an
|
||||
// existing instance since these flags have no effect otherwise.
|
||||
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
|
||||
return args[cur as keyof Args] ? prev + 1 : prev
|
||||
}, 0)
|
||||
if (openInFlagCount > 0) {
|
||||
return readSocketPath()
|
||||
}
|
||||
|
||||
// It's possible the user is trying to spawn another instance of code-server.
|
||||
// Check if any unrelated flags are set (check against one because `_` always
|
||||
// exists), that a file or directory was passed, and that the socket is
|
||||
// active.
|
||||
if (Object.keys(args).length === 1 && args._.length > 0) {
|
||||
const socketPath = await readSocketPath()
|
||||
if (socketPath && (await canConnect(socketPath))) {
|
||||
return socketPath
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
43
src/node/coder-cloud.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import { spawn } from "child_process"
|
||||
import path from "path"
|
||||
import split2 from "split2"
|
||||
|
||||
// https://github.com/cdr/coder-cloud
|
||||
const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent")
|
||||
|
||||
function runAgent(...args: string[]): Promise<void> {
|
||||
logger.debug(`running agent with ${args}`)
|
||||
|
||||
const agent = spawn(coderCloudAgent, args, {
|
||||
stdio: ["inherit", "inherit", "pipe"],
|
||||
})
|
||||
|
||||
agent.stderr.pipe(split2()).on("data", (line) => {
|
||||
line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
|
||||
logger.info(line)
|
||||
})
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
agent.on("error", rej)
|
||||
|
||||
agent.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
rej({
|
||||
message: `coder cloud agent exited with ${code}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function coderCloudBind(csAddr: string, serverName = ""): Promise<void> {
|
||||
logger.info("Remember --link is a beta feature and requires being accepted for testing")
|
||||
logger.info("See https://github.com/cdr/code-server/discussions/2137")
|
||||
// addr needs to be in host:port format.
|
||||
// So we trim the protocol.
|
||||
csAddr = csAddr.replace(/^https?:\/\//, "")
|
||||
return runAgent("bind", `--code-server-addr=${csAddr}`, serverName)
|
||||
}
|
||||
13
src/node/constants.ts
Normal 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, "../..")
|
||||
@@ -1,149 +1,175 @@
|
||||
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 { 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 { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
|
||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||
import { loadPlugins } from "./plugin"
|
||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
||||
import { ipcMain, wrap } from "./wrapper"
|
||||
import { createApp, ensureAddress } from "./app"
|
||||
import {
|
||||
AuthType,
|
||||
DefaultedArgs,
|
||||
optionDescriptions,
|
||||
parse,
|
||||
readConfigFile,
|
||||
setDefaults,
|
||||
shouldOpenInExistingInstance,
|
||||
shouldRunVsCodeCli,
|
||||
} from "./cli"
|
||||
import { coderCloudBind } from "./coder-cloud"
|
||||
import { commit, version } from "./constants"
|
||||
import { register } from "./routes"
|
||||
import { humanPath, isFile, open } from "./util"
|
||||
import { isChild, wrapper } from "./wrapper"
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
})
|
||||
|
||||
let pkg: { version?: string; commit?: string } = {}
|
||||
try {
|
||||
pkg = require("../../package.json")
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
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: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
},
|
||||
})
|
||||
vscode.once("message", (message: any) => {
|
||||
logger.debug("got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||
process.exit(1)
|
||||
}
|
||||
const send: CliMessage = { type: "cli", args }
|
||||
vscode.send(send)
|
||||
})
|
||||
vscode.once("error", (error) => {
|
||||
logger.error("Got error from VS Code", field("error", error))
|
||||
process.exit(1)
|
||||
})
|
||||
vscode.on("exit", (code) => process.exit(code || 0))
|
||||
}
|
||||
|
||||
const version = pkg.version || "development"
|
||||
const commit = pkg.commit || "development"
|
||||
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
||||
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||
type: "open",
|
||||
folderURIs: [],
|
||||
fileURIs: [],
|
||||
forceReuseWindow: args["reuse-window"],
|
||||
forceNewWindow: args["new-window"],
|
||||
}
|
||||
|
||||
const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void> => {
|
||||
if (!args.auth) {
|
||||
args = {
|
||||
...args,
|
||||
auth: AuthType.Password,
|
||||
for (let i = 0; i < args._.length; i++) {
|
||||
const fp = path.resolve(args._[i])
|
||||
if (await isFile(fp)) {
|
||||
pipeArgs.fileURIs.push(fp)
|
||||
} else {
|
||||
pipeArgs.folderURIs.push(fp)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
|
||||
logger.error("--new-window can only be used with folder paths")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
|
||||
logger.error("Please specify at least one file or folder")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const vscode = http.request(
|
||||
{
|
||||
path: "/",
|
||||
method: "POST",
|
||||
socketPath,
|
||||
},
|
||||
(response) => {
|
||||
response.on("data", (message) => {
|
||||
logger.debug("got message from VS Code", field("message", message.toString()))
|
||||
})
|
||||
},
|
||||
)
|
||||
vscode.on("error", (error: unknown) => {
|
||||
logger.error("got error from VS Code", field("error", error))
|
||||
})
|
||||
vscode.write(JSON.stringify(pipeArgs))
|
||||
vscode.end()
|
||||
}
|
||||
|
||||
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(cliArgs, 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)
|
||||
|
||||
await loadPlugins(httpServer, args)
|
||||
|
||||
ipcMain().onDispose(() => {
|
||||
httpServer.dispose().then((errors) => {
|
||||
errors.forEach((error) => logger.error(error.message))
|
||||
})
|
||||
})
|
||||
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
const serverAddress = await httpServer.listen()
|
||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
||||
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||
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 (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 (serverAddress && !options.socket && args.open) {
|
||||
if (args.link) {
|
||||
try {
|
||||
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
|
||||
logger.info(" - Connected to cloud agent")
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
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")
|
||||
await open(openAddress).catch(console.error)
|
||||
logger.info(`Opened ${openAddress}`)
|
||||
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 tryParse = async (): Promise<[Args, Args, Args]> => {
|
||||
try {
|
||||
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)
|
||||
return [args, cliArgs, configArgs]
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
// 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. 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 [args, cliArgs, configArgs] = await tryParse()
|
||||
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("")
|
||||
@@ -153,7 +179,10 @@ async function entry(): Promise<void> {
|
||||
optionDescriptions().forEach((description) => {
|
||||
console.log("", description)
|
||||
})
|
||||
} else if (args.version) {
|
||||
return
|
||||
}
|
||||
|
||||
if (args.version) {
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
@@ -163,83 +192,22 @@ async function entry(): Promise<void> {
|
||||
} else {
|
||||
console.log(version, commit)
|
||||
}
|
||||
process.exit(0)
|
||||
} else if (process.env.VSCODE_IPC_HOOK_CLI) {
|
||||
const pipeArgs: OpenCommandPipeArgs = {
|
||||
type: "open",
|
||||
folderURIs: [],
|
||||
forceReuseWindow: args["reuse-window"],
|
||||
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 (!pipeArgs.fileURIs) {
|
||||
pipeArgs.fileURIs = []
|
||||
}
|
||||
pipeArgs.fileURIs.push(fp)
|
||||
}
|
||||
}
|
||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) {
|
||||
logger.error("new-window can only be used with folder paths")
|
||||
process.exit(1)
|
||||
}
|
||||
if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) {
|
||||
logger.error("Please specify at least one file or folder argument")
|
||||
process.exit(1)
|
||||
}
|
||||
const vscode = http.request(
|
||||
{
|
||||
path: "/",
|
||||
method: "POST",
|
||||
socketPath: process.env["VSCODE_IPC_HOOK_CLI"],
|
||||
},
|
||||
(res) => {
|
||||
res.on("data", (message) => {
|
||||
logger.debug("Got message from VS Code", field("message", message.toString()))
|
||||
})
|
||||
},
|
||||
)
|
||||
vscode.on("error", (err) => {
|
||||
logger.debug("Got error from VS Code", field("error", err))
|
||||
})
|
||||
vscode.write(JSON.stringify(pipeArgs))
|
||||
vscode.end()
|
||||
} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) {
|
||||
logger.debug("forking vs code cli...")
|
||||
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
},
|
||||
})
|
||||
vscode.once("message", (message: any) => {
|
||||
logger.debug("Got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response")
|
||||
process.exit(1)
|
||||
}
|
||||
const send: CliMessage = { type: "cli", args }
|
||||
vscode.send(send)
|
||||
})
|
||||
vscode.once("error", (error) => {
|
||||
logger.error(error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
vscode.on("exit", (code) => process.exit(code || 0))
|
||||
} else {
|
||||
wrap(() => main(args, cliArgs, configArgs))
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldRunVsCodeCli(args)) {
|
||||
return runVsCodeCli(args)
|
||||
}
|
||||
|
||||
const socketPath = await shouldOpenInExistingInstance(cliArgs)
|
||||
if (socketPath) {
|
||||
return openInExistingInstance(args, socketPath)
|
||||
}
|
||||
|
||||
return wrapper.start(args)
|
||||
}
|
||||
|
||||
entry()
|
||||
entry().catch((error) => {
|
||||
logger.error(error.message)
|
||||
wrapper.exit(error)
|
||||
})
|
||||
|
||||
48
src/node/heart.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
999
src/node/http.ts
@@ -1,60 +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 * 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
try {
|
||||
const plugin: Plugin = require(pluginPath)
|
||||
plugin.activate(httpServer, args)
|
||||
logger.debug("Loaded plugin", field("name", path.basename(pluginPath)))
|
||||
} catch (error) {
|
||||
if (error.code !== "MODULE_NOT_FOUND") {
|
||||
logger.warn(error.message)
|
||||
} else {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
const pluginPath = path.resolve(__dirname, "../../plugins")
|
||||
const files = await util.promisify(fs.readdir)(pluginPath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args)))
|
||||
}
|
||||
/**
|
||||
* 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()
|
||||
|
||||
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
try {
|
||||
await _loadPlugins(httpServer, args)
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
logger.warn(error.message)
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.PLUGIN_DIR) {
|
||||
await loadPlugin(process.env.PLUGIN_DIR, httpServer, args)
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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
|
||||
}
|
||||
89
src/node/routes/domainProxy.ts
Normal 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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
})
|
||||
47
src/node/routes/pathProxy.ts
Normal 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
@@ -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
@@ -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),
|
||||
})
|
||||
})
|
||||
106
src/node/routes/vscode.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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"],
|
||||
disableUpdateCheck: !!req.args["disable-update-check"],
|
||||
},
|
||||
)
|
||||
.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)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from "path"
|
||||
import * as tls from "tls"
|
||||
import { Emitter } from "../common/emitter"
|
||||
import { generateUuid } from "../common/util"
|
||||
import { tmpdir } from "./util"
|
||||
import { canConnect, tmpdir } from "./util"
|
||||
|
||||
/**
|
||||
* Provides a way to proxy a TLS socket. Can be used when you need to pass a
|
||||
@@ -89,17 +89,6 @@ export class SocketProxyProvider {
|
||||
}
|
||||
|
||||
public async findFreeSocketPath(basePath: string, maxTries = 100): Promise<string> {
|
||||
const canConnect = (path: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect(path)
|
||||
socket.once("error", () => resolve(false))
|
||||
socket.once("connect", () => {
|
||||
socket.destroy()
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let i = 0
|
||||
let path = basePath
|
||||
while ((await canConnect(path)) && i < maxTries) {
|
||||
|
||||
@@ -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)
|
||||
@@ -2,6 +2,7 @@ import * as cp from "child_process"
|
||||
import * as crypto from "crypto"
|
||||
import envPaths from "env-paths"
|
||||
import * as fs from "fs-extra"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as util from "util"
|
||||
@@ -53,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> => {
|
||||
@@ -246,3 +267,26 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that resolves with whether the socket path is active.
|
||||
*/
|
||||
export function canConnect(path: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect(path)
|
||||
socket.once("error", () => resolve(false))
|
||||
socket.once("connect", () => {
|
||||
socket.destroy()
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const isFile = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(path)
|
||||
return stat.isFile()
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||