mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-04 00:36:58 +03:00
web ide: secure docker containers through networks (#304)
This pr addresses security concerns regarding the web ide docker container. The bulk of the changes involve managing and coordinating docker networks so that the proxy runs on internal docker network as well as external one, while the web ide containers run only on the internal network.
This commit is contained in:
parent
f786210169
commit
64707a6804
2
.gitignore
vendored
2
.gitignore
vendored
@ -38,6 +38,7 @@ tags
|
||||
**/.classpath
|
||||
**/.factorypath
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
ghc-bindist
|
||||
*.result.xml
|
||||
**/.ensime
|
||||
@ -76,6 +77,7 @@ ledger-api/.bin
|
||||
.vscode
|
||||
.m2/
|
||||
.history/
|
||||
**/.history/
|
||||
### Bazel: https://www.gitignore.io/api/bazel ###
|
||||
/bazel-*
|
||||
.bazelrc.local
|
||||
|
6
web-ide/README.md
Normal file
6
web-ide/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
### DAML WEB IDE
|
||||
This is a project for managing multi user web ide. The main components are
|
||||
* daml web ide docker image: this hosts a [web ide code-server](https://github.com/codercom/code-server) bundled with the sdk
|
||||
* proxy server and image: this manages multiple daml web ide docker containers and proxies requests into them
|
||||
|
||||
Further details can be found in `proxy/README.md` and `ide-server/README.md`
|
@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
set -eux
|
||||
|
||||
if [ "$1" = 'web-ide' ]; then
|
||||
shift
|
||||
echo "running code server with $@"
|
||||
code-server "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
@ -5,20 +5,19 @@ FROM "digitalasset/daml-sdk:${VERSION}-master"
|
||||
RUN da list
|
||||
|
||||
USER root
|
||||
# Install VS Code's deps, libxkbfile-dev and libsecret-1-dev
|
||||
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
|
||||
# Install VS Code's deps. These are the only two it seems we need.
|
||||
apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
libxkbfile-dev \
|
||||
libsecret-1-dev \
|
||||
net-tools
|
||||
libxkbfile-dev \
|
||||
libsecret-1-dev
|
||||
|
||||
USER sdk
|
||||
ENV VERSION=0.11.19 \
|
||||
CODESERVER_SHA256=902b2b56ff9e41bb3d9f5dafd23a9f1a09655a1a112cf3d583fe5d92e63b718a
|
||||
CODESERVER_SHA256=355463f35b9c355cf99ea2f1d87367194daae8aa4fdabaace4a9ea2f1b0490f6
|
||||
RUN mkdir -p /home/sdk/.code-server/extensions/ &&\
|
||||
mkdir /home/sdk/workspace &&\
|
||||
curl -Lo - "https://github.com/codercom/code-server/releases/download/1.32.0-310/code-server-1.32.0-310-linux-x64.tar.gz" | tar xzvf - --strip-components 1 "code-server-1.32.0-310-linux-x64/code-server" &&\
|
||||
curl -Lo - "https://github.com/codercom/code-server/releases/download/1.408-vsc1.32.0/code-server1.408-vsc1.32.0-linux-x64.tar.gz" | tar xzvf - --strip-components 1 "code-server1.408-vsc1.32.0-linux-x64" &&\
|
||||
echo "${CODESERVER_SHA256} code-server" | sha256sum -c &&\
|
||||
mv ./code-server /home/sdk/.da/bin/ &&\
|
||||
ln -s /home/sdk/.da/packages/daml-extension/10${VERSION} /home/sdk/.code-server/extensions/da-vscode-daml-extension &&\
|
@ -2,8 +2,8 @@
|
||||
This is the dockerfile for creating and image with DAML SDK and the [code-server](https://github.com/codercom/code-server) IDE
|
||||
|
||||
### Building
|
||||
Under the main daml directory `docker build --rm -t digitalasset/daml-webide:0.11.19-master web-ide/docker/`
|
||||
the tagged version should match what is configured in `web-ide/proxy/config.json` if you want to run locally
|
||||
Under the main daml directory `docker build --rm -t digitalasset/daml-webide:0.11.19-master web-ide/ide-server/`
|
||||
the tagged version should match what is configured in `web-ide/proxy/config.json` if you want to run locally.
|
||||
|
||||
### Running
|
||||
We haven't uploaded to github yet, so you must first create an image (mentioned above).
|
21
web-ide/proxy/Dockerfile
Normal file
21
web-ide/proxy/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
# This docker file runs the proxy server, which in turn creates docker containers running the web ide.
|
||||
# We don't run docker within the containers created by this image, instead we mount docker.sock and use the
|
||||
# docker binaries to start web ide containers on the host
|
||||
#
|
||||
# for example: docker run --rm -it -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock 753e224e9b8b
|
||||
#
|
||||
# docker build --rm -t digitalasset/daml-webide-proxy:0.11.19-master
|
||||
|
||||
|
||||
FROM node:8.15-alpine
|
||||
|
||||
COPY *.js /
|
||||
COPY *.json /
|
||||
|
||||
RUN npm install \
|
||||
&& apk update \
|
||||
&& apk add docker
|
||||
|
||||
EXPOSE 3000 8443
|
||||
LABEL WEB-IDE-PROXY=""
|
||||
CMD ["node", "proxy.js"]
|
@ -3,4 +3,24 @@ This proxies the docker hosted web ide. It spins up a docker instance for each u
|
||||
Session state is managed by cookie (webide.connect.sid by default)
|
||||
|
||||
### Running
|
||||
node proxy.js
|
||||
For quick developement cycles we can disable some cumbersome security features; remove `docker.hostConfig.NetworkMode` entry from `config.json` and simply run
|
||||
|
||||
```
|
||||
cd web-ide/proxy
|
||||
npm install
|
||||
node proxy.js
|
||||
```
|
||||
|
||||
When running on server, the proxy creates containers on an internal network "web-int". In order for the proxy to communicate with them it must run under docker attached to two networks, "web-int" and "web-ext". The network creation and attachment happen automatically but it simply means that in order to run the proxy locally the same way it runs on server you have to run it via docker image and mount docker.sock.
|
||||
|
||||
We haven't uploaded to github yet, so you must first create an image.
|
||||
```
|
||||
docker build --rm -t digitalasset/daml-webide-proxy:0.11.19-master web-ide/proxy/
|
||||
```
|
||||
then run it
|
||||
```
|
||||
docker run --rm -it -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock ${IMAGE_ID_OF_PROXY}
|
||||
```
|
||||
|
||||
### Container settings
|
||||
The proxy uses [dockerode](https://github.com/apocas/dockerode) to manage docker containers, which is a small library over the rest based docker API. Most settings can be configured from `config.json` hostConfig entry. [See API details](https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate) HostConfig entry
|
||||
|
@ -1,15 +1,28 @@
|
||||
{
|
||||
"devMode" : true,
|
||||
"http" : {
|
||||
"port" : 80
|
||||
"port" : 3000
|
||||
},
|
||||
"session" : {
|
||||
"name" : "webide.connect.sid",
|
||||
"secret" : "super-safe",
|
||||
"cookie" : { "path": "/", "httpOnly": false, "secure": false, "maxAge": 43200},
|
||||
"inactiveTimeout" : "3600"
|
||||
"inactiveTimeout" : 900,
|
||||
"timeout" : 2700
|
||||
},
|
||||
"docker" : {
|
||||
"image" : "digitalasset/daml-webide:0.11.19-master",
|
||||
"maxInstances" : 100
|
||||
"internalNetwork" : "web-int",
|
||||
"externalNetwork" : "web-ext",
|
||||
"webIdeLabel" : "WEB-IDE",
|
||||
"proxyLabel" : "WEB-IDE-PROXY",
|
||||
"maxInstances" : 50,
|
||||
"hostConfig" : {
|
||||
"NanoCPUs" : 1750000000,
|
||||
"Memory" : 1610612736,
|
||||
"KernelMemory" : 1073741824,
|
||||
"DiskQuota" : 2147483648,
|
||||
"NetworkMode" : "web-int"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const Docker = require('dockerode'),
|
||||
fs = require('fs')
|
||||
docker = new Docker(),
|
||||
fs = require('fs'),
|
||||
{ URL } = require('url'),
|
||||
config = require('./config.json')
|
||||
|
||||
const docker = new Docker()
|
||||
|
||||
module.exports = {
|
||||
getImage: getImage,
|
||||
getOde: getOde,
|
||||
init : init,
|
||||
getContainerUrl : getContainerUrl,
|
||||
startContainer: startContainer,
|
||||
stopContainer: stopContainer,
|
||||
getImage: getImage
|
||||
stopContainer: stopContainer
|
||||
}
|
||||
|
||||
ensureDocker()
|
||||
@ -22,16 +27,67 @@ function getImage(imageId) {
|
||||
return docker.getImage(imageId).inspect()
|
||||
}
|
||||
|
||||
function init() {
|
||||
const webIdeNetwork = config.docker.hostConfig.NetworkMode ? config.docker.hostConfig.NetworkMode : 'bridge'
|
||||
if (!onInternalNetwork()) {
|
||||
console.log("running web ide containers on network[%s], this is a non-internal network and is only suitable for local development", webIdeNetwork)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const initNetworksP = docker.listNetworks()
|
||||
.then(networks => {
|
||||
//create networks if they don't exist
|
||||
const internalName = config.docker.internalNetwork,
|
||||
externalName = config.docker.externalNetwork,
|
||||
hasInternal = networks.some(n => n.Name === internalName),
|
||||
hasExternal = networks.some(n => n.Name === externalName),
|
||||
internalNetworkP = hasInternal ? getNetwork(networks, internalName) : createNetwork(internalName, true),
|
||||
externalNetworkP = hasExternal ? getNetwork(networks, externalName) : createNetwork(externalName, false)
|
||||
return Promise.all([internalNetworkP, externalNetworkP])
|
||||
})
|
||||
const proxyIdP = docker.listContainers({all: false, filters: { label: [config.docker.proxyLabel] }})
|
||||
.then(containers => {
|
||||
if (containers.length !== 1) return new Error(`Problems finding web ide proxy. Found ${containers.length} instances labelled with ${config.docker.proxyLabel}`)
|
||||
return containers[0].Id
|
||||
})
|
||||
|
||||
return Promise.all([initNetworksP, proxyIdP])
|
||||
.then(all => {
|
||||
const networkIds = all[0]
|
||||
const proxyContainerId = all[1]
|
||||
networkIds.forEach(id => console.log("connecting container[%s] to network[%s]", proxyContainerId, id))
|
||||
return Promise.all(networkIds.map(id => {
|
||||
return docker.getNetwork(id).connect({Container: proxyContainerId})
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function onInternalNetwork() {
|
||||
const webIdeNetwork = config.docker.hostConfig.NetworkMode ? config.docker.hostConfig.NetworkMode : 'bridge'
|
||||
return config.docker.internalNetwork === webIdeNetwork
|
||||
}
|
||||
|
||||
function getNetwork(networks, name) {
|
||||
return Promise.resolve(networks.find(n => n.Name === name).Id)
|
||||
}
|
||||
|
||||
function createNetwork(name, internal) {
|
||||
console.log("creating %s network %s", internal ? 'internal' : 'external', name)
|
||||
return docker.createNetwork({
|
||||
Name: name,
|
||||
Internal: internal
|
||||
}).then(n => n.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the container and returns a promise when the container is initialized
|
||||
* @param {*} imageId
|
||||
*/
|
||||
function startContainer (imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
//TODO open ports
|
||||
const createOptions = {
|
||||
HostConfig: { PublishAllPorts : true }
|
||||
}
|
||||
const createOptions = { HostConfig: config.docker.hostConfig }
|
||||
if (!onInternalNetwork() && config.devMode) createOptions.HostConfig.PublishAllPorts=true
|
||||
|
||||
docker.run(imageId, ["code-server", "--no-auth", "--allow-http"], process.stdout, createOptions, function (err, data, container) {
|
||||
if (err) reject(err)
|
||||
})
|
||||
@ -67,6 +123,19 @@ function ensureDocker() {
|
||||
const stats = fs.statSync(socket)
|
||||
|
||||
if (!stats.isSocket()) {
|
||||
throw new Error('Are you sure the docker is running?')
|
||||
throw new Error('Are you sure the docker daemon is running?')
|
||||
}
|
||||
}
|
||||
|
||||
function getContainerUrl(containerInfo, protocol) {
|
||||
if (onInternalNetwork()) {
|
||||
const ip = containerInfo.NetworkSettings.Networks[`${config.docker.internalNetwork}`].IPAddress
|
||||
return new URL(`${protocol}://${ip}:8443`)
|
||||
} else {
|
||||
const containerPort = containerInfo.NetworkSettings.Ports['8443/tcp']
|
||||
if (containerPort === undefined) throw "Missing coder-server port[8443] mapping on container"
|
||||
return new URL(`${protocol}://0.0.0.0:${containerPort[0].HostPort}`)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
348
web-ide/proxy/package-lock.json
generated
Normal file
348
web-ide/proxy/package-lock.json
generated
Normal file
@ -0,0 +1,348 @@
|
||||
{
|
||||
"name": "web-ide-proxy",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"JSONStream": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
|
||||
"integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=",
|
||||
"requires": {
|
||||
"jsonparse": "^1.2.0",
|
||||
"through": ">=2.2.7 <3"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
|
||||
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"buffer-alloc": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
||||
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
|
||||
"requires": {
|
||||
"buffer-alloc-unsafe": "^1.1.0",
|
||||
"buffer-fill": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"buffer-alloc-unsafe": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
|
||||
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
|
||||
},
|
||||
"buffer-fill": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
|
||||
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g=="
|
||||
},
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
||||
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz",
|
||||
"integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"docker-modem": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.9.tgz",
|
||||
"integrity": "sha512-lVjqCSCIAUDZPAZIeyM125HXfNvOmYYInciphNrLrylUtKyW66meAjSPXWchKVzoIYZx69TPnAepVSSkeawoIw==",
|
||||
"requires": {
|
||||
"JSONStream": "1.3.2",
|
||||
"debug": "^3.2.6",
|
||||
"readable-stream": "~1.0.26-4",
|
||||
"split-ca": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
|
||||
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"dockerode": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.8.tgz",
|
||||
"integrity": "sha512-+7iOUYBeDTScmOmQqpUYQaE7F4vvIt6+gIZNHWhqAQEI887tiPFB9OvXI/HzQYqfUNvukMK+9myLW63oTJPZpw==",
|
||||
"requires": {
|
||||
"concat-stream": "~1.6.2",
|
||||
"docker-modem": "^1.0.8",
|
||||
"tar-fs": "~1.16.3"
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
|
||||
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
|
||||
"integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
|
||||
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6"
|
||||
}
|
||||
},
|
||||
"fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
|
||||
"integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
|
||||
"requires": {
|
||||
"eventemitter3": "^3.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
},
|
||||
"node-cache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.0.tgz",
|
||||
"integrity": "sha512-obRu6/f7S024ysheAjoYFEEBqqDWv4LOMNJEuO8vMeEw2AT4z+NCzO4hlc2lhI4vATzbCQv6kke9FVdx0RbCOw==",
|
||||
"requires": {
|
||||
"clone": "2.x",
|
||||
"lodash": "4.x"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz",
|
||||
"integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"split-ca": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||
"integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz",
|
||||
"integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==",
|
||||
"requires": {
|
||||
"chownr": "^1.0.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"pump": "^1.0.0",
|
||||
"tar-stream": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
|
||||
"requires": {
|
||||
"bl": "^1.0.0",
|
||||
"buffer-alloc": "^1.2.0",
|
||||
"end-of-stream": "^1.0.0",
|
||||
"fs-constants": "^1.0.0",
|
||||
"readable-stream": "^2.3.0",
|
||||
"to-buffer": "^1.1.1",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"to-buffer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
|
||||
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"requires": {
|
||||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
|
||||
}
|
||||
}
|
||||
}
|
19
web-ide/proxy/package.json
Normal file
19
web-ide/proxy/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "web-ide-proxy",
|
||||
"version": "1.0.0",
|
||||
"description": "Spins up docker images hosting code-server ide and routes traffic",
|
||||
"main": "proxy.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Digital Asset",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"cookie": "^0.3.1",
|
||||
"cookie-signature": "^1.1.0",
|
||||
"dockerode": "^2.5.8",
|
||||
"http-proxy": "^1.17.0",
|
||||
"node-cache": "^4.2.0",
|
||||
"uid-safe": "^2.1.5"
|
||||
}
|
||||
}
|
@ -12,61 +12,90 @@
|
||||
|
||||
const http = require('http'),
|
||||
httpProxy = require('http-proxy'),
|
||||
{ URL } = require('url'),
|
||||
Session = require('./session'),
|
||||
docker = require('./docker'),
|
||||
config = require('./config.json')
|
||||
|
||||
class ProxyError extends Error {
|
||||
constructor (message, status) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.status = status || 500;
|
||||
}
|
||||
}
|
||||
|
||||
const proxy = new httpProxy.createProxyServer({}),
|
||||
getImage = docker.getImage(config.docker.image)
|
||||
|
||||
const proxyServer = http.createServer((req, res) => {
|
||||
//console.log("requesting %s", req.url)
|
||||
Session.session(req, res, function (err, state, sessionId, saveSession) {
|
||||
const proxyServer = http.createServer((req, res) => handleHttpRequest(req, res));
|
||||
//start listening once docker is initialized
|
||||
docker.init()
|
||||
.then(() => {
|
||||
proxyServer.listen(config.http.port, () => {
|
||||
console.log(`docker proxy listening on port ${config.http.port}!`)
|
||||
getImage
|
||||
.then(image => ensureDockerContainer(req, state, saveSession, image))
|
||||
.then(containerInfo => {
|
||||
const url = getContainerUrl(containerInfo, 'http')
|
||||
//console.log("forwarding to %s", url.href)
|
||||
proxy.web(req, res, { target: url.href })
|
||||
})
|
||||
.catch(err => console.error(`could not initiate connection: ${err}`))
|
||||
.then(image => console.log(`found image ${image.Id} ${image.RepoTags}`))
|
||||
.catch(e => console.error(e))
|
||||
})
|
||||
});
|
||||
|
||||
proxyServer.on('error', (err) => {
|
||||
console.error("could not handle request", err)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("error initializing proxy", err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
/**
|
||||
* Listen to the `upgrade` event and proxy the WebSocket requests.
|
||||
*/
|
||||
proxyServer.on('upgrade', (req, socket, head) => {
|
||||
//console.log('ws connected %s cookie: %O', req.url, req.headers.cookie)
|
||||
Session.readSession(req, function (err, state, sessionId) {
|
||||
//keep session active upon any data
|
||||
socket.on('data', () => {
|
||||
Session.keepActive(sessionId) //TODO debounce this as it could get chatty
|
||||
})
|
||||
if (!state.docker) {
|
||||
proxyServer.on('upgrade', (req, socket, head) => handleWsRequest(req, socket, head));
|
||||
|
||||
function handleHttpRequest(req, res) {
|
||||
try {
|
||||
if (config.devMode && req.url === '/session-status') {
|
||||
handleSessionStatus(res);
|
||||
return
|
||||
}
|
||||
const url = getContainerUrl(state.docker, 'ws')
|
||||
proxy.ws(req, socket, head, { target: url.href });
|
||||
})
|
||||
});
|
||||
|
||||
proxyServer.listen(config.http.port, () => {
|
||||
console.log(`docker proxy listening on port ${config.http.port}!`)
|
||||
getImage
|
||||
.then(image => console.log(`found image ${image.Id} ${image.RepoTags}`))
|
||||
.catch(e => {
|
||||
console.error(`could not find image ${config.docker.image}. Only found:`);
|
||||
docker.listImages().then(images => images.forEach(element => {
|
||||
console.error(`${element.RepoTags.join(", ")}`)
|
||||
}));
|
||||
//console.log("requesting %s", req.url)
|
||||
Session.session(req, res, function (err, state, sessionId, saveSession) {
|
||||
getImage
|
||||
.then(image => ensureDockerContainer(req, state, saveSession, image))
|
||||
.then(containerInfo => {
|
||||
const url = docker.getContainerUrl(containerInfo, 'http')
|
||||
//console.log("forwarding to %s", url.href)
|
||||
proxy.web(req, res, { target: url.href })
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`could not initiate connection to web-ide: ${err}`)
|
||||
if (err instanceof ProxyError) res.statusCode = err.status
|
||||
else res.statusCode = 500
|
||||
res.end()
|
||||
})
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
res.statusCode = 500
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
|
||||
function handleWsRequest(req, socket, head) {
|
||||
try {
|
||||
//console.log('ws connected %s cookie: %O', req.url, req.headers.cookie)
|
||||
Session.readSession(req, function (err, state, sessionId) {
|
||||
if (!state.docker) {
|
||||
return
|
||||
}
|
||||
//keep session active upon any data
|
||||
socket.on('data', () => {
|
||||
//console.log("keep active: session[%s] container[%s]", sessionId, state.docker.Id)
|
||||
Session.keepActive(sessionId) //TODO debounce this as it could get chatty
|
||||
})
|
||||
const url = docker.getContainerUrl(state.docker, 'ws')
|
||||
proxy.ws(req, socket, head, { target: url.href });
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
//stop docker container when session timeouts
|
||||
Session.onTimeout((state) => {
|
||||
@ -80,13 +109,23 @@ Session.onTimeout((state) => {
|
||||
process.on('SIGINT', () => destroy());
|
||||
process.on('SIGTERM', () => destroy());
|
||||
|
||||
function handleSessionStatus(res) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
Session.allSessionEntries().then(entries => {
|
||||
const body = JSON.stringify(entries)
|
||||
res.write(JSON.stringify(body));
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
console.info('graceful shutdown.')
|
||||
proxyServer.close()
|
||||
Session.close()
|
||||
|
||||
const dockerOde = docker.getOde()
|
||||
dockerOde.listContainers({all: false, filters: { label: ["WEB-IDE"] }})
|
||||
dockerOde.listContainers({all: false, filters: { label: [`${config.docker.webIdeLabel}`] }})
|
||||
.then(containers => {
|
||||
containers.forEach(c => {
|
||||
console.log("removing container %s", c.Id)
|
||||
@ -103,14 +142,16 @@ function destroy() {
|
||||
function ensureDockerContainer(req, state, saveSession, image) {
|
||||
if (!state.docker) {
|
||||
if (!state.initializing) {
|
||||
state.initializing = true;
|
||||
saveSession(state);
|
||||
const dockerOde = docker.getOde()
|
||||
return dockerOde.listContainers({all: false, filters: { label: ["WEB-IDE"] }})
|
||||
return dockerOde.listContainers({all: false, filters: { label: [`${config.docker.webIdeLabel}`] }})
|
||||
.then(containers => {
|
||||
if (containers.length >= config.docker.maxInstances) {
|
||||
return Promise.reject(new Error(`Breach max instances ${config.docker.maxInstances}`))
|
||||
state.initializing = false;
|
||||
saveSession(state);
|
||||
return Promise.reject(new ProxyError(`Breach max instances ${config.docker.maxInstances}`, 503))
|
||||
}
|
||||
state.initializing = true;
|
||||
saveSession(state);
|
||||
return docker.startContainer(image.Id).then(c => {
|
||||
state.initializing = false
|
||||
state.docker = c
|
||||
@ -134,11 +175,3 @@ function ensureDockerContainer(req, state, saveSession, image) {
|
||||
}
|
||||
return state.docker
|
||||
}
|
||||
|
||||
function getContainerUrl(containerInfo, protocol) {
|
||||
const containerPort = containerInfo.NetworkSettings.Ports['8443/tcp']
|
||||
if (containerPort === undefined) throw "Missing coder-server port[8443] mapping on container"
|
||||
|
||||
const url = new URL(`${protocol}://0.0.0.0:${containerPort[0].HostPort}`)
|
||||
return url
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ module.exports = {
|
||||
allSessionIds: allSessionIds,
|
||||
allSessionEntries: allSessionEntries,
|
||||
close: close,
|
||||
remove: remove,
|
||||
keepActive: keepActive,
|
||||
session: session,
|
||||
readSession: readSession,
|
||||
@ -26,11 +27,15 @@ module.exports = {
|
||||
*/
|
||||
function onTimeout(callback) {
|
||||
store.on( "del", function( key, value ){
|
||||
console.log("inactive session occured: %s", key)
|
||||
console.log("session removed: %s", key)
|
||||
callback(value || {})
|
||||
});
|
||||
}
|
||||
|
||||
function remove(sessionId) {
|
||||
store.del(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* returns promise resolved to array of session entries :: [sessionId, state]
|
||||
*/
|
||||
@ -41,7 +46,14 @@ function allSessionEntries() {
|
||||
if (!keys) resolve([])
|
||||
store.mget(keys, (err2, storeObj) => {
|
||||
if (err2) reject(err2)
|
||||
resolve(storeObj.entries)
|
||||
|
||||
resolve(Object.entries(storeObj).map(e => {
|
||||
return {
|
||||
sessionId : e[0],
|
||||
container : e[1],
|
||||
ttl : store.getTtl(e[0])
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -89,22 +101,33 @@ function session(req, res, callback) {
|
||||
function readSession(req, callback) {
|
||||
const sessionId = getSessionIdFromCookie(req)
|
||||
keepActive(sessionId)
|
||||
store.get(sessionId, function(err, value) {
|
||||
store.get(sessionId, (err, value) => {
|
||||
const state = (value || {})
|
||||
callback(err, state, sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
function handleSessionCallback(sessionId, callback) {
|
||||
store.get(sessionId, function (err, value) {
|
||||
store.get(sessionId, (err, value) => {
|
||||
keepActive(sessionId);
|
||||
callback(err, (value || {}), sessionId, (state) => {
|
||||
//console.log("saving state %s : %O", sessionId, state)
|
||||
save(sessionId, state);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function save(sessionId, state) {
|
||||
if (config.session.timeout && !state._started) {
|
||||
state._started = Date.now()
|
||||
const delay = config.session.timeout * 1000
|
||||
//console.log("session started %s, ending in %s seconds", state._started, config.session.timeout)
|
||||
const timeout = setTimeout(() => {
|
||||
console.log("session timeout occured, started %s", state._started)
|
||||
clearTimeout(timeout)
|
||||
store.del(sessionId)
|
||||
}, delay)
|
||||
}
|
||||
store.set(sessionId, state)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user