mirror of
https://github.com/coder/code-server.git
synced 2026-04-13 21:32:52 -05:00
Compare commits
9 Commits
v4.10.0
...
v4.11.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0431069a1 | ||
|
|
f9cc07926b | ||
|
|
be40eca5d9 | ||
|
|
9ba08907da | ||
|
|
d477972c68 | ||
|
|
a47cd81d8c | ||
|
|
c9fbcffd53 | ||
|
|
befa76d09d | ||
|
|
e0ece195c1 |
10
.github/workflows/build.yaml
vendored
10
.github/workflows/build.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v26.1
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: |
|
||||
docs/**
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v26.1
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: |
|
||||
ci/helm-chart/**
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v26.1
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: |
|
||||
**/*.ts
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v26.1
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: |
|
||||
**/*.ts
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
# different and we need to rebuild.
|
||||
- name: Get latest lib/vscode rev
|
||||
id: vscode-rev
|
||||
run: echo "::set-output name=rev::$(git rev-parse HEAD:./lib/vscode)"
|
||||
run: echo "rev=$(git rev-parse HEAD:./lib/vscode)" >> $GITHUB_OUTPUT
|
||||
|
||||
# We need to rebuild when we have a new version of Code, when any of
|
||||
# the patches changed, or when the code-server version changes (since
|
||||
|
||||
10
.github/workflows/publish.yaml
vendored
10
.github/workflows/publish.yaml
vendored
@@ -29,6 +29,12 @@ jobs:
|
||||
- name: Checkout code-server
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js v16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Download npm package from release artifacts
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
@@ -141,7 +147,7 @@ jobs:
|
||||
VERSION: ${{ env.VERSION }}
|
||||
run: |
|
||||
git checkout -b update-version-${{ env.VERSION }}
|
||||
git add .
|
||||
git add .
|
||||
git commit -m "chore: updating version to ${{ env.VERSION }}"
|
||||
git push -u origin $(git branch --show)
|
||||
gh pr create --repo coder/code-server-aur --title "chore: bump version to ${{ env.VERSION }}" --body "PR opened by @$GITHUB_ACTOR" --assignee $GITHUB_ACTOR
|
||||
@@ -186,7 +192,7 @@ jobs:
|
||||
out-file-path: "release-packages"
|
||||
|
||||
- name: Publish to Docker
|
||||
run: yarn publish:docker
|
||||
run: ./ci/steps/docker-buildx-push.sh
|
||||
env:
|
||||
VERSION: ${{ env.VERSION }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -20,7 +20,20 @@ Code v99.99.999
|
||||
|
||||
-->
|
||||
|
||||
## [4.10.0](https://github.com/coder/code-server/releases/tag/v4.10.0) - TBD
|
||||
## [4.10.1](https://github.com/coder/code-server/releases/tag/v4.10.1) - 2023-03-04
|
||||
|
||||
Code v1.75.1
|
||||
|
||||
### Security
|
||||
|
||||
Added an origin check to web sockets to prevent cross-site hijacking attacks on
|
||||
users using older or niche browser that do not support SameSite cookies and
|
||||
attacks across sub-domains that share the same root domain.
|
||||
|
||||
The check requires the host header to be set so if you use a reverse proxy
|
||||
ensure it forwards that information otherwise web sockets will be blocked.
|
||||
|
||||
## [4.10.0](https://github.com/coder/code-server/releases/tag/v4.10.0) - 2023-02-15
|
||||
|
||||
Code v1.75.1
|
||||
|
||||
@@ -30,7 +43,7 @@ Code v1.75.1
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `--link` (was deprecated in 4.0.1).
|
||||
- Removed `--link` (was deprecated over thirteen months ago in 4.0.1).
|
||||
|
||||
## [4.9.1](https://github.com/coder/code-server/releases/tag/v4.9.1) - 2022-12-15
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 3.5.1
|
||||
version: 3.6.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
appVersion: 4.9.1
|
||||
appVersion: 4.10.1
|
||||
|
||||
@@ -6,7 +6,7 @@ replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: codercom/code-server
|
||||
tag: '4.9.1'
|
||||
tag: '4.10.1'
|
||||
pullPolicy: Always
|
||||
|
||||
# Specifies one or more secrets to be used when pulling images from a
|
||||
|
||||
38
docs/FAQ.md
38
docs/FAQ.md
@@ -390,19 +390,13 @@ Theia doesn't allow you to reuse your existing VS Code config.
|
||||
## What's the difference between code-server and OpenVSCode-Server?
|
||||
|
||||
code-server and OpenVSCode-Server both allow you to access VS Code via a
|
||||
browser. The two projects also use their own [forks of VS Code](https://github.com/coder/vscode) to
|
||||
leverage modern VS Code APIs and stay up to date with the upsteam version.
|
||||
browser. OpenVSCode-Server is a direct fork of VS Code with changes comitted
|
||||
directly while code-server pulls VS Code in via a submodule and makes changes
|
||||
via patch files.
|
||||
|
||||
However, OpenVSCode-Server is scoped at only making VS Code available in the web browser.
|
||||
code-server includes some other features:
|
||||
|
||||
- password auth
|
||||
- proxy web ports
|
||||
- certificate support
|
||||
- plugin API
|
||||
- settings sync (coming soon)
|
||||
|
||||
For more details, see [this discussion post](https://github.com/coder/code-server/discussions/4267#discussioncomment-1411583).
|
||||
However, OpenVSCode-Server is scoped at only making VS Code available as-is in
|
||||
the web browser. code-server contains additional changes to make the self-hosted
|
||||
experience better (see the next section for details).
|
||||
|
||||
## What's the difference between code-server and GitHub Codespaces?
|
||||
|
||||
@@ -410,8 +404,24 @@ Both code-server and GitHub Codespaces allow you to access VS Code via a
|
||||
browser. GitHub Codespaces, however, is a closed-source, paid service offered by
|
||||
GitHub and Microsoft.
|
||||
|
||||
On the other hand, code-server is self-hosted, free, open-source, and
|
||||
can be run on any machine with few limitations.
|
||||
On the other hand, code-server is self-hosted, free, open-source, and can be run
|
||||
on any machine with few limitations.
|
||||
|
||||
Specific changes include:
|
||||
|
||||
- Password authentication
|
||||
- The ability to host at sub-paths
|
||||
- Self-contained web views that do not call out to Microsoft's servers
|
||||
- The ability to use your own marketplace and collect your own telemetry
|
||||
- Built-in proxy for accessing ports on the remote machine integrated into
|
||||
VS Code's ports panel
|
||||
- Wrapper process that spawns VS Code on-demand and has a separate CLI
|
||||
- Notification when updates are available
|
||||
- [Some other things](https://github.com/coder/code-server/tree/main/patches)
|
||||
|
||||
Some of these changes appear very unlikely to ever be adopted by Microsoft.
|
||||
Some may make their way upstream, further closing the gap, but at the moment it
|
||||
looks like there will always be some subtle differences.
|
||||
|
||||
## Does code-server have any security login validation?
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Usage:
|
||||
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
||||
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
||||
and the binary symlinked into ~/.local/bin/code-server
|
||||
To install system wide pass ---prefix=/usr/local
|
||||
To install system wide pass --prefix=/usr/local
|
||||
|
||||
--rsh <bin>
|
||||
Specifies the remote shell for remote installation. Defaults to ssh.
|
||||
|
||||
Submodule lib/vscode updated: 441438abd1...5e805b79fc
@@ -174,7 +174,7 @@ Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
|
||||
+ `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=';`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
|
||||
'child-src \'self\';',
|
||||
`frame-src 'self' https://*.vscode-cdn.net data:;`,
|
||||
'worker-src \'self\' data:;',
|
||||
'worker-src \'self\' data: blob:;',
|
||||
@@ -417,3 +421,70 @@ export class WebClientServer {
|
||||
return void res.end(data);
|
||||
}
|
||||
@@ -250,7 +250,7 @@ Index: code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/base/common/product.ts
|
||||
+++ code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
@@ -32,6 +32,7 @@ export type ExtensionVirtualWorkspaceSup
|
||||
@@ -33,6 +33,7 @@ export type ExtensionVirtualWorkspaceSup
|
||||
|
||||
export interface IProductConfiguration {
|
||||
readonly codeServerVersion?: string
|
||||
@@ -290,7 +290,7 @@ Index: code-server/lib/vscode/src/vs/platform/extensionResourceLoader/common/ext
|
||||
-import { RemoteAuthorities } from 'vs/base/common/network';
|
||||
import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts';
|
||||
|
||||
export const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource';
|
||||
const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource';
|
||||
@@ -75,7 +74,7 @@ export abstract class AbstractExtensionR
|
||||
public getExtensionGalleryResourceURL(galleryExtension: { publisher: string; name: string; version: string }, path?: string): URI | undefined {
|
||||
if (this._extensionGalleryResourceUrlTemplate) {
|
||||
|
||||
@@ -12,7 +12,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
@@ -266,6 +266,11 @@ export interface IWorkbenchConstructionO
|
||||
@@ -260,6 +260,11 @@ export interface IWorkbenchConstructionO
|
||||
*/
|
||||
readonly userDataPath?: string
|
||||
|
||||
@@ -172,7 +172,7 @@ Index: code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/common/contextkeys.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
|
||||
@@ -32,6 +32,8 @@ export const IsFullscreenContext = new R
|
||||
@@ -33,6 +33,8 @@ export const IsFullscreenContext = new R
|
||||
|
||||
export const HasWebFileSystemAccess = new RawContextKey<boolean>('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access)
|
||||
|
||||
|
||||
@@ -14,12 +14,14 @@ We can remove this once upstream supports all language packs.
|
||||
one but is worse because it does not handle non-existent or empty files.
|
||||
7. Replace some caching and Node requires because code-server does not restart
|
||||
when changing the language unlike native Code.
|
||||
8. Make language extensions installable like normal rather than using the
|
||||
special set/clear language actions.
|
||||
|
||||
Index: code-server/lib/vscode/src/vs/server/node/serverServices.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/server/node/serverServices.ts
|
||||
+++ code-server/lib/vscode/src/vs/server/node/serverServices.ts
|
||||
@@ -220,6 +220,9 @@ export async function setupServerService
|
||||
@@ -234,6 +234,9 @@ export async function setupServerService
|
||||
const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority));
|
||||
socketServer.registerChannel('extensions', channel);
|
||||
|
||||
@@ -138,7 +140,7 @@ Index: code-server/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts
|
||||
+++ code-server/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts
|
||||
@@ -30,6 +30,12 @@ export function getNLSConfiguration(lang
|
||||
@@ -32,6 +32,12 @@ export function getNLSConfiguration(lang
|
||||
if (InternalNLSConfiguration.is(value)) {
|
||||
value._languagePackSupport = true;
|
||||
}
|
||||
@@ -151,7 +153,7 @@ Index: code-server/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts
|
||||
return value;
|
||||
});
|
||||
_cache.set(key, result);
|
||||
@@ -44,3 +50,43 @@ export namespace InternalNLSConfiguratio
|
||||
@@ -46,3 +52,43 @@ export namespace InternalNLSConfiguratio
|
||||
return candidate && typeof candidate._languagePackId === 'string';
|
||||
}
|
||||
}
|
||||
@@ -248,6 +250,15 @@ Index: code-server/lib/vscode/src/vs/workbench/workbench.web.main.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/workbench.web.main.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/workbench.web.main.ts
|
||||
@@ -52,7 +52,7 @@ import 'vs/workbench/services/dialogs/br
|
||||
import 'vs/workbench/services/host/browser/browserHostService';
|
||||
import 'vs/workbench/services/lifecycle/browser/lifecycleService';
|
||||
import 'vs/workbench/services/clipboard/browser/clipboardService';
|
||||
-import 'vs/workbench/services/localization/browser/localeService';
|
||||
+import 'vs/workbench/services/localization/electron-sandbox/localeService';
|
||||
import 'vs/workbench/services/path/browser/pathService';
|
||||
import 'vs/workbench/services/themes/browser/browserHostColorSchemeService';
|
||||
import 'vs/workbench/services/encryption/browser/encryptionService';
|
||||
@@ -119,8 +119,9 @@ import 'vs/workbench/contrib/logs/browse
|
||||
// Explorer
|
||||
import 'vs/workbench/contrib/files/browser/files.web.contribution';
|
||||
@@ -264,9 +275,9 @@ Index: code-server/lib/vscode/src/vs/platform/languagePacks/browser/languagePack
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts
|
||||
+++ code-server/lib/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts
|
||||
@@ -6,18 +6,24 @@
|
||||
@@ -5,18 +5,24 @@
|
||||
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { Language } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
+import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -289,8 +300,8 @@ Index: code-server/lib/vscode/src/vs/platform/languagePacks/browser/languagePack
|
||||
+ this.languagePackService = ProxyChannel.toService<ILanguagePackService>(remoteAgentService.getConnection()!.getChannel('languagePacks'))
|
||||
}
|
||||
|
||||
async getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined> {
|
||||
@@ -73,6 +79,6 @@ export class WebLanguagePacksService ext
|
||||
async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined> {
|
||||
@@ -72,6 +78,6 @@ export class WebLanguagePacksService ext
|
||||
|
||||
// Web doesn't have a concept of language packs, so we just return an empty array
|
||||
getInstalledLanguages(): Promise<ILanguagePackItem[]> {
|
||||
@@ -298,11 +309,11 @@ Index: code-server/lib/vscode/src/vs/platform/languagePacks/browser/languagePack
|
||||
+ return this.languagePackService.getInstalledLanguages()
|
||||
}
|
||||
}
|
||||
Index: code-server/lib/vscode/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts
|
||||
Index: code-server/lib/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts
|
||||
@@ -41,7 +41,8 @@ export class NativeLocaleService impleme
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts
|
||||
@@ -51,7 +51,8 @@ class NativeLocaleService implements ILo
|
||||
@IProductService private readonly productService: IProductService
|
||||
) { }
|
||||
|
||||
@@ -312,7 +323,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/localization/electron-san
|
||||
try {
|
||||
const content = await this.textFileService.read(this.environmentService.argvResource, { encoding: 'utf8' });
|
||||
|
||||
@@ -68,9 +69,6 @@ export class NativeLocaleService impleme
|
||||
@@ -78,9 +79,6 @@ class NativeLocaleService implements ILo
|
||||
}
|
||||
|
||||
private async writeLocaleValue(locale: string | undefined): Promise<boolean> {
|
||||
@@ -322,3 +333,70 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/localization/electron-san
|
||||
await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true);
|
||||
return true;
|
||||
}
|
||||
Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
|
||||
@@ -318,9 +318,6 @@ export abstract class AbstractInstallAct
|
||||
if (this.extension.isBuiltin) {
|
||||
return;
|
||||
}
|
||||
- if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) {
|
||||
- return;
|
||||
- }
|
||||
if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) {
|
||||
this.enabled = this.installPreReleaseVersion ? this.extension.hasPreReleaseVersion : this.extension.hasReleaseVersion;
|
||||
this.updateLabel();
|
||||
@@ -697,7 +694,7 @@ export abstract class InstallInOtherServ
|
||||
}
|
||||
|
||||
if (isLanguagePackExtension(this.extension.local.manifest)) {
|
||||
- return true;
|
||||
+ return false;
|
||||
}
|
||||
|
||||
// Prefers to run on UI
|
||||
@@ -1785,17 +1782,6 @@ export class SetLanguageAction extends E
|
||||
update(): void {
|
||||
this.enabled = false;
|
||||
this.class = SetLanguageAction.DisabledClass;
|
||||
- if (!this.extension) {
|
||||
- return;
|
||||
- }
|
||||
- if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) {
|
||||
- return;
|
||||
- }
|
||||
- if (this.extension.gallery && language === getLocale(this.extension.gallery)) {
|
||||
- return;
|
||||
- }
|
||||
- this.enabled = true;
|
||||
- this.class = SetLanguageAction.EnabledClass;
|
||||
}
|
||||
|
||||
override async run(): Promise<any> {
|
||||
@@ -1812,7 +1798,6 @@ export class ClearLanguageAction extends
|
||||
private static readonly DisabledClass = `${ClearLanguageAction.EnabledClass} disabled`;
|
||||
|
||||
constructor(
|
||||
- @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
@ILocaleService private readonly localeService: ILocaleService,
|
||||
) {
|
||||
super(ClearLanguageAction.ID, ClearLanguageAction.TITLE.value, ClearLanguageAction.DisabledClass, false);
|
||||
@@ -1822,17 +1807,6 @@ export class ClearLanguageAction extends
|
||||
update(): void {
|
||||
this.enabled = false;
|
||||
this.class = ClearLanguageAction.DisabledClass;
|
||||
- if (!this.extension) {
|
||||
- return;
|
||||
- }
|
||||
- if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) {
|
||||
- return;
|
||||
- }
|
||||
- if (this.extension.gallery && language !== getLocale(this.extension.gallery)) {
|
||||
- return;
|
||||
- }
|
||||
- this.enabled = true;
|
||||
- this.class = ClearLanguageAction.EnabledClass;
|
||||
}
|
||||
|
||||
override async run(): Promise<any> {
|
||||
|
||||
@@ -143,7 +143,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
@@ -271,6 +271,11 @@ export interface IWorkbenchConstructionO
|
||||
@@ -265,6 +265,11 @@ export interface IWorkbenchConstructionO
|
||||
*/
|
||||
readonly isEnabledFileDownloads?: boolean
|
||||
|
||||
@@ -201,7 +201,7 @@ Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
|
||||
'auth'?: string
|
||||
'disable-file-downloads'?: boolean;
|
||||
'locale'?: string
|
||||
+ 'disable-getting-started-override'?: boolean;
|
||||
+ 'disable-getting-started-override'?: boolean,
|
||||
|
||||
/* ----- server setup ----- */
|
||||
|
||||
@@ -242,7 +242,7 @@ Index: code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/common/contextkeys.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/common/contextkeys.ts
|
||||
@@ -33,6 +33,7 @@ export const IsFullscreenContext = new R
|
||||
@@ -34,6 +34,7 @@ export const IsFullscreenContext = new R
|
||||
export const HasWebFileSystemAccess = new RawContextKey<boolean>('hasWebFileSystemAccess', false, true); // Support for FileSystemAccess web APIs (https://wicg.github.io/file-system-access)
|
||||
|
||||
export const IsEnabledFileDownloads = new RawContextKey<boolean>('isEnabledFileDownloads', true, true);
|
||||
|
||||
@@ -107,7 +107,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/parts/dialogs/dialogHandl
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts
|
||||
@@ -145,8 +145,11 @@ export class BrowserDialogHandler implem
|
||||
@@ -76,8 +76,11 @@ export class BrowserDialogHandler extend
|
||||
|
||||
async about(): Promise<void> {
|
||||
const detailString = (useAgo: boolean): string => {
|
||||
@@ -198,7 +198,7 @@ Index: code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/base/common/product.ts
|
||||
+++ code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
@@ -31,6 +31,8 @@ export type ExtensionVirtualWorkspaceSup
|
||||
@@ -32,6 +32,8 @@ export type ExtensionVirtualWorkspaceSup
|
||||
};
|
||||
|
||||
export interface IProductConfiguration {
|
||||
|
||||
@@ -32,7 +32,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
|
||||
@@ -261,6 +261,11 @@ export interface IWorkbenchConstructionO
|
||||
@@ -255,6 +255,11 @@ export interface IWorkbenchConstructionO
|
||||
*/
|
||||
readonly configurationDefaults?: Record<string, any>;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Index: code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/base/common/product.ts
|
||||
+++ code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
@@ -34,6 +34,7 @@ export interface IProductConfiguration {
|
||||
@@ -35,6 +35,7 @@ export interface IProductConfiguration {
|
||||
readonly codeServerVersion?: string
|
||||
readonly rootEndpoint?: string
|
||||
readonly updateEndpoint?: string
|
||||
|
||||
@@ -75,7 +75,7 @@ Index: code-server/lib/vscode/src/vs/platform/extensionResourceLoader/common/ext
|
||||
import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
-import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts';
|
||||
|
||||
export const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource';
|
||||
const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource';
|
||||
|
||||
@@ -60,7 +59,7 @@ export abstract class AbstractExtensionR
|
||||
private readonly _environmentService: IEnvironmentService,
|
||||
|
||||
@@ -10,7 +10,7 @@ Index: code-server/lib/vscode/src/vs/workbench/services/extensions/common/abstra
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/services/extensions/common/abstractExtensionService.ts
|
||||
+++ code-server/lib/vscode/src/vs/workbench/services/extensions/common/abstractExtensionService.ts
|
||||
@@ -1486,7 +1486,7 @@ class ProposedApiController {
|
||||
@@ -1488,7 +1488,7 @@ class ProposedApiController {
|
||||
|
||||
this._envEnabledExtensions = new Set((_environmentService.extensionEnabledProposedApi ?? []).map(id => ExtensionIdentifier.toKey(id)));
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Index: code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/base/common/product.ts
|
||||
+++ code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
@@ -35,6 +35,7 @@ export interface IProductConfiguration {
|
||||
@@ -36,6 +36,7 @@ export interface IProductConfiguration {
|
||||
readonly rootEndpoint?: string
|
||||
readonly updateEndpoint?: string
|
||||
readonly logoutEndpoint?: string
|
||||
|
||||
@@ -6,7 +6,7 @@ Index: code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/base/common/product.ts
|
||||
+++ code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
@@ -36,6 +36,10 @@ export interface IProductConfiguration {
|
||||
@@ -37,6 +37,10 @@ export interface IProductConfiguration {
|
||||
readonly updateEndpoint?: string
|
||||
readonly logoutEndpoint?: string
|
||||
readonly proxyEndpointTemplate?: string
|
||||
|
||||
@@ -10,7 +10,7 @@ Index: code-server/lib/vscode/build/gulpfile.reh.js
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/build/gulpfile.reh.js
|
||||
+++ code-server/lib/vscode/build/gulpfile.reh.js
|
||||
@@ -192,8 +192,7 @@ function packageTask(type, platform, arc
|
||||
@@ -191,8 +191,7 @@ function packageTask(type, platform, arc
|
||||
|
||||
const src = gulp.src(sourceFolderName + '/**', { base: '.' })
|
||||
.pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); }))
|
||||
@@ -20,7 +20,7 @@ Index: code-server/lib/vscode/build/gulpfile.reh.js
|
||||
|
||||
const workspaceExtensionPoints = ['debuggers', 'jsonValidation'];
|
||||
const isUIExtension = (manifest) => {
|
||||
@@ -232,9 +231,9 @@ function packageTask(type, platform, arc
|
||||
@@ -231,9 +230,9 @@ function packageTask(type, platform, arc
|
||||
.map(name => `.build/extensions/${name}/**`);
|
||||
|
||||
const extensions = gulp.src(extensionPaths, { base: '.build', dot: true });
|
||||
@@ -32,7 +32,7 @@ Index: code-server/lib/vscode/build/gulpfile.reh.js
|
||||
|
||||
let version = packageJson.version;
|
||||
const quality = product.quality;
|
||||
@@ -389,7 +388,7 @@ function tweakProductForServerWeb(produc
|
||||
@@ -388,7 +387,7 @@ function tweakProductForServerWeb(produc
|
||||
const minifyTask = task.define(`minify-vscode-${type}`, task.series(
|
||||
optimizeTask,
|
||||
util.rimraf(`out-vscode-${type}-min`),
|
||||
|
||||
@@ -20,8 +20,8 @@ Index: code-server/lib/vscode/src/vs/server/node/serverServices.ts
|
||||
import { NullPolicyService } from 'vs/platform/policy/common/policy';
|
||||
import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender';
|
||||
import { LoggerService } from 'vs/platform/log/node/loggerService';
|
||||
@@ -142,10 +143,13 @@ export async function setupServerService
|
||||
const machineId = await getMachineId();
|
||||
@@ -151,10 +152,13 @@ export async function setupServerService
|
||||
let oneDsAppender: ITelemetryAppender = NullAppender;
|
||||
const isInternal = isInternalTelemetry(productService, configurationService);
|
||||
if (supportsTelemetry(productService, environmentService)) {
|
||||
- if (productService.aiConfig && productService.aiConfig.ariaKey) {
|
||||
|
||||
@@ -93,7 +93,7 @@ Index: code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/base/common/product.ts
|
||||
+++ code-server/lib/vscode/src/vs/base/common/product.ts
|
||||
@@ -33,6 +33,7 @@ export type ExtensionVirtualWorkspaceSup
|
||||
@@ -34,6 +34,7 @@ export type ExtensionVirtualWorkspaceSup
|
||||
export interface IProductConfiguration {
|
||||
readonly codeServerVersion?: string
|
||||
readonly rootEndpoint?: string
|
||||
|
||||
@@ -62,15 +62,6 @@ Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
|
||||
_wrapWebWorkerExtHostInIframe,
|
||||
developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined, logLevel: this._logService.getLevel() },
|
||||
settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined,
|
||||
@@ -344,7 +345,7 @@ export class WebClientServer {
|
||||
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=';`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
|
||||
'child-src \'self\';',
|
||||
`frame-src 'self' https://*.vscode-cdn.net data:;`,
|
||||
- 'worker-src \'self\' data:;',
|
||||
+ 'worker-src \'self\' data: blob:;',
|
||||
'style-src \'self\' \'unsafe-inline\';',
|
||||
'connect-src \'self\' ws: wss: https:;',
|
||||
'font-src \'self\' blob:;',
|
||||
Index: code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index.html
|
||||
===================================================================
|
||||
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index.html
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum HttpCode {
|
||||
NotFound = 404,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
LargePayload = 413,
|
||||
ServerError = 500,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,15 @@ import { promises as fs } from "fs"
|
||||
import { load } from "js-yaml"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { canConnect, generateCertificate, generatePassword, humanPath, paths, isNodeJSErrnoException } from "./util"
|
||||
import {
|
||||
canConnect,
|
||||
generateCertificate,
|
||||
generatePassword,
|
||||
humanPath,
|
||||
paths,
|
||||
isNodeJSErrnoException,
|
||||
splitOnFirstEquals,
|
||||
} from "./util"
|
||||
|
||||
const DEFAULT_SOCKET_PATH = path.join(os.tmpdir(), "vscode-ipc")
|
||||
|
||||
@@ -292,19 +300,6 @@ export const optionDescriptions = (opts: Partial<Options<Required<UserProvidedAr
|
||||
})
|
||||
}
|
||||
|
||||
export function splitOnFirstEquals(str: string): string[] {
|
||||
// we use regex instead of "=" to ensure we split at the first
|
||||
// "=" and return the following substring with it
|
||||
// important for the hashed-password which looks like this
|
||||
// $argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY
|
||||
// 2 means return two items
|
||||
// Source: https://stackoverflow.com/a/4607799/3015595
|
||||
// We use the ? to say the the substr after the = is optional
|
||||
const split = str.split(/=(.+)?/, 2)
|
||||
|
||||
return split
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arguments into UserProvidedArgs. This should not go beyond checking
|
||||
* that arguments are valid types and have values when required.
|
||||
|
||||
@@ -12,7 +12,15 @@ import { version as codeServerVersion } from "./constants"
|
||||
import { Heart } from "./heart"
|
||||
import { CoderSettings, SettingsProvider } from "./settings"
|
||||
import { UpdateProvider } from "./update"
|
||||
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
|
||||
import {
|
||||
getPasswordMethod,
|
||||
IsCookieValidArgs,
|
||||
isCookieValid,
|
||||
sanitizeString,
|
||||
escapeHtml,
|
||||
escapeJSON,
|
||||
splitOnFirstEquals,
|
||||
} from "./util"
|
||||
|
||||
/**
|
||||
* Base options included on every page.
|
||||
@@ -308,3 +316,68 @@ export const getCookieOptions = (req: express.Request): express.CookieOptions =>
|
||||
export const self = (req: express.Request): string => {
|
||||
return normalize(`${req.baseUrl}${req.originalUrl.endsWith("/") ? "/" : ""}`, true)
|
||||
}
|
||||
|
||||
function getFirstHeader(req: http.IncomingMessage, headerName: string): string | undefined {
|
||||
const val = req.headers[headerName]
|
||||
return Array.isArray(val) ? val[0] : val
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error if origin checks fail. Call `next` if provided.
|
||||
*/
|
||||
export function ensureOrigin(req: express.Request, _?: express.Response, next?: express.NextFunction): void {
|
||||
if (!authenticateOrigin(req)) {
|
||||
throw new HttpError("Forbidden", HttpCode.Forbidden)
|
||||
}
|
||||
if (next) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the request origin against the host.
|
||||
*/
|
||||
export function authenticateOrigin(req: express.Request): boolean {
|
||||
// A missing origin probably means the source is non-browser. Not sure we
|
||||
// have a use case for this but let it through.
|
||||
const originRaw = getFirstHeader(req, "origin")
|
||||
if (!originRaw) {
|
||||
return true
|
||||
}
|
||||
|
||||
let origin: string
|
||||
try {
|
||||
origin = new URL(originRaw).host.trim().toLowerCase()
|
||||
} catch (error) {
|
||||
return false // Malformed URL.
|
||||
}
|
||||
|
||||
// Honor Forwarded if present.
|
||||
const forwardedRaw = getFirstHeader(req, "forwarded")
|
||||
if (forwardedRaw) {
|
||||
const parts = forwardedRaw.split(/[;,]/)
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
const [key, value] = splitOnFirstEquals(parts[i])
|
||||
if (key.trim().toLowerCase() === "host" && value) {
|
||||
return origin === value.trim().toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Honor X-Forwarded-Host if present.
|
||||
const xHost = getFirstHeader(req, "x-forwarded-host")
|
||||
if (xHost) {
|
||||
return origin === xHost.trim().toLowerCase()
|
||||
}
|
||||
|
||||
// A missing host likely means the reverse proxy has not been configured to
|
||||
// forward the host which means we cannot perform the check. Emit a warning
|
||||
// so an admin can fix the issue.
|
||||
const host = getFirstHeader(req, "host")
|
||||
if (!host) {
|
||||
logger.warn(`no host headers found; blocking request to ${req.originalUrl}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return origin === host.trim().toLowerCase()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Router } from "express"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
|
||||
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
|
||||
import { proxy } from "../proxy"
|
||||
import { Router as WsRouter } from "../wsRouter"
|
||||
|
||||
@@ -78,10 +78,8 @@ wsRouter.ws("*", async (req, _, next) => {
|
||||
if (!port) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// Must be authenticated to use the proxy.
|
||||
ensureOrigin(req)
|
||||
await ensureAuthenticated(req)
|
||||
|
||||
proxy.ws(req, req.ws, req.head, {
|
||||
ignorePath: true,
|
||||
target: `http://0.0.0.0:${port}${req.originalUrl}`,
|
||||
|
||||
@@ -63,5 +63,11 @@ export const errorHandler: express.ErrorRequestHandler = async (err, req, res, n
|
||||
|
||||
export const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
logger.error(`${err.message} ${err.stack}`)
|
||||
;(req as WebsocketRequest).ws.end()
|
||||
let statusCode = 500
|
||||
if (errorHasStatusCode(err)) {
|
||||
statusCode = err.statusCode
|
||||
} else if (errorHasCode(err) && notFoundCodes.includes(err.code)) {
|
||||
statusCode = HttpCode.NotFound
|
||||
}
|
||||
;(req as WebsocketRequest).ws.end(`HTTP/1.1 ${statusCode} ${err.message}\r\n\r\n`)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as path from "path"
|
||||
import * as qs from "qs"
|
||||
import * as pluginapi from "../../../typings/pluginapi"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
|
||||
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
|
||||
import { proxy as _proxy } from "../proxy"
|
||||
|
||||
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
|
||||
@@ -50,6 +50,7 @@ export async function wsProxy(
|
||||
passthroughPath?: boolean
|
||||
},
|
||||
): Promise<void> {
|
||||
ensureOrigin(req)
|
||||
await ensureAuthenticated(req)
|
||||
_proxy.ws(req, req.ws, req.head, {
|
||||
ignorePath: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { WebsocketRequest } from "../../../typings/pluginapi"
|
||||
import { logError } from "../../common/util"
|
||||
import { CodeArgs, toCodeArgs } from "../cli"
|
||||
import { isDevMode } from "../constants"
|
||||
import { authenticated, ensureAuthenticated, redirect, replaceTemplates, self } from "../http"
|
||||
import { authenticated, ensureAuthenticated, ensureOrigin, redirect, replaceTemplates, self } from "../http"
|
||||
import { SocketProxyProvider } from "../socket"
|
||||
import { isFile, loadAMDModule } from "../util"
|
||||
import { Router as WsRouter } from "../wsRouter"
|
||||
@@ -173,7 +173,7 @@ export class CodeServerRouteWrapper {
|
||||
this.router.get("/", this.ensureCodeServerLoaded, this.$root)
|
||||
this.router.get("/manifest.json", this.manifest)
|
||||
this.router.all("*", ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyRequest)
|
||||
this._wsRouterWrapper.ws("*", ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket)
|
||||
this._wsRouterWrapper.ws("*", ensureOrigin, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
@@ -541,3 +541,13 @@ export const loadAMDModule = async <T>(amdPath: string, exportName: string): Pro
|
||||
|
||||
return module[exportName] as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string on the first equals. The result will always be an array with
|
||||
* two items regardless of how many equals there are. The second item will be
|
||||
* undefined if empty or missing.
|
||||
*/
|
||||
export function splitOnFirstEquals(str: string): [string, string | undefined] {
|
||||
const split = str.split(/=(.+)?/, 2)
|
||||
return [split[0], split[1]]
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ export class WebsocketRouter {
|
||||
/**
|
||||
* Handle a websocket at this route. Note that websockets are immediately
|
||||
* paused when they come in.
|
||||
*
|
||||
* If the origin header exists it must match the host or the connection will
|
||||
* be prevented.
|
||||
*/
|
||||
public ws(route: expressCore.PathParams, ...handlers: pluginapi.WebSocketHandler[]): void {
|
||||
this.router.get(
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
readSocketPath,
|
||||
setDefaults,
|
||||
shouldOpenInExistingInstance,
|
||||
splitOnFirstEquals,
|
||||
toCodeArgs,
|
||||
optionDescriptions,
|
||||
options,
|
||||
@@ -535,31 +534,6 @@ describe("cli", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("splitOnFirstEquals", () => {
|
||||
it("should split on the first equals", () => {
|
||||
const testStr = "enabled-proposed-api=test=value"
|
||||
const actual = splitOnFirstEquals(testStr)
|
||||
const expected = ["enabled-proposed-api", "test=value"]
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
it("should split on first equals regardless of multiple equals signs", () => {
|
||||
const testStr =
|
||||
"hashed-password=$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY"
|
||||
const actual = splitOnFirstEquals(testStr)
|
||||
const expected = [
|
||||
"hashed-password",
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||
]
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
it("should always return the first element before an equals", () => {
|
||||
const testStr = "auth="
|
||||
const actual = splitOnFirstEquals(testStr)
|
||||
const expected = ["auth"]
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
||||
|
||||
describe("shouldSpawnCliProcess", () => {
|
||||
it("should return false if no 'extension' related args passed in", async () => {
|
||||
const args = {}
|
||||
|
||||
@@ -1,55 +1,118 @@
|
||||
import { getMockReq } from "@jest-mock/express"
|
||||
import { constructRedirectPath, relativeRoot } from "../../../src/node/http"
|
||||
import * as http from "../../../src/node/http"
|
||||
import { mockLogger } from "../../utils/helpers"
|
||||
|
||||
describe("http", () => {
|
||||
it("should construct a relative path to the root", () => {
|
||||
expect(relativeRoot("/")).toStrictEqual(".")
|
||||
expect(relativeRoot("/foo")).toStrictEqual(".")
|
||||
expect(relativeRoot("/foo/")).toStrictEqual("./..")
|
||||
expect(relativeRoot("/foo/bar ")).toStrictEqual("./..")
|
||||
expect(relativeRoot("/foo/bar/")).toStrictEqual("./../..")
|
||||
beforeEach(() => {
|
||||
mockLogger()
|
||||
})
|
||||
})
|
||||
|
||||
describe("constructRedirectPath", () => {
|
||||
it("should preserve slashes in queryString so they are human-readable", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
})
|
||||
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
|
||||
const mockTo = ""
|
||||
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./?folder=/Users/jp/dev/coder"
|
||||
expect(actual).toBe(expected)
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
it("should use an empty string if no query params", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
})
|
||||
const mockQueryParams = {}
|
||||
const mockTo = ""
|
||||
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./"
|
||||
expect(actual).toBe(expected)
|
||||
|
||||
it("should construct a relative path to the root", () => {
|
||||
expect(http.relativeRoot("/")).toStrictEqual(".")
|
||||
expect(http.relativeRoot("/foo")).toStrictEqual(".")
|
||||
expect(http.relativeRoot("/foo/")).toStrictEqual("./..")
|
||||
expect(http.relativeRoot("/foo/bar ")).toStrictEqual("./..")
|
||||
expect(http.relativeRoot("/foo/bar/")).toStrictEqual("./../..")
|
||||
})
|
||||
it("should append the 'to' path relative to the originalUrl", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
|
||||
describe("origin", () => {
|
||||
;[
|
||||
{
|
||||
origin: "",
|
||||
host: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
origin: "http://localhost:8080",
|
||||
host: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
origin: "http://localhost:8080",
|
||||
host: "localhost:8080",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
origin: "http://localhost:8080",
|
||||
host: "localhost:8081",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
origin: "localhost:8080",
|
||||
host: "localhost:8080",
|
||||
expected: false, // Gets parsed as host: localhost and path: 8080.
|
||||
},
|
||||
{
|
||||
origin: "test.org",
|
||||
host: "localhost:8080",
|
||||
expected: false, // Parsing fails completely.
|
||||
},
|
||||
].forEach((test) => {
|
||||
;[
|
||||
["host", test.host],
|
||||
["x-forwarded-host", test.host],
|
||||
["forwarded", `for=127.0.0.1, host=${test.host}, proto=http`],
|
||||
["forwarded", `for=127.0.0.1;proto=http;host=${test.host}`],
|
||||
["forwarded", `proto=http;host=${test.host}, for=127.0.0.1`],
|
||||
].forEach(([key, value]) => {
|
||||
it(`${test.origin} -> [${key}: ${value}]`, () => {
|
||||
const req = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
headers: {
|
||||
origin: test.origin,
|
||||
[key]: value,
|
||||
},
|
||||
})
|
||||
expect(http.authenticateOrigin(req)).toBe(test.expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
const mockQueryParams = {}
|
||||
const mockTo = "vscode"
|
||||
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./vscode"
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it("should append append queryParams after 'to' path", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
|
||||
describe("constructRedirectPath", () => {
|
||||
it("should preserve slashes in queryString so they are human-readable", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
})
|
||||
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
|
||||
const mockTo = ""
|
||||
const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./?folder=/Users/jp/dev/coder"
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it("should use an empty string if no query params", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
})
|
||||
const mockQueryParams = {}
|
||||
const mockTo = ""
|
||||
const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./"
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it("should append the 'to' path relative to the originalUrl", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
})
|
||||
const mockQueryParams = {}
|
||||
const mockTo = "vscode"
|
||||
const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./vscode"
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it("should append append queryParams after 'to' path", () => {
|
||||
const mockReq = getMockReq({
|
||||
originalUrl: "localhost:8080",
|
||||
})
|
||||
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
|
||||
const mockTo = "vscode"
|
||||
const actual = http.constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./vscode?folder=/Users/jp/dev/coder"
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
const mockQueryParams = { folder: "/Users/jp/dev/coder" }
|
||||
const mockTo = "vscode"
|
||||
const actual = constructRedirectPath(mockReq, mockQueryParams, mockTo)
|
||||
const expected = "./vscode?folder=/Users/jp/dev/coder"
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,21 +4,26 @@ import * as http from "http"
|
||||
import nodeFetch from "node-fetch"
|
||||
import { HttpCode } from "../../../src/common/http"
|
||||
import { proxy } from "../../../src/node/proxy"
|
||||
import { getAvailablePort } from "../../utils/helpers"
|
||||
import { wss, Router as WsRouter } from "../../../src/node/wsRouter"
|
||||
import { getAvailablePort, mockLogger } from "../../utils/helpers"
|
||||
import * as httpserver from "../../utils/httpserver"
|
||||
import * as integration from "../../utils/integration"
|
||||
|
||||
describe("proxy", () => {
|
||||
const nhooyrDevServer = new httpserver.HttpServer()
|
||||
const wsApp = express.default()
|
||||
const wsRouter = WsRouter()
|
||||
let codeServer: httpserver.HttpServer | undefined
|
||||
let proxyPath: string
|
||||
let absProxyPath: string
|
||||
let e: express.Express
|
||||
|
||||
beforeAll(async () => {
|
||||
wsApp.use("/", wsRouter.router)
|
||||
await nhooyrDevServer.listen((req, res) => {
|
||||
e(req, res)
|
||||
})
|
||||
nhooyrDevServer.listenUpgrade(wsApp)
|
||||
proxyPath = `/proxy/${nhooyrDevServer.port()}/wsup`
|
||||
absProxyPath = proxyPath.replace("/proxy/", "/absproxy/")
|
||||
})
|
||||
@@ -29,6 +34,7 @@ describe("proxy", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
e = express.default()
|
||||
mockLogger()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -36,6 +42,7 @@ describe("proxy", () => {
|
||||
await codeServer.dispose()
|
||||
codeServer = undefined
|
||||
}
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should rewrite the base path", async () => {
|
||||
@@ -151,6 +158,35 @@ describe("proxy", () => {
|
||||
expect(resp.status).toBe(500)
|
||||
expect(resp.statusText).toMatch("Internal Server Error")
|
||||
})
|
||||
|
||||
it("should pass origin check", async () => {
|
||||
wsRouter.ws("/wsup", async (req) => {
|
||||
wss.handleUpgrade(req, req.ws, req.head, (ws) => {
|
||||
ws.send("hello")
|
||||
req.ws.resume()
|
||||
})
|
||||
})
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const ws = await codeServer.wsWait(proxyPath, {
|
||||
headers: {
|
||||
host: "localhost:8080",
|
||||
origin: "https://localhost:8080",
|
||||
},
|
||||
})
|
||||
ws.terminate()
|
||||
})
|
||||
|
||||
it("should fail origin check", async () => {
|
||||
await expect(async () => {
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
await codeServer.wsWait(proxyPath, {
|
||||
headers: {
|
||||
host: "localhost:8080",
|
||||
origin: "https://evil.org",
|
||||
},
|
||||
})
|
||||
}).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// NOTE@jsjoeio
|
||||
@@ -190,18 +226,18 @@ describe("proxy (standalone)", () => {
|
||||
})
|
||||
|
||||
// Start both servers
|
||||
await proxyTarget.listen(PROXY_PORT)
|
||||
await testServer.listen(PORT)
|
||||
proxyTarget.listen(PROXY_PORT)
|
||||
testServer.listen(PORT)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await testServer.close()
|
||||
await proxyTarget.close()
|
||||
testServer.close()
|
||||
proxyTarget.close()
|
||||
})
|
||||
|
||||
it("should return a 500 when proxy target errors ", async () => {
|
||||
// Close the proxy target so that proxy errors
|
||||
await proxyTarget.close()
|
||||
proxyTarget.close()
|
||||
const errorResp = await nodeFetch(`${URL}/error`)
|
||||
expect(errorResp.status).toBe(HttpCode.ServerError)
|
||||
expect(errorResp.statusText).toBe("Internal Server Error")
|
||||
|
||||
@@ -23,7 +23,9 @@ describe("health", () => {
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const ws = codeServer.ws("/healthz")
|
||||
const message = await new Promise((resolve, reject) => {
|
||||
ws.on("error", console.error)
|
||||
ws.on("error", (err) => {
|
||||
console.error("[healthz]", err)
|
||||
})
|
||||
ws.on("message", (message) => {
|
||||
try {
|
||||
const j = JSON.parse(message.toString())
|
||||
|
||||
30
test/unit/node/routes/vscode.test.ts
Normal file
30
test/unit/node/routes/vscode.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as httpserver from "../../../utils/httpserver"
|
||||
import * as integration from "../../../utils/integration"
|
||||
import { mockLogger } from "../../../utils/helpers"
|
||||
|
||||
describe("vscode", () => {
|
||||
let codeServer: httpserver.HttpServer | undefined
|
||||
beforeEach(() => {
|
||||
mockLogger()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (codeServer) {
|
||||
await codeServer.dispose()
|
||||
codeServer = undefined
|
||||
}
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should fail origin check", async () => {
|
||||
await expect(async () => {
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
await codeServer.wsWait("/vscode", {
|
||||
headers: {
|
||||
host: "localhost:8080",
|
||||
origin: "https://evil.org",
|
||||
},
|
||||
})
|
||||
}).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -601,3 +601,41 @@ describe("constructOpenOptions", () => {
|
||||
expect(urlSearch).toBe("?q=^&test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("splitOnFirstEquals", () => {
|
||||
const tests = [
|
||||
{
|
||||
name: "empty",
|
||||
key: "",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "split on first equals",
|
||||
key: "foo",
|
||||
value: "bar",
|
||||
},
|
||||
{
|
||||
name: "split on first equals even with multiple equals",
|
||||
key: "foo",
|
||||
value: "bar=baz",
|
||||
},
|
||||
{
|
||||
name: "split with empty value",
|
||||
key: "foo",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "split with no value",
|
||||
key: "foo",
|
||||
value: undefined,
|
||||
},
|
||||
]
|
||||
tests.forEach((test) => {
|
||||
it("should ${test.name}", () => {
|
||||
const input = test.key && typeof test.value !== "undefined" ? `${test.key}=${test.value}` : test.key
|
||||
const [key, value] = util.splitOnFirstEquals(input)
|
||||
expect(key).toStrictEqual(test.key)
|
||||
expect(value).toStrictEqual(test.value || undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Disposable } from "../../src/common/emitter"
|
||||
import * as util from "../../src/common/util"
|
||||
import { ensureAddress } from "../../src/node/app"
|
||||
import { disposer } from "../../src/node/http"
|
||||
|
||||
import { handleUpgrade } from "../../src/node/wsRouter"
|
||||
|
||||
// Perhaps an abstraction similar to this should be used in app.ts as well.
|
||||
@@ -76,14 +75,25 @@ export class HttpServer {
|
||||
/**
|
||||
* Open a websocket against the request path.
|
||||
*/
|
||||
public ws(requestPath: string): Websocket {
|
||||
public ws(requestPath: string, options?: Websocket.ClientOptions): Websocket {
|
||||
const address = ensureAddress(this.hs, "ws")
|
||||
if (typeof address === "string") {
|
||||
throw new Error("Cannot open websocket to socket path")
|
||||
}
|
||||
address.pathname = requestPath
|
||||
|
||||
return new Websocket(address.toString())
|
||||
return new Websocket(address.toString(), options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a websocket and wait for it to fully open.
|
||||
*/
|
||||
public wsWait(requestPath: string, options?: Websocket.ClientOptions): Promise<Websocket> {
|
||||
const ws = this.ws(requestPath, options)
|
||||
return new Promise<Websocket>((resolve, reject) => {
|
||||
ws.on("error", (err) => reject(err))
|
||||
ws.on("open", () => resolve(ws))
|
||||
})
|
||||
}
|
||||
|
||||
public port(): number {
|
||||
|
||||
Reference in New Issue
Block a user