iodine 0.0.1 → 0.0.2
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.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +86 -8
- data/lib/iodine.rb +45 -2
- data/lib/iodine/core.rb +9 -37
- data/lib/iodine/http.rb +135 -0
- data/lib/iodine/http/hpack.rb +543 -0
- data/lib/iodine/http/http1.rb +217 -0
- data/lib/iodine/http/http2.rb +465 -0
- data/lib/iodine/http/rack_support.rb +105 -0
- data/lib/iodine/http/request.rb +413 -0
- data/lib/iodine/http/response.rb +355 -0
- data/lib/iodine/http/session.rb +110 -0
- data/lib/iodine/http/websocket_client.rb +224 -0
- data/lib/iodine/http/websocket_handler.rb +40 -0
- data/lib/iodine/http/websockets.rb +319 -0
- data/lib/iodine/io.rb +71 -28
- data/lib/iodine/protocol.rb +74 -25
- data/lib/iodine/settings.rb +44 -3
- data/lib/iodine/ssl_connector.rb +47 -0
- data/lib/iodine/timers.rb +5 -21
- data/lib/iodine/version.rb +1 -1
- data/lib/rack/handler/iodine.rb +3 -0
- data/{bin/http_test → manual tests/core_http_test } +6 -2
- data/bin/echo b/data/manual → tests/echo +0 -0
- data/bin/em playground b/data/manual tests/em → playground +0 -0
- data/manual tests/hello_world +56 -0
- metadata +20 -6
- data/lib/iodine/ssl_protocol.rb +0 -108
@@ -0,0 +1,224 @@
|
|
1
|
+
|
2
|
+
module Iodine
|
3
|
+
class Http < Iodine::Protocol
|
4
|
+
# Create a simple Websocket Client(!).
|
5
|
+
#
|
6
|
+
# This should be done from within an Iodine task, or the callbacks will not be called.
|
7
|
+
#
|
8
|
+
# Use {Iodine::Http::WebsocketClient.connect} to initialize a client with all the callbacks needed.
|
9
|
+
class WebsocketClient
|
10
|
+
attr_accessor :response, :request
|
11
|
+
|
12
|
+
def initialize request
|
13
|
+
@response = nil
|
14
|
+
@request = request
|
15
|
+
params = request[:ws_client_params]
|
16
|
+
@on_message = params[:on_message]
|
17
|
+
raise "Websocket client must have an #on_message Proc or handler." unless @on_message && @on_message.respond_to?(:call)
|
18
|
+
@on_open = params[:on_open]
|
19
|
+
@on_close = params[:on_close]
|
20
|
+
end
|
21
|
+
|
22
|
+
def on event_name, &block
|
23
|
+
return false unless block
|
24
|
+
case event_name
|
25
|
+
when :message
|
26
|
+
@on_message = block
|
27
|
+
when :close
|
28
|
+
@on_close = block
|
29
|
+
when :open
|
30
|
+
raise 'The on_open even is invalid at this point.'
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_message(data = nil, &block)
|
36
|
+
unless data
|
37
|
+
@on_message = block if block
|
38
|
+
return @on_message
|
39
|
+
end
|
40
|
+
instance_exec( data, &@on_message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_open(protocol = nil, &block)
|
44
|
+
unless protocol
|
45
|
+
raise 'The on_open even is invalid at this point.' if block
|
46
|
+
# @on_open = block if block
|
47
|
+
return @on_open
|
48
|
+
end
|
49
|
+
@io = protocol
|
50
|
+
Iodine::Http::Request.parse @request
|
51
|
+
instance_exec(&@on_open) if @on_open
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_close(&block)
|
55
|
+
@on_close = block if block
|
56
|
+
instance_exec(&@on_close) if @on_close
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sends data through the socket. a shortcut for ws_client.response <<
|
60
|
+
#
|
61
|
+
# @return [true, false] Returns the true if the data was actually sent or nil if no data was sent.
|
62
|
+
def << data
|
63
|
+
# raise 'Cannot send data when the connection is closed.' if closed?
|
64
|
+
@io << data
|
65
|
+
end
|
66
|
+
alias :write :<<
|
67
|
+
|
68
|
+
# closes the connection, if open
|
69
|
+
def close
|
70
|
+
@io.close if @io
|
71
|
+
end
|
72
|
+
|
73
|
+
# checks if the socket is open (if the websocket was terminated abnormally, this might returs true when it should be false).
|
74
|
+
def closed?
|
75
|
+
@io.io.closed?
|
76
|
+
end
|
77
|
+
|
78
|
+
# checks if this is an SSL websocket connection.
|
79
|
+
def ssl?
|
80
|
+
@request.ssl?
|
81
|
+
end
|
82
|
+
|
83
|
+
# return the HTTP's handshake data, including any cookies sent by the server.
|
84
|
+
def request
|
85
|
+
@request
|
86
|
+
end
|
87
|
+
# return a Hash with the HTTP cookies recieved during the HTTP's handshake.
|
88
|
+
def cookies
|
89
|
+
@request.cookies
|
90
|
+
end
|
91
|
+
|
92
|
+
# Create a simple Websocket Client(!).
|
93
|
+
#
|
94
|
+
# This method accepts two parameters:
|
95
|
+
# url:: a String representing the URL of the websocket. i.e.: 'ws://foo.bar.com:80/ws/path'
|
96
|
+
# options:: a Hash with options to be used. The options will be used to define the connection's details (i.e. ssl etc') and the Websocket callbacks (i.e. on_open(ws), on_close(ws), on_message(ws))
|
97
|
+
# &block:: an optional block that accepts one parameter (data) and will be used as the `#on_message(data)`
|
98
|
+
#
|
99
|
+
# Acceptable options are:
|
100
|
+
# on_open:: the on_open callback. Must be an objects that answers `call(ws)`, usually a Proc.
|
101
|
+
# on_message:: the on_message callback. Must be an objects that answers `call(ws)`, usually a Proc.
|
102
|
+
# on_close:: the on_close callback. Must be an objects that answers `call(ws)`, usually a Proc.
|
103
|
+
# headers:: a Hash of custom HTTP headers to be sent with the request. Header data, including cookie headers, should be correctly encoded.
|
104
|
+
# cookies:: a Hash of cookies to be sent with the request. cookie data will be encoded before being sent.
|
105
|
+
# timeout:: the number of seconds to wait before the connection is established. Defaults to 5 seconds.
|
106
|
+
#
|
107
|
+
# The method will block until the connection is established or until 5 seconds have passed (the timeout). The method will either return a WebsocketClient instance object or raise an exception it the connection was unsuccessful.
|
108
|
+
#
|
109
|
+
# An on_message Proc must be defined, or the method will fail.
|
110
|
+
#
|
111
|
+
# The on_message Proc can be defined using the optional block:
|
112
|
+
#
|
113
|
+
# Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") {|data| write data} #echo example
|
114
|
+
#
|
115
|
+
# OR, the on_message Proc can be defined using the options Hash:
|
116
|
+
#
|
117
|
+
# Iodine::Http::WebsocketClient.connect("ws://localhost:3000/", on_open: -> {}, on_message: -> {|data| write data })
|
118
|
+
#
|
119
|
+
# The #on_message(data), #on_open and #on_close methods will be executed within the context of the WebsocketClient
|
120
|
+
# object, and will have native acess to the Websocket response object.
|
121
|
+
#
|
122
|
+
# After the WebsocketClient had been created, it's possible to update the #on_message and #on_close methods:
|
123
|
+
#
|
124
|
+
# # updates #on_message
|
125
|
+
# wsclient.on_message do |data|
|
126
|
+
# response << "I'll disconnect on the next message!"
|
127
|
+
# # updates #on_message again.
|
128
|
+
# on_message {|data| disconnect }
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
#
|
132
|
+
# !!please be aware that the Websockt Client will not attempt to verify SSL certificates,
|
133
|
+
# so that even SSL connections are vulnerable to a possible man in the middle attack.
|
134
|
+
#
|
135
|
+
# @return [Iodine::Http::WebsocketClient] this method returns the connected {Iodine::Http::WebsocketClient} or raises an exception if something went wrong (such as a connection timeout).
|
136
|
+
def self.connect url, options={}, &block
|
137
|
+
socket = nil
|
138
|
+
options = options.dup
|
139
|
+
options[:on_message] ||= block
|
140
|
+
raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[:on_message]
|
141
|
+
url = URI.parse(url) unless url.is_a?(URI)
|
142
|
+
|
143
|
+
ssl = url.scheme == "https" || url.scheme == "wss"
|
144
|
+
|
145
|
+
url.port ||= ssl ? 443 : 80
|
146
|
+
url.path = '/' if url.path.to_s.empty?
|
147
|
+
socket = TCPSocket.new(url.host, url.port)
|
148
|
+
if ssl
|
149
|
+
context = OpenSSL::SSL::SSLContext.new
|
150
|
+
context.cert_store = OpenSSL::X509::Store.new
|
151
|
+
context.cert_store.set_default_paths
|
152
|
+
context.set_params verify_mode: (options[:verify_mode] || OpenSSL::SSL::VERIFY_NONE) # OpenSSL::SSL::VERIFY_PEER #OpenSSL::SSL::VERIFY_NONE
|
153
|
+
ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
|
154
|
+
ssl.sync_close = true
|
155
|
+
ssl.connect
|
156
|
+
end
|
157
|
+
# prep custom headers
|
158
|
+
custom_headers = ''
|
159
|
+
custom_headers = options[:headers] if options[:headers].is_a?(String)
|
160
|
+
options[:headers].each {|k, v| custom_headers << "#{k.to_s}: #{v.to_s}\r\n"} if options[:headers].is_a?(Hash)
|
161
|
+
options[:cookies].each {|k, v| raise 'Illegal cookie name' if k.to_s.match(/[\x00-\x20\(\)<>@,;:\\\"\/\[\]\?\=\{\}\s]/); custom_headers << "Cookie: #{ k }=#{ Iodine::Http::Request.encode_url v }\r\n"} if options[:cookies].is_a?(Hash)
|
162
|
+
|
163
|
+
# send protocol upgrade request
|
164
|
+
websocket_key = [(Array.new(16) {rand 255} .pack 'c*' )].pack('m0*')
|
165
|
+
(ssl || socket).write "GET #{url.path}#{url.query.to_s.empty? ? '' : ('?' + url.query)} HTTP/1.1\r\nHost: #{url.host}#{url.port ? (':'+url.port.to_s) : ''}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nOrigin: #{options[:ssl_client] ? 'https' : 'http'}://#{url.host}\r\nSec-WebSocket-Key: #{websocket_key}\r\nSec-WebSocket-Version: 13\r\n#{custom_headers}\r\n"
|
166
|
+
# wait for answer - make sure we don't over-read
|
167
|
+
# (a websocket message might be sent immidiately after connection is established)
|
168
|
+
reply = ''
|
169
|
+
reply.force_encoding(::Encoding::ASCII_8BIT)
|
170
|
+
stop_time = Time.now + (options[:timeout] || 5)
|
171
|
+
stop_reply = "\r\n\r\n"
|
172
|
+
sleep 0.2
|
173
|
+
until reply[-4..-1] == stop_reply
|
174
|
+
begin
|
175
|
+
reply << ( ssl ? ssl.read_nonblock(1) : socket.recv_nonblock(1) )
|
176
|
+
rescue Errno::EWOULDBLOCK => e
|
177
|
+
raise "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply}" if Time.now >= stop_time
|
178
|
+
IO.select [socket], nil, nil, (options[:timeout] || 5)
|
179
|
+
retry
|
180
|
+
end
|
181
|
+
raise "Connection failed" if socket.closed?
|
182
|
+
end
|
183
|
+
# review reply
|
184
|
+
raise "Connection Refused. Reply was:\r\n #{reply}" unless reply.lines[0].match(/^HTTP\/[\d\.]+ 101/i)
|
185
|
+
raise 'Websocket Key Authentication failed.' unless reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i) && reply.match(/^Sec-WebSocket-Accept:[\s]*([^\s]*)/i)[1] == Digest::SHA1.base64digest(websocket_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
186
|
+
# read the body's data and parse any incoming data.
|
187
|
+
request = Iodine::Http::Request.new
|
188
|
+
request[:method] = 'GET'
|
189
|
+
request['host'] = "#{url.host}:#{url.port}"
|
190
|
+
request[:query] = url.path
|
191
|
+
request[:version] = '1.1'
|
192
|
+
reply = StringIO.new reply
|
193
|
+
reply.gets
|
194
|
+
|
195
|
+
until reply.eof?
|
196
|
+
until request[:headers_complete] || (l = reply.gets).nil?
|
197
|
+
if l.include? ':'
|
198
|
+
l = l.strip.split(/:[\s]?/, 2)
|
199
|
+
l[0].strip! ; l[0].downcase!;
|
200
|
+
request[l[0]] ? (request[l[0]].is_a?(Array) ? (request[l[0]] << l[1]) : request[l[0]] = [request[l[0]], l[1] ]) : (request[l[0]] = l[1])
|
201
|
+
elsif l =~ /^[\r]?\n/
|
202
|
+
request[:headers_complete] = true
|
203
|
+
else
|
204
|
+
#protocol error
|
205
|
+
raise 'Protocol Error, closing connection.'
|
206
|
+
return close
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
reply.string.clear
|
211
|
+
|
212
|
+
request[:ws_client_params] = options
|
213
|
+
|
214
|
+
Iodine::Http::Websockets.new( ( ssl || socket), self.new(request), request )
|
215
|
+
|
216
|
+
rescue => e
|
217
|
+
(ssl || socket).tap {|io| next if io.nil?; io.close unless io.closed?}
|
218
|
+
raise e
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'iodine'
|
2
|
+
require 'stringio'
|
3
|
+
require 'time'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
require 'uri'
|
7
|
+
require 'tmpdir'
|
8
|
+
require 'zlib'
|
9
|
+
|
10
|
+
# require 'securerandom'
|
11
|
+
|
12
|
+
module Iodine
|
13
|
+
class Http < Iodine::Protocol
|
14
|
+
class WebsocketEchoDemo
|
15
|
+
def initialize request, response
|
16
|
+
@request = request
|
17
|
+
@response = response
|
18
|
+
on_open
|
19
|
+
end
|
20
|
+
def on_open
|
21
|
+
end
|
22
|
+
def on_message data
|
23
|
+
@response << "You >> #{data}"
|
24
|
+
end
|
25
|
+
def on_close
|
26
|
+
end
|
27
|
+
|
28
|
+
# This method allows the class itself to act as the Websocket handler, usable with:
|
29
|
+
# Iodine::Http.on_websocket Iodine::Http::WebsocketEchoDemo
|
30
|
+
def self.call request, response
|
31
|
+
return false if request[:path] =~ /refuse/i
|
32
|
+
self.new request, response
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
@@ -0,0 +1,319 @@
|
|
1
|
+
module Iodine
|
2
|
+
class Http < Iodine::Protocol
|
3
|
+
class Websockets < ::Iodine::Protocol
|
4
|
+
def initialize io, handler, request, ws_extentions = nil
|
5
|
+
@handler = handler
|
6
|
+
@ws_extentions = ws_extentions
|
7
|
+
request[:io] = self
|
8
|
+
super(io)
|
9
|
+
end
|
10
|
+
def on_open
|
11
|
+
set_timeout 45
|
12
|
+
@parser = {body: '', stage: 0, step: 0, mask_key: [], len_bytes: []}
|
13
|
+
set_timeout = self.class.default_timeout
|
14
|
+
@handler.on_open self if @handler.respond_to? :on_open
|
15
|
+
end
|
16
|
+
def on_message data
|
17
|
+
extract_message StringIO.new(data)
|
18
|
+
end
|
19
|
+
def on_broadcast data
|
20
|
+
@handler.on_broadcast(data) if @handler.respond_to? :on_broadcast
|
21
|
+
end
|
22
|
+
def on_close
|
23
|
+
@handler.on_close if @handler.respond_to? :on_close
|
24
|
+
if @ws_extentions
|
25
|
+
@ws_extentions.each { |ex| ex.close }
|
26
|
+
@ws_extentions.clear
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_response response, finish = false
|
31
|
+
body = response.extract_body
|
32
|
+
send_data body
|
33
|
+
end
|
34
|
+
alias :stream_response :send_response
|
35
|
+
|
36
|
+
# sends the data as one (or more) Websocket frames
|
37
|
+
def send_data data, op_code = nil, fin = true, ext = 0
|
38
|
+
return false if !data || data.empty?
|
39
|
+
return false if @io.closed?
|
40
|
+
data = data.dup # needed?
|
41
|
+
unless op_code # apply extenetions to the message as a whole
|
42
|
+
op_code = (data.encoding == ::Encoding::UTF_8 ? 1 : 2)
|
43
|
+
@ws_extentions.each { |ex| ext |= ex.edit_message data } if @ws_extentions
|
44
|
+
end
|
45
|
+
byte_size = data.bytesize
|
46
|
+
if byte_size > (FRAME_SIZE_LIMIT+2)
|
47
|
+
sections = byte_size/FRAME_SIZE_LIMIT + (byte_size%FRAME_SIZE_LIMIT ? 1 : 0)
|
48
|
+
send_data( data.slice!( 0...FRAME_SIZE_LIMIT ), op_code, data.empty?, ext) && (ext = op_code = 0) until data.empty?
|
49
|
+
return true # avoid sending an empty frame.
|
50
|
+
end
|
51
|
+
@ws_extentions.each { |ex| ext |= ex.edit_frame data } if @ws_extentions
|
52
|
+
header = ( (fin ? 0b10000000 : 0) | (op_code & 0b00001111) | ext).chr.force_encoding(::Encoding::ASCII_8BIT)
|
53
|
+
|
54
|
+
if byte_size < 125
|
55
|
+
header << byte_size.chr
|
56
|
+
elsif byte_size.bit_length <= 16
|
57
|
+
header << 126.chr
|
58
|
+
header << [byte_size].pack('S>'.freeze)
|
59
|
+
else
|
60
|
+
header << 127.chr
|
61
|
+
header << [byte_size].pack('Q>'.freeze)
|
62
|
+
end
|
63
|
+
write header
|
64
|
+
write(data) && true
|
65
|
+
end
|
66
|
+
alias :<< :send_data
|
67
|
+
|
68
|
+
# Sends a ping.
|
69
|
+
def ping
|
70
|
+
write PING_FRAME
|
71
|
+
end
|
72
|
+
# Sends an empty pong.
|
73
|
+
def pong
|
74
|
+
write PONG_FRAME
|
75
|
+
end
|
76
|
+
|
77
|
+
# Broadcasts data to ALL the websocket connections EXCEPT the once specified (if specified).
|
78
|
+
#
|
79
|
+
# Data broadcasted will be recived by the websocket handler it's #on_broadcast(ws) method (if exists).
|
80
|
+
#
|
81
|
+
# Accepts:
|
82
|
+
#
|
83
|
+
# data:: One object of data. Usually a Hash, Array, String or a JSON formatted object.
|
84
|
+
# ignore_io (optional):: The IO to be ignored by the broadcast. Usually the broadcaster's IO.
|
85
|
+
#
|
86
|
+
def self.broadcast data, ignore_io = nil
|
87
|
+
if ignore_io
|
88
|
+
ig_id = ignore_io.object_id
|
89
|
+
each {|io| Iodine.run io, data, &broadcast_proc unless io.object_id == ig_id}
|
90
|
+
else
|
91
|
+
each {|io| Iodine.run io, data, &broadcast_proc }
|
92
|
+
end
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Broadcasts the data to all the listening websockets, except self. See {::Iodine::Http::Websockets.broadcast}
|
97
|
+
def broadcast data
|
98
|
+
self.class.broadcast data, self
|
99
|
+
end
|
100
|
+
|
101
|
+
# Unicast data to a specific websocket connection (ONLY the connection specified).
|
102
|
+
#
|
103
|
+
# Data broadcasted will be recived by the websocket handler it's #on_broadcast(ws) method (if exists).
|
104
|
+
# Accepts:
|
105
|
+
# uuid:: the UUID of the websocket connection recipient.
|
106
|
+
# data:: the data to be sent.
|
107
|
+
#
|
108
|
+
# @return [true, false] Returns true if the object was found and the unicast was sent (the task will be executed asynchronously once the unicast was sent).
|
109
|
+
def self.unicast id, data
|
110
|
+
return false unless id && data
|
111
|
+
each {|io| next unless io.id == id; Iodine.run io, data, &broadcast_proc; return true}
|
112
|
+
false
|
113
|
+
end
|
114
|
+
# @return [true, false] Unicasts the data to the requested connection. returns `true` if the requested connection id was found on this server. See {::Iodine::Http::Websockets.unicast}
|
115
|
+
def unicast id, data
|
116
|
+
self.class.unicast id, data
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.handshake request, response, handler
|
120
|
+
# review handshake (version, extentions)
|
121
|
+
# should consider adopting the websocket gem for handshake and framing:
|
122
|
+
# https://github.com/imanel/websocket-ruby
|
123
|
+
# http://www.rubydoc.info/github/imanel/websocket-ruby
|
124
|
+
return refuse response unless handler || handler == true
|
125
|
+
io = request[:io]
|
126
|
+
response.keep_alive = true
|
127
|
+
response.status = 101
|
128
|
+
response['upgrade'.freeze] = 'websocket'.freeze
|
129
|
+
response['content-length'.freeze] = '0'.freeze
|
130
|
+
response['connection'.freeze] = 'Upgrade'.freeze
|
131
|
+
response['sec-websocket-version'.freeze] = '13'.freeze
|
132
|
+
# Note that the client is only offering to use any advertised extensions
|
133
|
+
# and MUST NOT use them unless the server indicates that it wishes to use the extension.
|
134
|
+
ws_extentions = []
|
135
|
+
ext = []
|
136
|
+
request['sec-websocket-extensions'.freeze].to_s.split(/[\s]*[,][\s]*/).each {|ex| ex = ex.split(/[\s]*;[\s]*/); ( ( tmp = SUPPORTED_EXTENTIONS[ ex[0] ].call(ex[1..-1]) ) && (ws_extentions << tmp) && (ext << tmp.name) ) if SUPPORTED_EXTENTIONS[ ex[0] ] }
|
137
|
+
ext.compact!
|
138
|
+
if ext.any?
|
139
|
+
response['sec-websocket-extensions'.freeze] = ext.join(', ')
|
140
|
+
else
|
141
|
+
ws_extentions = nil
|
142
|
+
end
|
143
|
+
response['Sec-WebSocket-Accept'.freeze] = Digest::SHA1.base64digest(request['sec-websocket-key'.freeze] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.freeze)
|
144
|
+
response.session
|
145
|
+
# Iodine.logger << "#{@request[:client_ip]} [#{Time.now.utc}] - #{@connection.object_id} Upgraded HTTP to WebSockets.\n"
|
146
|
+
# request.io.handler.send_response response
|
147
|
+
response.finish
|
148
|
+
self.new(io.io, handler, request, ws_extentions)
|
149
|
+
return true
|
150
|
+
end
|
151
|
+
|
152
|
+
# Gets the new connection timeout in seconds. Whenever this timeout is reached, a ping will be sent. Defaults to 40 (seconds).
|
153
|
+
def self.default_timeout
|
154
|
+
@default_timeout
|
155
|
+
end
|
156
|
+
# Sets the new connection timeout in seconds. Whenever this timeout is reached, a ping will be sent. Defaults to 40 (seconds).
|
157
|
+
def self.default_timeout= val
|
158
|
+
@default_timeout = val
|
159
|
+
end
|
160
|
+
# Sets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
161
|
+
#
|
162
|
+
# Although memory will be allocated for the latest TCP/IP frame,
|
163
|
+
# this allows the websocket to disconnect if the incoming expected message size exceeds the allowed maximum size.
|
164
|
+
#
|
165
|
+
# If the sessage size limit is exceeded, the disconnection will be immidiate as an attack will be assumed. The protocol's normal disconnect sequesnce will be discarded.
|
166
|
+
def self.message_size_limit=val
|
167
|
+
@message_size_limit = val
|
168
|
+
end
|
169
|
+
# Gets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
170
|
+
def self.message_size_limit
|
171
|
+
@message_size_limit ||= 0
|
172
|
+
end
|
173
|
+
|
174
|
+
protected
|
175
|
+
FRAME_SIZE_LIMIT = 17_895_697
|
176
|
+
SUPPORTED_EXTENTIONS = {}
|
177
|
+
CLOSE_FRAME = "\x88\x00".freeze
|
178
|
+
PONG_FRAME = "\x8A\x00".freeze
|
179
|
+
PING_FRAME = "\x89\x00".freeze
|
180
|
+
@default_timeout = 40
|
181
|
+
|
182
|
+
def self.broadcast_proc
|
183
|
+
@broadcast_proc ||= Proc.new {|io, data| io.on_broadcast data }
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.refuse response
|
187
|
+
response.status = 400
|
188
|
+
response['sec-websocket-extensions'.freeze] = SUPPORTED_EXTENTIONS.keys.join(', ')
|
189
|
+
response['sec-websocket-version'.freeze] = '13'.freeze
|
190
|
+
false
|
191
|
+
end
|
192
|
+
|
193
|
+
# parse the message and send it to the handler
|
194
|
+
#
|
195
|
+
# test: frame = ["819249fcd3810b93b2fb69afb6e62c8af3e83adc94ee2ddd"].pack("H*").bytes; parser[:stage] = 0; parser = {}
|
196
|
+
# accepts:
|
197
|
+
# data:: an IO object (usually a StringIO object)
|
198
|
+
def extract_message data
|
199
|
+
parser = @parser
|
200
|
+
until data.eof?
|
201
|
+
if parser[:stage] == 0
|
202
|
+
tmp = data.getbyte
|
203
|
+
return unless tmp
|
204
|
+
parser[:fin] = tmp[7] == 1
|
205
|
+
parser[:rsv1] = tmp[6] == 1
|
206
|
+
parser[:rsv2] = tmp[5] == 1
|
207
|
+
parser[:rsv3] = tmp[4] == 1
|
208
|
+
parser[:op_code] = tmp & 0b00001111
|
209
|
+
parser[:p_op_code] ||= tmp & 0b00001111
|
210
|
+
parser[:stage] += 1
|
211
|
+
end
|
212
|
+
if parser[:stage] == 1
|
213
|
+
tmp = data.getbyte
|
214
|
+
return unless tmp
|
215
|
+
parser[:mask] = tmp[7]
|
216
|
+
parser[:mask_key].clear
|
217
|
+
parser[:len] = tmp & 0b01111111
|
218
|
+
parser[:len_bytes].clear
|
219
|
+
parser[:stage] += 1
|
220
|
+
end
|
221
|
+
if parser[:stage] == 2
|
222
|
+
tmp = 0
|
223
|
+
tmp = 2 if parser[:len] == 126
|
224
|
+
tmp = 8 if parser[:len] == 127
|
225
|
+
while parser[:len_bytes].length < tmp
|
226
|
+
parser[:len_bytes] << data.getbyte
|
227
|
+
return parser[:len_bytes].pop unless parser[:len_bytes].last
|
228
|
+
end
|
229
|
+
parser[:len] = merge_bytes( parser[:len_bytes] ) if tmp > 0
|
230
|
+
parser[:step] = 0
|
231
|
+
parser[:stage] += 1
|
232
|
+
return false unless review_message_size
|
233
|
+
end
|
234
|
+
if parser[:stage] == 3 && parser[:mask] == 1
|
235
|
+
until parser[:mask_key].length == 4
|
236
|
+
parser[:mask_key] << data.getbyte
|
237
|
+
return parser[:mask_key].pop unless parser[:mask_key].last
|
238
|
+
end
|
239
|
+
parser[:stage] += 1
|
240
|
+
elsif parser[:stage] == 3 && parser[:mask] != 1
|
241
|
+
parser[:stage] += 1
|
242
|
+
end
|
243
|
+
if parser[:stage] == 4
|
244
|
+
if parser[:body].bytesize < parser[:len]
|
245
|
+
tmp = data.read(parser[:len] - parser[:body].bytesize)
|
246
|
+
return unless tmp
|
247
|
+
parser[:body] << tmp
|
248
|
+
end
|
249
|
+
if parser[:body].bytesize >= parser[:len]
|
250
|
+
parser[:body].bytesize.times {|i| parser[:body][i] = (parser[:body][i].ord ^ parser[:mask_key][i % 4]).chr} if parser[:mask] == 1
|
251
|
+
parser[:stage] = 99
|
252
|
+
end
|
253
|
+
end
|
254
|
+
complete_frame if parser[:stage] == 99
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# takes and Array of bytes and combines them to an int(16 Bit), 32Bit or 64Bit number
|
259
|
+
def merge_bytes bytes
|
260
|
+
return 0 unless bytes.any?
|
261
|
+
return bytes.pop if bytes.length == 1
|
262
|
+
bytes.pop ^ (merge_bytes(bytes) << 8)
|
263
|
+
end
|
264
|
+
|
265
|
+
# handles the completed frame and sends a message to the handler once all the data has arrived.
|
266
|
+
def complete_frame
|
267
|
+
parser = @parser
|
268
|
+
@ws_extentions.each {|ex| ex.parse_frame(parser) } if @ws_extentions
|
269
|
+
|
270
|
+
case parser[:op_code]
|
271
|
+
when 9 # ping
|
272
|
+
# handle parser[:op_code] == 9 (ping)
|
273
|
+
::Iodine.run { send_data parser[:body], 10 }
|
274
|
+
parser[:p_op_code] = nil if parser[:p_op_code] == 9
|
275
|
+
when 10 #pong
|
276
|
+
# handle parser[:op_code] == 10 (pong)
|
277
|
+
parser[:p_op_code] = nil if parser[:p_op_code] == 10
|
278
|
+
when 8 # close
|
279
|
+
# handle parser[:op_code] == 8 (close)
|
280
|
+
write( CLOSE_FRAME )
|
281
|
+
close
|
282
|
+
parser[:p_op_code] = nil if parser[:p_op_code] == 8
|
283
|
+
else
|
284
|
+
parser[:message] ? ((parser[:message] << parser[:body]) && parser[:body].clear) : ((parser[:message] = parser[:body]) && parser[:body] = '')
|
285
|
+
# handle parser[:op_code] == 0 / fin == false (continue a frame that hasn't ended yet)
|
286
|
+
if parser[:fin]
|
287
|
+
@ws_extentions.each {|ex| ex.parse_message(parser) } if @ws_extentions
|
288
|
+
Iodine::Http::Request.make_utf8! parser[:message] if parser[:p_op_code] == 1
|
289
|
+
@handler.on_message parser[:message]
|
290
|
+
parser[:message] = nil
|
291
|
+
parser[:p_op_code] = nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
parser[:stage] = 0
|
295
|
+
parser[:body].clear
|
296
|
+
parser[:step] = 0
|
297
|
+
parser[:mask_key].clear
|
298
|
+
parser[:p_op_code] = nil
|
299
|
+
end
|
300
|
+
#reviews the message size and closes the connection if expected message size is over the allowed limit.
|
301
|
+
def review_message_size
|
302
|
+
if ( self.class.message_size_limit.to_i > 0 ) && ( ( @parser[:len] + (@parser[:message] ? @parser[:message].bytesize : 0) ) > self.class.message_size_limit.to_i )
|
303
|
+
close
|
304
|
+
@parser.delete :message
|
305
|
+
@parser[:step] = 0
|
306
|
+
@parser[:body].clear
|
307
|
+
@parser[:mask_key].clear
|
308
|
+
Iodine.warn "Websocket message above limit's set - closing connection."
|
309
|
+
return false
|
310
|
+
end
|
311
|
+
true
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
|