apnserver 0.1.10 → 0.2.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.textile +31 -22
- data/bin/apnsend +10 -6
- data/bin/apnserverd +24 -6
- data/bin/apnserverd.fedora.init +0 -0
- data/bin/apnserverd.ubuntu.init +116 -0
- data/lib/apnserver.rb +0 -4
- data/lib/apnserver/client.rb +9 -10
- data/lib/apnserver/notification.rb +27 -28
- data/lib/apnserver/payload.rb +3 -7
- data/lib/apnserver/protocol.rb +6 -8
- data/lib/apnserver/server.rb +10 -11
- data/spec/models/client_spec.rb +21 -0
- data/spec/models/notification_spec.rb +85 -0
- data/spec/models/payload_spec.rb +57 -0
- data/spec/models/protocol_spec.rb +19 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/test_server.rb +10 -0
- metadata +74 -42
- data/Rakefile +0 -41
- data/VERSION +0 -1
- data/test/test_client.rb +0 -11
- data/test/test_helper.rb +0 -4
- data/test/test_notification.rb +0 -79
- data/test/test_payload.rb +0 -59
- data/test/test_protocol.rb +0 -29
data/README.textile
CHANGED
@@ -5,48 +5,54 @@ h1. Apple Push Notification Server Toolkit
|
|
5
5
|
h2. Description
|
6
6
|
|
7
7
|
apnserver is a server and set of command line programs to send push notifications to the iPhone.
|
8
|
-
Apple
|
9
|
-
opening up and tearing down SSL connections
|
10
|
-
network server is introduced that queues are requests to the APN service and sends them via a
|
8
|
+
Apple recommends to maintain an open connection to the push notification service and refrain from
|
9
|
+
opening up and tearing down SSL connections repeated. To solve this problem an intermediate
|
10
|
+
network server is introduced that queues are requests to the APN service and sends them via a
|
11
11
|
persistent connection.
|
12
12
|
|
13
13
|
h2. Remaining Tasks
|
14
|
-
|
14
|
+
|
15
15
|
* Implement feedback service mechanism
|
16
16
|
* Implement robust notification sending in reactor periodic scheduler
|
17
|
-
|
17
|
+
|
18
18
|
h2. Issues Fixed
|
19
19
|
|
20
20
|
* second attempt at retry logic, SSL Errors close down sockets now
|
21
21
|
* apnsend --badge option correctly sends integer number rather than string of number for aps json payload
|
22
22
|
* connections are properly closed in Notification#push method now
|
23
|
-
*
|
23
|
+
* removed json gem in favor of ActiveSupport
|
24
|
+
* Rails 3.x support
|
25
|
+
* drop the erroneous puts statements in favor a configurable logger
|
26
|
+
* moved to Rspec
|
24
27
|
|
25
28
|
h2. APN Server Daemon
|
26
|
-
|
29
|
+
|
27
30
|
<pre>
|
28
31
|
<code>
|
29
32
|
Usage: apnserverd [options] --pem /path/to/pem
|
30
33
|
--bind-address bind address (defaults to 0.0.0.0)
|
31
34
|
bind address of the server daemon
|
32
|
-
|
33
|
-
--proxy-port port
|
35
|
+
|
36
|
+
--proxy-port port
|
34
37
|
the port that the daemon will listen on (defaults to 22195)
|
35
|
-
|
38
|
+
|
36
39
|
--server server
|
37
40
|
APN Server (defaults to gateway.push.apple.com)
|
38
|
-
|
41
|
+
|
39
42
|
--port port of the APN Server
|
40
43
|
APN server port (defaults to 2195)
|
41
|
-
|
44
|
+
|
42
45
|
--pem pem file path
|
43
46
|
The PEM encoded private key and certificate.
|
44
|
-
To export a PEM ecoded file execute
|
47
|
+
To export a PEM ecoded file execute
|
45
48
|
# openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
|
46
|
-
|
49
|
+
|
50
|
+
--pem-passphrase passphrase
|
51
|
+
The PEM passphrase to decode key.
|
52
|
+
|
47
53
|
--help
|
48
|
-
usage message
|
49
|
-
|
54
|
+
usage message
|
55
|
+
|
50
56
|
--daemon
|
51
57
|
Runs process as daemon, not available on Windows
|
52
58
|
</code>
|
@@ -54,7 +60,7 @@ Usage: apnserverd [options] --pem /path/to/pem
|
|
54
60
|
|
55
61
|
h2. APN Server Client
|
56
62
|
|
57
|
-
With the APN server client script you can send push notifications directly to
|
63
|
+
With the APN server client script you can send push notifications directly to
|
58
64
|
Apple's APN server over an SSL connection or to the above daemon using a plain socket.
|
59
65
|
To send a notification to Apple's APN server using SSL the *--pem* option must be used.
|
60
66
|
|
@@ -63,8 +69,9 @@ To send a notification to Apple's APN server using SSL the *--pem* option must b
|
|
63
69
|
Usage: apnsend [switches] (--b64-token | --hex-token) <token>
|
64
70
|
--server <localhost> the apn server defaults to a locally running apnserverd
|
65
71
|
--port <2195> the port of the apn server
|
66
|
-
--pem <path> the path to the pem file, if a pem is supplied the server
|
72
|
+
--pem <path> the path to the pem file, if a pem is supplied the server
|
67
73
|
defaults to gateway.push.apple.com:2195
|
74
|
+
--pem-passphrase <passphrase> the pem passphrase
|
68
75
|
--alert <message> the message to send"
|
69
76
|
--sound <default> the sound to play, defaults to 'default'
|
70
77
|
--badge <number> the badge number
|
@@ -94,6 +101,7 @@ To configure the client to send to the local apnserverd process configure the Ap
|
|
94
101
|
# configured for a using the apnserverd proxy
|
95
102
|
ApnServer::Config.host = 'localhost'
|
96
103
|
ApnServer::Config.port = 22195
|
104
|
+
ApnServer::Config.logger = Rails.logger
|
97
105
|
</code>
|
98
106
|
</pre>
|
99
107
|
|
@@ -123,11 +131,11 @@ Finally within we can send a push notification using the following code
|
|
123
131
|
|
124
132
|
h2. Installation
|
125
133
|
|
126
|
-
|
134
|
+
Apnserver is hosted on "rubygems":https://rubygems.org/gems/apnserver
|
127
135
|
|
128
136
|
<pre>
|
129
137
|
<code>
|
130
|
-
$ gem install
|
138
|
+
$ gem install apnserver
|
131
139
|
</code>
|
132
140
|
</pre>
|
133
141
|
|
@@ -135,15 +143,16 @@ Adding apnserver to your Rails application
|
|
135
143
|
|
136
144
|
<pre>
|
137
145
|
<code>
|
138
|
-
|
146
|
+
gem 'apnserver', '>= 0.2.0
|
139
147
|
</code>
|
140
148
|
</pre>
|
141
149
|
|
150
|
+
|
142
151
|
h2. License
|
143
152
|
|
144
153
|
(The MIT License)
|
145
154
|
|
146
|
-
Copyright (c)
|
155
|
+
Copyright (c) 2011 Ben Poweski
|
147
156
|
|
148
157
|
Permission is hereby granted, free of charge, to any person obtaining
|
149
158
|
a copy of this software and associated documentation files (the
|
data/bin/apnsend
CHANGED
@@ -13,12 +13,13 @@ def usage
|
|
13
13
|
puts " --server <localhost> the apn server defaults to a locally running apnserverd"
|
14
14
|
puts " --port <2195> the port of the apn server"
|
15
15
|
puts " --pem <path> the path to the pem file, if a pem is supplied the server defaults to gateway.push.apple.com:2195"
|
16
|
+
puts " --pem-passphrase <passphrase> the pem passphrase"
|
16
17
|
puts " --alert <message> the message to send"
|
17
18
|
puts " --sound <default> the sound to play, defaults to 'default'"
|
18
19
|
puts " --badge <number> the badge number"
|
19
|
-
puts " --custom <json string> a custom json string to be added to the main object"
|
20
|
+
puts " --custom <json string> a custom json string to be added to the main object"
|
20
21
|
puts " --b64-token <token> a base 64 encoded device token"
|
21
|
-
puts " --hex-token <token> a hex encoded device token"
|
22
|
+
puts " --hex-token <token> a hex encoded device token"
|
22
23
|
puts " --help this message"
|
23
24
|
end
|
24
25
|
|
@@ -26,10 +27,11 @@ opts = GetoptLong.new(
|
|
26
27
|
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
|
27
28
|
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
|
28
29
|
["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
|
30
|
+
["--pem-passphrase", "-C", GetoptLong::REQUIRED_ARGUMENT],
|
29
31
|
["--alert", "-a", GetoptLong::REQUIRED_ARGUMENT],
|
30
32
|
["--sound", "-S", GetoptLong::REQUIRED_ARGUMENT],
|
31
33
|
["--badge", "-b", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
-
["--custom", "-j", GetoptLong::REQUIRED_ARGUMENT],
|
34
|
+
["--custom", "-j", GetoptLong::REQUIRED_ARGUMENT],
|
33
35
|
["--b64-token", "-B", GetoptLong::REQUIRED_ARGUMENT],
|
34
36
|
["--hex-token", "-H", GetoptLong::REQUIRED_ARGUMENT],
|
35
37
|
["--help", "-h", GetoptLong::NO_ARGUMENT]
|
@@ -48,6 +50,8 @@ opts.each do |opt, arg|
|
|
48
50
|
ApnServer::Config.port = arg.to_i
|
49
51
|
when '--pem'
|
50
52
|
ApnServer::Config.pem = arg
|
53
|
+
when '--pem-passphrase'
|
54
|
+
ApnServer::Config.password = arg
|
51
55
|
when '--alert'
|
52
56
|
notification.alert = arg
|
53
57
|
when '--sound'
|
@@ -55,16 +59,16 @@ opts.each do |opt, arg|
|
|
55
59
|
when '--badge'
|
56
60
|
notification.badge = arg.to_i
|
57
61
|
when '--custom'
|
58
|
-
notification.custom = arg
|
62
|
+
notification.custom = ActiveSupport::JSON.decode(arg)
|
59
63
|
when '--b64-token'
|
60
64
|
notification.device_token = Base64::decode64(arg)
|
61
65
|
when '--hex-token'
|
62
|
-
notification.device_token = arg.
|
66
|
+
notification.device_token = arg.scan(/[0-9a-f][0-9a-f]/).map {|s| s.hex.chr}.join
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
66
70
|
if notification.device_token.nil?
|
67
|
-
usage
|
71
|
+
usage
|
68
72
|
exit
|
69
73
|
else
|
70
74
|
notification.push
|
data/bin/apnserverd
CHANGED
@@ -5,24 +5,28 @@ require 'getoptlong'
|
|
5
5
|
require 'rubygems'
|
6
6
|
require 'daemons'
|
7
7
|
require 'apnserver'
|
8
|
+
require 'apnserver/server'
|
8
9
|
|
9
10
|
def usage
|
10
11
|
puts "Usage: apnserverd [switches] --pem <path>"
|
12
|
+
puts " --pem-passphrase <passphrase> pem passphrase"
|
11
13
|
puts " --bind-address [0.0.0.0] bind address of proxy"
|
12
14
|
puts " --proxy-port [22195] port proxy listens on"
|
13
15
|
puts " --server <gateway.push.apple.com> the apn server to send messages to"
|
14
16
|
puts " --port <2195> the port of the apn server"
|
17
|
+
puts " --pid </var/run/apnserverd.pid the path to store the pid"
|
18
|
+
puts " --log </var/log/apnserverd.log the path to store the log"
|
19
|
+
puts " --daemon to daemonize the server"
|
15
20
|
puts " --help this message"
|
16
21
|
end
|
17
22
|
|
18
23
|
def daemonize
|
19
|
-
Daemonize.daemonize(
|
20
|
-
@pid_file
|
21
|
-
open(@pid_file,"w") {|f| f.write(Process.pid) } # copied from mongrel
|
24
|
+
Daemonize.daemonize(@log_file, 'apnserverd')
|
25
|
+
open(@pid_file,"w") { |f| f.write(Process.pid) }
|
22
26
|
open(@pid_file,"w") do |f|
|
23
27
|
f.write(Process.pid)
|
24
28
|
File.chmod(0644, @pid_file)
|
25
|
-
end
|
29
|
+
end
|
26
30
|
end
|
27
31
|
|
28
32
|
opts = GetoptLong.new(
|
@@ -30,7 +34,10 @@ opts = GetoptLong.new(
|
|
30
34
|
["--proxy-port", "-P", GetoptLong::REQUIRED_ARGUMENT],
|
31
35
|
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
|
32
36
|
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
|
37
|
+
["--pid", "-i", GetoptLong::REQUIRED_ARGUMENT],
|
38
|
+
["--log", "-l", GetoptLong::REQUIRED_ARGUMENT],
|
33
39
|
["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
|
40
|
+
["--pem-passphrase", "-C", GetoptLong::REQUIRED_ARGUMENT],
|
34
41
|
["--help", "-h", GetoptLong::NO_ARGUMENT],
|
35
42
|
["--daemon", "-d", GetoptLong::NO_ARGUMENT]
|
36
43
|
)
|
@@ -40,22 +47,32 @@ proxy_port = 22195
|
|
40
47
|
host = 'gateway.push.apple.com'
|
41
48
|
port = 2195
|
42
49
|
pem = nil
|
50
|
+
pem_passphrase = nil
|
51
|
+
@pid_file = '/var/run/apnserverd.pid'
|
52
|
+
@log_file = '/var/log/apnserverd.log'
|
43
53
|
daemon = false
|
44
54
|
|
45
55
|
opts.each do |opt, arg|
|
46
56
|
case opt
|
47
57
|
when '--help'
|
48
58
|
usage
|
59
|
+
exit 1
|
49
60
|
when '--bind-address'
|
50
61
|
bind_address = arg
|
51
62
|
when '--proxy-port'
|
52
63
|
proxy_port = arg.to_i
|
53
64
|
when '--server'
|
54
|
-
|
65
|
+
host = arg
|
55
66
|
when '--port'
|
56
|
-
|
67
|
+
port = arg.to_i
|
68
|
+
when '--pid'
|
69
|
+
@pid_file = arg
|
70
|
+
when '--log'
|
71
|
+
@log_file = arg
|
57
72
|
when '--pem'
|
58
73
|
pem = arg
|
74
|
+
when '--pem-passphrase'
|
75
|
+
pem_passphrase = arg
|
59
76
|
when '--daemon'
|
60
77
|
daemon = true
|
61
78
|
end
|
@@ -69,5 +86,6 @@ else
|
|
69
86
|
server = ApnServer::Server.new(pem, bind_address, proxy_port)
|
70
87
|
server.client.host = host
|
71
88
|
server.client.port = port
|
89
|
+
server.client.password = pem_passphrase
|
72
90
|
server.start!
|
73
91
|
end
|
data/bin/apnserverd.fedora.init
CHANGED
File without changes
|
@@ -0,0 +1,116 @@
|
|
1
|
+
#! /bin/sh
|
2
|
+
### BEGIN INIT INFO
|
3
|
+
# Provides: apnserverd
|
4
|
+
# Required-Start: $remote_fs
|
5
|
+
# Required-Stop: $remote_fs
|
6
|
+
# Default-Start: 2 3 4 5
|
7
|
+
# Default-Stop: 0 1 6
|
8
|
+
# Short-Description: Apple Push Notification Server Daemon
|
9
|
+
### END INIT INFO
|
10
|
+
|
11
|
+
# Author: Philipp Schmid <philipp.schmid@openresearch.com>
|
12
|
+
|
13
|
+
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
14
|
+
DESC="Apple Push Notification Server Daemon"
|
15
|
+
NAME=apnserverd
|
16
|
+
DAEMON=/usr/bin/$NAME
|
17
|
+
PEMPATH=""
|
18
|
+
DAEMON_ARGS="--daemon --pem $PEMPATH"
|
19
|
+
PIDFILE=/var/run/$NAME.pid
|
20
|
+
SCRIPTNAME=/etc/init.d/$NAME
|
21
|
+
|
22
|
+
# Exit if the package is not installed
|
23
|
+
[ -x "$DAEMON" ] || exit 0
|
24
|
+
|
25
|
+
# Load the VERBOSE setting and other rcS variables
|
26
|
+
. /lib/init/vars.sh
|
27
|
+
|
28
|
+
# Define LSB log_* functions.
|
29
|
+
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
|
30
|
+
. /lib/lsb/init-functions
|
31
|
+
|
32
|
+
#
|
33
|
+
# Function that starts the daemon/service
|
34
|
+
#
|
35
|
+
do_start()
|
36
|
+
{
|
37
|
+
# Return
|
38
|
+
# 0 if daemon has been started
|
39
|
+
# 1 if daemon was already running
|
40
|
+
# 2 if daemon could not be started
|
41
|
+
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|
42
|
+
|| return 1
|
43
|
+
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
|
44
|
+
$DAEMON_ARGS \
|
45
|
+
|| return 2
|
46
|
+
}
|
47
|
+
|
48
|
+
#
|
49
|
+
# Function that stops the daemon/service
|
50
|
+
#
|
51
|
+
do_stop()
|
52
|
+
{
|
53
|
+
# Return
|
54
|
+
# 0 if daemon has been stopped
|
55
|
+
# 1 if daemon was already stopped
|
56
|
+
# 2 if daemon could not be stopped
|
57
|
+
# other if a failure occurred
|
58
|
+
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
|
59
|
+
RETVAL="$?"
|
60
|
+
[ "$RETVAL" = 2 ] && return 2
|
61
|
+
# Wait for children to finish too if this is a daemon that forks
|
62
|
+
# and if the daemon is only ever run from this initscript.
|
63
|
+
# If the above conditions are not satisfied then add some other code
|
64
|
+
# that waits for the process to drop all resources that could be
|
65
|
+
# needed by services started subsequently. A last resort is to
|
66
|
+
# sleep for some time.
|
67
|
+
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
|
68
|
+
[ "$?" = 2 ] && return 2
|
69
|
+
# Many daemons don't delete their pidfiles when they exit.
|
70
|
+
rm -f $PIDFILE
|
71
|
+
return "$RETVAL"
|
72
|
+
}
|
73
|
+
|
74
|
+
|
75
|
+
case "$1" in
|
76
|
+
start)
|
77
|
+
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
|
78
|
+
do_start
|
79
|
+
case "$?" in
|
80
|
+
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
81
|
+
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
82
|
+
esac
|
83
|
+
;;
|
84
|
+
stop)
|
85
|
+
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
|
86
|
+
do_stop
|
87
|
+
case "$?" in
|
88
|
+
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
89
|
+
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
90
|
+
esac
|
91
|
+
;;
|
92
|
+
restart|force-reload)
|
93
|
+
log_daemon_msg "Restarting $DESC" "$NAME"
|
94
|
+
do_stop
|
95
|
+
case "$?" in
|
96
|
+
0|1)
|
97
|
+
do_start
|
98
|
+
case "$?" in
|
99
|
+
0) log_end_msg 0 ;;
|
100
|
+
1) log_end_msg 1 ;; # Old process is still running
|
101
|
+
*) log_end_msg 1 ;; # Failed to start
|
102
|
+
esac
|
103
|
+
;;
|
104
|
+
*)
|
105
|
+
# Failed to stop
|
106
|
+
log_end_msg 1
|
107
|
+
;;
|
108
|
+
esac
|
109
|
+
;;
|
110
|
+
*)
|
111
|
+
echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
|
112
|
+
exit 3
|
113
|
+
;;
|
114
|
+
esac
|
115
|
+
|
116
|
+
:
|
data/lib/apnserver.rb
CHANGED
data/lib/apnserver/client.rb
CHANGED
@@ -3,40 +3,39 @@ require 'socket'
|
|
3
3
|
|
4
4
|
module ApnServer
|
5
5
|
class Client
|
6
|
-
|
7
6
|
attr_accessor :pem, :host, :port, :password
|
8
|
-
|
7
|
+
|
9
8
|
def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil)
|
10
9
|
@pem, @host, @port, @password = pem, host, port, pass
|
11
10
|
end
|
12
|
-
|
11
|
+
|
13
12
|
def connect!
|
14
13
|
raise "The path to your pem file is not set." unless self.pem
|
15
14
|
raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
|
16
|
-
|
15
|
+
|
17
16
|
@context = OpenSSL::SSL::SSLContext.new
|
18
17
|
@context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
|
19
18
|
@context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.password)
|
20
|
-
|
19
|
+
|
21
20
|
@sock = TCPSocket.new(self.host, self.port.to_i)
|
22
21
|
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context)
|
23
22
|
@ssl.connect
|
24
|
-
|
23
|
+
|
25
24
|
return @sock, @ssl
|
26
25
|
end
|
27
|
-
|
26
|
+
|
28
27
|
def disconnect!
|
29
28
|
@ssl.close
|
30
29
|
@sock.close
|
31
30
|
@ssl = nil
|
32
31
|
@sock = nil
|
33
32
|
end
|
34
|
-
|
33
|
+
|
35
34
|
def write(notification)
|
36
|
-
|
35
|
+
Config.logger.debug "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}"
|
37
36
|
@ssl.write(notification.to_bytes)
|
38
37
|
end
|
39
|
-
|
38
|
+
|
40
39
|
def connected?
|
41
40
|
@ssl
|
42
41
|
end
|