├── .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 | [!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/coder/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://coder.com/community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq) [![codecov](https://codecov.io/gh/coder/code-server/branch/main/graph/badge.svg?token=5iM9farjnC)](https://codecov.io/gh/coder/code-server) [![See latest](https://img.shields.io/static/v1?label=Docs&message=see%20latest&color=blue)](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 | ![Screenshot](./assets/screenshot-1.png) 9 | ![Screenshot](./assets/screenshot-2.png) 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 | 2 | 3 | -------------------------------------------------------------------------------- /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 |

{{ERROR_HEADER}}

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 |
26 |
27 |
28 |

{{WELCOME_TEXT}}

29 |
{{I18N_LOGIN_BELOW}} {{PASSWORD_MSG}}
30 |
31 |
32 | 50 |
51 |
52 |
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("