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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d2733227a287e2dfb58f393566e6afea44470aba32c7a8eda8bd9a8f25c2d72
4
- data.tar.gz: bdd0966fcc837f1314cf2b88e55699f2de57e6d6af36ecbe3bcdf0c12350935a
3
+ metadata.gz: 0ca30621d2f489b08f475047f539d2fe048661dbe2b109a8d0ef441c6e95ed37
4
+ data.tar.gz: fe325e6b9041103c704cf912f73f3838c2e9d50af6368b2db54e280906600aa4
5
5
  SHA512:
6
- metadata.gz: a3fdd8546a648fa96663aafe278499e3a19266d1dfd9d2c68cd6d330f24c8af7d31ee94fd88c632a4bddec85dd56456980bf5a27d02197548279e069eae3d515
7
- data.tar.gz: 2b4faa47644b9811dab2d856831176b282c83df85863fad5db5a7b28055b123230ac89ae2e29eb3529843fec12d80ec11f26b2894ca85a2f243eef2d09bf10fb
6
+ metadata.gz: 35a0841efafc3a3b1fb98cbfdccff848a51262cf3e493d78cb95c4f300a1ced40779837d51e31edfd5d2cc3626b4ad22fd9f1066bcedfd4b9d8d460d44e9ddf4
7
+ data.tar.gz: 1528871ed4e31683e9334eb91096d7ad6cf592d60020bd151e70e93324d727067dfc2a62c8ec8471ba3f3f56e289cc95890f7a6d89e32afce62c2a5fd55506fc
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,3 @@
1
+ [�uy�jZ�L!=�5C���14���r ��{���i���ij��1��I��|�żKU��(���][�H����q\��yB�C�3J�3�*���Lf�rEc��oa�$qVY;PWcG^M'^*c��(�4�Fy�����G��q1�o��Ǵ��Z���i�b��S
2
+ �&{�\S�s^C��1�<S�GRbxD�
3
+ 3OEI}��"�ͿՓ�}w�Ɖ0�s#�-f��Q�Y��� $~ʲa����/I��˩&s���t���&��E�,#�kzD�*<�
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
- return data.unpack(FORMAT)
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
- super [code, reason].pack(FORMAT)
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, message = frame.unpack
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 message, code
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
- # @param buffer [String] a unicode or binary string.
177
- def write(buffer)
178
- # https://tools.ietf.org/html/rfc6455#section-5.6
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
- # Text: The "Payload data" is text data encoded as UTF-8
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
- # @return [String] a unicode or binary string.
189
- def read
190
- @framer.flush
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
- while read_frame
193
- if @frames.last&.finished?
194
- buffer = @frames.map(&:unpack).join
195
- @frames = []
196
-
197
- return buffer
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
- # Deprecated.
203
- def next_message
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
- return frames
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
- OPCODE = 0
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.b
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.b
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
- # rsv = byte & 0b0111_0000
147
+ flags = (byte & 0b0111_0000) >> 4
139
148
  opcode = byte & 0b0000_1111
140
149
 
141
- return finished, opcode
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.b
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
- # These general headers are used to negotiate the connection.
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
@@ -25,6 +25,7 @@ module Protocol
25
25
  class PongFrame < Frame
26
26
  OPCODE = 0xA
27
27
 
28
+ # Apply this frame to the specified connection.
28
29
  def apply(connection)
29
30
  connection.receive_pong(self)
30
31
  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
- def unpack
33
- super.force_encoding(Encoding::UTF_8)
34
- end
35
-
36
- def pack(data)
37
- super(data.b)
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
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Protocol
22
22
  module WebSocket
23
- VERSION = "0.7.5"
23
+ VERSION = "0.9.1"
24
24
  end
25
25
  end
@@ -20,3 +20,4 @@
20
20
 
21
21
  require_relative 'websocket/version'
22
22
  require_relative 'websocket/framer'
23
+ require_relative 'websocket/connection'
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.7.5
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
- date: 2020-09-10 00:00:00.000000000 Z
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: rspec
103
+ name: sus
71
104
  requirement: !ruby/object:Gem::Requirement
72
105
  requirements:
73
106
  - - "~>"
74
107
  - !ruby/object:Gem::Version
75
- version: '3.0'
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: '3.0'
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.1.2
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