racoon 0.3.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.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
|