#!/usr/bin/env node /*! * ws: a node.js websocket client * Copyright(c) 2011 Einar Otto Stangvik * MIT Licensed */ const EventEmitter = require('events'); const fs = require('fs'); const program = require('commander'); const read = require('read'); const readline = require('readline'); const tty = require('tty'); const WebSocket = require('ws'); /** * InputReader - processes console input. * * @extends EventEmitter */ class Console extends EventEmitter { constructor() { super(); this.stdin = process.stdin; this.stdout = process.stdout; this.readlineInterface = readline.createInterface(this.stdin, this.stdout); this.readlineInterface .on('line', data => { this.emit('line', data); }) .on('close', () => { this.emit('close'); }); this._resetInput = () => { this.clear(); }; } static get Colors() { return { Red: '\u001b[31m', Green: '\u001b[32m', Yellow: '\u001b[33m', Blue: '\u001b[34m', Default: '\u001b[39m' }; } static get Types() { return { Incoming: '< ', Control: '', Error: 'error: ' }; } prompt() { this.readlineInterface.prompt(); } print(type, msg, color) { if (tty.isatty(1)) { this.clear(); if (program.execute) color = type = ''; else if (!program.color) color = ''; this.stdout.write(color + type + msg + Console.Colors.Default + '\n'); this.prompt(); } else if (type === Console.Types.Incoming) { this.stdout.write(msg + '\n'); } else { // is a control message and we're not in a tty... drop it. } } clear() { if (tty.isatty(1)) { this.stdout.write('\u001b[2K\u001b[3D'); } } pause() { this.stdin.on('keypress', this._resetInput); } resume() { this.stdin.removeListener('keypress', this._resetInput); } } function collect(val, memo) { memo.push(val); return memo; } function noop() {} /** * The actual application */ const version = require('../package.json').version; program .version(version) .usage('[options] (--listen | --connect )') .option('-l, --listen ', 'listen on port') .option('-c, --connect ', 'connect to a websocket server') .option('-p, --protocol ', 'optional protocol version') .option('-o, --origin ', 'optional origin') .option('-x, --execute ', 'execute command after connecting') .option('-w, --wait ', 'wait given seconds after executing command') .option('--host ', 'optional host') .option('-s, --subprotocol ', 'optional subprotocol', collect, []) .option('-n, --no-check', 'Do not check for unauthorized certificates') .option( '-H, --header ', 'Set an HTTP header. Repeat to set multiple. (--connect only)', collect, [] ) .option( '--auth ', 'Add basic HTTP authentication header. (--connect only)' ) .option('--ca ', 'Specify a Certificate Authority (--connect only)') .option('--cert ', 'Specify a Client SSL Certificate (--connect only)') .option( '--key ', "Specify a Client SSL Certificate's key (--connect only)" ) .option( '--passphrase [passphrase]', "Specify a Client SSL Certificate Key's passphrase (--connect only). " + "If you don't provide a value, it will be prompted for." ) .option('--no-color', 'Run without color') .option( '--slash', 'Enable slash commands for control frames ' + '(/ping, /pong, /close [code [, reason]])' ) .parse(process.argv); if (program.listen && program.connect) { console.error('\u001b[33merror: use either --listen or --connect\u001b[39m'); process.exit(-1); } if (program.listen) { const wsConsole = new Console(); wsConsole.pause(); let ws = null; const wss = new WebSocket.Server({ port: program.listen }, () => { wsConsole.print( Console.Types.Control, `listening on port ${program.listen} (press CTRL+C to quit)`, Console.Colors.Green ); wsConsole.clear(); }); wsConsole.on('close', () => { if (ws) ws.close(); process.exit(0); }); wsConsole.on('line', data => { if (ws) { ws.send(data); wsConsole.prompt(); } }); wss.on('connection', newClient => { if (ws) return newClient.terminate(); ws = newClient; wsConsole.resume(); wsConsole.prompt(); wsConsole.print( Console.Types.Control, 'client connected', Console.Colors.Green ); ws.on('close', code => { wsConsole.print( Console.Types.Control, `disconnected (code: ${code})`, Console.Colors.Green ); wsConsole.clear(); wsConsole.pause(); ws = null; }); ws.on('error', err => { wsConsole.print(Console.Types.Error, err.message, Console.Colors.Yellow); }); ws.on('message', data => { wsConsole.print(Console.Types.Incoming, data, Console.Colors.Blue); }); }); wss.on('error', err => { wsConsole.print(Console.Types.Error, err.message, Console.Colors.Yellow); process.exit(-1); }); } else if (program.connect) { const options = {}; const cont = () => { const wsConsole = new Console(); const headers = program.header.reduce((acc, cur) => { const i = cur.indexOf(':'); const key = cur.slice(0, i); const value = cur.slice(i + 1); acc[key] = value; return acc; }, {}); if (program.auth) { headers.Authorization = 'Basic ' + Buffer.from(program.auth).toString('base64'); } if (program.host) headers.Host = program.host; if (program.protocol) options.protocolVersion = +program.protocol; if (program.origin) options.origin = program.origin; if (!program.check) options.rejectUnauthorized = program.check; if (program.ca) options.ca = fs.readFileSync(program.ca); if (program.cert) options.cert = fs.readFileSync(program.cert); if (program.key) options.key = fs.readFileSync(program.key); let connectUrl = program.connect; if (!connectUrl.match(/\w+:\/\/.*$/i)) { connectUrl = `ws://${connectUrl}`; } options.headers = headers; const ws = new WebSocket(connectUrl, program.subprotocol, options); ws.on('open', () => { if (program.execute) { ws.send(program.execute); setTimeout(() => { ws.close(); }, program.wait ? program.wait * 1000 : 2000); } else { wsConsole.print( Console.Types.Control, 'connected (press CTRL+C to quit)', Console.Colors.Green ); wsConsole.on('line', data => { if (program.slash && data[0] === '/') { const toks = data.split(/\s+/); switch (toks[0].substr(1)) { case 'ping': ws.ping(noop); break; case 'pong': ws.pong(noop); break; case 'close': { let closeStatusCode = 1000; let closeReason = ''; if (toks.length >= 2) { closeStatusCode = parseInt(toks[1]); } if (toks.length >= 3) { closeReason = toks.slice(2).join(' '); } if (closeReason.length > 0) { ws.close(closeStatusCode, closeReason); } else { ws.close(closeStatusCode); } break; } default: wsConsole.print( Console.Types.Error, 'Unrecognized slash command.', Console.Colors.Yellow ); } } else { ws.send(data); } wsConsole.prompt(); }); } }); ws.on('close', code => { if (!program.execute) { wsConsole.print( Console.Types.Control, `disconnected (code: ${code})`, Console.Colors.Green ); } wsConsole.clear(); process.exit(); }); ws.on('error', err => { wsConsole.print(Console.Types.Error, err.message, Console.Colors.Yellow); process.exit(-1); }); ws.on('message', data => { wsConsole.print(Console.Types.Incoming, data, Console.Colors.Blue); }); ws.on('ping', () => { wsConsole.print( Console.Types.Incoming, 'Received ping', Console.Colors.Blue ); }); ws.on('pong', () => { wsConsole.print( Console.Types.Incoming, 'Received pong', Console.Colors.Blue ); }); wsConsole.on('close', () => { ws.close(); process.exit(); }); }; if (program.passphrase === true) { read( { prompt: 'Passphrase: ', silent: true, replace: '*' }, (err, passphrase) => { options.passphrase = passphrase; cont(); } ); } else if (typeof program.passphrase === 'string') { options.passphrase = program.passphrase; cont(); } else { cont(); } } else { program.help(); }