apple_shove 1.0.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.
- data/.gitignore +20 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +64 -0
- data/Rakefile +4 -0
- data/apple_shove.gemspec +27 -0
- data/lib/apple_shove/apns/connection.rb +52 -0
- data/lib/apple_shove/apns/feedback_connection.rb +25 -0
- data/lib/apple_shove/apns/notify_connection.rb +75 -0
- data/lib/apple_shove/apple_shove.rb +33 -0
- data/lib/apple_shove/config.rb +6 -0
- data/lib/apple_shove/demultiplexer.rb +61 -0
- data/lib/apple_shove/logger.rb +29 -0
- data/lib/apple_shove/notification.rb +34 -0
- data/lib/apple_shove/notification_queue.rb +26 -0
- data/lib/apple_shove/tasks.rb +56 -0
- data/lib/apple_shove/version.rb +3 -0
- data/lib/apple_shove.rb +2 -0
- data/lib/tasks/apple_shove.rake +3 -0
- data/script/daemon +38 -0
- data/spec/demultiplexer_spec.rb +14 -0
- data/spec/notification_helper.rb +21 -0
- data/spec/notification_queue_spec.rb +34 -0
- data/spec/notification_spec.rb +28 -0
- metadata +176 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Taylor Boyko
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# AppleShove [](https://codeclimate.com/github/tboyko/apple_shove)
|
2
|
+
|
3
|
+
APN Service Provider. More powerful than a push...
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'apple_shove'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install apple_shove
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
# bundle exec rake -T
|
22
|
+
bundle exec rake apple_shove:run
|
23
|
+
bundle exec rake apple_shove:start
|
24
|
+
bundle exec rake apple_shove:stop
|
25
|
+
bundle exec rake apple_shove:status
|
26
|
+
bundle exec rake apple_shove:stats
|
27
|
+
|
28
|
+
### Optional Command Line Arguments
|
29
|
+
|
30
|
+
log_dir: specify an absolute path if you want to log
|
31
|
+
pid_dir: specify an absolute or relative path where the PID file
|
32
|
+
is to be stored. Defaults to the current directory.
|
33
|
+
connection_limit: maximum number of simultaneous connections to Apple
|
34
|
+
allowed.
|
35
|
+
|
36
|
+
Example usage:
|
37
|
+
|
38
|
+
bundle exec rake apple_shove:start connection_limit=100 log_dir=log
|
39
|
+
|
40
|
+
## TCP Keep-Alives
|
41
|
+
|
42
|
+
Apple Shove has the ability to maintain connections to Apple for long durations of time without sending a notification. These connections will generally stay open, however, intermediate NATs and firewalls may expire and close the connection prematurely.
|
43
|
+
|
44
|
+
To combat this, Apple Shove enables keep-alive on all connections to Apple. Apple Shove is not able to set the interval between keep-alives, however, as this is generally managed by the operating system. If you are aware of a relatively short NAT or firewall timer, you can either manually shorten your OS's keep-alive timer to be shorter than the timer. As this likely breaks the portability of your code, you can alternatively change the `AppleShove::CONFIG[:reconnect_timer]` to a value less than the NAT/firewall timer. This will force Apple Shove to re-establish the SSL connection after enough idle time has passed.
|
45
|
+
|
46
|
+
For reference, we have observed the following keep-alive timeout values:
|
47
|
+
|
48
|
+
* OS X: 4 minutes, 45 seconds
|
49
|
+
* Linux: 2 hours
|
50
|
+
* WIndows: 2 hours
|
51
|
+
|
52
|
+
Apple also seems to send a keep-alive packet if it sees the connection as idle for 10 minutes.
|
53
|
+
|
54
|
+
## Gotchas
|
55
|
+
|
56
|
+
Due to the TCP/IP stack, AppleShove will only know about a broken pipe to APNS after it writes two notifications to the socket. When this occurs, AppleShove will re-transmit the first as well as the second notification. Because time may have elapsed between the first and second notification writes, a non-trivial delay in the delivery of the first notification may occur.
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
1. Fork it
|
61
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
62
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
63
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
64
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/apple_shove.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'apple_shove/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "apple_shove"
|
8
|
+
spec.version = AppleShove::VERSION
|
9
|
+
spec.authors = ["Taylor Boyko"]
|
10
|
+
spec.email = ["tboyko@unwiredrevolution.com"]
|
11
|
+
spec.description = %q{APN Service Provider. More powerful than a push...}
|
12
|
+
spec.summary = %q{}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency 'rspec'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_dependency 'redis', '~> 3.0'
|
25
|
+
spec.add_dependency 'daemons', '~> 1.1'
|
26
|
+
spec.add_dependency 'celluloid', '~> 0.13'
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module AppleShove
|
4
|
+
module APNS
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
attr_reader :last_used
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
@host = opts[:host]
|
11
|
+
@port = opts[:port]
|
12
|
+
@certificate = opts[:certificate]
|
13
|
+
end
|
14
|
+
|
15
|
+
# lazy connect the socket
|
16
|
+
def socket
|
17
|
+
connect unless connected?
|
18
|
+
@ssl_sock
|
19
|
+
end
|
20
|
+
|
21
|
+
def disconnect
|
22
|
+
@ssl_sock.close if @ssl_sock
|
23
|
+
@sock.close if @sock
|
24
|
+
end
|
25
|
+
|
26
|
+
def reconnect
|
27
|
+
disconnect
|
28
|
+
connect
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def connect
|
34
|
+
@sock = TCPSocket.new(@host, @port)
|
35
|
+
@sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
36
|
+
|
37
|
+
context = ::OpenSSL::SSL::SSLContext.new
|
38
|
+
context.cert = ::OpenSSL::X509::Certificate.new(@certificate)
|
39
|
+
context.key = ::OpenSSL::PKey::RSA.new(@certificate)
|
40
|
+
@ssl_sock = ::OpenSSL::SSL::SSLSocket.new(@sock, context)
|
41
|
+
@ssl_sock.sync = true
|
42
|
+
|
43
|
+
@ssl_sock.connect
|
44
|
+
end
|
45
|
+
|
46
|
+
def connected?
|
47
|
+
@sock && @ssl_sock
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module AppleShove
|
2
|
+
module APNS
|
3
|
+
class FeedbackConnection < Connection
|
4
|
+
|
5
|
+
def initialize(opts = {})
|
6
|
+
host = "feedback.#{opts[:sandbox] ? 'sandbox.' : ''}push.apple.com"
|
7
|
+
|
8
|
+
super certificate: opts[:certificate],
|
9
|
+
host: host,
|
10
|
+
port: 2196
|
11
|
+
end
|
12
|
+
|
13
|
+
def device_tokens
|
14
|
+
tokens = []
|
15
|
+
while response = socket.read(38)
|
16
|
+
timestamp, token_length, device_token = response.unpack('N1n1H*')
|
17
|
+
tokens << device_token
|
18
|
+
end
|
19
|
+
|
20
|
+
tokens
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
|
3
|
+
module AppleShove
|
4
|
+
module APNS
|
5
|
+
class NotifyConnection < Connection
|
6
|
+
include Celluloid
|
7
|
+
|
8
|
+
attr_accessor :pending_notifications
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(opts = {})
|
12
|
+
@name = self.class.generate_name(opts[:certificate], opts[:sandbox])
|
13
|
+
@last_message = nil
|
14
|
+
@pending_notifications = 0
|
15
|
+
|
16
|
+
host = "gateway.#{opts[:sandbox] ? 'sandbox.' : ''}push.apple.com"
|
17
|
+
|
18
|
+
super certificate: opts[:certificate],
|
19
|
+
host: host,
|
20
|
+
port: 2195
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.generate_name(certificate, sandbox)
|
24
|
+
Digest::SHA1.hexdigest("#{certificate}#{sandbox}")
|
25
|
+
end
|
26
|
+
|
27
|
+
exclusive
|
28
|
+
|
29
|
+
def connect
|
30
|
+
super
|
31
|
+
@last_used = Time.now
|
32
|
+
end
|
33
|
+
|
34
|
+
def send(notification)
|
35
|
+
message = notification.binary_message
|
36
|
+
|
37
|
+
if @last_used && Time.now - @last_used > CONFIG[:reconnect_timer] * 60
|
38
|
+
Logger.info("#{@name}\trefreshing connection")
|
39
|
+
reconnect
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
socket.write message
|
44
|
+
rescue Errno::EPIPE
|
45
|
+
Logger.warn("#{@name}\tbroken pipe. reconnecting.")
|
46
|
+
reconnect
|
47
|
+
# EPIPE raises on the second write to a closed pipe. We need to resend
|
48
|
+
# the previous notification that didn't make it through.
|
49
|
+
socket.write @last_message if @last_message
|
50
|
+
retry
|
51
|
+
rescue Errno::ETIMEDOUT
|
52
|
+
Logger.warn("#{@name}\ttimeout. reconnecting.")
|
53
|
+
reconnect
|
54
|
+
retry
|
55
|
+
end
|
56
|
+
|
57
|
+
@last_message = message
|
58
|
+
@last_used = Time.now
|
59
|
+
@pending_notifications -= 1
|
60
|
+
Logger.info("#{@name}\tdelivered notification")
|
61
|
+
end
|
62
|
+
|
63
|
+
def shutdown
|
64
|
+
while @pending_notifications > 0
|
65
|
+
Logger.info("#{@name}\twaiting to shut down. #{@pending_notifications} job(s) remaining.")
|
66
|
+
sleep 1
|
67
|
+
end
|
68
|
+
|
69
|
+
self.disconnect
|
70
|
+
self.terminate
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AppleShove
|
2
|
+
|
3
|
+
def self.notify(certificate, device_token, payload, sandbox = false)
|
4
|
+
notification = Notification.new certificate: certificate,
|
5
|
+
device_token: device_token,
|
6
|
+
payload: payload,
|
7
|
+
sandbox: sandbox
|
8
|
+
|
9
|
+
queue = NotificationQueue.new(CONFIG[:redis_key])
|
10
|
+
queue.add(notification)
|
11
|
+
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.feedback_tokens(certificate, sandbox = false)
|
16
|
+
conn = APNS::FeedbackConnection.new certificate: certificate,
|
17
|
+
sandbox: sandbox
|
18
|
+
|
19
|
+
conn.device_tokens
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stats
|
23
|
+
redis = ::Redis.new
|
24
|
+
queue = NotificationQueue.new(CONFIG[:redis_key], redis)
|
25
|
+
|
26
|
+
size = queue.size
|
27
|
+
|
28
|
+
redis.quit
|
29
|
+
|
30
|
+
"queue size:\t#{size}"
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module AppleShove
|
2
|
+
class Demultiplexer
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
unless opts[:max_apns_connections]
|
6
|
+
raise ArgumentError, 'max_apns_connections must be specified'
|
7
|
+
end
|
8
|
+
|
9
|
+
@max_connections = opts[:max_apns_connections].to_i
|
10
|
+
|
11
|
+
@connections = {}
|
12
|
+
@queue = NotificationQueue.new(CONFIG[:redis_key])
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
|
17
|
+
while true
|
18
|
+
|
19
|
+
if notification = @queue.get
|
20
|
+
conn = get_connection(notification)
|
21
|
+
conn.pending_notifications += 1
|
22
|
+
conn.async.send(notification)
|
23
|
+
else
|
24
|
+
sleep 1
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def get_connection(notification)
|
34
|
+
key = APNS::NotifyConnection.generate_name(notification.certificate, notification.sandbox)
|
35
|
+
connection = @connections[key]
|
36
|
+
|
37
|
+
unless connection
|
38
|
+
retire_oldest_connection if @connections.count >= @max_connections
|
39
|
+
|
40
|
+
connection = APNS::NotifyConnection.new certificate: notification.certificate,
|
41
|
+
sandbox: notification.sandbox
|
42
|
+
@connections[key] = connection
|
43
|
+
Logger.info "#{connection.name}\tcreated connection to APNS (#{@connections.count} total)"
|
44
|
+
end
|
45
|
+
|
46
|
+
connection
|
47
|
+
end
|
48
|
+
|
49
|
+
def retire_oldest_connection
|
50
|
+
if oldest = @connections.min_by { |_k, v| v.last_used }
|
51
|
+
key, conn = oldest[0], oldest[1]
|
52
|
+
conn_name = conn.name
|
53
|
+
@connections.delete key
|
54
|
+
conn.shutdown
|
55
|
+
|
56
|
+
Logger.info "#{conn_name}\tdestroyed connection to APNS (#{@connections.count} total)"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module AppleShove
|
5
|
+
class Logger < ::Logger
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
class Formatter
|
9
|
+
def call(severity, time, progname, msg)
|
10
|
+
formatted_severity = sprintf("%-5s",severity.to_s)
|
11
|
+
formatted_time = time.strftime("%Y-%m-%d %H:%M:%S")
|
12
|
+
"[#{formatted_severity} #{formatted_time}] #{msg.strip}\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(output_stream = STDOUT)
|
17
|
+
super(output_stream)
|
18
|
+
self.formatter = Formatter.new
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.error(msg); instance.error(msg) end
|
23
|
+
def self.debug(msg); instance.debug(msg) end
|
24
|
+
def self.fatal(msg); instance.fatal(msg) end
|
25
|
+
def self.info(msg); instance.info(msg) end
|
26
|
+
def self.warn(msg); instance.warn(msg) end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module AppleShove
|
2
|
+
class Notification
|
3
|
+
|
4
|
+
attr_accessor :certificate, :sandbox, :device_token, :payload
|
5
|
+
|
6
|
+
def initialize(attributes = {})
|
7
|
+
attributes.each { |k, v| self.send("#{k}=", v) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parse(json)
|
11
|
+
self.new(JSON.parse(json))
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_json(*a)
|
15
|
+
hash = {}
|
16
|
+
clean_instance_variables.each { |k| hash[k] = self.send(k) }
|
17
|
+
hash.to_json(*a)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Apple APNS format
|
21
|
+
def binary_message
|
22
|
+
payload_json = @payload.to_json
|
23
|
+
message = [0, 32, @device_token, payload_json.length, payload_json]
|
24
|
+
message.pack('CnH*na*')
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def clean_instance_variables
|
30
|
+
self.instance_variables.collect { |i| i[1..-1] }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module AppleShove
|
5
|
+
class NotificationQueue
|
6
|
+
|
7
|
+
def initialize(key, redis = Redis.new)
|
8
|
+
@redis = redis
|
9
|
+
@key = key
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(notification)
|
13
|
+
@redis.rpush @key, notification.to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def get
|
17
|
+
element = @redis.lpop @key
|
18
|
+
element ? Notification.parse(element) : nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
@redis.llen @key
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
include Rake::DSL
|
4
|
+
|
5
|
+
namespace :apple_shove do
|
6
|
+
|
7
|
+
desc 'Display service statistics every second'
|
8
|
+
task :stats do
|
9
|
+
require 'apple_shove'
|
10
|
+
|
11
|
+
begin
|
12
|
+
puts AppleShove.stats
|
13
|
+
sleep 1
|
14
|
+
end while true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Start the daemon in the foreground'
|
18
|
+
task :run do
|
19
|
+
exec "ruby #{path_to_daemon} run#{argument_string}"
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'Start the daemon'
|
23
|
+
task :start do
|
24
|
+
exec "ruby #{path_to_daemon} start#{argument_string}"
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Stop the daemon'
|
28
|
+
task :stop do
|
29
|
+
exec "ruby #{path_to_daemon} stop#{argument_string}"
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'Restart the daemon'
|
33
|
+
task :restart do
|
34
|
+
exec "ruby #{path_to_daemon} restart#{argument_string}"
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Show the status (PID) of the daemon'
|
38
|
+
task :status do
|
39
|
+
exec "ruby #{path_to_daemon} status"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def path_to_daemon
|
45
|
+
File.join(File.dirname(__FILE__), '..', '..', 'script', 'daemon')
|
46
|
+
end
|
47
|
+
|
48
|
+
def argument_string
|
49
|
+
watched_args = ['log_dir', 'pid_dir', 'connection_limit']
|
50
|
+
arg_str = watched_args.collect { |a| ENV[a] ? "--#{a}=#{ENV[a]}" : nil }.compact.join(' ')
|
51
|
+
|
52
|
+
arg_str.empty? ? nil : " -- #{arg_str}"
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
end
|
data/lib/apple_shove.rb
ADDED
data/script/daemon
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'daemons'
|
4
|
+
require 'apple_shove'
|
5
|
+
|
6
|
+
# process command line arguments
|
7
|
+
|
8
|
+
args = {}
|
9
|
+
ARGV.each do |arg|
|
10
|
+
if m = arg.match(/^--(?<key>[^=]+)=(?<val>.+)$/)
|
11
|
+
key = m[:key].gsub('-','_').to_sym
|
12
|
+
args[key] = m[:val]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
options = {
|
17
|
+
stop_proc: Proc.new { puts "Stopping daemon" },
|
18
|
+
dir_mode: :script
|
19
|
+
}
|
20
|
+
|
21
|
+
options[:dir] = args[:pid_dir] if args[:pid_dir]
|
22
|
+
|
23
|
+
if args[:log_dir]
|
24
|
+
options[:log_output] = true
|
25
|
+
options[:log_dir] = args[:log_dir]
|
26
|
+
end
|
27
|
+
|
28
|
+
Daemons.run_proc('apple_shove', options) do
|
29
|
+
# max of 15 connections recommended by Apple: http://bit.ly/YNHTfE
|
30
|
+
# note: this may be per-certificate, in which case we can crank this number
|
31
|
+
# up much higher.
|
32
|
+
conn_limit = args[:connection_limit] || 100
|
33
|
+
|
34
|
+
puts "Starting daemon with a APNS connection limit of #{conn_limit}"
|
35
|
+
|
36
|
+
dmp = AppleShove::Demultiplexer.new max_apns_connections: conn_limit
|
37
|
+
dmp.start
|
38
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'apple_shove'
|
2
|
+
|
3
|
+
describe AppleShove::Demultiplexer do
|
4
|
+
|
5
|
+
it 'initializes without error' do
|
6
|
+
dmp = AppleShove::Demultiplexer.new max_apns_connections: 10
|
7
|
+
dmp.should be_an_instance_of(AppleShove::Demultiplexer)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'raises an error when a connection limit is omitted' do
|
11
|
+
expect { AppleShove::Demultiplexer.new }.to raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module NotificationHelper
|
2
|
+
|
3
|
+
def generate_notification
|
4
|
+
certificate = "DummyCertificate"
|
5
|
+
sandbox = false
|
6
|
+
device_token = hex(64)
|
7
|
+
payload = { mdm: "#{hex(8)}-#{hex(4)}-#{hex(4)}-#{hex(4)}-#{hex(12)}".downcase }
|
8
|
+
|
9
|
+
AppleShove::Notification.new certificate: certificate,
|
10
|
+
sandbox: sandbox,
|
11
|
+
device_token: device_token,
|
12
|
+
payload: payload
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def hex(length)
|
18
|
+
length.times.map { ((0..9).to_a + ('a'..'f').to_a)[rand(16)] }.join
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'apple_shove'
|
2
|
+
require './spec/notification_helper'
|
3
|
+
|
4
|
+
describe AppleShove::NotificationQueue do
|
5
|
+
include NotificationHelper
|
6
|
+
|
7
|
+
before do
|
8
|
+
@q = AppleShove::NotificationQueue.new('dummy_key')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should initialize without error' do
|
12
|
+
@q.should_not eql(nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should add notifications to the queue' do
|
16
|
+
n = generate_notification
|
17
|
+
@q.add(n)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should count notification on the queue when they are there' do
|
21
|
+
@q.size.should_not eql(0)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should get notifications from the queue' do
|
25
|
+
while n = @q.get
|
26
|
+
n.should be_an_instance_of(AppleShove::Notification)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should count 0 notifications when the queue is empty' do
|
31
|
+
@q.size.should eql(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'apple_shove'
|
2
|
+
require './spec/notification_helper'
|
3
|
+
|
4
|
+
describe AppleShove::Notification do
|
5
|
+
include NotificationHelper
|
6
|
+
|
7
|
+
before do
|
8
|
+
@n = generate_notification
|
9
|
+
end
|
10
|
+
|
11
|
+
it "converts to and from json" do
|
12
|
+
json = @n.to_json
|
13
|
+
|
14
|
+
json.should be_an_instance_of(String)
|
15
|
+
|
16
|
+
n2 = AppleShove::Notification.parse(json)
|
17
|
+
|
18
|
+
@n.to_json.should == n2.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
it "creates a binary message for apns" do
|
22
|
+
m = @n.binary_message
|
23
|
+
|
24
|
+
m.should be_an_instance_of(String)
|
25
|
+
m.length.should > 0
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apple_shove
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Taylor Boyko
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redis
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: daemons
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '1.1'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '1.1'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: celluloid
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0.13'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.13'
|
110
|
+
description: APN Service Provider. More powerful than a push...
|
111
|
+
email:
|
112
|
+
- tboyko@unwiredrevolution.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- .gitignore
|
118
|
+
- Gemfile
|
119
|
+
- LICENSE.txt
|
120
|
+
- README.md
|
121
|
+
- Rakefile
|
122
|
+
- apple_shove.gemspec
|
123
|
+
- lib/apple_shove.rb
|
124
|
+
- lib/apple_shove/apns/connection.rb
|
125
|
+
- lib/apple_shove/apns/feedback_connection.rb
|
126
|
+
- lib/apple_shove/apns/notify_connection.rb
|
127
|
+
- lib/apple_shove/apple_shove.rb
|
128
|
+
- lib/apple_shove/config.rb
|
129
|
+
- lib/apple_shove/demultiplexer.rb
|
130
|
+
- lib/apple_shove/logger.rb
|
131
|
+
- lib/apple_shove/notification.rb
|
132
|
+
- lib/apple_shove/notification_queue.rb
|
133
|
+
- lib/apple_shove/tasks.rb
|
134
|
+
- lib/apple_shove/version.rb
|
135
|
+
- lib/tasks/apple_shove.rake
|
136
|
+
- script/daemon
|
137
|
+
- spec/demultiplexer_spec.rb
|
138
|
+
- spec/notification_helper.rb
|
139
|
+
- spec/notification_queue_spec.rb
|
140
|
+
- spec/notification_spec.rb
|
141
|
+
homepage: ''
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
hash: -1646100289206665010
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ! '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
segments:
|
164
|
+
- 0
|
165
|
+
hash: -1646100289206665010
|
166
|
+
requirements: []
|
167
|
+
rubyforge_project:
|
168
|
+
rubygems_version: 1.8.24
|
169
|
+
signing_key:
|
170
|
+
specification_version: 3
|
171
|
+
summary: ''
|
172
|
+
test_files:
|
173
|
+
- spec/demultiplexer_spec.rb
|
174
|
+
- spec/notification_helper.rb
|
175
|
+
- spec/notification_queue_spec.rb
|
176
|
+
- spec/notification_spec.rb
|