Class: Iodine::Http::WebsocketClient
- Inherits:
-
Object
- Object
- Iodine::Http::WebsocketClient
- Defined in:
- lib/iodine/http/websocket_client.rb
Overview
Create a simple Websocket Client(!).
This should be done from within an Iodine task, or the callbacks will not be called.
Use WebsocketClient.connect to initialize a client with all the callbacks needed.
Instance Attribute Summary collapse
-
#params ⇒ Object
Returns the value of attribute params.
-
#request ⇒ Object
return the HTTP’s handshake data, including any cookies sent by the server.
-
#response ⇒ Object
Returns the value of attribute response.
Class Method Summary collapse
-
.connect(url, options = {}, &block) ⇒ Iodine::Http::WebsocketClient
Create a simple Websocket Client(!).
Instance Method Summary collapse
-
#<<(data) ⇒ true, false
(also: #write)
Sends data through the socket.
-
#close ⇒ Object
closes the connection, if open.
-
#closed? ⇒ Boolean
checks if the socket is open (if the websocket was terminated abnormally, this might returs true when it should be false).
-
#cookies ⇒ Object
return a Hash with the HTTP cookies recieved during the HTTP’s handshake.
-
#initialize(request) ⇒ WebsocketClient
constructor
A new instance of WebsocketClient.
- #on(event_name, &block) ⇒ Object
- #on_close(&block) ⇒ Object
- #on_message(data = nil, &block) ⇒ Object
- #on_open ⇒ Object
-
#ssl? ⇒ Boolean
checks if this is an SSL websocket connection.
Constructor Details
#initialize(request) ⇒ WebsocketClient
Returns a new instance of WebsocketClient.
13 14 15 16 17 18 19 20 21 22 |
# File 'lib/iodine/http/websocket_client.rb', line 13 def initialize request @response = nil @request = request @params = request[:ws_client_params] @on_message = @params[:on_message] raise "Websocket client must have an #on_message Proc or handler." unless @on_message && @on_message.respond_to?(:call) @on_open = @params[:on_open] @on_close = @params[:on_close] @renew = @params[:renew].to_i end |
Instance Attribute Details
#params ⇒ Object
Returns the value of attribute params.
11 12 13 |
# File 'lib/iodine/http/websocket_client.rb', line 11 def params @params end |
#request ⇒ Object
return the HTTP’s handshake data, including any cookies sent by the server.
115 116 117 |
# File 'lib/iodine/http/websocket_client.rb', line 115 def request @request end |
#response ⇒ Object
Returns the value of attribute response.
11 12 13 |
# File 'lib/iodine/http/websocket_client.rb', line 11 def response @response end |
Class Method Details
.connect(url, options = {}, &block) ⇒ Iodine::Http::WebsocketClient
Create a simple Websocket Client(!).
This method accepts two parameters:
- url
-
a String representing the URL of the websocket. i.e.: ‘ws://foo.bar.com:80/ws/path’
- 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))
- &block
-
an optional block that accepts one parameter (data) and will be used as the ‘#on_message(data)`
Acceptable options are:
- on_open
-
the on_open callback. Must be an objects that answers ‘call(ws)`, usually a Proc.
- on_message
-
the on_message callback. Must be an objects that answers ‘call(ws)`, usually a Proc.
- on_close
-
the on_close callback - this will ONLY be called if the connection WASN’T renewed. Must be an objects that answers ‘call(ws)`, usually a Proc.
- headers
-
a Hash of custom HTTP headers to be sent with the request. Header data, including cookie headers, should be correctly encoded.
- cookies
-
a Hash of cookies to be sent with the request. cookie data will be encoded before being sent.
- timeout
-
the number of seconds to wait before the connection is established. Defaults to 5 seconds.
- every
-
this option, together with ‘:send` and `:renew`, implements a polling websocket. :every is the number of seconds between each polling event. without `:send`, this option will be ignored. defaults to nil.
- send
-
a String to be sent or a Proc to be performed each polling interval. This option, together with ‘:every` and `:renew`, implements a polling websocket. without `:every`, this option will be ignored. defaults to nil. If `:send` is a Proc, it will be executed within the context of the websocket client object, with acess to the websocket client’s instance variables and methods.
- renew
-
the number of times to attempt to renew the connection if the connection is terminated by the remote server. Attempts are made in 2 seconds interval. The default for a polling websocket is 5 attempts to renew. For all other clients, the default is 0 (no renewal).
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.
Use Iodine::Http.ws_connect for a non-blocking initialization.
An #on_close callback will only be called if the connection isn’t or cannot be renewed. If the connection is renewed, the #on_open callback will be called again for a new Websocket client instance - but the #on_close callback will NOT be called.
Due to this design, the #on_open and #on_close methods should NOT be used for opening IO resources (i.e. file handles) nor for cleanup IF the ‘:renew` option is enabled.
An on_message Proc must be defined, or the method will fail.
The on_message Proc can be defined using the optional block:
Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") {|data| write data} #echo example
OR, the on_message Proc can be defined using the options Hash:
Iodine::Http::WebsocketClient.connect("ws://localhost:3000/", on_open: -> {}, on_message: -> {|data| write data })
The #on_message(data), #on_open and #on_close methods will be executed within the context of the WebsocketClient object, and will have native acess to the Websocket response object.
After the WebsocketClient had been created, it’s possible to update the #on_message and #on_close methods:
# updates #on_message
wsclient. do |data|
response << "I'll disconnect on the next message!"
# updates #on_message again.
{|data| disconnect }
end
!!please be aware that the Websockt Client will not attempt to verify SSL certificates, so that even SSL connections are vulnerable to a possible man in the middle attack.
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/iodine/http/websocket_client.rb', line 177 def self.connect url, ={}, &block socket = nil = .dup [:on_message] ||= block raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless [:on_message] url = URI.parse(url) unless url.is_a?(URI) [:url] = url [:renew] ||= 5 if [:every] && [:send] ssl = url.scheme == "https" || url.scheme == "wss" url.port ||= ssl ? 443 : 80 url.path = '/' if url.path.to_s.empty? socket = TCPSocket.new(url.host, url.port) if ssl context = OpenSSL::SSL::SSLContext.new context.cert_store = OpenSSL::X509::Store.new context.cert_store.set_default_paths context.set_params verify_mode: ([:verify_mode] || OpenSSL::SSL::VERIFY_NONE) # OpenSSL::SSL::VERIFY_PEER #OpenSSL::SSL::VERIFY_NONE ssl = OpenSSL::SSL::SSLSocket.new(socket, context) ssl.sync_close = true ssl.connect end # prep custom headers custom_headers = '' custom_headers = [:headers] if [:headers].is_a?(String) [:headers].each {|k, v| custom_headers << "#{k.to_s}: #{v.to_s}\r\n"} if [:headers].is_a?(Hash) [: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 [:cookies].is_a?(Hash) # send protocol upgrade request websocket_key = [(Array.new(16) {rand 255} .pack 'c*' )].pack('m0*') (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: #{[:ssl_client] ? 'https' : 'http'}://#{url.host}\r\nSec-WebSocket-Key: #{websocket_key}\r\nSec-WebSocket-Version: 13\r\n#{custom_headers}\r\n" # wait for answer - make sure we don't over-read # (a websocket message might be sent immidiately after connection is established) reply = '' reply.force_encoding(::Encoding::ASCII_8BIT) stop_time = Time.now + ([:timeout] || 5) stop_reply = "\r\n\r\n" sleep 0.2 until reply[-4..-1] == stop_reply begin reply << ( ssl ? ssl.read_nonblock(1) : socket.recv_nonblock(1) ) rescue Errno::EWOULDBLOCK => e raise "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply}" if Time.now >= stop_time IO.select [socket], nil, nil, ([:timeout] || 5) retry end raise "Connection failed" if socket.closed? end # review reply raise "Connection Refused. Reply was:\r\n #{reply}" unless reply.lines[0].match(/^HTTP\/[\d\.]+ 101/i) 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') # read the body's data and parse any incoming data. request = Iodine::Http::Request.new request[:method] = 'GET' request['host'] = "#{url.host}:#{url.port}" request[:query] = url.path request[:version] = '1.1' reply = StringIO.new reply reply.gets until reply.eof? until request[:headers_complete] || (l = reply.gets).nil? if l.include? ':' l = l.strip.split(/:[\s]?/, 2) l[0].strip! ; l[0].downcase!; 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]) elsif l =~ /^[\r]?\n/ request[:headers_complete] = true else #protocol error raise 'Protocol Error, closing connection.' return close end end end reply.string.clear request[:ws_client_params] = client = self.new(request) Iodine::Http::Websockets.new( ( ssl || socket), client, request ) return client rescue => e (ssl || socket).tap {|io| next if io.nil?; io.close unless io.closed?} raise e end |
Instance Method Details
#<<(data) ⇒ true, false Also known as: write
Sends data through the socket. a shortcut for ws_client.response <<
93 94 95 96 |
# File 'lib/iodine/http/websocket_client.rb', line 93 def << data raise 'Cannot send data when the connection is closed.' if closed? @io << data end |
#close ⇒ Object
closes the connection, if open
100 101 102 |
# File 'lib/iodine/http/websocket_client.rb', line 100 def close @io.close if @io end |
#closed? ⇒ Boolean
checks if the socket is open (if the websocket was terminated abnormally, this might returs true when it should be false).
105 106 107 |
# File 'lib/iodine/http/websocket_client.rb', line 105 def closed? @io.io.closed? if @io && @io.io end |
#cookies ⇒ Object
return a Hash with the HTTP cookies recieved during the HTTP’s handshake.
119 120 121 |
# File 'lib/iodine/http/websocket_client.rb', line 119 def @request. end |
#on(event_name, &block) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/iodine/http/websocket_client.rb', line 24 def on event_name, &block return false unless block case event_name when :message @on_message = block when :close @on_close = block when :open raise 'The on_open even is invalid at this point.' end end |
#on_close(&block) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/iodine/http/websocket_client.rb', line 66 def on_close(&block) return @on_close = block if block if @renew > 0 renew_proc = Proc.new do begin Iodine::Http::WebsocketClient.connect(@params[:url], @params) rescue @renew -= 1 if @renew <= 0 Iodine.fatal "WebsocketClient renewal FAILED for #{@params[:url]}" instance_exec(&@on_close) if @on_close else Iodine.run_after 2, &renew_proc Iodine.warn "WebsocketClient renewal failed for #{@params[:url]}, #{@renew} attempts left" end false end end renew_proc.call else instance_exec(&@on_close) if @on_close end end |
#on_message(data = nil, &block) ⇒ Object
37 38 39 40 41 42 43 |
# File 'lib/iodine/http/websocket_client.rb', line 37 def (data = nil, &block) unless data @on_message = block if block return @on_message end instance_exec( data, &@on_message) end |
#on_open ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/iodine/http/websocket_client.rb', line 45 def on_open raise 'The on_open even is invalid at this point.' if block_given? @io = @request[:io] Iodine::Http::Request.parse @request instance_exec(&@on_open) if @on_open if request[:ws_client_params][:every] && @params[:send] raise TypeError, "Websocket Client `:send` should be either a String or a Proc object." unless @params[:send].is_a?(String) || @params[:send].is_a?(Proc) Iodine.run_every @params[:every], self, @params do |ws, client_params, timer| if ws.closed? timer.stop! next end if client_params[:send].is_a?(String) ws.write client_params[:send] elsif client_params[:send].is_a?(Proc) ws.instance_exec(&client_params[:send]) end end end end |
#ssl? ⇒ Boolean
checks if this is an SSL websocket connection.
110 111 112 |
# File 'lib/iodine/http/websocket_client.rb', line 110 def ssl? @request.ssl? end |