-
Notifications
You must be signed in to change notification settings - Fork 3
An example of the application for LAppS protocol
In services section we add the service descriptor:
"echo_lapps" : {
"internal" : false,
"request_target" : "/echo_lapps",
"protocol" : "LAppS",
"instances" : 3
}
This descriptor defines the protocol we intend to use (LAppS), number of parallel instances of this application, and the request target (/echo_lapps).
Separate folder is not required though it makes better overview of the application content, when all the files of the application are under same root. The name of the application will be added to the search path on LAppS start (/opt/lapps/apps/echo_lapps/?.lua)
mkdir /opt/lapps/apps/echo_lapps
The name of the module-file must be exactly the same as the name of service plus '.lua' extension. In our case it is echo_lapps.lua
--[[ echo_lapps.lua module]] --
-- definition of the module table
echo_lapps = {}
echo_lapps.__index = echo_lapps;
-- lets separate the application into several files, and then import them into main module.
maps=require("lapps_echo_maps"); -- some global objects
local methods=require("lapps_echo_methods"); -- implementation of the methods
-- two obligatory methods we will not use in this application, though we must define them
echo_lapps["onStart"]=function()
end
echo_lapps["onShutdown"]=function()
end
-- lets define method for handling incorrect remote calls to non-existing methods
echo_lapps["method_not_found"] = function(handler)
-- prepare the message
local err_msg=nljson.decode([[{
"status" : 0,
"error" : {
"code" : -32601,
"message": "No such method"
},
"cid" : 0
}]]);
-- send the message
ws:send(handler,err_msg);
-- close the socket.
ws:close(handler,1003); -- WebSocket close code here. 1003 - "no comprende",
-- lets close clients which asking for wrong methods
end
-- now lets define the onMessage handler
--[[
-- @param handler - io handler to use for ws:send and ws:close
-- @param msg_type - helper enum defining type of the message received:
-- 1 - Client Notification Message
-- 2 - Client Notification Message with params attribute
-- 3 - Request
-- 4 - Request with params attribute
-- @param message - an nljson userdata object
--]]
echo_lapps["onMessage"]=function(handler,msg_type, message)
-- lets define basic skeleton of reactions to different kind of requests we receive:
local switch={
[1] = function() -- a CN message does not require any response. Let's restrict CN's without params
-- NOTE: all error messages for requests are delivered to CCH (cid:0)
local err_msg=nljson.decode([[{
"status" : 0,
"error" : {
"code" : -32600,
"message": "This server does not accept Client Notifications without params"
},
"cid" : 0
}]]);
ws:send(handler,err_msg);
ws:close(handler,1003); -- WebSocket close code here. 1003 - "no comprende"
end,
[2] = function() -- a CN with params, does not require any response.
local method=methods._cn_w_params_method[message.method] or echo_lapps.method_not_found;
method(handler,message.params);
end,
[3] = function() -- requests without params are unsupported by this app
local method=echo_lapps.method_not_found;
method(handler);
end,
[4] = function()
local method=methods._request_w_params_method[message.method] or echo_lapps.method_not_found;
method(handler,message.params);
end
}
-- execute
switch[msg_type]();
-- onMessage must return boolean value. if you return false here, the socket will be closed by application server.
return true;
end
-- return table
return echo_lapps;
Now lets define a submodule simulating key-value storage for logins and session keys.
lapps_echo_maps={}
lapps_echo_maps.__index=lapps_echo_maps
-- this is a storage for session keys
lapps_echo_maps["keys"]=nljson.decode('{}');
-- user/password table (never use it with widely open ports, this is just an example, a simulation)
lapps_echo_maps["logins"]=nljson.decode([[{
"admin" : "admin",
"guest" : "guest"
}]]);
return lapps_echo_maps;
Now we going to implement all the methods we are using in echo_lapps module and those we want to provide for client applications.
lapps_echo_methods={}
lapps_echo_methods.__index=lapps_echo_methods
-- maps are global here too. we want to share these data between modules.
maps=require("lapps_echo_maps"); -- must be global
-- Definition of method with params for Client Notifications
lapps_echo_methods["_cn_w_params_method"]={
["logout"]=function(handler,params) -- logout method
if(nljson.find(params[1],"authkey") ~= nil) and (type(params[1].authkey) == "number")
then
local hkey=handler; -- prevent the tostring function to convert our handler into string
local connection_authkey=nljson.find(maps.keys, tostring(hkey))
-- is this connection logged in and is it an owner of the key?
if(connection_authkey ~= nil) and (connection_authkey == params[1].authkey)
then
nljson.erase(maps.keys,hkey)
ws:close(handler,1000);
end
else
local can_not_logout=nljson.decode([[{
"status" : 0,
"error" : {
"code" : 20,
"message": "Can not logout. Not logged in."
},
"cid" : 3
}]]);
-- lets try to send a notification to channel 3
-- (suppose we want all out of order notifications with errors appear on channel 3)
ws:send(handler,can_not_logout);
ws:close(handler,1000); -- normal close code
end
end
}
-- Definition of methods for requests.
lapps_echo_methods["_request_w_params_method"]={
["login"]=function(handler,params) -- login requires params
-- authentication error message
local login_authentication_error=nljson.decode([[{
"status" : 0,
"error" : {
"code" : 10,
"message": "Authentication error"
},
"cid" : 0
}]]);
-- login successful message
local login_success=nljson.decode([[{
"status" : 1,
"result" : [
{ "authkey" : 0 }
],
"cid" : 0
}]]);
local wrong_params=nljson.decode([[{
"status" : 0,
"error" : {
"code" : -32602,
"message" : "there must be only one object inside the params array for method login(). \
This object must have two key/value pairs: { \"user\" : \"someusername\", \"password\" : \"somepassword\" }"
},
"cid" : 0
}]]);
local username=""
local password=""
if(nljson.typename(params[1]) == "object")
then
username=nljson.find(params[1],"user");
password=nljson.find(params[1],"password");
else
ws:send(handler,wrong_params);
ws:close(handler,1003); -- WebSocket close code here. 1002 - "protocol violation"
return
end
if((type(username) ~= "string") or (type(password) ~= "string"))
then
ws:send(handler,wrong_params);
ws:close(handler,1003); -- WebSocket close code here. 1002 - "protocol violation"
return
end
local user_exists = nljson.find(maps.logins,username) ~= nil;
if(user_exists)
then
if(maps.logins[username] == password)
then -- generate authkey. it is a bad example of keys generation.
-- Do not use it in production. Do not use it ever. Im just lasy here.
local authkey=math.random(4294967296);
local idxkey=handler;
maps.keys[tostring(idxkey)]=authkey;
login_success.result[1].authkey=authkey;
ws:send(handler,login_success)
else
ws:send(handler,login_authentication_error);
ws:close(handler,1000); -- WebSocket close code here. 1000 - "normal close"
end
else
ws:send(handler,login_authentication_error);
ws:close(handler,1000); -- WebSocket close code here. 1000 - "normal close"
end
end,
["echo"] = function(handler,params)
local not_authenticated=nljson.decode([[{
"status" : 0,
"error" : {
"code" : -32000,
"message" : "Not authenticated. Permission deneid"
},
"cid" : 0
}]]);
local authkey=nljson.find(params[1],"authkey");
local current_session_authkey=nljson.find(maps.keys,handler);
if (authkey ~= nil) and (current_session_authkey ~=nil)
and (authkey == current_session_authkey)
then
local response=nljson.decode([[{
"cid" : 0,
"status" : 1,
"result" : []
}]]);
response.result=params;
ws:send(handler,response);
else -- close connection on not authenticated sessions
ws:send(handler,not_authenticated);
ws:close(handler,1000); -- WebSocket close code here. 1000 - "normal close"
end
end
}
return lapps_echo_methods
Two external libraries are used for client application: webix and cbor.js. The HTML page with code is self-explanatory.
<body>
<link rel="stylesheet" href="http://cdn.webix.com/edge/webix.css" type="text/css">
<script src="http://cdn.webix.com/edge/webix.js" type="text/javascript"></script>
<script src="cbor.js" type="text/javascript"></script>
<div id="chart" style="width:100%;height:300px;margin:3px"></div>
<script>
function now()
{
return Math.floor(Date.now() / 1000);
}
// globals
window["teststart"]=now();
window["testend"]=now();
window["secs_since_start"]=0;
window["roundtrips"]=0;
window["lapps"]={
authkey : 0
};
console.log("timestamp: "+now());
// initial data set for the chart
var dataset = [
{ id:1, rps:0, second:0 }
]
// the chart
webix.ui({
id:"barChart",
container:"chart",
view:"chart",
type:"bar",
value:"#rps#",
label:"#rps#",
radius:0,
gradient:"rising",
barWidth:40,
tooltip:{
template:"#rps#"
},
xAxis:{
title:"Ticking RPS",
template:"#second#",
lines: false
},
padding:{
left:10,
right:10,
top:50
},
data: dataset
});
// might be a dialog instead
var login = {
lapps : 1,
method: "login",
params: [
{
user : "admin",
password : "admin"
}
]
};
// echo request
var echo= {
lapps : 1,
method: "echo",
params: [
{ authkey : 0 },
[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]
]
};
// create a websocket
var websocket = new WebSocket("wss://127.0.0.1:5083/echo_lapps");
websocket.binaryType = "arraybuffer";
// on response
websocket.onmessage = function(event)
{
window.roundtrips=window.roundtrips+1;
window.testend=Date.now()/1000;
if((window.testend - window.teststart) >=1)
{
window.secs_since_start++;
console.log("secs since start: ["+ window.secs_since_start+"]");
window.teststart=window.testend;
$$("barChart").add({rps: window.roundtrips, second: window.secs_since_start});
window.roundtrips=0;
if(window.secs_since_start > 30 )
{
$$("barChart").remove($$("barChart").getFirstId());
}
}
// CBOR to native JavaScript object
var message = CBOR.decode(event.data);
// Verifying the channel
if(message.cid === 0)
{
if(message.status === 1)
{
if(window.lapps.authkey === 0)
{
if(typeof message.result[0].authkey !== "undefined") // authkey is arrived
{
window.lapps.authkey=message.result[0].authkey;
echo.params[0].authkey = window.lapps.authkey;
websocket.send(CBOR.encode(echo));
}
else
{
console.log("No authkey: "+JSON.stringify(message));
}
}
else websocket.send(CBOR.encode(echo));
}
else
{
//
}
}
else // OONs are just printed to console
{
console.log("OON: "+JSON.stringify(message));
}
};
// login on connection
websocket.onopen=function()
{
console.log('is open');
window.teststart=Date.now()/1000;
websocket.send(CBOR.encode(login));
}
// close connection if peer sent close frame
websocket.onclose=function()
{
console.log("is closed");
}
</script>
</body>
Do not forget to add an exclusion for https://127.0.0.1:5083 self-signed certificate into Mozilla Firefox. In Google Chrome you have to use the https://127.0.0.1:5083/echo_lapps URL first, then ADVANCED->PROCEED, and then load your client.html file.