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

17
wi/BUILD.md Normal file
View File

@ -0,0 +1,17 @@
# Building EQEmu Web Interface Reference Implementation
## Required Software
- [NodeJS](https://nodejs.org)
## Install
First: Make sure you have required software installed.
Install 3rd Party Libraries first with the following command:
npm install
## Run
Run with either your favorite NodeJS process manager or with the following command:
node .

27
wi/core/jwt_auth.js Normal file
View File

@ -0,0 +1,27 @@
const jwt = require('jsonwebtoken');
var Auth = function (req, res, next) {
var token = '';
try {
token = req.headers.authorization.substring(7);
} catch(ex) {
console.log(ex);
res.sendStatus(401);
return;
}
jwt.verify(token, req.key, function(err, decoded) {
if(err) {
console.log(err);
res.sendStatus(401);
return;
}
req.token = decoded;
next();
});
};
module.exports = {
'auth': Auth
}

4
wi/generate_pw_hash.js Normal file
View File

@ -0,0 +1,4 @@
var sodium = require('libsodium-wrappers-sumo');
var hash = sodium.crypto_pwhash_str('password', 3, 32768);
console.log(hash);

37
wi/http/eqw.js Normal file
View File

@ -0,0 +1,37 @@
var auth = require('../core/jwt_auth.js').auth;
var RegisterEQW = function(app, api) {
app.post('/api/eqw/islocked', auth, function (req, res) {
api.Call('EQW::IsLocked', [])
.then(function(value) {
res.send({ response: value });
})
.catch(function(reason) {
res.sendStatus(500);
});
});
app.post('/api/eqw/lock', auth, function (req, res) {
api.Call('EQW::Lock', [])
.then(function(value) {
res.send({ response: value });
})
.catch(function(reason) {
res.sendStatus(500);
});
});
app.post('/api/eqw/unlock', auth, function (req, res) {
api.Call('EQW::Unlock', [])
.then(function(value) {
res.send({ response: value });
})
.catch(function(reason) {
res.sendStatus(500);
});
});
};
module.exports = {
'Register': RegisterEQW
}

43
wi/http/token.js Normal file
View File

@ -0,0 +1,43 @@
const sodium = require('libsodium-wrappers-sumo');
const jwt = require('jsonwebtoken');
var RegisterToken = function(app) {
app.post('/api/token', function (req, res) {
try {
req.mysql.getConnection(function(err, connection) {
if(err) {
console.log(err);
res.sendStatus(500);
connection.release();
return;
}
connection.query('SELECT password FROM account WHERE name = ? LIMIT 1', [req.body.username], function (error, results, fields) {
if(results.length == 0) {
res.sendStatus(401);
connection.release();
return;
}
if(sodium.crypto_pwhash_str_verify(results[0].password, req.body.password)) {
var expires = Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 7);
var token = jwt.sign({ username: req.body.username, exp: expires }, req.key);
res.send({token: token, expires: expires});
connection.release();
} else {
res.sendStatus(401);
connection.release();
}
});
});
} catch(ex) {
res.sendStatus(500);
console.log(ex);
}
});
};
module.exports = {
'Register': RegisterToken
}

View File

