I've written a Node js server that create, read, and update user data from a SQL server hosted in Azure. Clients connect to the server using a standard TCP socket, every message sent or received will be encrypted/decrypted using AES 256 CBC.
Communication protocol
TCP is used to create a simple communication mean without the over head of HTTP/HTTPS, and the client will try to persist that connection in order to eliminate the need to connect every time a message is sent. That will also give us the ability to know the number of connected players, and to be able to send messages from the server to all connected players.
Authentication
There are no authentication at the moment, anyone could connect to the server using telnet or any TCP based client. However any data sent will be decrypted if decryption or JSON parsing failed the connection will be closed. I am open for any suggestion in this area.
Message format
All messages exchanged between the server and client uses JSON format. Messages sent from client to server are formatted as the following JSON message:
{
"ID": 0 // a number indicating which function to call
"msg": "{
// embedded JSON contains the data
.....
}"
}
Messages sent from server to client
- Number indicating if the operation was successful(1) or not(0).
Or
- JSON containing data about the player, if the operation was about fetching player data.
Message process
All JSON messages received are parsed asynchronously and messages sent to the client are stringifyied asynchronously to avoid Nodejs main loop blocking.
Server.js
/* Library to add date and time to any console.log */
require('log-timestamp');
/* Asynchronous JSON parse library */
var parseJSON = require('json-parse-async');
/* Asynchronous JSON stringify library */
var asyncJSON = require('async-json');
/* Keymetrics advance metrics */
var pmx = require('pmx').init({
http: false, // HTTP routes logging (default: true)
ignore_routes: [], // Ignore http routes (Default: [])
errors: true, // Exceptions loggin (default: true)
custom_probes: false, // Auto expose JS Loop Latency and HTTP req/s
network: true, // Network monitoring at the application level
ports: false // Shows which ports your app is listening on (default: false)
});
var probe = pmx.probe();
// The counter will start at 0
var counter = probe.counter({
name: 'Current connected players'
});
/* Encryption situp */
var crypto = require('crypto');
var key = new Buffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]);
var iv = new Buffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
var cipherAlgorithmUsed = 'aes-256-cbc';
/* Encrypt buffer */
function encrypt(textBuffer) {
try {
var cipher = crypto.createCipheriv(cipherAlgorithmUsed, key, iv);
var crypted = Buffer.concat([cipher.update(textBuffer), cipher.final()]);
return crypted;
} catch (err) {
// Handle the error here.
console.error('Error trying to encrypt, ' + err);
return '';
}
return '';
}
/* Decrypt buffer */
function decrypt(encryptedBuffer) {
try {
var decipher = crypto.createDecipheriv(cipherAlgorithmUsed, key, iv);
var dec = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
return dec;
} catch (err) {
// Handle the error here.
console.error('Error trying to decrypt, ' + err);
return '';
}
return '';
}
/* Database Connection */
var ConnectionPool = require('tedious-connection-pool');
var DBTDSConnection = require('tedious').Connection;
var Request = require('tedious').Request;
var poolConfig = {
min: 10, // The minimun of connections there can be in the pool. Default = 10
max: 1000, //The maximum number of connections there can be in the pool. Default = 50
log: false
};
var dBConnectionconfig = {
userName: 'kskskhr@939jdn',
password: 'kwpfnekososken',
server: 'jjsmcmnxjfhsn.database.windows.net',
options: {
requestTimeout: 0,
debug: {
packet: false,
data: false,
payload: false,
token: false,
log: false
},
encrypt: true,// If you are on Microsoft Azure, you need this:
database: 'ylldkgnje_db'
}
};
//create the pool
var pool = new ConnectionPool(poolConfig, dBConnectionconfig);
pool.on('error', function(err) {
console.error('Error when trying to create DB connection pool' + err);
});
/* Login Server */
var net = require('net');
var HOST = '127.0.0.1';
var PORT = 4000; // nginx is used infront as a reverse proxy
var server = net.createServer(); // Create a server instance
server.listen(PORT, HOST);
onClientConnected_ServerEvent = function(sock) {
counter.inc(); // Increment the counter, metrics
sock.on('data', function(data) { // data from client
parseJSON(decrypt(data), function(err, content) {
if (err) {
console.error('Error when trying to parse json received from client, json received is ' + data + ', and the error is ' + err);
sock.end();
} else {
switch (content.reqID) {
case 0:
setPlayerData(sock, content.msg);
break;
case 1:
getPlayerData(sock, content.msg);
break;
case 2:
createUser(sock, content.msg);
break;
default:
console.log('request id sent doesnt match any of the cases, id sent is ' + content.reqID);
return;
}
}
});
});
sock.on('close', function(data) {
counter.dec(); // Decrement the counter, metrics
});
sock.on('timeout', function(data) {
});
}
/*
Emitted when an error occurs. The 'close' event will be called directly following this event. See example in discussion of server.listen.
*/
onError_ServerEvent = function(err) {
// handle errors here
console.error('onError_ServerEvent occured, ' + err);
throw err;
}
server.on('connection', onClientConnected_ServerEvent);
server.on('error', onError_ServerEvent);
var PlayerData = function() {
this.appVersionAndroid = "2.7",
this.appVersioniOS = "2.7",
this.kickStatus = "true",
this.maxNumOfPlayerInRoom = "10",
this.userName = "potato",
this.coin = 2311,
this.ruby = 22,
this.rep = 352,
this.driftMax = 2133,
this.dailyRepsGiveAway = 132,
this.equipedProducts = "3-1-2-1,3-6-2-1,2-1-1-1",
this.boughtProducts = "3-1-2-1,3-6-2-1,2-1-1-1";
}
// case 0
function setPlayerData(sock, dataSent) { // update player data
// parse json, then update player row in DB
parseJSON(dataSent, function(err, playerDataSent) {
if (err) {
console.error('(setPlayerData) Error when trying to parse json received from switch statment, json received is ' + dataSent + ', and the error is ' + err);
} else {
pool.acquire(function(err, connection) { //acquire a db connection from connection pool
if (err) {
console.error('(setPlayerData) Error when trying to acquire a DB connection from pool, ' + err);
return;
}
var sqlUpdatePlayerDataQuery = "UPDATE PlayerDataTable SET rep=" + playerDataSent.rep + ", driftDistance=" + playerDataSent.driftMax + ", coin = " + playerDataSent.coin + ", ruby = " + playerDataSent.ruby + ", dailyReps = " + playerDataSent.dailyRepsGiveAway + ", boughtProducts = '" + playerDataSent.boughtProducts + "', equippedProducts = '" + playerDataSent.equipedProducts + "', lastUpdated = GETDATE() WHERE UserName ='" + playerDataSent.userName + "'";
var request = new Request(sqlUpdatePlayerDataQuery, function(err, rowCount) {
if (err) {
sock.write(encrypt('0'));
console.error('(setPlayerData) Error when trying to execute SQL update player data, ' + err);
return;
}
if (rowCount == 1) {
// 1 row updated
sock.write(encrypt('1'));
} else {
sock.write(encrypt('0'));
}
//release the connection back to the pool when finished
connection.release();
});
connection.execSql(request);
});
}
});
}
// case 1
function getPlayerData(sock, dataSent) { // get player data when game starts
// dataSent will have the username
// get all player data, parse, then send
// acquire a db connection from connection pool
pool.acquire(function(err, connection) {
if (err) {
console.error('(getPlayerData) Error when trying to acquire a DB connection from pool, ' + err);
return;
}
//use the connection as normal
var request = new Request("select * from PlayerDataTable where UserName = '" + dataSent + "'", function(err, rowCount) {
if (err) {
sock.write(encrypt('0'));
console.error('(getPlayerData) Error when trying to execute SQL select * from , ' + err);
return;
}
if (rowCount < 1) {
// error retrieving user
sock.write(encrypt('0'));
}
//release the connection back to the pool when finished
connection.release();
});
request.on('row', function(columns) {
var playerData = new PlayerData();
columns.forEach(function(column) {
if (column.metadata.colName === "UserName") {
playerData.userName = column.value;
} else if (column.metadata.colName === "rep") {
playerData.rep = column.value;
} else if (column.metadata.colName === "driftDistance") {
playerData.driftMax = column.value;
} else if (column.metadata.colName === "coin") {
playerData.coin = column.value;
} else if (column.metadata.colName === "ruby") {
playerData.ruby = column.value;
} else if (column.metadata.colName === "boughtProducts") {
playerData.boughtProducts = column.value;
} else if (column.metadata.colName === "equippedProducts") {
playerData.equipedProducts = column.value;
} else if (column.metadata.colName === "dailyReps") {
playerData.dailyRepsGiveAway = column.value;
}
});
asyncJSON.stringify(playerData, function(err, jsonValue) {
if (err) {
console.error('(getPlayerData) Error when trying to stringify json, json is ' + playerData + ', and the error is ' + err);
} else {
sock.write(encrypt(jsonValue));
}
});
});
connection.execSql(request);
});
}
// case 2
function createUser(sock, dataSent) { // create a new username for a player
// dataSent will have the username
// if query succeeded print 1 else 0
//acquire a db connection from connection pool
pool.acquire(function(err, connection) {
if (err) {
console.error('(createUser) Error when trying to acquire a DB connection from pool, ' + err);
sock.write(encrypt('0'));
return;
}
//console.error('name sent is ' + name);
//use the connection as normal
var request = new Request("INSERT INTO PlayerDataTable (username,coin,ruby) VALUES ('" + dataSent + "',150,20)", function(err, rowCount) {
if (err) {
console.error('(createUser) Error when trying to execute SQL INSERT INTO , ' + err);
sock.write(encrypt('0'));
} else {
sock.write(encrypt('1'));
}
//release the connection back to the pool when finished
connection.release();
});
connection.execSql(request);
});
}
<username>
/<password>
or so. \$\endgroup\$