protocol-websocket 0.1.0 → 0.2.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
- data/lib/protocol/websocket/binary_frame.rb +33 -0
- data/lib/protocol/websocket/close_frame.rb +44 -0
- data/lib/protocol/websocket/connection.rb +172 -0
- data/lib/protocol/websocket/continuation_frame.rb +33 -0
- data/lib/protocol/websocket/error.rb +34 -2
- data/lib/protocol/websocket/frame.rb +75 -75
- data/lib/protocol/websocket/framer.rb +37 -9
- data/lib/protocol/websocket/ping_frame.rb +38 -0
- data/lib/protocol/websocket/pong_frame.rb +33 -0
- data/lib/protocol/websocket/text_frame.rb +41 -0
- data/lib/protocol/websocket/version.rb +1 -1
- data/protocol-websocket.gemspec +2 -1
- metadata +27 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cbd73109c7ac70b53855864e3f3413473c4b375a46378b128e65c0a77f7c4881
|
4
|
+
data.tar.gz: 13c1bbfdbe82d21a03af3dc93944302a8cd5eec1a06154ccd8bfb44a8b251f73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33deb526b4c97ff9bd8887065dadab6750ee10022748c3cc56b240e0f0e3346fbd86ae92ab2c301de82fdbad539e47e4f2558567465bd193a13cf555fc633866
|
7
|
+
data.tar.gz: 857c58853914db3e6f5ab6924f5ac593cb094f7818553a2400ff1b80af07f2aece6e0ddcc044b39f684b6749a1c427527b5bffc862e8270b3eb012da78d40030
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'frame'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module WebSocket
|
25
|
+
class BinaryFrame < Frame
|
26
|
+
OPCODE = 0x2
|
27
|
+
|
28
|
+
def apply(connection)
|
29
|
+
connection.receive_binary(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'frame'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module WebSocket
|
25
|
+
class CloseFrame < Frame
|
26
|
+
OPCODE = 0x8
|
27
|
+
FORMAT = "na*"
|
28
|
+
|
29
|
+
def unpack
|
30
|
+
data = super
|
31
|
+
|
32
|
+
return data.unpack(FORMAT)
|
33
|
+
end
|
34
|
+
|
35
|
+
def pack(code, reason)
|
36
|
+
super [code, reason].pack(FORMAT)
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply(connection)
|
40
|
+
connection.receive_close(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# Copyright, 2019, 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 'framer'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module WebSocket
|
25
|
+
class Connection
|
26
|
+
def initialize(framer)
|
27
|
+
@framer = framer
|
28
|
+
@state = :open
|
29
|
+
@frames = []
|
30
|
+
end
|
31
|
+
|
32
|
+
attr :framer
|
33
|
+
|
34
|
+
# Buffered frames which form part of a complete message.
|
35
|
+
attr_accessor :frames
|
36
|
+
|
37
|
+
def closed?
|
38
|
+
@state == :closed
|
39
|
+
end
|
40
|
+
|
41
|
+
def close
|
42
|
+
send_close
|
43
|
+
|
44
|
+
@framer.close
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_frame
|
48
|
+
return nil if closed?
|
49
|
+
|
50
|
+
frame = @framer.read_frame
|
51
|
+
|
52
|
+
yield frame if block_given?
|
53
|
+
|
54
|
+
frame.apply(self)
|
55
|
+
|
56
|
+
return frame
|
57
|
+
rescue ProtocolError => error
|
58
|
+
send_close(error.code, error.message)
|
59
|
+
|
60
|
+
raise
|
61
|
+
rescue
|
62
|
+
send_close(Error::PROTOCOL_ERROR, $!.message)
|
63
|
+
|
64
|
+
raise
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_frame(frame)
|
68
|
+
@framer.write_frame(frame)
|
69
|
+
end
|
70
|
+
|
71
|
+
def receive_text(frame)
|
72
|
+
if @frames.empty?
|
73
|
+
@frames << frame
|
74
|
+
else
|
75
|
+
raise ProtocolError, "Received text, but expecting continuation!"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def receive_binary(frame)
|
80
|
+
if @frames.empty?
|
81
|
+
@frames << frame
|
82
|
+
else
|
83
|
+
raise ProtocolError, "Received binary, but expecting continuation!"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive_continuation(frame)
|
88
|
+
if @frames.any?
|
89
|
+
@frames << frame
|
90
|
+
else
|
91
|
+
raise ProtocolError, "Received unexpected continuation!"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def send_text(buffer)
|
96
|
+
frame = TextFrame.new
|
97
|
+
frame.pack buffer
|
98
|
+
|
99
|
+
write_frame(frame)
|
100
|
+
end
|
101
|
+
|
102
|
+
def send_binary(buffer)
|
103
|
+
frame = BinaryFrame.new
|
104
|
+
frame.pack buffer
|
105
|
+
|
106
|
+
write_frame(frame)
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_close(code = Error::NO_ERROR, message = nil)
|
110
|
+
frame = CloseFrame.new
|
111
|
+
frame.pack(code, message)
|
112
|
+
|
113
|
+
write_frame(frame)
|
114
|
+
|
115
|
+
@state = :closed
|
116
|
+
end
|
117
|
+
|
118
|
+
def receive_close(frame)
|
119
|
+
@state = :closed
|
120
|
+
|
121
|
+
code, message = frame.unpack
|
122
|
+
|
123
|
+
if code and code != Error::NO_ERROR
|
124
|
+
raise ClosedError.new message, code
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def send_ping(data)
|
129
|
+
if @state != :closed
|
130
|
+
frame = PingFrame.new
|
131
|
+
frame.pack data
|
132
|
+
|
133
|
+
write_frame(frame)
|
134
|
+
else
|
135
|
+
raise ProtocolError, "Cannot send ping in state #{@state}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def open!
|
140
|
+
@state = :open
|
141
|
+
|
142
|
+
return self
|
143
|
+
end
|
144
|
+
|
145
|
+
def receive_ping(frame)
|
146
|
+
if @state != :closed
|
147
|
+
write_frame(frame.reply)
|
148
|
+
else
|
149
|
+
raise ProtocolError, "Cannot receive ping in state #{@state}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def receive_frame(frame)
|
154
|
+
warn "Unhandled frame #{frame.inspect}"
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Array<Frame>] sequence of frames, the first being either text or binary, optionally followed by a number of continuation frames.
|
158
|
+
def next_message
|
159
|
+
@framer.flush
|
160
|
+
|
161
|
+
while read_frame
|
162
|
+
if @frames.last&.finished?
|
163
|
+
frames = @frames
|
164
|
+
@frames = []
|
165
|
+
|
166
|
+
return frames
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'frame'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module WebSocket
|
25
|
+
class ContinuationFrame < Frame
|
26
|
+
OPCODE = 0x0
|
27
|
+
|
28
|
+
def apply(connection)
|
29
|
+
connection.receive_continuation(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -22,10 +22,42 @@ require 'protocol/http/error'
|
|
22
22
|
|
23
23
|
module Protocol
|
24
24
|
module WebSocket
|
25
|
-
|
25
|
+
# Status codes as defined by <https://tools.ietf.org/html/rfc6455#section-7.4.1>.
|
26
|
+
class Error < HTTP::Error
|
27
|
+
# Indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled.
|
28
|
+
NO_ERROR = 1000
|
29
|
+
|
30
|
+
# Indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page.
|
31
|
+
GOING_AWAY = 1001
|
32
|
+
|
33
|
+
# Indicates that an endpoint is terminating the connection due to a protocol error.
|
34
|
+
PROTOCOL_ERROR = 1002
|
35
|
+
|
36
|
+
# Indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept.
|
37
|
+
INVALID_DATA = 1003
|
38
|
+
|
39
|
+
# There are other status codes but most of them are "implementation specific".
|
26
40
|
end
|
27
41
|
|
28
|
-
|
42
|
+
# Raised by stream or connection handlers, results in GOAWAY frame
|
43
|
+
# which signals termination of the current connection. You *cannot*
|
44
|
+
# recover from this exception, or any exceptions subclassed from it.
|
45
|
+
class ProtocolError < Error
|
46
|
+
def initialize(message, code = PROTOCOL_ERROR)
|
47
|
+
super(message)
|
48
|
+
|
49
|
+
@code = code
|
50
|
+
end
|
51
|
+
|
52
|
+
attr :code
|
53
|
+
end
|
54
|
+
|
55
|
+
# The connection was closed, maybe unexpectedly.
|
56
|
+
class ClosedError < ProtocolError
|
57
|
+
end
|
58
|
+
|
59
|
+
# When the frame payload does not match expectations.
|
60
|
+
class FrameSizeError < ProtocolError
|
29
61
|
end
|
30
62
|
end
|
31
63
|
end
|
@@ -25,47 +25,35 @@ module Protocol
|
|
25
25
|
class Frame
|
26
26
|
include Comparable
|
27
27
|
|
28
|
-
|
29
|
-
CONTINUATION = 0x0
|
30
|
-
TEXT = 0x1
|
31
|
-
BINARY = 0x2
|
32
|
-
CLOSE = 0x8
|
33
|
-
PING = 0x9
|
34
|
-
PONG = 0xA
|
28
|
+
OPCODE = 0
|
35
29
|
|
36
30
|
# @param length [Integer] the length of the payload, or nil if the header has not been read yet.
|
37
|
-
def initialize(
|
38
|
-
@
|
31
|
+
def initialize(finished = true, opcode = self.class::OPCODE, mask = false, payload = nil)
|
32
|
+
@finished = finished
|
39
33
|
@opcode = opcode
|
40
34
|
@mask = mask
|
41
35
|
@length = payload&.bytesize
|
42
36
|
@payload = payload
|
43
37
|
end
|
44
38
|
|
45
|
-
def self.read(stream)
|
46
|
-
frame = self.new
|
47
|
-
|
48
|
-
if frame.read(stream)
|
49
|
-
return frame
|
50
|
-
end
|
51
|
-
rescue EOFError
|
52
|
-
return nil
|
53
|
-
end
|
54
|
-
|
55
39
|
def <=> other
|
56
40
|
to_ary <=> other.to_ary
|
57
41
|
end
|
58
42
|
|
59
43
|
def to_ary
|
60
|
-
[@
|
44
|
+
[@finished, @opcode, @mask, @length, @payload]
|
61
45
|
end
|
62
46
|
|
63
47
|
def control?
|
64
48
|
@opcode & 0x8
|
65
49
|
end
|
66
50
|
|
51
|
+
def finished?
|
52
|
+
@finished == true
|
53
|
+
end
|
54
|
+
|
67
55
|
def continued?
|
68
|
-
@
|
56
|
+
@finished == false
|
69
57
|
end
|
70
58
|
|
71
59
|
# The generic frame header uses the following binary representation:
|
@@ -89,7 +77,7 @@ module Protocol
|
|
89
77
|
# | Payload Data continued ... |
|
90
78
|
# +---------------------------------------------------------------+
|
91
79
|
|
92
|
-
attr_accessor :
|
80
|
+
attr_accessor :finished
|
93
81
|
attr_accessor :opcode
|
94
82
|
attr_accessor :mask
|
95
83
|
attr_accessor :length
|
@@ -99,54 +87,90 @@ module Protocol
|
|
99
87
|
@payload
|
100
88
|
end
|
101
89
|
|
102
|
-
def pack(
|
103
|
-
|
104
|
-
@length = payload.bytesize
|
90
|
+
def pack(data, mask = @mask)
|
91
|
+
length = data.bytesize
|
105
92
|
|
106
|
-
if
|
107
|
-
raise ProtocolError, "Frame length #{@length} bigger than allowed
|
93
|
+
if length.bit_length > 63
|
94
|
+
raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
|
95
|
+
end
|
96
|
+
|
97
|
+
if @mask = mask
|
98
|
+
@payload = String.new.b
|
99
|
+
|
100
|
+
for i in 0...data.bytesize do
|
101
|
+
@payload << (data.getbyte(i) ^ mask.getbyte(i % 4))
|
102
|
+
end
|
103
|
+
else
|
104
|
+
@payload = data
|
105
|
+
@length = length
|
108
106
|
end
|
109
107
|
end
|
110
108
|
|
111
|
-
def
|
112
|
-
|
113
|
-
|
109
|
+
def unpack
|
110
|
+
if @mask
|
111
|
+
data = String.new.b
|
112
|
+
|
113
|
+
for i in 0...@payload.bytesize do
|
114
|
+
data << (@payload.getbyte(i) ^ @mask.getbyte(i % 4))
|
115
|
+
end
|
116
|
+
|
117
|
+
return data
|
118
|
+
else
|
119
|
+
return @payload
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def apply(connection)
|
124
|
+
connection.receive_frame(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.parse_header(buffer)
|
128
|
+
byte = buffer.unpack("C").first
|
114
129
|
|
115
|
-
|
130
|
+
finished = (byte & 0b1000_0000 != 0)
|
116
131
|
# rsv = byte & 0b0111_0000
|
117
|
-
|
132
|
+
opcode = byte & 0b0000_1111
|
118
133
|
|
119
|
-
|
120
|
-
|
134
|
+
return finished, opcode
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.read(finished, opcode, stream, maximum_frame_size)
|
138
|
+
buffer = stream.read(1) or raise EOFError
|
139
|
+
byte = buffer.unpack("C").first
|
121
140
|
|
122
|
-
|
141
|
+
mask = (byte & 0b1000_0000 != 0)
|
142
|
+
length = byte & 0b0111_1111
|
143
|
+
|
144
|
+
if length == 126
|
123
145
|
buffer = stream.read(2) or raise EOFError
|
124
|
-
|
125
|
-
elsif
|
146
|
+
length = buffer.unpack('n').first
|
147
|
+
elsif length == 127
|
126
148
|
buffer = stream.read(4) or raise EOFError
|
127
|
-
|
149
|
+
length = buffer.unpack('Q>').first
|
128
150
|
end
|
129
151
|
|
130
|
-
if
|
131
|
-
|
132
|
-
@payload = read_mask(@mask, @length, stream)
|
133
|
-
else
|
134
|
-
@mask = nil
|
135
|
-
@payload = stream.read(@length) or raise EOFError
|
152
|
+
if length > maximum_frame_size
|
153
|
+
raise ProtocolError, "Invalid payload length: #{@length} > #{maximum_frame_size}!"
|
136
154
|
end
|
137
155
|
|
138
|
-
if
|
139
|
-
|
156
|
+
if mask
|
157
|
+
mask = stream.read(4) or raise EOFError
|
158
|
+
end
|
159
|
+
|
160
|
+
payload = stream.read(length) or raise EOFError
|
161
|
+
|
162
|
+
if payload.bytesize != length
|
163
|
+
raise EOFError, "Incorrect payload length: #{@length} != #{@payload.bytesize}!"
|
140
164
|
end
|
141
165
|
|
142
|
-
return
|
166
|
+
return self.new(finished, opcode, mask, payload)
|
143
167
|
end
|
144
168
|
|
145
169
|
def write(stream)
|
146
170
|
buffer = String.new.b
|
147
171
|
|
148
172
|
if @payload&.bytesize != @length
|
149
|
-
raise ProtocolError, "Invalid payload
|
173
|
+
raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize}!"
|
150
174
|
end
|
151
175
|
|
152
176
|
if @mask and @mask.bytesize != 4
|
@@ -162,7 +186,7 @@ module Protocol
|
|
162
186
|
end
|
163
187
|
|
164
188
|
buffer << [
|
165
|
-
(@
|
189
|
+
(@finished ? 0b1000_0000 : 0) | @opcode,
|
166
190
|
(@mask ? 0b1000_0000 : 0) | short_length,
|
167
191
|
].pack('CC')
|
168
192
|
|
@@ -172,32 +196,8 @@ module Protocol
|
|
172
196
|
buffer << [@length].pack('Q>')
|
173
197
|
end
|
174
198
|
|
175
|
-
|
176
|
-
|
177
|
-
write_mask(@mask, @payload, buffer)
|
178
|
-
stream.write(buffer)
|
179
|
-
else
|
180
|
-
stream.write(buffer)
|
181
|
-
stream.write(@payload)
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
private
|
186
|
-
|
187
|
-
def read_mask(mask, length, stream)
|
188
|
-
data = stream.read(length) or raise EOFError
|
189
|
-
|
190
|
-
for i in 0...data.bytesize do
|
191
|
-
data.setbyte(i, data.getbyte(i) ^ mask.getbyte(i % 4))
|
192
|
-
end
|
193
|
-
|
194
|
-
return data
|
195
|
-
end
|
196
|
-
|
197
|
-
def write_mask(mask, data, buffer)
|
198
|
-
for i in 0...data.bytesize do
|
199
|
-
buffer << (data.getbyte(i) ^ mask.getbyte(i % 4))
|
200
|
-
end
|
199
|
+
stream.write(buffer)
|
200
|
+
stream.write(@payload)
|
201
201
|
end
|
202
202
|
end
|
203
203
|
end
|
@@ -20,11 +20,31 @@
|
|
20
20
|
|
21
21
|
require_relative 'frame'
|
22
22
|
|
23
|
+
require_relative 'continuation_frame'
|
24
|
+
require_relative 'text_frame'
|
25
|
+
require_relative 'binary_frame'
|
26
|
+
require_relative 'close_frame'
|
27
|
+
require_relative 'ping_frame'
|
28
|
+
require_relative 'pong_frame'
|
29
|
+
|
23
30
|
module Protocol
|
24
31
|
module WebSocket
|
32
|
+
# HTTP/2 frame type mapping as defined by the spec
|
33
|
+
FRAMES = {
|
34
|
+
0x0 => ContinuationFrame,
|
35
|
+
0x1 => TextFrame,
|
36
|
+
0x2 => BinaryFrame,
|
37
|
+
0x8 => CloseFrame,
|
38
|
+
0x9 => PingFrame,
|
39
|
+
0xA => PongFrame,
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
MAXIMUM_ALLOWED_FRAME_SIZE = 2**63
|
43
|
+
|
25
44
|
class Framer
|
26
|
-
def initialize(stream)
|
45
|
+
def initialize(stream, frames = FRAMES)
|
27
46
|
@stream = stream
|
47
|
+
@frames = frames
|
28
48
|
end
|
29
49
|
|
30
50
|
def close
|
@@ -35,20 +55,28 @@ module Protocol
|
|
35
55
|
@stream.flush
|
36
56
|
end
|
37
57
|
|
38
|
-
def read_frame
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
def read_message
|
58
|
+
def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
|
59
|
+
# Read the header:
|
60
|
+
finished, opcode = read_header
|
43
61
|
|
44
|
-
|
45
|
-
|
46
|
-
|
62
|
+
# Read the frame:
|
63
|
+
klass = @frames[opcode] || Frame
|
64
|
+
frame = klass.read(finished, opcode, @stream, maximum_frame_size)
|
65
|
+
|
66
|
+
return frame
|
47
67
|
end
|
48
68
|
|
49
69
|
def write_frame(frame)
|
50
70
|
frame.write(@stream)
|
51
71
|
end
|
72
|
+
|
73
|
+
def read_header
|
74
|
+
if buffer = @stream.read(1)
|
75
|
+
return Frame.parse_header(buffer)
|
76
|
+
end
|
77
|
+
|
78
|
+
raise EOFError, "Could not read frame header!"
|
79
|
+
end
|
52
80
|
end
|
53
81
|
end
|
54
82
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'frame'
|
22
|
+
require_relative 'pong_frame'
|
23
|
+
|
24
|
+
module Protocol
|
25
|
+
module WebSocket
|
26
|
+
class PingFrame < Frame
|
27
|
+
OPCODE = 0x9
|
28
|
+
|
29
|
+
def reply
|
30
|
+
PongFrame.new(true, PongFrame::OPCODE, @mask, @payload)
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply(connection)
|
34
|
+
connection.receive_ping(self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'frame'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module WebSocket
|
25
|
+
class PongFrame < Frame
|
26
|
+
OPCODE = 0xA
|
27
|
+
|
28
|
+
def apply(connection)
|
29
|
+
connection.receive_pong(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'frame'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module WebSocket
|
25
|
+
class TextFrame < Frame
|
26
|
+
OPCODE = 0x1
|
27
|
+
|
28
|
+
def unpack
|
29
|
+
super.force_encoding(Encoding::UTF_8)
|
30
|
+
end
|
31
|
+
|
32
|
+
def pack(data, mask = @mask)
|
33
|
+
super(data.b, mask = @mask)
|
34
|
+
end
|
35
|
+
|
36
|
+
def apply(connection)
|
37
|
+
connection.receive_text(self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/protocol-websocket.gemspec
CHANGED
@@ -18,7 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "protocol-http"
|
21
|
+
spec.add_dependency "protocol-http", "~> 0.2"
|
22
|
+
spec.add_dependency "protocol-http1", "~> 0.2"
|
22
23
|
|
23
24
|
spec.add_development_dependency "covered"
|
24
25
|
spec.add_development_dependency "bundler"
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: protocol-websocket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-05-
|
11
|
+
date: 2019-05-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: protocol-http
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
19
|
+
version: '0.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
26
|
+
version: '0.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: protocol-http1
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: covered
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,10 +108,17 @@ files:
|
|
94
108
|
- README.md
|
95
109
|
- Rakefile
|
96
110
|
- lib/protocol/websocket.rb
|
111
|
+
- lib/protocol/websocket/binary_frame.rb
|
112
|
+
- lib/protocol/websocket/close_frame.rb
|
113
|
+
- lib/protocol/websocket/connection.rb
|
114
|
+
- lib/protocol/websocket/continuation_frame.rb
|
97
115
|
- lib/protocol/websocket/digest.rb
|
98
116
|
- lib/protocol/websocket/error.rb
|
99
117
|
- lib/protocol/websocket/frame.rb
|
100
118
|
- lib/protocol/websocket/framer.rb
|
119
|
+
- lib/protocol/websocket/ping_frame.rb
|
120
|
+
- lib/protocol/websocket/pong_frame.rb
|
121
|
+
- lib/protocol/websocket/text_frame.rb
|
101
122
|
- lib/protocol/websocket/version.rb
|
102
123
|
- protocol-websocket.gemspec
|
103
124
|
homepage: https://github.com/socketry/protocol-websocket
|