webide: bugfix for content policy (#1262)

* safari has stricter intepretations of the policy. This adds explicit
web socket connect-src field

* bugfix for multiple requests for webpage in a very small amount of
time. This would previously start up multiple containers
This commit is contained in:
Bolek@DigitalAsset 2019-05-21 09:59:26 -04:00 committed by GitHub
parent 9684e1325e
commit b12b94163d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 30 additions and 24 deletions

View File

@ -4,6 +4,7 @@
"port" : 3000,
"managementPort" : 3001,
"httpToHttpsPort" : 3002,
"hostname" : "localhost",
"secureHeaders" :true
},
"session" : {

View File

@ -77,6 +77,7 @@ export default class Docker {
const createOptions = { HostConfig: config.docker.hostConfig }
if (!this.onInternalNetwork && config.devMode) createOptions.HostConfig.PublishAllPorts=true
debug("sending run api command")
const runner = this.api.run(imageId, ["code-server", "--no-auth", "--allow-http", "--disable-telemetry"], [], createOptions, (err :any, result :any) => {
if (err) reject(err)
})
@ -86,7 +87,7 @@ export default class Docker {
cResolve(container)
})
})
runner.on('container', (container :Container) => debug(`created container ${container.id}`))
runner.on('container', (container :Container) => { debug(`created container ${container.id}`) })
runner.on('stream', (stream :Stream) => {
let started = false
stream.pipe(process.stdout) //TODO should we create context aware log messages per container (including the user session info)??

View File

@ -48,7 +48,8 @@ http.createServer(httpToHttpsApp).listen(conf.http.httpToHttpsPort, () => {
if (!conf.http.port) throw new Error("MUST configure port for webide: 'conf.http.port'")
const webideServer = createWebIdeServer()
if (conf.secureHeaders || conf.secureHeaders === undefined) {
if (conf.http.secureHeaders || conf.http.secureHeaders === undefined) {
console.log("INFO adding security headers")
webIdeApp.use((req, res, next) => {
addSecureHeaders(res, conf)
next()
@ -143,5 +144,5 @@ function addSecureHeaders(res :Response, config :any) {
res.setHeader("X-Frame-Options", "sameorigin")
res.setHeader("X-XSS-Protection", "1; mode=block")
res.setHeader("Referrer-Policy", "no-referrer-when-downgrade")
res.setHeader("Content-Security-Policy", `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;`)
res.setHeader("Content-Security-Policy", `default-src 'self'; connect-src 'self' ws://${config.http.hostname}:${config.http.port}/; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;`)
}

View File

@ -13,6 +13,7 @@ import { ImageInspectInfo } from "dockerode";
import request, { HttpArchiveRequest } from "request"
import zlib from "zlib"
import { Readable } from "stream"
import NodeCache from "node-cache"
const conf = require('../config').read()
const debug = require('debug')('webide:route')
@ -80,7 +81,7 @@ export default class WebIdeRoute {
const route = this
debug("requesting %s", req.url)
Session.session(req, res, (err :any, state :any, sessionId :string, saveSession :any) => {
route.getImage()
route.getDockerImage()
.then(image => route.ensureDockerContainer(req, state, saveSession, sessionId, image))
.then(containerInfo => {
const url = route.docker.getContainerUrl(containerInfo, 'http')
@ -88,15 +89,11 @@ export default class WebIdeRoute {
})
.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()
route.sendErrorResponse(err, req, res)
})
})
} catch (error) {
console.error(error)
res.statusCode = 500
res.end()
this.sendErrorResponse(error, req, res)
}
}
@ -125,7 +122,7 @@ export default class WebIdeRoute {
//doing nothing we let the proxy handle the response
}
private getImage() :Promise<ImageInspectInfo> {
private getDockerImage() :Promise<ImageInspectInfo> {
return this.docker.getImage(conf.docker.webIdeReference)
}
@ -169,7 +166,9 @@ export default class WebIdeRoute {
private ensureDockerContainer(req :Request, state :any, saveSession :Session.SaveSession, sessionId :string, image :ImageInspectInfo) {
if (!state.docker) {
if (!state.initializing) {
//double check current state whether it is initializing or not
const currentState :any = Session.getStateSync(sessionId) || {}
if (!currentState.initializing) {
state.initializing = true;
saveSession(state);
return this.docker.api.listContainers({all: false, filters: { ancestor: [`${conf.docker.webIdeReference}`] }})
@ -177,7 +176,7 @@ export default class WebIdeRoute {
if (containers.length >= conf.docker.maxInstances) {
state.initializing = false;
saveSession(state);
return Promise.reject(new ProxyError(`Breach max instances ${conf.docker.maxInstances}`, 503))
return Promise.reject(new ProxyError(`Breach max instances ${conf.docker.maxInstances}`, 503, "There is unusually high server load. Please try again in a couple of minutes."))
}
return this.docker.startContainer(image.Id).then(c => {
console.log("INFO attaching container %s to session %s", c.Id, sessionId)
@ -188,16 +187,16 @@ export default class WebIdeRoute {
});
});
} else {
//this occurs sporadically (perhaps when developer tools is open) sending another request
//TODO create better promise handling without timeout
console.log("INFO request sent during initialization...waiting for docker to come up")
return new Promise((resolve, reject) => {
Session.readSession(req, (err, state, sessionId) => {
const wait = setTimeout(() => {
clearTimeout(wait);
resolve(state.docker)
}, 10000)
})
const interval = setInterval(() => {
Session.readSession(req, (err, state, sessionId) => {
if (state.docker && !state.initializing) {
clearInterval(interval);
resolve(state.docker);
}
})
}, 1000)
})
}
}

View File

@ -28,8 +28,12 @@ export function onTimeout(callback: Callback<any>) {
});
}
function remove(sessionId :string) {
store.del(sessionId)
/**
* explicit synchronous getter in case we need state current state
* @param sessionId
*/
export function getStateSync(sessionId :string) :any {
return store.get(sessionId)
}
/**
@ -122,7 +126,7 @@ function save(sessionId :string, state :any) :boolean {
store.del(sessionId)
}, delay)
}
return store.set(sessionId, state)
return store.set(sessionId, state, 0)
}
function generateSessionId() {