| /* SSHOUT client-side API implementation for Node.js |
| * Copyright 2015-2018 Rivoreo |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU Lesser General Public License as published by |
| * the Free Software Foundation, either version 3 of the License, or (at your |
| * option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License |
| * for more details. |
| */ |
| |
| var events = require('events'); |
| var fs = require("fs"); |
| var util = require("util"); |
| var ssh = require("ssh2"); |
| var ALGORITHMS = require("ssh2-streams").constants.ALGORITHMS; |
| var os = require("os"); |
| |
| const SSHOUT_API_HELLO = 1; |
| const SSHOUT_API_GET_ONLINE_USER = 2; |
| const SSHOUT_API_SEND_MESSAGE = 3; |
| const SSHOUT_API_GET_MOTD = 4; |
| |
| const SSHOUT_API_PASS = 128; |
| const SSHOUT_API_ONLINE_USERS_INFO = 129; |
| const SSHOUT_API_RECEIVE_MESSAGE = 130; |
| const SSHOUT_API_USER_STATE_CHANGE = 131; |
| const SSHOUT_API_ERROR = 132; |
| const SSHOUT_API_MOTD = 133; |
| |
| const SSHOUT_API_MESSAGE_TYPE_PLAIN = 1; |
| const SSHOUT_API_MESSAGE_TYPE_RICH = 2; |
| const SSHOUT_API_MESSAGE_TYPE_IMAGE = 3; |
| |
| const SSHOUT_API_ERROR_SERVER_CLOSED = 1; |
| const SSHOUT_API_ERROR_LOCAL_PACKET_CORRUPT = 2; |
| const SSHOUT_API_ERROR_LOCAL_PACKET_TOO_LARGE = 3; |
| const SSHOUT_API_ERROR_OUT_OF_MEMORY = 4; |
| const SSHOUT_API_ERROR_INTERNAL_ERROR = 5; |
| const SSHOUT_API_ERROR_USER_NOT_FOUND = 6; |
| const SSHOUT_API_ERROR_MOTD_NOT_AVAILABLE = 7; |
| |
| const SSHOUT_API_PACKET_MAX_LENGTH = 1024 * 1024; |
| |
| const SSHOUT_MAX_API_VERSION = 1; |
| |
| var Client = function(ssh_options) { |
| if(ssh_options.host === undefined) throw new Error("host undefined"); |
| if(ssh_options.username === undefined) ssh_options.username = "sshout"; |
| this.ssh_options = ssh_options; |
| this.ssh_connection = new ssh.Client(); |
| this.text_encoding = "utf-8"; |
| }; |
| |
| util.inherits(Client, events.EventEmitter); |
| |
| Client.prototype.start = function() { |
| this.enabled = true; |
| var this_sshout = this; |
| this.ssh_connection.on("ready", function() { |
| this_sshout.api_version = SSHOUT_MAX_API_VERSION; |
| var exec_request_callback = function(e, stream) { |
| if(e) throw e; |
| this_sshout._stream = stream; |
| stream.on("close", function() { |
| this._read_buffer = null; |
| if(this_sshout.enabled) setTimeout(function() { |
| this.exec("api", null, exec_request_callback); |
| }); |
| }); |
| stream.on("data", function(chunk) { |
| process.stderr.write(util.format("chunk.length = %d\n", chunk.length)); |
| var buffer; |
| if(this._read_buffer) { |
| buffer = new Buffer(this._read_buffer.length + chunk.length); |
| this._read_buffer.copy(buffer, 0); |
| chunk.copy(buffer, this._read_buffer.length); |
| } else { |
| buffer = chunk; |
| } |
| if(buffer.length < 4) { |
| this._read_buffer = buffer; |
| return; |
| } |
| var length = chunk.readUInt32BE(0); |
| if(length > SSHOUT_API_PACKET_MAX_LENGTH) { |
| this_sshout.emit("error", new Error("packet too large")); |
| stream.end(); |
| this._read_buffer = null; |
| return; |
| } |
| var offset = 0; |
| while(chunk.length - offset >= 4 + length) { |
| //this_sshout.emit("packet", chunk.slice(offset, offset + 4 + length)); |
| this_sshout._do_packet(chunk.slice(offset + 4, offset + 4 + length)); |
| offset += 4 + length; |
| } |
| this._read_buffer = offset < chunk.length ? chunk.slice(offset) : null; |
| }); |
| this_sshout.send_hello(); |
| }; |
| var opts = {}; |
| this.exec("api", opts, exec_request_callback); |
| }); |
| this.ssh_connection.on("close", function() { |
| if(this_sshout.enabled) setTimeout(function() { this.connect(this_sshout.ssh_options); }); |
| }); |
| this.ssh_connection.connect(this.ssh_options); |
| }; |
| |
| Client.prototype.stop = function() { |
| this.enabled = false; |
| this.ssh_connection.end(); |
| }; |
| |
| Client.prototype.send_hello = function() { |
| var length = 1 + 6 + 2; |
| var packet = new Buffer(4 + length); |
| packet.writeUInt32BE(length, 0); |
| packet.writeUInt8(SSHOUT_API_HELLO, 4); |
| packet.write("SSHOUT", 5, 6); |
| packet.writeUInt16BE(SSHOUT_MAX_API_VERSION, 11); |
| this._stream.write(packet); |
| }; |
| |
| Client.prototype.send_message = function(to_user, type, body) { |
| if(typeof type === "string") switch(type) { |
| case "plain": |
| type = SSHOUT_API_MESSAGE_TYPE_PLAIN; |
| break; |
| case "rich": |
| case "html": |
| case "HTML": |
| type = SSHOUT_API_MESSAGE_TYPE_RICH; |
| break; |
| case "image": |
| case "jpeg": |
| case "JPEG": |
| type = SSHOUT_API_MESSAGE_TYPE_IMAGE; |
| break; |
| default: |
| throw new Error(util.format("Message type '%s' not recognized", type)); |
| } |
| if(typeof body === "string") { |
| if(type === SSHOUT_API_MESSAGE_TYPE_IMAGE) throw new Error("body cannot be string for image data"); |
| body = new Buffer(body, this.text_encoding); |
| } |
| /* |
| if(typeof to_user === "string") { |
| // Likely |
| to_user = new Buffer(to_user, this.text_encoding); |
| } |
| */ |
| if(to_user.length > 255) throw new Error("to_user too long"); |
| var length = 1 + 1 + to_user.length + 1 + 4 + body.length; |
| var packet = new Buffer(4 + length); |
| packet.writeUInt32BE(length, 0); |
| var i = 4; |
| packet.writeUInt8(SSHOUT_API_SEND_MESSAGE, i++); |
| packet.writeUInt8(to_user.length, i++); |
| switch(typeof to_user) { |
| case "string": |
| // Likely |
| packet.write(to_user, i, to_user.length, this.text_encoding); |
| break; |
| case "object": |
| // Buffer? |
| to_user.copy(packet, i); |
| break; |
| default: |
| throw new Error("Invalid type of to_user"); |
| } |
| i += to_user.length; |
| packet.writeUInt8(type, i++); |
| packet.writeUInt32BE(body.length, i); |
| i += 4; |
| body.copy(packet, i); |
| process.stderr.write("send_message: sending\n"); |
| this._stream.write(packet); |
| process.stderr.write("send_message: ok\n"); |
| }; |
| |
| // packet buffer doesn't include length field |
| Client.prototype._do_packet = function(packet) { |
| process.stderr.write(util.format("Client.prototype._do_packet: packet.length = %d\n", packet.length)); |
| var type = packet.readUInt8(0); |
| var i = 1; |
| switch(type) { |
| case SSHOUT_API_PASS: |
| if(1 + 6 + 1 > packet.length) { |
| this.emit("error", new Error("SSHOUT_API_PASS: packet too small")); |
| return; |
| } |
| if(packet.toString("ascii", i, i + 6) !== "SSHOUT") { |
| this.emit("error", new Error("handshake failed: magic mismatch")); |
| this.ssh_connection.close(); |
| return; |
| } |
| i += 6; |
| var version = packet.readUInt16BE(i); |
| i += 2; |
| if(version > SSHOUT_MAX_API_VERSION) { |
| this.emit("error", new Error(util.format("SSHOUT_API_PASS: invalid API version %d from server\n", version))); |
| this.ssh_connection.close(); |
| return; |
| } |
| this.api_version = version; |
| var user_name_len = packet.readUInt8(i++); |
| if(i + user_name_len > packet.length) { |
| this.emit("error", new Error("SSHOUT_API_PASS: field your_user_name located out of packet")); |
| //this._stream.end(); |
| this.ssh_connection.close(); |
| return; |
| } |
| var your_user_name = packet.toString(this.text_encoding, i, i + user_name_len); |
| this.emit("ready", your_user_name); |
| break; |
| case SSHOUT_API_ONLINE_USERS_INFO: |
| process.stderr.write("SSHOUT_API_ONLINE_USERS_INFO: not implemented\n"); |
| break; |
| case SSHOUT_API_RECEIVE_MESSAGE: |
| i += 8; // Skip the time for now |
| var msg_o = { time:null }; |
| var from_user_len = packet.readUInt8(i++); |
| msg_o.from_user = packet.toString(this.text_encoding, i, i + from_user_len); |
| i += from_user_len; |
| var to_user_len = packet.readUInt8(i++); |
| msg_o.to_user = packet.toString(this.text_encoding, i, i + to_user_len); |
| i += to_user_len; |
| var message_type = packet.readUInt8(i++); |
| switch(message_type) { |
| case SSHOUT_API_MESSAGE_TYPE_PLAIN: |
| msg_o.message_type = "plain"; |
| break; |
| case SSHOUT_API_MESSAGE_TYPE_RICH: |
| msg_o.message_type = "rich"; |
| break; |
| case SSHOUT_API_MESSAGE_TYPE_IMAGE: |
| msg_o.message_type = "image"; |
| break; |
| default: |
| msg_o.message_type = util.format("Unknown message type %d", message_type); |
| break; |
| } |
| var message_len = packet.readUInt32BE(i); |
| i += 4; |
| msg_o.message = packet.slice(i, i + message_len); |
| this.emit("message", msg_o); |
| break; |
| case SSHOUT_API_USER_STATE_CHANGE: |
| var state = packet.readUInt8(i++); |
| var user_name_len = packet.readUInt8(i++); |
| var user_name = packet.toString(this.text_encoding, i, i + user_name_len); |
| this.emit("user-state-changed", state, user_name); |
| this.emit(state ? "user-online" : "user-offline", user_name); |
| break; |
| case SSHOUT_API_ERROR: |
| var error_code = packet.readUInt32BE(i); |
| i += 4; |
| var error_msg_len = packet.readUInt32BE(i); |
| i += 4; |
| var error_msg = packet.toString(this.text_encoding, i, i + error_msg_len); |
| this.emit("error", error_code, error_msg); |
| break; |
| case SSHOUT_API_MOTD: |
| var len = packet.readUInt32BE(i); |
| i += 4; |
| this.emit("motd", packet.toString(this.text_encoding, i, i + len)); |
| break; |
| default: |
| process.stderr.write(util.format("Client.prototype._do_packet: packet has unknown type %d\n", type)); |
| this.emit("error", new Error(util.format("packet has unknown type %d\n", type))); |
| return; |
| } |
| }; |
| |
| module.exports.Client = Client; |