├── LICENSE ├── README.md ├── lib └── index.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Brian White. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | SSH into your [node.js](http://nodejs.org/) process and access a REPL. 5 | 6 | Requirements 7 | ============ 8 | 9 | * [node.js](http://nodejs.org/) -- v4.0.0 or newer 10 | 11 | 12 | Install 13 | ======= 14 | 15 | npm install ssh-repl 16 | 17 | 18 | Example 19 | ======= 20 | 21 | ```javascript 22 | const fs = require('fs'); 23 | 24 | const sshrepl = require('ssh-repl'); 25 | 26 | const repl = sshrepl({ 27 | server: { 28 | hostKeys: [ fs.readFileSync('host.key') ] 29 | }, 30 | users: { 31 | foo: { 32 | publicKey: fs.readFileSync('foo-key.pub'), 33 | repl: { prompt: 'foo> ' } 34 | }, 35 | bar: { 36 | password: 'baz', 37 | repl: { prompt: 'bar> ' } 38 | } 39 | }, 40 | port: 2244 41 | }, function(err, boundPort) { 42 | if (err) throw err; 43 | console.log('SSH REPL listening'); 44 | }); 45 | 46 | // Call `repl.close()` to stop listening 47 | ``` 48 | 49 | API 50 | === 51 | 52 | `require('ssh-repl')` returns a function that creates and starts an SSH REPL. It has the signature: 53 | 54 | * (< _object_ >config[, < _function_ >callback]) - _object_ - Creates and starts an SSH REPL. The object returned contains a `.close()` method to stop the server. It accepts an optional callback that is called when the server is closed. `config` can contain: 55 | 56 | * **server** - _object_ - The configuration for the SSH server. See the [`ssh2`](https://github.com/mscdex/ssh2#server-methods) documentation for a list of supported properties. 57 | 58 | * **port** - _integer_ - Port number to listen on. 59 | 60 | * **users** - _mixed_ - The user configuration. This is used to both authenticate users and to optionally pass settings to [`repl.start()`](https://nodejs.org/docs/latest/api/repl.html#repl_repl_start_options). If `users` is a _function_, it is passed two arguments: (< _string_ >username, < _function_ >callback), where `callback` has the signature (< _Error_ >err, < _object_ >userConfig). If `users` is an object, it should be keyed on username, with the value being the user configuration. Allowed user configuration properties: 61 | 62 | * One of two authentication methods is required: 63 | 64 | * **password** - _string_ - The password for the user. 65 | 66 | * **publicKey** - _mixed_ - The public key for the user. This value can be a _Buffer_ instance or a _string_. 67 | 68 | * **repl** - _object_ - If supplied, the properties on this object are passed on to [`repl.start()`](https://nodejs.org/docs/latest/api/repl.html#repl_repl_start_options). 69 | 70 | If `callback` is supplied, it is called once the SSH REPL is listening for incoming connections. It has the signature (< _Error_ >err, < _number_ >boundPort). The `boundPort` argument is useful when binding on port 0. 71 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const repl = require('repl'); 2 | const crypto = require('crypto'); 3 | const Transform = require('stream').Transform; 4 | const inherits = require('util').inherits; 5 | const inspect = require('util').inspect; 6 | 7 | const ssh2 = require('ssh2'); 8 | const Server = ssh2.Server; 9 | const genPublicKey = ssh2.utils.genPublicKey; 10 | const parseKey = ssh2.utils.parseKey; 11 | 12 | module.exports = function createServer(cfg, cb) { 13 | if (typeof cfg !== 'object' || cfg === null) 14 | throw new Error('Missing/Invalid configuration'); 15 | const srv = new Server(cfg.server); 16 | if (typeof cfg.port !== 'number') 17 | throw new Error('Missing/Invalid port'); 18 | if (typeof cfg.users !== 'object' && typeof cfg.users !== 'function') 19 | throw new Error('Missing/Invalid users configuration'); 20 | 21 | const users_ = cfg.users; 22 | var users; 23 | if (typeof users_ === 'function') 24 | users = users_; 25 | else { 26 | users = function usersWrapper(user, callback) { 27 | const userCfg = users_[user]; 28 | if (userCfg === undefined) 29 | process.nextTick(callback, true); 30 | else 31 | process.nextTick(callback, null, userCfg); 32 | }; 33 | } 34 | 35 | srv.on('connection', function onConnection(client, info) { 36 | var inSession = false; 37 | var replCfg; 38 | client.on('authentication', function onAuthentication(ctx) { 39 | users(ctx.username, function usersCallback(err, userCfg) { 40 | if (typeof userCfg === 'object' && userCfg !== null) { 41 | if (ctx.method === 'password' 42 | && typeof userCfg.password === 'string') { 43 | return bufferEquals(new Buffer(ctx.password), 44 | new Buffer('' + userCfg.password), 45 | function(err, equal) { 46 | if (err || !equal) 47 | return ctx.reject(); 48 | replCfg = userCfg.repl; 49 | ctx.accept(); 50 | }); 51 | } else if (ctx.method === 'publickey' 52 | && (Buffer.isBuffer(userCfg.publicKey) 53 | || typeof userCfg.publicKey === 'string')) { 54 | // TODO: cache parsed/generated result? 55 | const pubKey = genPublicKey(parseKey(userCfg.publicKey)); 56 | if (ctx.key.algo === pubKey.fulltype) { 57 | return bufferEquals(ctx.key.data, 58 | pubKey.public, 59 | function(err, equal) { 60 | if (err || !equal) 61 | return ctx.reject(); 62 | if (ctx.signature) { 63 | const verifier = crypto.createVerify(ctx.sigAlgo); 64 | verifier.update(ctx.blob); 65 | if (verifier.verify(pubKey.publicOrig, ctx.signature)) { 66 | replCfg = userCfg.repl; 67 | ctx.accept(); 68 | } else { 69 | ctx.reject(); 70 | } 71 | } else { 72 | // if no signature present, that means the client is just 73 | // checking the validity of the given public key 74 | ctx.accept(); 75 | } 76 | }); 77 | } 78 | } 79 | } 80 | ctx.reject(); 81 | }); 82 | }); 83 | client.once('ready', function onClientReady() { 84 | client.on('session', function(accept, reject) { 85 | if (inSession) 86 | return reject(); 87 | inSession = true; 88 | const session = accept(); 89 | var columns = 0; 90 | session.once('pty', function(accept, reject, info) { 91 | columns = info.cols; 92 | accept(); 93 | }); 94 | session.once('shell', function(accept, reject) { 95 | const stream = accept(); 96 | 97 | // XXX: Using a newline converter is a hack until modern versions of 98 | // node output `\r\n` instead of `\n` for REPL-specific output and 99 | // `util.inspect()` output 100 | const convertStream = new NLConverter(); 101 | convertStream.pipe(stream); 102 | if (columns > 0) 103 | convertStream.columns = columns; //stream.columns = columns; 104 | stream.on('setWindow', function(rows, cols, height, width) { 105 | convertStream.columns = cols; //stream.columns = cols; 106 | }); 107 | 108 | const options = { 109 | input: stream, 110 | output: convertStream, //stream, 111 | terminal: (columns > 0), 112 | }; 113 | 114 | if (typeof replCfg === 'object' && replCfg !== null) { 115 | const keys = Object.keys(replCfg); 116 | for (var i = 0; i < keys.length; ++i) { 117 | const key = keys[i]; 118 | if (key === 'input' || key === 'output') 119 | continue; 120 | options[key] = replCfg[key]; 121 | } 122 | } 123 | 124 | repl.start(options).once('exit', function () { 125 | stream.close(); 126 | }); 127 | stream.once('close', function() { 128 | client.end(); 129 | }); 130 | }); 131 | session.once('close', function() { 132 | inSession = false; 133 | }); 134 | }); 135 | }); 136 | client.on('error', function onClientError(err) { }); 137 | }); 138 | 139 | 140 | if (typeof cb === 'function') { 141 | srv.on('error', function onServerError(err) { 142 | cb(err); 143 | }); 144 | } 145 | srv.listen(cfg.port, function onListening() { 146 | typeof cb === 'function' && cb(null, srv.address().port); 147 | }); 148 | 149 | return { 150 | close: function() { 151 | srv.close(); 152 | } 153 | }; 154 | }; 155 | 156 | function bufferEquals(a, b, cb) { 157 | crypto.randomBytes(32, function(err, key) { 158 | if (err) 159 | return cb(err); 160 | const ah = crypto.createHmac('sha256', key).update(a).digest(); 161 | const bh = crypto.createHmac('sha256', key).update(b).digest(); 162 | cb(null, ah.equals(bh)); 163 | }); 164 | } 165 | 166 | // Converts `\n` to `\r\n` 167 | const CR = Buffer.from('\r'); 168 | function NLConverter() { 169 | Transform.call(this); 170 | } 171 | inherits(NLConverter, Transform); 172 | NLConverter.prototype._transform = function(chunk, enc, cb) { 173 | var i = 0; 174 | var last = 0; 175 | while ((i = chunk.indexOf(10, i)) !== -1) { 176 | if (i === 0) { 177 | this.push(CR); 178 | } else if (chunk[i - 1] !== 13) { 179 | this.push(chunk.slice(last, i)); 180 | this.push(CR); 181 | last = i; 182 | } 183 | ++i; 184 | } 185 | if (last === 0) 186 | this.push(chunk); 187 | else 188 | this.push(chunk.slice(last)); 189 | cb(); 190 | }; 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssh-repl", 3 | "version": "0.0.4", 4 | "description": "SSH into your node.js process and access a REPL", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/mscdex/ssh-repl.git" 9 | }, 10 | "keywords": [ 11 | "ssh", 12 | "repl" 13 | ], 14 | "author": "Brian White ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/mscdex/ssh-repl/issues" 18 | }, 19 | "homepage": "https://github.com/mscdex/ssh-repl#readme", 20 | "dependencies": { 21 | "ssh2": "^0.5.0" 22 | }, 23 | "engines": { 24 | "node": ">= 4.0.0" 25 | } 26 | } 27 | --------------------------------------------------------------------------------