Compare commits

..

46 Commits
3.0.0 ... 3.1.0

Author SHA1 Message Date
Asher
5aded14b87 Merge pull request #1453 from cdr/proxy
HTTP proxy
2020-04-08 12:44:29 -05:00
Asher
a288351ad4 Respond when proxy errors
Otherwise the request will just hang.
2020-04-08 11:54:18 -05:00
Asher
3b39482420 Document workspace and folder behavior
Also fixed a type issue.
2020-04-07 17:49:50 -05:00
Asher
a5c35af81b Fix encoding issues with folder and workspace params
The raw value is now passed back to VS Code so it can do the parsing
with its own URI class rather than trying to parse using Node's url
module first since that has no guarantee of working the same way. It
also lets us keep the vscode-remote bit internal to VS Code.

Removed the logic that keeps trying paths until it finds a valid one
because it seems confusing to open a path and silently get some other
path instead of an error for the one you tried to open. Now it'll just
use exactly what you specified or fail trying.

Fixes #1488. The problem here was that url.parse was encoding the spaces
then the validation failed looking for a literal %20.
2020-04-07 15:18:19 -05:00
Charles Moog
b78bdaf46e Merge pull request #1496 from cdr/report-issue-url
Send report issues to code-server repo
2020-04-06 17:29:53 -05:00
cmoog
aefef5b0e8 Send report issues to code-server repo 2020-04-06 22:23:14 +00:00
Abin Simon
ca998240a0 Fix typo in FAQ (#1489) 2020-04-03 13:09:32 -05:00
Asher
d2a31477c7 Merge pull request #1486 from cdr/update-backup
Back up old directory when updating
2020-04-02 17:28:27 -05:00
Asher
9c6581273e Show proper error when an update fails 2020-04-02 17:20:25 -05:00
Asher
d1445a8135 Back up code-server directory when updating 2020-04-02 16:21:48 -05:00
Asher
5fc00acc39 Fix incorrect reporting that an update failed 2020-04-02 14:48:15 -05:00
Asher
363cdd02df Improve proxy documentation 2020-04-02 13:40:30 -05:00
Asher
a5d1d3b90e Move proxy logic into main HTTP server
This makes the code much more internally consistent (providers just
return payloads, include the proxy provider).
2020-04-02 13:40:29 -05:00
Asher
aaa6c279a1 Use Set for proxy domains 2020-04-02 13:40:28 -05:00
Asher
498becd11f Use route.fullPath when adding trailing slash
There's no need to specially construct the path.
2020-04-02 13:40:27 -05:00
Asher
411c61fb02 Create helper for determining if route is the root 2020-04-02 13:40:26 -05:00
Asher
74a0bacdcf Rename hxxp to isHttp 2020-04-02 13:40:25 -05:00
Asher
e7e7b0ffb7 Fix redirects through subpath proxy 2020-04-02 13:40:25 -05:00
Asher
fd339a7433 Include query parameters when proxying 2020-04-02 13:40:24 -05:00
Asher
561b6343c8 Ensure a trailing slash on subpath proxy 2020-04-02 13:40:23 -05:00
Asher
e68d72c4d6 Add documentation for proxying 2020-04-02 13:40:22 -05:00
Asher
737a8f5965 Catch proxy errors
Otherwise they'll crash code-server.
2020-04-02 13:40:21 -05:00
Asher
c0dd29c591 Fix domains with ports & localhost subdomains 2020-04-02 13:40:20 -05:00
Asher
8aa5675ba2 Implement the actual proxy 2020-04-02 13:40:19 -05:00
Asher
2086648c87 Only handle exact domain matches
This simplifies the logic a bit.
2020-04-02 13:40:18 -05:00
Asher
3a98d856a5 Handle authentication with proxy
The cookie will be set for the proxy domain so it'll work for all of its
subdomains.
2020-04-02 13:40:17 -05:00
Asher
90fd1f7dd1 Add proxy provider
It'll be able to handle /proxy requests as well as subdomains.
2020-04-02 13:40:16 -05:00
Asher
77ad73d579 Set domain on cookie
This allows it to be used in subdomains.
2020-04-02 13:40:15 -05:00
Asher
13534fa0c0 Add proxy-domain flag
This will be used for proxying ports.
2020-04-02 13:40:14 -05:00
Asher
37299abcc9 Minor startup code improvements
- Add type to HTTP options.
- Fix certificate message always saying it was generated.
- Dedent output not directly related to the HTTP server.
- Remove unnecessary comma.
2020-04-02 13:40:13 -05:00
Asher
e480f6527e Update VS Code to 1.43.2 2020-04-01 15:27:28 -05:00
Asher
26584f2060 Strip protocol from remote authority
In Google cloud shell the host header is 127.0.0.1:8080 instead of the
actual URL. This is what we write out to the HTML so VS Code can pick it
up. However cloud shell rewrites this string when found in the HTML
before serving it so it becomes https://8080-[...].appspot.com,
resulting in an extra unexpected https:// in the
URI (vscode-remote://https://8080[...]). The resulting malformed URI
causes the extension host to exit.

- Fixes #1471
- Fixes #1468
- Fixes #1440 (most likely).
2020-04-01 13:41:05 -05:00
Asher
a4c0fd1fdc Run ssh server listen after http
That way if they happen to conflict code-server doesn't crash.
2020-03-30 17:43:11 -05:00
Asher
6c104c016e Prevent exiting when an exception is uncaught 2020-03-30 17:43:10 -05:00
Asher
599670136d Output commit along with the version 2020-03-30 17:43:09 -05:00
Asher
ce637d318d Add descriptions to SSH flags 2020-03-30 17:43:08 -05:00
Anmol Sethi
d8654b5a19 Merge pull request #1460 from mjgallag/peg-yarn-version
Peg yarn version to ensure deterministic builds
2020-03-30 01:52:14 -04:00
Michael Gallagher
12c3ccd6c7 Peg yarn version to ensure deterministic builds
"Yarn is fully deterministic as long as all your teammates are using the same Yarn version." (https://classic.yarnpkg.com/blog/2017/05/31/determinism/)
2020-03-28 14:29:04 -07:00
Asher
7954656610 Set background color using VS Code theme 2020-03-27 16:58:50 -05:00
Asher
87ebf03eb7 Skip vscode dependencies for test phase
They aren't used so we can skip them.
2020-03-27 13:40:42 -05:00
Asher
df1c34e291 Overwrite GitHub releases again
I was under the impression this was causing existing releases to become
drafts again but that happens without this flag.
2020-03-27 12:03:01 -05:00
Asher
4a65b58772 Fix arm builds 2020-03-27 12:02:56 -05:00
Asher
11fdb8854b Skip unused dependencies 2020-03-26 15:12:17 -05:00
Asher
0a92bb1607 Fix node version mismatch 2020-03-26 13:54:41 -05:00
Asher
5bac2cbdb8 Add build test 2020-03-26 13:54:40 -05:00
Asher
511c3e95b2 Remove npm rebuild 2020-03-25 17:07:26 -05:00
25 changed files with 702 additions and 225 deletions

View File

@@ -4,17 +4,17 @@ jobs:
include: include:
- name: Test - name: Test
if: tag IS blank if: tag IS blank
script: ./ci/image/run.sh "yarn && GITHUB_TOKEN=3229b0eec0a1622d6d1d1e00fca5b626070f5a10 yarn vscode && ./ci/ci.sh" script: ./ci/image/run.sh "yarn && git submodule update --init && yarn vscode:patch && ./ci/ci.sh"
deploy: null deploy: null
- name: Linux Release - name: Linux Release
if: tag IS present if: tag IS present
script: script:
- travis_wait 60 ./ci/image/run.sh "yarn && yarn vscode && ci/release.sh" - travis_wait 60 ./ci/image/run.sh "yarn && yarn vscode && ci/release.sh && ./ci/build-test.sh"
- ./ci/release-image/push.sh - ./ci/release-image/push.sh
- name: Linux ARM64 Release - name: Linux ARM64 Release
if: tag IS present if: tag IS present
script: script:
- ./ci/image/run.sh "yarn && yarn vscode && ci/release.sh" - ./ci/image/run.sh "yarn && yarn vscode && ci/release.sh && ./ci/build-test.sh"
- ./ci/release-image/push.sh - ./ci/release-image/push.sh
arch: arm64 arch: arm64
- name: MacOS Release - name: MacOS Release
@@ -22,7 +22,7 @@ jobs:
os: osx os: osx
language: node_js language: node_js
node_js: 12 node_js: 12
script: yarn && yarn vscode && travis_wait 60 ci/release.sh script: yarn && yarn vscode && travis_wait 60 ci/release.sh && ./ci/build-test.sh
before_deploy: before_deploy:
- echo "$JSON_KEY" | base64 --decode > ./ci/key.json - echo "$JSON_KEY" | base64 --decode > ./ci/key.json
@@ -31,6 +31,7 @@ deploy:
- provider: releases - provider: releases
edge: true edge: true
draft: true draft: true
overwrite: true
tag_name: $TRAVIS_TAG tag_name: $TRAVIS_TAG
target_commitish: $TRAVIS_COMMIT target_commitish: $TRAVIS_COMMIT
name: $TRAVIS_TAG name: $TRAVIS_TAG
@@ -47,6 +48,8 @@ deploy:
local_dir: release-upload local_dir: release-upload
on: on:
tags: true tags: true
# TODO: The gcs provider fails to install on arm64.
condition: $TRAVIS_CPU_ARCH = amd64
cache: cache:
timeout: 600 timeout: 600

21
ci/build-test.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# build-test.bash -- Make sure the build worked.
# This is to make sure we don't have Node version errors or any other
# compilation-related errors.
set -euo pipefail
function main() {
cd "$(dirname "${0}")/.." || exit 1
local output
output=$(node ./build/out/node/entry.js --list-extensions 2>&1)
if echo "$output" | grep 'was compiled against a different Node.js version'; then
echo "$output"
exit 1
else
echo "Build ran successfully"
fi
}
main "$@"

View File

@@ -15,7 +15,7 @@ RUN yum update -y && yum install -y \
RUN mkdir /usr/share/node && cd /usr/share/node \ RUN mkdir /usr/share/node && cd /usr/share/node \
&& curl "https://nodejs.org/dist/v12.14.0/node-v12.14.0-linux-$(uname -m | sed 's/86_//; s/aarch/arm/').tar.xz" | tar xJ --strip-components=1 -- && curl "https://nodejs.org/dist/v12.14.0/node-v12.14.0-linux-$(uname -m | sed 's/86_//; s/aarch/arm/').tar.xz" | tar xJ --strip-components=1 --
ENV PATH "$PATH:/usr/share/node/bin" ENV PATH "$PATH:/usr/share/node/bin"
RUN npm install -g yarn RUN npm install -g yarn@1.22.4
RUN curl -L "https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_$(uname -m | sed 's/x86_/amd/; s/aarch64/arm/')" > /usr/local/bin/shfmt \ RUN curl -L "https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_$(uname -m | sed 's/x86_/amd/; s/aarch64/arm/')" > /usr/local/bin/shfmt \
&& chmod +x /usr/local/bin/shfmt && chmod +x /usr/local/bin/shfmt

View File

@@ -5,9 +5,22 @@ set -euo pipefail
main() { main() {
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
# This, strangely enough, fixes the arm build being terminated for not having
# output on Travis. It's as if output is buffered and only displayed once a
# certain amount is collected. Five seconds didn't work but one second seems
# to generate enough output to make it work.
local pid
while true; do
echo 'Still running...'
sleep 1
done &
pid=$!
docker build ci/image docker build ci/image
imageTag="$(docker build -q ci/image)" imageTag="$(docker build -q ci/image)"
docker run -t --rm -e CI -e GITHUB_TOKEN -e TRAVIS_TAG -v "$(yarn cache dir):/usr/local/share/.cache/yarn/v6" -v "$PWD:/repo" -w /repo "$imageTag" "$*" docker run -t --rm -e CI -e GITHUB_TOKEN -e TRAVIS_TAG -v "$(yarn cache dir):/usr/local/share/.cache/yarn/v6" -v "$PWD:/repo" -w /repo "$imageTag" "$*"
kill $pid
} }
main "$@" main "$@"

View File

@@ -17,7 +17,7 @@ function package() {
fi fi
local arch local arch
arch="$(uname -m)" arch=$(uname -m | sed 's/aarch/arm/')
echo -n "Creating release..." echo -n "Creating release..."

View File

@@ -10,6 +10,44 @@ index e73dd4d9e8..e3192b3a0d 100644
resources/server resources/server
build/node_modules build/node_modules
coverage/ coverage/
diff --git a/.yarnrc b/.yarnrc
index 7808166004..1e16cde724 100644
--- a/.yarnrc
+++ b/.yarnrc
@@ -1,3 +1,3 @@
-disturl "https://atom.io/download/electron"
-target "7.1.11"
-runtime "electron"
+disturl "http://nodejs.org/dist"
+target "12.4.0"
+runtime "node"
diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js
index 7a2320d828..5768890636 100644
--- a/build/npm/postinstall.js
+++ b/build/npm/postinstall.js
@@ -33,9 +33,9 @@ function yarnInstall(location, opts) {
yarnInstall('extensions'); // node modules shared by all extensions
-yarnInstall('remote'); // node modules used by vscode server
+// yarnInstall('remote'); // node modules used by vscode server
-yarnInstall('remote/web'); // node modules used by vscode web
+// yarnInstall('remote/web'); // node modules used by vscode web
const allExtensionFolders = fs.readdirSync('extensions');
const extensions = allExtensionFolders.filter(e => {
@@ -68,7 +68,7 @@ runtime "${runtime}"`;
}
yarnInstall(`build`); // node modules required for build
-yarnInstall('test/automation'); // node modules required for smoketest
-yarnInstall('test/smoke'); // node modules required for smoketest
-yarnInstall('test/integration/browser'); // node modules required for integration
+// yarnInstall('test/automation'); // node modules required for smoketest
+// yarnInstall('test/smoke'); // node modules required for smoketest
+// yarnInstall('test/integration/browser'); // node modules required for integration
yarnInstallBuildDependencies(); // node modules for watching, specific to host node version, not electron
diff --git a/coder.js b/coder.js diff --git a/coder.js b/coder.js
new file mode 100644 new file mode 100644
index 0000000000..6aee0e46bc index 0000000000..6aee0e46bc
@@ -119,7 +157,7 @@ index 2d8b725ff2..a8d93a17ca 100644
unique-stream@^2.0.2: unique-stream@^2.0.2:
version "2.2.1" version "2.2.1"
diff --git a/package.json b/package.json diff --git a/package.json b/package.json
index 6e9b9dc0a0..49b14e536a 100644 index 29d3cb6677..d3788cb1ab 100644
--- a/package.json --- a/package.json
+++ b/package.json +++ b/package.json
@@ -33,6 +33,9 @@ @@ -33,6 +33,9 @@
@@ -132,6 +170,19 @@ index 6e9b9dc0a0..49b14e536a 100644
"applicationinsights": "1.0.8", "applicationinsights": "1.0.8",
"chokidar": "3.2.3", "chokidar": "3.2.3",
"graceful-fs": "4.2.3", "graceful-fs": "4.2.3",
diff --git a/product.json b/product.json
index 759d765533..e1c33a008a 100644
--- a/product.json
+++ b/product.json
@@ -18,7 +18,7 @@
"darwinBundleIdentifier": "com.visualstudio.code.oss",
"linuxIconName": "com.visualstudio.code.oss",
"licenseFileName": "LICENSE.txt",
- "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new",
+ "reportIssueUrl": "https://github.com/cdr/code-server/issues/new",
"urlProtocol": "code-oss",
"extensionAllowedProposedApi": [
"ms-vscode.references-view"
diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts
index a68e020f9f..c31e7befa3 100644 index a68e020f9f..c31e7befa3 100644
--- a/src/vs/base/common/network.ts --- a/src/vs/base/common/network.ts
@@ -210,16 +261,32 @@ index 2c64061da7..c0ef8faedd 100644
// Do nothing. If we can't read the file we have no // Do nothing. If we can't read the file we have no
// language pack config. // language pack config.
diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts
index 45f6f17ce0..79fde0b92c 100644 index 45f6f17ce0..546b4c24de 100644
--- a/src/vs/code/browser/workbench/workbench.ts --- a/src/vs/code/browser/workbench/workbench.ts
+++ b/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts
@@ -16,6 +16,7 @@ import product from 'vs/platform/product/common/product';
import { Schemas } from 'vs/base/common/network';
import { posix } from 'vs/base/common/path';
import { localize } from 'vs/nls';
+import { encodePath } from 'vs/server/node/util';
interface ICredential {
service: string;
@@ -237,7 +238,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
}
private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): string | undefined {
-
// Empty
let targetHref: string | undefined = undefined;
if (!workspace) {
@@ -246,12 +246,18 @@ class WorkspaceProvider implements IWorkspaceProvider { @@ -246,12 +246,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
// Folder // Folder
else if (isFolderToOpen(workspace)) { else if (isFolderToOpen(workspace)) {
- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`; - targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`;
+ const target = workspace.folderUri.scheme === Schemas.vscodeRemote + const target = workspace.folderUri.scheme === Schemas.vscodeRemote
+ ? encodeURIComponent(workspace.folderUri.path).replace(/%2F/g, "/") + ? encodePath(workspace.folderUri.path)
+ : encodeURIComponent(workspace.folderUri.toString()); + : encodeURIComponent(workspace.folderUri.toString());
+ targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${target}`; + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${target}`;
} }
@@ -228,13 +295,32 @@ index 45f6f17ce0..79fde0b92c 100644
else if (isWorkspaceToOpen(workspace)) { else if (isWorkspaceToOpen(workspace)) {
- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`; - targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`;
+ const target = workspace.workspaceUri.scheme === Schemas.vscodeRemote + const target = workspace.workspaceUri.scheme === Schemas.vscodeRemote
+ ? encodeURIComponent(workspace.workspaceUri.path).replace(/%2F/g, "/") + ? encodePath(workspace.workspaceUri.path)
+ : encodeURIComponent(workspace.workspaceUri.toString()); + : encodeURIComponent(workspace.workspaceUri.toString());
+ targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${target}`; + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${target}`;
} }
// Append payload if any // Append payload if any
@@ -302,35 +308,6 @@ class WorkspaceProvider implements IWorkspaceProvider { @@ -290,6 +296,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute);
+ // Strip the protocol from the authority if it exists.
+ const normalizeAuthority = (authority: string): string => authority.replace(/^https?:\/\//, "");
+ if (config.remoteAuthority) {
+ (config as any).remoteAuthority = normalizeAuthority(config.remoteAuthority);
+ }
+ if (config.workspaceUri && config.workspaceUri.authority) {
+ config.workspaceUri.authority = normalizeAuthority(config.workspaceUri.authority);
+ }
+ if (config.folderUri && config.folderUri.authority) {
+ config.folderUri.authority = normalizeAuthority(config.folderUri.authority);
+ }
+
// Revive static extension locations
if (Array.isArray(config.staticExtensions)) {
config.staticExtensions.forEach(extension => {
@@ -302,35 +320,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
let workspace: IWorkspace; let workspace: IWorkspace;
let payload = Object.create(null); let payload = Object.create(null);
@@ -455,10 +541,10 @@ index eab8591492..26668701f7 100644
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts
new file mode 100644 new file mode 100644
index 0000000000..4042e32f74 index 0000000000..4f8543d975
--- /dev/null --- /dev/null
+++ b/src/vs/server/browser/client.ts +++ b/src/vs/server/browser/client.ts
@@ -0,0 +1,263 @@ @@ -0,0 +1,266 @@
+import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event';
+import { URI } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri';
+import { localize } from 'vs/nls'; +import { localize } from 'vs/nls';
@@ -477,6 +563,7 @@ index 0000000000..4042e32f74
+import { LocalizationsService } from 'vs/workbench/services/localizations/electron-browser/localizationsService'; +import { LocalizationsService } from 'vs/workbench/services/localizations/electron-browser/localizationsService';
+import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
+import { Options } from 'vs/server/ipc.d'; +import { Options } from 'vs/server/ipc.d';
+import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
+ +
+class TelemetryService extends TelemetryChannelClient { +class TelemetryService extends TelemetryChannelClient {
+ public constructor( + public constructor(
@@ -634,14 +721,10 @@ index 0000000000..4042e32f74
+ headers: { "content-type": "application/json" }, + headers: { "content-type": "application/json" },
+ }); + });
+ if (response.status !== 200) { + if (response.status !== 200) {
+ throw new Error("Unexpected response"); + throw new Error(response.statusText);
+ } + }
+ +
+ const json = await response.json(); + const json = await response.json();
+ if (!json.isLatest) {
+ throw new Error("Update failed");
+ }
+
+ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`); + (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`);
+ }; + };
+ +
@@ -687,6 +770,12 @@ index 0000000000..4042e32f74
+ }; + };
+ +
+ updateLoop(); + updateLoop();
+
+ // This will be used to set the background color while VS Code loads.
+ const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL);
+ if (theme) {
+ localStorage.setItem("colorThemeData", theme);
+ }
+}; +};
+ +
+export interface Query { +export interface Query {
@@ -2273,10 +2362,10 @@ index 0000000000..3c74512192
+} +}
diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts
new file mode 100644 new file mode 100644
index 0000000000..d6dcfe1fe7 index 0000000000..52311bf756
--- /dev/null --- /dev/null
+++ b/src/vs/server/node/server.ts +++ b/src/vs/server/node/server.ts
@@ -0,0 +1,257 @@ @@ -0,0 +1,269 @@
+import * as net from 'net'; +import * as net from 'net';
+import * as path from 'path'; +import * as path from 'path';
+import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event';
@@ -2356,10 +2445,22 @@ index 0000000000..d6dcfe1fe7
+ await this.servicesPromise; + await this.servicesPromise;
+ const environment = this.services.get(IEnvironmentService) as IEnvironmentService; + const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
+ const startPath = options.startPath; + const startPath = options.startPath;
+ const parseUrl = (url: string): URI => {
+ // This might be a fully-specified URL or just a path.
+ try {
+ return URI.parse(url, true);
+ } catch (error) {
+ return URI.from({
+ scheme: Schemas.vscodeRemote,
+ authority: options.remoteAuthority,
+ path: url,
+ });
+ }
+ };
+ return { + return {
+ workbenchWebConfiguration: { + workbenchWebConfiguration: {
+ workspaceUri: startPath && startPath.workspace ? URI.parse(startPath.url) : undefined, + workspaceUri: startPath && startPath.workspace ? parseUrl(startPath.url) : undefined,
+ folderUri: startPath && !startPath.workspace ? URI.parse(startPath.url) : undefined, + folderUri: startPath && !startPath.workspace ? parseUrl(startPath.url) : undefined,
+ remoteAuthority: options.remoteAuthority, + remoteAuthority: options.remoteAuthority,
+ logLevel: getLogLevel(environment), + logLevel: getLogLevel(environment),
+ }, + },
@@ -2566,10 +2667,10 @@ index 0000000000..fc69441cf0
+}; +};
diff --git a/src/vs/server/node/util.ts b/src/vs/server/node/util.ts diff --git a/src/vs/server/node/util.ts b/src/vs/server/node/util.ts
new file mode 100644 new file mode 100644
index 0000000000..06b080044c index 0000000000..dd7fdf7b58
--- /dev/null --- /dev/null
+++ b/src/vs/server/node/util.ts +++ b/src/vs/server/node/util.ts
@@ -0,0 +1,9 @@ @@ -0,0 +1,17 @@
+import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { getPathFromAmdModule } from 'vs/base/common/amd';
+import { URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; +import { URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
+ +
@@ -2579,6 +2680,14 @@ index 0000000000..06b080044c
+ const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority); + const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
+ return new URITransformer(rawURITransformer); + return new URITransformer(rawURITransformer);
+}; +};
+
+/**
+ * Encode a path for opening via the folder or workspace query parameter. This
+ * preserves slashes so it can be edited by hand more easily.
+ */
+export const encodePath = (path: string): string => {
+ return path.split("/").map((p) => encodeURIComponent(p)).join("/");
+};
diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts
index e69aa80159..71a899d37b 100644 index e69aa80159..71a899d37b 100644
--- a/src/vs/workbench/api/browser/extensionHost.contribution.ts --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts

View File

@@ -16,9 +16,6 @@ main() {
cd lib/vscode cd lib/vscode
# Install VS Code dependencies. # Install VS Code dependencies.
yarn yarn
# NODE_MODULE_VERSION mismatch errors without this.
npm rebuild
) )
} }

View File

@@ -19,7 +19,7 @@ As a result, Coder has created its own marketplace for open source extensions. I
GitHub for VS Code extensions and building them. It's not perfect but getting better by the day with GitHub for VS Code extensions and building them. It's not perfect but getting better by the day with
more and more extensions. more and more extensions.
Issue [https://github.com/cdr/code-server/issues/1299](#1299) is a big one in making the experience here Issue [#1299](https://github.com/cdr/code-server/issues/1299) is a big one in making the experience here
better by allowing the community to submit extensions and repos to avoid waiting until the scraper finds better by allowing the community to submit extensions and repos to avoid waiting until the scraper finds
an extension. an extension.
@@ -65,6 +65,35 @@ only to HTTP requests.
You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate
for free. for free.
## How do I securely access web services?
code-server is capable of proxying to any port using either a subdomain or a
subpath which means you can securely access these services using code-server's
built-in authentication.
### Sub-domains
You will need a DNS entry that points to your server for each port you want to
access. You can either set up a wildcard DNS entry for `*.<domain>` if your domain
name registrar supports it or you can create one for every port you want to
access (`3000.<domain>`, `8080.<domain>`, etc).
You should also set up TLS certificates for these subdomains, either using a
wildcard certificate for `*.<domain>` or individual certificates for each port.
Start code-server with the `--proxy-domain` flag set to your domain.
```
code-server --proxy-domain <domain>
```
Now you can browse to `<port>.<domain>`. Note that this uses the host header so
ensure your reverse proxy forwards that information if you are using one.
### Sub-paths
Just browse to `/proxy/<port>/`.
## x86 releases? ## x86 releases?
node has dropped support for x86 and so we decided to as well. See node has dropped support for x86 and so we decided to as well. See
@@ -104,6 +133,15 @@ demand and will work on it when the time is right.
Use the `--disable-telemetry` flag to completely disable telemetry. We use the Use the `--disable-telemetry` flag to completely disable telemetry. We use the
data collected only to improve code-server. data collected only to improve code-server.
## How does code-server decide what workspace or folder to open?
code-server tries the following in order:
1. The `workspace` query parameter.
2. The `folder` query parameter.
3. The directory passed on the command line.
4. The last opened workspace or folder.
## Enterprise ## Enterprise
Visit [our enterprise page](https://coder.com) for more information about our Visit [our enterprise page](https://coder.com) for more information about our

View File

@@ -1,7 +1,7 @@
{ {
"name": "code-server", "name": "code-server",
"license": "MIT", "license": "MIT",
"version": "3.0.1", "version": "3.0.2",
"scripts": { "scripts": {
"clean": "ci/clean.sh", "clean": "ci/clean.sh",
"vscode": "ci/vscode.sh", "vscode": "ci/vscode.sh",
@@ -17,6 +17,7 @@
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.4.32", "@types/adm-zip": "^0.4.32",
"@types/fs-extra": "^8.0.1", "@types/fs-extra": "^8.0.1",
"@types/http-proxy": "^1.17.4",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/node": "^12.12.7", "@types/node": "^12.12.7",
"@types/parcel-bundler": "^1.12.1", "@types/parcel-bundler": "^1.12.1",
@@ -52,13 +53,14 @@
"@coder/logger": "1.1.11", "@coder/logger": "1.1.11",
"adm-zip": "^0.4.14", "adm-zip": "^0.4.14",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"http-proxy": "^1.18.0",
"httpolyglot": "^0.1.2", "httpolyglot": "^0.1.2",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"pem": "^1.14.2", "pem": "^1.14.2",
"safe-compare": "^1.1.4", "safe-compare": "^1.1.4",
"semver": "^7.1.3", "semver": "^7.1.3",
"tar": "^6.0.1",
"ssh2": "^0.8.7", "ssh2": "^0.8.7",
"tar": "^6.0.1",
"tar-fs": "^2.0.0", "tar-fs": "^2.0.0",
"ws": "^7.2.0" "ws": "^7.2.0"
} }

View File

@@ -17,7 +17,7 @@
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials" crossorigin="use-credentials"
/> />
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.pnggg" /> <link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" /> <link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
</head> </head>

View File

@@ -100,4 +100,11 @@
<script> <script>
require(["vs/code/browser/workbench/workbench"], function() {}) require(["vs/code/browser/workbench/workbench"], function() {})
</script> </script>
<script>
try {
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")).colorMap["editor.background"]
} catch (error) {
// Oh well.
}
</script>
</html> </html>

View File

@@ -43,7 +43,7 @@ export class ApiHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> { public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureAuthenticated(request) this.ensureAuthenticated(request)
if (route.requestPath !== "/index.html") { if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }

View File

@@ -20,7 +20,7 @@ export class DashboardHttpProvider extends HttpProvider {
} }
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> { public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (route.requestPath !== "/index.html") { if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }

View File

@@ -18,7 +18,7 @@ interface LoginPayload {
*/ */
export class LoginHttpProvider extends HttpProvider { export class LoginHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> { public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") { if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }
switch (route.base) { switch (route.base) {

43
src/node/app/proxy.ts Normal file
View File

@@ -0,0 +1,43 @@
import * as http from "http"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
/**
* Proxy HTTP provider.
*/
export class ProxyHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (!this.authenticated(request)) {
if (this.isRoot(route)) {
return { redirect: "/login", query: { to: route.fullPath } }
}
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
// Ensure there is a trailing slash so relative paths work correctly.
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
return {
redirect: `${route.fullPath}/`,
}
}
const port = route.base.replace(/^\//, "")
return {
proxy: {
base: `${this.options.base}/${port}`,
port,
},
}
}
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
this.ensureAuthenticated(request)
const port = route.base.replace(/^\//, "")
return {
proxy: {
base: `${this.options.base}/${port}`,
port,
},
}
}
}

View File

@@ -61,7 +61,7 @@ export class UpdateHttpProvider extends HttpProvider {
this.ensureAuthenticated(request) this.ensureAuthenticated(request)
this.ensureMethod(request) this.ensureMethod(request)
if (route.requestPath !== "/index.html") { if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }
@@ -221,8 +221,13 @@ export class UpdateHttpProvider extends HttpProvider {
targetPath = path.resolve(__dirname, "../../../") targetPath = path.resolve(__dirname, "../../../")
} }
logger.debug("Replacing files", field("target", targetPath)) // Move the old directory to prevent potential data loss.
await fs.move(directoryPath, targetPath, { overwrite: true }) const backupPath = path.resolve(targetPath, `../${path.basename(targetPath)}.${Date.now().toString()}`)
logger.debug("Replacing files", field("target", targetPath), field("backup", backupPath))
await fs.move(targetPath, backupPath)
// Move the new directory.
await fs.move(directoryPath, targetPath)
await fs.remove(downloadPath) await fs.remove(downloadPath)

View File

@@ -1,11 +1,9 @@
import { field, logger } from "@coder/logger" import { field, logger } from "@coder/logger"
import * as cp from "child_process" import * as cp from "child_process"
import * as crypto from "crypto" import * as crypto from "crypto"
import * as fs from "fs-extra"
import * as http from "http" import * as http from "http"
import * as net from "net" import * as net from "net"
import * as path from "path" import * as path from "path"
import * as url from "url"
import { import {
CodeServerMessage, CodeServerMessage,
Options, Options,
@@ -128,7 +126,7 @@ export class VscodeHttpProvider extends HttpProvider {
switch (route.base) { switch (route.base) {
case "/": case "/":
if (route.requestPath !== "/index.html") { if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) { } else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } } return { redirect: "/login", query: { to: this.options.base } }
@@ -168,15 +166,12 @@ export class VscodeHttpProvider extends HttpProvider {
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> { private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
const remoteAuthority = request.headers.host as string const remoteAuthority = request.headers.host as string
const { lastVisited } = await settings.read() const { lastVisited } = await settings.read()
const startPath = await this.getFirstValidPath( const startPath = await this.getFirstPath([
[
{ url: route.query.workspace, workspace: true }, { url: route.query.workspace, workspace: true },
{ url: route.query.folder, workspace: false }, { url: route.query.folder, workspace: false },
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined, this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
lastVisited, lastVisited,
], ])
remoteAuthority,
)
const [response, options] = await Promise.all([ const [response, options] = await Promise.all([
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"), await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
this.initialize({ this.initialize({
@@ -209,41 +204,19 @@ export class VscodeHttpProvider extends HttpProvider {
} }
/** /**
* Choose the first valid path. If `workspace` is undefined then either a * Choose the first non-empty path.
* workspace or a directory are acceptable. Otherwise it must be a file if a
* workspace or a directory otherwise.
*/ */
private async getFirstValidPath( private async getFirstPath(
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>, startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
remoteAuthority: string,
): Promise<StartPath | undefined> { ): Promise<StartPath | undefined> {
for (let i = 0; i < startPaths.length; ++i) { for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i] const startPath = startPaths[i]
if (!startPath) { const url =
continue startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
} if (startPath && url) {
const paths = typeof startPath.url === "string" ? [startPath.url] : startPath.url || []
for (let j = 0; j < paths.length; ++j) {
const uri = url.parse(paths[j])
try {
if (!uri.pathname) {
throw new Error(`${paths[j]} is not a valid URL`)
}
const stat = await fs.stat(uri.pathname)
if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
return { return {
url: url.format({ url,
protocol: uri.protocol || "vscode-remote", workspace: !!startPath.workspace,
hostname: remoteAuthority.split(":")[0],
port: remoteAuthority.split(":")[1],
pathname: uri.pathname,
slashes: true,
}),
workspace: !stat.isDirectory(),
}
}
} catch (error) {
logger.warn(error.message)
} }
} }
} }

View File

@@ -39,6 +39,7 @@ export interface Args extends VsArgs {
readonly "install-extension"?: string[] readonly "install-extension"?: string[]
readonly "show-versions"?: boolean readonly "show-versions"?: boolean
readonly "uninstall-extension"?: string[] readonly "uninstall-extension"?: string[]
readonly "proxy-domain"?: string[]
readonly locale?: string readonly locale?: string
readonly _: string[] readonly _: string[]
} }
@@ -98,8 +99,8 @@ const options: Options<Required<Args>> = {
version: { type: "boolean", short: "v", description: "Display version information." }, version: { type: "boolean", short: "v", description: "Display version information." },
_: { type: "string[]" }, _: { type: "string[]" },
"disable-ssh": { type: "boolean" }, "disable-ssh": { type: "boolean", description: "Disable the SSH server." },
"ssh-host-key": { type: "string", path: true }, "ssh-host-key": { type: "string", path: true, description: "SSH server host key." },
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." }, "user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." }, "extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
@@ -111,6 +112,7 @@ const options: Options<Required<Args>> = {
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." }, "install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." }, "uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
"show-versions": { type: "boolean", description: "Show VS Code extension versions." }, "show-versions": { type: "boolean", description: "Show VS Code extension versions." },
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
locale: { type: "string" }, locale: { type: "string" },
log: { type: LogLevel }, log: { type: LogLevel },

View File

@@ -5,87 +5,76 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
import { ApiHttpProvider } from "./app/api" import { ApiHttpProvider } from "./app/api"
import { DashboardHttpProvider } from "./app/dashboard" import { DashboardHttpProvider } from "./app/dashboard"
import { LoginHttpProvider } from "./app/login" import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static" import { StaticHttpProvider } from "./app/static"
import { UpdateHttpProvider } from "./app/update" import { UpdateHttpProvider } from "./app/update"
import { VscodeHttpProvider } from "./app/vscode" import { VscodeHttpProvider } from "./app/vscode"
import { Args, optionDescriptions, parse } from "./cli" import { Args, optionDescriptions, parse } from "./cli"
import { AuthType, HttpServer } from "./http" import { AuthType, HttpServer, HttpServerOptions } from "./http"
import { SshProvider } from "./ssh/server" import { SshProvider } from "./ssh/server"
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util" import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
import { ipcMain, wrap } from "./wrapper" import { ipcMain, wrap } from "./wrapper"
process.on("uncaughtException", (error) => {
logger.error(`Uncaught exception: ${error.message}`)
if (typeof error.stack !== "undefined") {
logger.error(error.stack)
}
})
let pkg: { version?: string; commit?: string } = {}
try {
pkg = require("../../package.json")
} catch (error) {
logger.warn(error.message)
}
const version = pkg.version || "development"
const commit = pkg.commit || "development"
const main = async (args: Args): Promise<void> => { const main = async (args: Args): Promise<void> => {
const auth = args.auth || AuthType.Password const auth = args.auth || AuthType.Password
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
let commit: string | undefined
try {
commit = require("../../package.json").commit
} catch (error) {
logger.warn(error.message)
}
// Spawn the main HTTP server. // Spawn the main HTTP server.
const options = { const options: HttpServerOptions = {
auth, auth,
cert: args.cert ? args.cert.value : undefined, commit,
certKey: args["cert-key"],
sshHostKey: args["ssh-host-key"],
commit: commit || "development",
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"), host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
password: originalPassword ? hash(originalPassword) : undefined, password: originalPassword ? hash(originalPassword) : undefined,
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080, port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
proxyDomains: args["proxy-domain"],
socket: args.socket, socket: args.socket,
...(args.cert && !args.cert.value
? await generateCertificate()
: {
cert: args.cert && args.cert.value,
certKey: args["cert-key"],
}),
} }
if (!options.cert && args.cert) { if (options.cert && !options.certKey) {
const { cert, certKey } = await generateCertificate()
options.cert = cert
options.certKey = certKey
} else if (args.cert && !args["cert-key"]) {
throw new Error("--cert-key is missing") throw new Error("--cert-key is missing")
} }
if (!args["disable-ssh"]) {
if (!options.sshHostKey && typeof options.sshHostKey !== "undefined") {
throw new Error("--ssh-host-key cannot be blank")
} else if (!options.sshHostKey) {
try {
options.sshHostKey = await generateSshHostKey()
} catch (error) {
logger.error("Unable to start SSH server", field("error", error.message))
}
}
}
const httpServer = new HttpServer(options) const httpServer = new HttpServer(options)
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args) const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"]) const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"]) const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider) httpServer.registerHttpProvider("/login", LoginHttpProvider)
httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update) httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
ipcMain().onDispose(() => httpServer.dispose()) ipcMain().onDispose(() => httpServer.dispose())
logger.info(`code-server ${require("../../package.json").version}`) logger.info(`code-server ${version} ${commit}`)
let sshPort = ""
if (!args["disable-ssh"] && options.sshHostKey) {
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string)
try {
sshPort = await sshProvider.listen()
} catch (error) {
logger.warn(`SSH server: ${error.message}`)
}
}
const serverAddress = await httpServer.listen() const serverAddress = await httpServer.listen()
logger.info(`Server listening on ${serverAddress}`) logger.info(`HTTP server listening on ${serverAddress}`)
if (auth === AuthType.Password && !process.env.PASSWORD) { if (auth === AuthType.Password && !process.env.PASSWORD) {
logger.info(` - Password is ${originalPassword}`) logger.info(` - Password is ${originalPassword}`)
logger.info(" - To use your own password, set the PASSWORD environment variable") logger.info(" - To use your own password set the PASSWORD environment variable")
if (!args.auth) { if (!args.auth) {
logger.info(" - To disable use `--auth none`") logger.info(" - To disable use `--auth none`")
} }
@@ -97,7 +86,7 @@ const main = async (args: Args): Promise<void> => {
if (httpServer.protocol === "https") { if (httpServer.protocol === "https") {
logger.info( logger.info(
typeof args.cert === "string" args.cert && args.cert.value
? ` - Using provided certificate and key for HTTPS` ? ` - Using provided certificate and key for HTTPS`
: ` - Using generated certificate and key for HTTPS`, : ` - Using generated certificate and key for HTTPS`,
) )
@@ -105,19 +94,44 @@ const main = async (args: Args): Promise<void> => {
logger.info(" - Not serving HTTPS") logger.info(" - Not serving HTTPS")
} }
logger.info(` - Automatic updates are ${update.enabled ? "enabled" : "disabled"}`) if (httpServer.proxyDomains.size > 0) {
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
}
if (sshPort) { logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
logger.info(` - SSH Server - Listening :${sshPort}`)
let sshHostKey = args["ssh-host-key"]
if (!args["disable-ssh"] && !sshHostKey) {
try {
sshHostKey = await generateSshHostKey()
} catch (error) {
logger.error("Unable to start SSH server", field("error", error.message))
}
}
let sshPort: number | undefined
if (!args["disable-ssh"] && sshHostKey) {
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey)
try {
sshPort = await sshProvider.listen()
} catch (error) {
logger.warn(`SSH server: ${error.message}`)
}
}
if (typeof sshPort !== "undefined") {
logger.info(`SSH server listening on localhost:${sshPort}`)
logger.info(" - To disable use `--disable-ssh`")
} else { } else {
logger.info(" - SSH Server - Disabled") logger.info("SSH server disabled")
} }
if (serverAddress && !options.socket && args.open) { if (serverAddress && !options.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0. // The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
await open(openAddress).catch(console.error) await open(openAddress).catch(console.error)
logger.info(` - Opened ${openAddress}`) logger.info(`Opened ${openAddress}`)
} }
} }
@@ -132,7 +146,7 @@ const tryParse = (): Args => {
const args = tryParse() const args = tryParse()
if (args.help) { if (args.help) {
console.log("code-server", require("../../package.json").version) console.log("code-server", version, commit)
console.log("") console.log("")
console.log(`Usage: code-server [options] [path]`) console.log(`Usage: code-server [options] [path]`)
console.log("") console.log("")
@@ -141,14 +155,14 @@ if (args.help) {
console.log("", description) console.log("", description)
}) })
} else if (args.version) { } else if (args.version) {
const version = require("../../package.json").version
if (args.json) { if (args.json) {
console.log({ console.log({
codeServer: version, codeServer: version,
commit,
vscode: require("../../lib/vscode/package.json").version, vscode: require("../../lib/vscode/package.json").version,
}) })
} else { } else {
console.log(version) console.log(version, commit)
} }
process.exit(0) process.exit(0)
} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) { } else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) {

View File

@@ -1,6 +1,7 @@
import { field, logger } from "@coder/logger" import { field, logger } from "@coder/logger"
import * as fs from "fs-extra" import * as fs from "fs-extra"
import * as http from "http" import * as http from "http"
import proxy from "http-proxy"
import * as httpolyglot from "httpolyglot" import * as httpolyglot from "httpolyglot"
import * as https from "https" import * as https from "https"
import * as net from "net" import * as net from "net"
@@ -18,6 +19,10 @@ import { getMediaMime, xdgLocalDir } from "./util"
export type Cookies = { [key: string]: string[] | undefined } export type Cookies = { [key: string]: string[] | undefined }
export type PostData = { [key: string]: string | string[] | undefined } export type PostData = { [key: string]: string | string[] | undefined }
interface ProxyRequest extends http.IncomingMessage {
base?: string
}
interface AuthPayload extends Cookies { interface AuthPayload extends Cookies {
key?: string[] key?: string[]
} }
@@ -29,6 +34,17 @@ export enum AuthType {
export type Query = { [key: string]: string | string[] | undefined } export type Query = { [key: string]: string | string[] | undefined }
export interface ProxyOptions {
/**
* A base path to strip from from the request before proxying if necessary.
*/
base?: string
/**
* The port to proxy.
*/
port: string
}
export interface HttpResponse<T = string | Buffer | object> { export interface HttpResponse<T = string | Buffer | object> {
/* /*
* Whether to set cache-control headers for this response. * Whether to set cache-control headers for this response.
@@ -77,6 +93,17 @@ export interface HttpResponse<T = string | Buffer | object> {
* `undefined` to remove a query variable. * `undefined` to remove a query variable.
*/ */
query?: Query query?: Query
/**
* Indicates the request should be proxied.
*/
proxy?: ProxyOptions
}
export interface WsResponse {
/**
* Indicates the web socket should be proxied.
*/
proxy?: ProxyOptions
} }
/** /**
@@ -100,14 +127,31 @@ export interface HttpServerOptions {
readonly host?: string readonly host?: string
readonly password?: string readonly password?: string
readonly port?: number readonly port?: number
readonly proxyDomains?: string[]
readonly socket?: string readonly socket?: string
} }
export interface Route { export interface Route {
/**
* Base path part (in /test/path it would be "/test").
*/
base: string base: string
/**
* Remaining part of the route (in /test/path it would be "/path"). It can be
* blank.
*/
requestPath: string requestPath: string
/**
* Query variables included in the request.
*/
query: querystring.ParsedUrlQuery query: querystring.ParsedUrlQuery
/**
* Normalized version of `originalPath`.
*/
fullPath: string fullPath: string
/**
* Original path of the request without any modifications.
*/
originalPath: string originalPath: string
} }
@@ -136,7 +180,9 @@ export abstract class HttpProvider {
} }
/** /**
* Handle web sockets on the registered endpoint. * Handle web sockets on the registered endpoint. Normally the provider
* handles the request itself but it can return a response when necessary. The
* default is to throw a 404.
*/ */
public handleWebSocket( public handleWebSocket(
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
@@ -145,7 +191,7 @@ export abstract class HttpProvider {
_socket: net.Socket, _socket: net.Socket,
_head: Buffer, _head: Buffer,
/* eslint-enable @typescript-eslint/no-unused-vars */ /* eslint-enable @typescript-eslint/no-unused-vars */
): Promise<void> { ): Promise<WsResponse | void> {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }
@@ -264,7 +310,7 @@ export abstract class HttpProvider {
* Return the provided password value if the payload contains the right * Return the provided password value if the payload contains the right
* password otherwise return false. If no payload is specified use cookies. * password otherwise return false. If no payload is specified use cookies.
*/ */
protected authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean { public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
switch (this.options.auth) { switch (this.options.auth) {
case AuthType.None: case AuthType.None:
return true return true
@@ -335,6 +381,14 @@ export abstract class HttpProvider {
} }
return cookies as T return cookies as T
} }
/**
* Return true if the route is for the root page. For example /base, /base/,
* or /base/index.html but not /base/path or /base/file.js.
*/
protected isRoot(route: Route): boolean {
return !route.requestPath || route.requestPath === "/index.html"
}
} }
/** /**
@@ -407,7 +461,18 @@ export class HttpServer {
private readonly heart: Heart private readonly heart: Heart
private readonly socketProvider = new SocketProxyProvider() private readonly socketProvider = new SocketProxyProvider()
/**
* Proxy domains are stored here without the leading `*.`
*/
public readonly proxyDomains: Set<string>
/**
* Provides the actual proxying functionality.
*/
private readonly proxy = proxy.createProxyServer({})
public constructor(private readonly options: HttpServerOptions) { public constructor(private readonly options: HttpServerOptions) {
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => { this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
const connections = await this.getConnections() const connections = await this.getConnections()
logger.trace(`${connections} active connection${plural(connections)}`) logger.trace(`${connections} active connection${plural(connections)}`)
@@ -425,6 +490,16 @@ export class HttpServer {
} else { } else {
this.server = http.createServer(this.onRequest) this.server = http.createServer(this.onRequest)
} }
this.proxy.on("error", (error, _request, response) => {
response.writeHead(HttpCode.ServerError)
response.end(error.message)
})
// Intercept the response to rewrite absolute redirects against the base path.
this.proxy.on("proxyRes", (response, request: ProxyRequest) => {
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
response.headers.location = request.base + response.headers.location
}
})
} }
public dispose(): void { public dispose(): void {
@@ -515,6 +590,9 @@ export class HttpServer {
this.heart.beat() this.heart.beat()
const route = this.parseUrl(request) const route = this.parseUrl(request)
const write = (payload: HttpResponse): void => { const write = (payload: HttpResponse): void => {
const host = request.headers.host || ""
const idx = host.indexOf(":")
const domain = idx !== -1 ? host.substring(0, idx) : host
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
"Content-Type": payload.mime || getMediaMime(payload.filePath), "Content-Type": payload.mime || getMediaMime(payload.filePath),
...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}), ...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}),
@@ -525,9 +603,12 @@ export class HttpServer {
"Set-Cookie": [ "Set-Cookie": [
`${payload.cookie.key}=${payload.cookie.value}`, `${payload.cookie.key}=${payload.cookie.value}`,
`Path=${normalize(payload.cookie.path || "/", true)}`, `Path=${normalize(payload.cookie.path || "/", true)}`,
domain ? `Domain=${this.getCookieDomain(domain)}` : undefined,
// "HttpOnly", // "HttpOnly",
"SameSite=strict", "SameSite=lax",
].join(";"), ]
.filter((l) => !!l)
.join(";"),
} }
: {}), : {}),
...payload.headers, ...payload.headers,
@@ -547,20 +628,27 @@ export class HttpServer {
response.end() response.end()
} }
} }
try { try {
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request)) const payload =
if (!payload) { this.maybeRedirect(request, route) ||
throw new HttpError("Not found", HttpCode.NotFound) (route.provider.authenticated(request) && this.maybeProxy(request)) ||
} (await route.provider.handleRequest(route, request))
if (payload.proxy) {
this.doProxy(route, request, response, payload.proxy)
} else {
write(payload) write(payload)
}
} catch (error) { } catch (error) {
let e = error let e = error
if (error.code === "ENOENT" || error.code === "EISDIR") { if (error.code === "ENOENT" || error.code === "EISDIR") {
e = new HttpError("Not found", HttpCode.NotFound) e = new HttpError("Not found", HttpCode.NotFound)
} }
logger.debug("Request error", field("url", request.url))
logger.debug(error.stack)
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
logger.debug("Request error", field("url", request.url), field("code", code))
if (code >= HttpCode.ServerError) {
logger.error(error.stack)
}
const payload = await route.provider.getErrorRoot(route, code, code, e.message) const payload = await route.provider.getErrorRoot(route, code, code, e.message)
write({ write({
code, code,
@@ -625,7 +713,14 @@ export class HttpServer {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head) // The socket proxy is so we can pass them to child processes (TLS sockets
// can't be transferred so we need an in-between).
const socketProxy = await this.socketProvider.createProxy(socket)
const payload =
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
if (payload && payload.proxy) {
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
}
} catch (error) { } catch (error) {
socket.destroy(error) socket.destroy(error)
logger.warn(`discarding socket connection: ${error.message}`) logger.warn(`discarding socket connection: ${error.message}`)
@@ -647,7 +742,6 @@ export class HttpServer {
// Happens if it's a plain `domain.com`. // Happens if it's a plain `domain.com`.
base = "/" base = "/"
} }
requestPath = requestPath || "/index.html"
return { base, requestPath } return { base, requestPath }
} }
@@ -670,4 +764,106 @@ export class HttpServer {
} }
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath } return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
} }
/**
* Proxy a request to the target.
*/
private doProxy(
route: Route,
request: http.IncomingMessage,
response: http.ServerResponse,
options: ProxyOptions,
): void
/**
* Proxy a web socket to the target.
*/
private doProxy(
route: Route,
request: http.IncomingMessage,
response: { socket: net.Socket; head: Buffer },
options: ProxyOptions,
): void
/**
* Proxy a request or web socket to the target.
*/
private doProxy(
route: Route,
request: http.IncomingMessage,
response: http.ServerResponse | { socket: net.Socket; head: Buffer },
options: ProxyOptions,
): void {
const port = parseInt(options.port, 10)
if (isNaN(port)) {
throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest)
}
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
// sure how best to get this information to the `proxyRes` event handler.
// For now I'm sticking it on the request object which is passed through to
// the event.
;(request as ProxyRequest).base = options.base
const isHttp = response instanceof http.ServerResponse
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath
const proxyOptions: proxy.ServerOptions = {
changeOrigin: true,
ignorePath: true,
target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${
Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
}`,
ws: !isHttp,
}
if (response instanceof http.ServerResponse) {
this.proxy.web(request, response, proxyOptions)
} else {
this.proxy.ws(request, response.socket, response.head, proxyOptions)
}
}
/**
* Get the domain that should be used for setting a cookie. This will allow
* the user to authenticate only once. This will return the highest level
* domain (e.g. `coder.com` over `test.coder.com` if both are specified).
*/
private getCookieDomain(host: string): string {
let current: string | undefined
this.proxyDomains.forEach((domain) => {
if (host.endsWith(domain) && (!current || domain.length < current.length)) {
current = domain
}
})
// Setting the domain to localhost doesn't seem to work for subdomains (for
// example dev.localhost).
return current && current !== "localhost" ? current : host
}
/**
* Return a response if the request should be proxied. Anything that ends in a
* proxy domain and has a *single* subdomain should be proxied. Anything else
* should return `undefined` and will be handled as normal.
*
* For example if `coder.com` is specified `8080.coder.com` will be proxied
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
*/
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
// Split into parts.
const host = request.headers.host || ""
const idx = host.indexOf(":")
const domain = idx !== -1 ? host.substring(0, idx) : host
const parts = domain.split(".")
// There must be an exact match.
const port = parts.shift()
const proxyDomain = parts.join(".")
if (!port || !this.proxyDomains.has(proxyDomain)) {
return undefined
}
return {
proxy: {
port,
},
}
}
} }

View File

@@ -24,11 +24,11 @@ export class SshProvider extends HttpProvider {
}) })
} }
public async listen(): Promise<string> { public async listen(): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.sshServer.once("error", reject) this.sshServer.once("error", reject)
this.sshServer.listen(() => { this.sshServer.listen(() => {
resolve(this.sshServer.address().port.toString()) resolve(this.sshServer.address().port)
}) })
}) })
} }

