slanger 0.1.0

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

Potentially problematic release.


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

@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby -Ku
2
+
3
+ require 'optparse'
4
+ require 'bundler/setup'
5
+ require 'eventmachine'
6
+
7
+ options = {
8
+ api_host: '0.0.0.0', api_port: '4567', websocket_host: '0.0.0.0',
9
+ websocket_port: '8080', debug: false, redis_address: 'redis://0.0.0.0:6379/0'
10
+ }
11
+
12
+ OptionParser.new do |opts|
13
+ opts.on '-h', '--help', 'Display this screen' do
14
+ exit
15
+ end
16
+
17
+ opts.on '-k', '--app_key APP_KEY', "Pusher application key" do |k|
18
+ options[:app_key] = k
19
+ end
20
+
21
+ opts.on '-s', '--secret SECRET', "Pusher application secret" do |k|
22
+ options[:secret] = k
23
+ end
24
+
25
+ opts.on '-r', '--redis_address URL', "Address to bind to (Default: redis://127.0.0.1:6379/0)" do |h|
26
+ options[:redis_address] = h
27
+ end
28
+
29
+ opts.on '-a', '--api_host HOST', "API service address (Default: 0.0.0.0:4567)" do |p|
30
+ options[:api_host], options[:api_port] = p.split(':')
31
+ end
32
+
33
+ opts.on '-w', '--websocket_host HOST', "WebSocket service address (Default: 0.0.0.0:8080)" do |p|
34
+ options[:websocket_host], options[:websocket_port] = p.split(':')
35
+ end
36
+
37
+ opts.on "-v", "--[no-]verbose", "Run verbosely" do |v|
38
+ options[:debug] = v
39
+ end
40
+ end.parse!
41
+
42
+ %w<app_key secret>.each do |parameter|
43
+ raise RuntimeError.new "--#{parameter} STRING is a required argument. Use your Pusher #{parameter}." unless options[parameter.to_sym]
44
+ end
45
+
46
+ EM.run do
47
+ File.tap { |f| require f.expand_path(f.join(f.dirname(__FILE__),'..', 'slanger.rb')) }
48
+ Slanger::Config.load options
49
+ Slanger::Service.run
50
+
51
+ puts "\x1b[2J\x1b[H"
52
+ puts "\n"
53
+ puts " .d8888b. 888 "
54
+ puts " d88P Y88b 888 "
55
+ puts " Y88b. 888 "
56
+ puts ' "Y888b. 888 8888b. 88888b. .d88b. .d88b. 888d888 '
57
+ puts ' "Y88b. 888 "88b 888 "88b d88P"88b d8P Y8b 888P" '
58
+ puts ' "888 888 .d888888 888 888 888 888 88888888 888 '
59
+ puts " Y88b d88P 888 888 888 888 888 Y88b 888 Y8b. 888 "
60
+ puts ' "Y8888P" 888 "Y888888 888 888 "Y88888 "Y8888 888 '
61
+ puts " 888 "
62
+ puts " Y8b d88P "
63
+ puts ' "Y88P" '
64
+ puts "\n" * 2
65
+
66
+ puts "Slanger API server listening on port #{options[:api_port]}"
67
+ puts "Slanger WebSocket server listening on port #{options[:websocket_port]}"
68
+ end
@@ -0,0 +1,42 @@
1
+ require 'sinatra/base'
2
+ require 'signature'
3
+ require 'active_support/json'
4
+ require 'active_support/core_ext/hash'
5
+ require 'eventmachine'
6
+ require 'em-hiredis'
7
+ require 'rack'
8
+ require 'fiber'
9
+ require 'rack/fiber_pool'
10
+
11
+ module Slanger
12
+ class ApiServer < Sinatra::Base
13
+ use Rack::FiberPool
14
+ set :raise_errors, lambda { false }
15
+ set :show_exceptions, false
16
+
17
+ # Respond with HTTP 401 Unauthorized if request cannot be authenticated.
18
+ error(Signature::AuthenticationError) { |c| halt 401, "401 UNAUTHORIZED\n" }
19
+
20
+ post '/apps/:app_id/channels/:channel_id/events' do
21
+ # authenticate request. exclude 'channel_id' and 'app_id', these are added the the params
22
+ # by the pusher client lib after computing HMAC
23
+ Signature::Request.new('POST', env['PATH_INFO'], params.except('channel_id', 'app_id')).
24
+ authenticate { |key| Signature::Token.new key, Slanger::Config.secret }
25
+
26
+ f = Fiber.current
27
+ Slanger::Redis.publish(params[:channel_id], payload).tap do |r|
28
+ r.callback { f.resume [202, {}, "202 ACCEPTED\n"] }
29
+ r.errback { f.resume [500, {}, "500 INTERNAL SERVER ERROR\n"] }
30
+ end
31
+ Fiber.yield
32
+ end
33
+
34
+ def payload
35
+ payload = {
36
+ event: params['name'], data: request.body.read, channel: params[:channel_id], socket_id: params[:socket_id]
37
+ }
38
+ Hash[payload.reject { |k,v| v.nil? }].to_json
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,25 @@
1
+ require 'glamazon'
2
+ require 'eventmachine'
3
+ require 'forwardable'
4
+
5
+ module Slanger
6
+ class Channel
7
+ include Glamazon::Base
8
+ extend Forwardable
9
+
10
+ def_delegators :channel, :subscribe, :unsubscribe, :push
11
+
12
+ def initialize(attrs)
13
+ super
14
+ Slanger::Redis.subscribe channel_id
15
+ end
16
+
17
+ def channel
18
+ @channel ||= EM::Channel.new
19
+ end
20
+
21
+ def dispatch(message, channel)
22
+ push(message.to_json) unless channel =~ /^slanger:/
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Slanger
2
+ module Config
3
+ def load(opts={})
4
+ options.update opts
5
+ end
6
+
7
+ def [](key)
8
+ options[key]
9
+ end
10
+
11
+ def options
12
+ @options ||= {
13
+ api_host: '0.0.0.0', api_port: '4567', websocket_host: '0.0.0.0',
14
+ websocket_port: '8080', debug: false, redis_address: 'redis://0.0.0.0:6379/0'
15
+ }
16
+ end
17
+
18
+ def method_missing(meth, *args, &blk)
19
+ @options[meth]
20
+ end
21
+
22
+ extend self
23
+ end
24
+ end
@@ -0,0 +1,113 @@
1
+ require 'active_support/json'
2
+ require 'active_support/core_ext/hash'
3
+ require 'securerandom'
4
+ require 'signature'
5
+ require 'fiber'
6
+
7
+ module Slanger
8
+ class Handler
9
+ def initialize(socket)
10
+ @socket = socket
11
+ authenticate
12
+ end
13
+
14
+ # Dispatches message handling to method with same name as the event name
15
+ def onmessage(msg)
16
+ msg = JSON.parse msg
17
+ send msg['event'].gsub('pusher:', 'pusher_'), msg
18
+ end
19
+
20
+ # Unsubscribe this connection from the channel
21
+ def onclose
22
+ const = @channel_id =~ /^presence-/ ? 'PresenceChannel' : 'Channel'
23
+ channel = Slanger.const_get(const).find_by_channel_id(@channel_id)
24
+ channel.try :unsubscribe, @subscription_id
25
+ end
26
+
27
+ private
28
+
29
+ # Verify app key. Send connection_established message to connection if it checks out. Send error message and disconnect if invalid.
30
+ def authenticate
31
+ app_key = @socket.request['path'].split(/\W/)[2]
32
+ if app_key == Slanger::Config.app_key
33
+ @socket_id = SecureRandom.uuid
34
+ @socket.send(payload 'pusher:connection_established', { socket_id: @socket_id })
35
+ else
36
+ @socket.send(payload 'pusher:error', { code: '4001', message: "Could not find app by key #{app_key}" })
37
+ @socket.close_websocket
38
+ end
39
+ end
40
+
41
+ # Dispatch to handler method if channel requires authentication, otherwise subscribe.
42
+ def pusher_subscribe(msg)
43
+ @channel_id = msg['data']['channel']
44
+ if match = @channel_id.match(/^((private)|(presence))-/)
45
+ send "handle_#{match.captures[0]}_subscription", msg
46
+ else
47
+ subscribe_channel
48
+ end
49
+ end
50
+
51
+ # Add connection to channel subscribers
52
+ def subscribe_channel
53
+ channel = Slanger::Channel.find_or_create_by_channel_id(@channel_id)
54
+ @subscription_id = channel.subscribe do |msg|
55
+ msg = JSON.parse(msg)
56
+ socket_id = msg.delete 'socket_id'
57
+ @socket.send msg.to_json unless socket_id == @socket_id
58
+ end
59
+ end
60
+
61
+ # Validate authentication token for private channel and add connection to channel subscribers if it checks out
62
+ def handle_private_subscription(msg)
63
+ if msg['data']['auth'] && token(msg['data']['channel_data']) != msg['data']['auth'].split(':')[1]
64
+ @socket.send(payload 'pusher:error', {
65
+ message: "Invalid signature: Expected HMAC SHA256 hex digest of #{@socket_id}:#{msg['data']['channel']}, but got #{msg['data']['auth']}"
66
+ })
67
+ else
68
+ subscribe_channel
69
+ end
70
+ end
71
+
72
+ # Validate authentication token and check channel_data. Add connection to channel subscribers if it checks out
73
+ def handle_presence_subscription(msg)
74
+ if token(msg['data']['channel_data']) != msg['data']['auth'].split(':')[1]
75
+ @socket.send(payload 'pusher:error', {
76
+ message: "Invalid signature: Expected HMAC SHA256 hex digest of #{@socket_id}:#{msg['data']['channel']}, but got #{msg['data']['auth']}"
77
+ })
78
+ elsif !msg['data']['channel_data']
79
+ @socket.send(payload 'pusher:error', {
80
+ message: "presence-channel is a presence channel and subscription must include channel_data"
81
+ })
82
+ else
83
+ channel = Slanger::PresenceChannel.find_or_create_by_channel_id(@channel_id)
84
+ callback = Proc.new {
85
+ @socket.send(payload 'pusher_internal:subscription_succeeded', {
86
+ presence: {
87
+ count: channel.subscribers.size,
88
+ ids: channel.ids,
89
+ hash: channel.subscribers
90
+ }
91
+ })
92
+ }
93
+ @subscription_id = channel.subscribe(msg, callback) do |msg|
94
+ msg = JSON.parse(msg)
95
+ socket_id = msg.delete 'socket_id'
96
+ @socket.send msg.to_json unless socket_id == @socket_id
97
+ end
98
+
99
+ end
100
+ end
101
+
102
+ # Message helper method. Converts a hash into the Pusher JSON protocol
103
+ def payload(event_name, payload = {})
104
+ { channel: @channel_id, event: event_name, data: payload }.to_json
105
+ end
106
+
107
+ # HMAC token validation
108
+ def token(params=nil)
109
+ string_to_sign = [@socket_id, @channel_id, params].compact.join ':'
110
+ HMAC::SHA256.hexdigest(Slanger::Config.secret, string_to_sign)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,7 @@
1
+ module Slanger
2
+ module Logger
3
+ def log(msg)
4
+ end
5
+ extend self
6
+ end
7
+ end
@@ -0,0 +1,120 @@
1
+ require 'glamazon'
2
+ require 'eventmachine'
3
+ require 'forwardable'
4
+ require 'fiber'
5
+
6
+ module Slanger
7
+ class PresenceChannel < Channel
8
+ def_delegators :channel, :push
9
+
10
+ def dispatch(message, channel)
11
+ if channel =~ /^slanger:/
12
+ update_subscribers message
13
+ else
14
+ push message.to_json
15
+ end
16
+ end
17
+
18
+ def initialize(attrs)
19
+ super
20
+ Slanger::Redis.subscribe 'slanger:connection_notification'
21
+ end
22
+
23
+ def subscribe(msg, callback, &blk)
24
+ channel_data = JSON.parse msg['data']['channel_data']
25
+ public_subscription_id = SecureRandom.uuid
26
+
27
+ publisher = publish_connection_notification subscription_id: public_subscription_id, online: true,
28
+ channel_data: channel_data, channel: channel_id
29
+
30
+ roster_add public_subscription_id, channel_data
31
+
32
+ # fuuuuuuuuuccccccck!
33
+ publisher.callback do
34
+ EM.next_tick do
35
+ callback.call
36
+ internal_subscription_table[public_subscription_id] = channel.subscribe &blk
37
+ end
38
+ end
39
+
40
+ public_subscription_id
41
+ end
42
+
43
+ def ids
44
+ subscriptions.map { |k,v| v['user_id'] }
45
+ end
46
+
47
+ def subscribers
48
+ Hash[subscriptions.map { |k,v| [v['user_id'], v['user_info']] }]
49
+ end
50
+
51
+ def unsubscribe(public_subscription_id)
52
+ # Unsubcribe from EM::Channel
53
+ channel.unsubscribe(internal_subscription_table.delete(public_subscription_id)) # if internal_subscription_table[public_subscription_id]
54
+ roster_remove public_subscription_id
55
+ # Notify all instances
56
+ publish_connection_notification subscription_id: public_subscription_id, online: false, channel: channel_id
57
+ end
58
+
59
+ private
60
+
61
+ def get_roster
62
+ Fiber.new do
63
+ f = Fiber.current
64
+ Slanger::Redis.hgetall(channel_id).
65
+ callback { |res| f.resume res }
66
+ Fiber.yield
67
+ end.resume
68
+ end
69
+
70
+ def roster_add(key, value)
71
+ Slanger::Redis.hset(channel_id, key, value)
72
+ end
73
+
74
+ def roster_remove(key)
75
+ Slanger::Redis.hdel(channel_id, key)
76
+ end
77
+
78
+ def publish_connection_notification(payload, retry_count=0)
79
+ Slanger::Redis.publish('slanger:connection_notification', payload.to_json).
80
+ tap { |r| r.errback { publish_connection_notification payload, retry_count.succ unless retry_count == 5 } }
81
+ end
82
+
83
+ # This is the state of the presence channel across the system. kept in sync
84
+ # with redis pubsub
85
+ def subscriptions
86
+ @subscriptions = @subscriptions || get_roster || Hash.new
87
+ end
88
+
89
+ # This is used map public subscription ids to em channel subscription ids.
90
+ # em channel subscription ids are incremented integers, so they cannot
91
+ # be used as keys in distributed system because they will not be unique
92
+ def internal_subscription_table
93
+ @internal_subscription_table ||= {}
94
+ end
95
+
96
+ def update_subscribers(message)
97
+ if message['online']
98
+ # Don't tell the channel subscriptions a new member has been added if the subscriber data
99
+ # is already present in the subscriptions hash, i.e. multiple browser windows open.
100
+ unless subscriptions.has_value? message['channel_data']
101
+ push payload('pusher_internal:member_added', message['channel_data'])
102
+ end
103
+ subscriptions[message['subscription_id']] = message['channel_data']
104
+ else
105
+ # Don't tell the channel subscriptions the member has been removed if the subscriber data
106
+ # still remains in the subscriptions hash, i.e. multiple browser windows open.
107
+ subscriber = subscriptions.delete message['subscription_id']
108
+ unless subscriptions.has_value? subscriber
109
+ push payload('pusher_internal:member_removed', {
110
+ user_id: subscriber['user_id']
111
+ })
112
+ end
113
+ end
114
+ end
115
+
116
+ def payload(event_name, payload = {})
117
+ { channel: channel_id, event: event_name, data: payload }.to_json
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,39 @@
1
+ require 'forwardable'
2
+
3
+ module Slanger
4
+ module Redis
5
+ extend Forwardable
6
+
7
+ def self.extended base
8
+ base.on(:message) do |channel, message|
9
+ message = JSON.parse message
10
+ const = message['channel'] =~ /^presence-/ ? 'PresenceChannel' : 'Channel'
11
+ Slanger.const_get(const).find_or_create_by_channel_id(message['channel']).dispatch message, channel
12
+ end
13
+ end
14
+
15
+ def_delegator :publisher, :publish
16
+ def_delegators :subscriber, :on, :subscribe
17
+ def_delegators :regular_connection, :hgetall, :hdel, :hset
18
+
19
+ private
20
+
21
+ def regular_connection
22
+ @regular_connection ||= new_connection
23
+ end
24
+
25
+ def publisher
26
+ @publisher ||= new_connection
27
+ end
28
+
29
+ def subscriber
30
+ @subscriber ||= new_connection
31
+ end
32
+
33
+ def new_connection
34
+ EM::Hiredis.connect Slanger::Config.redis_address
35
+ end
36
+
37
+ extend self
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ require 'thin'
2
+ require 'rack'
3
+
4
+ module Slanger
5
+ module Service
6
+ def run
7
+ Thin::Logging.silent = true
8
+ Rack::Handler::Thin.run Slanger::ApiServer, Host: Slanger::Config.api_host, Port: Slanger::Config.api_port
9
+ Slanger::WebSocketServer.run
10
+ end
11
+
12
+ def stop
13
+ EM.stop if EM.reactor_running?
14
+ end
15
+
16
+ extend self
17
+ Signal.trap('HUP') { Slanger::Service.stop }
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require 'eventmachine'
2
+ require 'em-websocket'
3
+
4
+ module Slanger
5
+ module WebSocketServer
6
+ def run
7
+ EM.run do
8
+ EM::WebSocket.start host: Slanger::Config[:websocket_host], port: Slanger::Config[:websocket_port], debug: Slanger::Config[:debug], app_key: Slanger::Config[:app_key] do |ws|
9
+ # Keep track of handler instance in instance of EM::Connection to ensure a unique handler instance is used per connection.
10
+ ws.class_eval { attr_accessor :connection_handler }
11
+ # Delegate connection management to handler instance.
12
+ ws.onopen { ws.connection_handler = Slanger::Handler.new ws }
13
+ ws.onmessage { |msg| ws.connection_handler.onmessage msg }
14
+ ws.onclose { ws.connection_handler.onclose }
15
+ end
16
+ end
17
+ end
18
+ extend self
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'eventmachine'
4
+ require 'em-hiredis'
5
+ require 'rack'
6
+ require 'active_support/core_ext/string'
7
+
8
+ module Slanger; end
9
+
10
+ EM.run do
11
+ File.tap do |f|
12
+ Dir[f.expand_path(f.join(f.dirname(__FILE__),'lib', 'slanger', '*.rb'))].each do |file|
13
+ Slanger.autoload File.basename(file, '.rb').classify, file
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slanger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stevie Graham
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-12 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &70311444305760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.12.10
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70311444305760
25
+ - !ruby/object:Gem::Dependency
26
+ name: em-hiredis
27
+ requirement: &70311444305300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70311444305300
36
+ - !ruby/object:Gem::Dependency
37
+ name: em-websocket
38
+ requirement: &70311444304840 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 0.3.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70311444304840
47
+ - !ruby/object:Gem::Dependency
48
+ name: rack
49
+ requirement: &70311444304380 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.3.3
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70311444304380
58
+ - !ruby/object:Gem::Dependency
59
+ name: rack-fiber_pool
60
+ requirement: &70311444303920 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - =
64
+ - !ruby/object:Gem::Version
65
+ version: 0.9.1
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70311444303920
69
+ - !ruby/object:Gem::Dependency
70
+ name: signature
71
+ requirement: &70311444303460 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: 0.1.2
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *70311444303460
80
+ - !ruby/object:Gem::Dependency
81
+ name: activesupport
82
+ requirement: &70311444303000 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: 3.1.0
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *70311444303000
91
+ - !ruby/object:Gem::Dependency
92
+ name: glamazon
93
+ requirement: &70311444302540 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: 0.3.1
99
+ type: :runtime
100
+ prerelease: false
101
+ version_requirements: *70311444302540
102
+ - !ruby/object:Gem::Dependency
103
+ name: sinatra
104
+ requirement: &70311444328700 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 1.2.6
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: *70311444328700
113
+ - !ruby/object:Gem::Dependency
114
+ name: thin
115
+ requirement: &70311444328240 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ~>
119
+ - !ruby/object:Gem::Version
120
+ version: 1.2.11
121
+ type: :runtime
122
+ prerelease: false
123
+ version_requirements: *70311444328240
124
+ - !ruby/object:Gem::Dependency
125
+ name: em-http-request
126
+ requirement: &70311444327780 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: 0.3.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: *70311444327780
135
+ - !ruby/object:Gem::Dependency
136
+ name: rspec
137
+ requirement: &70311444327320 !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ~>
141
+ - !ruby/object:Gem::Version
142
+ version: 2.6.0
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: *70311444327320
146
+ - !ruby/object:Gem::Dependency
147
+ name: pusher
148
+ requirement: &70311444326860 !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ~>
152
+ - !ruby/object:Gem::Version
153
+ version: 0.8.2
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: *70311444326860
157
+ - !ruby/object:Gem::Dependency
158
+ name: haml
159
+ requirement: &70311444326400 !ruby/object:Gem::Requirement
160
+ none: false
161
+ requirements:
162
+ - - ~>
163
+ - !ruby/object:Gem::Version
164
+ version: 3.1.2
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: *70311444326400
168
+ description: A websocket service compatible with Pusher libraries
169
+ email: sjtgraham@mac.com
170
+ executables:
171
+ - slanger
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - lib/slanger/api_server.rb
176
+ - lib/slanger/channel.rb
177
+ - lib/slanger/config.rb
178
+ - lib/slanger/handler.rb
179
+ - lib/slanger/logger.rb
180
+ - lib/slanger/presence_channel.rb
181
+ - lib/slanger/redis.rb
182
+ - lib/slanger/service.rb
183
+ - lib/slanger/web_socket_server.rb
184
+ - slanger.rb
185
+ - bin/slanger
186
+ homepage: http://github.com/stevegraham/pusher
187
+ licenses: []
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - .
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: 1.9.2
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ none: false
200
+ requirements:
201
+ - - ! '>='
202
+ - !ruby/object:Gem::Version
203
+ version: '0'
204
+ requirements: []
205
+ rubyforge_project:
206
+ rubygems_version: 1.8.6
207
+ signing_key:
208
+ specification_version: 3
209
+ summary: A websocket service compatible with Pusher libraries
210
+ test_files: []