OverviewFilesCommitsIssuesDocsReleasesPeers

hypermosh-shell / bin / hypermosh.mjs

152 lines · javascript ref:
#!/usr/bin/env node
// hypermosh — resilient P2P mosh sessions to peers reached by public key.
//
//   hypermosh id                             print this daemon's server pubkey
//   hypermosh serve [--hyperident [--firewall <f> | --disable-firewall]]
//                                            run the session daemon
//   hypermosh [user@]<name-or-pubkey> [-- cmd]   open a mosh session
//   hypermosh add-name <name> <pubkey>       remember a name -> pubkey (optional)
//   hypermosh names                          list known names
//
// AUTH — two models:
//   • DEFAULT (SSH-style login): the tunnel carries a normal SSH connection to
//     the server's system sshd, so password / ssh-key / authorized_keys all work
//     exactly like `mosh user@host`. sshd is the auth gate; the hyperdht firewall
//     is open. This is what most people want.
//   • --hyperident: no SSH — auth is the hyperdht pubkey firewall (allow-list of
//     client identity keys). The client presents its ~/.hyperident device key.
//
// A raw public key ALWAYS works; names are an optional convenience.
import process from '#node:process'
import cp from '#node:child_process'
import { serveSession, connectSession } from '../lib/session.mjs'
import { serveSSH, connectSSH } from '../lib/ssh-mode.mjs'
import { loadOrCreateKey, loadDeviceKeyPair, readAllowed, HypercoreId } from '../lib/identity.mjs'
import { resolveAsync as resolveName } from '../lib/names.mjs'
import { addName, loadNames } from '../lib/names.mjs'

function flags (argv) {
  const pos = []; const f = {}
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i]
    if (a === '--') { f._rest = argv.slice(i + 1); break }
    if (a === '-v' || a === '--verbose') { f.verbose = true; continue }
    if (a.startsWith('--')) { const k = a.slice(2); const v = (argv[i + 1] && !argv[i + 1].startsWith('--')) ? argv[++i] : true; f[k] = v }
    else pos.push(a)
  }
  return { pos, f }
}

function usage () {
  console.error('usage:\n' +
    '  hypermosh id\n' +
    '  hypermosh serve [--hyperident [--firewall <f> | --disable-firewall]]\n' +
    '  hypermosh [user@]<name-or-pubkey> [-- cmd...]     (SSH-style login by default)\n' +
    '  hypermosh add-name <name> <pubkey>\n' +
    '  hypermosh names\n' +
    '\noptions: -v/--verbose (or HYPERMOSH_DEBUG=1) for connection logging')
  process.exit(1)
}

// Split an optional "user@" prefix off the name/pubkey target.
function splitTarget (s) {
  const at = s.indexOf('@')
  if (at === -1) return { user: null, host: s }
  return { user: s.slice(0, at), host: s.slice(at + 1) }
}

async function main () {
  const [cmd, ...rest] = process.argv.slice(2)
  const { pos, f } = flags(rest)
  if (f.verbose) process.env.HYPERMOSH_DEBUG = '1'

  if (cmd === 'id') { console.log(HypercoreId.encode(loadOrCreateKey().publicKey)); return }

  if (cmd === 'add-name') {
    if (!pos[0] || !pos[1]) usage()
    addName(pos[0], pos[1])
    console.log('added: ' + pos[0] + ' -> ' + pos[1])
    return
  }

  if (cmd === 'names') {
    const map = loadNames()
    if (map.size === 0) console.log('(no names — add one with: hypermosh add-name <name> <pubkey>)')
    else for (const [n, k] of map) console.log(n + '  ' + k)
    return
  }

  if (cmd === 'serve') {
    const keyPair = loadOrCreateKey()
    if (f.hyperident) {
      // Pubkey-firewall mode. Empty allow-list == deny-all — proclaim it loudly.
      const allow = f['disable-firewall'] ? true : readAllowed(f.firewall)
      const s = await serveSession({ keyPair, allow })
      console.log('hypermosh daemon serving as ' + HypercoreId.encode(s.publicKey) + '  [--hyperident]')
      if (allow === true) {
        console.log('  firewall: DISABLED — ANY peer with this key may connect')
      } else if (allow.length === 0) {
        console.error('  ⚠  HYPERIDENT FIREWALL ENABLED WITH EMPTY ALLOWLIST — DENYING ALL PEERS.')
        console.error('     Nobody can connect. Add client keys with --firewall <file>, or open with --disable-firewall.')
      } else {
        console.log('  firewall: ' + allow.length + ' allowed key(s)')
      }
      process.on('SIGINT', async () => { await s.close(); process.exit(0) })
      return
    }
    // DEFAULT: SSH-style. sshd is the auth gate; hyperdht transport is open.
    if (f.firewall) console.error('  note: --firewall is ignored without --hyperident (SSH mode auth is your system sshd)')
    const s = await serveSSH({ keyPair, allow: true })
    console.log('hypermosh daemon serving as ' + HypercoreId.encode(s.publicKey))
    console.log('  SSH-style login: tunnels to your system sshd (127.0.0.1:22). Auth = password / ssh-key / authorized_keys.')
    console.log('  connect with:  hypermosh <user>@' + HypercoreId.encode(s.publicKey))
    process.on('SIGINT', async () => { await s.close(); process.exit(0) })
    return
  }

  if (cmd && !cmd.startsWith('-')) {
    // one-command client
    const { user, host } = splitTarget(cmd)
    let remoteKey
    try { remoteKey = await resolveName(host) } catch (e) { console.error('hypermosh: ' + e.message); process.exit(1) }

    let keyPair = null
    try { keyPair = loadDeviceKeyPair() } catch { /* ephemeral identity is fine */ }

    const spawnClient = (moshKey, localPort) => {
      const env = { ...process.env, MOSH_KEY: moshKey, LC_ALL: process.env.LC_ALL || 'C.UTF-8' }
      const child = cp.spawn('mosh-client', ['127.0.0.1', String(localPort)], { stdio: 'inherit', env })
      child.on('error', (e) => { console.error('hypermosh: cannot run mosh-client (' + (e.code || e.message) + ') — is mosh installed locally?'); process.exit(1) })
      return child
    }

    if (f.hyperident) {
      const start = { term: process.env.TERM || 'xterm', cmd: f._rest || [] }
      const session = connectSession({
        remoteKey, keyPair, start,
        onerror: (e) => { console.error('hypermosh: ' + (e.code || e.message)); process.exit(1) },
        onstarted: (moshKey, localPort) => {
          const child = spawnClient(moshKey, localPort)
          child.on('exit', (code) => { session.close().then(() => process.exit(code || 0)) })
        }
      })
      return
    }

    // DEFAULT: SSH-style login.
    const session = connectSSH({
      remoteKey, keyPair, user, cmd: f._rest || [],
      onerror: (e) => { console.error('hypermosh: ' + (e.code || e.message)); process.exit(1) },
      onstarted: (moshKey, localPort) => {
        const child = spawnClient(moshKey, localPort)
        child.on('exit', (code) => { session.close().then(() => process.exit(code || 0)) })
      }
    })
    return
  }

  usage()
}

main().catch((e) => { console.error('hypermosh: ' + e.message); process.exit(1) })