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.
|
- prettier/@typescript-eslint # Remove conflicts again.
|
||||||
|
|
||||||
rules:
|
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.
|
# For overloads.
|
||||||
no-dupe-class-members: off
|
no-dupe-class-members: off
|
||||||
"@typescript-eslint/no-use-before-define": off
|
"@typescript-eslint/no-use-before-define": off
|
||||||
@@ -30,6 +33,9 @@ rules:
|
|||||||
eqeqeq: error
|
eqeqeq: error
|
||||||
import/order:
|
import/order:
|
||||||
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
[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:
|
settings:
|
||||||
# Does not work with CommonJS unfortunately.
|
# Does not work with CommonJS unfortunately.
|
||||||
|
|||||||
2
.github/CODEOWNERS
vendored
@@ -1 +1,3 @@
|
|||||||
* @code-asher @nhooyr
|
* @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:
|
contact_links:
|
||||||
- name: Question
|
- name: Question
|
||||||
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/fmt.sh
|
- name: Run ./ci/steps/fmt.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/fmt.sh
|
args: ./ci/steps/fmt.sh
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/lint.sh
|
- name: Run ./ci/steps/lint.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/lint.sh
|
args: ./ci/steps/lint.sh
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/test.sh
|
- name: Run ./ci/steps/test.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/test.sh
|
args: ./ci/steps/test.sh
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/release.sh
|
- name: Run ./ci/steps/release.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/release.sh
|
args: ./ci/steps/release.sh
|
||||||
- name: Upload npm package artifact
|
- name: Upload npm package artifact
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
name: release-packages
|
name: release-packages
|
||||||
path: ./release-packages
|
path: ./release-packages
|
||||||
- name: Run ./ci/steps/build-docker-image.sh
|
- name: Run ./ci/steps/build-docker-image.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/build-docker-image.sh
|
args: ./ci/steps/build-docker-image.sh
|
||||||
- name: Upload release image
|
- name: Upload release image
|
||||||
|
|||||||
4
.github/workflows/publish.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/publish-npm.sh
|
- name: Run ./ci/steps/publish-npm.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/publish-npm.sh
|
args: ./ci/steps/publish-npm.sh
|
||||||
env:
|
env:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/push-docker-manifest.sh
|
- name: Run ./ci/steps/push-docker-manifest.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/push-docker-manifest.sh
|
args: ./ci/steps/push-docker-manifest.sh
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -11,3 +11,5 @@ release-images/
|
|||||||
node_modules
|
node_modules
|
||||||
node-*
|
node-*
|
||||||
/plugins
|
/plugins
|
||||||
|
/lib/coder-cloud-agent
|
||||||
|
.home
|
||||||
|
|||||||
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
|||||||
[submodule "lib/vscode"]
|
[submodule "lib/vscode"]
|
||||||
path = lib/vscode
|
path = lib/vscode
|
||||||
url = https://github.com/microsoft/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.
|
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
|
## Highlights
|
||||||
|
|
||||||
- **Code everywhere**
|
- Code on any device with a consistent development environment
|
||||||
- Code on your Chromebook, tablet, and laptop with a consistent development environment.
|
- Use cloud servers to speed up tests, compilations, downloads, and more
|
||||||
- Develop on a Linux machine and pick up from any device with a web browser.
|
- Preserve battery life when you're on the go; all intensive tasks run on your server
|
||||||
- **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.
|
|
||||||
|
|
||||||
## Getting Started
|
## 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.
|
If you choose to use the install script, you can preview what occurs during the install process:
|
||||||
|
|
||||||
It tries to use the system package manager if possible.
|
|
||||||
|
|
||||||
First run to print out the install process:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Now to actually install:
|
To install, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://code-server.dev/install.sh | sh
|
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
|
## FAQ
|
||||||
|
|
||||||
See [./doc/FAQ.md](./doc/FAQ.md).
|
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
|
## Hiring
|
||||||
|
|
||||||
We ([@cdr](https://github.com/cdr)) are looking for a engineers to help maintain
|
We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain
|
||||||
code-server, innovate on open source and streamline dev workflows.
|
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
|
Our main office is in Austin, Texas. Remote is ok as long as
|
||||||
you're in North America or Europe.
|
you're in North America or Europe.
|
||||||
|
|
||||||
Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested.
|
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).
|
|
||||||
|
|
||||||
## For Organizations
|
## 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 the version of code-server and make a PR.
|
||||||
1. Update in `package.json`
|
1. Update in `package.json`
|
||||||
2. Update in [./doc/install.md](../doc/install.md)
|
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.
|
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
|
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with
|
||||||
the updated version.
|
the updated version.
|
||||||
1. Summarize the major changes in the release notes and link to the relevant issues.
|
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.
|
4. Wait for the artifacts in step 2 to build.
|
||||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact and
|
5. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
||||||
upload them to the draft release.
|
- It will upload them to the draft release.
|
||||||
6. Run some basic sanity tests on one of the released packages.
|
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
|
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.
|
`hub` where uploading assets in step 5 will break the tag.
|
||||||
8. Publish the release and merge the PR.
|
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.
|
10. Wait for the npm package to be published.
|
||||||
11. Update the homebrew package.
|
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.
|
- 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
|
## 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`)
|
- [./ci/dev/watch.ts](./dev/watch.ts) (`yarn watch`)
|
||||||
- Starts a process to build and launch code-server and restart on any code changes.
|
- Starts a process to build and launch code-server and restart on any code changes.
|
||||||
- Example usage in [./doc/CONTRIBUTING.md](../doc/CONTRIBUTING.md).
|
- 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
|
## build
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ main() {
|
|||||||
chmod +x out/node/entry.js
|
chmod +x out/node/entry.js
|
||||||
fi
|
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 \
|
parcel build \
|
||||||
--public-url "." \
|
--public-url "." \
|
||||||
--out-dir dist \
|
--out-dir dist \
|
||||||
|
|||||||
@@ -11,15 +11,6 @@ main() {
|
|||||||
mkdir -p release-packages
|
mkdir -p release-packages
|
||||||
|
|
||||||
release_archive
|
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
|
if [[ $OS == "linux" ]]; then
|
||||||
release_nfpm
|
release_nfpm
|
||||||
@@ -30,12 +21,6 @@ release_archive() {
|
|||||||
local release_name="code-server-$VERSION-$OS-$ARCH"
|
local release_name="code-server-$VERSION-$OS-$ARCH"
|
||||||
if [[ $OS == "linux" ]]; then
|
if [[ $OS == "linux" ]]; then
|
||||||
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
|
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
|
else
|
||||||
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ set -euo pipefail
|
|||||||
# MINIFY controls whether minified vscode is bundled.
|
# MINIFY controls whether minified vscode is bundled.
|
||||||
MINIFY="${MINIFY-true}"
|
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() {
|
main() {
|
||||||
cd "$(dirname "${0}")/../.."
|
cd "$(dirname "${0}")/../.."
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
@@ -37,6 +41,7 @@ bundle_code_server() {
|
|||||||
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
||||||
mkdir -p "$RELEASE_PATH/src/browser/pages"
|
mkdir -p "$RELEASE_PATH/src/browser/pages"
|
||||||
rsync src/browser/pages/*.html "$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
|
# Adds the commit to package.json
|
||||||
jq --slurp '.[0] * .[1]' package.json <(
|
jq --slurp '.[0] * .[1]' package.json <(
|
||||||
@@ -51,15 +56,25 @@ EOF
|
|||||||
) > "$RELEASE_PATH/package.json"
|
) > "$RELEASE_PATH/package.json"
|
||||||
rsync yarn.lock "$RELEASE_PATH"
|
rsync yarn.lock "$RELEASE_PATH"
|
||||||
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
|
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() {
|
bundle_vscode() {
|
||||||
mkdir -p "$VSCODE_OUT_PATH"
|
mkdir -p "$VSCODE_OUT_PATH"
|
||||||
rsync "$VSCODE_SRC_PATH/yarn.lock" "$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"
|
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
|
||||||
|
if [ "$KEEP_MODULES" = 0 ]; then
|
||||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
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/package.json" "$VSCODE_OUT_PATH/extensions"
|
||||||
rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$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"
|
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"
|
||||||
|
|||||||
@@ -5,16 +5,7 @@ main() {
|
|||||||
cd "$(dirname "${0}")/../.."
|
cd "$(dirname "${0}")/../.."
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
|
||||||
rm -rf \
|
git clean -Xffd
|
||||||
out \
|
|
||||||
release \
|
|
||||||
release-standalone \
|
|
||||||
release-packages \
|
|
||||||
release-gcp \
|
|
||||||
release-images \
|
|
||||||
dist \
|
|
||||||
.cache \
|
|
||||||
node-*
|
|
||||||
|
|
||||||
pushd lib/vscode
|
pushd lib/vscode
|
||||||
git clean -xffd
|
git clean -xffd
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ homepage: "https://github.com/cdr/code-server"
|
|||||||
license: "MIT"
|
license: "MIT"
|
||||||
files:
|
files:
|
||||||
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
./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
|
# Only included for backwards compat with previous releases that shipped
|
||||||
# the user service. See #1997
|
# the user service. See #1997
|
||||||
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ main() {
|
|||||||
;;
|
;;
|
||||||
esac
|
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
|
if ! vscode_yarn; then
|
||||||
echo "You may not have the required dependencies to build the native modules."
|
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"
|
echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md"
|
||||||
@@ -36,6 +43,13 @@ vscode_yarn() {
|
|||||||
yarn --production --frozen-lockfile
|
yarn --production --frozen-lockfile
|
||||||
cd extensions
|
cd extensions
|
||||||
yarn --production --frozen-lockfile
|
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 "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ main() {
|
|||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
|
||||||
download_artifact release-packages ./release-packages
|
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
|
for i in "${!assets[@]}"; do
|
||||||
assets[$i]="--attach=${assets[$i]}"
|
assets[$i]="--attach=${assets[$i]}"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -15,7 +15,17 @@ v$VERSION
|
|||||||
|
|
||||||
VS Code v$(vscode_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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ main() {
|
|||||||
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
||||||
local installed_extensions
|
local installed_extensions
|
||||||
installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
|
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
|
# We use grep as ms-python.python may have dependency extensions that change.
|
||||||
ms-python.python" ]]; then
|
if ! echo "$installed_extensions" | grep -q "ms-python.python"; then
|
||||||
echo "Unexpected output from listing extensions:"
|
echo "Unexpected output from listing extensions:"
|
||||||
echo "$installed_extensions"
|
echo "$installed_extensions"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ main() {
|
|||||||
|
|
||||||
cd ./lib/vscode
|
cd ./lib/vscode
|
||||||
git add -A
|
git add -A
|
||||||
git diff HEAD > ../../ci/dev/vscode.patch
|
git diff HEAD --full-index > ../../ci/dev/vscode.patch
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -19,13 +19,16 @@ main() {
|
|||||||
"*.yaml"
|
"*.yaml"
|
||||||
"*.yml"
|
"*.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 '# FAQ' doc/FAQ.md > /dev/null
|
||||||
doctoc --title '# Setup Guide' doc/guide.md > /dev/null
|
doctoc --title '# Setup Guide' doc/guide.md > /dev/null
|
||||||
doctoc --title '# Install' doc/install.md > /dev/null
|
doctoc --title '# Install' doc/install.md > /dev/null
|
||||||
doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null
|
doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null
|
||||||
doctoc --title '# Contributing' doc/CONTRIBUTING.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
|
if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
|
||||||
echo "Files need generation or are formatted incorrectly:"
|
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() {
|
main() {
|
||||||
cd "$(dirname "$0")/../../.."
|
cd "$(dirname "$0")/../../.."
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
mkdir -p .home
|
||||||
|
|
||||||
docker run \
|
docker run \
|
||||||
-it \
|
-it \
|
||||||
--rm \
|
--rm \
|
||||||
-v "$PWD:/src" \
|
-v "$PWD:/src" \
|
||||||
|
-e HOME="/src/.home" \
|
||||||
|
-e USER="coder" \
|
||||||
|
-e GITHUB_TOKEN \
|
||||||
|
-e KEEP_MODULES \
|
||||||
|
-e MINIFY \
|
||||||
-w /src \
|
-w /src \
|
||||||
-p 127.0.0.1:8080:8080 \
|
-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")
|
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
|
||||||
stylelint $(git ls-files "*.css")
|
stylelint $(git ls-files "*.css")
|
||||||
tsc --noEmit
|
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
|
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 "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ set -euo pipefail
|
|||||||
main() {
|
main() {
|
||||||
cd "$(dirname "$0")/../.."
|
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 "$@"
|
main "$@"
|
||||||
|
|||||||
1635
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
|
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/')" && \
|
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 && \
|
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"
|
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 epel-release && yum install -y jq
|
||||||
RUN yum install -y rsync
|
RUN yum install -y rsync
|
||||||
|
|
||||||
# Copied from ../debian8/Dockerfile
|
# Copied from ../debian10/Dockerfile
|
||||||
# Install Go dependencies
|
# Install Go.
|
||||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
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
|
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
|
ENV GO111MODULE=on
|
||||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
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
|
RUN curl -fsSL https://get.docker.com | sh
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:8
|
FROM debian:10
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
|
|
||||||
@@ -24,30 +24,31 @@ RUN apt-get install -y build-essential \
|
|||||||
RUN apt-get install -y gettext-base
|
RUN apt-get install -y gettext-base
|
||||||
|
|
||||||
# Misc build dependencies.
|
# Misc build dependencies.
|
||||||
RUN apt-get install -y git rsync unzip
|
RUN apt-get install -y git rsync unzip jq
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Installs shellcheck.
|
# 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 | \
|
||||||
#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
tar -xJ && \
|
||||||
# tar -xJ && \
|
mv shellcheck*/shellcheck /usr/local/bin && \
|
||||||
# mv shellcheck*/shellcheck /usr/local/bin && \
|
rm -R shellcheck*
|
||||||
# rm -R shellcheck*
|
|
||||||
|
|
||||||
# Install Go dependencies
|
# Install Go.
|
||||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
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
|
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
|
ENV GO111MODULE=on
|
||||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
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
|
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
|
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
|
||||||
|
|
||||||
EXPOSE 8080
|
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
|
WORKDIR /home/coder
|
||||||
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
set -eu
|
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
|
if [ "${DOCKER_USER-}" ]; then
|
||||||
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
||||||
sudo usermod --login "$DOCKER_USER" \
|
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
|
||||||
--move-home --home "/home/$DOCKER_USER" \
|
# nor can we bind mount $HOME into a new home as that requires a privileged container.
|
||||||
coder
|
sudo usermod --login "$DOCKER_USER" coder
|
||||||
sudo groupmod -n "$DOCKER_USER" coder
|
sudo groupmod -n "$DOCKER_USER" coder
|
||||||
|
|
||||||
|
USER="$DOCKER_USER"
|
||||||
|
|
||||||
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
|
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
|
fi
|
||||||
|
|
||||||
# This isn't set by default.
|
dumb-init /usr/bin/code-server "$@"
|
||||||
export USER="$(whoami)"
|
|
||||||
dumb-init fixuid -q /usr/bin/code-server "$@"
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ main() {
|
|||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
|
||||||
git submodule update --init
|
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
|
# If the patch fails to apply, then it's likely already applied
|
||||||
yarn vscode:patch &> /dev/null || true
|
yarn vscode:patch &> /dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ main() {
|
|||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
# We do not `yarn vscode` to make test.sh faster.
|
# We need to fetch VS Code's deps for lint dependencies.
|
||||||
# If the patch fails to apply, then it's likely already applied
|
yarn vscode
|
||||||
yarn vscode:patch &> /dev/null || true
|
|
||||||
|
|
||||||
yarn lint
|
yarn lint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
main() {
|
main() {
|
||||||
cd "$(dirname "$0")/../.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
NODE_VERSION=v12.18.3
|
NODE_VERSION=v12.18.4
|
||||||
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||||
NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
|
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
|
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)
|
- [Build](#build)
|
||||||
- [Structure](#structure)
|
- [Structure](#structure)
|
||||||
- [VS Code Patch](#vs-code-patch)
|
- [VS Code Patch](#vs-code-patch)
|
||||||
|
- [Currently Known Issues](#currently-known-issues)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
@@ -15,24 +16,26 @@
|
|||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
Please link to the issue each PR solves.
|
Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue
|
||||||
If there is no existing issue, please first create one unless the fix is minor.
|
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
|
In your Pull Requests (PR), link to the issue that the PR solves.
|
||||||
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
|
Please ensure that the base of your PR is the **master** branch. (Note: The default
|
||||||
features.
|
GitHub branch is the latest release branch, though you should point all of your changes to be merged into
|
||||||
|
master).
|
||||||
|
|
||||||
## Requirements
|
## 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.
|
The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all
|
||||||
- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
of the dependencies code-server uses.
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
@@ -40,48 +43,48 @@ Differences:
|
|||||||
yarn
|
yarn
|
||||||
yarn vscode
|
yarn vscode
|
||||||
yarn watch
|
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
|
```shell
|
||||||
./ci/dev/image/exec.sh yarn
|
./ci/dev/image/run.sh yarn
|
||||||
./ci/dev/image/exec.sh yarn vscode
|
./ci/dev/image/run.sh yarn vscode
|
||||||
./ci/dev/image/exec.sh yarn watch
|
./ci/dev/image/run.sh yarn watch
|
||||||
```
|
```
|
||||||
|
|
||||||
`yarn watch` will live reload changes to the source.
|
`yarn watch` will live reload changes to the source.
|
||||||
|
|
||||||
If changes are made to the patch and you've built previously you must manually
|
If you introduce changes to the patch and you've previously built, you
|
||||||
reset VS Code then run `yarn vscode:patch`.
|
must (1) manually reset VS Code and (2) run `yarn vscode:patch`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
You can build with:
|
You can build using:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./ci/steps/release.sh
|
./ci/dev/image/run.sh ./ci/steps/release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Run your build with:
|
Run your build with:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
cd release
|
cd release
|
||||||
yarn --production
|
yarn --production
|
||||||
# Runs the built JavaScript with Node.
|
# Runs the built JavaScript with Node.
|
||||||
node .
|
node .
|
||||||
```
|
```
|
||||||
|
|
||||||
Build release packages (make sure you run `./ci/steps/release.sh` first):
|
Build the release packages (make sure that you run `./ci/steps/release.sh` first):
|
||||||
|
|
||||||
```
|
```shell
|
||||||
./ci/dev/image/exec.sh ./ci/steps/release-packages.sh
|
IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh
|
||||||
# The standalone release is in ./release-standalone
|
# The standalone release is in ./release-standalone
|
||||||
# .deb, .rpm and the standalone archive are in ./release-packages
|
# .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
|
```shell
|
||||||
yarn
|
yarn
|
||||||
@@ -91,66 +94,69 @@ yarn build:vscode
|
|||||||
yarn release
|
yarn release
|
||||||
```
|
```
|
||||||
|
|
||||||
And `release-packages.sh` is:
|
And `release-packages.sh` is equal to:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
yarn release:standalone
|
yarn release:standalone
|
||||||
yarn test:standalone-release
|
yarn test:standalone-release
|
||||||
yarn package
|
yarn package
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a faster release build, you can run instead:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
KEEP_MODULES=1 ./ci/steps/release.sh
|
||||||
|
node ./release
|
||||||
|
```
|
||||||
|
|
||||||
## Structure
|
## 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
|
The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in
|
||||||
[./src/node/app](./src/node/app).
|
[./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
|
### VS Code Patch
|
||||||
|
|
||||||
Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase
|
In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end
|
||||||
into a frontend and server. The frontend consisted of all UI code and the server ran
|
and a server. The front-end consisted of all UI code, while the server ran the extensions
|
||||||
the extensions and exposed an API to the frontend for file access and everything else
|
and exposed an API to the front-end for file access and all UI needs.
|
||||||
that the UI needed.
|
|
||||||
|
|
||||||
This worked but eventually Microsoft added support to VS Code to run it in the web.
|
Over time, Microsoft added support to VS Code to run it on the web. They have made
|
||||||
They have open sourced the frontend but have kept the server closed source.
|
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
|
||||||
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
|
|
||||||
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
|
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
|
||||||
|
|
||||||
Other notable changes in our patch include:
|
Other notable changes in our patch include:
|
||||||
|
|
||||||
- Add our own build file which includes our code and VS Code's web code.
|
- Adding our build file, which includes our code and VS Code's web code
|
||||||
- Allow multiple extension directories (both user and built-in).
|
- Allowing multiple extension directories (both user and built-in)
|
||||||
- Modify the loader, websocket, webview, service worker, and asset requests to
|
- 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).
|
use the URL of the page as a base (and TLS, if necessary for the websocket)
|
||||||
- Send client-side telemetry through the server.
|
- Sending client-side telemetry through the server
|
||||||
- Allow modification of the display language.
|
- Allowing modification of the display language
|
||||||
- Make it possible for us to load code on the client.
|
- Making it possible for us to load code on the client
|
||||||
- Make extensions work in the browser.
|
- Making extensions work in the browser
|
||||||
- Make it possible to install extensions of any kind.
|
- Making it possible to install extensions of any kind
|
||||||
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
- Fixing issue with getting disconnected when your machine sleeps or hibernates
|
||||||
- Add connection type to web socket query parameters.
|
- Adding connection type to web socket query parameters
|
||||||
|
|
||||||
Some known issues presently:
|
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
|
||||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
us to ensure that the patch is applied and works as intended. In the future,
|
||||||
- Extension profiling and tips are currently disabled.
|
we'd like to run VS Code unit tests against our builds to ensure that features
|
||||||
|
|
||||||
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
|
|
||||||
work as expected.
|
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
|
# FAQ
|
||||||
|
|
||||||
- [Questions?](#questions)
|
- [Questions?](#questions)
|
||||||
|
- [iPad Status?](#ipad-status)
|
||||||
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
|
- [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)
|
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
|
||||||
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
|
- [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 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)
|
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
|
||||||
- [Heartbeat File](#heartbeat-file)
|
- [Heartbeat File](#heartbeat-file)
|
||||||
|
- [Healthz endpoint](#healthz-endpoint)
|
||||||
- [How does the config file work?](#how-does-the-config-file-work)
|
- [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)
|
- [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)
|
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
|
||||||
- [Differences compared to Theia?](#differences-compared-to-theia)
|
- [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.
|
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?
|
## 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.
|
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
|
pass in an existing certificate by providing the path to `--cert` and the path to
|
||||||
the key with `--cert-key`.
|
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
|
If `code-server` has been passed a certificate it will also respond to HTTPS
|
||||||
requests and will redirect all HTTP requests 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.
|
[#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?
|
## 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
|
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`.
|
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?
|
## Isn't an install script piped into sh insecure?
|
||||||
|
|
||||||
Please give
|
Please give
|
||||||
|
|||||||
@@ -251,8 +251,7 @@ Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
|
|||||||
|
|
||||||
### Self Signed Certificate
|
### Self Signed Certificate
|
||||||
|
|
||||||
**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
|
**note:** Self signed certificates do not work with iPad normally. See [./ipad.md](./ipad.md) for details.
|
||||||
have to use [Let's Encrypt](#lets-encrypt) instead. See the [FAQ](./FAQ.md#blank-screen-on-ipad).
|
|
||||||
|
|
||||||
Recommended reading: https://security.stackexchange.com/a/8112.
|
Recommended reading: https://security.stackexchange.com/a/8112.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
# Install
|
# Install
|
||||||
|
|
||||||
|
- [Upgrading](#upgrading)
|
||||||
- [install.sh](#installsh)
|
- [install.sh](#installsh)
|
||||||
- [Flags](#flags)
|
- [Flags](#flags)
|
||||||
- [Detection Reference](#detection-reference)
|
- [Detection Reference](#detection-reference)
|
||||||
@@ -12,12 +13,19 @@
|
|||||||
- [macOS](#macos)
|
- [macOS](#macos)
|
||||||
- [Standalone Releases](#standalone-releases)
|
- [Standalone Releases](#standalone-releases)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
|
- [helm](#helm)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
This document demonstrates how to install `code-server` on
|
This document demonstrates how to install `code-server` on
|
||||||
various distros and operating systems.
|
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
|
## install.sh
|
||||||
|
|
||||||
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
|
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
|
## Debian, Ubuntu
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/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.5.0_amd64.deb
|
sudo dpkg -i code-server_3.7.4_amd64.deb
|
||||||
sudo systemctl enable --now code-server@$USER
|
sudo systemctl enable --now code-server@$USER
|
||||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
# 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
|
## Fedora, CentOS, RHEL, SUSE
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/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.5.0-amd64.rpm
|
sudo rpm -i code-server-3.7.4-amd64.rpm
|
||||||
sudo systemctl enable --now code-server@$USER
|
sudo systemctl enable --now code-server@$USER
|
||||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
# 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
|
```bash
|
||||||
mkdir -p ~/.local/lib ~/.local/bin
|
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
|
| tar -C ~/.local/lib -xz
|
||||||
mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0
|
mv ~/.local/lib/code-server-3.7.4-linux-amd64 ~/.local/lib/code-server-3.7.4
|
||||||
ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server
|
ln -s ~/.local/lib/code-server-3.7.4/bin/code-server ~/.local/bin/code-server
|
||||||
PATH="~/.local/bin:$PATH"
|
PATH="~/.local/bin:$PATH"
|
||||||
code-server
|
code-server
|
||||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
# 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
|
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
|
||||||
# outside the container.
|
# outside the container.
|
||||||
mkdir -p ~/.config
|
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 "$HOME/.config:/home/coder/.config" \
|
||||||
-v "$PWD:/home/coder/project" \
|
-v "$PWD:/home/coder/project" \
|
||||||
-u "$(id -u):$(id -g)" \
|
-u "$(id -u):$(id -g)" \
|
||||||
|
-e "DOCKER_USER=$USER" \
|
||||||
codercom/code-server:latest
|
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:
|
For `arm32` support there is a popular community maintained alternative:
|
||||||
|
|
||||||
https://hub.docker.com/r/linuxserver/code-server
|
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.
|
Installs code-server for Linux, macOS and FreeBSD.
|
||||||
It tries to use the system package manager if possible.
|
It tries to use the system package manager if possible.
|
||||||
After successful installation it explains how to start using code-server.
|
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-}
|
${not_curl_usage-}
|
||||||
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
|
--dry-run
|
||||||
Echo the commands for the install process without running them.
|
Echo the commands for the install process without running them.
|
||||||
|
|
||||||
--version X.X.X
|
--version X.X.X
|
||||||
Install a specific version instead of the latest.
|
Install a specific version instead of the latest.
|
||||||
|
|
||||||
--method [detect | standalone]
|
--method [detect | standalone]
|
||||||
Choose the installation method. Defaults to detect.
|
Choose the installation method. Defaults to detect.
|
||||||
- detect detects the system package manager and tries to use it.
|
- detect detects the system package manager and tries to use it.
|
||||||
Full reference on the process is further below.
|
Full reference on the process is further below.
|
||||||
- standalone installs a standalone release archive into ~/.local
|
- standalone installs a standalone release archive into ~/.local
|
||||||
Add ~/.local/bin to your \$PATH to use it.
|
Add ~/.local/bin to your \$PATH to use it.
|
||||||
|
|
||||||
--prefix <dir>
|
--prefix <dir>
|
||||||
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
||||||
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
||||||
and the binary symlinked into ~/.local/bin/code-server
|
and the binary symlinked into ~/.local/bin/code-server
|
||||||
To install system wide pass ---prefix=/usr/local
|
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 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 Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
||||||
- For Arch Linux it will install the AUR package.
|
- For Arch Linux it will install the AUR package.
|
||||||
@@ -100,9 +110,19 @@ main() {
|
|||||||
METHOD \
|
METHOD \
|
||||||
STANDALONE_INSTALL_PREFIX \
|
STANDALONE_INSTALL_PREFIX \
|
||||||
VERSION \
|
VERSION \
|
||||||
OPTIONAL
|
OPTIONAL \
|
||||||
|
ALL_FLAGS \
|
||||||
|
RSH_ARGS \
|
||||||
|
RSH
|
||||||
|
|
||||||
|
ALL_FLAGS=""
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-*)
|
||||||
|
ALL_FLAGS="${ALL_FLAGS} $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--dry-run)
|
--dry-run)
|
||||||
DRY_RUN=1
|
DRY_RUN=1
|
||||||
@@ -128,20 +148,45 @@ main() {
|
|||||||
--version=*)
|
--version=*)
|
||||||
VERSION="$(parse_arg "$@")"
|
VERSION="$(parse_arg "$@")"
|
||||||
;;
|
;;
|
||||||
|
--rsh)
|
||||||
|
RSH="$(parse_arg "$@")"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--rsh=*)
|
||||||
|
RSH="$(parse_arg "$@")"
|
||||||
|
;;
|
||||||
-h | --h | -help | --help)
|
-h | --h | -help | --help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
--)
|
||||||
|
shift
|
||||||
|
# We remove the -- added above.
|
||||||
|
ALL_FLAGS="${ALL_FLAGS% --}"
|
||||||
|
RSH_ARGS="$*"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
echoerr "Unknown flag $1"
|
echoerr "Unknown flag $1"
|
||||||
echoerr "Run with --help to see usage."
|
echoerr "Run with --help to see usage."
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
|
*)
|
||||||
|
RSH_ARGS="$*"
|
||||||
|
break
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
shift
|
shift
|
||||||
done
|
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)}"
|
VERSION="${VERSION-$(echo_latest_version)}"
|
||||||
METHOD="${METHOD-detect}"
|
METHOD="${METHOD-detect}"
|
||||||
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
||||||
@@ -446,7 +491,7 @@ arch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
sh_c() {
|
sh_c() {
|
||||||
@@ -500,4 +545,15 @@ humanpath() {
|
|||||||
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
|
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 "$@"
|
main "$@"
|
||||||
|
|||||||
20
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "code-server",
|
"name": "code-server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "3.5.0",
|
"version": "3.7.4",
|
||||||
"description": "Run VS Code on a remote server.",
|
"description": "Run VS Code on a remote server.",
|
||||||
"homepage": "https://github.com/cdr/code-server",
|
"homepage": "https://github.com/cdr/code-server",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@@ -26,10 +26,14 @@
|
|||||||
"lint": "./ci/dev/lint.sh",
|
"lint": "./ci/dev/lint.sh",
|
||||||
"test": "./ci/dev/test.sh",
|
"test": "./ci/dev/test.sh",
|
||||||
"ci": "./ci/dev/ci.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",
|
"main": "out/node/entry.js",
|
||||||
"devDependencies": {
|
"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/fs-extra": "^8.0.1",
|
||||||
"@types/http-proxy": "^1.17.4",
|
"@types/http-proxy": "^1.17.4",
|
||||||
"@types/js-yaml": "^3.12.3",
|
"@types/js-yaml": "^3.12.3",
|
||||||
@@ -39,11 +43,13 @@
|
|||||||
"@types/pem": "^1.9.5",
|
"@types/pem": "^1.9.5",
|
||||||
"@types/safe-compare": "^1.1.0",
|
"@types/safe-compare": "^1.1.0",
|
||||||
"@types/semver": "^7.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-fs": "^2.0.0",
|
||||||
"@types/tar-stream": "^2.1.0",
|
"@types/tar-stream": "^2.1.0",
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||||
"@typescript-eslint/parser": "^3.10.1",
|
"@typescript-eslint/parser": "^4.7.0",
|
||||||
"doctoc": "^1.4.0",
|
"doctoc": "^1.4.0",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-config-prettier": "^6.0.0",
|
"eslint-config-prettier": "^6.0.0",
|
||||||
@@ -55,6 +61,7 @@
|
|||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"stylelint": "^13.0.0",
|
"stylelint": "^13.0.0",
|
||||||
"stylelint-config-recommended": "^3.0.0",
|
"stylelint-config-recommended": "^3.0.0",
|
||||||
|
"supertest": "^6.0.1",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
"typescript": "4.0.2"
|
"typescript": "4.0.2"
|
||||||
},
|
},
|
||||||
@@ -65,17 +72,22 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coder/logger": "1.1.16",
|
"@coder/logger": "1.1.16",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
"env-paths": "^2.2.0",
|
"env-paths": "^2.2.0",
|
||||||
|
"express": "^5.0.0-alpha.8",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"http-proxy": "^1.18.0",
|
"http-proxy": "^1.18.0",
|
||||||
"httpolyglot": "^0.1.2",
|
"httpolyglot": "^0.1.2",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"limiter": "^1.1.5",
|
"limiter": "^1.1.5",
|
||||||
"pem": "^1.14.2",
|
"pem": "^1.14.2",
|
||||||
|
"qs": "6.7.0",
|
||||||
"rotating-file-stream": "^2.1.1",
|
"rotating-file-stream": "^2.1.1",
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"safe-compare": "^1.1.4",
|
"safe-compare": "^1.1.4",
|
||||||
"semver": "^7.1.3",
|
"semver": "^7.1.3",
|
||||||
|
"split2": "^3.2.2",
|
||||||
"tar": "^6.0.1",
|
"tar": "^6.0.1",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"ws": "^7.2.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",
|
"background-color": "#fff",
|
||||||
"description": "Run editors on a remote server.",
|
"description": "Run editors on a remote server.",
|
||||||
"icons": [
|
"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",
|
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"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",
|
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||||
"type": "image/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:;"
|
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
/>
|
/>
|
||||||
<title>{{ERROR_TITLE}} - code-server</title>
|
<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="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" />
|
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -37,3 +37,7 @@ body {
|
|||||||
.login-form > .field > .submit {
|
.login-form > .field > .submit {
|
||||||
margin-left: 20px;
|
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:;"
|
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>
|
<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="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" />
|
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
@@ -47,5 +49,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
<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>
|
</html>
|
||||||
|
|||||||
@@ -9,11 +9,6 @@
|
|||||||
|
|
||||||
<meta charset="utf-8" />
|
<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 -->
|
<!-- Disable pinch zooming -->
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
@@ -29,19 +24,16 @@
|
|||||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||||
|
|
||||||
<!-- Workbench Icon/Manifest/CSS -->
|
<!-- 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" />
|
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||||
<!-- PROD_ONLY
|
<!-- 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">
|
<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 -->
|
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" />
|
<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}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ try {
|
|||||||
}
|
}
|
||||||
// FIXME: Only works if path separators are /.
|
// FIXME: Only works if path separators are /.
|
||||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
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((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
bundles[bundle] = json
|
bundles[bundle] = json
|
||||||
@@ -31,7 +31,8 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
;(self.require as any) = {
|
;(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,
|
recordStats: true,
|
||||||
paths: {
|
paths: {
|
||||||
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
"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-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-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||||
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.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`,
|
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||||
jschardet: `../node_modules/jschardet/dist/jschardet.min.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`)
|
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(path, {
|
.register(path, {
|
||||||
scope: options.base || "/",
|
scope: (options.base ?? "") + "/",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("[Service Worker] registered")
|
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 {
|
export interface Disposable {
|
||||||
dispose(): void
|
dispose(): void
|
||||||
@@ -32,8 +38,21 @@ export class Emitter<T> {
|
|||||||
/**
|
/**
|
||||||
* Emit an event with a value.
|
* Emit an event with a value.
|
||||||
*/
|
*/
|
||||||
public emit(value: T): void {
|
public async emit(value: T): Promise<void> {
|
||||||
this.listeners.forEach((cb) => cb(value))
|
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 {
|
public dispose(): void {
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ export enum HttpCode {
|
|||||||
ServerError = 500,
|
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 {
|
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)
|
super(message)
|
||||||
this.name = this.constructor.name
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
292
src/node/cli.ts
@@ -4,8 +4,12 @@ import yaml from "js-yaml"
|
|||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { AuthType } from "./http"
|
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
|
||||||
import { generatePassword, humanPath, paths } from "./util"
|
|
||||||
|
export enum AuthType {
|
||||||
|
Password = "password",
|
||||||
|
None = "none",
|
||||||
|
}
|
||||||
|
|
||||||
export class Optional<T> {
|
export class Optional<T> {
|
||||||
public constructor(public readonly value?: T) {}
|
public constructor(public readonly value?: T) {}
|
||||||
@@ -22,31 +26,35 @@ export enum LogLevel {
|
|||||||
export class OptionalString extends Optional<string> {}
|
export class OptionalString extends Optional<string> {}
|
||||||
|
|
||||||
export interface Args extends VsArgs {
|
export interface Args extends VsArgs {
|
||||||
readonly config?: string
|
config?: string
|
||||||
readonly auth?: AuthType
|
auth?: AuthType
|
||||||
readonly password?: string
|
password?: string
|
||||||
readonly cert?: OptionalString
|
cert?: OptionalString
|
||||||
readonly "cert-key"?: string
|
"cert-host"?: string
|
||||||
readonly "disable-telemetry"?: boolean
|
"cert-key"?: string
|
||||||
readonly help?: boolean
|
"disable-telemetry"?: boolean
|
||||||
readonly host?: string
|
"disable-update-check"?: boolean
|
||||||
readonly json?: boolean
|
help?: boolean
|
||||||
|
host?: string
|
||||||
|
json?: boolean
|
||||||
log?: LogLevel
|
log?: LogLevel
|
||||||
readonly open?: boolean
|
open?: boolean
|
||||||
readonly port?: number
|
port?: number
|
||||||
readonly "bind-addr"?: string
|
"bind-addr"?: string
|
||||||
readonly socket?: string
|
socket?: string
|
||||||
readonly version?: boolean
|
version?: boolean
|
||||||
readonly force?: boolean
|
force?: boolean
|
||||||
readonly "list-extensions"?: boolean
|
"list-extensions"?: boolean
|
||||||
readonly "install-extension"?: string[]
|
"install-extension"?: string[]
|
||||||
readonly "show-versions"?: boolean
|
"show-versions"?: boolean
|
||||||
readonly "uninstall-extension"?: string[]
|
"uninstall-extension"?: string[]
|
||||||
readonly "proxy-domain"?: string[]
|
"proxy-domain"?: string[]
|
||||||
readonly locale?: string
|
locale?: string
|
||||||
readonly _: string[]
|
_: string[]
|
||||||
readonly "reuse-window"?: boolean
|
"reuse-window"?: boolean
|
||||||
readonly "new-window"?: boolean
|
"new-window"?: boolean
|
||||||
|
|
||||||
|
link?: OptionalString
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option<T> {
|
interface Option<T> {
|
||||||
@@ -63,6 +71,11 @@ interface Option<T> {
|
|||||||
* Description of the option. Leave blank to hide the option.
|
* Description of the option. Leave blank to hide the option.
|
||||||
*/
|
*/
|
||||||
description?: string
|
description?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If marked as beta, the option is not printed unless $CS_BETA is set.
|
||||||
|
*/
|
||||||
|
beta?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionType<T> = T extends boolean
|
type OptionType<T> = T extends boolean
|
||||||
@@ -94,10 +107,20 @@ const options: Options<Required<Args>> = {
|
|||||||
cert: {
|
cert: {
|
||||||
type: OptionalString,
|
type: OptionalString,
|
||||||
path: true,
|
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." },
|
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||||
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
"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." },
|
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||||
json: { type: "boolean" },
|
json: { type: "boolean" },
|
||||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||||
@@ -130,7 +153,8 @@ const options: Options<Required<Args>> = {
|
|||||||
"install-extension": {
|
"install-extension": {
|
||||||
type: "string[]",
|
type: "string[]",
|
||||||
description:
|
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": {
|
"enable-proposed-api": {
|
||||||
type: "string[]",
|
type: "string[]",
|
||||||
@@ -144,17 +168,29 @@ const options: Options<Required<Args>> = {
|
|||||||
"new-window": {
|
"new-window": {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
short: "n",
|
short: "n",
|
||||||
description: "Force to open a new window. (use with open-in)",
|
description: "Force to open a new window.",
|
||||||
},
|
},
|
||||||
"reuse-window": {
|
"reuse-window": {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
short: "r",
|
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" },
|
locale: { type: "string" },
|
||||||
log: { type: LogLevel },
|
log: { type: LogLevel },
|
||||||
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
|
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[] => {
|
export const optionDescriptions = (): string[] => {
|
||||||
@@ -166,12 +202,32 @@ export const optionDescriptions = (): string[] => {
|
|||||||
}),
|
}),
|
||||||
{ short: 0, long: 0 },
|
{ short: 0, long: 0 },
|
||||||
)
|
)
|
||||||
return entries.map(
|
return entries
|
||||||
([k, v]) =>
|
.filter(([, v]) => {
|
||||||
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
|
// If CS_BETA is set, we show beta options but if not, then we do not want
|
||||||
widths.long - k.length,
|
// to show beta options.
|
||||||
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
|
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 = (
|
export const parse = (
|
||||||
@@ -285,7 +341,46 @@ export const parse = (
|
|||||||
args._.push(arg)
|
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
|
// --verbose takes priority over --log and --log takes priority over the
|
||||||
// environment variable.
|
// environment variable.
|
||||||
@@ -326,22 +421,49 @@ export const parse = (
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
// Default to using a password.
|
||||||
|
if (!args.auth) {
|
||||||
|
args.auth = AuthType.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setDefaults(args: Args): Promise<Args> {
|
const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs)
|
||||||
args = { ...args }
|
args.host = addr.host
|
||||||
|
args.port = addr.port
|
||||||
|
|
||||||
if (!args["user-data-dir"]) {
|
// If we're being exposed to the cloud, we listen on a random address and
|
||||||
await copyOldMacOSDataDir()
|
// disable auth.
|
||||||
args["user-data-dir"] = paths.data
|
if (args.link) {
|
||||||
|
args.host = "localhost"
|
||||||
|
args.port = 0
|
||||||
|
args.socket = undefined
|
||||||
|
args.cert = undefined
|
||||||
|
args.auth = AuthType.None
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args["extensions-dir"]) {
|
if (args.cert && !args.cert.value) {
|
||||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
const { cert, certKey } = await generateCertificate(args["cert-host"] || "localhost")
|
||||||
|
args.cert = {
|
||||||
|
value: cert,
|
||||||
|
}
|
||||||
|
args["cert-key"] = certKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
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> {
|
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.
|
* 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.
|
* @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) {
|
if (!configPath) {
|
||||||
configPath = process.env.CODE_SERVER_CONFIG
|
configPath = process.env.CODE_SERVER_CONFIG
|
||||||
if (!configPath) {
|
if (!configPath) {
|
||||||
@@ -370,10 +496,6 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
|||||||
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
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 configFile = await fs.readFile(configPath)
|
||||||
const config = yaml.safeLoad(configFile.toString(), {
|
const config = yaml.safeLoad(configFile.toString(), {
|
||||||
filename: configPath,
|
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}`)
|
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 {
|
interface Addr {
|
||||||
@@ -412,7 +540,7 @@ interface Addr {
|
|||||||
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||||
addr = { ...addr }
|
addr = { ...addr }
|
||||||
if (args["bind-addr"]) {
|
if (args["bind-addr"]) {
|
||||||
;[addr.host, addr.port] = parseBindAddr(args["bind-addr"])
|
addr = parseBindAddr(args["bind-addr"])
|
||||||
}
|
}
|
||||||
if (args.host) {
|
if (args.host) {
|
||||||
addr.host = args.host
|
addr.host = args.host
|
||||||
@@ -427,16 +555,17 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
|||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] {
|
function bindAddrFromAllSources(...argsConfig: Args[]): Addr {
|
||||||
let addr: Addr = {
|
let addr: Addr = {
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = bindAddrFromArgs(addr, configArgs)
|
for (const args of argsConfig) {
|
||||||
addr = bindAddrFromArgs(addr, cliArgs)
|
addr = bindAddrFromArgs(addr, args)
|
||||||
|
}
|
||||||
|
|
||||||
return [addr.host, addr.port]
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyOldMacOSDataDir(): Promise<void> {
|
async function copyOldMacOSDataDir(): Promise<void> {
|
||||||
@@ -453,3 +582,52 @@ async function copyOldMacOSDataDir(): Promise<void> {
|
|||||||
await fs.copy(oldDataDir, paths.data)
|
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 { field, logger } from "@coder/logger"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import { promises as fs } from "fs"
|
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
|
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { plural } from "../common/util"
|
import { plural } from "../common/util"
|
||||||
import { LoginHttpProvider } from "./app/login"
|
import { createApp, ensureAddress } from "./app"
|
||||||
import { ProxyHttpProvider } from "./app/proxy"
|
import {
|
||||||
import { StaticHttpProvider } from "./app/static"
|
AuthType,
|
||||||
import { UpdateHttpProvider } from "./app/update"
|
DefaultedArgs,
|
||||||
import { VscodeHttpProvider } from "./app/vscode"
|
optionDescriptions,
|
||||||
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
|
parse,
|
||||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
readConfigFile,
|
||||||
import { loadPlugins } from "./plugin"
|
setDefaults,
|
||||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
shouldOpenInExistingInstance,
|
||||||
import { ipcMain, wrap } from "./wrapper"
|
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) => {
|
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
||||||
logger.error(`Uncaught exception: ${error.message}`)
|
logger.debug("forking vs code cli...")
|
||||||
if (typeof error.stack !== "undefined") {
|
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
||||||
logger.error(error.stack)
|
env: {
|
||||||
}
|
...process.env,
|
||||||
|
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
vscode.once("message", (message: any) => {
|
||||||
let pkg: { version?: string; commit?: string } = {}
|
logger.debug("got message from VS Code", field("message", message))
|
||||||
try {
|
if (message.type !== "ready") {
|
||||||
pkg = require("../../package.json")
|
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||||
} catch (error) {
|
process.exit(1)
|
||||||
logger.warn(error.message)
|
}
|
||||||
|
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"
|
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
||||||
const commit = pkg.commit || "development"
|
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> => {
|
for (let i = 0; i < args._.length; i++) {
|
||||||
if (!args.auth) {
|
const fp = path.resolve(args._[i])
|
||||||
args = {
|
if (await isFile(fp)) {
|
||||||
...args,
|
pipeArgs.fileURIs.push(fp)
|
||||||
auth: AuthType.Password,
|
} else {
|
||||||
|
pipeArgs.folderURIs.push(fp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||||
|
|
||||||
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
||||||
|
|
||||||
const envPassword = !!process.env.PASSWORD
|
if (args.auth === AuthType.Password && !args.password) {
|
||||||
const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password)
|
|
||||||
if (args.auth === AuthType.Password && !password) {
|
|
||||||
throw new Error("Please pass in a password via the config file or $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 [app, wsApp, server] = await createApp(args)
|
||||||
const options: HttpServerOptions = {
|
const serverAddress = ensureAddress(server)
|
||||||
auth: args.auth,
|
await register(app, wsApp, server, args)
|
||||||
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"],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.cert && !options.certKey) {
|
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||||
throw new Error("--cert-key is missing")
|
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
|
||||||
}
|
|
||||||
|
|
||||||
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}`)
|
|
||||||
|
|
||||||
if (args.auth === AuthType.Password) {
|
if (args.auth === AuthType.Password) {
|
||||||
if (envPassword) {
|
logger.info(" - Authentication is enabled")
|
||||||
|
if (args.usingEnvPassword) {
|
||||||
logger.info(" - Using password from $PASSWORD")
|
logger.info(" - Using password from $PASSWORD")
|
||||||
} else {
|
} else {
|
||||||
logger.info(` - Using password from ${humanPath(args.config)}`)
|
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||||
}
|
}
|
||||||
logger.info(" - To disable use `--auth none`")
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(" - No authentication")
|
logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
|
||||||
}
|
}
|
||||||
delete process.env.PASSWORD
|
|
||||||
|
|
||||||
if (httpServer.protocol === "https") {
|
if (args.cert) {
|
||||||
logger.info(
|
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
|
||||||
args.cert && args.cert.value
|
|
||||||
? ` - Using provided certificate and key for HTTPS`
|
|
||||||
: ` - Using generated certificate and key for HTTPS`,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(" - Not serving HTTPS")
|
logger.info(" - Not serving HTTPS")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (httpServer.proxyDomains.size > 0) {
|
if (args["proxy-domain"].length > 0) {
|
||||||
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
|
logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`)
|
||||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${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.
|
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
const openAddress = serverAddress.replace("://0.0.0.0", "://localhost")
|
||||||
await open(openAddress).catch(console.error)
|
try {
|
||||||
|
await open(openAddress)
|
||||||
logger.info(`Opened ${openAddress}`)
|
logger.info(`Opened ${openAddress}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function entry(): Promise<void> {
|
async function entry(): Promise<void> {
|
||||||
const tryParse = async (): Promise<[Args, Args, Args]> => {
|
// There's no need to check flags like --help or to spawn in an existing
|
||||||
try {
|
// instance for the child process because these would have already happened in
|
||||||
const cliArgs = parse(process.argv.slice(2))
|
// the parent and the child wouldn't have been spawned. We also get the
|
||||||
const configArgs = await readConfigFile(cliArgs.config)
|
// arguments from the parent so we don't have to parse twice and to account
|
||||||
// This prioritizes the flags set in args over the ones in the config file.
|
// for environment manipulation (like how PASSWORD gets removed to avoid
|
||||||
let args = Object.assign(configArgs, cliArgs)
|
// leaking to child processes).
|
||||||
args = await setDefaults(args)
|
if (isChild(wrapper)) {
|
||||||
return [args, cliArgs, configArgs]
|
const args = await wrapper.handshake()
|
||||||
} catch (error) {
|
wrapper.preventExit()
|
||||||
console.error(error.message)
|
return main(args)
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (args.help) {
|
||||||
console.log("code-server", version, commit)
|
console.log("code-server", version, commit)
|
||||||
console.log("")
|
console.log("")
|
||||||
@@ -153,7 +179,10 @@ async function entry(): Promise<void> {
|
|||||||
optionDescriptions().forEach((description) => {
|
optionDescriptions().forEach((description) => {
|
||||||
console.log("", description)
|
console.log("", description)
|
||||||
})
|
})
|
||||||
} else if (args.version) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.version) {
|
||||||
if (args.json) {
|
if (args.json) {
|
||||||
console.log({
|
console.log({
|
||||||
codeServer: version,
|
codeServer: version,
|
||||||
@@ -163,83 +192,22 @@ async function entry(): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
console.log(version, commit)
|
console.log(version, commit)
|
||||||
}
|
}
|
||||||
process.exit(0)
|
return
|
||||||
} 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry()
|
if (shouldRunVsCodeCli(args)) {
|
||||||
|
return runVsCodeCli(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketPath = await shouldOpenInExistingInstance(cliArgs)
|
||||||
|
if (socketPath) {
|
||||||
|
return openInExistingInstance(args, socketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper.start(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
939
src/node/http.ts
@@ -1,913 +1,144 @@
|
|||||||
import { field, logger } from "@coder/logger"
|
import { field, logger } from "@coder/logger"
|
||||||
import * as fs from "fs-extra"
|
import * as express from "express"
|
||||||
import * as http from "http"
|
import * as expressCore from "express-serve-static-core"
|
||||||
import proxy from "http-proxy"
|
import qs from "qs"
|
||||||
import * as httpolyglot from "httpolyglot"
|
|
||||||
import * as https from "https"
|
|
||||||
import * as net from "net"
|
|
||||||
import * as path from "path"
|
|
||||||
import * as querystring from "querystring"
|
|
||||||
import safeCompare from "safe-compare"
|
import safeCompare from "safe-compare"
|
||||||
import { Readable } from "stream"
|
|
||||||
import * as tls from "tls"
|
|
||||||
import * as url from "url"
|
|
||||||
import { HttpCode, HttpError } from "../common/http"
|
import { HttpCode, HttpError } from "../common/http"
|
||||||
import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util"
|
import { normalize, Options } from "../common/util"
|
||||||
import { SocketProxyProvider } from "./socket"
|
import { AuthType } from "./cli"
|
||||||
import { getMediaMime, paths } from "./util"
|
import { commit, rootPath } from "./constants"
|
||||||
|
import { hash } from "./util"
|
||||||
|
|
||||||
export type Cookies = { [key: string]: string[] | undefined }
|
/**
|
||||||
export type PostData = { [key: string]: string | string[] | undefined }
|
* Replace common variable strings in HTML templates.
|
||||||
|
*/
|
||||||
interface ProxyRequest extends http.IncomingMessage {
|
export const replaceTemplates = <T extends object>(
|
||||||
base?: string
|
req: express.Request,
|
||||||
|
content: string,
|
||||||
|
extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
|
||||||
|
): string => {
|
||||||
|
const base = relativeRoot(req)
|
||||||
|
const options: Options = {
|
||||||
|
base,
|
||||||
|
csStaticBase: base + "/static/" + commit + rootPath,
|
||||||
|
logLevel: logger.level,
|
||||||
|
...extraOpts,
|
||||||
}
|
}
|
||||||
|
return content
|
||||||
interface AuthPayload extends Cookies {
|
.replace(/{{TO}}/g, (typeof req.query.to === "string" && req.query.to) || "/")
|
||||||
key?: string[]
|
.replace(/{{BASE}}/g, options.base)
|
||||||
}
|
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
||||||
|
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||||
export enum AuthType {
|
|
||||||
Password = "password",
|
|
||||||
None = "none",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Query = { [key: string]: string | string[] | undefined }
|
|
||||||
|
|
||||||
export interface ProxyOptions {
|
|
||||||
/**
|
|
||||||
* A path to strip from from the beginning of the request before proxying
|
|
||||||
*/
|
|
||||||
strip?: string
|
|
||||||
/**
|
|
||||||
* A path to add to the beginning of the request before proxying.
|
|
||||||
*/
|
|
||||||
prepend?: string
|
|
||||||
/**
|
|
||||||
* The port to proxy.
|
|
||||||
*/
|
|
||||||
port: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpResponse<T = string | Buffer | object> {
|
|
||||||
/*
|
|
||||||
* Whether to set cache-control headers for this response.
|
|
||||||
*/
|
|
||||||
cache?: boolean
|
|
||||||
/**
|
|
||||||
* If the code cannot be determined automatically set it here. The
|
|
||||||
* defaults are 302 for redirects and 200 for successful requests. For errors
|
|
||||||
* you should throw an HttpError and include the code there. If you
|
|
||||||
* use Error it will default to 404 for ENOENT and EISDIR and 500 otherwise.
|
|
||||||
*/
|
|
||||||
code?: number
|
|
||||||
/**
|
|
||||||
* Content to write in the response. Mutually exclusive with stream.
|
|
||||||
*/
|
|
||||||
content?: T
|
|
||||||
/**
|
|
||||||
* Cookie to write with the response.
|
|
||||||
* NOTE: Cookie paths must be absolute. The default is /.
|
|
||||||
*/
|
|
||||||
cookie?: { key: string; value: string; path?: string }
|
|
||||||
/**
|
|
||||||
* Used to automatically determine the appropriate mime type.
|
|
||||||
*/
|
|
||||||
filePath?: string
|
|
||||||
/**
|
|
||||||
* Additional headers to include.
|
|
||||||
*/
|
|
||||||
headers?: http.OutgoingHttpHeaders
|
|
||||||
/**
|
|
||||||
* If the mime type cannot be determined automatically set it here.
|
|
||||||
*/
|
|
||||||
mime?: string
|
|
||||||
/**
|
|
||||||
* Redirect to this path. This is constructed against the site base (not the
|
|
||||||
* provider's base).
|
|
||||||
*/
|
|
||||||
redirect?: string
|
|
||||||
/**
|
|
||||||
* Stream this to the response. Mutually exclusive with content.
|
|
||||||
*/
|
|
||||||
stream?: Readable
|
|
||||||
/**
|
|
||||||
* Query variables to add in addition to current ones when redirecting. Use
|
|
||||||
* `undefined` to remove a query variable.
|
|
||||||
*/
|
|
||||||
query?: Query
|
|
||||||
/**
|
|
||||||
* Indicates the request should be proxied.
|
|
||||||
*/
|
|
||||||
proxy?: ProxyOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WsResponse {
|
|
||||||
/**
|
|
||||||
* Indicates the web socket should be proxied.
|
|
||||||
*/
|
|
||||||
proxy?: ProxyOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use when you need to run search and replace on a file's content before
|
* Throw an error if not authorized. Call `next` if provided.
|
||||||
* sending it.
|
|
||||||
*/
|
*/
|
||||||
export interface HttpStringFileResponse extends HttpResponse {
|
export const ensureAuthenticated = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => {
|
||||||
content: string
|
if (!authenticated(req)) {
|
||||||
filePath: string
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
}
|
}
|
||||||
|
if (next) {
|
||||||
export interface RedirectResponse extends HttpResponse {
|
next()
|
||||||
redirect: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpServerOptions {
|
|
||||||
readonly auth?: AuthType
|
|
||||||
readonly cert?: string
|
|
||||||
readonly certKey?: string
|
|
||||||
readonly commit?: string
|
|
||||||
readonly host?: string
|
|
||||||
readonly password?: string
|
|
||||||
readonly port?: number
|
|
||||||
readonly proxyDomains?: string[]
|
|
||||||
readonly socket?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Route {
|
|
||||||
/**
|
|
||||||
* Provider base path part (for /provider/base/path it would be /provider).
|
|
||||||
*/
|
|
||||||
providerBase: string
|
|
||||||
/**
|
|
||||||
* Base path part (for /provider/base/path it would be /base).
|
|
||||||
*/
|
|
||||||
base: string
|
|
||||||
/**
|
|
||||||
* Remaining part of the route after factoring out the base and provider base
|
|
||||||
* (for /provider/base/path it would be /path). It can be blank.
|
|
||||||
*/
|
|
||||||
requestPath: string
|
|
||||||
/**
|
|
||||||
* Query variables included in the request.
|
|
||||||
*/
|
|
||||||
query: querystring.ParsedUrlQuery
|
|
||||||
/**
|
|
||||||
* Normalized version of `originalPath`.
|
|
||||||
*/
|
|
||||||
fullPath: string
|
|
||||||
/**
|
|
||||||
* Original path of the request without any modifications.
|
|
||||||
*/
|
|
||||||
originalPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProviderRoute extends Route {
|
|
||||||
provider: HttpProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpProviderOptions {
|
|
||||||
readonly auth: AuthType
|
|
||||||
readonly commit: string
|
|
||||||
readonly password?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides HTTP responses. This abstract class provides some helpers for
|
* Return true if authenticated via cookies.
|
||||||
* interpreting, creating, and authenticating responses.
|
|
||||||
*/
|
*/
|
||||||
export abstract class HttpProvider {
|
export const authenticated = (req: express.Request): boolean => {
|
||||||
protected readonly rootPath = path.resolve(__dirname, "../..")
|
switch (req.args.auth) {
|
||||||
|
case AuthType.None:
|
||||||
public constructor(protected readonly options: HttpProviderOptions) {}
|
return true
|
||||||
|
case AuthType.Password:
|
||||||
public async dispose(): Promise<void> {
|
// The password is stored in the cookie after being hashed.
|
||||||
// No default behavior.
|
return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password))
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported auth type ${req.args.auth}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle web sockets on the registered endpoint. Normally the provider
|
* Get the relative path that will get us to the root of the page. For each
|
||||||
* handles the request itself but it can return a response when necessary. The
|
* slash we need to go up a directory. For example:
|
||||||
* default is to throw a 404.
|
|
||||||
*/
|
|
||||||
public handleWebSocket(
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
_route: Route,
|
|
||||||
_request: http.IncomingMessage,
|
|
||||||
_socket: net.Socket,
|
|
||||||
_head: Buffer,
|
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
||||||
): Promise<WsResponse | void> {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle requests to the registered endpoint.
|
|
||||||
*/
|
|
||||||
public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base relative to the provided route. For each slash we need to go
|
|
||||||
* up a directory. For example:
|
|
||||||
* / => .
|
* / => .
|
||||||
* /foo => .
|
* /foo => .
|
||||||
* /foo/ => ./..
|
* /foo/ => ./..
|
||||||
* /foo/bar => ./..
|
* /foo/bar => ./..
|
||||||
* /foo/bar/ => ./../..
|
* /foo/bar/ => ./../..
|
||||||
*/
|
*/
|
||||||
public base(route: Route): string {
|
export const relativeRoot = (req: express.Request): string => {
|
||||||
const depth = (route.originalPath.match(/\//g) || []).length
|
const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length
|
||||||
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
|
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get error response.
|
* Redirect relatively to `/${to}`. Query variables will be preserved.
|
||||||
|
* `override` will merge with the existing query (use `undefined` to unset).
|
||||||
*/
|
*/
|
||||||
public async getErrorRoot(route: Route, title: string, header: string, body: string): Promise<HttpResponse> {
|
export const redirect = (
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/error.html")
|
req: express.Request,
|
||||||
response.content = response.content
|
res: express.Response,
|
||||||
.replace(/{{ERROR_TITLE}}/g, title)
|
to: string,
|
||||||
.replace(/{{ERROR_HEADER}}/g, header)
|
override: expressCore.Query = {},
|
||||||
.replace(/{{ERROR_BODY}}/g, body)
|
): void => {
|
||||||
return this.replaceTemplates(route, response)
|
const query = Object.assign({}, req.query, override)
|
||||||
}
|
Object.keys(override).forEach((key) => {
|
||||||
|
if (typeof override[key] === "undefined") {
|
||||||
/**
|
|
||||||
* Replace common templates strings.
|
|
||||||
*/
|
|
||||||
protected replaceTemplates<T extends object>(
|
|
||||||
route: Route,
|
|
||||||
response: HttpStringFileResponse,
|
|
||||||
extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">,
|
|
||||||
): HttpStringFileResponse {
|
|
||||||
const base = this.base(route)
|
|
||||||
const options: Options = {
|
|
||||||
base,
|
|
||||||
csStaticBase: base + "/static/" + this.options.commit + this.rootPath,
|
|
||||||
logLevel: logger.level,
|
|
||||||
...extraOptions,
|
|
||||||
}
|
|
||||||
response.content = response.content
|
|
||||||
.replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard")
|
|
||||||
.replace(/{{BASE}}/g, options.base)
|
|
||||||
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
|
||||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get isDev(): boolean {
|
|
||||||
return this.options.commit === "development"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a file resource.
|
|
||||||
* TODO: Would a stream be faster, at least for large files?
|
|
||||||
*/
|
|
||||||
protected async getResource(...parts: string[]): Promise<HttpResponse> {
|
|
||||||
const filePath = path.join(...parts)
|
|
||||||
return { content: await fs.readFile(filePath), filePath }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a file resource as a string.
|
|
||||||
*/
|
|
||||||
protected async getUtf8Resource(...parts: string[]): Promise<HttpStringFileResponse> {
|
|
||||||
const filePath = path.join(...parts)
|
|
||||||
return { content: await fs.readFile(filePath, "utf8"), filePath }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to error on invalid methods (default GET).
|
|
||||||
*/
|
|
||||||
protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void {
|
|
||||||
const check = arrayify(method || "GET")
|
|
||||||
if (!request.method || !check.includes(request.method)) {
|
|
||||||
throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to error if not authorized.
|
|
||||||
*/
|
|
||||||
protected ensureAuthenticated(request: http.IncomingMessage): void {
|
|
||||||
if (!this.authenticated(request)) {
|
|
||||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the first query value or the default if there isn't one.
|
|
||||||
*/
|
|
||||||
protected queryOrDefault(value: string | string[] | undefined, def: string): string {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value = value[0]
|
|
||||||
}
|
|
||||||
return typeof value !== "undefined" ? value : def
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the provided password value if the payload contains the right
|
|
||||||
* password otherwise return false. If no payload is specified use cookies.
|
|
||||||
*/
|
|
||||||
public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
|
||||||
switch (this.options.auth) {
|
|
||||||
case AuthType.None:
|
|
||||||
return true
|
|
||||||
case AuthType.Password:
|
|
||||||
if (typeof payload === "undefined") {
|
|
||||||
payload = this.parseCookies<AuthPayload>(request)
|
|
||||||
}
|
|
||||||
if (this.options.password && payload.key) {
|
|
||||||
for (let i = 0; i < payload.key.length; ++i) {
|
|
||||||
if (safeCompare(payload.key[i], this.options.password)) {
|
|
||||||
return payload.key[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported auth type ${this.options.auth}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse POST data.
|
|
||||||
*/
|
|
||||||
protected getData(request: http.IncomingMessage): Promise<string | undefined> {
|
|
||||||
return request.method === "POST" || request.method === "DELETE"
|
|
||||||
? new Promise<string>((resolve, reject) => {
|
|
||||||
let body = ""
|
|
||||||
const onEnd = (): void => {
|
|
||||||
off() // eslint-disable-line @typescript-eslint/no-use-before-define
|
|
||||||
resolve(body || undefined)
|
|
||||||
}
|
|
||||||
const onError = (error: Error): void => {
|
|
||||||
off() // eslint-disable-line @typescript-eslint/no-use-before-define
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
const onData = (d: Buffer): void => {
|
|
||||||
body += d
|
|
||||||
if (body.length > 1e6) {
|
|
||||||
onError(new HttpError("Payload is too large", HttpCode.LargePayload))
|
|
||||||
request.connection.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const off = (): void => {
|
|
||||||
request.off("error", onError)
|
|
||||||
request.off("data", onError)
|
|
||||||
request.off("end", onEnd)
|
|
||||||
}
|
|
||||||
request.on("error", onError)
|
|
||||||
request.on("data", onData)
|
|
||||||
request.on("end", onEnd)
|
|
||||||
})
|
|
||||||
: Promise.resolve(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse cookies.
|
|
||||||
*/
|
|
||||||
protected parseCookies<T extends Cookies>(request: http.IncomingMessage): T {
|
|
||||||
const cookies: { [key: string]: string[] } = {}
|
|
||||||
if (request.headers.cookie) {
|
|
||||||
request.headers.cookie.split(";").forEach((keyValue) => {
|
|
||||||
const [key, value] = split(keyValue, "=")
|
|
||||||
if (!cookies[key]) {
|
|
||||||
cookies[key] = []
|
|
||||||
}
|
|
||||||
cookies[key].push(decodeURI(value))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return cookies as T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the route is for the root page. For example /base, /base/,
|
|
||||||
* or /base/index.html but not /base/path or /base/file.js.
|
|
||||||
*/
|
|
||||||
protected isRoot(route: Route): boolean {
|
|
||||||
return !route.requestPath || route.requestPath === "/index.html"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a heartbeat using a local file to indicate activity.
|
|
||||||
*/
|
|
||||||
export class Heart {
|
|
||||||
private heartbeatTimer?: NodeJS.Timeout
|
|
||||||
private heartbeatInterval = 60000
|
|
||||||
private lastHeartbeat = 0
|
|
||||||
|
|
||||||
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
const now = Date.now()
|
|
||||||
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
|
|
||||||
logger.trace("heartbeat")
|
|
||||||
fs.outputFile(this.heartbeatPath, "").catch((error) => {
|
|
||||||
logger.warn(error.message)
|
|
||||||
})
|
|
||||||
this.lastHeartbeat = 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpProvider0<T> {
|
|
||||||
new (options: HttpProviderOptions): T
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpProvider1<A1, T> {
|
|
||||||
new (options: HttpProviderOptions, a1: A1): T
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpProvider2<A1, A2, T> {
|
|
||||||
new (options: HttpProviderOptions, a1: A1, a2: A2): T
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpProvider3<A1, A2, A3, T> {
|
|
||||||
new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An HTTP server. Its main role is to route incoming HTTP requests to the
|
|
||||||
* appropriate provider for that endpoint then write out the response. It also
|
|
||||||
* covers some common use cases like redirects and caching.
|
|
||||||
*/
|
|
||||||
export class HttpServer {
|
|
||||||
protected readonly server: http.Server | https.Server
|
|
||||||
private listenPromise: Promise<string | null> | undefined
|
|
||||||
public readonly protocol: "http" | "https"
|
|
||||||
private readonly providers = new Map<string, HttpProvider>()
|
|
||||||
private readonly heart: Heart
|
|
||||||
private readonly socketProvider = new SocketProxyProvider()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy domains are stored here without the leading `*.`
|
|
||||||
*/
|
|
||||||
public readonly proxyDomains: Set<string>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the actual proxying functionality.
|
|
||||||
*/
|
|
||||||
private readonly proxy = proxy.createProxyServer({})
|
|
||||||
|
|
||||||
public constructor(private readonly options: HttpServerOptions) {
|
|
||||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
|
||||||
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
|
||||||
const connections = await this.getConnections()
|
|
||||||
logger.trace(plural(connections, `${connections} active connection`))
|
|
||||||
return connections !== 0
|
|
||||||
})
|
|
||||||
this.protocol = this.options.cert ? "https" : "http"
|
|
||||||
if (this.protocol === "https") {
|
|
||||||
this.server = httpolyglot.createServer(
|
|
||||||
{
|
|
||||||
cert: this.options.cert && fs.readFileSync(this.options.cert),
|
|
||||||
key: this.options.certKey && fs.readFileSync(this.options.certKey),
|
|
||||||
},
|
|
||||||
this.onRequest,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.server = http.createServer(this.onRequest)
|
|
||||||
}
|
|
||||||
this.proxy.on("error", (error, _request, response) => {
|
|
||||||
response.writeHead(HttpCode.ServerError)
|
|
||||||
response.end(error.message)
|
|
||||||
})
|
|
||||||
// Intercept the response to rewrite absolute redirects against the base path.
|
|
||||||
this.proxy.on("proxyRes", (response, request: ProxyRequest) => {
|
|
||||||
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
|
|
||||||
response.headers.location = request.base + response.headers.location
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop and dispose everything. Return an array of disposal errors.
|
|
||||||
*/
|
|
||||||
public async dispose(): Promise<Error[]> {
|
|
||||||
this.socketProvider.stop()
|
|
||||||
const providers = Array.from(this.providers.values())
|
|
||||||
// Catch so all the errors can be seen rather than just the first one.
|
|
||||||
const responses = await Promise.all<Error | undefined>(providers.map((p) => p.dispose().catch((e) => e)))
|
|
||||||
return responses.filter<Error>((r): r is Error => typeof r !== "undefined")
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getConnections(): Promise<number> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.server.getConnections((error, count) => {
|
|
||||||
return error ? reject(error) : resolve(count)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a provider for a top-level endpoint.
|
|
||||||
*/
|
|
||||||
public registerHttpProvider<T extends HttpProvider>(endpoint: string | string[], provider: HttpProvider0<T>): T
|
|
||||||
public registerHttpProvider<A1, T extends HttpProvider>(
|
|
||||||
endpoint: string | string[],
|
|
||||||
provider: HttpProvider1<A1, T>,
|
|
||||||
a1: A1,
|
|
||||||
): T
|
|
||||||
public registerHttpProvider<A1, A2, T extends HttpProvider>(
|
|
||||||
endpoint: string | string[],
|
|
||||||
provider: HttpProvider2<A1, A2, T>,
|
|
||||||
a1: A1,
|
|
||||||
a2: A2,
|
|
||||||
): T
|
|
||||||
public registerHttpProvider<A1, A2, A3, T extends HttpProvider>(
|
|
||||||
endpoint: string | string[],
|
|
||||||
provider: HttpProvider3<A1, A2, A3, T>,
|
|
||||||
a1: A1,
|
|
||||||
a2: A2,
|
|
||||||
a3: A3,
|
|
||||||
): T
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void {
|
|
||||||
const p = new provider(
|
|
||||||
{
|
|
||||||
auth: this.options.auth || AuthType.None,
|
|
||||||
commit: this.options.commit,
|
|
||||||
password: this.options.password,
|
|
||||||
},
|
|
||||||
...args,
|
|
||||||
)
|
|
||||||
const endpoints = arrayify(endpoint).map(trimSlashes)
|
|
||||||
endpoints.forEach((endpoint) => {
|
|
||||||
if (/\//.test(endpoint)) {
|
|
||||||
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
|
|
||||||
}
|
|
||||||
const existingProvider = this.providers.get(`/${endpoint}`)
|
|
||||||
this.providers.set(`/${endpoint}`, p)
|
|
||||||
if (existingProvider) {
|
|
||||||
logger.debug(`Overridding existing /${endpoint} provider`)
|
|
||||||
// If the existing provider isn't registered elsewhere we can dispose.
|
|
||||||
if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) {
|
|
||||||
logger.debug(`Disposing existing /${endpoint} provider`)
|
|
||||||
existingProvider.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start listening on the specified port.
|
|
||||||
*/
|
|
||||||
public listen(): Promise<string | null> {
|
|
||||||
if (!this.listenPromise) {
|
|
||||||
this.listenPromise = new Promise((resolve, reject) => {
|
|
||||||
this.server.on("error", reject)
|
|
||||||
this.server.on("upgrade", this.onUpgrade)
|
|
||||||
const onListen = (): void => resolve(this.address())
|
|
||||||
if (this.options.socket) {
|
|
||||||
this.server.listen(this.options.socket, onListen)
|
|
||||||
} else {
|
|
||||||
this.server.listen(this.options.port, this.options.host, onListen)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return this.listenPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The *local* address of the server.
|
|
||||||
*/
|
|
||||||
public address(): string | null {
|
|
||||||
const address = this.server.address()
|
|
||||||
const endpoint =
|
|
||||||
typeof address !== "string" && address !== null
|
|
||||||
? (address.address === "::" ? "localhost" : address.address) + ":" + address.port
|
|
||||||
: address
|
|
||||||
return endpoint && `${this.protocol}://${endpoint}`
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
|
||||||
this.heart.beat()
|
|
||||||
const route = this.parseUrl(request)
|
|
||||||
const write = (payload: HttpResponse): void => {
|
|
||||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
|
||||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
|
||||||
...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}),
|
|
||||||
...(request.headers["service-worker"] ? { "Service-Worker-Allowed": route.provider.base(route) } : {}),
|
|
||||||
...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}),
|
|
||||||
...(payload.cookie
|
|
||||||
? {
|
|
||||||
"Set-Cookie": [
|
|
||||||
`${payload.cookie.key}=${payload.cookie.value}`,
|
|
||||||
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
|
||||||
this.getCookieDomain(request.headers.host || ""),
|
|
||||||
// "HttpOnly",
|
|
||||||
"SameSite=lax",
|
|
||||||
]
|
|
||||||
.filter((l) => !!l)
|
|
||||||
.join(";"),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...payload.headers,
|
|
||||||
})
|
|
||||||
if (payload.stream) {
|
|
||||||
payload.stream.on("error", (error: NodeJS.ErrnoException) => {
|
|
||||||
response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError)
|
|
||||||
response.end(error.message)
|
|
||||||
})
|
|
||||||
payload.stream.on("close", () => response.end())
|
|
||||||
payload.stream.pipe(response)
|
|
||||||
} else if (typeof payload.content === "string" || payload.content instanceof Buffer) {
|
|
||||||
response.end(payload.content)
|
|
||||||
} else if (payload.content && typeof payload.content === "object") {
|
|
||||||
response.end(JSON.stringify(payload.content))
|
|
||||||
} else {
|
|
||||||
response.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload =
|
|
||||||
this.maybeRedirect(request, route) ||
|
|
||||||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
|
||||||
(await route.provider.handleRequest(route, request))
|
|
||||||
if (payload.proxy) {
|
|
||||||
this.doProxy(route, request, response, payload.proxy)
|
|
||||||
} else {
|
|
||||||
write(payload)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
let e = error
|
|
||||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
||||||
e = new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
|
||||||
logger.debug("Request error", field("url", request.url), field("code", code), field("error", error))
|
|
||||||
if (code >= HttpCode.ServerError) {
|
|
||||||
logger.error(error.stack)
|
|
||||||
}
|
|
||||||
if (request.headers["content-type"] === "application/json") {
|
|
||||||
write({
|
|
||||||
code,
|
|
||||||
mime: "application/json",
|
|
||||||
content: {
|
|
||||||
error: e.message,
|
|
||||||
...(e.details || {}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
write({
|
|
||||||
code,
|
|
||||||
...(await route.provider.getErrorRoot(route, code, code, e.message)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return any necessary redirection before delegating to a provider.
|
|
||||||
*/
|
|
||||||
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): RedirectResponse | undefined {
|
|
||||||
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
|
||||||
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
|
||||||
return { redirect: route.fullPath }
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a path that goes from the base, construct a relative redirect URL
|
|
||||||
* that will get you there considering that the app may be served from an
|
|
||||||
* unknown base path. If handling TLS, also ensure HTTPS.
|
|
||||||
*/
|
|
||||||
private constructRedirect(request: http.IncomingMessage, route: ProviderRoute, payload: RedirectResponse): string {
|
|
||||||
const query = {
|
|
||||||
...route.query,
|
|
||||||
...(payload.query || {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(query).forEach((key) => {
|
|
||||||
if (typeof query[key] === "undefined") {
|
|
||||||
delete query[key]
|
delete query[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const secure = (request.connection as tls.TLSSocket).encrypted
|
const relativePath = normalize(`${relativeRoot(req)}/${to}`, true)
|
||||||
const redirect =
|
const queryString = qs.stringify(query)
|
||||||
(this.options.cert && !secure ? `${this.protocol}://${request.headers.host}/` : "") +
|
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
|
||||||
normalize(`${route.provider.base(route)}/${payload.redirect}`, true) +
|
logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`)
|
||||||
(Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "")
|
res.redirect(redirectPath)
|
||||||
logger.debug("redirecting", field("secure", !!secure), field("from", request.url), field("to", redirect))
|
|
||||||
return redirect
|
|
||||||
}
|
|
||||||
|
|
||||||
private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> => {
|
|
||||||
try {
|
|
||||||
this.heart.beat()
|
|
||||||
socket.on("error", () => socket.destroy())
|
|
||||||
|
|
||||||
if (this.options.cert && !(socket as tls.TLSSocket).encrypted) {
|
|
||||||
throw new HttpError("HTTP websocket", HttpCode.BadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") {
|
|
||||||
throw new HttpError("HTTP/1.1 400 Bad Request", HttpCode.BadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = this.parseUrl(request)
|
|
||||||
if (!route.provider) {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The socket proxy is so we can pass them to child processes (TLS sockets
|
|
||||||
// can't be transferred so we need an in-between).
|
|
||||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
|
||||||
const payload =
|
|
||||||
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
|
||||||
if (payload && payload.proxy) {
|
|
||||||
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
socket.destroy(error)
|
|
||||||
logger.warn(`discarding socket connection: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a request URL so we can route it.
|
|
||||||
*/
|
|
||||||
private parseUrl(request: http.IncomingMessage): ProviderRoute {
|
|
||||||
const parse = (fullPath: string): { base: string; requestPath: string } => {
|
|
||||||
const match = fullPath.match(/^(\/?[^/]*)(.*)$/)
|
|
||||||
let [, /* ignore */ base, requestPath] = match ? match.map((p) => p.replace(/\/+$/, "")) : ["", "", ""]
|
|
||||||
if (base.indexOf(".") !== -1) {
|
|
||||||
// Assume it's a file at the root.
|
|
||||||
requestPath = base
|
|
||||||
base = "/"
|
|
||||||
} else if (base === "") {
|
|
||||||
// Happens if it's a plain `domain.com`.
|
|
||||||
base = "/"
|
|
||||||
}
|
|
||||||
return { base, requestPath }
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}, pathname: "" }
|
|
||||||
const originalPath = parsedUrl.pathname || "/"
|
|
||||||
const fullPath = normalize(originalPath, true)
|
|
||||||
const { base, requestPath } = parse(fullPath)
|
|
||||||
|
|
||||||
// Providers match on the path after their base so we need to account for
|
|
||||||
// that by shifting the next base out of the request path.
|
|
||||||
let provider = this.providers.get(base)
|
|
||||||
if (base !== "/" && provider) {
|
|
||||||
return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to the top-level provider.
|
|
||||||
provider = this.providers.get("/")
|
|
||||||
if (!provider) {
|
|
||||||
throw new Error(`No provider for ${base}`)
|
|
||||||
}
|
|
||||||
return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy a request to the target.
|
|
||||||
*/
|
|
||||||
private doProxy(
|
|
||||||
route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
response: http.ServerResponse,
|
|
||||||
options: ProxyOptions,
|
|
||||||
): void
|
|
||||||
/**
|
|
||||||
* Proxy a web socket to the target.
|
|
||||||
*/
|
|
||||||
private doProxy(
|
|
||||||
route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
response: { socket: net.Socket; head: Buffer },
|
|
||||||
options: ProxyOptions,
|
|
||||||
): void
|
|
||||||
/**
|
|
||||||
* Proxy a request or web socket to the target.
|
|
||||||
*/
|
|
||||||
private doProxy(
|
|
||||||
route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
response: http.ServerResponse | { socket: net.Socket; head: Buffer },
|
|
||||||
options: ProxyOptions,
|
|
||||||
): void {
|
|
||||||
const port = parseInt(options.port, 10)
|
|
||||||
if (isNaN(port)) {
|
|
||||||
throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
|
|
||||||
// sure how best to get this information to the `proxyRes` event handler.
|
|
||||||
// For now I'm sticking it on the request object which is passed through to
|
|
||||||
// the event.
|
|
||||||
;(request as ProxyRequest).base = options.strip
|
|
||||||
|
|
||||||
const isHttp = response instanceof http.ServerResponse
|
|
||||||
const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath
|
|
||||||
const path = normalize("/" + (options.prepend || "") + "/" + base, true)
|
|
||||||
const proxyOptions: proxy.ServerOptions = {
|
|
||||||
changeOrigin: true,
|
|
||||||
ignorePath: true,
|
|
||||||
target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${
|
|
||||||
Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
|
|
||||||
}`,
|
|
||||||
ws: !isHttp,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response instanceof http.ServerResponse) {
|
|
||||||
this.proxy.web(request, response, proxyOptions)
|
|
||||||
} else {
|
|
||||||
this.proxy.ws(request, response.socket, response.head, proxyOptions)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value that should be used for setting a cookie domain. This will
|
* Get the value that should be used for setting a cookie domain. This will
|
||||||
* allow the user to authenticate only once. This will use the highest level
|
* allow the user to authenticate once no matter what sub-domain they use to log
|
||||||
* domain (e.g. `coder.com` over `test.coder.com` if both are specified).
|
* in. This will use the highest level proxy domain (e.g. `coder.com` over
|
||||||
|
* `test.coder.com` if both are specified).
|
||||||
*/
|
*/
|
||||||
private getCookieDomain(host: string): string | undefined {
|
export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => {
|
||||||
const idx = host.lastIndexOf(":")
|
const idx = host.lastIndexOf(":")
|
||||||
host = idx !== -1 ? host.substring(0, idx) : host
|
host = idx !== -1 ? host.substring(0, idx) : host
|
||||||
|
// If any of these are true we will still set cookies but without an explicit
|
||||||
|
// `Domain` attribute on the cookie.
|
||||||
if (
|
if (
|
||||||
// Might be blank/missing, so there's nothing more to do.
|
// The host can be be blank or missing so there's nothing we can set.
|
||||||
!host ||
|
!host ||
|
||||||
// IP addresses can't have subdomains so there's no value in setting the
|
// IP addresses can't have subdomains so there's no value in setting the
|
||||||
// domain for them. Assume anything with a : is ipv6 (valid domain name
|
// domain for them. Assume that anything with a : is ipv6 (valid domain name
|
||||||
// characters are alphanumeric or dashes).
|
// characters are alphanumeric or dashes)...
|
||||||
host.includes(":") ||
|
host.includes(":") ||
|
||||||
// Assume anything entirely numbers and dots is ipv4 (currently tlds
|
// ...and that anything entirely numbers and dots is ipv4 (currently tlds
|
||||||
// cannot be entirely numbers).
|
// cannot be entirely numbers).
|
||||||
!/[^0-9.]/.test(host) ||
|
!/[^0-9.]/.test(host) ||
|
||||||
// localhost subdomains don't seem to work at all (browser bug?).
|
// localhost subdomains don't seem to work at all (browser bug?). A cookie
|
||||||
|
// set at dev.localhost cannot be read by 8080.dev.localhost.
|
||||||
host.endsWith(".localhost") ||
|
host.endsWith(".localhost") ||
|
||||||
// It might be localhost (or an IP, see above) if it's a proxy and it
|
// Domains without at least one dot (technically two since domain.tld will
|
||||||
// isn't setting the host header to match the access domain.
|
// become .domain.tld) are considered invalid according to the spec so don't
|
||||||
host === "localhost"
|
// set the domain for them. In my testing though localhost is the only
|
||||||
|
// problem (the browser just doesn't store the cookie at all). localhost has
|
||||||
|
// an additional problem which is that a reverse proxy might give
|
||||||
|
// code-server localhost even though the domain is really domain.tld (by
|
||||||
|
// default NGINX does this).
|
||||||
|
!host.includes(".")
|
||||||
) {
|
) {
|
||||||
logger.debug("no valid cookie doman", field("host", host))
|
logger.debug("no valid cookie doman", field("host", host))
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
this.proxyDomains.forEach((domain) => {
|
proxyDomains.forEach((domain) => {
|
||||||
if (host.endsWith(domain) && domain.length < host.length) {
|
if (host.endsWith(domain) && domain.length < host.length) {
|
||||||
host = domain
|
host = domain
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug("got cookie doman", field("host", host))
|
logger.debug("got cookie doman", field("host", host))
|
||||||
return host ? `Domain=${host}` : undefined
|
return host || undefined
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a response 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.
|
|
||||||
*/
|
|
||||||
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
|
||||||
// Split into parts.
|
|
||||||
const host = request.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 || !this.proxyDomains.has(proxyDomain)) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
proxy: {
|
|
||||||
port,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as util from "util"
|
import * as semver from "semver"
|
||||||
import { Args } from "./cli"
|
import * as pluginapi from "../../typings/pluginapi"
|
||||||
import { HttpServer } from "./http"
|
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 {
|
interface Application extends pluginapi.Application {
|
||||||
activate: Activate
|
/*
|
||||||
|
* 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
|
* PluginAPI implements the plugin API described in typings/pluginapi.d.ts
|
||||||
* import it.
|
* Please see that file for details.
|
||||||
*/
|
*/
|
||||||
const originalLoad = require("module")._load
|
export class PluginAPI {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
private readonly plugins = new Map<string, Plugin>()
|
||||||
require("module")._load = function (request: string, parent: object, isMain: boolean): any {
|
private readonly logger: Logger
|
||||||
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
|
|
||||||
|
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 loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
/**
|
||||||
|
* applications grabs the full list of applications from
|
||||||
|
* all loaded plugins.
|
||||||
|
*/
|
||||||
|
public async applications(): Promise<Application[]> {
|
||||||
|
const apps = new Array<Application>()
|
||||||
|
for (const [, p] of this.plugins) {
|
||||||
|
if (!p.applications) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const pluginApps = await p.applications()
|
||||||
|
|
||||||
|
// Add plugin key to each app.
|
||||||
|
apps.push(
|
||||||
|
...pluginApps.map((app) => {
|
||||||
|
app = { ...app, path: path.join(p.routerPath, app.path || "") }
|
||||||
|
app = { ...app, iconPath: path.join(app.path || "", app.iconPath) }
|
||||||
|
return {
|
||||||
|
...app,
|
||||||
|
plugin: {
|
||||||
|
name: p.name,
|
||||||
|
version: p.version,
|
||||||
|
modulePath: p.modulePath,
|
||||||
|
|
||||||
|
displayName: p.displayName,
|
||||||
|
description: p.description,
|
||||||
|
routerPath: p.routerPath,
|
||||||
|
homepageURL: p.homepageURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mount mounts all plugin routers onto r.
|
||||||
|
*/
|
||||||
|
public mount(r: express.Router): void {
|
||||||
|
for (const [, p] of this.plugins) {
|
||||||
|
if (!p.router) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.use(`${p.routerPath}`, p.router())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadPlugins loads all plugins based on this.csPlugin,
|
||||||
|
* this.csPluginPath and the built in plugins.
|
||||||
|
*/
|
||||||
|
public async loadPlugins(): Promise<void> {
|
||||||
|
for (const dir of this.csPlugin.split(":")) {
|
||||||
|
if (!dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await this.loadPlugin(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of this.csPluginPath.split(":")) {
|
||||||
|
if (!dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await this._loadPlugins(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in plugins.
|
||||||
|
await this._loadPlugins(path.join(__dirname, "../../plugins"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* _loadPlugins is the counterpart to loadPlugins.
|
||||||
|
*
|
||||||
|
* It differs in that it loads all plugins in a single
|
||||||
|
* directory whereas loadPlugins uses all available directories
|
||||||
|
* as documented.
|
||||||
|
*/
|
||||||
|
private async _loadPlugins(dir: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const plugin: Plugin = require(pluginPath)
|
const entries = await fsp.readdir(dir, { withFileTypes: true })
|
||||||
plugin.activate(httpServer, args)
|
for (const ent of entries) {
|
||||||
logger.debug("Loaded plugin", field("name", path.basename(pluginPath)))
|
if (!ent.isDirectory()) {
|
||||||
} catch (error) {
|
continue
|
||||||
if (error.code !== "MODULE_NOT_FOUND") {
|
}
|
||||||
logger.warn(error.message)
|
await this.loadPlugin(path.join(dir, ent.name))
|
||||||
} else {
|
}
|
||||||
logger.error(error.message)
|
} catch (err) {
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
private async loadPlugin(dir: string): Promise<void> {
|
||||||
const pluginPath = path.resolve(__dirname, "../../plugins")
|
try {
|
||||||
const files = await util.promisify(fs.readdir)(pluginPath, {
|
const str = await fsp.readFile(path.join(dir, "package.json"), {
|
||||||
withFileTypes: true,
|
encoding: "utf8",
|
||||||
})
|
})
|
||||||
await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args)))
|
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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PLUGIN_DIR) {
|
/**
|
||||||
await loadPlugin(process.env.PLUGIN_DIR, httpServer, args)
|
* _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 { logger } from "@coder/logger"
|
||||||
|
import { Query } from "express-serve-static-core"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Route } from "./http"
|
|
||||||
import { paths } from "./util"
|
import { paths } from "./util"
|
||||||
|
|
||||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||||
@@ -58,7 +58,7 @@ export interface CoderSettings extends UpdateSettings {
|
|||||||
url: string
|
url: string
|
||||||
workspace: boolean
|
workspace: boolean
|
||||||
}
|
}
|
||||||
query: Route["query"]
|
query: Query
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as path from "path"
|
|||||||
import * as tls from "tls"
|
import * as tls from "tls"
|
||||||
import { Emitter } from "../common/emitter"
|
import { Emitter } from "../common/emitter"
|
||||||
import { generateUuid } from "../common/util"
|
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
|
* 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> {
|
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 i = 0
|
||||||
let path = basePath
|
let path = basePath
|
||||||
while ((await canConnect(path)) && i < maxTries) {
|
while ((await canConnect(path)) && i < maxTries) {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { field, logger } from "@coder/logger"
|
import { field, logger } from "@coder/logger"
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as https from "https"
|
import * as https from "https"
|
||||||
import * as path from "path"
|
|
||||||
import * as semver from "semver"
|
import * as semver from "semver"
|
||||||
import * as url from "url"
|
import * as url from "url"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { version } from "./constants"
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
|
||||||
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
|
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
checked: number
|
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 update?: Promise<Update>
|
||||||
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
options: HttpProviderOptions,
|
|
||||||
public readonly enabled: boolean,
|
|
||||||
/**
|
/**
|
||||||
* The URL for getting the latest version of code-server. Should return JSON
|
* The URL for getting the latest version of code-server. Should return JSON
|
||||||
* that fulfills `LatestResponse`.
|
* that fulfills `LatestResponse`.
|
||||||
@@ -37,37 +33,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
* settings will be used.
|
* settings will be used.
|
||||||
*/
|
*/
|
||||||
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
|
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.
|
* Query for and return the latest update.
|
||||||
@@ -89,7 +55,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
if (!update || update.checked + this.updateInterval < now) {
|
if (!update || update.checked + this.updateInterval < now) {
|
||||||
const buffer = await this.request(this.latestUrl)
|
const buffer = await this.request(this.latestUrl)
|
||||||
const data = JSON.parse(buffer.toString()) as LatestResponse
|
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 })
|
await this.settings.write({ update })
|
||||||
}
|
}
|
||||||
logger.debug("got latest version", field("latest", update.version))
|
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.
|
* Return true if the currently installed version is the latest.
|
||||||
*/
|
*/
|
||||||
public isLatestVersion(latest: Update): boolean {
|
public isLatestVersion(latest: Update): boolean {
|
||||||
const version = this.currentVersion
|
|
||||||
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
||||||
try {
|
try {
|
||||||
return latest.version === version || semver.lt(latest.version, version)
|
return semver.lte(latest.version, version)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -144,22 +105,20 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
logger.debug("Making request", field("uri", uri))
|
logger.debug("Making request", field("uri", uri))
|
||||||
const httpx = uri.startsWith("https") ? https : http
|
const httpx = uri.startsWith("https") ? https : http
|
||||||
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
||||||
if (
|
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
||||||
response.statusCode &&
|
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
||||||
response.statusCode >= 300 &&
|
}
|
||||||
response.statusCode < 400 &&
|
|
||||||
response.headers.location
|
if (response.statusCode >= 300) {
|
||||||
) {
|
|
||||||
++redirects
|
++redirects
|
||||||
|
response.destroy()
|
||||||
if (redirects > maxRedirects) {
|
if (redirects > maxRedirects) {
|
||||||
return reject(new Error("reached max redirects"))
|
return reject(new Error("reached max redirects"))
|
||||||
}
|
}
|
||||||
response.destroy()
|
if (!response.headers.location) {
|
||||||
return request(url.resolve(uri, 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)
|
resolve(response)
|
||||||
@@ -2,6 +2,7 @@ import * as cp from "child_process"
|
|||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import envPaths from "env-paths"
|
import envPaths from "env-paths"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
|
import * as net from "net"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
@@ -53,25 +54,45 @@ export function humanPath(p?: string): string {
|
|||||||
return p.replace(os.homedir(), "~")
|
return p.replace(os.homedir(), "~")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
|
export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => {
|
||||||
const paths = {
|
const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`)
|
||||||
cert: path.join(tmpdir, "self-signed.cert"),
|
const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`)
|
||||||
certKey: path.join(tmpdir, "self-signed.key"),
|
|
||||||
}
|
const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)])
|
||||||
const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)])
|
|
||||||
if (!checks[0] || !checks[1]) {
|
if (!checks[0] || !checks[1]) {
|
||||||
// Require on demand so openssl isn't required if you aren't going to
|
// Require on demand so openssl isn't required if you aren't going to
|
||||||
// generate certificates.
|
// generate certificates.
|
||||||
const pem = require("pem") as typeof import("pem")
|
const pem = require("pem") as typeof import("pem")
|
||||||
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
|
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
|
||||||
pem.createCertificate({ selfSigned: true }, (error, 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)
|
return error ? reject(error) : resolve(result)
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
await fs.mkdirp(paths.data)
|
||||||
await fs.mkdirp(tmpdir)
|
await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)])
|
||||||
await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)])
|
}
|
||||||
|
return {
|
||||||
|
cert: certPath,
|
||||||
|
certKey: certKeyPath,
|
||||||
}
|
}
|
||||||
return paths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generatePassword = async (length = 24): Promise<string> => {
|
export const generatePassword = async (length = 24): Promise<string> => {
|
||||||
@@ -246,3 +267,26 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin
|
|||||||
}
|
}
|
||||||
return value
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||