apnd 0.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/README.markdown +97 -0
- data/Rakefile +13 -0
- data/bin/apnd +107 -0
- data/bin/apnd-push +90 -0
- data/lib/apnd.rb +28 -0
- data/lib/apnd/daemon.rb +65 -0
- data/lib/apnd/daemon/apple_connection.rb +76 -0
- data/lib/apnd/daemon/protocol.rb +30 -0
- data/lib/apnd/errors.rb +13 -0
- data/lib/apnd/notification.rb +139 -0
- data/lib/apnd/settings.rb +128 -0
- data/lib/apnd/version.rb +11 -0
- data/test/daemon_test.rb +5 -0
- data/test/notification_test.rb +5 -0
- data/test/test_helper.rb +28 -0
- metadata +130 -0
data/README.markdown
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# APND
|
2
|
+
|
3
|
+
APND (Apple Push Notification Daemon) is a ruby library to send Apple Push
|
4
|
+
Notifications (APNs) to iPhones.
|
5
|
+
|
6
|
+
Apple recommends application developers create one connection to their
|
7
|
+
upstream push notification server, rather than creating one per notification.
|
8
|
+
|
9
|
+
APND acts as an intermediary between your application and Apple (see **APND
|
10
|
+
Daemon** below). Your application's notifications are queued to APND, which
|
11
|
+
are then sent to Apple over a single connection.
|
12
|
+
|
13
|
+
Within ruby applications, `APND::Notification` can be used to send
|
14
|
+
notifications to a running APND instance (see **APND Notification** below) or
|
15
|
+
directly to Apple. A command line utility, `apnd-push`, can be used to send
|
16
|
+
single notifications for testing purposes (see **APND Client** below).
|
17
|
+
|
18
|
+
|
19
|
+
## General Usage
|
20
|
+
|
21
|
+
### APND Daemon
|
22
|
+
|
23
|
+
APND receives push notifications from your application and relays them to
|
24
|
+
Apple over a single connection as explained above. The `apnd` command line
|
25
|
+
utility is used to start APND.
|
26
|
+
|
27
|
+
Usage:
|
28
|
+
apnd [OPTIONS] --apple-cert </path/to/cert>
|
29
|
+
|
30
|
+
Required Arguments:
|
31
|
+
--apple-cert [PATH] PATH to APN certificate from Apple
|
32
|
+
|
33
|
+
Optional Arguments:
|
34
|
+
--apple-host [HOST] Connect to Apple at HOST (default is gateway.sandbox.push.apple.com)
|
35
|
+
--apple-port [PORT] Connect to Apple on PORT (default is 2195)
|
36
|
+
--apple-cert-pass [PASSWORD] PASSWORD for APN certificate from Apple
|
37
|
+
--daemon-port [PORT] Run APND on PORT (default is 22195)
|
38
|
+
--daemon-bind [ADDRESS] Bind APND to ADDRESS (default is 0.0.0.0)
|
39
|
+
--daemon-log-file [PATH] PATH to APND log file (default is /var/log/apnd.log)
|
40
|
+
--foreground Run APND in foreground without daemonizing
|
41
|
+
|
42
|
+
Help:
|
43
|
+
--version Show version
|
44
|
+
--help Show this message
|
45
|
+
|
46
|
+
|
47
|
+
### APND Client
|
48
|
+
|
49
|
+
APND includes a command line utility, `apnd-push`, which can be used to send
|
50
|
+
notifications to a running APND instance, or Apple directly. It is only
|
51
|
+
recommended to send notifications directly to Apple for testing purposes.
|
52
|
+
|
53
|
+
Usage:
|
54
|
+
apnd-push [OPTIONS] --token <token> --alert <alert>
|
55
|
+
|
56
|
+
Required Arguments:
|
57
|
+
--token [TOKEN] Set Notification's iPhone token to TOKEN
|
58
|
+
--alert [MESSAGE] Set Notification's alert to MESSAGE
|
59
|
+
|
60
|
+
Optional Arguments:
|
61
|
+
--sound [SOUND] Set Notification's sound to SOUND (default is 'default')
|
62
|
+
--badge [NUMBER] Set Notification's badge number to NUMBER
|
63
|
+
--custom [JSON] Set Notification's custom data to JSON
|
64
|
+
--host [HOST] Send Notification to HOST, usually the one running APND (default is 'localhost')
|
65
|
+
--port [PORT] Send Notification on PORT (default is 22195)
|
66
|
+
|
67
|
+
Help:
|
68
|
+
--version Show version
|
69
|
+
--help Show this message
|
70
|
+
|
71
|
+
|
72
|
+
### APND Notification
|
73
|
+
|
74
|
+
The `APND::Notification` class can be used within your application to send
|
75
|
+
push notifications to APND.
|
76
|
+
|
77
|
+
require 'apnd'
|
78
|
+
|
79
|
+
# Set the host/port APND is running on
|
80
|
+
# (not needed if you're using localhost:22195)
|
81
|
+
|
82
|
+
APND::Notification.upstream_host = 'localhost'
|
83
|
+
APND::Notification.upstream_port = 22195
|
84
|
+
|
85
|
+
notification = APND::Notification.create(
|
86
|
+
:alert => 'Alert!',
|
87
|
+
:token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
|
88
|
+
:badge => 99
|
89
|
+
)
|
90
|
+
|
91
|
+
|
92
|
+
## Credit
|
93
|
+
|
94
|
+
APND is based on [apnserver](http://github.com/bpoweski/apnserver) and
|
95
|
+
[apn_on_rails](http://github.com/PRX/apn_on_rails). Either worked just how I
|
96
|
+
wanted, so I rolled my own using theirs as starting points. If APND doesn't
|
97
|
+
suit you, check them out instead.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
Rake::TestTask.new(:test) do |test|
|
5
|
+
test.libs << 'lib' << 'test' << '.'
|
6
|
+
test.pattern = 'test/**/*_test.rb'
|
7
|
+
test.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Open an irb session preloaded with this library"
|
11
|
+
task :console do
|
12
|
+
sh "irb -rubygems -r ./lib/apnd.rb -I ./lib"
|
13
|
+
end
|
data/bin/apnd
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
|
5
|
+
require 'apnd'
|
6
|
+
|
7
|
+
require 'daemons'
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
help = <<HELP
|
11
|
+
Usage:
|
12
|
+
apnd [OPTIONS] --apple-cert </path/to/cert>
|
13
|
+
|
14
|
+
HELP
|
15
|
+
|
16
|
+
options = {}
|
17
|
+
|
18
|
+
opts = OptionParser.new do |opt|
|
19
|
+
opt.banner = help
|
20
|
+
|
21
|
+
opt.separator "Required Arguments:\n"
|
22
|
+
|
23
|
+
opt.on('--apple-cert [PATH]', 'PATH to APN certificate from Apple') do |cert|
|
24
|
+
options[:apple_cert] = cert
|
25
|
+
end
|
26
|
+
|
27
|
+
opt.separator "\nOptional Arguments:\n"
|
28
|
+
|
29
|
+
opt.on('--apple-host [HOST]', "Connect to Apple at HOST (default is gateway.sandbox.push.apple.com)") do |host|
|
30
|
+
options[:apple_host] = host
|
31
|
+
end
|
32
|
+
|
33
|
+
opt.on('--apple-port [PORT]', 'Connect to Apple on PORT (default is 2195)') do |port|
|
34
|
+
options[:apple_port] = port.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
opt.on('--apple-cert-pass [PASSWORD]', 'PASSWORD for APN certificate from Apple') do |pass|
|
38
|
+
options[:apple_cert_pass] = pass
|
39
|
+
end
|
40
|
+
|
41
|
+
opt.on('--daemon-port [PORT]', 'Run APND on PORT (default is 22195)') do |port|
|
42
|
+
options[:daemon_port] = port.to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
opt.on('--daemon-bind [ADDRESS]', 'Bind APND to ADDRESS (default is 0.0.0.0)') do |bind|
|
46
|
+
options[:daemon_bind] = bind
|
47
|
+
end
|
48
|
+
|
49
|
+
opt.on('--daemon-log-file [PATH]', 'PATH to APND log file (default is /var/log/apnd.log)') do |log|
|
50
|
+
options[:daemon_log_file] = log
|
51
|
+
end
|
52
|
+
|
53
|
+
opt.on('--foreground', 'Run APND in foreground without daemonizing') do
|
54
|
+
options[:foreground] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
opt.separator "\nHelp:\n"
|
58
|
+
|
59
|
+
opt.on('--version', 'Show version') do
|
60
|
+
puts "APND #{APND::Version}"
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
|
64
|
+
opt.on('--help', 'Show this message') do
|
65
|
+
puts opt
|
66
|
+
exit
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
begin
|
71
|
+
opts.parse!
|
72
|
+
if options.empty?
|
73
|
+
puts opts
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
|
77
|
+
unless options[:apple_cert]
|
78
|
+
raise OptionParser::MissingArgument, "must specify --apple-cert"
|
79
|
+
end
|
80
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
81
|
+
puts "#{$0}: #{$!.message}"
|
82
|
+
puts "#{$0}: try '#{$0} --help' for more information"
|
83
|
+
exit
|
84
|
+
end
|
85
|
+
|
86
|
+
APND.configure do |config|
|
87
|
+
# Setup AppleConnection
|
88
|
+
config.apple.cert = options[:apple_cert] if options[:apple_cert]
|
89
|
+
config.apple.cert_pass = options[:apple_cert_pass] if options[:apple_cert_pass]
|
90
|
+
config.apple.host = options[:apple_host] if options[:apple_host]
|
91
|
+
config.apple.port = options[:apple_port] if options[:apple_port]
|
92
|
+
|
93
|
+
# Setup Daemon
|
94
|
+
config.daemon.bind = options[:daemon_bind] if options[:daemon_bind]
|
95
|
+
config.daemon.port = options[:daemon_port] if options[:daemon_port]
|
96
|
+
config.daemon.log_file = options[:daemon_log_file] if options[:daemon_log_file]
|
97
|
+
end
|
98
|
+
|
99
|
+
if APND.settings.apple.cert.nil?
|
100
|
+
puts opts
|
101
|
+
exit
|
102
|
+
else
|
103
|
+
unless options[:foreground]
|
104
|
+
Daemonize.daemonize(APND.settings.daemon.log_file, 'apnd')
|
105
|
+
end
|
106
|
+
APND::Daemon.run!
|
107
|
+
end
|
data/bin/apnd-push
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
|
5
|
+
require 'apnd'
|
6
|
+
|
7
|
+
require 'optparse'
|
8
|
+
|
9
|
+
help = <<HELP
|
10
|
+
Usage:
|
11
|
+
apnd-push [OPTIONS] --token <token> --alert <alert>
|
12
|
+
|
13
|
+
HELP
|
14
|
+
|
15
|
+
options = {}
|
16
|
+
|
17
|
+
opts = OptionParser.new do |opt|
|
18
|
+
opt.banner = help
|
19
|
+
|
20
|
+
opt.separator "Required Arguments:\n"
|
21
|
+
|
22
|
+
opt.on('--token [TOKEN]', "Set Notification's iPhone token to TOKEN") do |token|
|
23
|
+
options[:token] = token
|
24
|
+
end
|
25
|
+
|
26
|
+
opt.on('--alert [MESSAGE]', "Set Notification's alert to MESSAGE") do |alert|
|
27
|
+
options[:alert] = alert
|
28
|
+
end
|
29
|
+
|
30
|
+
opt.separator "\nOptional Arguments:\n"
|
31
|
+
|
32
|
+
opt.on('--sound [SOUND]', "Set Notification's sound to SOUND (default is 'default')") do |sound|
|
33
|
+
options[:sound] = sound
|
34
|
+
end
|
35
|
+
|
36
|
+
opt.on('--badge [NUMBER]', "Set Notification's badge number to NUMBER") do |badge|
|
37
|
+
options[:badge] = badge.to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
opt.on('--custom [JSON]', "Set Notification's custom data to JSON") do |custom|
|
41
|
+
begin
|
42
|
+
options[:custom] = JSON.parse(custom)
|
43
|
+
rescue JSON::ParserError => e
|
44
|
+
puts "Invalid JSON: #{e}"
|
45
|
+
exit -1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
opt.on('--host [HOST]', "Send Notification to HOST, usually the one running APND (default is 'localhost')") do |host|
|
50
|
+
options[:host] = host
|
51
|
+
end
|
52
|
+
|
53
|
+
opt.on('--port [PORT]', 'Send Notification on PORT (default is 22195)') do |port|
|
54
|
+
options[:port] = port.to_i
|
55
|
+
end
|
56
|
+
|
57
|
+
opt.separator "\nHelp:\n"
|
58
|
+
|
59
|
+
opt.on('--version', 'Show version') do
|
60
|
+
puts "APND #{APND::Version}"
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
|
64
|
+
opt.on('--help', 'Show this message') do
|
65
|
+
puts opt
|
66
|
+
exit
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
begin
|
71
|
+
opts.parse!
|
72
|
+
if options.empty?
|
73
|
+
puts opts
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
|
77
|
+
unless options[:token] && options[:alert]
|
78
|
+
raise OptionParser::MissingArgument, "must specify --token and --alert"
|
79
|
+
end
|
80
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
81
|
+
puts "#{$0}: #{$!.message}"
|
82
|
+
puts "#{$0}: try '#{$0} --help' for more information"
|
83
|
+
exit
|
84
|
+
end
|
85
|
+
|
86
|
+
# Configure Notification upstream host/port
|
87
|
+
APND::Notification.upstream_host = options.delete(:host) if options[:host]
|
88
|
+
APND::Notification.upstream_port = options.delete(:port) if options[:port]
|
89
|
+
|
90
|
+
APND::Notification.create(options)
|
data/lib/apnd.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module APND
|
4
|
+
autoload :Version, 'apnd/version'
|
5
|
+
autoload :Errors, 'apnd/errors'
|
6
|
+
autoload :Settings, 'apnd/settings'
|
7
|
+
autoload :Daemon, 'apnd/daemon'
|
8
|
+
autoload :Notification, 'apnd/notification'
|
9
|
+
|
10
|
+
#
|
11
|
+
# APND Settings
|
12
|
+
#
|
13
|
+
def self.settings
|
14
|
+
@@settings ||= Settings.new
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Yields APND Settings
|
19
|
+
#
|
20
|
+
def self.configure
|
21
|
+
yield settings
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def ohai(message)
|
27
|
+
puts "[%s] %s" % [Time.now.strftime("%Y-%m-%d %H:%M:%S"), message]
|
28
|
+
end
|
data/lib/apnd/daemon.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module APND
|
4
|
+
#
|
5
|
+
# The APND::Daemon maintains a persistent secure connection with Apple,
|
6
|
+
# (APND::Daemon::AppleConnection). Notifications are queued and periodically
|
7
|
+
# writen to the AppleConnection
|
8
|
+
#
|
9
|
+
class Daemon
|
10
|
+
autoload :Protocol, 'apnd/daemon/protocol'
|
11
|
+
autoload :AppleConnection, 'apnd/daemon/apple_connection'
|
12
|
+
|
13
|
+
#
|
14
|
+
# Create a new Daemon and run it
|
15
|
+
#
|
16
|
+
def self.run!
|
17
|
+
server = APND::Daemon.new
|
18
|
+
server.run!
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Create a connection to Apple and a new EM queue
|
23
|
+
#
|
24
|
+
def initialize
|
25
|
+
@queue = EM::Queue.new
|
26
|
+
@apple = APND::Daemon::AppleConnection.new
|
27
|
+
@bind = APND.settings.daemon.bind
|
28
|
+
@port = APND.settings.daemon.port
|
29
|
+
@timer = APND.settings.daemon.timer
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Run the daemon
|
34
|
+
#
|
35
|
+
def run!
|
36
|
+
EventMachine::run do
|
37
|
+
ohai "Starting APND Daemon on #{@bind}:#{@port}"
|
38
|
+
EventMachine::start_server(@bind, @port, APND::Daemon::Protocol) do |server|
|
39
|
+
server.queue = @queue
|
40
|
+
end
|
41
|
+
|
42
|
+
EventMachine::PeriodicTimer.new(@timer) do
|
43
|
+
count = @queue.size
|
44
|
+
if count > 0
|
45
|
+
ohai "Queue has #{count} item#{count == 1 ? '' : 's'}"
|
46
|
+
count.times do
|
47
|
+
@queue.pop do |notification|
|
48
|
+
begin
|
49
|
+
ohai "Sending notification"
|
50
|
+
@apple.write(notification.to_bytes)
|
51
|
+
rescue Errno::EPIPE, OpenSSL::SSL::SSLError
|
52
|
+
ohai "Error, notification has been added back to the queue"
|
53
|
+
@queue.push(notification)
|
54
|
+
rescue RuntimeError => error
|
55
|
+
ohai "Error: #{error}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module APND
|
5
|
+
#
|
6
|
+
# Daemon::AppleConnection handles the persistent connection between
|
7
|
+
# APND and Apple
|
8
|
+
#
|
9
|
+
class Daemon::AppleConnection
|
10
|
+
attr_reader :ssl, :sock
|
11
|
+
|
12
|
+
#
|
13
|
+
# Setup a new connection
|
14
|
+
#
|
15
|
+
def initialize(params = {})
|
16
|
+
@options = {
|
17
|
+
:cert => APND.settings.apple.cert,
|
18
|
+
:cert_pass => APND.settings.apple.cert_pass,
|
19
|
+
:host => APND.settings.apple.host,
|
20
|
+
:port => APND.settings.apple.port.to_i
|
21
|
+
}.merge(params)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Returns true if the connection to Apple is open
|
26
|
+
#
|
27
|
+
def connected?
|
28
|
+
! @ssl.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Connect to Apple over SSL
|
33
|
+
#
|
34
|
+
def connect!
|
35
|
+
cert = File.read(@options[:cert])
|
36
|
+
context = OpenSSL::SSL::SSLContext.new
|
37
|
+
context.key = OpenSSL::PKey::RSA.new(cert, @options[:cert_pass])
|
38
|
+
context.cert = OpenSSL::X509::Certificate.new(cert)
|
39
|
+
|
40
|
+
@sock = TCPSocket.new(@options[:host], @options[:port])
|
41
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, context)
|
42
|
+
@ssl.sync = true
|
43
|
+
@ssl.connect
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Close connection
|
48
|
+
#
|
49
|
+
def disconnect!
|
50
|
+
@ssl.close
|
51
|
+
@sock.close
|
52
|
+
@ssl = nil
|
53
|
+
@sock = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Establishes a connection if needed and yields it
|
58
|
+
#
|
59
|
+
# Ex: open { |conn| conn.write('write to socket) }
|
60
|
+
#
|
61
|
+
def open(&block)
|
62
|
+
unless connected?
|
63
|
+
connect!
|
64
|
+
end
|
65
|
+
|
66
|
+
yield @ssl
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Write to the connection socket
|
71
|
+
#
|
72
|
+
def write(raw)
|
73
|
+
open { |conn| conn.write(raw) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module APND
|
2
|
+
#
|
3
|
+
# Daemon::Protocol handles incoming APNs
|
4
|
+
#
|
5
|
+
class Daemon::Protocol < ::EventMachine::Connection
|
6
|
+
attr_accessor :queue
|
7
|
+
|
8
|
+
def post_init
|
9
|
+
@address = Socket.unpack_sockaddr_in(self.get_peername)
|
10
|
+
ohai "#{@address.last}:#{@address.first} opened connection"
|
11
|
+
end
|
12
|
+
|
13
|
+
def unbind
|
14
|
+
ohai "#{@address.last}:#{@address.first} closed connection"
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Add incoming notification to the queue if it is valid
|
19
|
+
#
|
20
|
+
def receive_data(data)
|
21
|
+
(@buffer ||= "") << data
|
22
|
+
if notification = APND::Notification.valid?(@buffer)
|
23
|
+
ohai "#{@address.last}:#{@address.first} added new Notification to queue"
|
24
|
+
queue.push(notification)
|
25
|
+
else
|
26
|
+
ohai "#{@address.last}:#{@address.first} submitted invalid Notification"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/apnd/errors.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module APND
|
4
|
+
#
|
5
|
+
# APND::Notification is the base class for creating new push notifications.
|
6
|
+
#
|
7
|
+
class Notification
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :upstream_host
|
11
|
+
attr_accessor :upstream_port
|
12
|
+
end
|
13
|
+
|
14
|
+
self.upstream_host = APND.settings.notification.host
|
15
|
+
self.upstream_port = APND.settings.notification.port.to_i
|
16
|
+
|
17
|
+
attr_accessor :token, :alert, :badge, :sound, :custom
|
18
|
+
|
19
|
+
#
|
20
|
+
# Create a new APN
|
21
|
+
#
|
22
|
+
def self.create(params = {}, push = true)
|
23
|
+
notification = Notification.new(params)
|
24
|
+
notification.push! if push
|
25
|
+
notification
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Try to create a new Notification from raw data
|
30
|
+
# Used by Daemon::Protocol to validate incoming data
|
31
|
+
#
|
32
|
+
def self.valid?(data)
|
33
|
+
parse(data)
|
34
|
+
rescue
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Parse raw data into a new Notification
|
40
|
+
#
|
41
|
+
def self.parse(data)
|
42
|
+
buffer = data.dup
|
43
|
+
notification = Notification.new
|
44
|
+
|
45
|
+
header = buffer.slice!(0, 3).unpack('ccc')
|
46
|
+
|
47
|
+
if header[0] != 0
|
48
|
+
raise RuntimeError, "Invalid Notification header: #{header.inspect}"
|
49
|
+
end
|
50
|
+
|
51
|
+
notification.token = buffer.slice!(0, 32).unpack('H*').first
|
52
|
+
|
53
|
+
json_length = buffer.slice!(0, 2).unpack('CC')
|
54
|
+
|
55
|
+
json = buffer.slice!(0, json_length.last)
|
56
|
+
|
57
|
+
payload = JSON.parse(json)
|
58
|
+
|
59
|
+
%w[alert sound badge].each do |key|
|
60
|
+
if payload['aps'] && payload['aps'][key]
|
61
|
+
notification.send("#{key}=", payload['aps'][key])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
payload.delete('aps')
|
66
|
+
|
67
|
+
unless payload.empty?
|
68
|
+
notification.custom = payload
|
69
|
+
end
|
70
|
+
|
71
|
+
notification
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Create a new Notification object from a hash
|
76
|
+
#
|
77
|
+
def initialize(params = {})
|
78
|
+
@token = params[:token]
|
79
|
+
@alert = params[:alert]
|
80
|
+
@badge = params[:badge]
|
81
|
+
@sound = params[:sound] || 'default'
|
82
|
+
@custom = params[:custom]
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Token in hex format
|
87
|
+
#
|
88
|
+
def hex_token
|
89
|
+
[self.token.delete(' ')].pack('H*')
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# aps hash sent to Apple
|
94
|
+
#
|
95
|
+
def aps
|
96
|
+
aps = {}
|
97
|
+
aps['alert'] = self.alert if self.alert
|
98
|
+
aps['badge'] = self.badge.to_i if self.badge
|
99
|
+
aps['sound'] = self.sound if self.sound
|
100
|
+
|
101
|
+
output = { 'aps' => aps }
|
102
|
+
|
103
|
+
if self.custom
|
104
|
+
self.custom.each do |key, value|
|
105
|
+
output[key.to_s] = value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
output
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Pushes notification to upstream host:port (default is localhost:22195)
|
113
|
+
#
|
114
|
+
def push!
|
115
|
+
socket = TCPSocket.new(self.class.upstream_host, self.class.upstream_port)
|
116
|
+
socket.write(to_bytes)
|
117
|
+
socket.close
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Returns the Notification's aps hash as json
|
122
|
+
#
|
123
|
+
def payload
|
124
|
+
return @payload if @payload
|
125
|
+
json = aps.to_json
|
126
|
+
raise APND::InvalidPayload.new(json) if json.size > 256
|
127
|
+
@payload = json
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Format the notification as a string for submission
|
132
|
+
# to Apple
|
133
|
+
#
|
134
|
+
def to_bytes
|
135
|
+
@bytes ||= "\0\0 %s\0%s%s" % [hex_token, payload.length.chr, payload]
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module APND
|
2
|
+
#
|
3
|
+
# Settings for APND
|
4
|
+
#
|
5
|
+
class Settings
|
6
|
+
|
7
|
+
#
|
8
|
+
# Settings for APND::Daemon::AppleConnection
|
9
|
+
#
|
10
|
+
class AppleConnection
|
11
|
+
|
12
|
+
#
|
13
|
+
# Host used to connect to Apple
|
14
|
+
#
|
15
|
+
# Development: gateway.sandbox.push.apple.com
|
16
|
+
# Production: gateway.push.apple.com
|
17
|
+
#
|
18
|
+
attr_accessor :host
|
19
|
+
|
20
|
+
#
|
21
|
+
# Port used to connect to Apple
|
22
|
+
#
|
23
|
+
attr_accessor :port
|
24
|
+
|
25
|
+
#
|
26
|
+
# Path to APN cert for your application
|
27
|
+
#
|
28
|
+
attr_accessor :cert
|
29
|
+
|
30
|
+
#
|
31
|
+
# Password for APN cert, optional
|
32
|
+
#
|
33
|
+
attr_accessor :cert_pass
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@host = 'gateway.sandbox.push.apple.com'
|
37
|
+
@port = 2195
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Settings for APND::Daemon
|
43
|
+
#
|
44
|
+
class Daemon
|
45
|
+
|
46
|
+
#
|
47
|
+
# IP to bind APND::Daemon to
|
48
|
+
#
|
49
|
+
# Default: '0.0.0.0'
|
50
|
+
#
|
51
|
+
attr_accessor :bind
|
52
|
+
|
53
|
+
#
|
54
|
+
# Port APND::Daemon will run on
|
55
|
+
#
|
56
|
+
# Default: 22195
|
57
|
+
#
|
58
|
+
attr_accessor :port
|
59
|
+
|
60
|
+
#
|
61
|
+
# Path to APND::Daemon log
|
62
|
+
#
|
63
|
+
# Default: /var/log/apnd.log
|
64
|
+
#
|
65
|
+
attr_accessor :log_file
|
66
|
+
|
67
|
+
#
|
68
|
+
# Interval (in seconds) the queue will be processed
|
69
|
+
#
|
70
|
+
# Default: 30
|
71
|
+
#
|
72
|
+
attr_accessor :timer
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
@timer = 30
|
76
|
+
@bind = '0.0.0.0'
|
77
|
+
@port = 22195
|
78
|
+
@log_file = '/var/log/apnd.log'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Settings for APND::Notification
|
84
|
+
#
|
85
|
+
class Notification
|
86
|
+
|
87
|
+
#
|
88
|
+
# Host to send notification to, usually the one running APND::Daemon
|
89
|
+
#
|
90
|
+
# Default: localhost
|
91
|
+
#
|
92
|
+
attr_accessor :host
|
93
|
+
|
94
|
+
#
|
95
|
+
# Port to send notifications to
|
96
|
+
#
|
97
|
+
# Default: 22195
|
98
|
+
#
|
99
|
+
attr_accessor :port
|
100
|
+
|
101
|
+
def initialize
|
102
|
+
@host = 'localhost'
|
103
|
+
@port = 22195
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Returns the AppleConnection settings
|
109
|
+
#
|
110
|
+
def apple
|
111
|
+
@apple ||= APND::Settings::AppleConnection.new
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Returns the Daemon settings
|
116
|
+
#
|
117
|
+
def daemon
|
118
|
+
@daemon ||= APND::Settings::Daemon.new
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Returns the Notification settings
|
123
|
+
#
|
124
|
+
def notification
|
125
|
+
@notification ||= APND::Settings::Notification.new
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/apnd/version.rb
ADDED
data/test/daemon_test.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
5
|
+
$LOAD_PATH.unshift(File.join(dir, '..', 'lib'))
|
6
|
+
$LOAD_PATH.unshift(dir)
|
7
|
+
|
8
|
+
require 'apnd'
|
9
|
+
|
10
|
+
##
|
11
|
+
# test/spec/mini 3
|
12
|
+
# http://gist.github.com/25455
|
13
|
+
# chris@ozmm.org
|
14
|
+
#
|
15
|
+
def context(*args, &block)
|
16
|
+
return super unless (name = args.first) && block
|
17
|
+
require 'test/unit'
|
18
|
+
klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
|
19
|
+
def self.test(name, &block)
|
20
|
+
define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
|
21
|
+
end
|
22
|
+
def self.xtest(*args) end
|
23
|
+
def self.setup(&block) define_method(:setup, &block) end
|
24
|
+
def self.teardown(&block) define_method(:teardown, &block) end
|
25
|
+
end
|
26
|
+
(class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
|
27
|
+
klass.class_eval &block
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apnd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Joshua Priddle
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-29 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: json
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id002
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: daemons
|
46
|
+
prerelease: false
|
47
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
type: :runtime
|
55
|
+
version_requirements: *id003
|
56
|
+
description: |
|
57
|
+
|
58
|
+
# APND
|
59
|
+
|
60
|
+
APND (Apple Push Notification Daemon) is a ruby library to send Apple Push
|
61
|
+
Notifications (APNs) to iPhones.
|
62
|
+
|
63
|
+
Apple recommends application developers create one connection to their
|
64
|
+
upstream push notification server, rather than creating one per notification.
|
65
|
+
|
66
|
+
APND acts as an intermediary between your application and Apple. Your
|
67
|
+
application's notifications are queued to APND, which are then sent to
|
68
|
+
Apple over a single connection.
|
69
|
+
|
70
|
+
Within ruby applications, `APND::Notification` can be used to send
|
71
|
+
notifications to a running APND instance or directly to Apple. A command
|
72
|
+
line utility, `apnd-push`, can be used to send single notifications for
|
73
|
+
testing purposes.
|
74
|
+
|
75
|
+
email: jpriddle@nevercraft.net
|
76
|
+
executables:
|
77
|
+
- apnd
|
78
|
+
- apnd-push
|
79
|
+
extensions: []
|
80
|
+
|
81
|
+
extra_rdoc_files:
|
82
|
+
- README.markdown
|
83
|
+
files:
|
84
|
+
- Rakefile
|
85
|
+
- README.markdown
|
86
|
+
- bin/apnd
|
87
|
+
- bin/apnd-push
|
88
|
+
- lib/apnd/daemon/apple_connection.rb
|
89
|
+
- lib/apnd/daemon/protocol.rb
|
90
|
+
- lib/apnd/daemon.rb
|
91
|
+
- lib/apnd/errors.rb
|
92
|
+
- lib/apnd/notification.rb
|
93
|
+
- lib/apnd/settings.rb
|
94
|
+
- lib/apnd/version.rb
|
95
|
+
- lib/apnd.rb
|
96
|
+
- test/daemon_test.rb
|
97
|
+
- test/notification_test.rb
|
98
|
+
- test/test_helper.rb
|
99
|
+
has_rdoc: true
|
100
|
+
homepage: http://github.com/itspriddle/apnd
|
101
|
+
licenses: []
|
102
|
+
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options:
|
105
|
+
- --charset=UTF-8
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
version: "0"
|
122
|
+
requirements: []
|
123
|
+
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.3.6
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: "APND: Apple Push Notification Daemon sends Apple Push Notifications to iPhones"
|
129
|
+
test_files: []
|
130
|
+
|