Class: GRHttp::WSClient

Inherits:
Object
  • Object
show all
Defined in:
lib/grhttp/ws_client.rb

Overview

Websocket client objects are members of this class.

This is a VERY simple Websocket client. It doesn't support cookies, HTTP authentication or… well… anything, really. It's just a simple client used for framework's testing. It's useful for simple WebSocket connections, but no more.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request) ⇒ WSClient

Returns a new instance of WSClient


10
11
12
13
14
15
16
17
18
# File 'lib/grhttp/ws_client.rb', line 10

def initialize request
	@response = nil
	@request = request
	params = request.io.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.


79
80
81
# File 'lib/grhttp/ws_client.rb', line 79

def request
  @request
end

#responseObject

Returns the value of attribute response


8
9
10
# File 'lib/grhttp/ws_client.rb', line 8

def response
  @response
end

Class Method Details

.async_connect(url, options = {}, &block) ⇒ true

Asynchronously connects to a websocket server.

Returns:

  • (true)

    this method always returns true or raises an exception if no block or :on_message handler were present (see the connect method for more details).


90
91
92
93
94
95
96
# File 'lib/grhttp/ws_client.rb', line 90

def self.async_connect url, options={}, &block
	GReactor.start unless GReactor.running?
	options[:on_message] ||= block
	raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[:on_message]
	GReactor.run_async { connect url, options }
	true
end

.connect(url, options = {}, &block) ⇒ GRHttp::WSClient Also known as: connect_to

Create a simple Websocket Client(!). This will implicitly start the IO reactor pattern.

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:

WebsocketClient.connect_to("ws://localhost:3000/") {|data| response << data} #echo example

OR, the on_message Proc can be defined using the options Hash:

WebsocketClient.connect_to("ws://localhost:3000/", on_connect: -> {}, on_message: -> {|data| response << data})

The #on_message(data), #on_connect and #on_disconnect methods will be executed within the context of the WebsocketClient object, and will have natice acess to the Websocket response object.

After the WebsocketClient had been created, it's possible to update the #on_message and #on_disconnect 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 subject to a possible man in the middle attack.

Returns:


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
# File 'lib/grhttp/ws_client.rb', line 141

def self.connect url, options={}, &block
	GReactor.start unless GReactor.running?
	socket = nil
	options[:on_message] ||= block
	raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[:on_message]
	options[:handler] = GRHttp::Base::WSHandler
	url = URI.parse(url) unless url.is_a?(URI)

	connection_type = GReactor::BasicIO
	if url.scheme == "https" || url.scheme == "wss"
		connection_type = GReactor::SSLBasicIO
		options[:ssl_client] = true
		url.port ||= 443
	end
	url.port ||= 80
	url.path = '/' if url.path.to_s.empty?
	socket = TCPSocket.new(url.host, url.port)
	io = options[:io] = connection_type.new(socket, options)

	# 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 }=#{ HTTP.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*')
	io.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
		add = io.read(1)
		add ? (reply << add) : (sleep 0.2)
		raise "connections was closed" if io.io.closed?
		raise "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply}" if Time.now >= stop_time
	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 = io[:request] ||= HTTPRequest.new(io)
	request[:method] = 'GET'
	request['host'] = "#{url.host}:#{url.port}"
	request[:query] = url.path
	request[:version] = 'HTTP/1.1'
	# reply.gsub! /set-cookie/i, 'Cookie'
	reply = StringIO.new reply
	reply.gets
	HTTP._parse_http io, reply

	# set-up handler response object. 
	io[:ws_extentions] = [].freeze
	(io[:websocket_handler] = WSClient.new io[:request]).on_open(WSEvent.new(io, nil))
	# add the socket to the EventMachine IO reactor
	GReactor.add_raw_io io.io, io
	return io[:websocket_handler]
	rescue => e
		socket.close if socket
		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.


62
63
64
65
# File 'lib/grhttp/ws_client.rb', line 62

def << data
	# raise 'Cannot send data when the connection is closed.' if closed?
	@response << data
end

#closeObject

closes the connection, if open


69
70
71
# File 'lib/grhttp/ws_client.rb', line 69

def close
	@response.close if @response
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)

74
75
76
# File 'lib/grhttp/ws_client.rb', line 74

def closed?
	@response.io.io.closed?
end

#cookiesObject

return a Hash with the HTTP cookies recieved during the HTTP's handshake.


83
84
85
# File 'lib/grhttp/ws_client.rb', line 83

def cookies
	@request.cookies
end

#on(event_name, &block) ⇒ Object


20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/grhttp/ws_client.rb', line 20

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(ws = nil, &block) ⇒ Object


51
52
53
54
55
56
57
# File 'lib/grhttp/ws_client.rb', line 51

def on_close(ws = nil, &block)
	unless ws
		@on_close = block if block
		return @on_close
	end
	instance_exec( ws, &@on_close) if @on_close
end

#on_message(ws = nil, &block) ⇒ Object


33
34
35
36
37
38
39
# File 'lib/grhttp/ws_client.rb', line 33

def on_message(ws = nil, &block)
	unless ws
		@on_message = block if block
		return @on_message
	end
	instance_exec( ws, &@on_message) 
end

#on_open(ws = nil, &block) ⇒ Object


41
42
43
44
45
46
47
48
49
# File 'lib/grhttp/ws_client.rb', line 41

def on_open(ws = nil, &block)
	unless ws
		raise 'The on_open even is invalid at this point.' if block
		# @on_open = block if block
		return @on_open
	end
	@response = ws
	instance_exec( ws, &@on_open) if @on_open
end