kalebr-pusher 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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