2018-08-13 21:10:53 +02:00
'use strict' ;
exports = module . exports = {
start : start ,
stop : stop
} ;
2018-08-14 19:35:14 -07:00
var apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
2019-10-24 10:39:47 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-07-25 15:33:34 -07:00
constants = require ( './constants.js' ) ,
2018-08-14 18:27:08 -07:00
express = require ( 'express' ) ,
2018-08-13 22:06:28 +02:00
debug = require ( 'debug' ) ( 'box:dockerproxy' ) ,
2018-08-13 22:14:56 +02:00
http = require ( 'http' ) ,
2018-08-14 19:35:14 -07:00
HttpError = require ( 'connect-lastmile' ) . HttpError ,
2018-08-14 18:27:08 -07:00
middleware = require ( './middleware' ) ,
2018-08-14 20:20:19 -07:00
net = require ( 'net' ) ,
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
_ = require ( 'underscore' ) ;
2018-08-13 21:10:53 +02:00
2018-08-14 19:03:10 -07:00
var gHttpServer = null ;
2018-08-13 21:10:53 +02:00
2018-08-14 18:27:08 -07:00
function authorizeApp ( req , res , next ) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
2018-08-13 21:10:53 +02:00
2018-08-15 17:23:58 +02:00
// make the tests pass for now
2019-07-26 10:10:14 -07:00
if ( constants . TEST ) {
2018-08-15 17:23:58 +02:00
req . app = { id : 'testappid' } ;
return next ( ) ;
}
2018-08-14 19:35:14 -07:00
apps . getByIpAddress ( req . connection . remoteAddress , function ( error , app ) {
2019-10-24 10:39:47 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new HttpError ( 401 , 'Unauthorized' ) ) ;
2018-08-14 19:35:14 -07:00
if ( error ) return next ( new HttpError ( 500 , error ) ) ;
if ( ! ( 'docker' in app . manifest . addons ) ) return next ( new HttpError ( 401 , 'Unauthorized' ) ) ;
req . app = app ;
next ( ) ;
} ) ;
2018-08-14 18:27:08 -07:00
}
2018-08-13 22:14:56 +02:00
2018-08-14 18:27:08 -07:00
function attachDockerRequest ( req , res , next ) {
var options = {
socketPath : '/var/run/docker.sock' ,
method : req . method ,
path : req . url ,
headers : req . headers
} ;
2018-08-13 22:14:56 +02:00
2018-08-14 18:27:08 -07:00
req . dockerRequest = http . request ( options , function ( dockerResponse ) {
res . writeHead ( dockerResponse . statusCode , dockerResponse . headers ) ;
2018-08-13 21:10:53 +02:00
2018-08-14 18:27:08 -07:00
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res . write ( ' ' ) ;
2018-08-13 21:10:53 +02:00
2018-08-14 18:27:08 -07:00
dockerResponse . on ( 'error' , function ( error ) { console . error ( 'dockerResponse error:' , error ) ; } ) ;
dockerResponse . pipe ( res , { end : true } ) ;
} ) ;
2018-08-13 21:10:53 +02:00
2018-08-14 18:27:08 -07:00
next ( ) ;
}
2018-08-13 22:01:51 +02:00
2019-10-24 10:39:47 -07:00
// eslint-disable-next-line no-unused-vars
2018-08-14 18:27:08 -07:00
function containersCreate ( req , res , next ) {
2018-08-14 20:20:19 -07:00
safe . set ( req . body , 'HostConfig.NetworkMode' , 'cloudron' ) ; // overwrite the network the container lives in
2018-08-17 16:50:11 +02:00
safe . set ( req . body , 'NetworkingConfig' , { } ) ; // drop any custom network configs
2019-01-17 23:32:24 -08:00
safe . set ( req . body , 'Labels' , _ . extend ( { } , safe . query ( req . body , 'Labels' ) , { appId : req . app . id , isCloudronManaged : String ( false ) } ) ) ; // overwrite the app id to track containers of an app
2018-08-20 15:22:08 +02:00
safe . set ( req . body , 'HostConfig.LogConfig' , { Type : 'syslog' , Config : { 'tag' : req . app . id , 'syslog-address' : 'udp://127.0.0.1:2514' , 'syslog-format' : 'rfc5424' } } ) ;
2018-08-14 20:20:19 -07:00
2018-08-15 16:47:06 -07:00
const appDataDir = path . join ( paths . APPS _DATA _DIR , req . app . id , 'data' ) ,
dockerDataDir = path . join ( paths . APPS _DATA _DIR , req . app . id , 'docker' ) ;
2018-08-15 17:23:58 +02:00
2018-08-15 16:51:10 +02:00
debug ( 'Original volume binds:' , req . body . HostConfig . Binds ) ;
2018-08-15 17:23:58 +02:00
2018-08-14 20:20:19 -07:00
let binds = [ ] ;
for ( let bind of ( req . body . HostConfig . Binds || [ ] ) ) {
2018-08-16 14:34:55 +02:00
if ( bind . startsWith ( appDataDir ) ) binds . push ( bind ) ; // eclipse will inspect docker to find out the host folders and pass that to child containers
else if ( bind . startsWith ( '/app/data' ) ) binds . push ( bind . replace ( new RegExp ( '^/app/data' ) , appDataDir ) ) ;
2018-08-15 16:47:06 -07:00
else binds . push ( ` ${ dockerDataDir } / ${ bind } ` ) ;
2018-08-14 20:20:19 -07:00
}
2018-08-15 17:23:58 +02:00
// cleanup the paths from potential double slashes
binds = binds . map ( function ( bind ) { return bind . replace ( /\/+/g , '/' ) ; } ) ;
debug ( 'Rewritten volume binds:' , binds ) ;
2018-08-14 20:20:19 -07:00
safe . set ( req . body , 'HostConfig.Binds' , binds ) ;
2018-08-13 22:14:56 +02:00
2018-08-14 19:35:14 -07:00
let plainBody = JSON . stringify ( req . body ) ;
2018-08-13 22:14:56 +02:00
2018-08-14 18:27:08 -07:00
req . dockerRequest . setHeader ( 'Content-Length' , Buffer . byteLength ( plainBody ) ) ;
req . dockerRequest . end ( plainBody ) ;
}
2019-10-24 10:39:47 -07:00
// eslint-disable-next-line no-unused-vars
2018-08-14 18:27:08 -07:00
function process ( req , res , next ) {
2018-08-16 14:28:51 +02:00
// we have to rebuild the body since we consumed in in the parser
2018-08-16 14:34:55 +02:00
if ( Object . keys ( req . body ) . length !== 0 ) {
2018-08-16 14:28:51 +02:00
let plainBody = JSON . stringify ( req . body ) ;
req . dockerRequest . setHeader ( 'Content-Length' , Buffer . byteLength ( plainBody ) ) ;
req . dockerRequest . end ( plainBody ) ;
} else if ( ! req . readable ) {
2018-08-14 18:27:08 -07:00
req . dockerRequest . end ( ) ;
} else {
req . pipe ( req . dockerRequest , { end : true } ) ;
}
}
function start ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2018-08-14 19:03:10 -07:00
assert ( gHttpServer === null , 'Already started' ) ;
2018-08-13 21:10:53 +02:00
2018-08-14 18:27:08 -07:00
let json = middleware . json ( { strict : true } ) ;
let router = new express . Router ( ) ;
2018-08-14 20:20:19 -07:00
router . post ( '/:version/containers/create' , containersCreate ) ;
2018-08-13 22:14:56 +02:00
2018-08-14 19:35:14 -07:00
let proxyServer = express ( ) ;
2018-08-16 14:34:55 +02:00
2019-07-26 10:10:14 -07:00
if ( constants . TEST ) {
2018-08-20 20:10:14 -07:00
proxyServer . use ( function ( req , res , next ) {
2019-01-04 10:04:28 -08:00
debug ( 'proxying: ' + req . method , req . url ) ;
2018-08-20 20:10:14 -07:00
next ( ) ;
} ) ;
}
2018-08-16 14:34:55 +02:00
2018-08-14 18:27:08 -07:00
proxyServer . use ( authorizeApp )
. use ( attachDockerRequest )
2018-08-14 19:35:14 -07:00
. use ( json )
2018-08-14 18:27:08 -07:00
. use ( router )
2018-08-14 19:35:14 -07:00
. use ( process )
. use ( middleware . lastMile ( ) ) ;
2018-08-14 22:52:00 +02:00
2018-08-14 19:03:10 -07:00
gHttpServer = http . createServer ( proxyServer ) ;
2019-07-25 15:33:34 -07:00
gHttpServer . listen ( constants . DOCKER _PROXY _PORT , '0.0.0.0' , callback ) ;
2018-08-14 22:52:00 +02:00
2019-07-25 15:33:34 -07:00
debug ( ` startDockerProxy: started proxy on port ${ constants . DOCKER _PROXY _PORT } ` ) ;
2018-08-13 22:14:56 +02:00
2019-10-24 10:39:47 -07:00
// eslint-disable-next-line no-unused-vars
2018-08-14 19:03:10 -07:00
gHttpServer . on ( 'upgrade' , function ( req , client , head ) {
2018-08-13 22:14:56 +02:00
// Create a new tcp connection to the TCP server
var remote = net . connect ( '/var/run/docker.sock' , function ( ) {
2018-08-17 15:30:23 +02:00
var upgradeMessage = req . method + ' ' + req . url + ' HTTP/1.1\r\n' +
` Host: ${ req . headers . host } \r \n ` +
'Connection: Upgrade\r\n' +
'Upgrade: tcp\r\n' ;
if ( req . headers [ 'content-type' ] === 'application/json' ) {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n' ;
2019-07-25 15:33:34 -07:00
upgradeMessage += 'Content-Type: application/json\r\n' ;
2018-08-17 15:30:23 +02:00
upgradeMessage += ` Content-Length: ${ Buffer . byteLength ( plainBody ) } \r \n ` ;
upgradeMessage += '\r\n' ;
upgradeMessage += plainBody ;
}
upgradeMessage += '\r\n' ;
2018-08-13 22:14:56 +02:00
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
2018-08-17 15:30:23 +02:00
remote . write ( upgradeMessage ) ;
// two-way pipes between client and docker daemon
client . pipe ( remote ) . pipe ( client ) ;
2018-08-13 22:14:56 +02:00
} ) ;
} ) ;
2018-08-13 21:10:53 +02:00
}
function stop ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2018-08-14 19:03:10 -07:00
if ( gHttpServer ) gHttpServer . close ( ) ;
gHttpServer = null ;
2018-08-13 21:10:53 +02:00
callback ( ) ;
2018-08-15 16:47:06 -07:00
}