├── .gitignore
├── license.txt
├── readme.md
├── src
├── WebSocketClient.xml
└── web_socket_client
│ ├── ByteUtil.brs
│ ├── Logger.brs
│ ├── TlsUtil.brs
│ ├── WebSocketClient.brs
│ └── WebSocketClientTask.brs
├── test
├── .gitignore
├── .rokudevignore
├── bright_web_socket.json.example
├── components
│ ├── Main.xml
│ ├── WebSocketClient.xml
│ └── web_socket_client
│ │ ├── ByteUtil.brs
│ │ ├── Logger.brs
│ │ ├── TlsUtil.brs
│ │ ├── WebSocketClient.brs
│ │ └── WebSocketClientTask.brs
├── images
│ ├── icon_focus_hd.png
│ ├── icon_focus_sd.png
│ ├── icon_side_hd.png
│ ├── icon_side_sd.png
│ ├── splash_fhd.jpg
│ ├── splash_hd.jpg
│ └── splash_sd.jpg
├── locale
│ └── en_US
│ │ ├── manifest
│ │ └── translations.xml
├── manifest
└── source
│ └── Main.brs
└── util
├── .gitignore
├── bulk_send
├── package.json
├── readme.md
└── websocket_bulk_send_server.js
└── echo
├── package.json
├── readme.md
└── websocket_echo_server.js
/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/.gitignore
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | BrightWebSocket - BrightScript websocket library
4 | Copyright © 2018 Rolando Islas
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | this software and associated documentation files (the “Software”), to deal in
8 | the Software without restriction, including without limitation the rights to
9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 | of the Software, and to permit persons to whom the Software is furnished to do
11 | so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22 | IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # BrightWebSocket
2 |
3 | A SceneGraph websocket client library written in BrightScript
4 |
5 | **This repo is no longer maintained. See [This fork](https://github.com/SuitestAutomation/BrightWebSocket) for an actively maintained repo.**
6 |
7 | # RFC 6455
8 |
9 | Follows [RFC 6455](https://tools.ietf.org/html/rfc6455)
10 |
11 | Notes:
12 |
13 | - Uses ASCII instead of UTF-8 for string operations
14 | - Does not support secure web sockets at this time
15 |
16 | # Installation
17 |
18 | The contents of the "src" folder in the repository's root should be placed
19 | in the "components" folder of a SceneGraph Roku app.
20 |
21 | # Using the Library
22 |
23 | The client follows the
24 | [HTML WebSocket interface](https://html.spec.whatwg.org/multipage/web-sockets.html#the-websocket-interface),
25 | modified to work with BrightScript conventions. Those familiar with browser
26 | (JavaScript) WebSocket implementations should find this client similar.
27 |
28 | Example:
29 |
30 | ```brightscript
31 | function init() as void
32 | m.ws = createObject("roSGNode", "WebSocketClient")
33 | m.ws.observeField("on_open", "on_open")
34 | m.ws.observeFiled("on_message", "on_message")
35 | m.ws.open = "ws://echo.websocket.org/"
36 | end function
37 |
38 | function on_open(event as object) as void
39 | m.ws.send = ["Hello World"]
40 | end function
41 |
42 | function on_message(event as object) as void
43 | print event.getData().message
44 | end function
45 | ```
46 |
47 | For a working sample app see the "test" folder. Its contents can be zipped for
48 | installation as a dev channel on a Roku.
49 |
50 | # License
51 |
52 | The MIT License. See license.txt.
53 |
--------------------------------------------------------------------------------
/src/WebSocketClient.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/web_socket_client/ByteUtil.brs:
--------------------------------------------------------------------------------
1 | ' ByteUtil.brs
2 | ' Copyright (C) 2018 Rolando Islas
3 | ' Released under the MIT license
4 | '
5 | ' Byte array operation related utilities
6 |
7 | ' Convert network ordered bytes to a short (16-bit int)
8 | function bytes_to_short(b1 as integer, b2 as integer) as integer
9 | return ((b1 and &hff) << 8) or (b2 and &hff)
10 | end function
11 |
12 | ' Convert network ordered bytes to a long (64-bit int)
13 | function bytes_to_long(b1 as integer, b2 as integer, b3 as integer, b4 as integer, b5 as integer, b6 as integer, b7 as integer, b8 as integer) as longinteger
14 | return ((b1 and &hff) << 24) or ((b2 and &hff) << 16) or ((b3 and &hff) << 8) or (b4 and &hff) or ((b5 and &hff) << 24) or ((b6 and &hff) << 16) or ((b7 and &hff) << 8) or (b8 and &hff)
15 | end function
16 |
17 | ' Convert network ordered bytes to a long (64-bit int)
18 | function byte_array_to_long(b as object) as longinteger
19 | return bytes_to_long(b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7])
20 | end function
21 |
22 | ' Convert network ordered bytes to an int (32-bit)
23 | function bytes_to_int(b1 as integer, b2 as integer, b3 as integer, b4 as integer) as integer
24 | return (b1 << 24) or ((b2 and &hff) << 16) or ((b3 and &hff) << 8) or (b4 and &hff)
25 | end function
26 |
27 | ' Convert network ordered byres to an int (24-bit)
28 | function bytes_to_int24(b1 as integer, b2 as integer, b3 as integer) as integer
29 | return ((b1 and &hff) << 16) or ((b2 and &hff) << 8) or (b3 and &hff)
30 | end function
31 |
32 | ' Convert a 32-bit int to a roByteArray
33 | ' Bytes are network ordered
34 | function int_to_bytes(number as integer) as object
35 | ba = createObject("roByteArray")
36 | if ba.isLittleEndianCPU()
37 | for bit = 24 to 0 step -8
38 | ba.push((number >> bit) and &hff)
39 | end for
40 | else
41 | for bit = 0 to 24 step 8
42 | ba.push((number >> bit) and &hff)
43 | end for
44 | end if
45 | return ba
46 | end function
47 |
48 | ' Convert a 16-bit int to a roByteArray
49 | ' Bytes are network ordered
50 | function short_to_bytes(number as integer) as object
51 | ba = createObject("roByteArray")
52 | if ba.isLittleEndianCPU()
53 | ba.push((number >> 8) and &hff)
54 | ba.push(number)
55 | else
56 | ba.push(number)
57 | ba.push((number >> 8) and &hff)
58 | end if
59 | return ba
60 | end function
61 |
62 | ' Convert a 64-bit int to a roByteArray
63 | ' Bytes are network ordered
64 | ' FIXME bitwise operations on numbers larger than 0xffffffff (max 32-bit)
65 | ' are problematic even when specifying longinteger
66 | function long_to_bytes(number as longinteger) as object
67 | ba = createObject("roByteArray")
68 | if ba.isLittleEndianCPU()
69 | for bit = 7 to 0 step -1
70 | ba[bit] = (number and &hff)
71 | number >>= 8
72 | end for
73 | else
74 | for bit = 0 to 56 step 8
75 | ba.push((number >> bit) and &hff)
76 | end for
77 | end if
78 | return ba
79 | end function
80 |
81 | ' Convert a 24-bit int to a roByteArray
82 | ' Bytes are network ordered
83 | function int24_to_bytes(number as integer) as object
84 | ba = createObject("roByteArray")
85 | if ba.isLittleEndianCPU()
86 | for bit = 16 to 0 step -8
87 | ba.push((number >> bit) and &hff)
88 | end for
89 | else
90 | for bit = 0 to 16 step 8
91 | ba.push((number >> bit) and &hff)
92 | end for
93 | end if
94 | return ba
95 | end function
96 |
97 | ' Convert a roArray to a roByteArray
98 | function byte_array(array as object) as object
99 | ba = createObject("roByteArray")
100 | for each byte in array
101 | ba.push(byte)
102 | end for
103 | return ba
104 | end function
105 |
106 | ' Return a sub roByteArray of a passed roByteArray
107 | ' @param ba roByteArray array to use for sub array operations
108 | ' @param start_index integer index to start sub array (inclusive)
109 | ' @param end_index integer index to end sub array (inclusive)
110 | function byte_array_sub(ba as object, start_index as integer, end_index = -1 as integer) as object
111 | if end_index = -1
112 | end_index = ba.count() - 1
113 | end if
114 | ba_sub = createObject("roByteArray")
115 | for byte_index = start_index to end_index
116 | ba_sub.push(ba[byte_index])
117 | end for
118 | return ba_sub
119 | end function
120 |
121 | ' Check that two byte arrays are equal
122 | ' @param a roByteArray first array
123 | ' @param b roByteArray seconds array
124 | ' @return byte arrays equivalence
125 | function byte_array_equals(a as object, b as object) as boolean
126 | if a.count() <> b.count()
127 | return false
128 | end if
129 | for byte_index = 0 to a.count() - 1
130 | if a[byte_index] <> b[byte_index]
131 | return false
132 | end if
133 | end for
134 | return true
135 | end function
136 |
137 | ' Encrtpt a byte array with an RSA public key
138 | function rsa_encrypt(public_key as object, ba as object) as object
139 | encrypted_ba = createObject("roByteArray")
140 | return encrypted_ba
141 | end function
142 |
143 | ' Perform a bitwise exclusive or on two integers
144 | function xor(a as integer, b as integer) as integer
145 | return (a or b) and not (a and b)
146 | end function
--------------------------------------------------------------------------------
/src/web_socket_client/Logger.brs:
--------------------------------------------------------------------------------
1 | ' Logger.brs
2 | ' Copyright (C) 2018 Rolando Islas
3 | ' Released under the MIT license
4 | '
5 | ' Internal logging utility
6 |
7 | ' Initialize a logging utility
8 | function Logger() as object
9 | log = {}
10 | log.FATAL = -2
11 | log.WARN = -1
12 | log.INFO = 0
13 | log.DEBUG = 1
14 | log.EXTRA = 2
15 | log.VERBOSE = 3
16 |
17 | ' Main
18 |
19 | ' Log a message
20 | ' @param level log level string or integer
21 | ' @param msg message to print
22 | log.printl = function (level as object, msg as object) as void
23 | if m._parse_level(level) > m.log_level
24 | return
25 | end if
26 | print "[" + m._level_to_string(level) + "] " + msg
27 | end function
28 |
29 | ' Parse level to a string
30 | ' @param level string or integer level
31 | log._level_to_string = function (level as object) as string
32 | if type(level) = "roString" or type(level) = "String"
33 | level = m._parse_level(level)
34 | end if
35 | if level = -2
36 | return "FATAL"
37 | else if level = -1
38 | return "WARN"
39 | else if level = 0
40 | return "INFO"
41 | else if level = 1
42 | return "DEBUG"
43 | else if level = 2
44 | return "EXTRA"
45 | else if level = 3
46 | return "VERBOSE"
47 | end if
48 | end function
49 |
50 | ' Parse level to an integer
51 | ' @param level string or integer level
52 | log._parse_level = function (level as object) as integer
53 | level_string = level.toStr()
54 | log_level = 0
55 | if level_string = "FATAL" or level_string = "-2"
56 | log_level = m.FATAL
57 | else if level_string = "WARN" or level_string = "-1"
58 | log_level = m.WARN
59 | else if level_string = "INFO" or level_string = "0"
60 | log_level = m.INFO
61 | else if level_string = "DEBUG" or level_string = "1"
62 | log_level = m.DEBUG
63 | else if level_string = "EXTRA" or level_string = "2"
64 | log_level = m.EXTRA
65 | else if level_string = "VERBOSE" or level_string = "3"
66 | log_level = m.VERBOSE
67 | end if
68 | return log_level
69 | end function
70 |
71 | ' Set the log level
72 | log.set_log_level = function (level as string) as void
73 | m.log_level = m._parse_level(level)
74 | end function
75 |
76 | ' Parse Config
77 | config_string = readAsciiFile("pkg:/bright_web_socket.json")
78 | config = parseJson(config_string)
79 | if config <> invalid
80 | if config.log_level <> invalid
81 | log.log_level = log._parse_level(config.log_level)
82 | else
83 | log.log_level = log.INFO
84 | log.printl(log.WARN, "WebSocketLogger: Missing log_level param in pkg:/bright_web_socket.json")
85 | end if
86 | else
87 | log.log_level = log.INFO
88 | log.printl(log.WARN, "WebSocketLogger: Missing pkg:/bright_web_socket.json")
89 | end if
90 | return log
91 | end function
92 |
--------------------------------------------------------------------------------
/src/web_socket_client/TlsUtil.brs:
--------------------------------------------------------------------------------
1 | ' WebSocketClient.brs
2 | ' Copyright (C) 2018 Rolando Islas
3 | ' Released under the MIT license
4 | '
5 | ' Utilities for operating with the Transport Layer Security (TLS) protocol
6 | ' Follows RFC 5246
7 |
8 | ' TlsUtil object
9 | ' Requires pkg:/components/web_socket_client/Logger.brs
10 | ' Requires pkg:/components/web_socket_client/ByteUtil.brs
11 | ' @param socket roStreamSocket async TCP socket
12 | function TlsUtil(socket = invalid as object) as object
13 | tls_util = {}
14 | ' Constants
15 | tls_util.STATE_DISCONNECTED = 0
16 | tls_util.STATE_CONNECTING = 1
17 | tls_util.STATE_FINISHED = 2
18 | tls_util._BUFFER_SIZE = cint(1024 * 1024 * 1)
19 | tls_util._TLS_VERSION = [3, 3]
20 | ' TLS Constants
21 | tls_util._TLS_FRAGMENT_MAX_LENGTH = 2^14
22 | tls_util._HANDSHAKE_TYPE = {
23 | HELLO_REQUEST: 0,
24 | CLIENT_HELLO: 1,
25 | SERVER_HELLO: 2,
26 | CERTIFICATE: 11,
27 | SERVER_KEY_EXCHANGE: 12,
28 | CERTIFICATE_REQUEST: 13,
29 | SERVER_HELLO_DONE: 14,
30 | CERTIFICATE_VERIFY: 15,
31 | CLIENT_KEY_EXCHANGE: 16,
32 | FINISHED: 20
33 | }
34 | tls_util._RECORD_TYPE = {
35 | CHANGE_CIPHER_SPEC: 20,
36 | ALERT: 21,
37 | HANDSHAKE: 22,
38 | APPLICATION_DATA: 23
39 | }
40 | tls_util._EXTENSION_TYPE = {
41 | SERVER_NAME: 0,
42 | SUPPORTED_GROUPS: 10,
43 | EC_POINT_FORMATS: 11
44 | }
45 | tls_util._ALERT_LEVEL = {
46 | WARNING: 1
47 | FATAL: 2
48 | }
49 | tls_util._ALERT_TYPE = {
50 | CLOSE_NOTIFY: 0,
51 | UNEXPECTED_MESSAGE: 10,
52 | HANDSHAKE_FAILURE: 40,
53 | BAD_CERTIFICATE: 42,
54 | PROTOCOL_VERSION: 70,
55 | UNSUPPORTED_EXTENSION: 110
56 | }
57 | tls_util._COMPRESSION_METHODS = {
58 | NULL: 0
59 | }
60 | tls_util._SUPPORTED_GROUPS = {
61 | SECP256R1: 23,
62 | SECP384R1: 24,
63 | SECP521R1: 25
64 | }
65 | tls_util._EC_POINT_FORMATS = {
66 | UNCOMPRESSED: 0
67 | }
68 | ' Variables
69 | tls_util._data = createObject("roByteArray")
70 | tls_util._data[tls_util._BUFFER_SIZE] = 0
71 | tls_util._data_size = 0
72 | tls_util.ready_state = tls_util.STATE_DISCONNECTED
73 | tls_util._socket = socket
74 | tls_util._hostname = invalid
75 | tls_util._cipher_suite = invalid
76 | tls_util._handshake_buffer = createObject("roByteArray")
77 | tls_util._supported_extensions = []
78 | tls_util._handshake_start_time = 0
79 | tls_util._certificates = []
80 | tls_util._server_public_key = invalid
81 |
82 | ' Decode TLS bytes
83 | ' This function buffers data if it does not have enough bytes to decode
84 | ' @param self TlsUtil object referring to the TlsObject to operate on,
85 | ' usually itself
86 | ' @param input roByteArray
87 | ' @param size integer size of usable data in the input array starting from
88 | ' index 0
89 | ' @return roByteArray of decoded data. The returned array may have a count
90 | ' of zero. May return invalid on error
91 | tls_util.read = function (self as object, input as object, size as integer) as object
92 | if self.ready_state = self.STATE_DISCONNECTED
93 | printl("DEBUG", "TlsUtil: Read failed: state is disconnected")
94 | return invalid
95 | end if
96 | if self._has_handshake_timed_out(self)
97 | printl("DEBUG", "TlsUtil: Handshake timed out")
98 | return invalid
99 | end if
100 | decoded_app_data = createObject("roByteArray")
101 | ' TLS Record Frame
102 | if size >= 0
103 | ' Save to buffer
104 | for byte_index = self._data_size to self._data_size + size - 1
105 | self._data[byte_index] = input[byte_index - self._data_size]
106 | end for
107 | self._data_size += size
108 | input = invalid
109 | ' Wait for frame size
110 | fragment_start_index = 5
111 | if self._data_size < fragment_start_index
112 | return decoded_app_data
113 | end if
114 | ' Record
115 | content_type = self._data[0]
116 | version_major = self._data[1]
117 | version_minor = self._data[2]
118 | fragment_length = bytes_to_short(self._data[3], self._data[4])
119 | ' Check version
120 | if self._TLS_VERSION[0] <> version_major or self._TLS_VERSION[1] <> version_minor
121 | self._error(self, self._ALERT_TYPE.PROTOCOL_VERSION, "Received a frame with an an unsupported version defined")
122 | return invalid
123 | end if
124 | ' Wait for fragment
125 | if self._data_size < fragment_start_index + fragment_length
126 | return decoded_app_data
127 | end if
128 | ' Fragment
129 | fragment = createObject("roByteArray")
130 | for byte_index = fragment_start_index to fragment_start_index + fragment_length - 1
131 | fragment.push(self._data[byte_index])
132 | end for
133 | handled_fragment = self._handle_fragment(self, content_type, fragment)
134 | if handled_fragment = invalid
135 | return invalid
136 | else
137 | decoded_app_data.append(handled_fragment)
138 | end if
139 | ' Delete fragment from buffer
140 | self._data = byte_array_sub(self._data, fragment_start_index + fragment_length, self._data_size - 1)
141 | self._data_size = self._data.count()
142 | self._data[self._BUFFER_SIZE] = 0
143 | end if
144 | return decoded_app_data
145 | end function
146 |
147 | ' Check if the handshake has timed out
148 | ' @param TlsUtil
149 | ' @return if the handshake has not completed before a timed out
150 | tls_util._has_handshake_timed_out = function (self as object) as boolean
151 | return uptime(0) - self._handshake_start_time >= 30 and self._handshake_start_time > -1
152 | end function
153 |
154 | ' Send a fatal alert and optionally log a debug message and set the state to disconnected if fatal
155 | ' @param self TlsUtil
156 | ' @param alert_type integer type
157 | ' @param message string optional message to log
158 | ' @param fatal boolean set the alert type to fatal and disconnect
159 | tls_util._error = function (self as object, alert_type as integer, message = "" as string, fatal = true as boolean) as void
160 | level = self._ALERT_LEVEL.FATAL
161 | if not fatal
162 | level = self._ALERT_LEVEL.WARNING
163 | end if
164 | self._send_alert(self, level, alert_type)
165 | if message <> ""
166 | printl("DEBUG", "TlsUtil: Error: " + message)
167 | end if
168 | if fatal
169 | self.ready_state = self.STATE_DICONNECTED
170 | end if
171 | end function
172 |
173 | ' Send an alert
174 | ' @param self TlsUtil
175 | ' @param alert_level integer level of alert
176 | ' @param alert_type integer alert description
177 | tls_util._send_alert = function (self as object, alert_level as integer, alert_type as integer)
178 | alert = createObject("roByteArray")
179 | alert.push(alert_level)
180 | alert.push(alert_type)
181 | self._send_record(self, self._RECORD_TYPE.ALERT, alert)
182 | end function
183 |
184 | ' Handle opaque fragment data
185 | ' @param self TlsUtil
186 | ' @param content_type type of frame
187 | ' @param frame roByteArray frame
188 | ' @return potentially empty roByteArray or invalid on error. disconnect state is handled on error
189 | tls_util._handle_fragment = function (self as object, content_type as integer, fragment as object) as object
190 | printl("VERBOSE", "TlsUtil: Received fragment of type " + content_type.toStr() + ": " + fragment.toHexString())
191 | decoded_app_data = createObject("roByteArray")
192 | while fragment.count() > 0
193 | ' Handshake
194 | if content_type = self._RECORD_TYPE.HANDSHAKE
195 | fragment_header_size = 4
196 | ' Invalid data
197 | if fragment.count() < fragment_header_size
198 | self._error_handshake(self)
199 | return invalid
200 | ' Handle handshake data
201 | else
202 | handshake_type = fragment[0]
203 | length = bytes_to_int24(fragment[1], fragment[2], fragment[3])
204 | if fragment.count() < fragment_header_size + length
205 | self._error_handshake(self)
206 | return invalid
207 | end if
208 | handshake_data = createObject("roByteArray")
209 | for byte_index = fragment_header_size to fragment_header_size + length - 1
210 | handshake_data.push(fragment[byte_index])
211 | end for
212 | if not self._handle_handshake(self, handshake_type, handshake_data)
213 | return invalid
214 | end if
215 | self._handshake_buffer.append(byte_array_sub(fragment, 0, fragment_header_size + length - 1))
216 | fragment = byte_array_sub(fragment, fragment_header_size + length, fragment.count() - 1)
217 | end if
218 | ' App data
219 | else if content_type = self._RECORD_TYPE.APPLICATION_DATA
220 | ' Alert
221 | else if content_type = self._RECORD_TYPE.ALERT
222 | if fragment.count() < 2
223 | self._error_handshake(self)
224 | return invalid
225 | else
226 | alert_level = fragment[0]
227 | alert_description = fragment[1]
228 | printl("DEBUG", "TlsUtil: Received alert:")
229 | printl("DEBUG", " level: " + alert_level.toStr())
230 | printl("DEBUG", " description: " + alert_description.toStr())
231 | if alert_level = self._ALERT_LEVEL.FATAL
232 | self._error(self, self._ALERT_TYPE.CLOSE_NOTIFY, "Received fatal alert", true)
233 | return invalid
234 | end if
235 | fragment = byte_array_sub(fragment, 2, fragment.count() - 1)
236 | end if
237 | ' Cipher spec
238 | else if content_type = self._CHANGE_CIPHER_SPEC
239 |
240 | end if
241 | end while
242 | return decoded_app_data
243 | end function
244 |
245 | ' Handle handshake data
246 | ' @param self TlsUtil
247 | ' @param handshake_type integer type of handshake data
248 | ' @param handshake roByteArray handshake data
249 | ' @return false on error
250 | tls_util._handle_handshake = function (self as object, handshake_type as integer, handshake as object) as boolean
251 | ' HelloRequest
252 | if handshake_type = self._HANDSHAKE_TYPE.HELLO_REQUEST
253 | if self.ready_state = self.STATE_DISCONNECTED
254 | self.connect(self, self._hostname)
255 | end if
256 | ' ServerHello
257 | else if handshake_type = self._HANDSHAKE_TYPE.SERVER_HELLO
258 | if handshake.count() < 38
259 | self._error_handshake(self)
260 | return false
261 | end if
262 | version_major = handshake[0]
263 | version_minor = handshake[1]
264 | if version_major <> self._tls_version[0] or version_minor <> self._tls_version[1]
265 | self._error_handshake(self)
266 | return false
267 | end if
268 | random_time = bytes_to_int(handshake[2], handshake[3], handshake[4], handshake[5])
269 | random = createObject("roByteArray")
270 | for byte_index = 6 to 33
271 | random.push(handshake[byte_index])
272 | end for
273 | session_id = createObject("roByteArray")
274 | session_id_length = handshake[34]
275 | for byte_index = 35 to 34 + session_id_length
276 | random.push(handshake[byte_index])
277 | end for
278 | cipher_suite = createObject("roByteArray")
279 | cipher_suite.push(handshake[35 + session_id_length])
280 | cipher_suite.push(handshake[36 + session_id_length])
281 | self._cipher_suite = cipher_suite
282 | compression_method = handshake[37 + session_id_length]
283 | if compression_method <> self._COMPRESSION_METHODS.NULL
284 | self._error_handshake(self)
285 | return false
286 | end if
287 | extensions = createObject("roByteArray")
288 | if handshake.count() > 38 + session_id_length
289 | extensions_length = bytes_to_short(handshake[38 + session_id_length], handshake[39 + session_id_length])
290 | for byte_index = 40 + session_id_length to 39 + session_id_length + extensions_length
291 | extensions.push(handshake[byte_index])
292 | end for
293 | end if
294 | extension_types = []
295 | if extensions.count() > 0
296 | extension_index = 0
297 | while extension_index < extensions.count()
298 | if extensions.count() - extension_index < 4
299 | self._error_handshake(self)
300 | return false
301 | end if
302 | extension_type = bytes_to_short(extensions[extension_index], extensions[extension_index + 1])
303 | extension_length = bytes_to_short(extensions[extension_index + 2], extensions[extension_index + 3])
304 | extension_index += 4 + extension_length
305 | extension_types.push(extension_type)
306 | end while
307 | end if
308 | for each extension_type in extension_types
309 | ' Check if extension was requested
310 | was_extension_requested = false
311 | for each supported_extension in self._supported_extensions
312 | if supported_extension = extension_type
313 | was_extension_requested = true
314 | end if
315 | end for
316 | if not was_extension_requested
317 | self._error(self, self._ALERT_TYPE.UNSUPPORTED_EXTENSION, "Received invalid extension data", true)
318 | return false
319 | end if
320 | ' Check if extension has been defined more than once
321 | extension_definitions = 0
322 | for each extension_type_loop in extension_types
323 | if extension_type = extension_type_loop
324 | extension_definitions++
325 | if extension_definitions > 1
326 | self._error(self, self._ALERT_TYPE.UNSUPPORTED_EXTENSION, "Received invalid extension data", true)
327 | return false
328 | end if
329 | end if
330 | end for
331 | end for
332 | printl("DEBUG", "TlsUtil: Received ServerHello:")
333 | printl("DEBUG", " cipher suite: " + cipher_suite.toHexString())
334 | ' Certificate
335 | else if handshake_type = self._HANDSHAKE_TYPE.CERTIFICATE
336 | certificate_list = []
337 | if handshake.count() < 3
338 | self._error_handshake(self)
339 | return false
340 | end if
341 | certificate_list_length = bytes_to_int24(handshake[0], handshake[1], handshake[2])
342 | if handshake.count() < 3 + certificate_list_length
343 | self._error_handshake(self)
344 | return false
345 | end if
346 | certificate_index = 3
347 | while certificate_index + 1 < handshake.count()
348 | certificate_size = bytes_to_int24(handshake[certificate_index], handshake[certificate_index + 1], handshake[certificate_index + 2])
349 | if handshake.count() < certificate_index + certificate_size
350 | self._error_handshake(self)
351 | return false
352 | end if
353 | certificate = createObject("roByteArray")
354 | for byte_index = certificate_index + 3 to certificate_index + certificate_size - 1
355 | certificate.push(handshake[byte_index])
356 | end for
357 | certificate_list.push(certificate)
358 | certificate_index += 3 + certificate_size
359 | end while
360 | self._certificates = certificate_list
361 | printl("DEBUG", "TlsUtil: Received Certificate: " + certificate_list.count().toStr() + " certificates")
362 | ' ServerKeyExchange
363 | else if handshake_type = self._HANDSHAKE_TYPE.SERVER_KEY_EXCHANGE
364 | printl("DEBUG", "TlsUtil: Received ServerKeyExchange: " + handshake.toHexString())
365 | self._error_handshake(self)
366 | return false
367 | ' Certificate Request
368 | else if handshake_type = self._HANDSHAKE_TYPE.CERTIFICATE_REQUEST
369 | printl("DEBUG", "TlsUtil: Received CertificateRequest: " + handshake.toHexString())
370 | self._error_handshake(self)
371 | return false
372 | ' ServerHelloDone
373 | else if handshake_type = self._HANDSHAKE_TYPE.SERVER_HELLO_DONE
374 | self._handshake_start_time = -1
375 | ' Verify certificate
376 | if not self._verify_certificate(self)
377 | self._error(self, self._ALERT_TYPE.BAD_CERTIFICATE, "Server certificate failed to verify", true)
378 | return false
379 | end if
380 | printl("DEBUG", "TlsUtil: Received ServerHelloDone")
381 | self._send_client_key_exchange(self)
382 | self._send_client_finish(self)
383 | ' Finished
384 | else if handshake_type = self._HANDSHAKE_TYPE.FINISHED
385 | printl("DEBUG", "TlsUtil: Received Finished: " + handshake.toHexString())
386 | end if
387 | return true
388 | end function
389 |
390 | ' Send the client finish message
391 | ' @param self TlsUtil
392 | tls_util._send_client_finish = function (self as object) as void
393 |
394 | end function
395 |
396 | ' Send the client key exchange handshake message
397 | ' @param self TlsUtil
398 | tls_util._send_client_key_exchange = function (self as object) as void
399 | printl("EXTRA", "TlsUtil: Generating client key exchange message")
400 | ' TLS_RSA_WITH_AES_128_GCM_SHA256 || TLS_RSA_WITH_AES_256_GCM_SHA384 || TLS_RSA_WITH_AES_128_CBC_SHA256 || TLS_RSA_WITH_AES_256_CBC_SHA256
401 | if byte_array_equals(byte_array([&h00, &h9c]), self._cipher_suite) or byte_array_equals(byte_array([&h00, &h9d]), self._cipher_suite) or byte_array_equals(byte_array([&h00, &h3c]), self._cipher_suite) or byte_array_equals(byte_array([&h00, &h3d]), self._cipher_suite)
402 | printl("DEBUG", "TlsUtil: Generating RSA premaster secret")
403 | secret = createObject("roByteArray")
404 | secret.append(byte_array(self._TLS_VERSION))
405 | for byte_index = 0 to 45
406 | secret.push(rnd(256) - 1)
407 | end for
408 | encrypted_secret = rsa_encrypt(self._server_public_key, secret)
409 | else
410 | printl("FATAL", "TlsUtil: Unhandled cipher suite: " + self._cipher_suite.toStr())
411 | end if
412 | end function
413 |
414 | ' Verify a certificate
415 | ' @param self TlsUtil
416 | tls_util._verify_certificate = function (self as object) as boolean
417 | ' TODO validate the certificate
418 | self._server_public_key = createObject("roByteArray")
419 | print self._certificates[0]
420 | print "--"
421 | print self._certificates[1]
422 | return true
423 | end function
424 |
425 | ' Set the ready state to disconnected and send a fatal alert
426 | ' @param self TlsUtil
427 | tls_util._error_handshake = function (self as object) as void
428 | self._error(self, self._ALERT_TYPE.HANDSHAKE_FAILURE, "Received invalid handshake data", true)
429 | end function
430 |
431 | ' Set the internal socket used for sending
432 | ' @param self TlsUtil object
433 | ' @param socket roStreamSocket async TCP socket
434 | tls_util.set_socket = function (self as object, socket as object) as void
435 | self._socket = socket
436 | end function
437 |
438 | ' Start the TLS handshake with a ClientHello
439 | ' Should only be called if the client socket is a new connection
440 | ' @param hostname string hostname
441 | ' @param self TlsUtil object
442 | tls_util.connect = function (self as object, hostname as string) as void
443 | self._hostname = hostname
444 | self._cipher_suite = invalid
445 | self._handshake_buffer.clear()
446 | self._supported_extensions.clear()
447 | self._handshake_start_time = uptime(0)
448 | self._certificates.clear()
449 | self.ready_state = self.STATE_CONNECTING
450 | handshake = createObject("roByteArray")
451 | ' Body - ClientHello
452 | client_hello = createObject("roByteArray")
453 | ' Protocol version
454 | client_hello.append(byte_array([3, 3])) ' TLS 1.2
455 | ' Random
456 | client_hello.append(self._Random())
457 | ' Session id
458 | client_hello.push(0) ' Length
459 | ' Ciphersuites
460 | ' https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29
461 | ciphersuites = createObject("roByteArray")
462 | ' TODO implement ecdhe ecdsa and chacha20
463 | 'ciphersuites.append(byte_array([&hc0, &h2c])) ' TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
464 | 'ciphersuites.append(byte_array([&hc0, &h30])) ' TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
465 | 'ciphersuites.append(byte_array([&hcc, &h14])) ' TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
466 | 'ciphersuites.append(byte_array([&hcc, &h13])) ' TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
467 | 'ciphersuites.append(byte_array([&hc0, &h2b])) ' TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
468 | 'ciphersuites.append(byte_array([&hc0, &h2f])) ' TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
469 | 'ciphersuites.append(byte_array([&hc0, &h24])) ' TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
470 | 'ciphersuites.append(byte_array([&hc0, &h28])) ' TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
471 | 'ciphersuites.append(byte_array([&hc0, &h23])) ' TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
472 | 'ciphersuites.append(byte_array([&hc0, &h27])) ' TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
473 | 'ciphersuites.append(byte_array([&hc0, &h28])) ' TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
474 | 'ciphersuites.append(byte_array([&hc0, &h24])) ' TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
475 | 'ciphersuites.append(byte_array([&h00, &h67])) ' TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
476 | 'ciphersuites.append(byte_array([&h00, &h6b])) ' TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
477 | ciphersuites.append(byte_array([&h00, &h9c])) ' TLS_RSA_WITH_AES_128_GCM_SHA256
478 | ciphersuites.append(byte_array([&h00, &h9d])) ' TLS_RSA_WITH_AES_256_GCM_SHA384
479 | ciphersuites.append(byte_array([&h00, &h3c])) ' TLS_RSA_WITH_AES_128_CBC_SHA256
480 | ciphersuites.append(byte_array([&h00, &h3d])) ' TLS_RSA_WITH_AES_256_CBC_SHA256
481 | client_hello.append(short_to_bytes(ciphersuites.count()))
482 | client_hello.append(ciphersuites)
483 | ' Compression method
484 | compression_methods = createObject("roByteArray")
485 | compression_methods.push(self._COMPRESSION_METHODS.NULL) ' Null
486 | client_hello.push(compression_methods.count())
487 | client_hello.append(compression_methods)
488 | ' Extensions
489 | extensions = createObject("roByteArray")
490 | extensions.append(self._generate_sni_extension(self, hostname))
491 | extensions.append(self._generate_supported_groups_extension(self))
492 | extensions.append(self._generate_ec_point_formats_extension(self))
493 | client_hello.append(short_to_bytes(extensions.count()))
494 | client_hello.append(extensions)
495 | ' Type
496 | handshake.push(self._HANDSHAKE_TYPE.CLIENT_HELLO)
497 | ' Length
498 | handshake.append(int24_to_bytes(client_hello.count()))
499 | ' Body
500 | handshake.append(client_hello)
501 | sent = self._send_handshake(self, handshake)
502 | end function
503 |
504 | ' Generate a ec point formats extension
505 | ' @param self TlsUtil
506 | ' @return roByteArray
507 | tls_util._generate_ec_point_formats_extension = function (self as object) as object
508 | extension = createObject("roByteArray")
509 | ' Type
510 | extension.append(short_to_bytes(self._EXTENSION_TYPE.EC_POINT_FORMATS))
511 | self._supported_extensions.push(self._EXTENSION_TYPE.EC_POINT_FORMATS)
512 | ' ec point formats
513 | ec_point_formats = createObject("roByteArray")
514 | formats = createObject("roByteArray")
515 | formats.push(self._EC_POINT_FORMATS.UNCOMPRESSED)
516 | ec_point_formats.push(formats.count())
517 | ec_point_formats.append(formats)
518 | extension.append(short_to_bytes(ec_point_formats.count()))
519 | extension.append(ec_point_formats)
520 | return extension
521 | end function
522 |
523 | ' Generate a supported groups extension
524 | ' @param self TlsUtil
525 | ' @return roByteArray
526 | tls_util._generate_supported_groups_extension = function (self as object) as object
527 | extension = createObject("roByteArray")
528 | ' Type
529 | extension.append(short_to_bytes(self._EXTENSION_TYPE.SUPPORTED_GROUPS))
530 | self._supported_extensions.push(self._EXTENSION_TYPE.SUPPORTED_GROUPS)
531 | ' Supported groups
532 | supported_groups = createObject("roByteArray")
533 | groups = createObject("roByteArray")
534 | for each supported_group in self._SUPPORTED_GROUPS
535 | groups.append(short_to_bytes(self._SUPPORTED_GROUPS[supported_group]))
536 | end for
537 | supported_groups.append(short_to_bytes(groups.count()))
538 | supported_groups.append(groups)
539 | extension.append(short_to_bytes(supported_groups.count()))
540 | extension.append(supported_groups)
541 | return extension
542 | end function
543 |
544 | ' Send handshake data
545 | ' Appends payload to handshake buffer
546 | ' @param self TlsUtil
547 | ' @param payload roByteArray handshake data
548 | tls_util._send_handshake = function (self as object, payload as object) as integer
549 | self._handshake_buffer.append(payload)
550 | return self._send_record(self, self._RECORD_TYPE.HANDSHAKE, payload)
551 | end function
552 |
553 | ' Generate a Server Name Indication (SNI) extension based on a hostname
554 | ' @param self TlsUtil
555 | ' @param hostname string hostname
556 | ' @return roByteArray
557 | tls_util._generate_sni_extension = function (self as object, hostname as string) as object
558 | extension = createObject("roByteArray")
559 | ' Type
560 | extension.append(short_to_bytes(self._EXTENSION_TYPE.SERVER_NAME))
561 | self._supported_extensions.push(self._EXTENSION_TYPE.SERVER_NAME)
562 | ' Server Name Indication
563 | sni = createObject("roByteArray")
564 | name_list = createObject("roByteArray")
565 | name_list.push(0) ' Type
566 | hostname_ba = createObject("roByteArray")
567 | hostname_ba.fromAsciiString(hostname)
568 | name_list.append(short_to_bytes(hostname_ba.count()))
569 | name_list.append(hostname_ba)
570 | sni.append(short_to_bytes(name_list.count()))
571 | sni.append(name_list)
572 | extension.append(short_to_bytes(sni.count()))
573 | extension.append(sni)
574 | return extension
575 | end function
576 |
577 | ' Returns a roByteArray that conforms to the Random struct used RFC 5246
578 | tls_util._Random = function () as object
579 | random = createObject("roByteArray")
580 | time = createObject("roDateTime")
581 | random.append(int_to_bytes(time.asSeconds()))
582 | for byte = 0 to 27
583 | random.push(rnd(256) - 1)
584 | end for
585 | return random
586 | end function
587 |
588 | ' Send data as a TLSPlaintext record
589 | ' @param self TlsUtil
590 | ' @param content_type integer TLS Plaintext record type
591 | ' @param payload roByteArray bytes to send
592 | tls_util._send_record = function (self as object, content_type as integer, payload as object) as integer
593 | if self._socket = invalid or not self._socket.isWritable()
594 | printl("DEBUG", "TlsUtil: Send failed: socket is not connected")
595 | return -1
596 | end if
597 | records = payload.count() \ self._TLS_FRAGMENT_MAX_LENGTH
598 | if payload.count() mod self._TLS_FRAGMENT_MAX_LENGTH <> 0
599 | records++
600 | end if
601 | sent_bytes = 0
602 | for record_index = 0 to records - 1
603 | ' Fragment
604 | fragment_index = record_index * self._TLS_FRAGMENT_MAX_LENGTH
605 | max = fragment_index + self._TLS_FRAGMENT_MAX_LENGTH - 1
606 | if payload.count() - 1 < max
607 | max = payload.count() - 1
608 | end if
609 | fragment = createObject("roByteArray")
610 | for byte_index = fragment_index to max
611 | fragment[byte_index] = payload[byte_index]
612 | end for
613 | ' Record
614 | record = createObject("roByteArray")
615 | record.push(content_type)
616 | ' TLS version
617 | record.append(byte_array(self._TLS_VERSION))
618 | ' Length
619 | record.append(short_to_bytes(fragment.count()))
620 | ' Payload fragment
621 | record.append(fragment)
622 | printl("VERBOSE", "TlsUtil: Sending: " + record.toHexString())
623 | ' Send
624 | sent = self._socket.send(record, 0, record.count())
625 | if sent = record.count()
626 | sent_bytes += fragment.count()
627 | end if
628 | end for
629 | return sent_bytes
630 | end function
631 |
632 | ' Send data through the TLS tunnel over the socket
633 | ' @param self TlsUtil object
634 | ' @param payload roByteArray data to send
635 | ' @return bytes of payload sent
636 | tls_util.send = function (self as object, payload as object) as integer
637 | ' TODO encrypt data
638 | return self._send_record(self, self._RECORD_TYPE.APPLICATION_DATA, payload)
639 | end function
640 |
641 | ' Send string through the TLS tunnel over the socket
642 | ' @param self TlsUtil object
643 | ' @return bytes of payload sent
644 | tls_util.send_str = function (self as object, payload as string) as integer
645 | ba = createObject("roByteArray")
646 | ba.fromAsciiString(payload)
647 | return self.send(self, ba)
648 | end function
649 |
650 | ' Set internal buffer size
651 | ' @param self TlsUtil
652 | ' @param size integer
653 | tls_util.set_buffer_size = function (self as object, size as integer) as void
654 | if self.ready_state <> self.STATE_DISCONNECTED
655 | printl("WARN", "TlsUtil: Cannot set buffer size while connection is open")
656 | return
657 | end if
658 | self._BUFFER_SIZE = size
659 | end function
660 |
661 | return tls_util
662 | end function
--------------------------------------------------------------------------------
/src/web_socket_client/WebSocketClient.brs:
--------------------------------------------------------------------------------
1 | ' WebSocketClient.brs
2 | ' Copyright (C) 2018 Rolando Islas
3 | ' Released under the MIT license
4 | '
5 | ' BrightScript web socket client (RFC 6455)
6 |
7 | ' Create a new WebSocketClient instance
8 | function WebSocketClient() as object
9 | ws = {}
10 | ' Constants
11 | ws.STATE = {
12 | CONNECTING: 0,
13 | OPEN: 1,
14 | CLOSING: 2,
15 | CLOSED: 3
16 | }
17 | ws.OPCODE = {
18 | CONTINUATION: 0,
19 | TEXT: 1,
20 | BINARY: 2,
21 | CLOSE: 8,
22 | PING: 9,
23 | PONG: 10
24 | }
25 | ws._REGEX_URL = createObject("roRegex", "(\w+):\/\/([^/:]+)(?::(\d+))?(.*)?", "")
26 | ws._CHARS = "0123456789abcdefghijklmnopqrstuvwxyz".split("")
27 | ws._NL = chr(13) + chr(10)
28 | ws._HTTP_STATUS_LINE_REGEX = createObject("roRegex", "(HTTP\/\d+(?:.\d)?)\s(\d{3})\s(.*)", "")
29 | ws._HTTP_HEADER_REGEX = createObject("roRegex", "(\w+):\s?(.*)", "")
30 | ws._WS_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
31 | ws._FRAME_SIZE = 1024
32 | ' Variables
33 | ws._logger = Logger()
34 | ws._ready_state = ws.state.CLOSED
35 | ws._protocols = []
36 | ws._headers = []
37 | ws._secure = false
38 | ws._tls = invalid
39 | ws._socket = invalid
40 | ws._sec_ws_key = invalid
41 | ws._handshake = invalid
42 | ws._sent_handshake = false
43 | ws._has_received_handshake = false
44 | ws._buffer_size = cint(1024 * 1024 * 0.5)
45 | ws._data = createObject("roByteArray")
46 | ws._data[ws._buffer_size] = 0
47 | ws._data_size = 0
48 | ws._frame_data = createObject("roByteArray")
49 | ws._last_ping_time = 0
50 | ws._started_closing = 0
51 | ws._hostname = ""
52 | ws._ws_port = createObject("roMessagePort")
53 | ws._message_port = invalid
54 |
55 | ' ========== Getters ==========
56 |
57 | ' Get web socket ready state
58 | ' @see WebSocketClient.STATE for values
59 | ' @param self WebSocketClient
60 | ' @return integer ready state
61 | ws.get_ready_state = function () as integer
62 | return m._ready_state
63 | end function
64 |
65 | ' Get web socket protocols sent on initial handshake
66 | ' @param self WebSocketClient
67 | ' @return roArray web socket protocols
68 | ws.get_protocols = function () as object
69 | return m._protocols
70 | end function
71 |
72 | ' Get HTTP headers sent on initial HTTP request
73 | ' @param self WebSocketClient
74 | ' @return roArray HTTP headers - even indices are the header keys, the odd
75 | ' indices are the header values
76 | ws.get_headers = function () as object
77 | return m._headers
78 | end function
79 |
80 | ' Get the status of a secure web socket connection having been used
81 | ' @param self WebSocketClient
82 | ' @return boolean true if a TLS connection should be attempted before
83 | ' attempting a web socket handshake
84 | ws.get_secure = function () as boolean
85 | return m._secure
86 | end function
87 |
88 | ' Get the maximum buffer size for data sent to the websocket
89 | ' @param self WebSocketClient
90 | ' @return integer max buffer size in bytes
91 | ws.get_buffer_size = function () as integer
92 | return m._buffer_size
93 | end function
94 |
95 | ' ========== Setters ==========
96 |
97 | ' Set the web socket protocols to request from the web socket server
98 | ' @param self WebSocketClient
99 | ' @param roArray of protocol strings
100 | ws.set_protocols = function (protocols as object) as void
101 | m._protocols = protocols
102 | m._post_message("protocols", m._protocols)
103 | end function
104 |
105 | ' Set the headers to send on the initial HTTP request for a web socket
106 | ' @param self WebSocketClient
107 | ' @param roArray of header strings - The even indices should be the header
108 | ' keys and the odd are the header values
109 | ws.set_headers = function (headers as object) as void
110 | m._headers = headers
111 | m._post_message("headers", m._headers)
112 | end function
113 |
114 | ' Set if a TLS connection should be attempted before the web socket
115 | ' connection
116 | ' @param self WebSocketClient
117 | ' @param secure boolean should a secure connetion be attempted
118 | ws.set_secure = function (secure as boolean) as void
119 | m._secure = secure
120 | m._post_message("secure", m._secure)
121 | end function
122 |
123 | ' Set the buffer size for web socket data
124 | ' @param self WebSocketClient
125 | ' @param size integer max size of buffer in bytes
126 | ws.set_buffer_size = function (size as integer) as void
127 | if m._ready_state <> m.STATE.CLOSED
128 | m._logger.printl(m.WARN, "WebSocketClient: Cannot resize buffer on a socket that is not closed")
129 | return
130 | else if size < m._FRAME_SIZE
131 | m._logger.printl(m.WARN, "WebSocketClient: Cannot set buffer to a size smaller than " + m._FRAME_SIZE.toStr() + " bytes")
132 | return
133 | end if
134 | m._buffer_size = size
135 | m._buffer = createObject("roByteArray")
136 | m._buffer[m._buffer_size] = 0
137 | m._data_size = 0
138 | m._post_message("buffer_size", m._buffer_size)
139 | end function
140 |
141 | ' Set the message port that should be used to relay web socket events and
142 | ' field change updates
143 | ' @param self WebSocketClient
144 | ' @param port roMessagePort
145 | ws.set_message_port = function (port as object) as void
146 | m._message_port = port
147 | end function
148 |
149 | ' Set the log level
150 | ws.set_log_level = function (log_level as string) as void
151 | m._logger.set_log_level(log_level)
152 | end function
153 |
154 | ' ========== Main ==========
155 |
156 | ' Parses one websocket frame or HTTP message, if one is available
157 | ' @param self WebSocketClient
158 | ws.run = function () as void
159 | msg = wait(1, m._ws_port)
160 | ' Socket event
161 | if type(msg) = "roSocketEvent"
162 | m._send_handshake()
163 | m._read_socket_data()
164 | end if
165 | ' Play ping pong
166 | m._try_send_ping()
167 | m._try_force_close()
168 | end function
169 |
170 | ' Force close a connection after the close frame has been sent and no
171 | ' response was given, after a timeout
172 | ' @param self WebSocketClient
173 | ws._try_force_close = function () as void
174 | if m._ready_state = m.STATE.CLOSING and uptime(0) - m._started_closing >= 30
175 | m._close()
176 | end if
177 | end function
178 |
179 | ' Try to send a ping
180 | ' @param self WebSocketClient
181 | ws._try_send_ping = function () as void
182 | if m._ready_state = m.STATE.OPEN and uptime(0) - m._last_ping_time >= 1
183 | if m.send("", m.OPCODE.PING, true) < 0
184 | m._close()
185 | m._error(16, "Lost connection")
186 | end if
187 | m._last_ping_time = uptime(0)
188 | end if
189 | end function
190 |
191 | ' Sends data through the socket
192 | ' @param self WebSocketClient
193 | ' @param message array - should contain one element of type roString or roArray
194 | ' cannot exceed 125 bytes if the the specified opcode
195 | ' is for a control frame
196 | ' @param _opcode int - define a **control** opcode data opcodes are determined
197 | ' by the type of the passed message
198 | ' @param silent boolean - does not send on_error event
199 | ' @param do_close boolean - if true, a close frame will be sent on errors
200 | ' @return integer amount of bytes sent
201 | ws.send = function (message as dynamic, _opcode = -1 as integer, silent = false as boolean, do_close = true as boolean) as integer
202 | if m._socket = invalid or not m._socket.isWritable()
203 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: socket is closed")
204 | return -1
205 | end if
206 | if m._ready_state <> m.STATE.OPEN
207 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: connection not open")
208 | return -1
209 | end if
210 | if type(message) = "roString" or type(message) = "String" or type(message) = "roByteArray"
211 | message = [message]
212 | end if
213 | if message.count() <> 1
214 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: too many parameters")
215 | return -1
216 | end if
217 | bytes = createObject("roByteArray")
218 | opcode = 0
219 | if type(message[0]) = "roString" or type(message[0]) = "String"
220 | bytes.fromAsciiString(message[0])
221 | opcode = m.OPCODE.TEXT
222 | else if type(message[0]) = "roArray" or type(message[0]) = "roByteArray"
223 | for each byte in message[0]
224 | bytes.push(byte)
225 | end for
226 | opcode = m.OPCODE.BINARY
227 | else
228 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: invalid parameter type")
229 | return -1
230 | end if
231 | if _opcode > -1 and (_opcode >> 3) <> 1
232 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: specified opcode was not a control opcode")
233 | return -1
234 | else if _opcode > -1
235 | if bytes.count() > 125
236 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: control frames cannot have a payload larger than 125 bytes")
237 | return -1
238 | end if
239 | opcode = _opcode
240 | end if
241 | ' Frame message
242 | frame_count = bytes.count() \ m._FRAME_SIZE
243 | if bytes.count() mod m._FRAME_SIZE <> 0 or frame_count = 0
244 | frame_count++
245 | end if
246 | total_sent = 0
247 | for frame_index = 0 to frame_count - 1
248 | ' Get sub array of payload bytes
249 | payload = createObject("roByteArray")
250 | max = bytes.count() - 1
251 | if (frame_index + 1) * m._FRAME_SIZE - 1 < max
252 | max = (frame_index + 1) * m._FRAME_SIZE - 1
253 | end if
254 | for byte_index = frame_index * m._FRAME_SIZE to max
255 | payload.push(bytes[byte_index])
256 | end for
257 | ' Construct frame
258 | frame = createObject("roByteArray")
259 | ' FIN(1) RSV1(1) RSV2(1) RSV3(1) opcode(4)
260 | final = 0
261 | if frame_index = frame_count - 1
262 | final = &h80
263 | end if
264 | opcode_frame = m.OPCODE.CONTINUATION
265 | if frame_index = 0
266 | opcode_frame = opcode
267 | end if
268 | frame.push(final or opcode_frame)
269 | ' mask(1) payload_length(7)
270 | length_7 = payload.count()
271 | if payload.count() > &hffff
272 | length_7 = 127
273 | else if payload.count() > 125
274 | length_7 = 126
275 | end if
276 | frame.push(&h80 or length_7)
277 | ' payload_length_continuation(64)
278 | ' 16 bit uint
279 | if length_7 = 126
280 | frame.append(short_to_bytes(payload.count()))
281 | ' 64 bit uint
282 | else if length_7 = 127
283 | frame.append(long_to_bytes(payload.count()))
284 | end if
285 | ' masking key(32)
286 | mask = rnd(&hffff)
287 | mask_bytes = int_to_bytes(mask)
288 | frame.append(mask_bytes)
289 | ' Mask payload
290 | masked_payload = createObject("roByteArray")
291 | for byte_index = 0 to payload.count() - 1
292 | masking_key = mask_bytes[byte_index mod 4]
293 | byte = payload[byte_index]
294 | masked_byte = xor(byte, masking_key)
295 | masked_payload.push(masked_byte)
296 | end for
297 | frame.append(masked_payload)
298 | ' Send frame
299 | if _opcode <> m.OPCODE.PING
300 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sending frame: " + frame.toHexString())
301 | end if
302 | sent = 0
303 | if m._secure
304 | sent = m._tls.send(m._tls, frame)
305 | else
306 | sent = m._socket.send(frame, 0, frame.count())
307 | end if
308 | if _opcode <> m.OPCODE.PING
309 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sent " + sent.toStr() + " bytes")
310 | end if
311 | total_sent += sent
312 | if sent <> frame.count()
313 | if do_close
314 | m._close()
315 | end if
316 | if not silent
317 | m._error(14, "Failed to send data")
318 | end if
319 | return total_sent
320 | end if
321 | end for
322 | return total_sent
323 | end function
324 |
325 | ' Send the initial websocket handshake if it has not been sent
326 | ' @param self WebSocketClient
327 | ws._send_handshake = function () as void
328 | if m._socket = invalid or not m._socket.isWritable() or m._sent_handshake or m._handshake = invalid or m._tls.ready_state = m._tls.STATE_CONNECTING
329 | return
330 | end if
331 | if m._secure and m._tls.ready_state = m._tls.STATE_DISCONNECTED
332 | m._tls.connect(m._tls, m._hostname)
333 | else
334 | m._logger.printl(m._logger.VERBOSE, m._handshake)
335 | sent = 0
336 | if m._secure
337 | sent = m._tls.send_str(m._tls, m._handshake)
338 | else
339 | sent = m._socket.sendStr(m._handshake)
340 | end if
341 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sent " + sent.toStr() + " bytes")
342 | if sent = -1
343 | m._close()
344 | m._error(4, "Failed to send data: " + m._socket.status().toStr())
345 | return
346 | end if
347 | m._sent_handshake = true
348 | end if
349 | end function
350 |
351 | ' Read socket data
352 | ' @param self WebSocketClient
353 | ws._read_socket_data = function () as void
354 | if m._socket = invalid or m._ready_state = m.STATE.CLOSED or (m._secure and m._tls.ready_state = m._tls.STATE_DISCONNECTED)
355 | return
356 | end if
357 | buffer = createObject("roByteArray")
358 | buffer[1024] = 0
359 | bytes_received = 0
360 | if m._socket.isReadable() and m._socket.getCountRcvBuf() > 0
361 | bytes_received = m._socket.receive(buffer, 0, 1024)
362 | end if
363 | if bytes_received < 0
364 | m._close()
365 | m._error(15, "Failed to read from socket")
366 | return
367 | end if
368 | if m._secure
369 | buffer = m._tls.read(m._tls, buffer, bytes_received)
370 | if buffer = invalid
371 | m._close()
372 | m._error(17, "TLS error")
373 | return
374 | end if
375 | bytes_received = buffer.count()
376 | end if
377 | buffer_index = 0
378 | for byte_index = m._data_size to m._data_size + bytes_received - 1
379 | m._data[byte_index] = buffer[buffer_index]
380 | buffer_index++
381 | end for
382 | m._data_size += bytes_received
383 | m._data[m._data_size] = 0
384 | ' WebSocket frames
385 | if m._has_received_handshake
386 | ' Wait for at least the payload 7-bit size
387 | if m._data_size < 2
388 | return
389 | end if
390 | final = (m._data[0] >> 7) = 1
391 | opcode = (m._data[0] and &hf)
392 | control = (opcode >> 3) = 1
393 | masked = (m._data[1] >> 7) = 1
394 | payload_size_7 = m._data[1] and &h7f
395 | payload_size = payload_size_7
396 | payload_index = 2
397 | mask = 0
398 | if payload_size_7 = 126
399 | ' Wait for the 16-bit payload size
400 | if m._data_size < 4
401 | return
402 | end if
403 | payload_size = bytes_to_short(m._data[2], m._data[3])
404 | payload_index += 2
405 | else if payload_size_7 = 127
406 | ' Wait for the 64-bit payload size
407 | if m._data_size < 10
408 | return
409 | end if
410 | payload_size = bytes_to_long(m._data[2], m._data[3], m._data[4], m._data[5], m._data[6], m._data[7], m._data[8], m._data[9])
411 | payload_index += 8
412 | end if
413 | if masked
414 | ' Wait for mask int
415 | if m._data_size < payload_index
416 | return
417 | end if
418 | mask = bytes_to_int(m._data[payload_index], m._data[payload_index + 1], m._data[payload_index + 2], m._data[payload_index + 3])
419 | payload_index += 4
420 | end if
421 | ' Wait for payload
422 | if m._data_size < payload_index + payload_size
423 | return
424 | end if
425 | payload = createObject("roByteArray")
426 | for byte_index = payload_index to payload_index + payload_size - 1
427 | payload.push(m._data[byte_index])
428 | end for
429 | ' Handle control frame
430 | if control
431 | m._handle_frame(opcode, payload)
432 | ' Handle data frame
433 | else if final
434 | full_payload = createObject("roByteArray")
435 | full_payload.append(m._frame_data)
436 | full_payload.append(payload)
437 | m._handle_frame(opcode, full_payload)
438 | m._frame_data.clear()
439 | ' Check for continuation frame
440 | else
441 | m._frame_data.append(payload)
442 | end if
443 | ' Save start of next frame
444 | if m._data_size > payload_index + payload_size
445 | data = createObject("roByteArray")
446 | data.append(m._data)
447 | m._data.clear()
448 | for byte_index = payload_index + payload_size to m._data_size - 1
449 | m._data.push(data[byte_index])
450 | end for
451 | else
452 | m._data.clear()
453 | end if
454 | ' HTTP/Handshake
455 | else
456 | data = m._data.toAsciiString()
457 | http_delimiter = m._NL + m._NL
458 | if data.len() <> data.replace(http_delimiter, "").len()
459 | split = data.split(http_delimiter)
460 | message = split[0]
461 | data = ""
462 | for split_index = 1 to split.count() - 1
463 | data += split[split_index]
464 | if split_index < split.count() - 1 or split[split_index].right(4) = m._NL + m._NL
465 | data += m._NL + m._NL
466 | end if
467 | end for
468 | ' Handle the message
469 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Message: " + message)
470 | m._handle_handshake_response(message)
471 | end if
472 | m._data.fromAsciiString(data)
473 | end if
474 | m._data_size = m._data.count()
475 | m._data[m._buffer_size] = 0
476 | end function
477 |
478 | ' Handle the handshake message or die trying
479 | ' @param self WebSocketClient
480 | ' @param string http response header
481 | ws._handle_handshake_response = function (message as string) as void
482 | lines = message.split(m._NL)
483 | if lines.count() = 0
484 | m._close()
485 | m._error(5, "Invalid handshake: Missing status line")
486 | return
487 | end if
488 | ' Check status line
489 | if not m._HTTP_STATUS_LINE_REGEX.isMatch(lines[0])
490 | m._close()
491 | m._error(6, "Invalid handshake: Status line malformed")
492 | return
493 | end if
494 | status_line = m._HTTP_STATUS_LINE_REGEX.match(lines[0])
495 | if status_line[1] <> "HTTP/1.1"
496 | m._close()
497 | m._error(7, "Invalid handshake: Response version mismatch. Expected HTTP/1.1, got " + status_line[0])
498 | return
499 | end if
500 | if status_line[2] <> "101"
501 | m._close()
502 | m._error(8, "Invalid handshake: HTTP status code is not 101: Received " + status_line[2])
503 | return
504 | end if
505 | ' Search headers
506 | protocol = ""
507 | for header_line_index = 1 to lines.count() - 1:
508 | if m._HTTP_HEADER_REGEX.isMatch(lines[header_line_index])
509 | header = m._HTTP_HEADER_REGEX.match(lines[header_line_index])
510 | ' Upgrade
511 | if ucase(header[1]) = "UPGRADE" and ucase(header[2]) <> "WEBSOCKET"
512 | m._close()
513 | m._error(9, "Invalid handshake: invalid upgrade header: " + header[2])
514 | return
515 | ' Connection
516 | else if ucase(header[1]) = "CONNECTION" and ucase(header[2]) <> "UPGRADE"
517 | m._close()
518 | m._error(10, "Invalid handshake: invalid connection header: " + header[2])
519 | return
520 | ' Sec-WebSocket-Accept
521 | else if ucase(header[1]) = "SEC-WEBSOCKET-ACCEPT"
522 | expected_array = createObject("roByteArray")
523 | expected_array.fromAsciiString(m._sec_ws_key + m._WS_ACCEPT_GUID)
524 | digest = createObject("roEVPDigest")
525 | digest.setup("sha1")
526 | expected = digest.process(expected_array)
527 | if expected <> header[2].trim()
528 | m._close()
529 | m._error(11, "Invalid handshake: Sec-WebSocket-Accept value is invalid: " + header[2])
530 | return
531 | end if
532 | ' Sec-WebSocket-Extensions
533 | else if ucase(header[1]) = "SEC-WEBSOCKET-EXTENSIONS" and header[2] <> ""
534 | m._close()
535 | m._error(12, "Invalid handshake: Sec-WebSocket-Extensions value is invalid: " + header[2])
536 | return
537 | ' Sec-WebSocket-Protocol
538 | else if ucase(header[1]) = "SEC-WEBSOCKET-PROTOCOL"
539 | p = header[2].trim()
540 | was_requested = false
541 | for each requested_protocol in m._protocols
542 | if requested_protocol = p
543 | was_requested = true
544 | end if
545 | end for
546 | if not was_requested
547 | m._close()
548 | m._error(13, "Invalid handshake: Sec-WebSocket-Protocol contains a protocol that was not requested: " + p)
549 | return
550 | end if
551 | protocol = p
552 | end if
553 | end if
554 | end for
555 | m._has_received_handshake = true
556 | m._state(m.STATE.OPEN)
557 | m._post_message("on_open", {
558 | protocol: protocol
559 | })
560 | end function
561 |
562 | ' Post a message to the message port
563 | ' @param self WebSocketClient
564 | ' @param id string message event id
565 | ' @param data dynamic message data
566 | ws._post_message = function (id as string, data as dynamic) as void
567 | if m._message_port <> invalid
568 | m._message_port.postMessage({
569 | id: id,
570 | data: data
571 | })
572 | end if
573 | end function
574 |
575 | ' Handle a web socket frame
576 | ' @param self WebSocketClient
577 | ' @param opcode int opcode
578 | ' @param payload roByteArray payload data
579 | ws._handle_frame = function (opcode as integer, payload as object) as void
580 | if opcode <> m.OPCODE.PONG
581 | frame_print = "WebSocketClient: " + "Received frame:" + m._NL
582 | frame_print += " Opcode: " + opcode.toStr() + m._NL
583 | frame_print += " Payload: " + payload.toHexString()
584 | m._logger.printl(m._logger.VERBOSE, frame_print)
585 | end if
586 | ' Close
587 | if opcode = m.OPCODE.CLOSE
588 | m._close()
589 | return
590 | ' Ping
591 | else if opcode = m.OPCODE.PING
592 | m.send("", m.OPCODE.PONG)
593 | return
594 | ' Text
595 | else if opcode = m.OPCODE.TEXT
596 | m._post_message("on_message", {
597 | type: 0,
598 | message: payload.toAsciiString()
599 | })
600 | return
601 | ' Data
602 | else if opcode = m.OPCODE.BINARY
603 | payload_array = []
604 | for each byte in payload
605 | payload_array.push(byte)
606 | end for
607 | m._post_message("on_message", {
608 | type: 1,
609 | message: payload_array
610 | })
611 | return
612 | end if
613 | end function
614 |
615 | ' Generate a 20 character [A-Za-z0-9] random string and base64 encode it
616 | ' @param self WebSocketClient
617 | ' @return string random 20 character base64 encoded string
618 | ws._generate_sec_ws_key = function () as string
619 | sec_ws_key = ""
620 | for char_index = 0 to 19
621 | char = m._CHARS[rnd(m._CHARS.count()) - 1]
622 | if rnd(2) = 1
623 | char = ucase(char)
624 | end if
625 | sec_ws_key += char
626 | end for
627 | ba = createObject("roByteArray")
628 | ba.fromAsciiString(sec_ws_key)
629 | return ba.toBase64String()
630 | end function
631 |
632 | ' Connect to the specified URL
633 | ' @param self WebSocketClient
634 | ' @param url_string web socket url to connect
635 | ' Format: ws://example.org:80/
636 | ' If the port is not specified the port will be assumed
637 | ' from the protocol (ws: 80, wss: 443).
638 | ws.open = function (url as string) as void
639 | if m._ready_state <> m.STATE.CLOSED
640 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Tried to open a web socket that was already open")
641 | return
642 | end if
643 | if m._REGEX_URL.isMatch(url)
644 | match = m._REGEX_URL.match(url)
645 | ws_type = lcase(match[1])
646 | host = lcase(match[2])
647 | port = match[3]
648 | path = match[4]
649 | m._hostname = host
650 | ' Port
651 | if port <> ""
652 | port = val(port, 10)
653 | else if ws_type = "wss"
654 | m.set_secure(true)
655 | port = 443
656 | else if ws_type = "ws"
657 | port = 80
658 | else
659 | m._close()
660 | m._error(0, "Invalid web socket type specified: " + ws_type)
661 | return
662 | end if
663 | ' Path
664 | if path = ""
665 | path = "/"
666 | end if
667 | ' WS(S) to HTTP(S)
668 | scheme = ws_type.replace("ws", "http")
669 | ' Construct handshake
670 | m._sec_ws_key = m._generate_sec_ws_key()
671 | protocols = ""
672 | for each proto in m._protocols
673 | protocols += proto + ", "
674 | end for
675 | if protocols <> ""
676 | protocols = protocols.left(len(protocols) - 2)
677 | end if
678 | handshake = "GET " + path + " HTTP/1.1" +m._NL
679 | handshake += "Host: " + host + ":" + port.toStr() + m._NL
680 | handshake += "Upgrade: websocket" + m._NL
681 | handshake += "Connection: Upgrade" + m._NL
682 | handshake += "Sec-WebSocket-Key: " + m._sec_ws_key + m._NL
683 | if protocols <> ""
684 | handshake += "Sec-WebSocket-Protocol: " + protocols + m._NL
685 | end if
686 | ' handshake += "Sec-WebSocket-Extensions: " + m._NL
687 | handshake += "Sec-WebSocket-Version: 13" + m._NL
688 | handshake += m._get_parsed_user_headers()
689 | handshake += m._NL
690 | m._handshake = handshake
691 | ' Create socket
692 | m._state(m.STATE.CONNECTING)
693 | address = createObject("roSocketAddress")
694 | address.setHostName(host)
695 | address.setPort(port)
696 | if not address.isAddressValid()
697 | m._close()
698 | m._error(2, "Invalid hostname")
699 | return
700 | end if
701 | m._data_size = 0
702 | m._socket = createObject("roStreamSocket")
703 | m._socket.notifyReadable(true)
704 | m._socket.notifyWritable(true)
705 | m._socket.notifyException(true)
706 | m._socket.setMessagePort(m._ws_port)
707 | m._socket.setSendToAddress(address)
708 | m._sent_handshake = false
709 | m._has_received_handshake = false
710 | m._tls = TlsUtil(m._socket)
711 | m._tls.set_buffer_size(m._tls, m._buffer_size)
712 | if not m._socket.connect()
713 | m._close()
714 | m._error(3, "Socket failed to connect: " + m._socket.status().toStr())
715 | return
716 | end if
717 | else
718 | m._close()
719 | m._error(1, "Invalid URL specified")
720 | end if
721 | end function
722 |
723 | ' Parse header array and return a string of headers delimited by CRLF
724 | ' @param self WebSocketClient
725 | ws._get_parsed_user_headers = function () as string
726 | if m._headers = invalid or m._headers.count() = 0 or (m._headers.count() mod 2) = 1
727 | return ""
728 | end if
729 | header_string = ""
730 | for header_index = 0 to m._headers.count() - 1 step 2
731 | header = m._headers[header_index]
732 | value = m._headers[header_index + 1]
733 | header_string += header + ": " + value + m._NL
734 | end for
735 | return header_string
736 | end function
737 |
738 | ' Set ready state
739 | ' @param self WebSocketClient
740 | ' @param state
741 | ws._state = function (_state as integer) as void
742 | m._ready_state = _state
743 | m._post_message("ready_state", _state)
744 | end function
745 |
746 | ' Send an error event
747 | ' Posts an on_error message to the message port
748 | ' @param self WebSocketClient
749 | ' @param code integer error code
750 | ' @param message string error message
751 | ws._error = function (code as integer, message as string) as void
752 | m._logger.printl(m._logger.EXTRA, "WebSocketClient: Error: " + message)
753 | m._post_message("on_error", {
754 | code: code,
755 | message: message
756 | })
757 | end function
758 |
759 | ' Close the socket
760 | ' @param WebSocketClient
761 | ' @param code integer - status code
762 | ' @param reason roByteArray - reason
763 | ws._close = function (code = 1000 as integer, reason = invalid as object) as void
764 | if m._socket <> invalid
765 | ' Send the closing frame
766 | if m._ready_state = m.STATE.OPEN
767 | m._send_close_frame(code, reason)
768 | m._started_closing = uptime(0)
769 | m._state(m.STATE.CLOSING)
770 | else
771 | m._state(m.STATE.CLOSED)
772 | m._post_message("on_close", "")
773 | m._socket.close()
774 | end if
775 | else if m._ready_state <> m.STATE.CLOSED
776 | m._state(m.STATE.CLOSED)
777 | end if
778 | end function
779 |
780 | ' Send a close frame to the server to initiate a close
781 | ' @param self WebSocketClient
782 | ' @param code integer - status code
783 | ' @param reason roByteArray - reason
784 | ws._send_close_frame = function (code as integer, reason as dynamic) as void
785 | message = createObject("roByteArray")
786 | message.push(code >> 8)
787 | message.push(code)
788 | if reason <> invalid
789 | message.append(reason)
790 | end if
791 | m.send(message, m.OPCODE.CLOSE, true, false)
792 | end function
793 |
794 | ' Close the socket
795 | ' @self WebSocketClient
796 | ' @param reason array - array [code as integer, message as roString]
797 | ws.close = function (params as object) as void
798 | code = 1000
799 | reason = createObject("roByteArray")
800 | if params.count() > 0
801 | code = params[0]
802 | if type(code) <> "Integer" or code > &hffff
803 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: close expects value at array index 0 to be a 16-bit integer")
804 | end if
805 | end if
806 | if params.count() > 1
807 | message = params[1]
808 | if type(message) <> "roString" or type(message) <> "String"
809 | reason.fromAsciiString(message)
810 | else
811 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: close expects value at array index 1 to be a string")
812 | end if
813 | end if
814 | m._close(code, reason)
815 | end function
816 |
817 | ' Return constructed instance
818 | return ws
819 | end function
820 |
--------------------------------------------------------------------------------
/src/web_socket_client/WebSocketClientTask.brs:
--------------------------------------------------------------------------------
1 | ' WebSocketClientTask.brs
2 | ' Copyright (C) 2018 Rolando Islas
3 | ' Released under the MIT license
4 | '
5 | ' BrightScript, SceneGraph Task wrapper for the web socket client
6 |
7 | ' Entry point
8 | function init() as void
9 | m.ws = WebSocketClient()
10 | m.port = createObject("roMessagePort")
11 | m.ws.set_message_port(m.port)
12 | ' Fields
13 | m.top.STATE_CONNECTING = m.ws.STATE.CONNECTING
14 | m.top.STATE_OPEN = m.ws.STATE.OPEN
15 | m.top.STATE_CLOSING = m.ws.STATE.CLOSING
16 | m.top.STATE_CLOSED = m.ws.STATE.CLOSED
17 | m.top.ready_state = m.ws.get_ready_state()
18 | m.top.protocols = m.ws.get_protocols()
19 | m.top.headers = m.ws.get_headers()
20 | m.top.secure = m.ws.get_secure()
21 | m.top.buffer_size = m.ws.get_buffer_size()
22 | ' Event listeners
23 | m.top.observeField("open", m.port)
24 | m.top.observeField("send", m.port)
25 | m.top.observeField("close", m.port)
26 | m.top.observeField("buffer_size", m.port)
27 | m.top.observeField("protocols", m.port)
28 | m.top.observeField("headers", m.port)
29 | m.top.observeField("secure", m.port)
30 | m.top.observeField("log_level", m.port)
31 | ' Task init
32 | m.top.functionName = "run"
33 | m.top.control = "RUN"
34 | end function
35 |
36 | ' Main task loop
37 | function run() as void
38 | while true
39 | ' Check task messages
40 | msg = wait(1, m.port)
41 | ' Field event
42 | if type(msg) = "roSGNodeEvent"
43 | if msg.getField() = "open"
44 | m.ws.open(msg.getData())
45 | else if msg.getField() = "send"
46 | m.ws.send(msg.getData())
47 | else if msg.getField() = "close"
48 | m.ws.close(msg.getData())
49 | else if msg.getField() = "buffer_size"
50 | m.ws.set_buffer_size(msg.getData())
51 | else if msg.getField() = "protocols"
52 | m.ws.set_protocols(msg.getData())
53 | else if msg.getField() = "headers"
54 | m.ws.set_headers(msg.getData())
55 | else if msg.getField() = "secure"
56 | m.ws.set_secure(msg.getData())
57 | else if msg.getField() = "log_level"
58 | m.ws.set_log_level(msg.getData())
59 | end if
60 | ' WebSocket event
61 | else if type(msg) = "roAssociativeArray"
62 | if msg.id = "on_open"
63 | m.top.on_open = msg.data
64 | else if msg.id = "on_close"
65 | m.top.on_close = msg.data
66 | else if msg.id = "on_message"
67 | m.top.on_message = msg.data
68 | else if msg.id = "on_error"
69 | m.top.on_error = msg.data
70 | else if msg.id = "ready_state"
71 | m.top.ready_state = msg.data
72 | else if msg.id = "buffer_size"
73 | m.top.unobserveField("buffer_size")
74 | m.top.buffer_size = msg.data
75 | m.top.observeField("buffer_size", m.task_port)
76 | else if msg.id = "protocols"
77 | m.top.unobserveField("protocols")
78 | m.top.protocols = msg.data
79 | m.top.observeField("protocols", m.task_port)
80 | else if msg.id = "headers"
81 | m.top.unobserveField("headers")
82 | m.top.headers = msg.data
83 | m.top.observeField("headers", m.task_port)
84 | else if msg.id = "secure"
85 | m.top.unobserveField("secure")
86 | m.top.secure = msg.data
87 | m.top.observeField("secure", m.task_port)
88 | end if
89 | end if
90 | m.ws.run()
91 | end while
92 | end function
93 |
--------------------------------------------------------------------------------
/test/.gitignore:
--------------------------------------------------------------------------------
1 | out/
2 | .buildpath
3 | .project
4 | bright_web_socket.json
5 |
--------------------------------------------------------------------------------
/test/.rokudevignore:
--------------------------------------------------------------------------------
1 | bright_web_socket.json.example
2 |
--------------------------------------------------------------------------------
/test/bright_web_socket.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "log_level": "FATAL/WARN/INFO/DEBUG/EXTRA/VERBOSE"
3 | }
4 |
--------------------------------------------------------------------------------
/test/components/Main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test/components/WebSocketClient.xml:
--------------------------------------------------------------------------------
1 | ../../src/WebSocketClient.xml
--------------------------------------------------------------------------------
/test/components/web_socket_client/ByteUtil.brs:
--------------------------------------------------------------------------------
1 | ../../../src/web_socket_client/ByteUtil.brs
--------------------------------------------------------------------------------
/test/components/web_socket_client/Logger.brs:
--------------------------------------------------------------------------------
1 | ../../../src/web_socket_client/Logger.brs
--------------------------------------------------------------------------------
/test/components/web_socket_client/TlsUtil.brs:
--------------------------------------------------------------------------------
1 | ../../../src/web_socket_client/TlsUtil.brs
--------------------------------------------------------------------------------
/test/components/web_socket_client/WebSocketClient.brs:
--------------------------------------------------------------------------------
1 | ../../../src/web_socket_client/WebSocketClient.brs
--------------------------------------------------------------------------------
/test/components/web_socket_client/WebSocketClientTask.brs:
--------------------------------------------------------------------------------
1 | ../../../src/web_socket_client/WebSocketClientTask.brs
--------------------------------------------------------------------------------
/test/images/icon_focus_hd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/icon_focus_hd.png
--------------------------------------------------------------------------------
/test/images/icon_focus_sd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/icon_focus_sd.png
--------------------------------------------------------------------------------
/test/images/icon_side_hd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/icon_side_hd.png
--------------------------------------------------------------------------------
/test/images/icon_side_sd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/icon_side_sd.png
--------------------------------------------------------------------------------
/test/images/splash_fhd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/splash_fhd.jpg
--------------------------------------------------------------------------------
/test/images/splash_hd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/splash_hd.jpg
--------------------------------------------------------------------------------
/test/images/splash_sd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolandoislas/BrightWebSocket/acd8840c18e5a7cde55adccc98c67ebb563a2c8f/test/images/splash_sd.jpg
--------------------------------------------------------------------------------
/test/locale/en_US/manifest:
--------------------------------------------------------------------------------
1 | title=BrightWebSocket
2 |
--------------------------------------------------------------------------------
/test/locale/en_US/translations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test/manifest:
--------------------------------------------------------------------------------
1 | title=BrightWebSocket
2 | major_version=1
3 | minor_version=0
4 | build_version=0
5 | mm_icon_focus_hd=pkg:/images/icon_focus_hd.png
6 | mm_icon_focus_sd=pkg:/images/icon_focus_sd.png
7 | mm_icon_side_hd=pkg:/images/icon_side_hd.png
8 | mm_icon_side_sd=pkg:/images/icon_side_sd.png
9 | splash_screen_fhd=pkg:/images/splash_fhd.jpg
10 | splash_screen_hd=pkg:/images/splash_hd.jpg
11 | splash_screen_sd=pkg:/images/splash_sd.jpg
12 | splash_color=#121212
13 | splash_min_time=0
14 | ui_resolutions=hd
15 | mm_icon_focus_fhd=pkg:/images/icon_focus_hd.png
16 |
--------------------------------------------------------------------------------
/test/source/Main.brs:
--------------------------------------------------------------------------------
1 | ' This source file is the entry point for the application and is not required
2 | ' to use the library. See readme.md for more info.
3 |
4 | ' Entry point for the application
5 | function main(args as dynamic) as void
6 | screen = createObject("roSGScreen")
7 | port = createObject("roMessagePort")
8 | screen.setMessagePort(port)
9 | scene = screen.createScene("Main")
10 | screen.show()
11 | scene.setFocus(true)
12 | scene.backExitsScene = false
13 | while true
14 | msg = wait(0, port)
15 | if type(msg) = "roSGScreenEvent" and msg.isScreenClosed()
16 | return
17 | end if
18 | end while
19 | end function
20 |
21 | ' Entry point for the main scene
22 | function init() as void
23 | m.ws = createObject("roSGNode", "WebSocketClient")
24 | m.ws.observeField("on_open", "on_open")
25 | m.ws.observeField("on_close", "on_close")
26 | m.ws.observeField("on_message", "on_message")
27 | m.ws.observeField("on_error", "on_error")
28 | m.ws.protocols = []
29 | m.ws.headers = []
30 | m.ws.log_level = "INFO"
31 | m.SERVER = "ws://echo.websocket.org/"
32 | m.ws.open = m.SERVER
33 | m.reinitialize = false
34 | end function
35 |
36 | ' Key events
37 | function onKeyEvent(key as string, press as boolean) as boolean
38 | if key = "back" and press
39 | print "Closing websocket"
40 | m.ws.close = [1000, "optional"]
41 | else if key = "OK" and press
42 | print "Reinitializing websocket"
43 | if m.ws.ready_state <> m.ws.STATE_CLOSED
44 | m.ws.close = []
45 | m.reinitialize = true
46 | else
47 | m.ws.open = m.SERVER
48 | end if
49 | end if
50 | end function
51 |
52 | ' Socket open event
53 | function on_open(event as object) as void
54 | print "WebSocket opened"
55 | print tab(2)"Protocol: " + event.getData().protocol
56 | send_test_data()
57 | end function
58 |
59 | ' Send test data to the websocket
60 | function send_test_data() as void
61 | print "Sending string: test string"
62 | m.ws.send = ["test string"]
63 | test_binary = []
64 | for bin = 0 to 3
65 | test_binary.push(bin)
66 | end for
67 | print "Sending data: 00010203"
68 | m.ws.send = [test_binary]
69 | end function
70 |
71 | ' Socket close event
72 | function on_close(event as object) as void
73 | print "WebSocket closed"
74 | if m.reinitialize
75 | m.ws.open = m.SERVER
76 | m.reinitialize = false
77 | end if
78 | end function
79 |
80 | ' Socket message event
81 | function on_message(event as object) as void
82 | message = event.getData().message
83 | if type(message) = "roString"
84 | print "WebSocket text message: " + message
85 | else
86 | ba = createObject("roByteArray")
87 | for each byte in message
88 | ba.push(byte)
89 | end for
90 | print "WebSocket binary message: " + ba.toHexString()
91 | end if
92 | end function
93 |
94 | ' Socket Error event
95 | function on_error(event as object) as void
96 | print "WebSocket error"
97 | print event.getData()
98 | end function
99 |
--------------------------------------------------------------------------------
/util/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/util/bulk_send/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "node ./websocket_bulk_send_server.js"
4 | },
5 | "dependencies": {
6 | "ws": "3.0.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/util/bulk_send/readme.md:
--------------------------------------------------------------------------------
1 | # WebSocket Bulk Send Server
2 |
3 | Web socket server that sends any client that connects to it a lot of message in
4 | a short time.
5 |
6 | # Running
7 |
8 | Install the node "ws" library
9 |
10 | 1. `npm install` from the build_send folder root
11 | 2. `node websocket_bulk_send_server.js`
12 |
13 | The server is started on port 5000.
14 |
--------------------------------------------------------------------------------
/util/bulk_send/websocket_bulk_send_server.js:
--------------------------------------------------------------------------------
1 | WebSocketServer = require("ws").Server
2 |
3 | ws = new WebSocketServer({port: 5000})
4 | ws.on("connection", function (socket) {
5 | socket.on("error", function (error) {
6 | console.error(error);
7 | });
8 | console.log("Sending one thousand requests to client")
9 | for (var messageIndex = 0; messageIndex < 1000; messageIndex++)
10 | socket.send("How now brown cow. " + messageIndex)
11 | console.log("Finished sending")
12 | });
13 | console.log("Started bulk send websocket server on port 5000")
14 |
--------------------------------------------------------------------------------
/util/echo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "node ./websocket_echo_server.js"
4 | },
5 | "dependencies": {
6 | "ws": "3.0.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/util/echo/readme.md:
--------------------------------------------------------------------------------
1 | # WebSocket Echo Server
2 |
3 | Tiny node script to create a websocket echo server
4 |
5 | # Running
6 |
7 | Install the node "ws" library
8 |
9 | 1. `npm install` from the echo folder root
10 | 2. `node websocket_echo_server.js`
11 |
12 | The server is started on port 5000 and echos both binary and text frames.
13 |
--------------------------------------------------------------------------------
/util/echo/websocket_echo_server.js:
--------------------------------------------------------------------------------
1 | WebSocketServer = require("ws").Server
2 |
3 | ws = new WebSocketServer({port: 5000})
4 | ws.on("connection", function (socket) {
5 | socket.on("message", function (message) {
6 | console.log(message)
7 | socket.send(message)
8 | });
9 | socket.on("error", function (error) {
10 | console.error(error);
11 | });
12 | });
13 | console.log("Started echo websocket server on port 5000")
14 |
--------------------------------------------------------------------------------