iodine 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

@@ -7,216 +7,215 @@ module Iodine
7
7
  #
8
8
  # Use {Iodine::Http::WebsocketClient.connect} to initialize a client with all the callbacks needed.
9
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
10
+
11
+ attr_accessor :response, :request
12
+
13
+ def initialize request
14
+ @response = nil
15
+ @request = request
16
+ params = request[:ws_client_params]
17
+ @on_message = params[:on_message]
18
+ raise "Websocket client must have an #on_message Proc or handler." unless @on_message && @on_message.respond_to?(:call)
19
+ @on_open = params[:on_open]
20
+ @on_close = params[:on_close]
21
+ end
21
22
 
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.'
23
+ def on event_name, &block
24
+ return false unless block
25
+ case event_name
26
+ when :message
27
+ @on_message = block
28
+ when :close
29
+ @on_close = block
30
+ when :open
31
+ raise 'The on_open even is invalid at this point.'
32
+ end
33
+
31
34
  end
32
-
33
- end
34
35
 
35
- def on_message(data = nil, &block)
36
- unless data
37
- @on_message = block if block
38
- return @on_message
36
+ def on_message(data = nil, &block)
37
+ unless data
38
+ @on_message = block if block
39
+ return @on_message
40
+ end
41
+ instance_exec( data, &@on_message)
39
42
  end
40
- instance_exec( data, &@on_message)
41
- end
42
43
 
43
- def on_open(protocol = nil, &block)
44
- unless protocol
44
+ def on_open(&block)
45
45
  raise 'The on_open even is invalid at this point.' if block
46
- # @on_open = block if block
47
- return @on_open
46
+ @io = @request[:io]
47
+ Iodine::Http::Request.parse @request
48
+ instance_exec(&@on_open) if @on_open
48
49
  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
50
 
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 :<<
51
+ def on_close(&block)
52
+ @on_close = block if block
53
+ instance_exec(&@on_close) if @on_close
54
+ end
67
55
 
68
- # closes the connection, if open
69
- def close
70
- @io.close if @io
71
- end
56
+ # Sends data through the socket. a shortcut for ws_client.response <<
57
+ #
58
+ # @return [true, false] Returns the true if the data was actually sent or nil if no data was sent.
59
+ def << data
60
+ raise 'Cannot send data when the connection is closed.' if closed?
61
+ @io << data
62
+ end
63
+ alias :write :<<
72
64
 
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
65
+ # closes the connection, if open
66
+ def close
67
+ @io.close if @io
68
+ end
77
69
 
78
- # checks if this is an SSL websocket connection.
79
- def ssl?
80
- @request.ssl?
81
- end
70
+ # checks if the socket is open (if the websocket was terminated abnormally, this might returs true when it should be false).
71
+ def closed?
72
+ @io.io.closed? if @io && @io.io
73
+ end
82
74
 
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
75
+ # checks if this is an SSL websocket connection.
76
+ def ssl?
77
+ @request.ssl?
78
+ end
91
79
 
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
80
+ # return the HTTP's handshake data, including any cookies sent by the server.
81
+ def request
82
+ @request
156
83
  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?
84
+ # return a Hash with the HTTP cookies recieved during the HTTP's handshake.
85
+ def cookies
86
+ @request.cookies
182
87
  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
