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
@@ -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
|
@@ -0,0 +1,189 @@
|
|
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
|
+
# Parse raw data into a new Notification
|
92
|
+
#
|
93
|
+
def self.parse(data)
|
94
|
+
buffer = data.dup
|
95
|
+
notification = Notification.new
|
96
|
+
|
97
|
+
header = buffer.slice!(0, 3).unpack('ccc')
|
98
|
+
|
99
|
+
if header[0] != 0
|
100
|
+
raise APND::Errors::InvalidNotificationHeader.new(header)
|
101
|
+
end
|
102
|
+
|
103
|
+
notification.token = buffer.slice!(0, 32).unpack('H*').first
|
104
|
+
|
105
|
+
json_length = buffer.slice!(0, 2).unpack('CC')
|
106
|
+
|
107
|
+
json = buffer.slice!(0, json_length.last)
|
108
|
+
|
109
|
+
payload = JSON.parse(json)
|
110
|
+
|
111
|
+
%w[alert sound badge].each do |key|
|
112
|
+
if payload['aps'] && payload['aps'][key]
|
113
|
+
notification.send("#{key}=", payload['aps'][key])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
payload.delete('aps')
|
118
|
+
|
119
|
+
unless payload.empty?
|
120
|
+
notification.custom = payload
|
121
|
+
end
|
122
|
+
|
123
|
+
notification
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# Create a new Notification object from a hash
|
128
|
+
#
|
129
|
+
def initialize(params = {})
|
130
|
+
@token = params[:token]
|
131
|
+
@alert = params[:alert]
|
132
|
+
@badge = params[:badge]
|
133
|
+
@sound = params[:sound]
|
134
|
+
@custom = params[:custom]
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# Token in hex format
|
139
|
+
#
|
140
|
+
def hex_token
|
141
|
+
[self.token.delete(' ')].pack('H*')
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# aps hash sent to Apple
|
146
|
+
#
|
147
|
+
def aps
|
148
|
+
aps = {}
|
149
|
+
aps['alert'] = self.alert if self.alert
|
150
|
+
aps['badge'] = self.badge.to_i if self.badge
|
151
|
+
aps['sound'] = self.sound if self.sound
|
152
|
+
|
153
|
+
output = { 'aps' => aps }
|
154
|
+
|
155
|
+
if self.custom
|
156
|
+
self.custom.each do |key, value|
|
157
|
+
output[key.to_s] = value
|
158
|
+
end
|
159
|
+
end
|
160
|
+
output
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
# Pushes notification to upstream host:port (default is localhost:22195)
|
165
|
+
#
|
166
|
+
def push!
|
167
|
+
self.class.open_upstream_socket { |sock| sock.write(to_bytes) }
|
168
|
+
end
|
169
|
+
|
170
|
+
#
|
171
|
+
# Returns the Notification's aps hash as json
|
172
|
+
#
|
173
|
+
def aps_json
|
174
|
+
return @aps_json if @aps_json
|
175
|
+
json = aps.to_json
|
176
|
+
raise APND::Errors::InvalidPayload.new(json) if json.size > 256
|
177
|
+
@aps_json = json
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Format the notification as a string for submission
|
182
|
+
# to Apple
|
183
|
+
#
|
184
|
+
def to_bytes
|
185
|
+
@bytes ||= "\0\0 %s\0%s%s" % [hex_token, aps_json.length.chr, aps_json]
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,205 @@
|
|
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
|
+
# 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
|
+
|
133
|
+
#
|
134
|
+
# Returns the AppleConnection settings
|
135
|
+
#
|
136
|
+
def apple
|
137
|
+
@apple ||= APND::Settings::AppleConnection.new
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Mass assign AppleConnection settings
|
142
|
+
#
|
143
|
+
def apple=(options = {})
|
144
|
+
if options.respond_to?(:keys)
|
145
|
+
apple.cert = options[:cert] if options[:cert]
|
146
|
+
apple.cert_pass = options[:cert_pass] if options[:cert_pass]
|
147
|
+
apple.host = options[:host] if options[:host]
|
148
|
+
apple.port = options[:port] if options[:port]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
#
|
153
|
+
# Returns the Daemon settings
|
154
|
+
#
|
155
|
+
def daemon
|
156
|
+
@daemon ||= APND::Settings::Daemon.new
|
157
|
+
end
|
158
|
+
|
159
|
+
#
|
160
|
+
# Mass assign Daemon settings
|
161
|
+
#
|
162
|
+
def daemon=(options = {})
|
163
|
+
if options.respond_to?(:keys)
|
164
|
+
daemon.bind = options[:bind] if options[:bind]
|
165
|
+
daemon.port = options[:port] if options[:port]
|
166
|
+
daemon.log_file = options[:log_file] if options[:log_file]
|
167
|
+
daemon.timer = options[:timer] if options[:timer]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# Returns the Notification settings
|
173
|
+
#
|
174
|
+
def notification
|
175
|
+
@notification ||= APND::Settings::Notification.new
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Mass assign Notification settings
|
180
|
+
#
|
181
|
+
def notification=(options = {})
|
182
|
+
if options.respond_to?(:keys)
|
183
|
+
notification.port = options[:port] if options[:port]
|
184
|
+
notification.host = options[:host] if options[:host]
|
185
|
+
end
|
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
|
204
|
+
end
|
205
|
+
end
|