slangerq 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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