├── .dockerignore
├── .editorconfig
├── .git-blame-ignore-revs
├── .gitattributes
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ ├── doc.md
│ └── feature-request.md
├── PULL_REQUEST_TEMPLATE.md
├── codecov.yml
├── codeql-config.yml
├── dependabot.yaml
├── semantic.yaml
├── stale.yml
└── workflows
│ ├── build.yaml
│ ├── installer.yaml
│ ├── publish.yaml
│ ├── release.yaml
│ ├── scripts.yaml
│ ├── security.yaml
│ └── trivy-docker.yaml
├── .gitignore
├── .gitmodules
├── .node-version
├── .nvmrc
├── .prettierignore
├── .prettierrc.yaml
├── .tours
├── contributing.tour
└── start-development.tour
├── CHANGELOG.md
├── LICENSE
├── ThirdPartyNotices.txt
├── ci
├── Caddyfile
├── README.md
├── build
│ ├── build-code-server.sh
│ ├── build-lib.sh
│ ├── build-packages.sh
│ ├── build-release.sh
│ ├── build-standalone-release.sh
│ ├── build-vscode.sh
│ ├── clean.sh
│ ├── code-server-nfpm.sh
│ ├── code-server-user.service
│ ├── code-server.sh
│ ├── code-server@.service
│ ├── nfpm.yaml
│ └── npm-postinstall.sh
├── dev
│ ├── doctoc.sh
│ ├── gen_icons.sh
│ ├── lint-scripts.sh
│ ├── postinstall.sh
│ ├── preinstall.js
│ ├── test-e2e.sh
│ ├── test-integration.sh
│ ├── test-native.sh
│ ├── test-scripts.sh
│ ├── test-unit.sh
│ └── watch.ts
├── helm-chart
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ ├── pvc.yaml
│ │ ├── secrets.yaml
│ │ ├── service.yaml
│ │ ├── serviceaccount.yaml
│ │ └── tests
│ │ │ └── test-connection.yaml
│ └── values.yaml
├── lib.sh
├── release-image
│ ├── Dockerfile
│ ├── Dockerfile.fedora
│ ├── Dockerfile.opensuse
│ ├── docker-bake.hcl
│ ├── entrypoint-catatonit.sh
│ └── entrypoint.sh
└── steps
│ ├── brew-bump.sh
│ ├── docker-buildx-push.sh
│ ├── publish-npm.sh
│ └── steps-lib.sh
├── docs
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FAQ.md
├── MAINTAINING.md
├── README.md
├── SECURITY.md
├── android.md
├── assets
│ ├── images
│ │ └── icons
│ │ │ ├── collab.svg
│ │ │ ├── contributing.svg
│ │ │ ├── faq.svg
│ │ │ ├── home.svg
│ │ │ ├── requirements.svg
│ │ │ ├── upgrade.svg
│ │ │ ├── usage.svg
│ │ │ └── wrench.svg
│ ├── screenshot-1.png
│ └── screenshot-2.png
├── coder.md
├── collaboration.md
├── guide.md
├── helm.md
├── install.md
├── ios.md
├── ipad.md
├── manifest.json
├── npm.md
├── requirements.md
├── termux.md
├── triage.md
└── upgrade.md
├── eslint.config.mjs
├── flake.lock
├── flake.nix
├── install.sh
├── package-lock.json
├── package.json
├── patches
├── base-path.diff
├── cli-window-open.diff
├── clipboard.diff
├── disable-builtin-ext-update.diff
├── display-language.diff
├── external-file-actions.diff
├── getting-started.diff
├── insecure-notification.diff
├── integration.diff
├── keepalive.diff
├── local-storage.diff
├── logout.diff
├── marketplace.diff
├── proposed-api.diff
├── proxy-uri.diff
├── series
├── service-worker.diff
├── signature-verification.diff
├── sourcemaps.diff
├── store-socket.diff
├── telemetry.diff
├── trusted-domains.diff
├── unique-db.diff
├── update-check.diff
└── webview.diff
├── renovate.json
├── src
├── browser
│ ├── favicon.afdesign
│ ├── media
│ │ ├── favicon-dark-support.svg
│ │ ├── favicon.ico
│ │ ├── favicon.svg
│ │ ├── pwa-icon-192.png
│ │ ├── pwa-icon-512.png
│ │ ├── pwa-icon.png
│ │ └── templates.png
│ ├── pages
│ │ ├── error.css
│ │ ├── error.html
│ │ ├── global.css
│ │ ├── login.css
│ │ └── login.html
│ ├── robots.txt
│ ├── security.txt
│ └── serviceWorker.ts
├── common
│ ├── emitter.ts
│ ├── http.ts
│ └── util.ts
└── node
│ ├── app.ts
│ ├── cli.ts
│ ├── constants.ts
│ ├── entry.ts
│ ├── heart.ts
│ ├── http.ts
│ ├── i18n
│ ├── index.ts
│ └── locales
│ │ ├── en.json
│ │ ├── ja.json
│ │ ├── th.json
│ │ ├── ur.json
│ │ └── zh-cn.json
│ ├── main.ts
│ ├── proxy.ts
│ ├── routes
│ ├── domainProxy.ts
│ ├── errors.ts
│ ├── health.ts
│ ├── index.ts
│ ├── login.ts
│ ├── logout.ts
│ ├── pathProxy.ts
│ ├── update.ts
│ └── vscode.ts
│ ├── settings.ts
│ ├── socket.ts
│ ├── update.ts
│ ├── util.ts
│ ├── vscodeSocket.ts
│ ├── wrapper.ts
│ └── wsRouter.ts
├── test
├── e2e
│ ├── baseFixture.ts
│ ├── codeServer.test.ts
│ ├── downloads.test.ts
│ ├── extensions.test.ts
│ ├── extensions
│ │ └── test-extension
│ │ │ ├── .gitignore
│ │ │ ├── extension.ts
│ │ │ ├── package-lock.json
│ │ │ ├── package.json
│ │ │ └── tsconfig.json
│ ├── github.test.ts
│ ├── login.test.ts
│ ├── logout.test.ts
│ ├── models
│ │ └── CodeServer.ts
│ ├── openHelpAbout.test.ts
│ ├── routes.test.ts
│ ├── terminal.test.ts
│ ├── uploads.test.ts
│ └── webview.test.ts
├── integration
│ ├── help.test.ts
│ └── installExtension.test.ts
├── package-lock.json
├── package.json
├── playwright.config.ts
├── scripts
│ ├── build-lib.bats
│ ├── install.bats
│ └── steps-lib.bats
├── tsconfig.json
├── unit
│ ├── common
│ │ ├── emitter.test.ts
│ │ ├── http.test.ts
│ │ └── util.test.ts
│ ├── helpers.test.ts
│ └── node
│ │ ├── app.test.ts
│ │ ├── cli.test.ts
│ │ ├── constants.test.ts
│ │ ├── heart.test.ts
│ │ ├── http.test.ts
│ │ ├── proxy.test.ts
│ │ ├── routes
│ │ ├── errors.test.ts
│ │ ├── health.test.ts
│ │ ├── login.test.ts
│ │ ├── static.test.ts
│ │ └── vscode.test.ts
│ │ ├── settings.test.ts
│ │ ├── socket.test.ts
│ │ ├── update.test.ts
│ │ ├── util.test.ts
│ │ ├── vscodeSocket.test.ts
│ │ └── wrapper.test.ts
└── utils
│ ├── constants.ts
│ ├── cssStub.ts
│ ├── globalE2eSetup.ts
│ ├── globalUnitSetup.ts
│ ├── helpers.ts
│ ├── httpserver.ts
│ ├── integration.ts
│ ├── runCodeServerCommand.ts
│ └── wtfnode.ts
├── tsconfig.json
└── typings
└── httpolyglot
└── index.d.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | **
2 | !release-packages
3 | !ci
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | trim_trailing_whitespace = true
6 | indent_size = 2
7 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Prettier 3.4.2
2 | 9b0340a09276f93c054d705d1b9a5f24cc5dbc97
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.afdesign filter=lfs diff=lfs merge=lfs -text
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @coder/code-server
2 |
3 | ci/helm-chart/ @Matthew-Beckett @alexgorbatchev
4 |
5 | docs/install.md @GNUxeava
6 |
7 | src/node/i18n/locales/zh-cn.json @zhaozhiming
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Question?
4 | url: https://github.com/coder/code-server/discussions/new?category_id=22503114
5 | about: Ask the community for help on our GitHub Discussions board
6 | - name: code-server Slack Community
7 | about: Need immediate help or just want to talk? Hop in our Slack. Note - this Slack is not actively monitored by code-server maintainers.
8 | url: https://cdr.co/join-community
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/doc.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation improvement
3 | about: Suggest a documentation improvement
4 | labels: "docs"
5 | ---
6 |
7 | ## What is your suggestion?
8 |
9 | ## How will this improve the docs?
10 |
11 | ## Are you interested in submitting a PR for this?
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea to improve code-server
4 | labels: enhancement
5 | ---
6 |
7 | ## What is your suggestion?
8 |
9 | ## Why do you want this feature?
10 |
11 | ## Are there any workarounds to get this functionality today?
12 |
13 | ## Are you interested in submitting a PR for this?
14 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | Fixes #
9 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: yes
3 | allow_coverage_offsets: True
4 |
5 | coverage:
6 | precision: 2
7 | round: down
8 | range: "40...70"
9 | status:
10 | patch: off
11 | notify:
12 | slack:
13 | default:
14 | url: secret:v1::tXC7VwEIKYjNU8HRgRv2GdKOSCt5UzpykKZb+o1eCDqBgb2PEqwE3A26QUPYMLo4BO2qtrJhFIvwhUvlPwyzDCNGoNiuZfXr0UeZZ0y1TcZu672R/NBNMwEPO/e1Ye0pHxjzKHnuH7HqbjFucox/RBQLtiL3J56SWGE3JtbkC6o=
15 | threshold: 1%
16 | only_pulls: false
17 | branches:
18 | - "main"
19 |
20 | parsers:
21 | gcov:
22 | branch_detection:
23 | conditional: yes
24 | loop: yes
25 | method: no
26 | macro: no
27 |
28 | comment:
29 | layout: "reach,diff,flags,files,footer"
30 | behavior: default
31 | require_changes: no
32 |
--------------------------------------------------------------------------------
/.github/codeql-config.yml:
--------------------------------------------------------------------------------
1 | name: "code-server CodeQL config"
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | time: "06:00"
8 | timezone: "America/Chicago"
9 | labels: []
10 | commit-message:
11 | prefix: "chore"
12 |
13 | - package-ecosystem: "npm"
14 | directory: "/"
15 | schedule:
16 | interval: "monthly"
17 | time: "06:00"
18 | timezone: "America/Chicago"
19 | commit-message:
20 | prefix: "chore"
21 | labels: []
22 | ignore:
23 | # Ignore patch updates for all dependencies
24 | - dependency-name: "*"
25 | update-types:
26 | - version-update:semver-patch
27 | # Ignore major updates to Node.js types, because they need to
28 | # correspond to the Node.js engine version
29 | - dependency-name: "@types/node"
30 | update-types:
31 | - version-update:semver-major
32 |
--------------------------------------------------------------------------------
/.github/semantic.yaml:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # This file configures "Semantic Pull Requests", which is documented here:
3 | # https://github.com/zeke/semantic-pull-requests
4 | ###############################################################################
5 |
6 | # Scopes are optionally supplied after a 'type'. For example, in
7 | #
8 | # feat(docs): autostart ui
9 | #
10 | # '(docs)' is the scope. Scopes are used to signify where the change occurred.
11 | scopes:
12 | # docs: changes to the code-server documentation.
13 | - docs
14 |
15 | # vendor: changes to vendored dependencies.
16 | - vendor
17 |
18 | # deps: changes to code-server's dependencies.
19 | - deps
20 |
21 | # cs: changes to code specific to code-server.
22 | - cs
23 |
24 | # cli: changes to the command-line interface.
25 | - cli
26 |
27 | # We only check that the PR title is semantic. The PR title is automatically
28 | # applied to the "Squash & Merge" flow as the suggested commit message, so this
29 | # should suffice unless someone drastically alters the message in that flow.
30 | titleOnly: true
31 |
32 | # Types are the 'tag' types in a commit or PR title. For example, in
33 | #
34 | # chore: fix thing
35 | #
36 | # 'chore' is the type.
37 | types:
38 | # A build of any kind.
39 | - build
40 |
41 | # A user-facing change that corrects a defect in code-server.
42 | - fix
43 |
44 | # Any code task that is ignored for changelog purposes. Examples include
45 | # devbin scripts and internal-only configurations.
46 | - chore
47 |
48 | # Any work performed on CI.
49 | - ci
50 |
51 | # Work that directly implements or supports the implementation of a feature.
52 | - feat
53 |
54 | # A refactor changes code structure without any behavioral change.
55 | - refactor
56 |
57 | # A git revert for any style of commit.
58 | - revert
59 |
60 | # Adding tests of any kind. Should be separate from feature or fix
61 | # implementations. For example, if a commit adds a fix + test, it's a fix
62 | # commit. If a commit is simply bumping coverage, it's a test commit.
63 | - test
64 |
65 | # A new release.
66 | - release
67 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 180
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 5
5 | # Label to apply when stale.
6 | staleLabel: stale
7 | # Comment to post when marking an issue as stale. Set to `false` to disable
8 | markComment: >
9 | This issue has been automatically marked as stale because it has not had
10 | recent activity. It will be closed if no activity occurs in the next 5 days.
11 | # Comment to post when closing a stale issue. Set to `false` to disable
12 | closeComment: false
13 |
--------------------------------------------------------------------------------
/.github/workflows/installer.yaml:
--------------------------------------------------------------------------------
1 | name: Installer integration
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - "install.sh"
9 | - ".github/workflows/installer.yaml"
10 | pull_request:
11 | branches:
12 | - main
13 | paths:
14 | - "install.sh"
15 | - ".github/workflows/installer.yaml"
16 |
17 | # Cancel in-progress runs for pull requests when developers push
18 | # additional changes, and serialize builds in branches.
19 | # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run
20 | concurrency:
21 | group: ${{ github.workflow }}-${{ github.ref }}
22 | cancel-in-progress: ${{ github.event_name == 'pull_request' }}
23 |
24 | permissions:
25 | contents: read
26 |
27 | jobs:
28 | ubuntu:
29 | name: Test installer on Ubuntu
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout repo
33 | uses: actions/checkout@v4
34 |
35 | - name: Install code-server
36 | run: ./install.sh
37 |
38 | - name: Test code-server was installed globally
39 | run: code-server --help
40 |
41 | alpine:
42 | name: Test installer on Alpine
43 | runs-on: ubuntu-latest
44 | container: "alpine:3.17"
45 | steps:
46 | - name: Checkout repo
47 | uses: actions/checkout@v4
48 |
49 | - name: Install curl
50 | run: apk add curl
51 |
52 | - name: Add user
53 | run: adduser coder --disabled-password
54 |
55 | # Standalone should work without root.
56 | - name: Test standalone to a non-existent prefix
57 | run: su coder -c "./install.sh --method standalone --prefix /tmp/does/not/yet/exist"
58 |
59 | # We do not actually have Alpine standalone builds so running code-server
60 | # will not work.
61 | - name: Test code-server was installed to prefix
62 | run: test -f /tmp/does/not/yet/exist/bin/code-server
63 |
64 | macos:
65 | name: Test installer on macOS
66 | runs-on: macos-latest
67 |
68 | steps:
69 | - name: Checkout repo
70 | uses: actions/checkout@v4
71 |
72 | - name: Install code-server
73 | run: ./install.sh
74 |
75 | - name: Test code-server was installed globally
76 | run: code-server --help
77 |
--------------------------------------------------------------------------------
/.github/workflows/scripts.yaml:
--------------------------------------------------------------------------------
1 | name: Script unit tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - "**.sh"
9 | - "**.bats"
10 | pull_request:
11 | branches:
12 | - main
13 | paths:
14 | - "**.sh"
15 | - "**.bats"
16 |
17 | permissions:
18 | actions: none
19 | checks: none
20 | contents: read
21 | deployments: none
22 | issues: none
23 | packages: none
24 | pull-requests: none
25 | repository-projects: none
26 | security-events: none
27 | statuses: none
28 |
29 | # Cancel in-progress runs for pull requests when developers push
30 | # additional changes, and serialize builds in branches.
31 | # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run
32 | concurrency:
33 | group: ${{ github.workflow }}-${{ github.ref }}
34 | cancel-in-progress: ${{ github.event_name == 'pull_request' }}
35 |
36 | jobs:
37 | test:
38 | name: Run script unit tests
39 | runs-on: ubuntu-latest
40 | # This runs on Alpine to make sure we're testing with actual sh.
41 | container: "alpine:3.17"
42 | steps:
43 | - name: Checkout repo
44 | uses: actions/checkout@v4
45 |
46 | - name: Install test utilities
47 | run: apk add bats checkbashisms
48 |
49 | - name: Check Bashisms
50 | run: checkbashisms ./install.sh
51 |
52 | - name: Run script unit tests
53 | run: ./ci/dev/test-scripts.sh
54 |
55 | lint:
56 | name: Lint shell files
57 | runs-on: ubuntu-latest
58 | timeout-minutes: 5
59 | steps:
60 | - name: Checkout repo
61 | uses: actions/checkout@v4
62 |
63 | - name: Install lint utilities
64 | run: sudo apt install shellcheck
65 |
66 | - name: Lint shell files
67 | run: ./ci/dev/lint-scripts.sh
68 |
--------------------------------------------------------------------------------
/.github/workflows/security.yaml:
--------------------------------------------------------------------------------
1 | name: Security
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - "package.json"
8 | pull_request:
9 | paths:
10 | - "package.json"
11 | schedule:
12 | # Runs every Monday morning PST
13 | - cron: "17 15 * * 1"
14 |
15 | # Cancel in-progress runs for pull requests when developers push additional
16 | # changes, and serialize builds in branches.
17 | concurrency:
18 | group: ${{ github.workflow }}-${{ github.ref }}
19 | cancel-in-progress: ${{ github.event_name == 'pull_request' }}
20 |
21 | jobs:
22 | audit:
23 | name: Audit node modules
24 | runs-on: ubuntu-latest
25 | timeout-minutes: 15
26 | steps:
27 | - name: Checkout repo
28 | uses: actions/checkout@v4
29 | with:
30 | fetch-depth: 0
31 |
32 | - name: Install Node.js
33 | uses: actions/setup-node@v4
34 | with:
35 | node-version-file: .node-version
36 |
37 | - name: Audit npm for vulnerabilities
38 | run: npm audit
39 | if: success()
40 |
41 | trivy-scan-repo:
42 | name: Scan repo with Trivy
43 | permissions:
44 | contents: read # for actions/checkout to fetch code
45 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
46 | runs-on: ubuntu-20.04
47 | steps:
48 | - name: Checkout repo
49 | uses: actions/checkout@v4
50 | with:
51 | fetch-depth: 0
52 |
53 | - name: Run Trivy vulnerability scanner in repo mode
54 | uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5
55 | with:
56 | scan-type: "fs"
57 | scan-ref: "."
58 | ignore-unfixed: true
59 | format: "template"
60 | template: "@/contrib/sarif.tpl"
61 | output: "trivy-repo-results.sarif"
62 | severity: "HIGH,CRITICAL"
63 |
64 | - name: Upload Trivy scan results to GitHub Security tab
65 | uses: github/codeql-action/upload-sarif@v3
66 | with:
67 | sarif_file: "trivy-repo-results.sarif"
68 |
69 | codeql-analyze:
70 | permissions:
71 | actions: read # for github/codeql-action/init to get workflow details
72 | contents: read # for actions/checkout to fetch code
73 | security-events: write # for github/codeql-action/autobuild to send a status report
74 | name: Analyze with CodeQL
75 | runs-on: ubuntu-20.04
76 |
77 | steps:
78 | - name: Checkout repository
79 | uses: actions/checkout@v4
80 |
81 | # Initializes the CodeQL tools for scanning.
82 | - name: Initialize CodeQL
83 | uses: github/codeql-action/init@v3
84 | with:
85 | config-file: ./.github/codeql-config.yml
86 | languages: javascript
87 |
88 | - name: Autobuild
89 | uses: github/codeql-action/autobuild@v3
90 |
91 | - name: Perform CodeQL Analysis
92 | uses: github/codeql-action/analyze@v3
93 |
--------------------------------------------------------------------------------
/.github/workflows/trivy-docker.yaml:
--------------------------------------------------------------------------------
1 | name: Trivy Nightly Docker Scan
2 |
3 | on:
4 | # Run scans if the workflow is modified, in order to test the
5 | # workflow itself. This results in some spurious notifications,
6 | # but seems okay for testing.
7 | pull_request:
8 | branches:
9 | - main
10 | paths:
11 | - .github/workflows/trivy-docker.yaml
12 |
13 | # Run scans against master whenever changes are merged.
14 | push:
15 | branches:
16 | - main
17 | paths:
18 | - .github/workflows/trivy-docker.yaml
19 |
20 | schedule:
21 | # Run at 10:15 am UTC (3:15am PT/5:15am CT)
22 | # Run at 0 minutes 0 hours of every day.
23 | - cron: "15 10 * * *"
24 |
25 | workflow_dispatch:
26 |
27 | permissions:
28 | actions: none
29 | checks: none
30 | contents: read
31 | deployments: none
32 | issues: none
33 | packages: none
34 | pull-requests: none
35 | repository-projects: none
36 | security-events: write
37 | statuses: none
38 |
39 | # Cancel in-progress runs for pull requests when developers push
40 | # additional changes, and serialize builds in branches.
41 | # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run
42 | concurrency:
43 | group: ${{ github.workflow }}-${{ github.ref }}
44 |
45 | jobs:
46 | trivy-scan-image:
47 | runs-on: ubuntu-20.04
48 |
49 | steps:
50 | - name: Checkout code
51 | uses: actions/checkout@v4
52 |
53 | - name: Run Trivy vulnerability scanner in image mode
54 | uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5
55 | with:
56 | image-ref: "docker.io/codercom/code-server:latest"
57 | ignore-unfixed: true
58 | format: "sarif"
59 | output: "trivy-image-results.sarif"
60 | severity: "HIGH,CRITICAL"
61 |
62 | - name: Upload Trivy scan results to GitHub Security tab
63 | uses: github/codeql-action/upload-sarif@v3
64 | with:
65 | sarif_file: "trivy-image-results.sarif"
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .tsbuildinfo
2 | .cache
3 | /out*/
4 | release/
5 | release-npm-package/
6 | release-standalone/
7 | release-packages/
8 | release-gcp/
9 | release-images/
10 | node_modules
11 | /plugins
12 | /lib/coder-cloud-agent
13 | .home
14 | coverage
15 | **/.DS_Store
16 |
17 | # Code packages itself here.
18 | /lib/vscode-reh-web-*
19 |
20 | # Failed e2e test videos are saved here
21 | test/test-results
22 |
23 | # Quilt's internal data.
24 | /.pc
25 | /patches/*.diff~
26 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/vscode"]
2 | path = lib/vscode
3 | url = https://github.com/microsoft/vscode
4 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 20.19.0
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | .node-version
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | lib/vscode
2 | lib/vscode-reh-web-linux-x64
3 | release-standalone
4 | release-packages
5 | release
6 | helm-chart
7 | test/scripts
8 | test/e2e/extensions/test-extension
9 | .pc
10 | package-lock.json
11 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | printWidth: 120
2 | semi: false
3 | trailingComma: all
4 | arrowParens: always
5 | singleQuote: false
6 | useTabs: false
7 |
--------------------------------------------------------------------------------
/.tours/start-development.tour:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://aka.ms/codetour-schema",
3 | "title": "Start Development",
4 | "steps": [
5 | {
6 | "file": "package.json",
7 | "line": 31,
8 | "description": "## Commands\n\nTo start developing, make sure you have Node 16+ and the [required dependencies](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites) installed. Then, run the following commands:\n\n1. Install dependencies:\n>> npm\n\n3. Start development mode (and watch for changes):\n>> npm run watch"
9 | },
10 | {
11 | "file": "src/node/app.ts",
12 | "line": 68,
13 | "description": "## Visit the web server\n\nIf all goes well, you should see something like this in your terminal. code-server should be live in development mode.\n\n---\n```bash\n[2020-12-09T21:03:37.156Z] info code-server 3.7.4 development\n[2020-12-09T21:03:37.157Z] info Using user-data-dir ~/.local/share/code-server\n[2020-12-09T21:03:37.165Z] info Using config file ~/.config/code-server/config.yaml\n[2020-12-09T21:03:37.165Z] info HTTP server listening on http://127.0.0.1:8080 \n[2020-12-09T21:03:37.165Z] info - Authentication is enabled\n[2020-12-09T21:03:37.165Z] info - Using password from ~/.config/code-server/config.yaml\n[2020-12-09T21:03:37.165Z] info - Not serving HTTPS\n```\n\n---\n\nIf you have the default configuration, you can access it at [http://localhost:8080](http://localhost:8080)."
14 | },
15 | {
16 | "file": "src/browser/pages/login.html",
17 | "line": 26,
18 | "description": "## Make a change\n\nThis is the login page, let's make a change and see it update on our web server! Perhaps change the text :)\n\n```html\n
Modifying the login page 👨🏼💻
\n```\n\nReminder, you can likely preview at [http://localhost:8080](http://localhost:8080)"
19 | },
20 | {
21 | "file": "src/node/app.ts",
22 | "line": 62,
23 | "description": "## That's it!\n\n\nThat's all there is to it! When this tour ends, your terminal session may stop, but just use `npm run watch` to start developing from here on out!\n\n\nIf you haven't already, be sure to check out these resources:\n- [Tour: Contributing](command:codetour.startTourByTitle?[\"Contributing\"])\n- [Docs: FAQ.md](https://github.com/coder/code-server/blob/main/docs/FAQ.md)\n- [Docs: CONTRIBUTING.md](https://github.com/coder/code-server/blob/main/docs/CONTRIBUTING.md)\n- [Community: GitHub Discussions](https://github.com/coder/code-server/discussions)\n- [Community: Slack](https://community.coder.com)"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2019 Coder Technologies Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/ThirdPartyNotices.txt:
--------------------------------------------------------------------------------
1 | code-server
2 |
3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
4 | Do Not Translate or Localize
5 |
6 | 1. Microsoft/vscode version 1.47.0 (https://github.com/Microsoft/vscode)
7 |
8 | %% Microsoft/vscode NOTICES AND INFORMATION BEGIN HERE
9 | =========================================
10 | MIT License
11 |
12 |
13 | Copyright (c) 2015 - present Microsoft Corporation
14 |
15 |
16 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17 |
18 |
19 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
20 |
21 |
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/ci/Caddyfile:
--------------------------------------------------------------------------------
1 | {
2 | admin localhost:4444
3 | }
4 | :8000 {
5 | @portLocalhost path_regexp port ^/([0-9]+)\/ide
6 | handle @portLocalhost {
7 | uri strip_prefix {re.port.1}/ide
8 | reverse_proxy localhost:{re.port.1}
9 | }
10 |
11 | handle {
12 | respond "Bad hostname" 400
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/ci/build/build-code-server.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | # Builds code-server into out and the frontend into dist.
5 |
6 | main() {
7 | cd "$(dirname "${0}")/../.."
8 |
9 | tsc
10 |
11 | # If out/node/entry.js does not already have the shebang,
12 | # we make sure to add it and make it executable.
13 | if ! grep -q -m1 "^#!/usr/bin/env node" out/node/entry.js; then
14 | sed -i.bak "1s;^;#!/usr/bin/env node\n;" out/node/entry.js && rm out/node/entry.js.bak
15 | chmod +x out/node/entry.js
16 | fi
17 | }
18 |
19 | main "$@"
20 |
--------------------------------------------------------------------------------
/ci/build/build-lib.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This is a library which contains functions used inside ci/build
4 | #
5 | # We separated it into it's own file so that we could easily unit test
6 | # these functions and helpers.
7 |
8 | # On some CPU architectures (notably node/uname "armv7l", default on Raspberry Pis),
9 | # different package managers have different labels for the same CPU (deb=armhf, rpm=armhfp).
10 | # This function returns the overriden arch on platforms
11 | # with alternate labels, or the same arch otherwise.
12 | get_nfpm_arch() {
13 | local PKG_FORMAT="${1:-}"
14 | local ARCH="${2:-}"
15 |
16 | case "$ARCH" in
17 | armv7l)
18 | if [ "$PKG_FORMAT" = "deb" ]; then
19 | echo armhf
20 | elif [ "$PKG_FORMAT" = "rpm" ]; then
21 | echo armhfp
22 | fi
23 | ;;
24 | *)
25 | echo "$ARCH"
26 | ;;
27 | esac
28 | }
29 |
--------------------------------------------------------------------------------
/ci/build/build-packages.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | # Given a platform-specific release found in ./release-standalone, generate an
5 | # compressed archives and bundles (as appropriate for the platform) named after
6 | # the platform's architecture and OS and place them in ./release-packages and
7 | # ./release-gcp.
8 |
9 | main() {
10 | cd "$(dirname "${0}")/../.."
11 | source ./ci/lib.sh
12 | source ./ci/build/build-lib.sh
13 |
14 | # Allow us to override architecture
15 | # we use this for our Linux ARM64 cross compile builds
16 | if [ "$#" -eq 1 ] && [ "$1" ]; then
17 | ARCH=$1
18 | fi
19 |
20 | mkdir -p release-packages
21 |
22 | release_archive
23 |
24 | if [[ $OS == "linux" ]]; then
25 | release_nfpm
26 | fi
27 | }
28 |
29 | release_archive() {
30 | local release_name="code-server-$VERSION-$OS-$ARCH"
31 | if [[ $OS == "linux" ]]; then
32 | tar -czf "release-packages/$release_name.tar.gz" --owner=0 --group=0 --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
33 | else
34 | tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
35 | fi
36 |
37 | echo "done (release-packages/$release_name)"
38 |
39 | release_gcp
40 | }
41 |
42 | release_gcp() {
43 | mkdir -p "release-gcp/$VERSION"
44 | cp "release-packages/$release_name.tar.gz" "./release-gcp/$VERSION/$OS-$ARCH.tar.gz"
45 | mkdir -p "release-gcp/latest"
46 | cp "./release-packages/$release_name.tar.gz" "./release-gcp/latest/$OS-$ARCH.tar.gz"
47 | }
48 |
49 | # Generates deb and rpm packages.
50 | release_nfpm() {
51 | local nfpm_config
52 |
53 | export NFPM_ARCH
54 |
55 | PKG_FORMAT="deb"
56 | NFPM_ARCH="$(get_nfpm_arch $PKG_FORMAT "$ARCH")"
57 | nfpm_config="$(envsubst < ./ci/build/nfpm.yaml)"
58 | echo "Building deb"
59 | echo "$nfpm_config" | head --lines=4
60 | nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server_${VERSION}_${NFPM_ARCH}.deb"
61 |
62 | PKG_FORMAT="rpm"
63 | NFPM_ARCH="$(get_nfpm_arch $PKG_FORMAT "$ARCH")"
64 | nfpm_config="$(envsubst < ./ci/build/nfpm.yaml)"
65 | echo "Building rpm"
66 | echo "$nfpm_config" | head --lines=4
67 | nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server-$VERSION-$NFPM_ARCH.rpm"
68 | }
69 |
70 | main "$@"
71 |
--------------------------------------------------------------------------------
/ci/build/build-standalone-release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | # Once we have an NPM package, use this script to copy it to a separate
5 | # directory (./release-standalone) and install the dependencies. This new
6 | # directory can then be packaged as a platform-specific release.
7 |
8 | main() {
9 | cd "$(dirname "${0}")/../.."
10 |
11 | source ./ci/lib.sh
12 |
13 | rsync "$RELEASE_PATH/" "$RELEASE_PATH-standalone"
14 | RELEASE_PATH+=-standalone
15 |
16 | # Package managers may shim their own "node" wrapper into the PATH, so run
17 | # node and ask it for its true path.
18 | local node_path
19 | node_path="$(node <<< 'console.info(process.execPath)')"
20 |
21 | mkdir -p "$RELEASE_PATH/bin"
22 | mkdir -p "$RELEASE_PATH/lib"
23 | rsync ./ci/build/code-server.sh "$RELEASE_PATH/bin/code-server"
24 | rsync "$node_path" "$RELEASE_PATH/lib/node"
25 |
26 | chmod 755 "$RELEASE_PATH/lib/node"
27 |
28 | pushd "$RELEASE_PATH"
29 | npm install --unsafe-perm --omit=dev
30 | # Code deletes some files from the extension node_modules directory which
31 | # leaves broken symlinks in the corresponding .bin directory. nfpm will fail
32 | # on these broken symlinks so clean them up.
33 | rm -fr "./lib/vscode/extensions/node_modules/.bin"
34 | popd
35 | }
36 |
37 | main "$@"
38 |
--------------------------------------------------------------------------------
/ci/build/clean.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | cd "$(dirname "${0}")/../.."
6 | source ./ci/lib.sh
7 |
8 | git clean -Xffd
9 | }
10 |
11 | main "$@"
12 |
--------------------------------------------------------------------------------
/ci/build/code-server-nfpm.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | exec /usr/lib/code-server/bin/code-server "$@"
4 |
--------------------------------------------------------------------------------
/ci/build/code-server-user.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=code-server
3 | After=network.target
4 |
5 | [Service]
6 | Type=exec
7 | ExecStart=/usr/bin/code-server
8 | Restart=always
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------
/ci/build/code-server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | # This script is intended to be bundled into the standalone releases.
5 | # Runs code-server with the bundled node binary.
6 |
7 | _realpath() {
8 | # See https://github.com/coder/code-server/issues/1537 on why no realpath or readlink -f.
9 |
10 | script="$1"
11 | cd "$(dirname "$script")"
12 |
13 | while [ -L "$(basename "$script")" ]; do
14 | script="$(readlink "$(basename "$script")")"
15 | cd "$(dirname "$script")"
16 | done
17 |
18 | echo "$PWD/$(basename "$script")"
19 | }
20 |
21 | root() {
22 | script="$(_realpath "$0")"
23 | bin_dir="$(dirname "$script")"
24 | dirname "$bin_dir"
25 | }
26 |
27 | ROOT="$(root)"
28 | exec "$ROOT/lib/node" "$ROOT" "$@"
29 |
--------------------------------------------------------------------------------
/ci/build/code-server@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=code-server
3 | After=network.target
4 |
5 | [Service]
6 | Type=exec
7 | ExecStart=/usr/bin/code-server
8 | Restart=always
9 | User=%i
10 |
11 | [Install]
12 | WantedBy=default.target
13 |
--------------------------------------------------------------------------------
/ci/build/nfpm.yaml:
--------------------------------------------------------------------------------
1 | name: "code-server"
2 | arch: "${NFPM_ARCH}"
3 | platform: "linux"
4 | version: "v${VERSION}"
5 | section: "devel"
6 | priority: "optional"
7 | maintainer: "Joe Previte "
8 | description: |
9 | Run VS Code in the browser.
10 | vendor: "Coder"
11 | homepage: "https://github.com/coder/code-server"
12 | license: "MIT"
13 |
14 | contents:
15 | - src: ./ci/build/code-server-nfpm.sh
16 | dst: /usr/bin/code-server
17 |
18 | - src: ./ci/build/code-server@.service
19 | dst: /usr/lib/systemd/system/code-server@.service
20 |
21 | - src: ./ci/build/code-server-user.service
22 | dst: /usr/lib/systemd/user/code-server.service
23 |
24 | - src: ./release-standalone/*
25 | dst: /usr/lib/code-server
26 |
--------------------------------------------------------------------------------
/ci/dev/doctoc.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | cd "$(dirname "$0")/../.."
6 |
7 | doctoc --title '# FAQ' docs/FAQ.md > /dev/null
8 | doctoc --title '# Setup Guide' docs/guide.md > /dev/null
9 | doctoc --title '# Install' docs/install.md > /dev/null
10 | doctoc --title '# npm Install Requirements' docs/npm.md > /dev/null
11 | doctoc --title '# Contributing' docs/CONTRIBUTING.md > /dev/null
12 | doctoc --title '# Maintaining' docs/MAINTAINING.md > /dev/null
13 | doctoc --title '# Contributor Covenant Code of Conduct' docs/CODE_OF_CONDUCT.md > /dev/null
14 | doctoc --title '# iPad' docs/ipad.md > /dev/null
15 | doctoc --title '# Termux' docs/termux.md > /dev/null
16 |
17 | if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
18 | echo "Files need generation or are formatted incorrectly:"
19 | git -c color.ui=always status | grep --color=no '\[31m'
20 | echo "Please run the following locally:"
21 | echo " npm run doctoc"
22 | exit 1
23 | fi
24 | }
25 |
26 | main "$@"
27 |
--------------------------------------------------------------------------------
/ci/dev/gen_icons.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | main() {
5 | cd src/browser/media
6 |
7 | # We need .ico for backwards compatibility.
8 | # The other two are the only icon sizes required by Chrome and
9 | # we use them for stuff like apple-touch-icon as well.
10 | # https://web.dev/add-manifest/
11 | #
12 | # This should be enough and we can always add more if there are problems.
13 |
14 | # -background defaults to white but we want it transparent.
15 | # https://imagemagick.org/script/command-line-options.php#background
16 | convert -quiet -background transparent -resize 256x256 favicon.svg favicon.ico
17 | # We do not generate the pwa-icon from the favicon as they are slightly different
18 | # designs and sizes.
19 | # See favicon.afdesign and #2401 for details on the differences.
20 | convert -quiet -background transparent -resize 192x192 pwa-icon.png pwa-icon-192.png
21 | convert -quiet -background transparent -resize 512x512 pwa-icon.png pwa-icon-512.png
22 |
23 | # We use -quiet above to avoid https://github.com/ImageMagick/ImageMagick/issues/884
24 |
25 | # The following adds dark mode support for the favicon as favicon-dark-support.svg
26 | # There is no similar capability for pwas or .ico so we can only add support to the svg.
27 | favicon_dark_style=""
34 | # See https://stackoverflow.com/a/22901380/4283659
35 | # This escapes all newlines so that sed will accept them.
36 | favicon_dark_style="$(printf "%s\n" "$favicon_dark_style" | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/\\n/g')"
37 | sed "$(
38 | cat -n << EOF
39 | s% favicon-dark-support.svg
42 | }
43 |
44 | main "$@"
45 |
--------------------------------------------------------------------------------
/ci/dev/lint-scripts.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | cd "$(dirname "$0")/../.."
6 | shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files '*.sh' | grep -v 'lib/vscode')
7 | }
8 |
9 | main "$@"
10 |
--------------------------------------------------------------------------------
/ci/dev/postinstall.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | # Install dependencies in $1.
5 | install-deps() {
6 | local args=()
7 | if [[ ${CI-} ]]; then
8 | args+=(ci)
9 | else
10 | args+=(install)
11 | fi
12 | # If there is no package.json then npm will look upward and end up installing
13 | # from the root resulting in an infinite loop (this can happen if you have not
14 | # checked out the submodule yet for example).
15 | if [[ ! -f "$1/package.json" ]]; then
16 | echo "$1/package.json is missing; did you run git submodule update --init?"
17 | exit 1
18 | fi
19 | pushd "$1"
20 | echo "Installing dependencies for $PWD"
21 | npm "${args[@]}"
22 | popd
23 | }
24 |
25 | main() {
26 | cd "$(dirname "$0")/../.."
27 | source ./ci/lib.sh
28 |
29 | install-deps test
30 | install-deps test/e2e/extensions/test-extension
31 | # We don't need these when running the integration tests
32 | # so you can pass SKIP_SUBMODULE_DEPS
33 | if [[ ! ${SKIP_SUBMODULE_DEPS-} ]]; then
34 | install-deps lib/vscode
35 | fi
36 | }
37 |
38 | main "$@"
39 |
--------------------------------------------------------------------------------
/ci/dev/preinstall.js:
--------------------------------------------------------------------------------
1 | if (process.env.npm_execpath.includes("yarn")) {
2 | throw new Error("`yarn` is no longer supported; please use `npm install` instead")
3 | }
4 |
--------------------------------------------------------------------------------
/ci/dev/test-e2e.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | help() {
5 | echo >&2 " You can build with 'npm run watch' or you can build a release"
6 | echo >&2 " For example: 'npm run build && npm run build:vscode && KEEP_MODULES=1 npm run release'"
7 | echo >&2 " Then 'CODE_SERVER_TEST_ENTRY=./release npm run test:e2e'"
8 | echo >&2 " You can manually run that release with 'node ./release'"
9 | }
10 |
11 | main() {
12 | cd "$(dirname "$0")/../.."
13 |
14 | source ./ci/lib.sh
15 |
16 | pushd test/e2e/extensions/test-extension
17 | echo "Building test extension"
18 | npm run build
19 | popd
20 |
21 | local dir="$PWD"
22 | if [[ ! ${CODE_SERVER_TEST_ENTRY-} ]]; then
23 | echo "Set CODE_SERVER_TEST_ENTRY to test another build of code-server"
24 | else
25 | pushd "$CODE_SERVER_TEST_ENTRY"
26 | dir="$PWD"
27 | popd
28 | fi
29 |
30 | echo "Testing build in '$dir'"
31 |
32 | # Simple sanity checks to see that we've built. There could still be things
33 | # wrong (native modules version issues, incomplete build, etc).
34 | if [[ ! -d $dir/out ]]; then
35 | echo >&2 "No code-server build detected"
36 | help
37 | exit 1
38 | fi
39 |
40 | if [[ ! -d $dir/lib/vscode/out ]]; then
41 | echo >&2 "No VS Code build detected"
42 | help
43 | exit 1
44 | fi
45 |
46 | cd test
47 | ./node_modules/.bin/playwright test "$@"
48 | }
49 |
50 | main "$@"
51 |
--------------------------------------------------------------------------------
/ci/dev/test-integration.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | help() {
5 | echo >&2 " You can build the standalone release with 'npm run release:standalone'"
6 | echo >&2 " Or you can pass in a custom path."
7 | echo >&2 " CODE_SERVER_PATH='/var/tmp/coder/code-server/bin/code-server' npm run test:integration"
8 | }
9 |
10 | # Make sure a code-server release works. You can pass in the path otherwise it
11 | # will look for release-standalone in the current directory.
12 | #
13 | # This is to make sure we don't have Node version errors or any other
14 | # compilation-related errors.
15 | main() {
16 | cd "$(dirname "$0")/../.."
17 |
18 | source ./ci/lib.sh
19 |
20 | local path="$RELEASE_PATH-standalone/bin/code-server"
21 | if [[ ! ${CODE_SERVER_PATH-} ]]; then
22 | echo "Set CODE_SERVER_PATH to test another build of code-server"
23 | else
24 | path="$CODE_SERVER_PATH"
25 | fi
26 |
27 | echo "Running tests with code-server binary: '$path'"
28 |
29 | if [[ ! -f $path ]]; then
30 | echo >&2 "No code-server build detected"
31 | echo >&2 "Looked in $path"
32 | help
33 | exit 1
34 | fi
35 |
36 | CODE_SERVER_PATH="$path" ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration" --testPathIgnorePatterns "./test/integration/fixtures"
37 | }
38 |
39 | main "$@"
40 |
--------------------------------------------------------------------------------
/ci/dev/test-native.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | help() {
5 | echo >&2 " You can build the standalone release with 'npm run release:standalone'"
6 | echo >&2 " Or you can pass in a custom path."
7 | echo >&2 " CODE_SERVER_PATH='/var/tmp/coder/code-server/bin/code-server' npm run test:integration"
8 | }
9 |
10 | # Make sure a code-server release works. You can pass in the path otherwise it
11 | # will look for release-standalone in the current directory.
12 | #
13 | # This is to make sure we don't have Node version errors or any other
14 | # compilation-related errors.
15 | main() {
16 | cd "$(dirname "$0")/../.."
17 |
18 | source ./ci/lib.sh
19 |
20 | local path="$RELEASE_PATH-standalone/bin/code-server"
21 | if [[ ! ${CODE_SERVER_PATH-} ]]; then
22 | echo "Set CODE_SERVER_PATH to test another build of code-server"
23 | else
24 | path="$CODE_SERVER_PATH"
25 | fi
26 |
27 | echo "Running tests with code-server binary: '$path'"
28 |
29 | if [[ ! -f $path ]]; then
30 | echo >&2 "No code-server build detected"
31 | echo >&2 "Looked in $path"
32 | help
33 | exit 1
34 | fi
35 |
36 | CODE_SERVER_PATH="$path" ./test/node_modules/.bin/jest "$@" --coverage=false --testRegex "./test/integration/help.test.ts"
37 | }
38 |
39 | main "$@"
40 |
--------------------------------------------------------------------------------
/ci/dev/test-scripts.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | cd "$(dirname "$0")/../.."
6 | bats ./test/scripts
7 | }
8 |
9 | main "$@"
10 |
--------------------------------------------------------------------------------
/ci/dev/test-unit.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | cd "$(dirname "$0")/../.."
6 |
7 | source ./ci/lib.sh
8 |
9 | # We must keep jest in a sub-directory. See ../../test/package.json for more
10 | # information. We must also run it from the root otherwise coverage will not
11 | # include our source files.
12 | ./test/node_modules/.bin/jest "$@" --testRegex "./test/unit/.*ts"
13 | }
14 |
15 | main "$@"
16 |
--------------------------------------------------------------------------------
/ci/helm-chart/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/ci/helm-chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: code-server
3 | description: A Helm chart for coder/code-server
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 3.27.2
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | appVersion: 4.100.2
24 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. Get the application URL by running these commands:
2 | {{- if .Values.ingress.enabled }}
3 | {{- range $host := .Values.ingress.hosts }}
4 | {{- range .paths }}
5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
6 | {{- end }}
7 | {{- end }}
8 | {{- else if contains "NodePort" .Values.service.type }}
9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "code-server.fullname" . }})
10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
11 | echo http://$NODE_IP:$NODE_PORT
12 | {{- else if contains "LoadBalancer" .Values.service.type }}
13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available.
14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "code-server.fullname" . }}'
15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
16 | echo http://$SERVICE_IP:{{ .Values.service.port }}
17 | {{- else if contains "ClusterIP" .Values.service.type }}
18 | echo "Visit http://127.0.0.1:8080 to use your application"
19 | kubectl port-forward --namespace {{ .Release.Namespace }} service/{{ include "code-server.fullname" . }} 8080:http
20 | {{- end }}
21 |
22 | Administrator credentials:
23 |
24 | Password: echo $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode)
25 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/* vim: set filetype=mustache: */}}
2 | {{/*
3 | Expand the name of the chart.
4 | */}}
5 | {{- define "code-server.name" -}}
6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
7 | {{- end -}}
8 |
9 | {{/*
10 | Create a default fully qualified app name.
11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12 | If release name contains chart name it will be used as a full name.
13 | */}}
14 | {{- define "code-server.fullname" -}}
15 | {{- if .Values.fullnameOverride -}}
16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
17 | {{- else -}}
18 | {{- $name := default .Chart.Name .Values.nameOverride -}}
19 | {{- if contains $name .Release.Name -}}
20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}}
21 | {{- else -}}
22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
23 | {{- end -}}
24 | {{- end -}}
25 | {{- end -}}
26 |
27 | {{/*
28 | Create chart name and version as used by the chart label.
29 | */}}
30 | {{- define "code-server.chart" -}}
31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
32 | {{- end -}}
33 |
34 | {{/*
35 | Common labels
36 | */}}
37 | {{- define "code-server.labels" -}}
38 | helm.sh/chart: {{ include "code-server.chart" . }}
39 | {{ include "code-server.selectorLabels" . }}
40 | {{- if .Chart.AppVersion }}
41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
42 | {{- end }}
43 | app.kubernetes.io/managed-by: {{ .Release.Service }}
44 | {{- end }}
45 |
46 | {{/*
47 | Selector labels
48 | */}}
49 | {{- define "code-server.selectorLabels" -}}
50 | app.kubernetes.io/name: {{ include "code-server.name" . }}
51 | app.kubernetes.io/instance: {{ .Release.Name }}
52 | {{- end }}
53 |
54 | {{/*
55 | Create the name of the service account to use
56 | */}}
57 | {{- define "code-server.serviceAccountName" -}}
58 | {{- if .Values.serviceAccount.create -}}
59 | {{ default (include "code-server.fullname" .) .Values.serviceAccount.name }}
60 | {{- else -}}
61 | {{ default "default" .Values.serviceAccount.name }}
62 | {{- end -}}
63 | {{- end -}}
64 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | {{- $fullName := include "code-server.fullname" . -}}
3 | {{- $svcPort := .Values.service.port -}}
4 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion -}}
5 | apiVersion: networking.k8s.io/v1
6 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
7 | apiVersion: networking.k8s.io/v1beta1
8 | {{- else -}}
9 | apiVersion: extensions/v1beta1
10 | {{- end }}
11 | kind: Ingress
12 | metadata:
13 | name: {{ $fullName }}
14 | labels:
15 | {{- include "code-server.labels" . | nindent 4 }}
16 | {{- with .Values.ingress.annotations }}
17 | annotations:
18 | {{- toYaml . | nindent 4 }}
19 | {{- end }}
20 | spec:
21 | {{- if .Values.ingress.ingressClassName }}
22 | ingressClassName: {{ .Values.ingress.ingressClassName }}
23 | {{- end }}
24 | {{- if .Values.ingress.tls }}
25 | tls:
26 | {{- range .Values.ingress.tls }}
27 | - hosts:
28 | {{- range .hosts }}
29 | - {{ . | quote }}
30 | {{- end }}
31 | secretName: {{ .secretName }}
32 | {{- end }}
33 | {{- end }}
34 | rules:
35 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion -}}
36 | {{- range .Values.ingress.hosts }}
37 | - host: {{ .host | quote }}
38 | http:
39 | paths:
40 | {{- range .paths }}
41 | - path: {{ . }}
42 | pathType: Prefix
43 | backend:
44 | service:
45 | name: {{ $fullName }}
46 | port:
47 | number: {{ $svcPort }}
48 | {{- end }}
49 | {{- end }}
50 | {{- else -}}
51 | {{- range .Values.ingress.hosts }}
52 | - host: {{ .host | quote }}
53 | http:
54 | paths:
55 | {{- range .paths }}
56 | - path: {{ . }}
57 | backend:
58 | serviceName: {{ $fullName }}
59 | servicePort: {{ $svcPort }}
60 | {{- end }}
61 | {{- end }}
62 | {{- end }}
63 | {{- end }}
--------------------------------------------------------------------------------
/ci/helm-chart/templates/pvc.yaml:
--------------------------------------------------------------------------------
1 | {{- if and (and .Values.persistence.enabled (not .Values.persistence.existingClaim)) (not .Values.persistence.hostPath) }}
2 | kind: PersistentVolumeClaim
3 | apiVersion: v1
4 | metadata:
5 | name: {{ include "code-server.fullname" . }}
6 | namespace: {{ .Release.Namespace }}
7 | {{- with .Values.persistence.annotations }}
8 | annotations:
9 | {{ toYaml . | indent 4 }}
10 | {{- end }}
11 | labels:
12 | app.kubernetes.io/name: {{ include "code-server.name" . }}
13 | helm.sh/chart: {{ include "code-server.chart" . }}
14 | app.kubernetes.io/instance: {{ .Release.Name }}
15 | app.kubernetes.io/managed-by: {{ .Release.Service }}
16 | spec:
17 | accessModes:
18 | - {{ .Values.persistence.accessMode | quote }}
19 | resources:
20 | requests:
21 | storage: {{ .Values.persistence.size | quote }}
22 | {{- if .Values.persistence.storageClass }}
23 | {{- if (eq "-" .Values.persistence.storageClass) }}
24 | storageClassName: ""
25 | {{- else }}
26 | storageClassName: "{{ .Values.persistence.storageClass }}"
27 | {{- end }}
28 | {{- end }}
29 | {{- end }}
30 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/secrets.yaml:
--------------------------------------------------------------------------------
1 | {{- if not .Values.existingSecret }}
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: {{ include "code-server.fullname" . }}
6 | annotations:
7 | "helm.sh/hook": "pre-install"
8 | labels:
9 | app.kubernetes.io/name: {{ include "code-server.name" . }}
10 | helm.sh/chart: {{ include "code-server.chart" . }}
11 | app.kubernetes.io/instance: {{ .Release.Name }}
12 | app.kubernetes.io/managed-by: {{ .Release.Service }}
13 | type: Opaque
14 | data:
15 | {{- if .Values.password }}
16 | password: "{{ .Values.password | b64enc }}"
17 | {{- else }}
18 | password: "{{ randAlphaNum 24 | b64enc }}"
19 | {{- end }}
20 | {{- end }}
21 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "code-server.fullname" . }}
5 | labels:
6 | app.kubernetes.io/name: {{ include "code-server.name" . }}
7 | helm.sh/chart: {{ include "code-server.chart" . }}
8 | app.kubernetes.io/instance: {{ .Release.Name }}
9 | app.kubernetes.io/managed-by: {{ .Release.Service }}
10 | spec:
11 | type: {{ .Values.service.type }}
12 | ports:
13 | - port: {{ .Values.service.port }}
14 | targetPort: http
15 | protocol: TCP
16 | name: http
17 | {{- range .Values.extraPorts }}
18 | - port: {{ .port }}
19 | targetPort: {{ .port }}
20 | protocol: {{ .protocol }}
21 | name: {{ .name }}
22 | {{- end }}
23 | selector:
24 | app.kubernetes.io/name: {{ include "code-server.name" . }}
25 | app.kubernetes.io/instance: {{ .Release.Name }}
26 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if or .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | labels:
6 | app.kubernetes.io/name: {{ include "code-server.name" . }}
7 | helm.sh/chart: {{ include "code-server.chart" . }}
8 | app.kubernetes.io/instance: {{ .Release.Name }}
9 | app.kubernetes.io/managed-by: {{ .Release.Service }}
10 | name: {{ template "code-server.serviceAccountName" . }}
11 | {{- end -}}
12 |
--------------------------------------------------------------------------------
/ci/helm-chart/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "code-server.fullname" . }}-test-connection"
5 | labels:
6 | app.kubernetes.io/name: {{ include "code-server.name" . }}
7 | helm.sh/chart: {{ include "code-server.chart" . }}
8 | app.kubernetes.io/instance: {{ .Release.Name }}
9 | app.kubernetes.io/managed-by: {{ .Release.Service }}
10 | annotations:
11 | "helm.sh/hook": test-success
12 | spec:
13 | containers:
14 | - name: wget
15 | image: busybox
16 | command: ['wget']
17 | args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}']
18 | restartPolicy: Never
19 |
--------------------------------------------------------------------------------
/ci/lib.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | pushd() {
5 | builtin pushd "$@" > /dev/null
6 | }
7 |
8 | popd() {
9 | builtin popd > /dev/null
10 | }
11 |
12 | vscode_version() {
13 | jq -r .version lib/vscode/package.json
14 | }
15 |
16 | os() {
17 | osname=$(uname | tr '[:upper:]' '[:lower:]')
18 | case $osname in
19 | linux)
20 | # Alpine's ldd doesn't have a version flag but if you use an invalid flag
21 | # (like --version) it outputs the version to stderr and exits with 1.
22 | # TODO: Better to check /etc/os-release; see ../install.sh.
23 | ldd_output=$(ldd --version 2>&1 || true)
24 | if echo "$ldd_output" | grep -iq musl; then
25 | osname="alpine"
26 | fi
27 | ;;
28 | darwin) osname="macos" ;;
29 | cygwin* | mingw*) osname="windows" ;;
30 | esac
31 | echo "$osname"
32 | }
33 |
34 | arch() {
35 | cpu="$(uname -m)"
36 | case "$cpu" in
37 | aarch64) cpu=arm64 ;;
38 | x86_64) cpu=amd64 ;;
39 | esac
40 | echo "$cpu"
41 | }
42 |
43 | rsync() {
44 | command rsync -a --del "$@"
45 | }
46 |
47 | ARCH="$(arch)"
48 | export ARCH
49 | OS=$(os)
50 | export OS
51 |
52 | # RELEASE_PATH is the destination directory for the release from the root.
53 | # Defaults to release
54 | RELEASE_PATH="${RELEASE_PATH-release}"
55 |
--------------------------------------------------------------------------------
/ci/release-image/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:experimental
2 |
3 | ARG BASE=debian:12
4 | FROM scratch AS packages
5 | COPY release-packages/code-server*.deb /tmp/
6 |
7 | FROM $BASE
8 |
9 | RUN apt-get update \
10 | && apt-get install -y \
11 | curl \
12 | dumb-init \
13 | git \
14 | git-lfs \
15 | htop \
16 | locales \
17 | lsb-release \
18 | man-db \
19 | nano \
20 | openssh-client \
21 | procps \
22 | sudo \
23 | vim-tiny \
24 | wget \
25 | zsh \
26 | && git lfs install \
27 | && rm -rf /var/lib/apt/lists/*
28 |
29 | # https://wiki.debian.org/Locale#Manually
30 | RUN sed -i "s/# en_US.UTF-8/en_US.UTF-8/" /etc/locale.gen \
31 | && locale-gen
32 | ENV LANG=en_US.UTF-8
33 |
34 | RUN if grep -q 1000 /etc/passwd; then \
35 | userdel -r "$(id -un 1000)"; \
36 | fi \
37 | && adduser --gecos '' --disabled-password coder \
38 | && echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
39 |
40 | RUN ARCH="$(dpkg --print-architecture)" \
41 | && curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - \
42 | && chown root:root /usr/local/bin/fixuid \
43 | && chmod 4755 /usr/local/bin/fixuid \
44 | && mkdir -p /etc/fixuid \
45 | && printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
46 |
47 | COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
48 | RUN --mount=from=packages,src=/tmp,dst=/tmp/packages dpkg -i /tmp/packages/code-server*$(dpkg --print-architecture).deb
49 |
50 | # Allow users to have scripts run on container startup to prepare workspace.
51 | # https://github.com/coder/code-server/issues/5177
52 | ENV ENTRYPOINTD=${HOME}/entrypoint.d
53 |
54 | EXPOSE 8080
55 | # This way, if someone sets $DOCKER_USER, docker-exec will still work as
56 | # the uid will remain the same. note: only relevant if -u isn't passed to
57 | # docker-run.
58 | USER 1000
59 | ENV USER=coder
60 | WORKDIR /home/coder
61 | ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
62 |
--------------------------------------------------------------------------------
/ci/release-image/Dockerfile.fedora:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:experimental
2 |
3 | ARG BASE=fedora:39
4 | FROM scratch AS packages
5 | COPY release-packages/code-server*.rpm /tmp/
6 |
7 | FROM $BASE
8 |
9 | RUN dnf update -y \
10 | && dnf install -y \
11 | curl \
12 | git \
13 | git-lfs \
14 | htop \
15 | nano \
16 | openssh-clients \
17 | procps \
18 | wget \
19 | zsh \
20 | dumb-init \
21 | glibc-langpack-en \
22 | && rm -rf /var/cache/dnf
23 | RUN git lfs install
24 |
25 | ENV LANG=en_US.UTF-8
26 | RUN echo 'LANG="en_US.UTF-8"' > /etc/locale.conf
27 |
28 | RUN useradd -u 1000 coder && echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
29 |
30 | RUN ARCH="$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g')" \
31 | && curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - \
32 | && chown root:root /usr/local/bin/fixuid \
33 | && chmod 4755 /usr/local/bin/fixuid \
34 | && mkdir -p /etc/fixuid \
35 | && printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
36 |
37 | COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
38 | RUN --mount=from=packages,src=/tmp,dst=/tmp/packages rpm -i /tmp/packages/code-server*$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g').rpm
39 |
40 | # Allow users to have scripts run on container startup to prepare workspace.
41 | # https://github.com/coder/code-server/issues/5177
42 | ENV ENTRYPOINTD=${HOME}/entrypoint.d
43 |
44 | EXPOSE 8080
45 | # This way, if someone sets $DOCKER_USER, docker-exec will still work as
46 | # the uid will remain the same. note: only relevant if -u isn't passed to
47 | # docker-run.
48 | USER 1000
49 | ENV USER=coder
50 | WORKDIR /home/coder
51 | ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
52 |
--------------------------------------------------------------------------------
/ci/release-image/Dockerfile.opensuse:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:experimental
2 |
3 | ARG BASE=opensuse/tumbleweed
4 | FROM scratch AS packages
5 | COPY release-packages/code-server*.rpm /tmp/
6 |
7 | FROM $BASE
8 |
9 | RUN zypper dup -y \
10 | && zypper in -y \
11 | curl \
12 | git \
13 | git-lfs \
14 | htop \
15 | nano \
16 | openssh-clients \
17 | procps \
18 | wget \
19 | zsh \
20 | sudo \
21 | catatonit \
22 | && rm -rf /var/cache/zypp /var/cache/zypper
23 | RUN git lfs install
24 |
25 | ENV LANG=en_US.UTF-8
26 | RUN echo 'LANG="en_US.UTF-8"' > /etc/locale.conf
27 |
28 | RUN useradd -u 1000 coder && echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
29 |
30 | RUN ARCH="$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g')" \
31 | && curl -fsSL "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - \
32 | && chown root:root /usr/local/bin/fixuid \
33 | && chmod 4755 /usr/local/bin/fixuid \
34 | && mkdir -p /etc/fixuid \
35 | && printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
36 |
37 | COPY ci/release-image/entrypoint-catatonit.sh /usr/bin/entrypoint-catatonit.sh
38 | RUN --mount=from=packages,src=/tmp,dst=/tmp/packages rpm -i /tmp/packages/code-server*$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g').rpm
39 |
40 | # Allow users to have scripts run on container startup to prepare workspace.
41 | # https://github.com/coder/code-server/issues/5177
42 | ENV ENTRYPOINTD=${HOME}/entrypoint.d
43 |
44 | EXPOSE 8080
45 | # This way, if someone sets $DOCKER_USER, docker-exec will still work as
46 | # the uid will remain the same. note: only relevant if -u isn't passed to
47 | # docker-run.
48 | USER 1000
49 | ENV USER=coder
50 | WORKDIR /home/coder
51 | ENTRYPOINT ["/usr/bin/entrypoint-catatonit.sh", "--bind-addr", "0.0.0.0:8080", "."]
52 |
--------------------------------------------------------------------------------
/ci/release-image/docker-bake.hcl:
--------------------------------------------------------------------------------
1 | # Use this file from the top of the repo, with `-f ci/release-image/docker-bake.hcl`
2 |
3 | # Uses env var VERSION if set;
4 | # normally, this is set by ci/lib.sh
5 | variable "VERSION" {
6 | default = "latest"
7 | }
8 |
9 | variable "DOCKER_REGISTRY" {
10 | default = "docker.io/codercom/code-server"
11 | }
12 |
13 | variable "GITHUB_REGISTRY" {
14 | default = "ghcr.io/coder/code-server"
15 | }
16 |
17 | group "default" {
18 | targets = [
19 | "code-server-debian-12",
20 | "code-server-ubuntu-focal",
21 | "code-server-ubuntu-noble",
22 | "code-server-fedora-39",
23 | "code-server-opensuse-tumbleweed",
24 | ]
25 | }
26 |
27 | function "prepend_hyphen_if_not_null" {
28 | params = [tag]
29 | result = notequal("","${tag}") ? "-${tag}" : "${tag}"
30 | }
31 |
32 | # use empty tag (tag="") to generate default tags
33 | function "gen_tags" {
34 | params = [registry, tag]
35 | result = notequal("","${registry}") ? [
36 | notequal("", "${tag}") ? "${registry}:${tag}" : "${registry}:latest",
37 | notequal("latest",VERSION) ? "${registry}:${VERSION}${prepend_hyphen_if_not_null(tag)}" : "",
38 | ] : []
39 | }
40 |
41 | # helper function to generate tags for docker registry and github registry.
42 | # set (DOCKER|GITHUB)_REGISTRY="" to disable corresponding registry
43 | function "gen_tags_for_docker_and_ghcr" {
44 | params = [tag]
45 | result = concat(
46 | gen_tags("${DOCKER_REGISTRY}", "${tag}"),
47 | gen_tags("${GITHUB_REGISTRY}", "${tag}"),
48 | )
49 | }
50 |
51 | target "code-server-debian-12" {
52 | dockerfile = "ci/release-image/Dockerfile"
53 | tags = concat(
54 | gen_tags_for_docker_and_ghcr(""),
55 | gen_tags_for_docker_and_ghcr("debian"),
56 | gen_tags_for_docker_and_ghcr("bookworm"),
57 | )
58 | platforms = ["linux/amd64", "linux/arm64"]
59 | }
60 |
61 | target "code-server-ubuntu-focal" {
62 | dockerfile = "ci/release-image/Dockerfile"
63 | tags = concat(
64 | gen_tags_for_docker_and_ghcr("ubuntu"),
65 | gen_tags_for_docker_and_ghcr("focal"),
66 | )
67 | args = {
68 | BASE = "ubuntu:focal"
69 | }
70 | platforms = ["linux/amd64", "linux/arm64"]
71 | }
72 |
73 | target "code-server-ubuntu-noble" {
74 | dockerfile = "ci/release-image/Dockerfile"
75 | tags = concat(
76 | gen_tags_for_docker_and_ghcr("noble"),
77 | )
78 | args = {
79 | BASE = "ubuntu:noble"
80 | }
81 | platforms = ["linux/amd64", "linux/arm64"]
82 | }
83 |
84 | target "code-server-fedora-39" {
85 | dockerfile = "ci/release-image/Dockerfile.fedora"
86 | tags = concat(
87 | gen_tags_for_docker_and_ghcr("fedora"),
88 | gen_tags_for_docker_and_ghcr("39"),
89 | )
90 | args = {
91 | BASE = "fedora:39"
92 | }
93 | platforms = ["linux/amd64", "linux/arm64"]
94 | }
95 |
96 | target "code-server-opensuse-tumbleweed" {
97 | dockerfile = "ci/release-image/Dockerfile.opensuse"
98 | tags = concat(
99 | gen_tags_for_docker_and_ghcr("opensuse"),
100 | gen_tags_for_docker_and_ghcr("tumbleweed"),
101 | )
102 | args = {
103 | BASE = "opensuse/tumbleweed"
104 | }
105 | platforms = ["linux/amd64", "linux/arm64"]
106 | }
107 |
--------------------------------------------------------------------------------
/ci/release-image/entrypoint-catatonit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | # We do this first to ensure sudo works below when renaming the user.
5 | # Otherwise the current container UID may not exist in the passwd database.
6 | eval "$(fixuid -q)"
7 |
8 | if [ "${DOCKER_USER-}" ]; then
9 | USER="$DOCKER_USER"
10 | if [ "$DOCKER_USER" != "$(whoami)" ]; then
11 | echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
12 | # Unfortunately we cannot change $HOME as we cannot move any bind mounts
13 | # nor can we bind mount $HOME into a new home as that requires a privileged container.
14 | sudo usermod --login "$DOCKER_USER" coder
15 | sudo groupmod -n "$DOCKER_USER" coder
16 |
17 | sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
18 | fi
19 | fi
20 |
21 | # Allow users to have scripts run on container startup to prepare workspace.
22 | # https://github.com/coder/code-server/issues/5177
23 | if [ -d "${ENTRYPOINTD}" ]; then
24 | find "${ENTRYPOINTD}" -type f -executable -print -exec {} \;
25 | fi
26 |
27 | exec catatonit -- /usr/bin/code-server "$@"
28 |
--------------------------------------------------------------------------------
/ci/release-image/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | # We do this first to ensure sudo works below when renaming the user.
5 | # Otherwise the current container UID may not exist in the passwd database.
6 | eval "$(fixuid -q)"
7 |
8 | if [ "${DOCKER_USER-}" ]; then
9 | USER="$DOCKER_USER"
10 | if [ -z "$(id -u "$DOCKER_USER" 2>/dev/null)" ]; then
11 | echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
12 | # Unfortunately we cannot change $HOME as we cannot move any bind mounts
13 | # nor can we bind mount $HOME into a new home as that requires a privileged container.
14 | sudo usermod --login "$DOCKER_USER" coder
15 | sudo groupmod -n "$DOCKER_USER" coder
16 |
17 | sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
18 | fi
19 | fi
20 |
21 | # Allow users to have scripts run on container startup to prepare workspace.
22 | # https://github.com/coder/code-server/issues/5177
23 | if [ -d "${ENTRYPOINTD}" ]; then
24 | find "${ENTRYPOINTD}" -type f -executable -print -exec {} \;
25 | fi
26 |
27 | exec dumb-init /usr/bin/code-server "$@"
28 |
--------------------------------------------------------------------------------
/ci/steps/brew-bump.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | # Only sourcing this so we get access to $VERSION
6 | source ./ci/lib.sh
7 | source ./ci/steps/steps-lib.sh
8 |
9 | echo "Checking environment variables"
10 |
11 | # We need VERSION to bump the brew formula
12 | if ! is_env_var_set "VERSION"; then
13 | echo "VERSION is not set"
14 | exit 1
15 | fi
16 |
17 | # We need HOMEBREW_GITHUB_API_TOKEN to push up commits
18 | if ! is_env_var_set "HOMEBREW_GITHUB_API_TOKEN"; then
19 | echo "HOMEBREW_GITHUB_API_TOKEN is not set"
20 | exit 1
21 | fi
22 |
23 | # Find the docs for bump-formula-pr here
24 | # https://github.com/Homebrew/brew/blob/master/Library/Homebrew/dev-cmd/bump-formula-pr.rb#L18
25 | local output
26 | if ! output=$(brew bump-formula-pr --version="${VERSION}" code-server --no-browse --no-audit --message="PR opened by @${GITHUB_ACTOR}" 2>&1); then
27 | if [[ $output == *"Duplicate PRs should not be opened"* ]]; then
28 | echo "$VERSION is already submitted"
29 | exit 0
30 | else
31 | echo "$output"
32 | exit 1
33 | fi
34 | fi
35 | }
36 |
37 | main "$@"
38 |
--------------------------------------------------------------------------------
/ci/steps/docker-buildx-push.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | main() {
5 | cd "$(dirname "$0")/../.."
6 | # NOTE@jsjoeio - this script assumes VERSION exists as an
7 | # environment variable.
8 |
9 | # NOTE@jsjoeio - this script assumes that you've downloaded
10 | # the release-packages artifact to ./release-packages before
11 | # running this docker buildx step
12 | docker buildx bake -f ci/release-image/docker-bake.hcl --push
13 | }
14 |
15 | main "$@"
16 |
--------------------------------------------------------------------------------
/ci/steps/steps-lib.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This is a library which contains functions used inside ci/steps
4 | #
5 | # We separated it into it's own file so that we could easily unit test
6 | # these functions and helpers
7 |
8 | # Checks whether and environment variable is set.
9 | # Source: https://stackoverflow.com/a/62210688/3015595
10 | is_env_var_set() {
11 | local name="${1:-}"
12 | if test -n "${!name:-}"; then
13 | return 0
14 | else
15 | return 1
16 | fi
17 | }
18 |
19 | # Checks whether a directory exists.
20 | directory_exists() {
21 | local dir="${1:-}"
22 | if [[ -d "${dir:-}" ]]; then
23 | return 0
24 | else
25 | return 1
26 | fi
27 | }
28 |
29 | # Checks whether a file exists.
30 | file_exists() {
31 | local file="${1:-}"
32 | if test -f "${file:-}"; then
33 | return 0
34 | else
35 | return 1
36 | fi
37 | }
38 |
39 | # Checks whether a file is executable.
40 | is_executable() {
41 | local file="${1:-}"
42 | if [ -f "${file}" ] && [ -r "${file}" ] && [ -x "${file}" ]; then
43 | return 0
44 | else
45 | return 1
46 | fi
47 | }
48 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # code-server
2 |
3 | [](https://github.com/coder/code-server/discussions) [](https://coder.com/community) [](https://twitter.com/coderhq) [](https://codecov.io/gh/coder/code-server) [](https://coder.com/docs/code-server/latest)
4 |
5 | Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and
6 | access it in the browser.
7 |
8 | 
9 | 
10 |
11 | ## Highlights
12 |
13 | - Code on any device with a consistent development environment
14 | - Use cloud servers to speed up tests, compilations, downloads, and more
15 | - Preserve battery life when you're on the go; all intensive tasks run on your
16 | server
17 |
18 | ## Requirements
19 |
20 | See [requirements](https://coder.com/docs/code-server/latest/requirements) for minimum specs, as well as instructions
21 | on how to set up a Google VM on which you can install code-server.
22 |
23 | **TL;DR:** Linux machine with WebSockets enabled, 1 GB RAM, and 2 vCPUs
24 |
25 | ## Getting started
26 |
27 | There are five ways to get started:
28 |
29 | 1. Using the [install
30 | script](https://github.com/coder/code-server/blob/main/install.sh), which
31 | automates most of the process. The script uses the system package manager if
32 | possible.
33 | 2. Manually [installing
34 | code-server](https://coder.com/docs/code-server/latest/install)
35 | 3. Deploy code-server to your team with [coder/coder](https://cdr.co/coder-github)
36 | 4. Using our one-click buttons and guides to [deploy code-server to a cloud
37 | provider](https://github.com/coder/deploy-code-server) ⚡
38 | 5. Using the [code-server feature for
39 | devcontainers](https://github.com/coder/devcontainer-features/blob/main/src/code-server/README.md),
40 | if you already use devcontainers in your project.
41 |
42 | If you use the install script, you can preview what occurs during the install
43 | process:
44 |
45 | ```bash
46 | curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
47 | ```
48 |
49 | To install, run:
50 |
51 | ```bash
52 | curl -fsSL https://code-server.dev/install.sh | sh
53 | ```
54 |
55 | When done, the install script prints out instructions for running and starting
56 | code-server.
57 |
58 | > **Note**
59 | > To manage code-server for a team on your infrastructure, see: [coder/coder](https://cdr.co/coder-github)
60 |
61 | We also have an in-depth [setup and
62 | configuration](https://coder.com/docs/code-server/latest/guide) guide.
63 |
64 | ## Questions?
65 |
66 | See answers to [frequently asked
67 | questions](https://coder.com/docs/code-server/latest/FAQ).
68 |
69 | ## Want to help?
70 |
71 | See [Contributing](https://coder.com/docs/code-server/latest/CONTRIBUTING) for
72 | details.
73 |
74 | ## Hiring
75 |
76 | Interested in [working at Coder](https://coder.com/careers)? Check out [our open
77 | positions](https://coder.com/careers#openings)!
78 |
79 | ## For Teams
80 |
81 | We develop [coder/coder](https://cdr.co/coder-github) to help teams to
82 | adopt remote development.
83 |
--------------------------------------------------------------------------------
/docs/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | Coder and the code-server team want to keep the code-server project secure and safe for end-users.
4 |
5 | ## Tools
6 |
7 | We use the following tools to help us stay on top of vulnerability mitigation.
8 |
9 | - [dependabot](https://dependabot.com/)
10 | - Submits pull requests to upgrade dependencies. We use dependabot's version
11 | upgrades as well as security updates.
12 | - code-scanning
13 | - [CodeQL](https://securitylab.github.com/tools/codeql/)
14 | - Semantic code analysis engine that runs on a regular schedule (see
15 | `codeql-analysis.yml`)
16 | - [trivy](https://github.com/aquasecurity/trivy)
17 | - Comprehensive vulnerability scanner that runs on PRs into the default
18 | branch and scans both our container image and repository code (see
19 | `trivy-scan-repo` and `trivy-scan-image` jobs in `build.yaml`)
20 | - `npm audit`
21 | - Audits NPM dependencies.
22 |
23 | ## Supported Versions
24 |
25 | Coder sponsors the development and maintenance of the code-server project. We will fix security issues within 90 days of receiving a report and publish the fix in a subsequent release. The code-server project does not provide backports or patch releases for security issues at this time.
26 |
27 | | Version | Supported |
28 | | ------------------------------------------------------- | ------------------ |
29 | | [Latest](https://github.com/coder/code-server/releases) | :white_check_mark: |
30 |
31 | ## Reporting a Vulnerability
32 |
33 | To report a vulnerability, please send an email to security[@]coder.com, and our security team will respond to you.
34 |
--------------------------------------------------------------------------------
/docs/android.md:
--------------------------------------------------------------------------------
1 | # Running code-server using UserLAnd
2 |
3 | 1. Install UserLAnd from [Google Play](https://play.google.com/store/apps/details?id=tech.ula&hl=en_US&gl=US)
4 | 2. Install an Ubuntu VM
5 | 3. Start app
6 | 4. Install Node.js and `curl` using `sudo apt install nodejs npm curl -y`
7 | 5. Install `nvm`:
8 |
9 | ```shell
10 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
11 | ```
12 |
13 | 6. Exit the terminal using `exit` and then reopen the terminal
14 | 7. Install and use Node.js 20:
15 |
16 | ```shell
17 | nvm install 18
18 | nvm use 18
19 | ```
20 |
21 | 8. Install code-server globally on device with: `npm install --global code-server`
22 | 9. Run code-server with `code-server`
23 | 10. Access on localhost:8080 in your browser
24 |
25 | # Running code-server using Nix-on-Droid
26 |
27 | 1. Install Nix-on-Droid from [F-Droid](https://f-droid.org/packages/com.termux.nix/)
28 | 2. Start app
29 | 3. Spawn a shell with code-server by running `nix-shell -p code-server`
30 | 4. Run code-server with `code-server`
31 | 5. Access on localhost:8080 in your browser
32 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/collab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/contributing.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/faq.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/requirements.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/upgrade.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/usage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/images/icons/wrench.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/docs/assets/screenshot-1.png
--------------------------------------------------------------------------------
/docs/assets/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/docs/assets/screenshot-2.png
--------------------------------------------------------------------------------
/docs/coder.md:
--------------------------------------------------------------------------------
1 | # Coder
2 |
3 | To install and run code-server in a Coder workspace, we suggest using the `install.sh`
4 | script in your template like so:
5 |
6 | ```terraform
7 | resource "coder_agent" "dev" {
8 | arch = "amd64"
9 | os = "linux"
10 | startup_script = < **VM Instances**.
31 | 2. Click **Create Instance**.
32 | 3. Provide a **name** for new instance.
33 | 4. Choose the **region** that's closest to you based on [GCP
34 | ping](https://gcping.com/).
35 | 5. Choose a **zone** (any option is fine).
36 | 6. We recommend choosing an **E2 series instance** from the [general-purpose
37 | family](https://cloud.google.com/compute/docs/machine-types#general_purpose).
38 | 7. Change the instance type to **custom** and set at least **2 cores** and **2
39 | GB of RAM**. You can add more resources if desired, though you can also edit
40 | your instance at a later point.
41 | 8. Though optional, we highly recommend switching the persistent disk to an SSD
42 | with at least 32 GB. To do so, click **change** under **Boot Disk**. Then,
43 | change the type to **SSD Persistent Disk**, and set the size to **32**. (You
44 | can also grow your disk at a later date).
45 | 9. Go to **Networking** > **Networking Interfaces** and edit the existing
46 | interface to use a static internal IP. Click **Done** to save.
47 | 10. If you don't have a [project-wide SSH
48 | key](https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys#project-wide),
49 | go to **Security** > **SSH Keys** to add your public key.
50 | 11. Click **Create** to proceed.
51 |
52 | Notes:
53 |
54 | - To lower costs, you can shut down your server when you're not using it.
55 | - We recommend using the `gcloud cli` to avoid using the GCP Dashboard if possible.
56 | - For serving code-server over HTTPS, we recommend using an external domain name along with a service such as Let's Encrypt
57 |
--------------------------------------------------------------------------------
/docs/triage.md:
--------------------------------------------------------------------------------
1 | # Triage
2 |
3 | Triaging code-server issues is done with the following issue filter:
4 |
5 | ```text
6 | is:issue is:open no:project sort:created-asc -label:blocked -label:upstream -label:waiting-for-info -label:extension-request
7 | ```
8 |
9 | This will show issues that:
10 |
11 | 1. Are open.
12 | 1. Have no assigned project.
13 | 1. Are not `blocked` or tagged for work by `upstream` (the VS Code core team).
14 | If an upstream issue is detrimental to the code-server experience we may fix
15 | it in our patch instead of waiting for the VS Code team to fix it. Someone
16 | should periodically go through these issues to see if they can be unblocked!
17 | 1. Are not labeled `waiting-for-info`.
18 | 1. Are not extension requests.
19 |
20 | ## Triage process
21 |
22 | 1. If an issue is a question/discussion, it should be converted into a GitHub
23 | discussion.
24 | 1. Otherwise, give the issue the appropriate labels (feel free to create new
25 | ones if necessary). There are no hard and set rules for labels. We don't have
26 | many so look through and see how they've been used throughout the repository.
27 | They all also have descriptions.
28 | 1. If more information is required, please ask the submitter and tag as
29 | `waiting-for-info` and wait.
30 | 1. Finally, the issue should be moved into the
31 | [code-server](https://github.com/coder/code-server/projects/1) project where we
32 | pick out issues to fix and track their progress.
33 |
34 | We also use [milestones](https://github.com/coder/code-server/milestones) to track
35 | what issues are planned/or were closed for what release.
36 |
--------------------------------------------------------------------------------
/docs/upgrade.md:
--------------------------------------------------------------------------------
1 | # Upgrade
2 |
3 | To upgrade code-server, install the new version over the old version. All user
4 | data is in `~/.local/share/code-server`, so they are preserved between
5 | installations.
6 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { fixupConfigRules } from "@eslint/compat"
2 | import globals from "globals"
3 | import tsParser from "@typescript-eslint/parser"
4 | import path from "node:path"
5 | import { fileURLToPath } from "node:url"
6 | import js from "@eslint/js"
7 | import { FlatCompat } from "@eslint/eslintrc"
8 |
9 | const __filename = fileURLToPath(import.meta.url)
10 | const __dirname = path.dirname(__filename)
11 | const compat = new FlatCompat({
12 | baseDirectory: __dirname,
13 | recommendedConfig: js.configs.recommended,
14 | allConfig: js.configs.all,
15 | })
16 |
17 | export default [
18 | ...fixupConfigRules(
19 | compat.extends(
20 | "eslint:recommended",
21 | "plugin:@typescript-eslint/recommended",
22 | "plugin:import/recommended",
23 | "plugin:import/typescript",
24 | "plugin:prettier/recommended",
25 | "prettier",
26 | ),
27 | ),
28 | {
29 | languageOptions: {
30 | globals: {
31 | ...globals.browser,
32 | ...globals.jest,
33 | ...globals.node,
34 | },
35 |
36 | parser: tsParser,
37 | ecmaVersion: 2018,
38 | sourceType: "module",
39 | },
40 |
41 | settings: {
42 | "import/resolver": {
43 | typescript: {
44 | alwaysTryTypes: true,
45 | },
46 | },
47 | },
48 |
49 | rules: {
50 | "@typescript-eslint/no-unused-vars": [
51 | "error",
52 | {
53 | args: "none",
54 | },
55 | ],
56 |
57 | "no-dupe-class-members": "off",
58 | "@typescript-eslint/no-use-before-define": "off",
59 | "@typescript-eslint/no-non-null-assertion": "off",
60 | "@typescript-eslint/ban-types": "off",
61 | "@typescript-eslint/no-var-requires": "off",
62 | "@typescript-eslint/explicit-module-boundary-types": "off",
63 | "@typescript-eslint/no-explicit-any": "off",
64 | "@typescript-eslint/no-extra-semi": "off",
65 | "@typescript-eslint/no-require-imports": "off",
66 | "@typescript-eslint/no-unused-vars": "off", // TODO: Fix these.
67 | "@typescript-eslint/no-empty-object-type": "off",
68 | eqeqeq: "error",
69 |
70 | "import/order": [
71 | "error",
72 | {
73 | alphabetize: {
74 | order: "asc",
75 | },
76 |
77 | groups: [["builtin", "external", "internal"], "parent", "sibling"],
78 | },
79 | ],
80 |
81 | "no-async-promise-executor": "off",
82 | },
83 | },
84 | ]
85 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1731533236,
9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1739303263,
24 | "narHash": "sha256-c/Z/6gZLN8BIpYh1B3qMzEn0TArjf4F2lmy59lDLVBM=",
25 | "owner": "nixos",
26 | "repo": "nixpkgs",
27 | "rev": "6cc4213488e886db863878a1e3dc26cc932d38b8",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "nixos",
32 | "ref": "nixos-unstable-small",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "code-server";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable-small";
6 | flake-utils.url = "github:numtide/flake-utils";
7 | };
8 |
9 | outputs = { self, nixpkgs, flake-utils }:
10 | flake-utils.lib.eachDefaultSystem
11 | (system:
12 | let pkgs = nixpkgs.legacyPackages.${system};
13 | nodejs = pkgs.nodejs_20;
14 | in {
15 | devShells.default = pkgs.mkShell {
16 | nativeBuildInputs = with pkgs; [
17 | nodejs
18 | python3
19 | pkg-config
20 | git
21 | rsync
22 | jq
23 | moreutils
24 | quilt
25 | bats
26 | openssl
27 | ];
28 | buildInputs = with pkgs; (lib.optionals (!stdenv.isDarwin) [ libsecret libkrb5 ]
29 | ++ (with xorg; [ libX11 libxkbfile ])
30 | ++ lib.optionals stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
31 | AppKit Cocoa CoreServices Security xcbuild
32 | ]));
33 | };
34 | }
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/patches/cli-window-open.diff:
--------------------------------------------------------------------------------
1 | Make opening files/folders from the terminal only open in the current instance
2 |
3 | Previously they would open in every code-server tab/window.
4 |
5 | To test:
6 |
7 | 1. Run code-server
8 | 2. Open code-server
9 | 3. Open terminal
10 | 4. Open another code-server window
11 | 5. Run node ./out/node/entry.js with a file or directory argument
12 |
13 | The file or directory should only open from the instance attached to that
14 | terminal.
15 |
16 | Index: code-server/lib/vscode/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts
17 | ===================================================================
18 | --- code-server.orig/lib/vscode/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts
19 | +++ code-server/lib/vscode/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts
20 | @@ -106,10 +106,14 @@ class RemoteTerminalBackend extends Base
21 | }
22 | const reqId = e.reqId;
23 | const commandId = e.commandId;
24 | + const terminalId = e.persistentProcessId;
25 | if (!allowedCommands.includes(commandId)) {
26 | this._remoteTerminalChannel.sendCommandResult(reqId, true, 'Invalid remote cli command: ' + commandId);
27 | return;
28 | }
29 | + if (typeof terminalId !== "undefined" && !this._ptys.has(terminalId)) {
30 | + return
31 | + }
32 | const commandArgs = e.commandArgs.map(arg => revive(arg));
33 | try {
34 | const result = await this._commandService.executeCommand(e.commandId, ...commandArgs);
35 |
--------------------------------------------------------------------------------
/patches/disable-builtin-ext-update.diff:
--------------------------------------------------------------------------------
1 | Prevent builtin extensions from being updated
2 |
3 | Updating builtin extensions from the marketplace prevents us from patching them
4 | (for example out GitHub authentication patches).
5 |
6 | Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
7 | ===================================================================
8 | --- code-server.orig/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
9 | +++ code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
10 | @@ -332,6 +332,10 @@ export class Extension implements IExten
11 | if (this.type === ExtensionType.System && this.productService.quality === 'stable') {
12 | return false;
13 | }
14 | + // Do not update builtin extensions.
15 | + if (this.type !== ExtensionType.User) {
16 | + return false;
17 | + }
18 | if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) {
19 | return false;
20 | }
21 |
--------------------------------------------------------------------------------
/patches/insecure-notification.diff:
--------------------------------------------------------------------------------
1 | Add a notification when accessing code-server in an insecure context
2 |
3 | This is done because otherwise when things like the clipboard do not work users
4 | may think code-server is broken. Ideally there would be a notification at the
5 | point where these things are used instead of this though.
6 |
7 | To test access over something like an HTTP domain or an IP address (not
8 | localhost). For example:
9 |
10 | 1. run code-server
11 | 2. use ngrok to expose code-server
12 | 3. access via HTTP
13 | 4. look for notification in bottom right
14 |
15 | Index: code-server/lib/vscode/src/vs/workbench/browser/client.ts
16 | ===================================================================
17 | --- code-server.orig/lib/vscode/src/vs/workbench/browser/client.ts
18 | +++ code-server/lib/vscode/src/vs/workbench/browser/client.ts
19 | @@ -1,7 +1,10 @@
20 | import { Disposable } from "../../base/common/lifecycle.js";
21 | +import { localize } from '../../nls.js';
22 | +import { INotificationService, Severity } from '../../platform/notification/common/notification.js';
23 |
24 | export class CodeServerClient extends Disposable {
25 | constructor (
26 | + @INotificationService private notificationService: INotificationService,
27 | ) {
28 | super();
29 | }
30 | @@ -42,5 +45,31 @@ export class CodeServerClient extends Di
31 | }
32 | });
33 | }
34 | +
35 | + if (!window.isSecureContext) {
36 | + this.notificationService.notify({
37 | + severity: Severity.Warning,
38 | + message: localize(
39 | + 'insecureContext',
40 | + "{0} is being accessed in an insecure context. Web views, the clipboard, and other functionality may not work as expected.",
41 | + 'code-server',
42 | + ),
43 | + actions: {
44 | + primary: [
45 | + {
46 | + id: 'understand',
47 | + label: localize('confirmInsecure', "I understand"),
48 | + tooltip: '',
49 | + class: undefined,
50 | + enabled: true,
51 | + checked: true,
52 | + run: () => {
53 | + return Promise.resolve();
54 | + },
55 | + },
56 | + ],
57 | + },
58 | + });
59 | + }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/patches/keepalive.diff:
--------------------------------------------------------------------------------
1 | This can be removed after upgrading to Node >= 19 as keepAlive is defaulted to
2 | true after 19.
3 |
4 | Index: code-server/lib/vscode/src/vs/platform/request/node/proxy.ts
5 | ===================================================================
6 | --- code-server.orig/lib/vscode/src/vs/platform/request/node/proxy.ts
7 | +++ code-server/lib/vscode/src/vs/platform/request/node/proxy.ts
8 | @@ -42,6 +42,7 @@ export async function getProxyAgent(rawR
9 | port: (proxyEndpoint.port ? +proxyEndpoint.port : 0) || (proxyEndpoint.protocol === 'https' ? 443 : 80),
10 | auth: proxyEndpoint.auth,
11 | rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true,
12 | + keepAlive: true,
13 | };
14 |
15 | if (requestURL.protocol === 'http:') {
16 |
--------------------------------------------------------------------------------
/patches/proposed-api.diff:
--------------------------------------------------------------------------------
1 | Unconditionally enable the proposed API
2 |
3 | To test run an extension that uses the proposed API (i.e.
4 | https://github.com/microsoft/vscode-extension-samples/tree/ddae6c0c9ff203b4ed6f6b43bfacdd0834215f83/proposed-api-sample)
5 |
6 | We also override isProposedApiEnabled in case an extension does not declare the
7 | APIs it needs correctly (the Jupyter extension had this issue).
8 |
9 | Index: code-server/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts
10 | ===================================================================
11 | --- code-server.orig/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts
12 | +++ code-server/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts
13 | @@ -314,10 +314,7 @@ function extensionDescriptionArrayToMap(
14 | }
15 |
16 | export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean {
17 | - if (!extension.enabledApiProposals) {
18 | - return false;
19 | - }
20 | - return extension.enabledApiProposals.includes(proposal);
21 | + return true
22 | }
23 |
24 | export function checkProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): void {
25 | Index: code-server/lib/vscode/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts
26 | ===================================================================
27 | --- code-server.orig/lib/vscode/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts
28 | +++ code-server/lib/vscode/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts
29 | @@ -31,7 +31,7 @@ export class ExtensionsProposedApi {
30 |
31 | this._envEnabledExtensions = new Set((_environmentService.extensionEnabledProposedApi ?? []).map(id => ExtensionIdentifier.toKey(id)));
32 |
33 | - this._envEnablesProposedApiForAll =
34 | + this._envEnablesProposedApiForAll = true ||
35 | !_environmentService.isBuilt || // always allow proposed API when running out of sources
36 | (_environmentService.isExtensionDevelopment && productService.quality !== 'stable') || // do not allow proposed API against stable builds when developing an extension
37 | (this._envEnabledExtensions.size === 0 && Array.isArray(_environmentService.extensionEnabledProposedApi)); // always allow proposed API if --enable-proposed-api is provided without extension ID
38 |
--------------------------------------------------------------------------------
/patches/series:
--------------------------------------------------------------------------------
1 | integration.diff
2 | base-path.diff
3 | proposed-api.diff
4 | marketplace.diff
5 | webview.diff
6 | disable-builtin-ext-update.diff
7 | insecure-notification.diff
8 | update-check.diff
9 | logout.diff
10 | store-socket.diff
11 | proxy-uri.diff
12 | unique-db.diff
13 | local-storage.diff
14 | service-worker.diff
15 | sourcemaps.diff
16 | external-file-actions.diff
17 | telemetry.diff
18 | cli-window-open.diff
19 | getting-started.diff
20 | keepalive.diff
21 | clipboard.diff
22 | display-language.diff
23 | trusted-domains.diff
24 | signature-verification.diff
25 |
--------------------------------------------------------------------------------
/patches/service-worker.diff:
--------------------------------------------------------------------------------
1 | Add a service worker
2 |
3 | To test try installing code-server as a PWA.
4 |
5 | Index: code-server/lib/vscode/src/vs/base/common/product.ts
6 | ===================================================================
7 | --- code-server.orig/lib/vscode/src/vs/base/common/product.ts
8 | +++ code-server/lib/vscode/src/vs/base/common/product.ts
9 | @@ -61,6 +61,10 @@ export interface IProductConfiguration {
10 | readonly updateEndpoint?: string
11 | readonly logoutEndpoint?: string
12 | readonly proxyEndpointTemplate?: string
13 | + readonly serviceWorker?: {
14 | + readonly path: string;
15 | + readonly scope: string;
16 | + }
17 |
18 | readonly version: string;
19 | readonly date?: string;
20 | Index: code-server/lib/vscode/src/vs/workbench/browser/client.ts
21 | ===================================================================
22 | --- code-server.orig/lib/vscode/src/vs/workbench/browser/client.ts
23 | +++ code-server/lib/vscode/src/vs/workbench/browser/client.ts
24 | @@ -89,6 +89,10 @@ export class CodeServerClient extends Di
25 | if (this.productService.logoutEndpoint) {
26 | this.addLogoutCommand(this.productService.logoutEndpoint);
27 | }
28 | +
29 | + if (this.productService.serviceWorker) {
30 | + await this.registerServiceWorker(this.productService.serviceWorker);
31 | + }
32 | }
33 |
34 | private checkUpdates(updateEndpoint: string) {
35 | @@ -161,4 +165,17 @@ export class CodeServerClient extends Di
36 | });
37 | }
38 | }
39 | +
40 | + private async registerServiceWorker(serviceWorker: { path: string; scope: string }) {
41 | + if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
42 | + try {
43 | + await navigator.serviceWorker.register(serviceWorker.path, {
44 | + scope: serviceWorker.scope,
45 | + });
46 | + this.logService.info('[Service Worker] registered');
47 | + } catch (error: any) {
48 | + this.logService.error('[Service Worker] registration', error as Error);
49 | + }
50 | + }
51 | + }
52 | }
53 | Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
54 | ===================================================================
55 | --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts
56 | +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts
57 | @@ -344,6 +344,10 @@ export class WebClientServer {
58 | updateEndpoint: !this._environmentService.args['disable-update-check'] ? rootBase + '/update/check' : undefined,
59 | logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? rootBase + '/logout' : undefined,
60 | proxyEndpointTemplate: process.env.VSCODE_PROXY_URI ?? rootBase + '/proxy/{{port}}/',
61 | + serviceWorker: {
62 | + scope: vscodeBase + '/',
63 | + path: rootBase + '/_static/out/browser/serviceWorker.js',
64 | + },
65 | embedderIdentifier: 'server-distro',
66 | extensionsGallery: this._productService.extensionsGallery,
67 | };
68 |
--------------------------------------------------------------------------------
/patches/signature-verification.diff:
--------------------------------------------------------------------------------
1 | Disable signature verification.
2 |
3 | Extension signature verification is now mandatory for all platforms and needs to be disabled.
4 |
5 | Index: code-server/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts
6 | ===================================================================
7 | --- code-server.orig/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts
8 | +++ code-server/lib/vscode/src/vs/platform/extensionManagement/node/extensionManagementService.ts
9 | @@ -34,6 +34,7 @@ import {
10 | ExtensionSignatureVerificationCode,
11 | computeSize,
12 | IAllowedExtensionsService,
13 | + // @ts-expect-error no-unused-variable
14 | VerifyExtensionSignatureConfigKey,
15 | shouldRequireRepositorySignatureFor,
16 | } from '../common/extensionManagement.js';
17 | @@ -87,6 +88,7 @@ export class ExtensionManagementService
18 | @IDownloadService private downloadService: IDownloadService,
19 | @IInstantiationService private readonly instantiationService: IInstantiationService,
20 | @IFileService private readonly fileService: IFileService,
21 | + // @ts-expect-error no-unused-variable
22 | @IConfigurationService private readonly configurationService: IConfigurationService,
23 | @IExtensionGalleryManifestService protected readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
24 | @IProductService productService: IProductService,
25 | @@ -331,8 +333,7 @@ export class ExtensionManagementService
26 |
27 | private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> {
28 | if (verifySignature) {
29 | - const value = this.configurationService.getValue(VerifyExtensionSignatureConfigKey);
30 | - verifySignature = isBoolean(value) ? value : true;
31 | + verifySignature = false;
32 | }
33 | const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform);
34 | const shouldRequireSignature = shouldRequireRepositorySignatureFor(extension.private, await this.extensionGalleryManifestService.getExtensionGalleryManifest());
35 |
--------------------------------------------------------------------------------
/patches/sourcemaps.diff:
--------------------------------------------------------------------------------
1 | Make sourcemaps self-hosted
2 |
3 | Normally source maps get removed as part of the build process so prevent that
4 | from happening. Also avoid using the windows.net host since obviously we can
5 | not host our source maps there and want them to be self-hosted even if we could.
6 |
7 | To test try debugging/browsing the source of a build in a browser.
8 |
9 | Index: code-server/lib/vscode/build/gulpfile.reh.js
10 | ===================================================================
11 | --- code-server.orig/lib/vscode/build/gulpfile.reh.js
12 | +++ code-server/lib/vscode/build/gulpfile.reh.js
13 | @@ -256,8 +256,7 @@ function packageTask(type, platform, arc
14 |
15 | const src = gulp.src(sourceFolderName + '/**', { base: '.' })
16 | .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); }))
17 | - .pipe(util.setExecutableBit(['**/*.sh']))
18 | - .pipe(filter(['**', '!**/*.js.map']));
19 | + .pipe(util.setExecutableBit(['**/*.sh']));
20 |
21 | const workspaceExtensionPoints = ['debuggers', 'jsonValidation'];
22 | const isUIExtension = (manifest) => {
23 | @@ -296,9 +295,9 @@ function packageTask(type, platform, arc
24 | .map(name => `.build/extensions/${name}/**`);
25 |
26 | const extensions = gulp.src(extensionPaths, { base: '.build', dot: true });
27 | - const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true });
28 | - const sources = es.merge(src, extensions, extensionsCommonDependencies)
29 | + const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true })
30 | .pipe(filter(['**', '!**/*.js.map'], { dot: true }));
31 | + const sources = es.merge(src, extensions, extensionsCommonDependencies);
32 |
33 | let version = packageJson.version;
34 | const quality = product.quality;
35 | @@ -451,7 +450,7 @@ function tweakProductForServerWeb(produc
36 | const minifyTask = task.define(`minify-vscode-${type}`, task.series(
37 | bundleTask,
38 | util.rimraf(`out-vscode-${type}-min`),
39 | - optimize.minifyTask(`out-vscode-${type}`, `https://main.vscode-cdn.net/sourcemaps/${commit}/core`)
40 | + optimize.minifyTask(`out-vscode-${type}`, ``)
41 | ));
42 | gulp.task(minifyTask);
43 |
44 |
--------------------------------------------------------------------------------
/patches/trusted-domains.diff:
--------------------------------------------------------------------------------
1 | Allow configuring trusted domains via product.json or flag.
2 |
3 | Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
4 | ===================================================================
5 | --- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
6 | +++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
7 | @@ -20,6 +20,7 @@ export const serverOptions: OptionDescri
8 | 'disable-file-uploads': { type: 'boolean' },
9 | 'disable-getting-started-override': { type: 'boolean' },
10 | 'locale': { type: 'string' },
11 | + 'link-protection-trusted-domains': { type: 'string[]' },
12 |
13 | /* ----- server setup ----- */
14 |
15 | @@ -109,6 +110,7 @@ export interface ServerParsedArgs {
16 | 'disable-file-uploads'?: boolean;
17 | 'disable-getting-started-override'?: boolean,
18 | 'locale'?: string
19 | + 'link-protection-trusted-domains'?: string[],
20 |
21 | /* ----- server setup ----- */
22 |
23 | Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
24 | ===================================================================
25 | --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts
26 | +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts
27 | @@ -339,6 +339,14 @@ export class WebClientServer {
28 | scopes: [['user:email'], ['repo']]
29 | } : undefined;
30 |
31 | + const linkProtectionTrustedDomains: string[] = [];
32 | + if (this._environmentService.args['link-protection-trusted-domains']) {
33 | + linkProtectionTrustedDomains.push(...this._environmentService.args['link-protection-trusted-domains']);
34 | + }
35 | + if (this._productService.linkProtectionTrustedDomains) {
36 | + linkProtectionTrustedDomains.push(...this._productService.linkProtectionTrustedDomains);
37 | + }
38 | +
39 | const productConfiguration: Partial> = {
40 | codeServerVersion: this._productService.codeServerVersion,
41 | rootEndpoint: rootBase,
42 | @@ -353,6 +361,7 @@ export class WebClientServer {
43 | telemetryEndpoint: this._productService.telemetryEndpoint,
44 | embedderIdentifier: 'server-distro',
45 | extensionsGallery: this._productService.extensionsGallery,
46 | + linkProtectionTrustedDomains,
47 | };
48 |
49 | const proposedApi = this._environmentService.args['enable-proposed-api'];
50 |
--------------------------------------------------------------------------------
/patches/unique-db.diff:
--------------------------------------------------------------------------------
1 | Prevent state collisions
2 |
3 | Previously if you opened different workspaces that had the same filesystem path
4 | (for example if you have /home/coder on two different machines that are both
5 | accessed through the same host) they would conflict with each other. This
6 | ensures that different browser paths will be unique (for example /workspace1 and
7 | /workspace2).
8 |
9 | The easiest way to test is to open files in the same workspace using both / and
10 | /vscode and make sure they are not interacting with each other.
11 |
12 | Index: code-server/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts
13 | ===================================================================
14 | --- code-server.orig/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts
15 | +++ code-server/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts
16 | @@ -18,6 +18,7 @@ import { AbstractStorageService, isProfi
17 | import { isUserDataProfile, IUserDataProfile } from '../../../../platform/userDataProfile/common/userDataProfile.js';
18 | import { IAnyWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';
19 | import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
20 | +import { hash } from '../../../../base/common/hash.js';
21 |
22 | export class BrowserStorageService extends AbstractStorageService {
23 |
24 | @@ -300,7 +301,11 @@ export class IndexedDBStorageDatabase ex
25 | }
26 |
27 | static async createWorkspaceStorage(workspaceId: string, logService: ILogService): Promise {
28 | - return IndexedDBStorageDatabase.create({ id: workspaceId }, logService);
29 | + // Add a unique ID based on the current path for per-workspace databases.
30 | + // This prevents workspaces on different machines that share the same domain
31 | + // and file path from colliding (since it does not appear IndexedDB can be
32 | + // scoped to a path) as long as they are hosted on different paths.
33 | + return IndexedDBStorageDatabase.create({ id: workspaceId + '-' + hash(location.pathname.toString().replace(/\/$/, "")).toString(16) }, logService);
34 | }
35 |
36 | static async create(options: IndexedDBStorageDatabaseOptions, logService: ILogService): Promise {
37 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "rangeStrategy": "replace",
3 | "extends": ["config:base"],
4 | "packageRules": [
5 | {
6 | "matchUpdateTypes": ["minor", "patch", "digest"],
7 | "automerge": true,
8 | "groupName": "Minor dependency updates"
9 | },
10 | {
11 | "matchDepTypes": ["peerDependencies"],
12 | "matchUpdateTypes": ["minor", "patch", "digest"],
13 | "automerge": true,
14 | "groupName": "Peer dependency updates"
15 | }
16 | ],
17 | "vulnerabilityAlerts": {
18 | "enabled": "true"
19 | },
20 | "ignoreDeps": ["express", "ansi-regex", "env-paths", "limiter", "node", "prettier"]
21 | }
22 |
--------------------------------------------------------------------------------
/src/browser/favicon.afdesign:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:625d2049c38ae27df0613fa533020e889fa98affd603050f46d3748be7b90d0b
3 | size 38675
4 |
--------------------------------------------------------------------------------
/src/browser/media/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/src/browser/media/favicon.ico
--------------------------------------------------------------------------------
/src/browser/media/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/browser/media/pwa-icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/src/browser/media/pwa-icon-192.png
--------------------------------------------------------------------------------
/src/browser/media/pwa-icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/src/browser/media/pwa-icon-512.png
--------------------------------------------------------------------------------
/src/browser/media/pwa-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/src/browser/media/pwa-icon.png
--------------------------------------------------------------------------------
/src/browser/media/templates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder/code-server/35e78fe35b61dc9965305ccf19566aed426521ed/src/browser/media/templates.png
--------------------------------------------------------------------------------
/src/browser/pages/error.css:
--------------------------------------------------------------------------------
1 | .error-display {
2 | box-sizing: border-box;
3 | padding: 20px;
4 | text-align: center;
5 | }
6 |
7 | .error-display > .header {
8 | font-size: 6rem;
9 | margin: 0;
10 | }
11 |
12 | .error-display > .body {
13 | color: #444;
14 | color: light-dark(#444, #ccc);
15 | font-size: 1.2rem;
16 | }
17 |
18 | .error-display > .links {
19 | margin-top: 16px;
20 | }
21 |
22 | .error-display > .links > .link {
23 | color: rgb(87, 114, 245);
24 | text-decoration: none;
25 | }
26 |
27 | .error-display > .links > .link:hover {
28 | text-decoration: underline;
29 | }
30 |
31 | .error-display .success {
32 | color: green;
33 | }
34 |
--------------------------------------------------------------------------------
/src/browser/pages/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
14 | {{ERROR_TITLE}} - code-server
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
{{ERROR_BODY}}
29 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/browser/pages/global.css:
--------------------------------------------------------------------------------
1 | :root {
2 | color-scheme: light dark;
3 | }
4 |
5 | html,
6 | body,
7 | #root {
8 | height: 100%;
9 | width: 100%;
10 | }
11 |
12 | body {
13 | background: rgb(244, 247, 252);
14 | background: light-dark(rgb(244, 247, 252), #111827);
15 | color: #111;
16 | color: light-dark(#111, #ddd);
17 | margin: 0;
18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
19 | "Segoe UI Emoji", "Segoe UI Symbol";
20 | overflow: hidden;
21 | }
22 |
23 | input,
24 | button {
25 | font-family: inherit;
26 | font-size: 1rem;
27 | line-height: 1rem;
28 | }
29 |
30 | .-button {
31 | background-color: rgb(87, 114, 245);
32 | background-color: light-dark(rgb(87, 114, 245), rgb(26, 86, 219));
33 | border-radius: 5px;
34 | border: none;
35 | box-sizing: border-box;
36 | color: white;
37 | color: light-dark(white, white);
38 | cursor: pointer;
39 | padding: 18px 20px;
40 | font-weight: 500;
41 | text-decoration: none;
42 | }
43 |
44 | .center-container {
45 | align-items: center;
46 | box-sizing: border-box;
47 | display: flex;
48 | flex-direction: column;
49 | justify-content: center;
50 | min-height: 100%;
51 | padding: 20px;
52 | width: 100%;
53 | }
54 |
55 | .card-box {
56 | background-color: rgb(250, 253, 258);
57 | background-color: light-dark(rgb(250, 253, 258), #1f2937);
58 | border-radius: 5px;
59 | box-shadow:
60 | light-dark(rgba(60, 66, 87, 0.117647), rgba(10, 10, 10, 0.617647)) 0px 7px 14px 0px,
61 | rgba(0, 0, 0, 0.117647) 0px 3px 6px 0px;
62 | max-width: 650px;
63 | width: 100%;
64 | }
65 |
66 | .card-box > .header {
67 | border-bottom: 1px solid #ddd;
68 | border-bottom: 1px solid light-dark(#ddd, #111827);
69 | color: #444;
70 | color: light-dark(#444, #fff);
71 | padding: 30px;
72 | }
73 |
74 | .card-box > .header > .main {
75 | margin: 0;
76 | font-size: 1.5rem;
77 | }
78 |
79 | .card-box > .header > .sub {
80 | color: #555;
81 | color: light-dark(#555, #9ca3af);
82 | margin-top: 10px;
83 | }
84 |
85 | .card-box > .content {
86 | padding: 40px;
87 | }
88 |
89 | .card-box > .content > .none {
90 | margin: 2px 0;
91 | }
92 |
93 | .card-box + .card-box {
94 | margin-top: 26px;
95 | }
96 |
97 | canvas {
98 | top: 0;
99 | left: 0;
100 | }
101 |
--------------------------------------------------------------------------------
/src/browser/pages/login.css:
--------------------------------------------------------------------------------
1 | body {
2 | min-height: 568px;
3 | min-width: 320px;
4 | overflow: auto;
5 | }
6 |
7 | .login-form {
8 | display: flex;
9 | flex-direction: column;
10 | flex: 1;
11 | justify-content: center;
12 | }
13 |
14 | .login-form > .field {
15 | display: flex;
16 | flex-direction: row;
17 | width: 100%;
18 | }
19 |
20 | @media (max-width: 600px) {
21 | .login-form > .field {
22 | flex-direction: column;
23 | }
24 | }
25 |
26 | .login-form > .error {
27 | color: red;
28 | margin-top: 16px;
29 | }
30 |
31 | .login-form > .field > .password {
32 | background-color: rgb(244, 247, 252);
33 | background-color: light-dark(rgb(244, 247, 252), #374151);
34 | border-radius: 5px;
35 | border: 1px solid #ddd;
36 | border: 1px solid light-dark(#ddd, #4b5563);
37 | box-sizing: border-box;
38 | flex: 1;
39 | padding: 16px;
40 | }
41 |
42 | .login-form > .field > .password::placeholder {
43 | color: rgb(148 163 184);
44 | }
45 |
46 | .login-form > .field > .password:focus {
47 | outline: 2px solid rgb(63, 131, 248);
48 | }
49 |
50 | .login-form > .user {
51 | display: none;
52 | }
53 |
54 | .login-form > .field > .submit {
55 | margin-left: 20px;
56 | }
57 |
58 | @media (max-width: 600px) {
59 | .login-form > .field > .submit {
60 | margin-left: 0px;
61 | margin-top: 16px;
62 | }
63 | }
64 |
65 | input {
66 | -webkit-appearance: none;
67 | }
68 |
--------------------------------------------------------------------------------
/src/browser/pages/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
14 | {{I18N_LOGIN_TITLE}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
53 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/browser/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/src/browser/security.txt:
--------------------------------------------------------------------------------
1 | Contact: mailto:security@coder.com
2 | Acknowledgments: https://coder.com/security/thanks
3 | Preferred-Languages: en-US
4 | Canonical: https://coder.com/.well-known/security.txt
5 | Policy: https://coder.com/security/policy
6 | Hiring: https://coder.com/careers
--------------------------------------------------------------------------------
/src/browser/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | self.addEventListener("install", () => {
2 | console.debug("[Service Worker] installed")
3 | })
4 |
5 | self.addEventListener("activate", (event: any) => {
6 | event.waitUntil((self as any).clients.claim())
7 | console.debug("[Service Worker] activated")
8 | })
9 |
10 | self.addEventListener("fetch", () => {
11 | // Without this event handler we won't be recognized as a PWA.
12 | })
13 |
--------------------------------------------------------------------------------
/src/common/emitter.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 |
3 | /**
4 | * Event emitter callback. Called with the emitted value and a promise that
5 | * resolves when all emitters have finished.
6 | */
7 | export type Callback> = (t: T, p: Promise) => R
8 |
9 | export interface Disposable {
10 | dispose(): void | Promise
11 | }
12 |
13 | export interface Event {
14 | (listener: Callback): Disposable
15 | }
16 |
17 | /**
18 | * Emitter typecasts for a single event type.
19 | */
20 | export class Emitter {
21 | private listeners: Array> = []
22 |
23 | public get event(): Event {
24 | return (cb: Callback): Disposable => {
25 | this.listeners.push(cb)
26 |
27 | return {
28 | dispose: (): void => {
29 | const i = this.listeners.indexOf(cb)
30 | if (i !== -1) {
31 | this.listeners.splice(i, 1)
32 | }
33 | },
34 | }
35 | }
36 | }
37 |
38 | /**
39 | * Emit an event with a value.
40 | */
41 | public async emit(value: T): Promise {
42 | let resolve: () => void
43 | const promise = new Promise((r) => (resolve = r))
44 |
45 | await Promise.all(
46 | this.listeners.map(async (cb) => {
47 | try {
48 | await cb(value, promise)
49 | } catch (error: any) {
50 | logger.error(error.message)
51 | }
52 | }),
53 | )
54 |
55 | resolve!()
56 | }
57 |
58 | public dispose(): void {
59 | this.listeners = []
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/common/http.ts:
--------------------------------------------------------------------------------
1 | export enum HttpCode {
2 | Ok = 200,
3 | Redirect = 302,
4 | NotFound = 404,
5 | BadRequest = 400,
6 | Unauthorized = 401,
7 | Forbidden = 403,
8 | LargePayload = 413,
9 | ServerError = 500,
10 | }
11 |
12 | /**
13 | * Represents an error with a message and an HTTP status code. This code will be
14 | * used in the HTTP response.
15 | */
16 | export class HttpError extends Error {
17 | public constructor(
18 | message: string,
19 | public readonly statusCode: HttpCode,
20 | public readonly details?: object,
21 | ) {
22 | super(message)
23 | this.name = this.constructor.name
24 | }
25 | }
26 |
27 | export enum CookieKeys {
28 | Session = "code-server-session",
29 | }
30 |
--------------------------------------------------------------------------------
/src/common/util.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Appends an 's' to the provided string if count is greater than one;
3 | * otherwise the string is returned
4 | */
5 | export const plural = (count: number, str: string): string => (count === 1 ? str : `${str}s`)
6 |
7 | export const generateUuid = (length = 24): string => {
8 | const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
9 | return Array(length)
10 | .fill(1)
11 | .map(() => possible[Math.floor(Math.random() * possible.length)])
12 | .join("")
13 | }
14 |
15 | /**
16 | * Remove extra slashes in a URL.
17 | *
18 | * This is meant to fill the job of `path.join` so you can concatenate paths and
19 | * then normalize out any extra slashes.
20 | *
21 | * If you are using `path.join` you do not need this but note that `path` is for
22 | * file system paths, not URLs.
23 | */
24 | export const normalize = (url: string, keepTrailing = false): string => {
25 | return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
26 | }
27 |
28 | // TODO: Might make sense to add Error handling to the logger itself.
29 | export function logError(logger: { error: (msg: string) => void }, prefix: string, err: unknown): void {
30 | if (err instanceof Error) {
31 | logger.error(`${prefix}: ${err.message} ${err.stack}`)
32 | } else {
33 | logger.error(`${prefix}: ${err}`)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/node/constants.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import type { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package"
3 | import * as os from "os"
4 | import * as path from "path"
5 |
6 | export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles {
7 | let pkg = {}
8 | try {
9 | pkg = require(relativePath)
10 | } catch (error: any) {
11 | logger.warn(error.message)
12 | }
13 |
14 | return pkg
15 | }
16 |
17 | export const rootPath = path.resolve(__dirname, "../..")
18 | export const vsRootPath = path.join(rootPath, "lib/vscode")
19 | const PACKAGE_JSON = "package.json"
20 | const pkg = getPackageJson(`${rootPath}/${PACKAGE_JSON}`)
21 | const codePkg = getPackageJson(`${vsRootPath}/${PACKAGE_JSON}`) || { version: "0.0.0" }
22 | export const version = pkg.version || "development"
23 | export const commit = pkg.commit || "development"
24 | export const codeVersion = codePkg.version || "development"
25 | export const tmpdir = path.join(os.tmpdir(), "code-server")
26 | export const isDevMode = commit === "development"
27 | export const httpProxyUri =
28 | process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy
29 |
30 | /**
31 | * getVersionString returns a human-readable version string suitable
32 | * for outputting to the console.
33 | */
34 | export function getVersionString(): string {
35 | return [version, commit, "with Code", codeVersion].join(" ")
36 | }
37 |
38 | /**
39 | * getVersionJsonString returns a machine-readable version string
40 | * suitable for outputting to the console.
41 | */
42 | export function getVersionJsonString(): string {
43 | return JSON.stringify({
44 | codeServer: version,
45 | commit,
46 | vscode: codeVersion,
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/src/node/entry.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import { optionDescriptions, parse, readConfigFile, setDefaults, shouldOpenInExistingInstance } from "./cli"
3 | import { getVersionString, getVersionJsonString } from "./constants"
4 | import { openInExistingInstance, runCodeServer, runCodeCli, shouldSpawnCliProcess } from "./main"
5 | import { isChild, wrapper } from "./wrapper"
6 |
7 | async function entry(): Promise {
8 | // There's no need to check flags like --help or to spawn in an existing
9 | // instance for the child process because these would have already happened in
10 | // the parent and the child wouldn't have been spawned. We also get the
11 | // arguments from the parent so we don't have to parse twice and to account
12 | // for environment manipulation (like how PASSWORD gets removed to avoid
13 | // leaking to child processes).
14 | if (isChild(wrapper)) {
15 | const args = await wrapper.handshake()
16 | wrapper.preventExit()
17 | const server = await runCodeServer(args)
18 | wrapper.onDispose(() => server.dispose())
19 | return
20 | }
21 |
22 | const cliArgs = parse(process.argv.slice(2))
23 | const configArgs = await readConfigFile(cliArgs.config)
24 | const args = await setDefaults(cliArgs, configArgs)
25 |
26 | if (args.help) {
27 | console.log("code-server", getVersionString())
28 | console.log("")
29 | console.log(`Usage: code-server [options] [path]`)
30 | console.log(` - Opening a directory: code-server ./path/to/your/project`)
31 | console.log(` - Opening a saved workspace: code-server ./path/to/your/project.code-workspace`)
32 | console.log("")
33 | console.log("Options")
34 | optionDescriptions().forEach((description) => {
35 | console.log("", description)
36 | })
37 | return
38 | }
39 |
40 | if (args.version) {
41 | if (args.json) {
42 | console.log(getVersionJsonString())
43 | } else {
44 | console.log(getVersionString())
45 | }
46 | return
47 | }
48 |
49 | if (shouldSpawnCliProcess(args)) {
50 | logger.debug("Found VS Code arguments; spawning VS Code CLI")
51 | return runCodeCli(args)
52 | }
53 |
54 | const socketPath = await shouldOpenInExistingInstance(cliArgs, args["session-socket"])
55 | if (socketPath) {
56 | logger.debug("Trying to open in existing instance")
57 | return openInExistingInstance(args, socketPath)
58 | }
59 |
60 | return wrapper.start(args)
61 | }
62 |
63 | entry().catch((error) => {
64 | logger.error(error.message)
65 | wrapper.exit(error)
66 | })
67 |
--------------------------------------------------------------------------------
/src/node/heart.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import { promises as fs } from "fs"
3 |
4 | /**
5 | * Provides a heartbeat using a local file to indicate activity.
6 | */
7 | export class Heart {
8 | private heartbeatTimer?: NodeJS.Timeout
9 | private heartbeatInterval = 60000
10 | public lastHeartbeat = 0
11 |
12 | public constructor(
13 | private readonly heartbeatPath: string,
14 | private readonly isActive: () => Promise,
15 | ) {
16 | this.beat = this.beat.bind(this)
17 | this.alive = this.alive.bind(this)
18 | }
19 |
20 | public alive(): boolean {
21 | const now = Date.now()
22 | return now - this.lastHeartbeat < this.heartbeatInterval
23 | }
24 | /**
25 | * Write to the heartbeat file if we haven't already done so within the
26 | * timeout and start or reset a timer that keeps running as long as there is
27 | * activity. Failures are logged as warnings.
28 | */
29 | public async beat(): Promise {
30 | if (this.alive()) {
31 | return
32 | }
33 |
34 | logger.debug("heartbeat")
35 | this.lastHeartbeat = Date.now()
36 | if (typeof this.heartbeatTimer !== "undefined") {
37 | clearTimeout(this.heartbeatTimer)
38 | }
39 | this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
40 | try {
41 | return await fs.writeFile(this.heartbeatPath, "")
42 | } catch (error: any) {
43 | logger.warn(error.message)
44 | }
45 | }
46 |
47 | /**
48 | * Call to clear any heartbeatTimer for shutdown.
49 | */
50 | public dispose(): void {
51 | if (typeof this.heartbeatTimer !== "undefined") {
52 | clearTimeout(this.heartbeatTimer)
53 | }
54 | }
55 | }
56 |
57 | /**
58 | * Helper function for the heartbeatTimer.
59 | *
60 | * If heartbeat is active, call beat. Otherwise do nothing.
61 | *
62 | * Extracted to make it easier to test.
63 | */
64 | export async function heartbeatTimer(isActive: Heart["isActive"], beat: Heart["beat"]) {
65 | try {
66 | if (await isActive()) {
67 | beat()
68 | }
69 | } catch (error: unknown) {
70 | logger.warn((error as Error).message)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/node/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18next, { init } from "i18next"
2 | import * as en from "./locales/en.json"
3 | import * as ja from "./locales/ja.json"
4 | import * as th from "./locales/th.json"
5 | import * as ur from "./locales/ur.json"
6 | import * as zhCn from "./locales/zh-cn.json"
7 |
8 | init({
9 | lng: "en",
10 | fallbackLng: "en", // language to use if translations in user language are not available.
11 | returnNull: false,
12 | lowerCaseLng: true,
13 | debug: process.env.NODE_ENV === "development",
14 | resources: {
15 | en: {
16 | translation: en,
17 | },
18 | "zh-cn": {
19 | translation: zhCn,
20 | },
21 | th: {
22 | translation: th,
23 | },
24 | ja: {
25 | translation: ja,
26 | },
27 | ur: {
28 | translation: ur,
29 | },
30 | },
31 | })
32 |
33 | export default i18next
34 |
--------------------------------------------------------------------------------
/src/node/i18n/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "LOGIN_TITLE": "{{app}} login",
3 | "LOGIN_BELOW": "Please log in below.",
4 | "WELCOME": "Welcome to {{app}}",
5 | "LOGIN_PASSWORD": "Check the config file at {{configFile}} for the password.",
6 | "LOGIN_USING_ENV_PASSWORD": "Password was set from $PASSWORD.",
7 | "LOGIN_USING_HASHED_PASSWORD": "Password was set from $HASHED_PASSWORD.",
8 | "SUBMIT": "SUBMIT",
9 | "PASSWORD_PLACEHOLDER": "PASSWORD",
10 | "LOGIN_RATE_LIMIT": "Login rate limited!",
11 | "MISS_PASSWORD": "Missing password",
12 | "INCORRECT_PASSWORD": "Incorrect password"
13 | }
14 |
--------------------------------------------------------------------------------
/src/node/i18n/locales/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "LOGIN_TITLE": "{{app}} ログイン",
3 | "LOGIN_BELOW": "以下によりログインしてください。",
4 | "WELCOME": "ようこそ {{app}} へ!",
5 | "LOGIN_PASSWORD": "パスワードは設定ファイル( {{configFile}} )を確認してください。",
6 | "LOGIN_USING_ENV_PASSWORD": "パスワードは環境変数 $PASSWORD で設定されています。",
7 | "LOGIN_USING_HASHED_PASSWORD": "パスワードは環境変数 $HASHED_PASSWORD で設定されています。",
8 | "SUBMIT": "実行",
9 | "PASSWORD_PLACEHOLDER": "パスワード",
10 | "LOGIN_RATE_LIMIT": "ログイン制限を超えました!",
11 | "MISS_PASSWORD": "パスワードを入力してください。",
12 | "INCORRECT_PASSWORD": "パスワードが間違っています。"
13 | }
14 |
--------------------------------------------------------------------------------
/src/node/i18n/locales/th.json:
--------------------------------------------------------------------------------
1 | {
2 | "LOGIN_TITLE": "เข้าสู่ระบบ {{app}}",
3 | "LOGIN_BELOW": "กรุณาเข้าสู่ระบบด้านล่าง",
4 | "WELCOME": "ยินดีต้อนรับสู่ {{app}}",
5 | "LOGIN_PASSWORD": "ตรวจสอบไฟล์กำหนดค่าที่ {{configFile}} เพื่อดูรหัสผ่าน",
6 | "LOGIN_USING_ENV_PASSWORD": "รหัสผ่านถูกกำหนดเป็น $PASSWORD",
7 | "LOGIN_USING_HASHED_PASSWORD": "รรหัสผ่านถูกกำหนดเป็น $HASHED_PASSWORD",
8 | "SUBMIT": "ส่ง",
9 | "PASSWORD_PLACEHOLDER": "รหัสผ่าน",
10 | "LOGIN_RATE_LIMIT": "ถึงขีดจำกัดอัตราการเข้าสู่ระบบ!",
11 | "MISS_PASSWORD": "รหัสผ่านหายไป",
12 | "INCORRECT_PASSWORD": "รหัสผ่านไม่ถูกต้อง"
13 | }
14 |
--------------------------------------------------------------------------------
/src/node/i18n/locales/ur.json:
--------------------------------------------------------------------------------
1 | {
2 | "LOGIN_TITLE": "{{app}} لاگ ان کریں",
3 | "LOGIN_BELOW": "براہ کرم نیچے لاگ ان کریں۔",
4 | "WELCOME": "میں خوش آمدید {{app}}",
5 | "LOGIN_PASSWORD": "پاس ورڈ کے لیے {{configFile}} پر کنفگ فائل چیک کریں۔",
6 | "LOGIN_USING_ENV_PASSWORD": "پاس ورڈ $PASSWORD سے سیٹ کیا گیا تھا۔",
7 | "LOGIN_USING_HASHED_PASSWORD": "پاس ورڈ $HASHED_PASSWORD سے سیٹ کیا گیا تھا۔",
8 | "SUBMIT": "جمع کرائیں",
9 | "PASSWORD_PLACEHOLDER": "پاس ورڈ",
10 | "LOGIN_RATE_LIMIT": "لاگ ان کی شرح محدود!",
11 | "MISS_PASSWORD": "پاس ورڈ غائب ہے۔",
12 | "INCORRECT_PASSWORD": "غلط پاس ورڈ"
13 | }
14 |
--------------------------------------------------------------------------------
/src/node/i18n/locales/zh-cn.json:
--------------------------------------------------------------------------------
1 | {
2 | "LOGIN_TITLE": "{{app}} 登录",
3 | "LOGIN_BELOW": "请在下面登录。",
4 | "WELCOME": "欢迎来到 {{app}}",
5 | "LOGIN_PASSWORD": "查看配置文件 {{configFile}} 中的密码。",
6 | "LOGIN_USING_ENV_PASSWORD": "密码在 $PASSWORD 中设置。",
7 | "LOGIN_USING_HASHED_PASSWORD": "密码在 $HASHED_PASSWORD 中设置。",
8 | "SUBMIT": "提交",
9 | "PASSWORD_PLACEHOLDER": "密码",
10 | "LOGIN_RATE_LIMIT": "登录速率限制!",
11 | "MISS_PASSWORD": "缺少密码",
12 | "INCORRECT_PASSWORD": "密码不正确"
13 | }
14 |
--------------------------------------------------------------------------------
/src/node/proxy.ts:
--------------------------------------------------------------------------------
1 | import proxyServer from "http-proxy"
2 | import { HttpCode } from "../common/http"
3 |
4 | export const proxy = proxyServer.createProxyServer({})
5 |
6 | // The error handler catches when the proxy fails to connect (for example when
7 | // there is nothing running on the target port).
8 | proxy.on("error", (error, _, res) => {
9 | // This could be for either a web socket or a regular request. Despite what
10 | // the types say, writeHead() will not exist on web socket requests (nor will
11 | // status() from Express). But writing out the code manually does not work
12 | // for regular requests thus the branching behavior.
13 | if (typeof res.writeHead !== "undefined") {
14 | res.writeHead(HttpCode.ServerError)
15 | res.end(error.message)
16 | } else {
17 | res.end(`HTTP/1.1 ${HttpCode.ServerError} ${error.message}\r\n\r\n`)
18 | }
19 | })
20 |
21 | // Intercept the response to rewrite absolute redirects against the base path.
22 | // Is disabled when the request has no base path which means /absproxy is in use.
23 | proxy.on("proxyRes", (res, req) => {
24 | if (res.headers.location && res.headers.location.startsWith("/") && (req as any).base) {
25 | res.headers.location = (req as any).base + res.headers.location
26 | }
27 | })
28 |
--------------------------------------------------------------------------------
/src/node/routes/errors.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import express from "express"
3 | import { promises as fs } from "fs"
4 | import path from "path"
5 | import { HttpCode } from "../../common/http"
6 | import type { WebsocketRequest } from "../wsRouter"
7 | import { rootPath } from "../constants"
8 | import { replaceTemplates } from "../http"
9 | import { escapeHtml, getMediaMime } from "../util"
10 |
11 | interface ErrorWithStatusCode {
12 | statusCode: number
13 | }
14 |
15 | interface ErrorWithCode {
16 | code: string
17 | }
18 |
19 | /** Error is network related. */
20 | export const errorHasStatusCode = (error: any): error is ErrorWithStatusCode => {
21 | return error && "statusCode" in error
22 | }
23 |
24 | /** Error originates from file system. */
25 | export const errorHasCode = (error: any): error is ErrorWithCode => {
26 | return error && "code" in error
27 | }
28 |
29 | const notFoundCodes = [404, "ENOENT", "EISDIR"]
30 |
31 | export const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
32 | let statusCode = 500
33 |
34 | if (errorHasStatusCode(err)) {
35 | statusCode = err.statusCode
36 | } else if (errorHasCode(err) && notFoundCodes.includes(err.code)) {
37 | statusCode = HttpCode.NotFound
38 | }
39 |
40 | res.status(statusCode)
41 |
42 | // Assume anything that explicitly accepts text/html is a user browsing a
43 | // page (as opposed to an xhr request). Don't use `req.accepts()` since
44 | // *every* request that I've seen (in Firefox and Chromium at least)
45 | // includes `*/*` making it always truthy. Even for css/javascript.
46 | if (req.headers.accept && req.headers.accept.includes("text/html")) {
47 | const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
48 | res.set("Content-Type", getMediaMime(resourcePath))
49 | const content = await fs.readFile(resourcePath, "utf8")
50 | res.send(
51 | replaceTemplates(req, content)
52 | .replace(/{{ERROR_TITLE}}/g, statusCode.toString())
53 | .replace(/{{ERROR_HEADER}}/g, statusCode.toString())
54 | .replace(/{{ERROR_BODY}}/g, escapeHtml(err.message)),
55 | )
56 | } else {
57 | res.json({
58 | error: err.message,
59 | ...(err.details || {}),
60 | })
61 | }
62 | }
63 |
64 | export const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
65 | let statusCode = 500
66 | if (errorHasStatusCode(err)) {
67 | statusCode = err.statusCode
68 | } else if (errorHasCode(err) && notFoundCodes.includes(err.code)) {
69 | statusCode = HttpCode.NotFound
70 | }
71 | if (statusCode >= 500) {
72 | logger.error(`${err.message} ${err.stack}`)
73 | } else {
74 | logger.debug(`${err.message} ${err.stack}`)
75 | }
76 | ;(req as WebsocketRequest).ws.end(`HTTP/1.1 ${statusCode} ${err.message}\r\n\r\n`)
77 | }
78 |
--------------------------------------------------------------------------------
/src/node/routes/health.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express"
2 | import { wss, Router as WsRouter } from "../wsRouter"
3 |
4 | export const router = Router()
5 |
6 | router.get("/", (req, res) => {
7 | res.json({
8 | status: req.heart.alive() ? "alive" : "expired",
9 | lastHeartbeat: req.heart.lastHeartbeat,
10 | })
11 | })
12 |
13 | export const wsRouter = WsRouter()
14 |
15 | wsRouter.ws("/", async (req) => {
16 | wss.handleUpgrade(req, req.ws, req.head, (ws) => {
17 | ws.addEventListener("message", () => {
18 | ws.send(
19 | JSON.stringify({
20 | event: "health",
21 | status: req.heart.alive() ? "alive" : "expired",
22 | lastHeartbeat: req.heart.lastHeartbeat,
23 | }),
24 | )
25 | })
26 | req.ws.resume()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/node/routes/logout.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express"
2 | import { CookieKeys } from "../../common/http"
3 | import { getCookieOptions, redirect } from "../http"
4 | import { sanitizeString } from "../util"
5 |
6 | export const router = Router()
7 |
8 | router.get<{}, undefined, undefined, { base?: string; to?: string }>("/", async (req, res) => {
9 | // Must use the *identical* properties used to set the cookie.
10 | res.clearCookie(CookieKeys.Session, getCookieOptions(req))
11 |
12 | const to = sanitizeString(req.query.to) || "/"
13 | return redirect(req, res, to, { to: undefined, base: undefined, href: undefined })
14 | })
15 |
--------------------------------------------------------------------------------
/src/node/routes/pathProxy.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express"
2 | import * as path from "path"
3 | import { HttpCode, HttpError } from "../../common/http"
4 | import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
5 | import { proxy as _proxy } from "../proxy"
6 | import type { WebsocketRequest } from "../wsRouter"
7 |
8 | const getProxyTarget = (
9 | req: Request,
10 | opts?: {
11 | proxyBasePath?: string
12 | },
13 | ): string => {
14 | // If there is a base path, strip it out.
15 | const base = (req as any).base || ""
16 | const port = parseInt(req.params.port, 10)
17 | if (isNaN(port)) {
18 | throw new HttpError("Invalid port", HttpCode.BadRequest)
19 | }
20 | return `http://0.0.0.0:${port}${opts?.proxyBasePath || ""}/${req.originalUrl.slice(base.length)}`
21 | }
22 |
23 | export async function proxy(
24 | req: Request,
25 | res: Response,
26 | opts?: {
27 | passthroughPath?: boolean
28 | proxyBasePath?: string
29 | },
30 | ): Promise {
31 | ensureProxyEnabled(req)
32 |
33 | if (req.method === "OPTIONS" && req.args["skip-auth-preflight"]) {
34 | // Allow preflight requests with `skip-auth-preflight` flag
35 | } else if (!(await authenticated(req))) {
36 | // If visiting the root (/:port only) redirect to the login page.
37 | if (!req.params.path || req.params.path === "/") {
38 | const to = self(req)
39 | return redirect(req, res, "login", {
40 | to: to !== "/" ? to : undefined,
41 | })
42 | }
43 | throw new HttpError("Unauthorized", HttpCode.Unauthorized)
44 | }
45 |
46 | // The base is used for rewriting (redirects, target).
47 | if (!opts?.passthroughPath) {
48 | ;(req as any).base = req.path.split(path.sep).slice(0, 3).join(path.sep)
49 | }
50 |
51 | _proxy.web(req, res, {
52 | ignorePath: true,
53 | target: getProxyTarget(req, opts),
54 | })
55 | }
56 |
57 | export async function wsProxy(
58 | req: WebsocketRequest,
59 | opts?: {
60 | passthroughPath?: boolean
61 | proxyBasePath?: string
62 | },
63 | ): Promise {
64 | ensureProxyEnabled(req)
65 | ensureOrigin(req)
66 | await ensureAuthenticated(req)
67 |
68 | // The base is used for rewriting (redirects, target).
69 | if (!opts?.passthroughPath) {
70 | ;(req as any).base = req.path.split(path.sep).slice(0, 3).join(path.sep)
71 | }
72 |
73 | _proxy.ws(req, req.ws, req.head, {
74 | ignorePath: true,
75 | target: getProxyTarget(req, opts),
76 | })
77 | }
78 |
--------------------------------------------------------------------------------
/src/node/routes/update.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express"
2 | import { version } from "../constants"
3 | import { ensureAuthenticated } from "../http"
4 |
5 | export const router = Router()
6 |
7 | router.get("/check", ensureAuthenticated, async (req, res) => {
8 | const update = await req.updater.getUpdate(req.query.force === "true")
9 | res.json({
10 | checked: update.checked,
11 | latest: update.version,
12 | current: version,
13 | isLatest: req.updater.isLatestVersion(update),
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/node/settings.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import { Query } from "express-serve-static-core"
3 | import { promises as fs } from "fs"
4 |
5 | export type Settings = { [key: string]: Settings | string | boolean | number }
6 |
7 | /**
8 | * Provides read and write access to settings.
9 | */
10 | export class SettingsProvider {
11 | public constructor(private readonly settingsPath: string) {}
12 |
13 | /**
14 | * Read settings from the file. On a failure return last known settings and
15 | * log a warning.
16 | */
17 | public async read(): Promise {
18 | try {
19 | const raw = (await fs.readFile(this.settingsPath, "utf8")).trim()
20 | return raw ? JSON.parse(raw) : {}
21 | } catch (error: any) {
22 | if (error.code !== "ENOENT") {
23 | logger.warn(error.message)
24 | }
25 | }
26 | return {} as T
27 | }
28 |
29 | /**
30 | * Write settings combined with current settings. On failure log a warning.
31 | * Settings will be merged shallowly.
32 | */
33 | public async write(settings: Partial): Promise {
34 | try {
35 | const oldSettings = await this.read()
36 | const nextSettings = { ...oldSettings, ...settings }
37 | await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
38 | } catch (error: any) {
39 | logger.warn(error.message)
40 | }
41 | }
42 | }
43 |
44 | export interface UpdateSettings {
45 | update: {
46 | checked: number
47 | version: string
48 | }
49 | }
50 |
51 | /**
52 | * Global code-server settings.
53 | */
54 | export interface CoderSettings extends UpdateSettings {
55 | query?: Query
56 | }
57 |
--------------------------------------------------------------------------------
/src/node/wsRouter.ts:
--------------------------------------------------------------------------------
1 | import * as express from "express"
2 | import * as expressCore from "express-serve-static-core"
3 | import * as http from "http"
4 | import * as stream from "stream"
5 | import Websocket from "ws"
6 |
7 | export interface WebsocketRequest extends express.Request {
8 | ws: stream.Duplex
9 | head: Buffer
10 | }
11 |
12 | interface InternalWebsocketRequest extends WebsocketRequest {
13 | _ws_handled: boolean
14 | }
15 |
16 | export const handleUpgrade = (app: express.Express, server: http.Server): void => {
17 | server.on("upgrade", (req, socket, head) => {
18 | socket.pause()
19 |
20 | const wreq = req as InternalWebsocketRequest
21 | wreq.ws = socket
22 | wreq.head = head
23 | wreq._ws_handled = false
24 |
25 | // Send the request off to be handled by Express.
26 | ;(app as any).handle(wreq, new http.ServerResponse(wreq), () => {
27 | if (!wreq._ws_handled) {
28 | socket.end("HTTP/1.1 404 Not Found\r\n\r\n")
29 | }
30 | })
31 | })
32 | }
33 |
34 | export type WebSocketHandler = (
35 | req: WebsocketRequest,
36 | res: express.Response,
37 | next: express.NextFunction,
38 | ) => void | Promise
39 |
40 | export class WebsocketRouter {
41 | public readonly router = express.Router()
42 |
43 | /**
44 | * Handle a websocket at this route. Note that websockets are immediately
45 | * paused when they come in.
46 | *
47 | * If the origin header exists it must match the host or the connection will
48 | * be prevented.
49 | */
50 | public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
51 | this.router.get(
52 | route,
53 | ...handlers.map((handler) => {
54 | const wrapped: express.Handler = (req, res, next) => {
55 | ;(req as InternalWebsocketRequest)._ws_handled = true
56 | return handler(req as WebsocketRequest, res, next)
57 | }
58 | return wrapped
59 | }),
60 | )
61 | }
62 | }
63 |
64 | export function Router(): WebsocketRouter {
65 | return new WebsocketRouter()
66 | }
67 |
68 | export const wss = new Websocket.Server({ noServer: true })
69 |
--------------------------------------------------------------------------------
/test/e2e/baseFixture.ts:
--------------------------------------------------------------------------------
1 | import { test as base } from "@playwright/test"
2 | import { CodeServer, CodeServerPage } from "./models/CodeServer"
3 |
4 | /**
5 | * Wraps `test.describe` to create and manage an instance of code-server. If you
6 | * don't use this you will need to create your own code-server instance and pass
7 | * it to `test.use`.
8 | *
9 | * If `includeCredentials` is `true` page requests will be authenticated.
10 | */
11 | export const describe = (
12 | name: string,
13 | codeServerArgs: string[],
14 | codeServerEnv: NodeJS.ProcessEnv,
15 | fn: (codeServer: CodeServer) => void,
16 | ) => {
17 | test.describe(name, () => {
18 | // This will spawn on demand so nothing is necessary on before.
19 | const codeServer = new CodeServer(name, codeServerArgs, codeServerEnv, undefined)
20 |
21 | // Kill code-server after the suite has ended. This may happen even without
22 | // doing it explicitly but it seems prudent to be sure.
23 | test.afterAll(async () => {
24 | await codeServer.close()
25 | })
26 |
27 | test.use({
28 | // Makes `codeServer` and `authenticated` available to the extend call
29 | // below.
30 | codeServer,
31 | // NOTE@jsjoeio some tests use --cert which uses a self-signed certificate
32 | // without this option, those tests will fail.
33 | ignoreHTTPSErrors: true,
34 | })
35 |
36 | fn(codeServer)
37 | })
38 | }
39 |
40 | interface TestFixtures {
41 | codeServer: CodeServer
42 | codeServerPage: CodeServerPage
43 | }
44 |
45 | /**
46 | * Create a test that spawns code-server if necessary and ensures the page is
47 | * ready.
48 | */
49 | export const test = base.extend({
50 | codeServer: undefined, // No default; should be provided through `test.use`.
51 | codeServerPage: async ({ codeServer, page }, use) => {
52 | // It's possible code-server might prevent navigation because of unsaved
53 | // changes (seems to happen based on timing even if no changes have been
54 | // made too). In these cases just accept.
55 | page.on("dialog", (d) => d.accept())
56 |
57 | const codeServerPage = new CodeServerPage(codeServer, page)
58 | await codeServerPage.navigate()
59 | await use(codeServerPage)
60 | },
61 | })
62 |
63 | /** Shorthand for test.expect. */
64 | export const expect = test.expect
65 |
--------------------------------------------------------------------------------
/test/e2e/codeServer.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 | import * as path from "path"
3 | import { getMaybeProxiedCodeServer } from "../utils/helpers"
4 | import { describe, test, expect } from "./baseFixture"
5 | import { CodeServer } from "./models/CodeServer"
6 |
7 | describe("code-server", ["--disable-workspace-trust"], {}, () => {
8 | // TODO@asher: Generalize this? Could be nice if we were to ever need
9 | // multiple migration tests in other suites.
10 | const instances = new Map()
11 | test.afterAll(async () => {
12 | const procs = Array.from(instances.values())
13 | instances.clear()
14 | await Promise.all(procs.map((cs) => cs.close()))
15 | })
16 |
17 | test("should navigate to home page", async ({ codeServerPage }) => {
18 | // We navigate codeServer before each test
19 | // and we start the test with a storage state
20 | // which means we should be logged in
21 | // so it should be on the address
22 | const url = codeServerPage.page.url()
23 | // We use match because there may be a / at the end
24 | // so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/
25 | const address = await getMaybeProxiedCodeServer(codeServerPage)
26 | expect(url).toMatch(address)
27 | })
28 |
29 | test("should always see the code-server editor", async ({ codeServerPage }) => {
30 | expect(await codeServerPage.isEditorVisible()).toBe(true)
31 | })
32 |
33 | test("should show the Integrated Terminal", async ({ codeServerPage }) => {
34 | await codeServerPage.focusTerminal()
35 | expect(await codeServerPage.page.isVisible("#terminal")).toBe(true)
36 | })
37 |
38 | test("should open a file", async ({ codeServerPage }) => {
39 | const dir = await codeServerPage.workspaceDir
40 | const file = path.join(dir, "foo")
41 | await fs.writeFile(file, "bar")
42 | await codeServerPage.openFile(file)
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/test/e2e/extensions.test.ts:
--------------------------------------------------------------------------------
1 | import { test as base } from "@playwright/test"
2 | import * as path from "path"
3 | import { getMaybeProxiedCodeServer } from "../utils/helpers"
4 | import { describe, test, expect } from "./baseFixture"
5 |
6 | function runTestExtensionTests() {
7 | // This will only work if the test extension is loaded into code-server.
8 | test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => {
9 | const address = await getMaybeProxiedCodeServer(codeServerPage)
10 |
11 | await codeServerPage.waitForTestExtensionLoaded()
12 | await codeServerPage.executeCommandViaMenus("code-server: Get proxy URI")
13 |
14 | // Remove end slash in address.
15 | const normalizedAddress = address.replace(/\/+$/, "")
16 | await codeServerPage.page.getByText(`Info: proxyUri: ${normalizedAddress}/proxy/{{port}}/`)
17 | })
18 | }
19 |
20 | const flags = ["--disable-workspace-trust", "--extensions-dir", path.join(__dirname, "./extensions")]
21 |
22 | describe("Extensions", flags, {}, () => {
23 | runTestExtensionTests()
24 | })
25 |
26 | if (process.env.USE_PROXY !== "1") {
27 | describe("Extensions with --cert", [...flags, "--cert"], {}, () => {
28 | runTestExtensionTests()
29 | })
30 | } else {
31 | base.describe("Extensions with --cert", () => {
32 | base.skip("skipped because USE_PROXY is set", () => {
33 | // Playwright will not show this without a function.
34 | })
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/test/e2e/extensions/test-extension/.gitignore:
--------------------------------------------------------------------------------
1 | /extension.js
2 |
--------------------------------------------------------------------------------
/test/e2e/extensions/test-extension/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode"
2 |
3 | export function activate(context: vscode.ExtensionContext) {
4 | vscode.window.showInformationMessage("test extension loaded")
5 | // Test extension
6 | context.subscriptions.push(
7 | vscode.commands.registerCommand("codeServerTest.proxyUri", () => {
8 | if (process.env.VSCODE_PROXY_URI) {
9 | vscode.window.showInformationMessage(`proxyUri: ${process.env.VSCODE_PROXY_URI}`)
10 | } else {
11 | vscode.window.showErrorMessage("No proxy URI was set")
12 | }
13 | }),
14 | )
15 |
16 | // asExternalUri extension
17 | context.subscriptions.push(
18 | vscode.commands.registerCommand("codeServerTest.asExternalUri", async () => {
19 | const input = await vscode.window.showInputBox({
20 | prompt: "URL to pass through to asExternalUri",
21 | })
22 |
23 | if (input) {
24 | const output = await vscode.env.asExternalUri(vscode.Uri.parse(input))
25 | vscode.window.showInformationMessage(`input: ${input} output: ${output}`)
26 | } else {
27 | vscode.window.showErrorMessage(`Failed to run test case. No input provided.`)
28 | }
29 | }),
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/test/e2e/extensions/test-extension/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "code-server-extension",
3 | "version": "0.0.1",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "code-server-extension",
9 | "version": "0.0.1",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "@types/vscode": "^1.56.0",
13 | "typescript": "^5.6.2"
14 | },
15 | "engines": {
16 | "vscode": "^1.56.0"
17 | }
18 | },
19 | "node_modules/@types/vscode": {
20 | "version": "1.94.0",
21 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz",
22 | "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==",
23 | "dev": true,
24 | "license": "MIT"
25 | },
26 | "node_modules/typescript": {
27 | "version": "5.6.3",
28 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
29 | "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
30 | "dev": true,
31 | "license": "Apache-2.0",
32 | "bin": {
33 | "tsc": "bin/tsc",
34 | "tsserver": "bin/tsserver"
35 | },
36 | "engines": {
37 | "node": ">=14.17"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/e2e/extensions/test-extension/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "code-server-extension",
3 | "description": "code-server test extension",
4 | "version": "0.0.1",
5 | "publisher": "coder",
6 | "license": "MIT",
7 | "activationEvents": [
8 | "onStartupFinished"
9 | ],
10 | "engines": {
11 | "vscode": "^1.56.0"
12 | },
13 | "main": "./extension.js",
14 | "contributes": {
15 | "commands": [
16 | {
17 | "command": "codeServerTest.proxyUri",
18 | "title": "Get proxy URI",
19 | "category": "code-server"
20 | },
21 | {
22 | "command": "codeServerTest.asExternalUri",
23 | "title": "asExternalUri test",
24 | "category": "code-server"
25 | }
26 | ]
27 | },
28 | "devDependencies": {
29 | "@types/vscode": "^1.56.0",
30 | "typescript": "^5.6.2"
31 | },
32 | "scripts": {
33 | "build": "tsc"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/e2e/extensions/test-extension/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "outDir": ".",
6 | "strict": true,
7 | "baseUrl": "./",
8 | "skipLibCheck": true
9 | },
10 | "include": ["./extension.ts"],
11 | "exclude": ["node_modules"]
12 | }
13 |
--------------------------------------------------------------------------------
/test/e2e/github.test.ts:
--------------------------------------------------------------------------------
1 | import { test as base } from "@playwright/test"
2 | import { describe, expect, test } from "./baseFixture"
3 |
4 | if (process.env.GITHUB_TOKEN) {
5 | describe("GitHub token", ["--disable-workspace-trust"], {}, () => {
6 | test("should be logged in to pull requests extension", async ({ codeServerPage }) => {
7 | await codeServerPage.exec("git init")
8 | await codeServerPage.exec("git remote add origin https://github.com/coder/code-server")
9 | await codeServerPage.installExtension("GitHub.vscode-pull-request-github")
10 | await codeServerPage.executeCommandViaMenus("View: Show Github")
11 | await codeServerPage.page.click("text=Sign in")
12 | await codeServerPage.page.click("text=Allow")
13 | // It should ask to select an account, one of which will be the one we
14 | // pre-injected.
15 | expect(await codeServerPage.page.isVisible("text=Select an account")).toBe(false)
16 | })
17 | })
18 |
19 | describe("No GitHub token", ["--disable-workspace-trust"], { GITHUB_TOKEN: "" }, () => {
20 | test("should not be logged in to pull requests extension", async ({ codeServerPage }) => {
21 | await codeServerPage.exec("git init")
22 | await codeServerPage.exec("git remote add origin https://github.com/coder/code-server")
23 | await codeServerPage.installExtension("GitHub.vscode-pull-request-github")
24 | await codeServerPage.executeCommandViaMenus("View: Show Github")
25 | await codeServerPage.page.click("text=Sign in")
26 | await codeServerPage.page.click("text=Allow")
27 | // Since there is no account it will ask directly for the token (because
28 | // we are on localhost; otherwise it would initiate the oauth flow).
29 | expect(await codeServerPage.page.isVisible("text=GitHub Personal Access Token")).toBe(false)
30 | })
31 | })
32 | } else {
33 | base.describe("GitHub token", () => {
34 | base.skip("skipped because GITHUB_TOKEN is not set", () => {
35 | // Playwright will not show this without a function.
36 | })
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/test/e2e/login.test.ts:
--------------------------------------------------------------------------------
1 | import { PASSWORD } from "../utils/constants"
2 | import { describe, test, expect } from "./baseFixture"
3 |
4 | describe("login", ["--disable-workspace-trust", "--auth", "password"], {}, () => {
5 | test("should see the login page", async ({ codeServerPage }) => {
6 | // It should send us to the login page
7 | expect(await codeServerPage.page.title()).toBe("code-server login")
8 | })
9 |
10 | test("should be able to login", async ({ codeServerPage }) => {
11 | // Type in password
12 | await codeServerPage.page.fill(".password", PASSWORD)
13 | // Click the submit button and login
14 | await codeServerPage.page.click(".submit")
15 | await codeServerPage.page.waitForLoadState("networkidle")
16 | // We do this because occassionally code-server doesn't load on Firefox
17 | // but loads if you reload once or twice
18 | await codeServerPage.reloadUntilEditorIsReady()
19 | // Make sure the editor actually loaded
20 | expect(await codeServerPage.isEditorVisible()).toBe(true)
21 | })
22 |
23 | test("should see an error message for missing password", async ({ codeServerPage }) => {
24 | // Skip entering password
25 | // Click the submit button and login
26 | await codeServerPage.page.click(".submit")
27 | await codeServerPage.page.waitForLoadState("networkidle")
28 | expect(await codeServerPage.page.isVisible("text=Missing password"))
29 | })
30 |
31 | test("should see an error message for incorrect password", async ({ codeServerPage }) => {
32 | // Type in password
33 | await codeServerPage.page.fill(".password", "password123")
34 | // Click the submit button and login
35 | await codeServerPage.page.click(".submit")
36 | await codeServerPage.page.waitForLoadState("networkidle")
37 | expect(await codeServerPage.page.isVisible("text=Incorrect password"))
38 | })
39 |
40 | test("should hit the rate limiter for too many unsuccessful logins", async ({ codeServerPage }) => {
41 | // Type in password
42 | await codeServerPage.page.fill(".password", "password123")
43 | // Click the submit button and login
44 | // The current RateLimiter allows 2 logins per minute plus
45 | // 12 logins per hour for a total of 14
46 | // See: src/node/routes/login.ts
47 | for (let i = 1; i <= 14; i++) {
48 | await codeServerPage.page.click(".submit")
49 | await codeServerPage.page.waitForLoadState("networkidle")
50 | // We double-check that the correct error message shows
51 | // which should be for incorrect password
52 | expect(await codeServerPage.page.isVisible("text=Incorrect password"))
53 | }
54 |
55 | // The 15th should fail for a different reason:
56 | // login rate
57 | await codeServerPage.page.click(".submit")
58 | await codeServerPage.page.waitForLoadState("networkidle")
59 | expect(await codeServerPage.page.isVisible("text=Login rate limited!"))
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/test/e2e/logout.test.ts:
--------------------------------------------------------------------------------
1 | // NOTE@jsjoeio commenting out until we can figure out what's wrong
2 | // import { describe, test, expect } from "./baseFixture"
3 |
4 | // describe("logout", true, ["--disable-workspace-trust"], {}, () => {
5 | // test("should be able logout", async ({ codeServerPage }) => {
6 | // // Recommended by Playwright for async navigation
7 | // // https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
8 | // await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.navigateMenus(["Log Out"])])
9 | // const currentUrl = codeServerPage.page.url()
10 | // expect(currentUrl).toBe(`${await codeServerPage.address()}/login`)
11 | // })
12 | // })
13 |
--------------------------------------------------------------------------------
/test/e2e/openHelpAbout.test.ts:
--------------------------------------------------------------------------------
1 | import { version } from "../../src/node/constants"
2 | import { describe, test, expect } from "./baseFixture"
3 |
4 | describe("Open Help > About", ["--disable-workspace-trust"], {}, () => {
5 | test("should see code-server version in about dialog", async ({ codeServerPage }) => {
6 | // Open using the menu.
7 | await codeServerPage.navigateMenus(["Help", "About"])
8 |
9 | const isDevMode = process.env.VSCODE_DEV === "1"
10 |
11 | // Look for code-server info div.
12 | const element = await codeServerPage.page.waitForSelector(
13 | `div[role="dialog"] >> text=code-server: ${isDevMode ? "Unknown" : "v" + version}`,
14 | )
15 | expect(element).not.toBeNull()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/test/e2e/terminal.test.ts:
--------------------------------------------------------------------------------
1 | import * as cp from "child_process"
2 | import { promises as fs } from "fs"
3 | import * as path from "path"
4 | import util from "util"
5 | import { clean, getMaybeProxiedCodeServer, tmpdir } from "../utils/helpers"
6 | import { describe, expect, test } from "./baseFixture"
7 |
8 | describe("Integrated Terminal", ["--disable-workspace-trust"], {}, () => {
9 | const testName = "integrated-terminal"
10 | test.beforeAll(async () => {
11 | await clean(testName)
12 | })
13 |
14 | test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => {
15 | const tmpFolderPath = await tmpdir(testName)
16 | const tmpFile = path.join(tmpFolderPath, "pipe")
17 |
18 | const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'`
19 | const exec = util.promisify(cp.exec)
20 | const output = exec(command, { encoding: "utf8" })
21 |
22 | // Open terminal and type in value
23 | await codeServerPage.focusTerminal()
24 |
25 | await codeServerPage.page.keyboard.type(`printenv VSCODE_PROXY_URI > ${tmpFile}`)
26 | await codeServerPage.page.keyboard.press("Enter")
27 |
28 | const { stdout } = await output
29 | const address = await getMaybeProxiedCodeServer(codeServerPage)
30 | expect(stdout).toMatch(address)
31 | })
32 |
33 | // TODO@jsjoeio - add test to make sure full code-server path works
34 | test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => {
35 | const tmpFolderPath = await tmpdir(testName)
36 | const tmpFile = path.join(tmpFolderPath, "test-file")
37 | await fs.writeFile(tmpFile, "test")
38 |
39 | await codeServerPage.focusTerminal()
40 |
41 | await codeServerPage.page.keyboard.type(`code-server ${tmpFile}`)
42 | await codeServerPage.page.keyboard.press("Enter")
43 |
44 | await codeServerPage.waitForTab(path.basename(tmpFile))
45 |
46 | const externalTmpFile = path.join(tmpFolderPath, "test-external-file")
47 | await fs.writeFile(externalTmpFile, "foobar")
48 |
49 | await codeServerPage.openFileExternally(externalTmpFile)
50 |
51 | await codeServerPage.waitForTab(path.basename(externalTmpFile))
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/test/e2e/uploads.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 | import * as path from "path"
3 | import { clean } from "../utils/helpers"
4 | import { describe, test, expect } from "./baseFixture"
5 |
6 | describe("Uploads (enabled)", ["--disable-workspace-trust"], {}, () => {
7 | const testName = "uploads-enabled"
8 | test.beforeAll(async () => {
9 | await clean(testName)
10 | })
11 |
12 | test("should see the 'Upload...' option", async ({ codeServerPage }) => {
13 | // Setup
14 | const workspaceDir = await codeServerPage.workspaceDir
15 | const tmpDirPath = path.join(workspaceDir, "test-directory")
16 | await fs.mkdir(tmpDirPath)
17 |
18 | // Action
19 | await codeServerPage.openContextMenu('span:has-text("test-directory")')
20 |
21 | expect(await codeServerPage.page.isVisible("text=Upload...")).toBe(true)
22 | })
23 |
24 | test("should see the 'Show Local' button on Open File", async ({ codeServerPage }) => {
25 | // Action
26 | await codeServerPage.navigateMenus(["File", "Open File..."])
27 | await codeServerPage.page.waitForSelector(".quick-input-widget")
28 | expect(await codeServerPage.page.isVisible("text=Show Local")).toBe(true)
29 | })
30 | })
31 |
32 | describe("Uploads (disabled)", ["--disable-workspace-trust", "--disable-file-uploads"], {}, () => {
33 | const testName = "uploads-disabled"
34 | test.beforeAll(async () => {
35 | await clean(testName)
36 | })
37 |
38 | test("should not see the 'Upload...' option", async ({ codeServerPage }) => {
39 | // Setup
40 | const workspaceDir = await codeServerPage.workspaceDir
41 | const tmpDirPath = path.join(workspaceDir, "test-directory")
42 | await fs.mkdir(tmpDirPath)
43 |
44 | // Action
45 | await codeServerPage.openContextMenu('span:has-text("test-directory")')
46 |
47 | expect(await codeServerPage.page.isVisible("text=Upload...")).toBe(false)
48 | })
49 |
50 | test("should not see the 'Show Local' button on Open File", async ({ codeServerPage }) => {
51 | // Action
52 | await codeServerPage.navigateMenus(["File", "Open File..."])
53 | await codeServerPage.page.waitForSelector(".quick-input-widget")
54 | expect(await codeServerPage.page.isVisible("text=Show Local")).toBe(false)
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/test/e2e/webview.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 | import * as path from "path"
3 | import { describe, test, expect } from "./baseFixture"
4 |
5 | describe("Webviews", ["--disable-workspace-trust"], {}, () => {
6 | test("should preview a Markdown file", async ({ codeServerPage }) => {
7 | // Create Markdown file
8 | const heading = "Hello world"
9 | const dir = await codeServerPage.workspaceDir
10 | const file = path.join(dir, "text.md")
11 | await fs.writeFile(file, `# ${heading}`)
12 | await codeServerPage.openFile(file)
13 |
14 | // Open Preview
15 | await codeServerPage.executeCommandViaMenus("Markdown: Open Preview to the Side")
16 | // Wait for the iframe to open and load
17 | await codeServerPage.waitForTab(`Preview ${file}`)
18 |
19 | // It's an iframe within an iframe
20 | // so we have to do .frameLocator twice
21 | await expect(
22 | codeServerPage.page.frameLocator("iframe.webview.ready").frameLocator("#active-frame").getByText("Hello world"),
23 | ).toBeVisible()
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/test/integration/help.test.ts:
--------------------------------------------------------------------------------
1 | import { runCodeServerCommand } from "../utils/runCodeServerCommand"
2 |
3 | // NOTE@jsjoeio
4 | // We have this test to ensure that native modules
5 | // work as expected. If this is called on the wrong
6 | // platform, the test will fail.
7 | describe("--help", () => {
8 | it("should list code-server usage", async () => {
9 | const expectedOutput = "Usage: code-server [options] [path]"
10 | const { stdout } = await runCodeServerCommand(["--help"])
11 | expect(stdout).toMatch(expectedOutput)
12 | }, 20000)
13 | })
14 |
--------------------------------------------------------------------------------
/test/integration/installExtension.test.ts:
--------------------------------------------------------------------------------
1 | import { clean, tmpdir } from "../utils/helpers"
2 | import { runCodeServerCommand } from "../utils/runCodeServerCommand"
3 |
4 | describe("--install-extension", () => {
5 | const testName = "installExtension"
6 | let tempDir: string
7 | let setupFlags: string[]
8 |
9 | beforeEach(async () => {
10 | await clean(testName)
11 | tempDir = await tmpdir(testName)
12 | setupFlags = ["--extensions-dir", tempDir]
13 | })
14 | it("should use EXTENSIONS_GALLERY when set", async () => {
15 | const extName = "author.extension"
16 | await expect(
17 | runCodeServerCommand([...setupFlags, "--install-extension", extName], {
18 | EXTENSIONS_GALLERY: "{}",
19 | }),
20 | ).rejects.toThrow("No extension gallery service configured")
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "license": "MIT",
3 | "#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.",
4 | "devDependencies": {
5 | "@jest-mock/express": "^1.4.5",
6 | "@playwright/test": "^1.46.0",
7 | "@types/jest": "^27.0.2",
8 | "@types/jsdom": "^16.2.13",
9 | "@types/node-fetch": "^2.5.8",
10 | "@types/supertest": "^2.0.11",
11 | "@types/wtfnode": "^0.7.0",
12 | "argon2": "^0.28.0",
13 | "extract-zip": "^2.0.1",
14 | "jest": "^27.3.1",
15 | "jest-fetch-mock": "^3.0.3",
16 | "jsdom": "^16.4.0",
17 | "node-fetch": "^2.6.7",
18 | "playwright": "^1.46.0",
19 | "supertest": "^6.1.6",
20 | "ts-jest": "^27.0.7",
21 | "wtfnode": "^0.9.1"
22 | },
23 | "resolutions": {
24 | "ansi-regex": "^5.0.1",
25 | "argon2/@mapbox/node-pre-gyp/tar": "^6.1.9",
26 | "set-value": "^4.0.1",
27 | "tmpl": "^1.0.5",
28 | "path-parse": "^1.0.7",
29 | "json-schema": "^0.4.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { PlaywrightTestConfig } from "@playwright/test"
2 |
3 | import path from "path"
4 |
5 | // The default configuration runs all tests in three browsers with workers equal
6 | // to half the available threads. See 'npm run test:e2e --help' to customize
7 | // from the command line. For example:
8 | // npm run test:e2e --workers 1 # Run with one worker
9 | // npm run test:e2e --project Chromium # Only run on Chromium
10 | // npm run test:e2e --grep login # Run tests matching "login"
11 | // PWDEBUG=1 npm run test:e2e # Run Playwright inspector
12 | const config: PlaywrightTestConfig = {
13 | testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
14 | timeout: 60000, // Each test is given 60 seconds.
15 | retries: process.env.CI ? 2 : 1, // Retry in CI due to flakiness.
16 | // Limit the number of failures on CI to save resources
17 | maxFailures: process.env.CI ? 3 : undefined,
18 | globalSetup: require.resolve("./utils/globalE2eSetup.ts"),
19 | reporter: "list",
20 | // Put any shared options on the top level.
21 | use: {
22 | headless: true, // Run tests in headless browsers.
23 | video: "retain-on-failure",
24 | },
25 |
26 | projects: [
27 | {
28 | name: "Chromium",
29 | use: { browserName: "chromium" },
30 | },
31 | // Firefox seems to have bugs with opening context menus in the file tree.
32 | // {
33 | // name: "Firefox",
34 | // use: { browserName: "firefox" },
35 | // },
36 | {
37 | name: "WebKit",
38 | use: { browserName: "webkit" },
39 | },
40 | ],
41 | }
42 |
43 | export default config
44 |
--------------------------------------------------------------------------------
/test/scripts/build-lib.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | SCRIPT_NAME="build-lib.sh"
4 | SCRIPT="$BATS_TEST_DIRNAME/../../ci/build/$SCRIPT_NAME"
5 |
6 | source "$SCRIPT"
7 |
8 | @test "get_nfpm_arch should return armhfp for rpm on armv7l" {
9 | run get_nfpm_arch rpm armv7l
10 | [ "$output" = "armhfp" ]
11 | }
12 |
13 | @test "get_nfpm_arch should return armhf for deb on armv7l" {
14 | run get_nfpm_arch deb armv7l
15 | [ "$output" = "armhf" ]
16 | }
17 |
18 | @test "get_nfpm_arch should return arch if no arch override exists " {
19 | run get_nfpm_arch deb i386
20 | [ "$output" = "i386" ]
21 | }
--------------------------------------------------------------------------------
/test/scripts/steps-lib.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | SCRIPT_NAME="steps-lib.sh"
4 | SCRIPT="$BATS_TEST_DIRNAME/../../ci/steps/$SCRIPT_NAME"
5 |
6 | source "$SCRIPT"
7 |
8 | @test "is_env_var_set should return 1 if env var is not set" {
9 | run is_env_var_set "ASDF_TEST_SET"
10 | [ "$status" = 1 ]
11 | }
12 |
13 | @test "is_env_var_set should return 0 if env var is set" {
14 | ASDF_TEST_SET="test" run is_env_var_set "ASDF_TEST_SET"
15 | [ "$status" = 0 ]
16 | }
17 |
18 | @test "directory_exists should 1 if directory doesn't exist" {
19 | run directory_exists "/tmp/asdfasdfasdf"
20 | [ "$status" = 1 ]
21 | }
22 |
23 | @test "directory_exists should 0 if directory exists" {
24 | run directory_exists "$(pwd)"
25 | [ "$status" = 0 ]
26 | }
27 |
28 | @test "file_exists should 1 if file doesn't exist" {
29 | run file_exists "hello-asfd.sh"
30 | [ "$status" = 1 ]
31 | }
32 |
33 | @test "file_exists should 0 if file exists" {
34 | run file_exists "$SCRIPT"
35 | [ "$status" = 0 ]
36 | }
37 |
38 | @test "is_executable should 1 if file isn't executable" {
39 | run is_executable "hello-asfd.sh"
40 | [ "$status" = 1 ]
41 | }
42 |
43 | @test "is_executable should 0 if file is executable" {
44 | run is_executable "$SCRIPT"
45 | [ "$status" = 0 ]
46 | }
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": ["./**/*.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/test/unit/common/emitter.test.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import { Emitter } from "../../../src/common/emitter"
3 | import { mockLogger } from "../../utils/helpers"
4 |
5 | describe("emitter", () => {
6 | beforeEach(() => {
7 | mockLogger()
8 | })
9 |
10 | afterEach(() => {
11 | jest.clearAllMocks()
12 | })
13 |
14 | it("should run the correct callbacks", async () => {
15 | const HELLO_WORLD = "HELLO_WORLD"
16 | const GOODBYE_WORLD = "GOODBYE_WORLD"
17 | const mockCallback = jest.fn(() => "Mock function called")
18 | const mockSecondCallback = jest.fn(() => undefined)
19 |
20 | const emitter = new Emitter<{ event: string; callback: () => void }>()
21 |
22 | const onHelloWorld = ({ event, callback }: { event: string; callback: () => void }): void => {
23 | if (event === HELLO_WORLD) {
24 | callback()
25 | }
26 | }
27 |
28 | const onGoodbyeWorld = ({ event, callback }: { event: string; callback: () => void }): void => {
29 | if (event === GOODBYE_WORLD) {
30 | callback()
31 | }
32 | }
33 |
34 | // Register the onHelloWorld listener
35 | // and the onGoodbyeWorld
36 | const _onHelloWorld = emitter.event(onHelloWorld)
37 | emitter.event(onGoodbyeWorld)
38 |
39 | await emitter.emit({ event: HELLO_WORLD, callback: mockCallback })
40 |
41 | // Double-check that our callback is called only once
42 | expect(mockCallback).toHaveBeenCalled()
43 | expect(mockCallback).toHaveBeenCalledTimes(1)
44 |
45 | await emitter.emit({ event: GOODBYE_WORLD, callback: mockSecondCallback })
46 |
47 | // Check that it works with multiple listeners
48 | expect(mockSecondCallback).toHaveBeenCalled()
49 | expect(mockSecondCallback).toHaveBeenCalledTimes(1)
50 |
51 | // Dispose of individual listener
52 | _onHelloWorld.dispose()
53 |
54 | // Try disposing twice
55 | _onHelloWorld.dispose()
56 |
57 | // Dispose of all the listeners
58 | emitter.dispose()
59 | })
60 |
61 | it("should log an error if something goes wrong", async () => {
62 | const HELLO_WORLD = "HELLO_WORLD"
63 | const mockCallback = jest.fn(() => "Mock function called")
64 | const message = "You don't have access to that folder."
65 |
66 | const emitter = new Emitter<{ event: string; callback: () => void }>()
67 |
68 | const onHelloWorld = ({ event, callback }: { event: string; callback: () => void }): void => {
69 | if (event === HELLO_WORLD) {
70 | callback()
71 | throw new Error(message)
72 | }
73 | }
74 |
75 | emitter.event(onHelloWorld)
76 |
77 | await emitter.emit({ event: HELLO_WORLD, callback: mockCallback })
78 |
79 | // Check that error was called
80 | expect(logger.error).toHaveBeenCalled()
81 | expect(logger.error).toHaveBeenCalledTimes(1)
82 | expect(logger.error).toHaveBeenCalledWith(message)
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/test/unit/common/http.test.ts:
--------------------------------------------------------------------------------
1 | import { HttpCode, HttpError } from "../../../src/common/http"
2 |
3 | describe("http", () => {
4 | describe("HttpCode", () => {
5 | it("should return the correct HTTP codes", () => {
6 | expect(HttpCode.Ok).toBe(200)
7 | expect(HttpCode.Redirect).toBe(302)
8 | expect(HttpCode.NotFound).toBe(404)
9 | expect(HttpCode.BadRequest).toBe(400)
10 | expect(HttpCode.Unauthorized).toBe(401)
11 | expect(HttpCode.LargePayload).toBe(413)
12 | expect(HttpCode.ServerError).toBe(500)
13 | })
14 | })
15 |
16 | describe("HttpError", () => {
17 | it("should work as expected", () => {
18 | const message = "Bad request from client"
19 | const httpError = new HttpError(message, HttpCode.BadRequest)
20 |
21 | expect(httpError.message).toBe(message)
22 | expect(httpError.statusCode).toBe(400)
23 | expect(httpError.details).toBeUndefined()
24 | })
25 | it("should have details if provided", () => {
26 | const details = {
27 | message: "User needs to be signed-in in order to perform action",
28 | }
29 | const message = "Unauthorized"
30 | const httpError = new HttpError(message, HttpCode.BadRequest, details)
31 |
32 | expect(httpError.details).toStrictEqual(details)
33 | })
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/test/unit/common/util.test.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@coder/logger"
2 | import { JSDOM } from "jsdom"
3 | import * as util from "../../../src/common/util"
4 | import { mockLogger } from "../../utils/helpers"
5 |
6 | const dom = new JSDOM()
7 | global.document = dom.window.document
8 |
9 | export type LocationLike = Pick
10 |
11 | describe("util", () => {
12 | describe("normalize", () => {
13 | it("should remove multiple slashes", () => {
14 | expect(util.normalize("//foo//bar//baz///mumble")).toBe("/foo/bar/baz/mumble")
15 | })
16 |
17 | it("should remove trailing slashes", () => {
18 | expect(util.normalize("qux///")).toBe("qux")
19 | })
20 |
21 | it("should preserve trailing slash if it exists", () => {
22 | expect(util.normalize("qux///", true)).toBe("qux/")
23 | expect(util.normalize("qux", true)).toBe("qux")
24 | })
25 | })
26 |
27 | describe("plural", () => {
28 | it("should add an s if count is greater than 1", () => {
29 | expect(util.plural(2, "dog")).toBe("dogs")
30 | })
31 | it("should NOT add an s if the count is 1", () => {
32 | expect(util.plural(1, "dog")).toBe("dog")
33 | })
34 | })
35 |
36 | describe("generateUuid", () => {
37 | it("should generate a unique uuid", () => {
38 | const uuid = util.generateUuid()
39 | const uuid2 = util.generateUuid()
40 | expect(uuid).toHaveLength(24)
41 | expect(typeof uuid).toBe("string")
42 | expect(uuid).not.toBe(uuid2)
43 | })
44 | it("should generate a uuid of a specific length", () => {
45 | const uuid = util.generateUuid(10)
46 | expect(uuid).toHaveLength(10)
47 | })
48 | })
49 |
50 | describe("logError", () => {
51 | beforeAll(() => {
52 | mockLogger()
53 | })
54 |
55 | afterEach(() => {
56 | jest.clearAllMocks()
57 | })
58 |
59 | it("should log an error with the message and stack trace", () => {
60 | const message = "You don't have access to that folder."
61 | const error = new Error(message)
62 |
63 | util.logError(logger, "ui", error)
64 |
65 | expect(logger.error).toHaveBeenCalled()
66 | expect(logger.error).toHaveBeenCalledWith(`ui: ${error.message} ${error.stack}`)
67 | })
68 |
69 | it("should log an error, even if not an instance of error", () => {
70 | util.logError(logger, "api", "oh no")
71 |
72 | expect(logger.error).toHaveBeenCalled()
73 | expect(logger.error).toHaveBeenCalledWith("api: oh no")
74 | })
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/test/unit/helpers.test.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs"
2 | import { clean, getAvailablePort, getMaybeProxiedPathname, tmpdir, useEnv } from "../../test/utils/helpers"
3 | import { REVERSE_PROXY_BASE_PATH } from "../utils/constants"
4 |
5 | /**
6 | * This file is for testing test helpers (not core code).
7 | */
8 | describe("test helpers", () => {
9 | const testName = "temp-dir"
10 | beforeAll(async () => {
11 | await clean(testName)
12 | })
13 |
14 | it("should return a temp directory", async () => {
15 | const pathToTempDir = await tmpdir(testName)
16 | expect(pathToTempDir).toContain(testName)
17 | expect(fs.access(pathToTempDir)).resolves.toStrictEqual(undefined)
18 | })
19 | })
20 |
21 | describe("useEnv", () => {
22 | beforeAll(() => {
23 | jest.resetModules()
24 | process.env.TEST_USE_ENV = "test environment variable"
25 | })
26 | afterAll(() => {
27 | delete process.env.TEST_USE_ENV
28 | })
29 | it("should set and reset the env var", () => {
30 | const envKey = "TEST_ENV_VAR"
31 | const [setValue, resetValue] = useEnv(envKey)
32 | setValue("hello-world")
33 | expect(process.env[envKey]).toEqual("hello-world")
34 | resetValue()
35 | expect(process.env[envKey]).toEqual(undefined)
36 | })
37 | it("should set and reset the env var where a value was already set", () => {
38 | const envKey = "TEST_USE_ENV"
39 | expect(process.env[envKey]).toEqual("test environment variable")
40 | const [setValue, resetValue] = useEnv(envKey)
41 | setValue("hello there")
42 | expect(process.env[envKey]).toEqual("hello there")
43 | resetValue()
44 | expect(process.env[envKey]).toEqual("test environment variable")
45 | })
46 | })
47 |
48 | describe("getAvailablePort", () => {
49 | it("should return a valid port", async () => {
50 | const port = await getAvailablePort()
51 | expect(port).toBeGreaterThan(0)
52 | expect(port).toBeLessThanOrEqual(65535)
53 | })
54 | it("should return different ports for different calls", async () => {
55 | const portOne = await getAvailablePort()
56 | const portTwo = await getAvailablePort()
57 | expect(portOne).not.toEqual(portTwo)
58 | })
59 | })
60 |
61 | describe("getMaybeProxiedPathname", () => {
62 | it("should return the route", () => {
63 | const route = "/vscode"
64 | const url = new URL(`http://localhost:3000${route}`)
65 | const actual = getMaybeProxiedPathname(url)
66 | expect(actual).toBe(route)
67 | })
68 | it("should strip proxy if env var set", () => {
69 | const envKey = "USE_PROXY"
70 | const [setValue, resetValue] = useEnv(envKey)
71 | setValue("1")
72 | const route = "/vscode"
73 | const url = new URL(`http://localhost:3000/8000/${REVERSE_PROXY_BASE_PATH}${route}`)
74 | const actual = getMaybeProxiedPathname(url)
75 | expect(actual).toBe(route)
76 | resetValue()
77 | })
78 | })
79 |
--------------------------------------------------------------------------------
/test/unit/node/routes/errors.test.ts:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import { errorHandler } from "../../../../src/node/routes/errors"
3 |
4 | describe("error page is rendered for text/html requests", () => {
5 | it("escapes any html in the error messages", async () => {
6 | const next = jest.fn()
7 | const err = {
8 | code: "ENOENT",
9 | statusCode: 404,
10 | message: ";>hello",
11 | }
12 | const req = createRequest()
13 | const res = {
14 | status: jest.fn().mockReturnValue(this),
15 | send: jest.fn().mockReturnValue(this),
16 | set: jest.fn().mockReturnValue(this),
17 | } as unknown as express.Response
18 |
19 | await errorHandler(err, req, res, next)
20 | expect(res.status).toHaveBeenCalledWith(404)
21 | expect(res.send).toHaveBeenCalledWith(expect.not.stringContaining("