slangerq 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
+ SHA256:
3
+ metadata.gz: 1d0b5ffcd0d29e6c4f50f655a1b2fc03508360b208d70482382d3f0938830632
4
+ data.tar.gz: 02b2177c14299ab6aba2721fde5cf7542fde18ce02bd70f64f127f5caf9ff559
5
+ SHA512:
6
+ metadata.gz: 567ef99d121dd12c79f18398b583b7376b79ab965690bb82321bef997f1a9e10ff7b6353a6b19d05493cf824b1b782cd42fa969eb3b8b3c57666cc7aece38f14
7
+ data.tar.gz: 9aede7eec67ea91b8eb86701023f1b3f54721472c0fb6570a00f2da974d6fb68fb5b69bc6836d1bc01cd5b647cbd19df93244ce9c2d5b5323528a375f3b70281
data/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # Slanger
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/slanger.svg)](http://badge.fury.io/rb/slanger) [![Build Status](https://travis-ci.org/stevegraham/slanger.svg?branch=master)](https://travis-ci.org/stevegraham/slanger)
4
+
5
+ **Important! Slanger is not supposed to be included in your Gemfile. RubyGems is used as a distribution mechanism. If you include it in your app, you will likely get dependency conflicts. PRs updating dependencies for compatibility with your app will be closed. Thank you for reading and enjoy Slanger!**
6
+
7
+ ## Typical usage
8
+
9
+ ```
10
+ gem install slanger
11
+ redis-server &> /dev/null &
12
+
13
+ slanger --app_key 765ec374ae0a69f4ce44 --secret your-pusher-secret
14
+ ```
15
+
16
+ Slanger is a standalone server ruby implementation of the Pusher protocol. It
17
+ is not designed to run inside a Rails or sinatra app, but it can be easily
18
+ installed as a gem.
19
+
20
+ Bundler has multiple purposes, one of which is useful for installation.
21
+
22
+ ## About
23
+
24
+ Slanger is an open source server implementation of the Pusher protocol written
25
+ in Ruby. It is designed to scale horizontally across N nodes and to be agnostic
26
+ as to which Slanger node a subscriber is connected to, i.e subscribers to the
27
+ same channel are NOT required to be connected to the same Slanger node.
28
+ Multiple Slanger nodes can sit behind a load balancer with no special
29
+ configuration. In essence it was designed to be very easy to scale.
30
+
31
+ Presence channel state is shared using Redis. Channels are lazily instantiated
32
+ internally within a given Slanger node when the first subscriber connects. When
33
+ a presence channel is instantiated within a Slanger node, it queries Redis for
34
+ the global state across all nodes within the system for that channel, and then
35
+ copies that state internally. Afterwards, when subscribers connect or
36
+ disconnect the node publishes a presence message to all interested nodes, i.e.
37
+ all nodes with at least one subscriber interested in the given channel.
38
+
39
+ Slanger is smart enough to know if a new channel subscription belongs to the
40
+ same user. It will not send presence messages to subscribers in this case. This
41
+ happens when the user has multiple browser tabs open for example. Using a chat
42
+ room backed by presence channels as a real example, one would not want
43
+ "Barrington" to show up N times in the presence roster because Barrington
44
+ has the chat room open in N browser tabs.
45
+
46
+ Slanger was designed to be highly available and partition tolerant with
47
+ eventual consistency, which in practise is instantaneous.
48
+
49
+ # How to use it
50
+
51
+ ## Requirements
52
+
53
+ - Ruby 2.1.2 or greater
54
+ - Redis
55
+
56
+ ## Server setup
57
+
58
+ 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:
59
+ Add to `/etc/sysctl.conf`:
60
+ ```
61
+ fs.file-max = 50000
62
+ ```
63
+ Add to `/etc/security/limits.conf`:
64
+ ```
65
+ * hard nofile 50000
66
+ * soft nofile 50000
67
+ * hard nproc 50000
68
+ * soft nproc 50000
69
+ ```
70
+
71
+ ## Cluster load-balancing setup with Haproxy
72
+
73
+ If you want to run multiple slanger instances in a cluster, one option will be to balance the connections with Haproxy.
74
+ A basic config can be found in the folder `examples`.
75
+ Haproxy can be also used for SSL termination, leaving slanger to not have to deal with SSL checks and so on, making it lighter.
76
+
77
+
78
+ ## Starting the service
79
+
80
+ Slanger is packaged as a Rubygem. Installing the gem makes the 'slanger' executable available. The `slanger` executable takes arguments, of which two are mandatory: `--app_key` and `--secret`. These can but do not have to be the same as the credentials you use for Pusher. They are required because Slanger performs the same HMAC signing of API requests that Pusher does.
81
+
82
+ __IMPORTANT:__ Redis must be running where Slanger expects it to be (either on localhost:6379 or somewhere else you told Slanger it would be using the option flag) or Slanger will fail silently. I haven't yet figured out how to get em-hiredis to treat an unreachable host as an error
83
+
84
+ ```bash
85
+ $ gem install slanger
86
+
87
+ $ redis-server &> /dev/null &
88
+
89
+ $ slanger --app_key 765ec374ae0a69f4ce44 --secret your-pusher-secret
90
+ ```
91
+
92
+ If all went to plan you should see the following output to STDOUT
93
+
94
+ ```
95
+
96
+ .d8888b. 888
97
+ d88P Y88b 888
98
+ Y88b. 888
99
+ "Y888b. 888 8888b. 88888b. .d88b. .d88b. 888d888
100
+ "Y88b. 888 "88b 888 "88b d88P"88b d8P Y8b 888P"
101
+ "888 888 .d888888 888 888 888 888 88888888 888
102
+ Y88b d88P 888 888 888 888 888 Y88b 888 Y8b. 888
103
+ "Y8888P" 888 "Y888888 888 888 "Y88888 "Y8888 888
104
+ 888
105
+ Y8b d88P
106
+ "Y88P"
107
+
108
+
109
+ Slanger API server listening on port 4567
110
+ Slanger WebSocket server listening on port 8080
111
+ ```
112
+
113
+ ## Ubuntu upstart script
114
+
115
+ If you're using Ubuntu, you might find this upscript very helpful. The steps below will create an init script that will make slanger run at boot and restart if it fails.
116
+ Open `/etc/init/slanger` and add:
117
+ ```
118
+ start on started networking and runlevel [2345]
119
+ stop on runlevel [016]
120
+ respawn
121
+ script
122
+ LANG=en_US.UTF-8 /usr/local/rvm/gems/ruby-RUBY_VERISON/wrappers/slanger --app_key KEY --secret SECRET --redis_address redis://REDIS_IP:REDIS_PORT/REDIS_DB
123
+ end script
124
+ ```
125
+ This example assumes you're using rvm and a custom redis configuration
126
+
127
+ Then, to start / stop the service, just do
128
+ ```
129
+ service slanger start
130
+ service slanger stop
131
+ ```
132
+
133
+
134
+ ## Modifying your application code to use the Slanger service
135
+
136
+ Once you have a Slanger instance listening for incoming connections you need to alter you application code to use the Slanger endpoint instead of Pusher. Fortunately this is very simple, unobtrusive, easily reversable, and very painless.
137
+
138
+
139
+ First you will need to add code to your server side component that publishes events to the Pusher HTTP REST API, usually this means telling the Pusher client to use a different host and port, e.g. consider this Ruby example
140
+
141
+ ```ruby
142
+ ...
143
+
144
+ Pusher.host = 'slanger.example.com'
145
+ Pusher.port = 4567
146
+ ```
147
+
148
+ You will also need to do the same to the Pusher JavaScript client in your client side JavaScript, e.g
149
+
150
+ ```html
151
+ <script type="text/javascript">
152
+ var pusher = new Pusher('#{Pusher.key}', {
153
+ wsHost: "0.0.0.0",
154
+ wsPort: "8080",
155
+ wssPort: "8080",
156
+ enabledTransports: ['ws', 'flash']
157
+ });
158
+ </script>
159
+ ```
160
+
161
+ Of course you could proxy all requests to `ws.example.com` to port 8080 of your Slanger node and `api.example.com` to port 4567 of your Slanger node for example, that way you would only need to set the host property of the Pusher client.
162
+
163
+ # Configuration Options
164
+
165
+ Slanger supports several configuration options, which can be supplied as command line arguments at invocation. You can also supply a yaml file containing config options. If you use the config file in combination with other configuration options, the values passed on the command line will win. Allows running multiple instances with only a few differences easy.
166
+
167
+ ```
168
+ -k or --app_key This is the Pusher app key you want to use. This is a required argument on command line or in optional config file
169
+
170
+ -s or --secret This is your Pusher secret. This is a required argument on command line or in optional config file
171
+
172
+ -C or --config_file Path to Yaml file that can contain all or some of the configuration options, including required arguments
173
+
174
+ -r or --redis_address An address where there is a Redis server running. This is an optional argument and defaults to redis://127.0.0.1:6379/0
175
+
176
+ -a or --api_host This is the address that Slanger will bind the HTTP REST API part of the service to. This is an optional argument and defaults to 0.0.0.0:4567
177
+
178
+ -w or --websocket_host This is the address that Slanger will bind the WebSocket part of the service to. This is an optional argument and defaults to 0.0.0.0:8080
179
+
180
+ -i or --require Require an additional file before starting Slanger to tune it to your needs. This is an optional argument
181
+
182
+ -p or --private_key_file Private key file for SSL support. This argument is optional, if given, SSL will be enabled
183
+
184
+ -b or --webhook_url URL for webhooks. This argument is optional, if given webhook callbacks will be made http://pusher.com/docs/webhooks
185
+
186
+ -c or --cert_file Certificate file for SSL support. This argument is optional, if given, SSL will be enabled
187
+
188
+ -v or --[no-]verbose This makes Slanger run verbosely, meaning WebSocket frames will be echoed to STDOUT. Useful for debugging
189
+
190
+ --pid_file The path to a file you want slanger to write it's PID to. Optional.
191
+ ```
192
+
193
+ # Why use Slanger instead of Pusher?
194
+
195
+ There a few reasons you might want to use Slanger instead of Pusher, e.g.
196
+
197
+ - You operate in a heavily regulated industry and are worried about sending data to 3rd parties, and it is an organisational requirement that you own your own infrastructure.
198
+ - You might be travelling on an airplane without internet connectivity as I am right now. Airplane rides are very good times to get a lot done, unfortunately external services are also usually unreachable. Remove internet connectivity as a dependency of your development envirionment by running a local Slanger instance in development and Pusher in production.
199
+ - Remove the network dependency from your test suite.
200
+ - You want to extend the Pusher protocol or have some special requirement. If this applies to you, chances are you are out of luck as Pusher is unlikely to implement something to suit your special use case, and rightly so. With Slanger you are free to modify and extend its behavior anyway that suits your purpose.
201
+
202
+ # Why did you write Slanger
203
+
204
+ I wanted to write a non-trivial evented app. I also want to write a book on evented programming in Ruby as I feel there is scant good information available on the topic and this project is handy to show publishers.
205
+
206
+ Pusher is an awesome service, very reasonably priced, and run by an awesome crew. Give them a spin on your next project.
207
+
208
+ # Author
209
+
210
+ - Stevie Graham
211
+
212
+ # Core Team
213
+
214
+ - Stevie Graham
215
+ - Mark Burns
216
+
217
+ # Contributors
218
+
219
+ - Stevie Graham
220
+ - Mark Burns
221
+ - Florian Gilcher
222
+ - Claudio Poli
223
+
224
+ &copy; 2011 a Stevie Graham joint.
data/bin/slanger ADDED
@@ -0,0 +1,137 @@
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 Slanger" 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 Slanger 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__),'..', 'slanger.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 " .d8888b. 888 "
120
+ puts " d88P Y88b 888 "
121
+ puts " Y88b. 888 "
122
+ puts ' "Y888b. 888 8888b. 88888b. .d88b. .d88b. 888d888 '
123
+ puts ' "Y88b. 888 "88b 888 "88b d88P"88b d8P Y8b 888P" '
124
+ puts ' "888 888 .d888888 888 888 888 888 88888888 888 '
125
+ puts " Y88b d88P 888 888 888 888 888 Y88b 888 Y8b. 888 "
126
+ puts ' "Y8888P" 888 "Y888888 888 888 "Y88888 "Y8888 888 '
127
+ puts " 888 "
128
+ puts " Y8b d88P "
129
+ puts ' "Y88P" '
130
+ puts "\n" * 2
131
+
132
+ puts "Running Slanger v.#{Slanger::VERSION}"
133
+ puts "\n"
134
+
135
+ puts "Slanger API server listening on port #{Slanger::Config.api_port}"
136
+ puts "Slanger WebSocket server listening on port #{Slanger::Config.websocket_port}"
137
+ end
@@ -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