├── .gitignore ├── LICENSE.md ├── README.md ├── examples ├── assets │ ├── button.gui │ ├── fonts │ │ └── example.font │ ├── images │ │ └── green_button08.png │ └── ui.atlas ├── websocket.collection ├── websocket.gui └── websocket.gui_script ├── game.project ├── input └── game.input_binding ├── logo.png ├── tools ├── SimpleWebSocketServer.py └── websocketserver.py └── websocket ├── bit.lua ├── client_async.lua ├── coxpcall.lua ├── frame.lua ├── handshake.lua ├── sync.lua └── tools.lua /.gitignore: -------------------------------------------------------------------------------- 1 | /.internal 2 | /build 3 | .externalToolBuilders 4 | .DS_Store 5 | Thumbs.db 6 | .lock-wscript 7 | *.pyc 8 | .project 9 | .cproject 10 | builtins -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012 by Gerhard Lipp 4 | Copyright (c) 2016 by Björn Ritzl 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](logo.png) 2 | 3 | # THIS REPOSITORY AND DEFOLD LIBRARY IS DEPRECATED! 4 | 5 | Please migrate from this repo to the official WebSocket implementation for Defold: 6 | 7 | https://github.com/defold/extension-websocket 8 | 9 |
DOCUMENTATION FOR THIS REPO 10 |

11 | 12 | # Defold-WebSocket 13 | This project aims to provide a cross platform asynchronous implementation of the WebSockets protocol for Defold projects. Defold-WebSocket is based on the [lua-websocket](https://github.com/lipp/lua-websockets) project with additional code to handle WebSocket connections for HTML5 builds. The additional code is required since Emscripten (which is used for Defold HTML5 builds) will automatically upgrade normal TCP sockets connections to WebSocket connections. Emscripten will also take care of encoding and decoding the WebSocket frames. The WebSocket implementation in this project will bypass the handshake and frame encode/decode of lua-websocket when running in HTML5 builds. 14 | 15 | 16 | # Installation 17 | You can use the modules from this project in your own project by adding this project as a [Defold library dependency](http://www.defold.com/manuals/libraries/). Open your game.project file and in the `dependencies` field under `project` add: 18 | 19 | https://github.com/britzl/defold-websocket/archive/master.zip 20 | 21 | Or point to the ZIP file of a [specific release](https://github.com/britzl/defold-websocket/releases). 22 | 23 | ## Dependencies 24 | This project depends on the LuaSocket and LuaSec projects: 25 | 26 | * [defold-luasocket](https://github.com/britzl/defold-luasocket) 27 | * [defold-luasec](https://github.com/britzl/defold-luasec) 28 | 29 | You need to add these as dependencies in your game.project file, along with the dependency to this project itself. 30 | 31 | 32 | # Usage 33 | 34 | ```lua 35 | local client_async = require "websocket.client_async" 36 | 37 | function init(self) 38 | self.ws = client_async({ 39 | connect_timeout = 5, -- optional timeout (in seconds) when connecting 40 | }) 41 | 42 | self.ws:on_connected(function(ok, err) 43 | if ok then 44 | print("Connected") 45 | msg.post("#", "acquire_input_focus") 46 | else 47 | print("Unable to connect", err) 48 | end 49 | end) 50 | 51 | self.ws:on_disconnected(function() 52 | print("Disconnected") 53 | end) 54 | 55 | self.ws:on_message(function(message) 56 | print("Received message", message) 57 | end) 58 | 59 | self.ws:connect("ws://localhost:9999") 60 | end 61 | 62 | function update(self, dt) 63 | self.ws:step() 64 | end 65 | 66 | function on_input(self, action_id, action) 67 | if action_id == hash("fire") and action.released then 68 | self.ws:send("Some data") 69 | end 70 | end 71 | ``` 72 | 73 | ## How to connect using a Secure Web Socket (wss://) 74 | When connecting to a secure web socket you need to specify the protocol as "wss" and pass SSL parameters like this when connecting the web socket: 75 | 76 | ```lua 77 | local sslparams = { 78 | mode = "client", 79 | protocol = "tlsv1_2", 80 | verify = "none", 81 | options = "all", 82 | } 83 | self.ws:connect("wss://echo.websocket.org", "wss", sslparams) 84 | ``` 85 | 86 | # Important note on Sec-WebSocket-Protocol and Chrome 87 | Emscripten will create WebSockets with the Sec-WebSocket-Protocol header set to "binary" during the handshake. Google Chrome expects the response header to include the same Sec-WebSocket-Protocol header. Some WebSocket examples and the commonly used [Echo Test service](https://www.websocket.org/echo.html) does not respect this and omits the response header. This will cause WebSocket connections to fail during the handshake phase in Chrome. Firefox does impose the same restriction. I'm not sure about other browsers. 88 | 89 | 90 | # Testing using a Python based echo server 91 | There's a Python based WebSocket echo server in the tools folder. The echo server is built using the [simple-websocket-server](https://github.com/dpallot/simple-websocket-server) library. Start it by running `python websocketserver.py` from a terminal. Connect to it from `localhost:9999`. The library has been modified to return the Sec-WebSocket-Protocol response header, as described above. 92 | 93 |

