diff --git a/ssh2/ssh2-tests.ts b/ssh2/ssh2-tests.ts new file mode 100644 index 000000000..1c59b1305 --- /dev/null +++ b/ssh2/ssh2-tests.ts @@ -0,0 +1,428 @@ +/// + +import * as ssh2 from 'ssh2'; + +declare var inspect: any; + +// +// # Client Examples +// + +// Authenticate using keys and execute uptime on a server: + +var Client = require('ssh2').Client; + +var conn = new Client(); +conn.on('ready', () => { + console.log('Client :: ready'); + conn.exec('uptime', (err: Error, stream: ssh2.Channel) => { + if (err) throw err; + stream + .on('close', (code: any, signal: any) => { + console.log('Stream :: close :: code: ' + code + ', signal: ' + signal); + conn.end(); + }).on('data', (data: any) => { + console.log('STDOUT: ' + data); + }).stderr.on('data', (data: any) => { + console.log('STDERR: ' + data); + }); + }); +}).connect({ + host: '192.168.100.100', + port: 22, + username: 'frylock', + privateKey: require('fs').readFileSync('/here/is/my/key') +}); + +// Authenticate using keys and start an interactive shell session: + +var Client = require('ssh2').Client; + +var conn = new Client(); +conn.on('ready', () => { + console.log('Client :: ready'); + conn.shell( (err: Error, stream: ssh2.Channel) => { + if (err) throw err; + stream.on('close', () => { + console.log('Stream :: close'); + conn.end(); + }).on('data', (data: any) => { + console.log('STDOUT: ' + data); + }).stderr.on('data', (data: any) => { + console.log('STDERR: ' + data); + }); + stream.end('ls -l\nexit\n'); + }); +}).connect({ + host: '192.168.100.100', + port: 22, + username: 'frylock', + privateKey: require('fs').readFileSync('/here/is/my/key') +}); + +// Authenticate using password and send an HTTP request to port 80 on the server: + +var Client = require('ssh2').Client; + +var conn = new Client(); +conn.on('ready', () => { + console.log('Client :: ready'); + conn.forwardOut('192.168.100.102', 8000, '127.0.0.1', 80, (err: Error, stream: ssh2.Channel) => { + if (err) throw err; + stream.on('close', () => { + console.log('TCP :: CLOSED'); + conn.end(); + }).on('data', (data: any) => { + console.log('TCP :: DATA: ' + data); + }).end([ + 'HEAD / HTTP/1.1', + 'User-Agent: curl/7.27.0', + 'Host: 127.0.0.1', + 'Accept: */*', + 'Connection: close', + '', + '' + ].join('\r\n')); + }); +}).connect({ + host: '192.168.100.100', + port: 22, + username: 'frylock', + password: 'nodejsrules' +}); + +// Authenticate using password and forward remote connections on port 8000 to us: + +var Client = require('ssh2').Client; + +var conn = new Client(); +conn.on('ready', () => { + console.log('Client :: ready'); + conn.forwardIn('127.0.0.1', 8000, (err: Error) => { + if (err) throw err; + console.log('Listening for connections on server on port 8000!'); + }); +}).on('tcp connection', (info: any, accept: Function, reject: Function) => { + console.log('TCP :: INCOMING CONNECTION:'); + console.dir(info); + accept().on('close', () => { + console.log('TCP :: CLOSED'); + }).on('data', (data: any) => { + console.log('TCP :: DATA: ' + data); + }).end([ + 'HTTP/1.1 404 Not Found', + 'Date: Thu, 15 Nov 2012 02:07:58 GMT', + 'Server: ForwardedConnection', + 'Content-Length: 0', + 'Connection: close', + '', + '' + ].join('\r\n')); +}).connect({ + host: '192.168.100.100', + port: 22, + username: 'frylock', + password: 'nodejsrules' +}); + +// Authenticate using password and get a directory listing via SFTP: + +var Client = require('ssh2').Client; + +var conn = new Client(); +conn.on('ready', () => { + console.log('Client :: ready'); + conn.sftp( (err: Error, sftp: any) => { + if (err) throw err; + sftp.readdir('foo', (err: Error, list: any) => { + if (err) throw err; + console.dir(list); + conn.end(); + }); + }); +}).connect({ + host: '192.168.100.100', + port: 22, + username: 'frylock', + password: 'nodejsrules' +}); + +// Connection hopping: + +var Client = require('ssh2').Client; + +var conn1 = new Client(), + conn2 = new Client(); + +conn1.on('ready', () => { + console.log('FIRST :: connection ready'); + conn1.exec('nc 192.168.1.2 22', (err: Error, stream: ssh2.Channel) => { + if (err) { + console.log('FIRST :: exec error: ' + err); + return conn1.end(); + } + conn2.connect({ + sock: stream, + username: 'user2', + password: 'password2', + }); + }); +}).connect({ + host: '192.168.1.1', + username: 'user1', + password: 'password1', +}); + +conn2.on('ready', () => { + console.log('SECOND :: connection ready'); + conn2.exec('uptime', (err: Error, stream: ssh2.Channel) => { + if (err) { + console.log('SECOND :: exec error: ' + err); + return conn1.end(); + } + stream.on('end', () => { + conn1.end(); // close parent (and this) connection + }).on('data', (data: any) => { + console.log(data.toString()); + }); + }); +}); + +// Forward X11 connections (xeyes): + +var net = require('net'), + Client = require('ssh2').Client; + +var conn = new Client(); + +conn.on('x11', (info: any, accept: any, reject: any) => { + var xserversock = new net.Socket(); + xserversock.on('connect', () => { + var xclientsock = accept(); + xclientsock.pipe(xserversock).pipe(xclientsock); + }); + // connects to localhost:0.0 + xserversock.connect(6000, 'localhost'); +}); + +conn.on('ready', () => { + conn.exec('xeyes', { x11: true }, (err: Error, stream: ssh2.Channel) => { + if (err) throw err; + var code = 0; + stream.on('end', () => { + if (code !== 0) + console.log('Do you have X11 forwarding enabled on your SSH server?'); + conn.end(); + }).on('exit', (exitcode: number) => { + code = exitcode; + }); + }); +}).connect({ + host: '192.168.1.1', + username: 'foo', + password: 'bar' +}); + +// Dynamic (1:1) port forwarding using a SOCKSv5 proxy (using socksv5): + +var socks = require('socksv5'), + Client = require('ssh2').Client; + +var ssh_config = { + host: '192.168.100.1', + port: 22, + username: 'nodejs', + password: 'rules' +}; + +socks.createServer( (info: any, accept: any, deny: any) => { + // NOTE: you could just use one ssh2 client connection for all forwards, but + // you could run into server-imposed limits if you have too many forwards open + // at any given time + var conn = new Client(); + conn.on('ready', () => { + conn.forwardOut(info.srcAddr, + info.srcPort, + info.dstAddr, + info.dstPort, + (err: Error, stream: ssh2.Channel) => { + if (err) { + conn.end(); + return deny(); + } + + var clientSocket: any; + if (clientSocket = accept(true)) { + stream.pipe(clientSocket).pipe(stream).on('close', () => { + conn.end(); + }); + } else + conn.end(); + }); + }).on('error', (err: Error) => { + deny(); + }).connect(ssh_config); +}).listen(1080, 'localhost', () => { + console.log('SOCKSv5 proxy server started on port 1080'); +}).useAuth(socks.auth.None()); + +// Invoke an arbitrary subsystem (netconf in this example): + +var Client = require('ssh2').Client, + xmlhello = ''+ + ''+ + ' '+ + ' urn:ietf:params:netconf:base:1.0'+ + ' '+ + ']]>]]>'; + +var conn = new Client(); + +conn.on('ready', () => { + console.log('Client :: ready'); + conn.subsys('netconf', (err: Error, stream: ssh2.Channel) => { + if (err) throw err; + stream.on('data', (data: any) => { + console.log(data); + }).write(xmlhello); + }); +}).connect({ + host: '1.2.3.4', + port: 22, + username: 'blargh', + password: 'honk' +}); + +// +// # Server Examples +// + +// Only allow password and public key authentication and command execution: + +var fs = require('fs'), + crypto = require('crypto'); +var buffersEqual = require('buffer-equal-constant-time'), + //ssh2 = require('ssh2'), + utils = ssh2.utils; + +var pubKey = utils.genPublicKey(utils.parseKey(fs.readFileSync('user.pub'))); + +new ssh2.Server({ + privateKey: fs.readFileSync('host.key') +}, (client: any) => { + console.log('Client connected!'); + + client.on('authentication', (ctx: any) => { + if (ctx.method === 'password' + && ctx.username === 'foo' + && ctx.password === 'bar') + ctx.accept(); + else if (ctx.method === 'publickey' + && ctx.key.algo === pubKey.fulltype + && buffersEqual(ctx.key.data, pubKey.public)) { + if (ctx.signature) { + var verifier = crypto.createVerify(ctx.sigAlgo); + verifier.update(ctx.blob); + if (verifier.verify(pubKey.publicOrig, ctx.signature, 'binary')) + ctx.accept(); + else + ctx.reject(); + } else { + // if no signature present, that means the client is just checking + // the validity of the given public key + ctx.accept(); + } + } else + ctx.reject(); + }).on('ready', () => { + console.log('Client authenticated!'); + + client.on('session', (accept: any, reject: any) => { + var session = accept(); + session.once('exec', (accept: any, reject: any, info: any) => { + console.log('Client wants to execute: ' + inspect(info.command)); + var stream = accept(); + stream.stderr.write('Oh no, the dreaded errors!\n'); + stream.write('Just kidding about the errors!\n'); + stream.exit(0); + stream.end(); + }); + }); + }).on('end', () => { + console.log('Client disconnected'); + }); +}).listen(0, '127.0.0.1', () => { + console.log('Listening on port ' + this.address().port); + }); + +// SFTP only server: + +var fs = require('fs'); +//var ssh2 = require('ssh2'); +var OPEN_MODE = ssh2.SFTP_OPEN_MODE, + STATUS_CODE = ssh2.SFTP_STATUS_CODE; + +new ssh2.Server({ + privateKey: fs.readFileSync('host.key') +}, (client: any) => { + console.log('Client connected!'); + + client.on('authentication', (ctx: any) => { + if (ctx.method === 'password' + && ctx.username === 'foo' + && ctx.password === 'bar') + ctx.accept(); + else + ctx.reject(); + }).on('ready', () => { + console.log('Client authenticated!'); + + client.on('session', (accept: any, reject: any) => { + var session = accept(); + session.on('sftp', (accept: any, reject: any) => { + console.log('Client SFTP session'); + var openFiles: any = {}; + var handleCount = 0; + // `sftpStream` is an `SFTPStream` instance in server mode + // see: https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md + var sftpStream = accept(); + sftpStream.on('OPEN', (reqid: any, filename: any, flags: any, attrs: any) => { + // only allow opening /tmp/foo.txt for writing + if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.WRITE)) + return sftpStream.status(reqid, STATUS_CODE.FAILURE); + // create a fake handle to return to the client, this could easily + // be a real file descriptor number for example if actually opening + // the file on the disk + var handle = new Buffer(4); + openFiles[handleCount] = true; + handle.writeUInt32BE(handleCount++, 0, true); + sftpStream.handle(reqid, handle); + console.log('Opening file for write') + }).on('WRITE', (reqid: any, handle: any, offset: any, data: any) => { + if (handle.length !== 4 || !openFiles[handle.readUInt32BE(0, true)]) + return sftpStream.status(reqid, STATUS_CODE.FAILURE); + // fake the write + sftpStream.status(reqid, STATUS_CODE.OK); + var inspected = require('util').inspect(data); + console.log('Write to file at offset %d: %s', offset, inspected); + }).on('CLOSE', (reqid: any, handle: any) => { + var fnum: any; + if (handle.length !== 4 || !openFiles[(fnum = handle.readUInt32BE(0, true))]) + return sftpStream.status(reqid, STATUS_CODE.FAILURE); + delete openFiles[fnum]; + sftpStream.status(reqid, STATUS_CODE.OK); + console.log('Closing file'); + }); + }); + }); + }).on('end', () => { + console.log('Client disconnected'); + }); +}).listen(0, '127.0.0.1', () => { + console.log('Listening on port ' + this.address().port); + }); + + + + + diff --git a/ssh2/ssh2.d.ts b/ssh2/ssh2.d.ts new file mode 100644 index 000000000..870104617 --- /dev/null +++ b/ssh2/ssh2.d.ts @@ -0,0 +1,346 @@ +// Type definitions for ssh2 +// Project: https://github.com/mscdex/ssh2 +// Definitions by: Qubo +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/// + +declare module "ssh2" { + import stream = require('stream'); + + namespace ssh2 { + interface Client { + Server: ServerStatic; + new(): Client; + /** + * SFTPStream.STATUS_CODE from ssh2-streams. + */ + SFTP_STATUS_CODE: Sftp.StatusCode; + /** + * SFTPStream.OPEN_MODE from ssh2-streams. + */ + SFTP_OPEN_MODE: Sftp.OpenMode; + //TODO: Type ssh2-streams + /** + * utility methods from ssh2-streams. + */ + utils: Utils; + + connect(config: ConnectConfig): void; + end(): void; + destroy(): void; + exec(command: string, options: ExecOption, callback: ChannelCallback): boolean; + exec(command: string, callback: ChannelCallback): boolean; + shell(window: boolean|PseudoTtySettings, options: boolean|number|X11Settings, callback: ChannelCallback): boolean; + shell(window: boolean|PseudoTtySettings, callback: ChannelCallback): boolean; + shell(callback: ChannelCallback): boolean; + subsys(subsystem: string, callback: ChannelCallback): boolean; + sftp(callback: SftpCallback): boolean; + forwardIn(bindAddr: string, bindPort: number, callback?: ForwardInCallback): boolean; + unforwardIn(bindAddr: string, bindPort: number, callback?: ErrorCallback): boolean; + forwardOut(srcIP: string, srcPort: number, dstIP: string, dstPort: number, callback: ChannelCallback): boolean; + openssh_noMoreSessions(callback?: ErrorCallback): boolean; + openssh_forwardInStreamLocal(socketPath: string, callback?: ErrorCallback): boolean; + openssh_unforwardInStreamLocal(socketPath: string, callback?: ErrorCallback): boolean; + openssh_forwardOutStreamLocal(socketPath: string, callback?: ChannelCallback): boolean; + } + + interface ConnectConfig { + /** + * @description Hostname or IP address of the server. + * @default 'localhost' + */ + host?: string; + /** + * @description Port number of the server. + * @default 22 + */ + port?: number; + /** + * @description Only connect via resolved IPv4 address for `host`. + * @default false + */ + forceIPv4?: boolean; + /** + * @description Only connect via resolved IPv6 address for `host`. + * @default false + */ + forceIPv6?: boolean; + /** + * @description 'md5' or 'sha1'. The host's key is hashed using this method and passed to the **hostVerifier** function. + * @default (none) + */ + hostHash?: string; + /** + * @description Function that is passed a string hex hash of the host's key for verification purposes. + * Return `true` to continue with the handshake or `false` to reject and disconnect. + */ + hostVerifier?: (keyHash: string) => boolean; + /** + * @description Username for authentication. + */ + username?: string; + /** + * @description Password for password-based user authentication. + */ + password?: string; + /** + * @description Path to ssh-agent's UNIX socket for ssh-agent-based user authentication. + * Windows users: set to 'pageant' for authenticating with Pageant or (actual) path to a cygwin "UNIX socket." + */ + agent?: string; + /** + * @description Buffer or string that contains a private key for either key-based or hostbased user authentication (OpenSSH format). + */ + privateKey?: Buffer|string; + /** + * @description For an encrypted private key, this is the passphrase used to decrypt it. + */ + passphrase?: string; + /** + * @description Along with **localUsername** and **privateKey**, set this to a non-empty string for hostbased user authentication. + */ + localHostname?: string; + /** + * @description Along with **localHostname** and **privateKey**, set this to a non-empty string for hostbased user authentication. + */ + localUsername?: string; + /** + * @description Try keyboard-interactive user authentication if primary user authentication method fails. + * If you set this to `true`, you need to handle the `keyboard-interactive` event. + */ + tryKeyboard: boolean; + /** + * @description How often (in milliseconds) to send SSH-level keepalive packets to the server + * (in a similar way as OpenSSH's ServerAliveInterval config option). Set to 0 to disable. + * @default 0 + */ + keepaliveInterval?: number; + /** + * @description How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server + * before disconnection (similar to OpenSSH's ServerAliveCountMax config option). + * @default 3 + */ + keepaliveCountMax?: number; + /** + * @description How long (in milliseconds) to wait for the SSH handshake to complete. + * @default 20000 + */ + readyTimeout?: number; + /** + * @description Performs a strict server vendor check before sending vendor-specific requests, + * etc. (e.g. check for OpenSSH server when using `openssh_noMoreSessions()`) + * @default true + */ + strictVendor?: boolean; + /** + * A ReadableStream to use for communicating with the server instead of creating and using a new TCP connection (useful for connection hopping). + */ + sock?: NodeJS.ReadableStream; + /** + * @description Set to `true` to use OpenSSH agent forwarding (`auth-agent@openssh.com`) for the life of the connection. `agent` must also be set to use this feature. + * @default false + */ + agentForward?: boolean; + /** + * @description Set this to a function that receives a single string argument to get detailed (local) debug information. + */ + debug: (information: string) => any; + } + + interface ExecOption { + /** + * @description An environment to use for the execution of the command. + */ + env?: any; + /** + * @description Set to true to allocate a pseudo-tty with defaults, or an object containing specific pseudo-tty settings + * (see 'Pseudo-TTY settings'). Setting up a pseudo-tty can be useful when working with remote processes + * that expect input from an actual terminal (e.g. sudo's password prompt). + */ + pty?: boolean|PseudoTtySettings; + /** + * @description Set to true to use defaults below, set to a number to specify a specific screen number, + * or an object. + */ + x11?: boolean|number|X11Settings; + } + + interface X11Settings { + /** + * Allow just a single connection? + * @default false + */ + single?: boolean; + /** + * Screen number to use + * @default 0 + */ + screen?: number; + } + + interface PseudoTtySettings { + /** + * @description * Number of rows + * @default 24 + */ + rows?: number; + /** + * @description * Number of columns + * @default 80 + */ + cols?: number; + /** + * @description * Height in pixels + * @default 480 + */ + height?: number; + /** + * @description * Width in pixels + * @default 640 + */ + width?: number; + /** + * @description The value to use for $TERM + * @default 'vt100' + */ + term?: string; + } + + interface ForwardInCallback { + (err?: Error, bindPort?: number): void; + } + + interface ChannelCallback { + (err?: Error, channel?: Channel): void; + } + + interface SftpCallback { + (err?: Error, sftp?: Sftp.Wrapper): void; + } + + interface ErrorCallback { + (err?: Error): void; + } + + interface Channel extends stream.Duplex { + new(info?: any, client?: any, options?: any): Channel; + eof(): boolean; + close(): boolean; + destroy(): void; + setWindow(rows: number, cols: number, height: number, width: number): boolean; + signal(signalName: string): boolean; + exit(name: string, coreDumped: boolean, msg: string): boolean; + exit(status: number): boolean; + stderr?: ServerStderr; + + // EventEmitter overrides + addListener(event: string, listener: Function): Channel; + on(event: string, listener: Function): Channel; + once(event: string, listener: Function): Channel; + removeListener(event: string, listener: Function): Channel; + removeAllListeners(event?: string): Channel; + } + + interface ServerStderr extends NodeJS.WritableStream { + new(channel: Channel): ServerStderr; + } + + interface ServerStatic { + new(config: ServerConfig, listener?: any): Server; + createServer(config: ServerConfig, listener?: any): Server; + KEEPALIVE_INTERVAL: number; + KEEPALIVE_CLIENT_INTERVAL: number; + KEEPALIVE_CLIENT_COUNT_MAX: number; + } + + interface Server extends NodeJS.EventEmitter { + listen(port: number, hostname?: string, backlog?: number, callback?: Function): Server; + listen(port: number, hostname?: string, callback?: Function): Server; + listen(path: string, callback?: Function): Server; + listen(handle: any, listeningListener?: Function): Server; + address(): { port: number; family: string; address: string; }; + getConnections(callback: any): any; //TODO: No type + close(callback?: Function): Server; + ref(): void; + unref(): void; + } + + interface ServerConfig { + /** + * @description Buffer or string that contains the host private key (OpenSSH format). + */ + privateKey: Buffer|string; + /** + * @description For an encrypted host private key, this is the passphrase used to decrypt it. + */ + passphrase?: string; + /** + * @description A message that is sent to clients immediately upon connection, before handshaking begins. + */ + banner?: string; + /** + * @description A custom server software name/version identifier. + * @default 'ssh2js' + moduleVersion + 'srv' + */ + indent?: string; + /** + * @description This is the highWaterMark to use for the parser stream. + * @default 32 * 1024 + */ + highWaterMark?: number; + /** + * @description Set this to a function that receives a single string argument to get detailed (local) debug information. + */ + debug?: (information: string) => any; + } + + // utility methods from ssh2-streams. + interface Utils { + iv_inc: Function; + isStreamCipher: Function; + isGCM: Function; + readInt: Function; + readString: Function; + parseKey: Function; + genPublicKey: Function; + convertPPKPrivate: Function; + verifyPPKMAC: Function; + decryptKey: Function; + } + + namespace Sftp { + // SFTPStream.STATUS_CODE from ssh2-streams. + interface StatusCode { + OK: number; + EOF: number; + NO_SUCH_FILE: number; + PERMISSION_DENIED: number; + FAILURE: number; + BAD_MESSAGE: number; + NO_CONNECTION: number; + CONNECTION_LOST: number; + OP_UNSUPPORTED: number; + } + + // SFTPStream.OPEN_MODE from ssh2-streams. + interface OpenMode { + READ: number; + WRITE: number; + APPEND: number; + CREAT: number; + TRUNC: number; + EXCL: number; + } + + interface Wrapper extends NodeJS.EventEmitter { + //TODO: extends `ssh2-streams.SFTPStream` + } + } + } + + var ssh2: ssh2.Client; + + export = ssh2; +} +