slanger 0.4.1 → 0.4.2

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: abbd4b1fc22d16cd1a72b1bd647fa4f3703f580d
4
- data.tar.gz: 0b6db960a6b39f26a219b8cb7cf56826b02ca111
3
+ metadata.gz: 5fea601e824e671ed0d87a8a5e44584d3e6a4c18
4
+ data.tar.gz: 64df554d024eef643d7b311afd8630e13c9a4784
5
5
  SHA512:
6
- metadata.gz: 2a5cab2cc4a2672405ec12bc830dd43c0051183a68aa54c24a07426d4012c0570a5dda15b1c9a78178347137e61fc2001c3ee9ca4bd716ba3b89b601384e1882
7
- data.tar.gz: a2fc49dbb668a8a343c69c852fb946faaa29d89cdadc4f50386a3b810b578bd388cdd0e93d73ad81462797825711badab9a59b91ffa7096d4e777ebb744c3f1c
6
+ metadata.gz: a4688a9c1191d2eb27bcedb34dbe6e492d0b01df3fb154d50bd7d20254f1495741cab833afc2f1c69538c513d7266f1d7fea298c952397c82327e518da92257d
7
+ data.tar.gz: 4c197dd2e23beef01734d01f7df2e9d5d21fe75908c2b61802691939de0941b6c3fc7a61dfa240bfaa373c3e95f5d4aaa495cc402c486886befe746fa07514a3
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Slanger
2
2
 
