mwotton-apnd 0.1.8
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 +193 -0
- data/Rakefile +31 -0
- data/bin/apnd +35 -0
- data/lib/apnd.rb +33 -0
- data/lib/apnd/#notification.rb# +193 -0
- data/lib/apnd/cli.rb +195 -0
- data/lib/apnd/daemon.rb +78 -0
- data/lib/apnd/daemon/apple_connection.rb +84 -0
- data/lib/apnd/daemon/protocol.rb +54 -0
- data/lib/apnd/daemon/server_connection.rb +15 -0
- data/lib/apnd/errors.rb +22 -0
- data/lib/apnd/feedback.rb +65 -0
- data/lib/apnd/notification.rb +189 -0
- data/lib/apnd/settings.rb +205 -0
- data/lib/apnd/version.rb +11 -0
- data/test/apnd_test.rb +104 -0
- data/test/test_helper.rb +27 -0
- metadata +144 -0
data/README.markdown
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# APND
|
2
|
+
|
3
|
+
APND (Apple Push Notification Daemon) is a ruby library to send Apple Push
|
4
|
+
Notifications 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. The command line can be used to send single notifications
|
16
|
+
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 daemon [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
|
+
--daemon-timer [SECONDS] Set APND queue refresh time to SECONDS (default is 30)
|
41
|
+
--foreground Run APND in foreground without daemonizing
|
42
|
+
|
43
|
+
Help:
|
44
|
+
--help Show this message
|
45
|
+
|
46
|
+
|
47
|
+
### APND Client
|
48
|
+
|
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.
|
52
|
+
|
53
|
+
Usage:
|
54
|
+
apnd push [OPTIONS] --token <token>
|
55
|
+
|
56
|
+
Required Arguments:
|
57
|
+
--token [TOKEN] Set Notification's iPhone token to TOKEN
|
58
|
+
|
59
|
+
Optional Arguments:
|
60
|
+
--alert [MESSAGE] Set Notification's alert to MESSAGE
|
61
|
+
--sound [SOUND] Set Notification's sound to SOUND
|
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
|
+
--help Show this message
|
69
|
+
|
70
|
+
|
71
|
+
### APND Notification
|
72
|
+
|
73
|
+
The `APND::Notification` class can be used within your application to send
|
74
|
+
push notifications to APND.
|
75
|
+
|
76
|
+
require 'apnd'
|
77
|
+
|
78
|
+
# Set the host/port APND is running on
|
79
|
+
# (not needed if you're using localhost:22195)
|
80
|
+
# Put this in config/initializers/apnd.rb for Rails
|
81
|
+
APND::Notification.upstream_host = 'localhost'
|
82
|
+
APND::Notification.upstream_port = 22195
|
83
|
+
|
84
|
+
|
85
|
+
# Initialize some notifications
|
86
|
+
notification1 = APND::Notification.new(
|
87
|
+
:alert => 'Alert!',
|
88
|
+
:token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
|
89
|
+
:badge => 1
|
90
|
+
)
|
91
|
+
|
92
|
+
notification2 = APND::Notification.new(
|
93
|
+
:alert => 'Red Alert!',
|
94
|
+
:token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
|
95
|
+
:badge => 99
|
96
|
+
)
|
97
|
+
|
98
|
+
|
99
|
+
# Send multiple notifications at once to avoid overhead in
|
100
|
+
# opening/closing the upstream socket connection each time
|
101
|
+
#
|
102
|
+
# *IMPORTANT!* Use sock.puts as it appends a new line. If you don't,
|
103
|
+
# you'll only receive the first notification.
|
104
|
+
|
105
|
+
APND::Notification.open_upstream_socket do |sock|
|
106
|
+
sock.puts(notification1.to_bytes)
|
107
|
+
sock.puts(notification2.to_bytes)
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# Send a notification to the upstream socket immediately
|
112
|
+
notification3 = APND::Notification.create(
|
113
|
+
:alert => 'Alert!',
|
114
|
+
:token => 'fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2',
|
115
|
+
:badge => 0
|
116
|
+
)
|
117
|
+
|
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
|
+
|
153
|
+
## Prerequisites
|
154
|
+
|
155
|
+
You must have a valid Apple Push Notification Certificate for your iPhone
|
156
|
+
application. Obtain your APN certificate from the iPhone Provisioning Portal
|
157
|
+
at [developer.apple.com](http://developer.apple.com/).
|
158
|
+
|
159
|
+
|
160
|
+
## Requirements
|
161
|
+
|
162
|
+
* [EventMachine](http://github.com/eventmachine/eventmachine)
|
163
|
+
* [Daemons](http://github.com/ghazel/daemons)
|
164
|
+
* [JSON](http://github.com/flori/json)
|
165
|
+
|
166
|
+
Ruby must be compiled with OpenSSL support.
|
167
|
+
|
168
|
+
Tests use [Shoulda](http://github.com/thoughtbot/shoulda), and optionally
|
169
|
+
[TURN](https://github.com/TwP/turn).
|
170
|
+
|
171
|
+
|
172
|
+
## Installation
|
173
|
+
|
174
|
+
RubyGems:
|
175
|
+
|
176
|
+
gem install apnd
|
177
|
+
|
178
|
+
Git:
|
179
|
+
|
180
|
+
git clone git://github.com/itspriddle/apnd.git
|
181
|
+
|
182
|
+
|
183
|
+
## Credit
|
184
|
+
|
185
|
+
APND is based on [apnserver](http://github.com/bpoweski/apnserver) and
|
186
|
+
[apn_on_rails](http://github.com/PRX/apn_on_rails). Either worked just how I
|
187
|
+
wanted, so I rolled my own using theirs as starting points. If APND doesn't
|
188
|
+
suit you, check them out instead.
|
189
|
+
|
190
|
+
|
191
|
+
## Copyright
|
192
|
+
|
193
|
+
Copyright (c) 2010-2011 Joshua Priddle. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
$:.unshift 'lib'
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
require 'rake/testtask'
|
6
|
+
Rake::TestTask.new(:test) do |test|
|
7
|
+
test.libs << 'lib' << 'test' << '.'
|
8
|
+
test.pattern = 'test/**/*_test.rb'
|
9
|
+
test.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Open an irb session preloaded with this library"
|
13
|
+
task :console do
|
14
|
+
sh "irb -rubygems -r ./lib/apnd.rb -I ./lib"
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'sdoc_helpers'
|
18
|
+
desc "Push a new version to Gemcutter"
|
19
|
+
task :publish do
|
20
|
+
require 'apnd/version'
|
21
|
+
|
22
|
+
ver = APND::Version
|
23
|
+
|
24
|
+
sh "gem build apnd.gemspec"
|
25
|
+
sh "gem push mwotton-apnd-#{ver}.gem"
|
26
|
+
sh "git tag -a -m 'APND v#{ver}' v#{ver}"
|
27
|
+
sh "git push origin v#{ver}"
|
28
|
+
sh "git push origin master"
|
29
|
+
sh "git clean -fd"
|
30
|
+
sh "rake pages"
|
31
|
+
end
|
data/bin/apnd
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
ARGV << '--help' if ARGV.empty?
|
4
|
+
|
5
|
+
if $0 == __FILE__
|
6
|
+
require 'rubygems'
|
7
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'apnd'
|
11
|
+
|
12
|
+
command = ARGV.shift
|
13
|
+
|
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}"
|
21
|
+
else
|
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
|
+
|
34
|
+
HELP
|
35
|
+
end
|
data/lib/apnd.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module APND
|
4
|
+
autoload :Version, 'apnd/version'
|
5
|
+
autoload :CLI, 'apnd/cli'
|
6
|
+
autoload :Errors, 'apnd/errors'
|
7
|
+
autoload :Settings, 'apnd/settings'
|
8
|
+
autoload :Daemon, 'apnd/daemon'
|
9
|
+
autoload :Notification, 'apnd/notification'
|
10
|
+
autoload :Feedback, 'apnd/feedback'
|
11
|
+
|
12
|
+
#
|
13
|
+
# Returns APND::Settings
|
14
|
+
#
|
15
|
+
def self.settings
|
16
|
+
@@settings ||= Settings.new
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Yields APND::Settings
|
21
|
+
#
|
22
|
+
def self.configure
|
23
|
+
yield settings
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Write message to stdout with date
|
28
|
+
#
|
29
|
+
def self.logger(message) #:nodoc:
|
30
|
+
puts "[%s] %s" % [Time.now.strftime("%Y-%m-%d %H:%M:%S"), message]
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,193 @@
|
|
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
|
+
#
|
11
|
+
# The host notifications will be written to, usually one
|
12
|
+
# running APND
|
13
|
+
#
|
14
|
+
attr_accessor :upstream_host
|
15
|
+
|
16
|
+
#
|
17
|
+
# The port to connect to upstream_host on
|
18
|
+
#
|
19
|
+
attr_accessor :upstream_port
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Set upstream host/port to default values
|
24
|
+
#
|
25
|
+
self.upstream_host = APND.settings.notification.host
|
26
|
+
self.upstream_port = APND.settings.notification.port.to_i
|
27
|
+
|
28
|
+
#
|
29
|
+
# The device token from Apple
|
30
|
+
#
|
31
|
+
attr_accessor :token
|
32
|
+
|
33
|
+
#
|
34
|
+
# The alert to send
|
35
|
+
#
|
36
|
+
attr_accessor :alert
|
37
|
+
|
38
|
+
#
|
39
|
+
# The badge number to set
|
40
|
+
#
|
41
|
+
attr_accessor :badge
|
42
|
+
|
43
|
+
#
|
44
|
+
# The sound to play
|
45
|
+
#
|
46
|
+
attr_accessor :sound
|
47
|
+
|
48
|
+
#
|
49
|
+
# Custom data to send
|
50
|
+
#
|
51
|
+
attr_accessor :custom
|
52
|
+
|
53
|
+
#
|
54
|
+
# Creates a new socket to upstream_host:upstream_port
|
55
|
+
#
|
56
|
+
def self.upstream_socket
|
57
|
+
@socket = TCPSocket.new(upstream_host, upstream_port)
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Opens a new socket upstream, yields it, and closes it
|
62
|
+
#
|
63
|
+
def self.open_upstream_socket(&block)
|
64
|
+
socket = upstream_socket
|
65
|
+
yield socket
|
66
|
+
socket.close
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Create a new APN
|
71
|
+
#
|
72
|
+
def self.create(params = {}, push = true)
|
73
|
+
notification = Notification.new(params)
|
74
|
+
notification.push! if push
|
75
|
+
notification
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Try to create a new Notification from raw data
|
80
|
+
# Used by Daemon::Protocol to validate incoming data
|
81
|
+
#
|
82
|
+
def self.valid?(data)
|
83
|
+
parse(data)
|
84
|
+
rescue => e
|
85
|
+
puts e.inspect
|
86
|
+
puts e.backtrace
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def self.parse_from_handle(handle)
|
92
|
+
|
93
|
+
end
|
94
|
+
#
|
95
|
+
# Parse raw data into a new Notification
|
96
|
+
#
|
97
|
+
def self.parse(data)
|
98
|
+
buffer = data.dup
|
99
|
+
notification = Notification.new
|
100
|
+
|
101
|
+
header = buffer.slice!(0, 3).unpack('ccc')
|
102
|
+
|
103
|
+
if header[0] != 0
|
104
|
+
raise APND::Errors::InvalidNotificationHeader.new(header)
|
105
|
+
end
|
106
|
+
|
107
|
+
notification.token = buffer.slice!(0, 32).unpack('H*').first
|
108
|
+
|
109
|
+
json_length = buffer.slice!(0, 2).unpack('CC')
|
110
|
+
|
111
|
+
json = buffer.slice!(0, json_length.last)
|
112
|
+
|
113
|
+
payload = JSON.parse(json)
|
114
|
+
|
115
|
+
%w[alert sound badge].each do |key|
|
116
|
+
if payload['aps'] && payload['aps'][key]
|
117
|
+
notification.send("#{key}=", payload['aps'][key])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
payload.delete('aps')
|
122
|
+
|
123
|
+
unless payload.empty?
|
124
|
+
notification.custom = payload
|
125
|
+
end
|
126
|
+
|
127
|
+
notification
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Create a new Notification object from a hash
|
132
|
+
#
|
133
|
+
def initialize(params = {})
|
134
|
+
@token = params[:token]
|
135
|
+
@alert = params[:alert]
|
136
|
+
@badge = params[:badge]
|
137
|
+
@sound = params[:sound]
|
138
|
+
@custom = params[:custom]
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Token in hex format
|
143
|
+
#
|
144
|
+
def hex_token
|
145
|
+
[self.token.delete(' ')].pack('H*')
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# aps hash sent to Apple
|
150
|
+
#
|
151
|
+
def aps
|
152
|
+
aps = {}
|
153
|
+
aps['alert'] = self.alert if self.alert
|
154
|
+
aps['badge'] = self.badge.to_i if self.badge
|
155
|
+
aps['sound'] = self.sound if self.sound
|
156
|
+
|
157
|
+
output = { 'aps' => aps }
|
158
|
+
|
159
|
+
if self.custom
|
160
|
+
self.custom.each do |key, value|
|
161
|
+
output[key.to_s] = value
|
162
|
+
end
|
163
|
+
end
|
164
|
+
output
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Pushes notification to upstream host:port (default is localhost:22195)
|
169
|
+
#
|
170
|
+
def push!
|
171
|
+
self.class.open_upstream_socket { |sock| sock.write(to_bytes) }
|
172
|
+
end
|
173
|
+
|
174
|
+
#
|
175
|
+
# Returns the Notification's aps hash as json
|
176
|
+
#
|
177
|
+
def aps_json
|
178
|
+
return @aps_json if @aps_json
|
179
|
+
json = aps.to_json
|
180
|
+
raise APND::Errors::InvalidPayload.new(json) if json.size > 256
|
181
|
+
@aps_json = json
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Format the notification as a string for submission
|
186
|
+
# to Apple
|
187
|
+
#
|
188
|
+
def to_bytes
|
189
|
+
@bytes ||= "\0\0 %s\0%s%s" % [hex_token, aps_json.length.chr, aps_json]
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
end
|