slanger 0.1.1 → 0.2.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/README.md CHANGED
@@ -4,7 +4,7 @@ Slanger is an open source server implementation of the Pusher protocol written i
4
4
 
5
5
  Presence channel state is shared using Redis. Channels are lazily instantiated internally within a given Slanger node when the first subscriber connects. When a presence channel is instantiated within a Slanger node, it queries Redis for the global state across all nodes within the system for that channel, and then copies that state internally. Afterwards, when subscribers connect or disconnect the node publishes a presence message to all interested nodes, i.e. all nodes with at least one subscriber interested in the given channel.
6
6
 
7
- Slanger is smart enough to know if a new channel subscription belongs to the same user. It will not send presence messages to subscribers in this case. This happens when the user has multiple browser tabs open for example. Using a chat room backed by presence channels as a real example, one would not want "Micheil" to show up N times in the presence roster because Micheil is a retard and has the chat room open in N browser tabs.
7
+ Slanger is smart enough to know if a new channel subscription belongs to the same user. It will not send presence messages to subscribers in this case. This happens when the user has multiple browser tabs open for example. Using a chat room backed by presence channels as a real example, one would not want "Barrington" to show up N times in the presence roster because Barrington is a retard and has the chat room open in N browser tabs.
8
8
 
9
9
  Slanger was designed to be highly available and partition tolerant with eventual consistency, which in practise is instantaneous.
10
10
 
@@ -12,7 +12,7 @@ Slanger was designed to be highly available and partition tolerant with eventual
12
12
 
13
13
  ## Requirements
14
14
 
15
- - Ruby 1.9
15
+ - Ruby 1.9.2-p290 or greater
16
16
  - Redis
17
17
 
18
18
  ## Starting the service
