protocol-websocket 0.7.3 → 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: 86e03089f165a2cf50a6b4cbd20328da1a782778c02f23a3be4e4542de0ea692
4
- data.tar.gz: d4f856117d7e13373cd115871276682f08793ddf353f3106f5569cc8e38fb4ad
3
+ metadata.gz: 076d348e98312526d98d8fc80840b9760cf30496ac4ca83d129ac53e120c10a0
4
+ data.tar.gz: 142d1fa0b2c8e06a3642cfff30f76f835308b23ca3d6964e1c3ae0870204d5e1
5
5
  SHA512:
6
- metadata.gz: fbd6e6cbec72148770e2c3979bc56cdea6f12726ae8270759a613814ee1e54492ee0ba24a51b5077d3dfeff93e19abb56ea5ff219e042a03445e87ff0b33c051
7
- data.tar.gz: 519c30e7774eac646675e8a940e9665214bd9d9a266297038d781030e8ca9150a9aa877c85b0ece209ff0a9475203403e7879155b472b8d283bb3a066af6c4bd
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,18 +23,20 @@ require 'securerandom'
23
23
 
24
24
  module Protocol
25
25
  module WebSocket
26
+ # Wraps a framer and implements for implementing connection specific interactions like reading and writing text.
26
27
  class Connection
27
- # @option mask [String] 4-byte mask to be used for frames generated by this connection.
28
- def initialize(framer, mask: nil)
29
- if mask == true
30
- mask = SecureRandom.bytes(4)
31
- end
32
-
28
+ # @parameter mask [String] 4-byte mask to be used for frames generated by this connection.
29
+ def initialize(framer, mask: nil, **options)
33
30
  @framer = framer
34
31
  @mask = mask
35
32
 
36
33
  @state = :open
37
34
  @frames = []
35
+
36
+ @reserved = Frame::RESERVED
37
+
38
+ @reader = self
39
+ @writer = self
38
40
  end
39
41
 
40
42
  # The framer which is used for reading and writing frames.
@@ -43,9 +45,25 @@ module Protocol
43
45
  # The (optional) mask which is used when generating frames.
44
46
  attr :mask
45
47
 
48
+ # The allowed reserved bits:
49
+ attr :reserved
50
+
46
51
  # Buffered frames which form part of a complete message.
47
52
  attr_accessor :frames
48
53
 
54
+ attr_accessor :reader
55
+ attr_accessor :writer
56
+
57
+ def reserve!(bit)
58
+ if (@reserved & bit).zero?
59
+ raise "Unable to use #{bit}!"
60
+ end
61
+
62
+ @reserved &= ~bit
63
+
64
+ return true
65
+ end
66
+
49
67
  def flush
50
68
  @framer.flush
51
69
  end
@@ -54,8 +72,8 @@ module Protocol
54
72
  @state == :closed
55
73
  end
56
74
 
57
- def close
58
- send_close unless closed?
75
+ def close(code = Error::NO_ERROR, reason = "")
76
+ send_close(code, reason) unless closed?
59
77
 
60
78
  @framer.close
61
79
  end
@@ -65,6 +83,10 @@ module Protocol
65
83
 
66
84
  frame = @framer.read_frame
67
85
 
86
+ unless (frame.flags & @reserved).zero?
87
+ raise ProtocolError, "Received frame with reserved flags set!"
88
+ end
89
+
68
90
  yield frame if block_given?
69
91
 
70
92
  frame.apply(self)
@@ -82,6 +104,8 @@ module Protocol
82
104
 
83
105
  def write_frame(frame)
84
106
  @framer.write_frame(frame)
107
+
108
+ return frame
85
109
  end
86
110
 
87
111
  def receive_text(frame)
@@ -108,23 +132,31 @@ module Protocol
108
132
  end
109
133
  end
110
134
 
111
- def send_text(buffer)
135
+ def text_message(buffer, **options)
112
136
  frame = TextFrame.new(mask: @mask)
113
- frame.pack buffer
137
+ frame.pack(buffer)
114
138
 
115
- write_frame(frame)
139
+ return frame
140
+ end
141
+
142
+ def send_text(buffer, **options)
143
+ write_frame(@writer.text_message(buffer, **options))
116
144
  end
117
145
 
118
- def send_binary(buffer)
146
+ def binary_message(buffer, **options)
119
147
  frame = BinaryFrame.new(mask: @mask)
120
- frame.pack buffer
148
+ frame.pack(buffer)
121
149
 
122
- write_frame(frame)
150
+ return frame
123
151
  end
124
152
 
125
- 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 = "")
126
158
  frame = CloseFrame.new(mask: @mask)
127
- frame.pack(code, message)
159
+ frame.pack(code, reason)
128
160
 
129
161
  self.write_frame(frame)
130
162
  self.flush
