From f45a1088088ab7316cbd7398bc6c494b96507f45 Mon Sep 17 00:00:00 2001 From: l-nmch Date: Mon, 11 May 2026 15:50:11 +0200 Subject: [PATCH] chore(core): Added bare TCP access --- .dockerignore | 2 + Dockerfile | 10 ++-- commands.js | 68 +++++++++++++++++++++++ server.js | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 .dockerignore create mode 100644 commands.js create mode 100644 server.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9b63d53 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +LICENSE +README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 27d7f24..dfee474 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,9 @@ -FROM nginx:stable-alpine +FROM node:18-alpine -WORKDIR /usr/share/nginx/html +WORKDIR /usr/src/app -COPY index.html ./ -COPY src ./src -COPY assets ./assets +COPY . . EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +CMD ["node", "server.js"] diff --git a/commands.js b/commands.js new file mode 100644 index 0000000..e916ebf --- /dev/null +++ b/commands.js @@ -0,0 +1,68 @@ +const services = [ + { name: 'Dev Workspaces', url: 'https://coder.phorge.fr' }, + { name: 'Monitoring', url: 'https://monitoring.phorge.fr' }, + { name: 'Status page', url: 'https://status.phorge.fr/status' }, + { name: 'Git', url: 'https://git.phorge.fr' }, + { name: 'IaaS', url: 'https://iaas.phorge.fr' }, +]; + +const commands = [ + { + names: ['/services'], + description: 'Displays available services', + execute: (socket) => { + socket.write('Available services:\r\n'); + for (const service of services) { + socket.write(`- ${service.name}: ${service.url}\r\n`); + } + }, + }, + { + names: ['/help', 'help'], + description: 'Displays command help', + execute: (socket) => { + socket.write('Available commands:\r\n'); + for (const command of commands) { + socket.write(`- ${command.names[0]}: ${command.description}\r\n`); + } + }, + }, + { + names: ['/exit', 'quit'], + description: 'Exits the terminal session', + execute: (socket) => { + socket.write('Goodbye.\r\n'); + socket.end(); + }, + }, +]; + +const resolveCommand = (input) => { + const normalized = input.trim().toLowerCase(); + return commands.find((command) => command.names.includes(normalized)); +}; + +const handleCommand = (socket, input) => { + const trimmed = input.trim(); + if (!trimmed) { + socket.write('phorge> '); + return; + } + + const command = resolveCommand(trimmed); + if (command) { + command.execute(socket); + if (command.names.includes('/exit') || command.names.includes('quit')) { + return; + } + } else { + socket.write(`Unknown command : ${trimmed}\r\n`); + socket.write('Type /help for assistance.\r\n'); + } + + socket.write('phorge> '); +}; + +module.exports = { + handleCommand, +}; diff --git a/server.js b/server.js new file mode 100644 index 0000000..4813a42 --- /dev/null +++ b/server.js @@ -0,0 +1,148 @@ +const fs = require('fs'); +const path = require('path'); +const net = require('net'); +const { handleCommand } = require('./commands'); + +const asciiLogo = ` + **==+= === + ===+ %========= +====== ++* =*==+ =====+=+=== ++====*+ + ===+ === +=== ============ ==========* ==========- ============ + ===+ %=== +=== ===* ==== ====+ === +=== %=== +=== + ===# %=== ==== ===* ===# ===+ *==# === === ===* +%=== %=== ==== #=== ==== === ==+ === %==+*********==== + ===# %=== +=== -==+ *=== ==+ +=== *===# *================ + ===+ %=== ====* #=== ==== ==+ ========+ %==+ + ====+ %=== +===+ ===* ===# ==+ === === + *=============== *==== ==== ==+ ===+ *===+ ===* + =*======*+ ============# ==+ ============+ ============% + %=== =+====++ **# ===+= ===++==== =*====++ + %=== === === + %=== ===+ ==== + %=== =============== + *======= `; + +const contentTypes = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.ico': 'image/x-icon', + '.json': 'application/json; charset=utf-8', +}; + +const buildPath = (requestedPath) => { + let safePath = requestedPath.split('?')[0].split('#')[0]; + safePath = decodeURIComponent(safePath); + safePath = safePath.replace(/^\//, ''); + if (!safePath || safePath === '') { + return path.join(__dirname, 'index.html'); + } + + const candidate = path.join(__dirname, safePath); + if (!candidate.startsWith(__dirname)) { + return null; + } + return candidate; +}; + +const isHttpRequest = (chunk) => { + return /^(GET|POST|HEAD|PUT|DELETE|OPTIONS|PATCH|TRACE|CONNECT)\s+/i.test(chunk); +}; + +const sendHttpResponse = (socket, statusCode, statusText, headers, body) => { + const head = [`HTTP/1.1 ${statusCode} ${statusText}`]; + for (const [key, value] of Object.entries(headers)) { + head.push(`${key}: ${value}`); + } + head.push('Connection: close'); + head.push(''); + if (body) { + socket.end(head.join('\r\n') + '\r\n' + body); + } else { + socket.end(head.join('\r\n') + '\r\n'); + } +}; + +const handleHttp = (socket, requestData) => { + const [requestLine] = requestData.split('\r\n'); + const [method, requestPath] = requestLine.split(' '); + const filePath = buildPath(requestPath); + + if (!filePath) { + return sendHttpResponse(socket, 400, 'Bad Request', { 'Content-Type': 'text/plain; charset=utf-8' }, '400 Bad Request'); + } + + fs.stat(filePath, (err, stats) => { + if (err || !stats.isFile()) { + return sendHttpResponse(socket, 404, 'Not Found', { 'Content-Type': 'text/plain; charset=utf-8' }, '404 Not Found'); + } + + fs.readFile(filePath, (readErr, data) => { + if (readErr) { + return sendHttpResponse(socket, 500, 'Internal Server Error', { 'Content-Type': 'text/plain; charset=utf-8' }, '500 Internal Server Error'); + } + + const ext = path.extname(filePath).toLowerCase(); + const contentType = contentTypes[ext] || 'application/octet-stream'; + sendHttpResponse(socket, 200, 'OK', { 'Content-Type': contentType, 'Content-Length': Buffer.byteLength(data) }, data); + }); + }); +}; + +const createTerminalSession = (socket, initialData) => { + socket.write(asciiLogo + '\r\n\r\n'); + socket.write('Open-Source & Community first Cloud Provider (/help for assistance)\r\n'); + socket.write('phorge> '); + + let buffer = ''; + if (initialData) { + buffer += initialData; + } + + const flushLines = () => { + let index; + while ((index = buffer.indexOf('\n')) !== -1) { + let line = buffer.slice(0, index).replace(/\r$/, ''); + buffer = buffer.slice(index + 1); + handleCommand(socket, line); + } + }; + + socket.on('data', (chunk) => { + buffer += chunk.toString('utf8'); + flushLines(); + }); + + socket.on('error', () => { + socket.destroy(); + }); +}; + +const server = net.createServer((socket) => { + socket.setEncoding('utf8'); + let hasHandled = false; + + const onFirstData = (chunk) => { + if (hasHandled) return; + hasHandled = true; + const data = chunk.toString('utf8'); + if (isHttpRequest(data)) { + handleHttp(socket, data); + } else { + createTerminalSession(socket, data); + } + }; + + socket.once('data', onFirstData); + + socket.on('error', () => { + socket.destroy(); + }); +}); + +const PORT = process.env.PORT || 80; +server.listen(PORT, () => { + console.log(`Phorge server listening on port ${PORT}`); +});