blob: 83b12ea6f374f89e0857da9ee3cfb67a6bf33c0d [file] [log] [blame] [raw]
/* 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 === null) 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 session_options = {};
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_sshout.ssh_connection.exec("api", session_options, exec_request_callback);
}, 2000);
});
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;
}
var offset = 0;
while(buffer.length - offset >= 4) {
var length = buffer.readUInt32BE(offset);
if(length > SSHOUT_API_PACKET_MAX_LENGTH) {
process.stderr.write(util.format("length = %d\n", length));
this_sshout.emit("error", new Error("packet too large"));
stream.end();
this._read_buffer = null;
return;
}
if(buffer.length < 4 + length) break;
//this_sshout.emit("packet", buffer.slice(offset, offset + 4 + length));
this_sshout._do_packet(buffer.slice(offset + 4, offset + 4 + length));
offset += 4 + length;
}
this._read_buffer = offset < buffer.length ? buffer.slice(offset) : null;
});
this_sshout.send_hello();
};
this.exec("api", session_options, exec_request_callback);
});
this.ssh_connection.on("close", function() {
if(this_sshout.enabled) setTimeout(function() { this.connect(this_sshout.ssh_options); }, 10000);
});
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.request_user_info = function() {
var packet = new Buffer(4 + 1);
packet.writeUInt32BE(1, 0);
packet.writeUInt8(SSHOUT_API_GET_ONLINE_USER, 4);
this._stream.write(packet);
};
Client.prototype.request_motd = function() {
var packet = new Buffer(4 + 1);
packet.writeUInt32BE(1, 0);
packet.writeUInt8(SSHOUT_API_GET_MOTD, 4);
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);
this._stream.write(packet);
};
// 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.end();
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.end();
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.end();
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:
var my_id = packet.readUInt16BE(i);
i += 2;
var count = packet.readUInt16BE(i);
i += 2;
var user_info = [];
while(count-- > 0) {
var id = packet.readUInt16BE(i);
i += 2;
var user_name_len = packet.readUInt8(i++);
var user_name = packet.toString(this.text_encoding, i, i + user_name_len);
i += user_name_len;
var host_name_len = packet.readUInt8(i++);
var host_name = packet.toString(this.text_encoding, i, i + host_name_len);
i += host_name_len;
user_info.push({
id:id,
user_name:user_name,
host_name:host_name
});
}
this.emit("user-info", my_id, user_info);
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;