SkillAgentSearch skills...

LuWS

A WebSocket client implementation for Luup (Vera and openLuup) systems, with optional async receive (responsive)

Install / Use

/learn @toggledbits/LuWS
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

LuWS - A Luup WebSocket Client

LuWS is a WebSocket client implementation for Luup systems (Vera and openLuup). It implements the WebSocket protocol as described in RFC6455, except for 64-bit frame length. It supports encrypted (SSL/TLS) connections.

LuWS supports the SockProxy plugin, which allows it to respond immediately, asynchronous to available receive data. This makes full duplex communications more efficient, as polling for receivable data isn't necessary. Polling for data introduces the possibility that a message may not be received and handled with any better response time than the poll interval -- that is, if the poll interval is 15 seconds, it will often be nearly that long before an available datagram is processed, unless asynchronous receive assisted by SockProxy is used.

LuWS' To-Do List

The following are known TBDs, which includes several known non-compliances with RFC6455:

  • SECURITY: The Sec-WebSocket-Accept response header is not verified in the current implementation, although correcting this is a high-priority task and expected to be completed shortly.
  • SECURITY? According to RFC6455 section 5.1, we MUST error and close when receiving a frame/fragment from a server that is masked; we don't do this currently, we unmask it and roll on.
  • 64-bit frame lengths are not supported (receive or transmit). There are no plans to fix this currently. Messages of virtually any size can be sent and received, as long as no fragment (a message is comprised of one or more fragments) exceeds 65535 bytes in length.
  • An LTN12 source can be optionally used for sending data, but there is no use of, for example, sinks on the receive side. I'm still contemplating how LTN12 could be useful elsewhere.

Installing LuWS

LuWS is a drop-in for your Luup plugin/subsystem. It is recommended that you change the filename to L_yourpluginname_LuWS.lua, and modify the module statement at the head of the file in the same way (omitting the .lua suffix there) to avoid potential version conflicts with other plugins that may want to use LuWS as well.

Using LuWS -- Basics

This section will cover basic LuWS integration -- don't skip it! If you are going to support asynchronous receive using SockProxy, we'll cover the additional details of that in the next section. Most of this section applies to all use cases, so please read on...

To load LuWS, just use a require statement as you would any other module:

luws = require "L_MyPluginName_LuWS"

To open a session, luws.wsopen() is called with the endpoint URL. The function will return a blind table -- a table you should not play with, but keep to pass around to other functions where needed. If an error occurs during the connection attempt, wsopen() will return two values, false and an error message, so always receive two values on your call:

local luws = require "L_MyPluginName_LuWS"

local conn, err = luws.wsopen( "wss://www.websocket.org/echo", message_handler, options )
if not conn then
    luup.log("The connection failed: " .. err)
else
    luup.log("Successful connection!")
end

The message_handler is a function reference (not a name in a string) or closure that will handle messages received from the endpoint. Messages are complete datagrams after any necessary reassemby resulting from fragmentation (a message is comprised of one or more fragments). The message handler is also called for certain "control" situations, such as an unexpected disconnect of the endpoint or other error in communication. See Your Message Handler below for more details.

The options argument is a table containing connection options, further described below.

