Protect generated host key and polish sessions

This commit is contained in:
2026-07-27 09:26:04 +09:00
parent b0eb054f88
commit 79f2386a24
3 changed files with 10 additions and 34 deletions
+1
View File
@@ -1,4 +1,5 @@
node_modules/
.env
data/config.json
data/host.key
*.log
-27
View File
@@ -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-----
+9 -7
View File
@@ -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 <page>';
if (command === 'contact') return `Email: ${config.persona?.email || 'not configured'}`;
if (command === 'chat') { if (!arg) return 'Usage: chat <message>'; 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 <message>'; 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');