├── .gitignore ├── History.md ├── .travis.yml ├── package.json ├── test └── test.js ├── repl.js ├── README.md └── lib └── telnet.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.0.1 / 2012-09-02 2 | ================== 3 | 4 | - Initial release 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.9 5 | - 0.10 6 | - 0.11 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telnet", 3 | "description": "Telnet implementation for Node.js", 4 | "keywords": [ 5 | "telnet" 6 | ], 7 | "version": "0.0.1", 8 | "author": "Nathan Rajlich (http://tootallnate.net)", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/TooTallNate/node-telnet.git" 12 | }, 13 | "main": "lib/telnet.js", 14 | "scripts": { 15 | "test": "mocha --reporter spec" 16 | }, 17 | "devDependencies": { 18 | "mocha": "~1.18.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var telnet = require('../'); 3 | var net = require('net'); 4 | 5 | var client_responder = null; 6 | var client = null; 7 | var server = null; 8 | var port = 1337; 9 | 10 | describe('telnet', function () { 11 | it('should export a function', function () { 12 | assert.equal('function', typeof telnet); 13 | }); 14 | 15 | describe('create server', function () { 16 | before(function (done) { 17 | server = telnet.createServer(function (c) { 18 | c.on('data', function (b) { 19 | c.write(b); 20 | }); 21 | }); 22 | server.listen(port); 23 | server.on('listening', function () { 24 | done(); 25 | }); 26 | }); 27 | 28 | after(function (done) { 29 | client.end(function () { 30 | client = null; 31 | server.close(function () { 32 | server = null; 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | it('should be listening on port ' + port, function (done) { 39 | client = net.connect({port: port}, function () { 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should echo any data sent to it', function (done) { 45 | var stringToSend = 'test string'; 46 | client.on('data', function (b) { 47 | b = b.toString('utf8'); 48 | assert.equal(stringToSend, b); 49 | done(); 50 | }); 51 | 52 | client.write(stringToSend); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /repl.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Usage of node-telnet with node's REPL, in full-featured "terminal" mode. 4 | * (Requires node >= v0.7.7) 5 | */ 6 | 7 | var telnet = require('./') 8 | , repl = require('repl') 9 | , port = Number(process.argv[2]) || 1337 10 | 11 | var server = telnet.createServer(function (client) { 12 | 13 | client.on('window size', function (e) { 14 | if (e.command === 'sb') { 15 | // a real "resize" event; 'readline' listens for this 16 | client.columns = e.columns 17 | client.rows = e.rows 18 | client.emit('resize') 19 | } 20 | }) 21 | 22 | client.on('suppress go ahead', console.log) 23 | client.on('echo', console.log) 24 | client.on('window size', console.log) 25 | client.on('x display location', console.log) 26 | client.on('terminal speed', console.log) 27 | client.on('environment variables', console.log) 28 | client.on('transmit binary', console.log) 29 | client.on('status', console.log) 30 | client.on('linemode', console.log) 31 | client.on('authentication', console.log) 32 | 33 | // 'readline' will call `setRawMode` when it is a function 34 | client.setRawMode = setRawMode 35 | 36 | // make unicode characters work properly 37 | client.do.transmit_binary() 38 | 39 | // emit 'window size' events 40 | client.do.window_size() 41 | 42 | // create the REPL 43 | var r = repl.start({ 44 | input: client 45 | , output: client 46 | , prompt: 'telnet repl> ' 47 | , terminal: true 48 | , useGlobal: false 49 | }).on('exit', function () { 50 | client.end() 51 | }) 52 | 53 | r.context.r = r 54 | r.context.client = client 55 | r.context.socket = client 56 | 57 | }) 58 | 59 | server.on('error', function (err) { 60 | if (err.code == 'EACCES') { 61 | console.error('%s: You must be "root" to bind to port %d', err.code, port) 62 | } else { 63 | throw err 64 | } 65 | }) 66 | 67 | server.on('listening', function () { 68 | console.log('node repl telnet(1) server listening on port %d', this.address().port) 69 | console.log(' $ telnet localhost' + (port != 23 ? ' ' + port : '')) 70 | }) 71 | 72 | server.listen(port) 73 | 74 | /** 75 | * The equivalent of "raw mode" via telnet option commands. 76 | * Set this function on a telnet `client` instance. 77 | */ 78 | 79 | function setRawMode (mode) { 80 | if (mode) { 81 | this.do.suppress_go_ahead() 82 | this.will.suppress_go_ahead() 83 | this.will.echo() 84 | } else { 85 | this.dont.suppress_go_ahead() 86 | this.wont.suppress_go_ahead() 87 | this.wont.echo() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-telnet 2 | =========== 3 | ### Telnet implementation for Node.js 4 | [![Build Status](https://secure.travis-ci.org/TooTallNate/node-telnet.png)](http://travis-ci.org/TooTallNate/node-telnet) 5 | 6 | 7 | This module offers an implementation of the [Telnet Protocol (RFC854)][rfc], 8 | making it possible to write a telnet server that can interact with the various 9 | telnet features. 10 | 11 | ### Implemented Options: 12 | 13 | | **Name** | **Event** |**Specification** 14 | |:--------------------|:----------------------|:------------------------- 15 | | Binary transmission | `'transmit binary'` | [RFC856](http://tools.ietf.org/html/rfc856) 16 | | Echo | `'echo'` | [RFC857](http://tools.ietf.org/html/rfc857) 17 | | Suppress Go Ahead | `'suppress go ahead'` | [RFC858](http://tools.ietf.org/html/rfc858) 18 | | Window Size | `'window size'` | [RFC1073](http://tools.ietf.org/html/rfc1073) 19 | 20 | 21 | Installation 22 | ------------ 23 | 24 | Install with `npm`: 25 | 26 | ``` bash 27 | $ npm install telnet 28 | ``` 29 | 30 | 31 | Examples 32 | -------- 33 | 34 | ``` js 35 | var telnet = require('telnet') 36 | 37 | telnet.createServer(function (client) { 38 | 39 | // make unicode characters work properly 40 | client.do.transmit_binary() 41 | 42 | // make the client emit 'window size' events 43 | client.do.window_size() 44 | 45 | // listen for the window size events from the client 46 | client.on('window size', function (e) { 47 | if (e.command === 'sb') { 48 | console.log('telnet window resized to %d x %d', e.width, e.height) 49 | } 50 | }) 51 | 52 | // listen for the actual data from the client 53 | client.on('data', function (b) { 54 | client.write(b) 55 | }) 56 | 57 | client.write('\nConnected to Telnet server!\n') 58 | 59 | }).listen(23) 60 | ``` 61 | 62 | And then you can connect to your server using `telnet(1)` 63 | 64 | ``` bash 65 | $ telnet localhost 66 | Trying ::1... 67 | telnet: connect to address ::1: Connection refused 68 | Trying 127.0.0.1... 69 | Connected to localhost. 70 | Escape character is '^]'. 71 | 72 | Connected to Telnet server! 73 | ``` 74 | 75 | 76 | License 77 | ------- 78 | 79 | (The MIT License) 80 | 81 | Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net> 82 | 83 | Permission is hereby granted, free of charge, to any person obtaining 84 | a copy of this software and associated documentation files (the 85 | 'Software'), to deal in the Software without restriction, including 86 | without limitation the rights to use, copy, modify, merge, publish, 87 | distribute, sublicense, and/or sell copies of the Software, and to 88 | permit persons to whom the Software is furnished to do so, subject to 89 | the following conditions: 90 | 91 | The above copyright notice and this permission notice shall be 92 | included in all copies or substantial portions of the Software. 93 | 94 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 95 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 96 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 97 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 98 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 99 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 100 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 101 | 102 | [rfc]: http://tools.ietf.org/html/rfc854 103 | -------------------------------------------------------------------------------- /lib/telnet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Telnet server implementation. 3 | * 4 | * References: 5 | * - http://tools.ietf.org/html/rfc854 6 | * - http://support.microsoft.com/kb/231866 7 | * - http://www.iana.org/assignments/telnet-options 8 | * 9 | */ 10 | 11 | /** 12 | * Modules 13 | */ 14 | 15 | var net = require('net') 16 | , assert = require('assert') 17 | , EventEmitter = require('events').EventEmitter 18 | , Stream = require('stream').Stream 19 | , util = require('util'); 20 | 21 | /** 22 | * Constants 23 | */ 24 | 25 | var COMMANDS = { 26 | SE: 240, // end of subnegotiation parameters 27 | NOP: 241, // no operation 28 | DM: 242, // data mark 29 | BRK: 243, // break 30 | IP: 244, // suspend (a.k.a. "interrupt process") 31 | AO: 245, // abort output 32 | AYT: 246, // are you there? 33 | EC: 247, // erase character 34 | EL: 248, // erase line 35 | GA: 249, // go ahead 36 | SB: 250, // subnegotiation 37 | WILL: 251, // will 38 | WONT: 252, // wont 39 | DO: 253, // do 40 | DONT: 254, // dont 41 | IAC: 255 // interpret as command 42 | }; 43 | 44 | var COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(out, key) { 45 | var value = COMMANDS[key]; 46 | out[value] = key.toLowerCase(); 47 | return out; 48 | }, {}); 49 | 50 | var OPTIONS = { 51 | TRANSMIT_BINARY: 0, // http://tools.ietf.org/html/rfc856 52 | ECHO: 1, // http://tools.ietf.org/html/rfc857 53 | RECONNECT: 2, // http://tools.ietf.org/html/rfc671 54 | SUPPRESS_GO_AHEAD: 3, // http://tools.ietf.org/html/rfc858 55 | AMSN: 4, // Approx Message Size Negotiation 56 | // https://google.com/search?q=telnet+option+AMSN 57 | STATUS: 5, // http://tools.ietf.org/html/rfc859 58 | TIMING_MARK: 6, // http://tools.ietf.org/html/rfc860 59 | RCTE: 7, // http://tools.ietf.org/html/rfc563 60 | // http://tools.ietf.org/html/rfc726 61 | NAOL: 8, // (Negotiate) Output Line Width 62 | // https://google.com/search?q=telnet+option+NAOL 63 | // http://tools.ietf.org/html/rfc1073 64 | NAOP: 9, // (Negotiate) Output Page Size 65 | // https://google.com/search?q=telnet+option+NAOP 66 | // http://tools.ietf.org/html/rfc1073 67 | NAOCRD: 10, // http://tools.ietf.org/html/rfc652 68 | NAOHTS: 11, // http://tools.ietf.org/html/rfc653 69 | NAOHTD: 12, // http://tools.ietf.org/html/rfc654 70 | NAOFFD: 13, // http://tools.ietf.org/html/rfc655 71 | NAOVTS: 14, // http://tools.ietf.org/html/rfc656 72 | NAOVTD: 15, // http://tools.ietf.org/html/rfc657 73 | NAOLFD: 16, // http://tools.ietf.org/html/rfc658 74 | EXTEND_ASCII: 17, // http://tools.ietf.org/html/rfc698 75 | LOGOUT: 18, // http://tools.ietf.org/html/rfc727 76 | BM: 19, // http://tools.ietf.org/html/rfc735 77 | DET: 20, // http://tools.ietf.org/html/rfc732 78 | // http://tools.ietf.org/html/rfc1043 79 | SUPDUP: 21, // http://tools.ietf.org/html/rfc734 80 | // http://tools.ietf.org/html/rfc736 81 | SUPDUP_OUTPUT: 22, // http://tools.ietf.org/html/rfc749 82 | SEND_LOCATION: 23, // http://tools.ietf.org/html/rfc779 83 | TERMINAL_TYPE: 24, // http://tools.ietf.org/html/rfc1091 84 | END_OF_RECORD: 25, // http://tools.ietf.org/html/rfc885 85 | TUID: 26, // http://tools.ietf.org/html/rfc927 86 | OUTMRK: 27, // http://tools.ietf.org/html/rfc933 87 | TTYLOC: 28, // http://tools.ietf.org/html/rfc946 88 | REGIME_3270: 29, // http://tools.ietf.org/html/rfc1041 89 | X3_PAD: 30, // http://tools.ietf.org/html/rfc1053 90 | NAWS: 31, // http://tools.ietf.org/html/rfc1073 91 | TERMINAL_SPEED: 32, // http://tools.ietf.org/html/rfc1079 92 | TOGGLE_FLOW_CONTROL: 33, // http://tools.ietf.org/html/rfc1372 93 | LINEMODE: 34, // http://tools.ietf.org/html/rfc1184 94 | X_DISPLAY_LOCATION: 35, // http://tools.ietf.org/html/rfc1096 95 | ENVIRON: 36, // http://tools.ietf.org/html/rfc1408 96 | AUTHENTICATION: 37, // http://tools.ietf.org/html/rfc2941 97 | // http://tools.ietf.org/html/rfc1416 98 | // http://tools.ietf.org/html/rfc2942 99 | // http://tools.ietf.org/html/rfc2943 100 | // http://tools.ietf.org/html/rfc2951 101 | ENCRYPT: 38, // http://tools.ietf.org/html/rfc2946 102 | NEW_ENVIRON: 39, // http://tools.ietf.org/html/rfc1572 103 | TN3270E: 40, // http://tools.ietf.org/html/rfc2355 104 | XAUTH: 41, // https://google.com/search?q=telnet+option+XAUTH 105 | CHARSET: 42, // http://tools.ietf.org/html/rfc2066 106 | RSP: 43, // http://tools.ietf.org/html/draft-barnes-telnet-rsp-opt-01 107 | COM_PORT_OPTION: 44, // http://tools.ietf.org/html/rfc2217 108 | SLE: 45, // http://tools.ietf.org/html/draft-rfced-exp-atmar-00 109 | START_TLS: 46, // http://tools.ietf.org/html/draft-altman-telnet-starttls-02 110 | KERMIT: 47, // http://tools.ietf.org/html/rfc2840 111 | SEND_URL: 48, // http://tools.ietf.org/html/draft-croft-telnet-url-trans-00 112 | FORWARD_X: 49, // http://tools.ietf.org/html/draft-altman-telnet-fwdx-01 113 | PRAGMA_LOGON: 138, // https://google.com/search?q=telnet+option+PRAGMA_LOGON 114 | SSPI_LOGON: 139, // https://google.com/search?q=telnet+option+SSPI_LOGON 115 | PRAGMA_HEARTBEAT: 140, // https://google.com/search?q=telnet+option+PRAMGA_HEARTBEAT 116 | EXOPL: 255 // http://tools.ietf.org/html/rfc861 117 | }; 118 | 119 | var OPTION_NAMES = Object.keys(OPTIONS).reduce(function(out, key) { 120 | var value = OPTIONS[key]; 121 | out[value] = key.toLowerCase(); 122 | return out; 123 | }, {}); 124 | 125 | var SUB = { 126 | IS: 0, 127 | SEND: 1, 128 | INFO: 2, 129 | VARIABLE: 0, 130 | VALUE: 1, 131 | ESC: 2, // unused, for env 132 | USER_VARIABLE: 3 133 | }; 134 | 135 | /** 136 | * Client 137 | */ 138 | 139 | function Client(options) { 140 | var self = this; 141 | 142 | if (!(this instanceof Client)) { 143 | return new Client(arguments[0], arguments[1], arguments[2]); 144 | } 145 | 146 | Stream.call(this); 147 | 148 | if (options.addListener) { 149 | options = { 150 | input: arguments[0], 151 | output: arguments[1], 152 | server: arguments[2] 153 | }; 154 | } 155 | 156 | if (options.socket) { 157 | options.input = options.socket; 158 | options.output = options.socket; 159 | } 160 | 161 | if (!options.output) { 162 | options.output = options.input; 163 | options.socket = options.input; 164 | } 165 | 166 | this.input = options.input; 167 | this.output = options.output; 168 | this.socket = options.socket; 169 | this.server = options.server; 170 | this.env = {}; 171 | this.terminal = 'ansi'; 172 | 173 | this.options = options; 174 | this.options.convertLF = options.convertLF !== false; 175 | 176 | if (this.options.tty) { 177 | this.setRawMode = this._setRawMode; 178 | this.isTTY = true; 179 | this.isRaw = false; 180 | this.columns = 80; 181 | this.rows = 24; 182 | } 183 | 184 | this.open(); 185 | } 186 | 187 | Client.prototype.__proto__ = Stream.prototype; 188 | 189 | Client.prototype.debug = function() { 190 | var args = Array.prototype.slice.call(arguments) 191 | , msg; 192 | 193 | if (!this.remoteAddress && this.input.remoteAddress) { 194 | this.remoteAddress = this.input.remoteAddress; 195 | } 196 | 197 | args.push('(' + this.remoteAddress + ')'); 198 | 199 | if (this.listeners('debug').length) { 200 | msg = util.format.apply(util.format, args); 201 | this.emit('debug', msg); 202 | } 203 | 204 | if (this.server && this.server.listeners('debug').length) { 205 | msg = util.format.apply(util.format, args); 206 | this.server.emit('debug', msg); 207 | } 208 | 209 | if (this.options.debug) { 210 | args.push('(' + this.input.remoteAddress + ')'); 211 | console.error(args); 212 | } 213 | }; 214 | 215 | Client.prototype.open = function() { 216 | var self = this; 217 | 218 | ['DO', 'DONT', 'WILL', 'WONT'].forEach(function(commandName) { 219 | self[commandName.toLowerCase()] = {}; 220 | Object.keys(OPTIONS).forEach(function(optionName) { 221 | var optionCode = OPTIONS[optionName]; 222 | self[commandName.toLowerCase()][optionName.toLowerCase()] = function() { 223 | var buf = new Buffer(3); 224 | buf[0] = COMMANDS.IAC; 225 | buf[1] = COMMANDS[commandName]; 226 | buf[2] = optionCode; 227 | return self.output.write(buf); 228 | } 229 | }); 230 | }); 231 | 232 | // compat 233 | ['DO', 'DONT', 'WILL', 'WONT'].forEach(function(commandName) { 234 | var cmd = commandName.toLowerCase(); 235 | self[cmd].window_size = self[cmd].naws; 236 | self[cmd].environment_variables = self[cmd].new_environ; 237 | }); 238 | 239 | this.input.on('end', function() { 240 | self.debug('ended'); 241 | self.emit('end'); 242 | }); 243 | 244 | this.input.on('close', function() { 245 | self.debug('closed'); 246 | self.emit('close'); 247 | }); 248 | 249 | this.input.on('drain', function() { 250 | self.emit('drain'); 251 | }); 252 | 253 | this.input.on('error', function(err) { 254 | self.debug('error: %s', err ? err.message + '' : 'Unknown'); 255 | self.emit('error', err); 256 | }); 257 | 258 | this.input.on('data', function(data) { 259 | self.parse(data); 260 | }); 261 | 262 | if (this.options.tty) { 263 | this.do.transmit_binary(); 264 | this.do.terminal_type(); 265 | this.do.naws(); 266 | this.do.new_environ(); 267 | } 268 | }; 269 | 270 | Client.prototype.parse = function(data) { 271 | var bufs = [] 272 | , i = 0 273 | , l = 0 274 | , needsPush = false 275 | , cdata 276 | , iacCode 277 | , iacName 278 | , commandCode 279 | , commandName 280 | , optionCode 281 | , optionName 282 | , cmd 283 | , len; 284 | 285 | if (this._last) { 286 | data = Buffer.concat([this._last.data, data]); 287 | i = this._last.i; 288 | l = this._last.l; 289 | delete this._last; 290 | } 291 | 292 | for (; i < data.length; i++) { 293 | if (data.length - 1 - i >= 2 294 | && data[i] === COMMANDS.IAC 295 | && COMMAND_NAMES[data[i + 1]] 296 | && OPTION_NAMES[data[i + 2]]) { 297 | cdata = data.slice(i); 298 | 299 | iacCode = cdata.readUInt8(0); 300 | iacName = COMMAND_NAMES[iacCode]; 301 | commandCode = cdata.readUInt8(1); 302 | commandName = COMMAND_NAMES[commandCode]; 303 | optionCode = cdata.readUInt8(2); 304 | optionName = OPTION_NAMES[optionCode]; 305 | 306 | cmd = { 307 | command: commandName, // compat 308 | option: optionName.replace(/_/g, ' '), // compat 309 | iacCode: iacCode, 310 | iacName: iacName, 311 | commandCode: commandCode, 312 | commandName: commandName, 313 | optionCode: optionCode, 314 | optionName: optionName, 315 | data: cdata 316 | }; 317 | 318 | // compat 319 | if (cmd.option === 'new environ') { 320 | cmd.option = 'environment variables'; 321 | } else if (cmd.option === 'naws') { 322 | cmd.option = 'window size'; 323 | } 324 | 325 | if (this[cmd.optionName]) { 326 | try { 327 | len = this[cmd.optionName](cmd); 328 | } catch (e) { 329 | if (!(e instanceof RangeError)) { 330 | this.debug('error: %s', e.message); 331 | this.emit('error', e); 332 | return; 333 | } 334 | len = -1; 335 | this.debug('Not enough data to parse.'); 336 | } 337 | } else { 338 | if (cmd.commandCode === COMMANDS.SB) { 339 | len = 0; 340 | while (cdata[len] && cdata[len] !== COMMANDS.SE) { 341 | len++; 342 | } 343 | if (!cdata[len]) { 344 | len = 3; 345 | } else { 346 | len++; 347 | } 348 | } else { 349 | len = 3; 350 | } 351 | cmd.data = cmd.data.slice(0, len); 352 | this.debug('Unknown option: %s', cmd.optionName); 353 | } 354 | 355 | if (len === -1) { 356 | this.debug('Waiting for more data.'); 357 | this.debug(iacName, commandName, optionName, cmd.values || len); 358 | this._last = { 359 | data: data, 360 | i: i, 361 | l: l 362 | }; 363 | return; 364 | } 365 | 366 | this.debug(iacName, commandName, optionName, cmd.values || len); 367 | 368 | this.emit('command', cmd); 369 | 370 | needsPush = true; 371 | l = i + len; 372 | i += len - 1; 373 | } else { 374 | if (data[i] === COMMANDS.IAC && data.length - 1 - i < 2) { 375 | this.debug('Waiting for more data.'); 376 | this._last = { 377 | data: data.slice(i), 378 | i: 0, 379 | l: 0 380 | }; 381 | if (i > l) { 382 | this.emit('data', data.slice(l, i)); 383 | } 384 | return; 385 | } 386 | if (needsPush || i === data.length - 1) { 387 | bufs.push(data.slice(l, i + 1)); 388 | needsPush = false; 389 | } 390 | } 391 | } 392 | 393 | if (bufs.length) { 394 | this.emit('data', Buffer.concat(bufs)); 395 | } 396 | }; 397 | 398 | Client.prototype.echo = function(cmd) { 399 | if (cmd.data.length < 3) return -1; 400 | cmd.data = cmd.data.slice(0, 3); 401 | this.emit('echo', cmd); 402 | return 3; 403 | }; 404 | 405 | Client.prototype.status = function(cmd) { 406 | if (cmd.data.length < 3) return -1; 407 | cmd.data = cmd.data.slice(0, 3); 408 | this.emit('status', cmd); 409 | return 3; 410 | }; 411 | 412 | Client.prototype.linemode = function(cmd) { 413 | if (cmd.data.length < 3) return -1; 414 | cmd.data = cmd.data.slice(0, 3); 415 | this.emit('linemode', cmd); 416 | return 3; 417 | }; 418 | 419 | Client.prototype.transmit_binary = function(cmd) { 420 | if (cmd.data.length < 3) return -1; 421 | cmd.data = cmd.data.slice(0, 3); 422 | this.emit('transmit binary', cmd); 423 | return 3; 424 | }; 425 | 426 | Client.prototype.authentication = function(cmd) { 427 | if (cmd.data.length < 3) return -1; 428 | cmd.data = cmd.data.slice(0, 3); 429 | this.emit('authentication', cmd); 430 | return 3; 431 | }; 432 | 433 | Client.prototype.terminal_speed = function(cmd) { 434 | if (cmd.data.length < 3) return -1; 435 | cmd.data = cmd.data.slice(0, 3); 436 | this.emit('terminal speed', cmd); 437 | return 3; 438 | }; 439 | 440 | Client.prototype.remote_flow_control = function(cmd) { 441 | if (cmd.data.length < 3) return -1; 442 | cmd.data = cmd.data.slice(0, 3); 443 | this.emit('remote flow control', cmd); 444 | return 3; 445 | }; 446 | 447 | Client.prototype.x_display_location = function(cmd) { 448 | if (cmd.data.length < 3) return -1; 449 | cmd.data = cmd.data.slice(0, 3); 450 | this.emit('x display location', cmd); 451 | return 3; 452 | }; 453 | 454 | Client.prototype.suppress_go_ahead = function(cmd) { 455 | if (cmd.data.length < 3) return -1; 456 | cmd.data = cmd.data.slice(0, 3); 457 | this.emit('suppress go ahead', cmd); 458 | return 3; 459 | }; 460 | 461 | Client.prototype.naws = function(cmd) { 462 | var data = cmd.data; 463 | var i = 0; 464 | 465 | if (cmd.commandCode !== COMMANDS.SB) { 466 | if (data.length < 3) return -1; 467 | cmd.data = cmd.data.slice(0, 3); 468 | this.emit('window size', cmd); // compat 469 | this.emit('naws', cmd); 470 | return 3; 471 | } 472 | 473 | if (data.length < 9) return -1; 474 | 475 | var iac1 = data.readUInt8(i); 476 | i += 1; 477 | var sb = data.readUInt8(i); 478 | i += 1; 479 | var naws = data.readUInt8(i); 480 | i += 1; 481 | var width = data.readUInt16BE(i); 482 | i += 2; 483 | var height = data.readUInt16BE(i); 484 | i += 2; 485 | var iac2 = data.readUInt8(i); 486 | i += 1; 487 | var se = data.readUInt8(i); 488 | i += 1; 489 | 490 | assert(iac1 === COMMANDS.IAC); 491 | assert(sb === COMMANDS.SB); 492 | assert(naws === OPTIONS.NAWS); 493 | assert(iac2 === COMMANDS.IAC); 494 | assert(se === COMMANDS.SE); 495 | 496 | cmd.cols = width; 497 | cmd.columns = width; 498 | cmd.width = width; 499 | cmd.rows = height; 500 | cmd.height = height; 501 | 502 | cmd.values = [cmd.width, cmd.height]; 503 | 504 | cmd.data = cmd.data.slice(0, i); 505 | 506 | if (this.options.tty) { 507 | this.columns = width; 508 | this.rows = height; 509 | this.emit('resize'); 510 | } 511 | 512 | this.emit('window size', cmd); // compat 513 | this.emit('naws', cmd); 514 | 515 | this.emit('size', width, height); 516 | 517 | return i; 518 | }; 519 | 520 | // compat 521 | Client.prototype.window_size = Client.prototype.naws; 522 | 523 | Client.prototype.new_environ = function(cmd) { 524 | var data = cmd.data; 525 | var i = 0; 526 | 527 | if (cmd.commandCode !== COMMANDS.SB) { 528 | if (data.length < 3) return -1; 529 | cmd.data = cmd.data.slice(0, 3); 530 | this.emit('environment variables', cmd); // compat 531 | this.emit('new environ', cmd); 532 | return 3; 533 | } 534 | 535 | if (data.length < 10) return -1; 536 | 537 | var iac1 = data.readUInt8(i); 538 | i += 1; 539 | var sb = data.readUInt8(i); 540 | i += 1; 541 | var newenv = data.readUInt8(i); 542 | i += 1; 543 | var info = data.readUInt8(i); 544 | i += 1; 545 | var variable = data.readUInt8(i); 546 | i += 1; 547 | 548 | var name; 549 | for (var s = i; i < data.length; i++) { 550 | if (data[i] === SUB.VALUE) { 551 | name = data.toString('ascii', s, i); 552 | i++; 553 | break; 554 | } 555 | } 556 | 557 | var value; 558 | for (var s = i; i < data.length; i++) { 559 | if (data[i] === COMMANDS.IAC) { 560 | value = data.toString('ascii', s, i); 561 | break; 562 | } 563 | } 564 | 565 | var iac2 = data.readUInt8(i); 566 | i += 1; 567 | var se = data.readUInt8(i); 568 | i += 1; 569 | 570 | assert(iac1 === COMMANDS.IAC); 571 | assert(sb === COMMANDS.SB); 572 | assert(newenv === OPTIONS.NEW_ENVIRON); 573 | assert(info === SUB.INFO); 574 | assert(variable === SUB.VARIABLE || variable === SUB.USER_VARIABLE); 575 | assert(name.length > 0); 576 | assert(value.length > 0); 577 | assert(iac2 === COMMANDS.IAC); 578 | assert(se === COMMANDS.SE); 579 | 580 | cmd.name = name; 581 | cmd.value = value; 582 | cmd.type = variable === SUB.VARIABLE 583 | ? 'system' 584 | : 'user'; 585 | 586 | // Always uppercase for some reason. 587 | if (cmd.name === 'TERM') { 588 | cmd.value = cmd.value.toLowerCase(); 589 | this.terminal = cmd.value; 590 | this.emit('term', cmd.value); 591 | } 592 | 593 | cmd.values = [cmd.name, cmd.value, cmd.type]; 594 | 595 | cmd.data = cmd.data.slice(0, i); 596 | 597 | this.env[cmd.name] = cmd.value; 598 | 599 | this.emit('environment variables', cmd); // compat 600 | this.emit('new environ', cmd); 601 | 602 | this.emit('env', cmd.name, cmd.value, cmd.type); 603 | 604 | return i; 605 | }; 606 | 607 | // compat 608 | Client.prototype.environment_variables = Client.prototype.new_environ; 609 | 610 | Client.prototype.terminal_type = function(cmd) { 611 | var data = cmd.data; 612 | var i = 0; 613 | 614 | if (cmd.commandCode !== COMMANDS.SB) { 615 | if (data.length < 3) return -1; 616 | cmd.data = cmd.data.slice(0, 3); 617 | this.emit('terminal type', cmd); 618 | if (cmd.commandCode === COMMANDS.WILL) { 619 | this.output.write(new Buffer([ 620 | COMMANDS.IAC, 621 | COMMANDS.SB, 622 | OPTIONS.TERMINAL_TYPE, 623 | SUB.SEND, 624 | COMMANDS.IAC, 625 | COMMANDS.SE 626 | ])); 627 | } 628 | return 3; 629 | } 630 | 631 | if (data.length < 7) return -1; 632 | 633 | var iac1 = data.readUInt8(i); 634 | i += 1; 635 | var sb = data.readUInt8(i); 636 | i += 1; 637 | var termtype = data.readUInt8(i); 638 | i += 1; 639 | var is = data.readUInt8(i); 640 | i += 1; 641 | 642 | var name; 643 | for (var s = i; i < data.length; i++) { 644 | if (data[i] === COMMANDS.IAC) { 645 | name = data.toString('ascii', s, i); 646 | break; 647 | } 648 | } 649 | 650 | var iac2 = data.readUInt8(i); 651 | i += 1; 652 | var se = data.readUInt8(i); 653 | i += 1; 654 | 655 | assert(iac1 === COMMANDS.IAC); 656 | assert(sb === COMMANDS.SB); 657 | assert(termtype === OPTIONS.TERMINAL_TYPE); 658 | assert(is === SUB.IS); 659 | assert(name.length > 0); 660 | assert(iac2 === COMMANDS.IAC); 661 | assert(se === COMMANDS.SE); 662 | 663 | // Always uppercase for some reason. 664 | cmd.name = name.toLowerCase(); 665 | 666 | cmd.values = [cmd.name]; 667 | 668 | cmd.data = cmd.data.slice(0, i); 669 | 670 | this.terminal = cmd.name; 671 | 672 | this.emit('terminal type', cmd); 673 | 674 | this.emit('term', cmd.name); 675 | 676 | return i; 677 | }; 678 | 679 | Client.prototype._setRawMode = function(mode) { 680 | this.isRaw = mode; 681 | if (!this.writable) return; 682 | if (mode) { 683 | this.debug('switching to raw:'); 684 | this.do.suppress_go_ahead(); 685 | this.will.suppress_go_ahead(); 686 | this.will.echo(); 687 | this.debug('switched to raw'); 688 | } else { 689 | this.debug('switching to cooked:'); 690 | this.dont.suppress_go_ahead(); 691 | this.wont.suppress_go_ahead(); 692 | this.wont.echo(); 693 | this.debug('switched to cooked'); 694 | } 695 | }; 696 | 697 | Client.prototype.__defineGetter__('readable', function() { 698 | return this.input.readable; 699 | }); 700 | 701 | Client.prototype.__defineGetter__('writable', function() { 702 | return this.output.writable; 703 | }); 704 | 705 | Client.prototype.__defineGetter__('destroyed', function() { 706 | return this.output.destroyed; 707 | }); 708 | 709 | Client.prototype.pause = function() { 710 | return this.input.pause.apply(this.output, arguments); 711 | } 712 | 713 | Client.prototype.resume = function() { 714 | return this.input.resume.apply(this.output, arguments); 715 | } 716 | 717 | Client.prototype.write = function(b) { 718 | if (this.options.convertLF) { 719 | arguments[0] = arguments[0].toString('utf8').replace(/\r?\n/g, '\r\n'); 720 | } 721 | return this.output.write.apply(this.output, arguments); 722 | }; 723 | 724 | Client.prototype.end = function() { 725 | return this.output.end.apply(this.output, arguments); 726 | }; 727 | 728 | Client.prototype.destroy = function() { 729 | return this.output.destroy.apply(this.output, arguments); 730 | }; 731 | 732 | Client.prototype.destroySoon = function() { 733 | return this.output.destroySoon.apply(this.output, arguments); 734 | }; 735 | 736 | /** 737 | * Server 738 | */ 739 | 740 | function Server(options, callback) { 741 | var self = this; 742 | 743 | if (!(this instanceof Server)) { 744 | return new Server(options, callback); 745 | } 746 | 747 | if (typeof options !== 'object') { 748 | callback = options; 749 | options = null; 750 | } 751 | 752 | options = options || {}; 753 | 754 | EventEmitter.call(this); 755 | 756 | this.server = net.createServer(function(socket) { 757 | var client = new Client(merge({}, options, { 758 | input: socket, 759 | output: socket, 760 | server: self 761 | })); 762 | self.emit('connection', client); 763 | self.emit('client', client); // compat 764 | if (callback) { 765 | callback(client); 766 | } 767 | }); 768 | 769 | ['error', 'listening', 'close'].forEach(function(name) { 770 | self.server.on(name, function() { 771 | var args = Array.prototype.slice.call(arguments); 772 | self.emit.apply(self, [name].concat(args)); 773 | }); 774 | }); 775 | 776 | return this; 777 | } 778 | 779 | Server.prototype.__proto__ = EventEmitter.prototype; 780 | 781 | Object.keys(net.Server.prototype).forEach(function(key) { 782 | var value = net.Server.prototype[key]; 783 | if (typeof value !== 'function') return; 784 | Server.prototype[key] = function() { 785 | return this.server[key].apply(this.server, arguments); 786 | }; 787 | }, this); 788 | 789 | /** 790 | * Telnet 791 | */ 792 | 793 | function Telnet(options) { 794 | if (options && (options.input || options.addListener)) { 795 | return new Client(arguments[0], arguments[1], arguments[2]); 796 | } 797 | return new Server(arguments[0], arguments[1]); 798 | } 799 | 800 | /** 801 | * Helpers 802 | */ 803 | 804 | function merge(target) { 805 | var objects = Array.prototype.slice.call(arguments, 1); 806 | objects.forEach(function(obj) { 807 | Object.keys(obj).forEach(function(key) { 808 | target[key] = obj[key]; 809 | }); 810 | }); 811 | return target; 812 | } 813 | 814 | /** 815 | * Expose 816 | */ 817 | 818 | exports = Telnet; 819 | exports.Client = Client; 820 | exports.Server = Server; 821 | exports.createClient = Client; 822 | exports.createServer = Server; 823 | 824 | exports.COMMANDS = COMMANDS; 825 | exports.COMMAND_NAMES = COMMAND_NAMES; 826 | exports.OPTIONS = OPTIONS; 827 | exports.OPTION_NAMES = OPTION_NAMES; 828 | exports.SUB = SUB; 829 | 830 | module.exports = exports; 831 | --------------------------------------------------------------------------------