88
+
89
+ # Create a simple Websocket Client(!).
90
+ #
91
+ # This method accepts two parameters:
92
+ # url:: a String representing the URL of the websocket. i.e.: 'ws://foo.bar.com:80/ws/path'
93
+ # 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))
94
+ # &block:: an optional block that accepts one parameter (data) and will be used as the `#on_message(data)`
95
+ #
96
+ # Acceptable options are:
97
+ # on_open:: the on_open callback. Must be an objects that answers `call(ws)`, usually a Proc.
98
+ # on_message:: the on_message callback. Must be an objects that answers `call(ws)`, usually a Proc.
99
+ # on_close:: the on_close callback. Must be an objects that answers `call(ws)`, usually a Proc.
100
+ # headers:: a Hash of custom HTTP headers to be sent with the request. Header data, including cookie headers, should be correctly encoded.
101
+ # cookies:: a Hash of cookies to be sent with the request. cookie data will be encoded before being sent.
102
+ # timeout:: the number of seconds to wait before the connection is established. Defaults to 5 seconds.
103
+ #
104
+ # 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.
105
+ #
106
+ # An on_message Proc must be defined, or the method will fail.
107
+ #
108
+ # The on_message Proc can be defined using the optional block:
109
+ #
110
+ # Iodine::Http::WebsocketClient.connect("ws://localhost:3000/") {|data| write data} #echo example
111
+ #
112
+ # OR, the on_message Proc can be defined using the options Hash:
113
+ #
114
+ # Iodine::Http::WebsocketClient.connect("ws://localhost:3000/", on_open: -> {}, on_message: -> {|data| write data })
115
+ #
116
+ # The #on_message(data), #on_open and #on_close methods will be executed within the context of the WebsocketClient
117
+ # object, and will have native acess to the Websocket response object.
118
+ #
119
+ # After the WebsocketClient had been created, it's possible to update the #on_message and #on_close methods:
120
+ #
121
+ # # updates #on_message
122
+ # wsclient.on_message do |data|
123
+ # response << "I'll disconnect on the next message!"
124
+ # # updates #on_message again.
125
+ # on_message {|data| disconnect }
126
+ # end
127
+ #
128
+ #
129
+ # !!please be aware that the Websockt Client will not attempt to verify SSL certificates,
130
+ # so that even SSL connections are vulnerable to a possible man in the middle attack.
131
+ #
132
+ # @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).
133
+ def self.connect url, options={}, &block
134
+ socket = nil
135
+ options = options.dup
136
+ options[:on_message] ||= block
137
+ raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[:on_message]
138
+ url = URI.parse(url) unless url.is_a?(URI)
139
+
140
+ ssl = url.scheme == "https" || url.scheme == "wss"
141
+
142
+ url.port ||= ssl ? 443 : 80
143
+ url.path = '/' if url.path.to_s.empty?
144
+ socket = TCPSocket.new(url.host, url.port)
145
+ if ssl
146
+ context = OpenSSL::SSL::SSLContext.new
147
+ context.cert_store = OpenSSL::X509::Store.new
148
+ context.cert_store.set_default_paths
149
+ context.set_params verify_mode: (options[:verify_mode] || OpenSSL::SSL::VERIFY_NONE) # OpenSSL::SSL::VERIFY_PEER #OpenSSL::SSL::VERIFY_NONE
150
+ ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
151
+ ssl.sync_close = true
152
+ ssl.connect
153
+ end
154
+ # prep custom headers
155
+ custom_headers = ''
156
+ custom_headers = options[:headers] if options[:headers].is_a?(String)
157
+ options[:headers].each {|k, v| custom_headers << "#{k.to_s}: #{v.to_s}\r\n"} if options[:headers].is_a?(Hash)
158
+ 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)
159
+
160
+ # send protocol upgrade request
161
+ websocket_key = [(Array.new(16) {rand 255} .pack 'c*' )].pack('m0*')
162
+ (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"
163
+ # wait for answer - make sure we don't over-read
164
+ # (a websocket message might be sent immidiately after connection is established)
165
+ reply = ''
166
+ reply.force_encoding(::Encoding::ASCII_8BIT)
167
+ stop_time = Time.now + (options[:timeout] || 5)
168
+ stop_reply = "\r\n\r\n"
169
+ sleep 0.2
170
+ until reply[-4..-1] == stop_reply
171
+ begin
172
+ reply << ( ssl ? ssl.read_nonblock(1) : socket.recv_nonblock(1) )
173
+ rescue Errno::EWOULDBLOCK => e
174
+ raise "Websocket client handshake timed out (HTTP reply not recieved)\n\n Got Only: #{reply}" if Time.now >= stop_time
175
+ IO.select [socket], nil, nil, (options[:timeout] || 5)
176
+ retry
207
177
  end
178
+ raise "Connection failed" if socket.closed?
208
179
  end
209
- end
210
- reply.string.clear
180
+ # review reply
181
+ raise "Connection Refused. Reply was:\r\n #{reply}" unless reply.lines[0].match(/^HTTP\/[\d\.]+ 101/i)
182
+ 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')
183
+ # read the body's data and parse any incoming data.
184
+ request = Iodine::Http::Request.new
185
+ request[:method] = 'GET'
186
+ request['host'] = "#{url.host}:#{url.port}"
187
+ request[:query] = url.path
188
+ request[:version] = '1.1'
189
+ reply = StringIO.new reply
190
+ reply.gets
191
+
192
+ until reply.eof?
193
+ until request[:headers_complete] || (l = reply.gets).nil?
194
+ if l.include? ':'
195
+ l = l.strip.split(/:[\s]?/, 2)
196
+ l[0].strip! ; l[0].downcase!;
197
+ 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])
198
+ elsif l =~ /^[\r]?\n/
199
+ request[:headers_complete] = true
200
+ else
201
+ #protocol error
202
+ raise 'Protocol Error, closing connection.'
203
+ return close
204
+ end
205
+ end
206
+ end
207
+ reply.string.clear
211
208
 