@@ -24,7 +24,7 @@ __IMPORTANT:__ Redis must be running where Slanger expects it to be (either on l
24
24
  <pre>
25
25
  $ gem install slanger
26
26
 
27
- $ redis-server 1>2 &
27
+ $ redis-server &> /dev/null &
28
28
 
29
29
  $ slanger --app_key 765ec374ae0a69f4ce44 --secret your-pusher-secret
30
30
  </pre>
@@ -81,11 +81,39 @@ You will also need to do the same to the Pusher JavaScript client in your client
81
81
 
82
82
  Of course you could proxy all requests to `ws.example.com` to port 8080 of your Slanger node and `api.example.com` to port 4567 of your Slanger node for example, that way you would only need to set the host property of the Pusher client.
83
83
 
84
+ # Configuration Options
85
+
86
+ Slanger supports several configuration options, which can be supplied as command line arguments at invocation.
87
+
88
+ <pre>
89
+ -k or --app_key This is the Pusher app key you want to use. This is a required argument
90
+
91
+ -s or --secret This is your Pusher secret. This is a required argument
92
+
93
+ -r or --redis_address An address where there is a Redis server running. This is an optional argument and defaults to redis://127.0.0.1:6379/0
94
+
95
+ -a or --api_host This is the address that Slanger will bind the HTTP REST API part of the service to. This is an optional argument and defaults to 0.0.0.0:4567
96
+
97
+ -w or --websocket_host This is the address that Slanger will bind the WebSocket part of the service to. This is an optional argument and defaults to 0.0.0.0:8080
98
+
99
+ -v or --[no-]verbose This makes Slanger run verbosely, meaning WebSocket frames will be echoed to STDOUT. Useful for debugging
100
+ </pre>
101
+
102
+
84
103
  # Why use Slanger instead of Pusher?
85
104
 
86
- There are many reasons you might want to use Slanger instead of Pusher, e.g.
105
+ There a few reasons you might want to use Slanger instead of Pusher, e.g.
87
106
 
88
107
  - You operate in a heavily regulated industry and are worried about sending data to 3rd parties, and it is an organisational requirement that you own your own infrastructure.
89
- - You might be travelling on an airplane without internet connctivity as I am right now. Airplane rides are very good times to get a lot done, unfortunately external services are also usually unreachable. Remove internet connectivity as a dependendancy of your development envirionment by running a local Slanger instance in development and Pusher in production.
108
+ - You might be travelling on an airplane without internet connectivity as I am right now. Airplane rides are very good times to get a lot done, unfortunately external services are also usually unreachable. Remove internet connectivity as a dependency of your development envirionment by running a local Slanger instance in development and Pusher in production.
109
+ - Remove the network dependency from your test suite.
90
110
  - You want to extend the Pusher protocol or have some special requirement. If this applies to you, chances are you are out of luck as Pusher is unlikely to implement something to suit your special use case, and rightly so. With Slanger you are free to modify and extend its behavior anyway that suits your purpose.
91
111
 
112
+ # Why did you write Slanger
113
+
114
+ I wanted to write a non-trivial evented app. I also want to write a book on evented programming in Ruby as I feel there is scant good information available on the topic and this project is handy to show publishers.
115
+
116
+ Pusher is an awesome service, very reasonably priced, and run by an awesome crew. Give them a spin on your next project.
117
+
118
+ &copy; 2011 a Stevie Graham joint.
119
+
data/bin/slanger CHANGED
@@ -34,6 +34,16 @@ OptionParser.new do |opts|
34
34
  options[:websocket_host], options[:websocket_port] = p.split(':')
35
35
  end
36
36
 
37
+ opts.on '-p', '--private_key_file FILE', "Private key file for SSL transport" do |p|
38
+ options[:tls_options] ||= {}
39
+ options[:tls_options][:private_key_file] = p
40
+ end
41
+
42
+ opts.on '-c', '--cert_chain_file FILE', "Certificate chain file for SSL transport" do |p|
43
+ options[:tls_options] ||= {}
44
+ options[:tls_options][:cert_chain_file] = p
45
+ end
46
+
37
47
  opts.on "-v", "--[no-]verbose", "Run verbosely" do |v|
38
48
  options[:debug] = v
39
49
  end
@@ -43,6 +53,13 @@ end.parse!
43
53
  raise RuntimeError.new "--#{parameter} STRING is a required argument. Use your Pusher #{parameter}." unless options[parameter.to_sym]
44
54
  end
45
55
 
56
+ if options[:tls_options]
57
+ [:cert_chain_file, :private_key_file].each do |param|
58
+ raise RuntimeError.new "--#{param} does not exist at `#{options[:tls_options][param]}`" unless File.exists? options[:tls_options][param]
59
+ raise RuntimeError.new "Both --cert_chain_file and --private_key_file need to be specified" unless options[:tls_options][param]
60
+ end
61
+ end
62
+
46
63
  EM.run do
47
64
  File.tap { |f| require f.expand_path(f.join(f.dirname(__FILE__),'..', 'slanger.rb')) }
48
65
  Slanger::Config.load options
@@ -18,12 +18,14 @@ module Slanger
18
18
  error(Signature::AuthenticationError) { |c| halt 401, "401 UNAUTHORIZED\n" }
19
19
 
20
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
21
+ # authenticate request. exclude 'channel_id' and 'app_id' included by sinatra but not sent by Pusher.
22
+ # Raises Signature::AuthenticationError if request does not authenticate.
23
23
  Signature::Request.new('POST', env['PATH_INFO'], params.except('channel_id', 'app_id')).
24
24
  authenticate { |key| Signature::Token.new key, Slanger::Config.secret }
25
25
 
26
26
  f = Fiber.current
27
+ # Publish the event in Redis and translate the result into an HTTP
28
+ # status to return to the client.
27
29
  Slanger::Redis.publish(params[:channel_id], payload).tap do |r|
28
30
  r.callback { f.resume [202, {}, "202 ACCEPTED\n"] }
29
31
  r.errback { f.resume [500, {}, "500 INTERNAL SERVER ERROR\n"] }
@@ -1,3 +1,10 @@
1
+ # Channel class.
2
+ #
3
+ # Uses an EventMachine channel to let clients interact with the
4
+ # Pusher channel. Relay events received from Redis into the
5
+ # EM channel.
6
+ #
7
+
1
8
  require 'glamazon'
2
9
  require 'eventmachine'
3
10
  require 'forwardable'
@@ -18,8 +25,21 @@ module Slanger
18
25
  @channel ||= EM::Channel.new
19
26
  end
20
27
 
28
+ # Send a client event to the EventMachine channel.
29
+ # Only events to channels requiring authentication (private or presence)
30
+ # are accepted. Public channels only get events from the API.
31
+ def send_client_message(message)
32
+ push message.to_json if authenticated?
33
+ end
34
+
35
+ # Send an event received from Redis to the EventMachine channel
36
+ # which will send it to subscribed clients.
21
37
  def dispatch(message, channel)
22
38
  push(message.to_json) unless channel =~ /^slanger:/
23
39
  end
40
+
41
+ def authenticated?
42
+ channel_id =~ /^private-/ || channel_id =~ /^presence-/
43
+ end
24
44
  end
25
45
  end
@@ -1,3 +1,5 @@
1
+ # Config singleton holding the configuration.
2
+
1
3
  module Slanger
2
4
  module Config
3
5
  def load(opts={})
@@ -1,3 +1,6 @@
1
+ # Handler class.
2
+ # Handles a client connected via a websocket connection.
3
+
1
4
  require 'active_support/json'
2
5
  require 'active_support/core_ext/hash'
3
6
  require 'securerandom'
@@ -7,52 +10,86 @@ require 'fiber'
7
10
  module Slanger
8
11
  class Handler
9
12
  def initialize(socket)
10
- @socket = socket
13
+ @socket = socket
14
+ @subscriptions = {}
11
15
  authenticate
12
16
  end
13
17
 
14
18
  # Dispatches message handling to method with same name as the event name
15
19
  def onmessage(msg)
16
- msg = JSON.parse msg
17
- send msg['event'].gsub('pusher:', 'pusher_'), msg
20
+ msg = JSON.parse msg
21
+ event = msg['event'].gsub('pusher:', 'pusher_')
22
+
23
+ if event =~ /^pusher_/
24
+ # Pusher event, call method if it exists.
25
+ send(event, msg) if respond_to? event, true
26
+ elsif event =~ /^client-/
27
+ # Client event. Send it to the destination channel.
28
+ msg['socket_id'] = @socket_id
29
+ channel = find_channel msg['channel']
30
+ channel.try :send_client_message, msg
31
+ end
32
+ rescue Exception => e
33
+ handle_error(e)
18
34
  end
19
35
 
20
- # Unsubscribe this connection from the channel
36
+ # Unsubscribe this connection from all the channels on close.
21
37
  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
38
+ @subscriptions.each do |channel_id, subscription_id|
39
+ channel = find_channel channel_id
40
+ channel.try :unsubscribe, subscription_id
41
+ end
25
42
  end
26
43
 
27
44
  private
28
45
 
46
+ def find_channel(channel_id)
47
+ channel_class(channel_id).find_by_channel_id(channel_id)
48
+ end
49
+
50
+ def channel_class(channel_name)
51
+ channel_name =~ /^presence-/ ? PresenceChannel : Channel
52
+ end
53
+
29
54
  # Verify app key. Send connection_established message to connection if it checks out. Send error message and disconnect if invalid.
30
55
  def authenticate
31
56
  app_key = @socket.request['path'].split(/\W/)[2]
32
57
  if app_key == Slanger::Config.app_key
33
58
  @socket_id = SecureRandom.uuid
34
- @socket.send(payload 'pusher:connection_established', { socket_id: @socket_id })
59
+ @socket.send(payload nil, 'pusher:connection_established', { socket_id: @socket_id })
35
60
  else
36
- @socket.send(payload 'pusher:error', { code: '4001', message: "Could not find app by key #{app_key}" })
61
+ @socket.send(payload nil, 'pusher:error', { code: '4001', message: "Could not find app by key #{app_key}" })
37
62
  @socket.close_websocket
38
63
  end
39
64
  end
40
65
 
41
66
  # Dispatch to handler method if channel requires authentication, otherwise subscribe.
42
67
  def pusher_subscribe(msg)
43
- @channel_id = msg['data']['channel']
44
- if match = @channel_id.match(/^((private)|(presence))-/)
68
+ channel_id = msg['data']['channel']
69
+ subscription_id = if match = channel_id.match(/^((private)|(presence))-/)
45
70
  send "handle_#{match.captures[0]}_subscription", msg
46
71
  else
47
- subscribe_channel
72
+ subscribe_channel channel_id
48
73
  end
74
+ @subscriptions[channel_id] = subscription_id
49
75
  end
50
76
 
77
+ def pusher_ping(msg)
78
+ @socket.send(payload nil, 'pusher:ping')
79
+ end
80
+
81
+ def pusher_pong msg; end
82
+
83
+ #TODO: think about moving all subscription stuff into channel classes
51
84
  # 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|
85
+ def subscribe_channel(channel_id)
86
+ channel = Slanger::Channel.find_or_create_by_channel_id(channel_id)
87
+ @socket.send(payload channel_id, 'pusher_internal:subscription_succeeded')
88
+ # Subscribe to the channel and have the events received from it
89
+ # sent to the client's socket.
90
+ subscription_id = channel.subscribe do |msg|
55
91
  msg = JSON.parse(msg)
92
+ # Don't send the event if it was sent by the client
56
93
  socket_id = msg.delete 'socket_id'
57
94
  @socket.send msg.to_json unless socket_id == @socket_id
58
95
  end
@@ -60,29 +97,31 @@ module Slanger
60
97
 
61
98
  # Validate authentication token for private channel and add connection to channel subscribers if it checks out
62
99
  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']}"
100
+ channel = msg['data']['channel']
101
+ if msg['data']['auth'] && token(channel, msg['data']['channel_data']) != msg['data']['auth'].split(':')[1]
102
+ @socket.send(payload nil, 'pusher:error', {
103
+ message: "Invalid signature: Expected HMAC SHA256 hex digest of #{@socket_id}:#{channel}, but got #{msg['data']['auth']}"
66
104
  })
67
105
  else
68
- subscribe_channel
106
+ subscribe_channel channel
69
107
  end
70
108
  end
71
109
 
72
110
  # Validate authentication token and check channel_data. Add connection to channel subscribers if it checks out
73
111
  def handle_presence_subscription(msg)
74
- if token(msg['data']['channel_data']) != msg['data']['auth'].split(':')[1]
75
- @socket.send(payload 'pusher:error', {
112
+ channel_id = msg['data']['channel']
113
+ if token(channel_id, msg['data']['channel_data']) != msg['data']['auth'].split(':')[1]
114
+ @socket.send(payload nil, 'pusher:error', {
76
115
  message: "Invalid signature: Expected HMAC SHA256 hex digest of #{@socket_id}:#{msg['data']['channel']}, but got #{msg['data']['auth']}"
77
116
  })
78
117
  elsif !msg['data']['channel_data']
79
- @socket.send(payload 'pusher:error', {
118
+ @socket.send(payload nil, 'pusher:error', {
80
119
  message: "presence-channel is a presence channel and subscription must include channel_data"
81
120
  })
82
121
  else
83
- channel = Slanger::PresenceChannel.find_or_create_by_channel_id(@channel_id)
122
+ channel = Slanger::PresenceChannel.find_or_create_by_channel_id(channel_id)
84
123
  callback = Proc.new {
85
- @socket.send(payload 'pusher_internal:subscription_succeeded', {
124
+ @socket.send(payload channel_id, 'pusher_internal:subscription_succeeded', {
86
125
  presence: {
87
126
  count: channel.subscribers.size,
88
127
  ids: channel.ids,
@@ -90,24 +129,36 @@ module Slanger
90
129
  }
91
130
  })
92
131
  }
93
- @subscription_id = channel.subscribe(msg, callback) do |msg|
132
+ # Subscribe to channel, call callback when done to send a
133
+ # subscription_succeeded event to the client.
134
+ channel.subscribe(msg, callback) do |msg|
135
+ # Send channel messages to the client, unless it is the
136
+ # sender of the event.
94
137
  msg = JSON.parse(msg)
95
138
  socket_id = msg.delete 'socket_id'
96
139
  @socket.send msg.to_json unless socket_id == @socket_id
97
140
  end
98
-
99
141
  end
100
142
  end
101
143
 
102
144
  # 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
145
+ def payload(channel_id, event_name, payload = {})
146
+ { channel: channel_id, event: event_name, data: payload }.to_json
105
147
  end
106
148
 
107
149
  # HMAC token validation
108
- def token(params=nil)
109
- string_to_sign = [@socket_id, @channel_id, params].compact.join ':'
150
+ def token(channel_id, params=nil)
151
+ string_to_sign = [@socket_id, channel_id, params].compact.join ':'
110
152
  HMAC::SHA256.hexdigest(Slanger::Config.secret, string_to_sign)
111
153
  end
154
+
155
+ def handle_error(e)
156
+ case e
157
+ when JSON::ParserError
158
+ @socket.send(payload nil, 'pusher:error', { code: '5001', message: "Invalid JSON" })
159
+ else
160
+ @socket.send(payload nil, 'pusher:error', { code: '5000', message: "Internal Server error" })
161
+ end
162
+ end
112
163
  end
113
164
  end
@@ -1,3 +1,10 @@
1
+ # PresenceChannel class.
2
+ #
3
+ # Uses an EventMachine channel to let handlers interact with the
4
+ # Pusher channel. Relay events received from Redis into the
5
+ # EM channel. Keeps data on the subscribers to send it to clients.
6
+ #
7
+
1
8
  require 'glamazon'
2
9
  require 'eventmachine'
3
10
  require 'forwardable'
@@ -7,8 +14,11 @@ module Slanger
7
14
  class PresenceChannel < Channel
8
15
  def_delegators :channel, :push
9
16
 
17
+ # Send an event received from Redis to the EventMachine channel
10
18
  def dispatch(message, channel)
11
19
  if channel =~ /^slanger:/
20
+ # Messages received from the Redis channel slanger:* carry info on
21
+ # subscriptions. Update our subscribers accordingly.
12
22
  update_subscribers message
13
23
  else
14
24
  push message.to_json
@@ -17,6 +27,7 @@ module Slanger
17
27
 
18
28
  def initialize(attrs)
19
29
  super
30
+ # Also subscribe the slanger daemon to a Redis channel used for events concerning subscriptions.
20
31
  Slanger::Redis.subscribe 'slanger:connection_notification'
21
32
  end
22
33
 
@@ -24,15 +35,20 @@ module Slanger
24
35
  channel_data = JSON.parse msg['data']['channel_data']
25
36
  public_subscription_id = SecureRandom.uuid
26
37
 
38
+ # Send event about the new subscription to the Redis slanger:connection_notification Channel.
27
39
  publisher = publish_connection_notification subscription_id: public_subscription_id, online: true,
28
40
  channel_data: channel_data, channel: channel_id
29
41
 
42
+ # Associate the subscription data to the public id in Redis.
30
43
  roster_add public_subscription_id, channel_data
31
44
 
32
45
  # fuuuuuuuuuccccccck!
33
46
  publisher.callback do
34
47
  EM.next_tick do
48
+ # The Subscription event has been sent to Redis successfully.
49
+ # Call the provided callback.
35
50
  callback.call
51
+ # Add the subscription to our table.
36
52
  internal_subscription_table[public_subscription_id] = channel.subscribe &blk
37
53
  end
38
54
  end
@@ -51,6 +67,7 @@ module Slanger
51
67
  def unsubscribe(public_subscription_id)
52
68
  # Unsubcribe from EM::Channel
53
69
  channel.unsubscribe(internal_subscription_table.delete(public_subscription_id)) # if internal_subscription_table[public_subscription_id]
70
+ # Remove subscription data from Redis
54
71
  roster_remove public_subscription_id
55
72
  # Notify all instances
56
73
  publish_connection_notification subscription_id: public_subscription_id, online: false, channel: channel_id
@@ -59,6 +76,7 @@ module Slanger
59
76
  private
60
77
 
61
78
  def get_roster
79
+ # Read subscription infos from Redis.
62
80
  Fiber.new do
63
81
  f = Fiber.current
64
82
  Slanger::Redis.hgetall(channel_id).
@@ -68,14 +86,18 @@ module Slanger
68
86
  end
69
87
 
70
88
  def roster_add(key, value)
89
+ # Add subscription info to Redis.
71
90
  Slanger::Redis.hset(channel_id, key, value)
72
91
  end
73
92
 
74
93
  def roster_remove(key)
94
+ # Remove subscription info from Redis.
75
95
  Slanger::Redis.hdel(channel_id, key)
76
96
  end
77
97
 
78
98
  def publish_connection_notification(payload, retry_count=0)
99
+ # Send a subscription notification to the global slanger:connection_notification
100
+ # channel.
79
101
  Slanger::Redis.publish('slanger:connection_notification', payload.to_json).
80
102
  tap { |r| r.errback { publish_connection_notification payload, retry_count.succ unless retry_count == 5 } }
81
103
  end
data/lib/slanger/redis.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # Redis class.
2
+ # Interface with Redis.
3
+
1
4
  require 'forwardable'
2
5
 
3
6
  module Slanger
@@ -5,10 +8,11 @@ module Slanger
5
8
  extend Forwardable
6
9
 
7
10
  def self.extended base
11
+ # Dispatch messages received from Redis to their destination channel.
8
12
  base.on(:message) do |channel, message|
9
13
  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
14
+ klass = message['channel'][/^presence-/] ? PresenceChannel : Channel
15
+ klass.find_or_create_by_channel_id(message['channel']).dispatch message, channel
12
16
  end
13
17
  end
14
18
 
@@ -5,7 +5,19 @@ module Slanger
5
5
  module WebSocketServer
6
6
  def run
7
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|
8
+ options = {
9
+ host: Slanger::Config[:websocket_host],
10
+ port: Slanger::Config[:websocket_port],
11
+ debug: Slanger::Config[:debug],
12
+ app_key: Slanger::Config[:app_key]
13
+ }
14
+
15
+ if Slanger::Config[:tls_options]
16
+ options.merge! secure: true,
17
+ tls_options: Slanger::Config[:tls_options]
18
+ end
19
+
20
+ EM::WebSocket.start options do |ws|
9
21
  # Keep track of handler instance in instance of EM::Connection to ensure a unique handler instance is used per connection.
10
22
  ws.class_eval { attr_accessor :connection_handler }
11
23
  # Delegate connection management to handler instance.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slanger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-13 00:00:00.000000000Z
12
+ date: 2012-04-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
16
- requirement: &70279989838940 !ruby/object:Gem::Requirement
16
+ requirement: &70260724123600 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.12.10
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70279989838940
24
+ version_requirements: *70260724123600
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: em-hiredis
27
- requirement: &70279989865080 !ruby/object:Gem::Requirement
27
+ requirement: &70260724155460 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.1.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70279989865080
35
+ version_requirements: *70260724155460
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: em-websocket
38
- requirement: &70279989864620 !ruby/object:Gem::Requirement
38
+ requirement: &70260724154320 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 0.3.5
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70279989864620
46
+ version_requirements: *70260724154320
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rack
49
- requirement: &70279989864160 !ruby/object:Gem::Requirement
49
+ requirement: &70260724152840 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 1.3.3
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *70279989864160
57
+ version_requirements: *70260724152840
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rack-fiber_pool
60
- requirement: &70279989863700 !ruby/object:Gem::Requirement
60
+ requirement: &70260724149120 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - =
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.9.1
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *70279989863700
68
+ version_requirements: *70260724149120
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: signature
71
- requirement: &70279989863240 !ruby/object:Gem::Requirement
71
+ requirement: &70260724182220 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 0.1.2
77
77
  type: :runtime
78
78
  prerelease: false
79
- version_requirements: *70279989863240
79
+ version_requirements: *70260724182220
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: activesupport
82
- requirement: &70279989862780 !ruby/object:Gem::Requirement
82
+ requirement: &70260724178720 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: 3.1.0
88
88
  type: :runtime
89
89
  prerelease: false
90
- version_requirements: *70279989862780
90
+ version_requirements: *70260724178720
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: glamazon
93
- requirement: &70279989862320 !ruby/object:Gem::Requirement
93
+ requirement: &70260724177880 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ~>
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: 0.3.1
99
99
  type: :runtime
100
100
  prerelease: false
101
- version_requirements: *70279989862320
101
+ version_requirements: *70260724177880
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: sinatra
104
- requirement: &70279989861860 !ruby/object:Gem::Requirement
104
+ requirement: &70260724206740 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ~>
@@ -109,10 +109,10 @@ dependencies:
109
109
  version: 1.2.6
110
110
  type: :runtime
111
111
  prerelease: false
112
- version_requirements: *70279989861860
112
+ version_requirements: *70260724206740
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: thin
115
- requirement: &70279989861400 !ruby/object:Gem::Requirement
115
+ requirement: &70260724205980 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
118
  - - ~>
@@ -120,10 +120,10 @@ dependencies:
120
120
  version: 1.2.11
121
121
  type: :runtime
122
122
  prerelease: false
123
- version_requirements: *70279989861400
123
+ version_requirements: *70260724205980
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: em-http-request
126
- requirement: &70279989860940 !ruby/object:Gem::Requirement
126
+ requirement: &70260724205260 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
129
  - - ~>
@@ -131,10 +131,10 @@ dependencies:
131
131
  version: 0.3.0
132
132
  type: :runtime
133
133
  prerelease: false
134
- version_requirements: *70279989860940
134
+ version_requirements: *70260724205260
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: rspec
137
- requirement: &70279989860480 !ruby/object:Gem::Requirement
137
+ requirement: &70260724203820 !ruby/object:Gem::Requirement
138
138
  none: false
139
139
  requirements:
140
140
  - - ~>
@@ -142,10 +142,10 @@ dependencies:
142
142
  version: 2.6.0
143
143
  type: :development
144
144
  prerelease: false
145
- version_requirements: *70279989860480
145
+ version_requirements: *70260724203820
146
146
  - !ruby/object:Gem::Dependency
147
147
  name: pusher
148
- requirement: &70279989860020 !ruby/object:Gem::Requirement
148
+ requirement: &70260724202560 !ruby/object:Gem::Requirement
149
149
  none: false
150
150
  requirements:
151
151
  - - ~>
@@ -153,10 +153,10 @@ dependencies:
153
153
  version: 0.8.2
154
154
  type: :development
155
155
  prerelease: false
156
- version_requirements: *70279989860020
156
+ version_requirements: *70260724202560
157
157
  - !ruby/object:Gem::Dependency
158
158
  name: haml
159
- requirement: &70279989859560 !ruby/object:Gem::Requirement
159
+ requirement: &70260724199480 !ruby/object:Gem::Requirement
160
160
  none: false
161
161
  requirements:
162
162
  - - ~>
@@ -164,7 +164,7 @@ dependencies:
164
164
  version: 3.1.2
165
165
  type: :development
166
166
  prerelease: false
167
- version_requirements: *70279989859560
167
+ version_requirements: *70260724199480
168
168
  description: A websocket service compatible with Pusher libraries
169
169
  email: sjtgraham@mac.com
170
170
  executables:
@@ -183,8 +183,9 @@ files:
183
183
  - lib/slanger/service.rb
184
184
  - lib/slanger/web_socket_server.rb
185
185
  - slanger.rb
186
- - bin/slanger
187
- homepage: http://github.com/stevegraham/pusher
186
+ - !binary |-
187
+ YmluL3NsYW5nZXI=
188
+ homepage: http://github.com/stevegraham/slanger
188
189
  licenses: []
189
190
  post_install_message:
190
191
  rdoc_options: []
@@ -204,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
205
  version: '0'
205
206
  requirements: []
206
207
  rubyforge_project:
207
- rubygems_version: 1.8.6
208
+ rubygems_version: 1.8.16
208
209
  signing_key:
209
210
  specification_version: 3
210
211
  summary: A websocket service compatible with Pusher libraries