diff --git a/Gemfile b/Gemfile index bf16f02..9f06b0e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,12 @@ source "http://rubygems.org" -gem "eventmachine" gem "daemons" -gem "activesupport", ">= 3.0.0" -gem "i18n" # active support whines without this +gem "yajl-ruby" +gem "beanstalk-client" +gem "ffi" +gem "ffi-rzmq" +gem "zmqmachine", :git => "git://github.com/jeremytregunna/zmqmachine.git" group :spec do gem "rspec" -end \ No newline at end of file +end diff --git a/Gemfile.lock b/Gemfile.lock index 408394e..3af895c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,20 @@ +GIT + remote: git://github.com/jeremytregunna/zmqmachine.git + revision: fb39523eafa41127d3042f2bbe65c92fc451d412 + specs: + zmqmachine (0.4.0) + ffi-rzmq (>= 0.7.0) + GEM remote: http://rubygems.org/ specs: - activesupport (3.0.3) + beanstalk-client (1.1.0) daemons (1.0.10) diff-lcs (1.1.2) - eventmachine (0.12.10) - i18n (0.5.0) + ffi (1.0.7) + rake (>= 0.8.7) + ffi-rzmq (0.8.0) + rake (0.8.7) rspec (2.3.0) rspec-core (~> 2.3.0) rspec-expectations (~> 2.3.0) @@ -14,13 +23,16 @@ GEM rspec-expectations (2.3.0) diff-lcs (~> 1.1.2) rspec-mocks (2.3.0) + yajl-ruby (0.8.1) PLATFORMS ruby DEPENDENCIES - activesupport (>= 3.0.0) + beanstalk-client daemons - eventmachine - i18n + ffi + ffi-rzmq rspec + yajl-ruby + zmqmachine! diff --git a/README.mdown b/README.mdown new file mode 100644 index 0000000..9952c3c --- /dev/null +++ b/README.mdown @@ -0,0 +1,201 @@ +# Racoon push notification server + +This project started off as a fork of [apnserver](https://github.com/bpoweski/apnserver). It +has since taken on a different path. How does it differ from apnserver? By a few key points: + +1. It implements the APNS feedback service; +2. Uses Yajl for JSON encoding/decoding rather than ActiveSupport; +3. Expects certificates as strings instead of paths to files; +4. Does not assume there is only one certificate (read: supports multiple projects); and +5. Receives packets containing notifications from beanstalkd instead of a listening socket; +6. Operates on a distributed architecture (many parallel workers, one firehose). + +The above changes were made because of the need to replace an existing APNs provider with something +more robust, and better suited to scaling upwards. This APNs provider had a couple requirements: + +1. Support fully the APNs protocol (including feedback) +2. Scale outwards horizontally +3. Support multiple projects + +It should be noted that the development of this project is independent of the work bpoweski +is doing on apnserver. If you're looking for that project, [go here](https://github.com/bpoweski/apnserver). + +## BIT FAT DEPENDENCY WARNING + +Since version 0.6.0, Racoon uses ZMQMachine. However, the mainline ZMQMachine does not support PUSH/PULL +sockets as of the time I wrote this (probably still the same way, else I'd have removed this by now). +As such, I strongly urge you to [fork this repository](https://github.com/jeremytregunna/zmqmachine) and install it before installing racoon. + +## Description + +Racoon consists of a firehose, which maintains the connections to Apple's APNs service. It also +consists of a worker, which works on a beanstalk tube to pop notifications off and process them, +before sending them off to the firehose. You can run many workers, they all run in parallel to +one another. Additionally, it includes a command line tool to send (test) the system. + +At this time, Racoon only supports Apple's APNs service. + +## Remaining Tasks & Issues + +You can see progress by looking at the [issue tracker](https://www.pivotaltracker.com/projects/279053). + +## Preparing Certificates + +Certificates must be prepared before they can be used with racoon. Unfortunately, Apple +gives us *.p12* files instead of *.pem* files. As such, we need to convert them. This +can be accomplished by dropping to the command line and running this command: + +
+$ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
+
+ +This will generate a file suitable for use with this daemon, called *cert.pem*. If you're +using frac.as, this is the file you would upload to the web service. + +If you're not using frac.as, then the contents of this file are what you need to use as +your certificate, not the path to the file. + +## Firehose + +The firehose sits at the end of the pipeline. All the workers deliver messages to this +part of racoon. Think of it as the drain in your sink. + +
+Usage: racoon-firehose [switches]
+ --pid    the path to store the pid
+ --log   the path to store the log
+ --daemon                               to daemonize the server
+ --help                                 this message
+
+ +## Worker + +The worker is the part of the system which interacts with a beanstalk cluster. Each worker +can talk to more than one beanstalk server, thus forming the "cluster". Since beanstalk +clustering is all done client side, it's not a traditional cluster, but I'm going to call +it that way anyway. + +The worker pops items that are ready off of the beanstalk cluster. It grabs those packets, +constructs a message out of it, and then forms another packet, suitable for sending to +Apple, before sending that packet to the firehose. + +You may run as many workers as your little heart desires. Keep in mind however, there will +come a point when having only one firehose is not suitable to handle the amount of traffic +you will be passing through from the workers. At this point, a second firehose will need +to be started. I doubt anyone will ever be processing enough messages a second to warrant +a second firehose. + +
+Usage: racoon-worker [switches]
+ --beanstalk <127.0.0.1:11300>        csv list of ip:port for beanstalk servers
+ --pid    the path to store the pid
+ --log    the path to store the log
+ --daemon                             to daemonize the server
+ --help                               this message
+
+ +It should be noted that the worker's `--beanstalk` parameter requires comma separated values +in `IP:port` pairings. Where `IP` is either an IP address, or a hostname to a host running a +beanstalkd server on the associated port `port`. In the future, I will create a config file +instead of having to specify this information on the command line each time. My apologies for +the inconvenience. + +## Sender + +The sender is the program used to form a packet, place it on beanstalk for racoon to consume. +It is useful during testing. + +
+Usage: racoon-send [switches] (--b64-token | --hex-token) 
+ --beanstalk <127.0.0.1:11300>        csv of ip:port for beanstalk servers
+ --pem                          the path to the pem file, if a pem is supplied the server defaults to gateway.push.apple.com:2195
+ --alert                     the message to send
+ --sound                     the sound to play, defaults to 'default'
+ --badge                      the badge number
+ --custom                a custom json string to be added to the main object
+ --b64-token                   a base 64 encoded device token
+ --hex-token                   a hex encoded device token
+ --help                               this message
+
+ +## Preparing packets to place on beanstalk + +Until an API can be built that you can tie into with your own applications, please take care +to construct your notifications as YAML payload with the following format. I will use ruby +syntax for this example: + +```ruby +{ + :project => { :name => "My awesome app", :certificate => "...", :sandbox => true }, + :identifier => 12345, + :notification => { :aps => { :alert => "text", + :sound => "default", + :badge => 1, + :custom => { ... } + } + }, + :device_token => "..." +} +``` + +A few key points need to be raised here. For starters, the `sandbox` key should only be true if +you desire to work in the sandbox, and your `certificate` contains the text of your development +certificate. + +Secondly, the `identifier` must be a unique 32-bit number identifying your message should you +choose to want useful error messages. (This feature is not presently written, as such, you can +supply anything you want, just make sure it falls within the range of `0` to `4294967295`. + +The `notification` key represents the payload we'll send to Apple. The `custom` key must be +present if you intend to send custom data. Note however, that `custom` will be removed, and the +items you place in its hash will be substituted in with the payload when the message is passed +to Apple. As such, if you want a custom key -> value pair of: `"foo" => "bar"`, you would ensure +you have: `:custom => { "foo" => "bar" }` in the notification. Your application should just look +for "foo" in the payload delivered to the app. + +Finally, the ```device_token``` is a binary encoded representation of your devices token. Your +app gets it in hex, please ensure you convert it to binary before sending it to beanstalk. + +TODO: Document new way of handling scheduling notifications in the future. Abstract beanstalk +away from users of the library, give them a proper client interface to use instead. + +## Installation + +Racoon is hosted on [rubygems](https://rubygems.org/gems/racoon) + +
+$ gem install racoon
+
+ +Adding racoon to your Rails application + +```ruby +gem 'racoon' +``` + +## License + +(The MIT License) + +Copyright (c) 2011 Jeremy Tregunna +Copyright (c) 2011 Ben Poweski + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.textile b/README.textile deleted file mode 100644 index 8546420..0000000 --- a/README.textile +++ /dev/null @@ -1,175 +0,0 @@ -h1. Apple Push Notification Server Toolkit - -* http://github.com/bpoweski/apnserver - -h2. Description - -apnserver is a server and set of command line programs to send push notifications to the iPhone. -Apple recommends to maintain an open connection to the push notification service and refrain from -opening up and tearing down SSL connections repeated. To solve this problem an intermediate -network server is introduced that queues are requests to the APN service and sends them via a -persistent connection. - -h2. Remaining Tasks - - * Implement feedback service mechanism - * Implement robust notification sending in reactor periodic scheduler - -h2. Issues Fixed - - * second attempt at retry logic, SSL Errors close down sockets now - * apnsend --badge option correctly sends integer number rather than string of number for aps json payload - * connections are properly closed in Notification#push method now - * removed json gem in favor of ActiveSupport - * Rails 3.x support - * drop the erroneous puts statements in favor a configurable logger - * moved to Rspec - -h2. APN Server Daemon - -
-  
-Usage: apnserverd [options] --pem /path/to/pem
-  --bind-address bind address (defaults to 0.0.0.0)
-    bind address of the server daemon
-
-  --proxy-port port
-    the port that the daemon will listen on (defaults to 22195)
-
-  --server server
-    APN Server (defaults to gateway.push.apple.com)
-
-  --port port of the APN Server
-    APN server port (defaults to 2195)
-
-  --pem pem file path
-    The PEM encoded private key and certificate.
-    To export a PEM ecoded file execute
-    # openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
-
-  --pem-passphrase passphrase
-    The PEM passphrase to decode key.
-
-  --help
-    usage message
-
-  --daemon
-    Runs process as daemon, not available on Windows
-  
-
- -h2. APN Server Client - -With the APN server client script you can send push notifications directly to -Apple's APN server over an SSL connection or to the above daemon using a plain socket. -To send a notification to Apple's APN server using SSL the *--pem* option must be used. - -
-  
-Usage: apnsend [switches] (--b64-token | --hex-token) 
-  --server                  the apn server defaults to a locally running apnserverd
-  --port <2195>                        the port of the apn server
-  --pem                          the path to the pem file, if a pem is supplied the server
-                                       defaults to gateway.push.apple.com:2195
-  --pem-passphrase         the pem passphrase
-  --alert                     the message to send"
-  --sound                     the sound to play, defaults to 'default'
-  --badge                      the badge number
-  --custom                a custom json string to be added to the main object
-  --b64-token                   a base 64 encoded device token
-  --hex-token                   a hex encoded device token
-  --help                               this message
-  
-
- -To send a base64 encoded push notification via the command line execute the following: - -
-  
-  $ apnsend --server gateway.push.apple.com --port 2195 --pem key.pem \
-     --b64-token j92f12jh8lqcAwcOVeSIrsBxibaJ0xyCi8/AkmzNlk8= --sound default \
-     --alert Hello
-  
-
- -h2. Sending Notifications from Ruby - -To configure the client to send to the local apnserverd process configure the ApnServer client with the following. - -
-  
-  # configured for a using the apnserverd proxy
-  ApnServer::Config.host = 'localhost'
-  ApnServer::Config.port = 22195
-  ApnServer::Config.logger = Rails.logger
-  
-
- -To configure the client to send directly to Apple's push notification server, bypassing the apnserverd process configure the following. - -
-  
-  ApnServer::Config.pem = '/path/to/pem'
-  ApnServer::Config.host = 'gateway.push.apple.com'
-  ApnServer::Config.port = 2195
-  
-
- -Finally within we can send a push notification using the following code - -
-  
-  notification = ApnServer::Notification.new
-  notification.device_token = Base64.decode64(apns_token) # if base64 encoded
-  notification.alert = message
-  notification.badge = 1
-  notification.sound = 'default'
-  notification.push
-  
-
- - -h2. Installation - -Apnserver is hosted on "rubygems":https://rubygems.org/gems/apnserver - -
-
-  $ gem install apnserver
-
-
- -Adding apnserver to your Rails application - -
-  
-  gem 'apnserver', '>= 0.2.0
-  
-
- - -h2. License - -(The MIT License) - -Copyright (c) 2011 Ben Poweski - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/Rakefile b/Rakefile index 279fcde..4bd6dac 100644 --- a/Rakefile +++ b/Rakefile @@ -2,9 +2,20 @@ Dir['tasks/**/*.rake'].each { |t| load t } require 'rake/rdoctask' Rake::RDocTask.new do |rd| - rd.main = "README.textile" + rd.main = "README.mdown" rd.rdoc_dir = 'rdoc' - rd.rdoc_files.include("README.textile", "lib/**/*.rb") + rd.rdoc_files.include("README.mdown", "lib/**/*.rb") +end + +gemspec = eval(File.read("racoon.gemspec")) +task :build => "#{gemspec.full_name}.gem" +file "#{gemspec.full_name}.gem" => gemspec.files + ["racoon.gemspec"] do + system "gem build racoon.gemspec" + system "gem install racoon-#{gemspec.version}.gem" +end + +task :submit_gem => "racoon-#{gemspec.version}.gem" do + system "gem push racoon-#{gemspec.version}.gem" end task :default => [:spec] diff --git a/apnserver.gemspec b/apnserver.gemspec deleted file mode 100644 index e872c70..0000000 --- a/apnserver.gemspec +++ /dev/null @@ -1,25 +0,0 @@ -Gem::Specification.new do |s| - s.name = %q{apnserver} - s.version = "0.2.1" - s.platform = Gem::Platform::RUBY - - s.authors = ["Ben Poweski"] - s.date = %q{2011-01-01} - s.description = %q{A toolkit for proxying and sending Apple Push Notifications} - s.email = %q{bpoweski@3factors.com} - s.executables = ["apnsend", "apnserverd"] - s.extra_rdoc_files = ["README.textile"] - s.files = Dir.glob("{bin,lib}/**/*") + %w(README.textile) - s.homepage = %q{http://github.com/bpoweski/apnserver} - s.rdoc_options = ["--charset=UTF-8"] - s.require_paths = ["lib"] - s.rubyforge_project = %q{apnserver} - s.rubygems_version = %q{1.3.5} - s.summary = %q{Apple Push Notification Toolkit} - s.test_files = Dir.glob("spec/**/*") - - s.required_rubygems_version = ">= 1.3.6" - s.add_dependency 'activesupport', '~> 3.0.0' - s.add_development_dependency 'bundler', '~> 1.0.0' - s.add_development_dependency 'eventmachine', '>= 0.12.8' -end diff --git a/bin/apnserverd b/bin/apnserverd deleted file mode 100755 index 82119ba..0000000 --- a/bin/apnserverd +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env ruby -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) - -require 'getoptlong' -require 'rubygems' -require 'daemons' -require 'apnserver' -require 'apnserver/server' - -def usage - puts "Usage: apnserverd [switches] --pem " - puts " --pem-passphrase pem passphrase" - puts " --bind-address [0.0.0.0] bind address of proxy" - puts " --proxy-port [22195] port proxy listens on" - puts " --server the apn server to send messages to" - puts " --port <2195> the port of the apn server" - puts " --pid - -PATH=/sbin:/usr/sbin:/bin:/usr/bin -DESC="Apple Push Notification Server Daemon" -NAME=apnserverd -DAEMON=/usr/bin/$NAME -PEMPATH="" -DAEMON_ARGS="--daemon --pem $PEMPATH" -PIDFILE=/var/run/$NAME.pid -SCRIPTNAME=/etc/init.d/$NAME - -# Exit if the package is not installed -[ -x "$DAEMON" ] || exit 0 - -# Load the VERBOSE setting and other rcS variables -. /lib/init/vars.sh - -# Define LSB log_* functions. -# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. -. /lib/lsb/init-functions - -# -# Function that starts the daemon/service -# -do_start() -{ - # Return - # 0 if daemon has been started - # 1 if daemon was already running - # 2 if daemon could not be started - start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ - || return 1 - start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ - $DAEMON_ARGS \ - || return 2 -} - -# -# Function that stops the daemon/service -# -do_stop() -{ - # Return - # 0 if daemon has been stopped - # 1 if daemon was already stopped - # 2 if daemon could not be stopped - # other if a failure occurred - start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME - RETVAL="$?" - [ "$RETVAL" = 2 ] && return 2 - # Wait for children to finish too if this is a daemon that forks - # and if the daemon is only ever run from this initscript. - # If the above conditions are not satisfied then add some other code - # that waits for the process to drop all resources that could be - # needed by services started subsequently. A last resort is to - # sleep for some time. - start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON - [ "$?" = 2 ] && return 2 - # Many daemons don't delete their pidfiles when they exit. - rm -f $PIDFILE - return "$RETVAL" -} - - -case "$1" in - start) - [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" - do_start - case "$?" in - 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; - esac - ;; - stop) - [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" - do_stop - case "$?" in - 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; - esac - ;; - restart|force-reload) - log_daemon_msg "Restarting $DESC" "$NAME" - do_stop - case "$?" in - 0|1) - do_start - case "$?" in - 0) log_end_msg 0 ;; - 1) log_end_msg 1 ;; # Old process is still running - *) log_end_msg 1 ;; # Failed to start - esac - ;; - *) - # Failed to stop - log_end_msg 1 - ;; - esac - ;; - *) - echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 - exit 3 - ;; -esac - -: diff --git a/bin/racoon-firehose b/bin/racoon-firehose new file mode 100755 index 0000000..9ab67bf --- /dev/null +++ b/bin/racoon-firehose @@ -0,0 +1,83 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'getoptlong' +require 'rubygems' +require 'daemons' +require 'racoon' +require 'csv' +require 'yaml' +require 'zmqmachine' + +def usage + puts "Usage: racoon-firehose [switches]" + puts " --pid the path to store the pid" + puts " --log the path to store the log" + puts " --daemon to daemonize the server" + puts " --help this message" +end + +def daemonize + Daemonize.daemonize(@log_file, 'racoon-firehose') + open(@pid_file,"w") { |f| f.write(Process.pid) } + open(@pid_file,"w") do |f| + f.write(Process.pid) + File.chmod(0644, @pid_file) + end +end + +opts = GetoptLong.new( + ["--pid", "-i", GetoptLong::REQUIRED_ARGUMENT], + ["--log", "-l", GetoptLong::REQUIRED_ARGUMENT], + ["--help", "-h", GetoptLong::NO_ARGUMENT], + ["--daemon", "-d", GetoptLong::NO_ARGUMENT] +) + +@pid_file = '/var/run/racoon-firehose.pid' +@log_file = '/var/log/racoon-firehose.log' +daemon = false + +opts.each do |opt, arg| + case opt + when '--help' + usage + exit 1 + when '--pid' + @pid_file = arg + when '--log' + @log_file = arg + when '--daemon' + daemon = true + end +end + +Racoon::Config.logger = Logger.new(@log_file) + +if daemon + daemonize +else + puts "Starting racoon firehose." +end + +ZM::Reactor.new(:firehose).run do |context| + firehose = Racoon::Firehose.new(context) do |feedback_record| + Racoon::Config.logger.info "[Feedback] Time: #{feedback_record[:feedback_at]} Device: #{feedback_record[:device_token]} Length: #{feedback_record[:length]}" + end + context.pull_socket(firehose) + + context.periodical_timer(28800) do + firehose.connections.each_pair do |key, data| + begin + project = data[:project] + uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" + feedback = Racoon::APNS::FeedbackConnection.new(data[:certificate], uri) + feedback.connect! + feedback.read.each do |record| + @feedback_callback.call(record) if @feedback_callback + end + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + feedback.disconnect! + end + end + end +end.join diff --git a/bin/apnsend b/bin/racoon-send similarity index 58% rename from bin/apnsend rename to bin/racoon-send index 5e55e6c..8fbd10e 100755 --- a/bin/apnsend +++ b/bin/racoon-send @@ -4,16 +4,18 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'logger' require 'getoptlong' require 'rubygems' -require 'apnserver' +require 'racoon' require 'base64' require 'socket' +require 'yajl' +require 'csv' +require 'beanstalk-client' def usage - puts "Usage: apnsend [switches] (--b64-token | --hex-token) " - puts " --server the apn server defaults to a locally running apnserverd" - puts " --port <2195> the port of the apn server" + puts "Usage: racoon-send [switches] (--b64-token | --hex-token) " + puts " --beanstalk <127.0.0.1:11300> csv of ip:port for beanstalk servers" + puts " --tube the beanstalk tube to use" puts " --pem the path to the pem file, if a pem is supplied the server defaults to gateway.push.apple.com:2195" - puts " --pem-passphrase the pem passphrase" puts " --alert the message to send" puts " --sound the sound to play, defaults to 'default'" puts " --badge the badge number" @@ -24,34 +26,34 @@ def usage end opts = GetoptLong.new( - ["--server", "-s", GetoptLong::REQUIRED_ARGUMENT], - ["--port", "-p", GetoptLong::REQUIRED_ARGUMENT], + ["--beanstalk", "-b", GetoptLong::REQUIRED_ARGUMENT], + ["--tube", "-t", GetoptLong::REQUIRED_ARGUMENT], ["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT], - ["--pem-passphrase", "-C", GetoptLong::REQUIRED_ARGUMENT], ["--alert", "-a", GetoptLong::REQUIRED_ARGUMENT], ["--sound", "-S", GetoptLong::REQUIRED_ARGUMENT], - ["--badge", "-b", GetoptLong::REQUIRED_ARGUMENT], + ["--badge", "-n", GetoptLong::REQUIRED_ARGUMENT], ["--custom", "-j", GetoptLong::REQUIRED_ARGUMENT], ["--b64-token", "-B", GetoptLong::REQUIRED_ARGUMENT], ["--hex-token", "-H", GetoptLong::REQUIRED_ARGUMENT], ["--help", "-h", GetoptLong::NO_ARGUMENT] ) -notification = ApnServer::Notification.new +beanstalks = ["127.0.0.1:11300"] +tube = 'racoon' +certificate = nil +notification = Racoon::Notification.new opts.each do |opt, arg| case opt when '--help' usage exit - when '--server' - ApnServer::Config.host = arg - when '--port' - ApnServer::Config.port = arg.to_i + when '--beanstalk' + beanstalks = CSV.parse(arg)[0] + when '--tube' + tube = arg when '--pem' - ApnServer::Config.pem = arg - when '--pem-passphrase' - ApnServer::Config.password = arg + certificate = File.read(arg) when '--alert' notification.alert = arg when '--sound' @@ -59,11 +61,11 @@ opts.each do |opt, arg| when '--badge' notification.badge = arg.to_i when '--custom' - notification.custom = ActiveSupport::JSON.decode(arg) + notification.custom = Yajl::Parser.parse(arg) when '--b64-token' notification.device_token = Base64::decode64(arg) when '--hex-token' - notification.device_token = arg.scan(/[0-9a-f][0-9a-f]/).map {|s| s.hex.chr}.join + notification.device_token = arg.scan(/[0-9a-f][0-9a-f]/).map {|s| s.hex.chr}.join end end @@ -71,5 +73,14 @@ if notification.device_token.nil? usage exit else - notification.push + bs = Beanstalk::Pool.new beanstalks + %w{use watch}.each { |s| bs.send(s, tube) } + bs.ignore("default") + project = { :name => "test", :certificate => certificate, :sandbox => true } + notif = { :project => project, + :identifier => 1, + :notification => notification.payload, + :device_token => notification.device_token, + } + bs.yput(notif) end diff --git a/bin/racoon-worker b/bin/racoon-worker new file mode 100755 index 0000000..e4c8e1c --- /dev/null +++ b/bin/racoon-worker @@ -0,0 +1,120 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'getoptlong' +require 'rubygems' +require 'daemons' +require 'racoon' +require 'csv' +require 'yaml' +require 'zmqmachine' +require 'beanstalk-client' + +def usage + puts "Usage: racoon-worker [switches]" + puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers" + puts " --tube the beanstalk tube to use" + puts " --firehose <127.0.0.1:11555> the address of the firehose" + puts " --pid the path to store the pid" + puts " --log the path to store the log" + puts " --daemon to daemonize the server" + puts " --help this message" +end + +def daemonize + Daemonize.daemonize(@log_file, 'racoon-worker') + open(@pid_file,"w") { |f| f.write(Process.pid) } + open(@pid_file,"w") do |f| + f.write(Process.pid) + File.chmod(0644, @pid_file) + end +end + +opts = GetoptLong.new( + ["--beanstalk", "-b", GetoptLong::REQUIRED_ARGUMENT], + ["--tube", "-t", GetoptLong::REQUIRED_ARGUMENT], + ["--firehose", "-f", GetoptLong::REQUIRED_ARGUMENT], + ["--pid", "-i", GetoptLong::REQUIRED_ARGUMENT], + ["--log", "-l", GetoptLong::REQUIRED_ARGUMENT], + ["--help", "-h", GetoptLong::NO_ARGUMENT], + ["--daemon", "-d", GetoptLong::NO_ARGUMENT] +) + +@beanstalks = ["127.0.0.1:11300"] +firehose_address = "127.0.0.1:11555".split(":") +@tube = 'racoon' +@pid_file = '/var/run/racoon-worker.pid' +@log_file = '/var/log/racoon-worker.log' +daemon = false + +opts.each do |opt, arg| + case opt + when '--help' + usage + exit 1 + when '--beanstalk' + @beanstalks = CSV.parse(arg)[0] + when '--tube' + @tube = arg + when '--firehose' + firehose_address = arg.split(":") + when '--pid' + @pid_file = arg + when '--log' + @log_file = arg + when '--daemon' + daemon = true + end +end + +Racoon::Config.logger = Logger.new(@log_file) + +def beanstalk + return @beanstalk if @beanstalk + @beanstalk = Beanstalk::Pool.new(@beanstalks) + %w{use watch}.each { |s| @beanstalk.send(s, @tube) } + @beanstalk.ignore('default') + @beanstalk +end + +# Expects json ala: +# json = { +# "project":{ +# "name":"foobar", +# "certificate":"...", +# "sandbox":false +# }, +# "bytes":"..." +# } +def process(job, worker) + packet = job.ybody + project = packet[:project] + + notification = Racoon::Notification.create_from_packet(packet) + + data = { :project => project, :bytes => notification.to_bytes } + worker.send_message(YAML::dump(data)) +end + +if daemon + daemonize +else + puts "Starting racoon worker." +end + +ZM::Reactor.new(:worker).run do |context| + worker = Racoon::Worker.new(context, ZM::Address.new(firehose_address[0], firehose_address[1], :tcp)) + context.push_socket(worker) + + context.periodical_timer(0.1) do + begin + if beanstalk.peek_ready + job = beanstalk.reserve(1) + process(job, worker) + job.delete + end + rescue Beanstalk::TimedOut + Racoon::Config.logger.info "[Beanstalk] Unable to secure job, timed out." + end + end +end.join diff --git a/doc/firehose.mdown b/doc/firehose.mdown new file mode 100644 index 0000000..e13e256 --- /dev/null +++ b/doc/firehose.mdown @@ -0,0 +1,4 @@ +# Racoon Firehose + +This component sits at the end of the pipeline. Its job is to pull notifications (as they are to be +sent to apple) in from all the workers, and fire the message off to Apple. diff --git a/doc/worker.mdown b/doc/worker.mdown new file mode 100644 index 0000000..542157d --- /dev/null +++ b/doc/worker.mdown @@ -0,0 +1,13 @@ +# Racoon Worker + +Workers are very simple devices. They sit at the front-end and pop items off the beanstalk cluster. +They create the notifications, and feed them out to the connector. + +## Parts + +* Popper + This piece pops items off of the beanstalk cluster. It creates notifications, and passes them + over the firehose. +* Firehose + This is a push socket where we send the results from the popper. These are the results preprepared + to be written over the socket. \ No newline at end of file diff --git a/lib/apnserver.rb b/lib/apnserver.rb deleted file mode 100644 index 8731c53..0000000 --- a/lib/apnserver.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'logger' -require 'apnserver/payload' -require 'apnserver/notification' -require 'apnserver/client' diff --git a/lib/apnserver/client.rb b/lib/apnserver/client.rb deleted file mode 100644 index 642acf0..0000000 --- a/lib/apnserver/client.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'openssl' -require 'socket' - -module ApnServer - class Client - attr_accessor :pem, :host, :port, :password - - def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil) - @pem, @host, @port, @password = pem, host, port, pass - end - - def connect! - raise "The path to your pem file is not set." unless self.pem - raise "The path to your pem file does not exist!" unless File.exist?(self.pem) - - @context = OpenSSL::SSL::SSLContext.new - @context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem)) - @context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.password) - - @sock = TCPSocket.new(self.host, self.port.to_i) - @ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context) - @ssl.connect - - return @sock, @ssl - end - - def disconnect! - @ssl.close - @sock.close - @ssl = nil - @sock = nil - end - - def write(notification) - Config.logger.debug "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}" - @ssl.write(notification.to_bytes) - end - - def connected? - @ssl - end - end -end \ No newline at end of file diff --git a/lib/apnserver/notification.rb b/lib/apnserver/notification.rb deleted file mode 100644 index 1a66189..0000000 --- a/lib/apnserver/notification.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'apnserver/payload' -require 'base64' -require 'active_support/ordered_hash' -require 'active_support/json' - -module ApnServer - class Config - class << self - attr_accessor :host, :port, :pem, :password, :logger - end - end - - Config.logger = Logger.new("/dev/null") - - class Notification - include ApnServer::Payload - - attr_accessor :device_token, :alert, :badge, :sound, :custom - - def payload - p = Hash.new - [:badge, :alert, :sound, :custom].each do |k| - p[k] = send(k) if send(k) - end - create_payload(p) - end - - def json_payload - j = ActiveSupport::JSON.encode(payload) - raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256 - j - end - - def push - if Config.pem.nil? - socket = TCPSocket.new(Config.host || 'localhost', Config.port.to_i || 22195) - socket.write(to_bytes) - socket.close - else - client = ApnServer::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195) - client.connect! - client.write(self) - client.disconnect! - end - end - - def to_bytes - j = json_payload - [0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*") - end - - def self.valid?(p) - begin - Notification.parse(p) - rescue PayloadInvalid => p - Config.logger.error "PayloadInvalid: #{p}" - false - rescue RuntimeError - false - rescue Exception => e - false - end - end - - def self.parse(p) - buffer = p.dup - notification = Notification.new - - header = buffer.slice!(0, 3).unpack('ccc') - if header[0] != 0 - raise RuntimeError.new("Header of notification is invalid: #{header.inspect}") - end - - # parse token - notification.device_token = buffer.slice!(0, 32).unpack('a*').first - - # parse json payload - payload_len = buffer.slice!(0, 2).unpack('CC') - j = buffer.slice!(0, payload_len.last) - result = ActiveSupport::JSON.decode(j) - - ['alert', 'badge', 'sound'].each do |k| - notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k] - end - result.delete('aps') - notification.custom = result - - notification - end - end -end diff --git a/lib/apnserver/protocol.rb b/lib/apnserver/protocol.rb deleted file mode 100644 index 3b10346..0000000 --- a/lib/apnserver/protocol.rb +++ /dev/null @@ -1,22 +0,0 @@ -module ApnServer - module Protocol - def post_init - @address = Socket.unpack_sockaddr_in(self.get_peername) - Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] CONNECT" - end - - def unbind - Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] DISCONNECT" - end - - def receive_data(data) - (@buf ||= "") << data - if notification = ApnServer::Notification.valid?(@buf) - Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] found valid Notification: #{notification}" - queue.push(notification) - else - Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] invalid notification: #{@buf}" - end - end - end -end \ No newline at end of file diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb deleted file mode 100644 index 9a1d19f..0000000 --- a/lib/apnserver/server.rb +++ /dev/null @@ -1,41 +0,0 @@ -module ApnServer - class Server - attr_accessor :client, :bind_address, :port - - def initialize(pem, bind_address = '0.0.0.0', port = 22195) - @queue = EM::Queue.new - @client = ApnServer::Client.new(pem) - @bind_address, @port = bind_address, port - end - - def start! - EventMachine::run do - Config.logger.info "#{Time.now} Starting APN Server on #{bind_address}:#{port}" - - EM.start_server(bind_address, port, ApnServer::ServerConnection) do |s| - s.queue = @queue - end - - EventMachine::PeriodicTimer.new(1) do - unless @queue.empty? - size = @queue.size - size.times do - @queue.pop do |notification| - begin - @client.connect! unless @client.connected? - @client.write(notification) - rescue Errno::EPIPE, OpenSSL::SSL::SSLError - Config.logger.error "Caught Error, closing connecting and adding notification back to queue" - @client.disconnect! - @queue.push(notification) - rescue RuntimeError => e - Config.logger.error "Unable to handle: #{e}" - end - end - end - end - end - end - end - end -end diff --git a/lib/apnserver/server_connection.rb b/lib/apnserver/server_connection.rb deleted file mode 100644 index fa0d470..0000000 --- a/lib/apnserver/server_connection.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'socket' -require 'apnserver/protocol' - -module ApnServer - class ServerConnection < EventMachine::Connection - include Protocol - attr_accessor :queue, :address - end -end \ No newline at end of file diff --git a/lib/racoon.rb b/lib/racoon.rb new file mode 100644 index 0000000..9887807 --- /dev/null +++ b/lib/racoon.rb @@ -0,0 +1,12 @@ +require 'logger' +require 'racoon/config' +require 'racoon/payload' +require 'racoon/notification' +require 'racoon/apns/connection' +require 'racoon/apns/feedback_connection' +require 'racoon/worker' +require 'racoon/firehose' + +module Racoon + VERSION = "1.0.1" +end diff --git a/lib/racoon/apns/connection.rb b/lib/racoon/apns/connection.rb new file mode 100644 index 0000000..72580a6 --- /dev/null +++ b/lib/racoon/apns/connection.rb @@ -0,0 +1,67 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# This module contains the connection to the APNs service. + +require 'openssl' +require 'socket' + +module Racoon + module APNS + class Connection + attr_accessor :pem, :host, :port, :password + + def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil) + @pem, @host, @port, @password = pem, host, port, pass + end + + def connect! + raise "Your certificate is not set." unless self.pem + + @context = OpenSSL::SSL::SSLContext.new + @context.cert = OpenSSL::X509::Certificate.new(self.pem) + @context.key = OpenSSL::PKey::RSA.new(self.pem, self.password) + + @sock = TCPSocket.new(self.host, self.port.to_i) + @ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context) + @ssl.connect + + return @sock, @ssl + end + + def disconnect! + @ssl.close + @sock.close + @ssl = nil + @sock = nil + end + + def read + errors ||= [] + while error = @ssl.read(6) + errors << parse_tuple(error) + end + errors + end + + def write(bytes) + if host.include? "sandbox" + notification = Notification.parse(bytes) + Config.logger.debug "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}" + end + @ssl.write(notification.to_bytes) + end + + def connected? + @ssl + end + + private + + def parse_tuple(data) + packet = data.unpack("c1c1N1") + { :command => packet[0], :status => packet[1], :identifier => packet[2] } + end + end + end +end diff --git a/lib/racoon/apns/feedback_connection.rb b/lib/racoon/apns/feedback_connection.rb new file mode 100644 index 0000000..a22f59b --- /dev/null +++ b/lib/racoon/apns/feedback_connection.rb @@ -0,0 +1,29 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# This module contains the code that connects to the feedback service. + +module Racoon + module APNS + class FeedbackConnection < Connection + def initialize(pem, host = 'feedback.push.apple.com', port = 2196, pass = nil) + @pem, @host, @port, @pass = pem, host, port, pass + end + + def read + records ||= [] + while record = @ssl.read(38) + records << parse_tuple(record) + end + records + end + + private + + def parse_tuple(data) + feedback = data.unpack("N1n1H*") + { :feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] } + end + end + end +end \ No newline at end of file diff --git a/lib/racoon/config.rb b/lib/racoon/config.rb new file mode 100644 index 0000000..2ede23e --- /dev/null +++ b/lib/racoon/config.rb @@ -0,0 +1,14 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# Configuration settings. + +module Racoon + class Config + class << self + attr_accessor :logger + end + end + + Config.logger = Logger.new("/dev/null") +end \ No newline at end of file diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb new file mode 100644 index 0000000..3641523 --- /dev/null +++ b/lib/racoon/firehose.rb @@ -0,0 +1,55 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# This module contains the firehose which is responsible for maintaining all the open +# connections to Apple, and sending data over the right ones. + +require 'digest/sha1' +require 'zmqmachine' +require 'yaml' + +module Racoon + class Firehose + attr_accessor :connections, :feedback_callback, :error_callback + + def initialize(reactor, address = ZM::Address.new('*', 11555, :tcp), &feedback_callback) + @connections = {} + @reactor = reactor + @address = address + @feedback_callback = feedback_callback + @error_callback = nil + end + + def on_attach(socket) + socket.bind(@address) + end + + def on_readable(socket, messages) + messages.each do |message| + packet = YAML::load(message.copy_out_string) + apns(packet[:project], packet[:bytes]) + end + end + + private + + def apns(project, bytes, retries=2) + uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" + hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") + + begin + @connections[hash] ||= { :connection => Racoon::APNS::Connection.new(project[:certificate], uri), + :certificate => project[:certificate], + :sandbox => project[:sandbox] } + connection = @connections[hash][:connection] + + connection.connect! unless connection.connected? + connection.write(bytes) + connection.read.each(&@error_callback) if @error_callback + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + connection.disconnect! + retry if (retries -= 1) > 0 + end + end + end +end diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb new file mode 100644 index 0000000..90ffd26 --- /dev/null +++ b/lib/racoon/notification.rb @@ -0,0 +1,98 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# This module contains the class that represents notifications and all their details. + +require 'racoon/payload' +require 'base64' +require 'yajl' + +module Racoon + class Notification + include Racoon::Payload + + attr_accessor :identifier, :expiry, :device_token, :alert, :badge, :sound, :custom, :expiry + + def initialize + @expiry = 0 + end + + def payload + p = Hash.new + [:badge, :alert, :sound, :custom].each do |k| + r = send(k) + p[k] = r if r + end + create_payload(p) + end + + def json_payload + j = Yajl::Encoder.encode(payload) + raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256 + j + end + + def to_bytes + j = json_payload + [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j].pack("cNNna*na*") + end + + def self.valid?(p) + begin + Notification.parse(p) + rescue PayloadInvalid => p + Config.logger.error "PayloadInvalid: #{p}" + false + rescue RuntimeError => r + Config.logger.error "Runtime error: #{r}" + false + rescue Exception => e + Config.logger.error "Unknown error: #{e}" + false + end + end + + def self.parse(p) + buffer = p.dup + notification = Notification.new + + header = buffer.slice!(0, 11).unpack("cNNn") + raise RuntimeError.new("Header of notification is invalid: #{header.inspect}") if header[0] != 1 + + # identifier + notification.identifier = header[1] + notification.expiry = header[2] + + # device token + notification.device_token = buffer.slice!(0, 32).unpack("a*").first + + # JSON payload + payload_len = buffer.slice!(0, 2).unpack("n") + j = buffer.slice!(0, payload_len.last) + result = Yajl::Parser.parse(j) + + ['alert', 'badge', 'sound'].each do |k| + notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k] + end + result.delete("aps") + notification.custom = result + + notification + end + + def self.create_from_packet(packet) + aps = packet[:notification][:aps] + + notification = Notification.new + notification.identifier = packet[:identifier] + notification.expiry = packet[:expiry] || 0 + notification.device_token = packet[:device_token] + notification.badge = aps[:badge] if aps.has_key? :badge + notification.alert = aps[:alert] if aps.has_key? :alert + notification.sound = aps[:sound] if aps.has_key? :sound + notification.custom = aps[:custom] if aps.has_key? :custom + + notification + end + end +end diff --git a/lib/apnserver/payload.rb b/lib/racoon/payload.rb similarity index 75% rename from lib/apnserver/payload.rb rename to lib/racoon/payload.rb index 07e4813..7d7c6e8 100644 --- a/lib/apnserver/payload.rb +++ b/lib/racoon/payload.rb @@ -1,4 +1,9 @@ -module ApnServer +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# APNs payload data + +module Racoon module Payload PayloadInvalid = Class.new(RuntimeError) diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb new file mode 100644 index 0000000..3337c07 --- /dev/null +++ b/lib/racoon/worker.rb @@ -0,0 +1,38 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# This module contains the worker which processes notifications before sending them off +# down to the firehose. + +require 'ffi-rzmq' +require 'zmqmachine' + +module Racoon + class Worker + def initialize(reactor, address) + @reactor = reactor + @address = address + @send_queue = [] + end + + def on_attach(socket) + @socket = socket + + socket.connect(@address.to_s) + end + + def on_writable(socket) + unless @send_queue.empty? + message = @send_queue.shift + socket.send_message_string(message) + else + @reactor.deregister_writable(socket) + end + end + + def send_message(message) + @send_queue.push(message) + @reactor.register_writable(@socket) + end + end +end diff --git a/racoon-firehose.god b/racoon-firehose.god new file mode 100644 index 0000000..950fad9 --- /dev/null +++ b/racoon-firehose.god @@ -0,0 +1,36 @@ +# Godfile for racoon-firehose + +[ 1 ].each do |instance| + name = "racoon-firehose-#{instance}" + pid_file = "/var/run/#{name}.pid" + + God.watch do |w| + w.name = name + w.interval = 30.seconds + w.start = "/fracas/deploy/racoon/bin/racoon-firehose -d --pid #{pid_file} --log /var/log/#{name}.log" + w.stop = "kill -9 `cat #{pid_file}`" + w.start_grace = 10.seconds + w.pid_file = pid_file + + w.behavior(:clean_pid_file) + + w.start_if do |start| + start.condition(:process_running) do |c| + c.interval = 5.seconds + c.running = false + end + end + + w.lifecycle do |on| + on.condition(:flapping) do |c| + c.to_state = [:start, :stop, :start] + c.times = 5 + c.within = 5.minutes + c.transition = :unmonitored + c.retry_in = 10.minutes + c.retry_times = 5 + c.retry_within = 2.hours + end + end + end +end diff --git a/racoon-worker.god b/racoon-worker.god new file mode 100644 index 0000000..bf79ee2 --- /dev/null +++ b/racoon-worker.god @@ -0,0 +1,36 @@ +# Godfile for racoon-worker + +[ 1 ].each do |instance| + name = "racoon-worker-#{instance}" + pid_file = "/var/run/#{name}.pid" + + God.watch do |w| + w.name = name + w.interval = 30.seconds + w.start = "/fracas/deploy/racoon/bin/racoon-worker -d --beanstalk 127.0.0.1:11300 --firehose localhost:11555 --pid #{pid_file} --log /var/log/#{name}.log" + w.stop = "kill -9 `cat #{pid_file}`" + w.start_grace = 10.seconds + w.pid_file = pid_file + + w.behavior(:clean_pid_file) + + w.start_if do |start| + start.condition(:process_running) do |c| + c.interval = 5.seconds + c.running = false + end + end + + w.lifecycle do |on| + on.condition(:flapping) do |c| + c.to_state = [:start, :stop, :start] + c.times = 5 + c.within = 5.minutes + c.transition = :unmonitored + c.retry_in = 10.minutes + c.retry_times = 5 + c.retry_within = 2.hours + end + end + end +end diff --git a/racoon.gemspec b/racoon.gemspec new file mode 100644 index 0000000..c80da74 --- /dev/null +++ b/racoon.gemspec @@ -0,0 +1,26 @@ +Gem::Specification.new do |s| + s.name = %q{racoon} + s.version = "1.0.1" + s.platform = Gem::Platform::RUBY + + s.authors = ["Jeremy Tregunna"] + s.date = %q{2011-04-24} + s.description = %q{A distributed Apple Push Notification Service (APNs) provider developed for hosting multiple projects.} + s.email = %q{jeremy.tregunna@me.com} + s.executables = ["racoon-send", "racoon-worker", "racoon-firehose"] + s.extra_rdoc_files = ["README.mdown"] + s.files = Dir.glob("{bin,lib}/**/*") + %w(README.mdown) + s.homepage = %q{https://github.com/jeremytregunna/racoon} + s.rdoc_options = ["--charset=UTF-8"] + s.require_paths = ["lib"] + s.rubyforge_project = %q{racoon} + s.rubygems_version = %q{1.3.5} + s.summary = %q{Distributed Apple Push Notification provider suitable for multi-project hosting.} + s.test_files = Dir.glob("spec/**/*") + + s.required_rubygems_version = ">= 1.3.6" + s.add_dependency 'yajl-ruby', '>= 0.7.0' + s.add_dependency 'beanstalk-client', '>= 1.0.0' + s.add_dependency 'ffi-rzmq', '~> 0.8.0' + s.add_development_dependency 'bundler', '~> 1.0.0' +end diff --git a/spec/models/feedback_client_spec.rb b/spec/models/feedback_client_spec.rb new file mode 100644 index 0000000..63304f9 --- /dev/null +++ b/spec/models/feedback_client_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +module ApnServer + describe FeedbackClient do + describe "#new" do + let(:feedback_client) { ApnServer::FeedbackClient.new('cert.pem', 'feedback.sandbox.push.apple.com', 2196) } + + it "sets the pem path" do + feedback_client.pem.should == 'cert.pem' + end + + it "sets the host" do + feedback_client.host.should == 'feedback.sandbox.push.apple.com' + end + + it "sets the port" do + feedback_client.port.should == 2196 + end + end + end +end diff --git a/spec/models/protocol_spec.rb b/spec/models/protocol_spec.rb deleted file mode 100644 index a819417..0000000 --- a/spec/models/protocol_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe "TestProtocol" do - before(:each) do - @server = TestServer.new - @server.queue = Array.new # fake out EM::Queue - end - - it "adds_notification_to_queue" do - token = "12345678123456781234567812345678" - @server.receive_data("\0\0 #{token}\0#{22.chr}{\"aps\":{\"alert\":\"Hi\"}}") - @server.queue.size.should == 1 - end - - it "does_not_add_invalid_notification" do - @server.receive_data('fakedata') - @server.queue.should be_empty - end -end diff --git a/spec/support/test_server.rb b/spec/support/test_server.rb deleted file mode 100644 index 37951c8..0000000 --- a/spec/support/test_server.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'apnserver/protocol' - -class TestServer - attr_accessor :queue - include ApnServer::Protocol - - def address - [12345, '127.0.0.1'] - end -end \ No newline at end of file