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