Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd36a99a4c | ||
|
|
4b09746c37 | ||
|
|
870cf4f3fe | ||
|
|
1ff35f177d | ||
|
|
f3edb1cc5f | ||
|
|
86dc38e69f | ||
|
|
a2b69c8f3f | ||
|
|
4cfd7c50ad | ||
|
|
a96606e589 | ||
|
|
30aefe19b5 | ||
|
|
37184f456c | ||
|
|
05456024c4 | ||
|
|
5accf3fe5f | ||
|
|
2dd27b4cb8 | ||
|
|
af28885ea6 | ||
|
|
f21ba53609 | ||
|
|
181e0ea6c8 | ||
|
|
6074ca275b | ||
|
|
d0d5461a67 | ||
|
|
8608ae2f08 | ||
|
|
401f08db63 | ||
|
|
caa299b60d | ||
|
|
dcde596002 | ||
|
|
ee14db20f1 | ||
|
|
27ba64c7e4 | ||
|
|
c7753f2cf9 | ||
|
|
974d4cb8fc | ||
|
|
29b6115c77 | ||
|
|
28e91ba70c | ||
|
|
5aded14b87 | ||
|
|
a288351ad4 | ||
|
|
3b39482420 | ||
|
|
a5c35af81b | ||
|
|
b78bdaf46e | ||
|
|
aefef5b0e8 | ||
|
|
ca998240a0 | ||
|
|
d2a31477c7 | ||
|
|
9c6581273e | ||
|
|
d1445a8135 | ||
|
|
5fc00acc39 | ||
|
|
363cdd02df | ||
|
|
a5d1d3b90e | ||
|
|
aaa6c279a1 | ||
|
|
498becd11f | ||
|
|
411c61fb02 | ||
|
|
74a0bacdcf | ||
|
|
e7e7b0ffb7 | ||
|
|
fd339a7433 | ||
|
|
561b6343c8 | ||
|
|
e68d72c4d6 | ||
|
|
737a8f5965 | ||
|
|
c0dd29c591 | ||
|
|
8aa5675ba2 | ||
|
|
2086648c87 | ||
|
|
3a98d856a5 | ||
|
|
90fd1f7dd1 | ||
|
|
77ad73d579 | ||
|
|
13534fa0c0 | ||
|
|
37299abcc9 |
@@ -21,3 +21,4 @@ extends:
|
|||||||
rules:
|
rules:
|
||||||
# For overloads.
|
# For overloads.
|
||||||
no-dupe-class-members: off
|
no-dupe-class-members: off
|
||||||
|
"@typescript-eslint/no-use-before-define": off
|
||||||
|
|||||||
5
.github/issue_template.md
vendored
5
.github/issue_template.md
vendored
@@ -2,5 +2,10 @@
|
|||||||
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
|
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
|
||||||
The issue tracker is only for bugs.
|
The issue tracker is only for bugs.
|
||||||
|
|
||||||
|
Please see https://github.com/cdr/code-server/blob/master/doc/FAQ.md#how-do-i-debug-issues-with-code-server
|
||||||
|
and include any logging information relevant to the issue.
|
||||||
|
|
||||||
Please search for existing issues before filing.
|
Please search for existing issues before filing.
|
||||||
|
|
||||||
|
Please ensure you cannot reproduce on VS Code before filing.
|
||||||
-->
|
-->
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
build
|
build
|
||||||
dist*
|
dist*
|
||||||
out*
|
out*
|
||||||
release*
|
release/
|
||||||
|
release-upload/
|
||||||
node_modules
|
node_modules
|
||||||
binaries
|
binaries
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ remote server, accessible through the browser.
|
|||||||
Try it out:
|
Try it out:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it -p 127.0.0.1:8080:8080 -v "$PWD:/home/coder/project" codercom/code-server
|
docker run -it -p 127.0.0.1:8080:8080 -v "$PWD:/home/coder/project" -u "$(id -u):$(id -g)" codercom/code-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Code anywhere:** Code on your Chromebook, tablet, and laptop with a
|
- **Code anywhere:** Code on your Chromebook, tablet, and laptop with a
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# code-server.sh -- Run code-server with the bundled Node binary.
|
# Runs code-server with the bundled Node binary.
|
||||||
|
|
||||||
dir="$(dirname "$(readlink -f "$0" || realpath "$0")")"
|
# More complicated than readlink -f or realpath to support macOS.
|
||||||
|
# See https://github.com/cdr/code-server/issues/1537
|
||||||
|
get_installation_dir() {
|
||||||
|
# We read the symlink, which may be relative from $0.
|
||||||
|
dst="$(readlink "$0")"
|
||||||
|
# We cd into the $0 directory.
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
# Now we can cd into the dst directory.
|
||||||
|
cd "$(dirname "$dst")"
|
||||||
|
# Finally we use pwd -P to print the absolute path of the directory of $dst.
|
||||||
|
pwd -P
|
||||||
|
}
|
||||||
|
|
||||||
|
dir=$(get_installation_dir)
|
||||||
exec "$dir/node" "$dir/out/node/entry.js" "$@"
|
exec "$dir/node" "$dir/out/node/entry.js" "$@"
|
||||||
|
|||||||
13
ci/dev-image/Dockerfile
Normal file
13
ci/dev-image/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:12
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
iproute2 \
|
||||||
|
vim \
|
||||||
|
iptables \
|
||||||
|
net-tools \
|
||||||
|
libsecret-1-dev \
|
||||||
|
libx11-dev \
|
||||||
|
libxkbfile-dev
|
||||||
|
|
||||||
|
CMD ["/bin/bash"]
|
||||||
49
ci/dev-image/exec.sh
Executable file
49
ci/dev-image/exec.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# exec.sh opens an interactive bash session inside of a docker container
|
||||||
|
# for improved isolation during development
|
||||||
|
# if the container exists it is restarted if necessary, then reused
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Ensure submodules are cloned and up to date.
|
||||||
|
git submodule update --init
|
||||||
|
|
||||||
|
container_name=code-server-dev
|
||||||
|
|
||||||
|
enter() {
|
||||||
|
echo "--- Entering $container_name"
|
||||||
|
docker exec -it $container_id /bin/bash
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
echo "--- Spawning $container_name"
|
||||||
|
container_id=$(docker run \
|
||||||
|
-it \
|
||||||
|
--name $container_name \
|
||||||
|
"-v=$PWD:/code-server" \
|
||||||
|
"-w=/code-server" \
|
||||||
|
"-p=127.0.0.1:8080:8080" \
|
||||||
|
$([[ -t 0 ]] && echo -it || true) \
|
||||||
|
$container_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
echo "--- Building $container_name"
|
||||||
|
cd ../../
|
||||||
|
docker build -t $container_name -f ./ci/dev-image/Dockerfile . > /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
container_id=$(docker container inspect --format="{{.Id}}" $container_name 2> /dev/null) || true
|
||||||
|
|
||||||
|
if [ "$container_id" != "" ]; then
|
||||||
|
echo "-- Starting container"
|
||||||
|
docker start $container_id > /dev/null
|
||||||
|
|
||||||
|
enter
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
build
|
||||||
|
run
|
||||||
|
enter
|
||||||
@@ -13,7 +13,7 @@ RUN yum update -y && yum install -y \
|
|||||||
libx11-devel
|
libx11-devel
|
||||||
|
|
||||||
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.16.3/node-v12.16.3-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@1.22.4
|
RUN npm install -g yarn@1.22.4
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,4 @@ RUN cd /tmp && tar -xzf code-server*.tar.gz && rm code-server*.tar.gz && \
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
USER coder
|
USER coder
|
||||||
WORKDIR /home/coder
|
WORKDIR /home/coder
|
||||||
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/local/bin/code-server", "--host", "0.0.0.0", "."]
|
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/local/bin/code-server", "--bind-addr", "0.0.0.0:8080", "."]
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function package() {
|
|||||||
|
|
||||||
echo "done (release/$archive_name)"
|
echo "done (release/$archive_name)"
|
||||||
|
|
||||||
|
# release-upload is for uploading to the GCP bucket whereas release is used for GitHub.
|
||||||
mkdir -p "./release-upload/$VERSION"
|
mkdir -p "./release-upload/$VERSION"
|
||||||
cp "./release/$archive_name$ext" "./release-upload/$VERSION/$target-$arch$ext"
|
cp "./release/$archive_name$ext" "./release-upload/$VERSION/$target-$arch$ext"
|
||||||
mkdir -p "./release-upload/latest"
|
mkdir -p "./release-upload/latest"
|
||||||
|
|||||||
519
ci/vscode.patch
519
ci/vscode.patch
@@ -11,7 +11,7 @@ index e73dd4d9e8..e3192b3a0d 100644
|
|||||||
build/node_modules
|
build/node_modules
|
||||||
coverage/
|
coverage/
|
||||||
diff --git a/.yarnrc b/.yarnrc
|
diff --git a/.yarnrc b/.yarnrc
|
||||||
index 7808166004..1e16cde724 100644
|
index 7808166004..a7300dbfb9 100644
|
||||||
--- a/.yarnrc
|
--- a/.yarnrc
|
||||||
+++ b/.yarnrc
|
+++ b/.yarnrc
|
||||||
@@ -1,3 +1,3 @@
|
@@ -1,3 +1,3 @@
|
||||||
@@ -19,7 +19,7 @@ index 7808166004..1e16cde724 100644
|
|||||||
-target "7.1.11"
|
-target "7.1.11"
|
||||||
-runtime "electron"
|
-runtime "electron"
|
||||||
+disturl "http://nodejs.org/dist"
|
+disturl "http://nodejs.org/dist"
|
||||||
+target "12.4.0"
|
+target "12.16.3"
|
||||||
+runtime "node"
|
+runtime "node"
|
||||||
diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js
|
diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js
|
||||||
index 7a2320d828..5768890636 100644
|
index 7a2320d828..5768890636 100644
|
||||||
@@ -50,10 +50,10 @@ index 7a2320d828..5768890636 100644
|
|||||||
yarnInstallBuildDependencies(); // node modules for watching, specific to host node version, not electron
|
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..d0a8f37714
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/coder.js
|
+++ b/coder.js
|
||||||
@@ -0,0 +1,70 @@
|
@@ -0,0 +1,69 @@
|
||||||
+// This must be ran from VS Code's root.
|
+// This must be ran from VS Code's root.
|
||||||
+const gulp = require("gulp");
|
+const gulp = require("gulp");
|
||||||
+const path = require("path");
|
+const path = require("path");
|
||||||
@@ -77,7 +77,6 @@ index 0000000000..6aee0e46bc
|
|||||||
+
|
+
|
||||||
+const vscodeResources = [
|
+const vscodeResources = [
|
||||||
+ "out-build/vs/server/fork.js",
|
+ "out-build/vs/server/fork.js",
|
||||||
+ "out-build/vs/server/node/uriTransformer.js",
|
|
||||||
+ "!out-build/vs/server/doc/**",
|
+ "!out-build/vs/server/doc/**",
|
||||||
+ "out-build/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js",
|
+ "out-build/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js",
|
||||||
+ "out-build/bootstrap.js",
|
+ "out-build/bootstrap.js",
|
||||||
@@ -124,40 +123,8 @@ index 0000000000..6aee0e46bc
|
|||||||
+ util.rimraf("out-vscode-min"),
|
+ util.rimraf("out-vscode-min"),
|
||||||
+ common.minifyTask("out-vscode")
|
+ common.minifyTask("out-vscode")
|
||||||
+));
|
+));
|
||||||
diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json
|
|
||||||
index 8ac6b2806c..8562a284db 100644
|
|
||||||
--- a/extensions/vscode-api-tests/package.json
|
|
||||||
+++ b/extensions/vscode-api-tests/package.json
|
|
||||||
@@ -121,7 +121,7 @@
|
|
||||||
"@types/node": "^12.11.7",
|
|
||||||
"mocha-junit-reporter": "^1.17.0",
|
|
||||||
"mocha-multi-reporters": "^1.1.7",
|
|
||||||
- "typescript": "^1.6.2",
|
|
||||||
+ "typescript": "3.7.2",
|
|
||||||
"vscode": "1.1.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock
|
|
||||||
index 2d8b725ff2..a8d93a17ca 100644
|
|
||||||
--- a/extensions/vscode-api-tests/yarn.lock
|
|
||||||
+++ b/extensions/vscode-api-tests/yarn.lock
|
|
||||||
@@ -1855,10 +1855,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|
||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
|
||||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
|
||||||
|
|
||||||
-typescript@^1.6.2:
|
|
||||||
- version "1.8.10"
|
|
||||||
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e"
|
|
||||||
- integrity sha1-tHXW4N/wv1DyluXKbvn7tccyDx4=
|
|
||||||
+typescript@3.7.2:
|
|
||||||
+ version "3.7.2"
|
|
||||||
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
|
|
||||||
+ integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
|
|
||||||
|
|
||||||
unique-stream@^2.0.2:
|
|
||||||
version "2.2.1"
|
|
||||||
diff --git a/package.json b/package.json
|
diff --git a/package.json b/package.json
|
||||||
index 29d3cb6677..d3788cb1ab 100644
|
index 6f76321389..5cd3616392 100644
|
||||||
--- a/package.json
|
--- a/package.json
|
||||||
+++ b/package.json
|
+++ b/package.json
|
||||||
@@ -33,6 +33,9 @@
|
@@ -33,6 +33,9 @@
|
||||||
@@ -170,11 +137,24 @@ index 29d3cb6677..d3788cb1ab 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 e4546b2cf6..ad2c544e89 100644
|
||||||
--- a/src/vs/base/common/network.ts
|
--- a/src/vs/base/common/network.ts
|
||||||
+++ b/src/vs/base/common/network.ts
|
+++ b/src/vs/base/common/network.ts
|
||||||
@@ -88,16 +88,17 @@ class RemoteAuthoritiesImpl {
|
@@ -94,16 +94,17 @@ class RemoteAuthoritiesImpl {
|
||||||
if (host && host.indexOf(':') !== -1) {
|
if (host && host.indexOf(':') !== -1) {
|
||||||
host = `[${host}]`;
|
host = `[${host}]`;
|
||||||
}
|
}
|
||||||
@@ -196,7 +176,7 @@ index a68e020f9f..c31e7befa3 100644
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts
|
diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts
|
||||||
index 5a631e0b39..4114bd9287 100644
|
index 2c30aaa188..a1e89578a8 100644
|
||||||
--- a/src/vs/base/common/platform.ts
|
--- a/src/vs/base/common/platform.ts
|
||||||
+++ b/src/vs/base/common/platform.ts
|
+++ b/src/vs/base/common/platform.ts
|
||||||
@@ -59,6 +59,17 @@ if (typeof navigator === 'object' && !isElectronRenderer) {
|
@@ -59,6 +59,17 @@ if (typeof navigator === 'object' && !isElectronRenderer) {
|
||||||
@@ -231,6 +211,72 @@ index c52f7b3774..08a87fa970 100644
|
|||||||
];
|
];
|
||||||
const envKeys = Object.keys(env);
|
const envKeys = Object.keys(env);
|
||||||
envKeys
|
envKeys
|
||||||
|
diff --git a/src/vs/base/common/uriIpc.ts b/src/vs/base/common/uriIpc.ts
|
||||||
|
index ef2291d49b..29b2f9dfc2 100644
|
||||||
|
--- a/src/vs/base/common/uriIpc.ts
|
||||||
|
+++ b/src/vs/base/common/uriIpc.ts
|
||||||
|
@@ -5,6 +5,7 @@
|
||||||
|
|
||||||
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||||
|
import { MarshalledObject } from 'vs/base/common/marshalling';
|
||||||
|
+import { Schemas } from './network';
|
||||||
|
|
||||||
|
export interface IURITransformer {
|
||||||
|
transformIncoming(uri: UriComponents): UriComponents;
|
||||||
|
@@ -31,29 +32,35 @@ function toJSON(uri: URI): UriComponents {
|
||||||
|
|
||||||
|
export class URITransformer implements IURITransformer {
|
||||||
|
|
||||||
|
- private readonly _uriTransformer: IRawURITransformer;
|
||||||
|
-
|
||||||
|
- constructor(uriTransformer: IRawURITransformer) {
|
||||||
|
- this._uriTransformer = uriTransformer;
|
||||||
|
+ constructor(private readonly remoteAuthority: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // NOTE@coder: Coming in from the browser it'll be vscode-remote so it needs
|
||||||
|
+ // to be transformed into file.
|
||||||
|
public transformIncoming(uri: UriComponents): UriComponents {
|
||||||
|
- const result = this._uriTransformer.transformIncoming(uri);
|
||||||
|
- return (result === uri ? uri : toJSON(URI.from(result)));
|
||||||
|
+ return uri.scheme === Schemas.vscodeRemote
|
||||||
|
+ ? toJSON(URI.file(uri.path))
|
||||||
|
+ : uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // NOTE@coder: Going out to the browser it'll be file so it needs to be
|
||||||
|
+ // transformed into vscode-remote.
|
||||||
|
public transformOutgoing(uri: UriComponents): UriComponents {
|
||||||
|
- const result = this._uriTransformer.transformOutgoing(uri);
|
||||||
|
- return (result === uri ? uri : toJSON(URI.from(result)));
|
||||||
|
+ return uri.scheme === Schemas.file
|
||||||
|
+ ? toJSON(URI.from({ authority: this.remoteAuthority, scheme: Schemas.vscodeRemote, path: uri.path }))
|
||||||
|
+ : uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public transformOutgoingURI(uri: URI): URI {
|
||||||
|
- const result = this._uriTransformer.transformOutgoing(uri);
|
||||||
|
- return (result === uri ? uri : URI.from(result));
|
||||||
|
+ return uri.scheme === Schemas.file
|
||||||
|
+ ? URI.from({ authority: this.remoteAuthority, scheme: Schemas.vscodeRemote, path:uri.path })
|
||||||
|
+ : uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public transformOutgoingScheme(scheme: string): string {
|
||||||
|
- return this._uriTransformer.transformOutgoingScheme(scheme);
|
||||||
|
+ return scheme === Schemas.file
|
||||||
|
+ ? Schemas.vscodeRemote
|
||||||
|
+ : scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -152,4 +159,4 @@ export function transformAndReviveIncomingURIs<T>(obj: T, transformer: IURITrans
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
-}
|
||||||
|
\ No newline at end of file
|
||||||
|
+}
|
||||||
diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js
|
diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js
|
||||||
index 2c64061da7..c0ef8faedd 100644
|
index 2c64061da7..c0ef8faedd 100644
|
||||||
--- a/src/vs/base/node/languagePacks.js
|
--- a/src/vs/base/node/languagePacks.js
|
||||||
@@ -248,16 +294,25 @@ 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..4d1a590a7c 100644
|
index ef926bf4fa..64efcc463c 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
|
||||||
@@ -246,12 +246,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
@@ -12,6 +12,8 @@ import { request } from 'vs/base/parts/request/browser/request';
|
||||||
|
import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows';
|
||||||
|
import { isEqual } from 'vs/base/common/resources';
|
||||||
|
import { isStandalone } from 'vs/base/browser/browser';
|
||||||
|
+import { Schemas } from 'vs/base/common/network';
|
||||||
|
+import { encodePath } from 'vs/server/node/util';
|
||||||
|
|
||||||
|
interface ICredential {
|
||||||
|
service: string;
|
||||||
|
@@ -242,12 +244,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}`;
|
||||||
}
|
}
|
||||||
@@ -266,13 +321,13 @@ index 45f6f17ce0..4d1a590a7c 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
|
||||||
@@ -290,6 +296,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
@@ -286,6 +294,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||||
|
|
||||||
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute);
|
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute);
|
||||||
|
|
||||||
@@ -281,20 +336,22 @@ index 45f6f17ce0..4d1a590a7c 100644
|
|||||||
+ if (config.remoteAuthority) {
|
+ if (config.remoteAuthority) {
|
||||||
+ (config as any).remoteAuthority = normalizeAuthority(config.remoteAuthority);
|
+ (config as any).remoteAuthority = normalizeAuthority(config.remoteAuthority);
|
||||||
+ }
|
+ }
|
||||||
+ if (config.workspaceUri) {
|
+ if (config.workspaceUri && config.workspaceUri.authority) {
|
||||||
+ config.workspaceUri.authority = normalizeAuthority(config.workspaceUri.authority);
|
+ config.workspaceUri.authority = normalizeAuthority(config.workspaceUri.authority);
|
||||||
+ }
|
+ }
|
||||||
+ if (config.folderUri) {
|
+ if (config.folderUri && config.folderUri.authority) {
|
||||||
+ config.folderUri.authority = normalizeAuthority(config.folderUri.authority);
|
+ config.folderUri.authority = normalizeAuthority(config.folderUri.authority);
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
// Revive static extension locations
|
// Revive static extension locations
|
||||||
if (Array.isArray(config.staticExtensions)) {
|
if (Array.isArray(config.staticExtensions)) {
|
||||||
config.staticExtensions.forEach(extension => {
|
config.staticExtensions.forEach(extension => {
|
||||||
@@ -302,35 +320,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
@@ -296,36 +316,7 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||||
|
// Find workspace to open and payload
|
||||||
|
let foundWorkspace = false;
|
||||||
let workspace: IWorkspace;
|
let workspace: IWorkspace;
|
||||||
let payload = Object.create(null);
|
- let payload = Object.create(null);
|
||||||
|
-
|
||||||
- const query = new URL(document.location.href).searchParams;
|
- const query = new URL(document.location.href).searchParams;
|
||||||
- query.forEach((value, key) => {
|
- query.forEach((value, key) => {
|
||||||
- switch (key) {
|
- switch (key) {
|
||||||
@@ -323,12 +380,12 @@ index 45f6f17ce0..4d1a590a7c 100644
|
|||||||
- break;
|
- break;
|
||||||
- }
|
- }
|
||||||
- });
|
- });
|
||||||
-
|
+ let payload = config.workspaceProvider?.payload || Object.create(null);
|
||||||
|
|
||||||
// If no workspace is provided through the URL, check for config attribute from server
|
// If no workspace is provided through the URL, check for config attribute from server
|
||||||
if (!foundWorkspace) {
|
if (!foundWorkspace) {
|
||||||
if (config.folderUri) {
|
|
||||||
diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts
|
diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts
|
||||||
index abd1e33b18..bf75952ce1 100644
|
index aa44ee75d7..884887a6a3 100644
|
||||||
--- a/src/vs/platform/environment/common/environment.ts
|
--- a/src/vs/platform/environment/common/environment.ts
|
||||||
+++ b/src/vs/platform/environment/common/environment.ts
|
+++ b/src/vs/platform/environment/common/environment.ts
|
||||||
@@ -37,6 +37,8 @@ export interface ParsedArgs {
|
@@ -37,6 +37,8 @@ export interface ParsedArgs {
|
||||||
@@ -339,8 +396,8 @@ index abd1e33b18..bf75952ce1 100644
|
|||||||
+ 'extra-builtin-extensions-dir'?: string[];
|
+ 'extra-builtin-extensions-dir'?: string[];
|
||||||
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
|
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
|
||||||
extensionTestsPath?: string; // either a local path or a URI
|
extensionTestsPath?: string; // either a local path or a URI
|
||||||
'extension-development-confirm-save'?: boolean;
|
'inspect-extensions'?: string;
|
||||||
@@ -147,6 +149,8 @@ export interface IEnvironmentService extends IUserHomeProvider {
|
@@ -144,6 +146,8 @@ export interface IEnvironmentService extends IUserHomeProvider {
|
||||||
disableExtensions: boolean | string[];
|
disableExtensions: boolean | string[];
|
||||||
builtinExtensionsPath: string;
|
builtinExtensionsPath: string;
|
||||||
extensionsPath?: string;
|
extensionsPath?: string;
|
||||||
@@ -348,12 +405,12 @@ index abd1e33b18..bf75952ce1 100644
|
|||||||
+ extraBuiltinExtensionPaths: string[];
|
+ extraBuiltinExtensionPaths: string[];
|
||||||
extensionDevelopmentLocationURI?: URI[];
|
extensionDevelopmentLocationURI?: URI[];
|
||||||
extensionTestsLocationURI?: URI;
|
extensionTestsLocationURI?: URI;
|
||||||
logExtensionHostCommunication?: boolean;
|
extensionEnabledProposedApi?: string[] | undefined;
|
||||||
diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts
|
diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts
|
||||||
index e68e0647c3..49a5aae2fa 100644
|
index c43ccfd997..d3550c1126 100644
|
||||||
--- a/src/vs/platform/environment/node/argv.ts
|
--- a/src/vs/platform/environment/node/argv.ts
|
||||||
+++ b/src/vs/platform/environment/node/argv.ts
|
+++ b/src/vs/platform/environment/node/argv.ts
|
||||||
@@ -55,6 +55,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
@@ -53,6 +53,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
||||||
|
|
||||||
'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") },
|
'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") },
|
||||||
'builtin-extensions-dir': { type: 'string' },
|
'builtin-extensions-dir': { type: 'string' },
|
||||||
@@ -368,10 +425,10 @@ index e68e0647c3..49a5aae2fa 100644
|
|||||||
}
|
}
|
||||||
-
|
-
|
||||||
diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts
|
diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts
|
||||||
index 15b5c20cbb..a95f1cd7b5 100644
|
index 9f518b9729..5bfd95c88a 100644
|
||||||
--- a/src/vs/platform/environment/node/environmentService.ts
|
--- a/src/vs/platform/environment/node/environmentService.ts
|
||||||
+++ b/src/vs/platform/environment/node/environmentService.ts
|
+++ b/src/vs/platform/environment/node/environmentService.ts
|
||||||
@@ -197,6 +197,13 @@ export class EnvironmentService implements IEnvironmentService {
|
@@ -191,6 +191,13 @@ export class EnvironmentService implements IEnvironmentService {
|
||||||
return path.join(this.userHome, product.dataFolderName, 'extensions');
|
return path.join(this.userHome, product.dataFolderName, 'extensions');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +443,7 @@ index 15b5c20cbb..a95f1cd7b5 100644
|
|||||||
get extensionDevelopmentLocationURI(): URI[] | undefined {
|
get extensionDevelopmentLocationURI(): URI[] | undefined {
|
||||||
const s = this._args.extensionDevelopmentPath;
|
const s = this._args.extensionDevelopmentPath;
|
||||||
diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
||||||
index 5b05650591..aa8712d8fb 100644
|
index ceab231b9f..edcd8e00b5 100644
|
||||||
--- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
--- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
||||||
+++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
+++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
||||||
@@ -743,11 +743,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
@@ -743,11 +743,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||||
@@ -460,7 +517,7 @@ index 5b05650591..aa8712d8fb 100644
|
|||||||
const toRemove: ILocalExtension[] = [];
|
const toRemove: ILocalExtension[] = [];
|
||||||
|
|
||||||
diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts
|
diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts
|
||||||
index da88376513..5bab4bd49c 100644
|
index 29927fec2a..6c836741e4 100644
|
||||||
--- a/src/vs/platform/product/common/product.ts
|
--- a/src/vs/platform/product/common/product.ts
|
||||||
+++ b/src/vs/platform/product/common/product.ts
|
+++ b/src/vs/platform/product/common/product.ts
|
||||||
@@ -27,6 +27,12 @@ if (isWeb) {
|
@@ -27,6 +27,12 @@ if (isWeb) {
|
||||||
@@ -512,10 +569,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..96fbd4b0bb
|
index 0000000000..649cf32f0a
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/src/vs/server/browser/client.ts
|
+++ b/src/vs/server/browser/client.ts
|
||||||
@@ -0,0 +1,270 @@
|
@@ -0,0 +1,264 @@
|
||||||
+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';
|
||||||
@@ -691,15 +748,10 @@ index 0000000000..96fbd4b0bb
|
|||||||
+ const response = await fetch(normalize(`${options.base}/update/apply`), {
|
+ const response = await fetch(normalize(`${options.base}/update/apply`), {
|
||||||
+ headers: { "content-type": "application/json" },
|
+ headers: { "content-type": "application/json" },
|
||||||
+ });
|
+ });
|
||||||
+ if (response.status !== 200) {
|
|
||||||
+ throw new Error("Unexpected response");
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ const json = await response.json();
|
+ const json = await response.json();
|
||||||
+ if (!json.isLatest) {
|
+ if (response.status !== 200 || json.error) {
|
||||||
+ throw new Error("Update failed");
|
+ throw new Error(json.error || response.statusText);
|
||||||
+ }
|
+ }
|
||||||
+
|
|
||||||
+ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`);
|
+ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`);
|
||||||
+ };
|
+ };
|
||||||
+
|
+
|
||||||
@@ -709,11 +761,10 @@ index 0000000000..96fbd4b0bb
|
|||||||
+ const response = await fetch(normalize(`${options.base}/update`), {
|
+ const response = await fetch(normalize(`${options.base}/update`), {
|
||||||
+ headers: { "content-type": "application/json" },
|
+ headers: { "content-type": "application/json" },
|
||||||
+ });
|
+ });
|
||||||
+ if (response.status !== 200) {
|
|
||||||
+ throw new Error("unexpected response");
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ const json = await response.json();
|
+ const json = await response.json();
|
||||||
|
+ if (response.status !== 200 || json.error) {
|
||||||
|
+ throw new Error(json.error || response.statusText);
|
||||||
|
+ }
|
||||||
+ if (json.isLatest) {
|
+ if (json.isLatest) {
|
||||||
+ return;
|
+ return;
|
||||||
+ }
|
+ }
|
||||||
@@ -1147,10 +1198,10 @@ index 0000000000..56331ff1fc
|
|||||||
+require('../../bootstrap-amd').load('vs/server/entry');
|
+require('../../bootstrap-amd').load('vs/server/entry');
|
||||||
diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts
|
diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..cb4d3a6afe
|
index 0000000000..d4771351de
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/src/vs/server/ipc.d.ts
|
+++ b/src/vs/server/ipc.d.ts
|
||||||
@@ -0,0 +1,113 @@
|
@@ -0,0 +1,116 @@
|
||||||
+/**
|
+/**
|
||||||
+ * External interfaces for integration into code-server over IPC. No vs imports
|
+ * External interfaces for integration into code-server over IPC. No vs imports
|
||||||
+ * should be made in this file.
|
+ * should be made in this file.
|
||||||
@@ -1247,6 +1298,9 @@ index 0000000000..cb4d3a6afe
|
|||||||
+ readonly folderUri?: UriComponents;
|
+ readonly folderUri?: UriComponents;
|
||||||
+ readonly workspaceUri?: UriComponents;
|
+ readonly workspaceUri?: UriComponents;
|
||||||
+ readonly logLevel?: number;
|
+ readonly logLevel?: number;
|
||||||
|
+ readonly workspaceProvider?: {
|
||||||
|
+ payload: [["userDataPath", string]];
|
||||||
|
+ };
|
||||||
+ };
|
+ };
|
||||||
+ readonly remoteUserDataUri: UriComponents;
|
+ readonly remoteUserDataUri: UriComponents;
|
||||||
+ readonly productConfiguration: {
|
+ readonly productConfiguration: {
|
||||||
@@ -1266,7 +1320,7 @@ index 0000000000..cb4d3a6afe
|
|||||||
+}
|
+}
|
||||||
diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts
|
diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..9c240b992d
|
index 0000000000..1729ec2fa8
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/src/vs/server/node/channel.ts
|
+++ b/src/vs/server/node/channel.ts
|
||||||
@@ -0,0 +1,343 @@
|
@@ -0,0 +1,343 @@
|
||||||
@@ -1513,7 +1567,7 @@ index 0000000000..9c240b992d
|
|||||||
+ connectionToken: this.connectionToken,
|
+ connectionToken: this.connectionToken,
|
||||||
+ appRoot: URI.file(this.environment.appRoot),
|
+ appRoot: URI.file(this.environment.appRoot),
|
||||||
+ appSettingsHome: this.environment.appSettingsHome,
|
+ appSettingsHome: this.environment.appSettingsHome,
|
||||||
+ settingsPath: this.environment.machineSettingsHome,
|
+ settingsPath: this.environment.machineSettingsResource,
|
||||||
+ logsPath: URI.file(this.environment.logsPath),
|
+ logsPath: URI.file(this.environment.logsPath),
|
||||||
+ extensionsPath: URI.file(this.environment.extensionsPath!),
|
+ extensionsPath: URI.file(this.environment.extensionsPath!),
|
||||||
+ extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, 'extension-host')),
|
+ extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, 'extension-host')),
|
||||||
@@ -1615,10 +1669,10 @@ index 0000000000..9c240b992d
|
|||||||
+}
|
+}
|
||||||
diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts
|
diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..9b8969690c
|
index 0000000000..8f52462797
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/src/vs/server/node/connection.ts
|
+++ b/src/vs/server/node/connection.ts
|
||||||
@@ -0,0 +1,158 @@
|
@@ -0,0 +1,157 @@
|
||||||
+import * as cp from 'child_process';
|
+import * as cp from 'child_process';
|
||||||
+import { getPathFromAmdModule } from 'vs/base/common/amd';
|
+import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||||
+import { VSBuffer } from 'vs/base/common/buffer';
|
+import { VSBuffer } from 'vs/base/common/buffer';
|
||||||
@@ -1629,7 +1683,6 @@ index 0000000000..9b8969690c
|
|||||||
+import { ILogService } from 'vs/platform/log/common/log';
|
+import { ILogService } from 'vs/platform/log/common/log';
|
||||||
+import { getNlsConfiguration } from 'vs/server/node/nls';
|
+import { getNlsConfiguration } from 'vs/server/node/nls';
|
||||||
+import { Protocol } from 'vs/server/node/protocol';
|
+import { Protocol } from 'vs/server/node/protocol';
|
||||||
+import { uriTransformerPath } from 'vs/server/node/util';
|
|
||||||
+import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
+import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
||||||
+
|
+
|
||||||
+export abstract class Connection {
|
+export abstract class Connection {
|
||||||
@@ -1734,7 +1787,7 @@ index 0000000000..9b8969690c
|
|||||||
+ const config = await getNlsConfiguration(locale, this.environment.userDataPath);
|
+ const config = await getNlsConfiguration(locale, this.environment.userDataPath);
|
||||||
+ const proc = cp.fork(
|
+ const proc = cp.fork(
|
||||||
+ getPathFromAmdModule(require, 'bootstrap-fork'),
|
+ getPathFromAmdModule(require, 'bootstrap-fork'),
|
||||||
+ [ '--type=extensionHost', `--uriTransformerPath=${uriTransformerPath}` ],
|
+ [ '--type=extensionHost' ],
|
||||||
+ {
|
+ {
|
||||||
+ env: {
|
+ env: {
|
||||||
+ ...process.env,
|
+ ...process.env,
|
||||||
@@ -1744,7 +1797,7 @@ index 0000000000..9b8969690c
|
|||||||
+ VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true',
|
+ VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true',
|
||||||
+ VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true',
|
+ VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true',
|
||||||
+ VSCODE_LOG_STACK: 'false',
|
+ VSCODE_LOG_STACK: 'false',
|
||||||
+ VSCODE_LOG_LEVEL: this.environment.verbose ? 'trace' : this.environment.log,
|
+ VSCODE_LOG_LEVEL: process.env.LOG_LEVEL,
|
||||||
+ VSCODE_NLS_CONFIG: JSON.stringify(config),
|
+ VSCODE_NLS_CONFIG: JSON.stringify(config),
|
||||||
+ },
|
+ },
|
||||||
+ silent: true,
|
+ silent: true,
|
||||||
@@ -2337,10 +2390,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..d1f14654cf
|
||||||
--- /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,272 @@
|
||||||
+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';
|
||||||
@@ -2420,12 +2473,27 @@ 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),
|
||||||
|
+ workspaceProvider: {
|
||||||
|
+ payload: [["userDataPath", environment.userDataPath]],
|
||||||
|
+ },
|
||||||
+ },
|
+ },
|
||||||
+ remoteUserDataUri: transformer.transformOutgoing(URI.file(environment.userDataPath)),
|
+ remoteUserDataUri: transformer.transformOutgoing(URI.file(environment.userDataPath)),
|
||||||
+ productConfiguration: product,
|
+ productConfiguration: product,
|
||||||
@@ -2598,57 +2666,31 @@ index 0000000000..d6dcfe1fe7
|
|||||||
+ return undefined;
|
+ return undefined;
|
||||||
+ }
|
+ }
|
||||||
+}
|
+}
|
||||||
diff --git a/src/vs/server/node/uriTransformer.js b/src/vs/server/node/uriTransformer.js
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000000..fc69441cf0
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/src/vs/server/node/uriTransformer.js
|
|
||||||
@@ -0,0 +1,24 @@
|
|
||||||
+// This file is included via a regular Node require. I'm not sure how (or if)
|
|
||||||
+// we can write this in Typescript and have it compile to non-AMD syntax.
|
|
||||||
+module.exports = (remoteAuthority) => {
|
|
||||||
+ return {
|
|
||||||
+ transformIncoming: (uri) => {
|
|
||||||
+ switch (uri.scheme) {
|
|
||||||
+ case "vscode-remote": return { scheme: "file", path: uri.path };
|
|
||||||
+ default: return uri;
|
|
||||||
+ }
|
|
||||||
+ },
|
|
||||||
+ transformOutgoing: (uri) => {
|
|
||||||
+ switch (uri.scheme) {
|
|
||||||
+ case "file": return { scheme: "vscode-remote", authority: remoteAuthority, path: uri.path };
|
|
||||||
+ default: return uri;
|
|
||||||
+ }
|
|
||||||
+ },
|
|
||||||
+ transformOutgoingScheme: (scheme) => {
|
|
||||||
+ switch (scheme) {
|
|
||||||
+ case "file": return "vscode-remote";
|
|
||||||
+ default: return scheme;
|
|
||||||
+ }
|
|
||||||
+ },
|
|
||||||
+ };
|
|
||||||
+};
|
|
||||||
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..fa47e993b4
|
||||||
--- /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,13 @@
|
||||||
+import { getPathFromAmdModule } from 'vs/base/common/amd';
|
+import { URITransformer } from 'vs/base/common/uriIpc';
|
||||||
+import { URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
|
|
||||||
+
|
+
|
||||||
+export const uriTransformerPath = getPathFromAmdModule(require, 'vs/server/node/uriTransformer');
|
|
||||||
+export const getUriTransformer = (remoteAuthority: string): URITransformer => {
|
+export const getUriTransformer = (remoteAuthority: string): URITransformer => {
|
||||||
+ const rawURITransformerFactory = <any>require.__$__nodeRequire(uriTransformerPath);
|
+ return new URITransformer(remoteAuthority);
|
||||||
+ const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
|
+};
|
||||||
+ 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 3f2de2c738..a967d8df69 100644
|
||||||
--- a/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
--- a/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
||||||
+++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
+++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
||||||
@@ -58,6 +58,7 @@ import './mainThreadWorkspace';
|
@@ -59,6 +59,7 @@ import './mainThreadComments';
|
||||||
import './mainThreadComments';
|
import './mainThreadNotebook';
|
||||||
import './mainThreadTask';
|
import './mainThreadTask';
|
||||||
import './mainThreadLabelService';
|
import './mainThreadLabelService';
|
||||||
+import 'vs/server/browser/mainThreadNodeProxy';
|
+import 'vs/server/browser/mainThreadNodeProxy';
|
||||||
@@ -2656,18 +2698,18 @@ index e69aa80159..71a899d37b 100644
|
|||||||
import './mainThreadAuthentication';
|
import './mainThreadAuthentication';
|
||||||
import './mainThreadTimeline';
|
import './mainThreadTimeline';
|
||||||
diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts
|
diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts
|
||||||
index cb57cc8227..9da59c028e 100644
|
index 054aaa0ad6..873793a6b8 100644
|
||||||
--- a/src/vs/workbench/api/common/extHost.api.impl.ts
|
--- a/src/vs/workbench/api/common/extHost.api.impl.ts
|
||||||
+++ b/src/vs/workbench/api/common/extHost.api.impl.ts
|
+++ b/src/vs/workbench/api/common/extHost.api.impl.ts
|
||||||
@@ -67,6 +67,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
@@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf
|
||||||
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
|
|
||||||
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
||||||
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
|
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
|
||||||
|
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
|
||||||
+import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy';
|
+import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy';
|
||||||
import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming';
|
import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming';
|
||||||
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
||||||
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
|
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
|
||||||
@@ -91,6 +92,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
@@ -92,6 +93,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||||
const rpcProtocol = accessor.get(IExtHostRpcService);
|
const rpcProtocol = accessor.get(IExtHostRpcService);
|
||||||
const extHostStorage = accessor.get(IExtHostStorage);
|
const extHostStorage = accessor.get(IExtHostStorage);
|
||||||
const extHostLogService = accessor.get(ILogService);
|
const extHostLogService = accessor.get(ILogService);
|
||||||
@@ -2675,7 +2717,7 @@ index cb57cc8227..9da59c028e 100644
|
|||||||
const extHostTunnelService = accessor.get(IExtHostTunnelService);
|
const extHostTunnelService = accessor.get(IExtHostTunnelService);
|
||||||
const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
|
const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
@@ -101,6 +103,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||||
rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration);
|
rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration);
|
||||||
rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService);
|
rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService);
|
||||||
rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage);
|
rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage);
|
||||||
@@ -2684,10 +2726,10 @@ index cb57cc8227..9da59c028e 100644
|
|||||||
|
|
||||||
// automatically create and register addressable instances
|
// automatically create and register addressable instances
|
||||||
diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts
|
diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts
|
||||||
index 10f51d2354..8dd34dffa9 100644
|
index 4b0770b4b5..bef6003042 100644
|
||||||
--- a/src/vs/workbench/api/common/extHost.protocol.ts
|
--- a/src/vs/workbench/api/common/extHost.protocol.ts
|
||||||
+++ b/src/vs/workbench/api/common/extHost.protocol.ts
|
+++ b/src/vs/workbench/api/common/extHost.protocol.ts
|
||||||
@@ -671,6 +671,16 @@ export interface MainThreadLabelServiceShape extends IDisposable {
|
@@ -751,6 +751,16 @@ export interface MainThreadLabelServiceShape extends IDisposable {
|
||||||
$unregisterResourceLabelFormatter(handle: number): void;
|
$unregisterResourceLabelFormatter(handle: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2704,24 +2746,24 @@ index 10f51d2354..8dd34dffa9 100644
|
|||||||
export interface MainThreadSearchShape extends IDisposable {
|
export interface MainThreadSearchShape extends IDisposable {
|
||||||
$registerFileSearchProvider(handle: number, scheme: string): void;
|
$registerFileSearchProvider(handle: number, scheme: string): void;
|
||||||
$registerTextSearchProvider(handle: number, scheme: string): void;
|
$registerTextSearchProvider(handle: number, scheme: string): void;
|
||||||
@@ -1513,6 +1523,7 @@ export const MainContext = {
|
@@ -1609,6 +1619,7 @@ export const MainContext = {
|
||||||
MainThreadTask: createMainId<MainThreadTaskShape>('MainThreadTask'),
|
|
||||||
MainThreadWindow: createMainId<MainThreadWindowShape>('MainThreadWindow'),
|
MainThreadWindow: createMainId<MainThreadWindowShape>('MainThreadWindow'),
|
||||||
MainThreadLabelService: createMainId<MainThreadLabelServiceShape>('MainThreadLabelService'),
|
MainThreadLabelService: createMainId<MainThreadLabelServiceShape>('MainThreadLabelService'),
|
||||||
|
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook'),
|
||||||
+ MainThreadNodeProxy: createMainId<MainThreadNodeProxyShape>('MainThreadNodeProxy'),
|
+ MainThreadNodeProxy: createMainId<MainThreadNodeProxyShape>('MainThreadNodeProxy'),
|
||||||
MainThreadTheming: createMainId<MainThreadThemingShape>('MainThreadTheming'),
|
MainThreadTheming: createMainId<MainThreadThemingShape>('MainThreadTheming'),
|
||||||
MainThreadTunnelService: createMainId<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
|
MainThreadTunnelService: createMainId<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
|
||||||
MainThreadTimeline: createMainId<MainThreadTimelineShape>('MainThreadTimeline')
|
MainThreadTimeline: createMainId<MainThreadTimelineShape>('MainThreadTimeline')
|
||||||
@@ -1550,6 +1561,7 @@ export const ExtHostContext = {
|
@@ -1647,6 +1658,7 @@ export const ExtHostContext = {
|
||||||
ExtHostUrls: createExtId<ExtHostUrlsShape>('ExtHostUrls'),
|
|
||||||
ExtHostOutputService: createMainId<ExtHostOutputServiceShape>('ExtHostOutputService'),
|
ExtHostOutputService: createMainId<ExtHostOutputServiceShape>('ExtHostOutputService'),
|
||||||
ExtHostLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
|
ExtHosLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
|
||||||
|
ExtHostNotebook: createMainId<ExtHostNotebookShape>('ExtHostNotebook'),
|
||||||
+ ExtHostNodeProxy: createMainId<ExtHostNodeProxyShape>('ExtHostNodeProxy'),
|
+ ExtHostNodeProxy: createMainId<ExtHostNodeProxyShape>('ExtHostNodeProxy'),
|
||||||
ExtHostTheming: createMainId<ExtHostThemingShape>('ExtHostTheming'),
|
ExtHostTheming: createMainId<ExtHostThemingShape>('ExtHostTheming'),
|
||||||
ExtHostTunnelService: createMainId<ExtHostTunnelServiceShape>('ExtHostTunnelService'),
|
ExtHostTunnelService: createMainId<ExtHostTunnelServiceShape>('ExtHostTunnelService'),
|
||||||
ExtHostAuthentication: createMainId<ExtHostAuthenticationShape>('ExtHostAuthentication'),
|
ExtHostAuthentication: createMainId<ExtHostAuthenticationShape>('ExtHostAuthentication'),
|
||||||
diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts
|
diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts
|
||||||
index 197aa88c85..1c337cdc39 100644
|
index 904c5afd8c..c0e760b68c 100644
|
||||||
--- a/src/vs/workbench/api/common/extHostExtensionService.ts
|
--- a/src/vs/workbench/api/common/extHostExtensionService.ts
|
||||||
+++ b/src/vs/workbench/api/common/extHostExtensionService.ts
|
+++ b/src/vs/workbench/api/common/extHostExtensionService.ts
|
||||||
@@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
|
@@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
|
||||||
@@ -2809,7 +2851,7 @@ index 72ad75d63e..07b8a3f20c 100644
|
|||||||
+}
|
+}
|
||||||
+registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(IExtHostNodeProxy) {});
|
+registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(IExtHostNodeProxy) {});
|
||||||
diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts
|
diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts
|
||||||
index 79189ba670..216f29b587 100644
|
index 3a02c5ce0b..3e1594129c 100644
|
||||||
--- a/src/vs/workbench/api/node/extHostExtensionService.ts
|
--- a/src/vs/workbench/api/node/extHostExtensionService.ts
|
||||||
+++ b/src/vs/workbench/api/node/extHostExtensionService.ts
|
+++ b/src/vs/workbench/api/node/extHostExtensionService.ts
|
||||||
@@ -13,6 +13,8 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer
|
@@ -13,6 +13,8 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer
|
||||||
@@ -2940,17 +2982,17 @@ index 4781f22676..86c9246f51 100644
|
|||||||
throw new Error(`Cannot load module '${request}'`);
|
throw new Error(`Cannot load module '${request}'`);
|
||||||
}
|
}
|
||||||
diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts
|
diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts
|
||||||
index 8973e3fc36..7e3286bd37 100644
|
index 4f1c00218a..c9099553ce 100644
|
||||||
--- a/src/vs/workbench/browser/web.main.ts
|
--- a/src/vs/workbench/browser/web.main.ts
|
||||||
+++ b/src/vs/workbench/browser/web.main.ts
|
+++ b/src/vs/workbench/browser/web.main.ts
|
||||||
@@ -49,6 +49,7 @@ import { IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedD
|
@@ -48,6 +48,7 @@ import { IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedD
|
||||||
import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLogProvider';
|
import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLogProvider';
|
||||||
import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows';
|
import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows';
|
||||||
import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces';
|
import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces';
|
||||||
+import { initialize } from 'vs/server/browser/client';
|
+import { initialize } from 'vs/server/browser/client';
|
||||||
import { coalesce } from 'vs/base/common/arrays';
|
import { coalesce } from 'vs/base/common/arrays';
|
||||||
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
||||||
|
import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService';
|
||||||
@@ -87,6 +88,7 @@ class BrowserMain extends Disposable {
|
@@ -87,6 +88,7 @@ class BrowserMain extends Disposable {
|
||||||
|
|
||||||
// Startup
|
// Startup
|
||||||
@@ -2960,7 +3002,7 @@ index 8973e3fc36..7e3286bd37 100644
|
|||||||
|
|
||||||
private registerListeners(workbench: Workbench, storageService: BrowserStorageService): void {
|
private registerListeners(workbench: Workbench, storageService: BrowserStorageService): void {
|
||||||
diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts
|
diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts
|
||||||
index 597dd5d96f..9041a1e81b 100644
|
index 2a7844da48..2812092983 100644
|
||||||
--- a/src/vs/workbench/common/resources.ts
|
--- a/src/vs/workbench/common/resources.ts
|
||||||
+++ b/src/vs/workbench/common/resources.ts
|
+++ b/src/vs/workbench/common/resources.ts
|
||||||
@@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob';
|
@@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob';
|
||||||
@@ -2982,10 +3024,10 @@ index 597dd5d96f..9041a1e81b 100644
|
|||||||
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null);
|
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null);
|
||||||
this._extensionKey.set(value ? extname(value) : null);
|
this._extensionKey.set(value ? extname(value) : null);
|
||||||
diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js
|
diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js
|
||||||
index 3fed6a33da..45baca0ad2 100644
|
index b378daa5a0..8d7b1b16df 100644
|
||||||
--- a/src/vs/workbench/contrib/webview/browser/pre/main.js
|
--- a/src/vs/workbench/contrib/webview/browser/pre/main.js
|
||||||
+++ b/src/vs/workbench/contrib/webview/browser/pre/main.js
|
+++ b/src/vs/workbench/contrib/webview/browser/pre/main.js
|
||||||
@@ -346,7 +346,8 @@
|
@@ -347,7 +347,8 @@
|
||||||
if (data.endpoint) {
|
if (data.endpoint) {
|
||||||
try {
|
try {
|
||||||
const endpointUrl = new URL(data.endpoint);
|
const endpointUrl = new URL(data.endpoint);
|
||||||
@@ -2996,10 +3038,18 @@ index 3fed6a33da..45baca0ad2 100644
|
|||||||
console.error('Could not rewrite csp');
|
console.error('Could not rewrite csp');
|
||||||
}
|
}
|
||||||
diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts
|
diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts
|
||||||
index c94ee4e88c..cce3cf6f13 100644
|
index f878c3de3d..ad6fb4606a 100644
|
||||||
--- a/src/vs/workbench/services/environment/browser/environmentService.ts
|
--- a/src/vs/workbench/services/environment/browser/environmentService.ts
|
||||||
+++ b/src/vs/workbench/services/environment/browser/environmentService.ts
|
+++ b/src/vs/workbench/services/environment/browser/environmentService.ts
|
||||||
@@ -195,8 +195,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
|
@@ -13,6 +13,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
|
||||||
|
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
|
||||||
|
import product from 'vs/platform/product/common/product';
|
||||||
|
import { memoize } from 'vs/base/common/decorators';
|
||||||
|
+import * as paths from 'vs/base/common/path';
|
||||||
|
|
||||||
|
export class BrowserWindowConfiguration implements IWindowConfiguration {
|
||||||
|
|
||||||
|
@@ -166,8 +167,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
|
||||||
|
|
||||||
@memoize
|
@memoize
|
||||||
get webviewExternalEndpoint(): string {
|
get webviewExternalEndpoint(): string {
|
||||||
@@ -3010,15 +3060,112 @@ index c94ee4e88c..cce3cf6f13 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
@memoize
|
@memoize
|
||||||
@@ -249,6 +249,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
|
@@ -246,22 +247,38 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
|
||||||
installSourcePath!: string;
|
driverHandle?: string;
|
||||||
|
driverVerbose!: boolean;
|
||||||
|
|
||||||
|
- installSourcePath!: string;
|
||||||
|
+ @memoize
|
||||||
|
+ get installSourcePath(): string { return paths.join(this.userDataPath, 'installSource'); }
|
||||||
|
|
||||||
builtinExtensionsPath!: string;
|
builtinExtensionsPath!: string;
|
||||||
|
|
||||||
|
- globalStorageHome!: string;
|
||||||
|
- workspaceStorageHome!: string;
|
||||||
|
+ @memoize
|
||||||
|
+ get globalStorageHome(): string { return paths.join(this.appSettingsHome.fsPath, 'globalStorage'); }
|
||||||
|
+ @memoize
|
||||||
|
+ get workspaceStorageHome(): string { return paths.join(this.appSettingsHome.fsPath, 'workspaceStorage'); }
|
||||||
|
|
||||||
|
- backupWorkspacesPath!: string;
|
||||||
|
+ @memoize
|
||||||
|
+ get backupWorkspacesPath(): string { return paths.join(this.backupHome.fsPath, 'workspaces.json'); }
|
||||||
|
|
||||||
|
- machineSettingsResource!: URI;
|
||||||
|
+ @memoize
|
||||||
|
+ get machineSettingsResource(): URI { return joinPath(URI.file(paths.join(this.userDataPath, 'Machine')), 'settings.json'); }
|
||||||
|
|
||||||
|
userHome!: string;
|
||||||
|
- userDataPath!: string;
|
||||||
|
+ @memoize
|
||||||
|
+ get userDataPath(): string {
|
||||||
|
+ const dataPath = this.payload?.get("userDataPath");
|
||||||
|
+ if (!dataPath) {
|
||||||
|
+ throw new Error("userDataPath was not provided to environment service");
|
||||||
|
+ }
|
||||||
|
+ return dataPath;
|
||||||
|
+ }
|
||||||
|
appRoot!: string;
|
||||||
|
- appSettingsHome!: URI;
|
||||||
|
+ @memoize
|
||||||
|
+ get appSettingsHome(): URI { return URI.file(paths.join(this.userDataPath, 'User')); }
|
||||||
|
execPath!: string;
|
||||||
|
|
||||||
+ extraExtensionPaths!: string[];
|
+ extraExtensionPaths!: string[];
|
||||||
+ extraBuiltinExtensionPaths!: string[];
|
+ extraBuiltinExtensionPaths!: string[];
|
||||||
|
+
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts
|
||||||
|
index 4728f3e510..2e38804ac2 100644
|
||||||
|
--- a/src/vs/workbench/services/environment/common/environmentService.ts
|
||||||
|
+++ b/src/vs/workbench/services/environment/common/environmentService.ts
|
||||||
|
@@ -24,4 +24,7 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService {
|
||||||
|
readonly webviewExternalEndpoint: string;
|
||||||
|
readonly webviewResourceRoot: string;
|
||||||
|
readonly webviewCspSource: string;
|
||||||
|
+
|
||||||
|
+ readonly extraExtensionPaths: string[]
|
||||||
|
+ readonly extraBuiltinExtensionPaths: string[]
|
||||||
|
}
|
||||||
|
diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts
|
||||||
|
index cfac383e8a..c535d38296 100644
|
||||||
|
--- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts
|
||||||
|
+++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts
|
||||||
|
@@ -153,7 +153,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- return true;
|
||||||
|
+ return false; // NOTE@coder: Don't disable anything by extensionKind.
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts
|
||||||
|
index 9f8c6ac6f5..69b5f36203 100644
|
||||||
|
--- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts
|
||||||
|
+++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts
|
||||||
|
@@ -5,7 +5,7 @@
|
||||||
|
|
||||||
globalStorageHome!: string;
|
import { Event, EventMultiplexer } from 'vs/base/common/event';
|
||||||
workspaceStorageHome!: string;
|
import {
|
||||||
|
- IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, INSTALL_ERROR_NOT_SUPPORTED
|
||||||
|
+ IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService
|
||||||
|
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||||
|
import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||||
|
import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||||
|
@@ -15,7 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||||
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||||
|
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
-import { prefersExecuteOnUI, canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||||
|
+import { prefersExecuteOnUI } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||||
|
import { IProductService } from 'vs/platform/product/common/productService';
|
||||||
|
import { Schemas } from 'vs/base/common/network';
|
||||||
|
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||||
|
@@ -208,11 +208,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||||
|
if (!manifest) {
|
||||||
|
return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name));
|
||||||
|
}
|
||||||
|
- if (!isLanguagePackExtension(manifest) && !canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) {
|
||||||
|
- const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name));
|
||||||
|
- error.name = INSTALL_ERROR_NOT_SUPPORTED;
|
||||||
|
- return Promise.reject(error);
|
||||||
|
- }
|
||||||
|
+ // NOTE@coder: Allow extensions of any kind.
|
||||||
|
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||||
|
}
|
||||||
|
return Promise.reject('No Servers to Install');
|
||||||
diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts
|
diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts
|
||||||
index 5b6a15e820..0f93c896e2 100644
|
index 5b6a15e820..0f93c896e2 100644
|
||||||
--- a/src/vs/workbench/services/extensions/browser/extensionService.ts
|
--- a/src/vs/workbench/services/extensions/browser/extensionService.ts
|
||||||
@@ -3031,6 +3178,19 @@ index 5b6a15e820..0f93c896e2 100644
|
|||||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !canExecuteOnWeb(extension, this._productService, this._configService));
|
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !canExecuteOnWeb(extension, this._productService, this._configService));
|
||||||
this._checkEnableProposedApi(remoteEnv.extensions);
|
this._checkEnableProposedApi(remoteEnv.extensions);
|
||||||
|
|
||||||
|
diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts
|
||||||
|
index 5e09934624..d70f8b5364 100644
|
||||||
|
--- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts
|
||||||
|
+++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts
|
||||||
|
@@ -142,7 +142,7 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter {
|
||||||
|
appLanguage: platform.language,
|
||||||
|
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||||
|
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||||
|
- globalStorageHome: URI.parse('fake:globalStorageHome'), //todo@joh URI.file(this._environmentService.globalStorageHome),
|
||||||
|
+ globalStorageHome: URI.file(this._environmentService.globalStorageHome),
|
||||||
|
userHome: URI.parse('fake:userHome'), //todo@joh URI.file(this._environmentService.userHome),
|
||||||
|
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||||
|
webviewCspSource: this._environmentService.webviewCspSource,
|
||||||
diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
||||||
index 9e8352ac88..22a2d296f9 100644
|
index 9e8352ac88..22a2d296f9 100644
|
||||||
--- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
--- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
||||||
@@ -3046,10 +3206,19 @@ index 9e8352ac88..22a2d296f9 100644
|
|||||||
|
|
||||||
export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] {
|
export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] {
|
||||||
diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
||||||
index 0f35c54431..32fff09b18 100644
|
index 79dd77aeb2..1d93c0f922 100644
|
||||||
--- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
--- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
||||||
+++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
+++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
||||||
@@ -53,12 +53,13 @@ const args = minimist(process.argv.slice(2), {
|
@@ -16,7 +16,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol';
|
||||||
|
import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
||||||
|
import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain';
|
||||||
|
import { VSBuffer } from 'vs/base/common/buffer';
|
||||||
|
-import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
|
||||||
|
+import { IURITransformer, URITransformer } from 'vs/base/common/uriIpc';
|
||||||
|
import { exists } from 'vs/base/node/pfs';
|
||||||
|
import { realpath } from 'vs/base/node/extpath';
|
||||||
|
import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService';
|
||||||
|
@@ -55,12 +55,13 @@ const args = minimist(process.argv.slice(2), {
|
||||||
const Module = require.__$__nodeRequire('module') as any;
|
const Module = require.__$__nodeRequire('module') as any;
|
||||||
const originalLoad = Module._load;
|
const originalLoad = Module._load;
|
||||||
|
|
||||||
@@ -3065,7 +3234,7 @@ index 0f35c54431..32fff09b18 100644
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -131,8 +132,11 @@ function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
@@ -133,8 +134,11 @@ function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
||||||
|
|
||||||
// Wait for rich client to reconnect
|
// Wait for rich client to reconnect
|
||||||
protocol.onSocketClose(() => {
|
protocol.onSocketClose(() => {
|
||||||
@@ -3079,6 +3248,20 @@ index 0f35c54431..32fff09b18 100644
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -307,11 +311,9 @@ export async function startExtensionHostProcess(): Promise<void> {
|
||||||
|
|
||||||
|
// Attempt to load uri transformer
|
||||||
|
let uriTransformer: IURITransformer | null = null;
|
||||||
|
- if (initData.remote.authority && args.uriTransformerPath) {
|
||||||
|
+ if (initData.remote.authority) {
|
||||||
|
try {
|
||||||
|
- const rawURITransformerFactory = <any>require.__$__nodeRequire(args.uriTransformerPath);
|
||||||
|
- const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(initData.remote.authority);
|
||||||
|
- uriTransformer = new URITransformer(rawURITransformer);
|
||||||
|
+ uriTransformer = new URITransformer(initData.remote.authority);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
diff --git a/src/vs/workbench/services/extensions/worker/extHost.services.ts b/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
diff --git a/src/vs/workbench/services/extensions/worker/extHost.services.ts b/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
||||||
index 9056862945..0785d3391d 100644
|
index 9056862945..0785d3391d 100644
|
||||||
--- a/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
--- a/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
||||||
@@ -3165,7 +3348,7 @@ index 99394090da..4891e0fece 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts
|
diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts
|
||||||
index a73f3a3e53..7c4ce1acb8 100644
|
index b9defe4bb9..6bfcc17734 100644
|
||||||
--- a/src/vs/workbench/workbench.web.main.ts
|
--- a/src/vs/workbench/workbench.web.main.ts
|
||||||
+++ b/src/vs/workbench/workbench.web.main.ts
|
+++ b/src/vs/workbench/workbench.web.main.ts
|
||||||
@@ -34,7 +34,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService';
|
@@ -34,7 +34,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService';
|
||||||
@@ -3178,18 +3361,8 @@ index a73f3a3e53..7c4ce1acb8 100644
|
|||||||
import 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
|
import 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
|
||||||
import 'vs/workbench/services/credentials/browser/credentialsService';
|
import 'vs/workbench/services/credentials/browser/credentialsService';
|
||||||
import 'vs/workbench/services/url/browser/urlService';
|
import 'vs/workbench/services/url/browser/urlService';
|
||||||
@@ -119,7 +120,7 @@ import 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.con
|
|
||||||
// Issues
|
|
||||||
import 'vs/workbench/contrib/issue/browser/issue.contribution';
|
|
||||||
|
|
||||||
-// Open In Desktop
|
|
||||||
-import 'vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution';
|
|
||||||
+// Open In Desktop NOTE@coder: remove
|
|
||||||
+// import 'vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution';
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
diff --git a/yarn.lock b/yarn.lock
|
diff --git a/yarn.lock b/yarn.lock
|
||||||
index a820c6344a..7e4f410db9 100644
|
index 07c789ca31..db51202a60 100644
|
||||||
--- a/yarn.lock
|
--- a/yarn.lock
|
||||||
+++ b/yarn.lock
|
+++ b/yarn.lock
|
||||||
@@ -140,6 +140,23 @@
|
@@ -140,6 +140,23 @@
|
||||||
@@ -3216,7 +3389,7 @@ index a820c6344a..7e4f410db9 100644
|
|||||||
"@electron/get@^1.0.1":
|
"@electron/get@^1.0.1":
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd"
|
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd"
|
||||||
@@ -5371,6 +5388,13 @@ jsprim@^1.2.2:
|
@@ -5376,6 +5393,13 @@ jsprim@^1.2.2:
|
||||||
json-schema "0.2.3"
|
json-schema "0.2.3"
|
||||||
verror "1.10.0"
|
verror "1.10.0"
|
||||||
|
|
||||||
@@ -3230,7 +3403,7 @@ index a820c6344a..7e4f410db9 100644
|
|||||||
just-debounce@^1.0.0:
|
just-debounce@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea"
|
resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea"
|
||||||
@@ -6729,6 +6753,11 @@ p-try@^2.0.0:
|
@@ -6739,6 +6763,11 @@ p-try@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
|
||||||
integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
|
integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ yarn vscode
|
|||||||
yarn watch # Visit http://localhost:8080 once completed.
|
yarn watch # Visit http://localhost:8080 once completed.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To develop inside of an isolated docker container:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./ci/dev-image/exec.sh
|
||||||
|
|
||||||
|
root@12345:/code-server# yarn
|
||||||
|
root@12345:/code-server# yarn vscode
|
||||||
|
root@12345:/code-server# yarn watch
|
||||||
|
```
|
||||||
|
|
||||||
Any changes made to the source will be live reloaded.
|
Any changes made to the source will be live reloaded.
|
||||||
|
|
||||||
If changes are made to the patch and you've built previously you must manually
|
If changes are made to the patch and you've built previously you must manually
|
||||||
|
|||||||
71
doc/FAQ.md
71
doc/FAQ.md
@@ -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.
|
||||||
|
|
||||||
@@ -52,6 +52,8 @@ randomly generated password so you can use that. You can set the `PASSWORD` envi
|
|||||||
to use your own instead. If you want to handle authentication yourself, use `--auth none`
|
to use your own instead. If you want to handle authentication yourself, use `--auth none`
|
||||||
to disable password authentication.
|
to disable password authentication.
|
||||||
|
|
||||||
|
**note**: code-server will rate limit password authentication attempts at 2 a minute and 12 an hour.
|
||||||
|
|
||||||
If you want to use external authentication you should handle this with a reverse
|
If you want to use external authentication you should handle this with a reverse
|
||||||
proxy using something like [oauth2_proxy](https://github.com/pusher/oauth2_proxy).
|
proxy using something like [oauth2_proxy](https://github.com/pusher/oauth2_proxy).
|
||||||
|
|
||||||
@@ -65,6 +67,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 +135,44 @@ 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 workspace or directory passed on the command line.
|
||||||
|
4. The last opened workspace or directory.
|
||||||
|
|
||||||
|
## How do I debug issues with code-server?
|
||||||
|
|
||||||
|
First run code-server with at least `debug` logging (or `trace` to be really
|
||||||
|
thorough) by setting the `--log` flag or the `LOG_LEVEL` environment variable.
|
||||||
|
`-vvv` and `--verbose` are aliases for `--log trace`.
|
||||||
|
|
||||||
|
```
|
||||||
|
code-server --log debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Once this is done, replicate the issue you're having then collect logging
|
||||||
|
information from the following places:
|
||||||
|
|
||||||
|
1. stdout.
|
||||||
|
2. The most recently created directory in the `logs` directory (found in the
|
||||||
|
data directory; see below for how to find that).
|
||||||
|
3. The browser console and network tabs.
|
||||||
|
|
||||||
|
Additionally, collecting core dumps (you may need to enable them first) if
|
||||||
|
code-server crashes can be helpful.
|
||||||
|
|
||||||
|
### Where is the data directory?
|
||||||
|
|
||||||
|
If the `XDG_DATA_HOME` environment variable is set the data directory will be
|
||||||
|
`$XDG_DATA_HOME/code-server`. Otherwise the default is:
|
||||||
|
|
||||||
|
1. Linux: `~/.local/share/code-server`.
|
||||||
|
2. Mac: `~/Library/Application\ Support/code-server`.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
Submodule lib/vscode updated: 0ba0ca5295...ff91584411
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "code-server",
|
"name": "code-server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "3.0.2",
|
"version": "3.2.0",
|
||||||
"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",
|
||||||
@@ -24,8 +25,6 @@
|
|||||||
"@types/safe-compare": "^1.1.0",
|
"@types/safe-compare": "^1.1.0",
|
||||||
"@types/semver": "^7.1.0",
|
"@types/semver": "^7.1.0",
|
||||||
"@types/tar-fs": "^1.16.2",
|
"@types/tar-fs": "^1.16.2",
|
||||||
"@types/ssh2": "0.5.39",
|
|
||||||
"@types/ssh2-streams": "^0.1.6",
|
|
||||||
"@types/tar-stream": "^1.6.1",
|
"@types/tar-stream": "^1.6.1",
|
||||||
"@types/ws": "^6.0.4",
|
"@types/ws": "^6.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||||
@@ -52,13 +51,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",
|
||||||
|
"limiter": "^1.1.5",
|
||||||
"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",
|
"tar": "^6.0.1",
|
||||||
"ssh2": "^0.8.7",
|
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"ws": "^7.2.0"
|
"ws": "^7.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"
|
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
/>
|
/>
|
||||||
<title>code-server</title>
|
<title>code-server</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||||
/>
|
/>
|
||||||
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;" />
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
|
/>
|
||||||
<title>{{ERROR_TITLE}} - code-server</title>
|
<title>{{ERROR_TITLE}} - code-server</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"
|
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
/>
|
/>
|
||||||
<title>code-server</title>
|
<title>code-server</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"
|
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
/>
|
/>
|
||||||
<title>code-server login</title>
|
<title>code-server login</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||||
/>
|
/>
|
||||||
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;" />
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
|
/>
|
||||||
<title>code-server</title>
|
<title>code-server</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="font-src 'self'; connect-src ws: wss: 'self' https:; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; manifest-src 'self'; img-src 'self' data: https:;"
|
content="font-src 'self' data:; connect-src ws: wss: 'self' https:; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; manifest-src 'self'; img-src 'self' data: https:;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Disable pinch zooming -->
|
<!-- Disable pinch zooming -->
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
import { field, logger, Logger } from "@coder/logger"
|
|
||||||
import { Emitter } from "../common/emitter"
|
|
||||||
import { generateUuid } from "../common/util"
|
|
||||||
|
|
||||||
const decoder = new TextDecoder("utf8")
|
|
||||||
export const decode = (buffer: string | ArrayBuffer): string => {
|
|
||||||
return typeof buffer !== "string" ? decoder.decode(buffer) : buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A web socket that reconnects itself when it closes. Sending messages while
|
|
||||||
* disconnected will throw an error.
|
|
||||||
*/
|
|
||||||
export class ReconnectingSocket {
|
|
||||||
protected readonly _onMessage = new Emitter<string | ArrayBuffer>()
|
|
||||||
public readonly onMessage = this._onMessage.event
|
|
||||||
protected readonly _onDisconnect = new Emitter<number | undefined>()
|
|
||||||
public readonly onDisconnect = this._onDisconnect.event
|
|
||||||
protected readonly _onClose = new Emitter<number | undefined>()
|
|
||||||
public readonly onClose = this._onClose.event
|
|
||||||
protected readonly _onConnect = new Emitter<void>()
|
|
||||||
public readonly onConnect = this._onConnect.event
|
|
||||||
|
|
||||||
// This helps distinguish messages between sockets.
|
|
||||||
private readonly logger: Logger
|
|
||||||
|
|
||||||
private socket?: WebSocket
|
|
||||||
private connecting?: Promise<void>
|
|
||||||
private closed = false
|
|
||||||
private readonly openTimeout = 10000
|
|
||||||
|
|
||||||
// Every time the socket fails to connect, the retry will be increasingly
|
|
||||||
// delayed up to a maximum.
|
|
||||||
private readonly retryBaseDelay = 1000
|
|
||||||
private readonly retryMaxDelay = 10000
|
|
||||||
private retryDelay?: number
|
|
||||||
private readonly retryDelayFactor = 1.5
|
|
||||||
|
|
||||||
// The socket must be connected for this amount of time before resetting the
|
|
||||||
// retry delay. This prevents rapid retries when the socket does connect but
|
|
||||||
// is closed shortly after.
|
|
||||||
private resetRetryTimeout?: NodeJS.Timeout
|
|
||||||
private readonly resetRetryDelay = 10000
|
|
||||||
|
|
||||||
private _binaryType: typeof WebSocket.prototype.binaryType = "arraybuffer"
|
|
||||||
|
|
||||||
public constructor(private path: string, public readonly id: string = generateUuid(4)) {
|
|
||||||
// On Firefox the socket seems to somehow persist a page reload so the close
|
|
||||||
// event runs and we see "attempting to reconnect".
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.addEventListener("beforeunload", () => this.close())
|
|
||||||
}
|
|
||||||
this.logger = logger.named(this.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public set binaryType(b: typeof WebSocket.prototype.binaryType) {
|
|
||||||
this._binaryType = b
|
|
||||||
if (this.socket) {
|
|
||||||
this.socket.binaryType = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Permanently close the connection. Will not attempt to reconnect. Will
|
|
||||||
* remove event listeners.
|
|
||||||
*/
|
|
||||||
public close(code?: number): void {
|
|
||||||
if (this.closed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
this.logger.info(`closing with code ${code}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.resetRetryTimeout) {
|
|
||||||
clearTimeout(this.resetRetryTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.closed = true
|
|
||||||
|
|
||||||
if (this.socket) {
|
|
||||||
this.socket.close()
|
|
||||||
} else {
|
|
||||||
this._onClose.emit(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
this._onMessage.dispose()
|
|
||||||
this._onDisconnect.dispose()
|
|
||||||
this._onClose.dispose()
|
|
||||||
this._onConnect.dispose()
|
|
||||||
this.logger.debug("disposed handlers")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message on the socket. Logs an error if currently disconnected.
|
|
||||||
*/
|
|
||||||
public send(message: string | ArrayBuffer): void {
|
|
||||||
this.logger.trace(() => ["sending message", field("message", decode(message))])
|
|
||||||
if (!this.socket) {
|
|
||||||
return logger.error("tried to send message on closed socket")
|
|
||||||
}
|
|
||||||
this.socket.send(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to the socket. Can also be called to wait until the connection is
|
|
||||||
* established in the case of disconnections. Multiple calls will be handled
|
|
||||||
* correctly.
|
|
||||||
*/
|
|
||||||
public async connect(): Promise<void> {
|
|
||||||
if (!this.connecting) {
|
|
||||||
this.connecting = new Promise((resolve, reject) => {
|
|
||||||
const tryConnect = (): void => {
|
|
||||||
if (this.closed) {
|
|
||||||
return reject(new Error("disconnected")) // Don't keep trying if we've closed permanently.
|
|
||||||
}
|
|
||||||
if (typeof this.retryDelay === "undefined") {
|
|
||||||
this.retryDelay = 0
|
|
||||||
} else {
|
|
||||||
this.retryDelay = this.retryDelay * this.retryDelayFactor || this.retryBaseDelay
|
|
||||||
if (this.retryDelay > this.retryMaxDelay) {
|
|
||||||
this.retryDelay = this.retryMaxDelay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._connect()
|
|
||||||
.then((socket) => {
|
|
||||||
this.logger.info("connected")
|
|
||||||
this.socket = socket
|
|
||||||
this.socket.binaryType = this._binaryType
|
|
||||||
if (this.resetRetryTimeout) {
|
|
||||||
clearTimeout(this.resetRetryTimeout)
|
|
||||||
}
|
|
||||||
this.resetRetryTimeout = setTimeout(() => (this.retryDelay = undefined), this.resetRetryDelay)
|
|
||||||
this.connecting = undefined
|
|
||||||
this._onConnect.emit()
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.logger.error(`failed to connect: ${error.message}`)
|
|
||||||
tryConnect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
tryConnect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return this.connecting
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _connect(): Promise<WebSocket> {
|
|
||||||
const socket = await new Promise<WebSocket>((resolve, _reject) => {
|
|
||||||
if (this.retryDelay) {
|
|
||||||
this.logger.info(`retrying in ${this.retryDelay}ms...`)
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.logger.info("connecting...", field("path", this.path))
|
|
||||||
const socket = new WebSocket(this.path)
|
|
||||||
|
|
||||||
const reject = (): void => {
|
|
||||||
_reject(new Error("socket closed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
||||||
socket.removeEventListener("open", open)
|
|
||||||
socket.removeEventListener("close", reject)
|
|
||||||
_reject(new Error("timeout"))
|
|
||||||
}, this.openTimeout)
|
|
||||||
|
|
||||||
const open = (): void => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
socket.removeEventListener("close", reject)
|
|
||||||
resolve(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.addEventListener("open", open)
|
|
||||||
socket.addEventListener("close", reject)
|
|
||||||
}, this.retryDelay)
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.addEventListener("message", (event) => {
|
|
||||||
this.logger.trace(() => ["got message", field("message", decode(event.data))])
|
|
||||||
this._onMessage.emit(event.data)
|
|
||||||
})
|
|
||||||
socket.addEventListener("close", (event) => {
|
|
||||||
this.socket = undefined
|
|
||||||
if (!this.closed) {
|
|
||||||
this._onDisconnect.emit(event.code)
|
|
||||||
// It might be closed in the event handler.
|
|
||||||
if (!this.closed) {
|
|
||||||
this.logger.info("connection closed; attempting to reconnect")
|
|
||||||
this.connect()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._onClose.emit(event.code)
|
|
||||||
this.logger.info("connection closed permanently")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return socket
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,8 +42,9 @@ To generate a new patch, **stage all the changes** you want to be included in
|
|||||||
the patch in the VS Code source, then run `yarn patch:generate` in this
|
the patch in the VS Code source, then run `yarn patch:generate` in this
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
Our changes include:
|
Notable changes include:
|
||||||
|
|
||||||
|
- Add our own build file which includes our code and VS Code's web code.
|
||||||
- Allow multiple extension directories (both user and built-in).
|
- Allow multiple extension directories (both user and built-in).
|
||||||
- Modify the loader, websocket, webview, service worker, and asset requests to
|
- Modify the loader, websocket, webview, service worker, and asset requests to
|
||||||
use the URL of the page as a base (and TLS if necessary for the websocket).
|
use the URL of the page as a base (and TLS if necessary for the websocket).
|
||||||
@@ -51,8 +52,8 @@ Our changes include:
|
|||||||
- Make changing the display language work.
|
- Make changing the display language work.
|
||||||
- Make it possible for us to load code on the client.
|
- Make it possible for us to load code on the client.
|
||||||
- Make extensions work in the browser.
|
- Make extensions work in the browser.
|
||||||
|
- Make it possible to install extensions of any kind.
|
||||||
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
||||||
- Make it possible to automatically update the binary.
|
|
||||||
- Add connection type to web socket query parameters.
|
- Add connection type to web socket query parameters.
|
||||||
|
|
||||||
## Future
|
## Future
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
|
import * as limiter from "limiter"
|
||||||
import * as querystring from "querystring"
|
import * as querystring from "querystring"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { AuthType, HttpProvider, HttpResponse, Route } from "../http"
|
import { AuthType, HttpProvider, HttpResponse, Route } from "../http"
|
||||||
@@ -18,7 +19,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) {
|
||||||
@@ -48,6 +49,8 @@ export class LoginHttpProvider extends HttpProvider {
|
|||||||
return this.replaceTemplates(route, response)
|
return this.replaceTemplates(route, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly limiter = new RateLimiter()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try logging in. On failure, show the login page with an error.
|
* Try logging in. On failure, show the login page with an error.
|
||||||
*/
|
*/
|
||||||
@@ -59,6 +62,10 @@ export class LoginHttpProvider extends HttpProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!this.limiter.try()) {
|
||||||
|
throw new Error("Login rate limited!")
|
||||||
|
}
|
||||||
|
|
||||||
const data = await this.getData(request)
|
const data = await this.getData(request)
|
||||||
const payload = data ? querystring.parse(data) : {}
|
const payload = data ? querystring.parse(data) : {}
|
||||||
return await this.login(payload, route, request)
|
return await this.login(payload, route, request)
|
||||||
@@ -108,3 +115,17 @@ export class LoginHttpProvider extends HttpProvider {
|
|||||||
throw new Error("Missing password")
|
throw new Error("Missing password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RateLimiter wraps around the limiter library for logins.
|
||||||
|
// It allows 2 logins every minute and 12 logins every hour.
|
||||||
|
class RateLimiter {
|
||||||
|
private readonly minuteLimiter = new limiter.RateLimiter(2, "minute")
|
||||||
|
private readonly hourLimiter = new limiter.RateLimiter(12, "hour")
|
||||||
|
|
||||||
|
public try(): boolean {
|
||||||
|
if (this.minuteLimiter.tryRemoveTokens(1)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return this.hourLimiter.tryRemoveTokens(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
43
src/node/app/proxy.ts
Normal file
43
src/node/app/proxy.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +87,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
public async getRoot(
|
public async getRoot(
|
||||||
route: Route,
|
route: Route,
|
||||||
request: http.IncomingMessage,
|
request: http.IncomingMessage,
|
||||||
appliedUpdate?: string,
|
errorOrUpdate?: Update | Error,
|
||||||
error?: Error,
|
|
||||||
): Promise<HttpResponse> {
|
): Promise<HttpResponse> {
|
||||||
if (request.headers["content-type"] === "application/json") {
|
if (request.headers["content-type"] === "application/json") {
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
@@ -108,8 +107,13 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
}
|
}
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
|
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
|
||||||
response.content = response.content
|
response.content = response.content
|
||||||
.replace(/{{UPDATE_STATUS}}/, appliedUpdate ? `Updated to ${appliedUpdate}` : await this.getUpdateHtml())
|
.replace(
|
||||||
.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
/{{UPDATE_STATUS}}/,
|
||||||
|
errorOrUpdate && !(errorOrUpdate instanceof Error)
|
||||||
|
? `Updated to ${errorOrUpdate.version}`
|
||||||
|
: await this.getUpdateHtml(),
|
||||||
|
)
|
||||||
|
.replace(/{{ERROR}}/, errorOrUpdate instanceof Error ? `<div class="error">${errorOrUpdate.message}</div>` : "")
|
||||||
return this.replaceTemplates(route, response)
|
return this.replaceTemplates(route, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,11 +190,16 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
const update = await this.getUpdate()
|
const update = await this.getUpdate()
|
||||||
if (!this.isLatestVersion(update)) {
|
if (!this.isLatestVersion(update)) {
|
||||||
await this.downloadAndApplyUpdate(update)
|
await this.downloadAndApplyUpdate(update)
|
||||||
return this.getRoot(route, request, update.version)
|
return this.getRoot(route, request, update)
|
||||||
}
|
}
|
||||||
return this.getRoot(route, request)
|
return this.getRoot(route, request)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return this.getRoot(route, request, undefined, error)
|
// For JSON requests propagate the error. Otherwise catch it so we can
|
||||||
|
// show the error inline with the update button instead of an error page.
|
||||||
|
if (request.headers["content-type"] === "application/json") {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return this.getRoot(route, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +230,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)
|
||||||
|
|
||||||
@@ -357,7 +371,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
||||||
return reject(new Error(`${response.statusCode || "500"}`))
|
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(response)
|
resolve(response)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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 +127,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 +167,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 +205,31 @@ 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) {
|
const isFile = async (path: string): Promise<boolean> => {
|
||||||
const startPath = startPaths[i]
|
|
||||||
if (!startPath) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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 {
|
try {
|
||||||
if (!uri.pathname) {
|
const stat = await fs.stat(path)
|
||||||
throw new Error(`${paths[j]} is not a valid URL`)
|
return stat.isFile()
|
||||||
}
|
|
||||||
const stat = await fs.stat(uri.pathname)
|
|
||||||
if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
|
|
||||||
return {
|
|
||||||
url: url.format({
|
|
||||||
protocol: uri.protocol || "vscode-remote",
|
|
||||||
hostname: remoteAuthority.split(":")[0],
|
|
||||||
port: remoteAuthority.split(":")[1],
|
|
||||||
pathname: uri.pathname,
|
|
||||||
slashes: true,
|
|
||||||
}),
|
|
||||||
workspace: !stat.isDirectory(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(error.message)
|
logger.warn(error.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < startPaths.length; ++i) {
|
||||||
|
const startPath = startPaths[i]
|
||||||
|
const url =
|
||||||
|
startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
|
||||||
|
if (startPath && url) {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
// The only time `workspace` is undefined is for the command-line
|
||||||
|
// argument, in which case it's a path (not a URL) so we can stat it
|
||||||
|
// without having to parse it.
|
||||||
|
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ export interface Args extends VsArgs {
|
|||||||
log?: LogLevel
|
log?: LogLevel
|
||||||
readonly open?: boolean
|
readonly open?: boolean
|
||||||
readonly port?: number
|
readonly port?: number
|
||||||
|
readonly "bind-addr"?: string
|
||||||
readonly socket?: string
|
readonly socket?: string
|
||||||
readonly "ssh-host-key"?: string
|
|
||||||
readonly "disable-ssh"?: boolean
|
|
||||||
readonly version?: boolean
|
readonly version?: boolean
|
||||||
readonly force?: boolean
|
readonly force?: boolean
|
||||||
readonly "list-extensions"?: boolean
|
readonly "list-extensions"?: boolean
|
||||||
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[]
|
||||||
}
|
}
|
||||||
@@ -89,18 +89,20 @@ const options: Options<Required<Args>> = {
|
|||||||
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||||
"disable-updates": { type: "boolean", description: "Disable automatic updates." },
|
"disable-updates": { type: "boolean", description: "Disable automatic updates." },
|
||||||
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
||||||
host: { type: "string", description: "Host for the HTTP server." },
|
|
||||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||||
json: { type: "boolean" },
|
json: { type: "boolean" },
|
||||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||||
port: { type: "number", description: "Port for the HTTP server." },
|
|
||||||
socket: { type: "string", path: true, description: "Path to a socket (host and port will be ignored)." },
|
"bind-addr": { type: "string", description: "Address to bind to in host:port." },
|
||||||
|
|
||||||
|
// These two have been deprecated by bindAddr.
|
||||||
|
host: { type: "string", description: "" },
|
||||||
|
port: { type: "number", description: "" },
|
||||||
|
|
||||||
|
socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." },
|
||||||
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", description: "Disable the SSH server." },
|
|
||||||
"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." },
|
||||||
"builtin-extensions-dir": { type: "string", path: true },
|
"builtin-extensions-dir": { type: "string", path: true },
|
||||||
@@ -111,6 +113,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 },
|
||||||
@@ -210,7 +213,7 @@ export const parse = (argv: string[]): Args => {
|
|||||||
;(args[key] as OptionalString) = new OptionalString(value)
|
;(args[key] as OptionalString) = new OptionalString(value)
|
||||||
break
|
break
|
||||||
default: {
|
default: {
|
||||||
if (!Object.values(option.type).find((v) => v === value)) {
|
if (!Object.values(option.type).includes(value)) {
|
||||||
throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
||||||
}
|
}
|
||||||
;(args[key] as string) = value
|
;(args[key] as string) = value
|
||||||
@@ -227,20 +230,26 @@ export const parse = (argv: string[]): Args => {
|
|||||||
|
|
||||||
logger.debug("parsed command line", field("args", args))
|
logger.debug("parsed command line", field("args", args))
|
||||||
|
|
||||||
// Ensure the environment variable and the flag are synced up. The flag takes
|
// --verbose takes priority over --log and --log takes priority over the
|
||||||
// priority over the environment variable.
|
// environment variable.
|
||||||
if (args.log === LogLevel.Trace || process.env.LOG_LEVEL === LogLevel.Trace || args.verbose) {
|
if (args.verbose) {
|
||||||
args.log = process.env.LOG_LEVEL = LogLevel.Trace
|
args.log = LogLevel.Trace
|
||||||
args.verbose = true
|
} else if (
|
||||||
} else if (!args.log && process.env.LOG_LEVEL) {
|
!args.log &&
|
||||||
|
process.env.LOG_LEVEL &&
|
||||||
|
Object.values(LogLevel).includes(process.env.LOG_LEVEL as LogLevel)
|
||||||
|
) {
|
||||||
args.log = process.env.LOG_LEVEL as LogLevel
|
args.log = process.env.LOG_LEVEL as LogLevel
|
||||||
} else if (args.log) {
|
|
||||||
process.env.LOG_LEVEL = args.log
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync --log, --verbose, the environment variable, and logger level.
|
||||||
|
if (args.log) {
|
||||||
|
process.env.LOG_LEVEL = args.log
|
||||||
|
}
|
||||||
switch (args.log) {
|
switch (args.log) {
|
||||||
case LogLevel.Trace:
|
case LogLevel.Trace:
|
||||||
logger.level = Level.Trace
|
logger.level = Level.Trace
|
||||||
|
args.verbose = true
|
||||||
break
|
break
|
||||||
case LogLevel.Debug:
|
case LogLevel.Debug:
|
||||||
logger.level = Level.Debug
|
logger.level = Level.Debug
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ 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 { generateCertificate, generatePassword, 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) => {
|
process.on("uncaughtException", (error) => {
|
||||||
@@ -35,43 +35,40 @@ 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 host = args.host
|
||||||
|
let port = args.port
|
||||||
|
if (args["bind-addr"] !== undefined) {
|
||||||
|
const u = new URL(`http://${args["bind-addr"]}`)
|
||||||
|
host = u.hostname
|
||||||
|
port = parseInt(u.port, 10)
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn the main HTTP server.
|
// Spawn the main HTTP server.
|
||||||
const options = {
|
const options: HttpServerOptions = {
|
||||||
auth,
|
auth,
|
||||||
cert: args.cert ? args.cert.value : undefined,
|
|
||||||
certKey: args["cert-key"],
|
|
||||||
sshHostKey: args["ssh-host-key"],
|
|
||||||
commit,
|
commit,
|
||||||
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
|
host: host || (args.auth === AuthType.Password && 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: port !== undefined ? 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)
|
||||||
@@ -84,7 +81,7 @@ const main = async (args: Args): Promise<void> => {
|
|||||||
|
|
||||||
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`")
|
||||||
}
|
}
|
||||||
@@ -96,7 +93,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`,
|
||||||
)
|
)
|
||||||
@@ -104,24 +101,13 @@ const main = async (args: Args): Promise<void> => {
|
|||||||
logger.info(" - Not serving HTTPS")
|
logger.info(" - Not serving HTTPS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (httpServer.proxyDomains.size > 0) {
|
||||||
|
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
||||||
|
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||||
|
|
||||||
let sshPort: number | undefined
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof sshPort !== "undefined") {
|
|
||||||
logger.info(`SSH server listening on localhost:${sshPort}`)
|
|
||||||
} else {
|
|
||||||
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")
|
||||||
|
|||||||
250
src/node/http.ts
250
src/node/http.ts
@@ -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 {
|
||||||
@@ -525,9 +600,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)}`,
|
||||||
|
this.getCookieDomain(request.headers.host || ""),
|
||||||
// "HttpOnly",
|
// "HttpOnly",
|
||||||
"SameSite=strict",
|
"SameSite=lax",
|
||||||
].join(";"),
|
]
|
||||||
|
.filter((l) => !!l)
|
||||||
|
.join(";"),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...payload.headers,
|
...payload.headers,
|
||||||
@@ -547,25 +625,40 @@ 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
|
||||||
const payload = await route.provider.getErrorRoot(route, code, code, e.message)
|
logger.debug("Request error", field("url", request.url), field("code", code))
|
||||||
|
if (code >= HttpCode.ServerError) {
|
||||||
|
logger.error(error.stack)
|
||||||
|
}
|
||||||
|
if (request.headers["content-type"] === "application/json") {
|
||||||
write({
|
write({
|
||||||
code,
|
code,
|
||||||
...payload,
|
content: {
|
||||||
|
error: e.message,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
write({
|
||||||
|
code,
|
||||||
|
...(await route.provider.getErrorRoot(route, code, code, e.message)),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +718,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 +747,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 +769,125 @@ 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 value that should be used for setting a cookie domain. This will
|
||||||
|
* allow the user to authenticate only once. This will use the highest level
|
||||||
|
* domain (e.g. `coder.com` over `test.coder.com` if both are specified).
|
||||||
|
*/
|
||||||
|
private getCookieDomain(host: string): string | undefined {
|
||||||
|
const idx = host.lastIndexOf(":")
|
||||||
|
host = idx !== -1 ? host.substring(0, idx) : host
|
||||||
|
if (
|
||||||
|
// Might be blank/missing, so there's nothing more to do.
|
||||||
|
!host ||
|
||||||
|
// IP addresses can't have subdomains so there's no value in setting the
|
||||||
|
// domain for them. Assume anything with a : is ipv6 (valid domain name
|
||||||
|
// characters are alphanumeric or dashes).
|
||||||
|
host.includes(":") ||
|
||||||
|
// Assume anything entirely numbers and dots is ipv4 (currently tlds
|
||||||
|
// cannot be entirely numbers).
|
||||||
|
!/[^0-9.]/.test(host) ||
|
||||||
|
// localhost subdomains don't seem to work at all (browser bug?).
|
||||||
|
host.endsWith(".localhost") ||
|
||||||
|
// It might be localhost (or an IP, see above) if it's a proxy and it
|
||||||
|
// isn't setting the host header to match the access domain.
|
||||||
|
host === "localhost"
|
||||||
|
) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
this.proxyDomains.forEach((domain) => {
|
||||||
|
if (host.endsWith(domain) && domain.length < host.length) {
|
||||||
|
host = domain
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return host ? `Domain=${host}` : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a response if the request should be proxied. Anything that ends in a
|
||||||
|
* proxy domain and has a *single* subdomain should be proxied. Anything else
|
||||||
|
* should return `undefined` and will be handled as normal.
|
||||||
|
*
|
||||||
|
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
||||||
|
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
||||||
|
*/
|
||||||
|
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||||
|
// Split into parts.
|
||||||
|
const host = request.headers.host || ""
|
||||||
|
const idx = host.indexOf(":")
|
||||||
|
const domain = idx !== -1 ? host.substring(0, idx) : host
|
||||||
|
const parts = domain.split(".")
|
||||||
|
|
||||||
|
// There must be an exact match.
|
||||||
|
const port = parts.shift()
|
||||||
|
const proxyDomain = parts.join(".")
|
||||||
|
if (!port || !this.proxyDomains.has(proxyDomain)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
proxy: {
|
||||||
|
port,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import * as http from "http"
|
|
||||||
import * as net from "net"
|
|
||||||
import * as ssh from "ssh2"
|
|
||||||
import * as ws from "ws"
|
|
||||||
import * as fs from "fs"
|
|
||||||
import { logger } from "@coder/logger"
|
|
||||||
import safeCompare from "safe-compare"
|
|
||||||
import { HttpProvider, HttpResponse, HttpProviderOptions, Route } from "../http"
|
|
||||||
import { HttpCode } from "../../common/http"
|
|
||||||
import { forwardSshPort, fillSshSession } from "./ssh"
|
|
||||||
import { hash } from "../util"
|
|
||||||
|
|
||||||
export class SshProvider extends HttpProvider {
|
|
||||||
private readonly wss = new ws.Server({ noServer: true })
|
|
||||||
private sshServer: ssh.Server
|
|
||||||
|
|
||||||
public constructor(options: HttpProviderOptions, hostKeyPath: string) {
|
|
||||||
super(options)
|
|
||||||
const hostKey = fs.readFileSync(hostKeyPath)
|
|
||||||
this.sshServer = new ssh.Server({ hostKeys: [hostKey] }, this.handleSsh)
|
|
||||||
|
|
||||||
this.sshServer.on("error", (err) => {
|
|
||||||
logger.trace(`SSH server error: ${err.stack}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async listen(): Promise<number> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.sshServer.once("error", reject)
|
|
||||||
this.sshServer.listen(() => {
|
|
||||||
resolve(this.sshServer.address().port)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(): Promise<HttpResponse> {
|
|
||||||
// SSH has no HTTP endpoints
|
|
||||||
return { code: HttpCode.NotFound }
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleWebSocket(
|
|
||||||
_route: Route,
|
|
||||||
request: http.IncomingMessage,
|
|
||||||
socket: net.Socket,
|
|
||||||
head: Buffer,
|
|
||||||
): Promise<void> {
|
|
||||||
// Create a fake websocket to the sshServer
|
|
||||||
const sshSocket = net.connect(this.sshServer.address().port, "localhost")
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
||||||
// Send SSH data to WS as compressed binary
|
|
||||||
sshSocket.on("data", (data) => {
|
|
||||||
ws.send(data, {
|
|
||||||
binary: true,
|
|
||||||
compress: true,
|
|
||||||
fin: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Send WS data to SSH as buffer
|
|
||||||
ws.on("message", (msg) => {
|
|
||||||
// Buffer.from is cool with all types, but casting as string keeps typing simple
|
|
||||||
sshSocket.write(Buffer.from(msg as string))
|
|
||||||
})
|
|
||||||
|
|
||||||
ws.on("error", (err) => {
|
|
||||||
logger.error(`SSH websocket error: ${err.stack}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine how to handle incoming SSH connections.
|
|
||||||
*/
|
|
||||||
private handleSsh = (client: ssh.Connection, info: ssh.ClientInfo): void => {
|
|
||||||
logger.debug(`Incoming SSH connection from ${info.ip}`)
|
|
||||||
client.on("authentication", (ctx) => {
|
|
||||||
// Allow any auth to go through if we have no password
|
|
||||||
if (!this.options.password) {
|
|
||||||
return ctx.accept()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise require the same password as code-server
|
|
||||||
if (ctx.method === "password") {
|
|
||||||
if (
|
|
||||||
safeCompare(this.options.password, hash(ctx.password)) ||
|
|
||||||
safeCompare(this.options.password, ctx.password)
|
|
||||||
) {
|
|
||||||
return ctx.accept()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject, letting them know that password is the only method we allow
|
|
||||||
ctx.reject(["password"])
|
|
||||||
})
|
|
||||||
client.on("tcpip", forwardSshPort)
|
|
||||||
client.on("session", fillSshSession)
|
|
||||||
client.on("error", (err) => {
|
|
||||||
// Don't bother logging Keepalive errors, they probably just disconnected
|
|
||||||
if (err.message === "Keepalive timeout") {
|
|
||||||
return logger.debug("SSH client keepalive timeout")
|
|
||||||
}
|
|
||||||
logger.error(`SSH client error: ${err.stack}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides utilities for handling SSH connections
|
|
||||||
*/
|
|
||||||
import * as fs from "fs"
|
|
||||||
import * as path from "path"
|
|
||||||
import * as ssh from "ssh2"
|
|
||||||
import { FileEntry, SFTPStream } from "ssh2-streams"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills out all the functionality of SFTP using fs.
|
|
||||||
*/
|
|
||||||
export function fillSftpStream(accept: () => SFTPStream): void {
|
|
||||||
const sftp = accept()
|
|
||||||
|
|
||||||
let oid = 0
|
|
||||||
const fds: { [key: number]: boolean } = {}
|
|
||||||
const ods: {
|
|
||||||
[key: number]: {
|
|
||||||
path: string
|
|
||||||
read: boolean
|
|
||||||
}
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
const sftpStatus = (reqID: number, err?: NodeJS.ErrnoException | null): boolean => {
|
|
||||||
let code = ssh.SFTP_STATUS_CODE.OK
|
|
||||||
if (err) {
|
|
||||||
if (err.code === "EACCES") {
|
|
||||||
code = ssh.SFTP_STATUS_CODE.PERMISSION_DENIED
|
|
||||||
} else if (err.code === "ENOENT") {
|
|
||||||
code = ssh.SFTP_STATUS_CODE.NO_SUCH_FILE
|
|
||||||
} else {
|
|
||||||
code = ssh.SFTP_STATUS_CODE.FAILURE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sftp.status(reqID, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
sftp.on("OPEN", (reqID, filename) => {
|
|
||||||
fs.open(filename, "w", (err, fd) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
fds[fd] = true
|
|
||||||
const buf = Buffer.alloc(4)
|
|
||||||
buf.writeUInt32BE(fd, 0)
|
|
||||||
return sftp.handle(reqID, buf)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("OPENDIR", (reqID, path) => {
|
|
||||||
const buf = Buffer.alloc(4)
|
|
||||||
const id = oid++
|
|
||||||
buf.writeUInt32BE(id, 0)
|
|
||||||
ods[id] = {
|
|
||||||
path,
|
|
||||||
read: false,
|
|
||||||
}
|
|
||||||
sftp.handle(reqID, buf)
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("READDIR", (reqID, handle) => {
|
|
||||||
const od = handle.readUInt32BE(0)
|
|
||||||
if (!ods[od]) {
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
|
||||||
}
|
|
||||||
if (ods[od].read) {
|
|
||||||
sftp.status(reqID, ssh.SFTP_STATUS_CODE.EOF)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return fs.readdir(ods[od].path, (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
return Promise.all(
|
|
||||||
files.map((f) => {
|
|
||||||
return new Promise<FileEntry>((resolve, reject) => {
|
|
||||||
const fullPath = path.join(ods[od].path, f)
|
|
||||||
fs.stat(fullPath, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
filename: f,
|
|
||||||
longname: fullPath,
|
|
||||||
attrs: {
|
|
||||||
atime: stats.atimeMs,
|
|
||||||
gid: stats.gid,
|
|
||||||
mode: stats.mode,
|
|
||||||
size: stats.size,
|
|
||||||
mtime: stats.mtimeMs,
|
|
||||||
uid: stats.uid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then((files) => {
|
|
||||||
sftp.name(reqID, files)
|
|
||||||
ods[od].read = true
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
sftp.status(reqID, ssh.SFTP_STATUS_CODE.FAILURE)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("WRITE", (reqID, handle, offset, data) => {
|
|
||||||
const fd = handle.readUInt32BE(0)
|
|
||||||
if (!fds[fd]) {
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
|
||||||
}
|
|
||||||
return fs.write(fd, data, offset, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("CLOSE", (reqID, handle) => {
|
|
||||||
const fd = handle.readUInt32BE(0)
|
|
||||||
if (!fds[fd]) {
|
|
||||||
if (ods[fd]) {
|
|
||||||
delete ods[fd]
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.OK)
|
|
||||||
}
|
|
||||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
|
||||||
}
|
|
||||||
return fs.close(fd, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("STAT", (reqID, path) => {
|
|
||||||
fs.stat(path, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
return sftp.attrs(reqID, {
|
|
||||||
atime: stats.atime.getTime(),
|
|
||||||
gid: stats.gid,
|
|
||||||
mode: stats.mode,
|
|
||||||
mtime: stats.mtime.getTime(),
|
|
||||||
size: stats.size,
|
|
||||||
uid: stats.uid,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("MKDIR", (reqID, path) => {
|
|
||||||
fs.mkdir(path, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("LSTAT", (reqID, path) => {
|
|
||||||
fs.lstat(path, (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
return sftpStatus(reqID, err)
|
|
||||||
}
|
|
||||||
return sftp.attrs(reqID, {
|
|
||||||
atime: stats.atimeMs,
|
|
||||||
gid: stats.gid,
|
|
||||||
mode: stats.mode,
|
|
||||||
mtime: stats.mtimeMs,
|
|
||||||
size: stats.size,
|
|
||||||
uid: stats.uid,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("REMOVE", (reqID, path) => {
|
|
||||||
fs.unlink(path, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("RMDIR", (reqID, path) => {
|
|
||||||
fs.rmdir(path, (err) => sftpStatus(reqID, err))
|
|
||||||
})
|
|
||||||
|
|
||||||
sftp.on("REALPATH", (reqID, path) => {
|
|
||||||
fs.realpath(path, (pathErr, resolved) => {
|
|
||||||
if (pathErr) {
|
|
||||||
return sftpStatus(reqID, pathErr)
|
|
||||||
}
|
|
||||||
fs.stat(path, (statErr, stat) => {
|
|
||||||
if (statErr) {
|
|
||||||
return sftpStatus(reqID, statErr)
|
|
||||||
}
|
|
||||||
sftp.name(reqID, [
|
|
||||||
{
|
|
||||||
filename: resolved,
|
|
||||||
longname: resolved,
|
|
||||||
attrs: {
|
|
||||||
mode: stat.mode,
|
|
||||||
uid: stat.uid,
|
|
||||||
gid: stat.gid,
|
|
||||||
size: stat.size,
|
|
||||||
atime: stat.atime.getTime(),
|
|
||||||
mtime: stat.mtime.getTime(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
return
|
|
||||||
})
|
|
||||||
return
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Provides utilities for handling SSH connections
|
|
||||||
*/
|
|
||||||
import * as net from "net"
|
|
||||||
import * as cp from "child_process"
|
|
||||||
import * as ssh from "ssh2"
|
|
||||||
import * as nodePty from "node-pty"
|
|
||||||
import { fillSftpStream } from "./sftp"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills out all of the functionality of SSH using node equivalents.
|
|
||||||
*/
|
|
||||||
export function fillSshSession(accept: () => ssh.Session): void {
|
|
||||||
let pty: nodePty.IPty | undefined
|
|
||||||
let activeProcess: cp.ChildProcess
|
|
||||||
let ptyInfo: ssh.PseudoTtyInfo | undefined
|
|
||||||
const env: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
const session = accept()
|
|
||||||
|
|
||||||
// Run a command, stream back the data
|
|
||||||
const cmd = (command: string, channel: ssh.ServerChannel): void => {
|
|
||||||
if (ptyInfo) {
|
|
||||||
// Remove undefined and project env vars
|
|
||||||
// keysToRemove taken from sanitizeProcessEnvironment
|
|
||||||
const keysToRemove = [/^ELECTRON_.+$/, /^GOOGLE_API_KEY$/, /^VSCODE_.+$/, /^SNAP(|_.*)$/]
|
|
||||||
const env = Object.keys(process.env).reduce((prev, k) => {
|
|
||||||
if (process.env[k] === undefined) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
const val = process.env[k] as string
|
|
||||||
if (keysToRemove.find((rx) => val.search(rx))) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
prev[k] = val
|
|
||||||
return prev
|
|
||||||
}, {} as { [key: string]: string })
|
|
||||||
|
|
||||||
pty = nodePty.spawn(command, [], {
|
|
||||||
cols: ptyInfo.cols,
|
|
||||||
rows: ptyInfo.rows,
|
|
||||||
env,
|
|
||||||
})
|
|
||||||
pty.onData((d) => channel.write(d))
|
|
||||||
pty.on("exit", (exitCode) => {
|
|
||||||
channel.exit(exitCode)
|
|
||||||
channel.close()
|
|
||||||
})
|
|
||||||
channel.on("data", (d: string) => pty && pty.write(d))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const proc = cp.spawn(command, { shell: true })
|
|
||||||
proc.stdout.on("data", (d) => channel.stdout.write(d))
|
|
||||||
proc.stderr.on("data", (d) => channel.stderr.write(d))
|
|
||||||
proc.on("exit", (exitCode) => {
|
|
||||||
channel.exit(exitCode || 0)
|
|
||||||
channel.close()
|
|
||||||
})
|
|
||||||
channel.stdin.on("data", (d: unknown) => proc.stdin.write(d))
|
|
||||||
channel.stdin.on("close", () => proc.stdin.end())
|
|
||||||
}
|
|
||||||
|
|
||||||
session.on("pty", (accept, _, info) => {
|
|
||||||
ptyInfo = info
|
|
||||||
accept && accept()
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("shell", (accept) => {
|
|
||||||
cmd(process.env.SHELL || "/usr/bin/env bash", accept())
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("exec", (accept, _, info) => {
|
|
||||||
cmd(info.command, accept())
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("sftp", fillSftpStream)
|
|
||||||
|
|
||||||
session.on("signal", (accept, _, info) => {
|
|
||||||
accept && accept()
|
|
||||||
process.kill((pty || activeProcess).pid, info.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("env", (accept, _reject, info) => {
|
|
||||||
accept && accept()
|
|
||||||
env[info.key] = info.value
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("auth-agent", (accept) => {
|
|
||||||
accept()
|
|
||||||
})
|
|
||||||
|
|
||||||
session.on("window-change", (accept, reject, info) => {
|
|
||||||
if (pty) {
|
|
||||||
pty.resize(info.cols, info.rows)
|
|
||||||
accept && accept()
|
|
||||||
} else {
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pipes a requested port over SSH
|
|
||||||
*/
|
|
||||||
export function forwardSshPort(
|
|
||||||
accept: () => ssh.ServerChannel,
|
|
||||||
reject: () => boolean,
|
|
||||||
info: ssh.TcpipRequestInfo,
|
|
||||||
): void {
|
|
||||||
const fwdSocket = net.createConnection(info.destPort, info.destIP)
|
|
||||||
fwdSocket.on("error", () => reject())
|
|
||||||
fwdSocket.on("connect", () => {
|
|
||||||
const channel = accept()
|
|
||||||
channel.pipe(fwdSocket)
|
|
||||||
channel.on("close", () => fwdSocket.end())
|
|
||||||
fwdSocket.pipe(channel)
|
|
||||||
fwdSocket.on("close", () => channel.close())
|
|
||||||
fwdSocket.on("error", () => channel.end())
|
|
||||||
fwdSocket.on("end", () => channel.end())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -44,12 +44,6 @@ export const generateCertificate = async (): Promise<{ cert: string; certKey: st
|
|||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateSshHostKey = async (): Promise<string> => {
|
|
||||||
// Just reuse the SSL cert as the SSH host key
|
|
||||||
const { certKey } = await generateCertificate()
|
|
||||||
return certKey
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generatePassword = async (length = 24): Promise<string> => {
|
export const generatePassword = async (length = 24): Promise<string> => {
|
||||||
const buffer = Buffer.alloc(Math.ceil(length / 2))
|
const buffer = Buffer.alloc(Math.ceil(length / 2))
|
||||||
await util.promisify(crypto.randomFill)(buffer)
|
await util.promisify(crypto.randomFill)(buffer)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { logger, Level } from "@coder/logger"
|
||||||
import * as assert from "assert"
|
import * as assert from "assert"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { parse } from "../src/node/cli"
|
import { parse } from "../src/node/cli"
|
||||||
@@ -8,17 +9,21 @@ describe("cli", () => {
|
|||||||
delete process.env.LOG_LEVEL
|
delete process.env.LOG_LEVEL
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should set defaults", () => {
|
// The parser will always fill these out.
|
||||||
assert.deepEqual(parse([]), {
|
const defaults = {
|
||||||
_: [],
|
_: [],
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||||
"user-data-dir": xdgLocalDir,
|
"user-data-dir": xdgLocalDir,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
it("should set defaults", () => {
|
||||||
|
assert.deepEqual(parse([]), defaults)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should parse all available options", () => {
|
it("should parse all available options", () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
parse([
|
parse([
|
||||||
|
"--bind-addr=192.169.0.1:8080",
|
||||||
"--auth",
|
"--auth",
|
||||||
"none",
|
"none",
|
||||||
"--extensions-dir",
|
"--extensions-dir",
|
||||||
@@ -74,42 +79,71 @@ describe("cli", () => {
|
|||||||
"user-data-dir": path.resolve("bar"),
|
"user-data-dir": path.resolve("bar"),
|
||||||
verbose: true,
|
verbose: true,
|
||||||
version: true,
|
version: true,
|
||||||
|
"bind-addr": "192.169.0.1:8080",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should work with short options", () => {
|
it("should work with short options", () => {
|
||||||
assert.deepEqual(parse(["-vvv", "-v"]), {
|
assert.deepEqual(parse(["-vvv", "-v"]), {
|
||||||
_: [],
|
...defaults,
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
|
||||||
"user-data-dir": xdgLocalDir,
|
|
||||||
log: "trace",
|
log: "trace",
|
||||||
verbose: true,
|
verbose: true,
|
||||||
version: true,
|
version: true,
|
||||||
})
|
})
|
||||||
assert.equal(process.env.LOG_LEVEL, "trace")
|
assert.equal(process.env.LOG_LEVEL, "trace")
|
||||||
|
assert.equal(logger.level, Level.Trace)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should use log level env var", () => {
|
it("should use log level env var", () => {
|
||||||
process.env.LOG_LEVEL = "debug"
|
process.env.LOG_LEVEL = "debug"
|
||||||
assert.deepEqual(parse([]), {
|
assert.deepEqual(parse([]), {
|
||||||
_: [],
|
...defaults,
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
|
||||||
"user-data-dir": xdgLocalDir,
|
|
||||||
log: "debug",
|
log: "debug",
|
||||||
})
|
})
|
||||||
assert.equal(process.env.LOG_LEVEL, "debug")
|
assert.equal(process.env.LOG_LEVEL, "debug")
|
||||||
|
assert.equal(logger.level, Level.Debug)
|
||||||
|
|
||||||
|
process.env.LOG_LEVEL = "trace"
|
||||||
|
assert.deepEqual(parse([]), {
|
||||||
|
...defaults,
|
||||||
|
log: "trace",
|
||||||
|
verbose: true,
|
||||||
|
})
|
||||||
|
assert.equal(process.env.LOG_LEVEL, "trace")
|
||||||
|
assert.equal(logger.level, Level.Trace)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prefer --log to env var", () => {
|
it("should prefer --log to env var and --verbose to --log", () => {
|
||||||
process.env.LOG_LEVEL = "debug"
|
process.env.LOG_LEVEL = "debug"
|
||||||
assert.deepEqual(parse(["--log", "info"]), {
|
assert.deepEqual(parse(["--log", "info"]), {
|
||||||
_: [],
|
...defaults,
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
|
||||||
"user-data-dir": xdgLocalDir,
|
|
||||||
log: "info",
|
log: "info",
|
||||||
})
|
})
|
||||||
assert.equal(process.env.LOG_LEVEL, "info")
|
assert.equal(process.env.LOG_LEVEL, "info")
|
||||||
|
assert.equal(logger.level, Level.Info)
|
||||||
|
|
||||||
|
process.env.LOG_LEVEL = "trace"
|
||||||
|
assert.deepEqual(parse(["--log", "info"]), {
|
||||||
|
...defaults,
|
||||||
|
log: "info",
|
||||||
|
})
|
||||||
|
assert.equal(process.env.LOG_LEVEL, "info")
|
||||||
|
assert.equal(logger.level, Level.Info)
|
||||||
|
|
||||||
|
process.env.LOG_LEVEL = "warn"
|
||||||
|
assert.deepEqual(parse(["--log", "info", "--verbose"]), {
|
||||||
|
...defaults,
|
||||||
|
log: "trace",
|
||||||
|
verbose: true,
|
||||||
|
})
|
||||||
|
assert.equal(process.env.LOG_LEVEL, "trace")
|
||||||
|
assert.equal(logger.level, Level.Trace)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should ignore invalid log level env var", () => {
|
||||||
|
process.env.LOG_LEVEL = "bogus"
|
||||||
|
assert.deepEqual(parse([]), defaults)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should error if value isn't provided", () => {
|
it("should error if value isn't provided", () => {
|
||||||
@@ -117,6 +151,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(["--bind-addr"]), /--bind-addr requires a value/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should error if value is invalid", () => {
|
it("should error if value is invalid", () => {
|
||||||
@@ -131,9 +166,7 @@ describe("cli", () => {
|
|||||||
|
|
||||||
it("should not error if the value is optional", () => {
|
it("should not error if the value is optional", () => {
|
||||||
assert.deepEqual(parse(["--cert"]), {
|
assert.deepEqual(parse(["--cert"]), {
|
||||||
_: [],
|
...defaults,
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
|
||||||
"user-data-dir": xdgLocalDir,
|
|
||||||
cert: {
|
cert: {
|
||||||
value: undefined,
|
value: undefined,
|
||||||
},
|
},
|
||||||
@@ -144,9 +177,7 @@ describe("cli", () => {
|
|||||||
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
||||||
// If you actually had a path like this you would do this instead:
|
// If you actually had a path like this you would do this instead:
|
||||||
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
|
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
|
||||||
_: [],
|
...defaults,
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
|
||||||
"user-data-dir": xdgLocalDir,
|
|
||||||
socket: path.resolve("--socket-path-value"),
|
socket: path.resolve("--socket-path-value"),
|
||||||
})
|
})
|
||||||
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
|
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
|
||||||
@@ -154,10 +185,20 @@ describe("cli", () => {
|
|||||||
|
|
||||||
it("should allow positional arguments before options", () => {
|
it("should allow positional arguments before options", () => {
|
||||||
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
||||||
|
...defaults,
|
||||||
_: ["foo", "test"],
|
_: ["foo", "test"],
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
|
||||||
"user-data-dir": xdgLocalDir,
|
|
||||||
auth: "none",
|
auth: "none",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should support repeatable flags", () => {
|
||||||
|
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
||||||
|
...defaults,
|
||||||
|
"proxy-domain": ["*.coder.com"],
|
||||||
|
})
|
||||||
|
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||||
|
...defaults,
|
||||||
|
"proxy-domain": ["*.coder.com", "test.com"],
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user