OverviewFilesCommitsIssuesDocsReleasesPeers

7rnx-proxy / bin / 7rnx

340 lines · text ref:
#!/usr/bin/env node

import { Registry } from '../lib/registry.js'
import { Deployer } from '../lib/deployer.js'
import { generateConfig } from '../lib/config-generator.js'

const args = process.argv.slice(2)
const command = args[0]
const subcommand = args[1]

function parseFlags (args) {
  const flags = {}
  for (let i = 0; i < args.length; i++) {
    if (args[i].startsWith('--')) {
      const key = args[i].slice(2)
      const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true
      flags[key] = val
      if (val !== true) i++
    }
  }
  return flags
}

function usage () {
  console.log(`Usage: 7rnx <command> [options]

Node Management:
  7rnx node add <petname> --ip <vpn-ip> [--keet-ident <key>]
  7rnx node list
  7rnx node remove <petname>

Service Management:
  7rnx service add <node> <name> --port <port> [--prefix <path>] [--root-path <path>]
  7rnx service add-backend <service> --node <petname> --port <port> [--mode active|standby]
  7rnx service remove-backend <service> --node <petname>
  7rnx service list [node]
  7rnx service remove <node> <name>
  7rnx service status [node]     Check which services are actually listening

Host (vhost) Management — subdomain reverse proxy at the URL root (for atproto PDS etc.):
  7rnx host add <name> <node> --port <port> --server-name <fqdn>
  7rnx host list
  7rnx host remove <name>

Deployment:
  7rnx deploy              Deploy path-config + vhosts to 7rnx.net and reload Apache
  7rnx deploy --dry-run    Show generated config (path + vhosts) without deploying
  7rnx status              Check Apache status on 7rnx.net

Remote:
  7rnx remote config [path]   Fetch live config from 7rnx.net (default: SSL vhost)
  7rnx remote ls              List sites-enabled configs

Web UI:
  7rnx ui [port]           Start dashboard (default: 7777)
  7rnx ui --network        Bind to 0.0.0.0 (LAN access)

Config:
  7rnx config              Show current registry
  7rnx config set-server --host <ip> --user <user>`)
}

function main () {
  if (!command || command === 'help' || command === '--help') {
    usage()
    process.exit(0)
  }

  const registry = new Registry().load()

  switch (command) {
    case 'node':
      handleNode(registry, subcommand, args.slice(2))
      break
    case 'service':
      handleService(registry, subcommand, args.slice(2))
      break
    case 'host':
      handleHost(registry, subcommand, args.slice(2))
      break
    case 'deploy':
      handleDeploy(registry, args.slice(1))
      break
    case 'status':
      handleStatus(registry)
      break
    case 'remote':
      handleRemote(registry, subcommand, args.slice(2))
      break
    case 'ui':
      handleUi(args.slice(1))
      break
    case 'config':
      handleConfig(registry, subcommand, args.slice(2))
      break
    default:
      console.error(`Unknown command: ${command}`)
      usage()
      process.exit(1)
  }
}

function handleNode (registry, sub, rest) {
  switch (sub) {
    case 'add': {
      const petname = rest[0]
      const flags = parseFlags(rest.slice(1))
      if (!petname || !flags.ip) {
        console.error('Usage: 7rnx node add <petname> --ip <vpn-ip> [--keet-ident <key>]')
        process.exit(1)
      }
      registry.addNode(petname, flags.ip, flags['keet-ident'])
      console.log(`Added node "${petname}" (${flags.ip})`)
      break
    }
    case 'list': {
      const nodes = registry.listNodes()
      if (nodes.length === 0) {
        console.log('No nodes registered.')
        return
      }
      for (const n of nodes) {
        const ident = n.keetIdent ? ` [${n.keetIdent.slice(0, 8)}...]` : ''
        console.log(`  ${n.petname}\t${n.ip}${ident}`)
      }
      break
    }
    case 'remove': {
      const petname = rest[0]
      if (!petname) {
        console.error('Usage: 7rnx node remove <petname>')
        process.exit(1)
      }
      const services = registry.listServices(petname)
      registry.removeNode(petname)
      console.log(`Removed node "${petname}" and ${services.length} service(s)`)
      break
    }
    default:
      console.error('Usage: 7rnx node <add|list|remove>')
      process.exit(1)
  }
}