@@ -135,10 +167,12 @@ module Protocol
135
167
  def receive_close(frame)
136
168
  @state = :closed
137
169
 
138
- code, message = frame.unpack
170
+ code, reason = frame.unpack
171
+
172
+ send_close(code, reason)
139
173
 
140
174
  if code and code != Error::NO_ERROR
141
- raise ClosedError.new message, code
175
+ raise ClosedError.new reason, code
142
176
  end
143
177
  end
144
178
 
@@ -161,7 +195,7 @@ module Protocol
161
195
 
162
196
  def receive_ping(frame)
163
197
  if @state != :closed
164
- write_frame(frame.reply)
198
+ write_frame(frame.reply(mask: @mask))
165
199
  else
166
200
  raise ProtocolError, "Cannot receive ping in state #{@state}"
167
201
  end
@@ -175,31 +209,24 @@ module Protocol
175
209
  warn "Unhandled frame #{frame.inspect}"
176
210
  end
177
211
 
178
- # @param buffer [String] a unicode or binary string.
179
- def write(buffer)
212
+ def write_message(buffer, **options)
213
+ # Text: The "Payload data" is text data encoded as UTF-8
180
214
  if buffer.encoding == Encoding::UTF_8
181
- send_text(buffer)
215
+ send_text(buffer, **options)
182
216
  else
183
- send_data(buffer)
217
+ send_binary(buffer, **options)
184
218
  end
185
219
  end
186
220
 
187
- # @return [String] a unicode or binary string.
188
- def read
189
- @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
190
224
 
191
- while read_frame
192
- if @frames.last&.finished?
193
- buffer = @frames.map(&:unpack).join
194
- @frames = []
195
-
196
- return buffer
197
- end
198
- end
225
+ self.write_message(buffer, **options)
199
226
  end
200
227
 
201
- # Deprecated.
202
- def next_message
228
+ # @return [String] a unicode or binary string.
229
+ def read(**options)
203
230
  @framer.flush
204
231
 
205
232
  while read_frame
@@ -207,9 +234,18 @@ module Protocol
207
234
  frames = @frames
208
235
  @frames = []
209
236
 
210
- return frames
237
+ buffer = @reader.read_message(frames, **options)
238
+ return frames.first.read_message(buffer)
211
239
  end
212
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("")
213
249
  end
214
250
  end
215
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,11 +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
 
30
- # @param length [Integer] the length of the payload, or nil if the header has not been read yet.
31
- def initialize(finished = true, payload = nil, opcode: self.class::OPCODE, mask: false)
33
+ OPCODE = 0
34
+
35
+ # @parameter length [Integer] The length of the payload, or nil if the header has not been read yet.
36
+ # @parameter mask [Boolean | String] An optional 4-byte string which is used to mask the payload.
37
+ def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPCODE, mask: false)
38
+ if mask == true
39
+ mask = SecureRandom.bytes(4)
40
+ end
41
+
32
42
  @finished = finished
43
+ @flags = flags
33
44
  @opcode = opcode
34
45
  @mask = mask
35
46
  @length = payload&.bytesize
@@ -41,11 +52,11 @@ module Protocol
41
52
  end
42
53
 
43
54
  def to_ary
44
- [@finished, @opcode, @mask, @length, @payload]
55
+ [@finished, @flags, @opcode, @mask, @length, @payload]
45
56
  end
46
57
 
47
58
  def control?
48
- @opcode & 0x8
59
+ @opcode & 0x8 != 0
49
60
  end
50
61
 
51
62
  def data?
@@ -82,6 +93,7 @@ module Protocol
82
93
  # +---------------------------------------------------------------+
83
94
 
84
95
  attr_accessor :finished
96
+ attr_accessor :flags
85
97
  attr_accessor :opcode
86
98
  attr_accessor :mask
87
99
  attr_accessor :length
@@ -93,9 +105,9 @@ module Protocol
93
105
  if length.bit_length > 63
94
106
  raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
95
107
  end
96
-
108
+
97
109
  if @mask
98
- @payload = String.new.b
110
+ @payload = String.new(encoding: Encoding::BINARY)
99
111
 
100
112
  for i in 0...data.bytesize do
101
113
  @payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
@@ -106,11 +118,13 @@ module Protocol
106
118
  @payload = data
107
119
  @length = length
108
120
  end
121
+
122
+ return self
109
123
  end
110
124
 
111
125
  def unpack
112
- if @mask
113
- data = String.new.b
126
+ if @mask and !@payload.empty?
127
+ data = String.new(encoding: Encoding::BINARY)
114
128
 
115
129
  for i in 0...@payload.bytesize do
116
130
  data << (@payload.getbyte(i) ^ @mask.getbyte(i % 4))
@@ -130,19 +144,33 @@ module Protocol
130
144
  byte = buffer.unpack("C").first