212
- request[:ws_client_params] = options
209
+ request[:ws_client_params] = options
210
+ client = self.new(request)
211
+ Iodine::Http::Websockets.new( ( ssl || socket), client, request )
213
212
 
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
213
+ return client
214
+
215
+ rescue => e
216
+ (ssl || socket).tap {|io| next if io.nil?; io.close unless io.closed?}
217
+ raise e
218
+ end
220
219
  end
221
220
  end
222
221
  end
@@ -1,38 +1,72 @@
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
1
 
12
2
  module Iodine
13
3
  class Http < Iodine::Protocol
14
- class WebsocketEchoDemo
4
+
5
+ # This class is a good demonstration for creating a Websocket handler with the Iodine API.
6
+ #
7
+ # Iodine is Object Oriented and for this reason the Websocket handler is expected to
8
+ # retain the information it needs - either through initialization, or through the `on_open(protocol)` callback.
9
+ class WebsocketHandler
10
+ # The original Http request
11
+ attr_reader :request
12
+ # The Http response, also allowing for websocket data
13
+ attr_reader :response
14
+ # this is called while still communicating over Http (during the upgrade process).
15
15
  def initialize request, response
16
16
  @request = request
17
17
  @response = response
18
- on_open
19
18
  end
19
+ # initialize the protocol data once the connection had opened.
20
20
  def on_open
21
21
  end
22
+ # Accept data using this callback - this is a required callback.
22
23
  def on_message data
23
- @response << "You >> #{data}"
24
24
  end
25
+ # Accept unicasts or broadcasts using this callback.
26
+ def on_broadcast data
27
+ end
28
+ # cleanup, if needed, using this callback.
25
29
  def on_close
26
30
  end
27
31
 
28
32
  # This method allows the class itself to act as the Websocket handler, usable with:
29
33
  # Iodine::Http.on_websocket Iodine::Http::WebsocketEchoDemo
30
34
  def self.call request, response
31
- return false if request[:path] =~ /refuse/i
32
35
  self.new request, response
33
36
  end
34
37
 
35
38
  protected
39
+
40
+ ### some helper methods
41
+
42
+ # Write data to the client, using Websockets encoded frames.
43
+ def write data
44
+ # We leverage the fact that the Http response can be used to send Websocket data.
45
+ #
46
+ # you can also use Websocket#send_data or it's alias Websocket#<<
47
+ # do NOT use Websocket#write (which writes the data directly, bypassing the protocol).
48
+ @response << data
49
+ end
50
+
51
+ # Send messages directly to a specific Websocket.
52
+ #
53
+ # This implementation is limited to a single process on a single server.
54
+ # Consider using Redis for a scalable implementation.
55
+ def unicast id, data
56
+ # @request[:io] contains the Websocket Protocol
57
+ @request[:io].unicast id, data
58
+ end
59
+ # Broadcast to all Websockets, except self.
60
+ #
61
+ # This implementation is limited to a single process on a single server.
62
+ # Consider using Redis for a scalable implementation.
63
+ def broadcast data
64
+ @request[:io].broadcast data
65
+ end
66
+ # Closes the connection
67
+ def close
68
+ @request[:io].go_away
69
+ end
36
70
  end
37
71
  end
38
72
  end