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