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
-
#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(&block) ⇒ 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 |
# 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] end |
Instance Attribute Details
#request ⇒ Object
return the HTTP’s handshake data, including any cookies sent by the server.
81 82 83 |
# File 'lib/iodine/http/websocket_client.rb', line 81 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. 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.
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.
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.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 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 |
# File 'lib/iodine/http/websocket_client.rb', line 133 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) 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 <<
59 60 61 62 |
# File 'lib/iodine/http/websocket_client.rb', line 59 def << data raise 'Cannot send data when the connection is closed.' if closed? @io << data end |
#close ⇒ Object
closes the connection, if open
66 67 68 |
# File 'lib/iodine/http/websocket_client.rb', line 66 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).
71 72 73 |
# File 'lib/iodine/http/websocket_client.rb', line 71 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.
85 86 87 |
# File 'lib/iodine/http/websocket_client.rb', line 85 def @request. end |
#on(event_name, &block) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/iodine/http/websocket_client.rb', line 23 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
51 52 53 54 |
# File 'lib/iodine/http/websocket_client.rb', line 51 def on_close(&block) @on_close = block if block instance_exec(&@on_close) if @on_close end |
#on_message(data = nil, &block) ⇒ Object
36 37 38 39 40 41 42 |
# File 'lib/iodine/http/websocket_client.rb', line 36 def (data = nil, &block) unless data @on_message = block if block return @on_message end instance_exec( data, &@on_message) end |
#on_open(&block) ⇒ Object
44 45 46 47 48 49 |
# File 'lib/iodine/http/websocket_client.rb', line 44 def on_open(&block) raise 'The on_open even is invalid at this point.' if block @io = @request[:io] Iodine::Http::Request.parse @request instance_exec(&@on_open) if @on_open end |
#ssl? ⇒ Boolean
checks if this is an SSL websocket connection.
76 77 78 |
# File 'lib/iodine/http/websocket_client.rb', line 76 def ssl? @request.ssl? end |