apnd 0.1.8 → 0.2.0
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/HISTORY.markdown +10 -0
- data/README.markdown +47 -10
- data/bin/apnd +25 -103
- data/lib/apnd.rb +3 -1
- data/lib/apnd/cli.rb +195 -0
- data/lib/apnd/daemon.rb +5 -5
- data/lib/apnd/daemon/protocol.rb +18 -7
- data/lib/apnd/feedback.rb +65 -0
- data/lib/apnd/settings.rb +43 -0
- data/lib/apnd/version.rb +2 -2
- data/test/apnd_test.rb +23 -0
- data/test/test_helper.rb +2 -2
- metadata +9 -8
- data/bin/apnd-push +0 -90
data/HISTORY.markdown
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# 0.2.0 - 2011-04-04
|
2
|
+
|
3
|
+
Note that v0.2.0 fixes a critical error, and that previous releases of APND
|
4
|
+
should not be used.
|
5
|
+
|
6
|
+
* Fixes a critical error in `APND::Daemon::Protocol` which caused problems
|
7
|
+
with notifications containing new line characters in their message. Props to
|
8
|
+
[mwotton](https://github.com/mwotton).
|
9
|
+
* Merged `apnd` and `apnd-push` CLI tools into `apnd`.
|
10
|
+
* Added support for Apple Push Notification Feedback Service
|
data/README.markdown
CHANGED
@@ -12,8 +12,8 @@ are then sent to Apple over a single connection.
|
|
12
12
|
|
13
13
|
Within ruby applications, `APND::Notification` can be used to send
|
14
14
|
notifications to a running APND instance (see **APND Notification** below) or
|
15
|
-
directly to Apple.
|
16
|
-
|
15
|
+
directly to Apple. The command line can be used to send single notifications
|
16
|
+
for testing purposes (see **APND Client** below).
|
17
17
|
|
18
18
|
|
19
19
|
## General Usage
|
@@ -25,7 +25,7 @@ Apple over a single connection as explained above. The `apnd` command line
|
|
25
25
|
utility is used to start APND.
|
26
26
|
|
27
27
|
Usage:
|
28
|
-
apnd [OPTIONS] --apple-cert </path/to/cert>
|
28
|
+
apnd daemon [OPTIONS] --apple-cert </path/to/cert>
|
29
29
|
|
30
30
|
Required Arguments:
|
31
31
|
--apple-cert [PATH] PATH to APN certificate from Apple
|
@@ -41,18 +41,17 @@ utility is used to start APND.
|
|
41
41
|
--foreground Run APND in foreground without daemonizing
|
42
42
|
|
43
43
|
Help:
|
44
|
-
--version Show version
|
45
44
|
--help Show this message
|
46
45
|
|
47
46
|
|
48
47
|
### APND Client
|
49
48
|
|
50
|
-
APND includes a command line
|
51
|
-
|
52
|
-
|
49
|
+
APND includes a command line client which can be used to send notifications to
|
50
|
+
a running APND instance. It is only recommended to send notifications via
|
51
|
+
`apnd push` for testing purposes.
|
53
52
|
|
54
53
|
Usage:
|
55
|
-
apnd
|
54
|
+
apnd push [OPTIONS] --token <token>
|
56
55
|
|
57
56
|
Required Arguments:
|
58
57
|
--token [TOKEN] Set Notification's iPhone token to TOKEN
|
@@ -66,7 +65,6 @@ recommended to send notifications directly to Apple for testing purposes.
|
|
66
65
|
--port [PORT] Send Notification on PORT (default is 22195)
|
67
66
|
|
68
67
|
Help:
|
69
|
-
--version Show version
|
70
68
|
--help Show this message
|
71
69
|
|
72
70
|
|
@@ -118,6 +116,40 @@ push notifications to APND.
|
|
118
116
|
)
|
119
117
|
|
120
118
|
|
119
|
+
### APND Feedback
|
120
|
+
|
121
|
+
Apple Push Notification Service keeps a log when you attempt to deliver
|
122
|
+
a notification to a device that has removed your application. A Feedback
|
123
|
+
Service is provided which applications should periodically check to remove
|
124
|
+
from their databases.
|
125
|
+
|
126
|
+
The `APND::Feedback` class can be used within your application to retrieve
|
127
|
+
a list of device tokens that you are sending notifications to but have
|
128
|
+
removed your application.
|
129
|
+
|
130
|
+
APND::Feedback.upstream_host = 'feedback.push.apple.com'
|
131
|
+
APND::Feedback.upstream_port = 2196
|
132
|
+
|
133
|
+
# Block form
|
134
|
+
APND::Feedback.find_stale_devices do |token, removed_at|
|
135
|
+
device = YourApp::Device.find_by_token(token)
|
136
|
+
unless device.registered_at > removed_at
|
137
|
+
device.push_enabled = 0
|
138
|
+
device.save
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Array form
|
143
|
+
stale = APND::Feedback.find_stale_devices
|
144
|
+
stale.each do |(token, removed_at)|
|
145
|
+
device = YourApp::Device.find_by_token(token)
|
146
|
+
unless device.registered_at > removed_at
|
147
|
+
device.push_enabled = 0
|
148
|
+
device.save
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
|
121
153
|
## Prerequisites
|
122
154
|
|
123
155
|
You must have a valid Apple Push Notification Certificate for your iPhone
|
@@ -156,6 +188,11 @@ wanted, so I rolled my own using theirs as starting points. If APND doesn't
|
|
156
188
|
suit you, check them out instead.
|
157
189
|
|
158
190
|
|
191
|
+
## Contributors
|
192
|
+
|
193
|
+
* Mark Wotton ([mwotton](https://github.com/mwotton))
|
194
|
+
|
195
|
+
|
159
196
|
## Copyright
|
160
197
|
|
161
|
-
Copyright (c) 2010 Joshua Priddle. See LICENSE for details.
|
198
|
+
Copyright (c) 2010-2011 Joshua Priddle. See LICENSE for details.
|
data/bin/apnd
CHANGED
@@ -1,112 +1,34 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
ARGV << '--help' if ARGV.empty?
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
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('--daemon-timer [SECONDS]', 'Set APND queue refresh time to SECONDS (default is 30)') do |seconds|
|
54
|
-
options[:daemon_timer] = seconds.to_i
|
55
|
-
end
|
56
|
-
|
57
|
-
opt.on('--foreground', 'Run APND in foreground without daemonizing') do
|
58
|
-
options[:foreground] = true
|
59
|
-
end
|
60
|
-
|
61
|
-
opt.separator "\nHelp:\n"
|
62
|
-
|
63
|
-
opt.on('--version', 'Show version') do
|
64
|
-
puts "APND #{APND::Version}"
|
65
|
-
exit
|
66
|
-
end
|
67
|
-
|
68
|
-
opt.on('--help', 'Show this message') do
|
69
|
-
puts opt
|
70
|
-
exit
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
begin
|
75
|
-
opts.parse!
|
76
|
-
if options.empty?
|
77
|
-
puts opts
|
78
|
-
exit
|
79
|
-
end
|
80
|
-
|
81
|
-
unless options[:apple_cert]
|
82
|
-
raise OptionParser::MissingArgument, "must specify --apple-cert"
|
83
|
-
end
|
84
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
85
|
-
puts "#{$0}: #{$!.message}"
|
86
|
-
puts "#{$0}: try '#{$0} --help' for more information"
|
87
|
-
exit
|
5
|
+
if $0 == __FILE__
|
6
|
+
require 'rubygems'
|
7
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
88
8
|
end
|
89
9
|
|
90
|
-
|
91
|
-
# Setup AppleConnection
|
92
|
-
config.apple.cert = options[:apple_cert] if options[:apple_cert]
|
93
|
-
config.apple.cert_pass = options[:apple_cert_pass] if options[:apple_cert_pass]
|
94
|
-
config.apple.host = options[:apple_host] if options[:apple_host]
|
95
|
-
config.apple.port = options[:apple_port] if options[:apple_port]
|
10
|
+
require 'apnd'
|
96
11
|
|
97
|
-
|
98
|
-
config.daemon.bind = options[:daemon_bind] if options[:daemon_bind]
|
99
|
-
config.daemon.port = options[:daemon_port] if options[:daemon_port]
|
100
|
-
config.daemon.log_file = options[:daemon_log_file] if options[:daemon_log_file]
|
101
|
-
config.daemon.timer = options[:daemon_timer] if options[:daemon_timer]
|
102
|
-
end
|
12
|
+
command = ARGV.shift
|
103
13
|
|
104
|
-
|
105
|
-
|
106
|
-
|
14
|
+
case command
|
15
|
+
when 'daemon'
|
16
|
+
APND::CLI.daemon(ARGV)
|
17
|
+
when 'push'
|
18
|
+
APND::CLI.push(ARGV)
|
19
|
+
when '--version', '-v'
|
20
|
+
puts "APND v#{APND::Version}"
|
107
21
|
else
|
108
|
-
unless
|
109
|
-
|
110
|
-
|
111
|
-
|
22
|
+
puts "Error: Invalid command" unless %w(-h --help).include?(command)
|
23
|
+
puts <<-HELP
|
24
|
+
Usage: apnd COMMAND [ARGS]
|
25
|
+
|
26
|
+
Command list:
|
27
|
+
daemon Start the APND Daemon
|
28
|
+
push Send a single push notification (for development use only)
|
29
|
+
|
30
|
+
Help:
|
31
|
+
--version Show version
|
32
|
+
--help Show this message
|
33
|
+
HELP
|
112
34
|
end
|
data/lib/apnd.rb
CHANGED
@@ -2,10 +2,12 @@ require 'json'
|
|
2
2
|
|
3
3
|
module APND
|
4
4
|
autoload :Version, 'apnd/version'
|
5
|
+
autoload :CLI, 'apnd/cli'
|
5
6
|
autoload :Errors, 'apnd/errors'
|
6
7
|
autoload :Settings, 'apnd/settings'
|
7
8
|
autoload :Daemon, 'apnd/daemon'
|
8
9
|
autoload :Notification, 'apnd/notification'
|
10
|
+
autoload :Feedback, 'apnd/feedback'
|
9
11
|
|
10
12
|
#
|
11
13
|
# Returns APND::Settings
|
@@ -24,7 +26,7 @@ module APND
|
|
24
26
|
#
|
25
27
|
# Write message to stdout with date
|
26
28
|
#
|
27
|
-
def self.
|
29
|
+
def self.logger(message) #:nodoc:
|
28
30
|
puts "[%s] %s" % [Time.now.strftime("%Y-%m-%d %H:%M:%S"), message]
|
29
31
|
end
|
30
32
|
|
data/lib/apnd/cli.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'daemons'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module APND
|
5
|
+
class CLI #:nodoc: all
|
6
|
+
|
7
|
+
#
|
8
|
+
# Run apnd push
|
9
|
+
#
|
10
|
+
def self.push(argv)
|
11
|
+
help = <<-HELP
|
12
|
+
Usage:
|
13
|
+
apnd push [OPTIONS] --token <token>
|
14
|
+
|
15
|
+
HELP
|
16
|
+
|
17
|
+
options = {}
|
18
|
+
|
19
|
+
opts = OptionParser.new do |opt|
|
20
|
+
opt.banner = help
|
21
|
+
|
22
|
+
opt.separator "Required Arguments:\n"
|
23
|
+
|
24
|
+
opt.on('--token [TOKEN]', "Set Notification's iPhone token to TOKEN") do |token|
|
25
|
+
options[:token] = token
|
26
|
+
end
|
27
|
+
|
28
|
+
opt.separator "\nOptional Arguments:\n"
|
29
|
+
|
30
|
+
opt.on('--alert [MESSAGE]', "Set Notification's alert to MESSAGE") do |alert|
|
31
|
+
options[:alert] = alert
|
32
|
+
end
|
33
|
+
|
34
|
+
opt.on('--sound [SOUND]', "Set Notification's sound to SOUND") do |sound|
|
35
|
+
options[:sound] = sound
|
36
|
+
end
|
37
|
+
|
38
|
+
opt.on('--badge [NUMBER]', "Set Notification's badge number to NUMBER") do |badge|
|
39
|
+
options[:badge] = badge.to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
opt.on('--custom [JSON]', "Set Notification's custom data to JSON") do |custom|
|
43
|
+
begin
|
44
|
+
options[:custom] = JSON.parse(custom)
|
45
|
+
rescue JSON::ParserError => e
|
46
|
+
puts "Invalid JSON: #{e}"
|
47
|
+
exit -1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
opt.on('--host [HOST]', "Send Notification to HOST, usually the one running APND (default is 'localhost')") do |host|
|
52
|
+
options[:host] = host
|
53
|
+
end
|
54
|
+
|
55
|
+
opt.on('--port [PORT]', 'Send Notification on PORT (default is 22195)') do |port|
|
56
|
+
options[:port] = port.to_i
|
57
|
+
end
|
58
|
+
|
59
|
+
opt.separator "\nHelp:\n"
|
60
|
+
|
61
|
+
opt.on('--help', 'Show this message') do
|
62
|
+
puts opt
|
63
|
+
exit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
begin
|
68
|
+
opts.parse!
|
69
|
+
if options.empty?
|
70
|
+
puts opts
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
|
74
|
+
unless options[:token]
|
75
|
+
raise OptionParser::MissingArgument, "must specify --token"
|
76
|
+
end
|
77
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
78
|
+
puts "#{$0}: #{$!.message}"
|
79
|
+
puts "#{$0}: try '#{$0} --help' for more information"
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
|
83
|
+
# Configure Notification upstream host/port
|
84
|
+
APND::Notification.upstream_host = options.delete(:host) if options[:host]
|
85
|
+
APND::Notification.upstream_port = options.delete(:port) if options[:port]
|
86
|
+
|
87
|
+
APND::Notification.create(options)
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Run apnd daemon
|
92
|
+
#
|
93
|
+
def self.daemon(argv)
|
94
|
+
help = <<-HELP
|
95
|
+
Usage:
|
96
|
+
apnd daemon --apple-cert </path/to/cert>
|
97
|
+
|
98
|
+
HELP
|
99
|
+
|
100
|
+
options = {}
|
101
|
+
|
102
|
+
opts = OptionParser.new do |opt|
|
103
|
+
opt.banner = help
|
104
|
+
|
105
|
+
opt.separator "Required Arguments:\n"
|
106
|
+
|
107
|
+
opt.on('--apple-cert [PATH]', 'PATH to APN certificate from Apple') do |cert|
|
108
|
+
options[:apple_cert] = cert
|
109
|
+
end
|
110
|
+
|
111
|
+
opt.separator "\nOptional Arguments:\n"
|
112
|
+
|
113
|
+
opt.on('--apple-host [HOST]', "Connect to Apple at HOST (default is gateway.sandbox.push.apple.com)") do |host|
|
114
|
+
options[:apple_host] = host
|
115
|
+
end
|
116
|
+
|
117
|
+
opt.on('--apple-port [PORT]', 'Connect to Apple on PORT (default is 2195)') do |port|
|
118
|
+
options[:apple_port] = port.to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
opt.on('--apple-cert-pass [PASSWORD]', 'PASSWORD for APN certificate from Apple') do |pass|
|
122
|
+
options[:apple_cert_pass] = pass
|
123
|
+
end
|
124
|
+
|
125
|
+
opt.on('--daemon-port [PORT]', 'Run APND on PORT (default is 22195)') do |port|
|
126
|
+
options[:daemon_port] = port.to_i
|
127
|
+
end
|
128
|
+
|
129
|
+
opt.on('--daemon-bind [ADDRESS]', 'Bind APND to ADDRESS (default is 0.0.0.0)') do |bind|
|
130
|
+
options[:daemon_bind] = bind
|
131
|
+
end
|
132
|
+
|
133
|
+
opt.on('--daemon-log-file [PATH]', 'PATH to APND log file (default is /var/log/apnd.log)') do |log|
|
134
|
+
options[:daemon_log_file] = log
|
135
|
+
end
|
136
|
+
|
137
|
+
opt.on('--daemon-timer [SECONDS]', 'Set APND queue refresh time to SECONDS (default is 30)') do |seconds|
|
138
|
+
options[:daemon_timer] = seconds.to_i
|
139
|
+
end
|
140
|
+
|
141
|
+
opt.on('--foreground', 'Run APND in foreground without daemonizing') do
|
142
|
+
options[:foreground] = true
|
143
|
+
end
|
144
|
+
|
145
|
+
opt.separator "\nHelp:\n"
|
146
|
+
|
147
|
+
opt.on('--help', 'Show this message') do
|
148
|
+
puts opt
|
149
|
+
exit
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
begin
|
154
|
+
opts.parse!
|
155
|
+
if options.empty?
|
156
|
+
puts opts
|
157
|
+
exit
|
158
|
+
end
|
159
|
+
|
160
|
+
unless options[:apple_cert]
|
161
|
+
raise OptionParser::MissingArgument, "must specify --apple-cert"
|
162
|
+
end
|
163
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
164
|
+
puts "#{$0}: #{$!.message}"
|
165
|
+
puts "#{$0}: try '#{$0} --help' for more information"
|
166
|
+
exit
|
167
|
+
end
|
168
|
+
|
169
|
+
APND.configure do |config|
|
170
|
+
# Setup AppleConnection
|
171
|
+
config.apple.cert = options[:apple_cert] if options[:apple_cert]
|
172
|
+
config.apple.cert_pass = options[:apple_cert_pass] if options[:apple_cert_pass]
|
173
|
+
config.apple.host = options[:apple_host] if options[:apple_host]
|
174
|
+
config.apple.port = options[:apple_port] if options[:apple_port]
|
175
|
+
|
176
|
+
# Setup Daemon
|
177
|
+
config.daemon.bind = options[:daemon_bind] if options[:daemon_bind]
|
178
|
+
config.daemon.port = options[:daemon_port] if options[:daemon_port]
|
179
|
+
config.daemon.log_file = options[:daemon_log_file] if options[:daemon_log_file]
|
180
|
+
config.daemon.timer = options[:daemon_timer] if options[:daemon_timer]
|
181
|
+
end
|
182
|
+
|
183
|
+
if APND.settings.apple.cert.nil?
|
184
|
+
puts opts
|
185
|
+
exit
|
186
|
+
else
|
187
|
+
unless options[:foreground]
|
188
|
+
Daemonize.daemonize(APND.settings.daemon.log_file, 'apnd')
|
189
|
+
end
|
190
|
+
APND::Daemon.run!
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/lib/apnd/daemon.rb
CHANGED
@@ -35,7 +35,7 @@ module APND
|
|
35
35
|
#
|
36
36
|
def run!
|
37
37
|
EventMachine::run do
|
38
|
-
APND.
|
38
|
+
APND.logger "Starting APND Daemon v#{APND::Version} on #{@bind}:#{@port}"
|
39
39
|
EventMachine::start_server(@bind, @port, APND::Daemon::ServerConnection) do |server|
|
40
40
|
server.queue = @queue
|
41
41
|
end
|
@@ -54,19 +54,19 @@ module APND
|
|
54
54
|
def process_notifications!
|
55
55
|
count = @queue.size
|
56
56
|
if count > 0
|
57
|
-
APND.
|
57
|
+
APND.logger "Queue has #{count} item#{count == 1 ? '' : 's'}"
|
58
58
|
@apple.connect!
|
59
59
|
count.times do
|
60
60
|
@queue.pop do |notification|
|
61
61
|
begin
|
62
|
-
APND.
|
62
|
+
APND.logger "Sending notification for #{notification.token}"
|
63
63
|
@apple.write(notification.to_bytes)
|
64
64
|
rescue Errno::EPIPE, OpenSSL::SSL::SSLError
|
65
|
-
APND.
|
65
|
+
APND.logger "Error, notification has been added back to the queue"
|
66
66
|
@queue.push(notification)
|
67
67
|
@apple.reconnect!
|
68
68
|
rescue RuntimeError => error
|
69
|
-
APND.
|
69
|
+
APND.logger "Error: #{error}"
|
70
70
|
end
|
71
71
|
end
|
72
72
|
end
|
data/lib/apnd/daemon/protocol.rb
CHANGED
@@ -11,7 +11,7 @@ module APND
|
|
11
11
|
#
|
12
12
|
def post_init
|
13
13
|
@address = ::Socket.unpack_sockaddr_in(self.get_peername)
|
14
|
-
APND.
|
14
|
+
APND.logger "#{@address.last}:#{@address.first} opened connection"
|
15
15
|
end
|
16
16
|
|
17
17
|
#
|
@@ -21,22 +21,33 @@ module APND
|
|
21
21
|
# queued
|
22
22
|
#
|
23
23
|
def unbind
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
# totally broken.
|
25
|
+
@buffer.chomp!
|
26
|
+
while(@buffer.length > 0) do
|
27
|
+
# 3 bytes for header
|
28
|
+
# 32 bytes for token
|
29
|
+
# 2 bytes for json length
|
30
|
+
|
31
|
+
# taking the last is acceptable because we know it's never
|
32
|
+
# longer than 256 bytes from the apple documentation.
|
33
|
+
json_length = @buffer.slice(35,37).unpack('CC').last
|
34
|
+
chunk = @buffer.slice!(0,json_length + 3 + 32 + 2)
|
35
|
+
if notification = APND::Notification.valid?(chunk)
|
36
|
+
APND.logger "#{@address.last}:#{@address.first} added new Notification to queue"
|
27
37
|
queue.push(notification)
|
28
38
|
else
|
29
|
-
APND.
|
39
|
+
APND.logger "#{@address.last}:#{@address.first} submitted invalid Notification"
|
30
40
|
end
|
41
|
+
@buffer.strip!
|
31
42
|
end
|
32
|
-
APND.
|
43
|
+
APND.logger "#{@address.last}:#{@address.first} closed connection"
|
33
44
|
end
|
34
45
|
|
35
46
|
#
|
36
47
|
# Add incoming notification(s) to @buffer
|
37
48
|
#
|
38
49
|
def receive_data(data)
|
39
|
-
APND.
|
50
|
+
APND.logger "#{@address.last}:#{@address.first} buffering data"
|
40
51
|
(@buffer ||= "") << data
|
41
52
|
end
|
42
53
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module APND
|
2
|
+
#
|
3
|
+
# APND::Feedback receives feedback from Apple when notifications are
|
4
|
+
# being rejected for a specific token. This is usually due to the user
|
5
|
+
# uninstalling your application.
|
6
|
+
#
|
7
|
+
class Feedback
|
8
|
+
|
9
|
+
class << self
|
10
|
+
#
|
11
|
+
# The host to receive feedback from, usually apple
|
12
|
+
#
|
13
|
+
attr_accessor :upstream_host
|
14
|
+
|
15
|
+
#
|
16
|
+
# The port to connect to upstream_host on
|
17
|
+
#
|
18
|
+
attr_accessor :upstream_port
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Set upstream host/port to default values
|
23
|
+
#
|
24
|
+
self.upstream_host = APND.settings.feedback.host
|
25
|
+
self.upstream_port = APND.settings.feedback.port.to_i
|
26
|
+
|
27
|
+
#
|
28
|
+
# Connect to Apple's Feedback Service and return an array of device
|
29
|
+
# tokens that should no longer receive push notifications
|
30
|
+
#
|
31
|
+
def self.find_stale_devices(&block)
|
32
|
+
feedback = self.new
|
33
|
+
devices = feedback.receive
|
34
|
+
devices.each { |device| yield *device } if block_given?
|
35
|
+
devices
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Create a connection to Apple's Feedback Service
|
40
|
+
#
|
41
|
+
def initialize
|
42
|
+
@apple = APND::Daemon::AppleConnection.new({
|
43
|
+
:cert => APND.settings.apple.cert,
|
44
|
+
:cert_pass => APND.settings.apple.cert_pass,
|
45
|
+
:host => self.class.upstream_host,
|
46
|
+
:port => self.class.upstream_port.to_i
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Receive feedback from Apple and return an array of device tokens
|
52
|
+
#
|
53
|
+
def receive
|
54
|
+
tokens = []
|
55
|
+
@apple.open do |sock|
|
56
|
+
while line = sock.gets
|
57
|
+
payload = line.strip.unpack('N1n1H140')
|
58
|
+
tokens << [payload[2].strip, Time.at(payload[0])]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
tokens
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/apnd/settings.rb
CHANGED
@@ -104,6 +104,32 @@ module APND
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
+
#
|
108
|
+
# Settings for APND::Feedback
|
109
|
+
#
|
110
|
+
class Feedback
|
111
|
+
|
112
|
+
#
|
113
|
+
# Host used to connect to Apple
|
114
|
+
#
|
115
|
+
# Development: feedback.sandbox.push.apple.com
|
116
|
+
# Production: feedback.push.apple.com
|
117
|
+
#
|
118
|
+
attr_accessor :host
|
119
|
+
|
120
|
+
#
|
121
|
+
# Port used to connect to Apple
|
122
|
+
#
|
123
|
+
# Default: 2196
|
124
|
+
#
|
125
|
+
attr_accessor :port
|
126
|
+
|
127
|
+
def initialize
|
128
|
+
@host = 'feedback.sandbox.push.apple.com'
|
129
|
+
@port = 2196
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
107
133
|
#
|
108
134
|
# Returns the AppleConnection settings
|
109
135
|
#
|
@@ -158,5 +184,22 @@ module APND
|
|
158
184
|
notification.host = options[:host] if options[:host]
|
159
185
|
end
|
160
186
|
end
|
187
|
+
|
188
|
+
#
|
189
|
+
# Returns the Feedback settings
|
190
|
+
#
|
191
|
+
def feedback
|
192
|
+
@feedback ||= APND::Settings::Feedback.new
|
193
|
+
end
|
194
|
+
|
195
|
+
#
|
196
|
+
# Mass assign Feedback settings
|
197
|
+
#
|
198
|
+
def feedback=(options = {})
|
199
|
+
if options.respond_to?(:keys)
|
200
|
+
feedback.port = options[:port] if options[:port]
|
201
|
+
feedback.host = options[:host] if options[:host]
|
202
|
+
end
|
203
|
+
end
|
161
204
|
end
|
162
205
|
end
|
data/lib/apnd/version.rb
CHANGED
data/test/apnd_test.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
2
|
|
3
3
|
class APNDTest < Test::Unit::TestCase
|
4
|
+
# FIXME: Tests fail if the hash keys here arent in this order. It shouldn't
|
5
|
+
# be so fragile.
|
4
6
|
@@bytes = %|\000\000 \376\025\242}]\363\303Gx\336\373\037O8\200&\\\305,\f\004v\202\";\345\237\266\205\000\251\242\000\\{\"location\":\"New York\",\"aps\":{\"badge\":10,\"sound\":\"default\",\"alert\":\"Red Alert, Numba One!\"}}|
|
5
7
|
|
6
8
|
context "APND Notification" do
|
@@ -51,6 +53,8 @@ class APNDTest < Test::Unit::TestCase
|
|
51
53
|
end
|
52
54
|
end
|
53
55
|
|
56
|
+
|
57
|
+
|
54
58
|
end
|
55
59
|
|
56
60
|
context "APND Daemon" do
|
@@ -77,6 +81,25 @@ class APNDTest < Test::Unit::TestCase
|
|
77
81
|
end
|
78
82
|
assert_equal 0, @daemon.queue.size
|
79
83
|
end
|
84
|
+
|
85
|
+
|
86
|
+
context "newlines" do
|
87
|
+
should "be able to parse a notification with an embedded newline character" do
|
88
|
+
@newline_notification = APND::Notification.new({
|
89
|
+
# :token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
|
90
|
+
:token => '74b2a2197d7727a70f939de05a4c7fe8bd4a7d960a77ef4701a80cb7b293ee23',
|
91
|
+
:alert => 'Red Alert, Numba One!',
|
92
|
+
:sound => 'default',
|
93
|
+
:badge => 10,
|
94
|
+
:custom => { 'location' => 'New York' }
|
95
|
+
})
|
96
|
+
@daemon.receive_data(@newline_notification.to_bytes)
|
97
|
+
@daemon.unbind
|
98
|
+
assert_equal 1, @daemon.queue.size
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
80
103
|
end
|
81
104
|
end
|
82
105
|
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apnd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Joshua Priddle
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-04-04 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -84,7 +84,6 @@ description: " APND (Apple Push Notification Daemon) is a ruby library to sen
|
|
84
84
|
email: jpriddle@nevercraft.net
|
85
85
|
executables:
|
86
86
|
- apnd
|
87
|
-
- apnd-push
|
88
87
|
extensions: []
|
89
88
|
|
90
89
|
extra_rdoc_files:
|
@@ -92,13 +91,15 @@ extra_rdoc_files:
|
|
92
91
|
files:
|
93
92
|
- Rakefile
|
94
93
|
- README.markdown
|
94
|
+
- HISTORY.markdown
|
95
95
|
- bin/apnd
|
96
|
-
-
|
96
|
+
- lib/apnd/cli.rb
|
97
97
|
- lib/apnd/daemon/apple_connection.rb
|
98
98
|
- lib/apnd/daemon/protocol.rb
|
99
99
|
- lib/apnd/daemon/server_connection.rb
|
100
100
|
- lib/apnd/daemon.rb
|
101
101
|
- lib/apnd/errors.rb
|
102
|
+
- lib/apnd/feedback.rb
|
102
103
|
- lib/apnd/notification.rb
|
103
104
|
- lib/apnd/settings.rb
|
104
105
|
- lib/apnd/version.rb
|
@@ -135,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
136
|
requirements: []
|
136
137
|
|
137
138
|
rubyforge_project:
|
138
|
-
rubygems_version: 1.
|
139
|
+
rubygems_version: 1.6.2
|
139
140
|
signing_key:
|
140
141
|
specification_version: 3
|
141
142
|
summary: "APND: Apple Push Notification Daemon sends Apple Push Notifications to iPhones"
|
data/bin/apnd-push
DELETED
@@ -1,90 +0,0 @@
|
|
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>
|
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.separator "\nOptional Arguments:\n"
|
27
|
-
|
28
|
-
opt.on('--alert [MESSAGE]', "Set Notification's alert to MESSAGE") do |alert|
|
29
|
-
options[:alert] = alert
|
30
|
-
end
|
31
|
-
|
32
|
-
opt.on('--sound [SOUND]', "Set Notification's sound to SOUND") 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]
|
78
|
-
raise OptionParser::MissingArgument, "must specify --token"
|
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)
|