protocol-websocket 0.7.4 → 0.9.0
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 +90 -57
- 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 +43 -15
- data/lib/protocol/websocket/framer.rb +7 -3
- data/lib/protocol/websocket/headers.rb +5 -1
- data/lib/protocol/websocket/message.rb +61 -0
- data/lib/protocol/websocket/ping_frame.rb +5 -2
- 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 +56 -36
- metadata.gz.sig +0 -0
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.travis.yml +0 -19
- data/Gemfile +0 -4
- data/README.md +0 -64
- data/Rakefile +0 -6
- data/protocol-websocket.gemspec +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c466ba64c434d16b197dacfb48341ab14eca48ba5aefde4cf06df4f10be84fc
|
4
|
+
data.tar.gz: 3d470e57b3c94936ec31adce97e829550344e6345de423f0d3c51915d951d782
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 271a7a12c932857ea8887e67d7303a008852cc700e2d3bea0cbf96a6f30c53b8cae442f62877f4ad89c215dfb5bae81c0948da6b3cd5c267304dc368d795e542
|
7
|
+
data.tar.gz: 826444b74fd01885c66389a1c08b8b2cb99617662c3e364453d4d1572fa0237447d70d196fc867bde6231bc11efd6b47c874619bafe1efa800bcc1304a055329
|
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,18 +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
|
-
def initialize(framer, mask: nil)
|
29
|
-
if mask == true
|
30
|
-
mask = SecureRandom.bytes(4)
|
31
|
-
end
|
32
|
-
|
28
|
+
# @parameter mask [String] 4-byte mask to be used for frames generated by this connection.
|
29
|
+
def initialize(framer, mask: nil, **options)
|
33
30
|
@framer = framer
|
34
31
|
@mask = mask
|
35
32
|
|
36
33
|
@state = :open
|
37
34
|
@frames = []
|
35
|
+
|
36
|
+
@reserved = Frame::RESERVED
|
37
|
+
|
38
|
+
@reader = self
|
39
|
+
@writer = self
|
38
40
|
end
|
39
41
|
|
40
42
|
# The framer which is used for reading and writing frames.
|
@@ -43,9 +45,25 @@ module Protocol
|
|
43
45
|
# The (optional) mask which is used when generating frames.
|
44
46
|
attr :mask
|
45
47
|
|
48
|
+
# The allowed reserved bits:
|
49
|
+
attr :reserved
|
50
|
+
|
46
51
|
# Buffered frames which form part of a complete message.
|
47
52
|
attr_accessor :frames
|
48
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
|
+
|
49
67
|
def flush
|
50
68
|
@framer.flush
|
51
69
|
end
|
@@ -54,8 +72,8 @@ module Protocol
|
|
54
72
|
@state == :closed
|
55
73
|
end
|
56
74
|
|
57
|
-
def close
|
58
|
-
send_close unless closed?
|
75
|
+
def close(code = Error::NO_ERROR, reason = "")
|
76
|
+
send_close(code, reason) unless closed?
|
59
77
|
|
60
78
|
@framer.close
|
61
79
|
end
|
@@ -65,6 +83,10 @@ module Protocol
|
|
65
83
|
|
66
84
|
frame = @framer.read_frame
|
67
85
|
|
86
|
+
unless (frame.flags & @reserved).zero?
|
87
|
+
raise ProtocolError, "Received frame with reserved flags set!"
|
88
|
+
end
|
89
|
+
|
68
90
|
yield frame if block_given?
|
69
91
|
|
70
92
|
frame.apply(self)
|
@@ -82,6 +104,8 @@ module Protocol
|
|
82
104
|
|
83
105
|
def write_frame(frame)
|
84
106
|
@framer.write_frame(frame)
|
107
|
+
|
108
|
+
return frame
|
85
109
|
end
|
86
110
|
|
87
111
|
def receive_text(frame)
|
@@ -107,38 +131,16 @@ module Protocol
|
|
107
131
|
raise ProtocolError, "Received unexpected continuation!"
|
108
132
|
end
|
109
133
|
end
|
110
|
-
|
111
|
-
def send_text(buffer)
|
112
|
-
frame = TextFrame.new(mask: @mask)
|
113
|
-
frame.pack buffer
|
114
|
-
|
115
|
-
write_frame(frame)
|
116
|
-
end
|
117
|
-
|
118
|
-
def send_binary(buffer)
|
119
|
-
frame = BinaryFrame.new(mask: @mask)
|
120
|
-
frame.pack buffer
|
121
|
-
|
122
|
-
write_frame(frame)
|
123
|
-
end
|
124
|
-
|
125
|
-
def send_close(code = Error::NO_ERROR, message = nil)
|
126
|
-
frame = CloseFrame.new(mask: @mask)
|
127
|
-
frame.pack(code, message)
|
128
|
-
|
129
|
-
self.write_frame(frame)
|
130
|
-
self.flush
|
131
|
-
|
132
|
-
@state = :closed
|
133
|
-
end
|
134
|
-
|
134
|
+
|
135
135
|
def receive_close(frame)
|
136
136
|
@state = :closed
|
137
137
|
|
138
|
-
code,
|
138
|
+
code, reason = frame.unpack
|
139
|
+
|
140
|
+
send_close(code, reason)
|
139
141
|
|
140
142
|
if code and code != Error::NO_ERROR
|
141
|
-
raise ClosedError.new
|
143
|
+
raise ClosedError.new reason, code
|
142
144
|
end
|
143
145
|
end
|
144
146
|
|
@@ -161,7 +163,7 @@ module Protocol
|
|
161
163
|
|
162
164
|
def receive_ping(frame)
|
163
165
|
if @state != :closed
|
164
|
-
write_frame(frame.reply)
|
166
|
+
write_frame(frame.reply(mask: @mask))
|
165
167
|
else
|
166
168
|
raise ProtocolError, "Cannot receive ping in state #{@state}"
|
167
169
|
end
|
@@ -175,34 +177,60 @@ module Protocol
|
|
175
177
|
warn "Unhandled frame #{frame.inspect}"
|
176
178
|
end
|
177
179
|
|
178
|
-
|
179
|
-
|
180
|
-
|
180
|
+
def pack_text_frame(buffer, **options)
|
181
|
+
frame = TextFrame.new(mask: @mask)
|
182
|
+
frame.pack(buffer)
|
181
183
|
|
182
|
-
|
183
|
-
if buffer.encoding == Encoding::UTF_8
|
184
|
-
send_text(buffer)
|
185
|
-
else
|
186
|
-
send_binary(buffer)
|
187
|
-
end
|
184
|
+
return frame
|
188
185
|
end
|
189
186
|
|
190
|
-
|
191
|
-
|
192
|
-
|
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)
|
193
194
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
195
|
+
return frame
|
196
|
+
end
|
197
|
+
|
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)
|
205
|
+
|
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
|
+
if message.is_a?(String)
|
216
|
+
if message.encoding == Encoding::UTF_8
|
217
|
+
return send_text(message, **options)
|
218
|
+
else
|
219
|
+
return send_binary(message, **options)
|
200
220
|
end
|
201
221
|
end
|
222
|
+
|
223
|
+
message.send(self, **options)
|
224
|
+
end
|
225
|
+
|
226
|
+
# The default implementation for reading a message buffer.
|
227
|
+
def unpack_frames(frames)
|
228
|
+
frames.map(&:unpack).join("")
|
202
229
|
end
|
203
230
|
|
204
|
-
#
|
205
|
-
|
231
|
+
# Read a message from the connection.
|
232
|
+
# @returns message [Message] The received message.
|
233
|
+
def read(**options)
|
206
234
|
@framer.flush
|
207
235
|
|
208
236
|
while read_frame
|
@@ -210,9 +238,14 @@ module Protocol
|
|
210
238
|
frames = @frames
|
211
239
|
@frames = []
|
212
240
|
|
213
|
-
|
241
|
+
buffer = @reader.unpack_frames(frames, **options)
|
242
|
+
return frames.first.read_message(buffer)
|
214
243
|
end
|
215
244
|
end
|
245
|
+
rescue ProtocolError => error
|
246
|
+
send_close(error.code, error.message)
|
247
|
+
|
248
|
+
raise
|
216
249
|
end
|
217
250
|
end
|
218
251
|
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
|