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' ) ,
2023-11-27 22:07:31 +01:00
util = require ( 'util' ) ,
volumes = require ( './volumes.js' ) ;
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
2025-01-29 10:35:09 +01:00
const [ error , app ] = await safe ( apps . getByIpAddress ( req . socket . 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' ) ) ;
2023-11-04 13:28:02 +01:00
if ( ! app . manifest . addons ? . docker ) 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
2023-04-16 10:49:59 +02:00
dockerResponse . on ( 'error' , function ( error ) { debug ( 'dockerResponse error: %o' , 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
2023-11-27 22:07:31 +01:00
async 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
2023-05-25 11:27:23 +02:00
safe . set ( req . body , 'Labels' , Object . assign ( { } , safe . query ( req . body , 'Labels' ) , { appId : req . app . id , isCloudronManaged : String ( false ) } ) ) ; // overwrite the app id to track containers of an app
2024-03-21 17:30:50 +01:00
safe . set ( req . body , 'HostConfig.LogConfig' , { Type : 'syslog' , Config : { 'tag' : req . app . id , 'syslog-address' : ` unix:// ${ paths . SYSLOG _SOCKET _FILE } ` , '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
2023-11-27 22:07:31 +01:00
debug ( 'containersCreate: original bind mounts:' , req . body . HostConfig . Binds ) ;
2023-12-04 00:11:11 +01:00
const [ error , result ] = await safe ( volumes . list ( ) ) ;
if ( error ) return next ( new HttpError ( 500 , ` Error listing volumes: ${ error . message } ` ) ) ;
2023-11-27 22:07:31 +01:00
const volumesByName = { } ;
result . forEach ( r => volumesByName [ r . name ] = r ) ;
2018-08-15 17:23:58 +02:00
2023-10-01 12:12:02 +05:30
const binds = [ ] ;
2023-11-27 22:07:31 +01:00
for ( const bind of ( req . body . HostConfig . Binds || [ ] ) ) { // bind is of the host:container:rw format
if ( bind . startsWith ( '/app/data' ) ) {
binds . push ( bind . replace ( new RegExp ( '^/app/data/' ) , appDataDir + '/' ) ) ;
} else if ( bind . startsWith ( '/media/' ) ) {
const volumeName = bind . match ( new RegExp ( '/media/([^:/]+)/?' ) ) [ 1 ] ;
const volume = volumesByName [ volumeName ] ;
if ( volume ) binds . push ( bind . replace ( new RegExp ( ` ^/media/ ${ volumeName } ` ) , volume . hostPath ) ) ;
else debug ( ` containersCreate: dropped unknown volume ${ volumeName } ` ) ;
} else {
2020-03-29 13:38:34 -07:00
req . dockerRequest . abort ( ) ;
2023-11-27 22:07:31 +01:00
return next ( new HttpError ( 400 , 'Binds must be under /app/data/ or /media' ) ) ;
2020-03-29 13:38:34 -07:00
}
}
2018-08-15 17:23:58 +02:00
2023-11-27 22:07:31 +01:00
debug ( 'containersCreate: 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
2023-10-01 12:12:02 +05:30
const plainBody = JSON . stringify ( req . body ) ;
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 ) {
2023-10-01 12:12:02 +05:30
const plainBody = JSON . stringify ( req . body ) ;
2018-08-16 14:28:51 +02:00
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
2024-07-19 22:11:30 +02:00
const json = express . 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
2023-04-10 10:35:25 +02:00
// disable slowloris prevention: https://github.com/nodejs/node/issues/47421
gHttpServer = http . createServer ( { headersTimeout : 0 , requestTimeout : 0 } , 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-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
2023-10-01 12:12:02 +05:30
const 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
2023-12-04 00:11:11 +01:00
debug ( ` start: listening on 172.18.0.1: ${ constants . DOCKER _PROXY _PORT } ` ) ;
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 ( ) {
2024-01-23 12:38:57 +01:00
if ( ! gHttpServer ) return ;
2018-08-14 19:03:10 -07:00
2024-01-23 12:38:57 +01:00
await util . promisify ( gHttpServer . close . bind ( gHttpServer ) ) ( ) ;
2018-08-14 19:03:10 -07:00
gHttpServer = null ;
2018-08-15 16:47:06 -07:00
}