File
Blob: images/container-client-test/app.js
| 1 | const { createServer } = require('http'); |
| 2 | const net = require('net'); |
| 3 | |
| 4 | const webSocketEnabled = process.env.WS_ENABLED === 'true'; |
| 5 | const wsProxyTarget = process.env.WS_PROXY_TARGET || null; |
| 6 | const wsProxySecure = process.env.WS_PROXY_SECURE === 'true'; |
| 7 | const tcpPort = parseInt(process.env.TCP_PORT || '0', 10); |
| 8 | |
| 9 | const server = createServer(function (req, res) { |
| 10 | if (req.url === '/ws') { |
| 11 | return; |
| 12 | } |
| 13 | |
| 14 | if (req.url === '/pid-namespace') { |
| 15 | // Return the PID namespace inode. When running in an isolated PID namespace, |
| 16 | // this will differ from the host's PID namespace. We return the inode so the |
| 17 | // test can verify isolation by comparing against a known value or checking |
| 18 | // that PID 1 in this namespace is NOT the host's init process. |
| 19 | const fs = require('fs'); |
| 20 | try { |
| 21 | // Read /proc/1/cmdline to see what process is PID 1 in this namespace. |
| 22 | // In an isolated namespace, PID 1 will be our container's init process. |
| 23 | // In host namespace, PID 1 will be the host's init (e.g., systemd, launchd). |
| 24 | const init = fs.readFileSync('/proc/1/cmdline', 'utf8'); |
| 25 | res.writeHead(200, { 'Content-Type': 'application/json' }); |
| 26 | res.write( |
| 27 | JSON.stringify({ |
| 28 | pid: process.pid, |
| 29 | ppid: process.ppid, |
| 30 | init: init.replace(/\0/g, ' ').trim(), |
| 31 | }) |
| 32 | ); |
| 33 | res.end(); |
| 34 | } catch (err) { |
| 35 | res.writeHead(500, { 'Content-Type': 'text/plain' }); |
| 36 | res.write(`Error reading /proc/1/cmdline: ${err.message}`); |
| 37 | res.end(); |
| 38 | } |
| 39 | |
| 40 | return; |
| 41 | } |
| 42 | |
| 43 | // Write a file inside the container (for snapshot testing) |
| 44 | if (req.url.startsWith('/write-file')) { |
| 45 | const url = new URL(req.url, 'http://localhost'); |
| 46 | const filePath = url.searchParams.get('path'); |
| 47 | if (!filePath) { |
| 48 | res.writeHead(400, { 'Content-Type': 'text/plain' }); |
| 49 | res.write('Missing "path" query param'); |
| 50 | res.end(); |
| 51 | return; |
| 52 | } |
| 53 | const fs = require('fs'); |
| 54 | const path = require('path'); |
| 55 | let body = ''; |
| 56 | req.on('data', (chunk) => (body += chunk)); |
| 57 | req.on('end', () => { |
| 58 | try { |
| 59 | fs.mkdirSync(path.dirname(filePath), { recursive: true }); |
| 60 | fs.writeFileSync(filePath, body); |
| 61 | res.writeHead(200, { 'Content-Type': 'text/plain' }); |
| 62 | res.write('ok'); |
| 63 | res.end(); |
| 64 | } catch (err) { |
| 65 | res.writeHead(500, { 'Content-Type': 'text/plain' }); |
| 66 | res.write(err.message); |
| 67 | res.end(); |
| 68 | } |
| 69 | }); |
| 70 | return; |
| 71 | } |
| 72 | |
| 73 | // Read a file inside the container (for snapshot testing) |
| 74 | if (req.url.startsWith('/read-file')) { |
| 75 | const url = new URL(req.url, 'http://localhost'); |
| 76 | const filePath = url.searchParams.get('path'); |
| 77 | if (!filePath) { |
| 78 | res.writeHead(400, { 'Content-Type': 'text/plain' }); |
| 79 | res.write('Missing "path" query param'); |
| 80 | res.end(); |
| 81 | return; |
| 82 | } |
| 83 | const fs = require('fs'); |
| 84 | try { |
| 85 | const content = fs.readFileSync(filePath, 'utf8'); |
| 86 | res.writeHead(200, { 'Content-Type': 'text/plain' }); |
| 87 | res.write(content); |
| 88 | res.end(); |
| 89 | } catch (err) { |
| 90 | res.writeHead(404, { 'Content-Type': 'text/plain' }); |
| 91 | res.write(err.message); |
| 92 | res.end(); |
| 93 | } |
| 94 | return; |
| 95 | } |
| 96 | |
| 97 | if (req.url === '/intercept') { |
| 98 | const targetHost = req.headers['x-host'] || '11.0.0.1'; |
| 99 | fetch(`http://${targetHost}`) |
| 100 | .then((result) => result.text()) |
| 101 | .then((body) => { |
| 102 | res.writeHead(200); |
| 103 | res.write(body); |
| 104 | res.end(); |
| 105 | }) |
| 106 | .catch((err) => { |
| 107 | res.writeHead(500); |
| 108 | res.write(`${targetHost} ${err.message}`); |
| 109 | res.end(); |
| 110 | }); |
| 111 | |
| 112 | return; |
| 113 | } |
| 114 | |
| 115 | // Make a raw TCP connection to x-tcp-target header (host:port), send |
| 116 | // "ping\n", read the response, and return it over HTTP. |
| 117 | if (req.url === '/intercept-tcp') { |
| 118 | const target = req.headers['x-tcp-target']; |
| 119 | if (!target) { |
| 120 | res.writeHead(400, { 'Content-Type': 'text/plain' }); |
| 121 | res.write('Missing x-tcp-target header'); |
| 122 | res.end(); |
| 123 | return; |
| 124 | } |
| 125 | |
| 126 | const [host, portStr] = target.split(':'); |
| 127 | const port = parseInt(portStr, 10); |
| 128 | const socket = net.createConnection({ host, port }, () => { |
| 129 | socket.write('ping\n'); |
| 130 | }); |
| 131 | |
| 132 | let data = ''; |
| 133 | socket.on('data', (chunk) => { |
| 134 | data += chunk.toString(); |
| 135 | socket.end(); |
| 136 | }); |
| 137 | |
| 138 | socket.on('close', () => { |
| 139 | res.writeHead(200, { 'Content-Type': 'text/plain' }); |
| 140 | res.write(data); |
| 141 | res.end(); |
| 142 | }); |
| 143 | |
| 144 | socket.on('error', (err) => { |
| 145 | res.writeHead(500, { 'Content-Type': 'text/plain' }); |
| 146 | res.write(`TCP error: ${err.message}`); |
| 147 | res.end(); |
| 148 | }); |
| 149 | |
| 150 | // Give it a timeout so the test doesn't hang forever. |
| 151 | socket.setTimeout(5000, () => { |
| 152 | socket.destroy(new Error('TCP connection timed out')); |
| 153 | }); |
| 154 | return; |
| 155 | } |
| 156 | |
| 157 | if (req.url === '/intercept-https') { |
| 158 | const targetHost = req.headers['x-host'] || 'example.com'; |
| 159 | fetch(`https://${targetHost}`) |
| 160 | .then((result) => result.text()) |
| 161 | .then((body) => { |
| 162 | res.writeHead(200); |
| 163 | res.write(body); |
| 164 | res.end(); |
| 165 | }) |
| 166 | .catch((err) => { |
| 167 | res.writeHead(500); |
| 168 | res.write(`${targetHost} ${err.message}`); |
| 169 | res.end(); |
| 170 | }); |
| 171 | |
| 172 | return; |
| 173 | } |
| 174 | |
| 175 | res.writeHead(200, { 'Content-Type': 'text/plain' }); |
| 176 | res.write('Hello World!'); |
| 177 | res.end(); |
| 178 | }); |
| 179 | |
| 180 | if (webSocketEnabled) { |
| 181 | const WebSocket = require('ws'); |
| 182 | const wss = new WebSocket.Server({ server, path: '/ws' }); |
| 183 | |
| 184 | wss.on('connection', function (clientWs) { |
| 185 | if (wsProxyTarget) { |
| 186 | const protocol = wsProxySecure ? 'wss' : 'ws'; |
| 187 | const targetWs = new WebSocket(`${protocol}://${wsProxyTarget}/ws`); |
| 188 | const ready = new Promise(function (resolve) { |
| 189 | targetWs.on('open', resolve); |
| 190 | }); |
| 191 | |
| 192 | targetWs.on('message', (data) => clientWs.send(data)); |
| 193 | clientWs.on('message', async function (data) { |
| 194 | await ready; |
| 195 | targetWs.send(data); |
| 196 | }); |
| 197 | |
| 198 | clientWs.on('close', targetWs.close); |
| 199 | targetWs.on('close', clientWs.close); |
| 200 | } else { |
| 201 | clientWs.on('message', function (data) { |
| 202 | clientWs.send('Echo: ' + data.toString()); |
| 203 | }); |
| 204 | } |
| 205 | }); |
| 206 | } |
| 207 | |
| 208 | server.listen(8080, function () { |
| 209 | console.log('Server listening on port 8080'); |
| 210 | if (webSocketEnabled) { |
| 211 | console.log('WebSocket support enabled'); |
| 212 | } |
| 213 | }); |
| 214 | |
| 215 | // Optional TCP echo server used by TCP egress intercept tests. |
| 216 | // When TCP_PORT is set to a non-zero value, we start a plain TCP server |
| 217 | // that echoes back whatever it receives prefixed with "echo:". |
| 218 | if (tcpPort > 0) { |
| 219 | const tcpServer = net.createServer(function (socket) { |
| 220 | socket.on('data', function (chunk) { |
| 221 | socket.write('echo:' + chunk.toString()); |
| 222 | socket.end(); |
| 223 | }); |
| 224 | }); |
| 225 | |
| 226 | tcpServer.listen(tcpPort, function () { |
| 227 | console.log('TCP echo server listening on port ' + tcpPort); |
| 228 | }); |
| 229 | } |