protocol-websocket 0.7.5 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +3 -0
- data/lib/.DS_Store +0 -0
- data/lib/protocol/.DS_Store +0 -0
- data/lib/protocol/websocket/binary_frame.rb +5 -0
- data/lib/protocol/websocket/close_frame.rb +32 -2
- data/lib/protocol/websocket/connection.rb +87 -51
- data/lib/protocol/websocket/extension/compression/constants.rb +34 -0
- data/lib/protocol/websocket/extension/compression/deflate.rb +105 -0
- data/lib/protocol/websocket/extension/compression/inflate.rb +91 -0
- data/lib/protocol/websocket/extension/compression.rb +137 -0
- data/lib/protocol/websocket/extensions.md +13 -0
- data/lib/protocol/websocket/extensions.rb +148 -0
- data/lib/protocol/websocket/frame.rb +36 -13
- data/lib/protocol/websocket/framer.rb +7 -3
- data/lib/protocol/websocket/headers.rb +5 -1
- data/lib/protocol/websocket/json_message.rb +47 -0
- data/lib/protocol/websocket/message.rb +62 -0
- data/lib/protocol/websocket/ping_frame.rb +3 -0
- data/lib/protocol/websocket/pong_frame.rb +1 -0
- data/lib/protocol/websocket/text_frame.rb +13 -6
- data/lib/protocol/websocket/version.rb +1 -1
- data/lib/protocol/websocket.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +50 -7
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ca30621d2f489b08f475047f539d2fe048661dbe2b109a8d0ef441c6e95ed37
|
|
4
|
+
data.tar.gz: fe325e6b9041103c704cf912f73f3838c2e9d50af6368b2db54e280906600aa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35a0841efafc3a3b1fb98cbfdccff848a51262cf3e493d78cb95c4f300a1ced40779837d51e31edfd5d2cc3626b4ad22fd9f1066bcedfd4b9d8d460d44e9ddf4
|
|
7
|
+
data.tar.gz: 1528871ed4e31683e9334eb91096d7ad6cf592d60020bd151e70e93324d727067dfc2a62c8ec8471ba3f3f56e289cc95890f7a6d89e32afce62c2a5fd55506fc
|
checksums.yaml.gz.sig
ADDED
data/lib/.DS_Store
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
# THE SOFTWARE.
|
|
20
20
|
|
|
21
21
|
require_relative 'frame'
|
|
22
|
+
require_relative 'message'
|
|
22
23
|
|
|
23
24
|
module Protocol
|
|
24
25
|
module WebSocket
|
|
@@ -29,6 +30,10 @@ module Protocol
|
|
|
29
30
|
true
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
def read_message(buffer)
|
|
34
|
+
return BinaryMessage.new(buffer)
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def apply(connection)
|
|
33
38
|
connection.receive_binary(self)
|
|
34
39
|
end
|
|
@@ -29,11 +29,41 @@ module Protocol
|
|
|
29
29
|
def unpack
|
|
30
30
|
data = super
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
case data.length
|
|
33
|
+
when 0
|
|
34
|
+
[nil, ""]
|
|
35
|
+
when 1
|
|
36
|
+
raise ProtocolError, "invalid close frame length!"
|
|
37
|
+
else
|
|
38
|
+
code, reason = *data.unpack(FORMAT)
|
|
39
|
+
|
|
40
|
+
case code
|
|
41
|
+
when 0 .. 999, 1005 .. 1006, 1015, 5000 .. 0xFFFF
|
|
42
|
+
raise ProtocolError, "invalid close code!"
|
|
43
|
+
when 1004, 1016 .. 2999
|
|
44
|
+
raise ProtocolError, "reserved close code!"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
reason.force_encoding(Encoding::UTF_8)
|
|
48
|
+
|
|
49
|
+
unless reason.valid_encoding?
|
|
50
|
+
raise ProtocolError, "invalid UTF-8 in close reason!"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
[code, reason]
|
|
54
|
+
end
|
|
33
55
|
end
|
|
34
56
|
|
|
35
57
|
def pack(code, reason)
|
|
36
|
-
|
|
58
|
+
if code
|
|
59
|
+
unless reason.encoding == Encoding::UTF_8
|
|
60
|
+
reason = reason.encode(Encoding::UTF_8)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
super [code, reason].pack(FORMAT)
|
|
64
|
+
else
|
|
65
|
+
super String.new(encoding: Encoding::BINARY)
|
|
66
|
+
end
|
|
37
67
|
end
|
|
38
68
|
|
|
39
69
|
def apply(connection)
|
|
@@ -23,14 +23,20 @@ require 'securerandom'
|
|
|
23
23
|
|
|
24
24
|
module Protocol
|
|
25
25
|
module WebSocket
|
|
26
|
+
# Wraps a framer and implements for implementing connection specific interactions like reading and writing text.
|
|
26
27
|
class Connection
|
|
27
28
|
# @parameter mask [String] 4-byte mask to be used for frames generated by this connection.
|
|
28
|
-
def initialize(framer, mask: nil)
|
|
29
|
+
def initialize(framer, mask: nil, **options)
|
|
29
30
|
@framer = framer
|
|
30
31
|
@mask = mask
|
|
31
32
|
|
|
32
33
|
@state = :open
|
|
33
34
|
@frames = []
|
|
35
|
+
|
|
36
|
+
@reserved = Frame::RESERVED
|
|
37
|
+
|
|
38
|
+
@reader = self
|
|
39
|
+
@writer = self
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
# The framer which is used for reading and writing frames.
|
|
@@ -39,9 +45,25 @@ module Protocol
|
|
|
39
45
|
# The (optional) mask which is used when generating frames.
|
|
40
46
|
attr :mask
|
|
41
47
|
|
|
48
|
+
# The allowed reserved bits:
|
|
49
|
+
attr :reserved
|
|
50
|
+
|
|
42
51
|
# Buffered frames which form part of a complete message.
|
|
43
52
|
attr_accessor :frames
|
|
44
53
|
|
|
54
|
+
attr_accessor :reader
|
|
55
|
+
attr_accessor :writer
|
|
56
|
+
|
|
57
|
+
def reserve!(bit)
|
|
58
|
+
if (@reserved & bit).zero?
|
|
59
|
+
raise "Unable to use #{bit}!"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@reserved &= ~bit
|
|
63
|
+
|
|
64
|
+
return true
|
|
65
|
+
end
|
|
66
|
+
|
|
45
67
|
def flush
|
|
46
68
|
@framer.flush
|
|
47
69
|
end
|
|
@@ -50,8 +72,8 @@ module Protocol
|
|
|
50
72
|
@state == :closed
|
|
51
73
|
end
|
|
52
74
|
|
|
53
|
-
def close
|
|
54
|
-
send_close unless closed?
|
|
75
|
+
def close(code = Error::NO_ERROR, reason = "")
|
|
76
|
+
send_close(code, reason) unless closed?
|
|
55
77
|
|
|
56
78
|
@framer.close
|
|
57
79
|
end
|
|
@@ -61,6 +83,10 @@ module Protocol
|
|
|
61
83
|
|
|
62
84
|
frame = @framer.read_frame
|
|
63
85
|
|
|
86
|
+
unless (frame.flags & @reserved).zero?
|
|
87
|
+
raise ProtocolError, "Received frame with reserved flags set!"
|
|
88
|
+
end
|
|
89
|
+
|
|
64
90
|
yield frame if block_given?
|
|
65
91
|
|
|
66
92
|
frame.apply(self)
|
|
@@ -105,38 +131,16 @@ module Protocol
|
|
|
105
131
|
raise ProtocolError, "Received unexpected continuation!"
|
|
106
132
|
end
|
|
107
133
|
end
|
|
108
|
-
|
|
109
|
-
def send_text(buffer)
|
|
110
|
-
frame = TextFrame.new(mask: @mask)
|
|
111
|
-
frame.pack buffer
|
|
112
|
-
|
|
113
|
-
write_frame(frame)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def send_binary(buffer)
|
|
117
|
-
frame = BinaryFrame.new(mask: @mask)
|
|
118
|
-
frame.pack buffer
|
|
119
|
-
|
|
120
|
-
write_frame(frame)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def send_close(code = Error::NO_ERROR, message = nil)
|
|
124
|
-
frame = CloseFrame.new(mask: @mask)
|
|
125
|
-
frame.pack(code, message)
|
|
126
|
-
|
|
127
|
-
self.write_frame(frame)
|
|
128
|
-
self.flush
|
|
129
|
-
|
|
130
|
-
@state = :closed
|
|
131
|
-
end
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
def receive_close(frame)
|
|
134
136
|
@state = :closed
|
|
135
137
|
|
|
136
|
-
code,
|
|
138
|
+
code, reason = frame.unpack
|
|
139
|
+
|
|
140
|
+
send_close(code, reason)
|
|
137
141
|
|
|
138
142
|
if code and code != Error::NO_ERROR
|
|
139
|
-
raise ClosedError.new
|
|
143
|
+
raise ClosedError.new reason, code
|
|
140
144
|
end
|
|
141
145
|
end
|
|
142
146
|
|
|
@@ -173,34 +177,61 @@ module Protocol
|
|
|
173
177
|
warn "Unhandled frame #{frame.inspect}"
|
|
174
178
|
end
|
|
175
179
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
def pack_text_frame(buffer, **options)
|
|
181
|
+
frame = TextFrame.new(mask: @mask)
|
|
182
|
+
frame.pack(buffer)
|
|
183
|
+
|
|
184
|
+
return frame
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def send_text(buffer, **options)
|
|
188
|
+
write_frame(@writer.pack_text_frame(buffer, **options))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pack_binary_frame(buffer, **options)
|
|
192
|
+
frame = BinaryFrame.new(mask: @mask)
|
|
193
|
+
frame.pack(buffer)
|
|
179
194
|
|
|
180
|
-
|
|
181
|
-
if buffer.encoding == Encoding::UTF_8
|
|
182
|
-
send_text(buffer)
|
|
183
|
-
else
|
|
184
|
-
send_binary(buffer)
|
|
185
|
-
end
|
|
195
|
+
return frame
|
|
186
196
|
end
|
|
187
197
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
def send_binary(buffer, **options)
|
|
199
|
+
write_frame(@writer.pack_binary_frame(buffer, **options))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def send_close(code = Error::NO_ERROR, reason = "")
|
|
203
|
+
frame = CloseFrame.new(mask: @mask)
|
|
204
|
+
frame.pack(code, reason)
|
|
191
205
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
self.write_frame(frame)
|
|
207
|
+
self.flush
|
|
208
|
+
|
|
209
|
+
@state = :closed
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Write a message to the connection.
|
|
213
|
+
# @parameter message [Message] The message to send.
|
|
214
|
+
def write(message, **options)
|
|
215
|
+
# This is a compatibility shim for the previous implementation. We may want to eventually deprecate this use case... or maybe it's convenient enough to leave it around.
|
|
216
|
+
if message.is_a?(String)
|
|
217
|
+
if message.encoding == Encoding::UTF_8
|
|
218
|
+
return send_text(message, **options)
|
|
219
|
+
else
|
|
220
|
+
return send_binary(message, **options)
|
|
198
221
|
end
|
|
199
222
|
end
|
|
223
|
+
|
|
224
|
+
message.send(self, **options)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The default implementation for reading a message buffer.
|
|
228
|
+
def unpack_frames(frames)
|
|
229
|
+
frames.map(&:unpack).join("")
|
|
200
230
|
end
|
|
201
231
|
|
|
202
|
-
#
|
|
203
|
-
|
|
232
|
+
# Read a message from the connection.
|
|
233
|
+
# @returns message [Message] The received message.
|
|
234
|
+
def read(**options)
|
|
204
235
|
@framer.flush
|
|
205
236
|
|
|
206
237
|
while read_frame
|
|
@@ -208,9 +239,14 @@ module Protocol
|
|
|
208
239
|
frames = @frames
|
|
209
240
|
@frames = []
|
|
210
241
|
|
|
211
|
-
|
|
242
|
+
buffer = @reader.unpack_frames(frames, **options)
|
|
243
|
+
return frames.first.read_message(buffer)
|
|
212
244
|
end
|
|
213
245
|
end
|
|
246
|
+
rescue ProtocolError => error
|
|
247
|
+
send_close(error.code, error.message)
|
|
248
|
+
|
|
249
|
+
raise
|
|
214
250
|
end
|
|
215
251
|
end
|
|
216
252
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require 'zlib'
|
|
22
|
+
|
|
23
|
+
module Protocol
|
|
24
|
+
module WebSocket
|
|
25
|
+
module Extension
|
|
26
|
+
module Compression
|
|
27
|
+
NAME = 'permessage-deflate'
|
|
28
|
+
|
|
29
|
+
# Zlib is not capable of handling < 9 window bits.
|
|
30
|
+
MINIMUM_WINDOW_BITS = 9
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require_relative 'constants'
|
|
22
|
+
|
|
23
|
+
module Protocol
|
|
24
|
+
module WebSocket
|
|
25
|
+
module Extension
|
|
26
|
+
module Compression
|
|
27
|
+
class Deflate
|
|
28
|
+
# Client writing to server.
|
|
29
|
+
def self.client(parent, client_max_window_bits: 15, client_no_context_takeover: false, **options)
|
|
30
|
+
self.new(parent,
|
|
31
|
+
window_bits: client_max_window_bits,
|
|
32
|
+
context_takeover: !client_no_context_takeover,
|
|
33
|
+
**options
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Server writing to client.
|
|
38
|
+
def self.server(parent, server_max_window_bits: 15, server_no_context_takeover: false, **options)
|
|
39
|
+
self.new(parent,
|
|
40
|
+
window_bits: server_max_window_bits,
|
|
41
|
+
context_takeover: !server_no_context_takeover,
|
|
42
|
+
**options
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(parent, level: Zlib::DEFAULT_COMPRESSION, memory_level: Zlib::DEF_MEM_LEVEL, strategy: Zlib::DEFAULT_STRATEGY, window_bits: 15, context_takeover: true, **options)
|
|
47
|
+
@parent = parent
|
|
48
|
+
|
|
49
|
+
@deflate = nil
|
|
50
|
+
|
|
51
|
+
@level = level
|
|
52
|
+
@memory_level = memory_level
|
|
53
|
+
@strategy = strategy
|
|
54
|
+
|
|
55
|
+
if window_bits < MINIMUM_WINDOW_BITS
|
|
56
|
+
window_bits = MINIMUM_WINDOW_BITS
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@window_bits = window_bits
|
|
60
|
+
@context_takeover = context_takeover
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def inspect
|
|
64
|
+
"#<#{self.class} window_bits=#{@window_bits} context_takeover=#{@context_takeover}>"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr :window_bits
|
|
68
|
+
attr :context_takeover
|
|
69
|
+
|
|
70
|
+
def pack_text_frame(buffer, compress: true, **options)
|
|
71
|
+
buffer = self.deflate(buffer)
|
|
72
|
+
|
|
73
|
+
frame = @parent.pack_text_frame(buffer, **options)
|
|
74
|
+
|
|
75
|
+
frame.flags |= Frame::RSV1
|
|
76
|
+
|
|
77
|
+
return frame
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def pack_binary_frame(buffer, compress: false, **options)
|
|
81
|
+
buffer = self.deflate(buffer)
|
|
82
|
+
|
|
83
|
+
frame = @parent.pack_binary_frame(buffer, **options)
|
|
84
|
+
|
|
85
|
+
frame.flags |= Frame::RSV1
|
|
86
|
+
|
|
87
|
+
return frame
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def deflate(buffer)
|
|
93
|
+
deflate = @deflate || Zlib::Deflate.new(@level, -@window_bits, @memory_level, @strategy)
|
|
94
|
+
|
|
95
|
+
if @context_takeover
|
|
96
|
+
@deflate = deflate
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return deflate.deflate(buffer, Zlib::SYNC_FLUSH)[0...-4]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require_relative 'constants'
|
|
22
|
+
|
|
23
|
+
module Protocol
|
|
24
|
+
module WebSocket
|
|
25
|
+
module Extension
|
|
26
|
+
module Compression
|
|
27
|
+
class Inflate
|
|
28
|
+
# Client reading from server.
|
|
29
|
+
def self.client(parent, server_max_window_bits: 15, server_no_context_takeover: false, **options)
|
|
30
|
+
self.new(parent,
|
|
31
|
+
window_bits: server_max_window_bits,
|
|
32
|
+
context_takeover: !server_no_context_takeover,
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Server reading from client.
|
|
37
|
+
def self.server(parent, client_max_window_bits: 15, client_no_context_takeover: false, **options)
|
|
38
|
+
self.new(parent,
|
|
39
|
+
window_bits: client_max_window_bits,
|
|
40
|
+
context_takeover: !client_no_context_takeover,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
TRAILER = [0x00, 0x00, 0xff, 0xff].pack('C*')
|
|
45
|
+
|
|
46
|
+
def initialize(parent, context_takeover: true, window_bits: 15)
|
|
47
|
+
@parent = parent
|
|
48
|
+
|
|
49
|
+
@inflate = nil
|
|
50
|
+
|
|
51
|
+
if window_bits < MINIMUM_WINDOW_BITS
|
|
52
|
+
window_bits = MINIMUM_WINDOW_BITS
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@window_bits = window_bits
|
|
56
|
+
@context_takeover = context_takeover
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr :window_bits
|
|
60
|
+
attr :context_takeover
|
|
61
|
+
|
|
62
|
+
def unpack_frames(frames, **options)
|
|
63
|
+
buffer = @parent.unpack_frames(frames, **options)
|
|
64
|
+
|
|
65
|
+
frame = frames.first
|
|
66
|
+
|
|
67
|
+
if frame.flags & Frame::RSV1
|
|
68
|
+
buffer = self.inflate(buffer)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
frame.flags &= ~Frame::RSV1
|
|
72
|
+
|
|
73
|
+
return buffer
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def inflate(buffer)
|
|
79
|
+
inflate = @inflate || Zlib::Inflate.new(-@window_bits)
|
|
80
|
+
|
|
81
|
+
if @context_takeover
|
|
82
|
+
@inflate = inflate
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return inflate.inflate(buffer + TRAILER)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require_relative 'compression/constants'
|
|
22
|
+
require_relative 'compression/inflate'
|
|
23
|
+
require_relative 'compression/deflate'
|
|
24
|
+
|
|
25
|
+
module Protocol
|
|
26
|
+
module WebSocket
|
|
27
|
+
module Extension
|
|
28
|
+
module Compression
|
|
29
|
+
# Client offer to server, construct a list of requested compression parameters suitable for the `Sec-WebSocket-Extensions` header.
|
|
30
|
+
# @returns [Array(String)] a list of compression parameters suitable to send to the server.
|
|
31
|
+
def self.offer(client_max_window_bits: true, server_max_window_bits: true, client_no_context_takeover: false, server_no_context_takeover: false)
|
|
32
|
+
|
|
33
|
+
header = [NAME]
|
|
34
|
+
|
|
35
|
+
case client_max_window_bits
|
|
36
|
+
when 8..15
|
|
37
|
+
header << "client_max_window_bits=#{client_max_window_bits}"
|
|
38
|
+
when true
|
|
39
|
+
header << 'client_max_window_bits'
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Invalid local maximum window bits!"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if client_no_context_takeover
|
|
45
|
+
header << 'client_no_context_takeover'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
case server_max_window_bits
|
|
49
|
+
when 8..15
|
|
50
|
+
header << "server_max_window_bits=#{server_max_window_bits}"
|
|
51
|
+
when true
|
|
52
|
+
# Default (unspecified) to the server maximum window bits.
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "Invalid remote maximum window bits!"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if server_no_context_takeover
|
|
58
|
+
header << 'server_no_context_takeover'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return header
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Negotiate on the server a response to client based on the incoming client offer.
|
|
65
|
+
# @parameter options [Hash] a hash of options which are accepted by the server.
|
|
66
|
+
# @returns [Array(String)] a list of compression parameters suitable to send back to the client.
|
|
67
|
+
def self.negotiate(arguments, **options)
|
|
68
|
+
header = [NAME]
|
|
69
|
+
|
|
70
|
+
arguments.each do |key, value|
|
|
71
|
+
case key
|
|
72
|
+
when "server_no_context_takeover"
|
|
73
|
+
options[:server_no_context_takeover] = true
|
|
74
|
+
header << key
|
|
75
|
+
when "client_no_context_takeover"
|
|
76
|
+
options[:client_no_context_takeover] = true
|
|
77
|
+
header << key
|
|
78
|
+
when "server_max_window_bits"
|
|
79
|
+
value = Integer(value || 15)
|
|
80
|
+
value = MINIMUM_WINDOW_BITS if value < MINIMUM_WINDOW_BITS
|
|
81
|
+
options[:server_max_window_bits] = value
|
|
82
|
+
header << "server_max_window_bits=#{value}"
|
|
83
|
+
when "client_max_window_bits"
|
|
84
|
+
value = Integer(value || 15)
|
|
85
|
+
value = MINIMUM_WINDOW_BITS if value < MINIMUM_WINDOW_BITS
|
|
86
|
+
options[:client_max_window_bits] = value
|
|
87
|
+
header << "client_max_window_bits=#{value}"
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError, "Unknown option #{key}!"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The header which represents the final accepted/negotiated configuration.
|
|
94
|
+
return header, options
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @parameter options [Hash] a hash of options which are accepted by the server.
|
|
98
|
+
def self.server(connection, **options)
|
|
99
|
+
connection.reserve!(Frame::RSV1)
|
|
100
|
+
|
|
101
|
+
connection.reader = Inflate.server(connection.reader, **options)
|
|
102
|
+
connection.writer = Deflate.server(connection.writer, **options)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Accept on the client, the negotiated server response.
|
|
106
|
+
# @parameter options [Hash] a hash of options which are accepted by the client.
|
|
107
|
+
# @parameter arguments [Array(String)] a list of compression parameters as accepted/negotiated by the server.
|
|
108
|
+
def self.accept(arguments, **options)
|
|
109
|
+
arguments.each do |key, value|
|
|
110
|
+
case key
|
|
111
|
+
when "server_no_context_takeover"
|
|
112
|
+
options[:server_no_context_takeover] = true
|
|
113
|
+
when "client_no_context_takeover"
|
|
114
|
+
options[:client_no_context_takeover] = true
|
|
115
|
+
when "server_max_window_bits"
|
|
116
|
+
options[:server_max_window_bits] = Integer(value || 15)
|
|
117
|
+
when "client_max_window_bits"
|
|
118
|
+
options[:client_max_window_bits] = Integer(value || 15)
|
|
119
|
+
else
|
|
120
|
+
raise ArgumentError, "Unknown option #{key}!"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
return options
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @parameter options [Hash] a hash of options which are accepted by the client.
|
|
128
|
+
def self.client(connection, **options)
|
|
129
|
+
connection.reserve!(Frame::RSV1)
|
|
130
|
+
|
|
131
|
+
connection.reader = Inflate.client(connection.reader, **options)
|
|
132
|
+
connection.writer = Deflate.client(connection.writer, **options)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Extensions
|
|
2
|
+
|
|
3
|
+
WebSockets have a mechanism for implementing extensions. The only published extension is for per-message compression. It operates on complete messages rather than individual frames.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Clients need to define a set of extensions they want to support. The server then receives this via the `Sec-WebSocket-Extensions` header which includes a list of:
|
|
8
|
+
|
|
9
|
+
Name, Options
|
|
10
|
+
|
|
11
|
+
The server processes this and returns a subset of accepted `(Name, Options)`. It also instantiates the extensions and applies them to the server connection object.
|
|
12
|
+
|
|
13
|
+
The client receives a list of accepted `(Name, Options)` and instantiates the extensions and applies them to the client connection object.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require_relative 'extension/compression'
|
|
22
|
+
require_relative 'headers'
|
|
23
|
+
|
|
24
|
+
module Protocol
|
|
25
|
+
module WebSocket
|
|
26
|
+
module Extensions
|
|
27
|
+
def self.parse(headers)
|
|
28
|
+
return to_enum(:parse, headers) unless block_given?
|
|
29
|
+
|
|
30
|
+
headers.each do |header|
|
|
31
|
+
name, *arguments = header.split(/\s*;\s*/)
|
|
32
|
+
|
|
33
|
+
arguments = arguments.map do |argument|
|
|
34
|
+
argument.split('=', 2)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
yield name, arguments
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Client
|
|
42
|
+
def self.default
|
|
43
|
+
self.new([
|
|
44
|
+
[Extension::Compression, {}]
|
|
45
|
+
])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(extensions = [])
|
|
49
|
+
@extensions = extensions
|
|
50
|
+
@accepted = []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr :extensions
|
|
54
|
+
attr :accepted
|
|
55
|
+
|
|
56
|
+
def named
|
|
57
|
+
@extensions.map do |extension|
|
|
58
|
+
[extension.first::NAME, extension]
|
|
59
|
+
end.to_h
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def offer
|
|
63
|
+
@extensions.each do |extension, options|
|
|
64
|
+
if header = extension.offer(**options)
|
|
65
|
+
yield header
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def accept(headers)
|
|
71
|
+
named = self.named
|
|
72
|
+
|
|
73
|
+
# Each response header should map to at least one extension.
|
|
74
|
+
Extensions.parse(headers) do |name, arguments|
|
|
75
|
+
if extension = named.delete(name)
|
|
76
|
+
klass, options = extension
|
|
77
|
+
|
|
78
|
+
options = klass.accept(arguments, **options)
|
|
79
|
+
|
|
80
|
+
@accepted << [klass, options]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def apply(connection)
|
|
86
|
+
@accepted.each do |(klass, options)|
|
|
87
|
+
klass.client(connection, **options)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Server
|
|
93
|
+
def self.default
|
|
94
|
+
self.new([
|
|
95
|
+
[Extension::Compression, {}]
|
|
96
|
+
])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initialize(extensions)
|
|
100
|
+
@extensions = extensions
|
|
101
|
+
@accepted = []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
attr :extensions
|
|
105
|
+
attr :accepted
|
|
106
|
+
|
|
107
|
+
def named
|
|
108
|
+
@extensions.map do |extension|
|
|
109
|
+
[extension.first::NAME, extension]
|
|
110
|
+
end.to_h
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def accept(headers)
|
|
114
|
+
extensions = []
|
|
115
|
+
|
|
116
|
+
named = self.named
|
|
117
|
+
response = []
|
|
118
|
+
|
|
119
|
+
# Each response header should map to at least one extension.
|
|
120
|
+
Extensions.parse(headers) do |name, arguments|
|
|
121
|
+
if extension = named[name]
|
|
122
|
+
klass, options = extension
|
|
123
|
+
|
|
124
|
+
if result = klass.negotiate(arguments, **options)
|
|
125
|
+
header, options = result
|
|
126
|
+
|
|
127
|
+
# The extension is accepted and no further offers will be considered:
|
|
128
|
+
named.delete(name)
|
|
129
|
+
|
|
130
|
+
yield header
|
|
131
|
+
|
|
132
|
+
@accepted << [klass, options]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return headers
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def apply(connection)
|
|
141
|
+
@accepted.reverse_each do |(klass, options)|
|
|
142
|
+
klass.server(connection, **options)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -25,16 +25,22 @@ module Protocol
|
|
|
25
25
|
class Frame
|
|
26
26
|
include Comparable
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
RSV1 = 0b0100
|
|
29
|
+
RSV2 = 0b0010
|
|
30
|
+
RSV3 = 0b0001
|
|
31
|
+
RESERVED = RSV1 | RSV2 | RSV3
|
|
29
32
|
|
|
33
|
+
OPCODE = 0
|
|
34
|
+
|
|
30
35
|
# @parameter length [Integer] The length of the payload, or nil if the header has not been read yet.
|
|
31
36
|
# @parameter mask [Boolean | String] An optional 4-byte string which is used to mask the payload.
|
|
32
|
-
def initialize(finished = true, payload = nil, opcode: self.class::OPCODE, mask: false)
|
|
37
|
+
def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPCODE, mask: false)
|
|
33
38
|
if mask == true
|
|
34
39
|
mask = SecureRandom.bytes(4)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
@finished = finished
|
|
43
|
+
@flags = flags
|
|
38
44
|
@opcode = opcode
|
|
39
45
|
@mask = mask
|
|
40
46
|
@length = payload&.bytesize
|
|
@@ -46,11 +52,11 @@ module Protocol
|
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
def to_ary
|
|
49
|
-
[@finished, @opcode, @mask, @length, @payload]
|
|
55
|
+
[@finished, @flags, @opcode, @mask, @length, @payload]
|
|
50
56
|
end
|
|
51
57
|
|
|
52
58
|
def control?
|
|
53
|
-
@opcode & 0x8
|
|
59
|
+
@opcode & 0x8 != 0
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
def data?
|
|
@@ -87,6 +93,7 @@ module Protocol
|
|
|
87
93
|
# +---------------------------------------------------------------+
|
|
88
94
|
|
|
89
95
|
attr_accessor :finished
|
|
96
|
+
attr_accessor :flags
|
|
90
97
|
attr_accessor :opcode
|
|
91
98
|
attr_accessor :mask
|
|
92
99
|
attr_accessor :length
|
|
@@ -98,9 +105,9 @@ module Protocol
|
|
|
98
105
|
if length.bit_length > 63
|
|
99
106
|
raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
|
|
100
107
|
end
|
|
101
|
-
|
|
108
|
+
|
|
102
109
|
if @mask
|
|
103
|
-
@payload = String.new
|
|
110
|
+
@payload = String.new(encoding: Encoding::BINARY)
|
|
104
111
|
|
|
105
112
|
for i in 0...data.bytesize do
|
|
106
113
|
@payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
|
|
@@ -111,11 +118,13 @@ module Protocol
|
|
|
111
118
|
@payload = data
|
|
112
119
|
@length = length
|
|
113
120
|
end
|
|
121
|
+
|
|
122
|
+
return self
|
|
114
123
|
end
|
|
115
124
|
|
|
116
125
|
def unpack
|
|
117
126
|
if @mask and !@payload.empty?
|
|
118
|
-
data = String.new
|
|
127
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
119
128
|
|
|
120
129
|
for i in 0...@payload.bytesize do
|
|
121
130
|
data << (@payload.getbyte(i) ^ @mask.getbyte(i % 4))
|
|
@@ -135,19 +144,33 @@ module Protocol
|
|
|
135
144
|
byte = buffer.unpack("C").first
|
|
136
145
|
|
|
137
146
|
finished = (byte & 0b1000_0000 != 0)
|
|
138
|
-
|
|
147
|
+
flags = (byte & 0b0111_0000) >> 4
|
|
139
148
|
opcode = byte & 0b0000_1111
|
|
140
149
|
|
|
141
|
-
|
|
150
|
+
if (0x3 .. 0x7).include?(opcode)
|
|
151
|
+
raise ProtocolError, "non-control opcode = #{opcode} is reserved!"
|
|
152
|
+
elsif (0xB .. 0xF).include?(opcode)
|
|
153
|
+
raise ProtocolError, "control opcode = #{opcode} is reserved!"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return finished, flags, opcode
|
|
142
157
|
end
|
|
143
158
|
|
|
144
|
-
def self.read(finished, opcode, stream, maximum_frame_size)
|
|
159
|
+
def self.read(finished, flags, opcode, stream, maximum_frame_size)
|
|
145
160
|
buffer = stream.read(1) or raise EOFError, "Could not read header!"
|
|
146
161
|
byte = buffer.unpack("C").first
|
|
147
162
|
|
|
148
163
|
mask = (byte & 0b1000_0000 != 0)
|
|
149
164
|
length = byte & 0b0111_1111
|
|
150
165
|
|
|
166
|
+
if opcode & 0x8 != 0
|
|
167
|
+
if length > 125
|
|
168
|
+
raise ProtocolError, "Invalid control frame payload length: #{length} > 125!"
|
|
169
|
+
elsif !finished
|
|
170
|
+
raise ProtocolError, "Fragmented control frame!"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
151
174
|
if length == 126
|
|
152
175
|
buffer = stream.read(2) or raise EOFError, "Could not read length!"
|
|
153
176
|
length = buffer.unpack('n').first
|
|
@@ -170,11 +193,11 @@ module Protocol
|
|
|
170
193
|
raise EOFError, "Incorrect payload length: #{@length} != #{@payload.bytesize}!"
|
|
171
194
|
end
|
|
172
195
|
|
|
173
|
-
return self.new(finished, payload, opcode: opcode, mask: mask)
|
|
196
|
+
return self.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
|
|
174
197
|
end
|
|
175
198
|
|
|
176
199
|
def write(stream)
|
|
177
|
-
buffer = String.new
|
|
200
|
+
buffer = String.new(encoding: Encoding::BINARY)
|
|
178
201
|
|
|
179
202
|
if @payload&.bytesize != @length
|
|
180
203
|
raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize} for #{self}!"
|
|
@@ -193,7 +216,7 @@ module Protocol
|
|
|
193
216
|
end
|
|
194
217
|
|
|
195
218
|
buffer << [
|
|
196
|
-
(@finished ? 0b1000_0000 : 0) | @opcode,
|
|
219
|
+
(@finished ? 0b1000_0000 : 0) | (@flags << 4) | @opcode,
|
|
197
220
|
(@mask ? 0b1000_0000 : 0) | short_length,
|
|
198
221
|
].pack('CC')
|
|
199
222
|
|
|
@@ -29,7 +29,7 @@ require_relative 'pong_frame'
|
|
|
29
29
|
|
|
30
30
|
module Protocol
|
|
31
31
|
module WebSocket
|
|
32
|
-
# HTTP/2 frame type mapping as defined by the spec
|
|
32
|
+
# HTTP/2 frame type mapping as defined by the spec.
|
|
33
33
|
FRAMES = {
|
|
34
34
|
0x0 => ContinuationFrame,
|
|
35
35
|
0x1 => TextFrame,
|
|
@@ -39,8 +39,10 @@ module Protocol
|
|
|
39
39
|
0xA => PongFrame,
|
|
40
40
|
}.freeze
|
|
41
41
|
|
|
42
|
+
# The maximum allowed frame size in bytes.
|
|
42
43
|
MAXIMUM_ALLOWED_FRAME_SIZE = 2**63
|
|
43
44
|
|
|
45
|
+
# Wraps an underlying {Async::IO::Stream} for reading and writing binary data into structured frames.
|
|
44
46
|
class Framer
|
|
45
47
|
def initialize(stream, frames = FRAMES)
|
|
46
48
|
@stream = stream
|
|
@@ -55,13 +57,15 @@ module Protocol
|
|
|
55
57
|
@stream.flush
|
|
56
58
|
end
|
|
57
59
|
|
|
60
|
+
# Read a frame from the underlying stream.
|
|
61
|
+
# @returns [Frame]
|
|
58
62
|
def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
|
|
59
63
|
# Read the header:
|
|
60
|
-
finished, opcode = read_header
|
|
64
|
+
finished, flags, opcode = read_header
|
|
61
65
|
|
|
62
66
|
# Read the frame:
|
|
63
67
|
klass = @frames[opcode] || Frame
|
|
64
|
-
frame = klass.read(finished, opcode, @stream, maximum_frame_size)
|
|
68
|
+
frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size)
|
|
65
69
|
|
|
66
70
|
return frame
|
|
67
71
|
end
|
|
@@ -27,13 +27,17 @@ module Protocol
|
|
|
27
27
|
# The protocol string used for the `upgrade:` header (HTTP/1) and `:protocol` pseudo-header (HTTP/2).
|
|
28
28
|
PROTOCOL = "websocket".freeze
|
|
29
29
|
|
|
30
|
-
#
|
|
30
|
+
# The WebSocket protocol header, used for application level protocol negotiation.
|
|
31
31
|
SEC_WEBSOCKET_PROTOCOL = 'sec-websocket-protocol'.freeze
|
|
32
|
+
|
|
33
|
+
# The WebSocket version header. Used for negotiating binary protocol version.
|
|
32
34
|
SEC_WEBSOCKET_VERSION = 'sec-websocket-version'.freeze
|
|
33
35
|
|
|
34
36
|
SEC_WEBSOCKET_KEY = 'sec-websocket-key'.freeze
|
|
35
37
|
SEC_WEBSOCKET_ACCEPT = 'sec-websocket-accept'.freeze
|
|
36
38
|
|
|
39
|
+
SEC_WEBSOCKET_EXTENSIONS = 'sec-websocket-extensions'.freeze
|
|
40
|
+
|
|
37
41
|
module Nounce
|
|
38
42
|
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
39
43
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Copyright, 2022, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require 'json'
|
|
22
|
+
|
|
23
|
+
require_relative 'message'
|
|
24
|
+
|
|
25
|
+
module Protocol
|
|
26
|
+
module WebSocket
|
|
27
|
+
class JSONMessage < TextMessage
|
|
28
|
+
def self.wrap(message)
|
|
29
|
+
if message.is_a?(TextMessage)
|
|
30
|
+
self.new(message.buffer)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.generate(object)
|
|
35
|
+
self.new(JSON.generate(object))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse(symbolize_names: true, **options)
|
|
39
|
+
JSON.parse(@buffer, symbolize_names: symbolize_names, **options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_h
|
|
43
|
+
parse.to_h
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright, 2022, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
# THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
require_relative 'frame'
|
|
22
|
+
|
|
23
|
+
module Protocol
|
|
24
|
+
module WebSocket
|
|
25
|
+
class Message
|
|
26
|
+
def initialize(buffer)
|
|
27
|
+
@buffer = buffer
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr :buffer
|
|
31
|
+
|
|
32
|
+
def size
|
|
33
|
+
@buffer.bytesize
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# This can be helpful for writing tests.
|
|
37
|
+
def == other
|
|
38
|
+
@buffer == other.to_str
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_str
|
|
42
|
+
@buffer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def encoding
|
|
46
|
+
@buffer.encoding
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class TextMessage < Message
|
|
51
|
+
def send(connection, **options)
|
|
52
|
+
connection.send_text(@buffer, **options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class BinaryMessage < Message
|
|
57
|
+
def send(connection, **options)
|
|
58
|
+
connection.send_binary(@buffer, **options)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -26,10 +26,13 @@ module Protocol
|
|
|
26
26
|
class PingFrame < Frame
|
|
27
27
|
OPCODE = 0x9
|
|
28
28
|
|
|
29
|
+
# Generate a suitable reply.
|
|
30
|
+
# @returns [PongFrame]
|
|
29
31
|
def reply(**options)
|
|
30
32
|
PongFrame.new(true, self.unpack, **options)
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# Apply this frame to the specified connection.
|
|
33
36
|
def apply(connection)
|
|
34
37
|
connection.receive_ping(self)
|
|
35
38
|
end
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
# THE SOFTWARE.
|
|
20
20
|
|
|
21
21
|
require_relative 'frame'
|
|
22
|
+
require_relative 'message'
|
|
22
23
|
|
|
23
24
|
module Protocol
|
|
24
25
|
module WebSocket
|
|
26
|
+
# Implements the text frame for sending and receiving text.
|
|
25
27
|
class TextFrame < Frame
|
|
26
28
|
OPCODE = 0x1
|
|
27
29
|
|
|
@@ -29,14 +31,19 @@ module Protocol
|
|
|
29
31
|
true
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
# Decode the binary buffer into a suitable text message.
|
|
35
|
+
# @parameter buffer [String] The binary data to unpack.
|
|
36
|
+
def read_message(buffer)
|
|
37
|
+
buffer.force_encoding(Encoding::UTF_8)
|
|
38
|
+
|
|
39
|
+
unless buffer.valid_encoding?
|
|
40
|
+
raise ProtocolError, "invalid UTF-8 in text frame!"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return TextMessage.new(buffer)
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# Apply this frame to the specified connection.
|
|
40
47
|
def apply(connection)
|
|
41
48
|
connection.receive_text(self)
|
|
42
49
|
end
|
data/lib/protocol/websocket.rb
CHANGED
data.tar.gz.sig
ADDED
|
Binary file
|
metadata
CHANGED
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-websocket
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
8
|
+
- Aurora
|
|
9
|
+
- Soumya
|
|
10
|
+
- Olle Jonsson
|
|
11
|
+
- William T. Nelson
|
|
8
12
|
autorequire:
|
|
9
13
|
bindir: bin
|
|
10
|
-
cert_chain:
|
|
11
|
-
|
|
14
|
+
cert_chain:
|
|
15
|
+
- |
|
|
16
|
+
-----BEGIN CERTIFICATE-----
|
|
17
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
|
18
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
|
19
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
|
20
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
|
21
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
|
22
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
|
23
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
|
24
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
|
25
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
|
26
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
|
27
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
|
28
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
|
29
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
|
30
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
|
31
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
|
32
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
|
33
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
|
34
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
|
35
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
|
36
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
|
37
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
|
38
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
|
39
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
40
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
41
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
42
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
43
|
+
-----END CERTIFICATE-----
|
|
44
|
+
date: 2022-08-22 00:00:00.000000000 Z
|
|
12
45
|
dependencies:
|
|
13
46
|
- !ruby/object:Gem::Dependency
|
|
14
47
|
name: protocol-http
|
|
@@ -67,34 +100,44 @@ dependencies:
|
|
|
67
100
|
- !ruby/object:Gem::Version
|
|
68
101
|
version: '0'
|
|
69
102
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
103
|
+
name: sus
|
|
71
104
|
requirement: !ruby/object:Gem::Requirement
|
|
72
105
|
requirements:
|
|
73
106
|
- - "~>"
|
|
74
107
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
108
|
+
version: 0.9.1
|
|
76
109
|
type: :development
|
|
77
110
|
prerelease: false
|
|
78
111
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
112
|
requirements:
|
|
80
113
|
- - "~>"
|
|
81
114
|
- !ruby/object:Gem::Version
|
|
82
|
-
version:
|
|
115
|
+
version: 0.9.1
|
|
83
116
|
description:
|
|
84
117
|
email:
|
|
85
118
|
executables: []
|
|
86
119
|
extensions: []
|
|
87
120
|
extra_rdoc_files: []
|
|
88
121
|
files:
|
|
122
|
+
- lib/.DS_Store
|
|
123
|
+
- lib/protocol/.DS_Store
|
|
89
124
|
- lib/protocol/websocket.rb
|
|
90
125
|
- lib/protocol/websocket/binary_frame.rb
|
|
91
126
|
- lib/protocol/websocket/close_frame.rb
|
|
92
127
|
- lib/protocol/websocket/connection.rb
|
|
93
128
|
- lib/protocol/websocket/continuation_frame.rb
|
|
94
129
|
- lib/protocol/websocket/error.rb
|
|
130
|
+
- lib/protocol/websocket/extension/compression.rb
|
|
131
|
+
- lib/protocol/websocket/extension/compression/constants.rb
|
|
132
|
+
- lib/protocol/websocket/extension/compression/deflate.rb
|
|
133
|
+
- lib/protocol/websocket/extension/compression/inflate.rb
|
|
134
|
+
- lib/protocol/websocket/extensions.md
|
|
135
|
+
- lib/protocol/websocket/extensions.rb
|
|
95
136
|
- lib/protocol/websocket/frame.rb
|
|
96
137
|
- lib/protocol/websocket/framer.rb
|
|
97
138
|
- lib/protocol/websocket/headers.rb
|
|
139
|
+
- lib/protocol/websocket/json_message.rb
|
|
140
|
+
- lib/protocol/websocket/message.rb
|
|
98
141
|
- lib/protocol/websocket/ping_frame.rb
|
|
99
142
|
- lib/protocol/websocket/pong_frame.rb
|
|
100
143
|
- lib/protocol/websocket/text_frame.rb
|
|
@@ -118,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
118
161
|
- !ruby/object:Gem::Version
|
|
119
162
|
version: '0'
|
|
120
163
|
requirements: []
|
|
121
|
-
rubygems_version: 3.
|
|
164
|
+
rubygems_version: 3.3.7
|
|
122
165
|
signing_key:
|
|
123
166
|
specification_version: 4
|
|
124
167
|
summary: A low level implementation of the WebSocket protocol.
|
metadata.gz.sig
ADDED
|
Binary file
|