Class: Iodine::Http::WebsocketClient

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

#requestObject

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

#responseObject

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.on_message do |data|
     response << "I'll disconnect on the next message!"
     # updates #on_message again.
     on_message {|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.

Returns:



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, options={}, &block
	socket = nil
	options = options.dup
	options[:on_message] ||= block
	raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[: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: (options[: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 = options[:headers] if options[:headers].is_a?(String)
	options[:headers].each {|k, v| custom_headers << "#{k.to_s}: #{v.to_s}\r\n"} if options[:headers].is_a?(Hash)
	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)

	# 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: #{options[: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 + (options[: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, (options[: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] = options
	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 <<

Returns:

  • (true, false)

    Returns the true if the data was actually sent or nil if no data was sent.



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

#closeObject

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).

Returns:

  • (Boolean)


71
72
73
# File 'lib/iodine/http/websocket_client.rb', line 71

def closed?
	@io.io.closed? if @io && @io.io
end

#cookiesObject

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 cookies
	@request.cookies
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 on_message(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.

Returns:

  • (Boolean)


76
77
78
# File 'lib/iodine/http/websocket_client.rb', line 76

def ssl?
	@request.ssl?
end