protocol-websocket 0.7.5 → 0.8.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 +0 -0
- data/lib/protocol/websocket/binary_frame.rb +4 -0
- data/lib/protocol/websocket/close_frame.rb +32 -2
- data/lib/protocol/websocket/connection.rb +68 -33
- data/lib/protocol/websocket/extension/compression/deflate.rb +93 -0
- data/lib/protocol/websocket/extension/compression/inflate.rb +85 -0
- data/lib/protocol/websocket/extension/compression.rb +131 -0
- data/lib/protocol/websocket/extensions.md +13 -0
- data/lib/protocol/websocket/extensions.rb +146 -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/ping_frame.rb +3 -0
- data/lib/protocol/websocket/pong_frame.rb +1 -0
- data/lib/protocol/websocket/text_frame.rb +12 -6
- data/lib/protocol/websocket/version.rb +1 -1
- data/lib/protocol/websocket.rb +1 -0
- data.tar.gz.sig +4 -0
- metadata +42 -4
- 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: 076d348e98312526d98d8fc80840b9760cf30496ac4ca83d129ac53e120c10a0
|
4
|
+
data.tar.gz: 142d1fa0b2c8e06a3642cfff30f76f835308b23ca3d6964e1c3ae0870204d5e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 941eda4ac12acbc31bdd359cae2b2aaffd59f78f328844fcd171f6aca517cc225831ea3726f919448eaef3c7246a630d415988528ef08f4f6a394239b5924aeb
|
7
|
+
data.tar.gz: 9c37cfc67173a3632c344d042682f906b9b9afc2c4cbc35263f9cc48fa0cf4e62ee7348b4ce5a1af7bed2b118d23f477721e2b575f8d91f991806fa53996da7c
|
checksums.yaml.gz.sig
ADDED
Binary file
|
@@ -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)
|
@@ -106,23 +132,31 @@ module Protocol
|
|
106
132
|
end
|
107
133
|
end
|
108
134
|
|
109
|
-
def
|
135
|
+
def text_message(buffer, **options)
|
110
136
|
frame = TextFrame.new(mask: @mask)
|
111
|
-
frame.pack
|
137
|
+
frame.pack(buffer)
|
112
138
|
|
113
|
-
|
139
|
+
return frame
|
114
140
|
end
|
115
141
|
|
116
|
-
def
|
142
|
+
def send_text(buffer, **options)
|
143
|
+
write_frame(@writer.text_message(buffer, **options))
|
144
|
+
end
|
145
|
+
|
146
|
+
def binary_message(buffer, **options)
|
117
147
|
frame = BinaryFrame.new(mask: @mask)
|
118
|
-
frame.pack
|
148
|
+
frame.pack(buffer)
|
119
149
|
|
120
|
-
|
150
|
+
return frame
|
121
151
|
end
|
122
152
|
|
123
|
-
def
|
153
|
+
def send_binary(buffer, **options)
|
154
|
+
write_frame(@writer.binary_message(buffer, **options))
|
155
|
+
end
|
156
|
+
|
157
|
+
def send_close(code = Error::NO_ERROR, reason = "")
|
124
158
|
frame = CloseFrame.new(mask: @mask)
|
125
|
-
frame.pack(code,
|
159
|
+
frame.pack(code, reason)
|
126
160
|
|
127
161
|
self.write_frame(frame)
|
128
162
|
self.flush
|
@@ -133,10 +167,12 @@ module Protocol
|
|
133
167
|
def receive_close(frame)
|
134
168
|
@state = :closed
|
135
169
|
|
136
|
-
code,
|
170
|
+
code, reason = frame.unpack
|
171
|
+
|
172
|
+
send_close(code, reason)
|
137
173
|
|
138
174
|
if code and code != Error::NO_ERROR
|
139
|
-
raise ClosedError.new
|
175
|
+
raise ClosedError.new reason, code
|
140
176
|
end
|
141
177
|
end
|
142
178
|
|
@@ -173,34 +209,24 @@ module Protocol
|
|
173
209
|
warn "Unhandled frame #{frame.inspect}"
|
174
210
|
end
|
175
211
|
|
176
|
-
|
177
|
-
def write(buffer)
|
178
|
-
# https://tools.ietf.org/html/rfc6455#section-5.6
|
179
|
-
|
212
|
+
def write_message(buffer, **options)
|
180
213
|
# Text: The "Payload data" is text data encoded as UTF-8
|
181
214
|
if buffer.encoding == Encoding::UTF_8
|
182
|
-
send_text(buffer)
|
215
|
+
send_text(buffer, **options)
|
183
216
|
else
|
184
|
-
send_binary(buffer)
|
217
|
+
send_binary(buffer, **options)
|
185
218
|
end
|
186
219
|
end
|
187
220
|
|
188
|
-
# @
|
189
|
-
def
|
190
|
-
|
221
|
+
# @param buffer [String] a unicode or binary string.
|
222
|
+
def write(buffer, **options)
|
223
|
+
# https://tools.ietf.org/html/rfc6455#section-5.6
|
191
224
|
|
192
|
-
|
193
|
-
if @frames.last&.finished?
|
194
|
-
buffer = @frames.map(&:unpack).join
|
195
|
-
@frames = []
|
196
|
-
|
197
|
-
return buffer
|
198
|
-
end
|
199
|
-
end
|
225
|
+
self.write_message(buffer, **options)
|
200
226
|
end
|
201
227
|
|
202
|
-
#
|
203
|
-
def
|
228
|
+
# @return [String] a unicode or binary string.
|
229
|
+
def read(**options)
|
204
230
|
@framer.flush
|
205
231
|
|
206
232
|
while read_frame
|
@@ -208,9 +234,18 @@ module Protocol
|
|
208
234
|
frames = @frames
|
209
235
|
@frames = []
|
210
236
|
|
211
|
-
|
237
|
+
buffer = @reader.read_message(frames, **options)
|
238
|
+
return frames.first.read_message(buffer)
|
212
239
|
end
|
213
240
|
end
|
241
|
+
rescue ProtocolError => error
|
242
|
+
send_close(error.code, error.message)
|
243
|
+
|
244
|
+
raise
|
245
|
+
end
|
246
|
+
|
247
|
+
def read_message(frames, **options)
|
248
|
+
frames.map(&:unpack).join("")
|
214
249
|
end
|
215
250
|
end
|
216
251
|
end
|
@@ -0,0 +1,93 @@
|
|
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
|
+
class Deflate
|
28
|
+
def self.client(parent, client_window_bits: 15, client_no_context_takeover: false, **options)
|
29
|
+
self.new(parent,
|
30
|
+
window_bits: client_window_bits,
|
31
|
+
context_takeover: !client_no_context_takeover,
|
32
|
+
**options
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.server(parent, server_window_bits: 15, server_no_context_takeover: false, **options)
|
37
|
+
self.new(parent,
|
38
|
+
window_bits: server_window_bits,
|
39
|
+
context_takeover: !server_no_context_takeover,
|
40
|
+
**options
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(parent, level: Zlib::DEFAULT_COMPRESSION, memory_level: Zlib::DEF_MEM_LEVEL, strategy: Zlib::DEFAULT_STRATEGY, window_bits: 15, context_takeover: true, **options)
|
45
|
+
@parent = parent
|
46
|
+
|
47
|
+
@deflate = nil
|
48
|
+
|
49
|
+
@compression_level = level
|
50
|
+
@memory_level = memory_level
|
51
|
+
@strategy = strategy
|
52
|
+
|
53
|
+
@window_bits = window_bits
|
54
|
+
@context_takeover = context_takeover
|
55
|
+
end
|
56
|
+
|
57
|
+
def text_message(buffer, compress: true, **options)
|
58
|
+
buffer = self.deflate(buffer)
|
59
|
+
|
60
|
+
frame = @parent.text_message(buffer, **options)
|
61
|
+
|
62
|
+
frame.flags |= Frame::RSV1
|
63
|
+
|
64
|
+
return frame
|
65
|
+
end
|
66
|
+
|
67
|
+
def binary_message(buffer, compress: false, **options)
|
68
|
+
message = self.deflate(buffer)
|
69
|
+
|
70
|
+
frame = parent.binary_message(buffer, **options)
|
71
|
+
|
72
|
+
frame.flags |= Frame::RSV1
|
73
|
+
|
74
|
+
return frame
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def deflate(buffer)
|
80
|
+
Console.logger.info(self, "Deflating #{buffer.size} bytes")
|
81
|
+
deflate = @deflate || Zlib::Deflate.new(@level, -@window_bits, @memory_level, @strategy)
|
82
|
+
|
83
|
+
if @context_takeover
|
84
|
+
@deflate = deflate
|
85
|
+
end
|
86
|
+
|
87
|
+
return @deflate.deflate(buffer, Zlib::SYNC_FLUSH)[0...-4]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,85 @@
|
|
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
|
+
class Inflate
|
28
|
+
def self.client(parent, client_window_bits: 15, client_no_context_takeover: false, **options)
|
29
|
+
self.new(parent,
|
30
|
+
window_bits: client_window_bits,
|
31
|
+
context_takeover: !client_no_context_takeover,
|
32
|
+
**options
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.server(parent, server_window_bits: 15, server_no_context_takeover: false, **options)
|
37
|
+
self.new(parent,
|
38
|
+
window_bits: server_window_bits,
|
39
|
+
context_takeover: !server_no_context_takeover,
|
40
|
+
**options
|
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
|
+
@window_bits = window_bits
|
52
|
+
@context_takeover = context_takeover
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_message(frames, **options)
|
56
|
+
buffer = @parent.read_message(frames, **options)
|
57
|
+
|
58
|
+
frame = frames.first
|
59
|
+
|
60
|
+
if frame.flags & Frame::RSV1
|
61
|
+
buffer = self.inflate(buffer)
|
62
|
+
end
|
63
|
+
|
64
|
+
frame.flags &= ~Frame::RSV1
|
65
|
+
|
66
|
+
return buffer
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def inflate(buffer)
|
72
|
+
Console.logger.info(self, "Inflating #{buffer.bytesize} bytes")
|
73
|
+
inflate = @inflate || Zlib::Inflate.new(-@window_bits)
|
74
|
+
|
75
|
+
if @context_takeover
|
76
|
+
@inflate = inflate
|
77
|
+
end
|
78
|
+
|
79
|
+
return inflate.inflate(buffer + TRAILER)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,131 @@
|
|
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
|
+
require_relative 'compression/inflate'
|
23
|
+
require_relative 'compression/deflate'
|
24
|
+
|
25
|
+
module Protocol
|
26
|
+
module WebSocket
|
27
|
+
module Extension
|
28
|
+
module Compression
|
29
|
+
NAME = 'permessage-deflate'
|
30
|
+
|
31
|
+
# Client offer to server, construct a list of requested compression parameters suitable for the `Sec-WebSocket-Extensions` header.
|
32
|
+
# @returns [Array(String)] a list of compression parameters suitable to send to the server.
|
33
|
+
def self.offer(client_window_bits: true, server_window_bits: true, client_no_context_takeover: false, server_no_context_takeover: false)
|
34
|
+
|
35
|
+
header = [NAME]
|
36
|
+
|
37
|
+
case client_window_bits
|
38
|
+
when 8..15
|
39
|
+
header << "client_max_window_bits=#{client_window_bits}"
|
40
|
+
when true
|
41
|
+
header << 'client_max_window_bits'
|
42
|
+
else
|
43
|
+
raise ArgumentError, "Invalid local maximum window bits!"
|
44
|
+
end
|
45
|
+
|
46
|
+
if client_no_context_takeover
|
47
|
+
header << 'client_no_context_takeover'
|
48
|
+
end
|
49
|
+
|
50
|
+
case server_window_bits
|
51
|
+
when 8..15
|
52
|
+
header << "server_max_window_bits=#{server_window_bits}"
|
53
|
+
when true
|
54
|
+
# Default (unspecified) to the server maximum window bits.
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Invalid remote maximum window bits!"
|
57
|
+
end
|
58
|
+
|
59
|
+
if server_no_context_takeover
|
60
|
+
header << 'server_no_context_takeover'
|
61
|
+
end
|
62
|
+
|
63
|
+
return header
|
64
|
+
end
|
65
|
+
|
66
|
+
# Negotiate on the server a response to client based on the incoming client offer.
|
67
|
+
# @parameter options [Hash] a hash of options which are accepted by the server.
|
68
|
+
# @returns [Array(String)] a list of compression parameters suitable to send back to the client.
|
69
|
+
def self.negotiate(arguments, **options)
|
70
|
+
header = [NAME]
|
71
|
+
|
72
|
+
arguments.each do |key, value|
|
73
|
+
case key
|
74
|
+
when "server_no_context_takeover"
|
75
|
+
options[:server_no_context_takeover] = false
|
76
|
+
header << key
|
77
|
+
when "client_no_context_takeover"
|
78
|
+
options[:client_no_context_takeover] = false
|
79
|
+
header << key
|
80
|
+
when "server_max_window_bits"
|
81
|
+
options[:server_max_window_bits] = Integer(value || 15)
|
82
|
+
when "client_max_window_bits"
|
83
|
+
options[:client_max_window_bits] = Integer(value || 15)
|
84
|
+
else
|
85
|
+
raise ArgumentError, "Unknown option #{key}!"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# The header which represents the final accepted/negotiated configuration.
|
90
|
+
return header
|
91
|
+
end
|
92
|
+
|
93
|
+
# @parameter options [Hash] a hash of options which are accepted by the server.
|
94
|
+
def self.server(connection, **options)
|
95
|
+
connection.reserve!(Frame::RSV1)
|
96
|
+
|
97
|
+
connection.reader = Inflate.server(connection.reader, **options)
|
98
|
+
connection.writer = Deflate.server(connection.writer, **options)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Accept on the client, the negotiated server response.
|
102
|
+
# @parameter options [Hash] a hash of options which are accepted by the client.
|
103
|
+
# @parameter arguments [Array(String)] a list of compression parameters as accepted/negotiated by the server.
|
104
|
+
def self.accept(arguments, **options)
|
105
|
+
arguments.each do |key, value|
|
106
|
+
case key
|
107
|
+
when "server_no_context_takeover"
|
108
|
+
options[:server_no_context_takeover] = false
|
109
|
+
when "client_no_context_takeover"
|
110
|
+
options[:client_no_context_takeover] = false
|
111
|
+
when "server_max_window_bits"
|
112
|
+
options[:server_max_window_bits] = Integer(value || 15)
|
113
|
+
when "client_max_window_bits"
|
114
|
+
options[:client_max_window_bits] = Integer(value || 15)
|
115
|
+
else
|
116
|
+
raise ArgumentError, "Unknown option #{key}!"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# @parameter options [Hash] a hash of options which are accepted by the client.
|
122
|
+
def self.client(connection, **options)
|
123
|
+
connection.reserve!(Frame::RSV1)
|
124
|
+
|
125
|
+
connection.reader = Inflate.client(connection.reader, **options)
|
126
|
+
connection.writer = Deflate.client(connection.writer, **options)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
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,146 @@
|
|
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
|
+
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.server(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 header = klass.negotiate(arguments, **options)
|
125
|
+
# The extension is accepted and no further offers will be considered:
|
126
|
+
named.delete(name)
|
127
|
+
|
128
|
+
yield header
|
129
|
+
|
130
|
+
@accepted << [klass, options]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
return headers
|
136
|
+
end
|
137
|
+
|
138
|
+
def apply(connection)
|
139
|
+
@accepted.reverse_each do |(klass, options)|
|
140
|
+
klass.server(connection, **options)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
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
|
|
@@ -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
|
@@ -22,6 +22,7 @@ require_relative 'frame'
|
|
22
22
|
|
23
23
|
module Protocol
|
24
24
|
module WebSocket
|
25
|
+
# Implements the text frame for sending and receiving text.
|
25
26
|
class TextFrame < Frame
|
26
27
|
OPCODE = 0x1
|
27
28
|
|
@@ -29,14 +30,19 @@ module Protocol
|
|
29
30
|
true
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
# Decode the binary buffer into a suitable text message.
|
34
|
+
# @parameter buffer [String] The binary data to unpack.
|
35
|
+
def read_message(buffer)
|
36
|
+
buffer.force_encoding(Encoding::UTF_8)
|
37
|
+
|
38
|
+
unless buffer.valid_encoding?
|
39
|
+
raise ProtocolError, "invalid UTF-8 in text frame!"
|
40
|
+
end
|
41
|
+
|
42
|
+
buffer
|
38
43
|
end
|
39
44
|
|
45
|
+
# Apply this frame to the specified connection.
|
40
46
|
def apply(connection)
|
41
47
|
connection.receive_text(self)
|
42
48
|
end
|
data/lib/protocol/websocket.rb
CHANGED
data.tar.gz.sig
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
}� *>��K��X�a��*3��j���ss�U��0p2�����SE�j9�*�{�b��v��z�� �π�hf<}�>Ɩȶ����us9�v��i�P���TB<�s^�W�AP!��*{����c�P��_�9�K*�{"(�T&%(�hy�g�M3lR4�C�sά�m�ڤ����`�4*/v
|
2
|
+
�`�LA0�d��R�U�ޥ��������������������8<I
|
3
|
+
x���軦�_����%uW>2��Z�*��C�"`0Ƞ[�`�M�C���F�F���7.aq��f��u˄!��������'�]�Q@�c�
|
4
|
+
���(���l�33MΩ:�w�C'����~r�AfC*���4yp���z���\JZ�%�;]c��
|
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.8.0
|
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-20 00:00:00.000000000 Z
|
12
45
|
dependencies:
|
13
46
|
- !ruby/object:Gem::Dependency
|
14
47
|
name: protocol-http
|
@@ -92,6 +125,11 @@ files:
|
|
92
125
|
- lib/protocol/websocket/connection.rb
|
93
126
|
- lib/protocol/websocket/continuation_frame.rb
|
94
127
|
- lib/protocol/websocket/error.rb
|
128
|
+
- lib/protocol/websocket/extension/compression.rb
|
129
|
+
- lib/protocol/websocket/extension/compression/deflate.rb
|
130
|
+
- lib/protocol/websocket/extension/compression/inflate.rb
|
131
|
+
- lib/protocol/websocket/extensions.md
|
132
|
+
- lib/protocol/websocket/extensions.rb
|
95
133
|
- lib/protocol/websocket/frame.rb
|
96
134
|
- lib/protocol/websocket/framer.rb
|
97
135
|
- lib/protocol/websocket/headers.rb
|
@@ -118,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
156
|
- !ruby/object:Gem::Version
|
119
157
|
version: '0'
|
120
158
|
requirements: []
|
121
|
-
rubygems_version: 3.
|
159
|
+
rubygems_version: 3.3.7
|
122
160
|
signing_key:
|
123
161
|
specification_version: 4
|
124
162
|
summary: A low level implementation of the WebSocket protocol.
|
metadata.gz.sig
ADDED
Binary file
|