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:
Bolek@DigitalAsset 2019-04-08 19:31:13 +00:00 committed by GitHub
parent f786210169
commit 64707a6804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 629 additions and 88 deletions

2
.gitignore vendored
View File

@ -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
View 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`

View File

@ -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

View File

@ -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 &&\

View File

@ -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
View 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"]

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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
View 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="
}
}
}

View 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"
}
}

View File

@ -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
}

View File

@ -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)
}