telnet-client
Version:
A simple node.js telnet client
506 lines • 21.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Telnet = void 0;
const events_1 = require("events");
const net_1 = require("net");
const utils_1 = require("./utils");
const string_decoder_1 = require("string_decoder");
const defaultOptions = {
debug: false,
echoLines: 1,
encoding: 'ascii',
execTimeout: 2000,
host: '127.0.0.1',
initialCtrlC: false,
initialLFCR: false,
irs: '\r\n',
localAddress: '',
loginPrompt: /login[: ]*$/i,
maxBufferLength: 1048576,
maxEndWait: 250,
negotiationMandatory: true,
ors: '\n',
pageSeparator: '---- More',
password: 'guest',
passwordPrompt: /password[: ]*$/i,
port: 23,
sendTimeout: 2000,
shellPrompt: /(?:\/ )?#\s/,
stripControls: false,
stripShellPrompt: true,
timeout: 2000,
username: 'root',
waitFor: false,
disableLogon: false
};
Object.freeze(defaultOptions);
// Convert various options which can be provided as strings into regexes.
function stringToRegex(opts) {
['failedLoginMatch', 'loginPrompt', 'passwordPrompt', 'shellPrompt', 'waitFor'].forEach(key => {
const value = opts[key];
opts[key] = typeof value === 'string' ? new RegExp(value) : value;
});
}
class Telnet extends events_1.EventEmitter {
constructor() {
super();
this.endEmitted = false;
this.inputBuffer = '';
this.loginPromptReceived = false;
this.opts = Object.assign({}, defaultOptions);
this.pendingData = [];
this.response = undefined;
this.socket = null;
this.state = null;
this.decoder = null;
this.on('data', data => this.pushNextData(data));
this.on('end', () => {
const remaining = this.decoder.end();
if (remaining)
this.pushNextData(remaining);
this.pushNextData(null);
this.state = 'end';
});
}
pushNextData(data) {
if (data instanceof Buffer)
data = this.decoder.write(data);
else if (data != null)
data = data.toString();
const chunks = data ? data.split(/(?<=\r\r\n|\r?\n)/) : [data];
if (this.dataResolver) {
this.dataResolver(chunks[0]);
this.dataResolver = undefined;
}
else
this.pendingData.push(chunks[0]);
if (chunks.length > 1)
this.pendingData.push(...chunks.slice(1));
}
nextData() {
return __awaiter(this, void 0, void 0, function* () {
if (this.pendingData.length > 0)
return this.pendingData.splice(0, 1)[0];
else if (this.state === 'end')
return null;
return new Promise(resolve => this.dataResolver = resolve);
});
}
connect(opts) {
return new Promise((resolve, reject) => {
var _a;
let connectionPending = true;
const rejectIt = (reason) => { connectionPending = false; reject(reason); };
const resolveIt = () => { connectionPending = false; resolve(); };
Object.assign(this.opts, opts !== null && opts !== void 0 ? opts : {});
this.opts.initialCtrlC = opts.initialCtrlC && this.opts.initialCTRLC;
this.opts.extSock = (_a = opts === null || opts === void 0 ? void 0 : opts.sock) !== null && _a !== void 0 ? _a : this.opts.extSock;
stringToRegex(this.opts);
this.decoder = new string_decoder_1.StringDecoder(this.opts.encoding);
// If socket is provided and in good state, just reuse it.
if (this.opts.extSock) {
if (!Telnet.checkSocket(this.opts.extSock))
return rejectIt(new Error('socket invalid'));
this.socket = this.opts.extSock;
this.state = 'ready';
this.emit('ready');
resolveIt();
}
else {
this.socket = (0, net_1.createConnection)(Object.assign({ port: this.opts.port, host: this.opts.host, localAddress: this.opts.localAddress }, this.opts.socketConnectOptions), () => {
this.state = 'start';
this.emit('connect');
if (this.opts.initialCtrlC === true)
this.socket.write('\x03');
if (this.opts.initialLFCR === true)
this.socket.write('\r\n');
if (!this.opts.negotiationMandatory)
resolveIt();
});
}
this.socket.setMaxListeners(Math.max(15, this.socket.getMaxListeners()));
this.socket.setTimeout(this.opts.timeout, () => {
if (connectionPending) {
// If cannot connect, emit error and destroy.
if (this.listeners('error').length > 0)
this.emit('error', 'Cannot connect');
this.socket.destroy();
return reject(new Error('Cannot connect'));
}
this.emit('timeout');
return reject(new Error('timeout'));
});
this.socket.on('connect', () => {
if (!this.opts.shellPrompt) {
this.state = 'standby';
resolveIt();
}
});
this.socket.on('data', data => {
let emitted = false;
if (this.state === 'standby' || !this.opts.negotiationMandatory) {
this.emit('data', this.opts.newlineReplace ? Buffer.from(this.decode(data), this.opts.encoding) : data);
emitted = true;
}
const isReady = [];
const parsedData = this.parseData(data, isReady);
if (parsedData && connectionPending && (isReady[0] || !this.opts.shellPrompt)) {
resolveIt();
if (!this.opts.shellPrompt && !emitted)
this.emit('data', parsedData);
}
});
this.socket.on('error', error => {
if (this.listeners('error').length > 0)
this.emit('error', error);
if (connectionPending)
rejectIt(error);
});
this.socket.on('end', () => {
if (!this.endEmitted) {
this.endEmitted = true;
this.emit('end');
}
if (connectionPending) {
if (this.state === 'start')
resolveIt();
else
rejectIt(new Error('Socket ends'));
}
});
this.socket.on('close', () => {
this.emit('close');
if (connectionPending) {
if (this.state === 'start')
resolveIt();
else
rejectIt(new Error('Socket closes'));
}
});
this.once('failedlogin', () => {
if (connectionPending)
rejectIt(new Error('Failed login'));
});
});
}
shell(callback) {
return __awaiter(this, void 0, void 0, function* () {
return (0, utils_1.asCallback)(new Promise(resolve => {
resolve(new utils_1.Stream(this.socket));
}), callback);
});
}
exec(cmd, opts, callback) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof opts === 'function') {
callback = opts;
opts = undefined;
}
return (0, utils_1.asCallback)(new Promise((resolve, reject) => {
Object.assign(this.opts, opts || {});
cmd += this.opts.ors;
if (!this.socket.writable)
return reject(new Error('socket not writable'));
this.socket.write(cmd, () => {
let execTimeout;
this.state = 'response';
this.emit('writedone');
const buffExecHandler = () => {
if (execTimeout)
clearTimeout(execTimeout);
if (!this.inputBuffer)
return reject(new Error('response not received'));
resolve(this.inputBuffer);
// Reset stored response.
this.inputBuffer = '';
// Set state back to 'standby' for possible telnet server push data.
this.state = 'standby';
};
const responseHandler = () => {
if (execTimeout)
clearTimeout(execTimeout);
if (this.response)
resolve(this.response.join(this.opts.newlineReplace || '\n'));
else
reject(new Error('invalid response'));
// Reset stored response.
this.inputBuffer = '';
// Set state back to 'standby' for possible telnet server push data.
this.state = 'standby';
this.removeListener('bufferexceeded', buffExecHandler);
};
this.once('responseready', responseHandler);
this.once('bufferexceeded', buffExecHandler);
if (this.opts.execTimeout) {
execTimeout = setTimeout(() => {
execTimeout = undefined;
this.removeListener('responseready', responseHandler);
this.removeListener('bufferexceeded', buffExecHandler);
reject(new Error('response not received'));
}, this.opts.execTimeout);
}
});
}), callback);
});
}
send(data, opts, callback) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof opts === 'function') {
callback = opts;
opts = undefined;
}
this.opts.ors = (opts === null || opts === void 0 ? void 0 : opts.ors) || this.opts.ors;
data += this.opts.ors;
return this.write(data, opts, callback);
});
}
write(data, opts, callback) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof opts === 'function') {
callback = opts;
opts = undefined;
}
return (0, utils_1.asCallback)(new Promise((resolve, reject) => {
var _a, _b;
Object.assign(this.opts, opts || {});
this.opts.waitFor = (_b = (_a = opts === null || opts === void 0 ? void 0 : opts.waitFor) !== null && _a !== void 0 ? _a : opts === null || opts === void 0 ? void 0 : opts.waitfor) !== null && _b !== void 0 ? _b : false;
stringToRegex(this.opts);
if (this.socket.writable) {
let response = '';
let sendTimer;
const sendHandler = (data) => {
response += this.decode(data);
if (this.opts.waitFor instanceof RegExp) {
if (this.opts.waitFor.test(response)) {
if (sendTimer)
clearTimeout(sendTimer);
this.socket.removeListener('data', sendHandler);
resolve(response);
}
}
else if (!sendTimer) {
this.socket.removeListener('data', sendHandler);
resolve(response);
}
};
this.socket.on('data', sendHandler);
try {
this.socket.write(data, () => {
if (this.opts.sendTimeout) {
sendTimer = setTimeout(() => {
sendTimer = undefined;
if (response === '') {
this.socket.removeListener('data', sendHandler);
reject(new Error('response not received'));
return;
}
this.socket.removeListener('data', sendHandler);
resolve(response);
}, this.opts.sendTimeout);
}
});
}
catch (e) {
this.socket.removeListener('data', sendHandler);
reject(new Error('send data failed'));
}
}
else {
reject(new Error('socket not writable'));
}
}), callback);
});
}
getSocket() {
return this.socket;
}
end() {
return new Promise(resolve => {
let timer = setTimeout(() => {
timer = undefined;
if (!this.endEmitted) {
this.endEmitted = true;
this.emit('end');
}
resolve();
}, this.opts.maxEndWait);
this.socket.end(() => {
if (timer) {
clearTimeout(timer);
timer = undefined;
resolve();
}
});
});
}
destroy() {
return new Promise(resolve => {
this.socket.destroy();
resolve();
});
}
parseData(chunk, isReady) {
if (chunk[0] === 255 && chunk[1] !== 255)
chunk = this.negotiate(chunk);
if (this.state === 'start')
this.state = 'getprompt';
if (this.state === 'getprompt') {
const stringData = chunk ? this.decoder.write(chunk) : '';
const decodedData = this.decode(stringData);
const promptIndex = (0, utils_1.search)(decodedData, this.opts.shellPrompt);
if ((0, utils_1.search)(decodedData, this.opts.loginPrompt) >= 0) {
// Make sure we don't end up in an infinite loop.
if (!this.loginPromptReceived) {
this.state = 'login';
this.login('username');
this.loginPromptReceived = true;
}
}
else if ((0, utils_1.search)(decodedData, this.opts.passwordPrompt) >= 0) {
this.state = 'login';
this.login('password');
}
else if ((0, utils_1.search)(decodedData, this.opts.failedLoginMatch) >= 0) {
this.state = 'failedlogin';
this.emit('failedlogin', decodedData);
this.destroy().finally();
}
else if (promptIndex >= 0) {
const shellPrompt = this.opts.shellPrompt instanceof RegExp ?
decodedData.substring(promptIndex) : this.opts.shellPrompt;
this.state = 'standby';
this.inputBuffer = '';
this.loginPromptReceived = false;
this.emit('ready', shellPrompt);
isReady === null || isReady === void 0 ? void 0 : isReady.push(true);
}
}
else if (this.state === 'response') {
if (this.inputBuffer.length >= this.opts.maxBufferLength) {
this.emit('bufferexceeded');
return Buffer.from(this.inputBuffer, this.opts.encoding);
}
const stringData = this.decode(chunk);
this.inputBuffer += stringData;
const promptIndex = (0, utils_1.search)(this.inputBuffer, this.opts.shellPrompt);
if (promptIndex < 0 && (stringData === null || stringData === void 0 ? void 0 : stringData.length) > 0) {
if ((0, utils_1.search)(stringData, this.opts.pageSeparator) >= 0)
this.socket.write(Buffer.from('20', 'hex'));
return null;
}
const response = this.inputBuffer.split(this.opts.irs);
for (let i = response.length - 1; i >= 0; --i) {
if ((0, utils_1.search)(response[i], this.opts.pageSeparator) >= 0) {
response[i] = response[i].replace(this.opts.pageSeparator, '');
if (response[i].length === 0)
response.splice(i, 1);
}
}
if (this.opts.echoLines === 1)
response.shift();
else if (this.opts.echoLines > 1)
response.splice(0, this.opts.echoLines);
else if (this.opts.echoLines < 0)
response.splice(0, response.length - 2);
// Remove prompt.
if (this.opts.stripShellPrompt && response.length > 0) {
const idx = response.length - 1;
response[idx] = (0, utils_1.search)(response[idx], this.opts.shellPrompt) >= 0
? response[idx].replace(this.opts.shellPrompt, '')
: '';
}
this.response = response;
chunk = null;
this.emit('responseready');
}
return chunk;
}
login(handle) {
if ((handle === 'username' || handle === 'password') && this.socket.writable && !this.opts.disableLogon) {
this.socket.write(this.opts[handle] + this.opts.ors, () => {
this.state = 'getprompt';
});
}
}
negotiate(chunk) {
/* info: http://tools.ietf.org/html/rfc1143#section-7
* Refuse to start performing and ack the start of performance
* DO -> WONT WILL -> DO */
const packetLength = chunk.length;
let negData = chunk;
let cmdData = null;
for (let i = 0; i < packetLength; i += 3) {
if (chunk[i] !== 255) {
negData = chunk.slice(0, i);
cmdData = chunk.slice(i);
break;
}
}
const chunkHex = chunk.toString('hex');
const defaultResponse = negData.toString('hex').replace(/fd/g, 'fc').replace(/fb/g, 'fd');
let negResp = '';
if (this.opts.terminalHeight && this.opts.terminalWidth) {
for (let i = 0; i < chunkHex.length; i += 6) {
let w, h;
switch (chunkHex.substr(i + 2, 4)) {
case 'fd18':
negResp += 'fffb18';
break;
case 'fd1f':
w = this.opts.terminalWidth.toString(16).padStart(4, '0');
h = this.opts.terminalHeight.toString(16).padStart(4, '0');
negResp += `fffb1ffffa1f${w}${h}fff0`;
break;
default:
negResp += defaultResponse.substr(i, 6);
}
}
}
else
negResp = defaultResponse;
if (this.socket.writable)
this.socket.write(Buffer.from(negResp, 'hex'));
return cmdData;
}
static checkSocket(sock) {
return sock !== null &&
typeof sock === 'object' &&
typeof sock.pipe === 'function' &&
sock.writable !== false &&
typeof sock._write === 'function' &&
typeof sock._writableState === 'object' &&
sock.readable !== false &&
typeof sock._read === 'function' &&
typeof sock._readableState === 'object';
}
decode(chunk) {
if (chunk instanceof Buffer)
chunk = chunk.toString(this.opts.encoding);
if (this.opts.escapeHandler) {
chunk === null || chunk === void 0 ? void 0 : chunk.replace(/\x1B((\[.*?[a-z])|.)/i, seq => {
const response = this.opts.escapeHandler(seq);
if (response)
this.socket.write(response);
return seq;
});
}
if (this.opts.stripControls) {
chunk = chunk === null || chunk === void 0 ? void 0 : chunk.replace(/\x1B((\[.*?[a-z])|.)/i, ''); // Escape sequences
chunk = chunk === null || chunk === void 0 ? void 0 : chunk.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); // All controls except tab, lf, and cr.
}
if (this.opts.newlineReplace)
chunk = chunk === null || chunk === void 0 ? void 0 : chunk.replace(/\r\r\n|\r\n?/g, this.opts.newlineReplace);
return chunk;
}
}
exports.Telnet = Telnet;
//# sourceMappingURL=index.js.map