To send a message, you call wssend( conn, opcode, data ). The conn parameter is the table handed back by the previous successful wsopen() (all functions take this argument, so I won't repeat this description on future calls). The opcode is the WebSocket message type (per the RFC, 1 = text data, 2 = binary data). The data can be either a string or a LTN12 source (see the LTN12 module documentation).

To receive a reply, you call wsreceive( conn ). You should call this function periodically, not just after sending data, so that any unprompted message received from the endpoint gets handled. This is receive polling (we'll talk about asynchronous receive later), and you should do this polling as often as makes sense for your application (every 15 seconds is sane for apps that need to respond quickly to receive messages -- it doesn't waste much CPU if no data is available). But it can be much longer, and you may apply other heuristics, depending on the nature of your endpoint. For example, if your endpoint only sends replies to things you send, there's no sense trying to receive data when you haven't sent anything. That's all up to you to determine; the protocol and LuWS do not enforce any rules in this respect.

Finally, any time you need to discard the current session, make sure you call wsclose() to tear down the session and free its resources. You must also always do this when an error occurs before recovering by opening a new session, as well as any exit of your plugin. If you don't do this, you will leak resources and eventually cause a Luup reload, or in some cases, a system crash/restart.

Your Message Handler

If your wsreceive() call actually picked up a message from the endpoint, LuWS will call the message handler function you specified to wsopen(). Your message handler declaration should look like this:

function message_handler( conn, opcode, data, ... )

The opcode is the opcode (numeric, per the RFC) of the received message, and the data is the accompanying data (as a string) of the received message. Fragmented messages are reassembled by LuWS before being passed to your handler, so there is no need to worry about that -- the message you receive is complete. The ... is any additional arguments that you asked be passed to the handler (by setting handler_args in wsopen()'s options -- see Options below).

If the opcode received by your handler is false (boolean), then an error has occurred in receiving data. You must always test for this. In this case, the data will be the error message. The most common errors are "message timeout" and "receiver error: closed"; the former indicates that a message was not received for longer than the message timeout setting (see Options below), and the latter indicates that the TCP connection has been broken for some reason. Typically, you will just want to log the message, wsclose() the connection (to be sure all of its resources are released), and open a new one (discard the old conn from the previous wsopen()).

Options

There are a few options you can define when calling wsopen() to control the behavior of LuWS' handling of connections and data. Generally, you won't need to supply any options, the defaults are usually acceptable.

  • handler_args: a Lua table (array) containing additional arguments that should be passed to your message handler when it is called, default: none;
  • receive_timeout: if no messages are received from the endpoint in this many seconds, the connection is assumed to have died somehow and will cause an error notification to the message handler (0=default, no timeout is enforced).
  • receive_chunk_size: maximum size (in bytes) of a socket read; it is usually not necessary to modify this, but some older Vera systems with little RAM may benefit from a size smaller than the default 2048.
  • max_payload_size: maximum size (in bytes) of a message that can be received and passed to the handler; longer messages generate an error notification to the handler. The default is 64K bytes. Note that this is the total size of the message, not the maximum size of a fragment (several fragments assemble into one message). Fragments are always limited to 65535 bytes in the current implementation, but the max_payload_size can be set much larger.
  • ssl_protocol: LuaSec protocol option value for SSL/TLS connections, default all (systems using older versions of LuaSec may not be able to use all and should use tlsv1_2);
  • ssl_verify: LuaSec verify option value for SSL/TLS connections, default none (Nota Bene! this turns off peer certificate validation and is thus less secure, but is necessary for connecting to systems with self-signed certificates. If you are connecting to a system with valid certificates, it is strongly recommended that you set this to peer);
  • ssl_mode: LuaSec mode option value for SSL/TLS connections, default client;
  • ssl_options: LuaSec options value for SSL/TLS connections, table/array, default { "all" };
  • use_masking: turn masking on (true) or off (false) for all frames sent to the endpoint. RFC6455 requires that all frames sent by a client be masked, so this is true by default. Some trivial WebSocket server implementations, however, don't do it, and some don't care (if your app sends a lot of data frequently, and your endpoint doesn't care, turning masking off saves a little CPU time). This setting has no effect on received frames;
  • control_handler: normally LuWS handles control frames and does not even notify the app that they have been received; a function or closure given at this key will be called when control messages are received. See the Reference section for additional details.
  • connect: connect function for creating a TCP socket; a default function is provided and this function only needs to be provided if somehow the caller needs to control how the socket is created.

The full details of the SSL option values can be found in the LuaSec documentation.

Using LuWS with SockProxy

To use SockProxy's asynchronous receive capabilities with LuWS, you need to modify your program/plugin as follows:

  1. Provide an override connect function (in options f

Related Skills

View on GitHub
GitHub Stars4
CategoryDevelopment
Updated4y ago
Forks2

Languages

Lua

Security Score

55/100

Audited on Feb 25, 2022

No findings