@ -1,33 +1,51 @@
var servertalk = require('./servertalk_client.js');
var fs = require('fs');
var settings = JSON.parse(fs.readFileSync('settings.json', 'utf8'));
const fs = require('fs');
const settings = JSON.parse(fs.readFileSync('settings.json', 'utf8'));
const key = fs.readFileSync(settings.key, 'utf8');
var client = new servertalk.client();
var server;
if(settings.https.enabled) {
const options = {
key: fs.readFileSync(settings.https.key),
cert: fs.readFileSync(settings.https.cert)
};
client.Init(settings.addr, settings.port, false, 'WebInterface', settings.key);
server = require('https').createServer();
} else {
server = require('http').createServer();
}
client.on('connecting', function(){
console.log('Connecting...');
const servertalk = require('./network/servertalk_api.js');
const websocket_iterface = require('./ws/ws_interface.js');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const uuid = require('node-uuid');
const jwt = require('jsonwebtoken');
var mysql = require('mysql').createPool(settings.db);
var wsi = new websocket_iterface.wsi(server, key);
var api = new servertalk.api();
api.Init(settings.servertalk.addr, settings.servertalk.port, false, settings.servertalk.key);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
//make sure all routes can see our injected dependencies
app.use(function (req, res, next) {
req.servertalk = api;
req.mysql = mysql;
req.key = key;
next();
});
client.on('connect', function(){
console.log('Connected...');
this.Send(47, Buffer.from(JSON.stringify({ method: 'IsLocked', params: [], id: '12345' })));
this.Send(47, Buffer.from(JSON.stringify({ method: 'Lock', params: [] })));
this.Send(47, Buffer.from(JSON.stringify({ method: 'IsLocked', params: [], id: '12346' })));
this.Send(47, Buffer.from(JSON.stringify({ method: 'Unlock', params: [] })));
this.Send(47, Buffer.from(JSON.stringify({ method: 'IsLocked', params: [], id: '12347' })));
app.get('/', function (req, res) {
res.send({ status: "online" });
});
client.on('close', function(){
console.log('Closed');
});
require('./http/token.js').Register(app);
require('./http/eqw.js').Register(app, api);
//require('./ws/token.js').Register(app);
require('./ws/eqw.js').Register(wsi, api);
client.on('error', function(err){
console.log(err);
});
client.on('message', function(opcode, packet) {
console.log(Buffer.from(packet).toString('utf8'));
});
server.on('request', app);
server.listen(settings.port, function () { console.log('Listening on ' + server.address().port) });

27
wi/key.pem Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAoijNhaW4sH2yLEQOUCNLSU0qIGnr9mxewEPXSNURKFExC1WE
ah983xy+WTbKjakH6Rp2OwCvLxNIu6QBKRgcJ963ICWY7ysn4bU2Q2KoSJgAEel8
UMDHWYfyyAPdr4DUwUw7YMf4LBThCGBC5DTPilZiVqQNyOf8KL5w/oKcavVMddod
eBNE1ewoxVveHN6WUDkYQKZK2AsrpNG6TjfJc3wI3Z722tRHui4E772l/sD0SuEj
41pBzOG0VM7DHwUpHQosnvnwx9kjefPNE/uvo14PuzP5yYG2h2PFkQ7uuXjK2/le
iVcyap/zgheOHjlYmOJGT1cnVSodv+rY56eilwIDAQABAoIBAQAM/sAZqcI3Qpt4
uKt8+Jcb9Lcfid2DDgQ53DXwfEK3vGn1wpCuAH/9UUxf0ehBmf4sTBaVe+SOHTmC
8A23wVrgRxTd2qV65TZ4/BCxLcLWrney98cioZBYOHDYXpbxbZ2fMADCLMRSpAm0
piI2L5VCPNH8p4EDTLQEf96GRulKGOWETeVNai3C7Ept6Fxv0YIiiER8j2oPsb1O
LVCBKBPsNs0IlabJAzfDnaqdfWzuLWIT0L4w/qvzfwkdM8tVxch2zjEVosbz4ser
rPO3tle3mobgDvXrW9jEYkIpOtEqCS7l4ybidVuEfY55KlkZ7rGBQ2N1jbLvKjb5
AUyHUBchAoGBAOKjzzBPB/mofycF8iF1QJwripGTDUGM7aXBS0Clp4mh0ksvBsUf
Zg+Qnzr2xZaN53lU65xQlMrebMJow4iJj71VesF9FWPPNbIhh7eMTX4pABcKZNvc
Y0iFf5XZAl3LFdDocQSuB3j5WLNrjSFMBZuYUiZhgiRadtcdpQr+O4lbAoGBALcq
ltbFogxXoo7/CIajbYdNUGbba96jQMOzC1D7yeim1MTtDNGs56ZhDjFZepMRMyfX
/Z7iqxjZQQ1m1THtuiM4g+ug08EYI8G/7DYO5DqMABGFb3vKU9ilhYASqfznpKMJ
2sl/d5j8ocS7crkKwR8Tbo3ZG8NgObQNTL+mIFR1AoGAJS66zzIoHM2IDt7q2pJi
Bz0dfsShaB+23XrY3cJPukTSO4N7mNuN4v/XH9VclVayozVLclnGD4JuVXbanYv0
CRv9B8F9wOI97PuTSIm8LPaNDTqnUWrW3w8H34261ah768o2wI3MrAw8gTMj9FKE
mQJkd+eHcm9lD+XNLgCHxAECgYBiMQ2t00L89NnraKLscp4b44GPsl9QehoVD12o
q2JhO1Ziv2WY3eVNV0hhgkNopdbTrEGFNKRebNEn2xG9c2DO0tQ9s/jw0f0RN87s
Z+1HyZebzPmn1h4+zPUVZGwGbTPgRz8nuBKoS/541bg5pJ9FBojEuDfe9C3a7SpQ
r0EzpQKBgBmYrKi07wTUSZ3TjHWvOK75XhJ5pOdfbuDZk+N02jzhmihzI2M/Sh7s
l1gavtY9o9JGUAW35L/Ju4X1Xgm3t5Cg9+4n6ecOfSKP9nJpgj1EvHyWvw9t8ZSg
V9M0Hf5EoSPWuEj+mlWrIuvV/HgkouUVqDzUm6wUuyTqdTCgUQrA
-----END RSA PRIVATE KEY-----

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
}

View File

@ -9,7 +9,14 @@
"author": "KimLS",
"license": "GPL-3.0",
"dependencies": {
"body-parser": "^1.15.2",
"express": "^4.14.0",
"jsonwebtoken": "^7.2.1",
"libsodium": "^0.4.8",
"libsodium-wrappers": "^0.4.8"
"libsodium-wrappers": "^0.4.8",
"libsodium-wrappers-sumo": "^0.4.8",
"mysql": "^2.12.0",
"node-uuid": "^1.4.7",
"ws": "^1.1.1"
}
}

View File

@ -1,5 +1,21 @@
{
"addr": "localhost",
"port": "9101",
"key": "ujwn2isnal1987scanb"
"db": {
"connectionLimit": 10,
"host": "localhost",
"user": "root",
"password": "blink",
"database": "eqdb"
},
"servertalk": {
"addr": "localhost",
"port": "9101",
"key": "ujwn2isnal1987scanb"
},
"https": {
"enabled": false,
"key": "key.pem",
"cert": "cert.pem"
},
"port": 9080,
"key": "key.pem"
}

10
wi/test.js Normal file
View File

@ -0,0 +1,10 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:9080');
ws.on('open', function open() {
ws.send(JSON.stringify({authorization: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IktTcHJpdGUxIiwiZXhwIjoxNDg0NzIzNDQxLCJpYXQiOjE0ODQxMTg2NDF9.Lmwm572yMWIu1DUrfer8JVvl1DGEkdnMsMFp5WDzp_A', id: '1', method: 'EQW::IsLocked', params: []}));
});
ws.on('message', function(data, flags) {
console.log(data);
});

22
wi/ws/eqw.js Normal file
View File

@ -0,0 +1,22 @@
function Register(name, wsi, api) {
wsi.Register(name,
function(request) {
api.Call(name, request.params)
.then(function(value) {
wsi.Send(request, value);
})
.catch(function(reason) {
wsi.SendError(request, reason);
});
}, true);
}
var RegisterEQW = function(wsi, api) {
Register('EQW::IsLocked', wsi, api);
Register('EQW::Lock', wsi, api);
Register('EQW::Unlock', wsi, api);
};
module.exports = {
'Register': RegisterEQW
}

99
wi/ws/ws_interface.js Normal file
View File

@ -0,0 +1,99 @@
const WebSocketServer = require('ws').Server;
const jwt = require('jsonwebtoken');
class WebSocketInterface
{
constructor(server, key) {
this.wss = new WebSocketServer({ server: server });
this.methods = {};
var self = this;
this.wss.on('connection', function connection(ws) {
self.ws = ws;
ws.on('message', function incoming(message) {
try {
var request = JSON.parse(message);
if(request.method) {
var method = self.methods[request.method];
if(!method) {
self.SendError(request, 'Method not found: ' + request.method);
return;
}
if(method.requires_auth) {
if(!request.authorization) {
self.SendError(request, 'Authorization Required');
return;
}
jwt.verify(request.authorization, key, function(err, decoded) {
if(err) {
self.SendError(request, 'Authorization Required');
return;
}
request.token = decoded;
method.fn(request);
});
return;
}
method.fn(request);
} else {
self.SendError(request, 'No method supplied');
}
} catch(ex) {
console.log('Error parsing message:', ex);
self.SendError(null, 'No method supplied');
}
});
});
}
Register(method, fn, requires_auth) {
var entry = { fn: fn, requires_auth: requires_auth };
this.methods[method] = entry;
}
SendError(request, msg) {
try {
if(this.ws) {
var error = {};
if(request && request.id) {
error.id = request.id;
}
error.error = msg;
this.ws.send(JSON.stringify(error));
}
} catch(ex) {
console.log(ex);
}
}
Send(request, value) {
try {
if(this.ws) {
var response = {};
if(request && request.id) {
response.id = response.id;
}
response.response = value;
this.ws.send(JSON.stringify(response));
}
} catch(ex) {
console.log(ex);
}
}
}
module.exports = {
'wsi': WebSocketInterface
}

View File

@ -34,7 +34,7 @@ void EQW__Unlock(WebInterface *i, const std::string& method, const std::string&
void RegisterEQW(WebInterface *i)
{
i->AddCall("IsLocked", std::bind(EQW__IsLocked, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
i->AddCall("Lock", std::bind(EQW__Lock, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
i->AddCall("Unlock", std::bind(EQW__Unlock, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
i->AddCall("EQW::IsLocked", std::bind(EQW__IsLocked, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
i->AddCall("EQW::Lock", std::bind(EQW__Lock, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
i->AddCall("EQW::Unlock", std::bind(EQW__Unlock, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
}