Posts: 113
Threads: 36
Joined: Oct 2016
Reputation:
1
How can I create custom package for LM?
I need custom websocket connections with TV and other stuff and I want to use this connection inside LUA resident or event scripts.
The main idea is I create a package which works as HTTP server on some local port and it has and ability to create websocket clients inside.
e.g.
event script
Code:
1 2 3 4
ws =
require (
'lua.websocket' )
tvConnection ,
err =
ws.connect (
'ws://my.tv.ip:3000' )
tvConnection.send (
'raw data' )
resident script 0 sec
Code:
1 2 3 4
ws =
require (
'lua.websocket' )
tvConnection ,
err =
ws.connect (
'ws://my.tv.ip:3000' )
data =
tvConnection.receive ()
Could you please create kind of this stuff? Or give me instruction how to build own package, I know C/C++, Golang, JS, Lua
Thank you in advance!
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
There's a pure Lua websocket client library. It should work on LM with some minimal changes.
As for custom packages we do not provide build tools but we can provide some custom packages by request.
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
Attached is websocket client library ported from
https://github.com/lipp/lua-websockets/
Main changes:
1. Support for basic auth in URL
2. Receive timeout does not close the websocket connection
3. Server support is removed
Add it as a user library named websocket
Minimal example (resident script):
Code:
1 2 3 4 5 6 7 8 9
if not ws then
ws =
require (
'user.websocket' )
url =
'ws://admin:admin@192.168.1.12/apps/localbus.lp'
client =
ws.client (
'sync' ,
10 )
client :
connect (
url )
end
log (
client :
receive ())
Attached Files
Posts: 200
Threads: 60
Joined: Jun 2015
Reputation:
7
Hi,
Thank you. This is great. Is there an recommend way for connecting to multiple websocket servers with this client?
Thanks,
Roger
Posts: 200
Threads: 60
Joined: Jun 2015
Reputation:
7
Hi,
I am trying to use the copas part because being a resident script i need to run a udp server to accept an event script client.
On connecting with the copas parameter I get the following in the error log.
Library copas:0: attempt to yield across C-call boundary
stack traceback:
[C]: in function 'yield'
Library copas: in function 'connect'
User library websocket:500: in function 'sock_connect'
User library websocket:438: in function 'connect'
Code:
1 2 3 4 5 6
ws =
require (
"user.websocket" )
url =
'ws://demos.kaazing.com/echo'
client =
ws.client (
'copas' ,
10 )
client :
connect (
url )
Posts: 200
Threads: 60
Joined: Jun 2015
Reputation:
7
Hi,
I am using the following UDP server code for taking requests from the LM objects. effectively the websockets client needs to running asynchronously so it doesn't block the udp server running. In the past I have used copas to do this.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
local server =
usocket.udp ()
server :
setsockname (
"127.0.0.1" ,
53450 )
function handler (
skts )
skts =
copas.wrap (
skts )
alert (
"UDP connection handler" )
while true do
local s ,
err
alert (
"UDP receiving.." )
s ,
erro =
skts :
receive (
2048 )
if not s then
alert (
"Receive error: %s" ,
erro )
break
end
alert (
"Received data, bytes: %s" ,
s )
client :
send (
s )
end
end
copas.addserver (
server ,
handler ,
1 )
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
You need to wrap websocket client in copas mode with function passed to copas.addthread
Posts: 200
Threads: 60
Joined: Jun 2015
Reputation:
7
Hi,
Is there an example from this? Because that's what I am trying. thx Roger
Code:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
if not ready then
socket =
require (
"user.websocket" )
usocket =
require (
"socket" )
copas =
require (
"copas" )
ready =
true
apps = {}
function increment (
n )
n =
n +
1
return n
end
function parse (
data )
alert (
'parsing: %s' ,
data )
local response =
string.split (
data ,
' = ' )
end
function sendCommand (
command )
skt :
send (
command )
sleep (
1 )
end
function init ()
end
function fromKNX (
command )
local telegram =
string.split (
command ,
',' )
log (
command )
sendCommand (
command )
end
local server =
usocket.udp ()
server :
setsockname (
"127.0.0.1" ,
53450 )
function handler (
skts )
skts =
copas.wrap (
skts )
alert (
"UDP connection handler" )
while true do
local s ,
err
alert (
"UDP receiving.." )
s ,
erro =
skts :
receive (
2048 )
if not s then
alert (
"Receive error: %s" ,
erro )
break
end
alert (
"Received data, bytes: %s" ,
s )
fromKNX (
s )
end
end
copas.addserver (
server ,
handler ,
1 )
end
if not skt then
url =
'ws://demos.kaazing.com/echo'
skt ,
err =
socket.client (
'copas' ,
5 )
skt :
connect (
url ,
80 )
skt :
send (
'hello' )
if (
not err )
then
copas.addthread (
function ()
while true do
local resp ,
err =
copas.receive (
skt )
if not resp then
alert (
"[tcp-client] Receive error: %s" ,
err )
copas.removeserver (
skt )
skt =
nil
break
end
local fd ,
prtd =
pcall (
parse ,
resp )
if (
fd ==
false )
then
alert (
"Error with parsemsg %s " ,
prtd )
end
end
end )
if skt then
alert (
'[tcp-client] connection ok' )
init ()
else
if warningfailed then alert (
'[tcp-client] connection failed (conn): %s' ,
err )
end
return
end
else
alert (
'[tcp-client] error connecting %s' ,
err )
return
end
end
copas.loop ()
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
connect part must be inside copas thread
Posts: 200
Threads: 60
Joined: Jun 2015
Reputation:
7
Hi,
Ok adding the connect in the thread works. Thank you. The timeout parameter doesn't seem to have any effect. The connection always lasts about 10 seconds disconnects and reconnects. Is there no way to keep it permanently connected?
Another thing I need to account for is that the webservice is not always running. It's live when the device is powered on. So what I see is that the script would be constantly trying to reconnect. I guess the only way to handle this is by disabling the resident script when the device is powered off. Unless you have any other idea?
thanks
Roger
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
Disconnect might happen from the server side. To keep connection alive you probably need to request data periodically or send ping frames.
As for reconnect, enable/disable is one approach, another one is just to keep script running and check if connect is ok or not. When device is offline then connect function will return once timeout is reached.
Posts: 1
Threads: 0
Joined: Feb 2019
Reputation:
0
(26.03.2018, 08:42) rocfusion Wrote: Hi,
Ok adding the connect in the thread works. Thank you. The timeout parameter doesn't seem to have any effect. The connection always lasts about 10 seconds disconnects and reconnects. Is there no way to keep it permanently connected?
Another thing I need to account for is that the webservice is not always running. It's live when the device is powered on. So what I see is that the script would be constantly trying to reconnect. I guess the only way to handle this is by disabling the resident script when the device is powered off. Unless you have any other idea?
thanks
Roger
Hi Roger,
Did you have any success getting your LG TV to work with this? Also did you manage to support connecting to multiple clients?
I'm trying to do the same (I have 2 LG WebOS TVs I'd like to control).. but I'm a complete newbie to Lua.
Nick
Posts: 1
Threads: 0
Joined: Jan 2016
Reputation:
0
Referring to the WebSocket post, we are able to connect to the web-socket server, but in the CASAMBI API documentation , they have asked to pass the "API-Key" (a string) as "protocol" (under "Create WebSocket Connection" section of documentation). They have also given an example in JavaScript which is shown below.
How shall this be implemented using LM WebSocket Library. ? we tried but it is not working (in LM Ambient)
Code:
1
webSocket =
new WebSocket (
"url" ,
"api_key" );
https://developer.casambi.com/
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
You need to modify the upgrade_request function in websocket library:
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
local upgrade_request =
function (
req ,
key )
local format =
string.format
local lines = {
format (
'GET %s HTTP/1.1' ,
req.path or '' ),
format (
'Host: %s' ,
req.host ),
'Upgrade: websocket' ,
'Connection: Upgrade' ,
format (
'Sec-WebSocket-Key: %s' ,
key ),
'Sec-WebSocket-Version: 13' ,
}
if self.protocol then
tinsert (
lines ,
format (
'Sec-WebSocket-Protocol: %s' ,
self.protocol ))
end
if req.port and req.port ~=
80 then
lines [
2 ] =
format (
'Host: %s:%d' ,
req.host ,
req.port )
end
if req.userinfo then
local auth =
format (
'Authorization: Basic %s' ,
base64enc (
req.userinfo ))
tinsert (
lines ,
auth )
end
tinsert (
lines ,
'\r\n' )
return tconcat (
lines ,
'\r\n' )
end
Then create connection like this (change API key):
Code:
1 2 3 4 5 6 7 8 9 10
ws =
require (
'user.websocket' )
json =
require (
'json' )
url =
'wss://door.casambi.com/v1/bridge/'
client ,
err =
ws.client (
'sync' ,
10 )
client.protocol =
'API-key'
res ,
err =
client :
connect (
url )
log (
res ,
err )
Posts: 1
Threads: 0
Joined: Mar 2020
Reputation:
0
(11.05.2020, 06:52) admin Wrote: You need to modify the upgrade_request function in websocket library:
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
local upgrade_request =
function (
req ,
key )
local format =
string.format
local lines = {
format (
'GET %s HTTP/1.1' ,
req.path or '' ),
format (
'Host: %s' ,
req.host ),
'Upgrade: websocket' ,
'Connection: Upgrade' ,
format (
'Sec-WebSocket-Key: %s' ,
key ),
'Sec-WebSocket-Version: 13' ,
}
if self.protocol then
tinsert (
lines ,
format (
'Sec-WebSocket-Protocol: %s' ,
self.protocol ))
end
if req.port and req.port ~=
80 then
lines [
2 ] =
format (
'Host: %s:%d' ,
req.host ,
req.port )
end
if req.userinfo then
local auth =
format (
'Authorization: Basic %s' ,
base64enc (
req.userinfo ))
tinsert (
lines ,
auth )
end
tinsert (
lines ,
'\r\n' )
return tconcat (
lines ,
'\r\n' )
end
Then create connection like this (change API key):
Code:
1 2 3 4 5 6 7 8 9 10
ws =
require (
'user.websocket' )
json =
require (
'json' )
url =
'wss://door.casambi.com/v1/bridge/'
client ,
err =
ws.client (
'sync' ,
10 )
client.protocol =
'API-key'
res ,
err =
client :
connect (
url )
log (
res ,
err )
After making the changes in library, it throws the following error:-
User library websocket:292: attempt to index global 'self' (a nil value)
stack traceback:
User library websocket:292: in function 'upgrade_request'
User library websocket:462: in function 'connect'
User script:8: in main chunk
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
Try this:
Code:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
local bit =
require (
'bit' )
local ssl =
require (
'ssl' )
local socket =
require (
'socket' )
local encdec =
require (
'encdec' )
local parse_url =
require (
'socket.url' ).
parse
local bxor =
bit.bxor
local bor =
bit.bor
local band =
bit.band
local lshift =
bit.lshift
local rshift =
bit.rshift
local ssub =
string.sub
local sbyte =
string.byte
local schar =
string.char
local tinsert =
table.insert
local tconcat =
table.concat
local mmin =
math.min
local mfloor =
math.floor
local mrandom =
math.random
local base64enc =
encdec.base64enc
local sha1 =
encdec.sha1
local unpack =
unpack
local CONTINUATION =
0
local TEXT =
1
local BINARY =
2
local CLOSE =
8
local PING =
9
local PONG =
10
local guid =
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
local read_n_bytes =
function (
str ,
pos ,
n )
pos =
pos or 1
return pos +
n ,
string.byte (
str ,
pos ,
pos +
n -
1 )
end
local read_int8 =
function (
str ,
pos )
return read_n_bytes (
str ,
pos ,
1 )
end
local read_int16 =
function (
str ,
pos )
local new_pos ,
a ,
b =
read_n_bytes (
str ,
pos ,
2 )
return new_pos ,
lshift (
a ,
8 ) +
b
end
local read_int32 =
function (
str ,
pos )
local new_pos ,
a ,
b ,
c ,
d =
read_n_bytes (
str ,
pos ,
4 )
return new_pos ,
lshift (
a ,
24 ) +
lshift (
b ,
16 ) +
lshift (
c ,
8 ) +
d
end
local write_int8 =
schar
local write_int16 =
function (
v )
return schar (
rshift (
v ,
8 ),
band (
v ,
0xFF ))
end
local write_int32 =
function (
v )
return schar (
band (
rshift (
v ,
24 ),
0xFF ),
band (
rshift (
v ,
16 ),
0xFF ),
band (
rshift (
v ,
8 ),
0xFF ),
band (
v ,
0xFF )
)
end
local generate_key =
function ()
math.randomseed (
os.time ())
local r1 =
mrandom (
0 ,
0xfffffff )
local r2 =
mrandom (
0 ,
0xfffffff )
local r3 =
mrandom (
0 ,
0xfffffff )
local r4 =
mrandom (
0 ,
0xfffffff )
local key =
write_int32 (
r1 )..
write_int32 (
r2 )..
write_int32 (
r3 )..
write_int32 (
r4 )
return base64enc (
key )
end
local bits =
function (...)
local n =
0
for _ ,
bitn in pairs {...}
do
n =
n +
2 ^
bitn
end
return n
end
local bit_7 =
bits (
7 )
local bit_0_3 =
bits (
0 ,
1 ,
2 ,
3 )
local bit_0_6 =
bits (
0 ,
1 ,
2 ,
3 ,
4 ,
5 ,
6 )
local xor_mask =
function (
encoded ,
mask ,
payload )
local transformed ,
transformed_arr = {},{}
for p =
1 ,
payload ,
2000 do
local last =
mmin (
p +
1999 ,
payload )
local original = {
sbyte (
encoded ,
p ,
last )}
for i =
1 ,#
original do
local j = (
i-1 ) %
4 +
1
transformed [
i ] =
bxor (
original [
i ],
mask [
j ])
end
local xored =
schar (
unpack (
transformed ,
1 ,#
original ))
tinsert (
transformed_arr ,
xored )
end
return tconcat (
transformed_arr )
end
local encode_header_small =
function (
header ,
payload )
return schar (
header ,
payload )
end
local encode_header_medium =
function (
header ,
payload ,
len )
return schar (
header ,
payload ,
band (
rshift (
len ,
8 ),
0xFF ),
band (
len ,
0xFF ))
end
local encode_header_big =
function (
header ,
payload ,
high ,
low )
return schar (
header ,
payload )..
write_int32 (
high )..
write_int32 (
low )
end
local encode =
function (
data ,
opcode ,
masked ,
fin )
local header =
opcode or 1
if fin ==
nil or fin ==
true then
header =
bor (
header ,
bit_7 )
end
local payload =
0
if masked then
payload =
bor (
payload ,
bit_7 )
end
local len = #
data
local chunks = {}
if len <
126 then
payload =
bor (
payload ,
len )
tinsert (
chunks ,
encode_header_small (
header ,
payload ))
elseif len <=
0xffff then
payload =
bor (
payload ,
126 )
tinsert (
chunks ,
encode_header_medium (
header ,
payload ,
len ))
elseif len <
2 ^
53 then
local high =
mfloor (
len /
2 ^
32 )
local low =
len -
high *
2 ^
32
payload =
bor (
payload ,
127 )
tinsert (
chunks ,
encode_header_big (
header ,
payload ,
high ,
low ))
end
if not masked then
tinsert (
chunks ,
data )
else
local m1 =
mrandom (
0 ,
0xff )
local m2 =
mrandom (
0 ,
0xff )
local m3 =
mrandom (
0 ,
0xff )
local m4 =
mrandom (
0 ,
0xff )
local mask = {
m1 ,
m2 ,
m3 ,
m4 }
tinsert (
chunks ,
write_int8 (
m1 ,
m2 ,
m3 ,
m4 ))
tinsert (
chunks ,
xor_mask (
data ,
mask ,#
data ))
end
return tconcat (
chunks )
end
local decode =
function (
encoded )
local encoded_bak =
encoded
if #
encoded <
2 then
return nil ,
2 -#
encoded
end
local pos ,
header ,
payload
pos ,
header =
read_int8 (
encoded ,
1 )
pos ,
payload =
read_int8 (
encoded ,
pos )
local high ,
low
encoded =
ssub (
encoded ,
pos )
local bytes =
2
local fin =
band (
header ,
bit_7 ) >
0
local opcode =
band (
header ,
bit_0_3 )
local mask =
band (
payload ,
bit_7 ) >
0
payload =
band (
payload ,
bit_0_6 )
if payload >
125 then
if payload ==
126 then
if #
encoded <
2 then
return nil ,
2 -#
encoded
end
pos ,
payload =
read_int16 (
encoded ,
1 )
elseif payload ==
127 then
if #
encoded <
8 then
return nil ,
8 -#
encoded
end
pos ,
high =
read_int32 (
encoded ,
1 )
pos ,
low =
read_int32 (
encoded ,
pos )
payload =
high *
2 ^
32 +
low
if payload <
0xffff or payload >
2 ^
53 then
assert (
false ,
'INVALID PAYLOAD ' ..
payload )
end
else
assert (
false ,
'INVALID PAYLOAD ' ..
payload )
end
encoded =
ssub (
encoded ,
pos )
bytes =
bytes +
pos -
1
end
local decoded
if mask then
local bytes_short =
payload +
4 - #
encoded
if bytes_short >
0 then
return nil ,
bytes_short
end
local m1 ,
m2 ,
m3 ,
m4
pos ,
m1 =
read_int8 (
encoded ,
1 )
pos ,
m2 =
read_int8 (
encoded ,
pos )
pos ,
m3 =
read_int8 (
encoded ,
pos )
pos ,
m4 =
read_int8 (
encoded ,
pos )
encoded =
ssub (
encoded ,
pos )
local mask = {
m1 ,
m2 ,
m3 ,
m4
}
decoded =
xor_mask (
encoded ,
mask ,
payload )
bytes =
bytes +
4 +
payload
else
local bytes_short =
payload - #
encoded
if bytes_short >
0 then
return nil ,
bytes_short
end
if #
encoded >
payload then
decoded =
ssub (
encoded ,
1 ,
payload )
else
decoded =
encoded
end
bytes =
bytes +
payload
end
return decoded ,
fin ,
opcode ,
encoded_bak :
sub (
bytes +
1 ),
mask
end
local encode_close =
function (
code ,
reason )
if code then
local data =
write_int16 (
code )
if reason then
data =
data..tostring (
reason )
end
return data
end
return ''
end
local decode_close =
function (
data )
local _ ,
code ,
reason
if data then
if #
data >
1 then
_ ,
code =
read_int16 (
data ,
1 )
end
if #
data >
2 then
reason =
data :
sub (
3 )
end
end
return code ,
reason
end
local sec_websocket_accept =
function (
sec_websocket_key )
local enc =
sha1 (
sec_websocket_key..guid ,
true )
return base64enc (
enc )
end
local http_headers =
function (
request )
local headers = {}
if not request :
match (
'.*HTTP/1%.1' )
then
return headers
end
request =
request :
match (
'[^\r\n]+\r\n(.*)' )
for line in request :
gmatch (
'[^\r\n]*\r\n' )
do
local name ,
val =
line :
match (
'([^%s]+)%s*:%s*([^\r\n]+)' )
if name and val then
name =
name :
lower ()
if not name :
match (
'sec%-websocket' )
then
val =
val :
lower ()
end
if not headers [
name ]
then
headers [
name ] =
val
else
headers [
name ] =
headers [
name ]..
',' ..
val
end
elseif line ~=
'\r\n' then
assert (
false ,
line.. '(' ..#
line.. ')' )
end
end
return headers ,
request :
match (
'\r\n\r\n(.*)' )
end
local upgrade_request =
function (
req ,
key ,
protocol )
local format =
string.format
local lines = {
format (
'GET %s HTTP/1.1' ,
req.path or '' ),
format (
'Host: %s' ,
req.host ),
'Upgrade: websocket' ,
'Connection: Upgrade' ,
format (
'Sec-WebSocket-Key: %s' ,
key ),
'Sec-WebSocket-Version: 13' ,
}
if protocol then
tinsert (
lines ,
format (
'Sec-WebSocket-Protocol: %s' ,
protocol ))
end
if req.port and req.port ~=
80 then
lines [
2 ] =
format (
'Host: %s:%d' ,
req.host ,
req.port )
end
if req.userinfo then
local auth =
format (
'Authorization: Basic %s' ,
base64enc (
req.userinfo ))
tinsert (
lines ,
auth )
end
tinsert (
lines ,
'\r\n' )
return tconcat (
lines ,
'\r\n' )
end
local receive =
function (
self )
if self.state ~=
'OPEN' and not self.is_closing then
return nil ,
nil ,
false ,
1006 ,
'wrong state'
end
local first_opcode
local frames
local bytes =
3
local encoded =
''
local clean =
function (
was_clean ,
code ,
reason )
self.state =
'CLOSED'
self :
sock_close ()
if self.on_close then
self :
on_close ()
end
return nil ,
nil ,
was_clean ,
code ,
reason or 'closed'
end
while true do
local chunk ,
err =
self :
sock_receive (
bytes )
if err then
if err ==
'timeout' then
return nil ,
nil ,
false ,
1006 ,
err
else
return clean (
false ,
1006 ,
err )
end
end
encoded =
encoded..chunk
local decoded ,
fin ,
opcode ,
_ ,
masked =
decode (
encoded )
if masked then
return clean (
false ,
1006 ,
'Websocket receive failed: frame was not masked' )
end
if decoded then
if opcode ==
CLOSE then
if not self.is_closing then
local code ,
reason =
decode_close (
decoded )
local msg =
encode_close (
code )
local encoded =
encode (
msg ,
CLOSE ,
true )
local n ,
err =
self :
sock_send (
encoded )
if n == #
encoded then
return clean (
true ,
code ,
reason )
else
return clean (
false ,
code ,
err )
end
else
return decoded ,
opcode
end
end
if not first_opcode then
first_opcode =
opcode
end
if not fin then
if not frames then
frames = {}
elseif opcode ~=
CONTINUATION then
return clean (
false ,
1002 ,
'protocol error' )
end
bytes =
3
encoded =
''
tinsert (
frames ,
decoded )
elseif not frames then
return decoded ,
first_opcode
else
tinsert (
frames ,
decoded )
return tconcat (
frames ),
first_opcode
end
else
assert (
type (
fin ) ==
'number' and fin >
0 )
bytes =
fin
end
end
end
local send =
function (
self ,
data ,
opcode )
if self.state ~=
'OPEN' then
return nil ,
false ,
1006 ,
'wrong state'
end
local encoded =
encode (
data ,
opcode or TEXT ,
true )
local n ,
err =
self :
sock_send (
encoded )
if n ~= #
encoded then
return nil ,
self :
close (
1006 ,
err )
end
return true
end
local close =
function (
self ,
code ,
reason )
if self.state ~=
'OPEN' then
return false ,
1006 ,
'wrong state'
end
if self.state ==
'CLOSED' then
return false ,
1006 ,
'wrong state'
end
local msg =
encode_close (
code or 1000 ,
reason )
local encoded =
encode (
msg ,
CLOSE ,
true )
local n ,
err =
self :
sock_send (
encoded )
local was_clean =
false
code =
1005
reason =
''
if n == #
encoded then
self.is_closing =
true
local rmsg ,
opcode =
self :
receive ()
if rmsg and opcode ==
CLOSE then
code ,
reason =
decode_close (
rmsg )
was_clean =
true
end
else
reason =
err
end
self :
sock_close ()
if self.on_close then
self :
on_close ()
end
self.state =
'CLOSED'
return was_clean ,
code ,
reason or ''
end
local DEFAULT_PORTS = {
ws =
80 ,
wss =
443 }
local connect =
function (
self ,
ws_url ,
ssl_params )
if self.state ~=
'CLOSED' then
return nil ,
'wrong state' ,
nil
end
local parsed =
parse_url (
ws_url )
if parsed.scheme ~=
'wss' and parsed.scheme ~=
'ws' then
return nil ,
'bad protocol'
end
if not parsed.port then
parsed.port =
DEFAULT_PORTS [
parsed.scheme ]
end
local _ ,
err =
self :
sock_connect (
parsed.host ,
parsed.port )
if err then
return nil ,
err ,
nil
end
if parsed.scheme ==
'wss' then
if type (
ssl_params ) ~=
'table' then
ssl_params = {
protocol =
'tlsv1' ,
options = {
'all' ,
'no_sslv2' ,
'no_sslv3' },
verify =
'none' ,
}
end
ssl_params.mode =
'client'
self.sock =
ssl.wrap (
self.sock ,
ssl_params )
self.sock :
dohandshake ()
elseif parsed.scheme ~=
'ws' then
return nil ,
'bad protocol'
end
local key =
generate_key ()
local req =
upgrade_request (
parsed ,
key ,
self.protocol )
local n ,
err =
self :
sock_send (
req )
if n ~= #
req then
return nil ,
err ,
nil
end
local resp = {}
repeat
local line ,
err =
self :
sock_receive (
'*l' )
resp [#
resp +
1 ] =
line
if err then
return nil ,
err ,
nil
end
until line ==
''
local response =
tconcat (
resp ,
'\r\n' )
local headers =
http_headers (
response )
local expected_accept =
sec_websocket_accept (
key )
if headers [
'sec-websocket-accept' ] ~=
expected_accept then
local msg =
'Websocket Handshake failed: Invalid Sec-Websocket-Accept (expected %s got %s)'
return nil ,
msg :
format (
expected_accept ,
headers [
'sec-websocket-accept' ]
or 'nil' ),
headers
end
self.state =
'OPEN'
return true ,
headers [
'sec-websocket-protocol' ],
headers
end
local extend =
function (
obj )
obj.state =
'CLOSED'
obj.receive =
receive
obj.send =
send
obj.close =
close
obj.connect =
connect
return obj
end
local client_copas =
function (
timeout )
local copas =
require (
'copas' )
local self = {}
self.sock_connect =
function (
self ,
host ,
port )
self.sock =
socket.tcp ()
self.sock :
settimeout (
timeout or 5 )
local _ ,
err =
copas.connect (
self.sock ,
host ,
port )
if err and err ~=
'already connected' then
self.sock :
close ()
return nil ,
err
end
end
self.sock_send =
function (
self ,...)
return copas.send (
self.sock ,...)
end
self.sock_receive =
function (
self ,...)
return copas.receive (
self.sock ,...)
end
self.sock_close =
function (
self )
self.sock :
close ()
end
self =
extend (
self )
return self
end
local client_sync =
function (
timeout )
local self = {}
self.sock_connect =
function (
self ,
host ,
port )
self.sock =
socket.tcp ()
self.sock :
settimeout (
timeout or 5 )
local _ ,
err =
self.sock :
connect (
host ,
port )
if err then
self.sock :
close ()
return nil ,
err
end
end
self.sock_send =
function (
self ,...)
return self.sock :
send (...)
end
self.sock_receive =
function (
self ,...)
return self.sock :
receive (...)
end
self.sock_close =
function (
self )
self.sock :
close ()
end
self =
extend (
self )
return self
end
local client =
function (
mode ,
timeout )
if mode ==
'copas' then
return client_copas (
timeout )
else
return client_sync (
timeout )
end
end
return {
client =
client ,
CONTINUATION =
CONTINUATION ,
TEXT =
TEXT ,
BINARY =
BINARY ,
CLOSE =
CLOSE ,
PING =
PING ,
PONG =
PONG
}
Posts: 74
Threads: 25
Joined: Mar 2021
Reputation:
0
(20.03.2018, 14:27) admin Wrote: Attached is websocket client library ported from https://github.com/lipp/lua-websockets/
Main changes:
1. Support for basic auth in URL
2. Receive timeout does not close the websocket connection
3. Server support is removed
Add it as a user library named websocket
Minimal example (resident script):
Code:
1 2 3 4 5 6 7 8 9
if not ws then
ws =
require (
'user.websocket' )
url =
'ws://admin:admin@192.168.1.12/apps/localbus.lp'
client =
ws.client (
'sync' ,
10 )
client :
connect (
url )
end
log (
client :
receive ())
Hi Admin,
Can I ask what the websocket.lua is meant for? What do you do with that?
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
This is a client library for connecting to a websocket server.
Posts: 45
Threads: 4
Joined: Jan 2020
Reputation:
4
refreshing old thread as i'm having troubles implementing websocket client. The problem is that client does not timeout – i.e. it runs indefinitely until it received data from the socket, and also it does not timeout if socket disconnects (for ex. when server this client connects to goes offline).
i've traced it down to websockets library, specifically to copas.receive() call which hangs there indefinitely. Is there a way to make it work? It seems this goes into the library itself, which is built-in into LM and doesn't seem to be up to date.
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
You can add newer copas library as a user library. It requires some extra Lua libraries which can added the same way. Just make sure to all require calls reference 'user.libname' instead of 'libname'.
Built-in copas in LM won't be upgraded to keep backward compatibility with existing scripts that rely on a certain behavior.