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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/websocket/binary_frame.rb +4 -0
- data/lib/protocol/websocket/close_frame.rb +32 -2
- data/lib/protocol/websocket/connection.rb +73 -37
- data/lib/protocol/websocket/extension/compression/deflate.rb +93 -0
- data/lib/protocol/websocket/extension/compression/inflate.rb +85 -0
- data/lib/protocol/websocket/extension/compression.rb +131 -0
- data/lib/protocol/websocket/extensions.md +13 -0
- data/lib/protocol/websocket/extensions.rb +146 -0
- data/lib/protocol/websocket/frame.rb +43 -15
- data/lib/protocol/websocket/framer.rb +7 -3
- data/lib/protocol/websocket/headers.rb +5 -1
- data/lib/protocol/websocket/ping_frame.rb +5 -2
- data/lib/protocol/websocket/pong_frame.rb +1 -0
- data/lib/protocol/websocket/text_frame.rb +12 -6
- data/lib/protocol/websocket/version.rb +1 -1
- data/lib/protocol/websocket.rb +1 -0
- data.tar.gz.sig +4 -0
- metadata +49 -33
- metadata.gz.sig +0 -0
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.travis.yml +0 -19
- data/Gemfile +0 -4
- data/README.md +0 -64
- data/Rakefile +0 -6
- data/protocol-websocket.gemspec +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 076d348e98312526d98d8fc80840b9760cf30496ac4ca83d129ac53e120c10a0
|
4
|
+
data.tar.gz: 142d1fa0b2c8e06a3642cfff30f76f835308b23ca3d6964e1c3ae0870204d5e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 941eda4ac12acbc31bdd359cae2b2aaffd59f78f328844fcd171f6aca517cc225831ea3726f919448eaef3c7246a630d415988528ef08f4f6a394239b5924aeb
|
7
|
+
data.tar.gz: 9c37cfc67173a3632c344d042682f906b9b9afc2c4cbc35263f9cc48fa0cf4e62ee7348b4ce5a1af7bed2b118d23f477721e2b575f8d91f991806fa53996da7c
|
checksums.yaml.gz.sig
ADDED
Binary file
|
@@ -29,11 +29,41 @@ module Protocol
|
|
29
29
|
def unpack
|
30
30
|
data = super
|
31
31
|
|
32
|
-
|
32
|
+
case data.length
|
33
|
+
when 0
|
34
|
+
[nil, ""]
|
35
|
+
when 1
|
36
|
+
raise ProtocolError, "invalid close frame length!"
|
37
|
+
else
|
38
|
+
code, reason = *data.unpack(FORMAT)
|
39
|
+
|
40
|
+
case code
|
41
|
+
when 0 .. 999, 1005 .. 1006, 1015, 5000 .. 0xFFFF
|
42
|
+
raise ProtocolError, "invalid close code!"
|
43
|
+
when 1004, 1016 .. 2999
|
44
|
+
raise ProtocolError, "reserved close code!"
|
45
|
+
end
|
46
|
+
|
47
|
+
reason.force_encoding(Encoding::UTF_8)
|
48
|
+
|
49
|
+
unless reason.valid_encoding?
|
50
|
+
raise ProtocolError, "invalid UTF-8 in close reason!"
|
51
|
+
end
|
52
|
+
|
53
|
+
[code, reason]
|
54
|
+
end
|
33
55
|
end
|
34
56
|
|
35
57
|
def pack(code, reason)
|
36
|
-
|
58
|
+
if code
|
59
|
+
unless reason.encoding == Encoding::UTF_8
|
60
|
+
reason = reason.encode(Encoding::UTF_8)
|
61
|
+
end
|
62
|
+
|
63
|
+
super [code, reason].pack(FORMAT)
|
64
|
+
else
|
65
|
+
super String.new(encoding: Encoding::BINARY)
|
66
|
+
end
|
37
67
|
end
|
38
68
|
|
39
69
|
def apply(connection)
|
@@ -23,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
|
-
# @
|
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
|
135
|
+
def text_message(buffer, **options)
|
112
136
|
frame = TextFrame.new(mask: @mask)
|
113
|
-
frame.pack
|
137
|
+
frame.pack(buffer)
|
114
138
|
|
115
|
-
|
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
|
146
|
+
def binary_message(buffer, **options)
|
119
147
|
frame = BinaryFrame.new(mask: @mask)
|
120
|
-
frame.pack
|
148
|
+
frame.pack(buffer)
|
121
149
|
|
122
|
-
|
150
|
+
return frame
|
123
151
|
end
|
124
152
|
|
125
|
-
def
|
153
|
+
def send_binary(buffer, **options)
|
154
|
+
write_frame(@writer.binary_message(buffer, **options))
|
155
|
+
end
|
156
|
+
|
157
|
+
def send_close(code = Error::NO_ERROR, reason = "")
|
126
158
|
frame = CloseFrame.new(mask: @mask)
|
127
|
-
frame.pack(code,
|
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,
|
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
|
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
|
-
|
179
|
-
|
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
|
-
|
217
|
+
send_binary(buffer, **options)
|
184
218
|
end
|
185
219
|
end
|
186
220
|
|
187
|
-
# @
|
188
|
-
def
|
189
|
-
|
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
|
-
|
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
|
-
#
|
202
|
-
def
|
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
|
-
|
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
|
-
|
28
|
+
RSV1 = 0b0100
|
29
|
+
RSV2 = 0b0010
|
30
|
+
RSV3 = 0b0001
|
31
|
+
RESERVED = RSV1 | RSV2 | RSV3
|
29
32
|
|
30
|
-
|
31
|
-
|
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
|
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
|
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
|
-
|
147
|
+
flags = (byte & 0b0111_0000) >> 4
|
134
148
|
opcode = byte & 0b0000_1111
|
135
149
|
|
136
|
-
|
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
|
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
|
-
#
|
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
|
-
|
30
|
-
|
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
|
@@ -22,6 +22,7 @@ require_relative 'frame'
|
|
22
22
|
|
23
23
|
module Protocol
|
24
24
|
module WebSocket
|
25
|
+
# Implements the text frame for sending and receiving text.
|
25
26
|
class TextFrame < Frame
|
26
27
|
OPCODE = 0x1
|
27
28
|
|
@@ -29,14 +30,19 @@ module Protocol
|
|
29
30
|
true
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
# Decode the binary buffer into a suitable text message.
|
34
|
+
# @parameter buffer [String] The binary data to unpack.
|
35
|
+
def read_message(buffer)
|
36
|
+
buffer.force_encoding(Encoding::UTF_8)
|
37
|
+
|
38
|
+
unless buffer.valid_encoding?
|
39
|
+
raise ProtocolError, "invalid UTF-8 in text frame!"
|
40
|
+
end
|
41
|
+
|
42
|
+
buffer
|
38
43
|
end
|
39
44
|
|
45
|
+
# Apply this frame to the specified connection.
|
40
46
|
def apply(connection)
|
41
47
|
connection.receive_text(self)
|
42
48
|
end
|
data/lib/protocol/websocket.rb
CHANGED
data.tar.gz.sig
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
}� *>��K��X�a��*3��j���ss�U��0p2�����SE�j9�*�{�b��v��z�� �π�hf<}�>Ɩȶ����us9�v��i�P���TB<�s^�W�AP!��*{����c�P��_�9�K*�{"(�T&%(�hy�g�M3lR4�C�sά�m�ڤ����`�4*/v
|
2
|
+
�`�LA0�d��R�U�ޥ��������������������8<I
|
3
|
+
x���軦�_����%uW>2��Z�*��C�"`0Ƞ[�`�M�C���F�F���7.aq��f��u˄!��������'�]�Q@�c�
|
4
|
+
���(���l�33MΩ:�w�C'����~r�AfC*���4yp���z���\JZ�%�;]c��
|
metadata
CHANGED
@@ -1,14 +1,47 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: protocol-websocket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
|
-
|
8
|
+
- Aurora
|
9
|
+
- Soumya
|
10
|
+
- Olle Jonsson
|
11
|
+
- William T. Nelson
|
12
|
+
autorequire:
|
9
13
|
bindir: bin
|
10
|
-
cert_chain:
|
11
|
-
|
14
|
+
cert_chain:
|
15
|
+
- |
|
16
|
+
-----BEGIN CERTIFICATE-----
|
17
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
18
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
19
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
20
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
21
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
22
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
23
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
24
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
25
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
26
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
27
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
28
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
29
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
30
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
31
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
32
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
33
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
34
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
35
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
36
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
37
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
38
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
39
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
40
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
41
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
42
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
43
|
+
-----END CERTIFICATE-----
|
44
|
+
date: 2022-08-20 00:00:00.000000000 Z
|
12
45
|
dependencies:
|
13
46
|
- !ruby/object:Gem::Dependency
|
14
47
|
name: protocol-http
|
@@ -39,7 +72,7 @@ dependencies:
|
|
39
72
|
- !ruby/object:Gem::Version
|
40
73
|
version: '0.2'
|
41
74
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
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:
|
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:
|
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.
|
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
data/.rspec
DELETED
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
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
|
-
[](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
data/protocol-websocket.gemspec
DELETED
@@ -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
|