From 79f2386a249765f2fdfe2ad46ad50d75e8998801 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Jul 2026 09:26:04 +0900 Subject: [PATCH] Protect generated host key and polish sessions --- .gitignore | 1 + data/host.key | 27 --------------------------- src/server.js | 16 +++++++++------- 3 files changed, 10 insertions(+), 34 deletions(-) delete mode 100644 data/host.key diff --git a/.gitignore b/.gitignore index 92210d2..4657ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ .env data/config.json +data/host.key *.log diff --git a/data/host.key b/data/host.key deleted file mode 100644 index 1d8c651..0000000 --- a/data/host.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAsFtKu2Sw4Y20f2vBfkFvKjO+p02pfFHD+IeSl6z9+8TREKJt -Uu0QbAr+h0LK2Y7Bk/OJHVjk0B/Bg5MPw9VO8hrMUurj0ftpGmhNHSuRVS/X4AMy -R81YNNuCK3dbm0pFf/e7SW0ur+pIBkqLnJvQgDjPJh1kf+oqOIhfiIRMfanlpLy6 -prk/3yVKHSe6zsM+fWS4fx1oKo22rVSqol/35V8HW1KxE4CwVPfC4nZEamz4KiH2 -wLVgv0Z+J7IBwhvL6jOMwlZGaNTnoNts5JN2lFHzMuYfSBMi0+3cfQ8Oc2OpHQBe -8btdEflFY8gD+TyJgdXlvi6n5XvvX/Ljm9FD9wIDAQABAoIBAA6aOIlksHq0o43r -a7WbEGvTpgUvWonHMW1aOGqjsdIPfWodsOStaW8wx9uGTcqO1ft8rk1GR4fuITVW -T3ddKbDFREbXWS7RXOYJXd089scxc3QCf77PQ275AAG+W9DzaObMKe9OAZAiW31d -3cqFYlHDom9a/q/SwVU2AYcJwVWkQQD9Y4aMN2XD1bkzEmlJIxi4cOT/APkZhlJz -GUb+wT5qeERbwYsVbXDu46ABlnIj6+THtUjvCIrM2IdKSniquwDBqFv1NCpOuWDy -dNu3Q4lQIUoixRUOLkmHwIuVvipTFjS3I6hUuLVmFDBqRpIgvyri9285sgs2BqHR -g5yV2j0CgYEA28Tv7+EvS/aUuC1rM1+7n4mSwFwC5lO8AVepsEULet9KJxFdyk/U -UAK6XeUjHhzDgP9N9rogiEqROUAyjBeFcW1xygdhq+1RcpccO98mxxnPMlfo4RXe -Rc6xmxf0/I+eLDMtF2ii3qFlc2LLXDytkseEU0jcJ8U9d825O8Tbau0CgYEAzW4u -kDq2XxfM6JjTEup8n2hFqam5ieCz347raNiZYKOvptArxYlXe2gjL388uEE8xUf0 -VgtUyCQQ8KRH38RV+VRFutKojNMTwmwLrDyLOE8BuPDgREQ3HJR7rgx35BQpec8h -jNymX6DTJlrJtLbn7G/qekWG5nRzH24tChQsOfMCgYAchhEGN1nmKBjhVrPFNTWq -3TY41ThjIJcULYaOpEd7KSP8shpUOcw6nkAGE8ldDSqqwXgi/8FdLyBuJ9Dnsd3V -/Ph52j9pCyI5FzfI5Yuo58amqBZHsIjq0ormsFNRvGoxHmtJ0IDjGFuMgFQYop98 -n3yjOp+oYcc6l27lPQWleQKBgBcZQYEUKPduTKsGrA0fcp3V8qdOHz+ilXcYsd2b -JTvZi+Gc3J8cf0qv/ezUSLaLF1MzcQTPx1utun3kavR9NhGP6TtFfpA/F6Sv02wt -bpYJRC9QXlcuuGetRm7elgh+P9+ja6lUEh0Ej1lrsexw+Ij8WIghEzcMdvlcwRG2 -DPcDAoGBAM74utJYm2bEiQGmtC16B7c7wdevA6yIsERcgUEdjb8GH3dMed1bcZJ1 -fFriktFd9iCgxEv/p/jhdMMqY/Ahi/izN3658+mO4NhhwNh0NRCNToVVz1kc9qpd -DgZlyYuqEkYRlWFMc3lAKUTEhh4gS41mGHTIN51WlY91jw7ycAEd ------END RSA PRIVATE KEY----- diff --git a/src/server.js b/src/server.js index 01f7f17..104084c 100644 --- a/src/server.js +++ b/src/server.js @@ -20,7 +20,8 @@ function count() { return sessions.size; } function sendNotification(message) { for (const socket of notifications) socket.write(JSON.stringify({ type: 'notification', message }) + '\n'); } function render(ctx, body = '') { const p = config.persona || {}; const primary = config.theme?.primary || 'cyan'; - return `${clear}${color(primary, `● ${p.name || 'Your Name'} — ${p.tagline || ''}`)}\n${color(config.theme?.muted || 'brightBlack', `${p.location || ''} • ${count()} connected`)}\n\n${body}\n\n${color(primary, 'portfolio')} ${isAdmin(ctx) ? color('yellow', '[admin]') : ''}> `; + const title = `● ${p.name || 'Your Name'} — ${p.tagline || ''}`; const users = `${count()} connected`; const gap = ' '.repeat(Math.max(2, (ctx.cols || 80) - title.length - users.length)); + return `${clear}${color(primary, title)}${gap}${color(config.theme?.muted || 'brightBlack', users)}\n${color(config.theme?.muted || 'brightBlack', p.location || '')}\n\n${body}\n\n${color(primary, 'portfolio')} ${isAdmin(ctx) ? color('yellow', '[admin]') : ''}> `; } function page(ctx, name) { const p = config.pages?.[name]; if (!p) return `${color('red', `No page named “${name}”.`)}\nTry: pages`; @@ -35,21 +36,22 @@ function execute(ctx, input) { if (command === 'pages') return Object.entries(config.pages || {}).map(([k, v]) => `${color(config.theme?.accent || 'yellow', k.padEnd(14))} ${v.title || ''}`).join('\n') || 'No pages configured.'; if (command === 'open') return arg ? page(ctx, arg) : 'Usage: open '; if (command === 'contact') return `Email: ${config.persona?.email || 'not configured'}`; - if (command === 'chat') { if (!arg) return 'Usage: chat '; const line = `${config.persona?.name || 'visitor'}: ${arg}`; for (const s of sessions) s.write(`\r\n${color(config.theme?.accent || 'yellow', line)}\r\n` + render(s.ctx)); sendNotification(line); return 'Message sent.'; } + if (command === 'chat') { if (!arg) return 'Usage: chat '; const author = isAdmin(ctx) ? `${config.persona?.name || 'Owner'} [owner]` : (ctx.username || 'visitor'); const line = `${author}: ${arg}`; for (const s of sessions) s.channel.write(`\r\n${color(config.theme?.accent || 'yellow', line)}\r\n` + render(s.ctx)); sendNotification(line); return 'Message sent.'; } if (command === 'reload' && isAdmin(ctx)) { Object.assign(config, loadConfig()); return 'Configuration reloaded.'; } if (command === 'theme' && isAdmin(ctx) && arg) { config.theme.primary = arg; return `Primary color set to ${arg} (runtime only; edit config to persist).`; } - if (command === 'announce' && isAdmin(ctx) && arg) { for (const s of sessions) s.write(`\r\n${color(config.theme?.accent || 'yellow', `OWNER: ${arg}`)}\r\n` + render(s.ctx)); sendNotification(`OWNER: ${arg}`); return 'Announcement sent.'; } + if (command === 'announce' && isAdmin(ctx) && arg) { for (const s of sessions) s.channel.write(`\r\n${color(config.theme?.accent || 'yellow', `OWNER: ${arg}`)}\r\n` + render(s.ctx)); sendNotification(`OWNER: ${arg}`); return 'Announcement sent.'; } if (command === 'quit' || command === 'exit') { ctx.channel.end(); return ''; } return `Unknown command: ${command}. Try “help”.`; } function handleSession(channel, ctx) { - let buffer = ''; ctx.channel = channel; sessions.add(ctx); channel.write(render(ctx, home(ctx))); + let buffer = ''; ctx.channel = channel; const active = { channel, ctx }; sessions.add(active); channel.write(render(ctx, home(ctx))); channel.on('data', data => { for (const byte of data.toString()) { if (byte === '\r' || byte === '\n') { channel.write(`\r\n${execute(ctx, buffer)}\n` + render(ctx)); buffer = ''; } else if (byte === '\x7f') { if (buffer) { buffer = buffer.slice(0, -1); channel.write('\b \b'); } } else if (byte >= ' ') { buffer += byte; channel.write(byte); } } }); - channel.on('close', () => sessions.delete(ctx)); + channel.on('close', () => sessions.delete(active)); } const server = new Server({ hostKeys: [fs.readFileSync(hostKey)], ident: 'ssh-portfolio' }, client => { - const ctx = { admin: false, channel: null }; + const ctx = { admin: false, channel: null, cols: 80, username: 'visitor' }; client.on('authentication', ctxAuth => { + ctx.username = ctxAuth.username || 'visitor'; if (ctxAuth.method === 'none') { ctxAuth.accept(); return; } if (ctxAuth.method === 'publickey' && ctxAuth.key) { const key = ctxAuth.key.data.toString('base64'); @@ -57,7 +59,7 @@ const server = new Server({ hostKeys: [fs.readFileSync(hostKey)], ident: 'ssh-po if (allowed && (!ctxAuth.signature || ctxAuth.key.verify(ctxAuth.blob, ctxAuth.signature, ctxAuth.hashAlgo))) { ctx.admin = true; ctxAuth.accept(); return; } } ctxAuth.reject(['none', 'publickey']); - }).on('ready', () => client.on('session', accept => { const sshSession = accept(); sshSession.on('shell', (acceptShell) => handleSession(acceptShell(), ctx)); sshSession.on('exec', (acceptExec, rejectExec, info) => { const e = acceptExec(); e.write((execute(ctx, info.command) || '') + '\n'); e.exit(0); e.end(); }); })).on('error', () => {}); + }).on('ready', () => client.on('session', accept => { const sshSession = accept(); sshSession.on('pty', (acceptPty, rejectPty, info) => { ctx.cols = info.cols || 80; acceptPty(); }); sshSession.on('shell', (acceptShell) => handleSession(acceptShell(), ctx)); sshSession.on('exec', (acceptExec, rejectExec, info) => { const e = acceptExec(); e.write((execute(ctx, info.command) || '') + '\n'); e.exit(0); e.end(); }); })).on('error', () => {}); }); server.listen(config.host.port, '0.0.0.0', () => console.log(`SSH portfolio listening on port ${config.host.port}; config: ${configPath}`)); net.createServer(socket => { notifications.add(socket); socket.on('data', () => {}); socket.on('close', () => notifications.delete(socket)); }).listen(config.host.notificationPort, '127.0.0.1');