| 1 | // One set of ssh rules for every ssh this plugin starts (the remote agent, |
| 2 | // its persistent channel, and the agent install copy). Codewhale's Fleet SSH |
| 3 | // host applies the same rules; tests/fixtures/ssh-destinations.json holds the |
| 4 | // vectors both implementations are tested against. |
| 5 | // - host keys must already be known: StrictHostKeyChecking=yes, no updates; |
| 6 | // a computer may name its own known-hosts file, which is then the only one |
| 7 | // - a host or user is a destination, never an option: neither may start |
| 8 | // with "-", and "--" ends the options before the destination |
| 9 | |
| 10 | export const SSH_HOST_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/; |
| 11 | export const SSH_USER_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/; |
| 12 | |
| 13 | export class SshTargetError extends Error { |
| 14 | constructor(code, message) { super(message); this.name = "SshTargetError"; this.code = code; } |
| 15 | } |
| 16 | |
| 17 | /** Throw when an ssh computer entry cannot be used as a destination. */ |
| 18 | export function validateSshTarget({ host, user, port, knownHosts } = {}) { |
| 19 | if (typeof host !== "string" || !SSH_HOST_RE.test(host)) { |
| 20 | throw new SshTargetError("invalid_host", "ssh computers need a valid host (letters, digits, dot, dash, underscore; not starting with '-')"); |
| 21 | } |
| 22 | if (user != null && (typeof user !== "string" || !SSH_USER_RE.test(user))) { |
| 23 | throw new SshTargetError("invalid_user", "user must be a plain name (not starting with '-')"); |
| 24 | } |
| 25 | if (port != null && (!Number.isInteger(port) || port < 1 || port > 65535)) { |
| 26 | throw new SshTargetError("invalid_port", "port must be an integer in 1..65535"); |
| 27 | } |
| 28 | if (knownHosts != null && (typeof knownHosts !== "string" || !knownHosts.startsWith("/") || /[\s"\\]/.test(knownHosts))) { |
| 29 | throw new SshTargetError("invalid_known_hosts", "knownHosts must be an absolute path without spaces or quotes"); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /** ssh options (no destination) for a computer entry. */ |
| 34 | export function sshOptions(computer, { portFlag = "-p" } = {}) { |
| 35 | const options = [ |
| 36 | "-o", "BatchMode=yes", |
| 37 | "-o", "ConnectTimeout=8", |
| 38 | "-o", "StrictHostKeyChecking=yes", |
| 39 | "-o", "UpdateHostKeys=no", |
| 40 | ]; |
| 41 | if (computer.knownHosts) { |
| 42 | options.push("-o", `UserKnownHostsFile=${computer.knownHosts}`, "-o", "GlobalKnownHostsFile=/dev/null"); |
| 43 | } |
| 44 | if (computer.port) options.push(portFlag, String(computer.port)); |
| 45 | return options; |
| 46 | } |
| 47 | |
| 48 | export function sshDestination(computer) { |
| 49 | return computer.user ? `${computer.user}@${computer.host}` : computer.host; |
| 50 | } |
| 51 | |
| 52 | /** Full ssh argv prefix: options, "--", destination. The remote command follows. */ |
| 53 | export function sshArgv(computer) { |
| 54 | validateSshTarget(computer); |
| 55 | return [...sshOptions(computer), "--", sshDestination(computer)]; |
| 56 | } |
| 57 |