├── .gitignore
├── license.txt
├── readme.md
├── src
├── WebSocketClient.xml
└── web_socket_client
│ ├── ByteUtil.brs
│ ├── Logger.brs
│ ├── TlsUtil.brs
│ ├── WebSocketClient.brs
│ └── WebSocketClientTask.brs
├── test
├── .gitignore
├── 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/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/.gitignore
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | BrightWebSocket - BrightScript websocket library
4 | Copyright © 2018 Rolando Islas
5 | Copyright © 2019 Suitest s.r.o.
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of
8 | this software and associated documentation files (the “Software”), to deal in
9 | the Software without restriction, including without limitation the rights to
10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11 | of the Software, and to permit persons to whom the Software is furnished to do
12 | so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23 | IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # BrightWebSocket
2 |
3 | A ScreneGraph websocket client library written in BrightScript. It is written to work in a separate task to not affect the main thread of the application.
4 |
5 | Many thanks to [rolandoislas/BrightWebSocket](https://github.com/rolandoislas/BrightWebSocket) who originally developed this library.
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.observeField("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 | # Additional information
51 |
52 | ## A differences between `roUrlTransfer` and `roStreamSocket`
53 |
54 | The `roUrltransfer` is sending larger data faster, Roku replied to this with the following:
55 | > Could be the difference between using curlLibrary and lower-level roStreamSocket implementation. The curl library is highly optimized as we use it for all the manifest handling in streaming.
56 |
57 | The `roStreamSocket` is separated into standalone thread but still is serialized, devices handling a higher amount of data could overload a buffer for some situations and data could send for longer.
58 |
59 | We approximately measured _230KB/s_ on **4660X - Roku Ultra** and _15KB/s_ on **3500EU - Roku Stick**.
60 |
61 | To send larger amount of data we suggest to use `roUrlTransfer` instead. Won't be improved/fixed in near future.
62 |
63 | # License
64 |
65 | The MIT License. See license.txt.
66 |
--------------------------------------------------------------------------------
/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 | ws._CLOSING_DELAY = 30
33 | ws._BUFFER_SOCKET_SIZE = 4096 ' we allow 4times higher amount in buffer as max limit without waiting to clear it, figured out the last amount of buffer is about 13000 before it overload
34 | ws._BUFFER_SLEEP = 10
35 | ws._BUFFER_LOOP_LIMIT = 5000 / ws._BUFFER_SLEEP ' max waiting time till try to push another value into buffer, _BUFFER_SLEEP * _BUFFER_LOOP_LIMIT
36 | ' Variables
37 | ws._logger = Logger()
38 | ws._ready_state = ws.state.CLOSED
39 | ws._protocols = []
40 | ws._headers = []
41 | ws._secure = false
42 | ws._tls = invalid
43 | ws._socket = invalid
44 | ws._sec_ws_key = invalid
45 | ws._handshake = invalid
46 | ws._sent_handshake = false
47 | ws._has_received_handshake = false
48 | ws._buffer_size = cint(1024 * 1024 * 0.5)
49 | ws._data = createObject("roByteArray")
50 | ws._data[ws._buffer_size] = 0
51 | ws._data_size = 0
52 | ws._frame_data = createObject("roByteArray")
53 | ws._started_closing = 0
54 | ws._hostname = ""
55 | ws._ws_port = createObject("roMessagePort")
56 | ws._message_port = invalid
57 |
58 | ' ========== Getters ==========
59 |
60 | ' Get web socket ready state
61 | ' @see WebSocketClient.STATE for values
62 | ' @param self WebSocketClient
63 | ' @return integer ready state
64 | ws.get_ready_state = function () as integer
65 | return m._ready_state
66 | end function
67 |
68 | ' Get web socket protocols sent on initial handshake
69 | ' @param self WebSocketClient
70 | ' @return roArray web socket protocols
71 | ws.get_protocols = function () as object
72 | return m._protocols
73 | end function
74 |
75 | ' Get HTTP headers sent on initial HTTP request
76 | ' @param self WebSocketClient
77 | ' @return roArray HTTP headers - even indices are the header keys, the odd
78 | ' indices are the header values
79 | ws.get_headers = function () as object
80 | return m._headers
81 | end function
82 |
83 | ' Get the status of a secure web socket connection having been used
84 | ' @param self WebSocketClient
85 | ' @return boolean true if a TLS connection should be attempted before
86 | ' attempting a web socket handshake
87 | ws.get_secure = function () as boolean
88 | return m._secure
89 | end function
90 |
91 | ' Get the maximum buffer size for data sent to the websocket
92 | ' @param self WebSocketClient
93 | ' @return integer max buffer size in bytes
94 | ws.get_buffer_size = function () as integer
95 | return m._buffer_size
96 | end function
97 |
98 | ' ========== Setters ==========
99 |
100 | ' Set the web socket protocols to request from the web socket server
101 | ' @param self WebSocketClient
102 | ' @param roArray of protocol strings
103 | ws.set_protocols = function (protocols as object) as void
104 | m._protocols = protocols
105 | m._post_message("protocols", m._protocols)
106 | end function
107 |
108 | ' Set the headers to send on the initial HTTP request for a web socket
109 | ' @param self WebSocketClient
110 | ' @param roArray of header strings - The even indices should be the header
111 | ' keys and the odd are the header values
112 | ws.set_headers = function (headers as object) as void
113 | m._headers = headers
114 | m._post_message("headers", m._headers)
115 | end function
116 |
117 | ' Set if a TLS connection should be attempted before the web socket
118 | ' connection
119 | ' @param self WebSocketClient
120 | ' @param secure boolean should a secure connetion be attempted
121 | ws.set_secure = function (secure as boolean) as void
122 | m._secure = secure
123 | m._post_message("secure", m._secure)
124 | end function
125 |
126 | ' Set the buffer size for web socket data
127 | ' @param self WebSocketClient
128 | ' @param size integer max size of buffer in bytes
129 | ws.set_buffer_size = function (size as integer) as void
130 | if m._ready_state <> m.STATE.CLOSED
131 | m._logger.printl(m.WARN, "WebSocketClient: Cannot resize buffer on a socket that is not closed")
132 | return
133 | else if size < m._FRAME_SIZE
134 | m._logger.printl(m.WARN, "WebSocketClient: Cannot set buffer to a size smaller than " + m._FRAME_SIZE.toStr() + " bytes")
135 | return
136 | end if
137 | m._buffer_size = size
138 | m._buffer = createObject("roByteArray")
139 | m._buffer[m._buffer_size] = 0
140 | m._data_size = 0
141 | m._post_message("buffer_size", m._buffer_size)
142 | end function
143 |
144 | ' Set the message port that should be used to relay web socket events and
145 | ' field change updates
146 | ' @param self WebSocketClient
147 | ' @param port roMessagePort
148 | ws.set_message_port = function (port as object) as void
149 | m._message_port = port
150 | end function
151 |
152 | ' Set the log level
153 | ws.set_log_level = function (log_level as string) as void
154 | m._logger.set_log_level(log_level)
155 | end function
156 |
157 | ' ========== Main ==========
158 |
159 | ' Parses one websocket frame or HTTP message, if one is available
160 | ' @param self WebSocketClient
161 | ws.run = function () as void
162 | msg = wait(200, m._ws_port)
163 | ' Socket event
164 | if type(msg) = "roSocketEvent"
165 | m._send_handshake()
166 | m._read_socket_data()
167 | end if
168 | m._try_force_close()
169 | end function
170 |
171 | ' Force close a connection after the close frame has been sent and no
172 | ' response was given, after a timeout
173 | ' @param self WebSocketClient
174 | ws._try_force_close = function () as void
175 | if m._ready_state = m.STATE.CLOSING and uptime(0) - m._started_closing >= m._CLOSING_DELAY
176 | m._close()
177 | end if
178 | end function
179 |
180 | ' Sends data through the socket
181 | ' @param self WebSocketClient
182 | ' @param message array - should contain one element of type roString or roArray
183 | ' cannot exceed 125 bytes if the the specified opcode
184 | ' is for a control frame
185 | ' @param _opcode int - define a **control** opcode data opcodes are determined
186 | ' by the type of the passed message
187 | ' @param silent boolean - does not send on_error event
188 | ' @param do_close boolean - if true, a close frame will be sent on errors
189 | ' @return integer amount of bytes sent
190 | ws.send = function (message as dynamic, _opcode = -1 as integer, silent = false as boolean, do_close = true as boolean) as integer
191 | if m._socket = invalid or not m._socket.isWritable()
192 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: socket is closed")
193 | return -1
194 | end if
195 | if m._ready_state <> m.STATE.OPEN
196 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: connection not open")
197 | return -1
198 | end if
199 | if type(message) = "roString" or type(message) = "String" or type(message) = "roByteArray"
200 | message = [message]
201 | end if
202 | if message.count() <> 1
203 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: too many parameters")
204 | return -1
205 | end if
206 | bytes = createObject("roByteArray")
207 | opcode = 0
208 | if type(message[0]) = "roString" or type(message[0]) = "String"
209 | bytes.fromAsciiString(message[0])
210 | opcode = m.OPCODE.TEXT
211 | else if type(message[0]) = "roArray" or type(message[0]) = "roByteArray"
212 | for each byte in message[0]
213 | bytes.push(byte)
214 | end for
215 | opcode = m.OPCODE.BINARY
216 | else
217 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: invalid parameter type")
218 | return -1
219 | end if
220 | if _opcode > -1 and (_opcode >> 3) <> 1
221 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: specified opcode was not a control opcode")
222 | return -1
223 | else if _opcode > -1
224 | if bytes.count() > 125
225 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Failed to send data: control frames cannot have a payload larger than 125 bytes")
226 | return -1
227 | end if
228 | opcode = _opcode
229 | end if
230 | ' Frame message
231 | frame_count = bytes.count() \ m._FRAME_SIZE
232 | if bytes.count() mod m._FRAME_SIZE <> 0 or frame_count = 0
233 | frame_count++
234 | end if
235 | total_sent = 0
236 | for frame_index = 0 to frame_count - 1
237 | ' Get sub array of payload bytes
238 | payload = createObject("roByteArray")
239 | max = bytes.count() - 1
240 | if (frame_index + 1) * m._FRAME_SIZE - 1 < max
241 | max = (frame_index + 1) * m._FRAME_SIZE - 1
242 | end if
243 | for byte_index = frame_index * m._FRAME_SIZE to max
244 | payload.push(bytes[byte_index])
245 | end for
246 | ' Construct frame
247 | frame = createObject("roByteArray")
248 | ' FIN(1) RSV1(1) RSV2(1) RSV3(1) opcode(4)
249 | final = 0
250 | if frame_index = frame_count - 1
251 | final = &h80
252 | end if
253 | opcode_frame = m.OPCODE.CONTINUATION
254 | if frame_index = 0
255 | opcode_frame = opcode
256 | end if
257 | frame.push(final or opcode_frame)
258 | ' mask(1) payload_length(7)
259 | length_7 = payload.count()
260 | if payload.count() > &hffff
261 | length_7 = 127
262 | else if payload.count() > 125
263 | length_7 = 126
264 | end if
265 | frame.push(&h80 or length_7)
266 | ' payload_length_continuation(64)
267 | ' 16 bit uint
268 | if length_7 = 126
269 | frame.append(short_to_bytes(payload.count()))
270 | ' 64 bit uint
271 | else if length_7 = 127
272 | frame.append(long_to_bytes(payload.count()))
273 | end if
274 | ' masking key(32)
275 | mask = rnd(&hffff)
276 | mask_bytes = int_to_bytes(mask)
277 | frame.append(mask_bytes)
278 | ' Mask payload
279 | masked_payload = createObject("roByteArray")
280 | for byte_index = 0 to payload.count() - 1
281 | masking_key = mask_bytes[byte_index mod 4]
282 | byte = payload[byte_index]
283 | masked_byte = xor(byte, masking_key)
284 | masked_payload.push(masked_byte)
285 | end for
286 | frame.append(masked_payload)
287 | ' Send frame
288 | if _opcode <> m.OPCODE.PING
289 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sending frame: " + frame.toHexString())
290 | end if
291 | sent = 0
292 | if m._secure
293 | sent = m._tls.send(m._tls, frame)
294 | else
295 | sent = m._socket.send(frame, 0, frame.count())
296 | end if
297 | if _opcode <> m.OPCODE.PING
298 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sent " + sent.toStr() + " bytes")
299 | loop_wait = 0
300 | while m._socket.GetCountSendBuf() > m._BUFFER_SOCKET_SIZE
301 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sleeping " + m._BUFFER_SLEEP.toStr() + "ms to reduce buffer")
302 | sleep(m._BUFFER_SLEEP)
303 | if loop_wait > m._BUFFER_LOOP_LIMIT
304 | exit while
305 | end if
306 | loop_wait++
307 | end while
308 | end if
309 | total_sent += sent
310 | if sent <> frame.count()
311 | if do_close
312 | m._close()
313 | end if
314 | if not silent
315 | m._error(14, "Failed to send data")
316 | end if
317 | return total_sent
318 | end if
319 | end for
320 | return total_sent
321 | end function
322 |
323 | ' Send the initial websocket handshake if it has not been sent
324 | ' @param self WebSocketClient
325 | ws._send_handshake = function () as void
326 | 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
327 | return
328 | end if
329 | if m._secure and m._tls.ready_state = m._tls.STATE_DISCONNECTED
330 | m._tls.connect(m._tls, m._hostname)
331 | else
332 | m._logger.printl(m._logger.VERBOSE, m._handshake)
333 | sent = 0
334 | if m._secure
335 | sent = m._tls.send_str(m._tls, m._handshake)
336 | else
337 | sent = m._socket.sendStr(m._handshake)
338 | end if
339 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Sent " + sent.toStr() + " bytes")
340 | if sent = -1
341 | m._close()
342 | m._error(4, "Failed to send data: " + m._socket.status().toStr())
343 | return
344 | end if
345 | m._sent_handshake = true
346 | end if
347 | end function
348 |
349 | ' Read socket data
350 | ' @param self WebSocketClient
351 | ws._read_socket_data = function () as void
352 | if m._socket = invalid or m._ready_state = m.STATE.CLOSED or (m._secure and m._tls.ready_state = m._tls.STATE_DISCONNECTED)
353 | return
354 | end if
355 | buffer = createObject("roByteArray")
356 | buffer[1024] = 0
357 | bytes_received = 0
358 | if m._socket.isReadable() and m._socket.getCountRcvBuf() > 0
359 | bytes_received = m._socket.receive(buffer, 0, 1024)
360 | end if
361 | if bytes_received < 0
362 | m._close()
363 | m._error(15, "Failed to read from socket")
364 | return
365 | end if
366 | if m._secure
367 | buffer = m._tls.read(m._tls, buffer, bytes_received)
368 | if buffer = invalid
369 | m._close()
370 | m._error(17, "TLS error")
371 | return
372 | end if
373 | bytes_received = buffer.count()
374 | end if
375 | buffer_index = 0
376 | for byte_index = m._data_size to m._data_size + bytes_received - 1
377 | m._data[byte_index] = buffer[buffer_index]
378 | buffer_index++
379 | end for
380 | m._data_size += bytes_received
381 | m._data[m._data_size] = 0
382 | ' WebSocket frames
383 | if m._has_received_handshake
384 | ' Wait for at least the payload 7-bit size
385 | if m._data_size < 2
386 | return
387 | end if
388 | final = (m._data[0] >> 7) = 1
389 | opcode = (m._data[0] and &hf)
390 | control = (opcode >> 3) = 1
391 | masked = (m._data[1] >> 7) = 1
392 | payload_size_7 = m._data[1] and &h7f
393 | payload_size = payload_size_7
394 | payload_index = 2
395 | mask = 0
396 | if payload_size_7 = 126
397 | ' Wait for the 16-bit payload size
398 | if m._data_size < 4
399 | return
400 | end if
401 | payload_size = bytes_to_short(m._data[2], m._data[3])
402 | payload_index += 2
403 | else if payload_size_7 = 127
404 | ' Wait for the 64-bit payload size
405 | if m._data_size < 10
406 | return
407 | end if
408 | 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])
409 | payload_index += 8
410 | end if
411 | if masked
412 | ' Wait for mask int
413 | if m._data_size < payload_index
414 | return
415 | end if
416 | mask = bytes_to_int(m._data[payload_index], m._data[payload_index + 1], m._data[payload_index + 2], m._data[payload_index + 3])
417 | payload_index += 4
418 | end if
419 | ' Wait for payload
420 | if m._data_size < payload_index + payload_size
421 | return
422 | end if
423 | payload = createObject("roByteArray")
424 | for byte_index = payload_index to payload_index + payload_size - 1
425 | payload.push(m._data[byte_index])
426 | end for
427 | ' Handle control frame
428 | if control
429 | m._handle_frame(opcode, payload)
430 | ' Handle data frame
431 | else if final
432 | full_payload = createObject("roByteArray")
433 | full_payload.append(m._frame_data)
434 | full_payload.append(payload)
435 | m._handle_frame(opcode, full_payload)
436 | m._frame_data.clear()
437 | ' Check for continuation frame
438 | else
439 | m._frame_data.append(payload)
440 | end if
441 | ' Save start of next frame
442 | if m._data_size > payload_index + payload_size
443 | data = createObject("roByteArray")
444 | data.append(m._data)
445 | m._data.clear()
446 | for byte_index = payload_index + payload_size to m._data_size - 1
447 | m._data.push(data[byte_index])
448 | end for
449 | else
450 | m._data.clear()
451 | end if
452 | ' HTTP/Handshake
453 | else
454 | data = m._data.toAsciiString()
455 | http_delimiter = m._NL + m._NL
456 | if data.len() <> data.replace(http_delimiter, "").len()
457 | split = data.split(http_delimiter)
458 | message = split[0]
459 | data = ""
460 | for split_index = 1 to split.count() - 1
461 | data += split[split_index]
462 | if split_index < split.count() - 1 or split[split_index].right(4) = m._NL + m._NL
463 | data += m._NL + m._NL
464 | end if
465 | end for
466 | ' Handle the message
467 | m._logger.printl(m._logger.VERBOSE, "WebSocketClient: Message: " + message)
468 | m._handle_handshake_response(message)
469 | end if
470 | m._data.fromAsciiString(data)
471 | end if
472 | m._data_size = m._data.count()
473 | m._data[m._buffer_size] = 0
474 | end function
475 |
476 | ' Handle the handshake message or die trying
477 | ' @param self WebSocketClient
478 | ' @param string http response header
479 | ws._handle_handshake_response = function (message as string) as void
480 | lines = message.split(m._NL)
481 | if lines.count() = 0
482 | m._close()
483 | m._error(5, "Invalid handshake: Missing status line")
484 | return
485 | end if
486 | ' Check status line
487 | if not m._HTTP_STATUS_LINE_REGEX.isMatch(lines[0])
488 | m._close()
489 | m._error(6, "Invalid handshake: Status line malformed")
490 | return
491 | end if
492 | status_line = m._HTTP_STATUS_LINE_REGEX.match(lines[0])
493 | if status_line[1] <> "HTTP/1.1"
494 | m._close()
495 | m._error(7, "Invalid handshake: Response version mismatch. Expected HTTP/1.1, got " + status_line[0])
496 | return
497 | end if
498 | if status_line[2] <> "101"
499 | m._close()
500 | m._error(8, "Invalid handshake: HTTP status code is not 101: Received " + status_line[2])
501 | return
502 | end if
503 | ' Search headers
504 | protocol = ""
505 | for header_line_index = 1 to lines.count() - 1:
506 | if m._HTTP_HEADER_REGEX.isMatch(lines[header_line_index])
507 | header = m._HTTP_HEADER_REGEX.match(lines[header_line_index])
508 | ' Upgrade
509 | if ucase(header[1]) = "UPGRADE" and ucase(header[2]) <> "WEBSOCKET"
510 | m._close()
511 | m._error(9, "Invalid handshake: invalid upgrade header: " + header[2])
512 | return
513 | ' Connection
514 | else if ucase(header[1]) = "CONNECTION" and ucase(header[2]) <> "UPGRADE"
515 | m._close()
516 | m._error(10, "Invalid handshake: invalid connection header: " + header[2])
517 | return
518 | ' Sec-WebSocket-Accept
519 | else if ucase(header[1]) = "SEC-WEBSOCKET-ACCEPT"
520 | expected_array = createObject("roByteArray")
521 | expected_array.fromAsciiString(m._sec_ws_key + m._WS_ACCEPT_GUID)
522 | digest = createObject("roEVPDigest")
523 | digest.setup("sha1")
524 | expected = digest.process(expected_array)
525 | if expected <> header[2].trim()
526 | m._close()
527 | m._error(11, "Invalid handshake: Sec-WebSocket-Accept value is invalid: " + header[2])
528 | return
529 | end if
530 | ' Sec-WebSocket-Extensions
531 | else if ucase(header[1]) = "SEC-WEBSOCKET-EXTENSIONS" and header[2] <> ""
532 | m._close()
533 | m._error(12, "Invalid handshake: Sec-WebSocket-Extensions value is invalid: " + header[2])
534 | return
535 | ' Sec-WebSocket-Protocol
536 | else if ucase(header[1]) = "SEC-WEBSOCKET-PROTOCOL"
537 | p = header[2].trim()
538 | was_requested = false
539 | for each requested_protocol in m._protocols
540 | if requested_protocol = p
541 | was_requested = true
542 | end if
543 | end for
544 | if not was_requested
545 | m._close()
546 | m._error(13, "Invalid handshake: Sec-WebSocket-Protocol contains a protocol that was not requested: " + p)
547 | return
548 | end if
549 | protocol = p
550 | end if
551 | end if
552 | end for
553 | m._has_received_handshake = true
554 | m._state(m.STATE.OPEN)
555 | m._post_message("on_open", {
556 | protocol: protocol
557 | })
558 | end function
559 |
560 | ' Post a message to the message port
561 | ' @param self WebSocketClient
562 | ' @param id string message event id
563 | ' @param data dynamic message data
564 | ws._post_message = function (id as string, data as dynamic) as void
565 | if m._message_port <> invalid
566 | m._message_port.postMessage({
567 | id: id,
568 | data: data
569 | })
570 | end if
571 | end function
572 |
573 | ' Handle a web socket frame
574 | ' @param self WebSocketClient
575 | ' @param opcode int opcode
576 | ' @param payload roByteArray payload data
577 | ws._handle_frame = function (opcode as integer, payload as object) as void
578 | if opcode <> m.OPCODE.PONG
579 | frame_print = "WebSocketClient: " + "Received frame:" + m._NL
580 | frame_print += " Opcode: " + opcode.toStr() + m._NL
581 | frame_print += " Payload: " + payload.toHexString()
582 | m._logger.printl(m._logger.VERBOSE, frame_print)
583 | end if
584 | ' Close
585 | if opcode = m.OPCODE.CLOSE
586 | m._close()
587 | return
588 | ' Ping
589 | else if opcode = m.OPCODE.PING
590 | m.send("", m.OPCODE.PONG)
591 | return
592 | ' Text
593 | else if opcode = m.OPCODE.TEXT
594 | m._post_message("on_message", {
595 | type: 0,
596 | message: payload.toAsciiString()
597 | })
598 | return
599 | ' Data
600 | else if opcode = m.OPCODE.BINARY
601 | payload_array = []
602 | for each byte in payload
603 | payload_array.push(byte)
604 | end for
605 | m._post_message("on_message", {
606 | type: 1,
607 | message: payload_array
608 | })
609 | return
610 | end if
611 | end function
612 |
613 | ' Generate a 16 character [A-Za-z0-9] random string and base64 encode it
614 | ' @link https://tools.ietf.org/html/rfc6455#section-4.1
615 | ' @param self WebSocketClient
616 | ' @return string random 16 character base64 encoded string
617 | ws._generate_sec_ws_key = function () as string
618 | sec_ws_key = ""
619 | for char_index = 0 to 15
620 | char = m._CHARS[rnd(m._CHARS.count()) - 1]
621 | if rnd(2) = 1
622 | char = ucase(char)
623 | end if
624 | sec_ws_key += char
625 | end for
626 | ba = createObject("roByteArray")
627 | ba.fromAsciiString(sec_ws_key)
628 | return ba.toBase64String()
629 | end function
630 |
631 | ' Connect to the specified URL
632 | ' @param self WebSocketClient
633 | ' @param url_string web socket url to connect
634 | ' Format: ws://example.org:80/
635 | ' If the port is not specified the port will be assumed
636 | ' from the protocol (ws: 80, wss: 443).
637 | ws.open = function (url as string) as void
638 | if m._ready_state <> m.STATE.CLOSED
639 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: Tried to open a web socket that was already open")
640 | return
641 | end if
642 | if m._REGEX_URL.isMatch(url)
643 | match = m._REGEX_URL.match(url)
644 | ws_type = lcase(match[1])
645 | host = lcase(match[2])
646 | port = match[3]
647 | path = match[4]
648 | m._hostname = host
649 | ' Port
650 | if port <> ""
651 | port = val(port, 10)
652 | else if ws_type = "wss"
653 | m.set_secure(true)
654 | port = 443
655 | else if ws_type = "ws"
656 | port = 80
657 | else
658 | m._close()
659 | m._error(0, "Invalid web socket type specified: " + ws_type)
660 | return
661 | end if
662 | ' Path
663 | if path = ""
664 | path = "/"
665 | end if
666 | ' WS(S) to HTTP(S)
667 | scheme = ws_type.replace("ws", "http")
668 | ' Construct handshake
669 | m._sec_ws_key = m._generate_sec_ws_key()
670 | protocols = ""
671 | for each proto in m._protocols
672 | protocols += proto + ", "
673 | end for
674 | if protocols <> ""
675 | protocols = protocols.left(len(protocols) - 2)
676 | end if
677 | handshake = "GET " + path + " HTTP/1.1" +m._NL
678 | handshake += "Host: " + host + ":" + port.toStr() + m._NL
679 | handshake += "Upgrade: websocket" + m._NL
680 | handshake += "Connection: Upgrade" + m._NL
681 | handshake += "Sec-WebSocket-Key: " + m._sec_ws_key + m._NL
682 | if protocols <> ""
683 | handshake += "Sec-WebSocket-Protocol: " + protocols + m._NL
684 | end if
685 | ' handshake += "Sec-WebSocket-Extensions: " + m._NL
686 | handshake += "Sec-WebSocket-Version: 13" + m._NL
687 | handshake += m._get_parsed_user_headers()
688 | handshake += m._NL
689 | m._handshake = handshake
690 | ' Create socket
691 | m._state(m.STATE.CONNECTING)
692 | address = createObject("roSocketAddress")
693 | address.setHostName(host)
694 | address.setPort(port)
695 | if not address.isAddressValid()
696 | m._close()
697 | m._error(2, "Invalid hostname")
698 | return
699 | end if
700 | m._data_size = 0
701 | m._socket = createObject("roStreamSocket")
702 | m._socket.notifyReadable(true)
703 | m._socket.notifyWritable(true)
704 | m._socket.notifyException(true)
705 | ' set up segmentation on 576 which is lowest value of MTU speed up
706 | ' transmition around 15%-22%, tested on 4660X - Roku Ultra and
707 | ' 3500EU - Roku Stick
708 | m._socket.setMaxSeg(576)
709 | m._socket.setMessagePort(m._ws_port)
710 | m._socket.setSendToAddress(address)
711 | m._sent_handshake = false
712 | m._has_received_handshake = false
713 | m._tls = TlsUtil(m._socket)
714 | m._tls.set_buffer_size(m._tls, m._buffer_size)
715 | if not m._socket.connect()
716 | m._close()
717 | m._error(3, "Socket failed to connect: " + m._socket.status().toStr())
718 | return
719 | end if
720 | else
721 | m._close()
722 | m._error(1, "Invalid URL specified")
723 | end if
724 | end function
725 |
726 | ' Parse header array and return a string of headers delimited by CRLF
727 | ' @param self WebSocketClient
728 | ws._get_parsed_user_headers = function () as string
729 | if m._headers = invalid or m._headers.count() = 0 or (m._headers.count() mod 2) = 1
730 | return ""
731 | end if
732 | header_string = ""
733 | for header_index = 0 to m._headers.count() - 1 step 2
734 | header = m._headers[header_index]
735 | value = m._headers[header_index + 1]
736 | header_string += header + ": " + value + m._NL
737 | end for
738 | return header_string
739 | end function
740 |
741 | ' Set ready state
742 | ' @param self WebSocketClient
743 | ' @param state
744 | ws._state = function (_state as integer) as void
745 | m._ready_state = _state
746 | m._post_message("ready_state", _state)
747 | end function
748 |
749 | ' Send an error event
750 | ' Posts an on_error message to the message port
751 | ' @param self WebSocketClient
752 | ' @param code integer error code
753 | ' @param message string error message
754 | ws._error = function (code as integer, message as string) as void
755 | m._logger.printl(m._logger.EXTRA, "WebSocketClient: Error: " + message)
756 | m._post_message("on_error", {
757 | code: code,
758 | message: message
759 | })
760 | end function
761 |
762 | ' Close the socket
763 | ' @param WebSocketClient
764 | ' @param code integer - status code
765 | ' @param reason roByteArray - reason
766 | ws._close = function (code = 1000 as integer, reason = invalid as object) as void
767 | if m._socket <> invalid
768 | ' Send the closing frame
769 | if m._ready_state = m.STATE.OPEN
770 | m._send_close_frame(code, reason)
771 | m._started_closing = uptime(0)
772 | m._state(m.STATE.CLOSING)
773 | else
774 | m._state(m.STATE.CLOSED)
775 | m._post_message("on_close", "")
776 | m._socket.close()
777 | end if
778 | else if m._ready_state <> m.STATE.CLOSED
779 | m._state(m.STATE.CLOSED)
780 | end if
781 | end function
782 |
783 | ' Send a close frame to the server to initiate a close
784 | ' @param self WebSocketClient
785 | ' @param code integer - status code
786 | ' @param reason roByteArray - reason
787 | ws._send_close_frame = function (code as integer, reason as dynamic) as void
788 | message = createObject("roByteArray")
789 | message.push(code >> 8)
790 | message.push(code)
791 | if reason <> invalid
792 | message.append(reason)
793 | end if
794 | m.send(message, m.OPCODE.CLOSE, true, false)
795 | end function
796 |
797 | ' Close the socket
798 | ' @self WebSocketClient
799 | ' @param reason array - array [code as integer, message as roString]
800 | ws.close = function (params as object) as void
801 | code = 1000
802 | reason = createObject("roByteArray")
803 | if params.count() > 0
804 | code = params[0]
805 | if type(code) <> "Integer" or code > &hffff
806 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: close expects value at array index 0 to be a 16-bit integer")
807 | end if
808 | end if
809 | if params.count() > 1
810 | message = params[1]
811 | if type(message) <> "roString" or type(message) <> "String"
812 | reason.fromAsciiString(message)
813 | else
814 | m._logger.printl(m._logger.DEBUG, "WebSocketClient: close expects value at array index 1 to be a string")
815 | end if
816 | end if
817 | m._close(code, reason)
818 | end function
819 |
820 | ' Return constructed instance
821 | return ws
822 | end function
823 |
--------------------------------------------------------------------------------
/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 | ' Task init
10 | m.top.functionName = "run"
11 | m.top.control = "RUN"
12 | end function
13 |
14 | ' Main task loop
15 | function run() as void
16 | m.ws = WebSocketClient()
17 | m.port = createObject("roMessagePort")
18 | m.ws.set_message_port(m.port)
19 | ' Fields
20 | m.top.STATE_CONNECTING = m.ws.STATE.CONNECTING
21 | m.top.STATE_OPEN = m.ws.STATE.OPEN
22 | m.top.STATE_CLOSING = m.ws.STATE.CLOSING
23 | m.top.STATE_CLOSED = m.ws.STATE.CLOSED
24 | m.top.ready_state = m.ws.get_ready_state()
25 | m.top.protocols = m.ws.get_protocols()
26 | m.top.headers = m.ws.get_headers()
27 | m.top.secure = m.ws.get_secure()
28 | m.top.buffer_size = m.ws.get_buffer_size()
29 | ' Event listeners
30 | m.top.observeField("open", m.port)
31 | m.top.observeField("send", m.port)
32 | m.top.observeField("close", m.port)
33 | m.top.observeField("buffer_size", m.port)
34 | m.top.observeField("protocols", m.port)
35 | m.top.observeField("headers", m.port)
36 | m.top.observeField("secure", m.port)
37 | m.top.observeField("log_level", m.port)
38 |
39 | if len(m.top.open) > 0
40 | m.ws.open(m.top.open)
41 | end if
42 |
43 | while true
44 | ' Check task messages
45 | msg = wait(1, m.port)
46 | ' Field event
47 | if type(msg) = "roSGNodeEvent"
48 | if msg.getField() = "open"
49 | m.ws.open(msg.getData())
50 | else if msg.getField() = "send"
51 | m.ws.send(msg.getData())
52 | else if msg.getField() = "close"
53 | m.ws.close(msg.getData())
54 | else if msg.getField() = "buffer_size"
55 | m.ws.set_buffer_size(msg.getData())
56 | else if msg.getField() = "protocols"
57 | m.ws.set_protocols(msg.getData())
58 | else if msg.getField() = "headers"
59 | m.ws.set_headers(msg.getData())
60 | else if msg.getField() = "secure"
61 | m.ws.set_secure(msg.getData())
62 | else if msg.getField() = "log_level"
63 | m.ws.set_log_level(msg.getData())
64 | end if
65 | ' WebSocket event
66 | else if type(msg) = "roAssociativeArray"
67 | if msg.id = "on_open"
68 | m.top.on_open = msg.data
69 | else if msg.id = "on_close"
70 | m.top.on_close = msg.data
71 | else if msg.id = "on_message"
72 | m.top.on_message = msg.data
73 | else if msg.id = "on_error"
74 | m.top.on_error = msg.data
75 | else if msg.id = "ready_state"
76 | m.top.ready_state = msg.data
77 | else if msg.id = "buffer_size"
78 | m.top.unobserveField("buffer_size")
79 | m.top.buffer_size = msg.data
80 | m.top.observeField("buffer_size", m.task_port)
81 | else if msg.id = "protocols"
82 | m.top.unobserveField("protocols")
83 | m.top.protocols = msg.data
84 | m.top.observeField("protocols", m.task_port)
85 | else if msg.id = "headers"
86 | m.top.unobserveField("headers")
87 | m.top.headers = msg.data
88 | m.top.observeField("headers", m.task_port)
89 | else if msg.id = "secure"
90 | m.top.unobserveField("secure")
91 | m.top.secure = msg.data
92 | m.top.observeField("secure", m.task_port)
93 | end if
94 | end if
95 | m.ws.run()
96 | end while
97 | end function
98 |
--------------------------------------------------------------------------------
/test/.gitignore:
--------------------------------------------------------------------------------
1 | out/
2 | .buildpath
3 | .project
4 | bright_web_socket.json
5 |
--------------------------------------------------------------------------------
/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/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/test/images/icon_focus_hd.png
--------------------------------------------------------------------------------
/test/images/icon_focus_sd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/test/images/icon_focus_sd.png
--------------------------------------------------------------------------------
/test/images/icon_side_hd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/test/images/icon_side_hd.png
--------------------------------------------------------------------------------
/test/images/icon_side_sd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/test/images/icon_side_sd.png
--------------------------------------------------------------------------------
/test/images/splash_fhd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/test/images/splash_fhd.jpg
--------------------------------------------------------------------------------
/test/images/splash_hd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/test/images/splash_hd.jpg
--------------------------------------------------------------------------------
/test/images/splash_sd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuitestAutomation/BrightWebSocket/3d64524354e41b0b0a06841dc53e696e685e334a/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 |
--------------------------------------------------------------------------------