88 lines
3 KiB
TypeScript
88 lines
3 KiB
TypeScript
|
|
import { Client, type ConnectConfig } from 'ssh2'
|
||
|
|
import { db } from '../db/index.js'
|
||
|
|
import { loadSecrets } from '../db/secrets.js'
|
||
|
|
|
||
|
|
interface IntegrationRow {
|
||
|
|
id: number
|
||
|
|
type: string
|
||
|
|
config_json: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function loadSshHost(integrationId: number) {
|
||
|
|
const row = db
|
||
|
|
.prepare('SELECT id, type, config_json FROM integrations WHERE id = ?')
|
||
|
|
.get(integrationId) as IntegrationRow | undefined
|
||
|
|
if (!row || row.type !== 'ssh') return null
|
||
|
|
const config = JSON.parse(row.config_json) as Record<string, string>
|
||
|
|
const secrets = loadSecrets(row.id)
|
||
|
|
return { id: row.id, config, secrets }
|
||
|
|
}
|
||
|
|
|
||
|
|
export type SshHost = NonNullable<ReturnType<typeof loadSshHost>>
|
||
|
|
|
||
|
|
function makeHostVerifier(integrationId: number): ConnectConfig['hostVerifier'] {
|
||
|
|
return (keyHash: string): boolean => {
|
||
|
|
const row = db
|
||
|
|
.prepare('SELECT fingerprint FROM ssh_host_keys WHERE integration_id = ?')
|
||
|
|
.get(integrationId) as { fingerprint: string } | undefined
|
||
|
|
if (!row) {
|
||
|
|
db.prepare('INSERT INTO ssh_host_keys (integration_id, fingerprint) VALUES (?, ?)').run(integrationId, keyHash)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return row.fingerprint === keyHash
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function baseConnectConfig(host: SshHost): ConnectConfig {
|
||
|
|
return {
|
||
|
|
host: host.config.host,
|
||
|
|
port: Number(host.config.port) || 22,
|
||
|
|
username: host.config.username,
|
||
|
|
password: host.secrets.password || undefined,
|
||
|
|
privateKey: host.secrets.privateKey || undefined,
|
||
|
|
passphrase: host.secrets.passphrase || undefined,
|
||
|
|
readyTimeout: 8000,
|
||
|
|
hostHash: 'sha256',
|
||
|
|
hostVerifier: makeHostVerifier(host.id),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Connects to `target`, transparently chaining through its jump host (if configured) via forwardOut(). */
|
||
|
|
export function connectTarget(
|
||
|
|
target: SshHost,
|
||
|
|
onReady: (client: Client) => void,
|
||
|
|
onError: (message: string) => void,
|
||
|
|
): { conn: Client; jumpConn: Client | null } {
|
||
|
|
const jumpHostId = target.config.jumpHostIntegrationId ? Number(target.config.jumpHostIntegrationId) : null
|
||
|
|
|
||
|
|
if (jumpHostId) {
|
||
|
|
const jumpHost = loadSshHost(jumpHostId)
|
||
|
|
if (!jumpHost) {
|
||
|
|
onError('Jump host integration not found')
|
||
|
|
return { conn: new Client(), jumpConn: null }
|
||
|
|
}
|
||
|
|
const jumpConn = new Client()
|
||
|
|
jumpConn.on('ready', () => {
|
||
|
|
jumpConn.forwardOut('127.0.0.1', 0, target.config.host, Number(target.config.port) || 22, (err, sock) => {
|
||
|
|
if (err) {
|
||
|
|
onError(`Jump host forward failed: ${err.message}`)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
client.connect({ ...baseConnectConfig(target), sock })
|
||
|
|
})
|
||
|
|
})
|
||
|
|
jumpConn.on('error', (err) => onError(`Jump host error: ${err.message}`))
|
||
|
|
const client = new Client()
|
||
|
|
client.on('ready', () => onReady(client))
|
||
|
|
client.on('error', (err) => onError(err.message))
|
||
|
|
jumpConn.connect(baseConnectConfig(jumpHost))
|
||
|
|
return { conn: client, jumpConn }
|
||
|
|
}
|
||
|
|
|
||
|
|
const client = new Client()
|
||
|
|
client.on('ready', () => onReady(client))
|
||
|
|
client.on('error', (err) => onError(err.message))
|
||
|
|
client.connect(baseConnectConfig(target))
|
||
|
|
return { conn: client, jumpConn: null }
|
||
|
|
}
|