apnserver 0.1.9
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 +166 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/bin/apnsend +71 -0
- data/bin/apnserverd +73 -0
- data/bin/apnserverd.fedora.init +71 -0
- data/lib/apnserver.rb +8 -0
- data/lib/apnserver/client.rb +44 -0
- data/lib/apnserver/notification.rb +92 -0
- data/lib/apnserver/payload.rb +23 -0
- data/lib/apnserver/protocol.rb +24 -0
- data/lib/apnserver/server.rb +45 -0
- data/lib/apnserver/server_connection.rb +9 -0
- data/test/test_client.rb +11 -0
- data/test/test_helper.rb +4 -0
- data/test/test_notification.rb +79 -0
- data/test/test_payload.rb +59 -0
- data/test/test_protocol.rb +29 -0
- metadata +105 -0
data/README.textile
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
h1. Apple Push Notification Server Toolkit
|
2
|
+
|
3
|
+
* http://github.com/bpoweski/apnserver
|
4
|
+
|
5
|
+
h2. Description
|
6
|
+
|
7
|
+
apnserver is a server and set of command line programs to send push notifications to the iPhone.
|
8
|
+
Apple recomends to maintain an open connection to the push notification service and refrain from
|
9
|
+
opening up and tearing down SSL connections reapeated. 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
|
+
persistent connection.
|
12
|
+
|
13
|
+
h2. Remaining Tasks
|
14
|
+
|
15
|
+
* Implement feedback service mechanism
|
16
|
+
* Implement robust notification sending in reactor periodic scheduler
|
17
|
+
|
18
|
+
h2. Issues Fixed
|
19
|
+
|
20
|
+
* second attempt at retry logic, SSL Errors close down sockets now
|
21
|
+
* apnsend --badge option correctly sends integer number rather than string of number for aps json payload
|
22
|
+
* connections are properly closed in Notification#push method now
|
23
|
+
* better compatibility with Rails
|
24
|
+
|
25
|
+
h2. APN Server Daemon
|
26
|
+
|
27
|
+
<pre>
|
28
|
+
<code>
|
29
|
+
Usage: apnserverd [options] --pem /path/to/pem
|
30
|
+
--bind-address bind address (defaults to 0.0.0.0)
|
31
|
+
bind address of the server daemon
|
32
|
+
|
33
|
+
--proxy-port port
|
34
|
+
the port that the daemon will listen on (defaults to 22195)
|
35
|
+
|
36
|
+
--server server
|
37
|
+
APN Server (defaults to gateway.push.apple.com)
|
38
|
+
|
39
|
+
--port port of the APN Server
|
40
|
+
APN server port (defaults to 2195)
|
41
|
+
|
42
|
+
--pem pem file path
|
43
|
+
The PEM encoded private key and certificate.
|
44
|
+
To export a PEM ecoded file execute
|
45
|
+
# openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
|
46
|
+
|
47
|
+
--help
|
48
|
+
usage message
|
49
|
+
|
50
|
+
--daemon
|
51
|
+
Runs process as daemon, not available on Windows
|
52
|
+
</code>
|
53
|
+
</pre>
|
54
|
+
|
55
|
+
h2. APN Server Client
|
56
|
+
|
57
|
+
With the APN server client script you can send push notifications directly to
|
58
|
+
Apple's APN server over an SSL connection or to the above daemon using a plain socket.
|
59
|
+
To send a notification to Apple's APN server using SSL the *--pem* option must be used.
|
60
|
+
|
61
|
+
<pre>
|
62
|
+
<code>
|
63
|
+
Usage: apnsend [switches] (--b64-token | --hex-token) <token>
|
64
|
+
--server <localhost> the apn server defaults to a locally running apnserverd
|
65
|
+
--port <2195> the port of the apn server
|
66
|
+
--pem <path> the path to the pem file, if a pem is supplied the server
|
67
|
+
defaults to gateway.push.apple.com:2195
|
68
|
+
--alert <message> the message to send"
|
69
|
+
--sound <default> the sound to play, defaults to 'default'
|
70
|
+
--badge <number> the badge number
|
71
|
+
--custom <json string> a custom json string to be added to the main object
|
72
|
+
--b64-token <token> a base 64 encoded device token
|
73
|
+
--hex-token <token> a hex encoded device token
|
74
|
+
--help this message
|
75
|
+
</code>
|
76
|
+
</pre>
|
77
|
+
|
78
|
+
To send a base64 encoded push notification via the command line execute the following:
|
79
|
+
|
80
|
+
<pre>
|
81
|
+
<code>
|
82
|
+
$ apnsend --server gateway.push.apple.com --port 2195 --pem key.pem \
|
83
|
+
--b64-token j92f12jh8lqcAwcOVeSIrsBxibaJ0xyCi8/AkmzNlk8= --sound default \
|
84
|
+
--alert Hello
|
85
|
+
</code>
|
86
|
+
</pre>
|
87
|
+
|
88
|
+
h2. Sending Notifications from Ruby
|
89
|
+
|
90
|
+
To configure the client to send to the local apnserverd process configure the ApnServer client with the following.
|
91
|
+
|
92
|
+
<pre>
|
93
|
+
<code>
|
94
|
+
# configured for a using the apnserverd proxy
|
95
|
+
ApnServer::Config.host = 'localhost'
|
96
|
+
ApnServer::Config.port = 22195
|
97
|
+
</code>
|
98
|
+
</pre>
|
99
|
+
|
100
|
+
To configure the client to send directly to Apple's push notification server, bypassing the apnserverd process configure the following.
|
101
|
+
|
102
|
+
<pre>
|
103
|
+
<code>
|
104
|
+
ApnServer::Config.pem = '/path/to/pem'
|
105
|
+
ApnServer::Config.host = 'gateway.push.apple.com'
|
106
|
+
ApnServer::Config.port = 2195
|
107
|
+
</code>
|
108
|
+
</pre>
|
109
|
+
|
110
|
+
Finally within we can send a push notification using the following code
|
111
|
+
|
112
|
+
<pre>
|
113
|
+
<code>
|
114
|
+
notification = ApnServer::Notification.new
|
115
|
+
notification.device_token = Base64.decode64(apns_token) # if base64 encoded
|
116
|
+
notification.alert = message
|
117
|
+
notification.badge = 1
|
118
|
+
notification.sound = 'default'
|
119
|
+
notification.push
|
120
|
+
</code>
|
121
|
+
</pre>
|
122
|
+
|
123
|
+
|
124
|
+
h2. Installation
|
125
|
+
|
126
|
+
To install apnserver execute the following gem command:
|
127
|
+
|
128
|
+
<pre>
|
129
|
+
<code>
|
130
|
+
$ gem install bpoweski-apnserver --source http://gems.github.com
|
131
|
+
</code>
|
132
|
+
</pre>
|
133
|
+
|
134
|
+
Adding apnserver to your Rails application
|
135
|
+
|
136
|
+
<pre>
|
137
|
+
<code>
|
138
|
+
config.gem "bpoweski-apnserver", :lib => 'apnserver', :source => "http://gems.github.com"
|
139
|
+
</code>
|
140
|
+
</pre>
|
141
|
+
|
142
|
+
h2. License
|
143
|
+
|
144
|
+
(The MIT License)
|
145
|
+
|
146
|
+
Copyright (c) 2009 Ben Poweski
|
147
|
+
|
148
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
149
|
+
a copy of this software and associated documentation files (the
|
150
|
+
'Software'), to deal in the Software without restriction, including
|
151
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
152
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
153
|
+
permit persons to whom the Software is furnished to do so, subject to
|
154
|
+
the following conditions:
|
155
|
+
|
156
|
+
The above copyright notice and this permission notice shall be
|
157
|
+
included in all copies or substantial portions of the Software.
|
158
|
+
|
159
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
160
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
161
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
162
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
163
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
164
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
165
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
166
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gemspec|
|
6
|
+
gemspec.name = "apnserver"
|
7
|
+
gemspec.summary = "Apple Push Notification Server"
|
8
|
+
gemspec.description = "A toolkit for proxying and sending Apple Push Notifications"
|
9
|
+
gemspec.email = "bpoweski@3factors.com"
|
10
|
+
gemspec.homepage = "http://github.com/bpoweski/apnserver"
|
11
|
+
gemspec.authors = ["Ben Poweski"]
|
12
|
+
gemspec.add_dependency 'eventmachine'
|
13
|
+
gemspec.add_dependency 'daemons'
|
14
|
+
gemspec.add_dependency 'json'
|
15
|
+
gemspec.rubyforge_project = 'apnserver'
|
16
|
+
gemspec.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*'].to_a
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
24
|
+
test.test_files = FileList.new('test/**/test_*.rb') do |list|
|
25
|
+
list.exclude 'test/test_helper.rb'
|
26
|
+
end
|
27
|
+
test.libs << 'test'
|
28
|
+
test.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
Jeweler::RubyforgeTasks.new do |rubyforge|
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => [:test]
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.9
|
data/bin/apnsend
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'logger'
|
5
|
+
require 'getoptlong'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'apnserver'
|
8
|
+
require 'base64'
|
9
|
+
require 'socket'
|
10
|
+
|
11
|
+
def usage
|
12
|
+
puts "Usage: apnsend [switches] (--b64-token | --hex-token) <token>"
|
13
|
+
puts " --server <localhost> the apn server defaults to a locally running apnserverd"
|
14
|
+
puts " --port <2195> the port of the apn server"
|
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 " --alert <message> the message to send"
|
17
|
+
puts " --sound <default> the sound to play, defaults to 'default'"
|
18
|
+
puts " --badge <number> the badge number"
|
19
|
+
puts " --custom <json string> a custom json string to be added to the main object"
|
20
|
+
puts " --b64-token <token> a base 64 encoded device token"
|
21
|
+
puts " --hex-token <token> a hex encoded device token"
|
22
|
+
puts " --help this message"
|
23
|
+
end
|
24
|
+
|
25
|
+
opts = GetoptLong.new(
|
26
|
+
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
|
27
|
+
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
|
28
|
+
["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
|
29
|
+
["--alert", "-a", GetoptLong::REQUIRED_ARGUMENT],
|
30
|
+
["--sound", "-S", GetoptLong::REQUIRED_ARGUMENT],
|
31
|
+
["--badge", "-b", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
+
["--custom", "-j", GetoptLong::REQUIRED_ARGUMENT],
|
33
|
+
["--b64-token", "-B", GetoptLong::REQUIRED_ARGUMENT],
|
34
|
+
["--hex-token", "-H", GetoptLong::REQUIRED_ARGUMENT],
|
35
|
+
["--help", "-h", GetoptLong::NO_ARGUMENT]
|
36
|
+
)
|
37
|
+
|
38
|
+
notification = ApnServer::Notification.new
|
39
|
+
|
40
|
+
opts.each do |opt, arg|
|
41
|
+
case opt
|
42
|
+
when '--help'
|
43
|
+
usage
|
44
|
+
exit
|
45
|
+
when '--server'
|
46
|
+
ApnServer::Config.host = arg
|
47
|
+
when '--port'
|
48
|
+
ApnServer::Config.port = arg.to_i
|
49
|
+
when '--pem'
|
50
|
+
ApnServer::Config.pem = arg
|
51
|
+
when '--alert'
|
52
|
+
notification.alert = arg
|
53
|
+
when '--sound'
|
54
|
+
notification.sound = arg
|
55
|
+
when '--badge'
|
56
|
+
notification.badge = arg.to_i
|
57
|
+
when '--custom'
|
58
|
+
notification.custom = arg
|
59
|
+
when '--b64-token'
|
60
|
+
notification.device_token = Base64::decode64(arg)
|
61
|
+
when '--hex-token'
|
62
|
+
notification.device_token = arg.unpack('H*')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if notification.device_token.nil?
|
67
|
+
usage
|
68
|
+
exit
|
69
|
+
else
|
70
|
+
notification.push
|
71
|
+
end
|
data/bin/apnserverd
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'getoptlong'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'daemons'
|
7
|
+
require 'apnserver'
|
8
|
+
|
9
|
+
def usage
|
10
|
+
puts "Usage: apnserverd [switches] --pem <path>"
|
11
|
+
puts " --bind-address [0.0.0.0] bind address of proxy"
|
12
|
+
puts " --proxy-port [22195] port proxy listens on"
|
13
|
+
puts " --server <gateway.push.apple.com> the apn server to send messages to"
|
14
|
+
puts " --port <2195> the port of the apn server"
|
15
|
+
puts " --help this message"
|
16
|
+
end
|
17
|
+
|
18
|
+
def daemonize
|
19
|
+
Daemonize.daemonize('/var/log/apnserverd.log', 'apnserverd')
|
20
|
+
@pid_file = '/var/run/apnserverd.pid'
|
21
|
+
open(@pid_file,"w") {|f| f.write(Process.pid) } # copied from mongrel
|
22
|
+
open(@pid_file,"w") do |f|
|
23
|
+
f.write(Process.pid)
|
24
|
+
File.chmod(0644, @pid_file)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
opts = GetoptLong.new(
|
29
|
+
["--bind-address", "-b", GetoptLong::REQUIRED_ARGUMENT],
|
30
|
+
["--proxy-port", "-P", GetoptLong::REQUIRED_ARGUMENT],
|
31
|
+
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
+
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
|
33
|
+
["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
|
34
|
+
["--help", "-h", GetoptLong::NO_ARGUMENT],
|
35
|
+
["--daemon", "-d", GetoptLong::NO_ARGUMENT]
|
36
|
+
)
|
37
|
+
|
38
|
+
bind_address = '0.0.0.0'
|
39
|
+
proxy_port = 22195
|
40
|
+
host = 'gateway.push.apple.com'
|
41
|
+
port = 2195
|
42
|
+
pem = nil
|
43
|
+
daemon = false
|
44
|
+
|
45
|
+
opts.each do |opt, arg|
|
46
|
+
case opt
|
47
|
+
when '--help'
|
48
|
+
usage
|
49
|
+
when '--bind-address'
|
50
|
+
bind_address = arg
|
51
|
+
when '--proxy-port'
|
52
|
+
proxy_port = arg.to_i
|
53
|
+
when '--server'
|
54
|
+
host = arg
|
55
|
+
when '--port'
|
56
|
+
port = arg.to_i
|
57
|
+
when '--pem'
|
58
|
+
pem = arg
|
59
|
+
when '--daemon'
|
60
|
+
daemon = true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if pem.nil?
|
65
|
+
usage
|
66
|
+
exit 1
|
67
|
+
else
|
68
|
+
daemonize if daemon
|
69
|
+
server = ApnServer::Server.new(pem, bind_address, proxy_port)
|
70
|
+
server.client.host = host
|
71
|
+
server.client.port = port
|
72
|
+
server.start!
|
73
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
#
|
3
|
+
# /etc/rc.d/init.d/apnserverd
|
4
|
+
# apnserverd This shell script takes care of starting and stopping
|
5
|
+
# the APN Server Proxy
|
6
|
+
#
|
7
|
+
# chkconfig: 345 20 80
|
8
|
+
# Author: Ben Poweski bpoweski@gmail.com
|
9
|
+
#
|
10
|
+
# Source function library.
|
11
|
+
. /etc/init.d/functions
|
12
|
+
|
13
|
+
NAME=apnserverd
|
14
|
+
APNSERVERD=/usr/bin/$NAME
|
15
|
+
PIDFILE=/var/run/$NAME.pid
|
16
|
+
|
17
|
+
if [ -f /etc/sysconfig/$NAME ]; then
|
18
|
+
. /etc/sysconfig/$NAME
|
19
|
+
fi
|
20
|
+
|
21
|
+
|
22
|
+
start() {
|
23
|
+
echo -n "Starting APN Server: "
|
24
|
+
if [ -f $PIDFILE ]; then
|
25
|
+
PID=`cat $PIDFILE`
|
26
|
+
echo $NAME already running: $PID
|
27
|
+
exit 2;
|
28
|
+
elif [ -f $PIDFILE ]; then
|
29
|
+
PID=`cat $PIDFILE`
|
30
|
+
echo $NAME already running: $PID
|
31
|
+
exit 2;
|
32
|
+
else
|
33
|
+
daemon $APNSERVERD $OPTIONS
|
34
|
+
RETVAL=$?
|
35
|
+
echo
|
36
|
+
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/$NAME
|
37
|
+
return $RETVAL
|
38
|
+
fi
|
39
|
+
|
40
|
+
}
|
41
|
+
|
42
|
+
stop() {
|
43
|
+
echo -n "Shutting down APN Server: "
|
44
|
+
echo
|
45
|
+
kill `cat $PIDFILE`
|
46
|
+
echo
|
47
|
+
rm -f /var/lock/subsys/$NAME
|
48
|
+
rm -f $PIDFILE
|
49
|
+
return 0
|
50
|
+
}
|
51
|
+
|
52
|
+
case "$1" in
|
53
|
+
start)
|
54
|
+
start
|
55
|
+
;;
|
56
|
+
stop)
|
57
|
+
stop
|
58
|
+
;;
|
59
|
+
status)
|
60
|
+
status $NAME
|
61
|
+
;;
|
62
|
+
restart)
|
63
|
+
stop
|
64
|
+
start
|
65
|
+
;;
|
66
|
+
*)
|
67
|
+
echo "Usage: {start|stop|status|restart}"
|
68
|
+
exit 1
|
69
|
+
;;
|
70
|
+
esac
|
71
|
+
exit $?
|
data/lib/apnserver.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module ApnServer
|
5
|
+
class Client
|
6
|
+
|
7
|
+
attr_accessor :pem, :host, :port, :password
|
8
|
+
|
9
|
+
def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil)
|
10
|
+
@pem, @host, @port, @password = pem, host, port, pass
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect!
|
14
|
+
raise "The path to your pem file is not set." unless self.pem
|
15
|
+
raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
|
16
|
+
|
17
|
+
@context = OpenSSL::SSL::SSLContext.new
|
18
|
+
@context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
|
19
|
+
@context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.password)
|
20
|
+
|
21
|
+
@sock = TCPSocket.new(self.host, self.port.to_i)
|
22
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context)
|
23
|
+
@ssl.connect
|
24
|
+
|
25
|
+
return @sock, @ssl
|
26
|
+
end
|
27
|
+
|
28
|
+
def disconnect!
|
29
|
+
@ssl.close
|
30
|
+
@sock.close
|
31
|
+
@ssl = nil
|
32
|
+
@sock = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def write(notification)
|
36
|
+
puts "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}"
|
37
|
+
@ssl.write(notification.to_bytes)
|
38
|
+
end
|
39
|
+
|
40
|
+
def connected?
|
41
|
+
@ssl
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'apnserver/payload'
|
2
|
+
require 'json'
|
3
|
+
require 'json/add/rails'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module ApnServer
|
7
|
+
|
8
|
+
class Config
|
9
|
+
class << self
|
10
|
+
attr_accessor :host, :port, :pem, :password
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
class Notification
|
16
|
+
include ApnServer::Payload
|
17
|
+
|
18
|
+
attr_accessor :device_token, :alert, :badge, :sound, :custom
|
19
|
+
|
20
|
+
|
21
|
+
def payload
|
22
|
+
p = Hash.new
|
23
|
+
[:badge, :alert, :sound, :custom].each do |k|
|
24
|
+
p[k] = send(k) if send(k)
|
25
|
+
end
|
26
|
+
create_payload(p)
|
27
|
+
end
|
28
|
+
|
29
|
+
def json_payload
|
30
|
+
j = defined?(Rails) ? payload.to_json : JSON.generate(payload)
|
31
|
+
raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256
|
32
|
+
j
|
33
|
+
end
|
34
|
+
|
35
|
+
def push
|
36
|
+
if Config.pem.nil?
|
37
|
+
socket = TCPSocket.new(Config.host || 'localhost', Config.port.to_i || 22195)
|
38
|
+
socket.write(to_bytes)
|
39
|
+
socket.close
|
40
|
+
else
|
41
|
+
client = ApnServer::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195)
|
42
|
+
client.connect!
|
43
|
+
client.write(self)
|
44
|
+
client.disconnect!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_bytes
|
49
|
+
j = json_payload
|
50
|
+
[0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*")
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.valid?(p)
|
54
|
+
begin
|
55
|
+
Notification.parse(p)
|
56
|
+
rescue PayloadInvalid => p
|
57
|
+
puts "PayloadInvalid: #{p}"
|
58
|
+
false
|
59
|
+
rescue JSON::ParserError => p
|
60
|
+
false
|
61
|
+
rescue RuntimeError
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.parse(p)
|
67
|
+
buffer = p.dup
|
68
|
+
notification = Notification.new
|
69
|
+
|
70
|
+
header = buffer.slice!(0, 3).unpack('ccc')
|
71
|
+
if header[0] != 0
|
72
|
+
raise RuntimeError.new("Header of notification is invalid: #{header.inspect}")
|
73
|
+
end
|
74
|
+
|
75
|
+
# parse token
|
76
|
+
notification.device_token = buffer.slice!(0, 32).unpack('a*').first
|
77
|
+
|
78
|
+
# parse json payload
|
79
|
+
payload_len = buffer.slice!(0, 2).unpack('CC')
|
80
|
+
j = buffer.slice!(0, payload_len.last)
|
81
|
+
result = JSON.parse(j)
|
82
|
+
|
83
|
+
['alert', 'badge', 'sound'].each do |k|
|
84
|
+
notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k]
|
85
|
+
end
|
86
|
+
result.delete('aps')
|
87
|
+
notification.custom = result
|
88
|
+
|
89
|
+
notification
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ApnServer
|
2
|
+
|
3
|
+
module Payload
|
4
|
+
|
5
|
+
class PayloadInvalid < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_payload(payload)
|
9
|
+
case payload
|
10
|
+
when String then { :aps => { :alert => payload } }
|
11
|
+
when Hash then create_payload_from_hash(payload)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_payload_from_hash(payload)
|
16
|
+
custom = payload.delete(:custom)
|
17
|
+
aps = {:aps => payload }
|
18
|
+
aps.merge!(custom) if custom
|
19
|
+
aps
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ApnServer
|
2
|
+
module Protocol
|
3
|
+
|
4
|
+
def post_init
|
5
|
+
@address = Socket.unpack_sockaddr_in(self.get_peername)
|
6
|
+
puts "#{Time.now} [#{address.last}:#{address.first}] CONNECT"
|
7
|
+
end
|
8
|
+
|
9
|
+
def unbind
|
10
|
+
puts "#{Time.now} [#{address.last}:#{address.first}] DISCONNECT"
|
11
|
+
end
|
12
|
+
|
13
|
+
def receive_data(data)
|
14
|
+
(@buf ||= "") << data
|
15
|
+
if notification = ApnServer::Notification.valid?(@buf)
|
16
|
+
puts "#{Time.now} [#{address.last}:#{address.first}] found valid Notification: #{notification}"
|
17
|
+
queue.push(notification)
|
18
|
+
else
|
19
|
+
puts "#{Time.now} [#{address.last}:#{address.first}] invalid notification: #{@buf}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ApnServer
|
2
|
+
|
3
|
+
class Server
|
4
|
+
attr_accessor :client, :bind_address, :port
|
5
|
+
|
6
|
+
def initialize(pem, bind_address = '0.0.0.0', port = 22195)
|
7
|
+
@queue = EM::Queue.new
|
8
|
+
@client = ApnServer::Client.new(pem)
|
9
|
+
@bind_address, @port = bind_address, port
|
10
|
+
end
|
11
|
+
|
12
|
+
def start!
|
13
|
+
EventMachine::run do
|
14
|
+
puts "#{Time.now} Starting APN Server on #{bind_address}:#{port}"
|
15
|
+
|
16
|
+
EM.start_server(bind_address, port, ApnServer::ServerConnection) do |s|
|
17
|
+
s.queue = @queue
|
18
|
+
end
|
19
|
+
|
20
|
+
EventMachine::PeriodicTimer.new(1) do
|
21
|
+
unless @queue.empty?
|
22
|
+
size = @queue.size
|
23
|
+
size.times do
|
24
|
+
@queue.pop do |notification|
|
25
|
+
begin
|
26
|
+
@client.connect! unless @client.connected?
|
27
|
+
@client.write(notification)
|
28
|
+
rescue Errno::EPIPE
|
29
|
+
puts "Caught Errno::EPIPE adding notification back to queue"
|
30
|
+
@queue.push(notification)
|
31
|
+
rescue OpenSSL::SSL::SSLError
|
32
|
+
puts "Caught OpenSSL Error, closing connecting and adding notification back to queue"
|
33
|
+
@client.disconnect!
|
34
|
+
@queue.push(notification)
|
35
|
+
rescue RuntimeError => e
|
36
|
+
puts "Unable to handle: #{e}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class TestClient < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_creates_client
|
6
|
+
client = ApnServer::Client.new('cert.pem', 'gateway.sandbox.push.apple.com', 2196)
|
7
|
+
assert_equal 'cert.pem', client.pem
|
8
|
+
assert_equal 'gateway.sandbox.push.apple.com', client.host
|
9
|
+
assert_equal 2196, client.port
|
10
|
+
end
|
11
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
class NotificationTest < Test::Unit::TestCase
|
5
|
+
include ApnServer
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@notification = Notification.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_should_generate_byte_array
|
12
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
13
|
+
device_token = "12345678123456781234567812345678"
|
14
|
+
@notification.device_token = device_token
|
15
|
+
@notification.alert = "You have not mail!"
|
16
|
+
expected = [0, 0, device_token.size, device_token, 0, payload.size, payload]
|
17
|
+
assert_equal expected.pack("ccca*CCa*"), @notification.to_bytes
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_should_create_payload_with_badge_attribute
|
21
|
+
expected = { :aps => { :badge => 1 }}
|
22
|
+
@notification.badge = 1
|
23
|
+
assert_equal expected, @notification.payload
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_should_create_payload_with_alert_attribute
|
27
|
+
expected = { :aps => { :alert => 'Hi' }}
|
28
|
+
@notification.alert = 'Hi'
|
29
|
+
assert_equal expected, @notification.payload
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_should_create_json_payload
|
33
|
+
expected = '{"aps":{"alert":"Hi"}}'
|
34
|
+
@notification.alert = 'Hi'
|
35
|
+
assert_equal expected, @notification.json_payload
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_should_not_allow_for_payloads_larger_than_256_chars
|
39
|
+
assert_raise Payload::PayloadInvalid do
|
40
|
+
alert = []
|
41
|
+
256.times { alert << 'Hi' }
|
42
|
+
@notification.alert = alert.join
|
43
|
+
@notification.json_payload
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_should_recognize_valid_request
|
48
|
+
device_token = '12345678123456781234567812345678'
|
49
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
50
|
+
request = [0, 0, device_token.size, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
|
51
|
+
assert Notification.valid?(request)
|
52
|
+
notification = Notification.parse(request)
|
53
|
+
assert_equal device_token, notification.device_token
|
54
|
+
assert_equal "You have not mail!", notification.alert
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_should_not_recognize_invalid_request
|
58
|
+
device_token = '123456781234567812345678'
|
59
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
60
|
+
request = [0, 0, 32, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
|
61
|
+
assert !Notification.valid?(request), request
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_should_pack_and_unpack_json
|
65
|
+
device_token = '12345678123456781234567812345678'
|
66
|
+
notification = Notification.new
|
67
|
+
notification.device_token = device_token
|
68
|
+
notification.badge = 10
|
69
|
+
notification.alert = 'Hi'
|
70
|
+
notification.sound = 'default'
|
71
|
+
notification.custom = { 'acme1' => "bar", 'acme2' => 42}
|
72
|
+
|
73
|
+
parsed = Notification.parse(notification.to_bytes)
|
74
|
+
[:device_token, :badge, :alert, :sound, :custom].each do |k|
|
75
|
+
expected = notification.send(k)
|
76
|
+
assert_equal expected, parsed.send(k), "Expected #{k} to be #{expected}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class NotificationTest < Test::Unit::TestCase
|
4
|
+
include ApnServer::Payload
|
5
|
+
|
6
|
+
def test_should_create_payload_with_simple_string
|
7
|
+
expected = { :aps => { :alert => 'Hi' }}
|
8
|
+
assert_equal expected, create_payload('Hi')
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_should_create_payload_with_alert_key
|
12
|
+
expected = { :aps => { :alert => 'Hi' }}
|
13
|
+
assert_equal expected, create_payload(:alert => 'Hi')
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_should_create_payload_with_badge_and_alert
|
17
|
+
expected = { :aps => { :alert => 'Hi', :badge => 1 }}
|
18
|
+
assert_equal expected, create_payload(:alert => 'Hi', :badge => 1)
|
19
|
+
end
|
20
|
+
|
21
|
+
# example 1
|
22
|
+
def test_should_create_payload_with_custom_payload
|
23
|
+
alert = 'Message received from Bob'
|
24
|
+
expected = {
|
25
|
+
:aps => { :alert => alert },
|
26
|
+
:acme2 => [ "bang", "whiz" ]
|
27
|
+
}
|
28
|
+
assert_equal expected, create_payload(:alert => alert, :custom => { :acme2 => ['bang', 'whiz']})
|
29
|
+
end
|
30
|
+
|
31
|
+
# example 3
|
32
|
+
def test_should_create_payload_with_sound_and_multiple_custom
|
33
|
+
expected = {
|
34
|
+
:aps => {
|
35
|
+
:alert => "You got your emails.",
|
36
|
+
:badge => 9,
|
37
|
+
:sound => "bingbong.aiff"
|
38
|
+
},
|
39
|
+
:acme1 => "bar",
|
40
|
+
:acme2 => 42
|
41
|
+
}
|
42
|
+
assert_equal expected, create_payload({
|
43
|
+
:alert => "You got your emails.",
|
44
|
+
:badge => 9,
|
45
|
+
:sound => "bingbong.aiff",
|
46
|
+
:custom => { :acme1 => "bar", :acme2 => 42}
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
# example 5
|
51
|
+
def test_should_create_payload_with_empty_aps
|
52
|
+
expected = {
|
53
|
+
:aps => {},
|
54
|
+
:acme2 => [ 5, 8 ]
|
55
|
+
}
|
56
|
+
assert_equal expected, create_payload(:custom => { :acme2 => [ 5, 8 ] })
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class TestServer
|
4
|
+
attr_accessor :queue
|
5
|
+
include ApnServer::Protocol
|
6
|
+
|
7
|
+
def address
|
8
|
+
[12345, '127.0.0.1']
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestProtocol < Test::Unit::TestCase
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@server = TestServer.new
|
16
|
+
@server.queue = Array.new # fake out EM::Queue
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_adds_notification_to_queue
|
20
|
+
token = "12345678123456781234567812345678"
|
21
|
+
@server.receive_data("\0\0 #{token}\0#{22.chr}{\"aps\":{\"alert\":\"Hi\"}}")
|
22
|
+
assert_equal 1, @server.queue.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_does_not_add_invalid_notification
|
26
|
+
@server.receive_data('fakedata')
|
27
|
+
assert @server.queue.empty?
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apnserver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Poweski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-25 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: eventmachine
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: daemons
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: json
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
description: A toolkit for proxying and sending Apple Push Notifications
|
46
|
+
email: bpoweski@3factors.com
|
47
|
+
executables:
|
48
|
+
- apnsend
|
49
|
+
- apnserverd
|
50
|
+
- apnserverd.fedora.init
|
51
|
+
extensions: []
|
52
|
+
|
53
|
+
extra_rdoc_files:
|
54
|
+
- README.textile
|
55
|
+
files:
|
56
|
+
- README.textile
|
57
|
+
- Rakefile
|
58
|
+
- VERSION
|
59
|
+
- bin/apnsend
|
60
|
+
- bin/apnserverd
|
61
|
+
- bin/apnserverd.fedora.init
|
62
|
+
- lib/apnserver.rb
|
63
|
+
- lib/apnserver/client.rb
|
64
|
+
- lib/apnserver/notification.rb
|
65
|
+
- lib/apnserver/payload.rb
|
66
|
+
- lib/apnserver/protocol.rb
|
67
|
+
- lib/apnserver/server.rb
|
68
|
+
- lib/apnserver/server_connection.rb
|
69
|
+
- test/test_client.rb
|
70
|
+
- test/test_helper.rb
|
71
|
+
- test/test_notification.rb
|
72
|
+
- test/test_payload.rb
|
73
|
+
- test/test_protocol.rb
|
74
|
+
has_rdoc: true
|
75
|
+
homepage: http://github.com/bpoweski/apnserver
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options:
|
78
|
+
- --charset=UTF-8
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: "0"
|
86
|
+
version:
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: "0"
|
92
|
+
version:
|
93
|
+
requirements: []
|
94
|
+
|
95
|
+
rubyforge_project: apnserver
|
96
|
+
rubygems_version: 1.3.1
|
97
|
+
signing_key:
|
98
|
+
specification_version: 2
|
99
|
+
summary: Apple Push Notification Server
|
100
|
+
test_files:
|
101
|
+
- test/test_client.rb
|
102
|
+
- test/test_helper.rb
|
103
|
+
- test/test_notification.rb
|
104
|
+
- test/test_payload.rb
|
105
|
+
- test/test_protocol.rb
|