2018-08-13 21:10:53 +02:00
'use strict' ;
exports = module . exports = {
2021-05-01 11:21:09 -07:00
start ,
stop
2018-08-13 21:10:53 +02:00
} ;
2021-08-20 09:19:44 -07:00
const apps = require ( './apps.js' ) ,
2018-08-14 19:35:14 -07:00
assert = require ( 'assert' ) ,
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' ) ,
2021-09-07 09:57:49 -07:00
util = require ( 'util' ) ,
2018-08-14 20:20:19 -07:00
_ = require ( 'underscore' ) ;
2018-08-13 21:10:53 +02:00
2021-08-20 09:19:44 -07:00
let gHttpServer = null ;
2018-08-13 21:10:53 +02:00
2021-08-20 09:19:44 -07:00
async function authorizeApp ( req , res , next ) {
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
2021-08-31 08:37:16 -07:00
const [ error , app ] = await safe ( apps . getByIpAddress ( req . connection . remoteAddress ) ) ;
2021-08-20 09:19:44 -07:00
if ( error ) return next ( new HttpError ( 500 , error ) ) ;
if ( ! app ) return next ( new HttpError ( 401 , 'Unauthorized' ) ) ;
2018-08-14 19:35:14 -07:00
2021-08-20 09:19:44 -07:00
if ( ! ( 'docker' in app . manifest . addons ) ) return next ( new HttpError ( 401 , 'Unauthorized' ) ) ;
2018-08-14 19:35:14 -07:00
2021-08-20 09:19:44 -07:00
req . app = app ;
2018-08-14 19:35:14 -07:00
2021-08-20 09:19:44 -07:00
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 ) {
2022-04-14 17:41:41 -05:00
const options = {
2018-08-14 18:27:08 -07:00
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
2020-08-02 11:43:18 -07:00
dockerResponse . on ( 'error' , function ( error ) { debug ( 'dockerResponse error:' , error ) ; } ) ;
2018-08-14 18:27:08 -07:00
dockerResponse . pipe ( res , { end : true } ) ;
} ) ;
2018-08-13 21:10:53 +02:00
2020-03-29 13:38:34 -07:00
req . dockerRequest . on ( 'error' , ( ) => { } ) ; // abort() throws
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
2020-03-29 13:38:34 -07:00
const appDataDir = path . join ( paths . APPS _DATA _DIR , req . app . id , 'data' ) ;
2018-08-15 17:23:58 +02:00
2020-03-29 13:38:34 -07:00
debug ( 'Original bind mounts:' , 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 || [ ] ) ) {
2020-03-29 13:38:34 -07:00
if ( ! bind . startsWith ( '/app/data/' ) ) {
req . dockerRequest . abort ( ) ;
return next ( new HttpError ( 400 , 'Binds must be under /app/data/' ) ) ;
}
2018-08-15 17:23:58 +02:00
2020-03-29 13:38:34 -07:00
binds . push ( bind . replace ( new RegExp ( '^/app/data/' ) , appDataDir + '/' ) ) ;
}
2018-08-15 17:23:58 +02:00
2020-03-29 13:38:34 -07:00
debug ( 'Rewritten bind mounts:' , 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 } ) ;
}
}
2021-09-07 09:57:49 -07:00
async function start ( ) {
2018-08-14 19:03:10 -07:00
assert ( gHttpServer === null , 'Already started' ) ;
2018-08-13 21:10:53 +02:00
2021-09-07 09:57:49 -07:00
const json = middleware . json ( { strict : true } ) ;
2020-03-29 13:38:34 -07:00
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
// protected other paths is done by preventing install/exec access of apps using docker addon
2021-09-07 09:57:49 -07:00
const 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
2021-09-07 09:57:49 -07:00
const 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 ) ;
2018-08-14 22:52:00 +02:00
2019-11-14 13:15:12 +01:00
// Overwrite the default 2min request timeout. This is required for large builds for example
gHttpServer . setTimeout ( 60 * 60 * 1000 ) ;
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
2022-04-14 17:41:41 -05:00
const remote = net . connect ( '/var/run/docker.sock' , function ( ) {
let upgradeMessage = req . method + ' ' + req . url + ' HTTP/1.1\r\n' +
2018-08-17 15:30:23 +02:00
` 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
2021-09-07 09:57:49 -07:00
await util . promisify ( gHttpServer . listen . bind ( gHttpServer ) ) ( constants . DOCKER _PROXY _PORT , '172.18.0.1' ) ;
}
2018-08-13 21:10:53 +02:00
2021-09-07 09:57:49 -07:00
async function stop ( ) {
2018-08-14 19:03:10 -07:00
if ( gHttpServer ) gHttpServer . close ( ) ;
gHttpServer = null ;
2018-08-15 16:47:06 -07:00
}