├── ludent.sh ├── test-server ├── favicon.ico ├── test-server-ev.lua ├── test-server-copas.lua ├── test-ws.js └── index.html ├── .gitignore ├── src ├── websocket │ ├── server.lua │ ├── bit.lua │ ├── client.lua │ ├── client_sync.lua │ ├── client_copas.lua │ ├── handshake.lua │ ├── server_copas.lua │ ├── ev_common.lua │ ├── tools.lua │ ├── sync.lua │ ├── frame.lua │ ├── client_ev.lua │ └── server_ev.lua └── websocket.lua ├── minify.sh ├── test.sh ├── echows.js ├── perf └── encode_perf.lua ├── examples ├── echo-client-ev.lua ├── echo-server-ev.lua └── echo-server-copas.lua ├── squishy ├── publish ├── COPYRIGHT ├── .luacov ├── lua-websockets.rockspec ├── rockspecs └── lua-websockets-scm-1.rockspec ├── .travis.yml ├── Dockerfile ├── spec ├── ev_common_spec.lua ├── tools_spec.lua ├── handshake_spec.lua ├── client_spec.lua ├── frame_spec.lua ├── client_ev_spec.lua ├── server_ev_spec.lua └── server_copas_spec.lua ├── README.md └── API.md /ludent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ludent $(find . -name "*.lua") 3 | -------------------------------------------------------------------------------- /test-server/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lipp/lua-websockets/HEAD/test-server/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.*~ 3 | *#*# 4 | *#* 5 | node_modules/* 6 | 7 | 8 | 9 | luacov.report.out 10 | 11 | *.swp 12 | -------------------------------------------------------------------------------- /src/websocket/server.lua: -------------------------------------------------------------------------------- 1 | return setmetatable({},{__index = function(self, name) 2 | local backend = require("websocket.server_" .. name) 3 | self[name] = backend 4 | return backend 5 | end}) 6 | -------------------------------------------------------------------------------- /minify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | mkdir min 3 | mkdir min/src 4 | mkdir min/src/websocket 5 | for i in `find src -name "*.lua"` 6 | do 7 | echo "minifying" $i 8 | luamin -f {$i} > min/$i 9 | done 10 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | killall node 2>/dev/null 3 | npm install ws 4 | node echows.js ${LUAWS_WSTEST_PORT:=11000} & 5 | pid=$! 6 | echo "Waiting for wstest to start..." 7 | sleep 5 8 | busted -c spec/ 9 | bustedcode=$? 10 | kill ${pid} 11 | exit $bustedcode 12 | -------------------------------------------------------------------------------- /src/websocket/bit.lua: -------------------------------------------------------------------------------- 1 | local has_bit32,bit = pcall(require,'bit32') 2 | if has_bit32 then 3 | -- lua 5.2 / bit32 library 4 | bit.rol = bit.lrotate 5 | bit.ror = bit.rrotate 6 | return bit 7 | else 8 | -- luajit / lua 5.1 + luabitop 9 | return require'bit' 10 | end 11 | -------------------------------------------------------------------------------- /echows.js: -------------------------------------------------------------------------------- 1 | var WebSocketServer = require('ws').Server; 2 | var wss = new WebSocketServer({ port: parseInt(process.argv[2]) }); 3 | 4 | wss.on('connection', function connection(ws) { 5 | ws.on('message', function incoming(message, flags) { 6 | ws.send(message, flags); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/websocket/client.lua: -------------------------------------------------------------------------------- 1 | return setmetatable({},{__index = function(self, name) 2 | if name == 'new' then name = 'sync' end 3 | local backend = require("websocket.client_" .. name) 4 | self[name] = backend 5 | if name == 'sync' then self.new = backend end 6 | return backend 7 | end}) 8 | -------------------------------------------------------------------------------- /src/websocket.lua: -------------------------------------------------------------------------------- 1 | local frame = require'websocket.frame' 2 | 3 | return { 4 | client = require'websocket.client', 5 | server = require'websocket.server', 6 | CONTINUATION = frame.CONTINUATION, 7 | TEXT = frame.TEXT, 8 | BINARY = frame.BINARY, 9 | CLOSE = frame.CLOSE, 10 | PING = frame.PING, 11 | PONG = frame.PONG 12 | } 13 | -------------------------------------------------------------------------------- /perf/encode_perf.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | local frame = require'websocket.frame' 3 | local socket = require'socket' 4 | local encode = frame.encode 5 | local TEXT = frame.TEXT 6 | local s = string.rep('abc',100) 7 | 8 | local tests = { 9 | ['---WITH XOR---'] = true, 10 | ['---WITHOUT XOR---'] = false 11 | } 12 | 13 | for name,do_xor in pairs(tests) do 14 | print(name) 15 | local n = 1000000 16 | local t1 = socket.gettime() 17 | for i=1,n do 18 | encode(s,TEXT,do_xor) 19 | end 20 | local dt = socket.gettime() - t1 21 | print('n=',n) 22 | print('dt=',dt) 23 | print('ops/sec=',n/dt) 24 | print('microsec/op=',1000000*dt/n) 25 | end 26 | -------------------------------------------------------------------------------- /examples/echo-client-ev.lua: -------------------------------------------------------------------------------- 1 | -- connects to a echo websocket server running a localhost:8080 2 | -- sends a strong every second and prints the echoed messages 3 | -- to stdout 4 | 5 | local ev = require'ev' 6 | local ws_client = require('websocket.client').ev() 7 | 8 | ws_client:on_open(function() 9 | print('connected') 10 | end) 11 | 12 | ws_client:connect('ws://echo.websocket.org','echo') 13 | 14 | ws_client:on_message(function(ws, msg) 15 | print('received',msg) 16 | end) 17 | 18 | local i = 0 19 | 20 | ev.Timer.new(function() 21 | i = i + 1 22 | ws_client:send('hello '..i) 23 | end,1,1):start(ev.Loop.default) 24 | 25 | ev.Loop.default:loop() 26 | -------------------------------------------------------------------------------- /examples/echo-server-ev.lua: -------------------------------------------------------------------------------- 1 | local ev = require'ev' 2 | 3 | -- this callback is called, whenever a new client connects. 4 | -- ws is a new websocket instance 5 | local echo_handler = function(ws) 6 | ws:on_message(function(ws,message) 7 | ws:send(message) 8 | end) 9 | end 10 | 11 | -- create a copas webserver and start listening 12 | local server = require'websocket'.server.ev.listen 13 | { 14 | -- listen on port 8080 15 | port = 8080, 16 | -- the protocols field holds 17 | -- key: protocol name 18 | -- value: callback on new connection 19 | protocols = { 20 | echo = echo_handler 21 | }, 22 | default = echo_handler 23 | } 24 | 25 | -- use the lua-ev loop 26 | ev.Loop.default:loop() 27 | -------------------------------------------------------------------------------- /squishy: -------------------------------------------------------------------------------- 1 | Module "websocket.sync" "src/websocket/sync.lua" 2 | Module "websocket.client" "src/websocket/client.lua" 3 | Module "websocket.client_copas" "src/websocket/client_copas.lua" 4 | Module "websocket.server" "src/websocket/server.lua" 5 | Module "websocket.server_copas" "src/websocket/server_copas.lua" 6 | Module "websocket.handshake" "src/websocket/handshake.lua" 7 | Module "websocket.tools" "src/websocket/tools.lua" 8 | Module "websocket.frame" "src/websocket/frame.lua" 9 | Module "websocket.bit" "src/websocket/bit.lua" 10 | Module "websocket.client_ev" "src/websocket/client_ev.lua" 11 | Module "websocket.ev_common" "src/websocket/ev_common.lua" 12 | Module "websocket.server_ev" "src/websocket/server_ev.lua" 13 | 14 | Main "src/websocket.lua" 15 | Output "websocket.lua" 16 | -------------------------------------------------------------------------------- /examples/echo-server-copas.lua: -------------------------------------------------------------------------------- 1 | local copas = require'copas' 2 | 3 | -- this callback is called, whenever a new client connects. 4 | -- ws is a new websocket instance 5 | local echo_handler = function(ws) 6 | while true do 7 | local message = ws:receive() 8 | if message then 9 | ws:send(message) 10 | else 11 | ws:close() 12 | return 13 | end 14 | end 15 | end 16 | 17 | -- create a copas webserver and start listening 18 | local server = require'websocket'.server.copas.listen 19 | { 20 | -- listen on port 8080 21 | port = 8080, 22 | -- the protocols field holds 23 | -- key: protocol name 24 | -- value: callback on new connection 25 | protocols = { 26 | -- this callback is called, whenever a new client connects. 27 | -- ws is a new websocket instance 28 | echo = echo_handler 29 | }, 30 | default = echo_handler 31 | } 32 | 33 | -- use the copas loop 34 | copas.loop() 35 | -------------------------------------------------------------------------------- /src/websocket/client_sync.lua: -------------------------------------------------------------------------------- 1 | local socket = require'socket' 2 | local sync = require'websocket.sync' 3 | local tools = require'websocket.tools' 4 | 5 | local new = function(ws) 6 | ws = ws or {} 7 | local self = {} 8 | 9 | self.sock_connect = function(self,host,port) 10 | self.sock = socket.tcp() 11 | if ws.timeout ~= nil then 12 | self.sock:settimeout(ws.timeout) 13 | end 14 | local _,err = self.sock:connect(host,port) 15 | if err then 16 | self.sock:close() 17 | return nil,err 18 | end 19 | end 20 | 21 | self.sock_send = function(self,...) 22 | return self.sock:send(...) 23 | end 24 | 25 | self.sock_receive = function(self,...) 26 | return self.sock:receive(...) 27 | end 28 | 29 | self.sock_close = function(self) 30 | --self.sock:shutdown() Causes errors? 31 | self.sock:close() 32 | end 33 | 34 | self = sync.extend(self) 35 | return self 36 | end 37 | 38 | return new 39 | -------------------------------------------------------------------------------- /publish: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # TODO: 4 | # 5 | # * Make it generate a new version number. 6 | # * Set a tag in git. 7 | # * Generate a rockspec. 8 | # * Upload the tarball to github. 9 | # * Announce 10 | 11 | version=$(git tag -l | tail -1) 12 | 13 | name="lua-websockets-$version" 14 | 15 | tmp="$TEMPDIR" 16 | if [ -z "$tmp" ]; then 17 | tmp="$HOME" 18 | fi 19 | 20 | src="$(cd "$(dirname $0)" && pwd)" 21 | 22 | cd $tmp 23 | rm -f "$name" 24 | ln -sf "$src" "$name" 25 | 26 | echo "Creating $tmp/$name.tar.gz" 27 | tar -czvpf "$name.tar.gz" \ 28 | --dereference \ 29 | --exclude "$name/.git*" \ 30 | --exclude "$name/*.o" \ 31 | --exclude "$name/*.so" \ 32 | --exclude "$name/lua-websockets.rockspec" \ 33 | --exclude "$name/rockspecs" \ 34 | --exclude "$name/$(basename $0)" \ 35 | "$name" 36 | echo "Creating $tmp/$name-1.rockspec" 37 | cat "$src/lua-websockets.rockspec" | \ 38 | sed s/@VERSION@/$version/ > \ 39 | "$name-1.rockspec" 40 | -------------------------------------------------------------------------------- /src/websocket/client_copas.lua: -------------------------------------------------------------------------------- 1 | local socket = require'socket' 2 | local sync = require'websocket.sync' 3 | local tools = require'websocket.tools' 4 | 5 | local new = function(ws) 6 | ws = ws or {} 7 | local copas = require'copas' 8 | 9 | local self = {} 10 | 11 | self.sock_connect = function(self,host,port) 12 | self.sock = socket.tcp() 13 | if ws.timeout ~= nil then 14 | self.sock:settimeout(ws.timeout) 15 | end 16 | local _,err = copas.connect(self.sock,host,port) 17 | if err and err ~= 'already connected' then 18 | self.sock:close() 19 | return nil,err 20 | end 21 | end 22 | 23 | self.sock_send = function(self,...) 24 | return copas.send(self.sock,...) 25 | end 26 | 27 | self.sock_receive = function(self,...) 28 | return copas.receive(self.sock,...) 29 | end 30 | 31 | self.sock_close = function(self) 32 | self.sock:shutdown() 33 | self.sock:close() 34 | end 35 | 36 | self = sync.extend(self) 37 | return self 38 | end 39 | 40 | return new 41 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 by Gerhard Lipp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | --- Global configuration file. Copy, customize and store in your 2 | -- project folder as '.luacov' for project specific configuration 3 | -- @class module 4 | -- @name luacov.defaults 5 | return { 6 | 7 | -- default filename to load for config options if not provided 8 | -- only has effect in 'luacov.defaults.lua' 9 | ["configfile"] = ".luacov", 10 | 11 | -- filename to store stats collected 12 | ["statsfile"] = "lcov.stats.out", 13 | 14 | -- filename to store report 15 | ["reportfile"] = "lcov.report.out", 16 | 17 | -- Run reporter on completion? (won't work for ticks) 18 | runreport = true, 19 | 20 | -- Delete stats file after reporting? 21 | deletestats = false, 22 | 23 | -- Patterns for files to include when reporting 24 | -- all will be included if nothing is listed 25 | -- (exclude overrules include, do not include 26 | -- the .lua extension) 27 | ["include"] = { 28 | "websocket%.*" 29 | }, 30 | 31 | -- Patterns for files to exclude when reporting 32 | -- all will be included if nothing is listed 33 | -- (exclude overrules include, do not include 34 | -- the .lua extension) 35 | ["exclude"] = { 36 | }, 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /test-server/test-server-ev.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --- lua websocket equivalent to test-server.c from libwebsockets. 3 | -- using lua-ev event loop 4 | package.path = '../src/?.lua;../src/?/?.lua;'..package.path 5 | local ev = require'ev' 6 | local loop = ev.Loop.default 7 | local websocket = require'websocket' 8 | local server = websocket.server.ev.listen 9 | { 10 | protocols = { 11 | ['lws-mirror-protocol'] = function(ws) 12 | ws:on_message( 13 | function(ws,data,opcode) 14 | if opcode == websocket.TEXT then 15 | ws:broadcast(data) 16 | end 17 | end) 18 | end, 19 | ['dumb-increment-protocol'] = function(ws) 20 | local number = 0 21 | local timer = ev.Timer.new( 22 | function() 23 | ws:send(tostring(number)) 24 | number = number + 1 25 | end,0.1,0.1) 26 | timer:start(loop) 27 | ws:on_message( 28 | function(ws,message,opcode) 29 | if opcode == websocket.TEXT then 30 | if message:match('reset') then 31 | number = 0 32 | end 33 | end 34 | end) 35 | ws:on_close( 36 | function() 37 | timer:stop(loop) 38 | end) 39 | end 40 | }, 41 | port = 12345 42 | } 43 | 44 | print('Open browser:') 45 | print('file://'..io.popen('pwd'):read()..'/index.html') 46 | loop:loop() 47 | 48 | -------------------------------------------------------------------------------- /lua-websockets.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-websockets" 2 | version = "@VERSION@-1" 3 | 4 | source = { 5 | url = "git://github.com/lipp/lua-websockets.git", 6 | tag = "@VERSION@" 7 | } 8 | 9 | description = { 10 | summary = "Websockets for Lua", 11 | homepage = "http://github.com/lipp/lua-websockets", 12 | license = "MIT/X11", 13 | detailed = "Provides sync and async clients and servers for copas and lua-ev." 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | "luasocket", 19 | "luabitop", 20 | "copas", 21 | "luasec" 22 | } 23 | 24 | build = { 25 | type = 'none', 26 | install = { 27 | lua = { 28 | ['websocket'] = 'src/websocket.lua', 29 | ['websocket.sync'] = 'src/websocket/sync.lua', 30 | ['websocket.client'] = 'src/websocket/client.lua', 31 | ['websocket.client_sync'] = 'src/websocket/client_sync.lua', 32 | ['websocket.client_copas'] = 'src/websocket/client_copas.lua', 33 | ['websocket.server'] = 'src/websocket/server.lua', 34 | ['websocket.server_copas'] = 'src/websocket/server_copas.lua', 35 | ['websocket.handshake'] = 'src/websocket/handshake.lua', 36 | ['websocket.tools'] = 'src/websocket/tools.lua', 37 | ['websocket.frame'] = 'src/websocket/frame.lua', 38 | ['websocket.bit'] = 'src/websocket/bit.lua', 39 | ['websocket.client_ev'] = 'src/websocket/client_ev.lua', 40 | ['websocket.ev_common'] = 'src/websocket/ev_common.lua', 41 | ['websocket.server_ev'] = 'src/websocket/server_ev.lua', 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rockspecs/lua-websockets-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-websockets" 2 | version = "scm-1" 3 | 4 | source = { 5 | url = "git://github.com/lipp/lua-websockets.git", 6 | } 7 | 8 | description = { 9 | summary = "Websockets for Lua", 10 | homepage = "http://github.com/lipp/lua-websockets", 11 | license = "MIT/X11", 12 | detailed = "Provides sync and async clients and servers for copas and lua-ev." 13 | } 14 | 15 | dependencies = { 16 | "lua >= 5.1", 17 | "luasocket", 18 | "luabitop", 19 | "lua-ev", 20 | "copas", 21 | "luasec" 22 | } 23 | 24 | build = { 25 | type = 'none', 26 | install = { 27 | lua = { 28 | ['websocket'] = 'src/websocket.lua', 29 | ['websocket.sync'] = 'src/websocket/sync.lua', 30 | ['websocket.client'] = 'src/websocket/client.lua', 31 | ['websocket.client_sync'] = 'src/websocket/client_sync.lua', 32 | ['websocket.client_ev'] = 'src/websocket/client_ev.lua', 33 | ['websocket.client_copas'] = 'src/websocket/client_copas.lua', 34 | ['websocket.ev_common'] = 'src/websocket/ev_common.lua', 35 | ['websocket.server'] = 'src/websocket/server.lua', 36 | ['websocket.server_ev'] = 'src/websocket/server_ev.lua', 37 | ['websocket.server_copas'] = 'src/websocket/server_copas.lua', 38 | ['websocket.handshake'] = 'src/websocket/handshake.lua', 39 | ['websocket.tools'] = 'src/websocket/tools.lua', 40 | ['websocket.frame'] = 'src/websocket/frame.lua', 41 | ['websocket.bit'] = 'src/websocket/bit.lua', 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | env: 4 | global: 5 | - LUAROCKS_VERSION=2.0.13 6 | - LUAROCKS_BASE=luarocks-$LUAROCKS_VERSION 7 | matrix: 8 | - LUA=lua5.2 LUA_DEV=liblua5.2-dev LUA_VER=5.2 LUA_SFX=5.2 LUA_INCDIR=/usr/include/lua5.2 9 | - LUA=luajit LUA_DEV=libluajit-5.1-dev LUA_VER=5.1 LUA_SFX=jit LUA_INCDIR=/usr/include/luajit-2.0 10 | 11 | before_install: 12 | - if [ $LUA = "luajit" ]; then 13 | sudo add-apt-repository ppa:mwild1/ppa -y && sudo apt-get update -y; 14 | fi 15 | - sudo apt-get install $LUA 16 | - sudo apt-get install $LUA_DEV 17 | - sudo apt-get install -y libssl-dev 18 | - lua$LUA_SFX -v 19 | # Install a recent luarocks release 20 | - wget https://github.com/keplerproject/luarocks/archive/v$LUAROCKS_VERSION.tar.gz -O $LUAROCKS_BASE.tar.gz 21 | - tar zxvpf $LUAROCKS_BASE.tar.gz 22 | - cd $LUAROCKS_BASE 23 | - ./configure 24 | --lua-version=$LUA_VER --lua-suffix=$LUA_SFX --with-lua-include="$LUA_INCDIR" 25 | - sudo make 26 | - sudo make install 27 | - cd $TRAVIS_BUILD_DIR 28 | 29 | install: 30 | - sudo apt-get install libev-dev 31 | - git clone http://github.com/brimworks/lua-ev 32 | - cd lua-ev && sudo luarocks make rockspec/lua-ev-scm-1.rockspec && cd .. 33 | - sudo luarocks install luacov-coveralls 34 | - sudo luarocks install lua_cliargs 2.3-3 35 | - sudo luarocks install busted 1.10.0-1 36 | - sudo luarocks install luasec OPENSSL_LIBDIR=/usr/lib/`gcc -print-multiarch` 37 | 38 | script: "sudo luarocks make rockspecs/lua-websockets-scm-1.rockspec && ./test.sh" 39 | 40 | after_success: 41 | - luacov-coveralls 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | # install autobahn tests suite (python) 3 | RUN apt-get update -y && apt-get install build-essential libssl-dev python -y 4 | # install lua 5 | ENV LUAROCKS_VERSION=2.0.13 6 | ENV LUAROCKS_BASE=luarocks-$LUAROCKS_VERSION 7 | ENV LUA luajit 8 | ENV LUA_DEV libluajit-5.1-dev 9 | ENV LUA_VER 5.1 10 | ENV LUA_SFX jit 11 | ENV LUA_INCDIR /usr/include/luajit-2.0 12 | 13 | # - LUA=lua5.1 LUA_DEV=liblua5.1-dev LUA_VER=5.1 LUA_SFX=5.1 LUA_INCDIR=/usr/include/lua5.1 14 | # - LUA=lua5.2 LUA_DEV=liblua5.2-dev LUA_VER=5.2 LUA_SFX=5.2 LUA_INCDIR=/usr/include/lua5.2 15 | # - LUA=luajit LUA_DEV=libluajit-5.1-dev LUA_VER=5.1 LUA_SFX=jit LUA_INCDIR=/usr/include/luajit-2.0 16 | RUN apt-get install ${LUA} ${LUA_DEV} wget libev-dev git-core unzip -y 17 | RUN lua${LUA_SFX} -v 18 | WORKDIR / 19 | RUN wget --quiet https://github.com/keplerproject/luarocks/archive/v$LUAROCKS_VERSION.tar.gz -O $LUAROCKS_BASE.tar.gz 20 | RUN wget --quiet https://nodejs.org/dist/v4.4.1/node-v4.4.1-linux-x64.tar.gz 21 | RUN tar xf node-v4.4.1-linux-x64.tar.gz 22 | ENV PATH /node-v4.4.1-linux-x64/bin:$PATH 23 | RUN node --version 24 | RUN npm install -g ws 25 | RUN tar zxpf $LUAROCKS_BASE.tar.gz 26 | RUN cd $LUAROCKS_BASE && ./configure --lua-version=$LUA_VER --lua-suffix=$LUA_SFX --with-lua-include="$LUA_INCDIR" && make install && cd .. 27 | RUN luarocks --version 28 | RUN git clone http://github.com/brimworks/lua-ev && cd lua-ev && luarocks make LIBEV_LIBDIR=/usr/lib/x86_64-linux-gnu/ rockspec/lua-ev-scm-1.rockspec && cd .. 29 | RUN luarocks install LuaCov 30 | RUN luarocks install lua_cliargs 2.3-3 31 | RUN luarocks install busted 1.10.0-1 32 | ADD . /lua-websockets 33 | WORKDIR /lua-websockets 34 | RUN luarocks make rockspecs/lua-websockets-scm-1.rockspec 35 | RUN ./test.sh 36 | 37 | -------------------------------------------------------------------------------- /test-server/test-server-copas.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | --- lua websocket equivalent to test-server.c from libwebsockets. 3 | -- using copas as server framework 4 | package.path = '../src/?.lua;../src/?/?.lua;'..package.path 5 | local copas = require'copas' 6 | local socket = require'socket' 7 | 8 | print('Open browser:') 9 | print('file://'..io.popen('pwd'):read()..'/index.html') 10 | 11 | local inc_clients = {} 12 | 13 | local websocket = require'websocket' 14 | local server = websocket.server.copas.listen 15 | { 16 | protocols = { 17 | ['lws-mirror-protocol'] = function(ws) 18 | while true do 19 | local msg,opcode = ws:receive() 20 | if not msg then 21 | ws:close() 22 | return 23 | end 24 | if opcode == websocket.TEXT then 25 | ws:broadcast(msg) 26 | end 27 | end 28 | end, 29 | ['dumb-increment-protocol'] = function(ws) 30 | inc_clients[ws] = 0 31 | while true do 32 | local message,opcode = ws:receive() 33 | if not message then 34 | ws:close() 35 | inc_clients[ws] = nil 36 | return 37 | end 38 | if opcode == websocket.TEXT then 39 | if message:match('reset') then 40 | inc_clients[ws] = 0 41 | end 42 | end 43 | end 44 | end 45 | }, 46 | port = 12345 47 | } 48 | 49 | -- this fairly complex mechanism is required due to the 50 | -- lack of copas timers... 51 | -- sends periodically the 'dumb-increment-protocol' count 52 | -- to the respective client. 53 | copas.addthread( 54 | function() 55 | local last = socket.gettime() 56 | while true do 57 | copas.step(0.1) 58 | local now = socket.gettime() 59 | if (now - last) >= 0.1 then 60 | last = now 61 | for ws,number in pairs(inc_clients) do 62 | ws:send(tostring(number)) 63 | inc_clients[ws] = number + 1 64 | end 65 | end 66 | end 67 | end) 68 | 69 | copas.loop() 70 | -------------------------------------------------------------------------------- /test-server/test-ws.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var WebSocketServer = require('websocket').server; 3 | var http = require('http'); 4 | 5 | var server = http.createServer(function(request, response) { 6 | console.log((new Date()) + ' Received request for ' + request.url); 7 | response.writeHead(404); 8 | response.end(); 9 | }); 10 | server.listen(12345, function() { 11 | console.log((new Date()) + ' Server is listening on port 8080'); 12 | }); 13 | 14 | wsServer = new WebSocketServer({ 15 | httpServer: server, 16 | // You should not use autoAcceptConnections for production 17 | // applications, as it defeats all standard cross-origin protection 18 | // facilities built into the protocol and the browser. You should 19 | // *always* verify the connection's origin and decide whether or not 20 | // to accept it. 21 | autoAcceptConnections: false 22 | }); 23 | 24 | function originIsAllowed(origin) { 25 | // put logic here to detect whether the specified origin is allowed. 26 | return true; 27 | } 28 | 29 | wsServer.on('request', function(request) { 30 | if (!originIsAllowed(request.origin)) { 31 | // Make sure we only accept requests from an allowed origin 32 | request.reject(); 33 | console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.'); 34 | return; 35 | } 36 | 37 | var connection = request.accept('dumb-increment-protocol', request.origin); 38 | console.log((new Date()) + ' Connection accepted.'); 39 | var i = 0; 40 | setInterval(function(){ 41 | console.log(i); 42 | connection.sendUTF(++i); 43 | },1000); 44 | /* connection.on('message', function(message) { 45 | if (message.type === 'utf8') { 46 | console.log('Received Message: ' + message.utf8Data); 47 | connection.sendUTF(message.utf8Data); 48 | } 49 | else if (message.type === 'binary') { 50 | console.log('Received Binary Message of ' + message.binaryData.length + ' bytes'); 51 | connection.sendBytes(message.binaryData); 52 | } 53 | }); 54 | connection.on('close', function(reasonCode, description) { 55 | console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); 56 | });*/ 57 | }); 58 | -------------------------------------------------------------------------------- /src/websocket/handshake.lua: -------------------------------------------------------------------------------- 1 | local sha1 = require'websocket.tools'.sha1 2 | local base64 = require'websocket.tools'.base64 3 | local tinsert = table.insert 4 | 5 | local guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 6 | 7 | local sec_websocket_accept = function(sec_websocket_key) 8 | local a = sec_websocket_key..guid 9 | local sha1 = sha1(a) 10 | assert((#sha1 % 2) == 0) 11 | return base64.encode(sha1) 12 | end 13 | 14 | local http_headers = function(request) 15 | local headers = {} 16 | if not request:match('.*HTTP/1%.1') then 17 | return headers 18 | end 19 | request = request:match('[^\r\n]+\r\n(.*)') 20 | local empty_line 21 | for line in request:gmatch('[^\r\n]*\r\n') do 22 | local name,val = line:match('([^%s]+)%s*:%s*([^\r\n]+)') 23 | if name and val then 24 | name = name:lower() 25 | if not name:match('sec%-websocket') then 26 | val = val:lower() 27 | end 28 | if not headers[name] then 29 | headers[name] = val 30 | else 31 | headers[name] = headers[name]..','..val 32 | end 33 | elseif line == '\r\n' then 34 | empty_line = true 35 | else 36 | assert(false,line..'('..#line..')') 37 | end 38 | end 39 | return headers,request:match('\r\n\r\n(.*)') 40 | end 41 | 42 | local upgrade_request = function(req) 43 | local format = string.format 44 | local lines = { 45 | format('GET %s HTTP/1.1',req.uri or ''), 46 | format('Host: %s',req.host), 47 | 'Upgrade: websocket', 48 | 'Connection: Upgrade', 49 | format('Sec-WebSocket-Key: %s',req.key), 50 | format('Sec-WebSocket-Protocol: %s',table.concat(req.protocols,', ')), 51 | 'Sec-WebSocket-Version: 13', 52 | } 53 | if req.origin then 54 | tinsert(lines,string.format('Origin: %s',req.origin)) 55 | end 56 | if req.port and req.port ~= 80 then 57 | lines[2] = format('Host: %s:%d',req.host,req.port) 58 | end 59 | tinsert(lines,'\r\n') 60 | return table.concat(lines,'\r\n') 61 | end 62 | 63 | local accept_upgrade = function(request,protocols) 64 | local headers = http_headers(request) 65 | if headers['upgrade'] ~= 'websocket' or 66 | not headers['connection'] or 67 | not headers['connection']:match('upgrade') or 68 | headers['sec-websocket-key'] == nil or 69 | headers['sec-websocket-version'] ~= '13' then 70 | return nil,'HTTP/1.1 400 Bad Request\r\n\r\n' 71 | end 72 | local prot 73 | if headers['sec-websocket-protocol'] then 74 | for protocol in headers['sec-websocket-protocol']:gmatch('([^,%s]+)%s?,?') do 75 | for _,supported in ipairs(protocols) do 76 | if supported == protocol then 77 | prot = protocol 78 | break 79 | end 80 | end 81 | if prot then 82 | break 83 | end 84 | end 85 | end 86 | local lines = { 87 | 'HTTP/1.1 101 Switching Protocols', 88 | 'Upgrade: websocket', 89 | 'Connection: '..headers['connection'], 90 | string.format('Sec-WebSocket-Accept: %s',sec_websocket_accept(headers['sec-websocket-key'])), 91 | } 92 | if prot then 93 | tinsert(lines,string.format('Sec-WebSocket-Protocol: %s',prot)) 94 | end 95 | tinsert(lines,'\r\n') 96 | return table.concat(lines,'\r\n'),prot 97 | end 98 | 99 | return { 100 | sec_websocket_accept = sec_websocket_accept, 101 | http_headers = http_headers, 102 | accept_upgrade = accept_upgrade, 103 | upgrade_request = upgrade_request, 104 | } 105 | -------------------------------------------------------------------------------- /spec/ev_common_spec.lua: -------------------------------------------------------------------------------- 1 | 2 | local ev = require'ev' 3 | local ev_common = require'websocket.ev_common' 4 | local socket = require'socket' 5 | 6 | setloop('ev') 7 | 8 | describe('The ev_common helper module',function() 9 | 10 | local listen_io 11 | setup(function() 12 | local listener = socket.bind('*',12345) 13 | listener:settimeout(0) 14 | listen_io = ev.IO.new( 15 | function() 16 | local client_sock = listener:accept() 17 | client_sock:settimeout(0) 18 | local client_io = ev.IO.new(function(loop,io) 19 | repeat 20 | local _,err = client_sock:receive(1000) 21 | if err ~= 'timeout' then 22 | io:stop(loop) 23 | client_sock:close() 24 | end 25 | until err 26 | end,client_sock:getfd(),ev.READ) 27 | client_io:start(ev.Loop.default) 28 | end,listener:getfd(),ev.READ) 29 | listen_io:start(ev.Loop.default) 30 | end) 31 | 32 | teardown(function() 33 | listen_io:stop(ev.Loop.default) 34 | end) 35 | 36 | local send,stop 37 | local sock 38 | before_each(function(done) 39 | sock = socket.tcp() 40 | sock:settimeout(0) 41 | ev.IO.new(async(function(loop,io) 42 | send,stop = ev_common.async_send(sock) 43 | io:stop(loop) 44 | done() 45 | end),sock:getfd(),ev.WRITE):start(ev.Loop.default) 46 | sock:connect('localhost',12345) 47 | send,stop = ev_common.async_send(sock) 48 | end) 49 | 50 | after_each(function() 51 | stop() 52 | sock:close() 53 | end) 54 | 55 | local chunk1 = 'some data' 56 | local chunk2 = string.rep('some more data',10000) 57 | 58 | it('calls on_sent callback and small amount of data gets send without buffering',function(done) 59 | local on_sent = async(function(buf) 60 | assert.is_equal(buf,chunk1) 61 | done() 62 | end) 63 | 64 | local on_err = async(function(err) 65 | assert.is_nil(err or 'should not happen') 66 | end) 67 | 68 | local buffered_bytes = send(chunk1,on_sent,on_err) 69 | assert.is_equal(buffered_bytes,0) 70 | end) 71 | 72 | it('can be stopped and big amount of data gets buffered',function(done) 73 | local on_sent = async(function(buf) 74 | assert.is_nil(err or 'should not happen') 75 | end) 76 | 77 | local on_err = async(function(err) 78 | assert.is_nil(err or 'should not happen') 79 | end) 80 | 81 | local data = string.rep('foo',3000000) 82 | local buffered_bytes = send(data,on_sent,on_err) 83 | assert.is_truthy(buffered_bytes >= 0 and buffered_bytes < #data) 84 | stop() 85 | ev.Timer.new(function() done() end,0.01):start(ev.Loop.default) 86 | end) 87 | 88 | it('calls on_error callback',function(done) 89 | sock:close() 90 | send('some data closing', 91 | async(function() 92 | assert.is_nil('should not happen') 93 | end), 94 | async(function(err) 95 | assert.is_equal(err,'closed') 96 | done() 97 | end)) 98 | end) 99 | 100 | end) 101 | -------------------------------------------------------------------------------- /src/websocket/server_copas.lua: -------------------------------------------------------------------------------- 1 | 2 | local socket = require'socket' 3 | local copas = require'copas' 4 | local tools = require'websocket.tools' 5 | local frame = require'websocket.frame' 6 | local handshake = require'websocket.handshake' 7 | local sync = require'websocket.sync' 8 | local tconcat = table.concat 9 | local tinsert = table.insert 10 | 11 | local clients = {} 12 | 13 | local client = function(sock,protocol) 14 | local copas = require'copas' 15 | 16 | local self = {} 17 | 18 | self.state = 'OPEN' 19 | self.is_server = true 20 | 21 | self.sock_send = function(self,...) 22 | return copas.send(sock,...) 23 | end 24 | 25 | self.sock_receive = function(self,...) 26 | return copas.receive(sock,...) 27 | end 28 | 29 | self.sock_close = function(self) 30 | sock:shutdown() 31 | sock:close() 32 | end 33 | 34 | self = sync.extend(self) 35 | 36 | self.on_close = function(self) 37 | clients[protocol][self] = nil 38 | end 39 | 40 | self.broadcast = function(self,...) 41 | for client in pairs(clients[protocol]) do 42 | if client ~= self then 43 | client:send(...) 44 | end 45 | end 46 | self:send(...) 47 | end 48 | 49 | return self 50 | end 51 | 52 | local listen = function(opts) 53 | 54 | local copas = require'copas' 55 | assert(opts and (opts.protocols or opts.default)) 56 | local on_error = opts.on_error or function(s) print(s) end 57 | local listener,err = socket.bind(opts.interface or '*',opts.port or 80) 58 | if err then 59 | error(err) 60 | end 61 | local protocols = {} 62 | if opts.protocols then 63 | for protocol in pairs(opts.protocols) do 64 | clients[protocol] = {} 65 | tinsert(protocols,protocol) 66 | end 67 | end 68 | -- true is the 'magic' index for the default handler 69 | clients[true] = {} 70 | copas.addserver( 71 | listener, 72 | function(sock) 73 | local request = {} 74 | repeat 75 | -- no timeout used, so should either return with line or err 76 | local line,err = copas.receive(sock,'*l') 77 | if line then 78 | request[#request+1] = line 79 | else 80 | sock:close() 81 | if on_error then 82 | on_error('invalid request') 83 | end 84 | return 85 | end 86 | until line == '' 87 | local upgrade_request = tconcat(request,'\r\n') 88 | local response,protocol = handshake.accept_upgrade(upgrade_request,protocols) 89 | if not response then 90 | copas.send(sock,protocol) 91 | sock:close() 92 | if on_error then 93 | on_error('invalid request') 94 | end 95 | return 96 | end 97 | copas.send(sock,response) 98 | local handler 99 | local new_client 100 | local protocol_index 101 | if protocol and opts.protocols[protocol] then 102 | protocol_index = protocol 103 | handler = opts.protocols[protocol] 104 | elseif opts.default then 105 | -- true is the 'magic' index for the default handler 106 | protocol_index = true 107 | handler = opts.default 108 | else 109 | sock:close() 110 | if on_error then 111 | on_error('bad protocol') 112 | end 113 | return 114 | end 115 | new_client = client(sock,protocol_index) 116 | clients[protocol_index][new_client] = true 117 | handler(new_client) 118 | -- this is a dirty trick for preventing 119 | -- copas from automatically and prematurely closing 120 | -- the socket 121 | while new_client.state ~= 'CLOSED' do 122 | local dummy = { 123 | send = function() end, 124 | close = function() end 125 | } 126 | copas.send(dummy) 127 | end 128 | end) 129 | local self = {} 130 | self.close = function(_,keep_clients) 131 | copas.removeserver(listener) 132 | listener = nil 133 | if not keep_clients then 134 | for protocol,clients in pairs(clients) do 135 | for client in pairs(clients) do 136 | client:close() 137 | end 138 | end 139 | end 140 | end 141 | return self 142 | end 143 | 144 | return { 145 | listen = listen 146 | } 147 | -------------------------------------------------------------------------------- /src/websocket/ev_common.lua: -------------------------------------------------------------------------------- 1 | local ev = require'ev' 2 | local frame = require'websocket.frame' 3 | local tinsert = table.insert 4 | local tconcat = table.concat 5 | local eps = 2^-40 6 | 7 | local detach = function(f,loop) 8 | if ev.Idle then 9 | ev.Idle.new(function(loop,io) 10 | io:stop(loop) 11 | f() 12 | end):start(loop) 13 | else 14 | ev.Timer.new(function(loop,io) 15 | io:stop(loop) 16 | f() 17 | end,eps):start(loop) 18 | end 19 | end 20 | 21 | local async_send = function(sock,loop) 22 | assert(sock) 23 | loop = loop or ev.Loop.default 24 | local sock_send = sock.send 25 | local buffer 26 | local index 27 | local callbacks = {} 28 | local send = function(loop,write_io) 29 | local len = #buffer 30 | local sent,err,last = sock_send(sock,buffer,index) 31 | if not sent and err ~= 'timeout' then 32 | write_io:stop(loop) 33 | if callbacks.on_err then 34 | if write_io:is_active() then 35 | callbacks.on_err(err) 36 | else 37 | detach(function() 38 | callbacks.on_err(err) 39 | end,loop) 40 | end 41 | end 42 | elseif sent then 43 | local copy = buffer 44 | buffer = nil 45 | index = nil 46 | write_io:stop(loop) 47 | if callbacks.on_sent then 48 | -- detach calling callbacks.on_sent from current 49 | -- exection if thiis call context is not 50 | -- the send io to let send_async(_,on_sent,_) truely 51 | -- behave async. 52 | if write_io:is_active() then 53 | 54 | callbacks.on_sent(copy) 55 | else 56 | -- on_sent is only defined when responding to "on message for close op" 57 | -- so this can happen only once per lifetime of a websocket instance. 58 | -- callbacks.on_sent may be overwritten by a new call to send_async 59 | -- (e.g. due to calling ws:close(...) or ws:send(...)) 60 | local on_sent = callbacks.on_sent 61 | detach(function() 62 | on_sent(copy) 63 | end,loop) 64 | end 65 | end 66 | else 67 | assert(last < len) 68 | index = last + 1 69 | end 70 | end 71 | local io = ev.IO.new(send,sock:getfd(),ev.WRITE) 72 | local stop = function() 73 | io:stop(loop) 74 | buffer = nil 75 | index = nil 76 | end 77 | local send_async = function(data,on_sent,on_err) 78 | if buffer then 79 | -- a write io is still running 80 | buffer = buffer..data 81 | return #buffer 82 | else 83 | buffer = data 84 | end 85 | callbacks.on_sent = on_sent 86 | callbacks.on_err = on_err 87 | if not io:is_active() then 88 | send(loop,io) 89 | if index ~= nil then 90 | io:start(loop) 91 | end 92 | end 93 | local buffered = (buffer and #buffer - (index or 0)) or 0 94 | return buffered 95 | end 96 | return send_async,stop 97 | end 98 | 99 | local message_io = function(sock,loop,on_message,on_error) 100 | assert(sock) 101 | assert(loop) 102 | assert(on_message) 103 | assert(on_error) 104 | local last 105 | local frames = {} 106 | local first_opcode 107 | assert(sock:getfd() > -1) 108 | local message_io 109 | local dispatch = function(loop,io) 110 | -- could be stopped meanwhile by on_message function 111 | while message_io:is_active() do 112 | local encoded,err,part = sock:receive(100000) 113 | if err then 114 | if err == 'timeout' and #part == 0 then 115 | return 116 | elseif #part == 0 then 117 | if message_io then 118 | message_io:stop(loop) 119 | end 120 | on_error(err,io,sock) 121 | return 122 | end 123 | end 124 | if last then 125 | encoded = last..(encoded or part) 126 | last = nil 127 | else 128 | encoded = encoded or part 129 | end 130 | 131 | repeat 132 | local decoded,fin,opcode,rest = frame.decode(encoded) 133 | if decoded then 134 | if not first_opcode then 135 | first_opcode = opcode 136 | end 137 | tinsert(frames,decoded) 138 | encoded = rest 139 | if fin == true then 140 | on_message(tconcat(frames),first_opcode) 141 | frames = {} 142 | first_opcode = nil 143 | end 144 | end 145 | until not decoded 146 | if #encoded > 0 then 147 | last = encoded 148 | end 149 | end 150 | end 151 | message_io = ev.IO.new(dispatch,sock:getfd(),ev.READ) 152 | message_io:start(loop) 153 | -- the might be already data waiting (which will not trigger the IO) 154 | dispatch(loop,message_io) 155 | return message_io 156 | end 157 | 158 | return { 159 | async_send = async_send, 160 | message_io = message_io 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Not maintained / maintainer wanted !!!! 2 | 3 | If someone wants to maintain / take ownership of this project, reach out to me (issue, email). I like Lua very much, but I don't have enough time / resources to stay engaged with it. 4 | 5 | # About 6 | 7 | This project provides Lua modules for [Websocket Version 13](http://tools.ietf.org/html/rfc6455) conformant clients and servers. 8 | [![Build Status](https://travis-ci.org/lipp/lua-websockets.svg?branch=master)](https://travis-ci.org/lipp/lua-websockets) 9 | [![Coverage Status](https://coveralls.io/repos/lipp/lua-websockets/badge.png?branch=add-coveralls)](https://coveralls.io/r/lipp/lua-websockets?branch=master) 10 | 11 | The minified version is only ~10k bytes in size. 12 | 13 | Clients are available in three different flavours: 14 | 15 | - synchronous 16 | - coroutine based ([copas](http://keplerproject.github.com/copas)) 17 | - asynchronous ([lua-ev](https://github.com/brimworks/lua-ev)) 18 | 19 | Servers are available as two different flavours: 20 | 21 | - coroutine based ([copas](http://keplerproject.github.com/copas)) 22 | - asynchronous ([lua-ev](https://github.com/brimworks/lua-ev)) 23 | 24 | 25 | A webserver is NOT part of lua-websockets. If you are looking for a feature rich webserver framework, have a look at [orbit](http://keplerproject.github.com/orbit/) or others. It is no problem to work with a "normal" webserver and lua-websockets side by side (two processes, different ports), since websockets are not subject of the 'Same origin policy'. 26 | 27 | # Usage 28 | ## copas echo server 29 | This implements a basic echo server via Websockets protocol. Once you are connected with the server, all messages you send will be returned ('echoed') by the server immediately. 30 | 31 | ```lua 32 | local copas = require'copas' 33 | 34 | -- create a copas webserver and start listening 35 | local server = require'websocket'.server.copas.listen 36 | { 37 | -- listen on port 8080 38 | port = 8080, 39 | -- the protocols field holds 40 | -- key: protocol name 41 | -- value: callback on new connection 42 | protocols = { 43 | -- this callback is called, whenever a new client connects. 44 | -- ws is a new websocket instance 45 | echo = function(ws) 46 | while true do 47 | local message = ws:receive() 48 | if message then 49 | ws:send(message) 50 | else 51 | ws:close() 52 | return 53 | end 54 | end 55 | end 56 | } 57 | } 58 | 59 | -- use the copas loop 60 | copas.loop() 61 | ``` 62 | 63 | ## lua-ev echo server 64 | This implements a basic echo server via Websockets protocol. Once you are connected with the server, all messages you send will be returned ('echoed') by the server immediately. 65 | 66 | ```lua 67 | local ev = require'ev' 68 | 69 | -- create a copas webserver and start listening 70 | local server = require'websocket'.server.ev.listen 71 | { 72 | -- listen on port 8080 73 | port = 8080, 74 | -- the protocols field holds 75 | -- key: protocol name 76 | -- value: callback on new connection 77 | protocols = { 78 | -- this callback is called, whenever a new client connects. 79 | -- ws is a new websocket instance 80 | echo = function(ws) 81 | ws:on_message(function(ws,message) 82 | ws:send(message) 83 | end) 84 | 85 | -- this is optional 86 | ws:on_close(function() 87 | ws:close() 88 | end) 89 | end 90 | } 91 | } 92 | 93 | -- use the lua-ev loop 94 | ev.Loop.default:loop() 95 | 96 | ``` 97 | 98 | ## Running test-server examples 99 | 100 | The folder test-server contains two re-implementations of the [libwebsocket](http://git.warmcat.com/cgi-bin/cgit/libwebsockets/) test-server.c example. 101 | 102 | ```shell 103 | cd test-server 104 | lua test-server-ev.lua 105 | ``` 106 | 107 | ```shell 108 | cd test-server 109 | lua test-server-copas.lua 110 | ``` 111 | 112 | Connect to the from Javascript (e.g. chrome's debugging console) like this: 113 | ```Javascript 114 | var echoWs = new WebSocket('ws://127.0.0.1:8002','echo'); 115 | ``` 116 | 117 | # Dependencies 118 | 119 | The client and server modules depend on: 120 | 121 | - luasocket 122 | - luabitop (if not using Lua 5.2 nor luajit) 123 | - luasec 124 | - copas (optionally) 125 | - lua-ev (optionally) 126 | 127 | # Install 128 | 129 | ```shell 130 | $ git clone git://github.com/lipp/lua-websockets.git 131 | $ cd lua-websockets 132 | $ luarocks make rockspecs/lua-websockets-scm-1.rockspec 133 | ``` 134 | 135 | # Minify 136 | 137 | A `squishy` file for [squish](http://matthewwild.co.uk/projects/squish/home) is 138 | provided. Creating the minified version (~10k) can be created with: 139 | 140 | ```sh 141 | $ squish --gzip 142 | ``` 143 | 144 | The minifed version has be to be installed manually though. 145 | 146 | 147 | # Tests 148 | 149 | Running tests requires: 150 | 151 | - [busted with async test support](https://github.com/lipp/busted) 152 | - [Docker](http://www.docker.com) 153 | 154 | ```shell 155 | docker build . 156 | ``` 157 | 158 | The first run will take A WHILE. 159 | -------------------------------------------------------------------------------- /spec/tools_spec.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path..'../src' 2 | 3 | local tools = require'websocket.tools' 4 | 5 | local bytes = string.char 6 | 7 | -- from wiki article 8 | local quick_brown_fox_sha1 = bytes(0x2f,0xd4,0xe1,0xc6, 9 | 0x7a,0x2d,0x28,0xfc, 10 | 0xed,0x84,0x9e,0xe1, 11 | 0xbb,0x76,0xe7,0x39, 12 | 0x1b,0x93,0xeb,0x12) 13 | 14 | describe( 15 | 'The tools module', 16 | function() 17 | it( 18 | 'SHA-1 algorithm works', 19 | function() 20 | local sha = tools.sha1('The quick brown fox jumps over the lazy dog') 21 | assert.is_same(sha,quick_brown_fox_sha1) 22 | end) 23 | 24 | it( 25 | 'Base64 encoding works', 26 | function() 27 | local base64 = tools.base64.encode('pleasure') 28 | assert.is_same(base64,'cGxlYXN1cmU=') 29 | local base64 = tools.base64.encode('leasure') 30 | assert.is_same(base64,'bGVhc3VyZQ==') 31 | local base64 = tools.base64.encode('easure') 32 | assert.is_same(base64,'ZWFzdXJl') 33 | end) 34 | 35 | it( 36 | 'Generate Key works', 37 | function() 38 | local keys = {} 39 | for i=1,200 do 40 | local key = tools.generate_key() 41 | assert.is_same(type(key),'string') 42 | assert.is_same(#key,24) 43 | assert.is_truthy(key:match('^[%w=/%+]*$')) 44 | for _,other in pairs(keys) do 45 | assert.is_not_same(other,key) 46 | end 47 | keys[i] = key 48 | end 49 | end) 50 | 51 | it( 52 | 'URL parser works', 53 | function() 54 | local protocol,host,port,uri = tools.parse_url('ws://www.example.com') 55 | assert.is_same(protocol,'ws') 56 | assert.is_same(host,'www.example.com') 57 | assert.is_same(port,80) 58 | assert.is_same(uri,'/') 59 | 60 | local protocol,host,port,uri = tools.parse_url('ws://www.example.com:8080') 61 | assert.is_same(protocol,'ws') 62 | assert.is_same(host,'www.example.com') 63 | assert.is_same(port,8080) 64 | assert.is_same(uri,'/') 65 | 66 | local protocol,host,port,uri = tools.parse_url('ws://www.example.com:8080/foo') 67 | assert.is_same(protocol,'ws') 68 | assert.is_same(host,'www.example.com') 69 | assert.is_same(port,8080) 70 | assert.is_same(uri,'/foo') 71 | 72 | local protocol,host,port,uri = tools.parse_url('ws://www.example.com:8080/') 73 | assert.is_same(protocol,'ws') 74 | assert.is_same(host,'www.example.com') 75 | assert.is_same(port,8080) 76 | assert.is_same(uri,'/') 77 | 78 | local protocol,host,port,uri = tools.parse_url('ws://www.example.com/') 79 | assert.is_same(protocol,'ws') 80 | assert.is_same(host,'www.example.com') 81 | assert.is_same(port,80) 82 | assert.is_same(uri,'/') 83 | 84 | local protocol,host,port,uri = tools.parse_url('ws://www.example.com/foo') 85 | assert.is_same(protocol,'ws') 86 | assert.is_same(host,'www.example.com') 87 | assert.is_same(port,80) 88 | assert.is_same(uri,'/foo') 89 | 90 | end) 91 | 92 | it( 93 | 'URL parser works with IPv6 and WSS', 94 | function() 95 | local URLS = { 96 | [ "WS://[::1]" ] = { "ws", "[::1]", 80, "/" }; 97 | [ "ws://[::1]" ] = { "ws", "[::1]", 80, "/" }; 98 | ["wss://[0:0:0:0:0:0:0:1]" ] = { "wss", "[0:0:0:0:0:0:0:1]", 443, "/" }; 99 | ["wss://[0:0:0:0:0:0:0:1]:8080" ] = { "wss", "[0:0:0:0:0:0:0:1]", 8080, "/" }; 100 | [ "ws://[0:0:0:0:0:0:0:1]" ] = { "ws", "[0:0:0:0:0:0:0:1]", 80, "/" }; 101 | [ "ws://[0:0:0:0:0:0:0:1]:8080" ] = { "ws", "[0:0:0:0:0:0:0:1]", 8080, "/" }; 102 | [ "ws://[0:0:0:0:0:0:0:1]:8080/query" ] = { "ws", "[0:0:0:0:0:0:0:1]", 8080, "/query" }; 103 | ["wss://127.0.0.1" ] = { "wss", "127.0.0.1", 443, "/" }; 104 | ["wss://127.0.0.1:8080" ] = { "wss", "127.0.0.1", 8080, "/" }; 105 | [ "ws://127.0.0.1" ] = { "ws", "127.0.0.1", 80, "/" }; 106 | [ "ws://127.0.0.1:8080" ] = { "ws", "127.0.0.1", 8080, "/" }; 107 | [ "ws://127.0.0.1:8080/query" ] = { "ws", "127.0.0.1", 8080, "/query" }; 108 | ["wss://echo.websockets.org" ] = { "wss", "echo.websockets.org", 443, "/" }; 109 | ["wss://echo.websockets.org:8080" ] = { "wss", "echo.websockets.org", 8080, "/" }; 110 | [ "ws://echo.websockets.org" ] = { "ws", "echo.websockets.org", 80, "/" }; 111 | [ "ws://echo.websockets.org:8080" ] = { "ws", "echo.websockets.org", 8080, "/" }; 112 | [ "ws://echo.websockets.org:8080/query" ] = { "ws", "echo.websockets.org", 8080, "/query" }; 113 | -- unknown protocol 114 | ["w2s://echo.websockets.org/query" ] = { "w2s", "echo.websockets.org", nil, "/query" }; 115 | } 116 | 117 | for url, res in pairs(URLS) do 118 | local a,b,c,d = tools.parse_url(url) 119 | assert.is_same(a, res[1]) 120 | assert.is_same(b, res[2]) 121 | assert.is_same(c, res[3]) 122 | assert.is_same(d, res[4]) 123 | end 124 | 125 | end 126 | ) 127 | 128 | end) 129 | -------------------------------------------------------------------------------- /src/websocket/tools.lua: -------------------------------------------------------------------------------- 1 | local bit = require'websocket.bit' 2 | local mime = require'mime' 3 | local rol = bit.rol 4 | local bxor = bit.bxor 5 | local bor = bit.bor 6 | local band = bit.band 7 | local bnot = bit.bnot 8 | local lshift = bit.lshift 9 | local rshift = bit.rshift 10 | local sunpack = string.unpack 11 | local srep = string.rep 12 | local schar = string.char 13 | local tremove = table.remove 14 | local tinsert = table.insert 15 | local tconcat = table.concat 16 | local mrandom = math.random 17 | 18 | local read_n_bytes = function(str, pos, n) 19 | pos = pos or 1 20 | return pos+n, string.byte(str, pos, pos + n - 1) 21 | end 22 | 23 | local read_int8 = function(str, pos) 24 | return read_n_bytes(str, pos, 1) 25 | end 26 | 27 | local read_int16 = function(str, pos) 28 | local new_pos,a,b = read_n_bytes(str, pos, 2) 29 | return new_pos, lshift(a, 8) + b 30 | end 31 | 32 | local read_int32 = function(str, pos) 33 | local new_pos,a,b,c,d = read_n_bytes(str, pos, 4) 34 | return new_pos, 35 | lshift(a, 24) + 36 | lshift(b, 16) + 37 | lshift(c, 8 ) + 38 | d 39 | end 40 | 41 | local pack_bytes = string.char 42 | 43 | local write_int8 = pack_bytes 44 | 45 | local write_int16 = function(v) 46 | return pack_bytes(rshift(v, 8), band(v, 0xFF)) 47 | end 48 | 49 | local write_int32 = function(v) 50 | return pack_bytes( 51 | band(rshift(v, 24), 0xFF), 52 | band(rshift(v, 16), 0xFF), 53 | band(rshift(v, 8), 0xFF), 54 | band(v, 0xFF) 55 | ) 56 | end 57 | 58 | -- used for generate key random ops 59 | math.randomseed(os.time()) 60 | 61 | -- SHA1 hashing from luacrypto, if available 62 | local sha1_crypto 63 | local done,crypto = pcall(require,'crypto') 64 | if done then 65 | sha1_crypto = function(msg) 66 | return crypto.digest('sha1',msg,true) 67 | end 68 | end 69 | 70 | -- from wiki article, not particularly clever impl 71 | local sha1_wiki = function(msg) 72 | local h0 = 0x67452301 73 | local h1 = 0xEFCDAB89 74 | local h2 = 0x98BADCFE 75 | local h3 = 0x10325476 76 | local h4 = 0xC3D2E1F0 77 | 78 | local bits = #msg * 8 79 | -- append b10000000 80 | msg = msg..schar(0x80) 81 | 82 | -- 64 bit length will be appended 83 | local bytes = #msg + 8 84 | 85 | -- 512 bit append stuff 86 | local fill_bytes = 64 - (bytes % 64) 87 | if fill_bytes ~= 64 then 88 | msg = msg..srep(schar(0),fill_bytes) 89 | end 90 | 91 | -- append 64 big endian length 92 | local high = math.floor(bits/2^32) 93 | local low = bits - high*2^32 94 | msg = msg..write_int32(high)..write_int32(low) 95 | 96 | assert(#msg % 64 == 0,#msg % 64) 97 | 98 | for j=1,#msg,64 do 99 | local chunk = msg:sub(j,j+63) 100 | assert(#chunk==64,#chunk) 101 | local words = {} 102 | local next = 1 103 | local word 104 | repeat 105 | next,word = read_int32(chunk, next) 106 | tinsert(words, word) 107 | until next > 64 108 | assert(#words==16) 109 | for i=17,80 do 110 | words[i] = bxor(words[i-3],words[i-8],words[i-14],words[i-16]) 111 | words[i] = rol(words[i],1) 112 | end 113 | local a = h0 114 | local b = h1 115 | local c = h2 116 | local d = h3 117 | local e = h4 118 | 119 | for i=1,80 do 120 | local k,f 121 | if i > 0 and i < 21 then 122 | f = bor(band(b,c),band(bnot(b),d)) 123 | k = 0x5A827999 124 | elseif i > 20 and i < 41 then 125 | f = bxor(b,c,d) 126 | k = 0x6ED9EBA1 127 | elseif i > 40 and i < 61 then 128 | f = bor(band(b,c),band(b,d),band(c,d)) 129 | k = 0x8F1BBCDC 130 | elseif i > 60 and i < 81 then 131 | f = bxor(b,c,d) 132 | k = 0xCA62C1D6 133 | end 134 | 135 | local temp = rol(a,5) + f + e + k + words[i] 136 | e = d 137 | d = c 138 | c = rol(b,30) 139 | b = a 140 | a = temp 141 | end 142 | 143 | h0 = h0 + a 144 | h1 = h1 + b 145 | h2 = h2 + c 146 | h3 = h3 + d 147 | h4 = h4 + e 148 | 149 | end 150 | 151 | -- necessary on sizeof(int) == 32 machines 152 | h0 = band(h0,0xffffffff) 153 | h1 = band(h1,0xffffffff) 154 | h2 = band(h2,0xffffffff) 155 | h3 = band(h3,0xffffffff) 156 | h4 = band(h4,0xffffffff) 157 | 158 | return write_int32(h0)..write_int32(h1)..write_int32(h2)..write_int32(h3)..write_int32(h4) 159 | end 160 | 161 | local base64_encode = function(data) 162 | return (mime.b64(data)) 163 | end 164 | 165 | local DEFAULT_PORTS = {ws = 80, wss = 443} 166 | 167 | local parse_url = function(url) 168 | local protocol, address, uri = url:match('^(%w+)://([^/]+)(.*)$') 169 | if not protocol then error('Invalid URL:'..url) end 170 | protocol = protocol:lower() 171 | local host, port = address:match("^(.+):(%d+)$") 172 | if not host then 173 | host = address 174 | port = DEFAULT_PORTS[protocol] 175 | end 176 | if not uri or uri == '' then uri = '/' end 177 | return protocol, host, tonumber(port), uri 178 | end 179 | 180 | local generate_key = function() 181 | local r1 = mrandom(0,0xfffffff) 182 | local r2 = mrandom(0,0xfffffff) 183 | local r3 = mrandom(0,0xfffffff) 184 | local r4 = mrandom(0,0xfffffff) 185 | local key = write_int32(r1)..write_int32(r2)..write_int32(r3)..write_int32(r4) 186 | assert(#key==16,#key) 187 | return base64_encode(key) 188 | end 189 | 190 | return { 191 | sha1 = sha1_crypto or sha1_wiki, 192 | base64 = { 193 | encode = base64_encode 194 | }, 195 | parse_url = parse_url, 196 | generate_key = generate_key, 197 | read_int8 = read_int8, 198 | read_int16 = read_int16, 199 | read_int32 = read_int32, 200 | write_int8 = write_int8, 201 | write_int16 = write_int16, 202 | write_int32 = write_int32, 203 | } 204 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Client Sync and Copas 2 | 3 | Besides its creation, the Sync and Copas interfaces are identical. The Copas Client performs all socket operations with the copas non-blocking calls though. 4 | 5 | ## websocket.client.sync / websocket.client.new 6 | 7 | Takes an optional table as parameter, which allows to specify the socket timeout. 8 | 9 | ```lua 10 | local websocket = require'websocket' 11 | local client = websocket.client.sync({timeout=2}) 12 | -- websocket.client.new is alias to websocket.client.sync 13 | ``` 14 | 15 | ## websocket.client.copas 16 | 17 | Takes an optional table as parameter, which allows to specify the socket timeout. 18 | 19 | ```lua 20 | local websocket = require'websocket' 21 | local client = websocket.client.copas({timeout=2}) 22 | ``` 23 | 24 | ## client:connect(ws_url,[protocol]) 25 | 26 | The first argument must be a websocket URL and the second is an optional string, which specifies the 27 | protocol. 28 | On success, the connect method returns true. On error it returns `nil` followed by an error 29 | description. 30 | 31 | ```lua 32 | local ok,err = client:connect('ws://localhost:12345','echo') 33 | if not ok then 34 | print('could not connect',err) 35 | end 36 | ``` 37 | 38 | ## client:receive() 39 | 40 | On success, the first return value is the message received as string and the second is 41 | the message's opcode, which can be `websocket.TEXT` or `websocket.BINARY`. In case the server closed the connection or an error happened, the additional return values `close_was_clean`,`close_code` and `close_reason` are returned. If the connection was closed for some reason during receive, it is not neccessary to call `client:close()`. 42 | 43 | ```lua 44 | local message,opcode,close_was_clean,close_code,close_reason = client:receive() 45 | ``` 46 | 47 | If the details about the close are not of interest, looking at `message` and `opcode` suffices. 48 | 49 | ```lua 50 | local message,opcode = client:receive() 51 | if message then 52 | print('msg',message,opcode) 53 | else 54 | print('connection closed') 55 | end 56 | ``` 57 | 58 | ## client:send(message,[type]) 59 | 60 | Takes a string containing the message content and an optional second param, specifying the type of message which can be either `websocket.TEXT` or `websocket.BINARY`. The default type is `websocket.TEXT`. 61 | On success, true is returned. On error, nil is returned followed by `close_was_clean`,`close_code` and `close_reason`. 62 | 63 | ```lua 64 | local ok,close_was_clean,close_code,close_reason = client:send('hello',websocket.TEXT) 65 | ``` 66 | 67 | If the details about the close are not of interest, looking at `ok` suffices. 68 | 69 | ```lua 70 | local ok = client:send('hello') 71 | if ok then 72 | print('msg sent') 73 | else 74 | print('connection closed') 75 | end 76 | ``` 77 | 78 | ## client:close([code],[reason]) 79 | 80 | The client con initiate the closing handshake by calling `client:close()`. The function takes two optional parameters `code` (Number) and `reason` (String) to provide additional information about the closing motivation to the server. The `code` defaults to 1000 (normal closure) and `reason` is empty string. The `close_was_clean`,`close_code` and `close_reason` are returned according to the protocol. 81 | 82 | ```lua 83 | local close_was_clean,close_code,close_reason = client:close(4001,'lost interest') 84 | ``` 85 | 86 | If the details about the close are not of interest, just ignore them and leave default arguments. 87 | 88 | ```lua 89 | client:close() 90 | ``` 91 | 92 | # Server Copas 93 | 94 | For a working complete example see test-server/test-server-copas.lua and examples/echo-server-copas.lua. 95 | 96 | ## websocket.server.copas.listen(config) 97 | 98 | Creates a new websocket server with copas compatible "event multi-plexing". 99 | All the beef is in the config table: 100 | 101 | ### config.port 102 | 103 | A number specifying the port number to listen for incoming connections. Default is 80. 104 | 105 | ### config.interface 106 | 107 | A string specifying the networking interfaces to listen on. Default is '*' (all). 108 | 109 | ### config.protocols 110 | 111 | A table, which holds all the protocol-handlers by name. See example. 112 | 113 | ### config.default 114 | 115 | The default protocol-handler. Is called if no other protocol matches or no protocol was provided. Optional. 116 | 117 | ```lua 118 | local websocket = require'websocket' 119 | local config = { 120 | port = 8080, 121 | interface = '*', 122 | protocols = { 123 | ['echo'] = function(ws) 124 | while true do 125 | local message = ws:receive() 126 | if message then 127 | ws:send(message) 128 | else 129 | ws:close() 130 | return 131 | end 132 | end 133 | end, 134 | ['echo-uppercase'] = function(ws) 135 | while true do 136 | local message = ws:receive() 137 | if message then 138 | ws:send(message:upper()) 139 | else 140 | ws:close() 141 | return 142 | end 143 | end 144 | end, 145 | } 146 | default = function(ws) 147 | ws:send('goodbye strange client') 148 | ws:close() 149 | end 150 | } 151 | local server = websocket.server.copas.listen(config) 152 | ``` 153 | 154 | ## Protocol Handlers 155 | 156 | The protocol handlers are called whenever a new client connects to the server. The new client instance is passed in as argument and has the same API interface as the Copas Client (see above). As the instance is already "connected", it provides no `client:connect()` method. 157 | 158 | ## server:close([keep_clients]) 159 | 160 | Closes the server and - if `keep_clients` is falsy - closes all clients connected to server. 161 | -------------------------------------------------------------------------------- /spec/handshake_spec.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path..'../src' 2 | 3 | local port = os.getenv('LUAWS_WSTEST_PORT') or 11000 4 | 5 | local url = 'ws://127.0.0.1:'..port 6 | 7 | local handshake = require'websocket.handshake' 8 | local socket = require'socket' 9 | 10 | local request_lines = { 11 | 'GET /chat HTTP/1.1', 12 | 'Host: server.example.com', 13 | 'Upgrade: websocket', 14 | 'Connection: Upgrade', 15 | 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', 16 | 'Sec-WebSocket-Protocol: chat, superchat', 17 | 'Sec-WebSocket-Version: 13', 18 | 'Origin: http://example.com', 19 | '\r\n' 20 | } 21 | local request_header = table.concat(request_lines,'\r\n') 22 | 23 | local bytes = string.char 24 | 25 | describe( 26 | 'The handshake module', 27 | function() 28 | it( 29 | 'RFC 1.3: calculate the correct accept sum', 30 | function() 31 | local sec_websocket_key = "dGhlIHNhbXBsZSBub25jZQ==" 32 | local accept = handshake.sec_websocket_accept(sec_websocket_key) 33 | assert.is_same(accept,"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") 34 | end) 35 | 36 | it( 37 | 'can create handshake header', 38 | function() 39 | local req = handshake.upgrade_request 40 | { 41 | key = 'dGhlIHNhbXBsZSBub25jZQ==', 42 | host = 'server.example.com', 43 | origin = 'http://example.com', 44 | protocols = {'chat','superchat'}, 45 | uri = '/chat' 46 | } 47 | assert.is_same(req,request_header) 48 | end) 49 | 50 | it( 51 | 'can parse handshake header', 52 | function() 53 | local headers,remainder = handshake.http_headers(request_header..'foo') 54 | assert.is_same(type(headers),'table') 55 | assert.is_same(headers['upgrade'],'websocket') 56 | assert.is_same(headers['connection'],'upgrade') 57 | assert.is_same(headers['sec-websocket-key'],'dGhlIHNhbXBsZSBub25jZQ==') 58 | assert.is_same(headers['sec-websocket-version'],'13') 59 | assert.is_same(headers['sec-websocket-protocol'],'chat, superchat') 60 | assert.is_same(headers['origin'],'http://example.com') 61 | assert.is_same(headers['host'],'server.example.com') 62 | assert.is_same(remainder,'foo') 63 | end) 64 | 65 | it( 66 | 'generates correct upgrade response', 67 | function() 68 | local response,protocol = handshake.accept_upgrade(request_header,{'chat'}) 69 | assert.is_same(type(response),'string') 70 | assert.is_truthy(response:match('^HTTP/1.1 101 Switching Protocols\r\n')) 71 | assert.is_same(protocol,'chat') 72 | local headers = handshake.http_headers(response) 73 | assert.is_same(type(headers),'table') 74 | assert.is_same(headers['upgrade'],'websocket') 75 | assert.is_same(headers['connection'],'upgrade') 76 | assert.is_same(headers['sec-websocket-protocol'],'chat') 77 | assert.is_same(headers['sec-websocket-accept'],'s3pPLMBiTxaQ9kYGzzhZRbK+xOo=') 78 | end) 79 | 80 | it( 81 | 'generates correct upgrade response for unsupported protocol', 82 | function() 83 | local response,protocol = handshake.accept_upgrade(request_header,{'bla'}) 84 | assert.is_same(type(response),'string') 85 | assert.is_truthy(response:match('^HTTP/1.1 101 Switching Protocols\r\n')) 86 | assert.is_same(protocol,nil) 87 | local headers = handshake.http_headers(response) 88 | assert.is_same(type(headers),'table') 89 | assert.is_same(headers['upgrade'],'websocket') 90 | assert.is_same(headers['connection'],'upgrade') 91 | assert.is_same(headers['sec-websocket-protocol'],nil) 92 | assert.is_same(headers['sec-websocket-accept'],'s3pPLMBiTxaQ9kYGzzhZRbK+xOo=') 93 | end) 94 | 95 | describe( 96 | 'connecting to echo server (echo-js.ws) on port 8081', 97 | function() 98 | local sock = socket.tcp() 99 | sock:settimeout(0.3) 100 | 101 | it( 102 | 'can connect and upgrade to websocket protocol', 103 | function() 104 | sock:connect('127.0.0.1',port) 105 | local req = handshake.upgrade_request 106 | { 107 | key = 'dGhlIHNhbXBsZSBub25jZQ==', 108 | host = '127.0.0.1:'..port, 109 | protocols = {'echo-protocol'}, 110 | origin = 'http://example.com', 111 | uri = '/' 112 | } 113 | sock:send(req) 114 | local resp = {} 115 | repeat 116 | local line,err = sock:receive('*l') 117 | assert.is_falsy(err) 118 | resp[#resp+1] = line 119 | until line == '' 120 | local response = table.concat(resp,'\r\n') 121 | assert.is_truthy(response:match('^HTTP/1.1 101 Switching Protocols\r\n')) 122 | 123 | local headers = handshake.http_headers(response) 124 | assert.is_same(type(headers),'table') 125 | assert.is_same(headers['upgrade'],'websocket') 126 | assert.is_same(headers['connection'],'upgrade') 127 | assert.is_same(headers['sec-websocket-accept'],'s3pPLMBiTxaQ9kYGzzhZRbK+xOo=') 128 | end) 129 | 130 | 131 | it( 132 | 'and can send and receive frames', 133 | function() 134 | -- from rfc doc 135 | local hello_unmasked = bytes(0x81,0x05,0x48,0x65,0x6c,0x6c,0x6f) 136 | local hello_masked = bytes(0x81,0x85,0x37,0xfa,0x21,0x3d,0x7f,0x9f,0x4d,0x51,0x58) 137 | -- the client MUST send masked 138 | sock:send(hello_masked) 139 | local resp,err = sock:receive(#hello_unmasked) 140 | assert.is_falsy(err) 141 | -- the server answers unmasked 142 | assert.is_same(resp,hello_unmasked) 143 | end) 144 | end) 145 | end) 146 | -------------------------------------------------------------------------------- /spec/client_spec.lua: -------------------------------------------------------------------------------- 1 | local socket = require'socket' 2 | local port = os.getenv('LUAWS_WSTEST_PORT') or 11000 3 | local url = 'ws://127.0.0.1:'..port 4 | 5 | local client = require'websocket.client' 6 | 7 | describe( 8 | 'The client module', 9 | function() 10 | local wsc 11 | it( 12 | 'exposes the correct interface', 13 | function() 14 | assert.is_table(client) 15 | assert.is_function(client.new) 16 | assert.is_equal(client.new,client.sync) 17 | end) 18 | 19 | it( 20 | 'can be constructed and closed', 21 | function() 22 | wsc = client.new() 23 | wsc:close() 24 | end) 25 | 26 | it( 27 | 'can be constructed with timeout', 28 | function() 29 | wsc = client.new({timeout=1}) 30 | end) 31 | 32 | it( 33 | 'returns error when trying to send or receive when not connected', 34 | function() 35 | local ok,was_clean,code,reason = wsc:send('test') 36 | assert.is_nil(ok) 37 | assert.is_false(was_clean) 38 | assert.is_equal(code,1006) 39 | assert.is_equal(reason,'wrong state') 40 | 41 | 42 | local message,opcode,was_clean,code,reason = wsc:receive() 43 | assert.is_nil(message) 44 | assert.is_nil(opcode) 45 | assert.is_false(was_clean) 46 | assert.is_equal(code,1006) 47 | assert.is_equal(reason,'wrong state') 48 | end) 49 | 50 | it( 51 | 'can connect (requires external websocket server)', 52 | function() 53 | assert.is_function(wsc.connect) 54 | local ok, protocol, headers = wsc:connect(url,'echo-protocol') 55 | assert.is_truthy(ok) 56 | assert.is_equal(protocol,'echo-protocol') 57 | assert.is_truthy(headers['sec-websocket-accept']) 58 | assert.is_truthy(headers['sec-websocket-protocol']) 59 | end) 60 | 61 | it( 62 | 'returns error on non-ws protocol', 63 | function() 64 | local c = client.new() 65 | local ok,err,h = c:connect('wsc://127.0.0.1:'..port,'echo-protocol') 66 | assert.is_falsy(ok) 67 | assert.is_equal(err,'bad protocol') 68 | assert.is_nil(h) 69 | end) 70 | 71 | it( 72 | 'forwards socket errors', 73 | function() 74 | local c = client.new() 75 | local ok,err,h = c:connect('ws://127.0.0.1:1','echo-protocol') 76 | assert.is_nil(ok) 77 | assert.is_equal(err,'connection refused') 78 | assert.is_nil(h) 79 | 80 | local ok,err,h = c:connect('ws://notexisting:8089','echo-protocol') 81 | assert.is_nil(ok) 82 | if socket.tcp6 then 83 | assert.is_equal(err,'host or service not provided, or not known') 84 | else 85 | assert.is_equal(err,'host not found') 86 | end 87 | assert.is_nil(h) 88 | end) 89 | 90 | it( 91 | 'returns error when sending in non-open state (requires external websocket server @port 8081)', 92 | function() 93 | local c = client.new() 94 | local ok,was_clean,code,reason = c:send('test') 95 | assert.is_nil(ok) 96 | assert.is_false(was_clean) 97 | assert.is_equal(code,1006) 98 | assert.is_equal(reason,'wrong state') 99 | 100 | c:connect(url,'echo-protocol') 101 | c:close() 102 | local ok,was_clean,code,reason = c:send('test') 103 | assert.is_nil(ok) 104 | assert.is_false(was_clean) 105 | assert.is_equal(code,1006) 106 | assert.is_equal(reason,'wrong state') 107 | end) 108 | 109 | it( 110 | 'returns error when connecting twice (requires external websocket server @port 8081)', 111 | function() 112 | local c = client.new() 113 | local ok,err,h = c:connect(url,'echo-protocol') 114 | assert.is_truthy(ok) 115 | assert.is_truthy(err) 116 | assert.is_truthy(h) 117 | 118 | local ok,err,h = c:connect(url,'echo-protocol') 119 | assert.is_falsy(ok) 120 | assert.is_equal(err,'wrong state') 121 | assert.is_nil(h) 122 | end) 123 | 124 | it( 125 | 'can send (requires external websocket server @port 8081)', 126 | function() 127 | assert.is_same(type(wsc.send),'function') 128 | wsc:send('Hello again') 129 | end) 130 | 131 | it( 132 | 'can receive (requires external websocket server @port 8081)', 133 | function() 134 | assert.is_same(type(wsc.receive),'function') 135 | local echoed = wsc:receive() 136 | assert.is_same(echoed,'Hello again') 137 | end) 138 | 139 | local random_text = function(len) 140 | local chars = {} 141 | for i=1,len do 142 | chars[i] = string.char(math.random(33,126)) 143 | end 144 | return table.concat(chars) 145 | end 146 | 147 | it( 148 | 'can send with payload 127 (requires external websocket server @port 8081)', 149 | function() 150 | local text = random_text(127) 151 | wsc:send(text) 152 | local echoed = wsc:receive() 153 | assert.is_same(text,echoed) 154 | end) 155 | 156 | it( 157 | 'can send with payload 0xffff-1 (requires external websocket server @port 8081)', 158 | function() 159 | local text = random_text(0xffff-1) 160 | assert.is_same(#text,0xffff-1) 161 | wsc:send(text) 162 | local echoed = wsc:receive() 163 | assert.is_same(#text,#echoed) 164 | assert.is_same(text,echoed) 165 | end) 166 | 167 | it( 168 | 'can send with payload 0xffff+1 (requires external websocket server @port 8081)', 169 | function() 170 | local text = random_text(0xffff+1) 171 | assert.is_same(#text,0xffff+1) 172 | wsc:send(text) 173 | local echoed = wsc:receive() 174 | assert.is_same(#text,#echoed) 175 | assert.is_same(text,echoed) 176 | end) 177 | 178 | it( 179 | 'can close cleanly (requires external websocket server @port 8081)', 180 | function() 181 | local was_clean,code,reason = wsc:close() 182 | assert.is_true(was_clean) 183 | assert.is_true(code >= 1000) 184 | assert.is_string(reason) 185 | end) 186 | 187 | end) 188 | -------------------------------------------------------------------------------- /src/websocket/sync.lua: -------------------------------------------------------------------------------- 1 | local frame = require'websocket.frame' 2 | local handshake = require'websocket.handshake' 3 | local tools = require'websocket.tools' 4 | local ssl = require'ssl' 5 | local tinsert = table.insert 6 | local tconcat = table.concat 7 | 8 | local receive = function(self) 9 | if self.state ~= 'OPEN' and not self.is_closing then 10 | return nil,nil,false,1006,'wrong state' 11 | end 12 | local first_opcode 13 | local frames 14 | local bytes = 3 15 | local encoded = '' 16 | local clean = function(was_clean,code,reason) 17 | self.state = 'CLOSED' 18 | self:sock_close() 19 | if self.on_close then 20 | self:on_close() 21 | end 22 | return nil,nil,was_clean,code,reason or 'closed' 23 | end 24 | while true do 25 | local chunk,err = self:sock_receive(bytes) 26 | if err then 27 | return clean(false,1006,err) 28 | end 29 | encoded = encoded..chunk 30 | local decoded,fin,opcode,_,masked = frame.decode(encoded) 31 | if not self.is_server and masked then 32 | return clean(false,1006,'Websocket receive failed: frame was not masked') 33 | end 34 | if decoded then 35 | if opcode == frame.CLOSE then 36 | if not self.is_closing then 37 | local code,reason = frame.decode_close(decoded) 38 | -- echo code 39 | local msg = frame.encode_close(code) 40 | local encoded = frame.encode(msg,frame.CLOSE,not self.is_server) 41 | local n,err = self:sock_send(encoded) 42 | if n == #encoded then 43 | return clean(true,code,reason) 44 | else 45 | return clean(false,code,err) 46 | end 47 | else 48 | return decoded,opcode 49 | end 50 | end 51 | if not first_opcode then 52 | first_opcode = opcode 53 | end 54 | if not fin then 55 | if not frames then 56 | frames = {} 57 | elseif opcode ~= frame.CONTINUATION then 58 | return clean(false,1002,'protocol error') 59 | end 60 | bytes = 3 61 | encoded = '' 62 | tinsert(frames,decoded) 63 | elseif not frames then 64 | return decoded,first_opcode 65 | else 66 | tinsert(frames,decoded) 67 | return tconcat(frames),first_opcode 68 | end 69 | else 70 | assert(type(fin) == 'number' and fin > 0) 71 | bytes = fin 72 | end 73 | end 74 | assert(false,'never reach here') 75 | end 76 | 77 | local send = function(self,data,opcode) 78 | if self.state ~= 'OPEN' then 79 | return nil,false,1006,'wrong state' 80 | end 81 | local encoded = frame.encode(data,opcode or frame.TEXT,not self.is_server) 82 | local n,err = self:sock_send(encoded) 83 | if n ~= #encoded then 84 | return nil,self:close(1006,err) 85 | end 86 | return true 87 | end 88 | 89 | local close = function(self,code,reason) 90 | if self.state ~= 'OPEN' then 91 | return false,1006,'wrong state' 92 | end 93 | if self.state == 'CLOSED' then 94 | return false,1006,'wrong state' 95 | end 96 | local msg = frame.encode_close(code or 1000,reason) 97 | local encoded = frame.encode(msg,frame.CLOSE,not self.is_server) 98 | local n,err = self:sock_send(encoded) 99 | local was_clean = false 100 | local code = 1005 101 | local reason = '' 102 | if n == #encoded then 103 | self.is_closing = true 104 | local rmsg,opcode = self:receive() 105 | if rmsg and opcode == frame.CLOSE then 106 | code,reason = frame.decode_close(rmsg) 107 | was_clean = true 108 | end 109 | else 110 | reason = err 111 | end 112 | self:sock_close() 113 | if self.on_close then 114 | self:on_close() 115 | end 116 | self.state = 'CLOSED' 117 | return was_clean,code,reason or '' 118 | end 119 | 120 | local connect = function(self,ws_url,ws_protocol,ssl_params) 121 | if self.state ~= 'CLOSED' then 122 | return nil,'wrong state',nil 123 | end 124 | local protocol,host,port,uri = tools.parse_url(ws_url) 125 | -- Preconnect (for SSL if needed) 126 | local _,err = self:sock_connect(host,port) 127 | if err then 128 | return nil,err,nil 129 | end 130 | if protocol == 'wss' then 131 | self.sock = ssl.wrap(self.sock, ssl_params) 132 | self.sock:dohandshake() 133 | elseif protocol ~= "ws" then 134 | return nil, 'bad protocol' 135 | end 136 | local ws_protocols_tbl = {''} 137 | if type(ws_protocol) == 'string' then 138 | ws_protocols_tbl = {ws_protocol} 139 | elseif type(ws_protocol) == 'table' then 140 | ws_protocols_tbl = ws_protocol 141 | end 142 | local key = tools.generate_key() 143 | local req = handshake.upgrade_request 144 | { 145 | key = key, 146 | host = host, 147 | port = port, 148 | protocols = ws_protocols_tbl, 149 | uri = uri 150 | } 151 | local n,err = self:sock_send(req) 152 | if n ~= #req then 153 | return nil,err,nil 154 | end 155 | local resp = {} 156 | repeat 157 | local line,err = self:sock_receive('*l') 158 | resp[#resp+1] = line 159 | if err then 160 | return nil,err,nil 161 | end 162 | until line == '' 163 | local response = table.concat(resp,'\r\n') 164 | local headers = handshake.http_headers(response) 165 | local expected_accept = handshake.sec_websocket_accept(key) 166 | if headers['sec-websocket-accept'] ~= expected_accept then 167 | local msg = 'Websocket Handshake failed: Invalid Sec-Websocket-Accept (expected %s got %s)' 168 | return nil,msg:format(expected_accept,headers['sec-websocket-accept'] or 'nil'),headers 169 | end 170 | self.state = 'OPEN' 171 | return true,headers['sec-websocket-protocol'],headers 172 | end 173 | 174 | local extend = function(obj) 175 | assert(obj.sock_send) 176 | assert(obj.sock_receive) 177 | assert(obj.sock_close) 178 | 179 | assert(obj.is_closing == nil) 180 | assert(obj.receive == nil) 181 | assert(obj.send == nil) 182 | assert(obj.close == nil) 183 | assert(obj.connect == nil) 184 | 185 | if not obj.is_server then 186 | assert(obj.sock_connect) 187 | end 188 | 189 | if not obj.state then 190 | obj.state = 'CLOSED' 191 | end 192 | 193 | obj.receive = receive 194 | obj.send = send 195 | obj.close = close 196 | obj.connect = connect 197 | 198 | return obj 199 | end 200 | 201 | return { 202 | extend = extend 203 | } 204 | -------------------------------------------------------------------------------- /src/websocket/frame.lua: -------------------------------------------------------------------------------- 1 | -- Following Websocket RFC: http://tools.ietf.org/html/rfc6455 2 | local bit = require'websocket.bit' 3 | local band = bit.band 4 | local bxor = bit.bxor 5 | local bor = bit.bor 6 | local tremove = table.remove 7 | local srep = string.rep 8 | local ssub = string.sub 9 | local sbyte = string.byte 10 | local schar = string.char 11 | local band = bit.band 12 | local rshift = bit.rshift 13 | local tinsert = table.insert 14 | local tconcat = table.concat 15 | local mmin = math.min 16 | local mfloor = math.floor 17 | local mrandom = math.random 18 | local unpack = unpack or table.unpack 19 | local tools = require'websocket.tools' 20 | local write_int8 = tools.write_int8 21 | local write_int16 = tools.write_int16 22 | local write_int32 = tools.write_int32 23 | local read_int8 = tools.read_int8 24 | local read_int16 = tools.read_int16 25 | local read_int32 = tools.read_int32 26 | 27 | local bits = function(...) 28 | local n = 0 29 | for _,bitn in pairs{...} do 30 | n = n + 2^bitn 31 | end 32 | return n 33 | end 34 | 35 | local bit_7 = bits(7) 36 | local bit_0_3 = bits(0,1,2,3) 37 | local bit_0_6 = bits(0,1,2,3,4,5,6) 38 | 39 | -- TODO: improve performance 40 | local xor_mask = function(encoded,mask,payload) 41 | local transformed,transformed_arr = {},{} 42 | -- xor chunk-wise to prevent stack overflow. 43 | -- sbyte and schar multiple in/out values 44 | -- which require stack 45 | for p=1,payload,2000 do 46 | local last = mmin(p+1999,payload) 47 | local original = {sbyte(encoded,p,last)} 48 | for i=1,#original do 49 | local j = (i-1) % 4 + 1 50 | transformed[i] = bxor(original[i],mask[j]) 51 | end 52 | local xored = schar(unpack(transformed,1,#original)) 53 | tinsert(transformed_arr,xored) 54 | end 55 | return tconcat(transformed_arr) 56 | end 57 | 58 | local encode_header_small = function(header, payload) 59 | return schar(header, payload) 60 | end 61 | 62 | local encode_header_medium = function(header, payload, len) 63 | return schar(header, payload, band(rshift(len, 8), 0xFF), band(len, 0xFF)) 64 | end 65 | 66 | local encode_header_big = function(header, payload, high, low) 67 | return schar(header, payload)..write_int32(high)..write_int32(low) 68 | end 69 | 70 | local encode = function(data,opcode,masked,fin) 71 | local header = opcode or 1-- TEXT is default opcode 72 | if fin == nil or fin == true then 73 | header = bor(header,bit_7) 74 | end 75 | local payload = 0 76 | if masked then 77 | payload = bor(payload,bit_7) 78 | end 79 | local len = #data 80 | local chunks = {} 81 | if len < 126 then 82 | payload = bor(payload,len) 83 | tinsert(chunks,encode_header_small(header,payload)) 84 | elseif len <= 0xffff then 85 | payload = bor(payload,126) 86 | tinsert(chunks,encode_header_medium(header,payload,len)) 87 | elseif len < 2^53 then 88 | local high = mfloor(len/2^32) 89 | local low = len - high*2^32 90 | payload = bor(payload,127) 91 | tinsert(chunks,encode_header_big(header,payload,high,low)) 92 | end 93 | if not masked then 94 | tinsert(chunks,data) 95 | else 96 | local m1 = mrandom(0,0xff) 97 | local m2 = mrandom(0,0xff) 98 | local m3 = mrandom(0,0xff) 99 | local m4 = mrandom(0,0xff) 100 | local mask = {m1,m2,m3,m4} 101 | tinsert(chunks,write_int8(m1,m2,m3,m4)) 102 | tinsert(chunks,xor_mask(data,mask,#data)) 103 | end 104 | return tconcat(chunks) 105 | end 106 | 107 | local decode = function(encoded) 108 | local encoded_bak = encoded 109 | if #encoded < 2 then 110 | return nil,2-#encoded 111 | end 112 | local pos,header,payload 113 | pos,header = read_int8(encoded,1) 114 | pos,payload = read_int8(encoded,pos) 115 | local high,low 116 | encoded = ssub(encoded,pos) 117 | local bytes = 2 118 | local fin = band(header,bit_7) > 0 119 | local opcode = band(header,bit_0_3) 120 | local mask = band(payload,bit_7) > 0 121 | payload = band(payload,bit_0_6) 122 | if payload > 125 then 123 | if payload == 126 then 124 | if #encoded < 2 then 125 | return nil,2-#encoded 126 | end 127 | pos,payload = read_int16(encoded,1) 128 | elseif payload == 127 then 129 | if #encoded < 8 then 130 | return nil,8-#encoded 131 | end 132 | pos,high = read_int32(encoded,1) 133 | pos,low = read_int32(encoded,pos) 134 | payload = high*2^32 + low 135 | if payload < 0xffff or payload > 2^53 then 136 | assert(false,'INVALID PAYLOAD '..payload) 137 | end 138 | else 139 | assert(false,'INVALID PAYLOAD '..payload) 140 | end 141 | encoded = ssub(encoded,pos) 142 | bytes = bytes + pos - 1 143 | end 144 | local decoded 145 | if mask then 146 | local bytes_short = payload + 4 - #encoded 147 | if bytes_short > 0 then 148 | return nil,bytes_short 149 | end 150 | local m1,m2,m3,m4 151 | pos,m1 = read_int8(encoded,1) 152 | pos,m2 = read_int8(encoded,pos) 153 | pos,m3 = read_int8(encoded,pos) 154 | pos,m4 = read_int8(encoded,pos) 155 | encoded = ssub(encoded,pos) 156 | local mask = { 157 | m1,m2,m3,m4 158 | } 159 | decoded = xor_mask(encoded,mask,payload) 160 | bytes = bytes + 4 + payload 161 | else 162 | local bytes_short = payload - #encoded 163 | if bytes_short > 0 then 164 | return nil,bytes_short 165 | end 166 | if #encoded > payload then 167 | decoded = ssub(encoded,1,payload) 168 | else 169 | decoded = encoded 170 | end 171 | bytes = bytes + payload 172 | end 173 | return decoded,fin,opcode,encoded_bak:sub(bytes+1),mask 174 | end 175 | 176 | local encode_close = function(code,reason) 177 | if code then 178 | local data = write_int16(code) 179 | if reason then 180 | data = data..tostring(reason) 181 | end 182 | return data 183 | end 184 | return '' 185 | end 186 | 187 | local decode_close = function(data) 188 | local _,code,reason 189 | if data then 190 | if #data > 1 then 191 | _,code = read_int16(data,1) 192 | end 193 | if #data > 2 then 194 | reason = data:sub(3) 195 | end 196 | end 197 | return code,reason 198 | end 199 | 200 | return { 201 | encode = encode, 202 | decode = decode, 203 | encode_close = encode_close, 204 | decode_close = decode_close, 205 | encode_header_small = encode_header_small, 206 | encode_header_medium = encode_header_medium, 207 | encode_header_big = encode_header_big, 208 | CONTINUATION = 0, 209 | TEXT = 1, 210 | BINARY = 2, 211 | CLOSE = 8, 212 | PING = 9, 213 | PONG = 10 214 | } 215 | -------------------------------------------------------------------------------- /src/websocket/client_ev.lua: -------------------------------------------------------------------------------- 1 | 2 | local socket = require'socket' 3 | local tools = require'websocket.tools' 4 | local frame = require'websocket.frame' 5 | local handshake = require'websocket.handshake' 6 | local debug = require'debug' 7 | local tconcat = table.concat 8 | local tinsert = table.insert 9 | 10 | local ev = function(ws) 11 | ws = ws or {} 12 | local ev = require'ev' 13 | local sock 14 | local loop = ws.loop or ev.Loop.default 15 | local fd 16 | local message_io 17 | local handshake_io 18 | local send_io_stop 19 | local async_send 20 | local self = {} 21 | self.state = 'CLOSED' 22 | local close_timer 23 | local user_on_message 24 | local user_on_close 25 | local user_on_open 26 | local user_on_error 27 | local cleanup = function() 28 | if close_timer then 29 | close_timer:stop(loop) 30 | close_timer = nil 31 | end 32 | if handshake_io then 33 | handshake_io:stop(loop) 34 | handshake_io:clear_pending(loop) 35 | handshake_io = nil 36 | end 37 | if send_io_stop then 38 | send_io_stop() 39 | send_io_stop = nil 40 | end 41 | if message_io then 42 | message_io:stop(loop) 43 | message_io:clear_pending(loop) 44 | message_io = nil 45 | end 46 | if sock then 47 | sock:shutdown() 48 | sock:close() 49 | sock = nil 50 | end 51 | end 52 | 53 | local on_close = function(was_clean,code,reason) 54 | cleanup() 55 | self.state = 'CLOSED' 56 | if user_on_close then 57 | user_on_close(self,was_clean,code,reason or '') 58 | end 59 | end 60 | local on_error = function(err,dont_cleanup) 61 | if not dont_cleanup then 62 | cleanup() 63 | end 64 | if user_on_error then 65 | user_on_error(self,err) 66 | else 67 | print('Error',err) 68 | end 69 | end 70 | local on_open = function(_,headers) 71 | self.state = 'OPEN' 72 | if user_on_open then 73 | user_on_open(self,headers['sec-websocket-protocol'],headers) 74 | end 75 | end 76 | local handle_socket_err = function(err,io,sock) 77 | if self.state == 'OPEN' then 78 | on_close(false,1006,err) 79 | elseif self.state ~= 'CLOSED' then 80 | on_error(err) 81 | end 82 | end 83 | local on_message = function(message,opcode) 84 | if opcode == frame.TEXT or opcode == frame.BINARY then 85 | if user_on_message then 86 | user_on_message(self,message,opcode) 87 | end 88 | elseif opcode == frame.CLOSE then 89 | if self.state ~= 'CLOSING' then 90 | self.state = 'CLOSING' 91 | local code,reason = frame.decode_close(message) 92 | local encoded = frame.encode_close(code) 93 | encoded = frame.encode(encoded,frame.CLOSE,true) 94 | async_send(encoded, 95 | function() 96 | on_close(true,code or 1005,reason) 97 | end,handle_socket_err) 98 | else 99 | on_close(true,1005,'') 100 | end 101 | end 102 | end 103 | 104 | self.send = function(_,message,opcode) 105 | local encoded = frame.encode(message,opcode or frame.TEXT,true) 106 | async_send(encoded, nil, handle_socket_err) 107 | end 108 | 109 | self.connect = function(_,url,ws_protocol) 110 | if self.state ~= 'CLOSED' then 111 | on_error('wrong state',true) 112 | return 113 | end 114 | local protocol,host,port,uri = tools.parse_url(url) 115 | if protocol ~= 'ws' then 116 | on_error('bad protocol') 117 | return 118 | end 119 | local ws_protocols_tbl = {''} 120 | if type(ws_protocol) == 'string' then 121 | ws_protocols_tbl = {ws_protocol} 122 | elseif type(ws_protocol) == 'table' then 123 | ws_protocols_tbl = ws_protocol 124 | end 125 | self.state = 'CONNECTING' 126 | assert(not sock) 127 | sock = socket.tcp() 128 | fd = sock:getfd() 129 | assert(fd > -1) 130 | -- set non blocking 131 | sock:settimeout(0) 132 | sock:setoption('tcp-nodelay',true) 133 | async_send,send_io_stop = require'websocket.ev_common'.async_send(sock,loop) 134 | handshake_io = ev.IO.new( 135 | function(loop,connect_io) 136 | connect_io:stop(loop) 137 | local key = tools.generate_key() 138 | local req = handshake.upgrade_request 139 | { 140 | key = key, 141 | host = host, 142 | port = port, 143 | protocols = ws_protocols_tbl, 144 | origin = ws.origin, 145 | uri = uri 146 | } 147 | async_send( 148 | req, 149 | function() 150 | local resp = {} 151 | local response = '' 152 | local read_upgrade = function(loop,read_io) 153 | -- this seems to be possible, i don't understand why though :( 154 | if not sock then 155 | read_io:stop(loop) 156 | handshake_io = nil 157 | return 158 | end 159 | repeat 160 | local byte,err,pp = sock:receive(1) 161 | if byte then 162 | response = response..byte 163 | elseif err then 164 | if err == 'timeout' then 165 | return 166 | else 167 | read_io:stop(loop) 168 | on_error('accept failed') 169 | return 170 | end 171 | end 172 | until response:sub(#response-3) == '\r\n\r\n' 173 | read_io:stop(loop) 174 | handshake_io = nil 175 | local headers = handshake.http_headers(response) 176 | local expected_accept = handshake.sec_websocket_accept(key) 177 | if headers['sec-websocket-accept'] ~= expected_accept then 178 | self.state = 'CLOSED' 179 | on_error('accept failed') 180 | return 181 | end 182 | message_io = require'websocket.ev_common'.message_io( 183 | sock,loop, 184 | on_message, 185 | handle_socket_err) 186 | on_open(self, headers) 187 | end 188 | handshake_io = ev.IO.new(read_upgrade,fd,ev.READ) 189 | handshake_io:start(loop)-- handshake 190 | end, 191 | handle_socket_err) 192 | end,fd,ev.WRITE) 193 | local connected,err = sock:connect(host,port) 194 | if connected then 195 | handshake_io:callback()(loop,handshake_io) 196 | elseif err == 'timeout' or err == 'Operation already in progress' then 197 | handshake_io:start(loop)-- connect 198 | else 199 | self.state = 'CLOSED' 200 | on_error(err) 201 | end 202 | end 203 | 204 | self.on_close = function(_,on_close_arg) 205 | user_on_close = on_close_arg 206 | end 207 | 208 | self.on_error = function(_,on_error_arg) 209 | user_on_error = on_error_arg 210 | end 211 | 212 | self.on_open = function(_,on_open_arg) 213 | user_on_open = on_open_arg 214 | end 215 | 216 | self.on_message = function(_,on_message_arg) 217 | user_on_message = on_message_arg 218 | end 219 | 220 | self.close = function(_,code,reason,timeout) 221 | if handshake_io then 222 | handshake_io:stop(loop) 223 | handshake_io:clear_pending(loop) 224 | end 225 | if self.state == 'CONNECTING' then 226 | self.state = 'CLOSING' 227 | on_close(false,1006,'') 228 | return 229 | elseif self.state == 'OPEN' then 230 | self.state = 'CLOSING' 231 | timeout = timeout or 3 232 | local encoded = frame.encode_close(code or 1000,reason) 233 | encoded = frame.encode(encoded,frame.CLOSE,true) 234 | -- this should let the other peer confirm the CLOSE message 235 | -- by 'echoing' the message. 236 | async_send(encoded) 237 | close_timer = ev.Timer.new(function() 238 | close_timer = nil 239 | on_close(false,1006,'timeout') 240 | end,timeout) 241 | close_timer:start(loop) 242 | end 243 | end 244 | 245 | return self 246 | end 247 | 248 | return ev 249 | -------------------------------------------------------------------------------- /src/websocket/server_ev.lua: -------------------------------------------------------------------------------- 1 | 2 | local socket = require'socket' 3 | local tools = require'websocket.tools' 4 | local frame = require'websocket.frame' 5 | local handshake = require'websocket.handshake' 6 | local tconcat = table.concat 7 | local tinsert = table.insert 8 | local ev 9 | local loop 10 | 11 | local clients = {} 12 | clients[true] = {} 13 | 14 | local client = function(sock,protocol) 15 | assert(sock) 16 | sock:setoption('tcp-nodelay',true) 17 | local fd = sock:getfd() 18 | local message_io 19 | local close_timer 20 | local async_send = require'websocket.ev_common'.async_send(sock,loop) 21 | local self = {} 22 | self.state = 'OPEN' 23 | self.sock = sock 24 | local user_on_error 25 | local on_error = function(s,err) 26 | if clients[protocol] ~= nil and clients[protocol][self] ~= nil then 27 | clients[protocol][self] = nil 28 | end 29 | if user_on_error then 30 | user_on_error(self,err) 31 | else 32 | print('Websocket server error',err) 33 | end 34 | end 35 | local user_on_close 36 | local on_close = function(was_clean,code,reason) 37 | if clients[protocol] ~= nil and clients[protocol][self] ~= nil then 38 | clients[protocol][self] = nil 39 | end 40 | if close_timer then 41 | close_timer:stop(loop) 42 | close_timer = nil 43 | end 44 | message_io:stop(loop) 45 | self.state = 'CLOSED' 46 | if user_on_close then 47 | user_on_close(self,was_clean,code,reason or '') 48 | end 49 | sock:shutdown() 50 | sock:close() 51 | end 52 | 53 | local handle_sock_err = function(err) 54 | if err == 'closed' then 55 | if self.state ~= 'CLOSED' then 56 | on_close(false,1006,'') 57 | end 58 | else 59 | on_error(err) 60 | end 61 | end 62 | local user_on_message = function() end 63 | local TEXT = frame.TEXT 64 | local BINARY = frame.BINARY 65 | local on_message = function(message,opcode) 66 | if opcode == TEXT or opcode == BINARY then 67 | user_on_message(self,message,opcode) 68 | elseif opcode == frame.CLOSE then 69 | if self.state ~= 'CLOSING' then 70 | self.state = 'CLOSING' 71 | local code,reason = frame.decode_close(message) 72 | local encoded = frame.encode_close(code) 73 | encoded = frame.encode(encoded,frame.CLOSE) 74 | async_send(encoded, 75 | function() 76 | on_close(true,code or 1006,reason) 77 | end,handle_sock_err) 78 | else 79 | on_close(true,1006,'') 80 | end 81 | end 82 | end 83 | 84 | self.send = function(_,message,opcode) 85 | local encoded = frame.encode(message,opcode or frame.TEXT) 86 | return async_send(encoded) 87 | end 88 | 89 | self.on_close = function(_,on_close_arg) 90 | user_on_close = on_close_arg 91 | end 92 | 93 | self.on_error = function(_,on_error_arg) 94 | user_on_error = on_error_arg 95 | end 96 | 97 | self.on_message = function(_,on_message_arg) 98 | user_on_message = on_message_arg 99 | end 100 | 101 | self.broadcast = function(_,...) 102 | for client in pairs(clients[protocol]) do 103 | if client.state == 'OPEN' then 104 | client:send(...) 105 | end 106 | end 107 | end 108 | 109 | self.close = function(_,code,reason,timeout) 110 | if clients[protocol] ~= nil and clients[protocol][self] ~= nil then 111 | clients[protocol][self] = nil 112 | end 113 | if not message_io then 114 | self:start() 115 | end 116 | if self.state == 'OPEN' then 117 | self.state = 'CLOSING' 118 | assert(message_io) 119 | timeout = timeout or 3 120 | local encoded = frame.encode_close(code or 1000,reason or '') 121 | encoded = frame.encode(encoded,frame.CLOSE) 122 | async_send(encoded) 123 | close_timer = ev.Timer.new(function() 124 | close_timer = nil 125 | on_close(false,1006,'timeout') 126 | end,timeout) 127 | close_timer:start(loop) 128 | end 129 | end 130 | 131 | self.start = function() 132 | message_io = require'websocket.ev_common'.message_io( 133 | sock,loop, 134 | on_message, 135 | handle_sock_err) 136 | end 137 | 138 | 139 | return self 140 | end 141 | 142 | local listen = function(opts) 143 | assert(opts and (opts.protocols or opts.default)) 144 | ev = require'ev' 145 | loop = opts.loop or ev.Loop.default 146 | local user_on_error 147 | local on_error = function(s,err) 148 | if user_on_error then 149 | user_on_error(s,err) 150 | else 151 | print(err) 152 | end 153 | end 154 | local protocols = {} 155 | if opts.protocols then 156 | for protocol in pairs(opts.protocols) do 157 | clients[protocol] = {} 158 | tinsert(protocols,protocol) 159 | end 160 | end 161 | local self = {} 162 | self.on_error = function(self,on_error) 163 | user_on_error = on_error 164 | end 165 | local listener,err = socket.bind(opts.interface or '*',opts.port or 80) 166 | if not listener then 167 | error(err) 168 | end 169 | listener:settimeout(0) 170 | 171 | self.sock = function() 172 | return listener 173 | end 174 | 175 | local listen_io = ev.IO.new( 176 | function() 177 | local client_sock = listener:accept() 178 | client_sock:settimeout(0) 179 | assert(client_sock) 180 | local request = {} 181 | local last 182 | ev.IO.new( 183 | function(loop,read_io) 184 | repeat 185 | local line,err,part = client_sock:receive('*l') 186 | if line then 187 | if last then 188 | line = last..line 189 | last = nil 190 | end 191 | request[#request+1] = line 192 | elseif err ~= 'timeout' then 193 | on_error(self,'Websocket Handshake failed due to socket err:'..err) 194 | read_io:stop(loop) 195 | return 196 | else 197 | last = part 198 | return 199 | end 200 | until line == '' 201 | read_io:stop(loop) 202 | local upgrade_request = tconcat(request,'\r\n') 203 | local response,protocol = handshake.accept_upgrade(upgrade_request,protocols) 204 | if not response then 205 | print('Handshake failed, Request:') 206 | print(upgrade_request) 207 | client_sock:close() 208 | return 209 | end 210 | local index 211 | ev.IO.new( 212 | function(loop,write_io) 213 | local len = #response 214 | local sent,err = client_sock:send(response,index) 215 | if not sent then 216 | write_io:stop(loop) 217 | print('Websocket client closed while handshake',err) 218 | elseif sent == len then 219 | write_io:stop(loop) 220 | local protocol_handler 221 | local new_client 222 | local protocol_index 223 | if protocol and opts.protocols[protocol] then 224 | protocol_index = protocol 225 | protocol_handler = opts.protocols[protocol] 226 | elseif opts.default then 227 | -- true is the 'magic' index for the default handler 228 | protocol_index = true 229 | protocol_handler = opts.default 230 | else 231 | client_sock:close() 232 | if on_error then 233 | on_error('bad protocol') 234 | end 235 | return 236 | end 237 | new_client = client(client_sock,protocol_index) 238 | clients[protocol_index][new_client] = true 239 | protocol_handler(new_client) 240 | new_client:start(loop) 241 | else 242 | assert(sent < len) 243 | index = sent 244 | end 245 | end,client_sock:getfd(),ev.WRITE):start(loop) 246 | end,client_sock:getfd(),ev.READ):start(loop) 247 | end,listener:getfd(),ev.READ) 248 | self.close = function(keep_clients) 249 | listen_io:stop(loop) 250 | listener:close() 251 | listener = nil 252 | if not keep_clients then 253 | for protocol,clients in pairs(clients) do 254 | for client in pairs(clients) do 255 | client:close() 256 | end 257 | end 258 | end 259 | end 260 | listen_io:start(loop) 261 | return self 262 | end 263 | 264 | return { 265 | listen = listen 266 | } 267 | -------------------------------------------------------------------------------- /spec/frame_spec.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path..'../src' 2 | 3 | local frame = require'websocket.frame' 4 | local tools = require'websocket.tools' 5 | 6 | local bytes = string.char 7 | 8 | -- from rfc doc 9 | local hello_unmasked = bytes(0x81,0x05,0x48,0x65,0x6c,0x6c,0x6f) 10 | local hello_masked = bytes(0x81,0x85,0x37,0xfa,0x21,0x3d,0x7f,0x9f,0x4d,0x51,0x58) 11 | local hel = bytes(0x01,0x03,0x48,0x65,0x6c) 12 | local lo = bytes(0x80,0x02,0x6c,0x6f) 13 | 14 | describe( 15 | 'The frame module', 16 | function() 17 | it( 18 | 'exposes a table', 19 | function() 20 | assert.is_same(type(frame),'table') 21 | end) 22 | 23 | it( 24 | 'provides a decode and a encode function', 25 | function() 26 | assert.is.same(type(frame.encode),'function') 27 | assert.is.same(type(frame.decode),'function') 28 | assert.is.same(type(frame.encode_close),'function') 29 | assert.is.same(type(frame.decode_close),'function') 30 | end) 31 | 32 | it( 33 | 'provides correct OPCODES', 34 | function() 35 | assert.is.same(frame.CONTINUATION,0) 36 | assert.is.same(frame.TEXT,1) 37 | assert.is.same(frame.BINARY,2) 38 | assert.is.same(frame.CLOSE,8) 39 | assert.is.same(frame.PING,9) 40 | assert.is.same(frame.PONG,10) 41 | end) 42 | 43 | it('.encode_header_small is correct', 44 | function() 45 | local enc = frame.encode_header_small(123,33) 46 | local enc_ref = tools.write_int8(123)..tools.write_int8(33) 47 | assert.is_same(enc, enc_ref) 48 | end) 49 | 50 | it('.encode_header_medium is correct', 51 | function() 52 | local enc = frame.encode_header_medium(123,33,555) 53 | local enc_ref = tools.write_int8(123)..tools.write_int8(33)..tools.write_int16(555) 54 | assert.is_same(enc, enc_ref) 55 | end) 56 | 57 | it('.encode_header_big is correct', 58 | function() 59 | local enc = frame.encode_header_big(123,33,555,12987) 60 | local enc_ref = tools.write_int8(123)..tools.write_int8(33)..tools.write_int32(555)..tools.write_int32(12987) 61 | assert.is_same(enc, enc_ref) 62 | end) 63 | 64 | 65 | it( 66 | 'RFC: decode a single-frame unmasked text message', 67 | function() 68 | local decoded,fin,opcode,rest,masked = frame.decode(hello_unmasked..'foo') 69 | assert.is_same(opcode,0x1) 70 | assert.is_true(fin) 71 | assert.is.same(decoded,'Hello') 72 | assert.is_same(rest,'foo') 73 | assert.is_false(masked) 74 | end) 75 | 76 | it( 77 | 'RFC: decode a single-frame unmasked text message bytewise and check min length', 78 | function() 79 | for i=1,#hello_unmasked do 80 | local sub = hello_unmasked:sub(1,i) 81 | local decoded,fin,opcode,rest,masked = frame.decode(sub) 82 | if i ~= #hello_unmasked then 83 | assert.is_same(decoded,nil) 84 | assert.is_same(type(fin),'number') 85 | assert.is_truthy(i+fin <= #hello_unmasked) 86 | else 87 | assert.is_same(opcode,0x1) 88 | assert.is_true(fin) 89 | assert.is.same(decoded,'Hello') 90 | assert.is_same(rest,'') 91 | assert.is_false(masked) 92 | end 93 | end 94 | end) 95 | 96 | it( 97 | 'RFC: decode a single-frame masked text message', 98 | function() 99 | local decoded,fin,opcode,rest,masked = frame.decode(hello_masked..'foo') 100 | assert.is_true(fin) 101 | assert.is_same(opcode,0x1) 102 | assert.is.same(decoded,'Hello') 103 | assert.is_same(rest,'foo') 104 | assert.is_truthy(masked) 105 | end) 106 | 107 | it( 108 | 'RFC: decode a fragmented test message', 109 | function() 110 | local decoded,fin,opcode,rest,masked = frame.decode(hel) 111 | assert.is_falsy(fin) 112 | assert.is_same(opcode,0x1) 113 | assert.is.same(decoded,'Hel') 114 | assert.is_same(rest,'') 115 | assert.is_falsy(masked) 116 | 117 | decoded,fin,opcode,rest,masked = frame.decode(lo) 118 | assert.is_true(fin) 119 | assert.is_same(opcode,0x0) 120 | assert.is.same(decoded,'lo') 121 | assert.is_same(rest,'') 122 | assert.is_falsy(masked) 123 | end) 124 | 125 | it( 126 | 'refuse incomplete unmasked frame', 127 | function() 128 | local decoded,fin,opcode = frame.decode(hello_unmasked:sub(1,4)) 129 | assert.is_falsy(decoded) 130 | assert.is_same(fin,#hello_unmasked-4) 131 | assert.is_falsy(opcode) 132 | end) 133 | 134 | it( 135 | 'refuse incomplete masked frame', 136 | function() 137 | local decoded,fin,opcode = frame.decode(hello_masked:sub(1,4)) 138 | assert.is_falsy(decoded) 139 | assert.is_same(fin,#hello_masked-4) 140 | assert.is_falsy(opcode) 141 | end) 142 | 143 | it( 144 | 'encode single-frame unmasked text', 145 | function() 146 | local encoded = frame.encode('Hello') 147 | assert.is_same(encoded,hello_unmasked) 148 | local encoded = frame.encode('Hello',frame.TEXT) 149 | assert.is_same(encoded,hello_unmasked) 150 | local encoded = frame.encode('Hello',frame.TEXT,false) 151 | assert.is_same(encoded,hello_unmasked) 152 | local encoded = frame.encode('Hello',frame.TEXT,false,true) 153 | assert.is_same(encoded,hello_unmasked) 154 | local decoded,fin,opcode,_,masked = frame.decode(encoded) 155 | assert.is_same('Hello',decoded) 156 | assert.is_true(fin) 157 | assert.is_same(opcode,frame.TEXT) 158 | assert.is_falsy(masked) 159 | end) 160 | 161 | local random_text = function(len) 162 | local chars = {} 163 | for i=1,len do 164 | chars[i] = string.char(math.random(33,126)) 165 | end 166 | return table.concat(chars) 167 | end 168 | 169 | it( 170 | 'encode and decode single-frame of length 127 unmasked text', 171 | function() 172 | local len = 127 173 | local text = random_text(len) 174 | assert.is_same(#text,len) 175 | local encoded = frame.encode(text) 176 | local decoded,fin,opcode,rest,masked = frame.decode(encoded) 177 | assert.is_same(text,decoded) 178 | assert.is_same(#text,len) 179 | assert.is_true(fin) 180 | assert.is_same(opcode,frame.TEXT) 181 | assert.is_same(rest,'') 182 | assert.is_falsy(masked) 183 | end) 184 | 185 | it( 186 | 'encode and decode single-frame of length 0xffff-1 unmasked text', 187 | function() 188 | local len = 0xffff-1 189 | local text = random_text(len) 190 | assert.is_same(#text,len) 191 | local encoded = frame.encode(text) 192 | local decoded,fin,opcode,rest,masked = frame.decode(encoded) 193 | assert.is_same(text,decoded) 194 | assert.is_same(#text,len) 195 | assert.is_true(fin) 196 | assert.is_same(opcode,frame.TEXT) 197 | assert.is_same(rest,'') 198 | assert.is_falsy(masked) 199 | end) 200 | 201 | it( 202 | 'encode and decode single-frame of length 0xffff+1 unmasked text', 203 | function() 204 | local len = 0xffff+1 205 | local text = random_text(len) 206 | assert.is_same(#text,len) 207 | local encoded = frame.encode(text) 208 | local decoded,fin,opcode = frame.decode(encoded) 209 | assert.is_same(text,decoded) 210 | assert.is_same(#text,len) 211 | assert.is_true(fin) 212 | assert.is_same(opcode,frame.TEXT) 213 | end) 214 | 215 | it( 216 | 'encode single-frame masked text', 217 | function() 218 | local encoded = frame.encode('Hello',frame.TEXT,true) 219 | local decoded,fin,opcode,rest,masked = frame.decode(encoded) 220 | assert.is_same('Hello',decoded) 221 | assert.is_true(fin) 222 | assert.is_same(opcode,frame.TEXT) 223 | assert.is_same(rest,'') 224 | assert.is_truthy(masked) 225 | end) 226 | 227 | it( 228 | 'encode fragmented unmasked text', 229 | function() 230 | local hel = frame.encode('Hel',frame.TEXT,false,false) 231 | local decoded,fin,opcode = frame.decode(hel) 232 | assert.is_falsy(fin) 233 | assert.is_same(opcode,0x1) 234 | assert.is.same(decoded,'Hel') 235 | 236 | local lo = frame.encode('lo',frame.CONTINUATION,false) 237 | decoded,fin,opcode = frame.decode(lo) 238 | assert.is_true(fin) 239 | assert.is_same(opcode,0x0) 240 | assert.is.same(decoded,'lo') 241 | end) 242 | 243 | it( 244 | 'encodes and decodes close packet correctly', 245 | function() 246 | local reason = 'foobar' 247 | local code = 0xfff1 248 | local close_frame = frame.encode_close(code,reason) 249 | assert.is_same(#close_frame,2+#reason) 250 | local dcode,dreason = frame.decode_close(close_frame) 251 | assert.is_same(dcode,code) 252 | assert.is_same(dreason,reason) 253 | end) 254 | 255 | end) 256 | -------------------------------------------------------------------------------- /spec/client_ev_spec.lua: -------------------------------------------------------------------------------- 1 | local websocket = require'websocket' 2 | local socket = require'socket' 3 | local client = require'websocket.client' 4 | local ev = require'ev' 5 | local frame = require'websocket.frame' 6 | local port = os.getenv('LUAWS_WSTEST_PORT') or 11000 7 | local req_ws = ' (requires external websocket server @port '..port..')' 8 | local url = 'ws://127.0.0.1:'..port 9 | 10 | setloop('ev') 11 | 12 | describe( 13 | 'The client (ev) module', 14 | function() 15 | local wsc 16 | it( 17 | 'exposes the correct interface', 18 | function() 19 | assert.is_table(client) 20 | assert.is_function(client.ev) 21 | end) 22 | 23 | it( 24 | 'can be constructed', 25 | function() 26 | wsc = client.ev() 27 | end) 28 | 29 | it( 30 | 'can connect and calls on_open'..req_ws, 31 | function(done) 32 | settimeout(10) 33 | wsc:on_open(async(function(ws,protocol,headers) 34 | assert.is_equal(ws,wsc) 35 | assert.is_equal(protocol,'echo-protocol') 36 | assert.is.truthy(headers['sec-websocket-accept']) 37 | assert.is.truthy(headers['sec-websocket-protocol']) 38 | done() 39 | end)) 40 | wsc:connect(url,'echo-protocol') 41 | end) 42 | 43 | it( 44 | 'calls on_error if already connected'..req_ws, 45 | function(done) 46 | settimeout(3) 47 | wsc:on_error(async(function(ws,err) 48 | assert.is_equal(ws,wsc) 49 | assert.is_equal(err,'wrong state') 50 | ws:on_error() 51 | ws:on_close(function() done() end) 52 | ws:close() 53 | end)) 54 | wsc:connect(url,'echo-protocol') 55 | end) 56 | 57 | it( 58 | 'calls on_error on bad protocol'..req_ws, 59 | function(done) 60 | settimeout(3) 61 | wsc:on_error(async(function(ws,err) 62 | assert.is_equal(ws,wsc) 63 | assert.is_equal(err,'bad protocol') 64 | ws:on_error() 65 | done() 66 | end)) 67 | wsc:connect('ws2://127.0.0.1:'..port,'echo-protocol') 68 | end) 69 | 70 | it( 71 | 'can parse HTTP request header byte per byte', 72 | function(done) 73 | local resp = { 74 | 'HTTP/1.1 101 Switching Protocols', 75 | 'Upgrade: websocket', 76 | 'Connection: Upgrade', 77 | 'Sec-Websocket-Accept: e2123as3', 78 | 'Sec-Websocket-Protocol: chat', 79 | '\r\n' 80 | } 81 | resp = table.concat(resp,'\r\n') 82 | assert.is_equal(resp:sub(#resp-3),'\r\n\r\n') 83 | local socket = require'socket' 84 | local http_serv = socket.bind('*',port + 20) 85 | local http_con 86 | wsc:on_error(async(function(ws,err) 87 | assert.is_equal(err,'accept failed') 88 | ws:close() 89 | http_serv:close() 90 | http_con:close() 91 | done() 92 | end)) 93 | wsc:on_open(async(function() 94 | assert.is_nil('should never happen') 95 | end)) 96 | wsc:connect('ws://127.0.0.1:'..(port+20),'chat') 97 | http_con = http_serv:accept() 98 | local i = 1 99 | ev.Timer.new(function(loop,timer) 100 | if i <= #resp then 101 | local byte = resp:sub(i,i) 102 | http_con:send(byte) 103 | i = i + 1 104 | else 105 | timer:stop(loop) 106 | end 107 | end,0.0001,0.0001):start(ev.Loop.default) 108 | end) 109 | 110 | it( 111 | 'properly calls on_error if socket error on handshake occurs', 112 | function(done) 113 | local resp = { 114 | 'HTTP/1.1 101 Switching Protocols', 115 | 'Upgrade: websocket', 116 | 'Connection: Upgrade', 117 | } 118 | resp = table.concat(resp,'\r\n') 119 | local socket = require'socket' 120 | local http_serv = socket.bind('*',port + 20) 121 | local http_con 122 | wsc:on_error(async(function(ws,err) 123 | assert.is_equal(err,'accept failed') 124 | ws:on_close(function() done() end) 125 | ws:close() 126 | http_serv:close() 127 | http_con:close() 128 | end)) 129 | wsc:on_open(async(function() 130 | assert.is_nil('should never happen') 131 | end)) 132 | wsc:connect('ws://127.0.0.1:'..(port+20),'chat') 133 | http_con = http_serv:accept() 134 | local i = 1 135 | ev.Timer.new(function(loop,timer) 136 | if i <= #resp then 137 | local byte = resp:sub(i,i) 138 | http_con:send(byte) 139 | i = i + 1 140 | else 141 | timer:stop(loop) 142 | http_con:close() 143 | end 144 | end,0.0001,0.0001):start(ev.Loop.default) 145 | end) 146 | 147 | it( 148 | 'can open and close immediatly (in CLOSING state)'..req_ws, 149 | function(done) 150 | wsc:on_error(async(function(_,err) 151 | assert.is_nil(err or 'should never happen') 152 | end)) 153 | wsc:on_close(function(_,was_clean,code) 154 | assert.is_false(was_clean) 155 | assert.is_equal(code,1006) 156 | done() 157 | end) 158 | wsc:connect(url,'echo-protocol') 159 | wsc:close() 160 | end) 161 | 162 | it( 163 | 'socket err gets forwarded to on_error', 164 | function(done) 165 | settimeout(6.0) 166 | wsc:on_error(async(function(ws,err) 167 | assert.is_same(ws,wsc) 168 | if socket.tcp6 then 169 | assert.is_equal(err, 'host or service not provided, or not known') 170 | else 171 | assert.is_equal(err,'host not found') 172 | end 173 | -- wsc:close() 174 | done() 175 | end)) 176 | wsc:on_close(async(function() 177 | assert.is_nil(err or 'should never happen') 178 | end)) 179 | wsc:connect('ws://does_not_exist','echo-protocol') 180 | end) 181 | 182 | 183 | it( 184 | 'can send and receive data'..req_ws, 185 | function(done) 186 | settimeout(6.0) 187 | assert.is_function(wsc.send) 188 | wsc:on_message( 189 | async( 190 | function(ws,message,opcode) 191 | assert.is_equal(ws,wsc) 192 | assert.is_same(message,'Hello again') 193 | assert.is_same(opcode,frame.TEXT) 194 | done() 195 | end)) 196 | wsc:on_open(function() 197 | wsc:send('Hello again') 198 | end) 199 | wsc:connect(url,'echo-protocol') 200 | end) 201 | 202 | local random_text = function(len) 203 | local chars = {} 204 | for i=1,len do 205 | chars[i] = string.char(math.random(33,126)) 206 | end 207 | return table.concat(chars) 208 | end 209 | 210 | it( 211 | 'can send and receive data 127 byte messages'..req_ws, 212 | function(done) 213 | settimeout(6.0) 214 | local msg = random_text(127) 215 | wsc:on_message( 216 | async( 217 | function(ws,message,opcode) 218 | assert.is_same(#msg,#message) 219 | assert.is_same(msg,message) 220 | assert.is_same(opcode,frame.TEXT) 221 | done() 222 | end)) 223 | wsc:send(msg) 224 | end) 225 | 226 | it( 227 | 'can send and receive data 0xffff-1 byte messages'..req_ws, 228 | function(done) 229 | settimeout(10.0) 230 | local msg = random_text(0xffff-1) 231 | wsc:on_message( 232 | async( 233 | function(ws,message,opcode) 234 | assert.is_same(#msg,#message) 235 | assert.is_same(msg,message) 236 | assert.is_same(opcode,frame.TEXT) 237 | done() 238 | end)) 239 | wsc:send(msg) 240 | end) 241 | 242 | it( 243 | 'can send and receive data 0xffff+1 byte messages'..req_ws, 244 | function(done) 245 | settimeout(10.0) 246 | local msg = random_text(0xffff+1) 247 | wsc:on_message( 248 | async( 249 | function(ws,message,opcode) 250 | assert.is_same(#msg,#message) 251 | assert.is_same(msg,message) 252 | assert.is_same(opcode,frame.TEXT) 253 | done() 254 | end)) 255 | wsc:send(msg) 256 | end) 257 | 258 | it( 259 | 'closes cleanly'..req_ws, 260 | function(done) 261 | settimeout(6.0) 262 | wsc:on_close(async(function(_,was_clean,code,reason) 263 | assert.is_true(was_clean) 264 | assert.is_true(code >= 1000) 265 | assert.is_string(reason) 266 | done() 267 | end)) 268 | wsc:close() 269 | end) 270 | 271 | it( 272 | 'echoing 10 messages works'..req_ws, 273 | function(done) 274 | settimeout(3.0) 275 | wsc:on_error(async(function(_,err) 276 | assert.is_nil(err or 'should never happen') 277 | end)) 278 | wsc:on_close(async(function() 279 | assert.is_nil('should not happen yet') 280 | end)) 281 | wsc:on_message(async(function() 282 | assert.is_nil('should not happen yet') 283 | end)) 284 | wsc:on_open(async(function(ws) 285 | assert.is_same(ws,wsc) 286 | local count = 0 287 | local msg = 'Hello websockets' 288 | wsc:on_message(async(function(ws,message,opcode) 289 | count = count + 1 290 | assert.is_same(ws,wsc) 291 | assert.is_equal(message,msg..count) 292 | assert.is_equal(opcode,websocket.TEXT) 293 | if count == 10 then 294 | ws:on_close(async(function(_,was_clean,opcode,reason) 295 | assert.is_true(was_clean) 296 | assert.is_true(opcode >= 1000) 297 | done() 298 | end)) 299 | ws:close() 300 | end 301 | end)) 302 | 303 | for i=1,10 do 304 | wsc:send(msg..i) 305 | end 306 | end)) 307 | wsc:connect(url,'echo-protocol') 308 | end) 309 | end) 310 | -------------------------------------------------------------------------------- /spec/server_ev_spec.lua: -------------------------------------------------------------------------------- 1 | local server = require'websocket.server' 2 | local client = require'websocket.client' 3 | local socket = require'socket' 4 | local ev = require'ev' 5 | local loop = ev.Loop.default 6 | local port = os.getenv('LUAWS_SERVER_EV_PORT') or 8083 7 | local url = 'ws://127.0.0.1:'..port 8 | 9 | setloop('ev') 10 | 11 | describe( 12 | 'The server (ev) module', 13 | function() 14 | local s 15 | it( 16 | 'exposes the correct interface', 17 | function() 18 | assert.is_same(type(server),'table') 19 | assert.is_same(type(server.ev),'table') 20 | assert.is_same(type(server.ev.listen),'function') 21 | end) 22 | 23 | it( 24 | 'call listen with default handler', 25 | function() 26 | local s = server.ev.listen 27 | { 28 | default = function() end, 29 | port = port 30 | } 31 | s:close() 32 | end) 33 | 34 | it( 35 | 's:sock() provides access to the listening socket', 36 | function() 37 | local s = server.ev.listen 38 | { 39 | default = function() end, 40 | port = port 41 | } 42 | assert.is_truthy(tostring(s:sock()):match('tcp')) 43 | s:close() 44 | end) 45 | 46 | 47 | it( 48 | 'call listen with protocol handlers', 49 | function() 50 | local s = server.ev.listen 51 | { 52 | port = port, 53 | protocols = { 54 | echo = function() end 55 | } 56 | } 57 | s:close() 58 | end) 59 | 60 | it( 61 | 'call listen without default nor protocol handlers has errors', 62 | function() 63 | assert.has_error( 64 | function() 65 | local s = server.ev.listen 66 | { 67 | port = port 68 | } 69 | end) 70 | end) 71 | 72 | describe( 73 | 'communicating with clients', 74 | function() 75 | local s 76 | local on_new_echo_client 77 | setup( 78 | function() 79 | s = server.ev.listen 80 | { 81 | port = port, 82 | protocols = { 83 | echo = function(client) 84 | on_new_echo_client(client) 85 | end 86 | } 87 | } 88 | end) 89 | 90 | teardown( 91 | function() 92 | s:close() 93 | end) 94 | 95 | it( 96 | 'accepts socket connection and does not die when abruptly closing', 97 | function(done) 98 | settimeout(10) 99 | local sock = socket.tcp() 100 | s:on_error(async(function() 101 | s:on_error(nil) 102 | done() 103 | end)) 104 | sock:settimeout(0) 105 | local connected,err = sock:connect('127.0.0.1',port) 106 | local connect_io = ev.IO.new(async(function(loop,io) 107 | io:stop(loop) 108 | sock:close() 109 | end),sock:getfd(),ev.WRITE) 110 | if connected then 111 | connect_io:callback()(loop,connect_io) 112 | else 113 | connect_io:start(loop) 114 | end 115 | end) 116 | 117 | it( 118 | 'open and close handshake work (client closes)', 119 | function(done) 120 | local wsc = client.ev() 121 | on_new_echo_client = async( 122 | function(client) 123 | assert.is_same(type(client),'table') 124 | assert.is_same(type(client.on_message),'function') 125 | assert.is_same(type(client.close),'function') 126 | assert.is_same(type(client.send),'function') 127 | end) 128 | wsc:on_open(async( 129 | function() 130 | wsc:on_close(async(function(_,was_clean,code,reason) 131 | assert.is_true(was_clean) 132 | assert.is_true(code >= 1000) 133 | assert.is_string(reason) 134 | done() 135 | end)) 136 | wsc:close() 137 | end)) 138 | wsc:connect(url,'echo') 139 | end) 140 | 141 | it( 142 | 'open and close handshake work (server closes)', 143 | function(done) 144 | local wsc = client.ev() 145 | on_new_echo_client = async( 146 | function(client) 147 | assert.is_same(type(client),'table') 148 | assert.is_same(type(client.on_message),'function') 149 | assert.is_same(type(client.close),'function') 150 | assert.is_same(type(client.send),'function') 151 | client:on_close(async(function(_,was_clean,code,reason) 152 | -- this is for hunting down some rare bug 153 | if not was_clean then 154 | print(debug.traceback('',2)) 155 | end 156 | assert.is_true(was_clean) 157 | assert.is_true(code >= 1000) 158 | assert.is_string(reason) 159 | done() 160 | end)) 161 | client:close() 162 | end) 163 | wsc:connect(url,'echo') 164 | end) 165 | 166 | it( 167 | 'echo works', 168 | function(done) 169 | local wsc = client.ev() 170 | on_new_echo_client = async( 171 | function(client) 172 | client:on_message( 173 | async( 174 | function(self,msg) 175 | assert.is_equal(self,client) 176 | self:send('Hello') 177 | end)) 178 | end) 179 | wsc:on_open(async( 180 | function(self) 181 | assert.is_equal(self,wsc) 182 | self:send('Hello') 183 | self:on_message( 184 | async( 185 | function(_,message) 186 | assert.is_same(message,'Hello') 187 | self:close() 188 | done() 189 | end)) 190 | end)) 191 | wsc:connect(url,'echo') 192 | end) 193 | 194 | local random_text = function(len) 195 | local chars = {} 196 | for i=1,len do 197 | chars[i] = string.char(math.random(33,126)) 198 | end 199 | return table.concat(chars) 200 | end 201 | 202 | it( 203 | 'echo works with 127 byte messages', 204 | function(done) 205 | local message = random_text(127) 206 | local wsc = client.ev() 207 | on_new_echo_client = async( 208 | function(client) 209 | client:on_message( 210 | async( 211 | function(self,msg) 212 | assert.is_equal(self,client) 213 | self:send(message) 214 | end)) 215 | end) 216 | 217 | wsc:on_open(async( 218 | function(self) 219 | assert.is_equal(self,wsc) 220 | self:send(message) 221 | self:on_message( 222 | async( 223 | function(_,echoed) 224 | assert.is_same(message,echoed) 225 | self:close() 226 | done() 227 | end)) 228 | end)) 229 | wsc:connect(url,'echo') 230 | end) 231 | 232 | it( 233 | 'echo works with 0xffff-1 byte messages', 234 | function(done) 235 | settimeout(10.0) 236 | local message = random_text(0xffff-1) 237 | local wsc = client.ev() 238 | on_new_echo_client = async( 239 | function(client) 240 | client:on_message( 241 | async( 242 | function(self,msg) 243 | assert.is_equal(self,client) 244 | self:send(message) 245 | end)) 246 | end) 247 | 248 | wsc:on_open(async( 249 | function(self) 250 | assert.is_equal(self,wsc) 251 | self:send(message) 252 | self:on_message( 253 | async( 254 | function(_,echoed) 255 | assert.is_same(message,echoed) 256 | self:close() 257 | done() 258 | end)) 259 | end)) 260 | 261 | wsc:connect(url,'echo') 262 | end) 263 | 264 | it( 265 | 'echo works with 0xffff+1 byte messages', 266 | function(done) 267 | settimeout(6.0) 268 | local message = random_text(0xffff+1) 269 | local wsc = client.ev() 270 | on_new_echo_client = async( 271 | function(client) 272 | client:on_message( 273 | async( 274 | function(self,msg) 275 | assert.is_equal(self,client) 276 | self:send(message) 277 | end)) 278 | end) 279 | 280 | wsc:on_open(async( 281 | function(self) 282 | assert.is_equal(self,wsc) 283 | self:send(message) 284 | self:on_message( 285 | async( 286 | function(_,echoed) 287 | assert.is_same(message,echoed) 288 | self:close() 289 | done() 290 | end)) 291 | end)) 292 | wsc:connect(url,'echo') 293 | end) 294 | 295 | it( 296 | 'can close immediatly', 297 | function(done) 298 | on_new_echo_client = async( 299 | function(client) 300 | client:on_close(function() 301 | done() 302 | end) 303 | client:close() 304 | end) 305 | client.ev():connect(url,'echo') 306 | end) 307 | 308 | it( 309 | 'provides access to underlying luasocket socket instance', 310 | function(done) 311 | on_new_echo_client = async( 312 | function(client) 313 | assert.is_truthy(tostring(client.sock):match('tcp{client}')) 314 | done() 315 | client:close() 316 | end) 317 | client.ev():connect(url,'echo') 318 | end) 319 | 320 | 321 | end) 322 | 323 | end) 324 | -------------------------------------------------------------------------------- /test-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Minimal Websocket test app 6 | 14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
Detected Browser:
...
24 | 25 |
26 | 27 |
28 |
libwebsockets "dumb-increment-protocol"
29 |
30 | 31 | 32 | 33 | 34 | 35 |
Not initialized
36 |
37 | The incrementing number is coming from the server and is individual for 38 | each connection to the server... try opening a second browser window. 39 |

