slangerq 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ module Slanger
2
+ class PresenceSubscription < Subscription
3
+ def subscribe
4
+ return handle_invalid_signature if invalid_signature?
5
+
6
+ unless channel_data?
7
+ return connection.error({
8
+ message: "presence-channel is a presence channel and subscription must include channel_data"
9
+ })
10
+ end
11
+
12
+ channel.subscribe(@msg, callback) { |m| send_message m }
13
+ end
14
+
15
+ private
16
+
17
+ def channel_data?
18
+ @msg['data']['channel_data']
19
+ end
20
+
21
+ def callback
22
+ Proc.new {
23
+ connection.send_payload(channel_id, 'pusher_internal:subscription_succeeded', {
24
+ presence: {
25
+ count: channel.subscribers.size,
26
+ ids: channel.ids,
27
+ hash: channel.subscribers
28
+ }
29
+ })
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Slanger
2
+ class PrivateSubscription < Subscription
3
+ def subscribe
4
+ return handle_invalid_signature if auth && invalid_signature?
5
+
6
+ Subscription.new(connection.socket, connection.socket_id, @msg).subscribe
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # Redis class.
2
+ # Interface with Redis.
3
+
4
+ require 'forwardable'
5
+ require 'oj'
6
+
7
+ module Slanger
8
+ module Redis
9
+ extend Forwardable
10
+
11
+ def_delegator :publisher, :publish
12
+ def_delegators :subscriber, :subscribe
13
+ def_delegators :regular_connection, :hgetall, :hdel, :hset, :hincrby
14
+
15
+ private
16
+
17
+ def regular_connection
18
+ @regular_connection ||= new_connection
19
+ end
20
+
21
+ def publisher
22
+ @publisher ||= new_connection
23
+ end
24
+
25
+ def subscriber
26
+ @subscriber ||= new_connection.pubsub.tap do |c|
27
+ c.on(:message) do |channel, message|
28
+ message = Oj.load(message)
29
+ c = Channel.from message['channel']
30
+ c.dispatch message, channel
31
+ end
32
+ end
33
+ end
34
+
35
+ def new_connection
36
+ EM::Hiredis.connect Slanger::Config.redis_address
37
+ end
38
+
39
+ extend self
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ require 'thin'
2
+ require 'rack'
3
+
4
+ module Slanger
5
+ module Service
6
+ def run
7
+ Slanger::Config[:require].each { |f| require f }
8
+ Thin::Logging.silent = true
9
+ Rack::Handler::Thin.run Slanger::Api::Server, Host: Slanger::Config.api_host, Port: Slanger::Config.api_port
10
+ Slanger::WebSocketServer.run
11
+ end
12
+
13
+ def stop
14
+ EM.stop if EM.reactor_running?
15
+ end
16
+
17
+ extend self
18
+ Signal.trap('HUP') { Slanger::Service.stop }
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ module Slanger
2
+ class Subscription
3
+ attr_accessor :connection, :socket
4
+ delegate :send_payload, :send_message, :error, :socket_id, to: :connection
5
+
6
+ def initialize socket, socket_id, msg
7
+ @connection = Connection.new socket, socket_id
8
+ @msg = msg
9
+ end
10
+
11
+ def subscribe
12
+ send_payload channel_id, 'pusher_internal:subscription_succeeded'
13
+
14
+ channel.subscribe { |m| send_message m }
15
+ end
16
+
17
+ private
18
+
19
+ def channel
20
+ Channel.from channel_id
21
+ end
22
+
23
+ def channel_id
24
+ @msg['data']['channel']
25
+ end
26
+
27
+ def token(channel_id, params=nil)
28
+ to_sign = [socket_id, channel_id, params].compact.join ':'
29
+
30
+ digest = OpenSSL::Digest::SHA256.new
31
+ OpenSSL::HMAC.hexdigest digest, Slanger::Config.secret, to_sign
32
+ end
33
+
34
+ def invalid_signature?
35
+ token(channel_id, data) != auth.split(':')[1]
36
+ end
37
+
38
+ def auth
39
+ @msg['data']['auth']
40
+ end
41
+
42
+ def data
43
+ @msg['data']['channel_data']
44
+ end
45
+
46
+ def handle_invalid_signature
47
+ message = "Invalid signature: Expected HMAC SHA256 hex digest of "
48
+ message << "#{socket_id}:#{channel_id}, but got #{auth}"
49
+
50
+ error({ message: message})
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module Slanger
2
+ VERSION = '0.6.1'
3
+ end
@@ -0,0 +1,37 @@
1
+ require 'eventmachine'
2
+ require 'em-websocket'
3
+
4
+ module Slanger
5
+ module WebSocketServer
6
+ def run
7
+ case
8
+ when EM.epoll? then EM.epoll
9
+ when EM.kqueue? then EM.kqueue
10
+ end
11
+
12
+ EM.run do
13
+ options = {
14
+ host: Slanger::Config[:websocket_host],
15
+ port: Slanger::Config[:websocket_port],
16
+ debug: Slanger::Config[:debug],
17
+ app_key: Slanger::Config[:app_key]
18
+ }
19
+
20
+ if Slanger::Config[:tls_options]
21
+ options.merge! secure: true,
22
+ tls_options: Slanger::Config[:tls_options]
23
+ end
24
+
25
+ EM::WebSocket.start options do |ws|
26
+ # Keep track of handler instance in instance of EM::Connection to ensure a unique handler instance is used per connection.
27
+ ws.class_eval { attr_accessor :connection_handler }
28
+ # Delegate connection management to handler instance.
29
+ ws.onopen { |handshake| ws.connection_handler = Slanger::Config.socket_handler.new ws, handshake }
30
+ ws.onmessage { |msg| ws.connection_handler.onmessage msg }
31
+ ws.onclose { ws.connection_handler.onclose }
32
+ end
33
+ end
34
+ end
35
+ extend self
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ require 'fiber'
2
+ require 'em-http-request'
3
+ require 'oj'
4
+
5
+ module Slanger
6
+ module Webhook
7
+ def post payload
8
+ return unless Slanger::Config.webhook_url
9
+
10
+ payload ={
11
+ time_ms: Time.now.strftime('%s%L'), events: [payload]
12
+ }
13
+
14
+ payload = Oj.dump(payload, mode: :compat)
15
+
16
+ digest = OpenSSL::Digest::SHA256.new
17
+ hmac = OpenSSL::HMAC.hexdigest(digest, Slanger::Config.secret, payload)
18
+ content_type = 'application/json'
19
+
20
+ EM::HttpRequest.new(Slanger::Config.webhook_url).
21
+ post(body: payload, head: {
22
+ "X-Pusher-Key" => Slanger::Config.app_key,
23
+ "X-Pusher-Signature" => hmac,
24
+ "Content-Type" => content_type
25
+ })
26
+ # TODO: Exponentially backed off retries for errors
27
+ end
28
+
29
+ extend self
30
+ end
31
+ end
data/slanger.rb ADDED
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ require 'eventmachine'
3
+ require 'em-hiredis'
4
+ require 'rack'
5
+ require 'active_support/core_ext/string'
6
+ require File.join(File.dirname(__FILE__), 'lib', 'slanger', 'version')
7
+
8
+ module Slanger; end
9
+
10
+ case
11
+ when EM.epoll? then EM.epoll
12
+ when EM.kqueue? then EM.kqueue
13
+ end
14
+
15
+ File.tap do |f|
16
+ Dir[f.expand_path(f.join(f.dirname(__FILE__),'lib', 'slanger', '*.rb'))].each do |file|
17
+ Slanger.autoload File.basename(file, '.rb').camelize, file
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
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
@@ -0,0 +1,114 @@
1
+ #encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe 'Integration:' do
5
+
6
+ before(:each) { start_slanger }
7
+
8
+ describe 'channel' do
9
+ it 'pushes messages to interested websocket connections' do
10
+ messages = em_stream do |websocket, messages|
11
+ case messages.length
12
+ when 1
13
+ websocket.callback { websocket.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json) }
14
+ when 2
15
+ Pusher['MY_CHANNEL'].trigger 'an_event', some: "Mit Raben Und Wölfen"
16
+ when 3
17
+ EM.stop
18
+ end
19
+ end
20
+
21
+ expect(messages).to have_attributes connection_established: true, id_present: true,
22
+ last_event: 'an_event', last_data: { some: "Mit Raben Und Wölfen" }.to_json
23
+ end
24
+
25
+ it 'does not send message to excluded sockets' do
26
+ messages = em_stream do |websocket, messages|
27
+ case messages.length
28
+ when 1
29
+ websocket.callback { websocket.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json) }
30
+ when 2
31
+ socket_id = JSON.parse(messages.first["data"])["socket_id"]
32
+ Pusher['MY_CHANNEL'].trigger 'not_excluded_socket_event', { some: "Mit Raben Und Wölfen" }
33
+ Pusher['MY_CHANNEL'].trigger 'excluded_socket_event', { some: "Mit Raben Und Wölfen" }, socket_id
34
+ when 3
35
+ EM.stop
36
+ end
37
+ end
38
+
39
+ expect(messages).to have_attributes connection_established: true, id_present: true,
40
+ last_event: 'not_excluded_socket_event', last_data: { some: "Mit Raben Und Wölfen" }.to_json
41
+ end
42
+
43
+ it 'enforces one subcription per channel, per socket' do
44
+ messages = em_stream do |websocket, messages|
45
+ case messages.length
46
+ when 1
47
+ websocket.callback { websocket.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json) }
48
+ when 2
49
+ websocket.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json)
50
+ when 3
51
+ EM.stop
52
+ end
53
+ end
54
+
55
+ expect(messages.last).to eq({"event"=>"pusher:error", "data"=>"{\"code\":null,\"message\":\"Existing subscription to MY_CHANNEL\"}"})
56
+ end
57
+
58
+ it 'supports unsubscribing to channels without closing the socket' do
59
+ client2_messages = nil
60
+
61
+ messages = em_stream do |client, messages|
62
+ case messages.length
63
+ when 1
64
+ client.callback { client.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json) }
65
+ when 2
66
+ client.send({ event: 'pusher:unsubscribe', data: { channel: 'MY_CHANNEL'} }.to_json)
67
+
68
+ client2_messages = em_stream do |client2, client2_messages|
69
+ case client2_messages.length
70
+ when 1
71
+ client2.callback { client2.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json) }
72
+ when 2
73
+ Pusher['MY_CHANNEL'].trigger 'an_event', { some: 'data' }
74
+ EM.next_tick { EM.stop }
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ expect(messages).to have_attributes connection_established: true, id_present: true,
81
+ last_event: 'pusher_internal:subscription_succeeded', count: 2
82
+ end
83
+
84
+ it 'avoids sending duplicate events' do
85
+ client2_messages = nil
86
+
87
+ client1_messages = em_stream do |client1, client1_messages|
88
+ # if this is the first message to client 1 set up another connection from the same client
89
+ if client1_messages.one?
90
+ client1.callback do
91
+ client1.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json)
92
+ end
93
+
94
+ client2_messages = em_stream do |client2, client2_messages|
95
+ case client2_messages.length
96
+ when 1
97
+ client2.callback { client2.send({ event: 'pusher:subscribe', data: { channel: 'MY_CHANNEL'} }.to_json) }
98
+ when 2
99
+ socket_id = JSON.parse(client1_messages.first['data'])['socket_id']
100
+ Pusher['MY_CHANNEL'].trigger 'an_event', { some: 'data' }, socket_id
101
+ when 3
102
+ EM.stop
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ expect(client1_messages).to have_attributes count: 2
109
+
110
+ expect(client2_messages).to have_attributes last_event: 'an_event',
111
+ last_data: { some: 'data' }.to_json
112
+ end
113
+ end
114
+ end