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}`); });