Files
cloudron-box/src/dockerproxy.js

188 lines
7.1 KiB
JavaScript
Raw Normal View History

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'),
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'),
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) {
// make the tests pass for now
if (constants.TEST) {
req.app = { id: 'testappid' };
return next();
}
2018-08-14 19:35:14 -07: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
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
req.dockerRequest.on('error', () => {}); // abort() throws
2018-08-14 18:27:08 -07:00
next();
}
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
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
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
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
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
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}`));
const volumesByName = {};
result.forEach(r => volumesByName[r.name] = r);
2023-10-01 12:12:02 +05:30
const binds = [];
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 {
req.dockerRequest.abort();
return next(new HttpError(400, 'Binds must be under /app/data/ or /media'));
}
}
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) {
// we have to rebuild the body since we consumed in in the parser
if (Object.keys(req.body).length !== 0) {
2023-10-01 12:12:02 +05:30
const 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
const json = express.json({ strict: true });
// 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();
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-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());
// disable slowloris prevention: https://github.com/nodejs/node/issues/47421
gHttpServer = http.createServer({ headersTimeout: 0, requestTimeout: 0 }, proxyServer);
// 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' +
`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';
upgradeMessage += 'Content-Type: application/json\r\n';
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
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
}