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 +4 -4
- data/README.md +44 -1
- data/lib/slanger/api.rb +5 -0
- data/lib/slanger/api/event.rb +15 -0
- data/lib/slanger/api/event_publisher.rb +24 -0
- data/lib/slanger/api/request_validation.rb +103 -0
- data/lib/slanger/api/server.rb +56 -0
- data/lib/slanger/channel.rb +19 -7
- data/lib/slanger/connection.rb +2 -1
- data/lib/slanger/handler.rb +3 -3
- data/lib/slanger/presence_channel.rb +1 -2
- data/lib/slanger/service.rb +1 -1
- data/lib/slanger/version.rb +1 -1
- data/slanger.rb +4 -0
- data/spec/have_attributes.rb +64 -0
- data/spec/integration/channel_spec.rb +114 -0
- data/spec/integration/integration_spec.rb +68 -0
- data/spec/integration/presence_channel_spec.rb +158 -0
- data/spec/integration/private_channel_spec.rb +79 -0
- data/spec/integration/replaced_handler_spec.rb +23 -0
- data/spec/integration/ssl_spec.rb +18 -0
- data/spec/server.crt +12 -0
- data/spec/server.key +15 -0
- data/spec/slanger_helper_methods.rb +109 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/unit/channel_spec.rb +69 -0
- data/spec/unit/request_validation_spec.rb +64 -0
- data/spec/unit/webhook_spec.rb +43 -0
- metadata +88 -38
- data/lib/slanger/api_server.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fea601e824e671ed0d87a8a5e44584d3e6a4c18
|
4
|
+
data.tar.gz: 64df554d024eef643d7b311afd8630e13c9a4784
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/lib/slanger/api.rb
ADDED
@@ -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
|
data/lib/slanger/channel.rb
CHANGED
@@ -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[
|
22
|
-
|
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
|
-
|
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 =~
|
84
|
+
push(message.to_json) unless channel =~ /\Aslanger:/
|
73
85
|
end
|
74
86
|
|
75
87
|
def authenticated?
|
76
|
-
channel_id =~
|
88
|
+
channel_id =~ /\Aprivate-/ || channel_id =~ /\Apresence-/
|
77
89
|
end
|
78
90
|
end
|
79
91
|
end
|
data/lib/slanger/connection.rb
CHANGED
@@ -25,7 +25,8 @@ module Slanger
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def establish
|
28
|
-
@socket_id = SecureRandom.
|
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
|
data/lib/slanger/handler.rb
CHANGED
@@ -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(
|
31
|
+
event = msg['event'].gsub(/\Apusher:/, 'pusher_')
|
32
32
|
|
33
|
-
if event =~
|
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(
|
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 =~
|
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
|
data/lib/slanger/service.rb
CHANGED
@@ -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::
|
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
|
|
data/lib/slanger/version.rb
CHANGED
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
|