racoon 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.mdown +172 -0
- data/bin/apnsend +75 -0
- data/bin/apnserverd.fedora.init +71 -0
- data/bin/apnserverd.ubuntu.init +116 -0
- data/bin/racoond +65 -0
- data/lib/racoon/client.rb +42 -0
- data/lib/racoon/config.rb +9 -0
- data/lib/racoon/feedback_client.rb +24 -0
- data/lib/racoon/notification.rb +87 -0
- data/lib/racoon/payload.rb +19 -0
- data/lib/racoon/server.rb +160 -0
- data/lib/racoon.rb +6 -0
- data/spec/models/client_spec.rb +21 -0
- data/spec/models/feedback_client_spec.rb +21 -0
- data/spec/models/notification_spec.rb +85 -0
- data/spec/models/payload_spec.rb +57 -0
- data/spec/spec_helper.rb +26 -0
- metadata +116 -0
data/README.mdown
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# Racoon push notification server
|
2
|
+
|
3
|
+
This project started off as a fork of [apnserver](https://github.com/bpoweski/apnserver). It
|
4
|
+
has since taken on a different path. How does it differ from apnserver? By a few key points:
|
5
|
+
|
6
|
+
1. It implements the APNS feedback service;
|
7
|
+
2. Uses Yajl for JSON encoding/decoding rather than ActiveSupport;
|
8
|
+
3. Expects certificates as strings instead of paths to files;
|
9
|
+
4. Does not assume there is only one certificate; and
|
10
|
+
5. Receives packets containing notifications from beanstalkd instead of a listening socket.
|
11
|
+
|
12
|
+
The above changes were made because of the need for an APNS provider to replace the current
|
13
|
+
provider used by [Diligent Street](http://www.diligentstreet.com/) with something more robust. As such, it needs to be
|
14
|
+
suitable for a hosted environment, where multiple—unrelated—users of the service will be
|
15
|
+
using it.
|
16
|
+
|
17
|
+
It should be noted that the development of this project is independent of the work bpoweski
|
18
|
+
is doing on apnserver. If you're looking for that project, [go here](https://github.com/bpoweski/apnserver).
|
19
|
+
|
20
|
+
## Description
|
21
|
+
|
22
|
+
racoon is a server and a set of command line programs to send push notifications to iOS devices.
|
23
|
+
Apple recommends to maintain an open connection to the push notification service, and refrain
|
24
|
+
from opening up and tearing down SSL connections repeatedly. As such, a separate daemon is
|
25
|
+
introduced that has messages queued up (beanstalkd) for consumption by this daemon. This
|
26
|
+
decouples the APNS server from your backend system. Those notifications are sent over a
|
27
|
+
persistent connection to Apple.
|
28
|
+
|
29
|
+
## Remaining Tasks & Issues
|
30
|
+
|
31
|
+
You can see progress by looking at the [issue tracker](https://www.pivotaltracker.com/projects/251991) page for fracas. Any labels related to
|
32
|
+
*apnserver* or *racoon* are related to this subproject.
|
33
|
+
|
34
|
+
## Preparing Certificates
|
35
|
+
|
36
|
+
Certificates must be prepared before they can be used with racoon. Unfortunately, Apple
|
37
|
+
gives us @.p12@ files instead of @.pem@ files. As such, we need to convert them. This
|
38
|
+
can be accomplished by dropping to the command line and running this command:
|
39
|
+
|
40
|
+
<pre>
|
41
|
+
$ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
|
42
|
+
</pre>
|
43
|
+
|
44
|
+
This will generate a file suitable for use with this daemon, called @cert.pem@. If you're
|
45
|
+
using frac.as, this is the file you would upload to the web service.
|
46
|
+
|
47
|
+
If you're not using frac.as, then the contents of this file are what you need to use as
|
48
|
+
your certificate, not the path to the file.
|
49
|
+
|
50
|
+
## APN Server Daemon
|
51
|
+
|
52
|
+
<pre>
|
53
|
+
Usage: racoond [options]
|
54
|
+
--beanstalk <csv ip:port mappings>
|
55
|
+
The comma-separated list of ip:port for beanstalk servers
|
56
|
+
|
57
|
+
--pid <pid file path>
|
58
|
+
Path used for the PID file. Defaults to /var/run/racoon.pid
|
59
|
+
|
60
|
+
--log <log file path>
|
61
|
+
Path used for the log file. Defaults to /var/log/racoon.log
|
62
|
+
|
63
|
+
--help
|
64
|
+
usage message
|
65
|
+
|
66
|
+
--daemon
|
67
|
+
Runs process as daemon, not available on Windows
|
68
|
+
</pre>
|
69
|
+
|
70
|
+
## APN Server Client
|
71
|
+
|
72
|
+
TODO: Document this
|
73
|
+
|
74
|
+
## Sending Notifications from Ruby
|
75
|
+
|
76
|
+
You need to set up a connection to the beanstalkd service. We can do this simply by defining
|
77
|
+
the following method:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
def beanstalk
|
81
|
+
return @beanstalk if @beanstalk
|
82
|
+
@beanstalk = Beanstalk::Pool.new ["127.0.0.1:11300"]
|
83
|
+
@beanstalk.use "awesome-tube"
|
84
|
+
@beanstalk
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
In this way, whenever we need access to beanstalk, we'll make the connection and set up to use
|
89
|
+
the appropriate tube whether the connection is open yet or not.
|
90
|
+
|
91
|
+
We will also need two pieces of information: A project, and a notification.
|
92
|
+
|
93
|
+
### Project
|
94
|
+
|
95
|
+
A project os comprised of a few pieces of information at a minimum (you may supply more if you
|
96
|
+
wish, but racoond will ignore them):
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
project = { :name => "Awesome project", :certificate => "contents of the generated .pem file" }
|
100
|
+
```
|
101
|
+
|
102
|
+
### Notification
|
103
|
+
|
104
|
+
A notification is a ruby hash containing the things to be sent along, including the device token.
|
105
|
+
An example notification may look like this:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
notification = { :device_token => "hex encoded device token",
|
109
|
+
:aps => { :alert => "Some text",
|
110
|
+
:sound => "Audio_file",
|
111
|
+
:badge => 42,
|
112
|
+
:custom => { :field => "lala",
|
113
|
+
:stuff => 42 }
|
114
|
+
}
|
115
|
+
}
|
116
|
+
```
|
117
|
+
|
118
|
+
Finally within we can send a push notification using the following code:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
beanstalk.yput({ :project => project, :notification => notification, :sandbox => true })
|
122
|
+
```
|
123
|
+
|
124
|
+
Note that the `sandbox` parameter is used to indicate whether or not we're using a development
|
125
|
+
certificate, and as such, should contact Apple's sandbox APN service instead of the production
|
126
|
+
certificate. If left out, we assume production.
|
127
|
+
|
128
|
+
This will schedule the push on beanstalkd. Racoon is constantly polling beanstalkd looking for
|
129
|
+
ready jobs it can pop off and process (send to Apple). Using beanstalkd however allows us to
|
130
|
+
queue up items, and during peak times, add another **N** more racoon servers to make up any
|
131
|
+
backlog, to ensure our messages are sent fast, and that we can scale.
|
132
|
+
|
133
|
+
## Installation
|
134
|
+
|
135
|
+
Racoon is hosted on [rubygems](https://rubygems.org/gems/racoon)
|
136
|
+
|
137
|
+
<pre>
|
138
|
+
$ gem install racoon
|
139
|
+
</pre>
|
140
|
+
|
141
|
+
Adding racoon to your Rails application
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
gem 'racoon'
|
145
|
+
```
|
146
|
+
|
147
|
+
## License
|
148
|
+
|
149
|
+
(The MIT License)
|
150
|
+
|
151
|
+
Copyright (c) 2011 Jeremy Tregunna
|
152
|
+
Copyright (c) 2011 Ben Poweski
|
153
|
+
|
154
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
155
|
+
a copy of this software and associated documentation files (the
|
156
|
+
'Software'), to deal in the Software without restriction, including
|
157
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
158
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
159
|
+
permit persons to whom the Software is furnished to do so, subject to
|
160
|
+
the following conditions:
|
161
|
+
|
162
|
+
The above copyright notice and this permission notice shall be
|
163
|
+
included in all copies or substantial portions of the Software.
|
164
|
+
|
165
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
166
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
167
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
168
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
169
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
170
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
171
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
172
|
+
|
data/bin/apnsend
ADDED
@@ -0,0 +1,75 @@
|
|
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 " --pem-passphrase <passphrase> the pem passphrase"
|
17
|
+
puts " --alert <message> the message to send"
|
18
|
+
puts " --sound <default> the sound to play, defaults to 'default'"
|
19
|
+
puts " --badge <number> the badge number"
|
20
|
+
puts " --custom <json string> a custom json string to be added to the main object"
|
21
|
+
puts " --b64-token <token> a base 64 encoded device token"
|
22
|
+
puts " --hex-token <token> a hex encoded device token"
|
23
|
+
puts " --help this message"
|
24
|
+
end
|
25
|
+
|
26
|
+
opts = GetoptLong.new(
|
27
|
+
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
|
28
|
+
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
|
29
|
+
["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
|
30
|
+
["--pem-passphrase", "-C", GetoptLong::REQUIRED_ARGUMENT],
|
31
|
+
["--alert", "-a", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
+
["--sound", "-S", GetoptLong::REQUIRED_ARGUMENT],
|
33
|
+
["--badge", "-b", GetoptLong::REQUIRED_ARGUMENT],
|
34
|
+
["--custom", "-j", GetoptLong::REQUIRED_ARGUMENT],
|
35
|
+
["--b64-token", "-B", GetoptLong::REQUIRED_ARGUMENT],
|
36
|
+
["--hex-token", "-H", GetoptLong::REQUIRED_ARGUMENT],
|
37
|
+
["--help", "-h", GetoptLong::NO_ARGUMENT]
|
38
|
+
)
|
39
|
+
|
40
|
+
notification = ApnServer::Notification.new
|
41
|
+
|
42
|
+
opts.each do |opt, arg|
|
43
|
+
case opt
|
44
|
+
when '--help'
|
45
|
+
usage
|
46
|
+
exit
|
47
|
+
when '--server'
|
48
|
+
ApnServer::Config.host = arg
|
49
|
+
when '--port'
|
50
|
+
ApnServer::Config.port = arg.to_i
|
51
|
+
when '--pem'
|
52
|
+
ApnServer::Config.pem = File.read(arg)
|
53
|
+
when '--pem-passphrase'
|
54
|
+
ApnServer::Config.password = arg
|
55
|
+
when '--alert'
|
56
|
+
notification.alert = arg
|
57
|
+
when '--sound'
|
58
|
+
notification.sound = arg
|
59
|
+
when '--badge'
|
60
|
+
notification.badge = arg.to_i
|
61
|
+
when '--custom'
|
62
|
+
notification.custom = ActiveSupport::JSON.decode(arg)
|
63
|
+
when '--b64-token'
|
64
|
+
notification.device_token = Base64::decode64(arg)
|
65
|
+
when '--hex-token'
|
66
|
+
notification.device_token = arg.scan(/[0-9a-f][0-9a-f]/).map {|s| s.hex.chr}.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if notification.device_token.nil?
|
71
|
+
usage
|
72
|
+
exit
|
73
|
+
else
|
74
|
+
notification.push
|
75
|
+
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 $?
|
@@ -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/bin/racoond
ADDED
@@ -0,0 +1,65 @@
|
|
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 'eventmachine'
|
8
|
+
require 'racoon'
|
9
|
+
require 'racoon/server'
|
10
|
+
require 'csv'
|
11
|
+
|
12
|
+
def usage
|
13
|
+
puts "Usage: racoond [switches] --beanstalk a.b.c.d:11300,...,w.x.y.z:11300"
|
14
|
+
puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers"
|
15
|
+
puts " --pid </var/run/racoond.pid> the path to store the pid"
|
16
|
+
puts " --log </var/log/racoond.log> the path to store the log"
|
17
|
+
puts " --daemon to daemonize the server"
|
18
|
+
puts " --help this message"
|
19
|
+
end
|
20
|
+
|
21
|
+
def daemonize
|
22
|
+
Daemonize.daemonize(@log_file, 'racoond')
|
23
|
+
open(@pid_file,"w") { |f| f.write(Process.pid) }
|
24
|
+
open(@pid_file,"w") do |f|
|
25
|
+
f.write(Process.pid)
|
26
|
+
File.chmod(0644, @pid_file)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
opts = GetoptLong.new(
|
31
|
+
["--beanstalk", "-b", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
+
["--pid", "-i", GetoptLong::REQUIRED_ARGUMENT],
|
33
|
+
["--log", "-l", GetoptLong::REQUIRED_ARGUMENT],
|
34
|
+
["--help", "-h", GetoptLong::NO_ARGUMENT],
|
35
|
+
["--daemon", "-d", GetoptLong::NO_ARGUMENT]
|
36
|
+
)
|
37
|
+
|
38
|
+
beanstalks = ["127.0.0.1:11300"]
|
39
|
+
@pid_file = '/var/run/racoond.pid'
|
40
|
+
@log_file = '/var/log/racoond.log'
|
41
|
+
daemon = false
|
42
|
+
|
43
|
+
opts.each do |opt, arg|
|
44
|
+
case opt
|
45
|
+
when '--help'
|
46
|
+
usage
|
47
|
+
exit 1
|
48
|
+
when '--beanstalk'
|
49
|
+
beanstalks = CSV.parse(arg)[0]
|
50
|
+
when '--pid'
|
51
|
+
@pid_file = arg
|
52
|
+
when '--log'
|
53
|
+
@log_file = arg
|
54
|
+
when '--daemon'
|
55
|
+
daemon = true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Racoon::Config.logger = Logger.new(@log_file)
|
60
|
+
|
61
|
+
daemonize if daemon
|
62
|
+
server = Racoon::Server.new(beanstalks) do |feedback_record|
|
63
|
+
Racoon::Config.logger.info "Received feedback at #{feedback_record[:feedback_at]} (length: #{feedback_record[:length]}): #{feedback_record[:device_token]}"
|
64
|
+
end
|
65
|
+
server.start!
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Racoon
|
5
|
+
class Client
|
6
|
+
attr_accessor :pem, :host, :port, :password
|
7
|
+
|
8
|
+
def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil)
|
9
|
+
@pem, @host, @port, @password = pem, host, port, pass
|
10
|
+
end
|
11
|
+
|
12
|
+
def connect!
|
13
|
+
raise "Your certificate is not set." unless self.pem
|
14
|
+
|
15
|
+
@context = OpenSSL::SSL::SSLContext.new
|
16
|
+
@context.cert = OpenSSL::X509::Certificate.new(self.pem)
|
17
|
+
@context.key = OpenSSL::PKey::RSA.new(self.pem, self.password)
|
18
|
+
|
19
|
+
@sock = TCPSocket.new(self.host, self.port.to_i)
|
20
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context)
|
21
|
+
@ssl.connect
|
22
|
+
|
23
|
+
return @sock, @ssl
|
24
|
+
end
|
25
|
+
|
26
|
+
def disconnect!
|
27
|
+
@ssl.close
|
28
|
+
@sock.close
|
29
|
+
@ssl = nil
|
30
|
+
@sock = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(notification)
|
34
|
+
Config.logger.debug "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}"
|
35
|
+
@ssl.write(notification.to_bytes)
|
36
|
+
end
|
37
|
+
|
38
|
+
def connected?
|
39
|
+
@ssl
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Feedback service
|
2
|
+
|
3
|
+
module Racoon
|
4
|
+
class FeedbackClient < Client
|
5
|
+
def initialize(pem, host = 'feedback.push.apple.com', port = 2196, pass = nil)
|
6
|
+
@pem, @host, @port, @pass = pem, host, port, pass
|
7
|
+
end
|
8
|
+
|
9
|
+
def read
|
10
|
+
records ||= []
|
11
|
+
while record = @ssl.read(38)
|
12
|
+
records << parse_tuple(record)
|
13
|
+
end
|
14
|
+
records
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse_tuple(data)
|
20
|
+
feedback = data.unpack("N1n1H*")
|
21
|
+
{ :feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'racoon/payload'
|
2
|
+
require 'base64'
|
3
|
+
require 'yajl'
|
4
|
+
|
5
|
+
module Racoon
|
6
|
+
class Notification
|
7
|
+
include Racoon::Payload
|
8
|
+
|
9
|
+
attr_accessor :device_token, :alert, :badge, :sound, :custom
|
10
|
+
|
11
|
+
def payload
|
12
|
+
p = Hash.new
|
13
|
+
[:badge, :alert, :sound, :custom].each do |k|
|
14
|
+
r = send(k)
|
15
|
+
p[k] = r if r
|
16
|
+
end
|
17
|
+
create_payload(p)
|
18
|
+
end
|
19
|
+
|
20
|
+
def json_payload
|
21
|
+
j = Yajl::Encoder.encode(payload)
|
22
|
+
raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256
|
23
|
+
j
|
24
|
+
end
|
25
|
+
|
26
|
+
=begin
|
27
|
+
def push
|
28
|
+
if Config.pem.nil?
|
29
|
+
socket = TCPSocket.new(Config.host || 'localhost', Config.port.to_i || 22195)
|
30
|
+
socket.write(to_bytes)
|
31
|
+
socket.close
|
32
|
+
else
|
33
|
+
client = Racoon::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195)
|
34
|
+
client.connect!
|
35
|
+
client.write(self)
|
36
|
+
client.disconnect!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
=end
|
40
|
+
|
41
|
+
def to_bytes
|
42
|
+
j = json_payload
|
43
|
+
[0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*")
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.valid?(p)
|
47
|
+
begin
|
48
|
+
Notification.parse(p)
|
49
|
+
rescue PayloadInvalid => p
|
50
|
+
Config.logger.error "PayloadInvalid: #{p}"
|
51
|
+
false
|
52
|
+
rescue RuntimeError => r
|
53
|
+
Config.logger.error "Runtime error: #{r}"
|
54
|
+
false
|
55
|
+
rescue Exception => e
|
56
|
+
Config.logger.error "Unknown error: #{e}"
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.parse(p)
|
62
|
+
buffer = p.dup
|
63
|
+
notification = Notification.new
|
64
|
+
|
65
|
+
header = buffer.slice!(0, 3).unpack('ccc')
|
66
|
+
if header[0] != 0
|
67
|
+
raise RuntimeError.new("Header of notification is invalid: #{header.inspect}")
|
68
|
+
end
|
69
|
+
|
70
|
+
# parse token
|
71
|
+
notification.device_token = buffer.slice!(0, 32).unpack('a*').first
|
72
|
+
|
73
|
+
# parse json payload
|
74
|
+
payload_len = buffer.slice!(0, 2).unpack('CC')
|
75
|
+
j = buffer.slice!(0, payload_len.last)
|
76
|
+
result = Yajl::Parser.parse(j)
|
77
|
+
|
78
|
+
['alert', 'badge', 'sound'].each do |k|
|
79
|
+
notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k]
|
80
|
+
end
|
81
|
+
result.delete('aps')
|
82
|
+
notification.custom = result
|
83
|
+
|
84
|
+
notification
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Racoon
|
2
|
+
module Payload
|
3
|
+
PayloadInvalid = Class.new(RuntimeError)
|
4
|
+
|
5
|
+
def create_payload(payload)
|
6
|
+
case payload
|
7
|
+
when String then { :aps => { :alert => payload } }
|
8
|
+
when Hash then create_payload_from_hash(payload)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_payload_from_hash(payload)
|
13
|
+
custom = payload.delete(:custom)
|
14
|
+
aps = {:aps => payload }
|
15
|
+
aps.merge!(custom) if custom
|
16
|
+
aps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'beanstalk-client'
|
2
|
+
|
3
|
+
module Racoon
|
4
|
+
class Server
|
5
|
+
attr_accessor :beanstalkd_uris, :feedback_callback
|
6
|
+
|
7
|
+
def initialize(beanstalkd_uris = ["127.0.0.1:11300"], &feedback_blk)
|
8
|
+
@beanstalks = {}
|
9
|
+
@clients = {}
|
10
|
+
@feedback_callback = feedback_blk
|
11
|
+
@beanstalkd_uris = beanstalkd_uris
|
12
|
+
end
|
13
|
+
|
14
|
+
def beanstalk(arg)
|
15
|
+
tube = "racoon-#{arg}"
|
16
|
+
return @beanstalks[tube] if @beanstalks[tube]
|
17
|
+
@beanstalks[tube] = Beanstalk::Pool.new @beanstalkd_uris
|
18
|
+
%w{watch use}.each { |s| @beanstalks[tube].send(s, "racoon-#{tube}") }
|
19
|
+
@beanstalks[tube].ignore('default')
|
20
|
+
@beanstalks[tube]
|
21
|
+
end
|
22
|
+
|
23
|
+
def start!
|
24
|
+
EventMachine::run do
|
25
|
+
EventMachine::PeriodicTimer.new(3600) do
|
26
|
+
begin
|
27
|
+
if beanstalk('feedback').peek_ready
|
28
|
+
item = beanstalk('feedback').reserve(1)
|
29
|
+
handle_feedback(item)
|
30
|
+
end
|
31
|
+
rescue Beanstalk::TimedOut
|
32
|
+
Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
EventMachine::PeriodicTimer.new(60) do
|
37
|
+
begin
|
38
|
+
if beanstalk('killer').peek_ready
|
39
|
+
item = beanstalk('killer').reserve(1)
|
40
|
+
purge_client(item)
|
41
|
+
end
|
42
|
+
rescue Beanstalk::TimedOut
|
43
|
+
Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
EventMachine::PeriodicTimer.new(1) do
|
48
|
+
begin
|
49
|
+
if beanstalk('apns').peek_ready
|
50
|
+
item = beanstalk('apns').reserve(1)
|
51
|
+
handle_job item
|
52
|
+
end
|
53
|
+
rescue Beanstalk::TimedOut
|
54
|
+
Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out."
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Received a notification. job is YAML encoded hash in the following format:
|
63
|
+
# job = {
|
64
|
+
# :project => {
|
65
|
+
# :name => "Foo",
|
66
|
+
# :certificate => "contents of a certificate.pem"
|
67
|
+
# },
|
68
|
+
# :device_token => "0f21ab...def",
|
69
|
+
# :notification => notification.json_payload,
|
70
|
+
# :sandbox => true # Development environment?
|
71
|
+
# }
|
72
|
+
def handle_job(job)
|
73
|
+
packet = job.ybody
|
74
|
+
project = packet[:project]
|
75
|
+
|
76
|
+
aps = packet[:notification][:aps]
|
77
|
+
|
78
|
+
notification = Notification.new
|
79
|
+
notification.device_token = packet[:device_token]
|
80
|
+
notification.badge = aps[:badge] if aps.has_key? :badge
|
81
|
+
notification.alert = aps[:alert] if aps.has_key? :alert
|
82
|
+
notification.sound = aps[:sound] if aps.has_key? :sound
|
83
|
+
notification.custom = aps[:custom] if aps.has_key? :custom
|
84
|
+
|
85
|
+
if notification
|
86
|
+
client = get_client(project[:name], project[:certificate], packet[:sandbox])
|
87
|
+
begin
|
88
|
+
client.connect! unless client.connected?
|
89
|
+
client.write(notification)
|
90
|
+
|
91
|
+
job.delete
|
92
|
+
rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET
|
93
|
+
Config.logger.error "Caught Error, closing connecting and adding notification back to queue"
|
94
|
+
|
95
|
+
client.disconnect!
|
96
|
+
|
97
|
+
# Queue back up the notification
|
98
|
+
job.release
|
99
|
+
rescue RuntimeError => e
|
100
|
+
Config.logger.error "Unable to handle: #{e}"
|
101
|
+
|
102
|
+
job.delete
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Will be a hash with two keys:
|
108
|
+
# :certificate and :sandbox.
|
109
|
+
def handle_feedback(job)
|
110
|
+
begin
|
111
|
+
packet = job.ybody
|
112
|
+
uri = "feedback.#{packet[:sandbox] ? 'sandbox.' : ''}push.apple.com"
|
113
|
+
feedback_client = Racoon::FeedbackClient.new(packet[:certificate], uri)
|
114
|
+
feedback_client.connect!
|
115
|
+
feedback_client.read.each do |record|
|
116
|
+
feedback_callback.call record
|
117
|
+
end
|
118
|
+
feedback_client.disconnect!
|
119
|
+
job.delete
|
120
|
+
rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET
|
121
|
+
Config.logger.error "(Feedback) Caught Error, closing connection"
|
122
|
+
feedback_client.disconnect!
|
123
|
+
job.release
|
124
|
+
rescue RuntimeError => e
|
125
|
+
Config.logger.error "(Feedback) Unable to handle: #{e}"
|
126
|
+
job.delete
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_client(project_name, certificate, sandbox = false)
|
131
|
+
uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com"
|
132
|
+
unless @clients[project_name]
|
133
|
+
@clients[project_name] = Racoon::Client.new(certificate, uri)
|
134
|
+
# in 18 hours (64800 seconds) we need to schedule this socket to be killed. Long opened
|
135
|
+
# sockets don't work.
|
136
|
+
beanstalk('killer').yput({:certificate => certificate, :sandbox => sandbox}, 65536, 64800)
|
137
|
+
end
|
138
|
+
@clients[project_name] ||= Racoon::Client.new(certificate, uri)
|
139
|
+
client = @clients[project_name]
|
140
|
+
|
141
|
+
# If the certificate has changed, but we still are connected using the old certificate,
|
142
|
+
# disconnect and reconnect.
|
143
|
+
unless client.pem.eql?(certificate)
|
144
|
+
client.disconnect! if client.connected?
|
145
|
+
@clients[project_name] = Racoon::Client.new(certificate, uri)
|
146
|
+
client = @clients[project_name]
|
147
|
+
end
|
148
|
+
|
149
|
+
client
|
150
|
+
end
|
151
|
+
|
152
|
+
def purge_client(job)
|
153
|
+
project_name = job.ybody
|
154
|
+
client = @clients[project_name]
|
155
|
+
client.disconnect! if client
|
156
|
+
@clients[project_name] = nil
|
157
|
+
job.delete
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/racoon.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe Client do
|
5
|
+
describe "#new" do
|
6
|
+
let(:client) { ApnServer::Client.new('cert.pem', 'gateway.sandbox.push.apple.com', 2196) }
|
7
|
+
|
8
|
+
it "sets the pem path" do
|
9
|
+
client.pem.should == 'cert.pem'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sets the host" do
|
13
|
+
client.host.should == 'gateway.sandbox.push.apple.com'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "sets the port" do
|
17
|
+
client.port.should == 2196
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe FeedbackClient do
|
5
|
+
describe "#new" do
|
6
|
+
let(:feedback_client) { ApnServer::FeedbackClient.new('cert.pem', 'feedback.sandbox.push.apple.com', 2196) }
|
7
|
+
|
8
|
+
it "sets the pem path" do
|
9
|
+
feedback_client.pem.should == 'cert.pem'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sets the host" do
|
13
|
+
feedback_client.host.should == 'feedback.sandbox.push.apple.com'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "sets the port" do
|
17
|
+
feedback_client.port.should == 2196
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe Notification do
|
5
|
+
let(:notification) { Notification.new }
|
6
|
+
|
7
|
+
describe "#to_bytes" do
|
8
|
+
it "generates a byte array" do
|
9
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
10
|
+
device_token = "12345678123456781234567812345678"
|
11
|
+
notification.device_token = device_token
|
12
|
+
notification.alert = "You have not mail!"
|
13
|
+
expected = [0, 0, device_token.size, device_token, 0, payload.size, payload]
|
14
|
+
notification.to_bytes.should == expected.pack("ccca*CCa*")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#payload" do
|
19
|
+
it "generates the badge element" do
|
20
|
+
expected = { :aps => { :badge => 1 }}
|
21
|
+
notification.badge = 1
|
22
|
+
notification.payload.should == expected
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates the alert alement" do
|
26
|
+
expected = { :aps => { :alert => 'Hi' }}
|
27
|
+
notification.alert = 'Hi'
|
28
|
+
notification.payload.should == expected
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#json_payload" do
|
33
|
+
it "converts payload to json" do
|
34
|
+
expected = '{"aps":{"alert":"Hi"}}'
|
35
|
+
notification.alert = 'Hi'
|
36
|
+
notification.json_payload.should == expected
|
37
|
+
end
|
38
|
+
|
39
|
+
it "does not allow payloads larger than 256 chars" do
|
40
|
+
lambda {
|
41
|
+
alert = []
|
42
|
+
256.times { alert << 'Hi' }
|
43
|
+
notification.alert = alert.join
|
44
|
+
notification.json_payload
|
45
|
+
}.should raise_error(Payload::PayloadInvalid)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#valid?" do
|
50
|
+
it "recognizes a valid request" do
|
51
|
+
device_token = '12345678123456781234567812345678'
|
52
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
53
|
+
request = [0, 0, device_token.size, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
|
54
|
+
Notification.valid?(request).should be_true
|
55
|
+
notification = Notification.parse(request)
|
56
|
+
notification.device_token.should == device_token
|
57
|
+
notification.alert.should == "You have not mail!"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "recognizes an invalid request" do
|
61
|
+
device_token = '123456781234567812345678'
|
62
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
63
|
+
request = [0, 0, 32, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
|
64
|
+
Notification.valid?(request).should be_false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#parse" do
|
69
|
+
it "reads a byte array and constructs a notification" do
|
70
|
+
device_token = '12345678123456781234567812345678'
|
71
|
+
notification.device_token = device_token
|
72
|
+
notification.badge = 10
|
73
|
+
notification.alert = 'Hi'
|
74
|
+
notification.sound = 'default'
|
75
|
+
notification.custom = { 'acme1' => "bar", 'acme2' => 42}
|
76
|
+
|
77
|
+
parsed = Notification.parse(notification.to_bytes)
|
78
|
+
[:device_token, :badge, :alert, :sound, :custom].each do |k|
|
79
|
+
expected = notification.send(k)
|
80
|
+
parsed.send(k).should == expected
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe Payload do
|
5
|
+
describe "#payload.create_payload" do
|
6
|
+
let(:payload) { Class.new.send(:include, Payload).new }
|
7
|
+
|
8
|
+
it "creates a payload_with_simple_string" do
|
9
|
+
payload.create_payload('Hi').should == { :aps => { :alert => 'Hi' }}
|
10
|
+
end
|
11
|
+
|
12
|
+
it "creates a payload_with_alert_key" do
|
13
|
+
payload.create_payload(:alert => 'Hi').should == { :aps => { :alert => 'Hi' }}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "creates payload with badge_and_alert" do
|
17
|
+
payload.create_payload(:alert => 'Hi', :badge => 1).should == { :aps => { :alert => 'Hi', :badge => 1 }}
|
18
|
+
end
|
19
|
+
|
20
|
+
# example 1
|
21
|
+
it "test_should_payload.create_payload_with_custom_payload" do
|
22
|
+
alert = 'Message received from Bob'
|
23
|
+
payload.create_payload(:alert => alert, :custom => { :acme2 => ['bang', 'whiz']}).should == {
|
24
|
+
:aps => { :alert => alert },
|
25
|
+
:acme2 => [ "bang", "whiz" ]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# example 3
|
30
|
+
it "test_should_payload.create_payload_with_sound_and_multiple_custom" do
|
31
|
+
expected = {
|
32
|
+
:aps => {
|
33
|
+
:alert => "You got your emails.",
|
34
|
+
:badge => 9,
|
35
|
+
:sound => "bingbong.aiff"
|
36
|
+
},
|
37
|
+
:acme1 => "bar",
|
38
|
+
:acme2 => 42
|
39
|
+
}
|
40
|
+
payload.create_payload({
|
41
|
+
:alert => "You got your emails.",
|
42
|
+
:badge => 9,
|
43
|
+
:sound => "bingbong.aiff",
|
44
|
+
:custom => { :acme1 => "bar", :acme2 => 42}
|
45
|
+
}).should == expected
|
46
|
+
end
|
47
|
+
|
48
|
+
# example 5
|
49
|
+
it "test_should_payload.create_payload_with_empty_aps" do
|
50
|
+
payload.create_payload(:custom => { :acme2 => [ 5, 8 ] }).should == {
|
51
|
+
:aps => {},
|
52
|
+
:acme2 => [ 5, 8 ]
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
# Set up gems listed in the Gemfile.
|
4
|
+
gemfile = File.expand_path('../Gemfile', File.dirname(__FILE__))
|
5
|
+
begin
|
6
|
+
ENV['BUNDLE_GEMFILE'] = gemfile
|
7
|
+
require 'bundler'
|
8
|
+
Bundler.setup
|
9
|
+
rescue Bundler::GemNotFound => e
|
10
|
+
STDERR.puts e.message
|
11
|
+
STDERR.puts "Try running `bundle install`."
|
12
|
+
exit!
|
13
|
+
end
|
14
|
+
|
15
|
+
Bundler.require(:spec)
|
16
|
+
|
17
|
+
Rspec.configure do |config|
|
18
|
+
config.mock_with :rspec
|
19
|
+
end
|
20
|
+
# Requires supporting files with custom matchers and macros, etc,
|
21
|
+
# in ./support/ and its subdirectories.
|
22
|
+
|
23
|
+
require "apnserver"
|
24
|
+
require 'base64'
|
25
|
+
|
26
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: racoon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeremy Tregunna
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-04-23 00:00:00.000000000 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: yajl-ruby
|
17
|
+
requirement: &70118651237540 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.7.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70118651237540
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: beanstalk-client
|
28
|
+
requirement: &70118651237080 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70118651237080
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: bundler
|
39
|
+
requirement: &70118651236620 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.0.0
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70118651236620
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: eventmachine
|
50
|
+
requirement: &70118651236160 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.12.8
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *70118651236160
|
59
|
+
description: A toolkit for proxying and sending Apple Push Notifications prepared
|
60
|
+
for a hosted environment
|
61
|
+
email: jeremy.tregunna@me.com
|
62
|
+
executables:
|
63
|
+
- racoond
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files:
|
66
|
+
- README.mdown
|
67
|
+
files:
|
68
|
+
- bin/apnsend
|
69
|
+
- bin/apnserverd.fedora.init
|
70
|
+
- bin/apnserverd.ubuntu.init
|
71
|
+
- bin/racoond
|
72
|
+
- lib/racoon/client.rb
|
73
|
+
- lib/racoon/config.rb
|
74
|
+
- lib/racoon/feedback_client.rb
|
75
|
+
- lib/racoon/notification.rb
|
76
|
+
- lib/racoon/payload.rb
|
77
|
+
- lib/racoon/server.rb
|
78
|
+
- lib/racoon.rb
|
79
|
+
- README.mdown
|
80
|
+
- spec/models/client_spec.rb
|
81
|
+
- spec/models/feedback_client_spec.rb
|
82
|
+
- spec/models/notification_spec.rb
|
83
|
+
- spec/models/payload_spec.rb
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
has_rdoc: true
|
86
|
+
homepage: https://github.com/jeremytregunna/racoon
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options:
|
90
|
+
- --charset=UTF-8
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 1.3.6
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project: racoon
|
107
|
+
rubygems_version: 1.6.2
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: Apple Push Notification Toolkit for hosted environments
|
111
|
+
test_files:
|
112
|
+
- spec/models/client_spec.rb
|
113
|
+
- spec/models/feedback_client_spec.rb
|
114
|
+
- spec/models/notification_spec.rb
|
115
|
+
- spec/models/payload_spec.rb
|
116
|
+
- spec/spec_helper.rb
|