Skip to content

An example of the application for LAppS protocol

Pavel Kraynyukhov edited this page May 5, 2018 · 6 revisions

An example of the application using LAppS protocol

Configuration

lapps.json

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).

Directory structure

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

Main module

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;

lapps_echo_maps.lua submodule

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;

lapps_echo_methods.lua sub-module

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
    

Client Application

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.