UNPKG

ssh2-connect

Version:

Callback-based api behind ssh2 to open an SSH connection

1 lines 9.45 kB
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { Client, ConnectConfig as Config } from \"ssh2\";\nimport { camelize } from \"mixme\";\n\ntype CamelToSnakeCase<S extends string> =\n S extends `${infer T}${infer U}` ?\n `${T extends Capitalize<T> ? \"_\" : \"\"}${Lowercase<T>}${CamelToSnakeCase<U>}`\n : S;\ntype KeysToSnakeCase<T> = {\n [K in keyof T as CamelToSnakeCase<string & K>]: T[K];\n};\n\ninterface ConnectConfig extends Config, KeysToSnakeCase<Config> {\n retry?: number | boolean;\n wait?: number;\n privateKey?: string | Buffer;\n private_key?: string | Buffer;\n privateKeyPath?: string | boolean;\n private_key_path?: string | boolean;\n}\n/**\n * Establishes an SSH connection using the provided configuration options.\n *\n * @param options - The configuration options for the SSH connection.\n * @param options.username - The username for authentication. Defaults to the current user if not provided.\n * @param options.retry - The number of connection retry attempts. Set to `0` or `false` to disable retries, default is `1`.\n * @param options.wait - The wait time in milliseconds between each attempts, default to `500`.\n * @param options.privateKey - The private key as a string or Buffer for authentication.\n * @param options.privateKeyPath - The path to the private key file, or true for auto-discovery in ~/.ssh.\n * @param options.password - The password for authentication.\n * @param options.[key: string] - Any other valid SSH2 connection options.\n *\n * @returns A Promise that resolves to an SSH2 Client instance when the connection is established.\n *\n * @throws Will reject the promise with an error if the connection fails after all retry attempts.\n *\n * @example\n * ```typescript\n * const client = await connect({\n * host: 'example.com',\n * username: 'user',\n * privateKeyPath: '~/.ssh/id_ed25519'\n * });\n * ```\n */\nconst connect = function (options: ConnectConfig): Promise<Client> {\n const work = async function (\n resolve: (value: Client) => void,\n reject: (reason?: any) => void,\n ) {\n if (options instanceof Client) {\n return resolve(options);\n }\n options = camelize(options, 1) as ConnectConfig;\n if (options.username == null) {\n options.username =\n process.env[\"USER\"] ||\n require(\"child_process\")\n .execSync(\"whoami\", {\n encoding: \"utf8\",\n timeout: 1000,\n })\n .trim();\n }\n if (options.username == null) {\n options.username = \"root\"; // We've seed 'USER' not inside env inside the docker centos6 container.\n }\n if (options.retry == null) {\n options.retry = 1;\n }\n if (options.retry === false) {\n options.retry = 0;\n }\n if (options.wait == null) {\n options.wait = 500;\n }\n if (!options.password && !options.privateKey) {\n if (options.privateKeyPath == null) {\n options.privateKeyPath = true; // Auto discovery\n }\n } else {\n options.privateKeyPath = undefined;\n }\n try {\n // Extract private key from file\n if (typeof options.privateKeyPath === \"string\") {\n let match;\n if ((match = /~(\\/.*)/.exec(options.privateKeyPath))) {\n options.privateKeyPath = path.join(process.env.HOME!, match[1]);\n }\n options.privateKey = await fs.readFile(options.privateKeyPath, \"ascii\");\n } else if (options.privateKeyPath === true) {\n for (const algo of [\"id_ed25519\", \"id_rsa\"]) {\n const source = path.resolve(process.env.HOME!, \".ssh\", algo);\n try {\n options.privateKey = await fs.readFile(source, \"ascii\");\n break;\n } catch {\n /* empty */\n }\n }\n if (options.privateKey == null) {\n throw Error(\"Failed to discover an ssh private key inside `~/.ssh`.\");\n }\n }\n } catch (error) {\n reject(error);\n }\n // Connection attempts\n let retry = options.retry;\n const connect = function () {\n if (retry !== true && retry > 0) {\n retry--;\n }\n let succeed = false;\n const connection = new Client();\n connection.on(\"error\", function (error) {\n connection.end();\n // Event \"error\" is thrown after a \"ready\" if the connection is lost\n if (succeed) {\n return;\n }\n if (retry === true || retry > 0) {\n setTimeout(connect, options.wait);\n } else {\n reject(error);\n }\n });\n connection.on(\"ready\", function () {\n succeed = true;\n resolve(connection);\n });\n return connection.connect(options);\n };\n return connect();\n };\n return new Promise(work);\n};\n\n/**\n * Close the the SSH client connection.\n *\n * @param conn - The SSH client connection to close.\n *\n * @returns A boolean value indicating whether the connection was closed (true) or if it was already closed (false).\n */\nconst close = (conn: Client): PromiseLike<boolean> => {\n if (closed(conn)) return Promise.resolve(false);\n return new Promise((resolve) => {\n conn.end();\n conn.on(\"close\", () => {\n resolve(true);\n });\n });\n};\n\n/**\n * Checks if the provided argument `conn` is an instance of the `Client` connection class from the ssh2 package.\n *\n * @param conn - The object to check, probably an SSH client connection.\n *\n * @returns A boolean value indicating whether the given object is an SSH client connection or not.\n */\nconst is = function (conn: unknown): boolean {\n return conn instanceof Client;\n};\n\n/**\n * Checks if the provided SSH client connection is closed.\n *\n * @param conn - The SSH client connection to check.\n *\n * @returns A boolean value indicating whether the connection is closed (true) or open (false).\n */\nconst closed = function (conn: Client): boolean {\n return !opened(conn);\n};\n\n/**\n * Checks if the provided SSH client connection is open and writable.\n *\n * @param conn - The SSH client connection to check.\n *\n * @returns A boolean value indicating whether the connection is open and writable (true) or closed (false).\n */\nconst opened = function (conn: Client): boolean {\n class _Client extends Client {\n _state?: string;\n _sshstream?: {\n writable: boolean;\n };\n _sock?: {\n writable: boolean;\n _writableState: {\n ended: boolean;\n };\n };\n }\n // ssh@0.3.x use \"_state\"\n // ssh@0.4.x use \"_sshstream\" and \"_sock\"\n // ssh@1.7.0 use \"ssh._writableState?.ended\"\n const _conn = conn as _Client;\n return (\n (_conn._state != null && _conn._state !== \"closed\") ||\n (_conn._sshstream?.writable && _conn._sock?.writable) ||\n _conn._sock?._writableState?.ended === false\n );\n};\n\nexport default connect;\nexport { is, connect, close, closed, opened, ConnectConfig };\n"],"mappings":"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","names":["resolve","connect"]}