92 Commits

Author SHA1 Message Date
Alexey Gulev
02198c9401 Merge pull request #58 from defold/AGulev-patch-1
Update handshake.cpp
2025-07-24 09:22:59 +02:00
Alexey Gulev
4c5d1ecd61 Update handshake.cpp
Typo?
2025-07-22 21:36:59 +02:00
Björn Ritzl
3331ec1a8d Update index.md 2025-06-04 07:57:48 +02:00
Kharkunov Eugene
c4d744ec49 Fixed websocket library linkage for all web targets (#56) 2025-05-05 11:10:36 +02:00
Brian
de6d493a1c Update README.md (#54) 2024-09-09 17:11:52 +02:00
Alexey Gulev
65b9e38850 Merge pull request #52 from ekharkunov/emscripten-update
Use builtin Emscripten webscoket implementation for html5.
2024-04-17 17:32:19 +02:00
Kharkunov Eugene
a75c9f6c8f Use builtin Emscripten webscoket implementation for html5.
Update .gitignore.
2024-04-10 13:45:27 +03:00
Mathias Westerdahl
0ac6e3f9a3 Removed hard length limit for received messages (#51) 2024-02-28 13:50:57 +01:00
Björn Ritzl
6ec47ddc17 Delete DefoldWebsocket.symbols 2022-11-02 13:57:59 +01:00
Björn Ritzl
24bb291c72 Added status code to callback when the server closes the connection (#45)
* Added WebSocket close code to callback

Also added message and code for HTML5

* Unify disconnect handling for wslay and html5
2022-06-23 20:52:46 +02:00
Björn Ritzl
5c5736d8bc Update README.md 2022-02-24 08:27:30 +01:00
Björn Ritzl
24dd20512b Update websocket.gui_script 2022-02-17 12:40:47 +01:00
Björn Ritzl
63cb5f8de6 Update websocket.gui_script 2022-02-17 12:40:15 +01:00
Björn Ritzl
ace3242158 Update index.md 2022-01-03 14:27:42 +01:00
Björn Ritzl
4cf11c3048 Added info about websocket configuration 2021-09-14 13:30:46 +02:00
Björn Ritzl
ecaac30238 Update README.md 2021-05-28 11:47:32 +02:00
Björn Ritzl
8fd283385f Merge branch 'Issue-38-crash-on-connect' 2021-05-28 11:37:23 +02:00
Björn Ritzl
dfc94aa94a Badge 2021-05-28 11:36:58 +02:00
JCash
d5237bbee1 Remove dold include dns.h 2021-05-01 10:13:36 +02:00
JCash
daffff4c06 Updated dmConnectionPool::Dial call for new beta release 2021-04-30 10:58:31 +02:00
VitaliiTihobrazov-Melsoft
0f841f16af fix: the connection must be closed in other states (#41) 2021-04-12 22:45:09 +02:00
Björn Ritzl
97cca427d7 Reduced thread name length (#39)
pthread_setname_np() restricts thread name length to 16 characters.
2021-03-18 20:41:03 +01:00
Björn Ritzl
bc56b02e5b Reduced thread name length
pthread_setname_np() restricts thread name length to 16 characters.
2021-03-18 18:31:57 +01:00
Björn Ritzl
cba11de5f1 Run DNS lookup in a separate thread (#37)
* Moved connection pool dial to a thread
* Make sure to join the thread when done
* Cleanup in AppInitialize. Early exit if pool creation failed.
* Only check connection timeout for emscripten
2021-03-17 12:59:01 +01:00
Björn Ritzl
4e67c4dfaa Add the port to the url for html5 builds (#35)
Fixes #34
2021-03-10 11:56:26 +01:00
Björn Ritzl
efe9115413 Use the Emscripten websocket library instead of POSIX socket emulation (#33)
* Switched to direct javascript websockets

* Skip the null termination on text messages when pushing

* Moved Emscripten callbacks and some other PR improvements

* STATE_CONNECT -> STATE_CREATE

* Changed disconnect logic

* Review feedback
2021-02-18 22:13:53 +01:00
Alexander Palagin
b3afb9a276 close socket when destroying websocket connection instead of returning it back to connection pool (#32) 2021-02-10 08:49:23 +01:00
Alexander Palagin
7346f142fa replace dmStriCmp with dmStrCaseCmp (#31) 2021-02-10 07:50:09 +01:00
Alexander Palagin
165b7333dc update exmaple of websocket_callback in documentation (#30) 2021-02-10 07:49:04 +01:00
JCash
bd4e5c0b35 Compiler warning fixes 2021-02-04 08:49:00 +01:00
Alexander Palagin
ac230a6278 Handshake response object if handshake failed (#29)
* added handshake response object, so if error occurs on websocket handhsake stage, server's response can be reachable for further decisions

* fixed typo; removed c++11 style constructors
2021-02-04 08:30:40 +01:00
Alexander Palagin
39abd7cdea always call lua callback when connection goes to STATE_DISCONNECTED (#28) 2021-01-25 11:59:17 +01:00
Björn Ritzl
f988413a74 Fix logging in example when error is nil 2021-01-14 09:49:08 +01:00
JCash
73b236b249 Added ci build status badge to README.md 2020-12-24 14:22:07 +01:00
JCash
636a11daa3 Added ci build status badge to README.md 2020-12-24 14:20:08 +01:00
JCash
0f147ef180 Added auth/email to resolve step 2020-12-24 12:48:53 +01:00
Mathias Westerdahl
3cd0328c2a Ci build script (#27)
* Added bob.yml

* Added all platforms

* Added macOS builder

* There's no jre on macOS

* jarsigner exists in the jdk

* Added pull_request_target

* Added cron job
2020-12-24 11:56:34 +01:00
Björn Ritzl
f5aa57452f Merge pull request #26 from defold/Issue-25-crash-in-callback
Destroy connections when finalizing the extension
2020-12-22 12:21:20 +01:00
Björn Ritzl
7e89b8a685 Destroy connections when finalizing the extension 2020-12-22 12:19:12 +01:00
Mathias Westerdahl
ba7454a431 Merge pull request #24 from defold/issue-22-skip-protocol-by-default
Issue 22: Don't send Sec-WebSocket-Protocol unless specified
2020-11-10 08:42:30 +01:00
JCash
b93a91c9b8 Issue 22: Don't send Sec-WebSocket-Protocol unless specified 2020-11-09 19:03:27 +01:00
Mathias Westerdahl
0bf63cbdf8 Added credit and link to wslay 2020-11-01 09:28:27 +01:00
Mathias Westerdahl
8a9dc759a4 Merge pull request #21 from defold/issue-18-error-message-fix
Issue 18: Documentation fix
2020-10-31 15:48:47 +01:00
Mathias Westerdahl
70afde4cfa Merge pull request #20 from defold/issue-17-path-fix
Issue 17: Fixed path argument in http handshake
2020-10-31 15:48:21 +01:00
Mathias Westerdahl
ed6d131470 Merge pull request #19 from defold/issue-16-conneciton-protocol
Issue 16: Added protocol as a connection parameter
2020-10-31 15:47:58 +01:00
JCash
2c9c0b74f1 Issue 18: Documentation fix 2020-10-31 10:55:36 +01:00
JCash
8450a88640 Issue 17: Fixed path argument in http handshake 2020-10-31 10:40:21 +01:00
JCash
3636ea73db added documentation 2020-10-31 10:18:50 +01:00
JCash
1cc1d802b3 Issue 16: Added protocol as a connection parameter 2020-10-31 10:14:44 +01:00
JCash
94bcc82f25 cleanup 2020-10-18 09:42:53 +02:00
JCash
d62a53cbcc Doc fix 2020-10-18 09:42:40 +02:00
Mathias Westerdahl
ea2fe97943 Merge pull request #15 from defold/issue-5-custom-headers
Issue 5: Added support for custom headers
2020-10-17 14:02:21 +02:00
Mathias Westerdahl
268e9bf472 Merge pull request #14 from defold/issue-13-connect-timeout
Issue 13: Added support for connect sequence timeout
2020-10-17 12:30:41 +02:00
Mathias Westerdahl
5ee358cbba Merge pull request #12 from defold/issue-11-support-text-frames
Issue 11: Added support for both binary and text frames
2020-10-17 12:29:45 +02:00
JCash
78d527d3a8 Issue 5: Added support for custom headers 2020-10-17 12:26:20 +02:00
JCash
b54e3e07ad review fix 2020-10-17 11:47:23 +02:00
JCash
e105702c79 Issue 13: Added support for connect sequence timeout
Decreased the select timeout when reading headers
Don't call callback with DISCONNECTED if the connection was never connected
2020-10-17 11:40:53 +02:00
JCash
9d1ace0a82 Issue 11: Added support for both binary and text frames 2020-10-17 10:28:19 +02:00
Mathias Westerdahl
833477a134 Added another tcpdump example 2020-10-15 15:26:23 +02:00
Mathias Westerdahl
15a589585a Added more debugging tools to README 2020-10-15 11:35:23 +02:00
Mathias Westerdahl
3fe8fbc0ff Added external debugging tools to README 2020-10-15 10:39:50 +02:00
JCash
ba2b8e4a69 Updated hex string logging to use our internal logging functions
This will keep the output in-order
2020-10-13 09:33:38 +02:00
JCash
176f213060 updated documentation 2020-10-13 09:14:21 +02:00
Mathias Westerdahl
dc1d57d661 Merge pull request #10 from defold/issue-4-case-insensitive-header
Issue 4: Compare header keys without case sensitivity
2020-10-12 09:59:55 +02:00
JCash
5b0a9960a8 Check the response headers more thoroughly 2020-10-12 09:45:05 +02:00
JCash
40ba1b334c Issue 4: Compare header keys without case sensitivity 2020-10-12 09:33:47 +02:00
Mathias Westerdahl
23ba179e2a Merge pull request #9 from defold/issue-8-close-event
Issue 8: Handle the websocket close event
2020-09-28 09:30:12 +02:00
JCash
36bf5d1c03 code cleanup 2020-09-27 17:04:00 +02:00
JCash
274f29d7e4 moved error checking code outside of socket implementation 2020-09-27 17:02:14 +02:00
JCash
bd8569f49a Issue 8: Handle the websocket close event 2020-09-27 16:54:22 +02:00
Mathias Westerdahl
18a768774f Merge pull request #7 from defold/issue-6-multiple-messages
Issue 6: Handle multiple messages per frame
2020-09-27 10:14:08 +02:00
JCash
8e32fa3c76 compile fixes 2020-09-26 15:50:24 +02:00
JCash
832a156395 Issue 6: Handle multiple messages per frame 2020-09-26 12:51:43 +02:00
JCash
8df9eed682 Removed redundant / in concatenated uri's 2020-09-06 09:35:09 +02:00
JCash
071adac853 Added separate code path for html5 2020-09-05 11:02:59 +02:00
JCash
6ef040aee6 more testing 2020-09-05 10:52:37 +02:00
JCash
337a389cf8 more testing 2020-09-04 18:37:58 +02:00
JCash
b9f3563652 more testing 2020-09-04 18:11:40 +02:00
JCash
358e652946 more testing 2020-09-04 17:56:03 +02:00
JCash
44fc5037cb more testing 2020-09-04 17:38:09 +02:00
JCash
18ecf3857f more testing 2020-09-04 17:16:49 +02:00
JCash
b31e5bec7f more testing 2020-09-04 16:59:30 +02:00
JCash
61916f4c27 more testing 2020-09-04 16:41:19 +02:00
JCash
72f081bf23 test wss+html5 2020-09-04 15:53:27 +02:00
JCash
ab1b52c676 Merge branch 'master' of github.com:defold/extension-websocket 2020-09-04 12:04:16 +02:00
JCash
2bd4e228c6 added reference demo 2020-09-04 12:04:06 +02:00
Björn Ritzl
1202731ec8 Added api docs for websocket.send() 2020-09-04 11:22:54 +02:00
JCash
f0cf078019 debugging 2020-09-04 11:14:13 +02:00
JCash
bd9e777b99 Windows compile fixes 2020-09-03 17:28:10 +02:00
JCash
90a654e639 Added doc builder trigger + funding.yml 2020-09-03 15:24:19 +02:00
JCash
f3ea270c68 Updated readme 2020-09-03 15:18:31 +02:00
Mathias Westerdahl
2bddcac495 Merge pull request #1 from defold/initial
Add first version of websocket extension
2020-09-03 08:45:42 +02:00
41 changed files with 14790 additions and 12363 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
github: defold
patreon: Defold
custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NBNBHTUW4GS4C']

76
.github/workflows/bob.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Build with bob
on:
push:
pull_request_target:
schedule:
# nightly at 05:00 on the 1st and 15th
- cron: 0 5 1,15 * *
env:
VERSION_FILENAME: 'info.json'
BUILD_SERVER: 'https://build.defold.com'
jobs:
build_with_bob:
strategy:
matrix:
platform: [armv7-android, x86_64-linux, x86_64-win32, x86-win32, js-web]
runs-on: ubuntu-latest
name: Build
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '11.0.2'
- name: Get Defold version
run: |
TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'`
echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV
echo "Found version ${TMPVAR}"
- name: Download bob.jar
run: |
wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar
java -jar bob.jar --version
- name: Resolve libraries
run: java -jar bob.jar resolve --email a@b.com --auth 123456
- name: Build
run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}}
- name: Bundle
run: java -jar bob.jar --platform=${{ matrix.platform }} bundle
# macOS is not technically needed for building, but we want to test bundling as well, since we're also testing the manifest merging
build_with_bob_macos:
strategy:
matrix:
platform: [armv7-darwin, x86_64-darwin]
runs-on: macOS-latest
name: Build
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '11.0.2'
- name: Get Defold version
run: |
TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'`
echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV
echo "Found version ${TMPVAR}"
- name: Download bob.jar
run: |
wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar
java -jar bob.jar --version
- name: Resolve libraries
run: java -jar bob.jar resolve --email a@b.com --auth 123456
- name: Build
run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}}
- name: Bundle
run: java -jar bob.jar --platform=${{ matrix.platform }} bundle

View File

@@ -0,0 +1,22 @@
name: Trigger site rebuild
on:
push:
branches:
- 'master'
jobs:
site-rebuild:
runs-on: ubuntu-latest
steps: [
{
name: 'Repository dispatch',
uses: defold/repository-dispatch@1.2.1,
with: {
repo: 'defold/defold.github.io',
token: '${{ secrets.SERVICES_GITHUB_TOKEN }}',
user: 'services@defold.se',
action: 'extension-websocket'
}
}]

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@ builtins
lws_source lws_source
lws_build lws_build
*.profraw *.profraw
*.der
/.editor_settings

View File

@@ -1,4 +1,61 @@
# Native extension template # Defold websocket extension
This template contains the basic setup for creation of a Defold native extension.
You can learn more about native extensions in the [official manual](https://www.defold.com/manuals/extensions/). [![Build Status](https://github.com/defold/extension-websocket/workflows/Build%20with%20bob/badge.svg)](https://github.com/defold/extension-websocket/actions)
## Installation
To use this library in your Defold project, add the following URL to your `game.project` dependencies:
https://github.com/defold/extension-websocket/archive/master.zip
We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-websocket/releases).
## API reference
https://defold.com/extension-websocket/
https://defold.com/extension-websocket/websocket_api/
## Debugging
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):
```
[websocket]
debug = level
```
Set it to:
* `0` to disable debugging (i.e. no debug output).
* `1` to display state changes.
* `2` to display the messages sent and received.
## External resources
To verify that your websocket server works, you can test it with some tools.
* [websocat](https://github.com/vi/websocat)
Or, you can test your server on this web page:
* https://www.websocket.org/echo.html
To monitor all the packets sent to/from the client/server, you can use e.g.
* [Wireshark](https://www.wireshark.org)
For command line debugging, there's
* tcpdump: `sudo tcpdump -X -s0 -ilo0 port 8080 ` (example for local ws:// connection)
* tcpdump: `sudo tcpdump -X -s0 host echo.websocket.org` (Monitors packets to/from echo.websocket.org)
## Credits
This extension makes use of the C library WSlay by @tatsuhiro-t:
* https://github.com/tatsuhiro-t/wslay
The test server used by the example:
* https://www.lob.com/blog/websocket-org-is-down-here-is-an-alternative

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +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}]}]}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,184 @@
[project]
title = Defold-Websocket
version = 1.0.0
write_log = 0
compress_archive = 1
publisher = unnamed
developer = unnamed
[display]
width = 640
height = 1136
high_dpi = 0
samples = 0
fullscreen = 0
update_frequency = 0
vsync = 1
display_profiles = /builtins/render/default.display_profilesc
dynamic_orientation = 0
[render]
clear_color_red = 0
clear_color_green = 0
clear_color_blue = 0
clear_color_alpha = 0
[physics]
type = 2D
gravity_y = -10
debug = 0
debug_alpha = 0.9
world_count = 4
gravity_x = 0
gravity_z = 0
scale = 0.02
allow_dynamic_transforms = 0
debug_scale = 30
max_collisions = 64
max_contacts = 128
contact_impulse_limit = 0
ray_cast_limit_2d = 64
ray_cast_limit_3d = 128
trigger_overlap_capacity = 16
[bootstrap]
main_collection = /examples/websocket.collectionc
render = /builtins/render/default.renderc
[graphics]
default_texture_min_filter = linear
default_texture_mag_filter = linear
max_draw_calls = 1024
max_characters = 8192
max_debug_vertices = 10000
texture_profiles = /builtins/graphics/default.texture_profiles
verify_graphics_calls = 1
memory_size = 512
[shader]
output_spirv = 0
[sound]
gain = 1
max_sound_data = 128
max_sound_buffers = 32
max_sound_sources = 16
max_sound_instances = 256
max_component_count = 32
[resource]
http_cache = 0
max_resources = 1024
[input]
repeat_delay = 0.5
repeat_interval = 0.2
gamepads = /builtins/input/default.gamepadsc
game_binding = /input/game.input_bindingc
use_accelerometer = 1
[sprite]
max_count = 128
subpixels = 1
[spine]
max_count = 128
[model]
max_count = 128
[mesh]
max_count = 128
[gui]
max_count = 64
max_particlefx_count = 64
max_particle_count = 1024
[collection]
max_instances = 1024
max_input_stack_entries = 16
[collection_proxy]
max_count = 8
[collectionfactory]
max_count = 128
[factory]
max_count = 128
[ios]
launch_screen = /builtins/manifests/ios/LaunchScreen.storyboardc
pre_renderered_icons = 0
bundle_identifier = com.defold.websocket
infoplist = /builtins/manifests/ios/Info.plist
default_language = en
localizations = en
[android]
version_code = 1
minimum_sdk_version = 16
target_sdk_version = 29
package = com.defold.websocket
manifest = /builtins/manifests/android/AndroidManifest.xml
iap_provider = GooglePlay
input_method = KeyEvent
immersive_mode = 0
display_cutout = 1
debuggable = 0
[osx]
infoplist = /builtins/manifests/osx/Info.plist
bundle_identifier = com.defold.websocket
default_language = en
localizations = en
[windows]
[html5]
custom_heap_size = 0
heap_size = 256
htmlfile = /builtins/manifests/web/engine_template.html
cssfile = /builtins/manifests/web/light_theme.css
archive_location_prefix = archive
show_fullscreen_button = 1
show_made_with_defold = 1
scale_mode = downscale_fit
[particle_fx]
max_count = 64
max_particle_count = 1024
[iap]
auto_finish_transactions = 1
[network]
http_timeout = 0
http_thread_count = 4
http_cache_enabled = 1
[library]
include_dirs = websocket
[script]
shared_state = 1
[label]
max_count = 64
subpixels = 1
[profiler]
track_cpu = 0
[liveupdate]
settings = /liveupdate.settings
enabled = 1
[tilemap]
max_count = 16
max_tile_count = 2048
[engine]
run_while_iconified = 0

Binary file not shown.

View File

@@ -0,0 +1,702 @@
/* ********************************************************************* */
/* Load and combine data that is split into archives */
/* ********************************************************************* */
var Combine = {
_targets: [],
_targetIndex: 0,
// target: build target
// name: intended filepath of built object
// size: expected size of built object.
// data: combined data
// downloaded: total amount of data downloaded
// pieces: array of name, offset and data objects
// numExpectedFiles: total number of files expected in description
// lastRequestedPiece: index of last data file requested (strictly ascending)
// totalLoadedPieces: counts the number of data files received
//MAX_CONCURRENT_XHR: 6, // remove comment if throttling of XHR is desired.
isCompleted: false, // status of process
_onCombineCompleted: [], // signature: name, data.
_onAllTargetsBuilt:[], // signature: void
_onDownloadProgress: [], // signature: downloaded, total
_currentDownloadBytes: 0,
_totalDownloadBytes: 0,
_retry_time: 0, // pause before retry file loading after error
_max_retry_count: 0, // how many attempts we do when trying to download a file.
_can_not_download_file_callback: undefined, //Function that is called if you can't download file after 'retry_count' attempts.
_archiveLocationFilter: function(path) { return "split" + path; },
can_not_download_file: function(file) {
if (typeof Combine._can_not_download_file_callback === 'function') {
Combine._can_not_download_file_callback(file);
}
},
addProgressListener: function(callback) {
if (typeof callback !== 'function') {
throw "Invalid callback registration";
}
this._onDownloadProgress.push(callback);
},
addCombineCompletedListener: function(callback) {
if (typeof callback !== 'function') {
throw "Invalid callback registration";
}
this._onCombineCompleted.push(callback);
},
addAllTargetsBuiltListener: function(callback) {
if (typeof callback !== 'function') {
throw "Invalid callback registration";
}
this._onAllTargetsBuilt.push(callback);
},
// descriptUrl: location of text file describing files to be preloaded
process: function(descriptUrl, attempt_count) {
if (!attempt_count) {
attempt_count = 0;
}
var xhr = new XMLHttpRequest();
xhr.open('GET', descriptUrl);
xhr.responseType = 'text';
xhr.onload = function(evt) {
Combine.onReceiveDescription(xhr);
};
xhr.onerror = function(evt) {
attempt_count += 1;
if (attempt_count < Combine._max_retry_count) {
console.warn("Can't download file '" + descriptUrl + "' . Next try in " + Combine._retry_time + " sec.");
setTimeout(function() {
Combine.process(descriptUrl, attempt_count);
}, Combine._retry_time * 1000);
} else {
Combine.can_not_download_file(descriptUrl);
}
};
xhr.send(null);
},
cleanUp: function() {
this._targets = [];
this._targetIndex = 0;
this.isCompleted = false;
this._onCombineCompleted = [];
this._onAllTargetsBuilt = [];
this._onDownloadProgress = [];
this._currentDownloadBytes = 0;
this._totalDownloadBytes = 0;
},
onReceiveDescription: function(xhr) {
var json = JSON.parse(xhr.responseText);
this._targets = json.content;
this._totalDownloadBytes = 0;
this._currentDownloadBytes = 0;
var targets = this._targets;
for(var i=0; i<targets.length; ++i) {
this._totalDownloadBytes += targets[i].size;
}
this.requestContent();
},
requestContent: function() {
var target = this._targets[this._targetIndex];
if (1 < target.pieces.length) {
target.data = new Uint8Array(target.size);
}
var limit = target.pieces.length;
if (typeof this.MAX_CONCURRENT_XHR !== 'undefined') {
limit = Math.min(limit, this.MAX_CONCURRENT_XHR);
}
for (var i=0; i<limit; ++i) {
this.requestPiece(target, i);
}
},
requestPiece: function(target, index, attempt_count) {
if (!attempt_count) {
attempt_count = 0;
}
if (index < target.lastRequestedPiece) {
throw "Request out of order";
}
var item = target.pieces[index];
target.lastRequestedPiece = index;
var total = 0;
var downloaded = 0;
var xhr = new XMLHttpRequest();
var url = this._archiveLocationFilter('/' + item.name);
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
// called periodically with information about the transaction
xhr.onprogress = function(evt) {
if (evt.total && evt.lengthComputable) {
total = evt.total;
}
if (evt.loaded && evt.lengthComputable) {
var delta = evt.loaded - downloaded;
downloaded = evt.loaded;
Combine._currentDownloadBytes += delta;
Combine.updateProgress(target);
}
};
// called when the transaction completes successfully
xhr.onload = function(evt) {
item.data = new Uint8Array(xhr.response);
item.dataLength = item.data.length;
total = item.dataLength;
downloaded = item.dataLength;
Combine.copyData(target, item);
Combine.onPieceLoaded(target, item);
Combine.updateProgress(target);
item.data = undefined;
};
// called when the transaction fails
xhr.onerror = function(evt) {
downloaded = 0;
Combine.updateProgress(target);
attempt_count += 1;
if (attempt_count < Combine._max_retry_count) {
console.warn("Can't download file '" + item.name + "' . Next try in " + Combine._retry_time + " sec.");
setTimeout(function() {
Combine.requestPiece(target, index, attempt_count);
}, Combine._retry_time * 1000);
} else {
Combine.can_not_download_file(item.name);
}
};
xhr.send(null);
},
updateProgress: function(target) {
for(i = 0; i<this._onDownloadProgress.length; ++i) {
this._onDownloadProgress[i](this._currentDownloadBytes, this._totalDownloadBytes);
}
},
copyData: function(target, item) {
if (1 == target.pieces.length) {
target.data = item.data;
} else {
var start = item.offset;
var end = start + item.data.length;
if (0 > start) {
throw "Buffer underflow";
}
if (end > target.data.length) {
throw "Buffer overflow";
}
target.data.set(item.data, item.offset);
}
},
onPieceLoaded: function(target, item) {
if (typeof target.totalLoadedPieces === 'undefined') {
target.totalLoadedPieces = 0;
}
++target.totalLoadedPieces;
if (target.totalLoadedPieces == target.pieces.length) {
this.finalizeTarget(target);
++this._targetIndex;
for (var i=0; i<this._onCombineCompleted.length; ++i) {
this._onCombineCompleted[i](target.name, target.data);
}
if (this._targetIndex < this._targets.length) {
this.requestContent();
} else {
this.isCompleted = true;
for (i=0; i<this._onAllTargetsBuilt.length; ++i) {
this._onAllTargetsBuilt[i]();
}
}
} else {
var next = target.lastRequestedPiece + 1;
if (next < target.pieces.length) {
this.requestPiece(target, next);
}
}
},
finalizeTarget: function(target) {
var actualSize = 0;
for (var i=0;i<target.pieces.length; ++i) {
actualSize += target.pieces[i].dataLength;
}
if (actualSize != target.size) {
throw "Unexpected data size";
}
if (1 < target.pieces.length) {
var output = target.data;
var pieces = target.pieces;
for (i=0; i<pieces.length; ++i) {
var item = pieces[i];
// Bounds check
var start = item.offset;
var end = start + item.dataLength;
if (0 < i) {
var previous = pieces[i - 1];
if (previous.offset + previous.dataLength > start) {
throw "Segment underflow";
}
}
if (pieces.length - 2 > i) {
var next = pieces[i + 1];
if (end > next.offset) {
throw "Segment overflow";
}
}
}
}
}
};
/* ********************************************************************* */
/* Default splash and progress visualisation */
/* ********************************************************************* */
var Progress = {
progress_id: "defold-progress",
bar_id: "defold-progress-bar",
addProgress : function (canvas) {
/* Insert default progress bar below canvas */
canvas.insertAdjacentHTML('afterend', '<div id="' + Progress.progress_id + '" class="canvas-app-progress"><div id="' + Progress.bar_id + '" class="canvas-app-progress-bar" style="width: 0%;"></div></div>');
Progress.bar = document.getElementById(Progress.bar_id);
Progress.progress = document.getElementById(Progress.progress_id);
},
updateProgress: function (percentage, text) {
Progress.bar.style.width = percentage + "%";
},
removeProgress: function () {
if (Progress.progress.parentElement !== null) {
Progress.progress.parentElement.removeChild(Progress.progress);
// Remove any background/splash image that was set in runApp().
// Workaround for Safari bug DEF-3061.
Module.canvas.style.background = "";
}
}
};
/* ********************************************************************* */
/* Default input override */
/* ********************************************************************* */
var CanvasInput = {
arrowKeysHandler : function(e) {
switch(e.keyCode) {
case 37: case 38: case 39: case 40: // Arrow keys
case 32: e.preventDefault(); e.stopPropagation(); // Space
default: break; // do not block other keys
}
},
onFocusIn : function(e) {
window.addEventListener("keydown", CanvasInput.arrowKeysHandler, false);
},
onFocusOut: function(e) {
window.removeEventListener("keydown", CanvasInput.arrowKeysHandler, false);
},
addToCanvas : function(canvas) {
canvas.addEventListener("focus", CanvasInput.onFocusIn, false);
canvas.addEventListener("blur", CanvasInput.onFocusOut, false);
canvas.focus();
CanvasInput.onFocusIn();
}
};
/* ********************************************************************* */
/* Module is Emscripten namespace */
/* ********************************************************************* */
var Module = {
noInitialRun: true,
_filesToPreload: [],
_archiveLoaded: false,
_preLoadDone: false,
_waitingForArchive: false,
// Persistent storage
persistentStorage: true,
_syncInProgress: false,
_syncNeeded: false,
_syncInitial: false,
_syncMaxTries: 3,
_syncTries: 0,
print: function(text) { console.log(text); },
printErr: function(text) { console.error(text); },
setStatus: function(text) { console.log(text); },
isWASMSupported: (function() {
try {
if (typeof WebAssembly === "object"
&& typeof WebAssembly.instantiate === "function") {
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
if (module instanceof WebAssembly.Module)
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
} catch (e) {
}
return false;
})(),
prepareErrorObject: function (err, url, line, column, errObj) {
line = typeof line == "undefined" ? 0 : line;
column = typeof column == "undefined" ? 0 : column;
url = typeof url == "undefined" ? "" : url;
var errorLine = url + ":" + line + ":" + column;
var error = errObj || (typeof window.event != "undefined" ? window.event.error : "" ) || err || "Undefined Error";
var message = "";
var stack = "";
var backtrace = "";
if (typeof error == "object" && typeof error.stack != "undefined" && typeof error.message != "undefined") {
stack = String(error.stack);
message = String(error.message);
} else {
stack = String(error).split("\n");
message = stack.shift();
stack = stack.join("\n");
}
stack = stack || errorLine;
var callLine = /at (\S+:\d*$)/.exec(message);
if (callLine) {
message = message.replace(/(at \S+:\d*$)/, "");
stack = callLine[1] + "\n" + stack;
}
message = message.replace(/(abort\(.+\)) at .+/, "$1");
stack = stack.replace(/\?{1}\S+(:\d+:\d+)/g, "$1");
stack = stack.replace(/ *at (\S+)$/gm, "@$1");
stack = stack.replace(/ *at (\S+)(?: \[as \S+\])? +\((.+)\)/g, "$1@$2");
stack = stack.replace(/^((?:Object|Array)\.)/gm, "");
stack = stack.split("\n");
return { stack:stack, message:message };
},
hasWebGLSupport: function() {
var webgl_support = false;
try {
var canvas = document.createElement("canvas");
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (gl && gl instanceof WebGLRenderingContext) {
webgl_support = true;
}
} catch (error) {
console.log("An error occurred while detecting WebGL support: " + error);
webgl_support = false;
}
return webgl_support;
},
handleVisibilityChange: function () {
GLFW.onFocusChanged(document[Module.hiddenProperty] ? 0 : 1);
},
getHiddenProperty: function () {
if ('hidden' in document) return 'hidden';
var prefixes = ['webkit','moz','ms','o'];
for (var i = 0; i < prefixes.length; i++) {
if ((prefixes[i] + 'Hidden') in document)
return prefixes[i] + 'Hidden';
}
return null;
},
setupVisibilityChangeListener: function() {
Module.hiddenProperty = Module.getHiddenProperty();
if( Module.hiddenProperty ) {
var eventName = Module.hiddenProperty.replace(/[H|h]idden/,'') + 'visibilitychange';
document.addEventListener(eventName, Module.handleVisibilityChange, false);
} else {
console.log("No document.hidden property found. The focus events won't be enabled.")
}
},
/**
* Module.runApp - Starts the application given a canvas element id
*
* 'extra_params' is an optional object that can have the following fields:
*
* 'archive_location_filter':
* Filter function that will run for each archive path.
*
* 'unsupported_webgl_callback':
* Function that is called if WebGL is not supported.
*
* 'engine_arguments':
* List of arguments (strings) that will be passed to the engine.
*
* 'persistent_storage':
* Boolean toggling the usage of persistent storage.
*
* 'custom_heap_size':
* Number of bytes specifying the memory heap size.
*
* 'disable_context_menu':
* Disables the right-click context menu on the canvas element if true.
*
* 'retry_time':
* Pause before retry file loading after error.
*
* 'retry_count':
* How many attempts we do when trying to download a file.
*
* 'can_not_download_file_callback':
* Function that is called if you can't download file after 'retry_count' attempts.
**/
runApp: function(app_canvas_id, extra_params) {
app_canvas_id = (typeof app_canvas_id === 'undefined') ? 'canvas' : app_canvas_id;
var params = {
archive_location_filter: function(path) { return 'split' + path; },
unsupported_webgl_callback: undefined,
engine_arguments: [],
persistent_storage: true,
custom_heap_size: undefined,
disable_context_menu: true,
retry_time: 1,
retry_count: 10,
can_not_download_file_callback: undefined,
};
for (var k in extra_params) {
if (extra_params.hasOwnProperty(k)) {
params[k] = extra_params[k];
}
}
Module.canvas = document.getElementById(app_canvas_id);
Module.arguments = params["engine_arguments"];
Module.persistentStorage = params["persistent_storage"];
Module["TOTAL_MEMORY"] = params["custom_heap_size"];
if (Module.hasWebGLSupport()) {
// Override game keys
CanvasInput.addToCanvas(Module.canvas);
Module.setupVisibilityChangeListener();
// Add progress visuals
Progress.addProgress(Module.canvas);
// Add context menu hide-handler if requested
if (params["disable_context_menu"])
{
Module.canvas.oncontextmenu = function(e) {
e.preventDefault();
};
}
Combine._retry_time = params["retry_time"];
Combine._max_retry_count = params["retry_count"];
if (typeof params["can_not_download_file_callback"] === "function") {
Combine._can_not_download_file_callback = params["can_not_download_file_callback"];
}
// Load and assemble archive
Combine.addCombineCompletedListener(Module.onArchiveFileLoaded);
Combine.addAllTargetsBuiltListener(Module.onArchiveLoaded);
Combine.addProgressListener(Module.onArchiveLoadProgress);
Combine._archiveLocationFilter = params["archive_location_filter"];
Combine.process(Combine._archiveLocationFilter('/archive_files.json'));
} else {
Progress.addProgress(Module.canvas);
Progress.updateProgress(100, "Unable to start game, WebGL not supported");
Module.setStatus = function(text) {
if (text) Module.printErr('[missing WebGL] ' + text);
};
if (typeof params["unsupported_webgl_callback"] === "function") {
params["unsupported_webgl_callback"]();
}
}
},
onArchiveLoadProgress: function(downloaded, total) {
Progress.updateProgress(downloaded / total * 100);
},
onArchiveFileLoaded: function(name, data) {
Module._filesToPreload.push({path: name, data: data});
},
onArchiveLoaded: function() {
Combine.cleanUp();
Module._archiveLoaded = true;
Progress.updateProgress(100, "Starting...");
if (Module._waitingForArchive) {
Module._preloadAndCallMain();
}
},
toggleFullscreen: function() {
if (GLFW.isFullscreen) {
GLFW.cancelFullScreen();
} else {
GLFW.requestFullScreen();
}
},
preSync: function(done) {
// Initial persistent sync before main is called
FS.syncfs(true, function(err) {
if(err) {
Module._syncTries += 1;
console.error("FS syncfs error: " + err);
if (Module._syncMaxTries > Module._syncTries) {
Module.preSync(done);
} else {
Module._syncInitial = true;
done();
}
} else {
Module._syncInitial = true;
if (done !== undefined) {
done();
}
}
});
},
preloadAll: function() {
if (Module._preLoadDone) {
return;
}
Module._preLoadDone = true;
for (var i = 0; i < Module._filesToPreload.length; ++i) {
var item = Module._filesToPreload[i];
FS.createPreloadedFile("", item.path, item.data, true, true);
}
},
// Tries to do a MEM->IDB sync
// It will flag that another one is needed if there is already one sync running.
persistentSync: function() {
// Need to wait for the initial sync to finish since it
// will call close on all its file streams which will trigger
// new persistentSync for each.
if (Module._syncInitial) {
if (Module._syncInProgress) {
Module._syncNeeded = true;
} else {
Module._startSyncFS();
}
}
},
preInit: [function() {
/* Mount filesystem on preinit */
var dir = DMSYS.GetUserPersistentDataRoot();
FS.mkdir(dir);
// If IndexedDB is supported we mount the persistent data root as IDBFS,
// then try to do a IDB->MEM sync before we start the engine to get
// previously saved data before boot.
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
if (Module.persistentStorage && window.indexedDB) {
FS.mount(IDBFS, {}, dir);
// Patch FS.close so it will try to sync MEM->IDB
var _close = FS.close; FS.close = function(stream) { var r = _close(stream); Module.persistentSync(); return r; }
// Sync IDB->MEM before calling main()
Module.preSync(function() {
Module._preloadAndCallMain();
});
} else {
Module._preloadAndCallMain();
}
}],
preRun: [function() {
/* If archive is loaded, preload all its files */
if(Module._archiveLoaded) {
Module.preloadAll();
}
}],
postRun: [function() {
if(Module._archiveLoaded) {
Progress.removeProgress();
}
}],
_preloadAndCallMain: function() {
// If the archive isn't loaded,
// we will have to wait with calling main.
if (!Module._archiveLoaded) {
Module._waitingForArchive = true;
} else {
// Need to set heap size before calling main
TOTAL_MEMORY = Module["TOTAL_MEMORY"] || TOTAL_MEMORY;
Module.preloadAll();
Progress.removeProgress();
if (Module.callMain === undefined) {
Module.noInitialRun = false;
} else {
Module.callMain(Module.arguments);
}
}
},
// Wrap IDBFS syncfs call with logic to avoid multiple syncs
// running at the same time.
_startSyncFS: function() {
Module._syncInProgress = true;
if (Module._syncMaxTries > Module._syncTries) {
FS.syncfs(false, function(err) {
Module._syncInProgress = false;
if (err) {
console.error("Module._startSyncFS error: " + err);
Module._syncTries += 1;
}
if (Module._syncNeeded) {
Module._syncNeeded = false;
Module._startSyncFS();
}
});
}
},
};
window.onerror = function(err, url, line, column, errObj) {
var errorObject = Module.prepareErrorObject(err, url, line, column, errObj);
Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]);
Module.setStatus('Exception thrown, see JavaScript console');
Module.setStatus = function(text) {
if (text) Module.printErr('[post-exception status] ' + text);
};
};

View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, minimal-ui, shrink-to-fit=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- The above 4 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Defold-Websocket 1.0.0</title>
<style type='text/css'>
/* Disable user selection to avoid strange bug in Chrome on Windows:
* Selecting a text outside the canvas, then clicking+draging would
* drag the selected text but block mouse down/up events to the engine.
*/
body {
position: fixed; /* Prevent overscroll */
margin:0;
padding:0;
}
.canvas-app-container {
width: 100%;
height: 100%;
position: absolute;
align-items: center;
justify-content: center;
overflow: hidden;
}
.canvas-app-container:-webkit-full-screen {
/* Auto width and height in Safari/Chrome fullscreen. */
width: auto;
height: auto;
}
#canvas {
outline: none;
border: 0;
width: 100%;
vertical-align: bottom;
}
canvas:focus, canvas:active {
outline: none;
border: 0;
ie-dummy: expression(this.hideFocus=true);
-moz-outline-style: none;
}
div {
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.canvas-app-progress {
position: absolute;
background-color: #d1dbeb;
height: 6px;
margin-top: -6px;
width: 100%;
}
.canvas-app-progress-bar {
font-size: 12px;
height: 6px;
color: rgb(255, 255, 255);
background-color: #1a72eb;
text-align: center;
line-height: 20px;
}
.button {
background-image: url("data:image/svg+xml,%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' baseProfile='full' width='16' height='16' viewBox='0 0 16 16' version='1.1' xml:space='preserve'%3E%3Ctitle%3Eic-16-fullscreen%3C/title%3E%3Cg id='ic-16-fullscreen' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M3,11.5 C3,11.776 3.224,12 3.5,12 L12.5,12 C12.776,12 13,11.776 13,11.5 L13,4.5 C13,4.224 12.776,4 12.5,4 L3.5,4 C3.224,4 3,4.224 3,4.5 L3,11.5 Z M14,11 L14,13 L12,13 C11.724,13 11.5,13.224 11.5,13.5 C11.5,13.776 11.724,14 12,14 L14.5,14 C14.776,14 15,13.776 15,13.5 L15,11 C15,10.724 14.776,10.5 14.5,10.5 C14.224,10.5 14,10.724 14,11 Z M12,2 C11.724,2 11.5,2.224 11.5,2.5 C11.5,2.776 11.724,3 12,3 L14,3 L14,5 C14,5.276 14.224,5.5 14.5,5.5 C14.776,5.5 15,5.276 15,5 L15,2.5 C15,2.224 14.776,2 14.5,2 L12,2 Z M2,13 L2,11 C2,10.724 1.776,10.5 1.5,10.5 C1.224,10.5 1,10.724 1,11 L1,13.5 C1,13.776 1.224,14 1.5,14 L4,14 C4.276,14 4.5,13.776 4.5,13.5 C4.5,13.224 4.276,13 4,13 L2,13 Z M1,2.5 C1,2.224 1.224,2 1.5,2 L4,2 C4.276,2 4.5,2.224 4.5,2.5 C4.5,2.776 4.276,3 4,3 L2,3 L2,5 C2,5.276 1.776,5.5 1.5,5.5 C1.224,5.5 1,5.276 1,5 L1,2.5 Z ' id='fill_1' fill='%23006fff'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
background-repeat: no-repeat;
border-color: transparent;
float: left;
color: #006fff;
padding-left: 50%;
padding: 0px 0px 0px 20px;
cursor:pointer;
background-position: left bottom;
margin-left: 2px;
}
.link {
text-align: right;
color: #4e5258;
margin-right: 2px;
}
a {
font-weight: 600;
color: #006fff;
text-decoration: none;
}
.link, .button {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
font-style: normal;
font-stretch: normal;
line-height: normal;
letter-spacing: 0px;
padding-top: 12px;
}
.buttons-background {
background-color: #ffffff;
width: 100%;
height: 42px;
}
body {
background-color: #ffffff;
}
.canvas-app-container {
background: rgba(250,252,255,1);
background: -moz-linear-gradient(-45deg, rgba(250,252,255,1) 0%, rgba(250,252,255,1) 50%, rgba(245,249,255,1) 50%, rgba(245,249,255,1) 100%);
background: -webkit-gradient(left top, right bottom, color-stop(0%, rgba(250,252,255,1)), color-stop(50%, rgba(250,252,255,1)), color-stop(50%, rgba(245,249,255,1)), color-stop(100%, rgba(245,249,255,1)));
background: -webkit-linear-gradient(-45deg, rgba(250,252,255,1) 0%, rgba(250,252,255,1) 50%, rgba(245,249,255,1) 50%, rgba(245,249,255,1) 100%);
background: -o-linear-gradient(-45deg, rgba(250,252,255,1) 0%, rgba(250,252,255,1) 50%, rgba(245,249,255,1) 50%, rgba(245,249,255,1) 100%);
background: -ms-linear-gradient(-45deg, rgba(250,252,255,1) 0%, rgba(250,252,255,1) 50%, rgba(245,249,255,1) 50%, rgba(245,249,255,1) 100%);
background: linear-gradient(135deg, rgba(250,252,255,1) 0%, rgba(250,252,255,1) 50%, rgba(245,249,255,1) 50%, rgba(245,249,255,1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fafcff', endColorstr='#f5f9ff', GradientType=1 );
}
.canvas-app-canvas {
background-repeat:no-repeat;
background-position: center center;
background-size: 70%;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='300px' height='64px' viewBox='-2467.5 2469 300 64' style='enable-background:new -2467.5 2469 300 64;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%2315244A;%7D .st1%7Bfill:url(%23SVGID_1_);%7D .st2%7Bfill:url(%23SVGID_2_);%7D%0A%3C/style%3E%3Ctitle%3Edefold-logo-html5-splash%3C/title%3E%3Cpolygon class='st0' points='-2177,2482.9 -2175.5,2482.9 -2175.5,2486.7 -2174.4,2486.7 -2174.4,2482.9 -2173.2,2482.9 -2173.2,2481.9 -2177,2481.9 '/%3E%3Cpolygon class='st0' points='-2169.8,2484.1 -2171,2482.1 -2172.1,2482.1 -2172.1,2486.7 -2171,2486.7 -2171,2483.5 -2169.7,2485.6 -2169.7,2485.6 -2168.5,2483.5 -2168.5,2486.7 -2167.5,2486.7 -2167.5,2482.1 -2168.6,2482.1 '/%3E%3Cpath class='st0' d='M-2376,2482h-13.8v38h13.6c6.6,0,12.2-1.9,16.1-5.5c3.8-3.5,5.8-8.5,5.7-13.7v-0.1 C-2354.5,2489.3-2362.9,2482-2376,2482z M-2364,2501.2c0,6.7-4.5,10.9-11.8,10.9h-4.7v-22h4.7c7.3,0,11.8,4.2,11.8,10.9 L-2364,2501.2z'/%3E%3Cpolygon class='st0' points='-2340.9,2505 -2325.1,2505 -2325.1,2497.4 -2340.9,2497.4 -2340.9,2489.6 -2322.4,2489.6 -2322.4,2481.9 -2350.1,2481.9 -2350.1,2520 -2322.2,2520 -2322.2,2512.4 -2340.9,2512.4 '/%3E%3Cpolygon class='st0' points='-2317.1,2481.9 -2317.1,2520 -2307.9,2520 -2307.9,2505.9 -2293,2505.9 -2293,2498.4 -2307.9,2498.4 -2307.9,2489.9 -2289.6,2489.9 -2289.6,2481.9 '/%3E%3Cpolygon class='st0' points='-2233,2482.1 -2242.2,2482.1 -2242.2,2520 -2216.3,2520 -2216.3,2512.2 -2233,2512.2 '/%3E%3Cpath class='st0' d='M-2197.1,2482h-13.7v38h13.5c6.7,0,12.2-1.9,16.1-5.5c3.8-3.5,5.8-8.5,5.7-13.7v-0.1 C-2175.5,2489.3-2184,2482-2197.1,2482z M-2185.1,2501.2c0,6.7-4.5,10.9-11.8,10.9h-4.7v-22h4.7c7.3,0,11.8,4.2,11.8,10.9V2501.2z' /%3E%3Cpath class='st0' d='M-2267.5,2481.7c-10.8,0-19.6,8.8-19.6,19.7c0,10.8,8.8,19.6,19.7,19.6c10.8,0,19.6-8.8,19.6-19.6l0,0 C-2247.8,2490.5-2256.6,2481.7-2267.5,2481.7C-2267.5,2481.7-2267.5,2481.7-2267.5,2481.7z M-2258,2507.9l-8.8,5.1 c-0.5,0.3-1.2,0.3-1.8,0l-8.8-5.1c-0.5-0.3-0.9-0.9-0.9-1.5v-10.2c0-0.6,0.3-1.2,0.9-1.5l8.8-5.1c0.5-0.3,1.2-0.3,1.8,0l8.8,5.1 c0.5,0.3,0.9,0.9,0.9,1.5v10.2C-2257.1,2507-2257.4,2507.6-2258,2507.9z'/%3E%3Cpath class='st0' d='M-2423.2,2494.6l-11.1,6.4l-11.1-6.4l11.1-6.4L-2423.2,2494.6z M-2412.1,2501v12.8l11.1-6.4L-2412.1,2501z M-2467.5,2507.4l11.1,6.4V2501L-2467.5,2507.4z M-2434.3,2526.6l11.1,6.4l11.1-6.4l-11.1-6.4l11.1-6.4l-11.1-6.4l-11.1,6.4 l-11.1-6.4l-11.1,6.4l11.1,6.4l-11.1,6.4l11.1,6.4L-2434.3,2526.6z'/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='-2451.2178' y1='2525.4604' x2='-2406.2178' y2='2499.6304' gradientTransform='matrix(1 0 0 -1 0 5004)'%3E%3Cstop offset='0' style='stop-color:%231C68EC'/%3E%3Cstop offset='1' style='stop-color:%2300E9DF'/%3E%3C/linearGradient%3E%3Cpath class='st1' d='M-2412.1,2513.8v12.8l-11.1-6.4L-2412.1,2513.8z M-2434.3,2513.8V2501l-11.1-6.4v12.8L-2434.3,2513.8z M-2445.4,2469v12.8l11.1-6.4L-2445.4,2469z M-2412.1,2488.2L-2412.1,2488.2 M-2423.2,2507.4l11.1,6.4V2501l11.1,6.4v-12.8 l-11.1-6.4v-12.8l0,0l-11.1-6.4v12.8l-11.1-6.4v12.8l11.1,6.4V2507.4z'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='-2465.9385' y1='2521.2493' x2='-2423.5085' y2='2496.7893' gradientTransform='matrix(1 0 0 -1 0 5004)'%3E%3Cstop offset='0' style='stop-color:%23FF3C2A'/%3E%3Cstop offset='1' style='stop-color:%23FFD215'/%3E%3C/linearGradient%3E%3Cpath class='st2' d='M-2434.3,2513.8V2501l11.1-6.4v12.8L-2434.3,2513.8z M-2434.3,2475.4l11.1,6.4V2469L-2434.3,2475.4z M-2456.5,2488.2L-2456.5,2488.2 M-2445.4,2494.6l11.1-6.4v-12.8l-11.1,6.4V2469l-11.1,6.4l0,0v12.8l-11.1,6.4v12.8l11.1-6.4v12.8 l11.1-6.4V2494.6z M-2456.5,2513.8v12.8l11.1-6.4L-2456.5,2513.8z'/%3E%3C/svg%3E%0A");
}
</style>
</head>
<body>
<div id="app-container" class="canvas-app-container">
<canvas id="canvas" class="canvas-app-canvas" tabindex="1" width="640" height="1136"></canvas>
<div class="buttons-background">
<div class="button" onclick="Module.toggleFullscreen();">Fullscreen</div>
<div class="link">Made with <a href="https://defold.com/" target="_blank">Defold</a></div>
</div>
</div>
<!-- -->
<script id='engine-loader' type='text/javascript' src="dmloader.js"></script>
<!-- -->
<script id='engine-setup' type='text/javascript'>
var extra_params = {
archive_location_filter: function( path ) {
return ("archive" + path + "");
},
engine_arguments: [],
custom_heap_size: 268435456,
disable_context_menu: true
}
Module['onRuntimeInitialized'] = function() {
Module.runApp("canvas", extra_params);
};
Module["locateFile"] = function(path, scriptDirectory)
{
// dmengine*.wasm is hardcoded in the built JS loader for WASM,
// we need to replace it here with the correct project name.
if (path == "dmengine.wasm" || path == "dmengine_release.wasm" || path == "dmengine_headless.wasm") {
path = "DefoldWebsocket.wasm";
}
return scriptDirectory + path;
};
var is_iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
var buttonHeight = 0;
buttonHeight = 42;
buttonHeight = 42;
// Resize on init, screen resize and orientation change
function resize_game_canvas() {
// Hack for iOS when exit from Fullscreen mode
if (is_iOS) {
window.scrollTo(0, 0);
}
var app_container = document.getElementById('app-container');
var game_canvas = document.getElementById('canvas');
var innerWidth = window.innerWidth;
var innerHeight = window.innerHeight - buttonHeight;
var width = 640;
var height = 1136;
var targetRatio = width / height;
var actualRatio = innerWidth / innerHeight;
//Downscale fit
if (innerWidth < width || innerHeight < height) {
if (actualRatio > targetRatio) {
width = innerHeight * targetRatio;
height = innerHeight;
app_container.style.marginLeft = ((innerWidth - width) / 2) + "px";
app_container.style.marginTop = "0px";
}
else {
width = innerWidth;
height = innerWidth / targetRatio;
app_container.style.marginLeft = "0px";
app_container.style.marginTop = ((innerHeight - height) / 2) + "px";
}
}
else {
app_container.style.marginLeft = ((innerWidth - width) / 2) + "px";
app_container.style.marginTop = ((innerHeight - height) / 2) + "px";
}
app_container.style.width = width + "px";
app_container.style.height = height + buttonHeight + "px";
game_canvas.width = width;
game_canvas.height = height;
}
resize_game_canvas();
window.addEventListener('resize', resize_game_canvas, false);
window.addEventListener('orientationchange', resize_game_canvas, false);
function load_engine() {
var engineJS = document.createElement('script');
engineJS.type = 'text/javascript';
if (Module['isWASMSupported']) {
engineJS.src = 'DefoldWebsocket_wasm.js';
} else {
engineJS.src = 'DefoldWebsocket_asmjs.js';
}
document.head.appendChild(engineJS);
}
</script>
<script id='engine-start' type='text/javascript'>
load_engine();
</script>
</body>
</html>

View File

@@ -1 +1 @@
{"content":[{"name":"game.projectc","size":3195,"pieces":[{"name":"game.projectc0","offset":0}]},{"name":"game.arci","size":1488,"pieces":[{"name":"game.arci0","offset":0}]},{"name":"game.arcd","size":19614,"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}]}]} {"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}]}]}

View File

@@ -3,8 +3,6 @@ title = extension-websocket
version = 1.0 version = 1.0
write_log = 0 write_log = 0
compress_archive = 1 compress_archive = 1
publisher = unnamed
developer = unnamed
_dependencies = https://github.com/GameAnalytics/defold-openssl/archive/1.0.0.zip _dependencies = https://github.com/GameAnalytics/defold-openssl/archive/1.0.0.zip
[display] [display]
@@ -54,7 +52,6 @@ max_characters = 8192
max_debug_vertices = 10000 max_debug_vertices = 10000
texture_profiles = /builtins/graphics/default.texture_profiles texture_profiles = /builtins/graphics/default.texture_profiles
verify_graphics_calls = 1 verify_graphics_calls = 1
memory_size = 512
[shader] [shader]
output_spirv = 0 output_spirv = 0
@@ -153,8 +150,6 @@ auto_finish_transactions = 1
[network] [network]
http_timeout = 0 http_timeout = 0
http_thread_count = 4
http_cache_enabled = 1
[library] [library]
include_dirs = websocket include_dirs = websocket
@@ -171,7 +166,6 @@ track_cpu = 0
[liveupdate] [liveupdate]
settings = /liveupdate.settings settings = /liveupdate.settings
enabled = 1
[tilemap] [tilemap]
max_count = 16 max_count = 16

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -14,13 +14,21 @@ Here is how you connect to a websocket and listen to events:
```lua ```lua
local function websocket_callback(self, conn, data) local function websocket_callback(self, conn, data)
if data.event == websocket.EVENT_DISCONNECTED then if data.event == websocket.EVENT_DISCONNECTED then
print("disconnected " .. conn) print("Disconnected: " .. tostring(conn))
self.connection = nil self.connection = nil
update_gui(self)
elseif data.event == websocket.EVENT_CONNECTED then elseif data.event == websocket.EVENT_CONNECTED then
print("Connected " .. conn) update_gui(self)
-- self.connection = conn print("Connected: " .. tostring(conn))
elseif data.event == websocket.EVENT_ERROR then elseif data.event == websocket.EVENT_ERROR then
print("Error:", data.error) print("Error: '" .. tostring(data.message) .. "'")
if data.handshake_response then
print("Handshake response status: '" .. tostring(data.handshake_response.status) .. "'")
for key, value in pairs(data.handshake_response.headers) do
log("Handshake response header: '" .. key .. ": " .. value .. "'")
end
print("Handshake response body: '" .. tostring(data.handshake_response.response) .. "'")
end
elseif data.event == websocket.EVENT_MESSAGE then elseif data.event == websocket.EVENT_MESSAGE then
print("Receiving: '" .. tostring(data.message) .. "'") print("Receiving: '" .. tostring(data.message) .. "'")
end end
@@ -48,10 +56,19 @@ https://github.com/defold/extension-websocket/archive/master.zip
We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-websocket/releases). We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-websocket/releases).
## Configuration
The following configuration options can be set in the game.project file:
```
[websocket]
debug = 1
socket_timeout = 10000000
```
* `debug` - Log level where 0 means no logs and higher values shows more logs (currently 1 or 2).
* `socket_timeout` - Timeout for the underlying socket connection. In microseconds.
## Source code ## Source code
The source code is available on [GitHub](https://github.com/defold/extension-websocket) The source code is available on [GitHub](https://github.com/defold/extension-websocket)
## API reference
https://defold.com/extension-websocket/api/

View File

@@ -1,4 +1,4 @@
local URL="://echo.websocket.org" local URL = "echo.websocket.events"
local function click_button(node, action) local function click_button(node, action)
return gui.is_enabled(node) and action.pressed and gui.pick_node(node, action.x, action.y) return gui.is_enabled(node) and action.pressed and gui.pick_node(node, action.x, action.y)
@@ -57,14 +57,21 @@ end
local function websocket_callback(self, conn, data) local function websocket_callback(self, conn, data)
if data.event == websocket.EVENT_DISCONNECTED then if data.event == websocket.EVENT_DISCONNECTED then
log("Disconnected: " .. tostring(conn)) log("Disconnected: " .. tostring(conn) .. " Code: " .. data.code .. " Message: " .. tostring(data.message))
self.connection = nil self.connection = nil
update_gui(self) update_gui(self)
elseif data.event == websocket.EVENT_CONNECTED then elseif data.event == websocket.EVENT_CONNECTED then
update_gui(self) update_gui(self)
log("Connected: " .. tostring(conn)) log("Connected: " .. tostring(conn))
elseif data.event == websocket.EVENT_ERROR then elseif data.event == websocket.EVENT_ERROR then
log("Error: '" .. data.error .. "'") log("Error: '" .. tostring(data.message) .. "'")
if data.handshake_response then
log("Handshake response status: '" .. tostring(data.handshake_response.status) .. "'")
for key, value in pairs(data.handshake_response.headers) do
log("Handshake response header: '" .. key .. ": " .. value .. "'")
end
log("Handshake response body: '" .. tostring(data.handshake_response.response) .. "'")
end
elseif data.event == websocket.EVENT_MESSAGE then elseif data.event == websocket.EVENT_MESSAGE then
log("Receiving: '" .. tostring(data.message) .. "'") log("Receiving: '" .. tostring(data.message) .. "'")
end end
@@ -73,7 +80,7 @@ end
local function connect(self, scheme) local function connect(self, scheme)
local params = {} local params = {}
self.url = scheme .. URL self.url = scheme .. "://" .. URL
log("Connecting to " .. self.url) log("Connecting to " .. self.url)
self.connection = websocket.connect(self.url, params, websocket_callback) self.connection = websocket.connect(self.url, params, websocket_callback)
end end

View File

@@ -14,4 +14,3 @@ _dependencies = https://github.com/GameAnalytics/defold-openssl/archive/1.0.0.zi
[library] [library]
include_dirs = websocket include_dirs = websocket

View File

@@ -17,6 +17,17 @@
type: table type: table
desc: optional parameters as properties. The following parameters can be set desc: optional parameters as properties. The following parameters can be set
members: members:
- name: timeout
type: number
desc: Timeout for the connection sequence (milliseconds). Not used on HTML5. (Default is 3000)
- name: protocol
type: string
desc: the protocol to use (e.g. 'chat'). If not set, no `Sec-WebSocket-Protocol` header is sent.
- name: headers
type: string
desc: list of http headers. Each pair is separated with "\r\n". Not used on HTML5.
- name: callback - name: callback
type: function type: function
@@ -48,11 +59,15 @@
- name: message - name: message
type: string type: string
desc: The received data. Only valid if event is `websocket.EVENT_MESSAGE` desc: The received data if event is `websocket.EVENT_MESSAGE`. Error message otherwise
- name: error - name: handshake_response
type: string type: table
desc: The error string. Only valid if event is `websocket.EVENT_ERROR` desc: Handshake response information (status, headers etc)
- name: code
type: number
desc: Status code received from the server if the server closed the connection. Only present if event is `EVENT_DISCONNECTED`.
returns: returns:
@@ -65,21 +80,25 @@
```lua ```lua
local function websocket_callback(self, conn, data) local function websocket_callback(self, conn, data)
if data.event == websocket.EVENT_DISCONNECTED then if data.event == websocket.EVENT_DISCONNECTED then
print("disconnected " .. conn) log("Disconnected: " .. tostring(conn))
self.connection = nil self.connection = nil
update_gui(self)
elseif data.event == websocket.EVENT_CONNECTED then elseif data.event == websocket.EVENT_CONNECTED then
print("Connected " .. conn) update_gui(self)
-- self.connection = conn log("Connected: " .. tostring(conn))
elseif data.event == websocket.EVENT_ERROR then elseif data.event == websocket.EVENT_ERROR then
print("Error:", data.error) log("Error: '" .. data.message .. "'")
elseif data.event == websocket.EVENT_MESSAGE then elseif data.event == websocket.EVENT_MESSAGE then
print("Receiving: '" .. tostring(data.message) .. "'") log("Receiving: '" .. tostring(data.message) .. "'")
end end
end end
function init(self) function init(self)
self.url = "ws://echo.websocket.org" self.url = "ws://echo.websocket.events"
local params = {} local params = {
timeout = 3000,
headers = "Sec-WebSocket-Protocol: chat\r\nOrigin: mydomain.com\r\n"
}
self.connection = websocket.connect(self.url, params, websocket_callback) self.connection = websocket.connect(self.url, params, websocket_callback)
end end
@@ -100,6 +119,46 @@
type: object type: object
desc: the websocket connection desc: the websocket connection
#*****************************************************************************************************
- name: send
type: function
desc: Send data on a websocket
parameters:
- name: connection
type: object
desc: the websocket connection
- name: message
type: string
desc: the message to send
- name: options
type: table
desc: options for this particular message. May be `nil`
members:
- name: type
type: number
desc: The data type of the message
- `websocket.DATA_TYPE_BINARY` (default)
- `websocket.DATA_TYPE_TEXT`
examples:
- desc: |-
```lua
local function websocket_callback(self, conn, data)
if data.event == websocket.EVENT_CONNECTED then
websocket.send(conn, "Hello from the other side")
end
end
function init(self)
self.url = "ws://echo.websocket.org"
local params = {}
self.connection = websocket.connect(self.url, params, websocket_callback)
end
```
#***************************************************************************************************** #*****************************************************************************************************
- name: EVENT_CONNECTED - name: EVENT_CONNECTED

View File

@@ -6,3 +6,7 @@ platforms:
context: context:
includes: ["upload/websocket/include/wslay"] includes: ["upload/websocket/include/wslay"]
defines: ["HAVE_CONFIG_H"] defines: ["HAVE_CONFIG_H"]
web:
context:
linkFlags: ["-lwebsocket.js"]

View File

@@ -2,8 +2,12 @@
#ifndef CONFIG_H #ifndef CONFIG_H
#define CONFIG_H #define CONFIG_H
#if defined(_WIN32)
#define HAVE_WINSOCK2_H
#else
#define HAVE_ARPA_INET_H #define HAVE_ARPA_INET_H
#define HAVE_NETINET_IN_H #define HAVE_NETINET_IN_H
#endif
/* #undef HAVE_WINSOCK2_H */ /* #undef HAVE_WINSOCK2_H */
/* #undef WORDS_BIGENDIAN */ /* #undef WORDS_BIGENDIAN */

View File

@@ -33,6 +33,14 @@ extern "C" {
#include <stdlib.h> #include <stdlib.h>
#include <sys/types.h> #include <sys/types.h>
// Defold addition
#if defined(_WIN32)
#if defined(_WIN64)
typedef int64_t ssize_t;
#else
typedef int32_t ssize_t;
#endif
#endif
/* /*
* wslay/wslayver.h is generated from wslay/wslayver.h.in by * wslay/wslayver.h is generated from wslay/wslayver.h.in by

View File

@@ -0,0 +1,42 @@
#include "websocket.h"
#if defined(__EMSCRIPTEN__)
namespace dmWebsocket
{
EM_BOOL Emscripten_WebSocketOnOpen(int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData) {
DebugLog(1, "WebSocket OnOpen");
WebsocketConnection* conn = (WebsocketConnection*)userData;
SetState(conn, STATE_CONNECTED);
HandleCallback(conn, EVENT_CONNECTED, 0, 0);
return EM_TRUE;
}
EM_BOOL Emscripten_WebSocketOnError(int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData) {
DebugLog(1, "WebSocket OnError");
WebsocketConnection* conn = (WebsocketConnection*)userData;
conn->m_Status = RESULT_ERROR;
SetState(conn, STATE_DISCONNECTED);
return EM_TRUE;
}
EM_BOOL Emscripten_WebSocketOnClose(int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData) {
DebugLog(1, "WebSocket OnClose");
WebsocketConnection* conn = (WebsocketConnection*)userData;
int length = strlen(websocketEvent->reason);
PushMessage(conn, MESSAGE_TYPE_CLOSE, length, (uint8_t*)websocketEvent->reason, websocketEvent->code);
return EM_TRUE;
}
EM_BOOL Emscripten_WebSocketOnMessage(int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData) {
DebugLog(1, "WebSocket OnMessage");
WebsocketConnection* conn = (WebsocketConnection*)userData;
int length = websocketEvent->numBytes;
if (websocketEvent->isText)
{
length--;
}
PushMessage(conn, MESSAGE_TYPE_NORMAL, length, websocketEvent->data, 0);
return EM_TRUE;
}
} // namespace
#endif // __EMSCRIPTEN__

View File

@@ -1,5 +1,7 @@
#include "websocket.h" #include "websocket.h"
#include <dmsdk/dlib/socket.h> #include <dmsdk/dlib/socket.h>
#include <dmsdk/dlib/http_client.h>
#include <ctype.h> // tolower
namespace dmWebsocket namespace dmWebsocket
{ {
@@ -39,8 +41,12 @@ static Result SendClientHandshakeHeaders(WebsocketConnection* conn)
dmSnPrintf(port, sizeof(port), ":%d", conn->m_Url.m_Port); dmSnPrintf(port, sizeof(port), ":%d", conn->m_Url.m_Port);
dmSocket::Result sr; dmSocket::Result sr;
WS_SENDALL("GET /"); WS_SENDALL("GET ");
if (conn->m_Url.m_Path[0] == '\0') {
WS_SENDALL("/"); // Default to / for empty path
} else {
WS_SENDALL(conn->m_Url.m_Path); WS_SENDALL(conn->m_Url.m_Path);
}
WS_SENDALL(" HTTP/1.1\r\n"); WS_SENDALL(" HTTP/1.1\r\n");
WS_SENDALL("Host: "); WS_SENDALL("Host: ");
WS_SENDALL(conn->m_Url.m_Hostname); WS_SENDALL(conn->m_Url.m_Hostname);
@@ -53,9 +59,23 @@ static Result SendClientHandshakeHeaders(WebsocketConnection* conn)
WS_SENDALL("\r\n"); WS_SENDALL("\r\n");
WS_SENDALL("Sec-WebSocket-Version: 13\r\n"); WS_SENDALL("Sec-WebSocket-Version: 13\r\n");
// Add custom protocols if (conn->m_CustomHeaders)
{
WS_SENDALL(conn->m_CustomHeaders);
// make sure we ended with '\r\n'
int len = strlen(conn->m_CustomHeaders);
bool ended_with_sentinel = len >= 2 && conn->m_CustomHeaders[len - 2] == '\r' && conn->m_CustomHeaders[len - 1] == '\n';
if (!ended_with_sentinel)
{
WS_SENDALL("\r\n");
}
}
// Add custom headers if (conn->m_Protocol) {
WS_SENDALL("Sec-WebSocket-Protocol: ");
WS_SENDALL(conn->m_Protocol);
WS_SENDALL("\r\n");
}
WS_SENDALL("\r\n"); WS_SENDALL("\r\n");
@@ -100,17 +120,12 @@ Result ReceiveHeaders(WebsocketConnection* conn)
#else #else
Result ReceiveHeaders(WebsocketConnection* conn) Result ReceiveHeaders(WebsocketConnection* conn)
{ {
dmSocket::Selector selector; dmSocket::Result sr = WaitForSocket(conn, dmSocket::SELECTOR_KIND_READ, SOCKET_WAIT_TIMEOUT);
dmSocket::SelectorZero(&selector);
dmSocket::SelectorSet(&selector, dmSocket::SELECTOR_KIND_READ, conn->m_Socket);
dmSocket::Result sr = dmSocket::Select(&selector, 200*1000);
if (dmSocket::RESULT_OK != sr) if (dmSocket::RESULT_OK != sr)
{ {
if (dmSocket::RESULT_WOULDBLOCK) if (dmSocket::RESULT_WOULDBLOCK == sr)
{ {
dmLogWarning("Waiting for socket to be available for reading"); DebugLog(2, "Waiting for socket to be available for reading");
return RESULT_WOULDBLOCK; return RESULT_WOULDBLOCK;
} }
@@ -146,7 +161,8 @@ Result ReceiveHeaders(WebsocketConnection* conn)
conn->m_Buffer[conn->m_BufferSize] = '\0'; conn->m_Buffer[conn->m_BufferSize] = '\0';
// Check if the end of the response has arrived // Check if the end of the response has arrived
if (conn->m_BufferSize >= 4 && strcmp(conn->m_Buffer + conn->m_BufferSize - 4, "\r\n\r\n") == 0) const char* endtag = strstr(conn->m_Buffer, "\r\n\r\n");
if (endtag != 0)
{ {
return RESULT_OK; return RESULT_OK;
} }
@@ -155,6 +171,65 @@ Result ReceiveHeaders(WebsocketConnection* conn)
} }
#endif #endif
static void HandleVersion(void* user_data, int major, int minor, int status, const char* status_str)
{
HandshakeResponse* response = (HandshakeResponse*)user_data;
response->m_HttpMajor = major;
response->m_HttpMinor = minor;
response->m_ResponseStatusCode = status;
}
static void HandleHeader(void* user_data, const char* key, const char* value)
{
HandshakeResponse* response = (HandshakeResponse*)user_data;
if (response->m_Headers.Remaining() == 0)
{
response->m_Headers.OffsetCapacity(4);
}
HttpHeader* new_header = new HttpHeader(key, value);
response->m_Headers.Push(new_header);
}
static void HandleContent(void* user_data, int offset)
{
HandshakeResponse* response = (HandshakeResponse*)user_data;
response->m_BodyOffset = offset;
}
bool ValidateSecretKey(WebsocketConnection* conn, const char* server_key)
{
uint8_t client_key[32 + 40];
uint32_t client_key_len = sizeof(client_key);
dmCrypt::Base64Encode(conn->m_Key, sizeof(conn->m_Key), client_key, &client_key_len);
client_key[client_key_len] = 0;
DebugLog(2, "Secret key (base64): %s", client_key);
memcpy(client_key + client_key_len, RFC_MAGIC, strlen(RFC_MAGIC));
client_key_len += strlen(RFC_MAGIC);
client_key[client_key_len] = 0;
DebugLog(2, "Secret key + RFC_MAGIC: %s", client_key);
uint8_t client_key_sha1[20];
dmCrypt::HashSha1(client_key, client_key_len, client_key_sha1);
DebugPrint(2, "Hashed key (sha1):", client_key_sha1, sizeof(client_key_sha1));
client_key_len = sizeof(client_key);
dmCrypt::Base64Encode(client_key_sha1, sizeof(client_key_sha1), client_key, &client_key_len);
client_key[client_key_len] = 0;
DebugLog(2, "Client key (base64): %s", client_key);
DebugLog(2, "Server key (base64): %s", server_key);
return strcmp(server_key, (const char*)client_key) == 0;
}
#if defined(__EMSCRIPTEN__) #if defined(__EMSCRIPTEN__)
Result VerifyHeaders(WebsocketConnection* conn) Result VerifyHeaders(WebsocketConnection* conn)
{ {
@@ -165,74 +240,54 @@ Result VerifyHeaders(WebsocketConnection* conn)
{ {
char* r = conn->m_Buffer; char* r = conn->m_Buffer;
// According to protocol, the response should start with "HTTP/1.1 <statuscode> <message>" // Find start of payload now because dmHttpClient::ParseHeader is destructive
const char* http_version_and_status_protocol = "HTTP/1.1 101"; const char* start_of_payload = strstr(conn->m_Buffer, "\r\n\r\n");
if (strstr(r, http_version_and_status_protocol) != r) { start_of_payload += 4;
return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Missing: '%s' in header", http_version_and_status_protocol);
}
r = strstr(r, "\r\n") + 2; HandshakeResponse* response = new HandshakeResponse();
conn->m_HandshakeResponse = response;
bool upgraded = false; dmHttpClient::ParseResult parse_result = dmHttpClient::ParseHeader(r, response, true, &HandleVersion, &HandleHeader, &HandleContent);
bool valid_key = false; if (parse_result != dmHttpClient::ParseResult::PARSE_RESULT_OK)
const char* protocol = "";
// TODO: Perhaps also support the Sec-WebSocket-Protocol
// parse the headers in place
while (r)
{ {
// Tokenize the each header line: "Key: Value\r\n" return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Failed to parse handshake response. 'dmHttpClient::ParseResult=%i'", parse_result);
const char* key = r;
r = strchr(r, ':');
*r = 0;
++r;
const char* value = r;
while(*value == ' ')
++value;
r = strstr(r, "\r\n");
*r = 0;
r += 2;
if (strcmp(key, "Connection") == 0 && strcmp(value, "Upgrade") == 0)
upgraded = true;
else if (strcmp(key, "Sec-WebSocket-Accept") == 0)
{
uint8_t client_key[32 + 40];
uint32_t client_key_len = sizeof(client_key);
dmCrypt::Base64Encode(conn->m_Key, sizeof(conn->m_Key), client_key, &client_key_len);
client_key[client_key_len] = 0;
memcpy(client_key + client_key_len, RFC_MAGIC, strlen(RFC_MAGIC));
client_key_len += strlen(RFC_MAGIC);
client_key[client_key_len] = 0;
uint8_t client_key_sha1[20];
dmCrypt::HashSha1(client_key, client_key_len, client_key_sha1);
client_key_len = sizeof(client_key);
dmCrypt::Base64Encode(client_key_sha1, sizeof(client_key_sha1), client_key, &client_key_len);
client_key[client_key_len] = 0;
if (strcmp(value, (const char*)client_key) == 0)
valid_key = true;
} }
if (strcmp(r, "\r\n") == 0) if (response->m_ResponseStatusCode != 101) {
break; return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Wrong response status: %i", response->m_ResponseStatusCode);
} }
if (!upgraded) HttpHeader *connection_header, *upgrade_header, *websocket_secret_header;
connection_header = response->GetHeader("Connection");
upgrade_header = response->GetHeader("Upgrade");
websocket_secret_header = response->GetHeader("Sec-WebSocket-Accept");
bool connection = connection_header && dmStrCaseCmp(connection_header->m_Value, "Upgrade") == 0;
bool upgrade = upgrade_header && dmStrCaseCmp(upgrade_header->m_Value, "websocket") == 0;
bool valid_key = websocket_secret_header && ValidateSecretKey(conn, websocket_secret_header->m_Value);
// Send error to lua?
if (!connection)
dmLogError("Failed to find the Connection keyword in the response headers");
if (!upgrade)
dmLogError("Failed to find the Upgrade keyword in the response headers"); dmLogError("Failed to find the Upgrade keyword in the response headers");
if (!valid_key) if (!valid_key)
dmLogError("Failed to find valid key in the response headers"); dmLogError("Failed to find valid key in the response headers");
if (!(upgraded && valid_key)) { bool ok = connection && upgrade && valid_key;
dmLogError("Response:\n\"%s\"\n", conn->m_Buffer); if(!ok)
{
return RESULT_HANDSHAKE_FAILED;
} }
return (upgraded && valid_key) ? RESULT_OK : RESULT_HANDSHAKE_FAILED; delete conn->m_HandshakeResponse;
conn->m_HandshakeResponse = 0;
// The response might contain both the headers, but also (if successful) the first batch of data
uint32_t size = conn->m_BufferSize - (start_of_payload - conn->m_Buffer);
conn->m_BufferSize = size;
memmove(conn->m_Buffer, start_of_payload, size);
conn->m_Buffer[size] = 0;
conn->m_HasHandshakeData = conn->m_BufferSize != 0 ? 1 : 0;
return RESULT_OK;
} }
#endif #endif

View File

@@ -1,8 +1,8 @@
#include "script_util.h" #include "script_util.h"
namespace dmWebsocket { namespace dmScript {
bool luaL_checkbool(lua_State *L, int numArg) bool CheckBool(lua_State *L, int numArg)
{ {
bool b = false; bool b = false;
if (lua_isboolean(L, numArg)) if (lua_isboolean(L, numArg))
@@ -16,17 +16,17 @@ bool luaL_checkbool(lua_State *L, int numArg)
return b; return b;
} }
bool luaL_checkboold(lua_State *L, int numArg, int def) bool CheckBoold(lua_State *L, int numArg, int def)
{ {
int type = lua_type(L, numArg); int type = lua_type(L, numArg);
if (type != LUA_TNONE && type != LUA_TNIL) if (type != LUA_TNONE && type != LUA_TNIL)
{ {
return luaL_checkbool(L, numArg); return CheckBool(L, numArg);
} }
return def; return def;
} }
lua_Number luaL_checknumberd(lua_State *L, int numArg, lua_Number def) lua_Number CheckNumberd(lua_State *L, int numArg, lua_Number def)
{ {
int type = lua_type(L, numArg); int type = lua_type(L, numArg);
if (type != LUA_TNONE && type != LUA_TNIL) if (type != LUA_TNONE && type != LUA_TNIL)
@@ -36,7 +36,7 @@ lua_Number luaL_checknumberd(lua_State *L, int numArg, lua_Number def)
return def; return def;
} }
char* luaL_checkstringd(lua_State *L, int numArg, const char* def) char* CheckStringd(lua_State *L, int numArg, const char* def)
{ {
int type = lua_type(L, numArg); int type = lua_type(L, numArg);
if (type != LUA_TNONE && type != LUA_TNIL) if (type != LUA_TNONE && type != LUA_TNIL)
@@ -46,7 +46,7 @@ char* luaL_checkstringd(lua_State *L, int numArg, const char* def)
return (char*)def; return (char*)def;
} }
lua_Number luaL_checktable_number(lua_State *L, int numArg, const char* field, lua_Number def) lua_Number CheckTableNumber(lua_State *L, int numArg, const char* field, lua_Number def)
{ {
lua_Number result = def; lua_Number result = def;
if(lua_istable(L, numArg)) if(lua_istable(L, numArg))
@@ -61,7 +61,7 @@ lua_Number luaL_checktable_number(lua_State *L, int numArg, const char* field, l
return result; return result;
} }
char* luaL_checktable_string(lua_State *L, int numArg, const char* field, char* def) char* CheckTableString(lua_State *L, int numArg, const char* field, char* def)
{ {
char* result = def; char* result = def;
if(lua_istable(L, numArg)) if(lua_istable(L, numArg))

View File

@@ -2,11 +2,11 @@
#include <dmsdk/sdk.h> #include <dmsdk/sdk.h>
namespace dmWebsocket { namespace dmScript {
bool luaL_checkbool(lua_State *L, int numArg); bool CheckBool(lua_State *L, int numArg);
bool luaL_checkboold(lua_State *L, int numArg, int def); bool CheckBoold(lua_State *L, int numArg, int def);
lua_Number luaL_checknumberd(lua_State *L, int numArg, lua_Number def); lua_Number CheckNumberd(lua_State *L, int numArg, lua_Number def);
char* luaL_checkstringd(lua_State *L, int numArg, const char* def); char* CheckStringd(lua_State *L, int numArg, const char* def);
lua_Number luaL_checktable_number(lua_State *L, int numArg, const char* field, lua_Number def); lua_Number CheckTableNumber(lua_State *L, int numArg, const char* field, lua_Number def);
char* luaL_checktable_string(lua_State *L, int numArg, const char* field, char* def); char* CheckTableString(lua_State *L, int numArg, const char* field, char* def);
} // namespace } // namespace

View File

@@ -42,15 +42,24 @@ dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length,
} }
if (out_sent_bytes) if (out_sent_bytes)
*out_sent_bytes = total_sent_bytes; *out_sent_bytes = total_sent_bytes;
DebugPrint(2, "Sent buffer:", buffer, length);
return dmSocket::RESULT_OK; return dmSocket::RESULT_OK;
} }
dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes) dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes)
{ {
dmSocket::Result sr;
if (conn->m_SSLSocket) if (conn->m_SSLSocket)
return dmSSLSocket::Receive(conn->m_SSLSocket, buffer, length, received_bytes); sr = dmSSLSocket::Receive(conn->m_SSLSocket, buffer, length, received_bytes);
else else
return dmSocket::Receive(conn->m_Socket, buffer, length, received_bytes); sr = dmSocket::Receive(conn->m_Socket, buffer, length, received_bytes);
int num_bytes = received_bytes ? (uint32_t)*received_bytes : 0;
if (sr == dmSocket::RESULT_OK && num_bytes > 0)
DebugPrint(2, "Received bytes:", buffer, num_bytes);
return sr;
} }
} // namespace } // namespace

View File

@@ -7,11 +7,23 @@
#include "websocket.h" #include "websocket.h"
#include "script_util.h" #include "script_util.h"
#include <dmsdk/dlib/connection_pool.h> #include <dmsdk/dlib/connection_pool.h>
#include <dmsdk/dlib/dns.h> #include <dmsdk/dlib/thread.h>
#include <dmsdk/dlib/sslsocket.h> #include <dmsdk/dlib/sslsocket.h>
#include <ctype.h> // isprint et al
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h> // for EM_ASM
#include <emscripten/websocket.h>
#endif
#if defined(WIN32)
#include <malloc.h>
#define alloca _alloca
#endif
namespace dmWebsocket { namespace dmWebsocket {
int g_DebugWebSocket = 0;
struct WebsocketContext struct WebsocketContext
{ {
@@ -19,14 +31,10 @@ struct WebsocketContext
int m_Timeout; int m_Timeout;
dmArray<WebsocketConnection*> m_Connections; dmArray<WebsocketConnection*> m_Connections;
dmConnectionPool::HPool m_Pool; dmConnectionPool::HPool m_Pool;
dmDNS::HChannel m_Channel;
uint32_t m_Initialized:1; uint32_t m_Initialized:1;
} g_Websocket; } g_Websocket;
static void HandleCallback(WebsocketConnection* conn, int event);
#define STRING_CASE(_X) case _X: return #_X; #define STRING_CASE(_X) case _X: return #_X;
const char* ResultToString(Result err) const char* ResultToString(Result err)
@@ -45,10 +53,12 @@ const char* ResultToString(Result err)
const char* StateToString(State err) const char* StateToString(State err)
{ {
switch(err) { switch(err) {
STRING_CASE(STATE_CREATE);
STRING_CASE(STATE_CONNECTING); STRING_CASE(STATE_CONNECTING);
STRING_CASE(STATE_HANDSHAKE_WRITE); STRING_CASE(STATE_HANDSHAKE_WRITE);
STRING_CASE(STATE_HANDSHAKE_READ); STRING_CASE(STATE_HANDSHAKE_READ);
STRING_CASE(STATE_CONNECTED); STRING_CASE(STATE_CONNECTED);
STRING_CASE(STATE_DISCONNECTING);
STRING_CASE(STATE_DISCONNECTED); STRING_CASE(STATE_DISCONNECTED);
default: return "Unknown error"; default: return "Unknown error";
}; };
@@ -56,21 +66,85 @@ const char* StateToString(State err)
#undef STRING_CASE #undef STRING_CASE
#define WS_DEBUG(...) void DebugLog(int level, const char* fmt, ...)
//#define WS_DEBUG(...) dmLogWarning(__VA_ARGS__); {
if (level > g_DebugWebSocket)
return;
size_t buffer_size = 4096;
char* buffer = (char*)alloca(buffer_size);
va_list lst;
va_start(lst, fmt);
buffer_size = vsnprintf(buffer, buffer_size, fmt, lst);
dmLogWarning("%s", buffer);
va_end(lst);
}
void DebugPrint(int level, const char* msg, const void* _bytes, uint32_t num_bytes)
{
if (level > g_DebugWebSocket)
return;
char buffer[1024];
char submessage[128];
uint32_t sublength = 0;
uint32_t len = dmSnPrintf(buffer, sizeof(buffer), "%s '", msg);
const uint8_t* bytes = (const uint8_t*)_bytes;
for (uint32_t i = 0; i < num_bytes; ++i)
{
if (len + 2 >= sizeof(buffer))
{
dmLogWarning("%s", buffer);
buffer[0] = 0;
len = 0;
}
sublength = 0;
int c = bytes[i];
if (isprint(c))
sublength = dmSnPrintf(submessage, sizeof(submessage), "%c", c);
else if (c == '\r')
sublength = dmSnPrintf(submessage, sizeof(submessage), "\\r");
else if (c == '\n')
sublength = dmSnPrintf(submessage, sizeof(submessage), "\\n");
else if (c == '\t')
sublength = dmSnPrintf(submessage, sizeof(submessage), "\\t");
else
sublength = dmSnPrintf(submessage, sizeof(submessage), "\\%02x", c);
dmStrlCat(buffer, submessage, sizeof(buffer));
len += sublength;
}
if (len + 2 >= sizeof(buffer))
{
dmLogWarning("%s", buffer);
buffer[0] = 0;
len = 0;
}
sublength = dmSnPrintf(submessage, sizeof(submessage), "' %u bytes", num_bytes);
dmStrlCat(buffer, submessage, sizeof(buffer));
len += sublength;
dmLogWarning("%s", buffer);
}
#define CLOSE_CONN(...) \ #define CLOSE_CONN(...) \
SetStatus(conn, RESULT_ERROR, __VA_ARGS__); \ SetStatus(conn, RESULT_ERROR, __VA_ARGS__); \
CloseConnection(conn); CloseConnection(conn);
static void SetState(WebsocketConnection* conn, State state) void SetState(WebsocketConnection* conn, State state)
{ {
State prev_state = conn->m_State; State prev_state = conn->m_State;
if (prev_state != state) if (prev_state != state)
{ {
conn->m_State = state; conn->m_State = state;
WS_DEBUG("%s -> %s", StateToString(prev_state), StateToString(conn->m_State)); DebugLog(1, "%s -> %s", StateToString(prev_state), StateToString(conn->m_State));
} }
} }
@@ -85,6 +159,8 @@ Result SetStatus(WebsocketConnection* conn, Result status, const char* format, .
conn->m_BufferSize = vsnprintf(conn->m_Buffer, conn->m_BufferCapacity, format, lst); conn->m_BufferSize = vsnprintf(conn->m_Buffer, conn->m_BufferCapacity, format, lst);
va_end(lst); va_end(lst);
conn->m_Status = status; conn->m_Status = status;
DebugLog(1, "STATUS: '%s' len: %u", conn->m_Buffer, conn->m_BufferSize);
} }
return status; return status;
} }
@@ -92,23 +168,38 @@ Result SetStatus(WebsocketConnection* conn, Result status, const char* format, .
// *************************************************************************************************** // ***************************************************************************************************
// LUA functions // LUA functions
static WebsocketConnection* CreateConnection(const char* url) static WebsocketConnection* CreateConnection(const char* url)
{ {
WebsocketConnection* conn = (WebsocketConnection*)malloc(sizeof(WebsocketConnection)); WebsocketConnection* conn = new WebsocketConnection;
memset(conn, 0, sizeof(WebsocketConnection));
conn->m_BufferCapacity = g_Websocket.m_BufferSize; conn->m_BufferCapacity = g_Websocket.m_BufferSize;
conn->m_Buffer = (char*)malloc(conn->m_BufferCapacity); conn->m_Buffer = (char*)malloc(conn->m_BufferCapacity);
conn->m_Buffer[0] = 0;
conn->m_BufferSize = 0;
conn->m_ConnectTimeout = 0;
dmURI::Parts uri;
dmURI::Parse(url, &conn->m_Url); dmURI::Parse(url, &conn->m_Url);
if (strcmp(conn->m_Url.m_Scheme, "https") == 0) if (strcmp(conn->m_Url.m_Scheme, "https") == 0)
strcpy(conn->m_Url.m_Scheme, "wss"); strcpy(conn->m_Url.m_Scheme, "wss");
conn->m_SSL = strcmp(conn->m_Url.m_Scheme, "wss") == 0 ? 1 : 0; conn->m_SSL = strcmp(conn->m_Url.m_Scheme, "wss") == 0 ? 1 : 0;
conn->m_State = STATE_CONNECTING; conn->m_State = STATE_CREATE;
conn->m_Callback = 0;
conn->m_Connection = 0;
conn->m_Socket = 0;
conn->m_SSLSocket = 0;
conn->m_Status = RESULT_OK;
conn->m_HasHandshakeData = 0;
conn->m_HandshakeResponse = 0;
conn->m_ConnectionThread = 0;
#if defined(HAVE_WSLAY)
conn->m_Ctx = 0;
#endif
#if defined(__EMSCRIPTEN__)
conn->m_WS = 0;
#endif
return conn; return conn;
} }
@@ -120,39 +211,72 @@ static void DestroyConnection(WebsocketConnection* conn)
WSL_Exit(conn->m_Ctx); WSL_Exit(conn->m_Ctx);
#endif #endif
free((void*)conn->m_CustomHeaders);
free((void*)conn->m_Protocol);
if (conn->m_Callback) if (conn->m_Callback)
dmScript::DestroyCallback(conn->m_Callback); dmScript::DestroyCallback(conn->m_Callback);
#if defined(__EMSCRIPTEN__)
if (conn->m_WS)
{
emscripten_websocket_delete(conn->m_WS);
}
#else
if (conn->m_Connection) if (conn->m_Connection)
dmConnectionPool::Return(g_Websocket.m_Pool, conn->m_Connection); dmConnectionPool::Close(g_Websocket.m_Pool, conn->m_Connection);
#endif
if (conn->m_HandshakeResponse)
delete conn->m_HandshakeResponse;
free((void*)conn->m_Buffer); free((void*)conn->m_Buffer);
free((void*)conn);
if (conn->m_ConnectionThread)
{
dmThread::Join(conn->m_ConnectionThread);
conn->m_ConnectionThread = 0;
}
delete conn;
DebugLog(2, "DestroyConnection: %p", conn);
} }
static void CloseConnection(WebsocketConnection* conn) static void CloseConnection(WebsocketConnection* conn)
{ {
State prev_state = conn->m_State;
// we want it to send this message in the polling // we want it to send this message in the polling
if (conn->m_State == STATE_CONNECTED) { if (conn->m_State == STATE_CONNECTED) {
#if defined(HAVE_WSLAY) #if defined(HAVE_WSLAY)
WSL_Close(conn->m_Ctx); WSL_Close(conn->m_Ctx);
#else
// start disconnecting by closing the WebSocket through the JS API
// we transition to the DISCONNECTED state when we receive the
// Emscripten callback that the connection has closed
emscripten_websocket_close(conn->m_WS, 1000, "CloseConnection");
SetState(conn, STATE_DISCONNECTING);
#endif #endif
} }
#if defined(HAVE_WSLAY)
// close the connection and immediately transition to the DISCONNECTED
// state
SetState(conn, STATE_DISCONNECTED); SetState(conn, STATE_DISCONNECTED);
#endif
} }
static int FindConnection(WebsocketConnection* conn) static bool IsConnectionValid(WebsocketConnection* conn)
{
if (conn)
{ {
for (int i = 0; i < g_Websocket.m_Connections.Size(); ++i ) for (int i = 0; i < g_Websocket.m_Connections.Size(); ++i )
{ {
if (g_Websocket.m_Connections[i] == conn) if (g_Websocket.m_Connections[i] == conn)
return i; return true;
} }
return -1; }
return false;
} }
/*# /*#
@@ -166,13 +290,22 @@ static int LuaConnect(lua_State* L)
return DM_LUA_ERROR("The web socket module isn't initialized"); return DM_LUA_ERROR("The web socket module isn't initialized");
const char* url = luaL_checkstring(L, 1); const char* url = luaL_checkstring(L, 1);
lua_Number timeout = dmScript::CheckTableNumber(L, 2, "timeout", 3000);
const char* custom_headers = dmScript::CheckTableString(L, 2, "headers", 0);
const char* protocol = dmScript::CheckTableString(L, 2, "protocol", 0);
// long playedTime = luaL_checktable_number(L, 2, "playedTime", -1); if (custom_headers != 0)
// long progressValue = luaL_checktable_number(L, 2, "progressValue", -1); {
// char *description = luaL_checktable_string(L, 2, "description", NULL); if (strstr(custom_headers, "\r\n\r\n") != 0)
// char *coverImage = luaL_checktable_string(L, 2, "coverImage", NULL); {
return DM_LUA_ERROR("The header field must not contain double '\\r\\n\\r\\n': '%s'", custom_headers);
}
}
WebsocketConnection* conn = CreateConnection(url); WebsocketConnection* conn = CreateConnection(url);
conn->m_ConnectTimeout = dmTime::GetTime() + timeout * 1000;
conn->m_CustomHeaders = custom_headers ? strdup(custom_headers) : 0;
conn->m_Protocol = protocol ? strdup(protocol) : 0;
conn->m_Callback = dmScript::CreateCallback(L, 3); conn->m_Callback = dmScript::CreateCallback(L, 3);
@@ -196,8 +329,7 @@ static int LuaDisconnect(lua_State* L)
WebsocketConnection* conn = (WebsocketConnection*)lua_touserdata(L, 1); WebsocketConnection* conn = (WebsocketConnection*)lua_touserdata(L, 1);
int i = FindConnection(conn); if (IsConnectionValid(conn))
if (i != -1)
{ {
CloseConnection(conn); CloseConnection(conn);
} }
@@ -216,8 +348,7 @@ static int LuaSend(lua_State* L)
WebsocketConnection* conn = (WebsocketConnection*)lua_touserdata(L, 1); WebsocketConnection* conn = (WebsocketConnection*)lua_touserdata(L, 1);
int i = FindConnection(conn); if (!IsConnectionValid(conn))
if (i == -1)
return DM_LUA_ERROR("Invalid connection"); return DM_LUA_ERROR("Invalid connection");
if (conn->m_State != STATE_CONNECTED) if (conn->m_State != STATE_CONNECTED)
@@ -227,7 +358,7 @@ static int LuaSend(lua_State* L)
const char* string = luaL_checklstring(L, 2, &string_length); const char* string = luaL_checklstring(L, 2, &string_length);
#if defined(HAVE_WSLAY) #if defined(HAVE_WSLAY)
int write_mode = WSLAY_BINARY_FRAME; // WSLAY_TEXT_FRAME int write_mode = dmScript::CheckTableNumber(L, 3, "type", WSLAY_BINARY_FRAME);
struct wslay_event_msg msg; struct wslay_event_msg msg;
msg.opcode = write_mode; msg.opcode = write_mode;
@@ -236,9 +367,17 @@ static int LuaSend(lua_State* L)
wslay_event_queue_msg(conn->m_Ctx, &msg); // it makes a copy of the data wslay_event_queue_msg(conn->m_Ctx, &msg); // it makes a copy of the data
#else #else
EMSCRIPTEN_RESULT result;
dmSocket::Result sr = Send(conn, string, string_length, 0); int write_mode = dmScript::CheckTableNumber(L, 3, "type", DATA_TYPE_BINARY);
if (dmSocket::RESULT_OK != sr) if (write_mode == DATA_TYPE_BINARY)
{
result = emscripten_websocket_send_binary(conn->m_WS, (void*)string, string_length);
}
else
{
result = emscripten_websocket_send_utf8_text(conn->m_WS, string);
}
if (result)
{ {
CLOSE_CONN("Failed to send on websocket"); CLOSE_CONN("Failed to send on websocket");
} }
@@ -247,7 +386,7 @@ static int LuaSend(lua_State* L)
return 0; return 0;
} }
static void HandleCallback(WebsocketConnection* conn, int event) void HandleCallback(WebsocketConnection* conn, int event, int msg_offset, int msg_length)
{ {
if (!dmScript::IsCallbackValid(conn->m_Callback)) if (!dmScript::IsCallbackValid(conn->m_Callback))
return; return;
@@ -268,13 +407,39 @@ static void HandleCallback(WebsocketConnection* conn, int event)
lua_pushinteger(L, event); lua_pushinteger(L, event);
lua_setfield(L, -2, "event"); lua_setfield(L, -2, "event");
if (EVENT_ERROR == event) { lua_pushlstring(L, conn->m_Buffer + msg_offset, msg_length);
lua_pushlstring(L, conn->m_Buffer, conn->m_BufferSize);
lua_setfield(L, -2, "error");
}
else if (EVENT_MESSAGE == event) {
lua_pushlstring(L, conn->m_Buffer, conn->m_BufferSize);
lua_setfield(L, -2, "message"); lua_setfield(L, -2, "message");
if(conn->m_HandshakeResponse)
{
HandshakeResponse* response = conn->m_HandshakeResponse;
lua_newtable(L);
lua_pushnumber(L, response->m_ResponseStatusCode);
lua_setfield(L, -2, "status");
lua_pushstring(L, &conn->m_Buffer[response->m_BodyOffset]);
lua_setfield(L, -2, "response");
lua_newtable(L);
for (uint32_t i = 0; i < response->m_Headers.Size(); ++i)
{
lua_pushstring(L, response->m_Headers[i]->m_Value);
lua_setfield(L, -2, response->m_Headers[i]->m_Key);
}
lua_setfield(L, -2, "headers");
lua_setfield(L, -2, "handshake_response");
delete conn->m_HandshakeResponse;
conn->m_HandshakeResponse = 0;
}
if (event == EVENT_DISCONNECTED)
{
lua_pushinteger(L, conn->m_CloseCode);
lua_setfield(L, -2, "code");
} }
dmScript::PCall(L, 3, 0); dmScript::PCall(L, 3, 0);
@@ -283,6 +448,42 @@ static void HandleCallback(WebsocketConnection* conn, int event)
} }
HttpHeader::HttpHeader(const char* key, const char* value)
{
m_Key = strdup(key);
m_Value = strdup(value);
}
HttpHeader::~HttpHeader()
{
free((void*)m_Key);
free((void*)m_Value);
m_Key = 0;
m_Value = 0;
}
HttpHeader* HandshakeResponse::GetHeader(const char* header_key)
{
for(uint32_t i = 0; i < m_Headers.Size(); ++i)
{
if (dmStrCaseCmp(m_Headers[i]->m_Key, header_key) == 0)
{
return m_Headers[i];
}
}
return 0;
}
HandshakeResponse::~HandshakeResponse()
{
for(uint32_t i = 0; i < m_Headers.Size(); ++i)
{
delete m_Headers[i];
}
}
// *************************************************************************************************** // ***************************************************************************************************
// Life cycle functions // Life cycle functions
@@ -311,56 +512,49 @@ static void LuaInit(lua_State* L)
SETCONSTANT(EVENT_MESSAGE); SETCONSTANT(EVENT_MESSAGE);
SETCONSTANT(EVENT_ERROR); SETCONSTANT(EVENT_ERROR);
#if defined(HAVE_WSLAY)
lua_pushnumber(L, (lua_Number) WSLAY_BINARY_FRAME);
lua_setfield(L, -2, "DATA_TYPE_BINARY");
lua_pushnumber(L, (lua_Number) WSLAY_TEXT_FRAME);
lua_setfield(L, -2, "DATA_TYPE_TEXT");
#else
SETCONSTANT(DATA_TYPE_BINARY);
SETCONSTANT(DATA_TYPE_TEXT);
#endif
#undef SETCONSTANT #undef SETCONSTANT
lua_pop(L, 1); lua_pop(L, 1);
assert(top == lua_gettop(L)); assert(top == lua_gettop(L));
} }
static dmExtension::Result WebsocketAppInitialize(dmExtension::AppParams* params) static dmExtension::Result AppInitialize(dmExtension::AppParams* params)
{ {
g_Websocket.m_BufferSize = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.buffer_size", 64 * 1024); g_Websocket.m_BufferSize = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.buffer_size", 64 * 1024);
g_Websocket.m_Timeout = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.socket_timeout", 500 * 1000); g_Websocket.m_Timeout = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.socket_timeout", 500 * 1000);
g_Websocket.m_Connections.SetCapacity(4); g_Websocket.m_Connections.SetCapacity(4);
g_Websocket.m_Channel = 0;
g_Websocket.m_Pool = 0; g_Websocket.m_Pool = 0;
g_Websocket.m_Initialized = 0;
dmConnectionPool::Params pool_params; dmConnectionPool::Params pool_params;
pool_params.m_MaxConnections = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.max_connections", 2); pool_params.m_MaxConnections = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.max_connections", 2);
dmConnectionPool::Result result = dmConnectionPool::New(&pool_params, &g_Websocket.m_Pool); dmConnectionPool::Result result = dmConnectionPool::New(&pool_params, &g_Websocket.m_Pool);
g_DebugWebSocket = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.debug", 0);
if (g_DebugWebSocket)
dmLogInfo("dmWebSocket::g_DebugWebSocket == %d", g_DebugWebSocket);
if (dmConnectionPool::RESULT_OK != result) if (dmConnectionPool::RESULT_OK != result)
{ {
dmLogError("Failed to create connection pool: %d", result); dmLogError("Failed to create connection pool: %d", result);
return dmExtension::RESULT_INIT_ERROR;
} }
// We can do without the channel, it will then fallback to the dmSocket::GetHostname (as opposed to dmDNS::GetHostname)
#if defined(HAVE_WSLAY)
dmDNS::Result dns_result = dmDNS::NewChannel(&g_Websocket.m_Channel);
if (dmDNS::RESULT_OK != dns_result)
{
dmLogError("Failed to create connection pool: %d", dns_result);
}
#endif
g_Websocket.m_Initialized = 1; g_Websocket.m_Initialized = 1;
if (!g_Websocket.m_Pool)
{
if (!g_Websocket.m_Pool)
{
dmLogInfo("pool is null!");
dmConnectionPool::Delete(g_Websocket.m_Pool);
}
dmLogInfo("%s extension not initialized", MODULE_NAME);
g_Websocket.m_Initialized = 0;
}
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
static dmExtension::Result WebsocketInitialize(dmExtension::Params* params) static dmExtension::Result Initialize(dmExtension::Params* params)
{ {
if (!g_Websocket.m_Initialized) if (!g_Websocket.m_Initialized)
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
@@ -371,19 +565,72 @@ static dmExtension::Result WebsocketInitialize(dmExtension::Params* params)
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
static dmExtension::Result WebsocketAppFinalize(dmExtension::AppParams* params) static dmExtension::Result AppFinalize(dmExtension::AppParams* params)
{ {
dmConnectionPool::Shutdown(g_Websocket.m_Pool, dmSocket::SHUTDOWNTYPE_READWRITE); dmConnectionPool::Shutdown(g_Websocket.m_Pool, dmSocket::SHUTDOWNTYPE_READWRITE);
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
static dmExtension::Result WebsocketFinalize(dmExtension::Params* params) static dmExtension::Result Finalize(dmExtension::Params* params)
{ {
while (!g_Websocket.m_Connections.Empty())
{
WebsocketConnection* conn = g_Websocket.m_Connections.Back();
g_Websocket.m_Connections.Pop();
DestroyConnection(conn);
}
return dmExtension::RESULT_OK; return dmExtension::RESULT_OK;
} }
static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params) Result PushMessage(WebsocketConnection* conn, MessageType type, int length, const uint8_t* buffer, uint16_t code)
{
if (conn->m_Messages.Full())
conn->m_Messages.OffsetCapacity(4);
Message msg;
msg.m_Type = (uint32_t)type;
msg.m_Length = length;
msg.m_Code = code;
conn->m_Messages.Push(msg);
if ((conn->m_BufferSize + length) >= conn->m_BufferCapacity)
{
conn->m_BufferCapacity = conn->m_BufferSize + length + 1;
conn->m_Buffer = (char*)realloc(conn->m_Buffer, conn->m_BufferCapacity);
}
// append to the end of the buffer
memcpy(conn->m_Buffer + conn->m_BufferSize, buffer, length);
conn->m_BufferSize += length;
conn->m_Buffer[conn->m_BufferCapacity-1] = 0;
// Instead of printing from the incoming buffer, we print from our own, to make sure it looks ok
DebugPrint(2, __FUNCTION__, conn->m_Buffer+conn->m_BufferSize-length, length);
return dmWebsocket::RESULT_OK;
}
// has the connect procedure taken too long?
static bool CheckConnectTimeout(WebsocketConnection* conn)
{
uint64_t t = dmTime::GetTime();
return t >= conn->m_ConnectTimeout;
}
static void ConnectionWorker(void* _conn)
{
WebsocketConnection* conn = (WebsocketConnection*)_conn;
dmSocket::Result sr;
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);
if (dmConnectionPool::RESULT_OK != pool_result)
{
CLOSE_CONN("Failed to open connection: %s", dmSocket::ResultToString(sr));
return;
}
SetState(conn, STATE_HANDSHAKE_WRITE);
}
static dmExtension::Result OnUpdate(dmExtension::Params* params)
{ {
uint32_t size = g_Websocket.m_Connections.Size(); uint32_t size = g_Websocket.m_Connections.Size();
@@ -395,17 +642,18 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params)
{ {
if (RESULT_OK != conn->m_Status) if (RESULT_OK != conn->m_Status)
{ {
HandleCallback(conn, EVENT_ERROR); HandleCallback(conn, EVENT_ERROR, 0, conn->m_BufferSize);
conn->m_BufferSize = 0;
} }
HandleCallback(conn, EVENT_DISCONNECTED); HandleCallback(conn, EVENT_DISCONNECTED, 0, conn->m_BufferSize);
g_Websocket.m_Connections.EraseSwap(i); g_Websocket.m_Connections.EraseSwap(i);
--i; --i;
--size; --size;
DestroyConnection(conn); DestroyConnection(conn);
} }
else if (STATE_CONNECTED == conn->m_State) else if ((STATE_CONNECTED == conn->m_State) || (STATE_DISCONNECTING == conn->m_State))
{ {
#if defined(HAVE_WSLAY) #if defined(HAVE_WSLAY)
int r = WSL_Poll(conn->m_Ctx); int r = WSL_Poll(conn->m_Ctx);
@@ -414,42 +662,37 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params)
CLOSE_CONN("Websocket closing for %s (%s)", conn->m_Url.m_Hostname, WSL_ResultToString(r)); CLOSE_CONN("Websocket closing for %s (%s)", conn->m_Url.m_Hostname, WSL_ResultToString(r));
continue; continue;
} }
r = WSL_WantsExit(conn->m_Ctx);
if (0 != r)
{
CLOSE_CONN("Websocket received close event for %s", conn->m_Url.m_Hostname);
continue;
}
#else
int recv_bytes = 0;
dmSocket::Result sr = Receive(conn, conn->m_Buffer, conn->m_BufferCapacity-1, &recv_bytes);
if( sr == dmSocket::RESULT_WOULDBLOCK )
{
continue;
}
if (dmSocket::RESULT_OK == sr)
{
conn->m_BufferSize += recv_bytes;
conn->m_Buffer[conn->m_BufferCapacity-1] = 0;
conn->m_HasMessage = 1;
}
else
{
CLOSE_CONN("Websocket failed to receive data %s", dmSocket::ResultToString(sr));
continue;
}
#endif #endif
if (conn->m_HasMessage) uint32_t offset = 0;
for (uint32_t i = 0; i < conn->m_Messages.Size(); ++i)
{ {
HandleCallback(conn, EVENT_MESSAGE); const Message& msg = conn->m_Messages[i];
conn->m_HasMessage = 0;
conn->m_BufferSize = 0; if (EVENT_DISCONNECTED == msg.m_Type)
{
conn->m_Status = RESULT_OK;
// close the connection and immediately transition to the DISCONNECTED
// state
SetState(conn, STATE_DISCONNECTED);
conn->m_CloseCode = msg.m_Code;
break;
} }
HandleCallback(conn, EVENT_MESSAGE, offset, msg.m_Length);
offset += msg.m_Length;
}
conn->m_Messages.SetSize(0);
conn->m_BufferSize = 0;
} }
else if (STATE_HANDSHAKE_READ == conn->m_State) else if (STATE_HANDSHAKE_READ == conn->m_State)
{ {
if (CheckConnectTimeout(conn))
{
CLOSE_CONN("Connect sequence timed out");
continue;
}
Result result = ReceiveHeaders(conn); Result result = ReceiveHeaders(conn);
if (RESULT_WOULDBLOCK == result) if (RESULT_WOULDBLOCK == result)
{ {
@@ -462,6 +705,7 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params)
continue; continue;
} }
// Verifies headers, and also stages any initial sent data
result = VerifyHeaders(conn); result = VerifyHeaders(conn);
if (RESULT_OK != result) if (RESULT_OK != result)
{ {
@@ -482,17 +726,27 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params)
dmSocket::SetReceiveTimeout(conn->m_Socket, 1000); dmSocket::SetReceiveTimeout(conn->m_Socket, 1000);
if (conn->m_SSLSocket) if (conn->m_SSLSocket)
dmSSLSocket::SetReceiveTimeout(conn->m_SSLSocket, 1000); dmSSLSocket::SetReceiveTimeout(conn->m_SSLSocket, 1000);
#endif
dmSocket::SetBlocking(conn->m_Socket, false); dmSocket::SetBlocking(conn->m_Socket, false);
#endif
conn->m_Buffer[0] = 0;
conn->m_BufferSize = 0;
SetState(conn, STATE_CONNECTED); SetState(conn, STATE_CONNECTED);
HandleCallback(conn, EVENT_CONNECTED); HandleCallback(conn, EVENT_CONNECTED, 0, 0);
} }
else if (STATE_HANDSHAKE_WRITE == conn->m_State) else if (STATE_HANDSHAKE_WRITE == conn->m_State)
{ {
if (CheckConnectTimeout(conn))
{
CLOSE_CONN("Connect sequence timed out");
continue;
}
if (conn->m_ConnectionThread)
{
dmThread::Join(conn->m_ConnectionThread);
conn->m_ConnectionThread = 0;
}
conn->m_Socket = dmConnectionPool::GetSocket(g_Websocket.m_Pool, conn->m_Connection);
conn->m_SSLSocket = dmConnectionPool::GetSSLSocket(g_Websocket.m_Pool, conn->m_Connection);
Result result = SendClientHandshake(conn); Result result = SendClientHandshake(conn);
if (RESULT_WOULDBLOCK == result) if (RESULT_WOULDBLOCK == result)
{ {
@@ -506,23 +760,67 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params)
SetState(conn, STATE_HANDSHAKE_READ); SetState(conn, STATE_HANDSHAKE_READ);
} }
else if (STATE_CONNECTING == conn->m_State) else if (STATE_CREATE == conn->m_State)
{ {
dmSocket::Result socket_result; if (CheckConnectTimeout(conn))
int timeout = g_Websocket.m_Timeout;
#if defined(__EMSCRIPTEN__)
timeout = 0;
#endif
dmConnectionPool::Result pool_result = dmConnectionPool::Dial(g_Websocket.m_Pool, conn->m_Url.m_Hostname, conn->m_Url.m_Port, g_Websocket.m_Channel, conn->m_SSL, timeout, &conn->m_Connection, &socket_result);
if (dmConnectionPool::RESULT_OK != pool_result)
{ {
CLOSE_CONN("Failed to open connection: %s", dmSocket::ResultToString(socket_result)); CLOSE_CONN("Connect sequence timed out");
continue; continue;
} }
conn->m_Socket = dmConnectionPool::GetSocket(g_Websocket.m_Pool, conn->m_Connection); #if defined(__EMSCRIPTEN__)
conn->m_SSLSocket = dmConnectionPool::GetSSLSocket(g_Websocket.m_Pool, conn->m_Connection); char uri_buffer[dmURI::MAX_URI_LEN];
SetState(conn, STATE_HANDSHAKE_WRITE); const char* uri;
bool no_path = conn->m_Url.m_Path[0] == '\0';
bool no_port = conn->m_Url.m_Port == -1;
if (no_path && no_port)
{
dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname);
}
else if (no_port)
{
dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Path);
}
else if (no_path)
{
dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port);
}
else {
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);
}
uri = uri_buffer;
EmscriptenWebSocketCreateAttributes ws_attrs = {
uri,
conn->m_Protocol,
EM_TRUE
};
EMSCRIPTEN_WEBSOCKET_T ws = emscripten_websocket_new(&ws_attrs);
if (ws < 0)
{
CLOSE_CONN("Failed to connect to '%s:%d': %d", conn->m_Url.m_Hostname, (int)conn->m_Url.m_Port, ws);
continue;
}
conn->m_WS = ws;
emscripten_websocket_set_onopen_callback(ws, conn, Emscripten_WebSocketOnOpen);
emscripten_websocket_set_onerror_callback(ws, conn, Emscripten_WebSocketOnError);
emscripten_websocket_set_onclose_callback(ws, conn, Emscripten_WebSocketOnClose);
emscripten_websocket_set_onmessage_callback(ws, conn, Emscripten_WebSocketOnMessage);
#else
conn->m_ConnectionThread = dmThread::New((dmThread::ThreadStart)ConnectionWorker, 0x80000, conn, "WSConnect");
#endif
SetState(conn, STATE_CONNECTING);
}
else if (STATE_CONNECTING == conn->m_State)
{
#if defined(__EMSCRIPTEN__)
if (CheckConnectTimeout(conn))
{
CLOSE_CONN("Connect sequence timed out");
continue;
}
#endif
} }
} }
@@ -531,6 +829,6 @@ static dmExtension::Result WebsocketOnUpdate(dmExtension::Params* params)
} // dmWebsocket } // dmWebsocket
DM_DECLARE_EXTENSION(Websocket, LIB_NAME, dmWebsocket::WebsocketAppInitialize, dmWebsocket::WebsocketAppFinalize, dmWebsocket::WebsocketInitialize, dmWebsocket::WebsocketOnUpdate, 0, dmWebsocket::WebsocketFinalize) DM_DECLARE_EXTENSION(Websocket, LIB_NAME, dmWebsocket::AppInitialize, dmWebsocket::AppFinalize, dmWebsocket::Initialize, dmWebsocket::OnUpdate, 0, dmWebsocket::Finalize)
#undef CLOSE_CONN #undef CLOSE_CONN

View File

@@ -1,5 +1,9 @@
#pragma once #pragma once
#if defined(_WIN32)
#include <WinSock2.h>
#endif
// include the Defold SDK // include the Defold SDK
#include <dmsdk/sdk.h> #include <dmsdk/sdk.h>
@@ -11,10 +15,15 @@
#include <wslay/wslay.h> #include <wslay/wslay.h>
#endif #endif
#if defined(__EMSCRIPTEN__)
#include <emscripten/websocket.h>
#endif
#include <dmsdk/dlib/connection_pool.h> #include <dmsdk/dlib/connection_pool.h>
#include <dmsdk/dlib/socket.h> #include <dmsdk/dlib/socket.h>
#include <dmsdk/dlib/dns.h>
#include <dmsdk/dlib/uri.h> #include <dmsdk/dlib/uri.h>
#include <dmsdk/dlib/array.h>
#include <dmsdk/dlib/thread.h>
namespace dmCrypt namespace dmCrypt
{ {
@@ -30,10 +39,12 @@ namespace dmWebsocket
enum State enum State
{ {
STATE_CREATE,
STATE_CONNECTING, STATE_CONNECTING,
STATE_HANDSHAKE_WRITE, STATE_HANDSHAKE_WRITE,
STATE_HANDSHAKE_READ, STATE_HANDSHAKE_READ,
STATE_CONNECTED, STATE_CONNECTED,
STATE_DISCONNECTING,
STATE_DISCONNECTED, STATE_DISCONNECTED,
}; };
@@ -55,24 +66,75 @@ namespace dmWebsocket
EVENT_ERROR, EVENT_ERROR,
}; };
enum MessageType
{
MESSAGE_TYPE_NORMAL = 0,
MESSAGE_TYPE_CLOSE = 1,
};
enum DataType
{
DATA_TYPE_BINARY = 0,
DATA_TYPE_TEXT = 1,
};
struct Message
{
uint16_t m_Code;
uint32_t m_Length:30;
uint32_t m_Type:2;
};
struct HttpHeader
{
const char* m_Key;
const char* m_Value;
HttpHeader(const char* key, const char* value);
~HttpHeader();
};
struct HandshakeResponse
{
int m_HttpMajor;
int m_HttpMinor;
int m_ResponseStatusCode;
int m_BodyOffset;
dmArray<HttpHeader*> m_Headers;
~HandshakeResponse();
HttpHeader* GetHeader(const char* header);
};
struct WebsocketConnection struct WebsocketConnection
{ {
dmScript::LuaCallbackInfo* m_Callback; dmScript::LuaCallbackInfo* m_Callback;
#if defined(HAVE_WSLAY) #if defined(HAVE_WSLAY)
wslay_event_context_ptr m_Ctx; wslay_event_context_ptr m_Ctx;
#endif
#if defined(__EMSCRIPTEN__)
EMSCRIPTEN_WEBSOCKET_T m_WS;
#endif #endif
dmURI::Parts m_Url; dmURI::Parts m_Url;
dmConnectionPool::HConnection m_Connection; dmConnectionPool::HConnection m_Connection;
dmSocket::Socket m_Socket; dmSocket::Socket m_Socket;
dmSSLSocket::Socket m_SSLSocket; dmSSLSocket::Socket m_SSLSocket;
dmThread::Thread m_ConnectionThread;
dmArray<Message> m_Messages; // lengths of the messages in the data buffer
uint64_t m_ConnectTimeout;
uint8_t m_Key[16]; uint8_t m_Key[16];
const char* m_Protocol;
const char* m_CustomHeaders;
State m_State; State m_State;
uint32_t m_SSL:1;
uint32_t m_HasMessage:1;
char* m_Buffer; char* m_Buffer;
int m_BufferSize; int m_BufferSize;
uint32_t m_BufferCapacity; uint32_t m_BufferCapacity;
Result m_Status; Result m_Status;
uint16_t m_CloseCode;
uint8_t m_SSL:1;
uint8_t m_HasHandshakeData:1;
uint8_t :7;
HandshakeResponse* m_HandshakeResponse;
}; };
// Set error message // Set error message
@@ -82,6 +144,9 @@ namespace dmWebsocket
Result SetStatus(WebsocketConnection* conn, Result status, const char* fmt, ...); Result SetStatus(WebsocketConnection* conn, Result status, const char* fmt, ...);
#endif #endif
// Set socket state
void SetState(WebsocketConnection* conn, State state);
// Communication // Communication
dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length, int* out_sent_bytes); dmSocket::Result Send(WebsocketConnection* conn, const char* buffer, int length, int* out_sent_bytes);
dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes); dmSocket::Result Receive(WebsocketConnection* conn, void* buffer, int length, int* received_bytes);
@@ -92,33 +157,41 @@ namespace dmWebsocket
Result ReceiveHeaders(WebsocketConnection* conn); Result ReceiveHeaders(WebsocketConnection* conn);
Result VerifyHeaders(WebsocketConnection* conn); Result VerifyHeaders(WebsocketConnection* conn);
// Callback to Lua
void HandleCallback(WebsocketConnection* conn, int event, int msg_offset, int msg_length);
// Messages
Result PushMessage(WebsocketConnection* conn, MessageType type, int length, const uint8_t* msg, uint16_t code);
#if defined(HAVE_WSLAY) #if defined(HAVE_WSLAY)
// Wslay callbacks // Wslay callbacks
int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx); int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx);
void WSL_Exit(wslay_event_context_ptr ctx); void WSL_Exit(wslay_event_context_ptr ctx);
int WSL_Close(wslay_event_context_ptr ctx); int WSL_Close(wslay_event_context_ptr ctx);
int WSL_Poll(wslay_event_context_ptr ctx); int WSL_Poll(wslay_event_context_ptr ctx);
int WSL_WantsExit(wslay_event_context_ptr ctx);
ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data); ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data);
ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data); ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data);
void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data); void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data);
int WSL_GenmaskCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data); int WSL_GenmaskCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data);
const char* WSL_ResultToString(int err); const char* WSL_ResultToString(int err);
#endif #endif
#if defined(__EMSCRIPTEN__)
EM_BOOL Emscripten_WebSocketOnOpen(int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData);
EM_BOOL Emscripten_WebSocketOnError(int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData);
EM_BOOL Emscripten_WebSocketOnClose(int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData);
EM_BOOL Emscripten_WebSocketOnMessage(int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData);
#endif
// Random numbers (PCG) // Random numbers (PCG)
typedef struct { uint64_t state; uint64_t inc; } pcg32_random_t; typedef struct { uint64_t state; uint64_t inc; } pcg32_random_t;
void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq); void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq);
uint32_t pcg32_random_r(pcg32_random_t* rng); uint32_t pcg32_random_r(pcg32_random_t* rng);
// If level <= dmWebSocket::g_DebugWebSocket, then it outputs the message
#ifdef __GNUC__
void DebugLog(int level, const char* fmt, ...) __attribute__ ((format (printf, 2, 3)));
#else
void DebugLog(int level, const char* fmt, ...);
#endif
void DebugPrint(int level, const char* msg, const void* _bytes, uint32_t num_bytes);
} }

View File

@@ -39,11 +39,7 @@ const char* WSL_ResultToString(int err)
int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx) int WSL_Init(wslay_event_context_ptr* ctx, ssize_t buffer_size, void* userctx)
{ {
// Currently only supports client implementation // Currently only supports client implementation
int ret = -1; return wslay_event_context_client_init(ctx, &g_WslCallbacks, userctx);
ret = wslay_event_context_client_init(ctx, &g_WslCallbacks, userctx);
if (ret == 0)
wslay_event_config_set_max_recv_msg_length(*ctx, buffer_size);
return ret;
} }
@@ -54,8 +50,8 @@ void WSL_Exit(wslay_event_context_ptr ctx)
int WSL_Close(wslay_event_context_ptr ctx) int WSL_Close(wslay_event_context_ptr ctx)
{ {
const char* reason = "Client wants to close"; const char* reason = "";
wslay_event_queue_close(ctx, 0, (const uint8_t*)reason, strlen(reason)); wslay_event_queue_close(ctx, WSLAY_CODE_NORMAL_CLOSURE, (const uint8_t*)reason, 0);
return 0; return 0;
} }
@@ -68,24 +64,25 @@ int WSL_Poll(wslay_event_context_ptr ctx)
return r; return r;
} }
int WSL_WantsExit(wslay_event_context_ptr ctx)
{
if ((wslay_event_get_close_sent(ctx) && wslay_event_get_close_received(ctx))) {
return 1;
}
return 0;
}
ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data) ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data)
{ {
WebsocketConnection* conn = (WebsocketConnection*)user_data; WebsocketConnection* conn = (WebsocketConnection*)user_data;
int r = -1; // received bytes if >=0, error if < 0 int r = -1; // received bytes if >=0, error if < 0
if (conn->m_HasHandshakeData)
{
r = conn->m_BufferSize;
memcpy(buf, conn->m_Buffer, r);
conn->m_BufferSize = 0;
conn->m_HasHandshakeData = 0;
return r;
}
dmSocket::Result socket_result = Receive(conn, buf, len, &r); dmSocket::Result socket_result = Receive(conn, buf, len, &r);
if (dmSocket::RESULT_OK == socket_result && r == 0) if (dmSocket::RESULT_OK == socket_result && r == 0)
socket_result = dmSocket::RESULT_WOULDBLOCK; socket_result = dmSocket::RESULT_CONNABORTED;
if (dmSocket::RESULT_OK != socket_result) if (dmSocket::RESULT_OK != socket_result)
{ {
@@ -117,20 +114,36 @@ ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_
return (ssize_t)sent_bytes; return (ssize_t)sent_bytes;
} }
// Might be called multiple times for a connection receiving multiple events
void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data)
{ {
WebsocketConnection* conn = (WebsocketConnection*)user_data; WebsocketConnection* conn = (WebsocketConnection*)user_data;
if (arg->opcode == WSLAY_TEXT_FRAME || arg->opcode == WSLAY_BINARY_FRAME) if (arg->opcode == WSLAY_TEXT_FRAME || arg->opcode == WSLAY_BINARY_FRAME)
{ {
if (arg->msg_length >= conn->m_BufferCapacity) PushMessage(conn, MESSAGE_TYPE_NORMAL, arg->msg_length, arg->msg, 0);
conn->m_Buffer = (char*)realloc(conn->m_Buffer, arg->msg_length + 1);
memcpy(conn->m_Buffer, arg->msg, arg->msg_length);
conn->m_BufferSize = arg->msg_length;
conn->m_HasMessage = 1;
} else if (arg->opcode == WSLAY_CONNECTION_CLOSE) } else if (arg->opcode == WSLAY_CONNECTION_CLOSE)
{ {
// TODO: Store the reason // The first two bytes is the close code
const uint8_t* reason = (const uint8_t*)"";
size_t len = arg->msg_length;
if (arg->msg_length > 2)
{
reason = arg->msg + 2;
len -= 2;
}
char buffer[1024];
uint16_t status_code = wslay_event_get_status_code_received(ctx);
len = dmSnPrintf(buffer, sizeof(buffer), "Server closing (%u). Reason: '%s'", status_code, reason);
PushMessage(conn, MESSAGE_TYPE_CLOSE, len, (const uint8_t*)buffer, status_code);
if (!wslay_event_get_close_sent(ctx))
{
wslay_event_queue_close(ctx, arg->status_code, (const uint8_t*)buffer, len);
}
DebugLog(1, "%s", buffer);
} }
} }