apnserver 0.1.10 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|