WebInterface reference implementation

This commit is contained in:
KimLS
2017-01-11 23:19:00 -08:00
parent 124728e0c7
commit f24770489e
15 changed files with 449 additions and 32 deletions
+90
View File
@@ -0,0 +1,90 @@
const servertalk = require('./servertalk_client.js');
const uuid = require('node-uuid');
class ServertalkAPI
{
Init(addr, port, ipv6, credentials) {
this.client = new servertalk.client();
this.client.Init(addr, port, ipv6, 'WebInterface', credentials);
this.pending_calls = {};
var self = this;
this.client.on('connecting', function() {
console.log('Connecting...');
});
this.client.on('connect', function(){
console.log('Connected');
});
this.client.on('close', function(){
console.log('Closed');
});
this.client.on('error', function(err){
});
this.client.on('message', function(opcode, packet) {
var response = Buffer.from(packet).toString('utf8');
try {
var res = JSON.parse(response);
if(res.id) {
if(self.pending_calls.hasOwnProperty(res.id)) {
var entry = self.pending_calls[res.id];
if(res.error) {
var reject = entry[1];
reject(res.error);
} else {
var resolve = entry[0];
resolve(res.response);
}
delete self.pending_calls[res.id];
}
}
} catch(ex) {
console.log('Error processing response from server:\n', ex);
}
});
}
Call(method, args, timeout) {
if(!timeout) {
timeout = 15000
}
var self = this;
return new Promise(
function(resolve, reject) {
if(!self.client.Connected()) {
reject('Not connected to world server.');
return;
}
var id = uuid.v4();
self.pending_calls[id] = [resolve, reject];
var c = { id: id, method: method, params: args };
self.client.Send(47, Buffer.from(JSON.stringify(c)));
setTimeout(function() {
delete self.pending_calls[id];
reject('Request timed out after ' + timeout + 'ms');
}, timeout);
}
);
}
Notify(method, args) {
var c = { method: method, params: args };
client.Send(47, Buffer.from(JSON.stringify(c)));
}
}
module.exports = {
'api': ServertalkAPI
}
+299
View File
@@ -0,0 +1,299 @@
var net = require('net');
var sodium = require('libsodium-wrappers');
const EventEmitter = require('events');
var ServertalkPacketType =
{
ServertalkClientHello: 1,
ServertalkServerHello: 2,
ServertalkClientHandshake: 3,
ServertalkClientDowngradeSecurityHandshake: 4,
ServertalkMessage: 5,
};
class ServertalkClient extends EventEmitter
{
Init(addr, port, ipv6, identifier, credentials) {
this.m_addr = addr;
this.m_identifier = identifier;
this.m_credentials = credentials;
this.m_connecting = false;
this.m_port = port;
this.m_ipv6 = ipv6;
this.m_encrypted = false;
this.m_connection = null;
this.m_buffer = Buffer.alloc(0);
this.m_public_key_ours = null;
this.m_private_key_ours = null;
this.m_nonce_ours = null;
this.m_public_key_theirs = null;
this.m_nonce_theirs = null;
this.m_shared_key = null;
var self = this;
setInterval(function() { self.Connect(); }, 100);
}
Send(opcode, p) {
try {
var out;
if(this.m_encrypted) {
if(p.length == 0) {
p = Buffer.alloc(1);
}
out = Buffer.alloc(6);
out.writeUInt32LE(p.length + sodium.crypto_secretbox_MACBYTES, 0);
out.writeUInt16LE(opcode, 4);
var cipher = sodium.crypto_box_easy_afternm(p, this.m_nonce_ours, this.m_shared_key);
this.IncrementUint64(this.m_nonce_ours);
out = Buffer.concat([out, Buffer.from(cipher)], out.length + cipher.length);
} else {
out = Buffer.alloc(6);
out.writeUInt32LE(p.length, 0);
out.writeUInt16LE(opcode, 4);
out = Buffer.concat([out, p], out.length + p.length);
}
this.InternalSend(ServertalkPacketType.ServertalkMessage, out);
} catch(ex) {
this.emit('error', new Error(ex));
}
}
Connected() {
return this.m_connection && !this.m_connecting;
}
Connect() {
if (this.m_port == 0 || this.m_connection || this.m_connecting) {
return;
}
this.m_connecting = true;
this.emit('connecting');
var self = this;
this.m_connection = net.connect({port: this.m_port, host: this.m_addr}, function() {
self.m_connection.on('close', function(had_error) {
self.emit('close');
self.m_connection = null;
self.m_encrypted = false;
});
self.m_connection.on('data', function(buffer) {
self.ProcessData(buffer);
});
self.SendHello();
self.m_connecting = false;
});
this.m_connection.on('error', function() {
self.emit('close');
self.m_connection = null;
self.m_connecting = false;
});
}
ProcessData(buffer) {
this.m_buffer = Buffer.concat([this.m_buffer, buffer], this.m_buffer.length + buffer.length);
this.ProcessReadBuffer();
}
SendHello() {
var p = Buffer.alloc(0);
this.InternalSend(ServertalkPacketType.ServertalkClientHello, p);
}
InternalSend(type, p) {
if(!this.m_connection) {
return;
}
var out = Buffer.alloc(5);
out.writeUInt32LE(p.length, 0);
out.writeUInt8(type, 4);
if (p.length > 0) {
out = Buffer.concat([out, p], out.length + p.length);
}
this.m_connection.write(out);
}
ProcessReadBuffer() {
var current = 0;
var total = this.m_buffer.length;
while (current < total) {
var left = total - current;
var length = 0;
var type = 0;
if (left < 5) {
break;
}
length = this.m_buffer.readUInt32LE(current);
type = this.m_buffer.readUInt8(current + 4);
if (current + 5 + length > total) {
break;
}
if (length == 0) {
var p = Buffer.alloc(0);
switch (type) {
case ServertalkPacketType.ServertalkServerHello:
this.ProcessHello(p);
break;
case ServertalkPacketType.ServertalkMessage:
this.ProcessMessage(p);
break;
}
}
else {
var p = this.m_buffer.slice(current + 5, current + 5 + length);
switch (type) {
case ServertalkPacketType.ServertalkServerHello:
this.ProcessHello(p);
break;
case ServertalkPacketType.ServertalkMessage:
this.ProcessMessage(p);
break;
}
}
current += length + 5;
}
if (current == total) {
this.m_buffer = Buffer.alloc(0);
}
else {
this.m_buffer = this.m_buffer.slice(current);
}
}
ProcessHello(p) {
this.m_encrypted = false;
this.m_public_key_ours = null;
this.m_public_key_theirs = null;
this.m_private_key_ours = null;
this.m_nonce_ours = null;
this.m_nonce_theirs = null;
this.m_shared_key = null;
try {
var enc = p.readUInt8(0) == 1 ? true : false;
if (enc) {
if (p.length == (1 + sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES)) {
this.m_public_key_theirs = p.slice(1, 1 + sodium.crypto_box_PUBLICKEYBYTES);
this.m_nonce_theirs = p.slice(1 + sodium.crypto_box_PUBLICKEYBYTES, 1 + sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES);
this.m_encrypted = true;
this.SendHandshake(false);
this.emit('connect');
}
else {
this.emit('error', new Error('Could not process hello, size !=', 1 + sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES));
}
} else {
this.SendHandshake(false);
this.emit('connect');
}
} catch(ex) {
this.emit('error', new Error(ex));
}
}
ProcessMessage(p) {
try {
var length = this.m_buffer.readUInt32LE(0);
var opcode = this.m_buffer.readUInt16LE(4);
if(length > 0) {
var data = p.slice(6);
if(this.m_encrypted) {
var message_len = length - sodium.crypto_secretbox_MACBYTES;
var decrypted = sodium.crypto_box_open_easy_afternm(data, this.m_nonce_theirs, this.m_shared_key);
this.IncrementUint64(this.m_nonce_theirs);
this.emit('message', opcode, decrypted);
} else {
this.emit('message', opcode, data);
}
} else {
this.emit('message', opcode, Buffer.alloc(0));
}
} catch(ex) {
this.emit('error', new Error(ex));
}
}
SendHandshake() {
var handshake;
if(this.m_encrypted) {
var keypair = sodium.crypto_box_keypair();
this.m_public_key_ours = keypair.publicKey;
this.m_private_key_ours = keypair.privateKey;
this.m_nonce_ours = Buffer.from(sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES));
this.m_shared_key = sodium.crypto_box_beforenm(this.m_public_key_theirs, this.m_private_key_ours);
this.m_public_key_theirs = null;
this.m_private_key_ours = null;
var message = Buffer.alloc(this.m_identifier.length + this.m_credentials.length + 2);
message.write(this.m_identifier, 0);
message.write(this.m_credentials, this.m_identifier.length + 1);
var ciphertext = sodium.crypto_box_easy_afternm(message, this.m_nonce_ours, this.m_shared_key);
handshake = Buffer.concat([Buffer.from(this.m_public_key_ours), Buffer.from(this.m_nonce_ours), Buffer.from(ciphertext)], sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES + ciphertext.length);
this.IncrementUint64(this.m_nonce_ours);
this.m_public_key_ours = null;
} else {
handshake = Buffer.alloc(this.m_identifier.length + this.m_credentials.length + 2);
handshake.write(this.m_identifier, 0);
handshake.write(this.m_credentials, this.m_identifier.length() + 1);
}
this.InternalSend(ServertalkPacketType.ServertalkClientHandshake, handshake);
}
IncrementUint64(value) {
var bytes = [];
for(var i = 0; i < 8; ++i) {
bytes[i] = value[i];
}
bytes[0] += 1;
for(i = 0; i < 7; ++i) {
if(bytes[i] >= 0x100) {
bytes[0] = 0;
bytes[i + 1] += 1;
}
}
if(bytes[7] >= 0x100) {
bytes[7] = 0;
}
for(var i = 0; i < 8; ++i) {
value[i] = bytes[i];
}
}
}
module.exports = {
'client': ServertalkClient
}