eventflit-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'eventflit-client/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'eventflit-client'
8
+ s.version = EventflitClient::VERSION
9
+ s.authors = ["Eventflit", "Logan Koester"]
10
+ s.email = ['support@eventflit.com']
11
+ s.homepage = 'http://github.com/eventflit/eventflit-websocket-ruby'
12
+ s.summary = 'Client for consuming WebSockets from http://eventflit.com'
13
+ s.description = 'Client for consuming WebSockets from http://eventflit.com'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f|
18
+ File.basename(f)
19
+ }
20
+ s.extra_rdoc_files = %w(LICENSE.txt README.md)
21
+ s.require_paths = ['lib']
22
+ s.licenses = ['MIT']
23
+
24
+ s.add_runtime_dependency 'websocket', '~> 1.0'
25
+ s.add_runtime_dependency 'json'
26
+
27
+ s.add_development_dependency "rspec"
28
+ s.add_development_dependency "rake"
29
+ s.add_development_dependency "bundler"
30
+ end
@@ -0,0 +1,19 @@
1
+ # Usage: $ EVENTFLIT_KEY=YOURKEY ruby examples/hello_eventflit.rb
2
+
3
+ $:.unshift(File.expand_path("../../lib", __FILE__))
4
+ require 'eventflit-client'
5
+ require 'pp'
6
+
7
+ APP_KEY = ENV['EVENTFLIT_KEY'] # || "YOUR_APPLICATION_KEY"
8
+
9
+ socket = EventflitClient::Socket.new(APP_KEY)
10
+
11
+ # Subscribe to a channel
12
+ socket.subscribe('helloeventflit')
13
+
14
+ # Bind to a channel event
15
+ socket['helloeventflit'].bind('hello') do |data|
16
+ pp data
17
+ end
18
+
19
+ socket.connect
@@ -0,0 +1,22 @@
1
+ # Usage: $ EVENTFLIT_KEY=YOURKEY ruby examples/hello_eventflit_async.rb
2
+
3
+ $:.unshift(File.expand_path("../../lib", __FILE__))
4
+ require 'eventflit-client'
5
+ require 'pp'
6
+
7
+ APP_KEY = ENV['EVENTFLIT_KEY'] # || "YOUR_APPLICATION_KEY"
8
+
9
+ socket = EventflitClient::Socket.new(APP_KEY)
10
+ socket.connect(true)
11
+
12
+ # Subscribe to a channel
13
+ socket.subscribe('helloeventflit')
14
+
15
+ # Bind to a channel event
16
+ socket['helloeventflit'].bind('hello') do |data|
17
+ pp data
18
+ end
19
+
20
+ loop do
21
+ sleep 1
22
+ end
@@ -0,0 +1,19 @@
1
+ # Usage: $ EVENTFLIT_KEY=YOURKEY ruby examples/hello_eventflit_ssl.rb
2
+
3
+ $:.unshift(File.expand_path("../../lib", __FILE__))
4
+ require 'eventflit-client'
5
+ require 'pp'
6
+
7
+ APP_KEY = ENV['EVENTFLIT_KEY'] # || "YOUR_APPLICATION_KEY"
8
+
9
+ socket = EventflitClient::Socket.new(APP_KEY, { :encrypted => true } )
10
+
11
+ # Subscribe to a channel
12
+ socket.subscribe('helloeventflit')
13
+
14
+ # Bind to a channel event
15
+ socket['helloeventflit'].bind('hello') do |data|
16
+ pp data
17
+ end
18
+
19
+ socket.connect
@@ -0,0 +1,20 @@
1
+ # Usage: $ EVENTFLIT_KEY=YOURKEY ruby examples/subscribe_private.rb
2
+
3
+ $:.unshift(File.expand_path("../../lib", __FILE__))
4
+ require 'eventflit-client'
5
+ require 'pp'
6
+
7
+ APP_KEY = ENV['EVENTFLIT_KEY'] # || "YOUR_APPLICATION_KEY"
8
+ APP_SECRET = ENV['EVENTFLIT_SECRET'] # || "YOUR_APPLICATION_SECRET"
9
+
10
+ socket = EventflitClient::Socket.new(APP_KEY, { :encrypted => true, :secret => APP_SECRET } )
11
+
12
+ # Subscribe to a channel
13
+ socket.subscribe('private-helloeventflit')
14
+
15
+ # Bind to a channel event
16
+ socket['helloeventflit'].bind('hello') do |data|
17
+ pp data
18
+ end
19
+
20
+ socket.connect
@@ -0,0 +1,56 @@
1
+ module EventflitClient
2
+
3
+ class Channel
4
+ attr_accessor :global, :subscribed
5
+ attr_reader :name, :callbacks, :user_data
6
+
7
+ def initialize(channel_name, user_data=nil, logger=EventflitClient.logger)
8
+ @name = channel_name
9
+ @user_data = user_data
10
+ @logger = logger
11
+ @global = false
12
+ @callbacks = {}
13
+ @subscribed = false
14
+ end
15
+
16
+ def bind(event_name, &callback)
17
+ EventflitClient.logger.debug "Binding #{event_name} to #{name}"
18
+ @callbacks[event_name] = callbacks[event_name] || []
19
+ @callbacks[event_name] << callback
20
+ return self
21
+ end
22
+
23
+ def dispatch_with_all(event_name, data)
24
+ dispatch(event_name, data)
25
+ end
26
+
27
+ def dispatch(event_name, data)
28
+ logger.debug("Dispatching #{global ? 'global ' : ''}callbacks for #{event_name}")
29
+ if @callbacks[event_name]
30
+ @callbacks[event_name].each do |callback|
31
+ callback.call(data)
32
+ end
33
+ else
34
+ logger.debug "No #{global ? 'global ' : ''}callbacks to dispatch for #{event_name}"
35
+ end
36
+ end
37
+
38
+ def acknowledge_subscription(data)
39
+ @subscribed = true
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :logger
45
+ end
46
+
47
+ class NullChannel
48
+ def initialize(channel_name, *a)
49
+ @name = channel_name
50
+ end
51
+ def method_missing(*a)
52
+ raise ArgumentError, "Channel `#{@name}` hasn't been subscribed yet."
53
+ end
54
+ end
55
+
56
+ end
@@ -0,0 +1,35 @@
1
+ module EventflitClient
2
+ class Channels
3
+
4
+ attr_reader :channels
5
+
6
+ def initialize(logger=EventflitClient.logger)
7
+ @logger = logger
8
+ @channels = {}
9
+ end
10
+
11
+ def add(channel_name, user_data=nil)
12
+ @channels[channel_name] ||= Channel.new(channel_name, user_data, @logger)
13
+ end
14
+
15
+ def find(channel_name)
16
+ @channels[channel_name]
17
+ end
18
+
19
+ def remove(channel_name)
20
+ @channels.delete(channel_name)
21
+ end
22
+
23
+ def empty?
24
+ @channels.empty?
25
+ end
26
+
27
+ def size
28
+ @channels.size
29
+ end
30
+
31
+ alias :<< :add
32
+ alias :[] :find
33
+
34
+ end
35
+ end
@@ -0,0 +1,244 @@
1
+ require 'json'
2
+ require 'openssl'
3
+ require 'digest/md5'
4
+
5
+ module EventflitClient
6
+ class Socket
7
+
8
+ CLIENT_ID = 'eventflit-ruby-client'
9
+ PROTOCOL = '6'
10
+
11
+ attr_reader :path, :connected, :channels, :global_channel, :socket_id
12
+
13
+ def initialize(app_key, options={})
14
+ raise ArgumentError, "Missing app_key" if app_key.to_s.empty?
15
+
16
+ @path = "#{options[:ws_path]}/app/#{app_key}?client=#{CLIENT_ID}&version=#{EventflitClient::VERSION}&protocol=#{PROTOCOL}"
17
+ @key = app_key.to_s
18
+ @secret = options[:secret]
19
+ @socket_id = nil
20
+ @logger = options[:logger] || EventflitClient.logger
21
+ @channels = Channels.new(@logger)
22
+ @global_channel = Channel.new('eventflit_global_channel')
23
+ @global_channel.global = true
24
+ @connected = false
25
+ @encrypted = options[:encrypted] || options[:secure] || false
26
+ # :private_auth_method is deprecated
27
+ @auth_method = options[:auth_method] || options[:private_auth_method]
28
+ @cert_file = options[:cert_file]
29
+ @ws_host = options[:ws_host] || HOST
30
+ @ws_port = options[:ws_port] || WS_PORT
31
+ @wss_port = options[:wss_port] || WSS_PORT
32
+ @ssl_verify = options.fetch(:ssl_verify, true)
33
+
34
+ if @encrypted
35
+ @url = "wss://#{@ws_host}:#{@wss_port}#{@path}"
36
+ else
37
+ @url = "ws://#{@ws_host}:#{@ws_port}#{@path}"
38
+ end
39
+
40
+ bind('eventflit:connection_established') do |data|
41
+ socket = parser(data)
42
+ @connected = true
43
+ @socket_id = socket['socket_id']
44
+ subscribe_all
45
+ end
46
+
47
+ bind('eventflit:connection_disconnected') do |data|
48
+ @connected = false
49
+ @channels.channels.each { |c| c.disconnect }
50
+ end
51
+
52
+ bind('eventflit:error') do |data|
53
+ logger.fatal("Eventflit : error : #{data.inspect}")
54
+ end
55
+
56
+ # Keep this in case we're using a websocket protocol that doesn't
57
+ # implement ping/pong
58
+ bind('eventflit:ping') do
59
+ send_event('eventflit:pong', nil)
60
+ end
61
+ end
62
+
63
+ def connect(async = false)
64
+ return if @connection
65
+ logger.debug("Eventflit : connecting : #{@url}")
66
+
67
+ if async
68
+ @connection_thread = Thread.new do
69
+ begin
70
+ connect_internal
71
+ rescue => ex
72
+ send_local_event "eventflit:error", ex
73
+ end
74
+ end
75
+ else
76
+ connect_internal
77
+ end
78
+ self
79
+ end
80
+
81
+ def disconnect
82
+ return unless @connection
83
+ logger.debug("Eventflit : disconnecting")
84
+ @connected = false
85
+ @connection.close
86
+ @connection = nil
87
+ if @connection_thread
88
+ @connection_thread.kill
89
+ @connection_thread = nil
90
+ end
91
+ end
92
+
93
+ def subscribe(channel_name, user_data = nil)
94
+ if user_data.is_a? Hash
95
+ user_data = user_data.to_json
96
+ elsif user_data
97
+ user_data = {:user_id => user_data}.to_json
98
+ elsif is_presence_channel(channel_name)
99
+ raise ArgumentError, "user_data is required for presence channels"
100
+ end
101
+
102
+ channel = @channels.add(channel_name, user_data)
103
+ if @connected
104
+ authorize(channel, method(:authorize_callback))
105
+ end
106
+ return channel
107
+ end
108
+
109
+ def unsubscribe(channel_name)
110
+ channel = @channels.remove channel_name
111
+ if channel && @connected
112
+ send_event('eventflit:unsubscribe', {
113
+ 'channel' => channel_name
114
+ })
115
+ end
116
+ return channel
117
+ end
118
+
119
+ def bind(event_name, &callback)
120
+ @global_channel.bind(event_name, &callback)
121
+ return self
122
+ end
123
+
124
+ def [](channel_name)
125
+ @channels[channel_name] || NullChannel.new(channel_name)
126
+ end
127
+
128
+ def subscribe_all
129
+ @channels.channels.clone.each { |k,v| subscribe(v.name, v.user_data) }
130
+ end
131
+
132
+ # auth for private and presence
133
+ def authorize(channel, callback)
134
+ if is_private_channel(channel.name)
135
+ auth_data = get_private_auth(channel)
136
+ elsif is_presence_channel(channel.name)
137
+ auth_data = get_presence_auth(channel)
138
+ end
139
+ # could both be nil if didn't require auth
140
+ callback.call(channel, auth_data, channel.user_data)
141
+ end
142
+
143
+ def authorize_callback(channel, auth_data, channel_data)
144
+ send_event('eventflit:subscribe', {
145
+ 'channel' => channel.name,
146
+ 'auth' => auth_data,
147
+ 'channel_data' => channel_data
148
+ })
149
+ channel.acknowledge_subscription(nil)
150
+ end
151
+
152
+ def is_private_channel(channel_name)
153
+ channel_name.match(/^private-/)
154
+ end
155
+
156
+ def is_presence_channel(channel_name)
157
+ channel_name.match(/^presence-/)
158
+ end
159
+
160
+ def get_private_auth(channel)
161
+ return @auth_method.call(@socket_id, channel) if @auth_method
162
+
163
+ string_to_sign = @socket_id + ':' + channel.name
164
+ signature = hmac(@secret, string_to_sign)
165
+ "#{@key}:#{signature}"
166
+ end
167
+
168
+ def get_presence_auth(channel)
169
+ return @auth_method.call(@socket_id, channel) if @auth_method
170
+
171
+ string_to_sign = @socket_id + ':' + channel.name + ':' + channel.user_data
172
+ signature = hmac(@secret, string_to_sign)
173
+ "#{@key}:#{signature}"
174
+ end
175
+
176
+
177
+ # for compatibility with JavaScript client API
178
+ alias :subscribeAll :subscribe_all
179
+
180
+ def send_event(event_name, data)
181
+ payload = {'event' => event_name, 'data' => data}.to_json
182
+ @connection.send(payload)
183
+ logger.debug("Eventflit : sending event : #{payload}")
184
+ end
185
+
186
+ def send_channel_event(channel, event_name, data)
187
+ payload = {'channel' => channel, 'event' => event_name, 'data' => data}.to_json
188
+ @connection.send(payload)
189
+ logger.debug("Eventflit : sending channel event : #{payload}")
190
+ end
191
+
192
+ protected
193
+
194
+ attr_reader :logger
195
+
196
+ def connect_internal
197
+ @connection = EventflitWebSocket.new(@url, {
198
+ :ssl => @encrypted,
199
+ :cert_file => @cert_file,
200
+ :ssl_verify => @ssl_verify
201
+ })
202
+
203
+ logger.debug("Websocket connected")
204
+
205
+ loop do
206
+ @connection.receive.each do |msg|
207
+ params = parser(msg)
208
+
209
+ # why ?
210
+ next if params['socket_id'] && params['socket_id'] == self.socket_id
211
+
212
+ send_local_event(params['event'], params['data'], params['channel'])
213
+ end
214
+ end
215
+ end
216
+
217
+ def send_local_event(event_name, event_data, channel_name=nil)
218
+ if channel_name
219
+ channel = @channels[channel_name]
220
+ if channel
221
+ channel.dispatch_with_all(event_name, event_data)
222
+ end
223
+ end
224
+
225
+ @global_channel.dispatch_with_all(event_name, event_data)
226
+ logger.debug("Eventflit : event received : channel: #{channel_name}; event: #{event_name}")
227
+ end
228
+
229
+ def parser(data)
230
+ return data if data.is_a? Hash
231
+ return JSON.parse(data)
232
+ rescue => err
233
+ logger.warn(err)
234
+ logger.warn("Eventflit : data attribute not valid JSON - you may wish to implement your own Eventflit::Client.parser")
235
+ return data
236
+ end
237
+
238
+ def hmac(secret, string_to_sign)
239
+ digest = OpenSSL::Digest::SHA256.new
240
+ signature = OpenSSL::HMAC.hexdigest(digest, secret, string_to_sign)
241
+ end
242
+ end
243
+
244
+ end
@@ -0,0 +1,3 @@
1
+ module EventflitClient
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,99 @@
1
+ require 'socket'
2
+ require 'websocket'
3
+ require 'openssl'
4
+
5
+ module EventflitClient
6
+ class EventflitWebSocket
7
+ WAIT_EXCEPTIONS = [Errno::EAGAIN, Errno::EWOULDBLOCK]
8
+ WAIT_EXCEPTIONS << IO::WaitReadable if defined?(IO::WaitReadable)
9
+
10
+ CA_FILE = File.expand_path('../../../certs/cacert.pem', __FILE__)
11
+
12
+ attr_accessor :socket
13
+
14
+ def initialize(url, params = {})
15
+ @hs ||= WebSocket::Handshake::Client.new(:url => url)
16
+ @frame ||= WebSocket::Frame::Incoming::Server.new(:version => @hs.version)
17
+ @socket = TCPSocket.new(@hs.host, @hs.port || 80)
18
+ @cert_file = params[:cert_file]
19
+ @logger = params[:logger] || EventflitClient.logger
20
+
21
+ if params[:ssl] == true
22
+ ctx = OpenSSL::SSL::SSLContext.new
23
+ if params[:ssl_verify]
24
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
25
+ # http://curl.haxx.se/ca/cacert.pem
26
+ ctx.ca_file = @cert_file || CA_FILE
27
+ else
28
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
29
+ end
30
+
31
+ ssl_sock = OpenSSL::SSL::SSLSocket.new(@socket, ctx)
32
+ ssl_sock.sync_close = true
33
+ ssl_sock.connect
34
+
35
+ @socket = ssl_sock
36
+ end
37
+
38
+ @socket.write(@hs.to_s)
39
+ @socket.flush
40
+
41
+ loop do
42
+ data = @socket.getc
43
+ next if data.nil?
44
+
45
+ @hs << data
46
+
47
+ if @hs.finished?
48
+ raise @hs.error.to_s unless @hs.valid?
49
+ @handshaked = true
50
+ break
51
+ end
52
+ end
53
+ end
54
+
55
+ def send(data, type = :text)
56
+ raise "no handshake!" unless @handshaked
57
+
58
+ data = WebSocket::Frame::Outgoing::Client.new(
59
+ :version => @hs.version,
60
+ :data => data,
61
+ :type => type
62
+ ).to_s
63
+ @socket.write data
64
+ @socket.flush
65
+ end
66
+
67
+ def receive
68
+ raise "no handshake!" unless @handshaked
69
+
70
+ begin
71
+ data = @socket.read_nonblock(1024)
72
+ rescue *WAIT_EXCEPTIONS
73
+ IO.select([@socket])
74
+ retry
75
+ end
76
+ @frame << data
77
+
78
+ messages = []
79
+ while message = @frame.next
80
+ if message.type === :ping
81
+ send(message.data, :pong)
82
+ return messages
83
+ end
84
+ messages << message.to_s
85
+ end
86
+ messages
87
+ end
88
+
89
+ def close
90
+ @socket.close
91
+ rescue IOError => error
92
+ logger.debug error.message
93
+ end
94
+
95
+ private
96
+
97
+ attr_reader :logger
98
+ end
99
+ end
@@ -0,0 +1,22 @@
1
+ module EventflitClient
2
+ HOST = 'service.eventflit.com'
3
+ WS_PORT = 80
4
+ WSS_PORT = 443
5
+
6
+ def self.logger
7
+ @logger ||= begin
8
+ require 'logger'
9
+ Logger.new(STDOUT)
10
+ end
11
+ end
12
+
13
+ def self.logger=(logger)
14
+ @logger = logger
15
+ end
16
+ end
17
+
18
+ require 'eventflit-client/version'
19
+ require 'eventflit-client/websocket'
20
+ require 'eventflit-client/socket'
21
+ require 'eventflit-client/channel'
22
+ require 'eventflit-client/channels'