kalebr-pusher 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0f501a6e7ae0150b3488b530dff35f2414706c28
4
+ data.tar.gz: b5d894b0e0c269434b2edad9eb0c6de3b60909e3
5
+ SHA512:
6
+ metadata.gz: 2fb04dd4ef2724efa44103e5964a70ad265a3f9688d488c16410eabb972d1cf14d6f8fa1a2fb409d8c73e8b76ec272e0319741cfbd6bf4314105a285d3b610e7
7
+ data.tar.gz: dba884b83725ddb7827a4847a018f19b4e9aaa7ba501eb240c830a9a76fc2529e96b11ab36bea90d604fdf15c27fc9a4a1108adc1685e63dff4076db7f9163e6
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # How to use it
2
+
3
+ ## Requirements
4
+
5
+ - Ruby 2.1.2 or greater
6
+ - Redis
7
+
8
+ ## Server setup
9
+
10
+ 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:
11
+ Add to `/etc/sysctl.conf`:
12
+ ```
13
+ fs.file-max = 50000
14
+ ```
15
+ Add to `/etc/security/limits.conf`:
16
+ ```
17
+ * hard nofile 50000
18
+ * soft nofile 50000
19
+ * hard nproc 50000
20
+ * soft nproc 50000
21
+ ```
22
+
23
+ ```bash
24
+ $ kalerbr-pusher --app_key 765ec374ae0a69f4ce44 --secret your-pusher-secret
25
+ ```
26
+
27
+ If all went to plan you should see the following output to STDOUT
28
+
29
+
30
+
31
+ kalebr-pusher API server listening on port 4567
32
+ kalebr-pusher WebSocket server listening on port 8080
33
+
34
+
35
+ ## Modifying your application code to use the Kalebr service
36
+
37
+
38
+
39
+ ```ruby
40
+ ...
41
+
42
+ Pusher.host = 'kalebr.example.com'
43
+ Pusher.port = 4567
44
+ ```
45
+
46
+ You will also need to do the same to the Pusher JavaScript client in your client side JavaScript, e.g
47
+
48
+ ```html
49
+ <script type="text/javascript">
50
+ var pusher = new Pusher('#{Pusher.key}', {
51
+ wsHost: "0.0.0.0",
52
+ wsPort: "8080",
53
+ wssPort: "8080",
54
+ enabledTransports: ['ws', 'flash']
55
+ });
56
+ </script>
57
+ ```
58
+
59
+ Of course you could proxy all requests to `ws.example.com` to port 8080 of your kalerbr-pusher node and `api.example.com` to port 4567 of your kalerbr-pusher node for example, that way you would only need to set the host property of the Pusher client.
60
+
61
+ # Author
62
+
63
+ - Nilanga Saluwadana
64
+
65
+
66
+ &copy; 2016
data/bin/kalebr-pusher ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'optparse'
5
+ require 'eventmachine'
6
+ require 'yaml'
7
+ require 'active_support/core_ext/hash'
8
+
9
+ options = {}
10
+
11
+
12
+ OptionParser.new do |opts|
13
+ opts.on '-h', '--help', 'Display this screen' do
14
+ puts opts
15
+ exit
16
+ end
17
+
18
+ opts.on '-k', '--app_key APP_KEY', "Pusher application key. This parameter is required on command line or in optional config file." do |k|
19
+ options[:app_key] = k
20
+ end
21
+
22
+ opts.on '-s', '--secret SECRET', "Pusher application secret. This parameter is required on command line or in optional config file." do |k|
23
+ options[:secret] = k
24
+ end
25
+
26
+ opts.on '-C', '--config_file FILE', "Path to Yaml file that can contain all configuration options, including required ones." do |k|
27
+ options[:config_file] = k
28
+ end
29
+
30
+ opts.on '-r', '--redis_address URL', "Address to bind to (Default: redis://127.0.0.1:6379/0)" do |h|
31
+ options[:redis_address] = h
32
+ end
33
+
34
+ opts.on '-a', '--api_host HOST', "API service address (Default: 0.0.0.0:4567)" do |p|
35
+ options[:api_host], options[:api_port] = p.split(':')
36
+ end
37
+
38
+ opts.on '-w', '--websocket_host HOST', "WebSocket service address (Default: 0.0.0.0:8080)" do |p|
39
+ options[:websocket_host], options[:websocket_port] = p.split(':')
40
+ end
41
+
42
+ opts.on '-i', '--require FILE', "Require a file before starting pusher" do |p|
43
+ options[:require] ||= []
44
+ options[:require] << p
45
+ end
46
+
47
+ opts.on '-p', '--private_key_file FILE', "Private key file for SSL transport" do |p|
48
+ options[:tls_options] ||= {}
49
+ options[:tls_options][:private_key_file] = p
50
+ end
51
+
52
+ opts.on '-b', '--webhook_url URL', "Callback URL for webhooks" do |p|
53
+ options[:webhook_url] = p
54
+ end
55
+
56
+ opts.on '-c', '--cert_file FILE', "Certificate file for SSL transport" do |p|
57
+ options[:tls_options] ||= {}
58
+ options[:tls_options][:cert_chain_file] = p
59
+ end
60
+
61
+ opts.on "-v", "--[no-]verbose", "Run verbosely" do |v|
62
+ options[:debug] = v
63
+ end
64
+
65
+ opts.on "-t" "--activity_timeout", "Activity (ping-pong) Timeout" do |t|
66
+ options[:activity_timeout] = t
67
+ end
68
+
69
+ opts.on '--pid_file PIDFILE', "The Kalebr-pusher process ID file name." do |k|
70
+ options[:pid_file] = k
71
+ end
72
+
73
+ opts.parse!
74
+
75
+ if options[:config_file] and File.exists? options[:config_file]
76
+ config_file_contents = YAML::load(File.open(options[:config_file]))
77
+ options.reverse_merge! config_file_contents.deep_symbolize_keys!
78
+ end
79
+
80
+ %w<app_key secret>.each do |parameter|
81
+ unless options[parameter.to_sym]
82
+ puts "--#{parameter} STRING is a required argument. Use your Pusher #{parameter}.\n"
83
+ puts opts
84
+ exit
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+
91
+
92
+ if options[:tls_options]
93
+ [:cert_chain_file, :private_key_file].each do |param|
94
+ raise RuntimeError.new "Both --cert_file and --private_key_file need to be specified" unless options[:tls_options][param]
95
+ raise RuntimeError.new "--#{param} does not exist at `#{options[:tls_options][param]}`" unless File.exists? options[:tls_options][param]
96
+ end
97
+ end
98
+
99
+ STDOUT.sync = true
100
+
101
+ case
102
+ when EM.epoll? then EM.epoll
103
+ when EM.kqueue? then EM.kqueue
104
+ end
105
+
106
+ EM.run do
107
+ File.tap { |f| require f.expand_path(f.join(f.dirname(__FILE__),'..', 'pusher.rb')) }
108
+ Slanger::Config.load options
109
+
110
+ # Write PID to file
111
+ unless options[:pid_file].nil?
112
+ File.open(options[:pid_file], 'w') { |f| f.puts Process.pid }
113
+ end
114
+
115
+ Slanger::Service.run
116
+
117
+ puts "\n"
118
+ puts "\n"
119
+ puts "\n"
120
+
121
+ puts " PPPPPPPPPPPPPPPPP hhhhhhh\n"
122
+ puts " P::::::::::::::::P h:::::h\n"
123
+ puts " P::::::PPPPPP:::::P h:::::h\n"
124
+ puts " PP:::::P P:::::P h:::::h\n"
125
+ puts " P::::P P:::::uuuuuu uuuuuu ssssssssss h::::h hhhhh eeeeeeeeeeee rrrrr rrrrrrrrr\n"
126
+ puts " P::::P P:::::u::::u u::::u ss::::::::::s h::::hh:::::hhh ee::::::::::::ee r::::rrr:::::::::r\n"
127
+ puts " P::::PPPPPP:::::Pu::::u u::::u ss:::::::::::::sh::::::::::::::hh e::::::eeeee:::::er:::::::::::::::::r\n"
128
+ puts " P:::::::::::::PP u::::u u::::u s::::::ssss:::::h:::::::hhh::::::he::::::e e:::::rr::::::rrrrr::::::r\n"
129
+ puts " P::::PPPPPPPPP u::::u u::::u s:::::s ssssssh::::::h h::::::e:::::::eeeee::::::er:::::r r:::::r\n"
130
+ puts " P::::P u::::u u::::u s::::::s h:::::h h:::::e:::::::::::::::::e r:::::r rrrrrrr\n"
131
+ puts " P::::P u::::u u::::u s::::::s h:::::h h:::::e::::::eeeeeeeeeee r:::::r\n"
132
+ puts " P::::P u:::::uuuu:::::u ssssss s:::::sh:::::h h:::::e:::::::e r:::::r\n"
133
+ puts " PP::::::PP u:::::::::::::::us:::::ssss::::::h:::::h h:::::e::::::::e r:::::r\n"
134
+ puts " P::::::::P u:::::::::::::::s::::::::::::::sh:::::h h:::::he::::::::eeeeeeee r:::::r \n"
135
+ puts " P::::::::P uu::::::::uu:::us:::::::::::ss h:::::h h:::::h ee:::::::::::::e r:::::r\n"
136
+ puts " PPPPPPPPPP uuuuuuuu uuuu sssssssssss hhhhhhh hhhhhhh eeeeeeeeeeeeee rrrrrrr\n"
137
+ puts "\n"
138
+ puts "\n"
139
+
140
+ puts "Running Kalebr-pusher v.#{Slanger::VERSION}"
141
+ puts "\n"
142
+
143
+ puts "Kalebr-pusher API server listening on port #{Slanger::Config.api_port}"
144
+ puts "Kalebr-pusher WebSocket server listening on port #{Slanger::Config.websocket_port}"
145
+ end
146
+
147
+
148
+
149
+
150
+
151
+
152
+
@@ -0,0 +1,5 @@
1
+ module Slanger
2
+ module Api
3
+ InvalidRequest = Class.new ArgumentError
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ require 'oj'
2
+
3
+ module Slanger
4
+ module Api
5
+ class Event < Struct.new :name, :data, :socket_id
6
+ def payload(channel_id)
7
+ Oj.dump({
8
+ event: name,
9
+ data: data,
10
+ channel: channel_id,
11
+ socket_id: socket_id
12
+ }.select { |_,v| v }, mode: :compat)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -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,105 @@
1
+ require 'oj'
2
+
3
+ module Slanger
4
+ module Api
5
+ class RequestValidation < Struct.new :raw_body, :raw_params, :path_info
6
+ def initialize(*args)
7
+ super(*args)
8
+
9
+ validate!
10
+ authenticate!
11
+ parse_body!
12
+ end
13
+
14
+ def data
15
+ @data ||= Oj.load(body["data"] || params["data"])
16
+ end
17
+
18
+ def body
19
+ @body ||= validate_body!
20
+ end
21
+
22
+ def auth_params
23
+ params.except('channel_id', 'app_id')
24
+ end
25
+
26
+ def socket_id
27
+ @socket_id ||= determine_valid_socket_id
28
+ end
29
+
30
+ def params
31
+ @params ||= validate_raw_params!
32
+ end
33
+
34
+ def channels
35
+ @channels ||= Array(body["channels"] || params["channels"])
36
+ end
37
+
38
+ private
39
+
40
+ def validate_body!
41
+ @body ||= assert_valid_json!(raw_body.tap{ |s| s.force_encoding('utf-8')})
42
+ end
43
+
44
+ def validate!
45
+ raise InvalidRequest.new "no body" unless raw_body.present?
46
+ raise InvalidRequest.new "invalid params" unless raw_params.is_a? Hash
47
+ raise InvalidRequest.new "invalid path" unless path_info.is_a? String
48
+
49
+ determine_valid_socket_id
50
+ channels.each{|id| validate_channel_id!(id)}
51
+ end
52
+
53
+ def validate_socket_id!(socket_id)
54
+ validate_with_regex!(/\A\d+\.\d+\z/, socket_id, "socket_id")
55
+ end
56
+
57
+ def validate_channel_id!(channel_id)
58
+ validate_with_regex!(/\A[\w@\-;_.=,]{1,164}\z/, channel_id, "channel_id")
59
+ end
60
+
61
+ def validate_with_regex!(regex, value, name)
62
+ raise InvalidRequest, "Invalid #{name} #{value.inspect}" unless value =~ regex
63
+
64
+ value
65
+ end
66
+
67
+ def validate_raw_params!
68
+ restricted = user_params.slice "body_md5", "auth_version", "auth_key", "auth_timestamp", "auth_signature", "app_id"
69
+
70
+ invalid_keys = restricted.keys - user_params.keys
71
+
72
+ if invalid_keys.any?
73
+ raise Slanger::InvalidRequest.new "Invalid params: #{invalid_keys}"
74
+ end
75
+
76
+ restricted
77
+ end
78
+
79
+ def authenticate!
80
+ # Raises Signature::AuthenticationError if request does not authenticate.
81
+ Signature::Request.new('POST', path_info, auth_params).
82
+ authenticate { |key| Signature::Token.new key, Slanger::Config.secret }
83
+ end
84
+
85
+ def parse_body!
86
+ assert_valid_json!(raw_body)
87
+ end
88
+
89
+ def assert_valid_json!(string)
90
+ Oj.load(string)
91
+ rescue Oj::ParserError
92
+ raise Slanger::InvalidRequest.new("Invalid request body: #{raw_body}")
93
+ end
94
+
95
+ def determine_valid_socket_id
96
+ return validate_socket_id!(body["socket_id"]) if body["socket_id"]
97
+ return validate_socket_id!(params["socket_id"]) if params["socket_id"]
98
+ end
99
+
100
+ def user_params
101
+ raw_params.reject{|k,_| %w(splat captures).include?(k)}
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,57 @@
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
+ require 'oj'
12
+
13
+ module Slanger
14
+ module Api
15
+ class Server < Sinatra::Base
16
+ use Rack::FiberPool
17
+ set :raise_errors, lambda { false }
18
+ set :show_exceptions, false
19
+
20
+ error(Signature::AuthenticationError) { |e| halt 401, "401 UNAUTHORIZED" }
21
+ error(Slanger::Api::InvalidRequest) { |c| halt 400, "400 Bad Request" }
22
+
23
+ before do
24
+ valid_request
25
+ end
26
+
27
+ post '/apps/:app_id/events' do
28
+ socket_id = valid_request.socket_id
29
+ body = valid_request.body
30
+
31
+ event = Slanger::Api::Event.new(body["name"], body["data"], socket_id)
32
+ EventPublisher.publish(valid_request.channels, event)
33
+
34
+ status 202
35
+ return Oj.dump({}, mode: :compat)
36
+ end
37
+
38
+ post '/apps/:app_id/channels/:channel_id/events' do
39
+ params = valid_request.params
40
+
41
+ event = Event.new(params["name"], valid_request.body, valid_request.socket_id)
42
+ EventPublisher.publish(valid_request.channels, event)
43
+
44
+ status 202
45
+ return Oj.dump({}, mode: :compat)
46
+ end
47
+
48
+ def valid_request
49
+ @valid_request ||=
50
+ begin
51
+ request_body ||= request.body.read.tap{|s| s.force_encoding("utf-8")}
52
+ RequestValidation.new(request_body, params, env["PATH_INFO"])
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,106 @@
1
+ # Channel class.
2
+ #
3
+ # Uses an EventMachine channel to let clients interact with the
4
+ # Pusher channel. Relay events received from Redis into the
5
+ # EM channel.
6
+ #
7
+
8
+ require 'eventmachine'
9
+ require 'forwardable'
10
+ require 'oj'
11
+
12
+ module Slanger
13
+ class Channel
14
+ extend Forwardable
15
+
16
+ def_delegators :channel, :push
17
+ attr_reader :channel_id
18
+
19
+ class << self
20
+ def from channel_id
21
+ klass = channel_id[/\Apresence-/] ? PresenceChannel : Channel
22
+
23
+ klass.lookup(channel_id) || klass.create(channel_id: channel_id)
24
+ end
25
+
26
+ def lookup(channel_id)
27
+ all.detect { |o| o.channel_id == channel_id }
28
+ end
29
+
30
+ def create(params = {})
31
+ new(params).tap { |r| all << r }
32
+ end
33
+
34
+ def all
35
+ @all ||= []
36
+ end
37
+
38
+ def unsubscribe channel_id, subscription_id
39
+ from(channel_id).try :unsubscribe, subscription_id
40
+ end
41
+
42
+ def send_client_message msg
43
+ from(msg['channel']).try :send_client_message, msg
44
+ end
45
+ end
46
+
47
+ def initialize(attrs)
48
+ @channel_id = attrs.with_indifferent_access[:channel_id]
49
+ Slanger::Redis.subscribe channel_id
50
+ end
51
+
52
+ def channel
53
+ @channel ||= EM::Channel.new
54
+ end
55
+
56
+ def subscribe *a, &blk
57
+ Slanger::Redis.hincrby('channel_subscriber_count', channel_id, 1).
58
+ callback do |value|
59
+ Slanger::Webhook.post name: 'channel_occupied', channel: channel_id if value == 1
60
+ end
61
+
62
+ channel.subscribe *a, &blk
63
+ end
64
+
65
+ def unsubscribe *a, &blk
66
+ Slanger::Redis.hincrby('channel_subscriber_count', channel_id, -1).
67
+ callback do |value|
68
+ Slanger::Webhook.post name: 'channel_vacated', channel: channel_id if value == 0
69
+ end
70
+
71
+ channel.unsubscribe *a, &blk
72
+ end
73
+
74
+
75
+ # Send a client event to the EventMachine channel.
76
+ # Only events to channels requiring authentication (private or presence)
77
+ # are accepted. Public channels only get events from the API.
78
+ def send_client_message(message)
79
+ Slanger::Redis.publish(message['channel'], Oj.dump(message, mode: :compat)) if authenticated?
80
+ end
81
+
82
+ # Send an event received from Redis to the EventMachine channel
83
+ # which will send it to subscribed clients.
84
+ def dispatch(message, channel)
85
+ push(Oj.dump(message, mode: :compat)) unless channel =~ /\Aslanger:/
86
+
87
+ perform_client_webhook!(message)
88
+ end
89
+
90
+ def authenticated?
91
+ channel_id =~ /\Aprivate-/ || channel_id =~ /\Apresence-/
92
+ end
93
+
94
+ private
95
+
96
+ def perform_client_webhook!(message)
97
+ if (message['event'].start_with?('client-')) then
98
+
99
+ event = message.merge({'name' => 'client_event'})
100
+ event['data'] = Oj.dump(event['data'])
101
+
102
+ Slanger::Webhook.post(event)
103
+ end
104
+ end
105
+ end
106
+ end