View File

@@ -117,6 +117,7 @@ describe("cli", () => {
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/) assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/) assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/) assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
assert.throws(() => parse(["--ssh-host-key"]), /--ssh-host-key requires a value/)
}) })
it("should error if value is invalid", () => { it("should error if value is invalid", () => {
@@ -160,4 +161,19 @@ describe("cli", () => {
auth: "none", auth: "none",
}) })
}) })
it("should support repeatable flags", () => {
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
_: [],
"extensions-dir": path.join(xdgLocalDir, "extensions"),
"user-data-dir": xdgLocalDir,
"proxy-domain": ["*.coder.com"],
})
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
_: [],
"extensions-dir": path.join(xdgLocalDir, "extensions"),
"user-data-dir": xdgLocalDir,
"proxy-domain": ["*.coder.com", "test.com"],
})
})
}) })

View File

@@ -214,13 +214,18 @@ describe("update", () => {
await p.downloadAndApplyUpdate(update, destination) await p.downloadAndApplyUpdate(update, destination)
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8")) assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
// Should still work if there is no existing version somehow. // There should be a backup.
await fs.remove(destination) const dir = (await fs.readdir(path.join(tmpdir, "tests/updates"))).filter((dir) => {
await p.downloadAndApplyUpdate(update, destination) return dir.startsWith("code-server.")
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8")) })
assert.equal(dir.length, 1)
assert.equal(
`console.log("OLD")`,
await fs.readFile(path.join(tmpdir, "tests/updates", dir[0], "code-server"), "utf8"),
)
const archiveName = await p.getReleaseName(update) const archiveName = await p.getReleaseName(update)
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`, `/download/${version}/${archiveName}`]) assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`])
}) })
it("should not reject if unable to fetch", async () => { it("should not reject if unable to fetch", async () => {

View File

@@ -871,6 +871,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/http-proxy@^1.17.4":
version "1.17.4"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
dependencies:
"@types/node" "*"
"@types/json-schema@^7.0.3": "@types/json-schema@^7.0.3":
version "7.0.4" version "7.0.4"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
@@ -2240,7 +2247,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@3.2.6: debug@3.2.6, debug@^3.0.0:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -2745,6 +2752,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter3@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
events@^3.0.0: events@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
@@ -2980,6 +2992,13 @@ flatted@^2.0.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
follow-redirects@^1.0.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb"
integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==
dependencies:
debug "^3.0.0"
for-in@^1.0.2: for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -3403,6 +3422,15 @@ http-errors@~1.7.2:
statuses ">= 1.5.0 < 2" statuses ">= 1.5.0 < 2"
toidentifier "1.0.0" toidentifier "1.0.0"
http-proxy@^1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
dependencies:
eventemitter3 "^4.0.0"
follow-redirects "^1.0.0"
requires-port "^1.0.0"
http-signature@~1.2.0: http-signature@~1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -5894,6 +5922,11 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resolve-from@^3.0.0: resolve-from@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"