131
145
 
132
146
  finished = (byte & 0b1000_0000 != 0)
133
- # rsv = byte & 0b0111_0000
147
+ flags = (byte & 0b0111_0000) >> 4
134
148
  opcode = byte & 0b0000_1111
135
149
 
136
- 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
137
157
  end
138
158
 
139
- def self.read(finished, opcode, stream, maximum_frame_size)
159
+ def self.read(finished, flags, opcode, stream, maximum_frame_size)
140
160
  buffer = stream.read(1) or raise EOFError, "Could not read header!"
141
161
  byte = buffer.unpack("C").first
142
162
 
143
163
  mask = (byte & 0b1000_0000 != 0)
144
164
  length = byte & 0b0111_1111
145
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
+
146
174
  if length == 126
147
175
  buffer = stream.read(2) or raise EOFError, "Could not read length!"
148
176
  length = buffer.unpack('n').first
@@ -165,11 +193,11 @@ module Protocol
165
193
  raise EOFError, "Incorrect payload length: #{@length} != #{@payload.bytesize}!"
166
194
  end
167
195
 
168
- return self.new(finished, payload, opcode: opcode, mask: mask)
196
+ return self.new(finished, payload, flags: flags, opcode: opcode, mask: mask)
169
197
  end
170
198
 
171
199
  def write(stream)
172
- buffer = String.new.b
200
+ buffer = String.new(encoding: Encoding::BINARY)
173
201
 
174
202
  if @payload&.bytesize != @length
175
203
  raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize} for #{self}!"
@@ -188,7 +216,7 @@ module Protocol
188
216
  end
189
217
 
190
218
  buffer << [
191
- (@finished ? 0b1000_0000 : 0) | @opcode,
219
+ (@finished ? 0b1000_0000 : 0) | (@flags << 4) | @opcode,
192
220
  (@mask ? 0b1000_0000 : 0) | short_length,
193
221
  ].pack('CC')
194
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
- def reply
30
- PongFrame.new(true, @payload, mask: @mask)
29
+ # Generate a suitable reply.
30
+ # @returns [PongFrame]
31
+ def reply(**options)
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.3"
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.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
- autorequire:
8
+ - Aurora
9
+ - Soumya
10
+ - Olle Jonsson
11
+ - William T. Nelson
12
+ autorequire:
9
13
  bindir: bin
10
- cert_chain: []
11
- date: 2019-07-13 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
@@ -39,7 +72,7 @@ dependencies:
39
72
  - !ruby/object:Gem::Version
40
73
  version: '0.2'
41
74
  - !ruby/object:Gem::Dependency
42
- name: covered
75
+ name: bundler
43
76
  requirement: !ruby/object:Gem::Requirement
44
77
  requirements:
45
78
  - - ">="
@@ -53,7 +86,7 @@ dependencies:
53
86
  - !ruby/object:Gem::Version
54
87
  version: '0'
55
88
  - !ruby/object:Gem::Dependency
56
- name: bundler
89
+ name: covered
57
90
  requirement: !ruby/object:Gem::Requirement
58
91
  requirements:
59
92
  - - ">="
@@ -66,20 +99,6 @@ dependencies:
66
99
  - - ">="
67
100
  - !ruby/object:Gem::Version
68
101
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '10.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '10.0'
83
102
  - !ruby/object:Gem::Dependency
84
103
  name: rspec
85
104
  requirement: !ruby/object:Gem::Requirement
@@ -94,25 +113,23 @@ dependencies:
94
113
  - - "~>"
95
114
  - !ruby/object:Gem::Version
96
115
  version: '3.0'
97
- description:
116
+ description:
98
117
  email:
99
- - samuel.williams@oriontransfer.co.nz
100
118
  executables: []
101
119
  extensions: []
102
120
  extra_rdoc_files: []
103
121
  files:
104
- - ".gitignore"
105
- - ".rspec"
106
- - ".travis.yml"
107
- - Gemfile
108
- - README.md
109
- - Rakefile
110
122
  - lib/protocol/websocket.rb
111
123
  - lib/protocol/websocket/binary_frame.rb
112
124
  - lib/protocol/websocket/close_frame.rb
113
125
  - lib/protocol/websocket/connection.rb
114
126
  - lib/protocol/websocket/continuation_frame.rb
115
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
116
133
  - lib/protocol/websocket/frame.rb
117
134
  - lib/protocol/websocket/framer.rb
118
135
  - lib/protocol/websocket/headers.rb
@@ -120,12 +137,11 @@ files:
120
137
  - lib/protocol/websocket/pong_frame.rb
121
138
  - lib/protocol/websocket/text_frame.rb
122
139
  - lib/protocol/websocket/version.rb
123
- - protocol-websocket.gemspec
124
140
  homepage: https://github.com/socketry/protocol-websocket
