Skip to content
File

Blob: images/container-client-test/app.js

javascript230 lines
1const { createServer } = require('http');
2const net = require('net');
3 
4const webSocketEnabled = process.env.WS_ENABLED === 'true';
5const wsProxyTarget = process.env.WS_PROXY_TARGET || null;
6const wsProxySecure = process.env.WS_PROXY_SECURE === 'true';
7const tcpPort = parseInt(process.env.TCP_PORT || '0', 10);
8 
9const 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 
180if (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 
208server.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:".
218if (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}