// Tiny Docker Engine API client over the bind-mounted unix socket. // // Extracted from service-routes.js so other routes (per-container stats, // system df, etc.) can talk to the daemon without duplicating the socket- // discovery + http-over-unix-socket dance. // // We deliberately do NOT add the `dockerode` package — node's built-in http // agent already supports `socketPath`, and the small subset of the Engine // API we use fits in a couple of dozen lines. Zero extra deps, easy to audit. const fs = require('fs'); const http = require('http'); // Whichever socket the host bind-mounted into the container is the one we // can reach. Rooted installs mount /var/run/docker.sock; rootless mounts // /run/user//docker.sock under the runtime dir. function detectDockerSocket() { if (fs.existsSync('/var/run/docker.sock')) return '/var/run/docker.sock'; try { for (const entry of fs.readdirSync('/run/user', { withFileTypes: true })) { if (!entry.isDirectory()) continue; const sock = `/run/user/${entry.name}/docker.sock`; if (fs.existsSync(sock)) return sock; } } catch { /* /run/user not readable — fine */ } return null; } const DOCKER_SOCKET = detectDockerSocket(); const DOCKER_API_VERSION = 'v1.41'; // Docker 20.10+ // Simple JSON GET (or other method without a body). Returns parsed JSON. function dockerRequest(method, pathname, query) { return new Promise((resolve, reject) => { if (!DOCKER_SOCKET) return reject(new Error('No docker socket available')); const qs = query ? '?' + new URLSearchParams(query).toString() : ''; const req = http.request( { socketPath: DOCKER_SOCKET, method, path: `/${DOCKER_API_VERSION}${pathname}${qs}`, headers: { Host: 'docker', Accept: 'application/json' }, }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const body = Buffer.concat(chunks).toString('utf8'); if (res.statusCode >= 200 && res.statusCode < 300) { try { resolve(body ? JSON.parse(body) : null); } catch (e) { reject(new Error(`Docker API parse error: ${e.message}`)); } } else { reject(new Error(`Docker API ${res.statusCode}: ${body}`)); } }); } ); req.on('error', reject); req.end(); }); } // Streaming GET — caller gets the raw IncomingMessage so they can pipe // or parse multiplexed log frames themselves. function dockerStream(pathname, query) { return new Promise((resolve, reject) => { if (!DOCKER_SOCKET) return reject(new Error('No docker socket available')); const qs = query ? '?' + new URLSearchParams(query).toString() : ''; const req = http.request( { socketPath: DOCKER_SOCKET, method: 'GET', path: `/${DOCKER_API_VERSION}${pathname}${qs}`, headers: { Host: 'docker' }, }, (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ stream: res, req }); } else { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => reject(new Error( `Docker API ${res.statusCode}: ${Buffer.concat(chunks).toString('utf8')}` ))); } } ); req.on('error', reject); req.end(); }); } // Docker's log frames over the API are multiplexed when no TTY is attached. // Each frame: 8-byte header [stream(1) 0 0 0 size(4 BE)] + N payload bytes. // stream: 0=stdin (unused), 1=stdout, 2=stderr. This decoder concatenates // the payload as a single string with no markers (callers don't care about // per-stream tagging for our use cases — they just want the text). If a // container WAS started with -t, frames are raw text with no header; we // detect that by failing to parse a sane header and falling back to a raw // utf-8 decode. function decodeMultiplexedLog(buf) { if (!Buffer.isBuffer(buf) || buf.length === 0) return ''; const out = []; let i = 0; let sawValidFrame = false; while (i + 8 <= buf.length) { const stream = buf[i]; if (stream > 2) break; // not a header — bail to raw fallback const size = buf.readUInt32BE(i + 4); const end = i + 8 + size; if (end > buf.length) break; out.push(buf.slice(i + 8, end).toString('utf8')); sawValidFrame = true; i = end; } if (!sawValidFrame) return buf.toString('utf8'); return out.join(''); } module.exports = { DOCKER_SOCKET, DOCKER_API_VERSION, dockerRequest, dockerStream, decodeMultiplexedLog, };