Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import apps from './apps.js' ;
import assert from 'node:assert' ;
import constants from './constants.js' ;
import express from 'express' ;
2026-03-12 22:55:28 +05:30
import logger from './logger.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import http from 'node:http' ;
import { HttpError } from '@cloudron/connect-lastmile' ;
2026-02-14 15:43:24 +01:00
import middleware from './middleware/index.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import net from 'node:net' ;
import path from 'node:path' ;
import paths from './paths.js' ;
import safe from 'safetydance' ;
import util from 'node:util' ;
2026-02-14 15:43:24 +01:00
import volumes from './volumes.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
2026-03-12 22:55:28 +05:30
const { log , trace } = logger ( 'dockerproxy' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
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 ) {
2025-06-10 11:02:41 +02:00
req . resources . app = { id : 'testappid' } ;
2018-08-15 17:23:58 +02:00
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
2025-06-10 11:02:41 +02:00
req . resources . 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
2026-03-12 22:55:28 +05:30
dockerResponse . on ( 'error' , function ( error ) { log ( '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
2025-06-10 11:02:41 +02:00
safe . set ( req . body , 'Labels' , Object . assign ( { } , safe . query ( req . body , 'Labels' ) , { appId : req . resources . app . id , isCloudronManaged : String ( false ) } ) ) ; // overwrite the app id to track containers of an app
safe . set ( req . body , 'HostConfig.LogConfig' , { Type : 'syslog' , Config : { 'tag' : req . resources . app . id , 'syslog-address' : ` unix:// ${ paths . SYSLOG _SOCKET _FILE } ` , 'syslog-format' : 'rfc5424' } } ) ;
2018-08-14 20:20:19 -07:00
2025-06-10 11:02:41 +02:00
const appDataDir = path . join ( paths . APPS _DATA _DIR , req . resources . app . id , 'data' ) ;
2018-08-15 17:23:58 +02:00
2026-03-12 22:55:28 +05:30
log ( 'containersCreate: original bind mounts:' , req . body . HostConfig . Binds ) ;
2023-11-27 22:07:31 +01:00
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 ) ) ;
2026-03-12 22:55:28 +05:30
else log ( ` containersCreate: dropped unknown volume ${ volumeName } ` ) ;
2023-11-27 22:07:31 +01:00
} 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
2026-03-12 22:55:28 +05:30
log ( '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
2025-06-06 18:07:03 +02:00
if ( req . body && 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 ) {
2026-03-12 22:55:28 +05:30
log ( 'proxying: ' + req . method , req . url ) ;
2018-08-20 20:10:14 -07:00
next ( ) ;
} ) ;
}
2018-08-16 14:34:55 +02:00
2025-06-10 11:27:26 +02:00
proxyServer
2025-06-10 11:02:41 +02:00
. use ( ( req , res , next ) => {
// we store our route resources, like app,volumes,... in req.resources. Those are added in the load() routes
req . resources = { } ;
next ( ) ;
} )
2025-06-10 11:27:26 +02:00
. use ( authorizeApp )
. use ( attachDockerRequest )
. 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
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-02-19 13:33:02 +01:00
gHttpServer . closeAllConnections ( ) ;
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
}
2026-02-14 15:43:24 +01:00
export default {
start ,
stop
} ;