3
- [![Build Status](https://travis-ci.org/stevegraham/slanger.svg?branch=master)](https://travis-ci.org/stevegraham/slanger)
3
+ [![Gem Version](https://badge.fury.io/rb/slanger.svg)](http://badge.fury.io/rb/slanger) [![Build Status](https://travis-ci.org/stevegraham/slanger.svg?branch=master)](https://travis-ci.org/stevegraham/slanger)
4
4
 
5
5
  **Important! Slanger is not supposed to be included in your Gemfile. RubyGems is used as a distribution mechanism. If you include it in your app, you will likely get dependency conflicts. PRs updating dependencies for compatibility with your app will be closed. Thank you for reading and enjoy Slanger!**
6
6
 
@@ -52,6 +52,28 @@ eventual consistency, which in practise is instantaneous.
52
52
  - Ruby 2.1.2 or greater
53
53
  - Redis
54
54
 
55
+ ## Server setup
56
+
57
+ Most linux distributions have by defualt a very low open files limit. In order to sustain more than 1024 ( default ) connections, you need to apply the following changes to your system:
58
+ Add to `/etc/sysctl.conf`:
59
+ ```
60
+ fs.file-max = 50000
61
+ ```
62
+ Add to `/etc/security/limits.conf`:
63
+ ```
64
+ * hard nofile 50000
65
+ * soft nofile 50000
66
+ * hard nproc 50000
67
+ * soft nproc 50000
68
+ ```
69
+
70
+ ## Cluster load-balancing setup with Haproxy
71
+
72
+ If you want to run multiple slanger instances in a cluster, one option will be to balance the connections with Haproxy.
73
+ A basic config can be found in the folder `examples`.
74
+ Haproxy can be also used for SSL termination, leaving slanger to not have to deal with SSL checks and so on, making it lighter.
75
+
76
+
55
77
  ## Starting the service
56
78
 
57
79
  Slanger is packaged as a Rubygem. Installing the gem makes the 'slanger' executable available. The `slanger` executable takes arguments, of which two are mandatory: `--app_key` and `--secret`. These can but do not have to be the same as the credentials you use for Pusher. They are required because Slanger performs the same HMAC signing of API requests that Pusher does.
@@ -87,6 +109,27 @@ Slanger API server listening on port 4567
87
109
  Slanger WebSocket server listening on port 8080
88
110
  ```
89
111
 
112
+ ## Ubuntu upstart script
113
+
114
+ If you're using Ubuntu, you might find this upscript very helpful. The steps below will create an init script that will make slanger run at boot and restart if it fails.
115
+ Open `/etc/init/slanger` and add:
116
+ ```
117
+ start on started networking and runlevel [2345]
118
+ stop on runlevel [016]
119
+ respawn
120
+ script
121
+ LANG=en_US.UTF-8 /usr/local/rvm/gems/ruby-RUBY_VERISON/wrappers/slanger --app_key KEY --secret SECRET --redis_address redis://REDIS_IP:REDIS_PORT/REDIS_DB
122
+ end script
123
+ ```
124
+ This example assumes you're using rvm and a custom redis configuration
125
+
126
+ Then, to start / stop the service, just do
127
+ ```
128
+ service slanger start
129
+ service slanger stop
130
+ ```
131
+
132
+
90
133
  ## Modifying your application code to use the Slanger service
91
134
 
92
135
  Once you have a Slanger instance listening for incoming connections you need to alter you application code to use the Slanger endpoint instead of Pusher. Fortunately this is very simple, unobtrusive, easily reversable, and very painless.
@@ -0,0 +1,5 @@
1
+ module Slanger
2
+ module Api
3
+ InvalidRequest = Class.new ArgumentError
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module Slanger
2
+ module Api
3
+ class Event < Struct.new :name, :data, :socket_id
4
+ def payload(channel_id)
5
+ {
6
+ event: name,
7
+ data: data,
8
+ channel: channel_id,
9
+ socket_id: socket_id
10
+ }.select { |_,v| v }.to_json
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,24 @@
1
+ module Slanger
2
+ module Api
3
+ class EventPublisher < Struct.new(:channels, :event)
4
+ def self.publish(channels, event)
5
+ new(channels, event).publish
6
+ end
7
+
8
+ def publish
9
+ Array(channels).each do |c|
10
+ publish_event(c)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def publish_event(channel_id)
17
+ Slanger::Redis.publish(channel_id, event.payload(channel_id))
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
24
+
@@ -0,0 +1,103 @@
1
+ module Slanger
2
+ module Api
3
+ class RequestValidation < Struct.new :raw_body, :raw_params, :path_info
4
+ def initialize(*args)
5
+ super(*args)
6
+
7
+ validate!
8
+ authenticate!
9
+ parse_body!
10
+ end
11
+
12
+ def data
13
+ @data ||= JSON.parse(body["data"] || params["data"])
14
+ end
15
+
16
+ def body
17
+ @body ||= validate_body!
18
+ end
19
+
20
+ def auth_params
21
+ params.except('channel_id', 'app_id')
22
+ end
23
+
24
+ def socket_id
25
+ @socket_id ||= determine_valid_socket_id
26
+ end
27
+
28
+ def params
29
+ @params ||= validate_raw_params!
30
+ end
31
+
32
+ def channels
33
+ @channels ||= Array(body["channels"] || params["channels"])
34
+ end
35
+
36
+ private
37
+
38
+ def validate_body!
39
+ @body ||= assert_valid_json!(raw_body.tap{ |s| s.force_encoding('utf-8')})
40
+ end
41
+
42
+ def validate!
43
+ raise InvalidRequest.new "no body" unless raw_body.present?
44
+ raise InvalidRequest.new "invalid params" unless raw_params.is_a? Hash
45
+ raise InvalidRequest.new "invalid path" unless path_info.is_a? String
46
+
47
+ determine_valid_socket_id
48
+ channels.each{|id| validate_channel_id!(id)}
49
+ end
50
+
51
+ def validate_socket_id!(socket_id)
52
+ validate_with_regex!(/\A\d+\.\d+\z/, socket_id, "socket_id")
53
+ end
54
+
55
+ def validate_channel_id!(channel_id)
56
+ validate_with_regex!(/\A[\w@\-;]+\z/, channel_id, "channel_id")
57
+ end
58
+
59
+ def validate_with_regex!(regex, value, name)
60
+ raise InvalidRequest, "Invalid #{name} #{value.inspect}" unless value =~ regex
61
+
62
+ value
63
+ end
64
+
65
+ def validate_raw_params!
66
+ restricted = user_params.slice "body_md5", "auth_version", "auth_key", "auth_timestamp", "auth_signature", "app_id"
67
+
68
+ invalid_keys = restricted.keys - user_params.keys
69
+
70
+ if invalid_keys.any?
71
+ raise Slanger::InvalidRequest.new "Invalid params: #{invalid_keys}"
72
+ end
73
+
74
+ restricted
75
+ end
76
+
77
+ def authenticate!
78
+ # Raises Signature::AuthenticationError if request does not authenticate.
79
+ Signature::Request.new('POST', path_info, auth_params).
80
+ authenticate { |key| Signature::Token.new key, Slanger::Config.secret }
81
+ end
82
+
83
+ def parse_body!
84
+ assert_valid_json!(raw_body)
85
+ end
86
+
87
+ def assert_valid_json!(string)
88
+ JSON.parse(string)
89
+ rescue JSON::ParserError
90
+ raise Slanger::InvalidRequest.new("Invalid request body: #{raw_body}")
91
+ end
92
+
93
+ def determine_valid_socket_id
94
+ return validate_socket_id!(body["socket_id"]) if body["socket_id"]
95
+ return validate_socket_id!(params["socket_id"]) if params["socket_id"]
96
+ end
97
+
98
+ def user_params
99
+ raw_params.reject{|k,_| %w(splat captures).include?(k)}
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ require 'sinatra/base'
3
+ require 'signature'
4
+ require 'json'
5
+ require 'active_support/core_ext/hash'
6
+ require 'eventmachine'
7
+ require 'em-hiredis'
8
+ require 'rack'
9
+ require 'fiber'
10
+ require 'rack/fiber_pool'
11
+
12
+ module Slanger
13
+ module Api
14
+ class Server < Sinatra::Base
15
+ use Rack::FiberPool
16
+ set :raise_errors, lambda { false }
17
+ set :show_exceptions, false
18
+
19
+ error(Signature::AuthenticationError) { |e| halt 401, "401 UNAUTHORIZED" }
20
+ error(Slanger::Api::InvalidRequest) { |c| halt 400, "400 Bad Request" }
21
+
22
+ before do
23
+ valid_request
24
+ end
25
+
26
+ post '/apps/:app_id/events' do
27
+ socket_id = valid_request.socket_id
28
+ body = valid_request.body
29
+
30
+ event = Slanger::Api::Event.new(body["name"], body["data"], socket_id)
31
+ EventPublisher.publish(valid_request.channels, event)
32
+
33
+ status 202
34
+ return {}.to_json
35
+ end
36
+
37
+ post '/apps/:app_id/channels/:channel_id/events' do
38
+ params = valid_request.params
39
+
40
+ event = Event.new(params["name"], valid_request.body, valid_request.socket_id)
41
+ EventPublisher.publish(valid_request.channels, event)
42
+
43
+ status 202
44
+ return {}.to_json
45
+ end
46
+
47
+ def valid_request
48
+ @valid_request ||=
49
+ begin
50
+ request_body ||= request.body.read.tap{|s| s.force_encoding("utf-8")}
51
+ RequestValidation.new(request_body, params, env["PATH_INFO"])
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -5,21 +5,33 @@
5
5
  # EM channel.
6
6
  #
7
7
 
8
- require 'glamazon'
9
8
  require 'eventmachine'
10
9
  require 'forwardable'
11
10
 
12
11
  module Slanger
13
12
  class Channel
14
- include Glamazon::Base
15
13
  extend Forwardable
16
14
 
17
15
  def_delegators :channel, :push
16
+ attr_reader :channel_id
18
17
 
19
18
  class << self
20
19
  def from channel_id
21
- klass = channel_id[/^presence-/] ? PresenceChannel : Channel
22
- klass.find_or_create_by_channel_id channel_id
20
+ klass = channel_id[/\Apresence-/] ? PresenceChannel : Channel
21
+
22
+ klass.lookup(channel_id) || klass.create(channel_id: channel_id)
23
+ end
24
+
25
+ def lookup(channel_id)
26
+ all.detect { |o| o.channel_id == channel_id }
27
+ end
28
+
29
+ def create(params = {})
30
+ new(params).tap { |r| all << r }
31
+ end
32
+
33
+ def all
34
+ @all ||= []
23
35
  end
24
36
 
25
37
  def unsubscribe channel_id, subscription_id
@@ -32,7 +44,7 @@ module Slanger
32
44
  end
33
45
 
34
46
  def initialize(attrs)
35
- super
47
+ @channel_id = attrs.with_indifferent_access[:channel_id]
36
48
  Slanger::Redis.subscribe channel_id
37
49
  end
38
50
 
@@ -69,11 +81,11 @@ module Slanger
69
81
  # Send an event received from Redis to the EventMachine channel
70
82
  # which will send it to subscribed clients.
71
83
  def dispatch(message, channel)
72
- push(message.to_json) unless channel =~ /^slanger:/
84
+ push(message.to_json) unless channel =~ /\Aslanger:/
73
85
  end
74
86
 
75
87
  def authenticated?
76
- channel_id =~ /^private-/ || channel_id =~ /^presence-/
88
+ channel_id =~ /\Aprivate-/ || channel_id =~ /\Apresence-/
77
89
  end
78
90
  end
79
91
  end
@@ -25,7 +25,8 @@ module Slanger
25
25
  end
26
26
 
27
27
  def establish
28
- @socket_id = SecureRandom.uuid
28
+ @socket_id = "%d.%d" % [Process.pid, SecureRandom.random_number(10 ** 6)]
29
+
29
30
  send_payload nil, 'pusher:connection_established', {
30
31
  socket_id: @socket_id,
31
32
  activity_timeout: Slanger::Config.activity_timeout
@@ -28,9 +28,9 @@ module Slanger
28
28
 
29
29
  msg['data'] = JSON.parse(msg['data']) if msg['data'].is_a? String
30
30
 
31
- event = msg['event'].gsub(/^pusher:/, 'pusher_')
31
+ event = msg['event'].gsub(/\Apusher:/, 'pusher_')
32
32
 
33
- if event =~ /^client-/
33
+ if event =~ /\Aclient-/
34
34
  msg['socket_id'] = connection.socket_id
35
35
  Channel.send_client_message msg
36
36
  elsif respond_to? event, true
@@ -106,7 +106,7 @@ module Slanger
106
106
  end
107
107
 
108
108
  def subscription_klass channel_id
109
- klass = channel_id.match(/^(private|presence)-/) do |match|
109
+ klass = channel_id.match(/\A(private|presence)-/) do |match|
110
110
  Slanger.const_get "#{match[1]}_subscription".classify
111
111
  end
112
112
 
@@ -5,7 +5,6 @@
5
5
  # EM channel. Keeps data on the subscribers to send it to clients.
6
6
  #
7
7
 
8
- require 'glamazon'
9
8
  require 'eventmachine'
10
9
  require 'forwardable'
11
10
  require 'fiber'
@@ -16,7 +15,7 @@ module Slanger
16
15
 
17
16
  # Send an event received from Redis to the EventMachine channel
18
17
  def dispatch(message, channel)
19
- if channel =~ /^slanger:/
18
+ if channel =~ /\Aslanger:/
20
19
  # Messages received from the Redis channel slanger:* carry info on
21
20
  # subscriptions. Update our subscribers accordingly.
22
21
  update_subscribers message
@@ -6,7 +6,7 @@ module Slanger
6
6
  def run
7
7
  Slanger::Config[:require].each { |f| require f }
8
8
  Thin::Logging.silent = true
9
- Rack::Handler::Thin.run Slanger::ApiServer, Host: Slanger::Config.api_host, Port: Slanger::Config.api_port
9
+ Rack::Handler::Thin.run Slanger::Api::Server, Host: Slanger::Config.api_host, Port: Slanger::Config.api_port
10
10
  Slanger::WebSocketServer.run
11
11
  end
12
12
 
@@ -1,3 +1,3 @@
1
1
  module Slanger
2
- VERSION = '0.4.1'
2
+ VERSION = '0.4.2'
3
3
  end
data/slanger.rb CHANGED
@@ -16,4 +16,8 @@ File.tap do |f|
16
16
  Dir[f.expand_path(f.join(f.dirname(__FILE__),'lib', 'slanger', '*.rb'))].each do |file|
17
17
  Slanger.autoload File.basename(file, '.rb').camelize, file
18
18
  end
19
+
20
+ Dir[f.expand_path(f.join(f.dirname(__FILE__),'lib', 'slanger', 'api', '*.rb'))].each do |file|
21
+ Slanger::Api.autoload File.basename(file, '.rb').camelize, file
22
+ end
19
23
  end
@@ -0,0 +1,64 @@
1
+ module SlangerHelperMethods
2
+ class HaveAttributes
3
+ attr_reader :messages, :attributes
4
+ def initialize attributes
5
+ @attributes = attributes
6
+ end
7
+
8
+ CHECKS = %w(first_event last_event last_data )
9
+
10
+ def matches?(messages)
11
+ @messages = messages
12
+ @failures = []
13
+
14
+ check_connection_established if attributes[:connection_established]
15
+ check_id_present if attributes[:id_present]
16
+
17
+ CHECKS.each { |a| attributes[a.to_sym] ? check(a) : true }
18
+
19
+ @failures.empty?
20
+ end
21
+
22
+ def check message
23
+ send(message) == attributes[message.to_sym] or @failures << message
24
+ end
25
+
26
+ def failure_message
27
+ @failures.map {|f| "expected #{f}: to equal #{attributes[f]} but got #{send(f)}"}.join "\n"
28
+ end
29
+
30
+ private
31
+
32
+ def check_connection_established
33
+ if first_event != 'pusher:connection_established'
34
+ @failures << :connection_established
35
+ end
36
+ end
37
+
38
+ def check_id_present
39
+ if messages.first['data']['socket_id'] == nil
40
+ @failures << :id_present
41
+ end
42
+ end
43
+
44
+ def first_event
45
+ messages.first['event']
46
+ end
47
+
48
+ def last_event
49
+ messages.last['event']
50
+ end
51
+
52
+ def last_data
53
+ messages.last['data']
54
+ end
55
+
56
+ def count
57
+ messages.length
58
+ end
59
+ end
60
+
61
+ def have_attributes attributes
62
+ HaveAttributes.new attributes
63
+ end
64
+ end