protocol-websocket 0.7.5 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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