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.
- data/bin/slanger +68 -0
- data/lib/slanger/api_server.rb +42 -0
- data/lib/slanger/channel.rb +25 -0
- data/lib/slanger/config.rb +24 -0
- data/lib/slanger/handler.rb +113 -0
- data/lib/slanger/logger.rb +7 -0
- data/lib/slanger/presence_channel.rb +120 -0
- data/lib/slanger/redis.rb +39 -0
- data/lib/slanger/service.rb +19 -0
- data/lib/slanger/web_socket_server.rb +20 -0
- data/slanger.rb +16 -0
- metadata +210 -0
data/bin/slanger
ADDED
@@ -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,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
|
data/slanger.rb
ADDED
@@ -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: []
|