125
141
  licenses:
126
142
  - MIT
127
143
  metadata: {}
128
- post_install_message:
144
+ post_install_message:
129
145
  rdoc_options: []
130
146
  require_paths:
131
147
  - lib
@@ -133,15 +149,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
133
149
  requirements:
134
150
  - - ">="
135
151
  - !ruby/object:Gem::Version
136
- version: '0'
152
+ version: 2.5.0
137
153
  required_rubygems_version: !ruby/object:Gem::Requirement
138
154
  requirements:
139
155
  - - ">="
140
156
  - !ruby/object:Gem::Version
141
157
  version: '0'
142
158
  requirements: []
143
- rubygems_version: 3.0.3
144
- signing_key:
159
+ rubygems_version: 3.3.7
160
+ signing_key:
145
161
  specification_version: 4
146
162
  summary: A low level implementation of the WebSocket protocol.
147
163
  test_files: []
metadata.gz.sig ADDED
Binary file
data/.gitignore DELETED
@@ -1,13 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status
12
- Gemfile.lock
13
- .covered.db
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --warnings
3
- --require spec_helper
data/.travis.yml DELETED
@@ -1,19 +0,0 @@
1
- language: ruby
2
- dist: xenial
3
- cache: bundler
4
-
5
- matrix:
6
- include:
7
- - rvm: 2.4
8
- - rvm: 2.5
9
- - rvm: 2.6
10
- - rvm: 2.6
11
- env: COVERAGE=PartialSummary,Coveralls
12
- - rvm: truffleruby
13
- - rvm: jruby-head
14
- env: JRUBY_OPTS="--debug -X+O"
15
- - rvm: ruby-head
16
- allow_failures:
17
- - rvm: truffleruby
18
- - rvm: ruby-head
19
- - rvm: jruby-head
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in protocol-websocket.gemspec
4
- gemspec
data/README.md DELETED
@@ -1,64 +0,0 @@
1
- # Protocol::WebSocket
2
-
3
- Provides a low-level implementation of the WebSocket protocol according to [RFC6455](https://tools.ietf.org/html/rfc6455). It only implements the latest stable version (13).
4
-
5
- [![Build Status](https://travis-ci.com/socketry/protocol-websocket.svg?branch=master)](http://travis-ci.com/socketry/protocol-websocket)
6
-
7
- ## Installation
8
-
9
- Add this line to your application's Gemfile:
10
-
11
- ```ruby
12
- gem 'protocol-websocket'
13
- ```
14
-
15
- And then execute:
16
-
17
- $ bundle
18
-
19
- Or install it yourself as:
20
-
21
- $ gem install protocol-websocket
22
-
23
- ## Usage
24
-
25
- Here is a basic WebSocket client:
26
-
27
- ```ruby
28
- stream = # connect to remote system
29
- framer = Protocol::WebSocket::Framer.new(stream)
30
-
31
- frame = framer.read_frame
32
- ```
33
-
34
- ## Contributing
35
-
36
- 1. Fork it
37
- 2. Create your feature branch (`git checkout -b my-new-feature`)
38
- 3. Commit your changes (`git commit -am 'Add some feature'`)
39
- 4. Push to the branch (`git push origin my-new-feature`)
40
- 5. Create new Pull Request
41
-
42
- ## License
43
-
44
- Released under the MIT license.
45
-
46
- Copyright, 2019, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
47
-
48
- Permission is hereby granted, free of charge, to any person obtaining a copy
49
- of this software and associated documentation files (the "Software"), to deal
50
- in the Software without restriction, including without limitation the rights
51
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
52
- copies of the Software, and to permit persons to whom the Software is
53
- furnished to do so, subject to the following conditions:
54
-
55
- The above copyright notice and this permission notice shall be included in
56
- all copies or substantial portions of the Software.
57
-
58
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
59
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
60
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
61
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
62
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
63
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
64
- THE SOFTWARE.
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
@@ -1,28 +0,0 @@
1
-
2
- require_relative "lib/protocol/websocket/version"
3
-
4
- Gem::Specification.new do |spec|
5
- spec.name = "protocol-websocket"
6
- spec.version = Protocol::WebSocket::VERSION
7
- spec.authors = ["Samuel Williams"]
8
- spec.email = ["samuel.williams@oriontransfer.co.nz"]
9
-
10
- spec.summary = "A low level implementation of the WebSocket protocol."
11
- spec.homepage = "https://github.com/socketry/protocol-websocket"
12
- spec.license = "MIT"
13
-
14
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
15
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
- end
17
-
18
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_dependency "protocol-http", "~> 0.2"
22
- spec.add_dependency "protocol-http1", "~> 0.2"
23
-
24
- spec.add_development_dependency "covered"
25
- spec.add_development_dependency "bundler"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec", "~> 3.0"
28
- end