oceanex-slanger 0.7.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.blank? || invalid_signature?
5
+
6
+ Subscription.new(connection.socket, connection.socket_id, @msg).subscribe
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,65 @@
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_delegators :subscriber, :subscribe
12
+
13
+ def regular_connection
14
+ @regular_connection ||= new_connection
15
+ end
16
+
17
+ def publisher
18
+ @publisher ||= new_connection
19
+ end
20
+
21
+ def send_command(method, *arg)
22
+ regular_connection.send(method, *arg)
23
+ end
24
+
25
+ def hincrby(*arg)
26
+ send_command :hincrby, *arg
27
+ end
28
+
29
+ def hset(*arg)
30
+ send_command :hset, *arg
31
+ end
32
+
33
+ def hdel(*arg)
34
+ send_command :hdel, *arg
35
+ end
36
+
37
+ def hgetall(*arg)
38
+ send_command :hgetall, *arg
39
+ end
40
+
41
+ def publish(*arg)
42
+ publish_event(:publish, *arg)
43
+ end
44
+
45
+ def publish_event(method, *args)
46
+ publisher.send(method, *args)
47
+ end
48
+
49
+ def subscriber
50
+ @subscriber ||= new_connection.pubsub.tap do |c|
51
+ c.on(:message) do |channel, message|
52
+ message = Oj.strict_load(message)
53
+ c = Channel.from message["channel"]
54
+ c.dispatch message, channel
55
+ end
56
+ end
57
+ end
58
+
59
+ def new_connection
60
+ EM::Hiredis.connect Slanger::Config.redis_address
61
+ end
62
+
63
+ extend self
64
+ end
65
+ 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.7.1"
3
+ end
@@ -0,0 +1,38 @@
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
+
36
+ extend self
37
+ end
38
+ 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
@@ -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,65 @@
1
+ module SlangerHelperMethods
2
+ class HaveAttributes
3
+ attr_reader :messages, :attributes
4
+
5
+ def initialize(attributes)
6
+ @attributes = attributes
7
+ end
8
+
9
+ CHECKS = %w(first_event last_event last_data)
10
+
11
+ def matches?(messages)
12
+ @messages = messages
13
+ @failures = []
14
+
15
+ check_connection_established if attributes[:connection_established]
16
+ check_id_present if attributes[:id_present]
17
+
18
+ CHECKS.each { |a| attributes[a.to_sym] ? check(a) : true }
19
+
20
+ @failures.empty?
21
+ end
22
+
23
+ def check(message)
24
+ send(message) == attributes[message.to_sym] or @failures << message
25
+ end
26
+
27
+ def failure_message
28
+ @failures.map { |f| "expected #{f}: to equal #{attributes[f]} but got #{send(f)}" }.join "\n"
29
+ end
30
+
31
+ # private
32
+
33
+ def check_connection_established
34
+ if first_event != "pusher:connection_established"
35
+ @failures << :connection_established
36
+ end
37
+ end
38
+
39
+ def check_id_present
40
+ if messages.first["data"]["socket_id"] == nil
41
+ @failures << :id_present
42
+ end
43
+ end
44
+
45
+ def first_event
46
+ messages.first["event"]
47
+ end
48
+
49
+ def last_event
50
+ messages.last["event"]
51
+ end
52
+
53
+ def last_data
54
+ messages.last["data"]
55
+ end
56
+
57
+ def count
58
+ messages.length
59
+ end
60
+ end
61
+
62
+ def have_attributes(attributes)
63
+ HaveAttributes.new attributes
64
+ end
65
+ end
@@ -0,0 +1,113 @@
1
+ #encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe "Integration:" do
5
+ before(:each) { start_slanger }
6
+
7
+ describe "channel" do
8
+ it "pushes messages to interested websocket connections" do
9
+ messages = em_stream do |websocket, messages|
10
+ case messages.length
11
+ when 1
12
+ websocket.callback { websocket.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json) }
13
+ when 2
14
+ Pusher["MY_CHANNEL"].trigger "an_event", some: "Mit Raben Und Wölfen"
15
+ when 3
16
+ EM.stop
17
+ end
18
+ end
19
+
20
+ expect(messages).to have_attributes connection_established: true, id_present: true,
21
+ last_event: "an_event", last_data: { some: "Mit Raben Und Wölfen" }.to_json
22
+ end
23
+
24
+ it "does not send message to excluded sockets" do
25
+ messages = em_stream do |websocket, messages|
26
+ case messages.length
27
+ when 1
28
+ websocket.callback { websocket.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json) }
29
+ when 2
30
+ socket_id = JSON.parse(messages.first["data"])["socket_id"]
31
+ Pusher["MY_CHANNEL"].trigger "not_excluded_socket_event", { some: "Mit Raben Und Wölfen" }
32
+ Pusher["MY_CHANNEL"].trigger "excluded_socket_event", { some: "Mit Raben Und Wölfen" }, socket_id
33
+ when 3
34
+ EM.stop
35
+ end
36
+ end
37
+
38
+ expect(messages).to have_attributes connection_established: true, id_present: true,
39
+ last_event: "not_excluded_socket_event", last_data: { some: "Mit Raben Und Wölfen" }.to_json
40
+ end
41
+
42
+ it "enforces one subcription per channel, per socket" do
43
+ messages = em_stream do |websocket, messages|
44
+ case messages.length
45
+ when 1
46
+ websocket.callback { websocket.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json) }
47
+ when 2
48
+ websocket.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json)
49
+ when 3
50
+ EM.stop
51
+ end
52
+ end
53
+
54
+ expect(messages.last).to eq({ "event" => "pusher:error", "data" => "{\"code\":null,\"message\":\"Existing subscription to MY_CHANNEL\"}" })
55
+ end
56
+
57
+ it "supports unsubscribing to channels without closing the socket" do
58
+ client2_messages = nil
59
+
60
+ messages = em_stream do |client, messages|
61
+ case messages.length
62
+ when 1
63
+ client.callback { client.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json) }
64
+ when 2
65
+ client.send({ event: "pusher:unsubscribe", data: { channel: "MY_CHANNEL" } }.to_json)
66
+
67
+ client2_messages = em_stream do |client2, client2_messages|
68
+ case client2_messages.length
69
+ when 1
70
+ client2.callback { client2.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json) }
71
+ when 2
72
+ Pusher["MY_CHANNEL"].trigger "an_event", { some: "data" }
73
+ EM.next_tick { EM.stop }
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ expect(messages).to have_attributes connection_established: true, id_present: true,
80
+ last_event: "pusher_internal:subscription_succeeded", count: 2
81
+ end
82
+
83
+ it "avoids sending duplicate events" do
84
+ client2_messages = nil
85
+
86
+ client1_messages = em_stream do |client1, client1_messages|
87
+ # if this is the first message to client 1 set up another connection from the same client
88
+ if client1_messages.one?
89
+ client1.callback do
90
+ client1.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json)
91
+ end
92
+
93
+ client2_messages = em_stream do |client2, client2_messages|
94
+ case client2_messages.length
95
+ when 1
96
+ client2.callback { client2.send({ event: "pusher:subscribe", data: { channel: "MY_CHANNEL" } }.to_json) }
97
+ when 2
98
+ socket_id = JSON.parse(client1_messages.first["data"])["socket_id"]
99
+ Pusher["MY_CHANNEL"].trigger "an_event", { some: "data" }, socket_id
100
+ when 3
101
+ EM.stop
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ expect(client1_messages).to have_attributes count: 2
108
+
109
+ expect(client2_messages).to have_attributes last_event: "an_event",
110
+ last_data: { some: "data" }.to_json
111
+ end
112
+ end
113
+ end