function handleService (registry, sub, rest) {
  switch (sub) {
    case 'add': {
      const nodePetname = rest[0]
      const serviceName = rest[1]
      const flags = parseFlags(rest.slice(2))
      if (!nodePetname || !serviceName || !flags.port) {
        console.error('Usage: 7rnx service add <node> <name> --port <port> [--prefix <path>] [--root-path <path>]')
        process.exit(1)
      }
      registry.addService(nodePetname, serviceName, flags.port, flags.prefix, flags['root-path'])
      const urlPath = flags['root-path'] || `/${nodePetname}/${serviceName}/`
      console.log(`Added service "${serviceName}" on "${nodePetname}" → ${urlPath} → :${flags.port}`)
      break
    }
    case 'list': {
      const nodePetname = rest[0]
      const services = registry.listServices(nodePetname)
      if (services.length === 0) {
        console.log(nodePetname ? `No services on node "${nodePetname}".` : 'No services registered.')
        return
      }
      for (const s of services) {
        const urlPath = s.rootPath || `/${s.node}/${s.name}/`
        if (s.backends && s.backends.length > 1) {
          console.log(`  ${s.name}\t→ ${urlPath}  (balancer)`)
          for (const b of s.backends) {
            console.log(`    ${b.node}:${b.port}\t${b.mode}`)
          }
        } else {
          console.log(`  ${s.node}/${s.name}\t:${s.port}\t→ ${urlPath}`)
        }
      }
      break
    }
    case 'remove': {
      const nodePetname = rest[0]
      const serviceName = rest[1]
      if (!nodePetname || !serviceName) {
        console.error('Usage: 7rnx service remove <node> <name>')
        process.exit(1)
      }
      registry.removeService(nodePetname, serviceName)
      console.log(`Removed service "${serviceName}" from "${nodePetname}"`)
      break
    }
    case 'add-backend': {
      const serviceName = rest[0]
      const flags = parseFlags(rest.slice(1))
      if (!serviceName || !flags.node || !flags.port) {
        console.error('Usage: 7rnx service add-backend <service> --node <petname> --port <port> [--mode active|standby]')
        process.exit(1)
      }
      registry.addBackend(serviceName, flags.node, flags.port, flags.mode || 'standby')
      console.log(`Added backend: ${flags.node}:${flags.port} (${flags.mode || 'standby'}) to service "${serviceName}"`)
      break
    }
    case 'remove-backend': {
      const serviceName = rest[0]
      const flags = parseFlags(rest.slice(1))
      if (!serviceName || !flags.node) {
        console.error('Usage: 7rnx service remove-backend <service> --node <petname>')
        process.exit(1)
      }
      registry.removeBackend(serviceName, flags.node)
      console.log(`Removed backend "${flags.node}" from service "${serviceName}"`)
      break
    }
    case 'status': {
      const nodePetname = rest[0]
      const deployer = new Deployer(registry.data)
      const services = registry.listServices(nodePetname)
      if (services.length === 0) {
        console.log(nodePetname ? `No services on node "${nodePetname}".` : 'No services registered.')
        return
      }
      const results = deployer.checkServices(registry.data.nodes, services)
      for (const r of results) {
        const icon = r.status === 'UP' ? '\x1b[32mUP\x1b[0m' : '\x1b[31m' + r.status + '\x1b[0m'
        const urlPath = services.find(s => s.node === r.node && s.name === r.name)
        const url = urlPath?.rootPath || `/${r.node}/${r.name}/`
        console.log(`  ${icon}\t${r.node}/${r.name}\t:${r.port}\t${url}`)
      }
      break
    }
    default:
      console.error('Usage: 7rnx service <add|list|remove|add-backend|remove-backend|status>')
      process.exit(1)
  }
}

function handleHost (registry, sub, rest) {
  switch (sub) {
    case 'add': {
      const name = rest[0]
      const nodePetname = rest[1]
      const flags = parseFlags(rest.slice(2))
      if (!name || !nodePetname || !flags.port || !flags['server-name']) {
        console.error('Usage: 7rnx host add <name> <node> --port <port> --server-name <fqdn>')
        process.exit(1)
      }
      registry.addHost(name, nodePetname, flags.port, flags['server-name'])
      console.log(`Added host "${name}": https://${flags['server-name']}/ → ${nodePetname}:${flags.port}`)
      console.log('Run `7rnx deploy` to apply (requires the vhosts sudoers rule from setup-7rnx-server.sh).')
      break
    }
    case 'list': {
      const hosts = registry.listHosts()
      if (hosts.length === 0) { console.log('No vhosts registered.'); return }
      for (const h of hosts) console.log(`  ${h.name}\t${h.serverName}\t→ ${h.node}:${h.port}`)
      break
    }
    case 'remove': {
      const name = rest[0]
      if (!name) { console.error('Usage: 7rnx host remove <name>'); process.exit(1) }
      registry.removeHost(name)
      console.log(`Removed host "${name}"`)
      break
    }
    default:
      console.error('Usage: 7rnx host <add|list|remove>')
      process.exit(1)
  }
}

function handleDeploy (registry, rest) {
  const flags = parseFlags(rest)
  const deployer = new Deployer(registry.data)
  const hasHosts = (registry.data.hosts || []).length > 0

  if (flags['dry-run']) {
    console.log('# ===== path-config (/etc/apache2/7rnx-proxy.conf) =====')
    console.log(deployer.dryRun())
    if (hasHosts) {
      console.log('\n# ===== vhosts (/etc/apache2/sites-enabled/7rnx-proxy-vhosts.conf) =====')
      console.log(deployer.dryRunVhosts())
    }
    return
  }

  deployer.deploy()
  if (hasHosts) deployer.deployVhosts()
}

function handleStatus (registry) {
  const deployer = new Deployer(registry.data)
  deployer.status()
}

function handleRemote (registry, sub, rest) {
  const deployer = new Deployer(registry.data)

  switch (sub) {
    case 'config': {
      const configPath = rest[0] || '/etc/apache2/sites-enabled/000-default-le-ssl.conf'
      console.log(`# Fetching ${configPath} from ${registry.data.server.host}\n`)
      const content = deployer.fetchRemoteConfig(configPath)
      if (content) console.log(content)
      break
    }
    case 'ls': {
      const listing = deployer.listRemoteConfigs()
      if (listing) console.log(listing.trim())
      break
    }
    default:
      console.error('Usage: 7rnx remote <config|ls>')
      process.exit(1)
  }
}

async function handleUi (rest) {
  const flags = parseFlags(rest)
  const port = rest.find(a => !a.startsWith('--')) || 7777
  const { startServer } = await import('../lib/web.js')
  startServer(Number(port), !!flags.network)
}

function handleConfig (registry, sub, rest) {
  if (sub === 'set-server') {
    const flags = parseFlags(rest)
    if (!flags.host && !flags.user) {
      console.error('Usage: 7rnx config set-server --host <ip> --user <user>')
      process.exit(1)
    }
    registry.setServer(flags.host, flags.user)
    console.log(`Server: ${registry.data.server.user}@${registry.data.server.host}`)
    return
  }

  // Default: show registry
  console.log(JSON.stringify(registry.data, null, 2))
}

main()