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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d2733227a287e2dfb58f393566e6afea44470aba32c7a8eda8bd9a8f25c2d72
4
- data.tar.gz: bdd0966fcc837f1314cf2b88e55699f2de57e6d6af36ecbe3bcdf0c12350935a
3
+ metadata.gz: 076d348e98312526d98d8fc80840b9760cf30496ac4ca83d129ac53e120c10a0
4
+ data.tar.gz: 142d1fa0b2c8e06a3642cfff30f76f835308b23ca3d6964e1c3ae0870204d5e1
5
5
  SHA512:
6
- metadata.gz: a3fdd8546a648fa96663aafe278499e3a19266d1dfd9d2c68cd6d330f24c8af7d31ee94fd88c632a4bddec85dd56456980bf5a27d02197548279e069eae3d515
7
- data.tar.gz: 2b4faa47644b9811dab2d856831176b282c83df85863fad5db5a7b28055b123230ac89ae2e29eb3529843fec12d80ec11f26b2894ca85a2f243eef2d09bf10fb
6
+ metadata.gz: 941eda4ac12acbc31bdd359cae2b2aaffd59f78f328844fcd171f6aca517cc225831ea3726f919448eaef3c7246a630d415988528ef08f4f6a394239b5924aeb
7
+ data.tar.gz: 9c37cfc67173a3632c344d042682f906b9b9afc2c4cbc35263f9cc48fa0cf4e62ee7348b4ce5a1af7bed2b118d23f477721e2b575f8d91f991806fa53996da7c
checksums.yaml.gz.sig ADDED
Binary file
@@ -29,6 +29,10 @@ module Protocol
29
29
  true
30
30
  end
31
31
 
32
+ def read_message(buffer)
33
+ buffer
34
+ end
35
+
32
36
  def apply(connection)
33
37
  connection.receive_binary(self)
34
38
  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)
@@ -106,23 +132,31 @@ module Protocol
106
132
  end
107
133
  end
108
134
 
109
- def send_text(buffer)
135
+ def text_message(buffer, **options)
110
136
  frame = TextFrame.new(mask: @mask)
111
- frame.pack buffer
137
+ frame.pack(buffer)
112
138
 
113
- write_frame(frame)
139
+ return frame
114
140
  end
115
141
 
116
- def send_binary(buffer)
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 buffer
148
+ frame.pack(buffer)
119
149
 
120
- write_frame(frame)
150
+ return frame
121
151
  end
122
152
 
123
- def send_close(code = Error::NO_ERROR, message = nil)
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, message)
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, message = frame.unpack
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 message, code
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
- # @param buffer [String] a unicode or binary string.
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
- # @return [String] a unicode or binary string.
189
- def read
190
- @framer.flush
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
- while read_frame
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
- # Deprecated.
203
- def next_message
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
- return frames
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
- 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
 
@@ -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
@@ -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
- def unpack
33
- super.force_encoding(Encoding::UTF_8)
34
- end
35
-
36
- def pack(data)
37
- super(data.b)
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
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Protocol
22
22
  module WebSocket
23
- VERSION = "0.7.5"
23
+ VERSION = "0.8.0"
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
@@ -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.7.5
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
- 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-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.1.2
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