94 |
95 | 96 | -------------------------------------------------------------------------------- /examples/assets/button.gui: -------------------------------------------------------------------------------- 1 | script: "" 2 | fonts { 3 | name: "example" 4 | font: "/examples/assets/fonts/example.font" 5 | } 6 | textures { 7 | name: "ui" 8 | texture: "/examples/assets/ui.atlas" 9 | } 10 | background_color { 11 | x: 0.0 12 | y: 0.0 13 | z: 0.0 14 | w: 1.0 15 | } 16 | nodes { 17 | position { 18 | x: 0.0 19 | y: 0.0 20 | z: 0.0 21 | w: 1.0 22 | } 23 | rotation { 24 | x: 0.0 25 | y: 0.0 26 | z: 0.0 27 | w: 1.0 28 | } 29 | scale { 30 | x: 1.0 31 | y: 1.0 32 | z: 1.0 33 | w: 1.0 34 | } 35 | size { 36 | x: 200.0 37 | y: 45.0 38 | z: 0.0 39 | w: 1.0 40 | } 41 | color { 42 | x: 1.0 43 | y: 1.0 44 | z: 1.0 45 | w: 1.0 46 | } 47 | type: TYPE_BOX 48 | blend_mode: BLEND_MODE_ALPHA 49 | texture: "ui/green_button08" 50 | id: "button" 51 | xanchor: XANCHOR_NONE 52 | yanchor: YANCHOR_NONE 53 | pivot: PIVOT_CENTER 54 | adjust_mode: ADJUST_MODE_FIT 55 | layer: "below" 56 | inherit_alpha: true 57 | slice9 { 58 | x: 8.0 59 | y: 8.0 60 | z: 8.0 61 | w: 8.0 62 | } 63 | clipping_mode: CLIPPING_MODE_NONE 64 | clipping_visible: true 65 | clipping_inverted: false 66 | alpha: 1.0 67 | template_node_child: false 68 | size_mode: SIZE_MODE_MANUAL 69 | } 70 | nodes { 71 | position { 72 | x: 0.0 73 | y: 0.0 74 | z: 0.0 75 | w: 1.0 76 | } 77 | rotation { 78 | x: 0.0 79 | y: 0.0 80 | z: 0.0 81 | w: 1.0 82 | } 83 | scale { 84 | x: 1.0 85 | y: 1.0 86 | z: 1.0 87 | w: 1.0 88 | } 89 | size { 90 | x: 200.0 91 | y: 40.0 92 | z: 0.0 93 | w: 1.0 94 | } 95 | color { 96 | x: 1.0 97 | y: 1.0 98 | z: 1.0 99 | w: 1.0 100 | } 101 | type: TYPE_TEXT 102 | blend_mode: BLEND_MODE_ALPHA 103 | text: "FOOBAR" 104 | font: "example" 105 | id: "label" 106 | xanchor: XANCHOR_NONE 107 | yanchor: YANCHOR_NONE 108 | pivot: PIVOT_CENTER 109 | outline { 110 | x: 1.0 111 | y: 1.0 112 | z: 1.0 113 | w: 1.0 114 | } 115 | shadow { 116 | x: 1.0 117 | y: 1.0 118 | z: 1.0 119 | w: 1.0 120 | } 121 | adjust_mode: ADJUST_MODE_FIT 122 | line_break: false 123 | parent: "button" 124 | layer: "text" 125 | inherit_alpha: true 126 | alpha: 1.0 127 | outline_alpha: 1.0 128 | shadow_alpha: 1.0 129 | template_node_child: false 130 | text_leading: 1.0 131 | text_tracking: 0.0 132 | } 133 | layers { 134 | name: "below" 135 | } 136 | layers { 137 | name: "text" 138 | } 139 | layers { 140 | name: "above" 141 | } 142 | material: "/builtins/materials/gui.material" 143 | adjust_reference: ADJUST_REFERENCE_PARENT 144 | max_nodes: 512 145 | -------------------------------------------------------------------------------- /examples/assets/fonts/example.font: -------------------------------------------------------------------------------- 1 | font: "/builtins/fonts/vera_mo_bd.ttf" 2 | material: "/builtins/fonts/font.material" 3 | size: 15 4 | -------------------------------------------------------------------------------- /examples/assets/images/green_button08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/britzl/defold-websocket/d281d54cea1af42ab6a7e85cfd9e3c2355c4112a/examples/assets/images/green_button08.png -------------------------------------------------------------------------------- /examples/assets/ui.atlas: -------------------------------------------------------------------------------- 1 | images { 2 | image: "/examples/assets/images/green_button08.png" 3 | } 4 | margin: 0 5 | extrude_borders: 2 6 | inner_padding: 0 7 | -------------------------------------------------------------------------------- /examples/websocket.collection: -------------------------------------------------------------------------------- 1 | name: "default" 2 | scale_along_z: 0 3 | embedded_instances { 4 | id: "go" 5 | data: "components {\n" 6 | " id: \"gui\"\n" 7 | " component: \"/examples/websocket.gui\"\n" 8 | " position {\n" 9 | " x: 0.0\n" 10 | " y: 0.0\n" 11 | " z: 0.0\n" 12 | " }\n" 13 | " rotation {\n" 14 | " x: 0.0\n" 15 | " y: 0.0\n" 16 | " z: 0.0\n" 17 | " w: 1.0\n" 18 | " }\n" 19 | "}\n" 20 | "" 21 | position { 22 | x: 0.0 23 | y: 0.0 24 | z: 0.0 25 | } 26 | rotation { 27 | x: 0.0 28 | y: 0.0 29 | z: 0.0 30 | w: 1.0 31 | } 32 | scale3 { 33 | x: 1.0 34 | y: 1.0 35 | z: 1.0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/websocket.gui: -------------------------------------------------------------------------------- 1 | script: "/examples/websocket.gui_script" 2 | fonts { 3 | name: "example" 4 | font: "/examples/assets/fonts/example.font" 5 | } 6 | textures { 7 | name: "ui" 8 | texture: "/examples/assets/ui.atlas" 9 | } 10 | background_color { 11 | x: 0.0 12 | y: 0.0 13 | z: 0.0 14 | w: 1.0 15 | } 16 | nodes { 17 | position { 18 | x: 10.0 19 | y: 1113.0 20 | z: 0.0 21 | w: 1.0 22 | } 23 | rotation { 24 | x: 0.0 25 | y: 0.0 26 | z: 0.0 27 | w: 1.0 28 | } 29 | scale { 30 | x: 1.0 31 | y: 1.0 32 | z: 1.0 33 | w: 1.0 34 | } 35 | size { 36 | x: 620.0 37 | y: 600.0 38 | z: 0.0 39 | w: 1.0 40 | } 41 | color { 42 | x: 1.0 43 | y: 1.0 44 | z: 1.0 45 | w: 1.0 46 | } 47 | type: TYPE_TEXT 48 | blend_mode: BLEND_MODE_ALPHA 49 | text: "" 50 | font: "example" 51 | id: "log" 52 | xanchor: XANCHOR_NONE 53 | yanchor: YANCHOR_NONE 54 | pivot: PIVOT_NW 55 | outline { 56 | x: 1.0 57 | y: 1.0 58 | z: 1.0 59 | w: 1.0 60 | } 61 | shadow { 62 | x: 1.0 63 | y: 1.0 64 | z: 1.0 65 | w: 1.0 66 | } 67 | adjust_mode: ADJUST_MODE_FIT 68 | line_break: true 69 | layer: "" 70 | inherit_alpha: true 71 | alpha: 1.0 72 | outline_alpha: 1.0 73 | shadow_alpha: 1.0 74 | template_node_child: false 75 | text_leading: 1.0 76 | text_tracking: 0.0 77 | } 78 | nodes { 79 | position { 80 | x: 214.0 81 | y: 568.0 82 | z: 0.0 83 | w: 1.0 84 | } 85 | rotation { 86 | x: 0.0 87 | y: 0.0 88 | z: 0.0 89 | w: 1.0 90 | } 91 | scale { 92 | x: 1.0 93 | y: 1.0 94 | z: 1.0 95 | w: 1.0 96 | } 97 | size { 98 | x: 200.0 99 | y: 100.0 100 | z: 0.0 101 | w: 1.0 102 | } 103 | color { 104 | x: 1.0 105 | y: 1.0 106 | z: 1.0 107 | w: 1.0 108 | } 109 | type: TYPE_TEMPLATE 110 | id: "connect_ws" 111 | layer: "" 112 | inherit_alpha: true 113 | alpha: 1.0 114 | template: "/examples/assets/button.gui" 115 | template_node_child: false 116 | } 117 | nodes { 118 | position { 119 | x: 0.0 120 | y: 0.0 121 | z: 0.0 122 | w: 1.0 123 | } 124 | rotation { 125 | x: 0.0 126 | y: 0.0 127 | z: 0.0 128 | w: 1.0 129 | } 130 | scale { 131 | x: 1.0 132 | y: 1.0 133 | z: 1.0 134 | w: 1.0 135 | } 136 | size { 137 | x: 200.0 138 | y: 45.0 139 | z: 0.0 140 | w: 1.0 141 | } 142 | color { 143 | x: 1.0 144 | y: 1.0 145 | z: 1.0 146 | w: 1.0 147 | } 148 | type: TYPE_BOX 149 | blend_mode: BLEND_MODE_ALPHA 150 | texture: "ui/green_button08" 151 | id: "connect_ws/button" 152 | xanchor: XANCHOR_NONE 153 | yanchor: YANCHOR_NONE 154 | pivot: PIVOT_CENTER 155 | adjust_mode: ADJUST_MODE_FIT 156 | parent: "connect_ws" 157 | layer: "below" 158 | inherit_alpha: true 159 | slice9 { 160 | x: 8.0 161 | y: 8.0 162 | z: 8.0 163 | w: 8.0 164 | } 165 | clipping_mode: CLIPPING_MODE_NONE 166 | clipping_visible: true 167 | clipping_inverted: false 168 | alpha: 1.0 169 | template_node_child: true 170 | size_mode: SIZE_MODE_MANUAL 171 | } 172 | nodes { 173 | position { 174 | x: 0.0 175 | y: 0.0 176 | z: 0.0 177 | w: 1.0 178 | } 179 | rotation { 180 | x: 0.0 181 | y: 0.0 182 | z: 0.0 183 | w: 1.0 184 | } 185 | scale { 186 | x: 1.0 187 | y: 1.0 188 | z: 1.0 189 | w: 1.0 190 | } 191 | size { 192 | x: 200.0 193 | y: 40.0 194 | z: 0.0 195 | w: 1.0 196 | } 197 | color { 198 | x: 1.0 199 | y: 1.0 200 | z: 1.0 201 | w: 1.0 202 | } 203 | type: TYPE_TEXT 204 | blend_mode: BLEND_MODE_ALPHA 205 | text: "CONNECT ws://" 206 | font: "example" 207 | id: "connect_ws/label" 208 | xanchor: XANCHOR_NONE 209 | yanchor: YANCHOR_NONE 210 | pivot: PIVOT_CENTER 211 | outline { 212 | x: 1.0 213 | y: 1.0 214 | z: 1.0 215 | w: 1.0 216 | } 217 | shadow { 218 | x: 1.0 219 | y: 1.0 220 | z: 1.0 221 | w: 1.0 222 | } 223 | adjust_mode: ADJUST_MODE_FIT 224 | line_break: false 225 | parent: "connect_ws/button" 226 | layer: "text" 227 | inherit_alpha: true 228 | alpha: 1.0 229 | outline_alpha: 1.0 230 | shadow_alpha: 1.0 231 | overridden_fields: 8 232 | template_node_child: true 233 | text_leading: 1.0 234 | text_tracking: 0.0 235 | } 236 | nodes { 237 | position { 238 | x: 320.0 239 | y: 438.0 240 | z: 0.0 241 | w: 1.0 242 | } 243 | rotation { 244 | x: 0.0 245 | y: 0.0 246 | z: 0.0 247 | w: 1.0 248 | } 249 | scale { 250 | x: 1.0 251 | y: 1.0 252 | z: 1.0 253 | w: 1.0 254 | } 255 | size { 256 | x: 200.0 257 | y: 100.0 258 | z: 0.0 259 | w: 1.0 260 | } 261 | color { 262 | x: 1.0 263 | y: 1.0 264 | z: 1.0 265 | w: 1.0 266 | } 267 | type: TYPE_TEMPLATE 268 | id: "close" 269 | layer: "" 270 | inherit_alpha: true 271 | alpha: 1.0 272 | template: "/examples/assets/button.gui" 273 | template_node_child: false 274 | } 275 | nodes { 276 | position { 277 | x: 0.0 278 | y: 0.0 279 | z: 0.0 280 | w: 1.0 281 | } 282 | rotation { 283 | x: 0.0 284 | y: 0.0 285 | z: 0.0 286 | w: 1.0 287 | } 288 | scale { 289 | x: 1.0 290 | y: 1.0 291 | z: 1.0 292 | w: 1.0 293 | } 294 | size { 295 | x: 200.0 296 | y: 45.0 297 | z: 0.0 298 | w: 1.0 299 | } 300 | color { 301 | x: 1.0 302 | y: 1.0 303 | z: 1.0 304 | w: 1.0 305 | } 306 | type: TYPE_BOX 307 | blend_mode: BLEND_MODE_ALPHA 308 | texture: "ui/green_button08" 309 | id: "close/button" 310 | xanchor: XANCHOR_NONE 311 | yanchor: YANCHOR_NONE 312 | pivot: PIVOT_CENTER 313 | adjust_mode: ADJUST_MODE_FIT 314 | parent: "close" 315 | layer: "below" 316 | inherit_alpha: true 317 | slice9 { 318 | x: 8.0 319 | y: 8.0 320 | z: 8.0 321 | w: 8.0 322 | } 323 | clipping_mode: CLIPPING_MODE_NONE 324 | clipping_visible: true 325 | clipping_inverted: false 326 | alpha: 1.0 327 | template_node_child: true 328 | size_mode: SIZE_MODE_MANUAL 329 | } 330 | nodes { 331 | position { 332 | x: 0.0 333 | y: 0.0 334 | z: 0.0 335 | w: 1.0 336 | } 337 | rotation { 338 | x: 0.0 339 | y: 0.0 340 | z: 0.0 341 | w: 1.0 342 | } 343 | scale { 344 | x: 1.0 345 | y: 1.0 346 | z: 1.0 347 | w: 1.0 348 | } 349 | size { 350 | x: 200.0 351 | y: 40.0 352 | z: 0.0 353 | w: 1.0 354 | } 355 | color { 356 | x: 1.0 357 | y: 1.0 358 | z: 1.0 359 | w: 1.0 360 | } 361 | type: TYPE_TEXT 362 | blend_mode: BLEND_MODE_ALPHA 363 | text: "CLOSE" 364 | font: "example" 365 | id: "close/label" 366 | xanchor: XANCHOR_NONE 367 | yanchor: YANCHOR_NONE 368 | pivot: PIVOT_CENTER 369 | outline { 370 | x: 1.0 371 | y: 1.0 372 | z: 1.0 373 | w: 1.0 374 | } 375 | shadow { 376 | x: 1.0 377 | y: 1.0 378 | z: 1.0 379 | w: 1.0 380 | } 381 | adjust_mode: ADJUST_MODE_FIT 382 | line_break: false 383 | parent: "close/button" 384 | layer: "text" 385 | inherit_alpha: true 386 | alpha: 1.0 387 | outline_alpha: 1.0 388 | shadow_alpha: 1.0 389 | overridden_fields: 8 390 | template_node_child: true 391 | text_leading: 1.0 392 | text_tracking: 0.0 393 | } 394 | nodes { 395 | position { 396 | x: 320.0 397 | y: 503.0 398 | z: 0.0 399 | w: 1.0 400 | } 401 | rotation { 402 | x: 0.0 403 | y: 0.0 404 | z: 0.0 405 | w: 1.0 406 | } 407 | scale { 408 | x: 1.0 409 | y: 1.0 410 | z: 1.0 411 | w: 1.0 412 | } 413 | size { 414 | x: 200.0 415 | y: 100.0 416 | z: 0.0 417 | w: 1.0 418 | } 419 | color { 420 | x: 1.0 421 | y: 1.0 422 | z: 1.0 423 | w: 1.0 424 | } 425 | type: TYPE_TEMPLATE 426 | id: "send" 427 | layer: "" 428 | inherit_alpha: true 429 | alpha: 1.0 430 | template: "/examples/assets/button.gui" 431 | template_node_child: false 432 | } 433 | nodes { 434 | position { 435 | x: 0.0 436 | y: 0.0 437 | z: 0.0 438 | w: 1.0 439 | } 440 | rotation { 441 | x: 0.0 442 | y: 0.0 443 | z: 0.0 444 | w: 1.0 445 | } 446 | scale { 447 | x: 1.0 448 | y: 1.0 449 | z: 1.0 450 | w: 1.0 451 | } 452 | size { 453 | x: 200.0 454 | y: 45.0 455 | z: 0.0 456 | w: 1.0 457 | } 458 | color { 459 | x: 1.0 460 | y: 1.0 461 | z: 1.0 462 | w: 1.0 463 | } 464 | type: TYPE_BOX 465 | blend_mode: BLEND_MODE_ALPHA 466 | texture: "ui/green_button08" 467 | id: "send/button" 468 | xanchor: XANCHOR_NONE 469 | yanchor: YANCHOR_NONE 470 | pivot: PIVOT_CENTER 471 | adjust_mode: ADJUST_MODE_FIT 472 | parent: "send" 473 | layer: "below" 474 | inherit_alpha: true 475 | slice9 { 476 | x: 8.0 477 | y: 8.0 478 | z: 8.0 479 | w: 8.0 480 | } 481 | clipping_mode: CLIPPING_MODE_NONE 482 | clipping_visible: true 483 | clipping_inverted: false 484 | alpha: 1.0 485 | template_node_child: true 486 | size_mode: SIZE_MODE_MANUAL 487 | } 488 | nodes { 489 | position { 490 | x: 0.0 491 | y: 0.0 492 | z: 0.0 493 | w: 1.0 494 | } 495 | rotation { 496 | x: 0.0 497 | y: 0.0 498 | z: 0.0 499 | w: 1.0 500 | } 501 | scale { 502 | x: 1.0 503 | y: 1.0 504 | z: 1.0 505 | w: 1.0 506 | } 507 | size { 508 | x: 200.0 509 | y: 40.0 510 | z: 0.0 511 | w: 1.0 512 | } 513 | color { 514 | x: 1.0 515 | y: 1.0 516 | z: 1.0 517 | w: 1.0 518 | } 519 | type: TYPE_TEXT 520 | blend_mode: BLEND_MODE_ALPHA 521 | text: "SEND" 522 | font: "example" 523 | id: "send/label" 524 | xanchor: XANCHOR_NONE 525 | yanchor: YANCHOR_NONE 526 | pivot: PIVOT_CENTER 527 | outline { 528 | x: 1.0 529 | y: 1.0 530 | z: 1.0 531 | w: 1.0 532 | } 533 | shadow { 534 | x: 1.0 535 | y: 1.0 536 | z: 1.0 537 | w: 1.0 538 | } 539 | adjust_mode: ADJUST_MODE_FIT 540 | line_break: false 541 | parent: "send/button" 542 | layer: "text" 543 | inherit_alpha: true 544 | alpha: 1.0 545 | outline_alpha: 1.0 546 | shadow_alpha: 1.0 547 | overridden_fields: 8 548 | template_node_child: true 549 | text_leading: 1.0 550 | text_tracking: 0.0 551 | } 552 | nodes { 553 | position { 554 | x: 429.0 555 | y: 568.0 556 | z: 0.0 557 | w: 1.0 558 | } 559 | rotation { 560 | x: 0.0 561 | y: 0.0 562 | z: 0.0 563 | w: 1.0 564 | } 565 | scale { 566 | x: 1.0 567 | y: 1.0 568 | z: 1.0 569 | w: 1.0 570 | } 571 | size { 572 | x: 200.0 573 | y: 100.0 574 | z: 0.0 575 | w: 1.0 576 | } 577 | color { 578 | x: 1.0 579 | y: 1.0 580 | z: 1.0 581 | w: 1.0 582 | } 583 | type: TYPE_TEMPLATE 584 | id: "connect_wss" 585 | layer: "" 586 | inherit_alpha: true 587 | alpha: 1.0 588 | template: "/examples/assets/button.gui" 589 | template_node_child: false 590 | } 591 | nodes { 592 | position { 593 | x: 0.0 594 | y: 0.0 595 | z: 0.0 596 | w: 1.0 597 | } 598 | rotation { 599 | x: 0.0 600 | y: 0.0 601 | z: 0.0 602 | w: 1.0 603 | } 604 | scale { 605 | x: 1.0 606 | y: 1.0 607 | z: 1.0 608 | w: 1.0 609 | } 610 | size { 611 | x: 200.0 612 | y: 45.0 613 | z: 0.0 614 | w: 1.0 615 | } 616 | color { 617 | x: 1.0 618 | y: 1.0 619 | z: 1.0 620 | w: 1.0 621 | } 622 | type: TYPE_BOX 623 | blend_mode: BLEND_MODE_ALPHA 624 | texture: "ui/green_button08" 625 | id: "connect_wss/button" 626 | xanchor: XANCHOR_NONE 627 | yanchor: YANCHOR_NONE 628 | pivot: PIVOT_CENTER 629 | adjust_mode: ADJUST_MODE_FIT 630 | parent: "connect_wss" 631 | layer: "below" 632 | inherit_alpha: true 633 | slice9 { 634 | x: 8.0 635 | y: 8.0 636 | z: 8.0 637 | w: 8.0 638 | } 639 | clipping_mode: CLIPPING_MODE_NONE 640 | clipping_visible: true 641 | clipping_inverted: false 642 | alpha: 1.0 643 | template_node_child: true 644 | size_mode: SIZE_MODE_MANUAL 645 | } 646 | nodes { 647 | position { 648 | x: 0.0 649 | y: 0.0 650 | z: 0.0 651 | w: 1.0 652 | } 653 | rotation { 654 | x: 0.0 655 | y: 0.0 656 | z: 0.0 657 | w: 1.0 658 | } 659 | scale { 660 | x: 1.0 661 | y: 1.0 662 | z: 1.0 663 | w: 1.0 664 | } 665 | size { 666 | x: 200.0 667 | y: 40.0 668 | z: 0.0 669 | w: 1.0 670 | } 671 | color { 672 | x: 1.0 673 | y: 1.0 674 | z: 1.0 675 | w: 1.0 676 | } 677 | type: TYPE_TEXT 678 | blend_mode: BLEND_MODE_ALPHA 679 | text: "CONNECT wss://" 680 | font: "example" 681 | id: "connect_wss/label" 682 | xanchor: XANCHOR_NONE 683 | yanchor: YANCHOR_NONE 684 | pivot: PIVOT_CENTER 685 | outline { 686 | x: 1.0 687 | y: 1.0 688 | z: 1.0 689 | w: 1.0 690 | } 691 | shadow { 692 | x: 1.0 693 | y: 1.0 694 | z: 1.0 695 | w: 1.0 696 | } 697 | adjust_mode: ADJUST_MODE_FIT 698 | line_break: false 699 | parent: "connect_wss/button" 700 | layer: "text" 701 | inherit_alpha: true 702 | alpha: 1.0 703 | outline_alpha: 1.0 704 | shadow_alpha: 1.0 705 | overridden_fields: 8 706 | template_node_child: true 707 | text_leading: 1.0 708 | text_tracking: 0.0 709 | } 710 | layers { 711 | name: "below" 712 | } 713 | layers { 714 | name: "dynamic" 715 | } 716 | layers { 717 | name: "text" 718 | } 719 | layers { 720 | name: "above" 721 | } 722 | material: "/builtins/materials/gui.material" 723 | adjust_reference: ADJUST_REFERENCE_PARENT 724 | max_nodes: 512 725 | -------------------------------------------------------------------------------- /examples/websocket.gui_script: -------------------------------------------------------------------------------- 1 | local websocket_async = require "websocket.client_async" 2 | 3 | local function click_button(node, action) 4 | return gui.is_enabled(node) and action.released and gui.pick_node(node, action.x, action.y) 5 | end 6 | 7 | local function update_buttons(self) 8 | if self.ws then 9 | gui.set_enabled(self.connect_ws_node, false) 10 | gui.set_enabled(self.connect_wss_node, false) 11 | gui.set_enabled(self.send_node, true) 12 | gui.set_enabled(self.close_node, true) 13 | else 14 | gui.set_enabled(self.connect_ws_node, true) 15 | gui.set_enabled(self.connect_wss_node, true) 16 | gui.set_enabled(self.send_node, false) 17 | gui.set_enabled(self.close_node, false) 18 | end 19 | end 20 | 21 | local function log(...) 22 | local text = "" 23 | local len = select("#", ...) 24 | for i=1,len do 25 | text = text .. tostring(select(i, ...)) .. (i == len and "" or ", ") 26 | end 27 | 28 | print(text) 29 | local node = gui.get_node("log") 30 | gui.set_text(node, gui.get_text(node) .. "\n" .. text) 31 | end 32 | 33 | function init(self) 34 | msg.post(".", "acquire_input_focus") 35 | msg.post("@render:", "clear_color", { color = vmath.vector4(0.2, 0.4, 0.8, 1.0) }) 36 | self.connect_ws_node = gui.get_node("connect_ws/button") 37 | self.connect_wss_node = gui.get_node("connect_wss/button") 38 | self.send_node = gui.get_node("send/button") 39 | self.close_node = gui.get_node("close/button") 40 | update_buttons(self) 41 | end 42 | 43 | function final(self) 44 | msg.post(".", "release_input_focus") 45 | end 46 | 47 | function update(self, dt) 48 | if self.ws then 49 | self.ws.step() 50 | end 51 | end 52 | 53 | local function connect(self, scheme) 54 | log("Creating async websocket") 55 | self.ws = websocket_async() 56 | self.ws:on_message(function(message) 57 | log("Receiving: '" .. tostring(message) .. "'") 58 | end) 59 | self.ws:on_connected(function(ok, err) 60 | log("Connected", ok, err, type(err)) 61 | if err then 62 | log("on_connected error", err) 63 | self.ws:close() 64 | self.ws = nil 65 | end 66 | update_buttons(self) 67 | end) 68 | self.ws:on_disconnected(function() 69 | log("Disconnected") 70 | self.ws = nil 71 | update_buttons(self) 72 | end) 73 | 74 | local url = nil 75 | local sslparams = nil 76 | if scheme == "ws" then 77 | url = "ws://echo.websocket.org" 78 | --url = "ws://localhost:9999" 79 | else 80 | sslparams = { 81 | mode = "client", 82 | protocol = "tlsv1_2", 83 | verify = "none", 84 | options = "all", 85 | } 86 | url = "wss://echo.websocket.org" 87 | end 88 | 89 | -- which Sec-WebSocket-Protocol to use in the header when doing the handshake/upgrade 90 | -- for now we can only influence the choice of protocol in non-html5 builds 91 | -- in html5 builds it seems like the Sec-WebSocket-Protocol will always be "binary" 92 | local ws_protocol = "binary" 93 | 94 | log("Connecting to " .. url) 95 | self.ws:connect(url, ws_protocol, sslparams) 96 | end 97 | 98 | function on_input(self, action_id, action) 99 | if click_button(self.connect_ws_node, action) then 100 | connect(self, "ws") 101 | elseif click_button(self.connect_wss_node, action) then 102 | connect(self, "wss") 103 | elseif click_button(gui.get_node("send/button"), action) then 104 | local message_to_send = 'sending to server' 105 | local ok, was_clean, code, reason = self.ws:send(message_to_send) 106 | log("Sending '" .. message_to_send .. "'", ok, was_clean, code, reason) 107 | elseif click_button(gui.get_node("close/button"), action) then 108 | log("Closing") 109 | self.ws:close() 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /game.project: -------------------------------------------------------------------------------- 1 | [project] 2 | title = Defold-Websocket 3 | version = 1.0.0 4 | dependencies = https://github.com/britzl/defold-luasocket/archive/0.11.zip,https://github.com/britzl/defold-luasec/archive/1.1.0.zip 5 | 6 | [bootstrap] 7 | main_collection = /examples/websocket.collectionc 8 | 9 | [input] 10 | game_binding = /input/game.input_bindingc 11 | 12 | [display] 13 | width = 640 14 | height = 1136 15 | 16 | [physics] 17 | scale = 0.02 18 | 19 | [script] 20 | shared_state = 1 21 | 22 | [library] 23 | include_dirs = websocket 24 | 25 | [android] 26 | package = com.defold.websocket 27 | 28 | [osx] 29 | bundle_identifier = com.defold.websocket 30 | 31 | [ios] 32 | bundle_identifier = com.defold.websocket 33 | 34 | -------------------------------------------------------------------------------- /input/game.input_binding: -------------------------------------------------------------------------------- 1 | key_trigger { 2 | input: KEY_LEFT 3 | action: "left" 4 | } 5 | key_trigger { 6 | input: KEY_DOWN 7 | action: "down" 8 | } 9 | key_trigger { 10 | input: KEY_RIGHT 11 | action: "right" 12 | } 13 | key_trigger { 14 | input: KEY_UP 15 | action: "up" 16 | } 17 | key_trigger { 18 | input: KEY_Z 19 | action: "jump" 20 | } 21 | key_trigger { 22 | input: KEY_X 23 | action: "fire" 24 | } 25 | key_trigger { 26 | input: KEY_SPACE 27 | action: "jump" 28 | } 29 | mouse_trigger { 30 | input: MOUSE_BUTTON_1 31 | action: "touch" 32 | } 33 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/britzl/defold-websocket/d281d54cea1af42ab6a7e85cfd9e3c2355c4112a/logo.png -------------------------------------------------------------------------------- /tools/SimpleWebSocketServer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The MIT License (MIT) 3 | Copyright (c) 2013 Dave P. 4 | ''' 5 | import sys 6 | VER = sys.version_info[0] 7 | if VER >= 3: 8 | import socketserver 9 | from http.server import BaseHTTPRequestHandler 10 | from io import StringIO, BytesIO 11 | else: 12 | import SocketServer 13 | from BaseHTTPServer import BaseHTTPRequestHandler 14 | from StringIO import StringIO 15 | 16 | import hashlib 17 | import base64 18 | import socket 19 | import struct 20 | import ssl 21 | import errno 22 | import codecs 23 | from collections import deque 24 | from select import select 25 | 26 | __all__ = ['WebSocket', 27 | 'SimpleWebSocketServer', 28 | 'SimpleSSLWebSocketServer'] 29 | 30 | def _check_unicode(val): 31 | if VER >= 3: 32 | return isinstance(val, str) 33 | else: 34 | return isinstance(val, unicode) 35 | 36 | class HTTPRequest(BaseHTTPRequestHandler): 37 | def __init__(self, request_text): 38 | if VER >= 3: 39 | self.rfile = BytesIO(request_text) 40 | else: 41 | self.rfile = StringIO(request_text) 42 | self.raw_requestline = self.rfile.readline() 43 | self.error_code = self.error_message = None 44 | self.parse_request() 45 | 46 | _VALID_STATUS_CODES = [1000, 1001, 1002, 1003, 1007, 1008, 47 | 1009, 1010, 1011, 3000, 3999, 4000, 4999] 48 | 49 | HANDSHAKE_STR = ( 50 | "HTTP/1.1 101 Switching Protocols\r\n" 51 | "Upgrade: WebSocket\r\n" 52 | "Connection: Upgrade\r\n" 53 | "Sec-WebSocket-Accept: %(acceptstr)s\r\n" 54 | "Sec-WebSocket-Protocol: %(protocolstr)s\r\n\r\n" 55 | ) 56 | 57 | GUID_STR = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 58 | 59 | STREAM = 0x0 60 | TEXT = 0x1 61 | BINARY = 0x2 62 | CLOSE = 0x8 63 | PING = 0x9 64 | PONG = 0xA 65 | 66 | HEADERB1 = 1 67 | HEADERB2 = 3 68 | LENGTHSHORT = 4 69 | LENGTHLONG = 5 70 | MASK = 6 71 | PAYLOAD = 7 72 | 73 | MAXHEADER = 65536 74 | MAXPAYLOAD = 33554432 75 | 76 | class WebSocket(object): 77 | 78 | def __init__(self, server, sock, address): 79 | self.server = server 80 | self.client = sock 81 | self.address = address 82 | 83 | self.handshaked = False 84 | self.headerbuffer = bytearray() 85 | self.headertoread = 2048 86 | 87 | self.fin = 0 88 | self.data = bytearray() 89 | self.opcode = 0 90 | self.hasmask = 0 91 | self.maskarray = None 92 | self.length = 0 93 | self.lengtharray = None 94 | self.index = 0 95 | self.request = None 96 | self.usingssl = False 97 | 98 | self.frag_start = False 99 | self.frag_type = BINARY 100 | self.frag_buffer = None 101 | self.frag_decoder = codecs.getincrementaldecoder('utf-8')(errors='strict') 102 | self.closed = False 103 | self.sendq = deque() 104 | 105 | self.state = HEADERB1 106 | 107 | # restrict the size of header and payload for security reasons 108 | self.maxheader = MAXHEADER 109 | self.maxpayload = MAXPAYLOAD 110 | 111 | def handleMessage(self): 112 | """ 113 | Called when websocket frame is received. 114 | To access the frame data call self.data. 115 | 116 | If the frame is Text then self.data is a unicode object. 117 | If the frame is Binary then self.data is a bytearray object. 118 | """ 119 | pass 120 | 121 | def handleConnected(self): 122 | """ 123 | Called when a websocket client connects to the server. 124 | """ 125 | pass 126 | 127 | def handleClose(self): 128 | """ 129 | Called when a websocket server gets a Close frame from a client. 130 | """ 131 | pass 132 | 133 | def _handlePacket(self): 134 | if self.opcode == CLOSE: 135 | pass 136 | elif self.opcode == STREAM: 137 | pass 138 | elif self.opcode == TEXT: 139 | pass 140 | elif self.opcode == BINARY: 141 | pass 142 | elif self.opcode == PONG or self.opcode == PING: 143 | if len(self.data) > 125: 144 | raise Exception('control frame length can not be > 125') 145 | else: 146 | # unknown or reserved opcode so just close 147 | raise Exception('unknown opcode') 148 | 149 | if self.opcode == CLOSE: 150 | status = 1000 151 | reason = u'' 152 | length = len(self.data) 153 | 154 | if length == 0: 155 | pass 156 | elif length >= 2: 157 | status = struct.unpack_from('!H', self.data[:2])[0] 158 | reason = self.data[2:] 159 | 160 | if status not in _VALID_STATUS_CODES: 161 | status = 1002 162 | 163 | if len(reason) > 0: 164 | try: 165 | reason = reason.decode('utf8', errors='strict') 166 | except: 167 | status = 1002 168 | else: 169 | status = 1002 170 | 171 | self.close(status, reason) 172 | return 173 | 174 | elif self.fin == 0: 175 | if self.opcode != STREAM: 176 | if self.opcode == PING or self.opcode == PONG: 177 | raise Exception('control messages can not be fragmented') 178 | 179 | self.frag_type = self.opcode 180 | self.frag_start = True 181 | self.frag_decoder.reset() 182 | 183 | if self.frag_type == TEXT: 184 | self.frag_buffer = [] 185 | utf_str = self.frag_decoder.decode(self.data, final = False) 186 | if utf_str: 187 | self.frag_buffer.append(utf_str) 188 | else: 189 | self.frag_buffer = bytearray() 190 | self.frag_buffer.extend(self.data) 191 | 192 | else: 193 | if self.frag_start is False: 194 | raise Exception('fragmentation protocol error') 195 | 196 | if self.frag_type == TEXT: 197 | utf_str = self.frag_decoder.decode(self.data, final = False) 198 | if utf_str: 199 | self.frag_buffer.append(utf_str) 200 | else: 201 | self.frag_buffer.extend(self.data) 202 | 203 | else: 204 | if self.opcode == STREAM: 205 | if self.frag_start is False: 206 | raise Exception('fragmentation protocol error') 207 | 208 | if self.frag_type == TEXT: 209 | utf_str = self.frag_decoder.decode(self.data, final = True) 210 | self.frag_buffer.append(utf_str) 211 | self.data = u''.join(self.frag_buffer) 212 | else: 213 | self.frag_buffer.extend(self.data) 214 | self.data = self.frag_buffer 215 | 216 | self.handleMessage() 217 | 218 | self.frag_decoder.reset() 219 | self.frag_type = BINARY 220 | self.frag_start = False 221 | self.frag_buffer = None 222 | 223 | elif self.opcode == PING: 224 | self._sendMessage(False, PONG, self.data) 225 | 226 | elif self.opcode == PONG: 227 | pass 228 | 229 | else: 230 | if self.frag_start is True: 231 | raise Exception('fragmentation protocol error') 232 | 233 | if self.opcode == TEXT: 234 | try: 235 | self.data = self.data.decode('utf8', errors='strict') 236 | except Exception as exp: 237 | raise Exception('invalid utf-8 payload') 238 | 239 | self.handleMessage() 240 | 241 | 242 | def _handleData(self): 243 | # do the HTTP header and handshake 244 | if self.handshaked is False: 245 | 246 | data = self.client.recv(self.headertoread) 247 | if not data: 248 | raise Exception('remote socket closed') 249 | 250 | else: 251 | # accumulate 252 | self.headerbuffer.extend(data) 253 | 254 | if len(self.headerbuffer) >= self.maxheader: 255 | raise Exception('header exceeded allowable size') 256 | 257 | # indicates end of HTTP header 258 | if b'\r\n\r\n' in self.headerbuffer: 259 | self.request = HTTPRequest(self.headerbuffer) 260 | 261 | # handshake rfc 6455 262 | try: 263 | key = self.request.headers['Sec-WebSocket-Key'] 264 | k = key.encode('ascii') + GUID_STR.encode('ascii') 265 | k_s = base64.b64encode(hashlib.sha1(k).digest()).decode('ascii') 266 | hStr = HANDSHAKE_STR % {'acceptstr': k_s, 'protocolstr': self.request.headers['Sec-WebSocket-Protocol']} 267 | self.sendq.append((BINARY, hStr.encode('ascii'))) 268 | self.handshaked = True 269 | self.handleConnected() 270 | except Exception as e: 271 | raise Exception('handshake failed: %s', str(e)) 272 | 273 | # else do normal data 274 | else: 275 | data = self.client.recv(8192) 276 | if not data: 277 | raise Exception("remote socket closed") 278 | 279 | if VER >= 3: 280 | for d in data: 281 | self._parseMessage(d) 282 | else: 283 | for d in data: 284 | self._parseMessage(ord(d)) 285 | 286 | def close(self, status = 1000, reason = u''): 287 | """ 288 | Send Close frame to the client. The underlying socket is only closed 289 | when the client acknowledges the Close frame. 290 | 291 | status is the closing identifier. 292 | reason is the reason for the close. 293 | """ 294 | try: 295 | if self.closed is False: 296 | close_msg = bytearray() 297 | close_msg.extend(struct.pack("!H", status)) 298 | if _check_unicode(reason): 299 | close_msg.extend(reason.encode('utf-8')) 300 | else: 301 | close_msg.extend(reason) 302 | 303 | self._sendMessage(False, CLOSE, close_msg) 304 | 305 | finally: 306 | self.closed = True 307 | 308 | 309 | def _sendBuffer(self, buff): 310 | size = len(buff) 311 | tosend = size 312 | already_sent = 0 313 | 314 | while tosend > 0: 315 | try: 316 | # i should be able to send a bytearray 317 | sent = self.client.send(buff[already_sent:]) 318 | if sent == 0: 319 | raise RuntimeError('socket connection broken') 320 | 321 | already_sent += sent 322 | tosend -= sent 323 | 324 | except socket.error as e: 325 | # if we have full buffers then wait for them to drain and try again 326 | if e.errno in [errno.EAGAIN, errno.EWOULDBLOCK]: 327 | return buff[already_sent:] 328 | else: 329 | raise e 330 | 331 | return None 332 | 333 | def sendFragmentStart(self, data): 334 | """ 335 | Send the start of a data fragment stream to a websocket client. 336 | Subsequent data should be sent using sendFragment(). 337 | A fragment stream is completed when sendFragmentEnd() is called. 338 | 339 | If data is a unicode object then the frame is sent as Text. 340 | If the data is a bytearray object then the frame is sent as Binary. 341 | """ 342 | opcode = BINARY 343 | if _check_unicode(data): 344 | opcode = TEXT 345 | self._sendMessage(True, opcode, data) 346 | 347 | def sendFragment(self, data): 348 | """ 349 | see sendFragmentStart() 350 | 351 | If data is a unicode object then the frame is sent as Text. 352 | If the data is a bytearray object then the frame is sent as Binary. 353 | """ 354 | self._sendMessage(True, STREAM, data) 355 | 356 | def sendFragmentEnd(self, data): 357 | """ 358 | see sendFragmentEnd() 359 | 360 | If data is a unicode object then the frame is sent as Text. 361 | If the data is a bytearray object then the frame is sent as Binary. 362 | """ 363 | self._sendMessage(False, STREAM, data) 364 | 365 | def sendMessage(self, data): 366 | """ 367 | Send websocket data frame to the client. 368 | 369 | If data is a unicode object then the frame is sent as Text. 370 | If the data is a bytearray object then the frame is sent as Binary. 371 | """ 372 | opcode = BINARY 373 | if _check_unicode(data): 374 | opcode = TEXT 375 | self._sendMessage(False, opcode, data) 376 | 377 | 378 | def _sendMessage(self, fin, opcode, data): 379 | 380 | payload = bytearray() 381 | 382 | b1 = 0 383 | b2 = 0 384 | if fin is False: 385 | b1 |= 0x80 386 | b1 |= opcode 387 | 388 | if _check_unicode(data): 389 | data = data.encode('utf-8') 390 | 391 | length = len(data) 392 | payload.append(b1) 393 | 394 | if length <= 125: 395 | b2 |= length 396 | payload.append(b2) 397 | 398 | elif length >= 126 and length <= 65535: 399 | b2 |= 126 400 | payload.append(b2) 401 | payload.extend(struct.pack("!H", length)) 402 | 403 | else: 404 | b2 |= 127 405 | payload.append(b2) 406 | payload.extend(struct.pack("!Q", length)) 407 | 408 | if length > 0: 409 | payload.extend(data) 410 | 411 | self.sendq.append((opcode, payload)) 412 | 413 | 414 | def _parseMessage(self, byte): 415 | # read in the header 416 | if self.state == HEADERB1: 417 | 418 | self.fin = byte & 0x80 419 | self.opcode = byte & 0x0F 420 | self.state = HEADERB2 421 | 422 | self.index = 0 423 | self.length = 0 424 | self.lengtharray = bytearray() 425 | self.data = bytearray() 426 | 427 | rsv = byte & 0x70 428 | if rsv != 0: 429 | raise Exception('RSV bit must be 0') 430 | 431 | elif self.state == HEADERB2: 432 | mask = byte & 0x80 433 | length = byte & 0x7F 434 | 435 | if self.opcode == PING and length > 125: 436 | raise Exception('ping packet is too large') 437 | 438 | if mask == 128: 439 | self.hasmask = True 440 | else: 441 | self.hasmask = False 442 | 443 | if length <= 125: 444 | self.length = length 445 | 446 | # if we have a mask we must read it 447 | if self.hasmask is True: 448 | self.maskarray = bytearray() 449 | self.state = MASK 450 | else: 451 | # if there is no mask and no payload we are done 452 | if self.length <= 0: 453 | try: 454 | self._handlePacket() 455 | finally: 456 | self.state = self.HEADERB1 457 | self.data = bytearray() 458 | 459 | # we have no mask and some payload 460 | else: 461 | #self.index = 0 462 | self.data = bytearray() 463 | self.state = PAYLOAD 464 | 465 | elif length == 126: 466 | self.lengtharray = bytearray() 467 | self.state = LENGTHSHORT 468 | 469 | elif length == 127: 470 | self.lengtharray = bytearray() 471 | self.state = LENGTHLONG 472 | 473 | 474 | elif self.state == LENGTHSHORT: 475 | self.lengtharray.append(byte) 476 | 477 | if len(self.lengtharray) > 2: 478 | raise Exception('short length exceeded allowable size') 479 | 480 | if len(self.lengtharray) == 2: 481 | self.length = struct.unpack_from('!H', self.lengtharray)[0] 482 | 483 | if self.hasmask is True: 484 | self.maskarray = bytearray() 485 | self.state = MASK 486 | else: 487 | # if there is no mask and no payload we are done 488 | if self.length <= 0: 489 | try: 490 | self._handlePacket() 491 | finally: 492 | self.state = HEADERB1 493 | self.data = bytearray() 494 | 495 | # we have no mask and some payload 496 | else: 497 | #self.index = 0 498 | self.data = bytearray() 499 | self.state = PAYLOAD 500 | 501 | elif self.state == LENGTHLONG: 502 | 503 | self.lengtharray.append(byte) 504 | 505 | if len(self.lengtharray) > 8: 506 | raise Exception('long length exceeded allowable size') 507 | 508 | if len(self.lengtharray) == 8: 509 | self.length = struct.unpack_from('!Q', self.lengtharray)[0] 510 | 511 | if self.hasmask is True: 512 | self.maskarray = bytearray() 513 | self.state = MASK 514 | else: 515 | # if there is no mask and no payload we are done 516 | if self.length <= 0: 517 | try: 518 | self._handlePacket() 519 | finally: 520 | self.state = HEADERB1 521 | self.data = bytearray() 522 | 523 | # we have no mask and some payload 524 | else: 525 | #self.index = 0 526 | self.data = bytearray() 527 | self.state = PAYLOAD 528 | 529 | # MASK STATE 530 | elif self.state == MASK: 531 | self.maskarray.append(byte) 532 | 533 | if len(self.maskarray) > 4: 534 | raise Exception('mask exceeded allowable size') 535 | 536 | if len(self.maskarray) == 4: 537 | # if there is no mask and no payload we are done 538 | if self.length <= 0: 539 | try: 540 | self._handlePacket() 541 | finally: 542 | self.state = HEADERB1 543 | self.data = bytearray() 544 | 545 | # we have no mask and some payload 546 | else: 547 | #self.index = 0 548 | self.data = bytearray() 549 | self.state = PAYLOAD 550 | 551 | # PAYLOAD STATE 552 | elif self.state == PAYLOAD: 553 | if self.hasmask is True: 554 | self.data.append( byte ^ self.maskarray[self.index % 4] ) 555 | else: 556 | self.data.append( byte ) 557 | 558 | # if length exceeds allowable size then we except and remove the connection 559 | if len(self.data) >= self.maxpayload: 560 | raise Exception('payload exceeded allowable size') 561 | 562 | # check if we have processed length bytes; if so we are done 563 | if (self.index+1) == self.length: 564 | try: 565 | self._handlePacket() 566 | finally: 567 | #self.index = 0 568 | self.state = HEADERB1 569 | self.data = bytearray() 570 | else: 571 | self.index += 1 572 | 573 | 574 | class SimpleWebSocketServer(object): 575 | def __init__(self, host, port, websocketclass, selectInterval = 0.1): 576 | self.websocketclass = websocketclass 577 | self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 578 | self.serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 579 | self.serversocket.bind((host, port)) 580 | self.serversocket.listen(5) 581 | self.selectInterval = selectInterval 582 | self.connections = {} 583 | self.listeners = [self.serversocket] 584 | 585 | def _decorateSocket(self, sock): 586 | return sock 587 | 588 | def _constructWebSocket(self, sock, address): 589 | return self.websocketclass(self, sock, address) 590 | 591 | def close(self): 592 | self.serversocket.close() 593 | 594 | for desc, conn in self.connections.items(): 595 | conn.close() 596 | conn.handleClose() 597 | 598 | 599 | def serveforever(self): 600 | while True: 601 | writers = [] 602 | for fileno in self.listeners: 603 | if fileno == self.serversocket: 604 | continue 605 | client = self.connections[fileno] 606 | if client.sendq: 607 | writers.append(fileno) 608 | 609 | if self.selectInterval: 610 | rList, wList, xList = select(self.listeners, writers, self.listeners, self.selectInterval) 611 | else: 612 | rList, wList, xList = select(self.listeners, writers, self.listeners) 613 | 614 | for ready in wList: 615 | client = self.connections[ready] 616 | try: 617 | while client.sendq: 618 | opcode, payload = client.sendq.popleft() 619 | remaining = client._sendBuffer(payload) 620 | if remaining is not None: 621 | client.sendq.appendleft((opcode, remaining)) 622 | break 623 | else: 624 | if opcode == CLOSE: 625 | raise Exception('received client close') 626 | 627 | except Exception as n: 628 | client.client.close() 629 | client.handleClose() 630 | del self.connections[ready] 631 | self.listeners.remove(ready) 632 | 633 | for ready in rList: 634 | if ready == self.serversocket: 635 | try: 636 | sock, address = self.serversocket.accept() 637 | newsock = self._decorateSocket(sock) 638 | newsock.setblocking(0) 639 | fileno = newsock.fileno() 640 | self.connections[fileno] = self._constructWebSocket(newsock, address) 641 | self.listeners.append(fileno) 642 | except Exception as n: 643 | if sock is not None: 644 | sock.close() 645 | else: 646 | if ready not in self.connections: 647 | continue 648 | client = self.connections[ready] 649 | try: 650 | client._handleData() 651 | except Exception as n: 652 | client.client.close() 653 | client.handleClose() 654 | del self.connections[ready] 655 | self.listeners.remove(ready) 656 | 657 | for failed in xList: 658 | if failed == self.serversocket: 659 | self.close() 660 | raise Exception('server socket failed') 661 | else: 662 | if failed not in self.connections: 663 | continue 664 | client = self.connections[failed] 665 | client.client.close() 666 | client.handleClose() 667 | del self.connections[failed] 668 | self.listeners.remove(failed) 669 | 670 | 671 | class SimpleSSLWebSocketServer(SimpleWebSocketServer): 672 | 673 | def __init__(self, host, port, websocketclass, certfile, 674 | keyfile, version = ssl.PROTOCOL_TLSv1, selectInterval = 0.1): 675 | 676 | SimpleWebSocketServer.__init__(self, host, port, 677 | websocketclass, selectInterval) 678 | 679 | self.context = ssl.SSLContext(version) 680 | self.context.load_cert_chain(certfile, keyfile) 681 | 682 | def close(self): 683 | super(SimpleSSLWebSocketServer, self).close() 684 | 685 | def _decorateSocket(self, sock): 686 | sslsock = self.context.wrap_socket(sock, server_side=True) 687 | return sslsock 688 | 689 | def _constructWebSocket(self, sock, address): 690 | ws = self.websocketclass(self, sock, address) 691 | ws.usingssl = True 692 | return ws 693 | 694 | def serveforever(self): 695 | super(SimpleSSLWebSocketServer, self).serveforever() 696 | -------------------------------------------------------------------------------- /tools/websocketserver.py: -------------------------------------------------------------------------------- 1 | from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket 2 | 3 | class SimpleEcho(WebSocket): 4 | 5 | def handleMessage(self): 6 | # echo message back to client 7 | print("Received message") 8 | self.sendMessage(self.data) 9 | 10 | def handleConnected(self): 11 | print self.address, 'connected' 12 | 13 | def handleClose(self): 14 | print self.address, 'closed' 15 | 16 | server = SimpleWebSocketServer('', 9999, SimpleEcho) 17 | server.serveforever() 18 | -------------------------------------------------------------------------------- /websocket/bit.lua: -------------------------------------------------------------------------------- 1 | return _G.bit -------------------------------------------------------------------------------- /websocket/client_async.lua: -------------------------------------------------------------------------------- 1 | local socket = require'socket.socket' 2 | local sync = require'websocket.sync' 3 | local tools = require'websocket.tools' 4 | 5 | local coxpcall = require "websocket.coxpcall" 6 | 7 | local VANILLA_LUA51 = _VERSION == "Lua 5.1" and not jit 8 | local pcall = VANILLA_LUA51 and coxpcall.pcall or pcall 9 | local corunning = VANILLA_LUA51 and coxpcall.running or coroutine.running 10 | 11 | local emscripten = sys.get_sys_info().system_name == "HTML5" 12 | 13 | if emscripten then 14 | -- avoid mixed content warning if trying to access wss resource from http page 15 | -- https://github.com/britzl/defold-websocket/issues/8 16 | -- https://github.com/kripken/emscripten/pull/6960 17 | html5.run([[ 18 | Module['websocket'].url = window['location']['protocol'].replace('http', 'ws') + '//'; 19 | ]]) 20 | end 21 | 22 | local new = function(config) 23 | config = config or {} 24 | local self = {} 25 | 26 | 27 | 28 | local on_connected_fn 29 | local on_disconnected_fn 30 | local on_message_fn 31 | 32 | local function pcall_and_print(fn, ...) 33 | local ok, err = pcall(fn, ...) 34 | if not ok then 35 | print(err) 36 | end 37 | end 38 | 39 | local function on_connected(ok, err) 40 | if on_connected_fn then 41 | if ok then err = nil end 42 | pcall_and_print(on_connected_fn, ok, err) 43 | end 44 | end 45 | 46 | local function on_message(message, err) 47 | if on_message_fn then 48 | if message then err = nil end 49 | pcall_and_print(on_message_fn, message, err) 50 | end 51 | end 52 | 53 | local function on_disconnected() 54 | if on_disconnected_fn then 55 | pcall_and_print(on_disconnected_fn) 56 | end 57 | end 58 | 59 | -- this must be defined before calling sync.extend() 60 | self.sock_connect = function(self, host, port) 61 | assert(corunning(), "You must call the connect function from a coroutine") 62 | local addrinfo = socket.dns.getaddrinfo(host) 63 | local connect_ts = socket.gettime() 64 | for _,info in pairs(addrinfo or {}) do 65 | if info.family == "inet6" then 66 | self.sock = socket.tcp6() 67 | else 68 | self.sock = socket.tcp() 69 | end 70 | self.sock:settimeout(0) 71 | self.sock:connect(host,port) 72 | 73 | local sendt = { self.sock } 74 | -- start polling for successful connection or error 75 | while true do 76 | local receive_ready, send_ready, err = socket.select(nil, sendt, 0) 77 | -- check for connection timeout if one has been configured 78 | -- this is particularly important on HTML5 when trying to connect 79 | -- to a server that is offline since the sock:connect() won't fail 80 | -- and `err` will be "timeout" indefinitely 81 | if config.connect_timeout and (socket.gettime() - connect_ts > config.connect_timeout) then 82 | break 83 | elseif err == "timeout" then 84 | coroutine.yield() 85 | elseif err then 86 | break 87 | elseif #send_ready == 1 then 88 | return true 89 | end 90 | end 91 | end 92 | self.sock = nil 93 | return nil, "Unable to connect" 94 | end 95 | 96 | -- this must be defined before calling sync.extend() 97 | self.sock_send = function(self, data, i, j) 98 | assert(corunning(), "You must call the send function from a coroutine") 99 | local sent = 0 100 | i = i or 1 101 | j = j or #data 102 | while i < j do 103 | self.sock:settimeout(0) 104 | local bytes_sent, err = self.sock:send(data, i, j) 105 | if err == "timeout" or err == "wantwrite" then 106 | coroutine.yield() 107 | elseif err then 108 | return nil, err 109 | else 110 | coroutine.yield() 111 | end 112 | i = i + bytes_sent 113 | sent = sent + bytes_sent 114 | end 115 | return sent 116 | end 117 | 118 | -- this must be defined before calling sync.extend() 119 | self.sock_receive = function(self, pattern, prefix) 120 | assert(corunning(), "You must call the receive function from a coroutine") 121 | prefix = prefix or "" 122 | local data, err 123 | repeat 124 | self.sock:settimeout(0) 125 | data, err, prefix = self.sock:receive(pattern, prefix) 126 | local timeout = (err == "timeout") or (err == "wantread") 127 | if timeout then 128 | coroutine.yield() 129 | end 130 | until data or (err and not timeout) 131 | return data, err, prefix 132 | end 133 | 134 | -- this must be defined before calling sync.extend() 135 | self.sock_close = function(self) 136 | if self.state ~= "CLOSED" then 137 | self.state = "CLOSED" 138 | on_disconnected() 139 | end 140 | 141 | -- doing a call to shutdown in HTML5 will result in 142 | -- "unsupported socketcall syscall 13" 143 | -- https://github.com/britzl/defold-websocket/issues/7 144 | -- to be honest not really sure shutdown is needed at all since the 145 | -- socket is closed... 146 | -- removed here: https://github.com/lipp/lua-websockets/blob/master/src/websocket/client_sync.lua#L30 147 | -- but not here: https://github.com/lipp/lua-websockets/blob/master/src/websocket/client_copas.lua#L32 148 | if not emscripten then 149 | self.sock:shutdown() 150 | end 151 | 152 | self.sock:close() 153 | end 154 | 155 | self = sync.extend(self) 156 | 157 | local coroutines = {} 158 | 159 | local sync_connect = self.connect 160 | local sync_send = self.send 161 | local sync_receive = self.receive 162 | local sync_close = self.close 163 | 164 | local function start_on_message_loop() 165 | local co = coroutine.create(function() 166 | while self.sock and self.state == "OPEN" do 167 | if emscripten then 168 | -- I haven't figured out how to know the length of the received data 169 | -- receiving with a pattern of "*a" or "*l" will block indefinitely 170 | -- A message is read as chunks of data at a time, concatenating it as 171 | -- it is received and repeated until an error 172 | local chunk_size = 1024 173 | local data, err, partial 174 | repeat 175 | self.sock:settimeout(0) 176 | local bytes_to_read = data and (#data + chunk_size) or chunk_size 177 | data, err, partial = self.sock:receive(bytes_to_read, data) 178 | if partial and partial ~= "" then 179 | data = partial 180 | end 181 | coroutine.yield() 182 | until err 183 | if data then 184 | on_message(data) 185 | end 186 | if err == "closed" then 187 | self:sock_close() 188 | end 189 | else 190 | local message, opcode, was_clean, code, reason = sync_receive(self) 191 | -- listen for PING opcode and reply with PONG 192 | if opcode == 0x9 then 193 | self.send(self, message, 0xA) 194 | elseif message then 195 | on_message(message) 196 | end 197 | end 198 | end 199 | coroutine.yield() 200 | end) 201 | coroutines[co] = "on_message" 202 | end 203 | 204 | -- monkeypatch self.connect 205 | self.connect = function(...) 206 | local co = coroutine.create(function(self, ws_url, ws_protocol, ssl_params) 207 | if emscripten then 208 | local scheme, host, port, uri = tools.parse_url(ws_url) 209 | local ok, err = self:sock_connect(host .. uri, port) 210 | if ok then 211 | self.state = "OPEN" 212 | end 213 | on_connected(ok, err) 214 | else 215 | local ok, err_or_protocol, headers 216 | local pcall_ok, pcall_err = pcall(function() 217 | ok, err_or_protocol, headers = sync_connect(self, ws_url, ws_protocol, ssl_params) 218 | end) 219 | on_connected(pcall_ok and ok, pcall_err or err_or_protocol) 220 | end 221 | start_on_message_loop() 222 | end) 223 | coroutines[co] = "connect" 224 | coroutine.resume(co, ...) 225 | end 226 | 227 | -- monkeypatch self.send 228 | self.send = function(self,data,opcode) 229 | local co = coroutine.create(function(self,data,opcode) 230 | if emscripten then 231 | local bytes, err = self.sock_send(self,data) 232 | if err or #data ~= bytes then 233 | print(err or "Didn't send all bytes") 234 | self:sock_close() 235 | end 236 | else 237 | local ok,was_clean,code,reason = sync_send(self,data,opcode) 238 | if not ok then 239 | print(reason) 240 | end 241 | end 242 | end) 243 | coroutines[co] = "send" 244 | coroutine.resume(co, self,data,opcode) 245 | end 246 | 247 | -- monkeypatch self.receive 248 | self.receive = function(...) 249 | local co = coroutine.create(function(...) 250 | if emscripten then 251 | local data, err = self.sock_receive(...) 252 | if not data or err then 253 | self:sock_close() 254 | else 255 | on_message(data) 256 | end 257 | else 258 | local message, opcode, was_clean, code, reason = sync_receive(...) 259 | if message then 260 | on_message(message) 261 | end 262 | end 263 | end) 264 | coroutines[co] = "receive" 265 | coroutine.resume(co, ...) 266 | end 267 | 268 | -- monkeypatch self.close 269 | self.close = function(...) 270 | local co = coroutine.create(function(...) 271 | if emscripten then 272 | self.sock_close(...) 273 | else 274 | sync_close(...) 275 | end 276 | end) 277 | coroutines[co] = "close" 278 | coroutine.resume(co, ...) 279 | end 280 | 281 | 282 | 283 | self.step = function(self) 284 | for co,action in pairs(coroutines) do 285 | local status = coroutine.status(co) 286 | if status == "suspended" then 287 | coroutine.resume(co) 288 | elseif status == "dead" then 289 | coroutines[co] = nil 290 | end 291 | end 292 | end 293 | 294 | self.on_message = function(self, fn) 295 | on_message_fn = fn 296 | end 297 | 298 | self.on_connected = function(self, fn) 299 | on_connected_fn = fn 300 | end 301 | 302 | self.on_disconnected = function(self, fn) 303 | on_disconnected_fn = fn 304 | end 305 | 306 | return self 307 | end 308 | 309 | return new 310 | -------------------------------------------------------------------------------- /websocket/coxpcall.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -- Coroutine safe xpcall and pcall versions 3 | -- 4 | -- Encapsulates the protected calls with a coroutine based loop, so errors can 5 | -- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines 6 | -- yielding inside the call to pcall or xpcall. 7 | -- 8 | -- Authors: Roberto Ierusalimschy and Andre Carregal 9 | -- Contributors: Thomas Harning Jr., Ignacio Burgueño, Fabio Mascarenhas 10 | -- 11 | -- Copyright 2005 - Kepler Project 12 | -- 13 | -- $Id: coxpcall.lua,v 1.13 2008/05/19 19:20:02 mascarenhas Exp $ 14 | ------------------------------------------------------------------------------- 15 | 16 | ------------------------------------------------------------------------------- 17 | -- Checks if (x)pcall function is coroutine safe 18 | ------------------------------------------------------------------------------- 19 | local function isCoroutineSafe(func) 20 | local co = coroutine.create(function() 21 | return func(coroutine.yield, function() end) 22 | end) 23 | 24 | coroutine.resume(co) 25 | return coroutine.resume(co) 26 | end 27 | 28 | -- No need to do anything if pcall and xpcall are already safe. 29 | if isCoroutineSafe(pcall) and isCoroutineSafe(xpcall) then 30 | copcall = pcall 31 | coxpcall = xpcall 32 | return { pcall = pcall, xpcall = xpcall, running = coroutine.running } 33 | end 34 | 35 | ------------------------------------------------------------------------------- 36 | -- Implements xpcall with coroutines 37 | ------------------------------------------------------------------------------- 38 | local performResume, handleReturnValue 39 | local oldpcall, oldxpcall = pcall, xpcall 40 | local pack = table.pack or function(...) return {n = select("#", ...), ...} end 41 | local unpack = table.unpack or unpack 42 | local running = coroutine.running 43 | local coromap = setmetatable({}, { __mode = "k" }) 44 | 45 | function handleReturnValue(err, co, status, ...) 46 | if not status then 47 | return false, err(debug.traceback(co, (...)), ...) 48 | end 49 | if coroutine.status(co) == 'suspended' then 50 | return performResume(err, co, coroutine.yield(...)) 51 | else 52 | return true, ... 53 | end 54 | end 55 | 56 | function performResume(err, co, ...) 57 | return handleReturnValue(err, co, coroutine.resume(co, ...)) 58 | end 59 | 60 | local function id(trace, ...) 61 | return trace 62 | end 63 | 64 | function coxpcall(f, err, ...) 65 | local current = running() 66 | if not current then 67 | if err == id then 68 | return oldpcall(f, ...) 69 | else 70 | if select("#", ...) > 0 then 71 | local oldf, params = f, pack(...) 72 | f = function() return oldf(unpack(params, 1, params.n)) end 73 | end 74 | return oldxpcall(f, err) 75 | end 76 | else 77 | local res, co = oldpcall(coroutine.create, f) 78 | if not res then 79 | local newf = function(...) return f(...) end 80 | co = coroutine.create(newf) 81 | end 82 | coromap[co] = current 83 | return performResume(err, co, ...) 84 | end 85 | end 86 | 87 | local function corunning(coro) 88 | if coro ~= nil then 89 | assert(type(coro)=="thread", "Bad argument; expected thread, got: "..type(coro)) 90 | else 91 | coro = running() 92 | end 93 | while coromap[coro] do 94 | coro = coromap[coro] 95 | end 96 | if coro == "mainthread" then return nil end 97 | return coro 98 | end 99 | 100 | ------------------------------------------------------------------------------- 101 | -- Implements pcall with coroutines 102 | ------------------------------------------------------------------------------- 103 | 104 | function copcall(f, ...) 105 | return coxpcall(f, id, ...) 106 | end 107 | 108 | return { pcall = copcall, xpcall = coxpcall, running = corunning } 109 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /websocket/handshake.lua: -------------------------------------------------------------------------------- 1 | local tools = require'websocket.tools' 2 | local sha1 = tools.sha1 3 | local base64 = tools.base64 4 | local tinsert = table.insert 5 | 6 | local guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 7 | 8 | local sec_websocket_accept = function(sec_websocket_key) 9 | local a = sec_websocket_key..guid 10 | local sha1 = sha1(a) 11 | assert((#sha1 % 2) == 0) 12 | return base64.encode(sha1) 13 | end 14 | 15 | local http_headers = function(request) 16 | local headers = {} 17 | if not request:match('.*HTTP/1%.1') then 18 | return headers 19 | end 20 | request = request:match('[^\r\n]+\r\n(.*)') 21 | local empty_line 22 | for line in request:gmatch('[^\r\n]*\r\n') do 23 | local name,val = line:match('([^%s]+)%s*:%s*([^\r\n]+)') 24 | if name and val then 25 | name = name:lower() 26 | if not name:match('sec%-websocket') then 27 | val = val:lower() 28 | end 29 | if not headers[name] then 30 | headers[name] = val 31 | else 32 | headers[name] = headers[name]..','..val 33 | end 34 | elseif line == '\r\n' then 35 | empty_line = true 36 | else 37 | assert(false,line..'('..#line..')') 38 | end 39 | end 40 | return headers,request:match('\r\n\r\n(.*)') 41 | end 42 | 43 | local upgrade_request = function(req) 44 | local format = string.format 45 | local lines = { 46 | format('GET %s HTTP/1.1',req.uri or ''), 47 | format('Host: %s',req.host), 48 | 'Upgrade: websocket', 49 | 'Connection: Upgrade', 50 | format('Sec-WebSocket-Key: %s',req.key), 51 | format('Sec-WebSocket-Protocol: %s',table.concat(req.protocols,', ')), 52 | 'Sec-WebSocket-Version: 13', 53 | } 54 | if req.origin then 55 | tinsert(lines,string.format('Origin: %s',req.origin)) 56 | end 57 | if req.port and req.port ~= 80 then 58 | lines[2] = format('Host: %s:%d',req.host,req.port) 59 | end 60 | tinsert(lines,'\r\n') 61 | return table.concat(lines,'\r\n') 62 | end 63 | 64 | local accept_upgrade = function(request,protocols) 65 | local headers = http_headers(request) 66 | if headers['upgrade'] ~= 'websocket' or 67 | not headers['connection'] or 68 | not headers['connection']:match('upgrade') or 69 | headers['sec-websocket-key'] == nil or 70 | headers['sec-websocket-version'] ~= '13' then 71 | return nil,'HTTP/1.1 400 Bad Request\r\n\r\n' 72 | end 73 | local prot 74 | if headers['sec-websocket-protocol'] then 75 | for protocol in headers['sec-websocket-protocol']:gmatch('([^,%s]+)%s?,?') do 76 | for _,supported in ipairs(protocols) do 77 | if supported == protocol then 78 | prot = protocol 79 | break 80 | end 81 | end 82 | if prot then 83 | break 84 | end 85 | end 86 | end 87 | local lines = { 88 | 'HTTP/1.1 101 Switching Protocols', 89 | 'Upgrade: websocket', 90 | 'Connection: '..headers['connection'], 91 | string.format('Sec-WebSocket-Accept: %s',sec_websocket_accept(headers['sec-websocket-key'])), 92 | } 93 | if prot then 94 | tinsert(lines,string.format('Sec-WebSocket-Protocol: %s',prot)) 95 | end 96 | tinsert(lines,'\r\n') 97 | return table.concat(lines,'\r\n'),prot 98 | end 99 | 100 | return { 101 | sec_websocket_accept = sec_websocket_accept, 102 | http_headers = http_headers, 103 | accept_upgrade = accept_upgrade, 104 | upgrade_request = upgrade_request, 105 | } 106 | -------------------------------------------------------------------------------- /websocket/sync.lua: -------------------------------------------------------------------------------- 1 | local frame = require'websocket.frame' 2 | local handshake = require'websocket.handshake' 3 | local tools = require'websocket.tools' 4 | local ssl = luasec and require'luasec.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 = 2 15 | local encoded = '' 16 | local clean = function(was_clean,code,reason) 17 | self:sock_close() 18 | self.state = 'CLOSED' 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 = 2 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 | if not ssl then 132 | return nil, "bad protocol" 133 | end 134 | self.sock = assert(ssl.wrap(self.sock, ssl_params)) 135 | assert(self.sock:dohandshake()) 136 | elseif protocol ~= "ws" then 137 | return nil, 'bad protocol' 138 | end 139 | local ws_protocols_tbl = {''} 140 | if type(ws_protocol) == 'string' then 141 | ws_protocols_tbl = {ws_protocol} 142 | elseif type(ws_protocol) == 'table' then 143 | ws_protocols_tbl = ws_protocol 144 | end 145 | local key = tools.generate_key() 146 | local req = handshake.upgrade_request 147 | { 148 | key = key, 149 | host = host, 150 | port = port, 151 | protocols = ws_protocols_tbl, 152 | uri = uri 153 | } 154 | local n,err = self:sock_send(req) 155 | if n ~= #req then 156 | return nil,err,nil 157 | end 158 | local resp = {} 159 | repeat 160 | local line,err = self:sock_receive('*l') 161 | resp[#resp+1] = line 162 | if err then 163 | return nil,err,nil 164 | end 165 | until line == '' 166 | local response = table.concat(resp,'\r\n') 167 | local headers = handshake.http_headers(response) 168 | local expected_accept = handshake.sec_websocket_accept(key) 169 | if headers['sec-websocket-accept'] ~= expected_accept then 170 | local msg = 'Websocket Handshake failed: Invalid Sec-Websocket-Accept (expected %s got %s)' 171 | return nil,msg:format(expected_accept,headers['sec-websocket-accept'] or 'nil'),headers 172 | end 173 | self.state = 'OPEN' 174 | return true,headers['sec-websocket-protocol'],headers 175 | end 176 | 177 | local extend = function(obj) 178 | assert(obj.sock_send) 179 | assert(obj.sock_receive) 180 | assert(obj.sock_close) 181 | 182 | assert(obj.is_closing == nil) 183 | assert(obj.receive == nil) 184 | assert(obj.send == nil) 185 | assert(obj.close == nil) 186 | assert(obj.connect == nil) 187 | 188 | if not obj.is_server then 189 | assert(obj.sock_connect) 190 | end 191 | 192 | if not obj.state then 193 | obj.state = 'CLOSED' 194 | end 195 | 196 | obj.receive = receive 197 | obj.send = send 198 | obj.close = close 199 | obj.connect = connect 200 | 201 | return obj 202 | end 203 | 204 | return { 205 | extend = extend 206 | } 207 | -------------------------------------------------------------------------------- /websocket/tools.lua: -------------------------------------------------------------------------------- 1 | local bit = require'websocket.bit' 2 | local mime = _G.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 | --------------------------------------------------------------------------------