├── .github ├── FUNDING.yml └── workflows │ ├── bob.yml │ └── trigger-site-rebuild.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── docs ├── Defold-Websocket │ ├── DefoldWebsocket.wasm │ ├── DefoldWebsocket_asmjs.js │ ├── DefoldWebsocket_wasm.js │ ├── archive │ │ ├── archive_files.json │ │ ├── game.arcd0 │ │ ├── game.arci0 │ │ ├── game.dmanifest0 │ │ ├── game.projectc0 │ │ └── game.public.der0 │ ├── dmloader.js │ └── index.html ├── extension-websocket │ ├── archive │ │ ├── archive_files.json │ │ ├── game.arcd0 │ │ ├── game.arci0 │ │ ├── game.dmanifest0 │ │ ├── game.projectc0 │ │ └── game.public.der0 │ ├── dmloader.js │ ├── extensionwebsocket.symbols │ ├── extensionwebsocket.wasm │ ├── extensionwebsocket_asmjs.js │ ├── extensionwebsocket_wasm.js │ └── index.html └── index.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 └── websocket ├── api └── api.script_api ├── ext.manifest ├── include └── wslay │ ├── config.h │ ├── wslay.h │ ├── wslay_event.h │ ├── wslay_frame.h │ ├── wslay_net.h │ ├── wslay_queue.h │ ├── wslay_stack.h │ └── wslayver.h └── src ├── emscripten_callbacks.cpp ├── handshake.cpp ├── pcg.cpp ├── script_util.cpp ├── script_util.h ├── socket.cpp ├── websocket.cpp ├── websocket.h ├── wslay ├── wslay_event.c ├── wslay_frame.c ├── wslay_net.c ├── wslay_queue.c └── wslay_stack.c └── wslay_callbacks.cpp /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: defold 2 | patreon: Defold 3 | custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NBNBHTUW4GS4C'] 4 | -------------------------------------------------------------------------------- /.github/workflows/bob.yml: -------------------------------------------------------------------------------- 1 | name: Build with bob 2 | 3 | on: 4 | push: 5 | pull_request_target: 6 | schedule: 7 | # nightly at 05:00 on the 1st and 15th 8 | - cron: 0 5 1,15 * * 9 | 10 | env: 11 | VERSION_FILENAME: 'info.json' 12 | BUILD_SERVER: 'https://build.defold.com' 13 | 14 | jobs: 15 | build_with_bob: 16 | strategy: 17 | matrix: 18 | platform: [armv7-android, x86_64-linux, x86_64-win32, x86-win32, js-web] 19 | runs-on: ubuntu-latest 20 | 21 | name: Build 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-java@v1 25 | with: 26 | java-version: '11.0.2' 27 | 28 | - name: Get Defold version 29 | run: | 30 | TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'` 31 | echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV 32 | echo "Found version ${TMPVAR}" 33 | 34 | - name: Download bob.jar 35 | run: | 36 | wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar 37 | java -jar bob.jar --version 38 | 39 | - name: Resolve libraries 40 | run: java -jar bob.jar resolve --email a@b.com --auth 123456 41 | - name: Build 42 | run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}} 43 | - name: Bundle 44 | run: java -jar bob.jar --platform=${{ matrix.platform }} bundle 45 | 46 | # macOS is not technically needed for building, but we want to test bundling as well, since we're also testing the manifest merging 47 | build_with_bob_macos: 48 | strategy: 49 | matrix: 50 | platform: [armv7-darwin, x86_64-darwin] 51 | runs-on: macOS-latest 52 | 53 | name: Build 54 | steps: 55 | - uses: actions/checkout@v2 56 | - uses: actions/setup-java@v1 57 | with: 58 | java-version: '11.0.2' 59 | 60 | - name: Get Defold version 61 | run: | 62 | TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'` 63 | echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV 64 | echo "Found version ${TMPVAR}" 65 | 66 | - name: Download bob.jar 67 | run: | 68 | wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar 69 | java -jar bob.jar --version 70 | 71 | - name: Resolve libraries 72 | run: java -jar bob.jar resolve --email a@b.com --auth 123456 73 | - name: Build 74 | run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}} 75 | - name: Bundle 76 | run: java -jar bob.jar --platform=${{ matrix.platform }} bundle 77 | -------------------------------------------------------------------------------- /.github/workflows/trigger-site-rebuild.yml: -------------------------------------------------------------------------------- 1 | name: Trigger site rebuild 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | site-rebuild: 10 | runs-on: ubuntu-latest 11 | 12 | steps: [ 13 | { 14 | name: 'Repository dispatch', 15 | uses: defold/repository-dispatch@1.2.1, 16 | with: { 17 | repo: 'defold/defold.github.io', 18 | token: '${{ secrets.SERVICES_GITHUB_TOKEN }}', 19 | user: 'services@defold.se', 20 | action: 'extension-websocket' 21 | } 22 | }] 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.internal 2 | /build 3 | /bundle* 4 | .externalToolBuilders 5 | .DS_Store 6 | Thumbs.db 7 | .lock-wscript 8 | *.pyc 9 | .project 10 | .cproject 11 | builtins 12 | lws_source 13 | lws_build 14 | *.profraw 15 | *.der 16 | /.editor_settings -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Defold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Defold websocket extension 2 | 3 | [![Build Status](https://github.com/defold/extension-websocket/workflows/Build%20with%20bob/badge.svg)](https://github.com/defold/extension-websocket/actions) 4 | 5 | ## Installation 6 | To use this library in your Defold project, add the following URL to your `game.project` dependencies: 7 | 8 | https://github.com/defold/extension-websocket/archive/master.zip 9 | 10 | We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-websocket/releases). 11 | 12 | ## API reference 13 | 14 | https://defold.com/extension-websocket/ 15 | 16 | https://defold.com/extension-websocket/websocket_api/ 17 | 18 | ## Debugging 19 | 20 | In order to make it easier to debug this extension, we provide a `game.project` setting `websocket.debug` (edit `game.project` as text and add): 21 | 22 | ``` 23 | [websocket] 24 | debug = level 25 | ``` 26 | 27 | Set it to: 28 | 29 | * `0` to disable debugging (i.e. no debug output). 30 | * `1` to display state changes. 31 | * `2` to display the messages sent and received. 32 | 33 | ## External resources 34 | 35 | To verify that your websocket server works, you can test it with some tools. 36 | 37 | * [websocat](https://github.com/vi/websocat) 38 | 39 | Or, you can test your server on this web page: 40 | 41 | * https://www.websocket.org/echo.html 42 | 43 | To monitor all the packets sent to/from the client/server, you can use e.g. 44 | 45 | * [Wireshark](https://www.wireshark.org) 46 | 47 | For command line debugging, there's 48 | 49 | * tcpdump: `sudo tcpdump -X -s0 -ilo0 port 8080 ` (example for local ws:// connection) 50 | 51 | * tcpdump: `sudo tcpdump -X -s0 host echo.websocket.org` (Monitors packets to/from echo.websocket.org) 52 | 53 | ## Credits 54 | 55 | This extension makes use of the C library WSlay by @tatsuhiro-t: 56 | 57 | * https://github.com/tatsuhiro-t/wslay 58 | 59 | The test server used by the example: 60 | 61 | * https://www.lob.com/blog/websocket-org-is-down-here-is-an-alternative 62 | -------------------------------------------------------------------------------- /docs/Defold-Websocket/DefoldWebsocket.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/Defold-Websocket/DefoldWebsocket.wasm -------------------------------------------------------------------------------- /docs/Defold-Websocket/archive/archive_files.json: -------------------------------------------------------------------------------- 1 | {"content":[{"name":"game.projectc","size":3154,"pieces":[{"name":"game.projectc0","offset":0}]},{"name":"game.arci","size":2208,"pieces":[{"name":"game.arci0","offset":0}]},{"name":"game.arcd","size":34370,"pieces":[{"name":"game.arcd0","offset":0}]},{"name":"game.dmanifest","size":4314,"pieces":[{"name":"game.dmanifest0","offset":0}]},{"name":"game.public.der","size":162,"pieces":[{"name":"game.public.der0","offset":0}]}]} -------------------------------------------------------------------------------- /docs/Defold-Websocket/archive/game.arcd0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/Defold-Websocket/archive/game.arcd0 -------------------------------------------------------------------------------- /docs/Defold-Websocket/archive/game.arci0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/Defold-Websocket/archive/game.arci0 -------------------------------------------------------------------------------- /docs/Defold-Websocket/archive/game.dmanifest0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/Defold-Websocket/archive/game.dmanifest0 -------------------------------------------------------------------------------- /docs/Defold-Websocket/archive/game.projectc0: -------------------------------------------------------------------------------- 1 | [project] 2 | title = Defold-Websocket 3 | version = 1.0.0 4 | write_log = 0 5 | compress_archive = 1 6 | publisher = unnamed 7 | developer = unnamed 8 | 9 | [display] 10 | width = 640 11 | height = 1136 12 | high_dpi = 0 13 | samples = 0 14 | fullscreen = 0 15 | update_frequency = 0 16 | vsync = 1 17 | display_profiles = /builtins/render/default.display_profilesc 18 | dynamic_orientation = 0 19 | 20 | [render] 21 | clear_color_red = 0 22 | clear_color_green = 0 23 | clear_color_blue = 0 24 | clear_color_alpha = 0 25 | 26 | [physics] 27 | type = 2D 28 | gravity_y = -10 29 | debug = 0 30 | debug_alpha = 0.9 31 | world_count = 4 32 | gravity_x = 0 33 | gravity_z = 0 34 | scale = 0.02 35 | allow_dynamic_transforms = 0 36 | debug_scale = 30 37 | max_collisions = 64 38 | max_contacts = 128 39 | contact_impulse_limit = 0 40 | ray_cast_limit_2d = 64 41 | ray_cast_limit_3d = 128 42 | trigger_overlap_capacity = 16 43 | 44 | [bootstrap] 45 | main_collection = /examples/websocket.collectionc 46 | render = /builtins/render/default.renderc 47 | 48 | [graphics] 49 | default_texture_min_filter = linear 50 | default_texture_mag_filter = linear 51 | max_draw_calls = 1024 52 | max_characters = 8192 53 | max_debug_vertices = 10000 54 | texture_profiles = /builtins/graphics/default.texture_profiles 55 | verify_graphics_calls = 1 56 | memory_size = 512 57 | 58 | [shader] 59 | output_spirv = 0 60 | 61 | [sound] 62 | gain = 1 63 | max_sound_data = 128 64 | max_sound_buffers = 32 65 | max_sound_sources = 16 66 | max_sound_instances = 256 67 | max_component_count = 32 68 | 69 | [resource] 70 | http_cache = 0 71 | max_resources = 1024 72 | 73 | [input] 74 | repeat_delay = 0.5 75 | repeat_interval = 0.2 76 | gamepads = /builtins/input/default.gamepadsc 77 | game_binding = /input/game.input_bindingc 78 | use_accelerometer = 1 79 | 80 | [sprite] 81 | max_count = 128 82 | subpixels = 1 83 | 84 | [spine] 85 | max_count = 128 86 | 87 | [model] 88 | max_count = 128 89 | 90 | [mesh] 91 | max_count = 128 92 | 93 | [gui] 94 | max_count = 64 95 | max_particlefx_count = 64 96 | max_particle_count = 1024 97 | 98 | [collection] 99 | max_instances = 1024 100 | max_input_stack_entries = 16 101 | 102 | [collection_proxy] 103 | max_count = 8 104 | 105 | [collectionfactory] 106 | max_count = 128 107 | 108 | [factory] 109 | max_count = 128 110 | 111 | [ios] 112 | launch_screen = /builtins/manifests/ios/LaunchScreen.storyboardc 113 | pre_renderered_icons = 0 114 | bundle_identifier = com.defold.websocket 115 | infoplist = /builtins/manifests/ios/Info.plist 116 | default_language = en 117 | localizations = en 118 | 119 | [android] 120 | version_code = 1 121 | minimum_sdk_version = 16 122 | target_sdk_version = 29 123 | package = com.defold.websocket 124 | manifest = /builtins/manifests/android/AndroidManifest.xml 125 | iap_provider = GooglePlay 126 | input_method = KeyEvent 127 | immersive_mode = 0 128 | display_cutout = 1 129 | debuggable = 0 130 | 131 | [osx] 132 | infoplist = /builtins/manifests/osx/Info.plist 133 | bundle_identifier = com.defold.websocket 134 | default_language = en 135 | localizations = en 136 | 137 | [windows] 138 | 139 | [html5] 140 | custom_heap_size = 0 141 | heap_size = 256 142 | htmlfile = /builtins/manifests/web/engine_template.html 143 | cssfile = /builtins/manifests/web/light_theme.css 144 | archive_location_prefix = archive 145 | show_fullscreen_button = 1 146 | show_made_with_defold = 1 147 | scale_mode = downscale_fit 148 | 149 | [particle_fx] 150 | max_count = 64 151 | max_particle_count = 1024 152 | 153 | [iap] 154 | auto_finish_transactions = 1 155 | 156 | [network] 157 | http_timeout = 0 158 | http_thread_count = 4 159 | http_cache_enabled = 1 160 | 161 | [library] 162 | include_dirs = websocket 163 | 164 | [script] 165 | shared_state = 1 166 | 167 | [label] 168 | max_count = 64 169 | subpixels = 1 170 | 171 | [profiler] 172 | track_cpu = 0 173 | 174 | [liveupdate] 175 | settings = /liveupdate.settings 176 | enabled = 1 177 | 178 | [tilemap] 179 | max_count = 16 180 | max_tile_count = 2048 181 | 182 | [engine] 183 | run_while_iconified = 0 184 | 185 | -------------------------------------------------------------------------------- /docs/Defold-Websocket/archive/game.public.der0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/Defold-Websocket/archive/game.public.der0 -------------------------------------------------------------------------------- /docs/Defold-Websocket/dmloader.js: -------------------------------------------------------------------------------- 1 | /* ********************************************************************* */ 2 | /* Load and combine data that is split into archives */ 3 | /* ********************************************************************* */ 4 | 5 | var Combine = { 6 | _targets: [], 7 | _targetIndex: 0, 8 | // target: build target 9 | // name: intended filepath of built object 10 | // size: expected size of built object. 11 | // data: combined data 12 | // downloaded: total amount of data downloaded 13 | // pieces: array of name, offset and data objects 14 | // numExpectedFiles: total number of files expected in description 15 | // lastRequestedPiece: index of last data file requested (strictly ascending) 16 | // totalLoadedPieces: counts the number of data files received 17 | 18 | //MAX_CONCURRENT_XHR: 6, // remove comment if throttling of XHR is desired. 19 | 20 | isCompleted: false, // status of process 21 | 22 | _onCombineCompleted: [], // signature: name, data. 23 | _onAllTargetsBuilt:[], // signature: void 24 | _onDownloadProgress: [], // signature: downloaded, total 25 | 26 | _currentDownloadBytes: 0, 27 | _totalDownloadBytes: 0, 28 | 29 | _retry_time: 0, // pause before retry file loading after error 30 | _max_retry_count: 0, // how many attempts we do when trying to download a file. 31 | _can_not_download_file_callback: undefined, //Function that is called if you can't download file after 'retry_count' attempts. 32 | 33 | _archiveLocationFilter: function(path) { return "split" + path; }, 34 | 35 | can_not_download_file: function(file) { 36 | if (typeof Combine._can_not_download_file_callback === 'function') { 37 | Combine._can_not_download_file_callback(file); 38 | } 39 | }, 40 | 41 | addProgressListener: function(callback) { 42 | if (typeof callback !== 'function') { 43 | throw "Invalid callback registration"; 44 | } 45 | this._onDownloadProgress.push(callback); 46 | }, 47 | 48 | addCombineCompletedListener: function(callback) { 49 | if (typeof callback !== 'function') { 50 | throw "Invalid callback registration"; 51 | } 52 | this._onCombineCompleted.push(callback); 53 | }, 54 | 55 | addAllTargetsBuiltListener: function(callback) { 56 | if (typeof callback !== 'function') { 57 | throw "Invalid callback registration"; 58 | } 59 | this._onAllTargetsBuilt.push(callback); 60 | }, 61 | 62 | // descriptUrl: location of text file describing files to be preloaded 63 | process: function(descriptUrl, attempt_count) { 64 | if (!attempt_count) { 65 | attempt_count = 0; 66 | } 67 | var xhr = new XMLHttpRequest(); 68 | xhr.open('GET', descriptUrl); 69 | xhr.responseType = 'text'; 70 | xhr.onload = function(evt) { 71 | Combine.onReceiveDescription(xhr); 72 | }; 73 | xhr.onerror = function(evt) { 74 | attempt_count += 1; 75 | if (attempt_count < Combine._max_retry_count) { 76 | console.warn("Can't download file '" + descriptUrl + "' . Next try in " + Combine._retry_time + " sec."); 77 | setTimeout(function() { 78 | Combine.process(descriptUrl, attempt_count); 79 | }, Combine._retry_time * 1000); 80 | } else { 81 | Combine.can_not_download_file(descriptUrl); 82 | } 83 | }; 84 | xhr.send(null); 85 | }, 86 | 87 | cleanUp: function() { 88 | this._targets = []; 89 | this._targetIndex = 0; 90 | this.isCompleted = false; 91 | this._onCombineCompleted = []; 92 | this._onAllTargetsBuilt = []; 93 | this._onDownloadProgress = []; 94 | 95 | this._currentDownloadBytes = 0; 96 | this._totalDownloadBytes = 0; 97 | }, 98 | 99 | onReceiveDescription: function(xhr) { 100 | var json = JSON.parse(xhr.responseText); 101 | this._targets = json.content; 102 | this._totalDownloadBytes = 0; 103 | this._currentDownloadBytes = 0; 104 | 105 | var targets = this._targets; 106 | for(var i=0; i start) { 197 | throw "Buffer underflow"; 198 | } 199 | if (end > target.data.length) { 200 | throw "Buffer overflow"; 201 | } 202 | target.data.set(item.data, item.offset); 203 | } 204 | }, 205 | 206 | onPieceLoaded: function(target, item) { 207 | if (typeof target.totalLoadedPieces === 'undefined') { 208 | target.totalLoadedPieces = 0; 209 | } 210 | ++target.totalLoadedPieces; 211 | if (target.totalLoadedPieces == target.pieces.length) { 212 | this.finalizeTarget(target); 213 | ++this._targetIndex; 214 | for (var i=0; i start) { 253 | throw "Segment underflow"; 254 | } 255 | } 256 | if (pieces.length - 2 > i) { 257 | var next = pieces[i + 1]; 258 | if (end > next.offset) { 259 | throw "Segment overflow"; 260 | } 261 | } 262 | } 263 | } 264 | } 265 | }; 266 | 267 | /* ********************************************************************* */ 268 | /* Default splash and progress visualisation */ 269 | /* ********************************************************************* */ 270 | 271 | var Progress = { 272 | progress_id: "defold-progress", 273 | bar_id: "defold-progress-bar", 274 | 275 | addProgress : function (canvas) { 276 | /* Insert default progress bar below canvas */ 277 | canvas.insertAdjacentHTML('afterend', '
'); 278 | Progress.bar = document.getElementById(Progress.bar_id); 279 | Progress.progress = document.getElementById(Progress.progress_id); 280 | }, 281 | 282 | updateProgress: function (percentage, text) { 283 | Progress.bar.style.width = percentage + "%"; 284 | }, 285 | 286 | removeProgress: function () { 287 | if (Progress.progress.parentElement !== null) { 288 | Progress.progress.parentElement.removeChild(Progress.progress); 289 | 290 | // Remove any background/splash image that was set in runApp(). 291 | // Workaround for Safari bug DEF-3061. 292 | Module.canvas.style.background = ""; 293 | } 294 | } 295 | }; 296 | 297 | /* ********************************************************************* */ 298 | /* Default input override */ 299 | /* ********************************************************************* */ 300 | 301 | var CanvasInput = { 302 | arrowKeysHandler : function(e) { 303 | switch(e.keyCode) { 304 | case 37: case 38: case 39: case 40: // Arrow keys 305 | case 32: e.preventDefault(); e.stopPropagation(); // Space 306 | default: break; // do not block other keys 307 | } 308 | }, 309 | 310 | onFocusIn : function(e) { 311 | window.addEventListener("keydown", CanvasInput.arrowKeysHandler, false); 312 | }, 313 | 314 | onFocusOut: function(e) { 315 | window.removeEventListener("keydown", CanvasInput.arrowKeysHandler, false); 316 | }, 317 | 318 | addToCanvas : function(canvas) { 319 | canvas.addEventListener("focus", CanvasInput.onFocusIn, false); 320 | canvas.addEventListener("blur", CanvasInput.onFocusOut, false); 321 | canvas.focus(); 322 | CanvasInput.onFocusIn(); 323 | } 324 | }; 325 | 326 | /* ********************************************************************* */ 327 | /* Module is Emscripten namespace */ 328 | /* ********************************************************************* */ 329 | 330 | var Module = { 331 | noInitialRun: true, 332 | 333 | _filesToPreload: [], 334 | _archiveLoaded: false, 335 | _preLoadDone: false, 336 | _waitingForArchive: false, 337 | 338 | // Persistent storage 339 | persistentStorage: true, 340 | _syncInProgress: false, 341 | _syncNeeded: false, 342 | _syncInitial: false, 343 | _syncMaxTries: 3, 344 | _syncTries: 0, 345 | 346 | print: function(text) { console.log(text); }, 347 | printErr: function(text) { console.error(text); }, 348 | 349 | setStatus: function(text) { console.log(text); }, 350 | 351 | isWASMSupported: (function() { 352 | try { 353 | if (typeof WebAssembly === "object" 354 | && typeof WebAssembly.instantiate === "function") { 355 | const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); 356 | if (module instanceof WebAssembly.Module) 357 | return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; 358 | } 359 | } catch (e) { 360 | } 361 | return false; 362 | })(), 363 | 364 | prepareErrorObject: function (err, url, line, column, errObj) { 365 | line = typeof line == "undefined" ? 0 : line; 366 | column = typeof column == "undefined" ? 0 : column; 367 | url = typeof url == "undefined" ? "" : url; 368 | var errorLine = url + ":" + line + ":" + column; 369 | 370 | var error = errObj || (typeof window.event != "undefined" ? window.event.error : "" ) || err || "Undefined Error"; 371 | var message = ""; 372 | var stack = ""; 373 | var backtrace = ""; 374 | 375 | if (typeof error == "object" && typeof error.stack != "undefined" && typeof error.message != "undefined") { 376 | stack = String(error.stack); 377 | message = String(error.message); 378 | } else { 379 | stack = String(error).split("\n"); 380 | message = stack.shift(); 381 | stack = stack.join("\n"); 382 | } 383 | stack = stack || errorLine; 384 | 385 | var callLine = /at (\S+:\d*$)/.exec(message); 386 | if (callLine) { 387 | message = message.replace(/(at \S+:\d*$)/, ""); 388 | stack = callLine[1] + "\n" + stack; 389 | } 390 | 391 | message = message.replace(/(abort\(.+\)) at .+/, "$1"); 392 | stack = stack.replace(/\?{1}\S+(:\d+:\d+)/g, "$1"); 393 | stack = stack.replace(/ *at (\S+)$/gm, "@$1"); 394 | stack = stack.replace(/ *at (\S+)(?: \[as \S+\])? +\((.+)\)/g, "$1@$2"); 395 | stack = stack.replace(/^((?:Object|Array)\.)/gm, ""); 396 | stack = stack.split("\n"); 397 | 398 | return { stack:stack, message:message }; 399 | }, 400 | 401 | hasWebGLSupport: function() { 402 | var webgl_support = false; 403 | try { 404 | var canvas = document.createElement("canvas"); 405 | var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); 406 | if (gl && gl instanceof WebGLRenderingContext) { 407 | webgl_support = true; 408 | } 409 | } catch (error) { 410 | console.log("An error occurred while detecting WebGL support: " + error); 411 | webgl_support = false; 412 | } 413 | 414 | return webgl_support; 415 | }, 416 | 417 | handleVisibilityChange: function () { 418 | GLFW.onFocusChanged(document[Module.hiddenProperty] ? 0 : 1); 419 | }, 420 | 421 | getHiddenProperty: function () { 422 | if ('hidden' in document) return 'hidden'; 423 | var prefixes = ['webkit','moz','ms','o']; 424 | for (var i = 0; i < prefixes.length; i++) { 425 | if ((prefixes[i] + 'Hidden') in document) 426 | return prefixes[i] + 'Hidden'; 427 | } 428 | return null; 429 | }, 430 | 431 | setupVisibilityChangeListener: function() { 432 | Module.hiddenProperty = Module.getHiddenProperty(); 433 | if( Module.hiddenProperty ) { 434 | var eventName = Module.hiddenProperty.replace(/[H|h]idden/,'') + 'visibilitychange'; 435 | document.addEventListener(eventName, Module.handleVisibilityChange, false); 436 | } else { 437 | console.log("No document.hidden property found. The focus events won't be enabled.") 438 | } 439 | }, 440 | 441 | /** 442 | * Module.runApp - Starts the application given a canvas element id 443 | * 444 | * 'extra_params' is an optional object that can have the following fields: 445 | * 446 | * 'archive_location_filter': 447 | * Filter function that will run for each archive path. 448 | * 449 | * 'unsupported_webgl_callback': 450 | * Function that is called if WebGL is not supported. 451 | * 452 | * 'engine_arguments': 453 | * List of arguments (strings) that will be passed to the engine. 454 | * 455 | * 'persistent_storage': 456 | * Boolean toggling the usage of persistent storage. 457 | * 458 | * 'custom_heap_size': 459 | * Number of bytes specifying the memory heap size. 460 | * 461 | * 'disable_context_menu': 462 | * Disables the right-click context menu on the canvas element if true. 463 | * 464 | * 'retry_time': 465 | * Pause before retry file loading after error. 466 | * 467 | * 'retry_count': 468 | * How many attempts we do when trying to download a file. 469 | * 470 | * 'can_not_download_file_callback': 471 | * Function that is called if you can't download file after 'retry_count' attempts. 472 | **/ 473 | runApp: function(app_canvas_id, extra_params) { 474 | app_canvas_id = (typeof app_canvas_id === 'undefined') ? 'canvas' : app_canvas_id; 475 | 476 | var params = { 477 | archive_location_filter: function(path) { return 'split' + path; }, 478 | unsupported_webgl_callback: undefined, 479 | engine_arguments: [], 480 | persistent_storage: true, 481 | custom_heap_size: undefined, 482 | disable_context_menu: true, 483 | retry_time: 1, 484 | retry_count: 10, 485 | can_not_download_file_callback: undefined, 486 | }; 487 | 488 | for (var k in extra_params) { 489 | if (extra_params.hasOwnProperty(k)) { 490 | params[k] = extra_params[k]; 491 | } 492 | } 493 | 494 | Module.canvas = document.getElementById(app_canvas_id); 495 | Module.arguments = params["engine_arguments"]; 496 | Module.persistentStorage = params["persistent_storage"]; 497 | Module["TOTAL_MEMORY"] = params["custom_heap_size"]; 498 | 499 | if (Module.hasWebGLSupport()) { 500 | // Override game keys 501 | CanvasInput.addToCanvas(Module.canvas); 502 | 503 | Module.setupVisibilityChangeListener(); 504 | 505 | // Add progress visuals 506 | Progress.addProgress(Module.canvas); 507 | 508 | // Add context menu hide-handler if requested 509 | if (params["disable_context_menu"]) 510 | { 511 | Module.canvas.oncontextmenu = function(e) { 512 | e.preventDefault(); 513 | }; 514 | } 515 | 516 | Combine._retry_time = params["retry_time"]; 517 | Combine._max_retry_count = params["retry_count"]; 518 | if (typeof params["can_not_download_file_callback"] === "function") { 519 | Combine._can_not_download_file_callback = params["can_not_download_file_callback"]; 520 | } 521 | // Load and assemble archive 522 | Combine.addCombineCompletedListener(Module.onArchiveFileLoaded); 523 | Combine.addAllTargetsBuiltListener(Module.onArchiveLoaded); 524 | Combine.addProgressListener(Module.onArchiveLoadProgress); 525 | Combine._archiveLocationFilter = params["archive_location_filter"]; 526 | Combine.process(Combine._archiveLocationFilter('/archive_files.json')); 527 | } else { 528 | Progress.addProgress(Module.canvas); 529 | Progress.updateProgress(100, "Unable to start game, WebGL not supported"); 530 | Module.setStatus = function(text) { 531 | if (text) Module.printErr('[missing WebGL] ' + text); 532 | }; 533 | 534 | if (typeof params["unsupported_webgl_callback"] === "function") { 535 | params["unsupported_webgl_callback"](); 536 | } 537 | } 538 | }, 539 | 540 | onArchiveLoadProgress: function(downloaded, total) { 541 | Progress.updateProgress(downloaded / total * 100); 542 | }, 543 | 544 | onArchiveFileLoaded: function(name, data) { 545 | Module._filesToPreload.push({path: name, data: data}); 546 | }, 547 | 548 | onArchiveLoaded: function() { 549 | Combine.cleanUp(); 550 | Module._archiveLoaded = true; 551 | Progress.updateProgress(100, "Starting..."); 552 | 553 | if (Module._waitingForArchive) { 554 | Module._preloadAndCallMain(); 555 | } 556 | }, 557 | 558 | toggleFullscreen: function() { 559 | if (GLFW.isFullscreen) { 560 | GLFW.cancelFullScreen(); 561 | } else { 562 | GLFW.requestFullScreen(); 563 | } 564 | }, 565 | 566 | preSync: function(done) { 567 | // Initial persistent sync before main is called 568 | FS.syncfs(true, function(err) { 569 | if(err) { 570 | Module._syncTries += 1; 571 | console.error("FS syncfs error: " + err); 572 | if (Module._syncMaxTries > Module._syncTries) { 573 | Module.preSync(done); 574 | } else { 575 | Module._syncInitial = true; 576 | done(); 577 | } 578 | } else { 579 | Module._syncInitial = true; 580 | if (done !== undefined) { 581 | done(); 582 | } 583 | } 584 | }); 585 | }, 586 | 587 | preloadAll: function() { 588 | if (Module._preLoadDone) { 589 | return; 590 | } 591 | Module._preLoadDone = true; 592 | for (var i = 0; i < Module._filesToPreload.length; ++i) { 593 | var item = Module._filesToPreload[i]; 594 | FS.createPreloadedFile("", item.path, item.data, true, true); 595 | } 596 | }, 597 | 598 | // Tries to do a MEM->IDB sync 599 | // It will flag that another one is needed if there is already one sync running. 600 | persistentSync: function() { 601 | 602 | // Need to wait for the initial sync to finish since it 603 | // will call close on all its file streams which will trigger 604 | // new persistentSync for each. 605 | if (Module._syncInitial) { 606 | if (Module._syncInProgress) { 607 | Module._syncNeeded = true; 608 | } else { 609 | Module._startSyncFS(); 610 | } 611 | } 612 | }, 613 | 614 | preInit: [function() { 615 | /* Mount filesystem on preinit */ 616 | var dir = DMSYS.GetUserPersistentDataRoot(); 617 | FS.mkdir(dir); 618 | 619 | // If IndexedDB is supported we mount the persistent data root as IDBFS, 620 | // then try to do a IDB->MEM sync before we start the engine to get 621 | // previously saved data before boot. 622 | window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 623 | if (Module.persistentStorage && window.indexedDB) { 624 | FS.mount(IDBFS, {}, dir); 625 | 626 | // Patch FS.close so it will try to sync MEM->IDB 627 | var _close = FS.close; FS.close = function(stream) { var r = _close(stream); Module.persistentSync(); return r; } 628 | 629 | // Sync IDB->MEM before calling main() 630 | Module.preSync(function() { 631 | Module._preloadAndCallMain(); 632 | }); 633 | } else { 634 | Module._preloadAndCallMain(); 635 | } 636 | }], 637 | 638 | preRun: [function() { 639 | /* If archive is loaded, preload all its files */ 640 | if(Module._archiveLoaded) { 641 | Module.preloadAll(); 642 | } 643 | }], 644 | 645 | postRun: [function() { 646 | if(Module._archiveLoaded) { 647 | Progress.removeProgress(); 648 | } 649 | }], 650 | 651 | _preloadAndCallMain: function() { 652 | // If the archive isn't loaded, 653 | // we will have to wait with calling main. 654 | if (!Module._archiveLoaded) { 655 | Module._waitingForArchive = true; 656 | } else { 657 | 658 | // Need to set heap size before calling main 659 | TOTAL_MEMORY = Module["TOTAL_MEMORY"] || TOTAL_MEMORY; 660 | 661 | Module.preloadAll(); 662 | Progress.removeProgress(); 663 | if (Module.callMain === undefined) { 664 | Module.noInitialRun = false; 665 | } else { 666 | Module.callMain(Module.arguments); 667 | } 668 | } 669 | }, 670 | 671 | // Wrap IDBFS syncfs call with logic to avoid multiple syncs 672 | // running at the same time. 673 | _startSyncFS: function() { 674 | Module._syncInProgress = true; 675 | 676 | if (Module._syncMaxTries > Module._syncTries) { 677 | FS.syncfs(false, function(err) { 678 | Module._syncInProgress = false; 679 | 680 | if (err) { 681 | console.error("Module._startSyncFS error: " + err); 682 | Module._syncTries += 1; 683 | } 684 | 685 | if (Module._syncNeeded) { 686 | Module._syncNeeded = false; 687 | Module._startSyncFS(); 688 | } 689 | 690 | }); 691 | } 692 | }, 693 | }; 694 | 695 | window.onerror = function(err, url, line, column, errObj) { 696 | var errorObject = Module.prepareErrorObject(err, url, line, column, errObj); 697 | Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]); 698 | Module.setStatus('Exception thrown, see JavaScript console'); 699 | Module.setStatus = function(text) { 700 | if (text) Module.printErr('[post-exception status] ' + text); 701 | }; 702 | }; 703 | -------------------------------------------------------------------------------- /docs/Defold-Websocket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Defold-Websocket 1.0.0 11 | 141 | 142 | 143 | 144 |
145 | 146 |
147 |
Fullscreen
148 | 149 |
150 |
151 | 152 | 153 | 154 | 241 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /docs/extension-websocket/archive/archive_files.json: -------------------------------------------------------------------------------- 1 | {"content":[{"name":"game.projectc","size":3080,"pieces":[{"name":"game.projectc0","offset":0}]},{"name":"game.arci","size":1488,"pieces":[{"name":"game.arci0","offset":0}]},{"name":"game.arcd","size":19498,"pieces":[{"name":"game.arcd0","offset":0}]},{"name":"game.dmanifest","size":2532,"pieces":[{"name":"game.dmanifest0","offset":0}]},{"name":"game.public.der","size":162,"pieces":[{"name":"game.public.der0","offset":0}]}]} -------------------------------------------------------------------------------- /docs/extension-websocket/archive/game.arcd0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/extension-websocket/archive/game.arcd0 -------------------------------------------------------------------------------- /docs/extension-websocket/archive/game.arci0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/extension-websocket/archive/game.arci0 -------------------------------------------------------------------------------- /docs/extension-websocket/archive/game.dmanifest0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/extension-websocket/archive/game.dmanifest0 -------------------------------------------------------------------------------- /docs/extension-websocket/archive/game.projectc0: -------------------------------------------------------------------------------- 1 | [project] 2 | title = extension-websocket 3 | version = 1.0 4 | write_log = 0 5 | compress_archive = 1 6 | _dependencies = https://github.com/GameAnalytics/defold-openssl/archive/1.0.0.zip 7 | 8 | [display] 9 | width = 640 10 | height = 960 11 | high_dpi = 0 12 | samples = 0 13 | fullscreen = 0 14 | update_frequency = 0 15 | vsync = 1 16 | display_profiles = /builtins/render/default.display_profilesc 17 | dynamic_orientation = 0 18 | 19 | [render] 20 | clear_color_red = 0 21 | clear_color_green = 0 22 | clear_color_blue = 0 23 | clear_color_alpha = 0 24 | 25 | [physics] 26 | type = 2D 27 | gravity_y = -10 28 | debug = 0 29 | debug_alpha = 0.9 30 | world_count = 4 31 | gravity_x = 0 32 | gravity_z = 0 33 | scale = 1 34 | allow_dynamic_transforms = 0 35 | debug_scale = 30 36 | max_collisions = 64 37 | max_contacts = 128 38 | contact_impulse_limit = 0 39 | ray_cast_limit_2d = 64 40 | ray_cast_limit_3d = 128 41 | trigger_overlap_capacity = 16 42 | 43 | [bootstrap] 44 | main_collection = /examples/websocket.collectionc 45 | render = /builtins/render/default.renderc 46 | 47 | [graphics] 48 | default_texture_min_filter = linear 49 | default_texture_mag_filter = linear 50 | max_draw_calls = 1024 51 | max_characters = 8192 52 | max_debug_vertices = 10000 53 | texture_profiles = /builtins/graphics/default.texture_profiles 54 | verify_graphics_calls = 1 55 | 56 | [shader] 57 | output_spirv = 0 58 | 59 | [sound] 60 | gain = 1 61 | max_sound_data = 128 62 | max_sound_buffers = 32 63 | max_sound_sources = 16 64 | max_sound_instances = 256 65 | max_component_count = 32 66 | 67 | [resource] 68 | http_cache = 0 69 | max_resources = 1024 70 | 71 | [input] 72 | repeat_delay = 0.5 73 | repeat_interval = 0.2 74 | gamepads = /builtins/input/default.gamepadsc 75 | game_binding = /input/game.input_bindingc 76 | use_accelerometer = 1 77 | 78 | [sprite] 79 | max_count = 128 80 | subpixels = 1 81 | 82 | [spine] 83 | max_count = 128 84 | 85 | [model] 86 | max_count = 128 87 | 88 | [gui] 89 | max_count = 64 90 | max_particlefx_count = 64 91 | max_particle_count = 1024 92 | 93 | [collection] 94 | max_instances = 1024 95 | max_input_stack_entries = 16 96 | 97 | [collection_proxy] 98 | max_count = 8 99 | 100 | [collectionfactory] 101 | max_count = 128 102 | 103 | [factory] 104 | max_count = 128 105 | 106 | [ios] 107 | launch_screen = /builtins/manifests/ios/LaunchScreen.storyboardc 108 | pre_renderered_icons = 0 109 | bundle_identifier = example.unnamed 110 | infoplist = /builtins/manifests/ios/Info.plist 111 | default_language = en 112 | localizations = en 113 | 114 | [android] 115 | version_code = 1 116 | minimum_sdk_version = 16 117 | target_sdk_version = 29 118 | package = com.example.todo 119 | manifest = /builtins/manifests/android/AndroidManifest.xml 120 | iap_provider = GooglePlay 121 | input_method = KeyEvent 122 | immersive_mode = 0 123 | display_cutout = 1 124 | debuggable = 0 125 | 126 | [osx] 127 | infoplist = /builtins/manifests/osx/Info.plist 128 | bundle_identifier = example.unnamed 129 | default_language = en 130 | localizations = en 131 | 132 | [windows] 133 | 134 | [html5] 135 | custom_heap_size = 0 136 | heap_size = 256 137 | htmlfile = /builtins/manifests/web/engine_template.html 138 | cssfile = /builtins/manifests/web/light_theme.css 139 | archive_location_prefix = archive 140 | show_fullscreen_button = 1 141 | show_made_with_defold = 1 142 | scale_mode = downscale_fit 143 | 144 | [particle_fx] 145 | max_count = 64 146 | max_particle_count = 1024 147 | 148 | [iap] 149 | auto_finish_transactions = 1 150 | 151 | [network] 152 | http_timeout = 0 153 | 154 | [library] 155 | include_dirs = websocket 156 | 157 | [script] 158 | shared_state = 1 159 | 160 | [label] 161 | max_count = 64 162 | subpixels = 1 163 | 164 | [profiler] 165 | track_cpu = 0 166 | 167 | [liveupdate] 168 | settings = /liveupdate.settings 169 | 170 | [tilemap] 171 | max_count = 16 172 | max_tile_count = 2048 173 | 174 | [engine] 175 | run_while_iconified = 0 176 | 177 | -------------------------------------------------------------------------------- /docs/extension-websocket/archive/game.public.der0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/extension-websocket/archive/game.public.der0 -------------------------------------------------------------------------------- /docs/extension-websocket/dmloader.js: -------------------------------------------------------------------------------- 1 | /* ********************************************************************* */ 2 | /* Load and combine data that is split into archives */ 3 | /* ********************************************************************* */ 4 | 5 | var Combine = { 6 | _targets: [], 7 | _targetIndex: 0, 8 | // target: build target 9 | // name: intended filepath of built object 10 | // size: expected size of built object. 11 | // data: combined data 12 | // downloaded: total amount of data downloaded 13 | // pieces: array of name, offset and data objects 14 | // numExpectedFiles: total number of files expected in description 15 | // lastRequestedPiece: index of last data file requested (strictly ascending) 16 | // totalLoadedPieces: counts the number of data files received 17 | 18 | //MAX_CONCURRENT_XHR: 6, // remove comment if throttling of XHR is desired. 19 | 20 | isCompleted: false, // status of process 21 | 22 | _onCombineCompleted: [], // signature: name, data. 23 | _onAllTargetsBuilt:[], // signature: void 24 | _onDownloadProgress: [], // signature: downloaded, total 25 | 26 | _currentDownloadBytes: 0, 27 | _totalDownloadBytes: 0, 28 | 29 | _retry_time: 0, // pause before retry file loading after error 30 | _max_retry_count: 0, // how many attempts we do when trying to download a file. 31 | _can_not_download_file_callback: undefined, //Function that is called if you can't download file after 'retry_count' attempts. 32 | 33 | _archiveLocationFilter: function(path) { return "split" + path; }, 34 | 35 | can_not_download_file: function(file) { 36 | if (typeof Combine._can_not_download_file_callback === 'function') { 37 | Combine._can_not_download_file_callback(file); 38 | } 39 | }, 40 | 41 | addProgressListener: function(callback) { 42 | if (typeof callback !== 'function') { 43 | throw "Invalid callback registration"; 44 | } 45 | this._onDownloadProgress.push(callback); 46 | }, 47 | 48 | addCombineCompletedListener: function(callback) { 49 | if (typeof callback !== 'function') { 50 | throw "Invalid callback registration"; 51 | } 52 | this._onCombineCompleted.push(callback); 53 | }, 54 | 55 | addAllTargetsBuiltListener: function(callback) { 56 | if (typeof callback !== 'function') { 57 | throw "Invalid callback registration"; 58 | } 59 | this._onAllTargetsBuilt.push(callback); 60 | }, 61 | 62 | // descriptUrl: location of text file describing files to be preloaded 63 | process: function(descriptUrl, attempt_count) { 64 | if (!attempt_count) { 65 | attempt_count = 0; 66 | } 67 | var xhr = new XMLHttpRequest(); 68 | xhr.open('GET', descriptUrl); 69 | xhr.responseType = 'text'; 70 | xhr.onload = function(evt) { 71 | Combine.onReceiveDescription(xhr); 72 | }; 73 | xhr.onerror = function(evt) { 74 | attempt_count += 1; 75 | if (attempt_count < Combine._max_retry_count) { 76 | console.warn("Can't download file '" + descriptUrl + "' . Next try in " + Combine._retry_time + " sec."); 77 | setTimeout(function() { 78 | Combine.process(descriptUrl, attempt_count); 79 | }, Combine._retry_time * 1000); 80 | } else { 81 | Combine.can_not_download_file(descriptUrl); 82 | } 83 | }; 84 | xhr.send(null); 85 | }, 86 | 87 | cleanUp: function() { 88 | this._targets = []; 89 | this._targetIndex = 0; 90 | this.isCompleted = false; 91 | this._onCombineCompleted = []; 92 | this._onAllTargetsBuilt = []; 93 | this._onDownloadProgress = []; 94 | 95 | this._currentDownloadBytes = 0; 96 | this._totalDownloadBytes = 0; 97 | }, 98 | 99 | onReceiveDescription: function(xhr) { 100 | var json = JSON.parse(xhr.responseText); 101 | this._targets = json.content; 102 | this._totalDownloadBytes = 0; 103 | this._currentDownloadBytes = 0; 104 | 105 | var targets = this._targets; 106 | for(var i=0; i start) { 197 | throw "Buffer underflow"; 198 | } 199 | if (end > target.data.length) { 200 | throw "Buffer overflow"; 201 | } 202 | target.data.set(item.data, item.offset); 203 | } 204 | }, 205 | 206 | onPieceLoaded: function(target, item) { 207 | if (typeof target.totalLoadedPieces === 'undefined') { 208 | target.totalLoadedPieces = 0; 209 | } 210 | ++target.totalLoadedPieces; 211 | if (target.totalLoadedPieces == target.pieces.length) { 212 | this.finalizeTarget(target); 213 | ++this._targetIndex; 214 | for (var i=0; i start) { 253 | throw "Segment underflow"; 254 | } 255 | } 256 | if (pieces.length - 2 > i) { 257 | var next = pieces[i + 1]; 258 | if (end > next.offset) { 259 | throw "Segment overflow"; 260 | } 261 | } 262 | } 263 | } 264 | } 265 | }; 266 | 267 | /* ********************************************************************* */ 268 | /* Default splash and progress visualisation */ 269 | /* ********************************************************************* */ 270 | 271 | var Progress = { 272 | progress_id: "defold-progress", 273 | bar_id: "defold-progress-bar", 274 | 275 | addProgress : function (canvas) { 276 | /* Insert default progress bar below canvas */ 277 | canvas.insertAdjacentHTML('afterend', '
'); 278 | Progress.bar = document.getElementById(Progress.bar_id); 279 | Progress.progress = document.getElementById(Progress.progress_id); 280 | }, 281 | 282 | updateProgress: function (percentage, text) { 283 | Progress.bar.style.width = percentage + "%"; 284 | }, 285 | 286 | removeProgress: function () { 287 | if (Progress.progress.parentElement !== null) { 288 | Progress.progress.parentElement.removeChild(Progress.progress); 289 | 290 | // Remove any background/splash image that was set in runApp(). 291 | // Workaround for Safari bug DEF-3061. 292 | Module.canvas.style.background = ""; 293 | } 294 | } 295 | }; 296 | 297 | /* ********************************************************************* */ 298 | /* Default input override */ 299 | /* ********************************************************************* */ 300 | 301 | var CanvasInput = { 302 | arrowKeysHandler : function(e) { 303 | switch(e.keyCode) { 304 | case 37: case 38: case 39: case 40: // Arrow keys 305 | case 32: e.preventDefault(); e.stopPropagation(); // Space 306 | default: break; // do not block other keys 307 | } 308 | }, 309 | 310 | onFocusIn : function(e) { 311 | window.addEventListener("keydown", CanvasInput.arrowKeysHandler, false); 312 | }, 313 | 314 | onFocusOut: function(e) { 315 | window.removeEventListener("keydown", CanvasInput.arrowKeysHandler, false); 316 | }, 317 | 318 | addToCanvas : function(canvas) { 319 | canvas.addEventListener("focus", CanvasInput.onFocusIn, false); 320 | canvas.addEventListener("blur", CanvasInput.onFocusOut, false); 321 | canvas.focus(); 322 | CanvasInput.onFocusIn(); 323 | } 324 | }; 325 | 326 | /* ********************************************************************* */ 327 | /* Module is Emscripten namespace */ 328 | /* ********************************************************************* */ 329 | 330 | var Module = { 331 | noInitialRun: true, 332 | 333 | _filesToPreload: [], 334 | _archiveLoaded: false, 335 | _preLoadDone: false, 336 | _waitingForArchive: false, 337 | 338 | // Persistent storage 339 | persistentStorage: true, 340 | _syncInProgress: false, 341 | _syncNeeded: false, 342 | _syncInitial: false, 343 | _syncMaxTries: 3, 344 | _syncTries: 0, 345 | 346 | print: function(text) { console.log(text); }, 347 | printErr: function(text) { console.error(text); }, 348 | 349 | setStatus: function(text) { console.log(text); }, 350 | 351 | isWASMSupported: (function() { 352 | try { 353 | if (typeof WebAssembly === "object" 354 | && typeof WebAssembly.instantiate === "function") { 355 | const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); 356 | if (module instanceof WebAssembly.Module) 357 | return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; 358 | } 359 | } catch (e) { 360 | } 361 | return false; 362 | })(), 363 | 364 | prepareErrorObject: function (err, url, line, column, errObj) { 365 | line = typeof line == "undefined" ? 0 : line; 366 | column = typeof column == "undefined" ? 0 : column; 367 | url = typeof url == "undefined" ? "" : url; 368 | var errorLine = url + ":" + line + ":" + column; 369 | 370 | var error = errObj || (typeof window.event != "undefined" ? window.event.error : "" ) || err || "Undefined Error"; 371 | var message = ""; 372 | var stack = ""; 373 | var backtrace = ""; 374 | 375 | if (typeof error == "object" && typeof error.stack != "undefined" && typeof error.message != "undefined") { 376 | stack = String(error.stack); 377 | message = String(error.message); 378 | } else { 379 | stack = String(error).split("\n"); 380 | message = stack.shift(); 381 | stack = stack.join("\n"); 382 | } 383 | stack = stack || errorLine; 384 | 385 | var callLine = /at (\S+:\d*$)/.exec(message); 386 | if (callLine) { 387 | message = message.replace(/(at \S+:\d*$)/, ""); 388 | stack = callLine[1] + "\n" + stack; 389 | } 390 | 391 | message = message.replace(/(abort\(.+\)) at .+/, "$1"); 392 | stack = stack.replace(/\?{1}\S+(:\d+:\d+)/g, "$1"); 393 | stack = stack.replace(/ *at (\S+)$/gm, "@$1"); 394 | stack = stack.replace(/ *at (\S+)(?: \[as \S+\])? +\((.+)\)/g, "$1@$2"); 395 | stack = stack.replace(/^((?:Object|Array)\.)/gm, ""); 396 | stack = stack.split("\n"); 397 | 398 | return { stack:stack, message:message }; 399 | }, 400 | 401 | hasWebGLSupport: function() { 402 | var webgl_support = false; 403 | try { 404 | var canvas = document.createElement("canvas"); 405 | var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); 406 | if (gl && gl instanceof WebGLRenderingContext) { 407 | webgl_support = true; 408 | } 409 | } catch (error) { 410 | console.log("An error occurred while detecting WebGL support: " + error); 411 | webgl_support = false; 412 | } 413 | 414 | return webgl_support; 415 | }, 416 | 417 | handleVisibilityChange: function () { 418 | GLFW.onFocusChanged(document[Module.hiddenProperty] ? 0 : 1); 419 | }, 420 | 421 | getHiddenProperty: function () { 422 | if ('hidden' in document) return 'hidden'; 423 | var prefixes = ['webkit','moz','ms','o']; 424 | for (var i = 0; i < prefixes.length; i++) { 425 | if ((prefixes[i] + 'Hidden') in document) 426 | return prefixes[i] + 'Hidden'; 427 | } 428 | return null; 429 | }, 430 | 431 | setupVisibilityChangeListener: function() { 432 | Module.hiddenProperty = Module.getHiddenProperty(); 433 | if( Module.hiddenProperty ) { 434 | var eventName = Module.hiddenProperty.replace(/[H|h]idden/,'') + 'visibilitychange'; 435 | document.addEventListener(eventName, Module.handleVisibilityChange, false); 436 | } else { 437 | console.log("No document.hidden property found. The focus events won't be enabled.") 438 | } 439 | }, 440 | 441 | /** 442 | * Module.runApp - Starts the application given a canvas element id 443 | * 444 | * 'extra_params' is an optional object that can have the following fields: 445 | * 446 | * 'archive_location_filter': 447 | * Filter function that will run for each archive path. 448 | * 449 | * 'unsupported_webgl_callback': 450 | * Function that is called if WebGL is not supported. 451 | * 452 | * 'engine_arguments': 453 | * List of arguments (strings) that will be passed to the engine. 454 | * 455 | * 'persistent_storage': 456 | * Boolean toggling the usage of persistent storage. 457 | * 458 | * 'custom_heap_size': 459 | * Number of bytes specifying the memory heap size. 460 | * 461 | * 'disable_context_menu': 462 | * Disables the right-click context menu on the canvas element if true. 463 | * 464 | * 'retry_time': 465 | * Pause before retry file loading after error. 466 | * 467 | * 'retry_count': 468 | * How many attempts we do when trying to download a file. 469 | * 470 | * 'can_not_download_file_callback': 471 | * Function that is called if you can't download file after 'retry_count' attempts. 472 | **/ 473 | runApp: function(app_canvas_id, extra_params) { 474 | app_canvas_id = (typeof app_canvas_id === 'undefined') ? 'canvas' : app_canvas_id; 475 | 476 | var params = { 477 | archive_location_filter: function(path) { return 'split' + path; }, 478 | unsupported_webgl_callback: undefined, 479 | engine_arguments: [], 480 | persistent_storage: true, 481 | custom_heap_size: undefined, 482 | disable_context_menu: true, 483 | retry_time: 1, 484 | retry_count: 10, 485 | can_not_download_file_callback: undefined, 486 | }; 487 | 488 | for (var k in extra_params) { 489 | if (extra_params.hasOwnProperty(k)) { 490 | params[k] = extra_params[k]; 491 | } 492 | } 493 | 494 | Module.canvas = document.getElementById(app_canvas_id); 495 | Module.arguments = params["engine_arguments"]; 496 | Module.persistentStorage = params["persistent_storage"]; 497 | Module["TOTAL_MEMORY"] = params["custom_heap_size"]; 498 | 499 | if (Module.hasWebGLSupport()) { 500 | // Override game keys 501 | CanvasInput.addToCanvas(Module.canvas); 502 | 503 | Module.setupVisibilityChangeListener(); 504 | 505 | // Add progress visuals 506 | Progress.addProgress(Module.canvas); 507 | 508 | // Add context menu hide-handler if requested 509 | if (params["disable_context_menu"]) 510 | { 511 | Module.canvas.oncontextmenu = function(e) { 512 | e.preventDefault(); 513 | }; 514 | } 515 | 516 | Combine._retry_time = params["retry_time"]; 517 | Combine._max_retry_count = params["retry_count"]; 518 | if (typeof params["can_not_download_file_callback"] === "function") { 519 | Combine._can_not_download_file_callback = params["can_not_download_file_callback"]; 520 | } 521 | // Load and assemble archive 522 | Combine.addCombineCompletedListener(Module.onArchiveFileLoaded); 523 | Combine.addAllTargetsBuiltListener(Module.onArchiveLoaded); 524 | Combine.addProgressListener(Module.onArchiveLoadProgress); 525 | Combine._archiveLocationFilter = params["archive_location_filter"]; 526 | Combine.process(Combine._archiveLocationFilter('/archive_files.json')); 527 | } else { 528 | Progress.addProgress(Module.canvas); 529 | Progress.updateProgress(100, "Unable to start game, WebGL not supported"); 530 | Module.setStatus = function(text) { 531 | if (text) Module.printErr('[missing WebGL] ' + text); 532 | }; 533 | 534 | if (typeof params["unsupported_webgl_callback"] === "function") { 535 | params["unsupported_webgl_callback"](); 536 | } 537 | } 538 | }, 539 | 540 | onArchiveLoadProgress: function(downloaded, total) { 541 | Progress.updateProgress(downloaded / total * 100); 542 | }, 543 | 544 | onArchiveFileLoaded: function(name, data) { 545 | Module._filesToPreload.push({path: name, data: data}); 546 | }, 547 | 548 | onArchiveLoaded: function() { 549 | Combine.cleanUp(); 550 | Module._archiveLoaded = true; 551 | Progress.updateProgress(100, "Starting..."); 552 | 553 | if (Module._waitingForArchive) { 554 | Module._preloadAndCallMain(); 555 | } 556 | }, 557 | 558 | toggleFullscreen: function() { 559 | if (GLFW.isFullscreen) { 560 | GLFW.cancelFullScreen(); 561 | } else { 562 | GLFW.requestFullScreen(); 563 | } 564 | }, 565 | 566 | preSync: function(done) { 567 | // Initial persistent sync before main is called 568 | FS.syncfs(true, function(err) { 569 | if(err) { 570 | Module._syncTries += 1; 571 | console.error("FS syncfs error: " + err); 572 | if (Module._syncMaxTries > Module._syncTries) { 573 | Module.preSync(done); 574 | } else { 575 | Module._syncInitial = true; 576 | done(); 577 | } 578 | } else { 579 | Module._syncInitial = true; 580 | if (done !== undefined) { 581 | done(); 582 | } 583 | } 584 | }); 585 | }, 586 | 587 | preloadAll: function() { 588 | if (Module._preLoadDone) { 589 | return; 590 | } 591 | Module._preLoadDone = true; 592 | for (var i = 0; i < Module._filesToPreload.length; ++i) { 593 | var item = Module._filesToPreload[i]; 594 | FS.createPreloadedFile("", item.path, item.data, true, true); 595 | } 596 | }, 597 | 598 | // Tries to do a MEM->IDB sync 599 | // It will flag that another one is needed if there is already one sync running. 600 | persistentSync: function() { 601 | 602 | // Need to wait for the initial sync to finish since it 603 | // will call close on all its file streams which will trigger 604 | // new persistentSync for each. 605 | if (Module._syncInitial) { 606 | if (Module._syncInProgress) { 607 | Module._syncNeeded = true; 608 | } else { 609 | Module._startSyncFS(); 610 | } 611 | } 612 | }, 613 | 614 | preInit: [function() { 615 | /* Mount filesystem on preinit */ 616 | var dir = DMSYS.GetUserPersistentDataRoot(); 617 | FS.mkdir(dir); 618 | 619 | // If IndexedDB is supported we mount the persistent data root as IDBFS, 620 | // then try to do a IDB->MEM sync before we start the engine to get 621 | // previously saved data before boot. 622 | window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 623 | if (Module.persistentStorage && window.indexedDB) { 624 | FS.mount(IDBFS, {}, dir); 625 | 626 | // Patch FS.close so it will try to sync MEM->IDB 627 | var _close = FS.close; FS.close = function(stream) { var r = _close(stream); Module.persistentSync(); return r; } 628 | 629 | // Sync IDB->MEM before calling main() 630 | Module.preSync(function() { 631 | Module._preloadAndCallMain(); 632 | }); 633 | } else { 634 | Module._preloadAndCallMain(); 635 | } 636 | }], 637 | 638 | preRun: [function() { 639 | /* If archive is loaded, preload all its files */ 640 | if(Module._archiveLoaded) { 641 | Module.preloadAll(); 642 | } 643 | }], 644 | 645 | postRun: [function() { 646 | if(Module._archiveLoaded) { 647 | Progress.removeProgress(); 648 | } 649 | }], 650 | 651 | _preloadAndCallMain: function() { 652 | // If the archive isn't loaded, 653 | // we will have to wait with calling main. 654 | if (!Module._archiveLoaded) { 655 | Module._waitingForArchive = true; 656 | } else { 657 | 658 | // Need to set heap size before calling main 659 | TOTAL_MEMORY = Module["TOTAL_MEMORY"] || TOTAL_MEMORY; 660 | 661 | Module.preloadAll(); 662 | Progress.removeProgress(); 663 | if (Module.callMain === undefined) { 664 | Module.noInitialRun = false; 665 | } else { 666 | Module.callMain(Module.arguments); 667 | } 668 | } 669 | }, 670 | 671 | // Wrap IDBFS syncfs call with logic to avoid multiple syncs 672 | // running at the same time. 673 | _startSyncFS: function() { 674 | Module._syncInProgress = true; 675 | 676 | if (Module._syncMaxTries > Module._syncTries) { 677 | FS.syncfs(false, function(err) { 678 | Module._syncInProgress = false; 679 | 680 | if (err) { 681 | console.error("Module._startSyncFS error: " + err); 682 | Module._syncTries += 1; 683 | } 684 | 685 | if (Module._syncNeeded) { 686 | Module._syncNeeded = false; 687 | Module._startSyncFS(); 688 | } 689 | 690 | }); 691 | } 692 | }, 693 | }; 694 | 695 | window.onerror = function(err, url, line, column, errObj) { 696 | var errorObject = Module.prepareErrorObject(err, url, line, column, errObj); 697 | Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]); 698 | Module.setStatus('Exception thrown, see JavaScript console'); 699 | Module.setStatus = function(text) { 700 | if (text) Module.printErr('[post-exception status] ' + text); 701 | }; 702 | }; 703 | -------------------------------------------------------------------------------- /docs/extension-websocket/extensionwebsocket.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/docs/extension-websocket/extensionwebsocket.wasm -------------------------------------------------------------------------------- /docs/extension-websocket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | extension-websocket 1.0 11 | 141 | 142 | 143 | 144 |
145 | 146 |
147 |
Fullscreen
148 | 149 |
150 |
151 | 152 | 153 | 154 | 241 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Defold websocket extension API documentation 3 | brief: This manual covers how to use websockets with Defold 4 | --- 5 | 6 | # Defold websocket extension API documentation 7 | 8 | This extension supports both secure (`wss://`) and non secure (`ws://`) websocket connections. 9 | All platforms should support this extension. 10 | 11 | 12 | Here is how you connect to a websocket and listen to events: 13 | 14 | ```lua 15 | local function websocket_callback(self, conn, data) 16 | if data.event == websocket.EVENT_DISCONNECTED then 17 | print("Disconnected: " .. tostring(conn)) 18 | self.connection = nil 19 | update_gui(self) 20 | elseif data.event == websocket.EVENT_CONNECTED then 21 | update_gui(self) 22 | print("Connected: " .. tostring(conn)) 23 | elseif data.event == websocket.EVENT_ERROR then 24 | print("Error: '" .. tostring(data.message) .. "'") 25 | if data.handshake_response then 26 | print("Handshake response status: '" .. tostring(data.handshake_response.status) .. "'") 27 | for key, value in pairs(data.handshake_response.headers) do 28 | log("Handshake response header: '" .. key .. ": " .. value .. "'") 29 | end 30 | print("Handshake response body: '" .. tostring(data.handshake_response.response) .. "'") 31 | end 32 | elseif data.event == websocket.EVENT_MESSAGE then 33 | print("Receiving: '" .. tostring(data.message) .. "'") 34 | end 35 | end 36 | 37 | function init(self) 38 | self.url = "ws://echo.websocket.org" 39 | local params = {} 40 | self.connection = websocket.connect(self.url, params, websocket_callback) 41 | end 42 | 43 | function finalize(self) 44 | if self.connection ~= nil then 45 | websocket.disconnect(self.connection) 46 | end 47 | end 48 | ``` 49 | 50 | 51 | ## Installation 52 | To use this library in your Defold project, add the following URL to your `game.project` dependencies: 53 | 54 | https://github.com/defold/extension-websocket/archive/master.zip 55 | 56 | We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-websocket/releases). 57 | 58 | 59 | ## Configuration 60 | The following configuration options can be set in the game.project file: 61 | 62 | ``` 63 | [websocket] 64 | debug = 1 65 | socket_timeout = 10000000 66 | ``` 67 | 68 | * `debug` - Log level where 0 means no logs and higher values shows more logs (currently 1 or 2). 69 | * `socket_timeout` - Timeout for the underlying socket connection. In microseconds. 70 | 71 | 72 | ## Source code 73 | 74 | The source code is available on [GitHub](https://github.com/defold/extension-websocket) 75 | -------------------------------------------------------------------------------- /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/defold/extension-websocket/3331ec1a8db068e56cd04f96302944ee46eabc71/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: 949.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: 314.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: 184.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: 249.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: 314.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 | nodes { 711 | position { 712 | x: 320.0 713 | y: 314.0 714 | z: 0.0 715 | w: 1.0 716 | } 717 | rotation { 718 | x: 0.0 719 | y: 0.0 720 | z: 0.0 721 | w: 1.0 722 | } 723 | scale { 724 | x: 1.0 725 | y: 1.0 726 | z: 1.0 727 | w: 1.0 728 | } 729 | size { 730 | x: 200.0 731 | y: 100.0 732 | z: 0.0 733 | w: 1.0 734 | } 735 | color { 736 | x: 1.0 737 | y: 1.0 738 | z: 1.0 739 | w: 1.0 740 | } 741 | type: TYPE_TEXT 742 | blend_mode: BLEND_MODE_ALPHA 743 | text: "" 744 | font: "example" 745 | id: "connection_text" 746 | xanchor: XANCHOR_NONE 747 | yanchor: YANCHOR_NONE 748 | pivot: PIVOT_CENTER 749 | outline { 750 | x: 1.0 751 | y: 1.0 752 | z: 1.0 753 | w: 1.0 754 | } 755 | shadow { 756 | x: 1.0 757 | y: 1.0 758 | z: 1.0 759 | w: 1.0 760 | } 761 | adjust_mode: ADJUST_MODE_FIT 762 | line_break: false 763 | layer: "" 764 | inherit_alpha: true 765 | alpha: 1.0 766 | outline_alpha: 1.0 767 | shadow_alpha: 1.0 768 | template_node_child: false 769 | text_leading: 1.0 770 | text_tracking: 0.0 771 | } 772 | layers { 773 | name: "below" 774 | } 775 | layers { 776 | name: "dynamic" 777 | } 778 | layers { 779 | name: "text" 780 | } 781 | layers { 782 | name: "above" 783 | } 784 | material: "/builtins/materials/gui.material" 785 | adjust_reference: ADJUST_REFERENCE_PARENT 786 | max_nodes: 512 787 | -------------------------------------------------------------------------------- /examples/websocket.gui_script: -------------------------------------------------------------------------------- 1 | local URL = "echo.websocket.events" 2 | 3 | local function click_button(node, action) 4 | return gui.is_enabled(node) and action.pressed and gui.pick_node(node, action.x, action.y) 5 | end 6 | 7 | local function update_gui(self) 8 | if self.connection 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 | gui.set_enabled(self.connection_text, true) 14 | gui.set_text(self.connection_text, "Connected to " .. self.url) 15 | else 16 | gui.set_enabled(self.connect_ws_node, true) 17 | gui.set_enabled(self.connect_wss_node, true) 18 | gui.set_enabled(self.send_node, false) 19 | gui.set_enabled(self.close_node, false) 20 | gui.set_enabled(self.connection_text, false) 21 | end 22 | end 23 | 24 | local function log(...) 25 | local text = "" 26 | local len = select("#", ...) 27 | for i=1,len do 28 | text = text .. tostring(select(i, ...)) .. (i == len and "" or ", ") 29 | end 30 | 31 | print(text) 32 | local node = gui.get_node("log") 33 | gui.set_text(node, gui.get_text(node) .. "\n" .. text) 34 | end 35 | 36 | function init(self) 37 | msg.post(".", "acquire_input_focus") 38 | msg.post("@render:", "clear_color", { color = vmath.vector4(0.2, 0.4, 0.8, 1.0) }) 39 | self.connect_ws_node = gui.get_node("connect_ws/button") 40 | self.connect_wss_node = gui.get_node("connect_wss/button") 41 | self.send_node = gui.get_node("send/button") 42 | self.close_node = gui.get_node("close/button") 43 | self.connection_text = gui.get_node("connection_text") 44 | self.connection = nil 45 | update_gui(self) 46 | end 47 | 48 | function final(self) 49 | msg.post(".", "release_input_focus") 50 | end 51 | 52 | function update(self, dt) 53 | if self.ws then 54 | self.ws.step() 55 | end 56 | end 57 | 58 | local function websocket_callback(self, conn, data) 59 | if data.event == websocket.EVENT_DISCONNECTED then 60 | log("Disconnected: " .. tostring(conn) .. " Code: " .. data.code .. " Message: " .. tostring(data.message)) 61 | self.connection = nil 62 | update_gui(self) 63 | elseif data.event == websocket.EVENT_CONNECTED then 64 | update_gui(self) 65 | log("Connected: " .. tostring(conn)) 66 | elseif data.event == websocket.EVENT_ERROR then 67 | log("Error: '" .. tostring(data.message) .. "'") 68 | if data.handshake_response then 69 | log("Handshake response status: '" .. tostring(data.handshake_response.status) .. "'") 70 | for key, value in pairs(data.handshake_response.headers) do 71 | log("Handshake response header: '" .. key .. ": " .. value .. "'") 72 | end 73 | log("Handshake response body: '" .. tostring(data.handshake_response.response) .. "'") 74 | end 75 | elseif data.event == websocket.EVENT_MESSAGE then 76 | log("Receiving: '" .. tostring(data.message) .. "'") 77 | end 78 | end 79 | 80 | local function connect(self, scheme) 81 | local params = {} 82 | 83 | self.url = scheme .. "://" .. URL 84 | log("Connecting to " .. self.url) 85 | self.connection = websocket.connect(self.url, params, websocket_callback) 86 | end 87 | 88 | local function disconnect(self) 89 | if self.connection ~= nil then 90 | websocket.disconnect(self.connection) 91 | end 92 | self.connection = nil 93 | end 94 | 95 | 96 | function on_input(self, action_id, action) 97 | if click_button(self.connect_ws_node, action) then 98 | connect(self, "ws") 99 | elseif click_button(self.connect_wss_node, action) then 100 | connect(self, "wss") 101 | elseif click_button(self.close_node, action) then 102 | disconnect(self) 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 = websocket.send(self.connection, 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 | [bootstrap] 2 | main_collection = /examples/websocket.collectionc 3 | 4 | [script] 5 | shared_state = 1 6 | 7 | [display] 8 | width = 640 9 | height = 960 10 | 11 | [project] 12 | title = extension-websocket 13 | _dependencies = https://github.com/GameAnalytics/defold-openssl/archive/1.0.0.zip 14 | 15 | [library] 16 | include_dirs = websocket 17 | -------------------------------------------------------------------------------- /input/game.input_binding: -------------------------------------------------------------------------------- 1 | mouse_trigger { 2 | input: MOUSE_BUTTON_1 3 | action: "touch" 4 | } 5 | -------------------------------------------------------------------------------- /websocket/api/api.script_api: -------------------------------------------------------------------------------- 1 | - name: websocket 2 | type: table 3 | desc: Functions and constants for using websockets. Supported on all platforms. 4 | members: 5 | 6 | #***************************************************************************************************** 7 | 8 | - name: connect 9 | type: function 10 | desc: Connects to a remote address 11 | parameters: 12 | - name: url 13 | type: string 14 | desc: url of the remote connection 15 | 16 | - name: params 17 | type: table 18 | desc: optional parameters as properties. The following parameters can be set 19 | members: 20 | - name: timeout 21 | type: number 22 | desc: Timeout for the connection sequence (milliseconds). Not used on HTML5. (Default is 3000) 23 | 24 | - name: protocol 25 | type: string 26 | desc: the protocol to use (e.g. 'chat'). If not set, no `Sec-WebSocket-Protocol` header is sent. 27 | 28 | - name: headers 29 | type: string 30 | desc: list of http headers. Each pair is separated with "\r\n". Not used on HTML5. 31 | 32 | - name: callback 33 | type: function 34 | desc: callback that receives all messages from the connection 35 | parameters: 36 | - name: self 37 | type: object 38 | desc: The script instance that was used to register the callback 39 | 40 | - name: connection 41 | type: object 42 | desc: the connection 43 | 44 | - name: data 45 | type: table 46 | desc: the event payload 47 | members: 48 | - name: event 49 | type: number 50 | desc: The current event. One of the following 51 | 52 | - `websocket.EVENT_CONNECTED` 53 | 54 | - `websocket.EVENT_DISCONNECTED` 55 | 56 | - `websocket.EVENT_ERROR` 57 | 58 | - `websocket.EVENT_MESSAGE` 59 | 60 | - name: message 61 | type: string 62 | desc: The received data if event is `websocket.EVENT_MESSAGE`. Error message otherwise 63 | 64 | - name: handshake_response 65 | type: table 66 | desc: Handshake response information (status, headers etc) 67 | 68 | - name: code 69 | type: number 70 | desc: Status code received from the server if the server closed the connection. Only present if event is `EVENT_DISCONNECTED`. 71 | 72 | 73 | returns: 74 | - name: connection 75 | type: object 76 | desc: the connection 77 | 78 | examples: 79 | - desc: |- 80 | ```lua 81 | local function websocket_callback(self, conn, data) 82 | if data.event == websocket.EVENT_DISCONNECTED then 83 | log("Disconnected: " .. tostring(conn)) 84 | self.connection = nil 85 | update_gui(self) 86 | elseif data.event == websocket.EVENT_CONNECTED then 87 | update_gui(self) 88 | log("Connected: " .. tostring(conn)) 89 | elseif data.event == websocket.EVENT_ERROR then 90 | log("Error: '" .. data.message .. "'") 91 | elseif data.event == websocket.EVENT_MESSAGE then 92 | log("Receiving: '" .. tostring(data.message) .. "'") 93 | end 94 | end 95 | 96 | function init(self) 97 | self.url = "ws://echo.websocket.events" 98 | local params = { 99 | timeout = 3000, 100 | headers = "Sec-WebSocket-Protocol: chat\r\nOrigin: mydomain.com\r\n" 101 | } 102 | self.connection = websocket.connect(self.url, params, websocket_callback) 103 | end 104 | 105 | function finalize(self) 106 | if self.connection ~= nil then 107 | websocket.disconnect(self.connection) 108 | end 109 | end 110 | ``` 111 | 112 | #***************************************************************************************************** 113 | 114 | - name: disconnect 115 | type: function 116 | desc: Explicitly close a websocket 117 | parameters: 118 | - name: connection 119 | type: object 120 | desc: the websocket connection 121 | 122 | #***************************************************************************************************** 123 | 124 | - name: send 125 | type: function 126 | desc: Send data on a websocket 127 | parameters: 128 | - name: connection 129 | type: object 130 | desc: the websocket connection 131 | - name: message 132 | type: string 133 | desc: the message to send 134 | - name: options 135 | type: table 136 | desc: options for this particular message. May be `nil` 137 | members: 138 | - name: type 139 | type: number 140 | desc: The data type of the message 141 | 142 | - `websocket.DATA_TYPE_BINARY` (default) 143 | 144 | - `websocket.DATA_TYPE_TEXT` 145 | 146 | examples: 147 | - desc: |- 148 | ```lua 149 | local function websocket_callback(self, conn, data) 150 | if data.event == websocket.EVENT_CONNECTED then 151 | websocket.send(conn, "Hello from the other side") 152 | end 153 | end 154 | 155 | function init(self) 156 | self.url = "ws://echo.websocket.org" 157 | local params = {} 158 | self.connection = websocket.connect(self.url, params, websocket_callback) 159 | end 160 | ``` 161 | 162 | #***************************************************************************************************** 163 | 164 | - name: EVENT_CONNECTED 165 | type: number 166 | desc: The websocket was connected 167 | 168 | - name: EVENT_DISCONNECTED 169 | type: number 170 | desc: The websocket disconnected 171 | 172 | - name: EVENT_MESSAGE 173 | type: number 174 | desc: The websocket received data 175 | 176 | - name: EVENT_ERROR 177 | type: number 178 | desc: The websocket encountered an error 179 | -------------------------------------------------------------------------------- /websocket/ext.manifest: -------------------------------------------------------------------------------- 1 | # C++ symbol in your extension 2 | name: "Websocket" 3 | 4 | platforms: 5 | common: 6 | context: 7 | includes: ["upload/websocket/include/wslay"] 8 | defines: ["HAVE_CONFIG_H"] 9 | 10 | web: 11 | context: 12 | linkFlags: ["-lwebsocket.js"] -------------------------------------------------------------------------------- /websocket/include/wslay/config.h: -------------------------------------------------------------------------------- 1 | /* This configuration file is used only by CMake build system. */ 2 | #ifndef CONFIG_H 3 | #define CONFIG_H 4 | 5 | #if defined(_WIN32) 6 | #define HAVE_WINSOCK2_H 7 | #else 8 | #define HAVE_ARPA_INET_H 9 | #define HAVE_NETINET_IN_H 10 | #endif 11 | /* #undef HAVE_WINSOCK2_H */ 12 | /* #undef WORDS_BIGENDIAN */ 13 | 14 | #endif /* CONFIG_H */ 15 | -------------------------------------------------------------------------------- /websocket/include/wslay/wslay_event.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #ifndef WSLAY_EVENT_H 26 | #define WSLAY_EVENT_H 27 | 28 | #ifdef HAVE_CONFIG_H 29 | # include 30 | #endif /* HAVE_CONFIG_H */ 31 | 32 | #include 33 | 34 | struct wslay_stack; 35 | struct wslay_queue; 36 | 37 | struct wslay_event_byte_chunk { 38 | uint8_t *data; 39 | size_t data_length; 40 | }; 41 | 42 | struct wslay_event_imsg { 43 | uint8_t fin; 44 | uint8_t rsv; 45 | uint8_t opcode; 46 | uint32_t utf8state; 47 | struct wslay_queue *chunks; 48 | size_t msg_length; 49 | }; 50 | 51 | enum wslay_event_msg_type { 52 | WSLAY_NON_FRAGMENTED, 53 | WSLAY_FRAGMENTED 54 | }; 55 | 56 | struct wslay_event_omsg { 57 | uint8_t fin; 58 | uint8_t opcode; 59 | uint8_t rsv; 60 | enum wslay_event_msg_type type; 61 | 62 | uint8_t *data; 63 | size_t data_length; 64 | 65 | union wslay_event_msg_source source; 66 | wslay_event_fragmented_msg_callback read_callback; 67 | }; 68 | 69 | struct wslay_event_frame_user_data { 70 | wslay_event_context_ptr ctx; 71 | void *user_data; 72 | }; 73 | 74 | enum wslay_event_close_status { 75 | WSLAY_CLOSE_RECEIVED = 1 << 0, 76 | WSLAY_CLOSE_QUEUED = 1 << 1, 77 | WSLAY_CLOSE_SENT = 1 << 2 78 | }; 79 | 80 | enum wslay_event_config { 81 | WSLAY_CONFIG_NO_BUFFERING = 1 << 0 82 | }; 83 | 84 | struct wslay_event_context { 85 | /* config status, bitwise OR of enum wslay_event_config values*/ 86 | uint32_t config; 87 | /* maximum message length that can be received */ 88 | uint64_t max_recv_msg_length; 89 | /* 1 if initialized for server, otherwise 0 */ 90 | uint8_t server; 91 | /* bitwise OR of enum wslay_event_close_status values */ 92 | uint8_t close_status; 93 | /* status code in received close control frame */ 94 | uint16_t status_code_recv; 95 | /* status code in sent close control frame */ 96 | uint16_t status_code_sent; 97 | wslay_frame_context_ptr frame_ctx; 98 | /* 1 if reading is enabled, otherwise 0. Upon receiving close 99 | control frame this value set to 0. If any errors in read 100 | operation will also set this value to 0. */ 101 | uint8_t read_enabled; 102 | /* 1 if writing is enabled, otherwise 0 Upon completing sending 103 | close control frame, this value set to 0. If any errors in write 104 | opration will also set this value to 0. */ 105 | uint8_t write_enabled; 106 | /* imsg buffer to allow interleaved control frame between 107 | non-control frames. */ 108 | struct wslay_event_imsg imsgs[2]; 109 | /* Pointer to imsgs to indicate current used buffer. */ 110 | struct wslay_event_imsg *imsg; 111 | /* payload length of frame currently being received. */ 112 | uint64_t ipayloadlen; 113 | /* next byte offset of payload currently being received. */ 114 | uint64_t ipayloadoff; 115 | /* error value set by user callback */ 116 | int error; 117 | /* Pointer to the message currently being sent. NULL if no message 118 | is currently sent. */ 119 | struct wslay_event_omsg *omsg; 120 | /* Queue for non-control frames */ 121 | struct wslay_queue/**/ *send_queue; 122 | /* Queue for control frames */ 123 | struct wslay_queue/**/ *send_ctrl_queue; 124 | /* Size of send_queue + size of send_ctrl_queue */ 125 | size_t queued_msg_count; 126 | /* The sum of message length in send_queue */ 127 | size_t queued_msg_length; 128 | /* Buffer used for fragmented messages */ 129 | uint8_t obuf[4096]; 130 | uint8_t *obuflimit; 131 | uint8_t *obufmark; 132 | /* payload length of frame currently being sent. */ 133 | uint64_t opayloadlen; 134 | /* next byte offset of payload currently being sent. */ 135 | uint64_t opayloadoff; 136 | struct wslay_event_callbacks callbacks; 137 | struct wslay_event_frame_user_data frame_user_data; 138 | void *user_data; 139 | uint8_t allowed_rsv_bits; 140 | }; 141 | 142 | #endif /* WSLAY_EVENT_H */ 143 | -------------------------------------------------------------------------------- /websocket/include/wslay/wslay_frame.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #ifndef WSLAY_FRAME_H 26 | #define WSLAY_FRAME_H 27 | 28 | #ifdef HAVE_CONFIG_H 29 | # include 30 | #endif /* HAVE_CONFIG_H */ 31 | 32 | #include 33 | 34 | enum wslay_frame_state { 35 | PREP_HEADER, 36 | SEND_HEADER, 37 | SEND_PAYLOAD, 38 | RECV_HEADER1, 39 | RECV_PAYLOADLEN, 40 | RECV_EXT_PAYLOADLEN, 41 | RECV_MASKKEY, 42 | RECV_PAYLOAD 43 | }; 44 | 45 | struct wslay_frame_opcode_memo { 46 | uint8_t fin; 47 | uint8_t opcode; 48 | uint8_t rsv; 49 | }; 50 | 51 | struct wslay_frame_context { 52 | uint8_t ibuf[4096]; 53 | uint8_t *ibufmark; 54 | uint8_t *ibuflimit; 55 | struct wslay_frame_opcode_memo iom; 56 | uint64_t ipayloadlen; 57 | uint64_t ipayloadoff; 58 | uint8_t imask; 59 | uint8_t imaskkey[4]; 60 | enum wslay_frame_state istate; 61 | size_t ireqread; 62 | 63 | uint8_t oheader[14]; 64 | uint8_t *oheadermark; 65 | uint8_t *oheaderlimit; 66 | uint64_t opayloadlen; 67 | uint64_t opayloadoff; 68 | uint8_t omask; 69 | uint8_t omaskkey[4]; 70 | enum wslay_frame_state ostate; 71 | 72 | struct wslay_frame_callbacks callbacks; 73 | void *user_data; 74 | }; 75 | 76 | #endif /* WSLAY_FRAME_H */ 77 | -------------------------------------------------------------------------------- /websocket/include/wslay/wslay_net.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #ifndef WSLAY_NET_H 26 | #define WSLAY_NET_H 27 | 28 | #ifdef HAVE_CONFIG_H 29 | # include 30 | #endif /* HAVE_CONFIG_H */ 31 | 32 | #include 33 | 34 | #ifdef HAVE_ARPA_INET_H 35 | # include 36 | #endif /* HAVE_ARPA_INET_H */ 37 | #ifdef HAVE_NETINET_IN_H 38 | # include 39 | #endif /* HAVE_NETINET_IN_H */ 40 | /* For Mingw build */ 41 | #ifdef HAVE_WINSOCK2_H 42 | # include 43 | #endif /* HAVE_WINSOCK2_H */ 44 | 45 | #ifdef WORDS_BIGENDIAN 46 | # define ntoh64(x) (x) 47 | # define hton64(x) (x) 48 | #else /* !WORDS_BIGENDIAN */ 49 | uint64_t wslay_byteswap64(uint64_t x); 50 | # define ntoh64(x) wslay_byteswap64(x) 51 | # define hton64(x) wslay_byteswap64(x) 52 | #endif /* !WORDS_BIGENDIAN */ 53 | 54 | #endif /* WSLAY_NET_H */ 55 | -------------------------------------------------------------------------------- /websocket/include/wslay/wslay_queue.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #ifndef WSLAY_QUEUE_H 26 | #define WSLAY_QUEUE_H 27 | 28 | #ifdef HAVE_CONFIG_H 29 | # include "config.h" 30 | #endif /* HAVE_CONFIG_H */ 31 | 32 | #include 33 | 34 | struct wslay_queue_cell { 35 | void *data; 36 | struct wslay_queue_cell *next; 37 | }; 38 | 39 | struct wslay_queue { 40 | struct wslay_queue_cell *top; 41 | struct wslay_queue_cell *tail; 42 | }; 43 | 44 | struct wslay_queue* wslay_queue_new(void); 45 | void wslay_queue_free(struct wslay_queue *queue); 46 | int wslay_queue_push(struct wslay_queue *queue, void *data); 47 | int wslay_queue_push_front(struct wslay_queue *queue, void *data); 48 | void wslay_queue_pop(struct wslay_queue *queue); 49 | void* wslay_queue_top(struct wslay_queue *queue); 50 | void* wslay_queue_tail(struct wslay_queue *queue); 51 | int wslay_queue_empty(struct wslay_queue *queue); 52 | 53 | #endif /* WSLAY_QUEUE_H */ 54 | -------------------------------------------------------------------------------- /websocket/include/wslay/wslay_stack.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #ifndef WSLAY_STACK_H 26 | #define WSLAY_STACK_H 27 | 28 | #ifdef HAVE_CONFIG_H 29 | # include "config.h" 30 | #endif /* HAVE_CONFIG_H */ 31 | 32 | #include 33 | 34 | struct wslay_stack_cell { 35 | void *data; 36 | struct wslay_stack_cell *next; 37 | }; 38 | 39 | struct wslay_stack { 40 | struct wslay_stack_cell *top; 41 | }; 42 | 43 | struct wslay_stack* wslay_stack_new(); 44 | void wslay_stack_free(struct wslay_stack *stack); 45 | int wslay_stack_push(struct wslay_stack *stack, void *data); 46 | void wslay_stack_pop(struct wslay_stack *stack); 47 | void* wslay_stack_top(struct wslay_stack *stack); 48 | int wslay_stack_empty(struct wslay_stack *stack); 49 | 50 | #endif /* WSLAY_STACK_H */ 51 | -------------------------------------------------------------------------------- /websocket/include/wslay/wslayver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #ifndef WSLAYVER_H 26 | #define WSLAYVER_H 27 | 28 | /* Version number of wslay release */ 29 | #define WSLAY_VERSION "1.0.1-DEV" 30 | 31 | #endif /* WSLAYVER_H */ 32 | -------------------------------------------------------------------------------- /websocket/src/emscripten_callbacks.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket.h" 2 | 3 | #if defined(__EMSCRIPTEN__) 4 | 5 | namespace dmWebsocket 6 | { 7 | EM_BOOL Emscripten_WebSocketOnOpen(int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData) { 8 | DebugLog(1, "WebSocket OnOpen"); 9 | WebsocketConnection* conn = (WebsocketConnection*)userData; 10 | SetState(conn, STATE_CONNECTED); 11 | HandleCallback(conn, EVENT_CONNECTED, 0, 0); 12 | return EM_TRUE; 13 | } 14 | EM_BOOL Emscripten_WebSocketOnError(int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData) { 15 | DebugLog(1, "WebSocket OnError"); 16 | WebsocketConnection* conn = (WebsocketConnection*)userData; 17 | conn->m_Status = RESULT_ERROR; 18 | SetState(conn, STATE_DISCONNECTED); 19 | return EM_TRUE; 20 | } 21 | EM_BOOL Emscripten_WebSocketOnClose(int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData) { 22 | DebugLog(1, "WebSocket OnClose"); 23 | WebsocketConnection* conn = (WebsocketConnection*)userData; 24 | int length = strlen(websocketEvent->reason); 25 | PushMessage(conn, MESSAGE_TYPE_CLOSE, length, (uint8_t*)websocketEvent->reason, websocketEvent->code); 26 | return EM_TRUE; 27 | } 28 | EM_BOOL Emscripten_WebSocketOnMessage(int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData) { 29 | DebugLog(1, "WebSocket OnMessage"); 30 | WebsocketConnection* conn = (WebsocketConnection*)userData; 31 | int length = websocketEvent->numBytes; 32 | if (websocketEvent->isText) 33 | { 34 | length--; 35 | } 36 | PushMessage(conn, MESSAGE_TYPE_NORMAL, length, websocketEvent->data, 0); 37 | return EM_TRUE; 38 | } 39 | 40 | } // namespace 41 | 42 | #endif // __EMSCRIPTEN__ 43 | -------------------------------------------------------------------------------- /websocket/src/handshake.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket.h" 2 | #include 3 | #include 4 | #include // tolower 5 | 6 | namespace dmWebsocket 7 | { 8 | 9 | const char* RFC_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // as per the rfc document on page 7 (https://tools.ietf.org/html/rfc6455) 10 | 11 | static void CreateKey(uint8_t* key, size_t len) 12 | { 13 | pcg32_random_t rnd; 14 | pcg32_srandom_r(&rnd, dmTime::GetTime(), 31452); 15 | for (unsigned int i = 0; i < len; i++) { 16 | key[i] = (char)(uint8_t)(pcg32_random_r(&rnd) & 0xFF); 17 | } 18 | } 19 | 20 | #define WS_SENDALL(s) \ 21 | sr = Send(conn, s, strlen(s), 0);\ 22 | if (sr != dmSocket::RESULT_OK)\ 23 | {\ 24 | goto bail;\ 25 | }\ 26 | 27 | static Result SendClientHandshakeHeaders(WebsocketConnection* conn) 28 | { 29 | CreateKey(conn->m_Key, sizeof(conn->m_Key)); 30 | 31 | char encoded_key[64] = {0}; 32 | uint32_t encoded_key_len = sizeof(encoded_key); 33 | 34 | if (!dmCrypt::Base64Encode((const unsigned char*)conn->m_Key, sizeof(conn->m_Key), (unsigned char*)encoded_key, &encoded_key_len)) 35 | { 36 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed to base64 encode key"); 37 | } 38 | 39 | char port[8] = ""; 40 | if (!(conn->m_Url.m_Port == 80 || conn->m_Url.m_Port == 443)) 41 | dmSnPrintf(port, sizeof(port), ":%d", conn->m_Url.m_Port); 42 | 43 | dmSocket::Result sr; 44 | WS_SENDALL("GET "); 45 | if (conn->m_Url.m_Path[0] == '\0') { 46 | WS_SENDALL("/"); // Default to / for empty path 47 | } else { 48 | WS_SENDALL(conn->m_Url.m_Path); 49 | } 50 | WS_SENDALL(" HTTP/1.1\r\n"); 51 | WS_SENDALL("Host: "); 52 | WS_SENDALL(conn->m_Url.m_Hostname); 53 | WS_SENDALL(port); 54 | WS_SENDALL("\r\n"); 55 | WS_SENDALL("Upgrade: websocket\r\n"); 56 | WS_SENDALL("Connection: Upgrade\r\n"); 57 | WS_SENDALL("Sec-WebSocket-Key: "); 58 | WS_SENDALL(encoded_key); 59 | WS_SENDALL("\r\n"); 60 | WS_SENDALL("Sec-WebSocket-Version: 13\r\n"); 61 | 62 | if (conn->m_CustomHeaders) 63 | { 64 | WS_SENDALL(conn->m_CustomHeaders); 65 | // make sure we ended with '\r\n' 66 | int len = strlen(conn->m_CustomHeaders); 67 | bool ended_with_sentinel = len >= 2 && conn->m_CustomHeaders[len - 2] == '\r' && conn->m_CustomHeaders[len - 1] == '\n'; 68 | if (!ended_with_sentinel) 69 | { 70 | WS_SENDALL("\r\n"); 71 | } 72 | } 73 | 74 | if (conn->m_Protocol) { 75 | WS_SENDALL("Sec-WebSocket-Protocol: "); 76 | WS_SENDALL(conn->m_Protocol); 77 | WS_SENDALL("\r\n"); 78 | } 79 | 80 | WS_SENDALL("\r\n"); 81 | 82 | bail: 83 | if (sr != dmSocket::RESULT_OK) 84 | { 85 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "SendClientHandshake failed: %s", dmSocket::ResultToString(sr)); 86 | } 87 | 88 | return RESULT_OK; 89 | } 90 | 91 | #undef WS_SENDALL 92 | 93 | Result SendClientHandshake(WebsocketConnection* conn) 94 | { 95 | dmSocket::Result sr = WaitForSocket(conn, dmSocket::SELECTOR_KIND_WRITE, SOCKET_WAIT_TIMEOUT); 96 | if (dmSocket::RESULT_WOULDBLOCK == sr) 97 | { 98 | return RESULT_WOULDBLOCK; 99 | } 100 | if (dmSocket::RESULT_OK != sr) 101 | { 102 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Connection not ready for sending data: %s", dmSocket::ResultToString(sr)); 103 | } 104 | 105 | // In emscripten, the sockets are actually already websockets, so no handshake necessary 106 | #if defined(__EMSCRIPTEN__) 107 | return RESULT_OK; 108 | #else 109 | return SendClientHandshakeHeaders(conn); 110 | #endif 111 | } 112 | 113 | 114 | #if defined(__EMSCRIPTEN__) 115 | Result ReceiveHeaders(WebsocketConnection* conn) 116 | { 117 | return RESULT_OK; 118 | } 119 | 120 | #else 121 | Result ReceiveHeaders(WebsocketConnection* conn) 122 | { 123 | dmSocket::Result sr = WaitForSocket(conn, dmSocket::SELECTOR_KIND_READ, SOCKET_WAIT_TIMEOUT); 124 | if (dmSocket::RESULT_OK != sr) 125 | { 126 | if (dmSocket::RESULT_WOULDBLOCK) 127 | { 128 | DebugLog(2, "Waiting for socket to be available for reading"); 129 | return RESULT_WOULDBLOCK; 130 | } 131 | 132 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed waiting for more handshake headers: %s", dmSocket::ResultToString(sr)); 133 | } 134 | 135 | int max_to_recv = (int)(conn->m_BufferCapacity - 1) - conn->m_BufferSize; // allow for a terminating null character 136 | 137 | if (max_to_recv <= 0) 138 | { 139 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Receive buffer full: %u bytes", conn->m_BufferCapacity); 140 | } 141 | 142 | int recv_bytes = 0; 143 | sr = Receive(conn, conn->m_Buffer + conn->m_BufferSize, max_to_recv, &recv_bytes); 144 | 145 | if( sr == dmSocket::RESULT_WOULDBLOCK ) 146 | { 147 | sr = dmSocket::RESULT_TRY_AGAIN; 148 | } 149 | 150 | if (sr == dmSocket::RESULT_TRY_AGAIN) 151 | return RESULT_WOULDBLOCK; 152 | 153 | if (sr != dmSocket::RESULT_OK) 154 | { 155 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Receive error: %s", dmSocket::ResultToString(sr)); 156 | } 157 | 158 | conn->m_BufferSize += recv_bytes; 159 | 160 | // NOTE: We have an extra byte for null-termination so no buffer overrun here. 161 | conn->m_Buffer[conn->m_BufferSize] = '\0'; 162 | 163 | // Check if the end of the response has arrived 164 | const char* endtag = strstr(conn->m_Buffer, "\r\n\r\n"); 165 | if (endtag != 0) 166 | { 167 | return RESULT_OK; 168 | } 169 | 170 | return RESULT_WOULDBLOCK; 171 | } 172 | #endif 173 | 174 | static void HandleVersion(void* user_data, int major, int minor, int status, const char* status_str) 175 | { 176 | HandshakeResponse* response = (HandshakeResponse*)user_data; 177 | response->m_HttpMajor = major; 178 | response->m_HttpMinor = minor; 179 | response->m_ResponseStatusCode = status; 180 | } 181 | 182 | 183 | static void HandleHeader(void* user_data, const char* key, const char* value) 184 | { 185 | HandshakeResponse* response = (HandshakeResponse*)user_data; 186 | if (response->m_Headers.Remaining() == 0) 187 | { 188 | response->m_Headers.OffsetCapacity(4); 189 | } 190 | HttpHeader* new_header = new HttpHeader(key, value); 191 | 192 | response->m_Headers.Push(new_header); 193 | } 194 | 195 | 196 | static void HandleContent(void* user_data, int offset) 197 | { 198 | HandshakeResponse* response = (HandshakeResponse*)user_data; 199 | response->m_BodyOffset = offset; 200 | } 201 | 202 | 203 | bool ValidateSecretKey(WebsocketConnection* conn, const char* server_key) 204 | { 205 | uint8_t client_key[32 + 40]; 206 | uint32_t client_key_len = sizeof(client_key); 207 | dmCrypt::Base64Encode(conn->m_Key, sizeof(conn->m_Key), client_key, &client_key_len); 208 | client_key[client_key_len] = 0; 209 | 210 | DebugLog(2, "Secret key (base64): %s", client_key); 211 | 212 | memcpy(client_key + client_key_len, RFC_MAGIC, strlen(RFC_MAGIC)); 213 | client_key_len += strlen(RFC_MAGIC); 214 | client_key[client_key_len] = 0; 215 | 216 | DebugLog(2, "Secret key + RFC_MAGIC: %s", client_key); 217 | 218 | uint8_t client_key_sha1[20]; 219 | dmCrypt::HashSha1(client_key, client_key_len, client_key_sha1); 220 | 221 | DebugPrint(2, "Hashed key (sha1):", client_key_sha1, sizeof(client_key_sha1)); 222 | 223 | client_key_len = sizeof(client_key); 224 | dmCrypt::Base64Encode(client_key_sha1, sizeof(client_key_sha1), client_key, &client_key_len); 225 | client_key[client_key_len] = 0; 226 | DebugLog(2, "Client key (base64): %s", client_key); 227 | DebugLog(2, "Server key (base64): %s", server_key); 228 | 229 | return strcmp(server_key, (const char*)client_key) == 0; 230 | } 231 | 232 | 233 | #if defined(__EMSCRIPTEN__) 234 | Result VerifyHeaders(WebsocketConnection* conn) 235 | { 236 | return RESULT_OK; 237 | } 238 | #else 239 | Result VerifyHeaders(WebsocketConnection* conn) 240 | { 241 | char* r = conn->m_Buffer; 242 | 243 | // Find start of payload now because dmHttpClient::ParseHeader is destructive 244 | const char* start_of_payload = strstr(conn->m_Buffer, "\r\n\r\n"); 245 | start_of_payload += 4; 246 | 247 | HandshakeResponse* response = new HandshakeResponse(); 248 | conn->m_HandshakeResponse = response; 249 | dmHttpClient::ParseResult parse_result = dmHttpClient::ParseHeader(r, response, true, &HandleVersion, &HandleHeader, &HandleContent); 250 | if (parse_result != dmHttpClient::ParseResult::PARSE_RESULT_OK) 251 | { 252 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed to parse handshake response. 'dmHttpClient::ParseResult=%i'", parse_result); 253 | } 254 | 255 | if (response->m_ResponseStatusCode != 101) { 256 | return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Wrong response status: %i", response->m_ResponseStatusCode); 257 | } 258 | 259 | HttpHeader *connection_header, *upgrade_header, *websocket_secret_header; 260 | connection_header = response->GetHeader("Connection"); 261 | upgrade_header = response->GetHeader("Upgrade"); 262 | websocket_secret_header = response->GetHeader("Sec-WebSocket-Accept"); 263 | bool connection = connection_header && dmStrCaseCmp(connection_header->m_Value, "Upgrade") == 0; 264 | bool upgrade = upgrade_header && dmStrCaseCmp(upgrade_header->m_Value, "websocket") == 0; 265 | bool valid_key = websocket_secret_header && ValidateSecretKey(conn, websocket_secret_header->m_Value); 266 | 267 | // Send error to lua? 268 | if (!connection) 269 | dmLogError("Failed to find the Connection keyword in the response headers"); 270 | if (!upgrade) 271 | dmLogError("Failed to find the Upgrade keyword in the response headers"); 272 | if (!valid_key) 273 | dmLogError("Failed to find valid key in the response headers"); 274 | 275 | bool ok = connection && upgrade && valid_key; 276 | if(!ok) 277 | { 278 | return RESULT_HANDSHAKE_FAILED; 279 | } 280 | 281 | delete conn->m_HandshakeResponse; 282 | conn->m_HandshakeResponse = 0; 283 | 284 | // The response might contain both the headers, but also (if successful) the first batch of data 285 | uint32_t size = conn->m_BufferSize - (start_of_payload - conn->m_Buffer); 286 | conn->m_BufferSize = size; 287 | memmove(conn->m_Buffer, start_of_payload, size); 288 | conn->m_Buffer[size] = 0; 289 | conn->m_HasHandshakeData = conn->m_BufferSize != 0 ? 1 : 0; 290 | return RESULT_OK; 291 | } 292 | #endif 293 | 294 | } // namespace -------------------------------------------------------------------------------- /websocket/src/pcg.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket.h" 2 | 3 | // https://www.pcg-random.org/using-pcg-c-basic.html 4 | 5 | namespace dmWebsocket 6 | { 7 | uint32_t pcg32_random_r(pcg32_random_t* rng) 8 | { 9 | uint64_t oldstate = rng->state; 10 | rng->state = oldstate * 6364136223846793005ULL + rng->inc; 11 | uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u; 12 | uint32_t rot = oldstate >> 59u; 13 | return (xorshifted >> rot) | (xorshifted << ((-rot) & 31)); 14 | } 15 | 16 | void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq) 17 | { 18 | rng->state = 0U; 19 | rng->inc = (initseq << 1u) | 1u; 20 | pcg32_random_r(rng); 21 | rng->state += initstate; 22 | pcg32_random_r(rng); 23 | } 24 | } -------------------------------------------------------------------------------- /websocket/src/script_util.cpp: -------------------------------------------------------------------------------- 1 | #include "script_util.h" 2 | 3 | namespace dmScript { 4 | 5 | bool CheckBool(lua_State *L, int numArg) 6 | { 7 | bool b = false; 8 | if (lua_isboolean(L, numArg)) 9 | { 10 | b = lua_toboolean(L, numArg); 11 | } 12 | else 13 | { 14 | luaL_typerror(L, numArg, lua_typename(L, LUA_TBOOLEAN)); 15 | } 16 | return b; 17 | } 18 | 19 | bool CheckBoold(lua_State *L, int numArg, int def) 20 | { 21 | int type = lua_type(L, numArg); 22 | if (type != LUA_TNONE && type != LUA_TNIL) 23 | { 24 | return CheckBool(L, numArg); 25 | } 26 | return def; 27 | } 28 | 29 | lua_Number CheckNumberd(lua_State *L, int numArg, lua_Number def) 30 | { 31 | int type = lua_type(L, numArg); 32 | if (type != LUA_TNONE && type != LUA_TNIL) 33 | { 34 | return luaL_checknumber(L, numArg); 35 | } 36 | return def; 37 | } 38 | 39 | char* CheckStringd(lua_State *L, int numArg, const char* def) 40 | { 41 | int type = lua_type(L, numArg); 42 | if (type != LUA_TNONE && type != LUA_TNIL) 43 | { 44 | return (char*)luaL_checkstring(L, numArg); 45 | } 46 | return (char*)def; 47 | } 48 | 49 | lua_Number CheckTableNumber(lua_State *L, int numArg, const char* field, lua_Number def) 50 | { 51 | lua_Number result = def; 52 | if(lua_istable(L, numArg)) 53 | { 54 | lua_getfield(L, numArg, field); 55 | if(!lua_isnil(L, -1)) 56 | { 57 | result = luaL_checknumber(L, -1); 58 | } 59 | lua_pop(L, 1); 60 | } 61 | return result; 62 | } 63 | 64 | char* CheckTableString(lua_State *L, int numArg, const char* field, char* def) 65 | { 66 | char* result = def; 67 | if(lua_istable(L, numArg)) 68 | { 69 | lua_getfield(L, numArg, field); 70 | if(!lua_isnil(L, -1)) 71 | { 72 | result = (char*)luaL_checkstring(L, -1); 73 | } 74 | lua_pop(L, 1); 75 | } 76 | return result; 77 | } 78 | 79 | } // namespace -------------------------------------------------------------------------------- /websocket/src/script_util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace dmScript { 6 | bool CheckBool(lua_State *L, int numArg); 7 | bool CheckBoold(lua_State *L, int numArg, int def); 8 | lua_Number CheckNumberd(lua_State *L, int numArg, lua_Number def); 9 | char* CheckStringd(lua_State *L, int numArg, const char* def); 10 | lua_Number CheckTableNumber(lua_State *L, int numArg, const char* field, lua_Number def); 11 | char* CheckTableString(lua_State *L, int numArg, const char* field, char* def); 12 | } // namespace -------------------------------------------------------------------------------- /websocket/src/socket.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "websocket.h" 4 | 5 | namespace dmWebsocket 6 | { 7 | 8 | dmSocket::Result WaitForSocket(WebsocketConnection* conn, dmSocket::SelectorKind kind, int timeout) 9 | { 10 | dmSocket::Selector selector; 11 | dmSocket::SelectorZero(&selector); 12 | dmSocket::SelectorSet(&selector, kind, conn->m_Socket); 13 | return dmSocket::Select(&selector, timeout); 14 | } 15 | 16 | dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length, int* out_sent_bytes) 17 | { 18 | int total_sent_bytes = 0; 19 | int sent_bytes = 0; 20 | 21 | while (total_sent_bytes < length) { 22 | dmSocket::Result r; 23 | 24 | if (conn->m_SSLSocket) 25 | r = dmSSLSocket::Send(conn->m_SSLSocket, buffer + total_sent_bytes, length - total_sent_bytes, &sent_bytes); 26 | else 27 | r = dmSocket::Send(conn->m_Socket, buffer + total_sent_bytes, length - total_sent_bytes, &sent_bytes); 28 | 29 | if( r == dmSocket::RESULT_WOULDBLOCK ) 30 | { 31 | r = dmSocket::RESULT_TRY_AGAIN; 32 | } 33 | 34 | if (r == dmSocket::RESULT_TRY_AGAIN) 35 | continue; 36 | 37 | if (r != dmSocket::RESULT_OK) { 38 | return r; 39 | } 40 | 41 | total_sent_bytes += sent_bytes; 42 | } 43 | if (out_sent_bytes) 44 | *out_sent_bytes = total_sent_bytes; 45 | 46 | DebugPrint(2, "Sent buffer:", buffer, length); 47 | return dmSocket::RESULT_OK; 48 | } 49 | 50 | dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes) 51 | { 52 | dmSocket::Result sr; 53 | if (conn->m_SSLSocket) 54 | sr = dmSSLSocket::Receive(conn->m_SSLSocket, buffer, length, received_bytes); 55 | else 56 | sr = dmSocket::Receive(conn->m_Socket, buffer, length, received_bytes); 57 | 58 | int num_bytes = received_bytes ? (uint32_t)*received_bytes : 0; 59 | if (sr == dmSocket::RESULT_OK && num_bytes > 0) 60 | DebugPrint(2, "Received bytes:", buffer, num_bytes); 61 | 62 | return sr; 63 | } 64 | 65 | } // namespace -------------------------------------------------------------------------------- /websocket/src/websocket.cpp: -------------------------------------------------------------------------------- 1 | // More info on websockets 2 | // https://tools.ietf.org/html/rfc6455 3 | 4 | #define LIB_NAME "Websocket" 5 | #define MODULE_NAME "websocket" 6 | 7 | #include "websocket.h" 8 | #include "script_util.h" 9 | #include 10 | #include 11 | #include 12 | #include // isprint et al 13 | 14 | #if defined(__EMSCRIPTEN__) 15 | #include // for EM_ASM 16 | #include 17 | #endif 18 | 19 | #if defined(WIN32) 20 | #include 21 | #define alloca _alloca 22 | #endif 23 | 24 | namespace dmWebsocket { 25 | 26 | int g_DebugWebSocket = 0; 27 | 28 | struct WebsocketContext 29 | { 30 | uint64_t m_BufferSize; 31 | int m_Timeout; 32 | dmArray m_Connections; 33 | dmConnectionPool::HPool m_Pool; 34 | uint32_t m_Initialized:1; 35 | } g_Websocket; 36 | 37 | 38 | #define STRING_CASE(_X) case _X: return #_X; 39 | 40 | const char* ResultToString(Result err) 41 | { 42 | switch(err) { 43 | STRING_CASE(RESULT_OK); 44 | STRING_CASE(RESULT_ERROR); 45 | STRING_CASE(RESULT_FAIL_WSLAY_INIT); 46 | STRING_CASE(RESULT_NOT_CONNECTED); 47 | STRING_CASE(RESULT_HANDSHAKE_FAILED); 48 | STRING_CASE(RESULT_WOULDBLOCK); 49 | default: return "Unknown result"; 50 | }; 51 | } 52 | 53 | const char* StateToString(State err) 54 | { 55 | switch(err) { 56 | STRING_CASE(STATE_CREATE); 57 | STRING_CASE(STATE_CONNECTING); 58 | STRING_CASE(STATE_HANDSHAKE_WRITE); 59 | STRING_CASE(STATE_HANDSHAKE_READ); 60 | STRING_CASE(STATE_CONNECTED); 61 | STRING_CASE(STATE_DISCONNECTING); 62 | STRING_CASE(STATE_DISCONNECTED); 63 | default: return "Unknown error"; 64 | }; 65 | } 66 | 67 | #undef STRING_CASE 68 | 69 | void DebugLog(int level, const char* fmt, ...) 70 | { 71 | if (level > g_DebugWebSocket) 72 | return; 73 | 74 | size_t buffer_size = 4096; 75 | char* buffer = (char*)alloca(buffer_size); 76 | va_list lst; 77 | va_start(lst, fmt); 78 | 79 | buffer_size = vsnprintf(buffer, buffer_size, fmt, lst); 80 | dmLogWarning("%s", buffer); 81 | va_end(lst); 82 | } 83 | 84 | void DebugPrint(int level, const char* msg, const void* _bytes, uint32_t num_bytes) 85 | { 86 | if (level > g_DebugWebSocket) 87 | return; 88 | 89 | char buffer[1024]; 90 | char submessage[128]; 91 | uint32_t sublength = 0; 92 | 93 | uint32_t len = dmSnPrintf(buffer, sizeof(buffer), "%s '", msg); 94 | 95 | const uint8_t* bytes = (const uint8_t*)_bytes; 96 | for (uint32_t i = 0; i < num_bytes; ++i) 97 | { 98 | if (len + 2 >= sizeof(buffer)) 99 | { 100 | dmLogWarning("%s", buffer); 101 | buffer[0] = 0; 102 | len = 0; 103 | } 104 | 105 | sublength = 0; 106 | 107 | int c = bytes[i]; 108 | if (isprint(c)) 109 | sublength = dmSnPrintf(submessage, sizeof(submessage), "%c", c); 110 | else if (c == '\r') 111 | sublength = dmSnPrintf(submessage, sizeof(submessage), "\\r"); 112 | else if (c == '\n') 113 | sublength = dmSnPrintf(submessage, sizeof(submessage), "\\n"); 114 | else if (c == '\t') 115 | sublength = dmSnPrintf(submessage, sizeof(submessage), "\\t"); 116 | else 117 | sublength = dmSnPrintf(submessage, sizeof(submessage), "\\%02x", c); 118 | dmStrlCat(buffer, submessage, sizeof(buffer)); 119 | len += sublength; 120 | } 121 | 122 | if (len + 2 >= sizeof(buffer)) 123 | { 124 | dmLogWarning("%s", buffer); 125 | buffer[0] = 0; 126 | len = 0; 127 | } 128 | 129 | sublength = dmSnPrintf(submessage, sizeof(submessage), "' %u bytes", num_bytes); 130 | dmStrlCat(buffer, submessage, sizeof(buffer)); 131 | len += sublength; 132 | dmLogWarning("%s", buffer); 133 | } 134 | 135 | 136 | #define CLOSE_CONN(...) \ 137 | SetStatus(conn, RESULT_ERROR, __VA_ARGS__); \ 138 | CloseConnection(conn); 139 | 140 | 141 | void SetState(WebsocketConnection* conn, State state) 142 | { 143 | State prev_state = conn->m_State; 144 | if (prev_state != state) 145 | { 146 | conn->m_State = state; 147 | DebugLog(1, "%s -> %s", StateToString(prev_state), StateToString(conn->m_State)); 148 | } 149 | } 150 | 151 | 152 | Result SetStatus(WebsocketConnection* conn, Result status, const char* format, ...) 153 | { 154 | if (conn->m_Status == RESULT_OK) 155 | { 156 | va_list lst; 157 | va_start(lst, format); 158 | 159 | conn->m_BufferSize = vsnprintf(conn->m_Buffer, conn->m_BufferCapacity, format, lst); 160 | va_end(lst); 161 | conn->m_Status = status; 162 | 163 | DebugLog(1, "STATUS: '%s' len: %u", conn->m_Buffer, conn->m_BufferSize); 164 | } 165 | return status; 166 | } 167 | 168 | // *************************************************************************************************** 169 | // LUA functions 170 | 171 | static WebsocketConnection* CreateConnection(const char* url) 172 | { 173 | WebsocketConnection* conn = new WebsocketConnection; 174 | conn->m_BufferCapacity = g_Websocket.m_BufferSize; 175 | conn->m_Buffer = (char*)malloc(conn->m_BufferCapacity); 176 | conn->m_Buffer[0] = 0; 177 | conn->m_BufferSize = 0; 178 | conn->m_ConnectTimeout = 0; 179 | 180 | dmURI::Parse(url, &conn->m_Url); 181 | 182 | if (strcmp(conn->m_Url.m_Scheme, "https") == 0) 183 | strcpy(conn->m_Url.m_Scheme, "wss"); 184 | 185 | conn->m_SSL = strcmp(conn->m_Url.m_Scheme, "wss") == 0 ? 1 : 0; 186 | conn->m_State = STATE_CREATE; 187 | 188 | conn->m_Callback = 0; 189 | conn->m_Connection = 0; 190 | conn->m_Socket = 0; 191 | conn->m_SSLSocket = 0; 192 | conn->m_Status = RESULT_OK; 193 | conn->m_HasHandshakeData = 0; 194 | conn->m_HandshakeResponse = 0; 195 | conn->m_ConnectionThread = 0; 196 | 197 | #if defined(HAVE_WSLAY) 198 | conn->m_Ctx = 0; 199 | #endif 200 | #if defined(__EMSCRIPTEN__) 201 | conn->m_WS = 0; 202 | #endif 203 | 204 | return conn; 205 | } 206 | 207 | static void DestroyConnection(WebsocketConnection* conn) 208 | { 209 | #if defined(HAVE_WSLAY) 210 | if (conn->m_Ctx) 211 | WSL_Exit(conn->m_Ctx); 212 | #endif 213 | 214 | free((void*)conn->m_CustomHeaders); 215 | free((void*)conn->m_Protocol); 216 | 217 | if (conn->m_Callback) 218 | dmScript::DestroyCallback(conn->m_Callback); 219 | 220 | #if defined(__EMSCRIPTEN__) 221 | if (conn->m_WS) 222 | { 223 | emscripten_websocket_delete(conn->m_WS); 224 | } 225 | #else 226 | if (conn->m_Connection) 227 | dmConnectionPool::Close(g_Websocket.m_Pool, conn->m_Connection); 228 | #endif 229 | 230 | if (conn->m_HandshakeResponse) 231 | delete conn->m_HandshakeResponse; 232 | 233 | 234 | free((void*)conn->m_Buffer); 235 | 236 | if (conn->m_ConnectionThread) 237 | { 238 | dmThread::Join(conn->m_ConnectionThread); 239 | conn->m_ConnectionThread = 0; 240 | } 241 | 242 | delete conn; 243 | DebugLog(2, "DestroyConnection: %p", conn); 244 | } 245 | 246 | 247 | static void CloseConnection(WebsocketConnection* conn) 248 | { 249 | // we want it to send this message in the polling 250 | if (conn->m_State == STATE_CONNECTED) { 251 | #if defined(HAVE_WSLAY) 252 | WSL_Close(conn->m_Ctx); 253 | #else 254 | // start disconnecting by closing the WebSocket through the JS API 255 | // we transition to the DISCONNECTED state when we receive the 256 | // Emscripten callback that the connection has closed 257 | emscripten_websocket_close(conn->m_WS, 1000, "CloseConnection"); 258 | SetState(conn, STATE_DISCONNECTING); 259 | #endif 260 | } 261 | 262 | #if defined(HAVE_WSLAY) 263 | // close the connection and immediately transition to the DISCONNECTED 264 | // state 265 | SetState(conn, STATE_DISCONNECTED); 266 | #endif 267 | } 268 | 269 | static bool IsConnectionValid(WebsocketConnection* conn) 270 | { 271 | if (conn) 272 | { 273 | for (int i = 0; i < g_Websocket.m_Connections.Size(); ++i ) 274 | { 275 | if (g_Websocket.m_Connections[i] == conn) 276 | return true; 277 | } 278 | } 279 | return false; 280 | } 281 | 282 | /*# 283 | * 284 | */ 285 | static int LuaConnect(lua_State* L) 286 | { 287 | DM_LUA_STACK_CHECK(L, 1); 288 | 289 | if (!g_Websocket.m_Initialized) 290 | return DM_LUA_ERROR("The web socket module isn't initialized"); 291 | 292 | const char* url = luaL_checkstring(L, 1); 293 | lua_Number timeout = dmScript::CheckTableNumber(L, 2, "timeout", 3000); 294 | const char* custom_headers = dmScript::CheckTableString(L, 2, "headers", 0); 295 | const char* protocol = dmScript::CheckTableString(L, 2, "protocol", 0); 296 | 297 | if (custom_headers != 0) 298 | { 299 | if (strstr(custom_headers, "\r\n\r\n") != 0) 300 | { 301 | return DM_LUA_ERROR("The header field must not contain double '\\r\\n\\r\\n': '%s'", custom_headers); 302 | } 303 | } 304 | 305 | WebsocketConnection* conn = CreateConnection(url); 306 | conn->m_ConnectTimeout = dmTime::GetTime() + timeout * 1000; 307 | conn->m_CustomHeaders = custom_headers ? strdup(custom_headers) : 0; 308 | conn->m_Protocol = protocol ? strdup(protocol) : 0; 309 | 310 | conn->m_Callback = dmScript::CreateCallback(L, 3); 311 | 312 | if (g_Websocket.m_Connections.Full()) 313 | g_Websocket.m_Connections.OffsetCapacity(2); 314 | g_Websocket.m_Connections.Push(conn); 315 | 316 | lua_pushlightuserdata(L, conn); 317 | return 1; 318 | } 319 | 320 | static int LuaDisconnect(lua_State* L) 321 | { 322 | DM_LUA_STACK_CHECK(L, 0); 323 | 324 | if (!g_Websocket.m_Initialized) 325 | return DM_LUA_ERROR("The web socket module isn't initialized"); 326 | 327 | if (!lua_islightuserdata(L, 1)) 328 | return DM_LUA_ERROR("The first argument must be a valid connection!"); 329 | 330 | WebsocketConnection* conn = (WebsocketConnection*)lua_touserdata(L, 1); 331 | 332 | if (IsConnectionValid(conn)) 333 | { 334 | CloseConnection(conn); 335 | } 336 | return 0; 337 | } 338 | 339 | static int LuaSend(lua_State* L) 340 | { 341 | DM_LUA_STACK_CHECK(L, 0); 342 | 343 | if (!g_Websocket.m_Initialized) 344 | return DM_LUA_ERROR("The web socket module isn't initialized"); 345 | 346 | if (!lua_islightuserdata(L, 1)) 347 | return DM_LUA_ERROR("The first argument must be a valid connection!"); 348 | 349 | WebsocketConnection* conn = (WebsocketConnection*)lua_touserdata(L, 1); 350 | 351 | if (!IsConnectionValid(conn)) 352 | return DM_LUA_ERROR("Invalid connection"); 353 | 354 | if (conn->m_State != STATE_CONNECTED) 355 | return DM_LUA_ERROR("Connection isn't connected"); 356 | 357 | size_t string_length = 0; 358 | const char* string = luaL_checklstring(L, 2, &string_length); 359 | 360 | #if defined(HAVE_WSLAY) 361 | int write_mode = dmScript::CheckTableNumber(L, 3, "type", WSLAY_BINARY_FRAME); 362 | 363 | struct wslay_event_msg msg; 364 | msg.opcode = write_mode; 365 | msg.msg = (const uint8_t*)string; 366 | msg.msg_length = string_length; 367 | 368 | wslay_event_queue_msg(conn->m_Ctx, &msg); // it makes a copy of the data 369 | #else 370 | EMSCRIPTEN_RESULT result; 371 | int write_mode = dmScript::CheckTableNumber(L, 3, "type", DATA_TYPE_BINARY); 372 | if (write_mode == DATA_TYPE_BINARY) 373 | { 374 | result = emscripten_websocket_send_binary(conn->m_WS, (void*)string, string_length); 375 | } 376 | else 377 | { 378 | result = emscripten_websocket_send_utf8_text(conn->m_WS, string); 379 | } 380 | if (result) 381 | { 382 | CLOSE_CONN("Failed to send on websocket"); 383 | } 384 | #endif 385 | 386 | return 0; 387 | } 388 | 389 | void HandleCallback(WebsocketConnection* conn, int event, int msg_offset, int msg_length) 390 | { 391 | if (!dmScript::IsCallbackValid(conn->m_Callback)) 392 | return; 393 | 394 | lua_State* L = dmScript::GetCallbackLuaContext(conn->m_Callback); 395 | DM_LUA_STACK_CHECK(L, 0) 396 | 397 | if (!dmScript::SetupCallback(conn->m_Callback)) 398 | { 399 | dmLogError("Failed to setup callback"); 400 | return; 401 | } 402 | 403 | lua_pushlightuserdata(L, conn); 404 | 405 | lua_newtable(L); 406 | 407 | lua_pushinteger(L, event); 408 | lua_setfield(L, -2, "event"); 409 | 410 | lua_pushlstring(L, conn->m_Buffer + msg_offset, msg_length); 411 | lua_setfield(L, -2, "message"); 412 | 413 | if(conn->m_HandshakeResponse) 414 | { 415 | HandshakeResponse* response = conn->m_HandshakeResponse; 416 | 417 | lua_newtable(L); 418 | 419 | lua_pushnumber(L, response->m_ResponseStatusCode); 420 | lua_setfield(L, -2, "status"); 421 | 422 | lua_pushstring(L, &conn->m_Buffer[response->m_BodyOffset]); 423 | lua_setfield(L, -2, "response"); 424 | 425 | lua_newtable(L); 426 | for (uint32_t i = 0; i < response->m_Headers.Size(); ++i) 427 | { 428 | lua_pushstring(L, response->m_Headers[i]->m_Value); 429 | lua_setfield(L, -2, response->m_Headers[i]->m_Key); 430 | } 431 | lua_setfield(L, -2, "headers"); 432 | 433 | lua_setfield(L, -2, "handshake_response"); 434 | 435 | delete conn->m_HandshakeResponse; 436 | conn->m_HandshakeResponse = 0; 437 | } 438 | 439 | if (event == EVENT_DISCONNECTED) 440 | { 441 | lua_pushinteger(L, conn->m_CloseCode); 442 | lua_setfield(L, -2, "code"); 443 | } 444 | 445 | dmScript::PCall(L, 3, 0); 446 | 447 | dmScript::TeardownCallback(conn->m_Callback); 448 | } 449 | 450 | 451 | HttpHeader::HttpHeader(const char* key, const char* value) 452 | { 453 | m_Key = strdup(key); 454 | m_Value = strdup(value); 455 | } 456 | 457 | HttpHeader::~HttpHeader() 458 | { 459 | free((void*)m_Key); 460 | free((void*)m_Value); 461 | m_Key = 0; 462 | m_Value = 0; 463 | } 464 | 465 | HttpHeader* HandshakeResponse::GetHeader(const char* header_key) 466 | { 467 | for(uint32_t i = 0; i < m_Headers.Size(); ++i) 468 | { 469 | if (dmStrCaseCmp(m_Headers[i]->m_Key, header_key) == 0) 470 | { 471 | return m_Headers[i]; 472 | } 473 | } 474 | 475 | return 0; 476 | } 477 | 478 | HandshakeResponse::~HandshakeResponse() 479 | { 480 | for(uint32_t i = 0; i < m_Headers.Size(); ++i) 481 | { 482 | delete m_Headers[i]; 483 | } 484 | } 485 | 486 | 487 | // *************************************************************************************************** 488 | // Life cycle functions 489 | 490 | // Functions exposed to Lua 491 | static const luaL_reg Websocket_module_methods[] = 492 | { 493 | {"connect", LuaConnect}, 494 | {"disconnect", LuaDisconnect}, 495 | {"send", LuaSend}, 496 | {0, 0} 497 | }; 498 | 499 | static void LuaInit(lua_State* L) 500 | { 501 | int top = lua_gettop(L); 502 | 503 | // Register lua names 504 | luaL_register(L, MODULE_NAME, Websocket_module_methods); 505 | 506 | #define SETCONSTANT(_X) \ 507 | lua_pushnumber(L, (lua_Number) _X); \ 508 | lua_setfield(L, -2, #_X); 509 | 510 | SETCONSTANT(EVENT_CONNECTED); 511 | SETCONSTANT(EVENT_DISCONNECTED); 512 | SETCONSTANT(EVENT_MESSAGE); 513 | SETCONSTANT(EVENT_ERROR); 514 | 515 | #if defined(HAVE_WSLAY) 516 | lua_pushnumber(L, (lua_Number) WSLAY_BINARY_FRAME); 517 | lua_setfield(L, -2, "DATA_TYPE_BINARY"); 518 | lua_pushnumber(L, (lua_Number) WSLAY_TEXT_FRAME); 519 | lua_setfield(L, -2, "DATA_TYPE_TEXT"); 520 | #else 521 | SETCONSTANT(DATA_TYPE_BINARY); 522 | SETCONSTANT(DATA_TYPE_TEXT); 523 | #endif 524 | 525 | #undef SETCONSTANT 526 | 527 | lua_pop(L, 1); 528 | assert(top == lua_gettop(L)); 529 | } 530 | 531 | static dmExtension::Result AppInitialize(dmExtension::AppParams* params) 532 | { 533 | g_Websocket.m_BufferSize = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.buffer_size", 64 * 1024); 534 | g_Websocket.m_Timeout = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.socket_timeout", 500 * 1000); 535 | g_Websocket.m_Connections.SetCapacity(4); 536 | g_Websocket.m_Pool = 0; 537 | g_Websocket.m_Initialized = 0; 538 | 539 | dmConnectionPool::Params pool_params; 540 | pool_params.m_MaxConnections = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.max_connections", 2); 541 | dmConnectionPool::Result result = dmConnectionPool::New(&pool_params, &g_Websocket.m_Pool); 542 | 543 | g_DebugWebSocket = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.debug", 0); 544 | if (g_DebugWebSocket) 545 | dmLogInfo("dmWebSocket::g_DebugWebSocket == %d", g_DebugWebSocket); 546 | 547 | if (dmConnectionPool::RESULT_OK != result) 548 | { 549 | dmLogError("Failed to create connection pool: %d", result); 550 | return dmExtension::RESULT_INIT_ERROR; 551 | } 552 | 553 | g_Websocket.m_Initialized = 1; 554 | return dmExtension::RESULT_OK; 555 | } 556 | 557 | static dmExtension::Result Initialize(dmExtension::Params* params) 558 | { 559 | if (!g_Websocket.m_Initialized) 560 | return dmExtension::RESULT_OK; 561 | 562 | LuaInit(params->m_L); 563 | dmLogInfo("Registered %s extension", MODULE_NAME); 564 | 565 | return dmExtension::RESULT_OK; 566 | } 567 | 568 | static dmExtension::Result AppFinalize(dmExtension::AppParams* params) 569 | { 570 | dmConnectionPool::Shutdown(g_Websocket.m_Pool, dmSocket::SHUTDOWNTYPE_READWRITE); 571 | return dmExtension::RESULT_OK; 572 | } 573 | 574 | static dmExtension::Result Finalize(dmExtension::Params* params) 575 | { 576 | while (!g_Websocket.m_Connections.Empty()) 577 | { 578 | WebsocketConnection* conn = g_Websocket.m_Connections.Back(); 579 | g_Websocket.m_Connections.Pop(); 580 | DestroyConnection(conn); 581 | } 582 | return dmExtension::RESULT_OK; 583 | } 584 | 585 | Result PushMessage(WebsocketConnection* conn, MessageType type, int length, const uint8_t* buffer, uint16_t code) 586 | { 587 | if (conn->m_Messages.Full()) 588 | conn->m_Messages.OffsetCapacity(4); 589 | 590 | Message msg; 591 | msg.m_Type = (uint32_t)type; 592 | msg.m_Length = length; 593 | msg.m_Code = code; 594 | conn->m_Messages.Push(msg); 595 | 596 | if ((conn->m_BufferSize + length) >= conn->m_BufferCapacity) 597 | { 598 | conn->m_BufferCapacity = conn->m_BufferSize + length + 1; 599 | conn->m_Buffer = (char*)realloc(conn->m_Buffer, conn->m_BufferCapacity); 600 | } 601 | // append to the end of the buffer 602 | memcpy(conn->m_Buffer + conn->m_BufferSize, buffer, length); 603 | 604 | conn->m_BufferSize += length; 605 | conn->m_Buffer[conn->m_BufferCapacity-1] = 0; 606 | 607 | // Instead of printing from the incoming buffer, we print from our own, to make sure it looks ok 608 | DebugPrint(2, __FUNCTION__, conn->m_Buffer+conn->m_BufferSize-length, length); 609 | 610 | return dmWebsocket::RESULT_OK; 611 | } 612 | 613 | // has the connect procedure taken too long? 614 | static bool CheckConnectTimeout(WebsocketConnection* conn) 615 | { 616 | uint64_t t = dmTime::GetTime(); 617 | return t >= conn->m_ConnectTimeout; 618 | } 619 | 620 | static void ConnectionWorker(void* _conn) 621 | { 622 | WebsocketConnection* conn = (WebsocketConnection*)_conn; 623 | dmSocket::Result sr; 624 | dmConnectionPool::Result pool_result = dmConnectionPool::Dial(g_Websocket.m_Pool, conn->m_Url.m_Hostname, conn->m_Url.m_Port, conn->m_SSL, g_Websocket.m_Timeout, &conn->m_Connection, &sr); 625 | if (dmConnectionPool::RESULT_OK != pool_result) 626 | { 627 | CLOSE_CONN("Failed to open connection: %s", dmSocket::ResultToString(sr)); 628 | return; 629 | } 630 | SetState(conn, STATE_HANDSHAKE_WRITE); 631 | } 632 | 633 | static dmExtension::Result OnUpdate(dmExtension::Params* params) 634 | { 635 | uint32_t size = g_Websocket.m_Connections.Size(); 636 | 637 | for (uint32_t i = 0; i < size; ++i) 638 | { 639 | WebsocketConnection* conn = g_Websocket.m_Connections[i]; 640 | 641 | if (STATE_DISCONNECTED == conn->m_State) 642 | { 643 | if (RESULT_OK != conn->m_Status) 644 | { 645 | HandleCallback(conn, EVENT_ERROR, 0, conn->m_BufferSize); 646 | conn->m_BufferSize = 0; 647 | } 648 | 649 | HandleCallback(conn, EVENT_DISCONNECTED, 0, conn->m_BufferSize); 650 | 651 | g_Websocket.m_Connections.EraseSwap(i); 652 | --i; 653 | --size; 654 | DestroyConnection(conn); 655 | } 656 | else if ((STATE_CONNECTED == conn->m_State) || (STATE_DISCONNECTING == conn->m_State)) 657 | { 658 | #if defined(HAVE_WSLAY) 659 | int r = WSL_Poll(conn->m_Ctx); 660 | if (0 != r) 661 | { 662 | CLOSE_CONN("Websocket closing for %s (%s)", conn->m_Url.m_Hostname, WSL_ResultToString(r)); 663 | continue; 664 | } 665 | #endif 666 | 667 | uint32_t offset = 0; 668 | for (uint32_t i = 0; i < conn->m_Messages.Size(); ++i) 669 | { 670 | const Message& msg = conn->m_Messages[i]; 671 | 672 | if (EVENT_DISCONNECTED == msg.m_Type) 673 | { 674 | conn->m_Status = RESULT_OK; 675 | // close the connection and immediately transition to the DISCONNECTED 676 | // state 677 | SetState(conn, STATE_DISCONNECTED); 678 | conn->m_CloseCode = msg.m_Code; 679 | break; 680 | } 681 | 682 | HandleCallback(conn, EVENT_MESSAGE, offset, msg.m_Length); 683 | offset += msg.m_Length; 684 | } 685 | conn->m_Messages.SetSize(0); 686 | conn->m_BufferSize = 0; 687 | } 688 | else if (STATE_HANDSHAKE_READ == conn->m_State) 689 | { 690 | if (CheckConnectTimeout(conn)) 691 | { 692 | CLOSE_CONN("Connect sequence timed out"); 693 | continue; 694 | } 695 | 696 | Result result = ReceiveHeaders(conn); 697 | if (RESULT_WOULDBLOCK == result) 698 | { 699 | continue; 700 | } 701 | 702 | if (RESULT_OK != result) 703 | { 704 | CLOSE_CONN("Failed receiving handshake headers. %d", result); 705 | continue; 706 | } 707 | 708 | // Verifies headers, and also stages any initial sent data 709 | result = VerifyHeaders(conn); 710 | if (RESULT_OK != result) 711 | { 712 | CLOSE_CONN("Failed verifying handshake headers:\n%s\n\n", conn->m_Buffer); 713 | continue; 714 | } 715 | 716 | #if defined(HAVE_WSLAY) 717 | int r = WSL_Init(&conn->m_Ctx, g_Websocket.m_BufferSize, (void*)conn); 718 | if (0 != r) 719 | { 720 | CLOSE_CONN("Failed initializing wslay: %s", WSL_ResultToString(r)); 721 | continue; 722 | } 723 | 724 | dmSocket::SetNoDelay(conn->m_Socket, true); 725 | // Don't go lower than 1000 since some platforms might not have that good precision 726 | dmSocket::SetReceiveTimeout(conn->m_Socket, 1000); 727 | if (conn->m_SSLSocket) 728 | dmSSLSocket::SetReceiveTimeout(conn->m_SSLSocket, 1000); 729 | 730 | dmSocket::SetBlocking(conn->m_Socket, false); 731 | #endif 732 | SetState(conn, STATE_CONNECTED); 733 | HandleCallback(conn, EVENT_CONNECTED, 0, 0); 734 | } 735 | else if (STATE_HANDSHAKE_WRITE == conn->m_State) 736 | { 737 | if (CheckConnectTimeout(conn)) 738 | { 739 | CLOSE_CONN("Connect sequence timed out"); 740 | continue; 741 | } 742 | 743 | if (conn->m_ConnectionThread) 744 | { 745 | dmThread::Join(conn->m_ConnectionThread); 746 | conn->m_ConnectionThread = 0; 747 | } 748 | conn->m_Socket = dmConnectionPool::GetSocket(g_Websocket.m_Pool, conn->m_Connection); 749 | conn->m_SSLSocket = dmConnectionPool::GetSSLSocket(g_Websocket.m_Pool, conn->m_Connection); 750 | Result result = SendClientHandshake(conn); 751 | if (RESULT_WOULDBLOCK == result) 752 | { 753 | continue; 754 | } 755 | if (RESULT_OK != result) 756 | { 757 | CLOSE_CONN("Failed sending handshake: %d", result); 758 | continue; 759 | } 760 | 761 | SetState(conn, STATE_HANDSHAKE_READ); 762 | } 763 | else if (STATE_CREATE == conn->m_State) 764 | { 765 | if (CheckConnectTimeout(conn)) 766 | { 767 | CLOSE_CONN("Connect sequence timed out"); 768 | continue; 769 | } 770 | 771 | #if defined(__EMSCRIPTEN__) 772 | char uri_buffer[dmURI::MAX_URI_LEN]; 773 | const char* uri; 774 | bool no_path = conn->m_Url.m_Path[0] == '\0'; 775 | bool no_port = conn->m_Url.m_Port == -1; 776 | if (no_path && no_port) 777 | { 778 | dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname); 779 | } 780 | else if (no_port) 781 | { 782 | dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Path); 783 | } 784 | else if (no_path) 785 | { 786 | dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port); 787 | } 788 | else { 789 | dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port, conn->m_Url.m_Path); 790 | } 791 | uri = uri_buffer; 792 | 793 | EmscriptenWebSocketCreateAttributes ws_attrs = { 794 | uri, 795 | conn->m_Protocol, 796 | EM_TRUE 797 | }; 798 | EMSCRIPTEN_WEBSOCKET_T ws = emscripten_websocket_new(&ws_attrs); 799 | if (ws < 0) 800 | { 801 | CLOSE_CONN("Failed to connect to '%s:%d': %d", conn->m_Url.m_Hostname, (int)conn->m_Url.m_Port, ws); 802 | continue; 803 | } 804 | conn->m_WS = ws; 805 | 806 | emscripten_websocket_set_onopen_callback(ws, conn, Emscripten_WebSocketOnOpen); 807 | emscripten_websocket_set_onerror_callback(ws, conn, Emscripten_WebSocketOnError); 808 | emscripten_websocket_set_onclose_callback(ws, conn, Emscripten_WebSocketOnClose); 809 | emscripten_websocket_set_onmessage_callback(ws, conn, Emscripten_WebSocketOnMessage); 810 | #else 811 | conn->m_ConnectionThread = dmThread::New((dmThread::ThreadStart)ConnectionWorker, 0x80000, conn, "WSConnect"); 812 | #endif 813 | SetState(conn, STATE_CONNECTING); 814 | } 815 | else if (STATE_CONNECTING == conn->m_State) 816 | { 817 | #if defined(__EMSCRIPTEN__) 818 | if (CheckConnectTimeout(conn)) 819 | { 820 | CLOSE_CONN("Connect sequence timed out"); 821 | continue; 822 | } 823 | #endif 824 | } 825 | } 826 | 827 | return dmExtension::RESULT_OK; 828 | } 829 | 830 | } // dmWebsocket 831 | 832 | DM_DECLARE_EXTENSION(Websocket, LIB_NAME, dmWebsocket::AppInitialize, dmWebsocket::AppFinalize, dmWebsocket::Initialize, dmWebsocket::OnUpdate, 0, dmWebsocket::Finalize) 833 | 834 | #undef CLOSE_CONN 835 | -------------------------------------------------------------------------------- /websocket/src/websocket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(_WIN32) 4 | #include 5 | #endif 6 | 7 | // include the Defold SDK 8 | #include 9 | 10 | #if !defined(__EMSCRIPTEN__) 11 | #define HAVE_WSLAY 1 12 | #endif 13 | 14 | #if defined(HAVE_WSLAY) 15 | #include 16 | #endif 17 | 18 | #if defined(__EMSCRIPTEN__) 19 | #include 20 | #endif 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | namespace dmCrypt 29 | { 30 | void HashSha1(const uint8_t* buf, uint32_t buflen, uint8_t* digest); 31 | bool Base64Encode(const uint8_t* src, uint32_t src_len, uint8_t* dst, uint32_t* dst_len); 32 | bool Base64Decode(const uint8_t* src, uint32_t src_len, uint8_t* dst, uint32_t* dst_len); 33 | } 34 | 35 | namespace dmWebsocket 36 | { 37 | // Maximum time to wait for a socket 38 | static const int SOCKET_WAIT_TIMEOUT = 4*1000; 39 | 40 | enum State 41 | { 42 | STATE_CREATE, 43 | STATE_CONNECTING, 44 | STATE_HANDSHAKE_WRITE, 45 | STATE_HANDSHAKE_READ, 46 | STATE_CONNECTED, 47 | STATE_DISCONNECTING, 48 | STATE_DISCONNECTED, 49 | }; 50 | 51 | enum Result 52 | { 53 | RESULT_OK, 54 | RESULT_ERROR, 55 | RESULT_FAIL_WSLAY_INIT, 56 | RESULT_NOT_CONNECTED, 57 | RESULT_HANDSHAKE_FAILED, 58 | RESULT_WOULDBLOCK, 59 | }; 60 | 61 | enum Event 62 | { 63 | EVENT_CONNECTED, 64 | EVENT_DISCONNECTED, 65 | EVENT_MESSAGE, 66 | EVENT_ERROR, 67 | }; 68 | 69 | enum MessageType 70 | { 71 | MESSAGE_TYPE_NORMAL = 0, 72 | MESSAGE_TYPE_CLOSE = 1, 73 | }; 74 | 75 | enum DataType 76 | { 77 | DATA_TYPE_BINARY = 0, 78 | DATA_TYPE_TEXT = 1, 79 | }; 80 | 81 | struct Message 82 | { 83 | uint16_t m_Code; 84 | uint32_t m_Length:30; 85 | uint32_t m_Type:2; 86 | }; 87 | 88 | struct HttpHeader 89 | { 90 | const char* m_Key; 91 | const char* m_Value; 92 | HttpHeader(const char* key, const char* value); 93 | ~HttpHeader(); 94 | }; 95 | 96 | struct HandshakeResponse 97 | { 98 | int m_HttpMajor; 99 | int m_HttpMinor; 100 | int m_ResponseStatusCode; 101 | int m_BodyOffset; 102 | dmArray m_Headers; 103 | 104 | ~HandshakeResponse(); 105 | HttpHeader* GetHeader(const char* header); 106 | }; 107 | 108 | 109 | struct WebsocketConnection 110 | { 111 | dmScript::LuaCallbackInfo* m_Callback; 112 | #if defined(HAVE_WSLAY) 113 | wslay_event_context_ptr m_Ctx; 114 | #endif 115 | #if defined(__EMSCRIPTEN__) 116 | EMSCRIPTEN_WEBSOCKET_T m_WS; 117 | #endif 118 | dmURI::Parts m_Url; 119 | dmConnectionPool::HConnection m_Connection; 120 | dmSocket::Socket m_Socket; 121 | dmSSLSocket::Socket m_SSLSocket; 122 | dmThread::Thread m_ConnectionThread; 123 | dmArray m_Messages; // lengths of the messages in the data buffer 124 | uint64_t m_ConnectTimeout; 125 | uint8_t m_Key[16]; 126 | const char* m_Protocol; 127 | const char* m_CustomHeaders; 128 | State m_State; 129 | char* m_Buffer; 130 | int m_BufferSize; 131 | uint32_t m_BufferCapacity; 132 | Result m_Status; 133 | uint16_t m_CloseCode; 134 | uint8_t m_SSL:1; 135 | uint8_t m_HasHandshakeData:1; 136 | uint8_t :7; 137 | HandshakeResponse* m_HandshakeResponse; 138 | }; 139 | 140 | // Set error message 141 | #ifdef __GNUC__ 142 | Result SetStatus(WebsocketConnection* conn, Result status, const char* fmt, ...) __attribute__ ((format (printf, 3, 4))); 143 | #else 144 | Result SetStatus(WebsocketConnection* conn, Result status, const char* fmt, ...); 145 | #endif 146 | 147 | // Set socket state 148 | void SetState(WebsocketConnection* conn, State state); 149 | 150 | // Communication 151 | dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length, int* out_sent_bytes); 152 | dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes); 153 | dmSocket::Result WaitForSocket(WebsocketConnection* conn, dmSocket::SelectorKind kind, int timeout); 154 | 155 | // Handshake 156 | Result SendClientHandshake(WebsocketConnection* conn); 157 | Result ReceiveHeaders(WebsocketConnection* conn); 158 | Result VerifyHeaders(WebsocketConnection* conn); 159 | 160 | // Callback to Lua 161 | void HandleCallback(WebsocketConnection* conn, int event, int msg_offset, int msg_length); 162 | 163 | // Messages 164 | Result PushMessage(WebsocketConnection* conn, MessageType type, int length, const uint8_t* msg, uint16_t code); 165 | 166 | #if defined(HAVE_WSLAY) 167 | // Wslay callbacks 168 | int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx); 169 | void WSL_Exit(wslay_event_context_ptr ctx); 170 | int WSL_Close(wslay_event_context_ptr ctx); 171 | int WSL_Poll(wslay_event_context_ptr ctx); 172 | ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data); 173 | ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data); 174 | void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data); 175 | int WSL_GenmaskCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data); 176 | const char* WSL_ResultToString(int err); 177 | #endif 178 | #if defined(__EMSCRIPTEN__) 179 | EM_BOOL Emscripten_WebSocketOnOpen(int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData); 180 | EM_BOOL Emscripten_WebSocketOnError(int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData); 181 | EM_BOOL Emscripten_WebSocketOnClose(int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData); 182 | EM_BOOL Emscripten_WebSocketOnMessage(int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData); 183 | #endif 184 | 185 | // Random numbers (PCG) 186 | typedef struct { uint64_t state; uint64_t inc; } pcg32_random_t; 187 | void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq); 188 | uint32_t pcg32_random_r(pcg32_random_t* rng); 189 | 190 | // If level <= dmWebSocket::g_DebugWebSocket, then it outputs the message 191 | #ifdef __GNUC__ 192 | void DebugLog(int level, const char* fmt, ...) __attribute__ ((format (printf, 2, 3))); 193 | #else 194 | void DebugLog(int level, const char* fmt, ...); 195 | #endif 196 | void DebugPrint(int level, const char* msg, const void* _bytes, uint32_t num_bytes); 197 | } 198 | -------------------------------------------------------------------------------- /websocket/src/wslay/wslay_frame.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #include "wslay_frame.h" 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include "wslay_net.h" 32 | 33 | #define wslay_min(A, B) (((A) < (B)) ? (A) : (B)) 34 | 35 | int wslay_frame_context_init(wslay_frame_context_ptr *ctx, 36 | const struct wslay_frame_callbacks *callbacks, 37 | void *user_data) 38 | { 39 | *ctx = (wslay_frame_context_ptr)malloc(sizeof(struct wslay_frame_context)); 40 | if(*ctx == NULL) { 41 | return -1; 42 | } 43 | memset(*ctx, 0, sizeof(struct wslay_frame_context)); 44 | (*ctx)->istate = RECV_HEADER1; 45 | (*ctx)->ireqread = 2; 46 | (*ctx)->ostate = PREP_HEADER; 47 | (*ctx)->user_data = user_data; 48 | (*ctx)->ibufmark = (*ctx)->ibuflimit = (*ctx)->ibuf; 49 | (*ctx)->callbacks = *callbacks; 50 | return 0; 51 | } 52 | 53 | void wslay_frame_context_free(wslay_frame_context_ptr ctx) 54 | { 55 | free(ctx); 56 | } 57 | 58 | ssize_t wslay_frame_send(wslay_frame_context_ptr ctx, 59 | struct wslay_frame_iocb *iocb) 60 | { 61 | if(iocb->data_length > iocb->payload_length) { 62 | return WSLAY_ERR_INVALID_ARGUMENT; 63 | } 64 | if(ctx->ostate == PREP_HEADER) { 65 | uint8_t *hdptr = ctx->oheader; 66 | memset(ctx->oheader, 0, sizeof(ctx->oheader)); 67 | *hdptr |= (iocb->fin << 7) & 0x80u; 68 | *hdptr |= (iocb->rsv << 4) & 0x70u; 69 | *hdptr |= iocb->opcode & 0xfu; 70 | ++hdptr; 71 | *hdptr |= (iocb->mask << 7) & 0x80u; 72 | if(wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) { 73 | return WSLAY_ERR_INVALID_ARGUMENT; 74 | } 75 | if(iocb->payload_length < 126) { 76 | *hdptr |= iocb->payload_length; 77 | ++hdptr; 78 | } else if(iocb->payload_length < (1 << 16)) { 79 | uint16_t len = htons(iocb->payload_length); 80 | *hdptr |= 126; 81 | ++hdptr; 82 | memcpy(hdptr, &len, 2); 83 | hdptr += 2; 84 | } else if(iocb->payload_length < (1ull << 63)) { 85 | uint64_t len = hton64(iocb->payload_length); 86 | *hdptr |= 127; 87 | ++hdptr; 88 | memcpy(hdptr, &len, 8); 89 | hdptr += 8; 90 | } else { 91 | /* Too large payload length */ 92 | return WSLAY_ERR_INVALID_ARGUMENT; 93 | } 94 | if(iocb->mask) { 95 | if(ctx->callbacks.genmask_callback(ctx->omaskkey, 4, 96 | ctx->user_data) != 0) { 97 | return WSLAY_ERR_INVALID_CALLBACK; 98 | } else { 99 | ctx->omask = 1; 100 | memcpy(hdptr, ctx->omaskkey, 4); 101 | hdptr += 4; 102 | } 103 | } 104 | ctx->ostate = SEND_HEADER; 105 | ctx->oheadermark = ctx->oheader; 106 | ctx->oheaderlimit = hdptr; 107 | ctx->opayloadlen = iocb->payload_length; 108 | ctx->opayloadoff = 0; 109 | } 110 | if(ctx->ostate == SEND_HEADER) { 111 | ptrdiff_t len = ctx->oheaderlimit-ctx->oheadermark; 112 | ssize_t r; 113 | int flags = 0; 114 | if(iocb->data_length > 0) { 115 | flags |= WSLAY_MSG_MORE; 116 | }; 117 | r = ctx->callbacks.send_callback(ctx->oheadermark, len, flags, 118 | ctx->user_data); 119 | if(r > 0) { 120 | if(r > len) { 121 | return WSLAY_ERR_INVALID_CALLBACK; 122 | } else { 123 | ctx->oheadermark += r; 124 | if(ctx->oheadermark == ctx->oheaderlimit) { 125 | ctx->ostate = SEND_PAYLOAD; 126 | } else { 127 | return WSLAY_ERR_WANT_WRITE; 128 | } 129 | } 130 | } else { 131 | return WSLAY_ERR_WANT_WRITE; 132 | } 133 | } 134 | if(ctx->ostate == SEND_PAYLOAD) { 135 | size_t totallen = 0; 136 | if(iocb->data_length > 0) { 137 | if(ctx->omask) { 138 | uint8_t temp[4096]; 139 | const uint8_t *datamark = iocb->data, 140 | *datalimit = iocb->data+iocb->data_length; 141 | while(datamark < datalimit) { 142 | size_t datalen = datalimit - datamark; 143 | const uint8_t *writelimit = datamark+ 144 | wslay_min(sizeof(temp), datalen); 145 | size_t writelen = writelimit-datamark; 146 | ssize_t r; 147 | size_t i; 148 | for(i = 0; i < writelen; ++i) { 149 | temp[i] = datamark[i]^ctx->omaskkey[(ctx->opayloadoff+i)%4]; 150 | } 151 | r = ctx->callbacks.send_callback(temp, writelen, 0, ctx->user_data); 152 | if(r > 0) { 153 | if((size_t)r > writelen) { 154 | return WSLAY_ERR_INVALID_CALLBACK; 155 | } else { 156 | datamark += r; 157 | ctx->opayloadoff += r; 158 | totallen += r; 159 | } 160 | } else { 161 | if(totallen > 0) { 162 | break; 163 | } else { 164 | return WSLAY_ERR_WANT_WRITE; 165 | } 166 | } 167 | } 168 | } else { 169 | ssize_t r; 170 | r = ctx->callbacks.send_callback(iocb->data, iocb->data_length, 0, 171 | ctx->user_data); 172 | if(r > 0) { 173 | if((size_t)r > iocb->data_length) { 174 | return WSLAY_ERR_INVALID_CALLBACK; 175 | } else { 176 | ctx->opayloadoff += r; 177 | totallen = r; 178 | } 179 | } else { 180 | return WSLAY_ERR_WANT_WRITE; 181 | } 182 | } 183 | } 184 | if(ctx->opayloadoff == ctx->opayloadlen) { 185 | ctx->ostate = PREP_HEADER; 186 | } 187 | return totallen; 188 | } 189 | return WSLAY_ERR_INVALID_ARGUMENT; 190 | } 191 | 192 | static void wslay_shift_ibuf(wslay_frame_context_ptr ctx) 193 | { 194 | ptrdiff_t len = ctx->ibuflimit-ctx->ibufmark; 195 | memmove(ctx->ibuf, ctx->ibufmark, len); 196 | ctx->ibuflimit = ctx->ibuf+len; 197 | ctx->ibufmark = ctx->ibuf; 198 | } 199 | 200 | static ssize_t wslay_recv(wslay_frame_context_ptr ctx) 201 | { 202 | ssize_t r; 203 | if(ctx->ibufmark != ctx->ibuf) { 204 | wslay_shift_ibuf(ctx); 205 | } 206 | r = ctx->callbacks.recv_callback 207 | (ctx->ibuflimit, ctx->ibuf+sizeof(ctx->ibuf)-ctx->ibuflimit, 208 | 0, ctx->user_data); 209 | if(r > 0) { 210 | ctx->ibuflimit += r; 211 | } else { 212 | r = WSLAY_ERR_WANT_READ; 213 | } 214 | return r; 215 | } 216 | 217 | #define WSLAY_AVAIL_IBUF(ctx) ((size_t)(ctx->ibuflimit - ctx->ibufmark)) 218 | 219 | ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx, 220 | struct wslay_frame_iocb *iocb) 221 | { 222 | ssize_t r; 223 | if(ctx->istate == RECV_HEADER1) { 224 | uint8_t fin, opcode, rsv, payloadlen; 225 | if(WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { 226 | if((r = wslay_recv(ctx)) <= 0) { 227 | return r; 228 | } 229 | } 230 | if(WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { 231 | return WSLAY_ERR_WANT_READ; 232 | } 233 | fin = (ctx->ibufmark[0] >> 7) & 1; 234 | rsv = (ctx->ibufmark[0] >> 4) & 7; 235 | opcode = ctx->ibufmark[0] & 0xfu; 236 | ctx->iom.opcode = opcode; 237 | ctx->iom.fin = fin; 238 | ctx->iom.rsv = rsv; 239 | ++ctx->ibufmark; 240 | ctx->imask = (ctx->ibufmark[0] >> 7) & 1; 241 | payloadlen = ctx->ibufmark[0] & 0x7fu; 242 | ++ctx->ibufmark; 243 | if(wslay_is_ctrl_frame(opcode) && (payloadlen > 125 || !fin)) { 244 | return WSLAY_ERR_PROTO; 245 | } 246 | if(payloadlen == 126) { 247 | ctx->istate = RECV_EXT_PAYLOADLEN; 248 | ctx->ireqread = 2; 249 | } else if(payloadlen == 127) { 250 | ctx->istate = RECV_EXT_PAYLOADLEN; 251 | ctx->ireqread = 8; 252 | } else { 253 | ctx->ipayloadlen = payloadlen; 254 | ctx->ipayloadoff = 0; 255 | if(ctx->imask) { 256 | ctx->istate = RECV_MASKKEY; 257 | ctx->ireqread = 4; 258 | } else { 259 | ctx->istate = RECV_PAYLOAD; 260 | } 261 | } 262 | } 263 | if(ctx->istate == RECV_EXT_PAYLOADLEN) { 264 | if(WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { 265 | if((r = wslay_recv(ctx)) <= 0) { 266 | return r; 267 | } 268 | if(WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { 269 | return WSLAY_ERR_WANT_READ; 270 | } 271 | } 272 | ctx->ipayloadlen = 0; 273 | ctx->ipayloadoff = 0; 274 | memcpy((uint8_t*)&ctx->ipayloadlen+(8-ctx->ireqread), 275 | ctx->ibufmark, ctx->ireqread); 276 | ctx->ipayloadlen = ntoh64(ctx->ipayloadlen); 277 | ctx->ibufmark += ctx->ireqread; 278 | if(ctx->ireqread == 8) { 279 | if(ctx->ipayloadlen < (1 << 16) || 280 | ctx->ipayloadlen & (1ull << 63)) { 281 | return WSLAY_ERR_PROTO; 282 | } 283 | } else if(ctx->ipayloadlen < 126) { 284 | return WSLAY_ERR_PROTO; 285 | } 286 | if(ctx->imask) { 287 | ctx->istate = RECV_MASKKEY; 288 | ctx->ireqread = 4; 289 | } else { 290 | ctx->istate = RECV_PAYLOAD; 291 | } 292 | } 293 | if(ctx->istate == RECV_MASKKEY) { 294 | if(WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { 295 | if((r = wslay_recv(ctx)) <= 0) { 296 | return r; 297 | } 298 | if(WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) { 299 | return WSLAY_ERR_WANT_READ; 300 | } 301 | } 302 | memcpy(ctx->imaskkey, ctx->ibufmark, 4); 303 | ctx->ibufmark += 4; 304 | ctx->istate = RECV_PAYLOAD; 305 | } 306 | if(ctx->istate == RECV_PAYLOAD) { 307 | uint8_t *readlimit, *readmark; 308 | uint64_t rempayloadlen = ctx->ipayloadlen-ctx->ipayloadoff; 309 | if(WSLAY_AVAIL_IBUF(ctx) == 0 && rempayloadlen > 0) { 310 | if((r = wslay_recv(ctx)) <= 0) { 311 | return r; 312 | } 313 | } 314 | readmark = ctx->ibufmark; 315 | readlimit = WSLAY_AVAIL_IBUF(ctx) < rempayloadlen ? 316 | ctx->ibuflimit : ctx->ibufmark+rempayloadlen; 317 | if(ctx->imask) { 318 | for(; ctx->ibufmark != readlimit; 319 | ++ctx->ibufmark, ++ctx->ipayloadoff) { 320 | ctx->ibufmark[0] ^= ctx->imaskkey[ctx->ipayloadoff % 4]; 321 | } 322 | } else { 323 | ctx->ibufmark = readlimit; 324 | ctx->ipayloadoff += readlimit-readmark; 325 | } 326 | iocb->fin = ctx->iom.fin; 327 | iocb->rsv = ctx->iom.rsv; 328 | iocb->opcode = ctx->iom.opcode; 329 | iocb->payload_length = ctx->ipayloadlen; 330 | iocb->mask = ctx->imask; 331 | iocb->data = readmark; 332 | iocb->data_length = ctx->ibufmark-readmark; 333 | if(ctx->ipayloadlen == ctx->ipayloadoff) { 334 | ctx->istate = RECV_HEADER1; 335 | ctx->ireqread = 2; 336 | } 337 | return iocb->data_length; 338 | } 339 | return WSLAY_ERR_INVALID_ARGUMENT; 340 | } 341 | -------------------------------------------------------------------------------- /websocket/src/wslay/wslay_net.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #include "wslay_net.h" 26 | 27 | #ifndef WORDS_BIGENDIAN 28 | 29 | uint64_t wslay_byteswap64(uint64_t x) 30 | { 31 | uint64_t u = ntohl(x & 0xffffffffllu); 32 | uint64_t l = ntohl(x >> 32); 33 | return (u << 32) | l; 34 | } 35 | 36 | #endif /* !WORDS_BIGENDIAN */ 37 | -------------------------------------------------------------------------------- /websocket/src/wslay/wslay_queue.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #include "wslay_queue.h" 26 | 27 | #include 28 | #include 29 | 30 | struct wslay_queue* wslay_queue_new(void) 31 | { 32 | struct wslay_queue *queue = (struct wslay_queue*)malloc 33 | (sizeof(struct wslay_queue)); 34 | if(!queue) { 35 | return NULL; 36 | } 37 | queue->top = queue->tail = NULL; 38 | return queue; 39 | } 40 | 41 | void wslay_queue_free(struct wslay_queue *queue) 42 | { 43 | if(!queue) { 44 | return; 45 | } else { 46 | struct wslay_queue_cell *p = queue->top; 47 | while(p) { 48 | struct wslay_queue_cell *next = p->next; 49 | free(p); 50 | p = next; 51 | } 52 | free(queue); 53 | } 54 | } 55 | 56 | int wslay_queue_push(struct wslay_queue *queue, void *data) 57 | { 58 | struct wslay_queue_cell *new_cell = (struct wslay_queue_cell*)malloc 59 | (sizeof(struct wslay_queue_cell)); 60 | if(!new_cell) { 61 | return WSLAY_ERR_NOMEM; 62 | } 63 | new_cell->data = data; 64 | new_cell->next = NULL; 65 | if(queue->tail) { 66 | queue->tail->next = new_cell; 67 | queue->tail = new_cell; 68 | 69 | } else { 70 | queue->top = queue->tail = new_cell; 71 | } 72 | return 0; 73 | } 74 | 75 | int wslay_queue_push_front(struct wslay_queue *queue, void *data) 76 | { 77 | struct wslay_queue_cell *new_cell = (struct wslay_queue_cell*)malloc 78 | (sizeof(struct wslay_queue_cell)); 79 | if(!new_cell) { 80 | return WSLAY_ERR_NOMEM; 81 | } 82 | new_cell->data = data; 83 | new_cell->next = queue->top; 84 | queue->top = new_cell; 85 | if(!queue->tail) { 86 | queue->tail = queue->top; 87 | } 88 | return 0; 89 | } 90 | 91 | void wslay_queue_pop(struct wslay_queue *queue) 92 | { 93 | struct wslay_queue_cell *top = queue->top; 94 | assert(top); 95 | queue->top = top->next; 96 | if(top == queue->tail) { 97 | queue->tail = NULL; 98 | } 99 | free(top); 100 | } 101 | 102 | void* wslay_queue_top(struct wslay_queue *queue) 103 | { 104 | assert(queue->top); 105 | return queue->top->data; 106 | } 107 | 108 | void* wslay_queue_tail(struct wslay_queue *queue) 109 | { 110 | assert(queue->tail); 111 | return queue->tail->data; 112 | } 113 | 114 | int wslay_queue_empty(struct wslay_queue *queue) 115 | { 116 | return queue->top == NULL; 117 | } 118 | -------------------------------------------------------------------------------- /websocket/src/wslay/wslay_stack.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Wslay - The WebSocket Library 3 | * 4 | * Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining 7 | * a copy of this software and associated documentation files (the 8 | * "Software"), to deal in the Software without restriction, including 9 | * without limitation the rights to use, copy, modify, merge, publish, 10 | * distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so, subject to 12 | * the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | #include "wslay_stack.h" 26 | 27 | #include 28 | #include 29 | 30 | struct wslay_stack* wslay_stack_new() 31 | { 32 | struct wslay_stack *stack = (struct wslay_stack*)malloc 33 | (sizeof(struct wslay_stack)); 34 | if(!stack) { 35 | return NULL; 36 | } 37 | stack->top = NULL; 38 | return stack; 39 | } 40 | 41 | void wslay_stack_free(struct wslay_stack *stack) 42 | { 43 | struct wslay_stack_cell *p; 44 | if(!stack) { 45 | return; 46 | } 47 | p = stack->top; 48 | while(p) { 49 | struct wslay_stack_cell *next = p->next; 50 | free(p); 51 | p = next; 52 | } 53 | free(stack); 54 | } 55 | 56 | int wslay_stack_push(struct wslay_stack *stack, void *data) 57 | { 58 | struct wslay_stack_cell *new_cell = (struct wslay_stack_cell*)malloc 59 | (sizeof(struct wslay_stack_cell)); 60 | if(!new_cell) { 61 | return WSLAY_ERR_NOMEM; 62 | } 63 | new_cell->data = data; 64 | new_cell->next = stack->top; 65 | stack->top = new_cell; 66 | return 0; 67 | } 68 | 69 | void wslay_stack_pop(struct wslay_stack *stack) 70 | { 71 | struct wslay_stack_cell *top = stack->top; 72 | assert(top); 73 | stack->top = top->next; 74 | free(top); 75 | } 76 | 77 | void* wslay_stack_top(struct wslay_stack *stack) 78 | { 79 | assert(stack->top); 80 | return stack->top->data; 81 | } 82 | 83 | int wslay_stack_empty(struct wslay_stack *stack) 84 | { 85 | return stack->top == NULL; 86 | } 87 | -------------------------------------------------------------------------------- /websocket/src/wslay_callbacks.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket.h" 2 | 3 | #if defined(HAVE_WSLAY) 4 | 5 | namespace dmWebsocket 6 | { 7 | 8 | const struct wslay_event_callbacks g_WslCallbacks = { 9 | WSL_RecvCallback, 10 | WSL_SendCallback, 11 | WSL_GenmaskCallback, 12 | NULL, 13 | NULL, 14 | NULL, 15 | WSL_OnMsgRecvCallback 16 | }; 17 | 18 | #define WSLAY_CASE(_X) case _X: return #_X; 19 | 20 | const char* WSL_ResultToString(int err) 21 | { 22 | switch(err) { 23 | WSLAY_CASE(WSLAY_ERR_WANT_READ); 24 | WSLAY_CASE(WSLAY_ERR_WANT_WRITE); 25 | WSLAY_CASE(WSLAY_ERR_PROTO); 26 | WSLAY_CASE(WSLAY_ERR_INVALID_ARGUMENT); 27 | WSLAY_CASE(WSLAY_ERR_INVALID_CALLBACK); 28 | WSLAY_CASE(WSLAY_ERR_NO_MORE_MSG); 29 | WSLAY_CASE(WSLAY_ERR_CALLBACK_FAILURE); 30 | WSLAY_CASE(WSLAY_ERR_WOULDBLOCK); 31 | WSLAY_CASE(WSLAY_ERR_NOMEM); 32 | default: return "Unknown error"; 33 | }; 34 | } 35 | 36 | #undef WSLAY_CASE 37 | 38 | 39 | int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx) 40 | { 41 | // Currently only supports client implementation 42 | return wslay_event_context_client_init(ctx, &g_WslCallbacks, userctx); 43 | } 44 | 45 | 46 | void WSL_Exit(wslay_event_context_ptr ctx) 47 | { 48 | wslay_event_context_free(ctx); 49 | } 50 | 51 | int WSL_Close(wslay_event_context_ptr ctx) 52 | { 53 | const char* reason = ""; 54 | wslay_event_queue_close(ctx, WSLAY_CODE_NORMAL_CLOSURE, (const uint8_t*)reason, 0); 55 | return 0; 56 | } 57 | 58 | int WSL_Poll(wslay_event_context_ptr ctx) 59 | { 60 | int r = 0; 61 | if ((r = wslay_event_recv(ctx)) != 0 || (r = wslay_event_send(ctx)) != 0) { 62 | dmLogError("Websocket poll error: %s", WSL_ResultToString(r)); 63 | } 64 | return r; 65 | } 66 | 67 | ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data) 68 | { 69 | WebsocketConnection* conn = (WebsocketConnection*)user_data; 70 | 71 | int r = -1; // received bytes if >=0, error if < 0 72 | 73 | if (conn->m_HasHandshakeData) 74 | { 75 | r = conn->m_BufferSize; 76 | memcpy(buf, conn->m_Buffer, r); 77 | conn->m_BufferSize = 0; 78 | conn->m_HasHandshakeData = 0; 79 | return r; 80 | } 81 | 82 | dmSocket::Result socket_result = Receive(conn, buf, len, &r); 83 | 84 | if (dmSocket::RESULT_OK == socket_result && r == 0) 85 | socket_result = dmSocket::RESULT_CONNABORTED; 86 | 87 | if (dmSocket::RESULT_OK != socket_result) 88 | { 89 | if (socket_result == dmSocket::RESULT_WOULDBLOCK || socket_result == dmSocket::RESULT_TRY_AGAIN) { 90 | wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK); 91 | } 92 | else 93 | wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); 94 | return -1; 95 | } 96 | return r; 97 | } 98 | 99 | ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) 100 | { 101 | WebsocketConnection* conn = (WebsocketConnection*)user_data; 102 | 103 | int sent_bytes = 0; 104 | dmSocket::Result socket_result = Send(conn, (const char*)data, len, &sent_bytes); 105 | 106 | if (socket_result != dmSocket::RESULT_OK) 107 | { 108 | if (socket_result == dmSocket::RESULT_WOULDBLOCK || socket_result == dmSocket::RESULT_TRY_AGAIN) 109 | wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK); 110 | else 111 | wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE); 112 | return -1; 113 | } 114 | return (ssize_t)sent_bytes; 115 | } 116 | 117 | // Might be called multiple times for a connection receiving multiple events 118 | void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) 119 | { 120 | WebsocketConnection* conn = (WebsocketConnection*)user_data; 121 | if (arg->opcode == WSLAY_TEXT_FRAME || arg->opcode == WSLAY_BINARY_FRAME) 122 | { 123 | PushMessage(conn, MESSAGE_TYPE_NORMAL, arg->msg_length, arg->msg, 0); 124 | } else if (arg->opcode == WSLAY_CONNECTION_CLOSE) 125 | { 126 | // The first two bytes is the close code 127 | const uint8_t* reason = (const uint8_t*)""; 128 | size_t len = arg->msg_length; 129 | if (arg->msg_length > 2) 130 | { 131 | reason = arg->msg + 2; 132 | len -= 2; 133 | } 134 | 135 | char buffer[1024]; 136 | uint16_t status_code = wslay_event_get_status_code_received(ctx); 137 | len = dmSnPrintf(buffer, sizeof(buffer), "Server closing (%u). Reason: '%s'", status_code, reason); 138 | PushMessage(conn, MESSAGE_TYPE_CLOSE, len, (const uint8_t*)buffer, status_code); 139 | 140 | if (!wslay_event_get_close_sent(ctx)) 141 | { 142 | wslay_event_queue_close(ctx, arg->status_code, (const uint8_t*)buffer, len); 143 | } 144 | 145 | DebugLog(1, "%s", buffer); 146 | 147 | } 148 | } 149 | 150 | // ************************************************************************************************ 151 | 152 | 153 | int WSL_GenmaskCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) { 154 | pcg32_random_t rnd; 155 | pcg32_srandom_r(&rnd, dmTime::GetTime(), 31452); 156 | for (unsigned int i = 0; i < len; i++) { 157 | buf[i] = (uint8_t)(pcg32_random_r(&rnd) & 0xFF); 158 | } 159 | return 0; 160 | } 161 | 162 | } // namespace 163 | 164 | #endif // HAVE_WSLAY 165 | --------------------------------------------------------------------------------