40 | Click the button to send the server a websocket message to 41 | reset the number. 42 |
43 |
44 |
45 |
46 |
libwebsockets "lws-mirror-protocol"
47 |
48 | Use the mouse to draw on the canvas below -- all other browser windows open 49 | on this page see your drawing in realtime and you can see any of theirs as 50 | well. 51 |

52 | The lws-mirror protocol doesn't interpret what is being sent to it, it just 53 | re-sends it to every other websocket it has a connection with using that 54 | protocol, including the guy who sent the packet. 55 |

56 | libwebsockets-test-client joins in by spamming circles on to this shared canvas when 57 | run. 58 |
59 | 60 | 61 | 69 | 70 | 71 | 72 | 75 | 76 |
Drawing color: 62 | 68 |
Not initialized
73 |
74 |
77 |
78 | 79 |
80 | Looking for support? http://libwebsockets.org
81 | Join the mailing list: ​http://ml.libwebsockets.org/mailman/listinfo/libwebsockets 82 | 83 |
84 | 85 |
86 | 87 | 385 | 386 | 387 | 388 | -------------------------------------------------------------------------------- /spec/server_copas_spec.lua: -------------------------------------------------------------------------------- 1 | local websocket = require'websocket' 2 | local server = require'websocket.server' 3 | local client = require'websocket.client' 4 | local port = os.getenv('LUAWS_SERVER_COPAS_PORT') or 8084 5 | local url = 'ws://localhost:'..port 6 | local socket = require'socket' 7 | 8 | local copas = require'copas' 9 | 10 | setloop('copas') 11 | 12 | describe( 13 | 'The server (copas) module', 14 | function() 15 | local s 16 | it( 17 | 'exposes the correct interface', 18 | function() 19 | assert.is_table(server) 20 | assert.is_table(server.copas) 21 | assert.is_function(server.copas.listen) 22 | end) 23 | 24 | it( 25 | 'call listen with default handler', 26 | function() 27 | local s = server.copas.listen 28 | { 29 | default = function() end, 30 | port = port 31 | } 32 | s:close() 33 | end) 34 | 35 | it( 36 | 'call listen with protocol handlers', 37 | function() 38 | local s = server.copas.listen 39 | { 40 | port = port, 41 | protocols = { 42 | echo = function() end 43 | } 44 | } 45 | s:close() 46 | end) 47 | 48 | it( 49 | 'call listen without default nor protocol handlers has errors', 50 | function() 51 | assert.has_error( 52 | function() 53 | local s = server.copas.listen 54 | { 55 | port = port 56 | } 57 | s:close() 58 | end) 59 | end) 60 | 61 | describe( 62 | 'communicating with clients', 63 | function() 64 | local s 65 | local on_new_echo_client 66 | setup( 67 | function() 68 | s = server.copas.listen 69 | { 70 | port = port, 71 | protocols = { 72 | echo = function(client) 73 | on_new_echo_client(client) 74 | end 75 | } 76 | } 77 | end) 78 | 79 | pending( 80 | 'client:connect forwards socket error', 81 | function() 82 | local wsc = client.copas() 83 | local ok,err,h = wsc:connect('ws://nonexisting.foo:'..port) 84 | assert.is_nil(ok) 85 | if socket.tcp6 then 86 | assert.is_equal(err,'host or service not provided, or not known') 87 | else 88 | assert.is_equal(err,'host not found') 89 | end 90 | assert.is_nil(h) 91 | end) 92 | 93 | it( 94 | 'handshake works with clean close (server inits close)', 95 | function(done) 96 | on_new_echo_client = async(function(client) 97 | assert.is_table(client) 98 | assert.is_function(client.receive) 99 | assert.is_function(client.close) 100 | assert.is_function(client.send) 101 | local was_clean,code,reason = client:close() 102 | assert.is_true(was_clean) 103 | assert.is_true(code >= 1000) 104 | assert.is_string(reason) 105 | done() 106 | end) 107 | 108 | copas.addthread(async(function() 109 | local wsc = client.copas() 110 | local ok,err,h = wsc:connect('ws://localhost:'..port,'echo') 111 | assert.is_true(ok) 112 | local was_clean,code,reason = wsc:close() 113 | assert.is_true(was_clean) 114 | assert.is_true(code >= 1000) 115 | assert.is_string(reason) 116 | end)) 117 | end) 118 | 119 | it( 120 | 'handshake works with clean close (client inits close)', 121 | function(done) 122 | on_new_echo_client = async(function(client) 123 | assert.is_table(client) 124 | assert.is_function(client.receive) 125 | assert.is_function(client.close) 126 | assert.is_function(client.send) 127 | local message,opcode,was_clean,code,reason = client:receive() 128 | assert.is_nil(message) 129 | assert.is_nil(opcode) 130 | assert.is_true(was_clean) 131 | assert.is_true(code >= 1000) 132 | assert.is_string(reason) 133 | done() 134 | end) 135 | 136 | copas.addthread(async(function() 137 | local wsc = client.copas() 138 | local ok = wsc:connect('ws://localhost:'..port,'echo') 139 | assert.is_true(ok) 140 | local was_clean,code,reason = wsc:close() 141 | assert.is_true(was_clean) 142 | assert.is_true(code >= 1000) 143 | assert.is_string(reason) 144 | end)) 145 | end) 146 | 147 | it( 148 | 'echo works', 149 | function(done) 150 | on_new_echo_client = async( 151 | function(client) 152 | local message = client:receive() 153 | client:send(message) 154 | client:close() 155 | end) 156 | 157 | copas.addthread( 158 | async( 159 | function() 160 | local wsc = client.copas() 161 | local hello = 'Hello' 162 | wsc:connect('ws://localhost:'..port,'echo') 163 | wsc:send(hello) 164 | local message = wsc:receive() 165 | assert.is_same(#message,#hello) 166 | assert.is_same(message,hello) 167 | wsc:close() 168 | done() 169 | end)) 170 | end) 171 | 172 | local random_text = function(len) 173 | local chars = {} 174 | for i=1,len do 175 | chars[i] = string.char(math.random(33,126)) 176 | end 177 | return table.concat(chars) 178 | end 179 | 180 | it( 181 | 'echo 127 bytes works', 182 | function(done) 183 | on_new_echo_client = async( 184 | function(client) 185 | local message = client:receive() 186 | client:send(message) 187 | client:close() 188 | end) 189 | 190 | copas.addthread( 191 | async( 192 | function() 193 | local wsc = client.copas() 194 | wsc:connect('ws://localhost:'..port,'echo') 195 | local message = random_text(127) 196 | wsc:send(message) 197 | local echoed = wsc:receive() 198 | assert.is_same(message,echoed) 199 | wsc:close() 200 | done() 201 | end)) 202 | end) 203 | 204 | it( 205 | 'echo 0xffff-1 bytes works', 206 | function(done) 207 | settimeout(5) 208 | on_new_echo_client = async( 209 | function(client) 210 | local message = client:receive() 211 | client:send(message) 212 | client:close() 213 | end) 214 | 215 | copas.addthread( 216 | async( 217 | function() 218 | local wsc = client.copas() 219 | wsc:connect('ws://localhost:'..port,'echo') 220 | local message = random_text(0xffff-1) 221 | wsc:send(message) 222 | local echoed = wsc:receive() 223 | assert.is_same(message,echoed) 224 | wsc:close() 225 | done() 226 | end)) 227 | end) 228 | 229 | it( 230 | 'echo 0xffff+1 bytes works', 231 | function(done) 232 | settimeout(5) 233 | on_new_echo_client = async( 234 | function(client) 235 | local message = client:receive() 236 | client:send(message) 237 | local was_clean = client:close() 238 | assert.is_true(was_clean) 239 | end) 240 | 241 | copas.addthread( 242 | async( 243 | function() 244 | local wsc = client.copas() 245 | wsc:connect('ws://localhost:'..port,'echo') 246 | local message = random_text(0xffff+1) 247 | wsc:send(message) 248 | local echoed = wsc:receive() 249 | assert.is_same(message,echoed) 250 | local echoed,_,was_clean = wsc:receive() 251 | assert.is_nil(echoed) 252 | assert.is_true(was_clean) 253 | done() 254 | end)) 255 | end) 256 | 257 | it( 258 | 'broadcast works', 259 | function(done) 260 | settimeout(6) 261 | local n = 20 262 | local n_clients = 0 263 | local closed = 0 264 | on_new_echo_client = async( 265 | function(client) 266 | n_clients = n_clients + 1 267 | if n_clients == n then 268 | client:broadcast('hello broadcast') 269 | end 270 | client.id = n_clients 271 | local message,opcode,was_clean = client:receive() 272 | assert.is_nil(message) 273 | assert.is_nil(opcode) 274 | assert.is_true(was_clean) 275 | n_clients = n_clients - 1 276 | if n_clients == 0 and closed == n then 277 | done() 278 | end 279 | end) 280 | 281 | for i=1,n do 282 | copas.addthread( 283 | async( 284 | function() 285 | local wsc = client.copas() 286 | local ok,err,h = wsc:connect('ws://localhost:'..port,'echo') 287 | assert.is_true(ok) 288 | assert.is_truthy(err) 289 | assert.is_truthy(h) 290 | 291 | local message,opcode = wsc:receive() 292 | assert.is_same(message,'hello broadcast') 293 | assert.is_same(opcode,websocket.TEXT) 294 | local was_clean = wsc:close() 295 | assert.is_true(was_clean) 296 | closed = closed + 1 297 | if n_clients == 0 and closed == n then 298 | done() 299 | end 300 | end)) 301 | end 302 | end) 303 | 304 | teardown( 305 | function() 306 | s:close(true) 307 | end) 308 | 309 | end) 310 | 311 | it( 312 | 'on_error is called if request is incomplete due to socket close', 313 | function(done) 314 | local serv 315 | serv = server.copas.listen 316 | { 317 | port = port, 318 | protocols = { 319 | echo = function(client) 320 | end 321 | }, 322 | on_error = async(function(err) 323 | assert.is_string(err) 324 | serv:close() 325 | done() 326 | end) 327 | } 328 | local s = socket.tcp() 329 | copas.connect(s,'localhost',port) 330 | s:send('GET / HTTP/1.1') 331 | s:close() 332 | end) 333 | 334 | it( 335 | 'on_error is called if request is invalid', 336 | function(done) 337 | local serv = server.copas.listen 338 | { 339 | port = port, 340 | protocols = { 341 | echo = function(client) 342 | end, 343 | }, 344 | on_error = function() end 345 | } 346 | copas.addthread(async(function() 347 | local s = socket.tcp() 348 | copas.connect(s,'localhost',port) 349 | copas.send(s,'GET / HTTP/1.1\r\n\r\n') 350 | local resp = copas.receive(s,'*l') 351 | assert.is_same(resp,'HTTP/1.1 400 Bad Request') 352 | local resp = copas.receive(s,2) 353 | assert.is_same(resp,'\r\n') 354 | s:close() 355 | serv:close() 356 | done() 357 | end)) 358 | end) 359 | 360 | it( 361 | 'default handler gets called when no protocol specified', 362 | function(done) 363 | local serv 364 | serv = server.copas.listen 365 | { 366 | port = port, 367 | protocols = { 368 | echo = async(function() 369 | assert.is_nil('should not happen') 370 | end) 371 | }, 372 | default = async(function(client) 373 | client:send('hello default') 374 | local message,opcode,was_clean = client:receive() 375 | assert.is_nil(message) 376 | assert.is_nil(opcode) 377 | assert.is_true(was_clean) 378 | end), 379 | } 380 | copas.addthread(async(function() 381 | local wsc = client.copas() 382 | local ok = wsc:connect('ws://localhost:'..port) 383 | assert.is_true(ok) 384 | local message = wsc:receive() 385 | assert.is_same(message,'hello default') 386 | wsc:close() 387 | serv:close() 388 | done() 389 | end)) 390 | end) 391 | 392 | it( 393 | 'closing server closes all clients', 394 | function(done) 395 | local clients = 0 396 | local closed = 0 397 | local n = 2 398 | local serv 399 | serv = server.copas.listen 400 | { 401 | port = port, 402 | protocols = { 403 | echo = async(function(client) 404 | clients = clients + 1 405 | if clients == n then 406 | copas.addthread(async(function() 407 | serv:close() 408 | assert.is_equal(closed,n) 409 | done() 410 | end)) 411 | end 412 | end) 413 | } 414 | } 415 | 416 | for i=1,n do 417 | copas.addthread(async(function() 418 | local wsc = client.copas() 419 | local ok = wsc:connect('ws://localhost:'..port,'echo') 420 | assert.is_true(ok) 421 | local message,opcode,was_clean = wsc:receive() 422 | assert.is_nil(message) 423 | assert.is_nil(opcode) 424 | assert.is_true(was_clean) 425 | closed = closed + 1 426 | end)) 427 | end 428 | end) 429 | 430 | end) 431 | --------------------------------------------------------------------------------