From 5c508005cf8fcb4176ef0b2749c1344c4b0d89e0 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 17 Mar 2011 22:20:52 -0400 Subject: [PATCH 01/98] replaced ActiveSupport with Yajl --- Gemfile | 5 ++--- Gemfile.lock | 6 ++---- README.textile | 9 ++++++++- lib/apnserver/notification.rb | 7 +++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index bf16f02..21a099d 100644 --- a/Gemfile +++ b/Gemfile @@ -2,9 +2,8 @@ source "http://rubygems.org" gem "eventmachine" gem "daemons" -gem "activesupport", ">= 3.0.0" -gem "i18n" # active support whines without this +gem "yajl-ruby" group :spec do gem "rspec" -end \ No newline at end of file +end diff --git a/Gemfile.lock b/Gemfile.lock index 408394e..74704f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,9 @@ GEM remote: http://rubygems.org/ specs: - activesupport (3.0.3) daemons (1.0.10) diff-lcs (1.1.2) eventmachine (0.12.10) - i18n (0.5.0) rspec (2.3.0) rspec-core (~> 2.3.0) rspec-expectations (~> 2.3.0) @@ -14,13 +12,13 @@ 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) daemons eventmachine - i18n rspec + yajl-ruby diff --git a/README.textile b/README.textile index 8546420..1e3af0b 100644 --- a/README.textile +++ b/README.textile @@ -2,6 +2,13 @@ h1. Apple Push Notification Server Toolkit * http://github.com/bpoweski/apnserver +This is an independent fork of the above repository, to run with the Fracas web service. Some +changes have been made, notably the JSON encoding/decoding. I've pulled out the dependency on +ActiveSupport, and introduced a dependency on Yajl. As such, I've rewritten all JSON decoding +and encoding to use Yajl. + +Other changes will be forthcoming. + h2. Description apnserver is a server and set of command line programs to send push notifications to the iPhone. @@ -20,7 +27,7 @@ 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 + * removed ActiveSupport in favor of Yajl * Rails 3.x support * drop the erroneous puts statements in favor a configurable logger * moved to Rspec diff --git a/lib/apnserver/notification.rb b/lib/apnserver/notification.rb index 1a66189..34b457b 100644 --- a/lib/apnserver/notification.rb +++ b/lib/apnserver/notification.rb @@ -1,7 +1,6 @@ require 'apnserver/payload' require 'base64' -require 'active_support/ordered_hash' -require 'active_support/json' +require 'yajl' module ApnServer class Config @@ -26,7 +25,7 @@ def payload end def json_payload - j = ActiveSupport::JSON.encode(payload) + j = Yajl::Encoder.encode(payload) raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256 j end @@ -77,7 +76,7 @@ def self.parse(p) # parse json payload payload_len = buffer.slice!(0, 2).unpack('CC') j = buffer.slice!(0, payload_len.last) - result = ActiveSupport::JSON.decode(j) + result = Yajl::Parser.parse(j) ['alert', 'badge', 'sound'].each do |k| notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k] From 3fb6edcf618cc56b29d8543310791d2d08878dfe Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 17 Mar 2011 23:07:30 -0400 Subject: [PATCH 02/98] Fixed #11256615 in pivotal tracker. feedback client now implemented --- lib/apnserver.rb | 1 + lib/apnserver/feedback_client.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 lib/apnserver/feedback_client.rb diff --git a/lib/apnserver.rb b/lib/apnserver.rb index 8731c53..e46be09 100644 --- a/lib/apnserver.rb +++ b/lib/apnserver.rb @@ -2,3 +2,4 @@ require 'apnserver/payload' require 'apnserver/notification' require 'apnserver/client' +require 'apnserver/feedback_client' diff --git a/lib/apnserver/feedback_client.rb b/lib/apnserver/feedback_client.rb new file mode 100644 index 0000000..d8bf251 --- /dev/null +++ b/lib/apnserver/feedback_client.rb @@ -0,0 +1,17 @@ +# Feedback service + +module ApnServer + class FeedbackClient < Client + 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 << record.unpack("NnH*") + end + records + end + end +end \ No newline at end of file From 7816c1958a54f289955d6246f2bd681683c37894 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 17 Mar 2011 23:14:36 -0400 Subject: [PATCH 03/98] Fixed a bug where reset connections wouldn't be handled properly. Also added missing requires. --- README.textile | 1 + bin/apnserverd | 3 ++- lib/apnserver.rb | 1 + lib/apnserver/server.rb | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.textile b/README.textile index 1e3af0b..e9008ec 100644 --- a/README.textile +++ b/README.textile @@ -160,6 +160,7 @@ h2. License (The MIT License) Copyright (c) 2011 Ben Poweski +Copyright (c) 2011 Jeremy Tregunna Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/bin/apnserverd b/bin/apnserverd index 82119ba..0a65b85 100755 --- a/bin/apnserverd +++ b/bin/apnserverd @@ -4,6 +4,7 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'getoptlong' require 'rubygems' require 'daemons' +require 'eventmachine' require 'apnserver' require 'apnserver/server' @@ -88,4 +89,4 @@ else server.client.port = port server.client.password = pem_passphrase server.start! -end \ No newline at end of file +end diff --git a/lib/apnserver.rb b/lib/apnserver.rb index e46be09..3ba7552 100644 --- a/lib/apnserver.rb +++ b/lib/apnserver.rb @@ -1,5 +1,6 @@ require 'logger' require 'apnserver/payload' require 'apnserver/notification' +require 'apnserver/server_connection' require 'apnserver/client' require 'apnserver/feedback_client' diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb index 9a1d19f..d4e97b4 100644 --- a/lib/apnserver/server.rb +++ b/lib/apnserver/server.rb @@ -24,7 +24,7 @@ def start! begin @client.connect! unless @client.connected? @client.write(notification) - rescue Errno::EPIPE, OpenSSL::SSL::SSLError + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET Config.logger.error "Caught Error, closing connecting and adding notification back to queue" @client.disconnect! @queue.push(notification) From 699af5d87138deb3a6329673aaf0dcedd89dd0a3 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 18 Mar 2011 00:26:15 -0400 Subject: [PATCH 04/98] added convenience method to parse the feedback data --- lib/apnserver/feedback_client.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/apnserver/feedback_client.rb b/lib/apnserver/feedback_client.rb index d8bf251..82dcd0a 100644 --- a/lib/apnserver/feedback_client.rb +++ b/lib/apnserver/feedback_client.rb @@ -5,13 +5,20 @@ class FeedbackClient < Client 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 << record.unpack("NnH*") + records << parse_tuple(record) end records end + + private + + def parse_tuple(data) + feedback = data.unpack("N1n1H64") + { :feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] } + end end end \ No newline at end of file From a71279704bed18fb08046413bb5001d6d5cfb963 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 18 Mar 2011 20:41:05 -0400 Subject: [PATCH 05/98] Removed the requirement for the certificate to be a file. Now it's expected to be a string. This will be handy for us when we go to hook apnserver into cassandra, where the certificate will be retreived as a string. --- lib/apnserver/client.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/apnserver/client.rb b/lib/apnserver/client.rb index 642acf0..389a72f 100644 --- a/lib/apnserver/client.rb +++ b/lib/apnserver/client.rb @@ -10,12 +10,11 @@ def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil) 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) + raise "Your certificate is not set." unless 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) + @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) From 28eb8624a79cee04ae8cdf1797b94bcddca5048e Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Mon, 21 Mar 2011 14:47:58 -0400 Subject: [PATCH 06/98] Updated dependency information in gemspec --- apnserver.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apnserver.gemspec b/apnserver.gemspec index e872c70..51d17c2 100644 --- a/apnserver.gemspec +++ b/apnserver.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |s| s.test_files = Dir.glob("spec/**/*") s.required_rubygems_version = ">= 1.3.6" - s.add_dependency 'activesupport', '~> 3.0.0' + s.add_dependency 'yajl-ruby', '>= 0.7.0' s.add_development_dependency 'bundler', '~> 1.0.0' s.add_development_dependency 'eventmachine', '>= 0.12.8' end From a2f7c7ef870cfe7ca694a9ef2f3a524e92a37702 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Mon, 21 Mar 2011 14:48:35 -0400 Subject: [PATCH 07/98] Able now to pass the key through to the modified apnserver as a file path. Behaviour changed internally to take a string instead, so we pass it a string reading it from a file. --- bin/apnsend | 2 +- bin/apnserverd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/apnsend b/bin/apnsend index 5e55e6c..f39ef97 100755 --- a/bin/apnsend +++ b/bin/apnsend @@ -49,7 +49,7 @@ opts.each do |opt, arg| when '--port' ApnServer::Config.port = arg.to_i when '--pem' - ApnServer::Config.pem = arg + ApnServer::Config.pem = File.read(arg) when '--pem-passphrase' ApnServer::Config.password = arg when '--alert' diff --git a/bin/apnserverd b/bin/apnserverd index 0a65b85..f1754cd 100755 --- a/bin/apnserverd +++ b/bin/apnserverd @@ -71,7 +71,7 @@ opts.each do |opt, arg| when '--log' @log_file = arg when '--pem' - pem = arg + pem = File.read(arg) when '--pem-passphrase' pem_passphrase = arg when '--daemon' From 54a09757998d4726fa9a54d69c5647a7f36f3171 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Mon, 21 Mar 2011 14:49:30 -0400 Subject: [PATCH 08/98] Feedback service now tied in, though we're only printing records we get back from the service, not actually acting on them. --- lib/apnserver/feedback_client.rb | 2 +- lib/apnserver/server.rb | 23 +++++++++++++++++++++++ lib/apnserver/server_connection.rb | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/apnserver/feedback_client.rb b/lib/apnserver/feedback_client.rb index 82dcd0a..726f30f 100644 --- a/lib/apnserver/feedback_client.rb +++ b/lib/apnserver/feedback_client.rb @@ -17,7 +17,7 @@ def read private def parse_tuple(data) - feedback = data.unpack("N1n1H64") + feedback = data.unpack("N1n1H*") { :feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] } end end diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb index d4e97b4..a2e06a5 100644 --- a/lib/apnserver/server.rb +++ b/lib/apnserver/server.rb @@ -2,10 +2,14 @@ module ApnServer class Server attr_accessor :client, :bind_address, :port + ONCE_A_DAY = 60 * 60 * 24 + def initialize(pem, bind_address = '0.0.0.0', port = 22195) @queue = EM::Queue.new @client = ApnServer::Client.new(pem) + @feedback_client = ApnServer::FeedbackClient.new(pem) @bind_address, @port = bind_address, port + Config.logger = Logger.new("/dev/stdout") end def start! @@ -16,6 +20,25 @@ def start! s.queue = @queue end + EventMachine::PeriodicTimer.new(ONCE_A_DAY) do + begin + @feedback_client.connect! unless @feedback_client.connected? + @feedback_client.read.each do |record| + # In here, we need to inspect the record, make sure that we yank it + # out from the database. record is a hash with keys: + # feedback_at, length, token + # For debugging purposes, just print it out + p record + end + @feedback_client.disconnect! + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + Config.logger.error "(Feedback) Caught Error, closing connection" + @feedback_client.disconnect! + rescue RuntimeError => e + Config.logger.error "(Feedback) Unable to handle: #{e}" + end + end + EventMachine::PeriodicTimer.new(1) do unless @queue.empty? size = @queue.size diff --git a/lib/apnserver/server_connection.rb b/lib/apnserver/server_connection.rb index fa0d470..a810a6b 100644 --- a/lib/apnserver/server_connection.rb +++ b/lib/apnserver/server_connection.rb @@ -1,5 +1,6 @@ require 'socket' require 'apnserver/protocol' +require 'eventmachine' module ApnServer class ServerConnection < EventMachine::Connection From 44fcec0fc3c98f26baefce014f03bd0998968215 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 22 Mar 2011 22:21:17 -0400 Subject: [PATCH 09/98] Updated feedback code introducing a callback on the server to handle feedback data. Suitable for primetime! --- bin/apnserverd | 4 +++- lib/apnserver/server.rb | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/bin/apnserverd b/bin/apnserverd index f1754cd..1d52a3b 100755 --- a/bin/apnserverd +++ b/bin/apnserverd @@ -84,7 +84,9 @@ if pem.nil? exit 1 else daemonize if daemon - server = ApnServer::Server.new(pem, bind_address, proxy_port) + server = ApnServer::Server.new(pem, bind_address, proxy_port) do |feedback_record| + ApnServer::Config.logger.info "Received feedback at #{feedback_record[:feedback_at]} (length: #{feedback_record[:length]}): #{feedback_record[:device_token]}" + end server.client.host = host server.client.port = port server.client.password = pem_passphrase diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb index a2e06a5..3025d22 100644 --- a/lib/apnserver/server.rb +++ b/lib/apnserver/server.rb @@ -1,14 +1,15 @@ module ApnServer class Server - attr_accessor :client, :bind_address, :port + attr_accessor :client, :bind_address, :port, :feedback_callback ONCE_A_DAY = 60 * 60 * 24 - def initialize(pem, bind_address = '0.0.0.0', port = 22195) + def initialize(pem, bind_address = '0.0.0.0', port = 22195, &feedback_blk) @queue = EM::Queue.new @client = ApnServer::Client.new(pem) @feedback_client = ApnServer::FeedbackClient.new(pem) @bind_address, @port = bind_address, port + @feedback_callback = feedback_blk Config.logger = Logger.new("/dev/stdout") end @@ -24,11 +25,7 @@ def start! begin @feedback_client.connect! unless @feedback_client.connected? @feedback_client.read.each do |record| - # In here, we need to inspect the record, make sure that we yank it - # out from the database. record is a hash with keys: - # feedback_at, length, token - # For debugging purposes, just print it out - p record + feedback_callback.call record end @feedback_client.disconnect! rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET From 45971ad4a4ed03b8e0757ab2ef206495b0e6ad71 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 22 Mar 2011 22:37:36 -0400 Subject: [PATCH 10/98] updated README to reflect state of modifications --- Gemfile | 1 + README.textile | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 21a099d..39b7701 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "http://rubygems.org" gem "eventmachine" gem "daemons" gem "yajl-ruby" +gem "fracassandra", ">= 0.3.2" group :spec do gem "rspec" diff --git a/README.textile b/README.textile index e9008ec..187a25c 100644 --- a/README.textile +++ b/README.textile @@ -3,11 +3,17 @@ h1. Apple Push Notification Server Toolkit * http://github.com/bpoweski/apnserver This is an independent fork of the above repository, to run with the Fracas web service. Some -changes have been made, notably the JSON encoding/decoding. I've pulled out the dependency on -ActiveSupport, and introduced a dependency on Yajl. As such, I've rewritten all JSON decoding -and encoding to use Yajl. +changes have been made, notably the JSON encoding/decoding, and the support of the feedback +service. I've pulled out the dependency on ActiveSupport, and introduced a dependency on Yajl. +As such, I've rewritten all JSON decoding and encoding to use Yajl. -Other changes will be forthcoming. +In addition, this fork varies in another critical way. The certificates are no longer passed in +as a path to a file, but rather the contents of that file. This is done due to the need for +Fracas, having the certificates recorded as a string in a database, not encoded in files. For +others who may have files, just File.read(your_path) first and you'll be fine. Note that the +apnsend and apnserverd still expect the certificate as a path. + +Other changes may be forthcoming. h2. Description @@ -19,7 +25,7 @@ persistent connection. h2. Remaining Tasks - * Implement feedback service mechanism + * -Implement feedback service mechanism- * Implement robust notification sending in reactor periodic scheduler h2. Issues Fixed From 7e6cc6ee8b0c9b64370e9fda05425de124495abe Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 22 Mar 2011 22:40:41 -0400 Subject: [PATCH 11/98] added feedback client spec, minimal. --- spec/models/feedback_client_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/models/feedback_client_spec.rb 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 From cad9fe52c5fbd63bf7bbfca25878a8e6bd810982 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 27 Mar 2011 11:23:04 -0400 Subject: [PATCH 12/98] Added beanstalk requirement --- Gemfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 39b7701..5c79005 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,8 @@ source "http://rubygems.org" gem "eventmachine" gem "daemons" gem "yajl-ruby" -gem "fracassandra", ">= 0.3.2" +gem "beanstalk-client" +gem "fracassandra", ">= 0.3.4" group :spec do gem "rspec" From 46f7bd1b70bdf949133192d2d4c96471124cb3bb Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 27 Mar 2011 11:23:21 -0400 Subject: [PATCH 13/98] Rewrote Server to use beanstalk for input, instead of opening a listen socket. --- lib/apnserver/server.rb | 88 ++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb index 3025d22..21335dd 100644 --- a/lib/apnserver/server.rb +++ b/lib/apnserver/server.rb @@ -1,28 +1,26 @@ +require 'beanstalk-client' + module ApnServer - class Server - attr_accessor :client, :bind_address, :port, :feedback_callback + class QueueServer - ONCE_A_DAY = 60 * 60 * 24 + attr_accessor :client, :beanstalkd_uris, :port, :feedback_callback - def initialize(pem, bind_address = '0.0.0.0', port = 22195, &feedback_blk) - @queue = EM::Queue.new - @client = ApnServer::Client.new(pem) - @feedback_client = ApnServer::FeedbackClient.new(pem) - @bind_address, @port = bind_address, port + def initialize(beanstalkd_uris = ["beanstalk://127.0.0.1:11300"], &feedback_blk) + @clients = {} @feedback_callback = feedback_blk + @beanstalkd_uris = beanstalkd_uris Config.logger = Logger.new("/dev/stdout") end + def beanstalk + @@beanstalk ||= Beanstalk::Pool.new @beanstalkd_uris + 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(ONCE_A_DAY) do + EventMachine::PeriodicTimer.new(28800) do begin + @feedback_client = nil # Until we pull in DB support @feedback_client.connect! unless @feedback_client.connected? @feedback_client.read.each do |record| feedback_callback.call record @@ -37,25 +35,53 @@ def start! 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, Errno::ECONNRESET - 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 + begin + if beanstalk.peek_ready + item = beanstalk.reserve(1) + handle_job item end + rescue Beanstalk::TimedOut + Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." end end end end + + private + + def handle_job(job) + job_hash = job.ybody + if notification = ApnServer::Notification.valid?(job_hash[:notification]) + client = get_client(job_hash[:project_name], job_hash[:certificate]) + begin + client.connect! unless client.connected? + client.write(notification) + job.delete + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + Config.logger.error "Caught Error, closing connecting and adding notification back to queue" + client.disconnect! + # Queue back up the notification + job.release + # TODO: Write a failure receipt + rescue RuntimeError => e + Config.logger.error "Unable to handle: #{e}" + end + end + end + + def get_client(project_name, certificate) + @clients[project_name] ||= ApnServer::Client.new(certificate) + client = @clients[project_name] + + # If the certificate has changed, but we still are connected using the old certificate, + # disconnect and reconnect. + unless client.pem.eql?(certificate) + client.disconnect! + @clients[project_name] = ApnServer::Client.new(certificate) + client = @clients[project_name] + end + + client + end end -end +end \ No newline at end of file From cede01d5e82ff6c37a75b8fa526eaee94dc739a1 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 27 Mar 2011 12:28:44 -0400 Subject: [PATCH 14/98] changed server daemon arguments, some don't apply anymore. cleanup of server in lib --- bin/apnserverd | 56 +++++++++++------------------------------ lib/apnserver/server.rb | 15 ++++++----- 2 files changed, 21 insertions(+), 50 deletions(-) diff --git a/bin/apnserverd b/bin/apnserverd index 1d52a3b..b1116da 100755 --- a/bin/apnserverd +++ b/bin/apnserverd @@ -7,14 +7,11 @@ require 'daemons' require 'eventmachine' require 'apnserver' require 'apnserver/server' +require 'csv' 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 "Usage: apnserverd [switches] --beanstalk a.b.c.d:11300,...,w.x.y.z:11300" + puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers" puts " --pid e Config.logger.error "Unable to handle: #{e}" end end end - def get_client(project_name, certificate) - @clients[project_name] ||= ApnServer::Client.new(certificate) + def get_client(project_name, certificate, sandbox = false) + uri = "gateway.#{sandbox ? 'sandbox.' : ''}.push.apple.com" + @clients[project_name] ||= ApnServer::Client.new(certificate, uri) client = @clients[project_name] # If the certificate has changed, but we still are connected using the old certificate, # disconnect and reconnect. unless client.pem.eql?(certificate) - client.disconnect! - @clients[project_name] = ApnServer::Client.new(certificate) + client.disconnect! if client.connected? + @clients[project_name] = ApnServer::Client.new(certificate, uri) client = @clients[project_name] end From d73eaa672ab08b468bbea622b8d43467e45bb5c3 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 27 Mar 2011 12:29:31 -0400 Subject: [PATCH 15/98] Updated README to reflect state of project --- README.textile | 89 ++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/README.textile b/README.textile index 187a25c..28587c9 100644 --- a/README.textile +++ b/README.textile @@ -1,67 +1,64 @@ -h1. Apple Push Notification Server Toolkit +h1. Apple Push Notification Server Toolkit for frac.as -* http://github.com/bpoweski/apnserver +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: -This is an independent fork of the above repository, to run with the Fracas web service. Some -changes have been made, notably the JSON encoding/decoding, and the support of the feedback -service. I've pulled out the dependency on ActiveSupport, and introduced a dependency on Yajl. -As such, I've rewritten all JSON decoding and encoding to use Yajl. +# It implements the APNS feedback service; +# Uses Yajl for JSON encoding/decoding rather than ActiveSupport; +# Expects certificates as strings instead of paths to files; +# Does not assume there is only one certificate; and +# Receives packets containing notifications from beanstalkd instead of a listening socket. -In addition, this fork varies in another critical way. The certificates are no longer passed in -as a path to a file, but rather the contents of that file. This is done due to the need for -Fracas, having the certificates recorded as a string in a database, not encoded in files. For -others who may have files, just File.read(your_path) first and you'll be fine. Note that the -apnsend and apnserverd still expect the certificate as a path. +The above changes were made because of the need for an APNS provider to replace the current +provider used by "Diligent Street":http://www.diligentstreet.com/ with something more robust. As such, it needs to be +suitable for a hosted environment, where multiple—unrelated—users of the service will be +using it. -Other changes may be forthcoming. +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. 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. +frac-apnserver is a server and a set of command line programs to send push notifications to iOS +devices. Apple recommends to maintain an open connection to the push notification service, and +refrain from opening up and tearing down SSL connections repeatedly. As such, a separate daemon +is introduced that has messages queued up (beanstalkd) for consumption by this daemon. This +decouples the APNS server from your backend system. Those notifications are sent over a +persistent connection to Apple. -h2. Remaining Tasks +h2. Remaining Tasks & Issues - * -Implement feedback service mechanism- - * Implement robust notification sending in reactor periodic scheduler +You can see progress by looking at the "Pivotal Tracker":https://www.pivotaltracker.com/projects/251991 page for fracas. Any labels related to +_apnserver_ are related to this subproject. -h2. Issues Fixed +h2. Preparing Certificates - * 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 ActiveSupport in favor of Yajl - * Rails 3.x support - * drop the erroneous puts statements in favor a configurable logger - * moved to Rspec - -h2. APN Server Daemon +Certificates must be prepared before they can be used with frac-apnserver. 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:
-  
-Usage: apnserverd [options] --pem /path/to/pem
-  --bind-address bind address (defaults to 0.0.0.0)
-    bind address of the server daemon
+	
+		$ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
+	
+
- --proxy-port port - the port that the daemon will listen on (defaults to 22195) +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. - --server server - APN Server (defaults to gateway.push.apple.com) +h2. APN Server Daemon - --port port of the APN Server - APN server port (defaults to 2195) +
+  
+Usage: apnserverd [options] --beanstalk
+  --beanstalk 
+	The comma-separated list of ip:port for beanstalk servers
 
-  --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
+  --pid 
+	Path used for the PID file. Defaults to /var/run/apnserver.pid
 
-  --pem-passphrase passphrase
-    The PEM passphrase to decode key.
+  --log 
+	Path used for the log file. Defaults to /var/log/apnserver.log
 
   --help
     usage message

From 0e23f12025df31c339e665f7d088674dfaeffe14 Mon Sep 17 00:00:00 2001
From: Jeremy Tregunna 
Date: Sun, 27 Mar 2011 13:39:03 -0400
Subject: [PATCH 16/98] removed unnecessary files, renaming project to
 frac-apnserver to avoid confusion, and cleaned up a bit of nonsense (wrong
 urls, etc).

---
 bin/apnserverd                              |  3 +--
 apnserver.gemspec => frac-apnserver.gemspec | 21 ++++++++++----------
 lib/apnserver.rb                            |  1 -
 lib/apnserver/notification.rb               |  4 +++-
 lib/apnserver/protocol.rb                   | 22 ---------------------
 lib/apnserver/server.rb                     |  8 ++++----
 lib/apnserver/server_connection.rb          | 10 ----------
 spec/models/protocol_spec.rb                | 19 ------------------
 spec/support/test_server.rb                 | 10 ----------
 9 files changed, 19 insertions(+), 79 deletions(-)
 rename apnserver.gemspec => frac-apnserver.gemspec (54%)
 delete mode 100644 lib/apnserver/protocol.rb
 delete mode 100644 lib/apnserver/server_connection.rb
 delete mode 100644 spec/models/protocol_spec.rb
 delete mode 100644 spec/support/test_server.rb

diff --git a/bin/apnserverd b/bin/apnserverd
index b1116da..21b77da 100755
--- a/bin/apnserverd
+++ b/bin/apnserverd
@@ -35,7 +35,7 @@ opts = GetoptLong.new(
   ["--daemon", "-d", GetoptLong::NO_ARGUMENT]
 )
 
-beanstalks = ["beanstalk://127.0.0.1:11300"]
+beanstalks = ["127.0.0.1:11300"]
 @pid_file = '/var/run/apnserverd.pid'
 @log_file = '/var/log/apnserverd.log'
 daemon = false
@@ -47,7 +47,6 @@ opts.each do |opt, arg|
     exit 1
   when '--beanstalk'
     beanstalks = CSV.parse(arg)[0]
-    beanstalks.map! { |e| "beanstalk://#{e}" }
   when '--pid'
     @pid_file = arg
   when '--log'
diff --git a/apnserver.gemspec b/frac-apnserver.gemspec
similarity index 54%
rename from apnserver.gemspec
rename to frac-apnserver.gemspec
index 51d17c2..9d3c5d6 100644
--- a/apnserver.gemspec
+++ b/frac-apnserver.gemspec
@@ -1,25 +1,26 @@
 Gem::Specification.new do |s|
-  s.name = %q{apnserver}
-  s.version = "0.2.1"
+  s.name = %q{frac-apnserver}
+  s.version = "0.3.0"
   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.authors = ["Jeremy Tregunna"]
+  s.date = %q{2011-03-27}
+  s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment}
+  s.email = %q{jeremy.tregunna@me.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.homepage = %q{https://github.com/jeremytregunna/apnserver}
   s.rdoc_options = ["--charset=UTF-8"]
   s.require_paths = ["lib"]
-  s.rubyforge_project = %q{apnserver}
+  s.rubyforge_project = %q{frac-apnserver}
   s.rubygems_version = %q{1.3.5}
-  s.summary = %q{Apple Push Notification Toolkit}
+  s.summary = %q{Apple Push Notification Toolkit for hosted environments}
   s.test_files = Dir.glob("spec/**/*")
 
   s.required_rubygems_version = ">= 1.3.6"
-  s.add_dependency 'yajl-ruby',       '>= 0.7.0'
+  s.add_dependency 'yajl-ruby', '>= 0.7.0'
+  s.add_dependency 'beanstalk-client', '>= 1.0.0'
   s.add_development_dependency 'bundler', '~> 1.0.0'
   s.add_development_dependency 'eventmachine', '>= 0.12.8'
 end
diff --git a/lib/apnserver.rb b/lib/apnserver.rb
index 3ba7552..e46be09 100644
--- a/lib/apnserver.rb
+++ b/lib/apnserver.rb
@@ -1,6 +1,5 @@
 require 'logger'
 require 'apnserver/payload'
 require 'apnserver/notification'
-require 'apnserver/server_connection'
 require 'apnserver/client'
 require 'apnserver/feedback_client'
diff --git a/lib/apnserver/notification.rb b/lib/apnserver/notification.rb
index 34b457b..71dd385 100644
--- a/lib/apnserver/notification.rb
+++ b/lib/apnserver/notification.rb
@@ -54,9 +54,11 @@ def self.valid?(p)
       rescue PayloadInvalid => p
         Config.logger.error "PayloadInvalid: #{p}"
         false
-      rescue RuntimeError
+      rescue RuntimeError => r
+        Config.logger.error "Runtime error: #{r}"
         false
       rescue Exception => e
+        Config.logger.error "Unknown error: #{e}"
         false
       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
index d267ca5..44e6d30 100644
--- a/lib/apnserver/server.rb
+++ b/lib/apnserver/server.rb
@@ -1,11 +1,11 @@
 require 'beanstalk-client'
 
 module ApnServer
-  class QueueServer
+  class Server
 
     attr_accessor :beanstalkd_uris, :feedback_callback
 
-    def initialize(beanstalkd_uris = ["beanstalk://127.0.0.1:11300"], &feedback_blk)
+    def initialize(beanstalkd_uris = ["127.0.0.1:11300"], &feedback_blk)
       @clients = {}
       @feedback_callback = feedback_blk
       @beanstalkd_uris = beanstalkd_uris
@@ -50,7 +50,7 @@ def start!
 
     def handle_job(job)
       job_hash = job.ybody
-      if notification = ApnServer::Notification.valid?(job_hash[:notification])
+      if notification = job_hash[:notification]
         client = get_client(job_hash[:project_name], job_hash[:certificate], job_hash[:sandbox])
         begin
           client.connect! unless client.connected?
@@ -68,7 +68,7 @@ def handle_job(job)
     end
 
     def get_client(project_name, certificate, sandbox = false)
-      uri = "gateway.#{sandbox ? 'sandbox.' : ''}.push.apple.com"
+      uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com"
       @clients[project_name] ||= ApnServer::Client.new(certificate, uri)
       client = @clients[project_name]
 
diff --git a/lib/apnserver/server_connection.rb b/lib/apnserver/server_connection.rb
deleted file mode 100644
index a810a6b..0000000
--- a/lib/apnserver/server_connection.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'socket'
-require 'apnserver/protocol'
-require 'eventmachine'
-
-module ApnServer
-  class ServerConnection < EventMachine::Connection
-    include Protocol
-    attr_accessor :queue, :address
-  end
-end
\ No newline at end of file
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

From 13822afae0bb9732afdbaf37fa18a101847cda61 Mon Sep 17 00:00:00 2001
From: Jeremy Tregunna 
Date: Sun, 27 Mar 2011 17:46:29 -0400
Subject: [PATCH 17/98] refactoring of server handle_job

---
 frac-apnserver.gemspec => apnserver.gemspec |  4 ++--
 lib/apnserver/server.rb                     | 23 ++++++++++++++++++---
 2 files changed, 22 insertions(+), 5 deletions(-)
 rename frac-apnserver.gemspec => apnserver.gemspec (93%)

diff --git a/frac-apnserver.gemspec b/apnserver.gemspec
similarity index 93%
rename from frac-apnserver.gemspec
rename to apnserver.gemspec
index 9d3c5d6..b466ddb 100644
--- a/frac-apnserver.gemspec
+++ b/apnserver.gemspec
@@ -1,5 +1,5 @@
 Gem::Specification.new do |s|
-  s.name = %q{frac-apnserver}
+  s.name = %q{apnserver}
   s.version = "0.3.0"
   s.platform    = Gem::Platform::RUBY
 
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
   s.homepage = %q{https://github.com/jeremytregunna/apnserver}
   s.rdoc_options = ["--charset=UTF-8"]
   s.require_paths = ["lib"]
-  s.rubyforge_project = %q{frac-apnserver}
+  s.rubyforge_project = %q{apnserver}
   s.rubygems_version = %q{1.3.5}
   s.summary = %q{Apple Push Notification Toolkit for hosted environments}
   s.test_files = Dir.glob("spec/**/*")
diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb
index 44e6d30..3391e2a 100644
--- a/lib/apnserver/server.rb
+++ b/lib/apnserver/server.rb
@@ -48,14 +48,25 @@ def start!
 
     private
 
+    # Received a notification. The job's body should be a YAML encoded hash containing the following keys:
+    #   :project_name => The name of the project
+    #   :certificate => Certificate to use (Should be able to easily look this up in the DB)
+    #   :receipt_uuid => UUID of the push receipt that was created when the API got the request
+    #   :sandbox => Boolean value to use the sandbox servers or not (optional, defaults to false)
+    #   :notification => An ApnServer::Notification object, fully formed.
     def handle_job(job)
-      job_hash = job.ybody
-      if notification = job_hash[:notification]
-        client = get_client(job_hash[:project_name], job_hash[:certificate], job_hash[:sandbox])
+      packet = job.ybody
+      if notification = packet[:notification]
+        client = get_client(packet[:project_name], packet[:certificate], packet[:sandbox])
         begin
           client.connect! unless client.connected?
           client.write(notification)
           job.delete
+          # TODO: Find the receipt and update the sent_at property.
+          #if receipt = PushLog[packet[:receipt_uuid]]
+          #  receipt.sent_at = Time.now.to_i.to_s
+          #  receipt.save
+          #end
         rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET
           Config.logger.error "Caught Error, closing connecting and adding notification back to queue"
           client.disconnect!
@@ -63,6 +74,12 @@ def handle_job(job)
           job.release
         rescue RuntimeError => e
           Config.logger.error "Unable to handle: #{e}"
+          # TODO: Find the receipt and write the failed_at property.
+          #if receipt = PushLog[packet[:receipt_uuid]]
+          #  receipt.failed_at = Time.now.to_i.to_s
+          #  receipt.save
+          #end
+          job.delete
         end
       end
     end

From bf6b0137c6a8a146bbbcec8b11125f58b65c540c Mon Sep 17 00:00:00 2001
From: Jeremy Tregunna 
Date: Mon, 28 Mar 2011 12:33:52 -0400
Subject: [PATCH 18/98] packet formation change

---
 lib/apnserver/notification.rb | 2 +-
 lib/apnserver/server.rb       | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/lib/apnserver/notification.rb b/lib/apnserver/notification.rb
index 71dd385..4fa12a4 100644
--- a/lib/apnserver/notification.rb
+++ b/lib/apnserver/notification.rb
@@ -5,7 +5,7 @@
 module ApnServer
   class Config
     class << self
-      attr_accessor :host, :port, :pem, :password, :logger
+      attr_accessor :logger
     end
   end
 
diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb
index 3391e2a..be4405e 100644
--- a/lib/apnserver/server.rb
+++ b/lib/apnserver/server.rb
@@ -56,8 +56,9 @@ def start!
     #   :notification => An ApnServer::Notification object, fully formed.
     def handle_job(job)
       packet = job.ybody
-      if notification = packet[:notification]
-        client = get_client(packet[:project_name], packet[:certificate], packet[:sandbox])
+      project = packet[:project]
+      if notification = Notification.new.create_payload(packet[:notification])
+        client = get_client(project.name, project.certificate, packet[:sandbox])
         begin
           client.connect! unless client.connected?
           client.write(notification)

From 36dab26d354f11c5b00e9e47661e0a6fb73314ee Mon Sep 17 00:00:00 2001
From: Jeremy Tregunna 
Date: Sat, 23 Apr 2011 15:33:13 -0400
Subject: [PATCH 19/98] Rename project from apnserver to racoon, to avoid
 confusion with apnserver

---
 README.mdown                                 | 170 +++++++++++++++++
 README.textile                               | 186 -------------------
 bin/{apnserverd => racoond}                  |  22 +--
 lib/apnserver.rb                             |   5 -
 lib/racoon.rb                                |   6 +
 lib/racoon/.server.rb.swp                    | Bin 0 -> 20480 bytes
 lib/{apnserver => racoon}/client.rb          |   2 +-
 lib/racoon/config.rb                         |   9 +
 lib/{apnserver => racoon}/feedback_client.rb |   2 +-
 lib/{apnserver => racoon}/notification.rb    |  16 +-
 lib/{apnserver => racoon}/payload.rb         |   2 +-
 lib/{apnserver => racoon}/server.rb          |  14 +-
 apnserver.gemspec => racoon.gemspec          |  16 +-
 13 files changed, 221 insertions(+), 229 deletions(-)
 create mode 100644 README.mdown
 delete mode 100644 README.textile
 rename bin/{apnserverd => racoond} (64%)
 delete mode 100644 lib/apnserver.rb
 create mode 100644 lib/racoon.rb
 create mode 100644 lib/racoon/.server.rb.swp
 rename lib/{apnserver => racoon}/client.rb (98%)
 create mode 100644 lib/racoon/config.rb
 rename lib/{apnserver => racoon}/feedback_client.rb (96%)
 rename lib/{apnserver => racoon}/notification.rb (87%)
 rename lib/{apnserver => racoon}/payload.rb (96%)
 rename lib/{apnserver => racoon}/server.rb (90%)
 rename apnserver.gemspec => racoon.gemspec (69%)

diff --git a/README.mdown b/README.mdown
new file mode 100644
index 0000000..70885a2
--- /dev/null
+++ b/README.mdown
@@ -0,0 +1,170 @@
+# 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; and
+5. Receives packets containing notifications from beanstalkd instead of a listening socket.
+
+The above changes were made because of the need for an APNS provider to replace the current
+provider used by [Diligent Street](http://www.diligentstreet.com/) with something more robust. As such, it needs to be
+suitable for a hosted environment, where multiple—unrelated—users of the service will be
+using it.
+
+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).
+
+## Description
+
+racoon is a server and a set of command line programs to send push notifications to iOS devices.
+Apple recommends to maintain an open connection to the push notification service, and refrain
+from opening up and tearing down SSL connections repeatedly. As such, a separate daemon is
+introduced that has messages queued up (beanstalkd) for consumption by this daemon. This
+decouples the APNS server from your backend system. Those notifications are sent over a
+persistent connection to Apple.
+
+## Remaining Tasks & Issues
+
+You can see progress by looking at the [issue tracker](https://www.pivotaltracker.com/projects/251991) page for fracas. Any labels related to
+*apnserver* or *racoon* are related to this subproject.
+
+## 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. + +## APN Server Daemon + +
+Usage: racoond [options]
+  --beanstalk 
+	The comma-separated list of ip:port for beanstalk servers
+
+  --pid 
+	Path used for the PID file. Defaults to /var/run/apnserver.pid
+
+  --log 
+	Path used for the log file. Defaults to /var/log/apnserver.log
+
+  --help
+    usage message
+
+  --daemon
+    Runs process as daemon, not available on Windows
+
+ +## APN Server Client + +TODO: Document this + +## Sending Notifications from Ruby + +You need to set up a connection to the beanstalkd service. We can do this simply by defining +the following method: + +```ruby +def beanstalk + return @beanstalk if @beanstalk + @beanstalk = Beanstalk::Pool.new ["127.0.0.1:11300"] + @beanstalk.use "awesome-tube" + @beanstalk +end +``` + +In this way, whenever we need access to beanstalk, we'll make the connection and set up to use +the appropriate tube whether the connection is open yet or not. + +We will also need two pieces of information: A project, and a notification. + +### Project + +A project os comprised of a few pieces of information at a minimum (you may supply more if you +wish, but racoond will ignore them): + +```ruby +project = { :name => "Awesome project", :certificate => "contents of the generated .pem file" } +``` + +### Notification + +A notification is a ruby hash containing the things to be sent along, including the device token. +An example notification may look like this: + +```ruby +notification = { :device_token => "hex encoded device token", + :aps => { :alert => "Some text", + :sound => "Audio_file", + :badge => 42 }, + :a_custom_key => "Any number of custom keys" + } +``` + +Finally within we can send a push notification using the following code: + +```ruby +beanstalk.yput({ :project => project, :notification => notification, :sandbox => true }) +``` + +Note that the `sandbox` parameter is used to indicate whether or not we're using a development +certificate, and as such, should contact Apple's sandbox APN service instead of the production +certificate. If left out, we assume production. + +This will schedule the push on beanstalkd. Racoon is constantly polling beanstalkd looking for +ready jobs it can pop off and process (send to Apple). Using beanstalkd however allows us to +queue up items, and during peak times, add another **N** more racoon servers to make up any +backlog, to ensure our messages are sent fast, and that we can scale. + +## Installation + +Racoon is hosted on [rubygems](https://rubygems.org/gems/racoon) + +
+$ gem install racoon
+
+ +Adding racoon to your Rails application + +```ruby +gem 'apnserver' +``` + +h2. 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 28587c9..0000000 --- a/README.textile +++ /dev/null @@ -1,186 +0,0 @@ -h1. Apple Push Notification Server Toolkit for frac.as - -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: - -# It implements the APNS feedback service; -# Uses Yajl for JSON encoding/decoding rather than ActiveSupport; -# Expects certificates as strings instead of paths to files; -# Does not assume there is only one certificate; and -# Receives packets containing notifications from beanstalkd instead of a listening socket. - -The above changes were made because of the need for an APNS provider to replace the current -provider used by "Diligent Street":http://www.diligentstreet.com/ with something more robust. As such, it needs to be -suitable for a hosted environment, where multiple—unrelated—users of the service will be -using it. - -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. - -h2. Description - -frac-apnserver is a server and a set of command line programs to send push notifications to iOS -devices. Apple recommends to maintain an open connection to the push notification service, and -refrain from opening up and tearing down SSL connections repeatedly. As such, a separate daemon -is introduced that has messages queued up (beanstalkd) for consumption by this daemon. This -decouples the APNS server from your backend system. Those notifications are sent over a -persistent connection to Apple. - -h2. Remaining Tasks & Issues - -You can see progress by looking at the "Pivotal Tracker":https://www.pivotaltracker.com/projects/251991 page for fracas. Any labels related to -_apnserver_ are related to this subproject. - -h2. Preparing Certificates - -Certificates must be prepared before they can be used with frac-apnserver. 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. - -h2. APN Server Daemon - -
-  
-Usage: apnserverd [options] --beanstalk
-  --beanstalk 
-	The comma-separated list of ip:port for beanstalk servers
-
-  --pid 
-	Path used for the PID file. Defaults to /var/run/apnserver.pid
-
-  --log 
-	Path used for the log file. Defaults to /var/log/apnserver.log
-
-  --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 -Copyright (c) 2011 Jeremy Tregunna - -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/bin/apnserverd b/bin/racoond similarity index 64% rename from bin/apnserverd rename to bin/racoond index 21b77da..64b93e8 100755 --- a/bin/apnserverd +++ b/bin/racoond @@ -5,21 +5,21 @@ require 'getoptlong' require 'rubygems' require 'daemons' require 'eventmachine' -require 'apnserver' -require 'apnserver/server' +require 'racoon' +require 'racoon/server' require 'csv' def usage - puts "Usage: apnserverd [switches] --beanstalk a.b.c.d:11300,...,w.x.y.z:11300" + puts "Usage: racoond [switches] --beanstalk a.b.c.d:11300,...,w.x.y.z:11300" puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers" - 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, 'apnserverd') + Daemonize.daemonize(@log_file, 'racoond') open(@pid_file,"w") { |f| f.write(Process.pid) } open(@pid_file,"w") do |f| f.write(Process.pid) @@ -36,8 +36,8 @@ opts = GetoptLong.new( ) beanstalks = ["127.0.0.1:11300"] -@pid_file = '/var/run/apnserverd.pid' -@log_file = '/var/log/apnserverd.log' +@pid_file = '/var/run/racoond.pid' +@log_file = '/var/log/racoond.log' daemon = false opts.each do |opt, arg| @@ -56,10 +56,10 @@ opts.each do |opt, arg| end end -ApnServer::Config.logger = Logger.new(@log_file) +Racoon::Config.logger = Logger.new(@log_file) daemonize if daemon -server = ApnServer::Server.new(beanstalks) do |feedback_record| - ApnServer::Config.logger.info "Received feedback at #{feedback_record[:feedback_at]} (length: #{feedback_record[:length]}): #{feedback_record[:device_token]}" +server = Racoon::Server.new(beanstalks) do |feedback_record| + Racoon::Config.logger.info "Received feedback at #{feedback_record[:feedback_at]} (length: #{feedback_record[:length]}): #{feedback_record[:device_token]}" end server.start! diff --git a/lib/apnserver.rb b/lib/apnserver.rb deleted file mode 100644 index e46be09..0000000 --- a/lib/apnserver.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'logger' -require 'apnserver/payload' -require 'apnserver/notification' -require 'apnserver/client' -require 'apnserver/feedback_client' diff --git a/lib/racoon.rb b/lib/racoon.rb new file mode 100644 index 0000000..fd93181 --- /dev/null +++ b/lib/racoon.rb @@ -0,0 +1,6 @@ +require 'logger' +require 'racoon/config' +require 'racoon/payload' +require 'racoon/notification' +require 'racoon/client' +require 'racoon/feedback_client' diff --git a/lib/racoon/.server.rb.swp b/lib/racoon/.server.rb.swp new file mode 100644 index 0000000000000000000000000000000000000000..2d4697d62be9ca785584d8fbe605f3999d816432 GIT binary patch literal 20480 zcmeI2ZEPGz8OOIxn?ggNNe_bCv7{64an>4ns(w?;n zSOu;}fjn#NKfK#HbZ~r3W*;5dOSkV?xSoxm|zWF!Xto{(Zmx+%t53hyMK*{aL;VNfZ*!clfGKb%_~$Oi`3`s-d;@$P z1mHpN0k8{PxfRz2PlH7;3#P$`z&IEKcY{&z^jjV0^WeSUmv}+Z0e69&;L0tI^DD3c zYTz#LkDZS5Ja`H$gAan6z{@u~&Y!`7yA2Jjz@@NSh*x5IV7!f)?pioy7g4 zj>?`#Ay3otbj+7sejz+sZSo`wT0xU#e4oxaO8qK{MLs25*XH(eoM=(j;bfGi4nyr7 z7PYxwqeh-lms6SrVMr`NPSr&2Q=SGjkL=;~D$Xo8NZZ|S*M#fH=)w#*lG zuB}hf`uYS-#?gEcLE?GSN~DN)&92rcPlB?)_=;`#4t-7OYDLUN{pP-{u(O=yP zc~q%v#Ch6rSsaJlZHncQq9s0WQJZIV)z+%n)Y?GzsH}H_S}aVVcTt{4zV1=~82etWT}}tO84g+B^&4)<#ExzNa#foh{%z(t3wU3x#FDi)HPZ? z&GL39qoO&O4WTF|zg`S-9Y|f6@5`^|MOACi*$c95n|g`*BH?*6b0_9zYBU@3Xny{r z=av8T<;q;sv!_p=nVFwCYqa+DOr8|x<_(%UdF*3(7GqPkVyl_6uhN2ZPG1|sa^+^bJVRlBYnUM{45)oLT*tE zBUnjEjxVMC_Rxcqr%uA-G=-0H@Z%lnt}_-yrKfI*Fce++Yl*L>o^o7q60BefUcVO` z^jE_cGl*J(MygX7{xoCZlE07oHb3Rf9Cr~+PS6@jm%-$?leG0di9B;hivDB3acE!A ztIG4Fuk&YfHDoH&u#c6g{mD*nRm)D9Q+>)LtdU09LO7FRI#$Qab*uBWj65;tOLa^p zy`)r*LE|yA0>gTWH9JAXJ#UUDf$)Q-Y;q!(I;tmMls8Ai&E8&AuAFD}Ue0~#;7m=o z_M<)Zw)3P!$6C5Zecy8pr#L*wo7=O!hZNBk<68Q)*YEvaL;HvICF=K_g`pJHMVNCa zO^8U!TRAn%n?#cHz~vZe3@ELu66=o2e2pT*9%)mZTlgkf)o?yc+$acP4i|8P6v~@( zeH=U1o6wF*GE&^29U+vvTG7_EQqJ^uch|ZsLq}ksH&mC_XgysgvoKt>UrlRN>4kJx z_R+@XtDtShTAhOrkn^7U4fE@`NiT0t6Q1QsWL`)<_ZZhLE+BnZzhgyGe<+7*Hdpn) zwG0*YU?Xd90yM&Wqh zjg2hU=sjlL`T>DuSyE@sCLZ75^}L~)YUK6G$ayFyq_h>ykl}GpJq9_;!q0K@+aj^Bqr+yKE7eD${ z#NehW%JrW9|HrPVeEygF&12Y)me2oXeE%zWzW*2aJNPwt6v(~*4}%-PZ?K>LZSYkP z00O_qUj8NUIWPs@52~O7{*8V8m%z`!qhJFpgZsc0?C1X#ya0Xyegb|Bz6ZVw?gMv# zXRw$5QSb`(@h^j~fer96up9gkFE1{GHShqK1pmQ){`25ra1ktlyTOmJpZ^%hK^+_c zw}5A{r~eT62$%paxD)IKJHS=!@4pIu04{=YunXJ@o`8m51Rn>5p2ifgU*8-GkUrG9 zGhc+;c#K6Gk7466Y&?eCg{#RZkRocs#$(91H5-qCdoml3;oEo&8;_yxpv Certificate to use (Should be able to easily look this up in the DB) # :receipt_uuid => UUID of the push receipt that was created when the API got the request # :sandbox => Boolean value to use the sandbox servers or not (optional, defaults to false) - # :notification => An ApnServer::Notification object, fully formed. + # :notification => An Racoon::Notification object, fully formed. def handle_job(job) packet = job.ybody project = packet[:project] @@ -87,7 +91,7 @@ def handle_job(job) def get_client(project_name, certificate, sandbox = false) uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com" - @clients[project_name] ||= ApnServer::Client.new(certificate, uri) + @clients[project_name] ||= Racoon::Client.new(certificate, uri) client = @clients[project_name] # If the certificate has changed, but we still are connected using the old certificate, @@ -101,4 +105,4 @@ def get_client(project_name, certificate, sandbox = false) client end end -end \ No newline at end of file +end diff --git a/apnserver.gemspec b/racoon.gemspec similarity index 69% rename from apnserver.gemspec rename to racoon.gemspec index b466ddb..9c2aa5f 100644 --- a/apnserver.gemspec +++ b/racoon.gemspec @@ -1,19 +1,19 @@ Gem::Specification.new do |s| - s.name = %q{apnserver} - s.version = "0.3.0" + s.name = %q{racoon} + s.version = "0.3.1" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] - s.date = %q{2011-03-27} + s.date = %q{2011-04-23} s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment} s.email = %q{jeremy.tregunna@me.com} - s.executables = ["apnsend", "apnserverd"] - s.extra_rdoc_files = ["README.textile"] - s.files = Dir.glob("{bin,lib}/**/*") + %w(README.textile) - s.homepage = %q{https://github.com/jeremytregunna/apnserver} + s.executables = ["racoon-send", "racoond"] + 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{apnserver} + s.rubyforge_project = %q{racoon} s.rubygems_version = %q{1.3.5} s.summary = %q{Apple Push Notification Toolkit for hosted environments} s.test_files = Dir.glob("spec/**/*") From 5e6a60b72f0a82def96e554ae32c1b08346a79a8 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 15:34:54 -0400 Subject: [PATCH 20/98] stale reference to apnserver --- README.mdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.mdown b/README.mdown index 70885a2..75d43ba 100644 --- a/README.mdown +++ b/README.mdown @@ -139,7 +139,7 @@ $ gem install racoon Adding racoon to your Rails application ```ruby -gem 'apnserver' +gem 'racoon' ``` h2. License From f6963fd3bd79cc15f9df6512f1cd291b06e71c43 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 15:35:19 -0400 Subject: [PATCH 21/98] leftover h2. --- README.mdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.mdown b/README.mdown index 75d43ba..9f52b19 100644 --- a/README.mdown +++ b/README.mdown @@ -142,7 +142,7 @@ Adding racoon to your Rails application gem 'racoon' ``` -h2. License +## License (The MIT License) From 7b649e73a111feecdf1ceb3e7961d6f3b447a721 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 15:36:46 -0400 Subject: [PATCH 22/98] pathnames were still showing as apnserver.log/pid --- README.mdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.mdown b/README.mdown index 9f52b19..bb98337 100644 --- a/README.mdown +++ b/README.mdown @@ -55,10 +55,10 @@ Usage: racoond [options] The comma-separated list of ip:port for beanstalk servers --pid - Path used for the PID file. Defaults to /var/run/apnserver.pid + Path used for the PID file. Defaults to /var/run/racoon.pid --log - Path used for the log file. Defaults to /var/log/apnserver.log + Path used for the log file. Defaults to /var/log/racoon.log --help usage message From 056a691c767059ae350124030eedc648002193a6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 16:18:57 -0400 Subject: [PATCH 23/98] Removed stale .swp, now closing sockets after 18 hours, apple doesn't like long opened sockets. --- lib/racoon/.server.rb.swp | Bin 20480 -> 0 bytes lib/racoon/server.rb | 85 ++++++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 21 deletions(-) delete mode 100644 lib/racoon/.server.rb.swp diff --git a/lib/racoon/.server.rb.swp b/lib/racoon/.server.rb.swp deleted file mode 100644 index 2d4697d62be9ca785584d8fbe605f3999d816432..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI2ZEPGz8OOIxn?ggNNe_bCv7{64an>4ns(w?;n zSOu;}fjn#NKfK#HbZ~r3W*;5dOSkV?xSoxm|zWF!Xto{(Zmx+%t53hyMK*{aL;VNfZ*!clfGKb%_~$Oi`3`s-d;@$P z1mHpN0k8{PxfRz2PlH7;3#P$`z&IEKcY{&z^jjV0^WeSUmv}+Z0e69&;L0tI^DD3c zYTz#LkDZS5Ja`H$gAan6z{@u~&Y!`7yA2Jjz@@NSh*x5IV7!f)?pioy7g4 zj>?`#Ay3otbj+7sejz+sZSo`wT0xU#e4oxaO8qK{MLs25*XH(eoM=(j;bfGi4nyr7 z7PYxwqeh-lms6SrVMr`NPSr&2Q=SGjkL=;~D$Xo8NZZ|S*M#fH=)w#*lG zuB}hf`uYS-#?gEcLE?GSN~DN)&92rcPlB?)_=;`#4t-7OYDLUN{pP-{u(O=yP zc~q%v#Ch6rSsaJlZHncQq9s0WQJZIV)z+%n)Y?GzsH}H_S}aVVcTt{4zV1=~82etWT}}tO84g+B^&4)<#ExzNa#foh{%z(t3wU3x#FDi)HPZ? z&GL39qoO&O4WTF|zg`S-9Y|f6@5`^|MOACi*$c95n|g`*BH?*6b0_9zYBU@3Xny{r z=av8T<;q;sv!_p=nVFwCYqa+DOr8|x<_(%UdF*3(7GqPkVyl_6uhN2ZPG1|sa^+^bJVRlBYnUM{45)oLT*tE zBUnjEjxVMC_Rxcqr%uA-G=-0H@Z%lnt}_-yrKfI*Fce++Yl*L>o^o7q60BefUcVO` z^jE_cGl*J(MygX7{xoCZlE07oHb3Rf9Cr~+PS6@jm%-$?leG0di9B;hivDB3acE!A ztIG4Fuk&YfHDoH&u#c6g{mD*nRm)D9Q+>)LtdU09LO7FRI#$Qab*uBWj65;tOLa^p zy`)r*LE|yA0>gTWH9JAXJ#UUDf$)Q-Y;q!(I;tmMls8Ai&E8&AuAFD}Ue0~#;7m=o z_M<)Zw)3P!$6C5Zecy8pr#L*wo7=O!hZNBk<68Q)*YEvaL;HvICF=K_g`pJHMVNCa zO^8U!TRAn%n?#cHz~vZe3@ELu66=o2e2pT*9%)mZTlgkf)o?yc+$acP4i|8P6v~@( zeH=U1o6wF*GE&^29U+vvTG7_EQqJ^uch|ZsLq}ksH&mC_XgysgvoKt>UrlRN>4kJx z_R+@XtDtShTAhOrkn^7U4fE@`NiT0t6Q1QsWL`)<_ZZhLE+BnZzhgyGe<+7*Hdpn) zwG0*YU?Xd90yM&Wqh zjg2hU=sjlL`T>DuSyE@sCLZ75^}L~)YUK6G$ayFyq_h>ykl}GpJq9_;!q0K@+aj^Bqr+yKE7eD${ z#NehW%JrW9|HrPVeEygF&12Y)me2oXeE%zWzW*2aJNPwt6v(~*4}%-PZ?K>LZSYkP z00O_qUj8NUIWPs@52~O7{*8V8m%z`!qhJFpgZsc0?C1X#ya0Xyegb|Bz6ZVw?gMv# zXRw$5QSb`(@h^j~fer96up9gkFE1{GHShqK1pmQ){`25ra1ktlyTOmJpZ^%hK^+_c zw}5A{r~eT62$%paxD)IKJHS=!@4pIu04{=YunXJ@o`8m51Rn>5p2ifgU*8-GkUrG9 zGhc+;c#K6Gk7466Y&?eCg{#RZkRocs#$(91H5-qCdoml3;oEo&8;_yxpv e - Config.logger.error "(Feedback) Unable to handle: #{e}" + rescue Beanstalk::TimedOut + Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." + end + end + + EventMachine::PeriodicTimer.new(60) do + begin + if beanstalk('killer').peek_ready + item = beanstalk('killer').reserve(1) + purge_client(item) + end + rescue Beanstalk::TimedOut + Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." end end EventMachine::PeriodicTimer.new(1) do begin - if beanstalk.peek_ready - item = beanstalk.reserve(1) + if beanstalk('apns').peek_ready + item = beanstalk('apns').reserve(1) handle_job item end rescue Beanstalk::TimedOut @@ -62,7 +68,7 @@ def handle_job(job) packet = job.ybody project = packet[:project] if notification = Notification.new.create_payload(packet[:notification]) - client = get_client(project.name, project.certificate, packet[:sandbox]) + client = get_client(project[:name], project[:certificate], packet[:sandbox]) begin client.connect! unless client.connected? client.write(notification) @@ -89,8 +95,37 @@ def handle_job(job) end end + # Will be a hash with two keys: + # :certificate and :sandbox. + def handle_feedback(job) + begin + packet = job.ybody + uri = "feedback.#{packet[:sandbox] ? 'sandbox.' : ''}push.apple.com" + feedback_client = Racoon::FeedbackClient.new(packet[:certificate], uri) + feedback_client.connect! + feedback_client.read.each do |record| + feedback_callback.call record + end + feedback_client.disconnect! + job.delete + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + Config.logger.error "(Feedback) Caught Error, closing connection" + feedback_client.disconnect! + job.release + rescue RuntimeError => e + Config.logger.error "(Feedback) Unable to handle: #{e}" + job.delete + end + end + def get_client(project_name, certificate, sandbox = false) uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com" + unless @clients[project_name] + @clients[project_name] = Racoon::Client.new(certificate, uri) + # in 18 hours (64800 seconds) we need to schedule this socket to be killed. Long opened + # sockets don't work. + beanstalk('killer').yput({:certificate => certificate, :sandbox => sandbox}, 65536, 64800) + end @clients[project_name] ||= Racoon::Client.new(certificate, uri) client = @clients[project_name] @@ -98,11 +133,19 @@ def get_client(project_name, certificate, sandbox = false) # disconnect and reconnect. unless client.pem.eql?(certificate) client.disconnect! if client.connected? - @clients[project_name] = ApnServer::Client.new(certificate, uri) + @clients[project_name] = Racoon::Client.new(certificate, uri) client = @clients[project_name] end client end + + def purge_client(job) + project_name = job.ybody + client = @clients[project_name] + client.disconnect! if client + @clients[project_name] = nil + job.delete + end end end From 5a166801f04174ed0150309ca9a6808ed66e07db Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 16:24:19 -0400 Subject: [PATCH 24/98] README.textile reference left over in Rakefile --- Rakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 279fcde..56973fe 100644 --- a/Rakefile +++ b/Rakefile @@ -2,9 +2,9 @@ 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 task :default => [:spec] From 3925b35c8f85f53e8aca0de5ad9024e5f6038366 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 20:48:39 +0000 Subject: [PATCH 25/98] added godfile, fixed an exception in server, wrong require in notification --- lib/racoon/notification.rb | 2 +- lib/racoon/server.rb | 13 +++++++------ racoon.god | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 racoon.god diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index 6149c2c..245aaab 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -1,4 +1,4 @@ -require 'apnserver/payload' +require 'racoon/payload' require 'base64' require 'yajl' diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 11b65c3..a798e50 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -2,21 +2,22 @@ module Racoon class Server - attr_accessor :beanstalkd_uris, :feedback_callback def initialize(beanstalkd_uris = ["127.0.0.1:11300"], &feedback_blk) + @beanstalks = {} @clients = {} @feedback_callback = feedback_blk @beanstalkd_uris = beanstalkd_uris end def beanstalk(arg) - return @beanstalks[arg] if @beanstalks[arg] - @beanstalks[arg] = Beanstalk::Pool.new @beanstalkd_uris - %w{watch use}.each { |s| @beanstalks[arg].send(s, "racoon-#{arg}") } - @beanstalks[arg].ignore('default') - @beanstalks[arg] + tube = "racoon-#{arg}" + return @beanstalks[tube] if @beanstalks[tube] + @beanstalks[tube] = Beanstalk::Pool.new @beanstalkd_uris + %w{watch use}.each { |s| @beanstalks[tube].send(s, "racoon-#{tube}") } + @beanstalks[tube].ignore('default') + @beanstalks[tube] end def start! diff --git a/racoon.god b/racoon.god new file mode 100644 index 0000000..37cdbaa --- /dev/null +++ b/racoon.god @@ -0,0 +1,36 @@ +# Godfile for racoon + +[ 1 ].each do |instance| + name = "racoon-#{instance}" + pid_file = "/var/run/#{name}.pid" + + God.watch do |w| + w.name = name + w.interval = 30.seconds + w.start = "/fracas/deploy/racoon/bin/racoond -d --beanstalk 127.0.0.1:11300 --pid #{pid_file} --log /var/log/#{name}.log" + w.stop = "kill -15 `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 From 6c7f9b7e5046dea63e8901df21eb072af1ed716e Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 17:59:20 -0400 Subject: [PATCH 26/98] cleanup of handle_job --- lib/racoon/notification.rb | 3 ++- lib/racoon/server.rb | 42 +++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index 245aaab..dc72cc2 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -11,7 +11,8 @@ class Notification def payload p = Hash.new [:badge, :alert, :sound, :custom].each do |k| - p[k] = send(k) if send(k) + r = send(k) + p[k] = r if r end create_payload(p) end diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index a798e50..80b7780 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -59,38 +59,46 @@ def start! private - # Received a notification. The job's body should be a YAML encoded hash containing the following keys: - # :project_name => The name of the project - # :certificate => Certificate to use (Should be able to easily look this up in the DB) - # :receipt_uuid => UUID of the push receipt that was created when the API got the request - # :sandbox => Boolean value to use the sandbox servers or not (optional, defaults to false) - # :notification => An Racoon::Notification object, fully formed. + # Received a notification. job is YAML encoded hash in the following format: + # job = { + # :project => { + # :name => "Foo", + # :certificate => "contents of a certificate.pem" + # }, + # :device_token => "0f21ab...def", + # :notification => notification.json_payload, + # :sandbox => true # Development environment? + # } def handle_job(job) packet = job.ybody project = packet[:project] - if notification = Notification.new.create_payload(packet[:notification]) + + aps = packet[:notification][:aps] + + notification = Notification.new + 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 + + if notification client = get_client(project[:name], project[:certificate], packet[:sandbox]) begin client.connect! unless client.connected? client.write(notification) + job.delete - # TODO: Find the receipt and update the sent_at property. - #if receipt = PushLog[packet[:receipt_uuid]] - # receipt.sent_at = Time.now.to_i.to_s - # receipt.save - #end rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET Config.logger.error "Caught Error, closing connecting and adding notification back to queue" + client.disconnect! + # Queue back up the notification job.release rescue RuntimeError => e Config.logger.error "Unable to handle: #{e}" - # TODO: Find the receipt and write the failed_at property. - #if receipt = PushLog[packet[:receipt_uuid]] - # receipt.failed_at = Time.now.to_i.to_s - # receipt.save - #end + job.delete end end From f3d5aa8188289b1084b676a2e3985d2cf713da26 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 18:01:48 -0400 Subject: [PATCH 27/98] mistake in notification hash example --- README.mdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.mdown b/README.mdown index bb98337..a937e37 100644 --- a/README.mdown +++ b/README.mdown @@ -108,8 +108,8 @@ An example notification may look like this: notification = { :device_token => "hex encoded device token", :aps => { :alert => "Some text", :sound => "Audio_file", - :badge => 42 }, - :a_custom_key => "Any number of custom keys" + :badge => 42, + :custom => "custom data, could be hash" } } ``` From 06cfffe7aac92a03775d6f47cf3aa2eb94a19ce7 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 18:20:04 -0400 Subject: [PATCH 28/98] reflecting custom fields a little better --- README.mdown | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.mdown b/README.mdown index a937e37..b4987c0 100644 --- a/README.mdown +++ b/README.mdown @@ -109,7 +109,9 @@ notification = { :device_token => "hex encoded device token", :aps => { :alert => "Some text", :sound => "Audio_file", :badge => 42, - :custom => "custom data, could be hash" } + :custom => { :field => "lala", + :stuff => 42 } + } } ``` From 2842e1e1b50d7986bf9f50f245e4e0737383abc6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sat, 23 Apr 2011 23:27:14 -0400 Subject: [PATCH 29/98] gem building --- Rakefile | 11 +++++++++++ lib/racoon.rb | 4 ++++ racoon.gemspec | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 56973fe..4bd6dac 100644 --- a/Rakefile +++ b/Rakefile @@ -7,4 +7,15 @@ Rake::RDocTask.new do |rd| 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/lib/racoon.rb b/lib/racoon.rb index fd93181..4e922c9 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -4,3 +4,7 @@ require 'racoon/notification' require 'racoon/client' require 'racoon/feedback_client' + +module Racoon + VERSION = "0.3.1" +end diff --git a/racoon.gemspec b/racoon.gemspec index 9c2aa5f..ae80680 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.date = %q{2011-04-23} s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment} s.email = %q{jeremy.tregunna@me.com} - s.executables = ["racoon-send", "racoond"] + s.executables = ["racoond"] s.extra_rdoc_files = ["README.mdown"] s.files = Dir.glob("{bin,lib}/**/*") + %w(README.mdown) s.homepage = %q{https://github.com/jeremytregunna/racoon} From 42bebf1c8e030e4b2c522941eadec75a00035664 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 12:41:04 -0400 Subject: [PATCH 30/98] changed periodic killing to 20 minutes of inactivity rather than static 18 hours --- lib/racoon/server.rb | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 80b7780..1ccfd4c 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -33,14 +33,19 @@ def start! end end + # Every minute,poll all the clients, ensuring they've been inactive for 20+ minutes. EventMachine::PeriodicTimer.new(60) do - begin - if beanstalk('killer').peek_ready - item = beanstalk('killer').reserve(1) - purge_client(item) + remove_clients = [] + + @clients.each_pair do |project_name, packet| + if Time.now - packet[:timestamp] >= 1200 # 20 minutes + packet[:connection].disconnect! + remove_clients << project_name end - rescue Beanstalk::TimedOut - Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." + end + + remove_clients.each do |project_name| + @clients[project_name] = nil end end @@ -84,15 +89,17 @@ def handle_job(job) if notification client = get_client(project[:name], project[:certificate], packet[:sandbox]) + connection = client[:connection] begin - client.connect! unless client.connected? - client.write(notification) + connection.connect! unless connection.connected? + connection.write(notification) + @clients[project[:name]][:timestamp] = Time.now job.delete rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET Config.logger.error "Caught Error, closing connecting and adding notification back to queue" - client.disconnect! + connection.disconnect! # Queue back up the notification job.release @@ -127,22 +134,22 @@ def handle_feedback(job) end end + # Returns a hash containing a timestamp referring to when the connection was opened. + # This timestamp will be updated to reflect when there was last activity over the socket. def get_client(project_name, certificate, sandbox = false) uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com" unless @clients[project_name] - @clients[project_name] = Racoon::Client.new(certificate, uri) - # in 18 hours (64800 seconds) we need to schedule this socket to be killed. Long opened - # sockets don't work. - beanstalk('killer').yput({:certificate => certificate, :sandbox => sandbox}, 65536, 64800) + @clients[project_name] = { :timestamp => Time.now, :connection => Racoon::Client.new(certificate, uri) } end - @clients[project_name] ||= Racoon::Client.new(certificate, uri) + @clients[project_name] ||= { :timestamp => Time.now, :connection => Racoon::Client.new(certificate, uri) } client = @clients[project_name] + connection = client[:connection] # If the certificate has changed, but we still are connected using the old certificate, # disconnect and reconnect. - unless client.pem.eql?(certificate) - client.disconnect! if client.connected? - @clients[project_name] = Racoon::Client.new(certificate, uri) + unless connection.pem.eql?(certificate) + connection.disconnect! if connection.connected? + @clients[project_name] = { :timestamp => Time.now, :connection => Racoon::Client.new(certificate, uri) } client = @clients[project_name] end From 12137856a8a2f333002dfd0b5c8cb2c087183031 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 20:57:28 +0000 Subject: [PATCH 31/98] fixed glitch in server --- lib/racoon/client.rb | 6 ++++-- lib/racoon/server.rb | 16 ++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/racoon/client.rb b/lib/racoon/client.rb index 0fad232..6351fd7 100644 --- a/lib/racoon/client.rb +++ b/lib/racoon/client.rb @@ -31,7 +31,9 @@ def disconnect! end def write(notification) - Config.logger.debug "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}" + if host.include? "sandbox" + Config.logger.debug "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}" + end @ssl.write(notification.to_bytes) end @@ -39,4 +41,4 @@ def connected? @ssl end end -end \ No newline at end of file +end diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 1ccfd4c..bff4ca6 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -15,8 +15,6 @@ def beanstalk(arg) tube = "racoon-#{arg}" return @beanstalks[tube] if @beanstalks[tube] @beanstalks[tube] = Beanstalk::Pool.new @beanstalkd_uris - %w{watch use}.each { |s| @beanstalks[tube].send(s, "racoon-#{tube}") } - @beanstalks[tube].ignore('default') @beanstalks[tube] end @@ -24,8 +22,11 @@ def start! EventMachine::run do EventMachine::PeriodicTimer.new(3600) do begin - if beanstalk('feedback').peek_ready - item = beanstalk('feedback').reserve(1) + b = beanstalk('feedback') + %w{watch use}.each { |s| b.send(s, "racoon-feedback") } + b.ignore('default') + if b.peek_ready + item = b.reserve(1) handle_feedback(item) end rescue Beanstalk::TimedOut @@ -51,8 +52,11 @@ def start! EventMachine::PeriodicTimer.new(1) do begin - if beanstalk('apns').peek_ready - item = beanstalk('apns').reserve(1) + b = beanstalk('apns') + %w{watch use}.each { |s| b.send(s, "racoon-apns") } + b.ignore('default') + if b.peek_ready + item = b.reserve(1) handle_job item end rescue Beanstalk::TimedOut From 0c3b09f4c778590166ca1d33d734b514621278e6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 16:57:43 -0400 Subject: [PATCH 32/98] added racoon-send --- Gemfile.lock | 17 +++++++++++++++ bin/{apnsend => racoon-send} | 41 ++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 20 deletions(-) rename bin/{apnsend => racoon-send} (63%) diff --git a/Gemfile.lock b/Gemfile.lock index 74704f3..10e8004 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,20 @@ GEM remote: http://rubygems.org/ specs: + beanstalk-client (1.1.0) + cassandra (0.9.1) + json + rake + simple_uuid (>= 0.1.0) + thrift_client (>= 0.6.0) daemons (1.0.10) diff-lcs (1.1.2) eventmachine (0.12.10) + fracassandra (0.3.4) + cassandra + cassandra + json (1.5.1) + rake (0.8.7) rspec (2.3.0) rspec-core (~> 2.3.0) rspec-expectations (~> 2.3.0) @@ -12,13 +23,19 @@ GEM rspec-expectations (2.3.0) diff-lcs (~> 1.1.2) rspec-mocks (2.3.0) + simple_uuid (0.1.2) + thrift (0.5.0) + thrift_client (0.6.0) + thrift (~> 0.5.0) yajl-ruby (0.8.1) PLATFORMS ruby DEPENDENCIES + beanstalk-client daemons eventmachine + fracassandra (>= 0.3.4) rspec yajl-ruby diff --git a/bin/apnsend b/bin/racoon-send similarity index 63% rename from bin/apnsend rename to bin/racoon-send index f39ef97..beb12d6 100755 --- a/bin/apnsend +++ b/bin/racoon-send @@ -4,16 +4,17 @@ $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 " --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 +25,30 @@ def usage end opts = GetoptLong.new( - ["--server", "-s", GetoptLong::REQUIRED_ARGUMENT], - ["--port", "-p", GetoptLong::REQUIRED_ARGUMENT], + ["--beanstalk", "-b", 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"] +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 '--pem' - ApnServer::Config.pem = File.read(arg) - when '--pem-passphrase' - ApnServer::Config.password = arg + certificate = File.read(arg) when '--alert' notification.alert = arg when '--sound' @@ -59,11 +56,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 +68,9 @@ if notification.device_token.nil? usage exit else - notification.push + bs = Beanstalk::Pool.new beanstalks + %w{use watch}.each { |s| bs.send(s, "racoon-apns") } + bs.ignore("default") + project = { :name => "test", :certificate => certificate } + bs.yput({ :project => project, :notification => notification.payload, :device_token => notification.device_token, :sandbox => true }) end From f53252f3c1025b0a14fdee2f64db50ef06d606d6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 17:01:13 -0400 Subject: [PATCH 33/98] version bump --- README.mdown | 18 ++++++++++++------ lib/racoon.rb | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.mdown b/README.mdown index b4987c0..8bd3399 100644 --- a/README.mdown +++ b/README.mdown @@ -34,14 +34,14 @@ You can see progress by looking at the [issue tracker](https://www.pivotaltracke ## 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 +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 +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 @@ -80,7 +80,7 @@ the following method: def beanstalk return @beanstalk if @beanstalk @beanstalk = Beanstalk::Pool.new ["127.0.0.1:11300"] - @beanstalk.use "awesome-tube" + @beanstalk.use "racoon-apns" @beanstalk end ``` @@ -90,6 +90,9 @@ the appropriate tube whether the connection is open yet or not. We will also need two pieces of information: A project, and a notification. +**Note that the tube should always be "racoon-apns". The server won't look on any other tube for +push notifications.** + ### Project A project os comprised of a few pieces of information at a minimum (you may supply more if you @@ -105,8 +108,7 @@ A notification is a ruby hash containing the things to be sent along, including An example notification may look like this: ```ruby -notification = { :device_token => "hex encoded device token", - :aps => { :alert => "Some text", +notification = { :aps => { :alert => "Some text", :sound => "Audio_file", :badge => 42, :custom => { :field => "lala", @@ -118,7 +120,11 @@ notification = { :device_token => "hex encoded device token", Finally within we can send a push notification using the following code: ```ruby -beanstalk.yput({ :project => project, :notification => notification, :sandbox => true }) +beanstalk.yput({ :project => project, + :notification => notification, + :device_token => "binary encoded token", + :sandbox => true +}) ``` Note that the `sandbox` parameter is used to indicate whether or not we're using a development diff --git a/lib/racoon.rb b/lib/racoon.rb index 4e922c9..5f506a9 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -6,5 +6,5 @@ require 'racoon/feedback_client' module Racoon - VERSION = "0.3.1" + VERSION = "0.3.2" end From 68898684501698b1a755e5d9ef64c35a2b1c33da Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 17:01:13 -0400 Subject: [PATCH 34/98] version bump --- README.mdown | 18 ++++++++++++------ lib/racoon.rb | 2 +- racoon.gemspec | 6 +++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.mdown b/README.mdown index b4987c0..8bd3399 100644 --- a/README.mdown +++ b/README.mdown @@ -34,14 +34,14 @@ You can see progress by looking at the [issue tracker](https://www.pivotaltracke ## 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 +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 +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 @@ -80,7 +80,7 @@ the following method: def beanstalk return @beanstalk if @beanstalk @beanstalk = Beanstalk::Pool.new ["127.0.0.1:11300"] - @beanstalk.use "awesome-tube" + @beanstalk.use "racoon-apns" @beanstalk end ``` @@ -90,6 +90,9 @@ the appropriate tube whether the connection is open yet or not. We will also need two pieces of information: A project, and a notification. +**Note that the tube should always be "racoon-apns". The server won't look on any other tube for +push notifications.** + ### Project A project os comprised of a few pieces of information at a minimum (you may supply more if you @@ -105,8 +108,7 @@ A notification is a ruby hash containing the things to be sent along, including An example notification may look like this: ```ruby -notification = { :device_token => "hex encoded device token", - :aps => { :alert => "Some text", +notification = { :aps => { :alert => "Some text", :sound => "Audio_file", :badge => 42, :custom => { :field => "lala", @@ -118,7 +120,11 @@ notification = { :device_token => "hex encoded device token", Finally within we can send a push notification using the following code: ```ruby -beanstalk.yput({ :project => project, :notification => notification, :sandbox => true }) +beanstalk.yput({ :project => project, + :notification => notification, + :device_token => "binary encoded token", + :sandbox => true +}) ``` Note that the `sandbox` parameter is used to indicate whether or not we're using a development diff --git a/lib/racoon.rb b/lib/racoon.rb index 4e922c9..5f506a9 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -6,5 +6,5 @@ require 'racoon/feedback_client' module Racoon - VERSION = "0.3.1" + VERSION = "0.3.2" end diff --git a/racoon.gemspec b/racoon.gemspec index ae80680..84af27f 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,13 +1,13 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "0.3.1" + s.version = "0.3.2" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] - s.date = %q{2011-04-23} + s.date = %q{2011-04-24} s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment} s.email = %q{jeremy.tregunna@me.com} - s.executables = ["racoond"] + s.executables = ["racoon-send", "racoond"] s.extra_rdoc_files = ["README.mdown"] s.files = Dir.glob("{bin,lib}/**/*") + %w(README.mdown) s.homepage = %q{https://github.com/jeremytregunna/racoon} From 9d0f6dd61c06eafa64b4197dd553c5b28adb1b8e Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 17:21:59 -0400 Subject: [PATCH 35/98] Removed unneeded gems from Gemfile --- Gemfile | 1 - Gemfile.lock | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/Gemfile b/Gemfile index 5c79005..8c47f4f 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ gem "eventmachine" gem "daemons" gem "yajl-ruby" gem "beanstalk-client" -gem "fracassandra", ">= 0.3.4" group :spec do gem "rspec" diff --git a/Gemfile.lock b/Gemfile.lock index 10e8004..9420740 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,19 +2,9 @@ GEM remote: http://rubygems.org/ specs: beanstalk-client (1.1.0) - cassandra (0.9.1) - json - rake - simple_uuid (>= 0.1.0) - thrift_client (>= 0.6.0) daemons (1.0.10) diff-lcs (1.1.2) eventmachine (0.12.10) - fracassandra (0.3.4) - cassandra - cassandra - json (1.5.1) - rake (0.8.7) rspec (2.3.0) rspec-core (~> 2.3.0) rspec-expectations (~> 2.3.0) @@ -23,10 +13,6 @@ GEM rspec-expectations (2.3.0) diff-lcs (~> 1.1.2) rspec-mocks (2.3.0) - simple_uuid (0.1.2) - thrift (0.5.0) - thrift_client (0.6.0) - thrift (~> 0.5.0) yajl-ruby (0.8.1) PLATFORMS @@ -36,6 +22,5 @@ DEPENDENCIES beanstalk-client daemons eventmachine - fracassandra (>= 0.3.4) rspec yajl-ruby From 772b46d3415652f2859ee2111c532bb1760c281c Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 17:37:53 -0400 Subject: [PATCH 36/98] Removed some code which doesn't make sense for a multiproject apns --- lib/racoon/notification.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index dc72cc2..522e76e 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -23,21 +23,6 @@ def json_payload j end -=begin - 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 = Racoon::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195) - client.connect! - client.write(self) - client.disconnect! - end - end -=end - def to_bytes j = json_payload [0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*") From c7228c22eee9d152ced0ac7d510cc253f50a41c0 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 17:38:48 -0400 Subject: [PATCH 37/98] fixed typographical error indicating json_payload when it should have indicated payload --- lib/racoon/server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index bff4ca6..121d410 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -75,7 +75,7 @@ def start! # :certificate => "contents of a certificate.pem" # }, # :device_token => "0f21ab...def", - # :notification => notification.json_payload, + # :notification => notification.payload, # :sandbox => true # Development environment? # } def handle_job(job) From 62ddd08c7883e9e6067e317b3c57734d86063c7f Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 24 Apr 2011 17:39:11 -0400 Subject: [PATCH 38/98] added send_at accessor to notification, updated docs accordingly --- README.mdown | 5 +++++ lib/racoon/notification.rb | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.mdown b/README.mdown index 8bd3399..6c5dc8f 100644 --- a/README.mdown +++ b/README.mdown @@ -122,11 +122,16 @@ Finally within we can send a push notification using the following code: ```ruby beanstalk.yput({ :project => project, :notification => notification, + :send_at => Time.mktime(2012, 12, 21, 4, 15) :device_token => "binary encoded token", :sandbox => true }) ``` +This will construct a notification which is to be scheduled to be delivered on December 21st, +2012 at 4:15 AM localtime to the server. That is, if the server's timezone is UTC, you must +account for that in your client application. + Note that the `sandbox` parameter is used to indicate whether or not we're using a development certificate, and as such, should contact Apple's sandbox APN service instead of the production certificate. If left out, we assume production. diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index 522e76e..cdebf0b 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -6,7 +6,11 @@ module Racoon class Notification include Racoon::Payload - attr_accessor :device_token, :alert, :badge, :sound, :custom + attr_accessor :device_token, :alert, :badge, :sound, :custom, :send_at + + def initialize + @send_at = Time.now + end def payload p = Hash.new From 7c7b3734c3f17f8f0229a8a59da21e81c28ab0c5 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Mon, 25 Apr 2011 00:41:11 -0400 Subject: [PATCH 39/98] formatting --- bin/racoon-send | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/racoon-send b/bin/racoon-send index beb12d6..15891ba 100755 --- a/bin/racoon-send +++ b/bin/racoon-send @@ -72,5 +72,9 @@ else %w{use watch}.each { |s| bs.send(s, "racoon-apns") } bs.ignore("default") project = { :name => "test", :certificate => certificate } - bs.yput({ :project => project, :notification => notification.payload, :device_token => notification.device_token, :sandbox => true }) + bs.yput({ :project => project, + :notification => notification.payload, + :device_token => notification.device_token, + :sandbox => true + }) end From 24c2ede578aa5564ea4247e47dc1b529f466900e Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Mon, 25 Apr 2011 15:24:40 -0400 Subject: [PATCH 40/98] corrected pivotal url --- README.mdown | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.mdown b/README.mdown index 6c5dc8f..8da6c37 100644 --- a/README.mdown +++ b/README.mdown @@ -28,8 +28,7 @@ persistent connection to Apple. ## Remaining Tasks & Issues -You can see progress by looking at the [issue tracker](https://www.pivotaltracker.com/projects/251991) page for fracas. Any labels related to -*apnserver* or *racoon* are related to this subproject. +You can see progress by looking at the [issue tracker](https://www.pivotaltracker.com/projects/279053). ## Preparing Certificates From bef4364d8b0ec4777cf5e69df6bcabb9f8dda6a0 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 26 Apr 2011 13:24:28 -0400 Subject: [PATCH 41/98] now support the enhanced push notification format. No longer the basic format --- lib/racoon/notification.rb | 25 ++++++------ lib/racoon/server.rb | 80 ++++++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index cdebf0b..7af7519 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -6,9 +6,10 @@ module Racoon class Notification include Racoon::Payload - attr_accessor :device_token, :alert, :badge, :sound, :custom, :send_at + attr_accessor :identifier, :expiry, :device_token, :alert, :badge, :sound, :custom, :send_at, :expiry def initialize + @expiry = 0 @send_at = Time.now end @@ -29,7 +30,7 @@ def json_payload def to_bytes j = json_payload - [0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*") + [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j].pack("cNNna*na*") end def self.valid?(p) @@ -51,23 +52,25 @@ 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 + 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] - # parse token - notification.device_token = buffer.slice!(0, 32).unpack('a*').first + # device token + notification.device_token = buffer.slice!(0, 32).unpack("a*").first - # parse json payload - payload_len = buffer.slice!(0, 2).unpack('CC') + # 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') + result.delete("aps") notification.custom = result notification diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 121d410..5a8561a 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -50,15 +50,17 @@ def start! end end - EventMachine::PeriodicTimer.new(1) do + EventMachine::PeriodicTimer.new(2) do begin b = beanstalk('apns') %w{watch use}.each { |s| b.send(s, "racoon-apns") } b.ignore('default') - if b.peek_ready + jobs = [] + until b.peek_ready.nil? item = b.reserve(1) - handle_job item + jobs << item end + handle_jobs jobs if jobs.count > 0 rescue Beanstalk::TimedOut Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." end @@ -78,39 +80,44 @@ def start! # :notification => notification.payload, # :sandbox => true # Development environment? # } - def handle_job(job) - packet = job.ybody - project = packet[:project] + def handle_jobs(jobs) + connections = {} + jobs.each do |job| + packet = job.ybody + project = packet[:project] - aps = packet[:notification][:aps] + client = get_client(project[:name], project[:certificate], packet[:sandbox]) + conn = client[:connection] + connections[conn] ||= [] - notification = Notification.new - 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 = create_notification_from_packet(packet) - if notification - client = get_client(project[:name], project[:certificate], packet[:sandbox]) - connection = client[:connection] - begin - connection.connect! unless connection.connected? - connection.write(notification) - @clients[project[:name]][:timestamp] = Time.now + connections[conn] << { :job => job, :notification => notification } + end + + connections.each_pair do |conn, tasks| + conn.connect! unless conn.connected? + tasks.each do |data| + job = data[:job] + notif = data[:notification] - job.delete - rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - Config.logger.error "Caught Error, closing connecting and adding notification back to queue" + begin + conn.write(notif) + @clients[project[:name]][:timestamp] = Time.now - connection.disconnect! + # TODO: Listen for error responses from Apple + job.delete + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + Config.logger.error "Caught error, closing connection and adding notification back to queue" - # Queue back up the notification - job.release - rescue RuntimeError => e - Config.logger.error "Unable to handle: #{e}" + connection.disconnect! - job.delete + job.release + rescue RuntimeError => e + Config.logger.error "Unable to handle: #{e}" + + job.delete + end end end end @@ -167,5 +174,20 @@ def purge_client(job) @clients[project_name] = nil job.delete end + + def create_notification_from_packet(packet) + aps = packet[:notification][:aps] + + notification = Notification.new + notification.identifier = packet[:identifier] + notification.expiry = packet[:expiry] + 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 From e74f423de40136e54aac71f0bc39c779344212c6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 26 Apr 2011 13:24:49 -0400 Subject: [PATCH 42/98] version bump --- lib/racoon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index 5f506a9..34608fe 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -6,5 +6,5 @@ require 'racoon/feedback_client' module Racoon - VERSION = "0.3.2" + VERSION = "0.4.0" end From a0659cedb05db7d648b165306c1bb6018ab0cd85 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 26 Apr 2011 13:24:49 -0400 Subject: [PATCH 43/98] version bump --- lib/racoon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index 5f506a9..34608fe 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -6,5 +6,5 @@ require 'racoon/feedback_client' module Racoon - VERSION = "0.3.2" + VERSION = "0.4.0" end From 18ac2f7259eefa543665b239a77a75aa7954fb4b Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Tue, 26 Apr 2011 17:15:45 -0400 Subject: [PATCH 44/98] moved create_notification_from_packet to Notification, renamed it create_from_packet --- lib/racoon/notification.rb | 15 +++++++++++++++ lib/racoon/server.rb | 17 +---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index 7af7519..e166691 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -75,5 +75,20 @@ def self.parse(p) notification end + + def self.create_from_packet(packet) + aps = packet[:notification][:aps] + + notification = Notification.new + notification.identifier = packet[:identifier] + notification.expiry = packet[:expiry] + 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/racoon/server.rb b/lib/racoon/server.rb index 5a8561a..19927d9 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -90,7 +90,7 @@ def handle_jobs(jobs) conn = client[:connection] connections[conn] ||= [] - notification = create_notification_from_packet(packet) + notification = Notification.create_from_packet(packet) connections[conn] << { :job => job, :notification => notification } end @@ -174,20 +174,5 @@ def purge_client(job) @clients[project_name] = nil job.delete end - - def create_notification_from_packet(packet) - aps = packet[:notification][:aps] - - notification = Notification.new - notification.identifier = packet[:identifier] - notification.expiry = packet[:expiry] - 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 From c4ffbb7dfa4df6848389f198ca2393b25d252147 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 18:41:28 -0400 Subject: [PATCH 45/98] after talking with apple, we were doing it right before. don't kill the socket, reconnect if we disconnect in the interim. --- lib/racoon/server.rb | 86 +++++++++++++------------------------------- 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 19927d9..7fbcceb 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -34,33 +34,15 @@ def start! end end - # Every minute,poll all the clients, ensuring they've been inactive for 20+ minutes. - EventMachine::PeriodicTimer.new(60) do - remove_clients = [] - - @clients.each_pair do |project_name, packet| - if Time.now - packet[:timestamp] >= 1200 # 20 minutes - packet[:connection].disconnect! - remove_clients << project_name - end - end - - remove_clients.each do |project_name| - @clients[project_name] = nil - end - end - - EventMachine::PeriodicTimer.new(2) do + EventMachine::PeriodicTimer.new(1) do begin b = beanstalk('apns') %w{watch use}.each { |s| b.send(s, "racoon-apns") } b.ignore('default') - jobs = [] - until b.peek_ready.nil? + if b.peek_ready item = b.reserve(1) - jobs << item + handle_job item end - handle_jobs jobs if jobs.count > 0 rescue Beanstalk::TimedOut Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." end @@ -80,44 +62,30 @@ def start! # :notification => notification.payload, # :sandbox => true # Development environment? # } - def handle_jobs(jobs) - connections = {} - jobs.each do |job| - packet = job.ybody - project = packet[:project] + def handle_job(jobs) + packet = job.ybody + project = packet[:project] - client = get_client(project[:name], project[:certificate], packet[:sandbox]) - conn = client[:connection] - connections[conn] ||= [] - - notification = Notification.create_from_packet(packet) - - connections[conn] << { :job => job, :notification => notification } - end + notification = Notification.create_from_packet(packet) - connections.each_pair do |conn, tasks| - conn.connect! unless conn.connected? - tasks.each do |data| - job = data[:job] - notif = data[:notification] + if notification + client = get_client(project[:name], project[:certificate], packet[:sandbox]) - begin - conn.write(notif) - @clients[project[:name]][:timestamp] = Time.now + begin + client.write(notification) - # TODO: Listen for error responses from Apple - job.delete - rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - Config.logger.error "Caught error, closing connection and adding notification back to queue" + # TODO: Listen for error responses from Apple + job.delete + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + Config.logger.error "Caught error, closing connection and adding notification back to queue" - connection.disconnect! + client.disconnect! - job.release - rescue RuntimeError => e - Config.logger.error "Unable to handle: #{e}" + job.release + rescue RuntimeError => e + Config.logger.error "Unable to handle: #{e}" - job.delete - end + job.delete end end end @@ -145,22 +113,16 @@ def handle_feedback(job) end end - # Returns a hash containing a timestamp referring to when the connection was opened. - # This timestamp will be updated to reflect when there was last activity over the socket. def get_client(project_name, certificate, sandbox = false) uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com" - unless @clients[project_name] - @clients[project_name] = { :timestamp => Time.now, :connection => Racoon::Client.new(certificate, uri) } - end - @clients[project_name] ||= { :timestamp => Time.now, :connection => Racoon::Client.new(certificate, uri) } + @clients[project_name] ||= Racoon::Client.new(certificate, uri) client = @clients[project_name] - connection = client[:connection] # If the certificate has changed, but we still are connected using the old certificate, # disconnect and reconnect. - unless connection.pem.eql?(certificate) - connection.disconnect! if connection.connected? - @clients[project_name] = { :timestamp => Time.now, :connection => Racoon::Client.new(certificate, uri) } + unless client.pem.eql?(certificate) + client.disconnect! if client.connected? + @clients[project_name] = Racoon::Client.new(certificate, uri) client = @clients[project_name] end From 1601b5ffc7a7794772edaab6d385494846381698 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 18:44:13 -0400 Subject: [PATCH 46/98] typo --- lib/racoon/server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 7fbcceb..bccad7a 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -62,7 +62,7 @@ def start! # :notification => notification.payload, # :sandbox => true # Development environment? # } - def handle_job(jobs) + def handle_job(job) packet = job.ybody project = packet[:project] From 34a0833bd33f041c3dfd0d3849820db5b76f3367 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 18:45:36 -0400 Subject: [PATCH 47/98] debugging --- lib/racoon/notification.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index e166691..35817ec 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -30,6 +30,7 @@ def json_payload def to_bytes j = json_payload + p [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j] [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j].pack("cNNna*na*") end From f387ad2bf0df1dd6fcde825ec60c67cf9b05044f Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 18:45:36 -0400 Subject: [PATCH 48/98] debugging --- lib/racoon/notification.rb | 1 + lib/racoon/server.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index e166691..35817ec 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -30,6 +30,7 @@ def json_payload def to_bytes j = json_payload + p [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j] [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j].pack("cNNna*na*") end diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index bccad7a..1a76496 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -72,6 +72,7 @@ def handle_job(job) client = get_client(project[:name], project[:certificate], packet[:sandbox]) begin + p notification client.write(notification) # TODO: Listen for error responses from Apple From b92b05235d17e107aa5c76d5d6b4221daeaaf77b Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 18:59:40 -0400 Subject: [PATCH 49/98] urgh --- lib/racoon/server.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 6d6193d..66463fd 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -72,6 +72,7 @@ def handle_job(job) client = get_client(project[:name], project[:certificate], packet[:sandbox]) begin + p client p notification client.write(notification) @@ -115,11 +116,9 @@ def handle_feedback(job) end def get_client(project_name, certificate, sandbox = false) - p project_name uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com" @clients[project_name] ||= Racoon::Client.new(certificate, uri) client = @clients[project_name] - p client # If the certificate has changed, but we still are connected using the old certificate, # disconnect and reconnect. @@ -129,7 +128,6 @@ def get_client(project_name, certificate, sandbox = false) client = @clients[project_name] end - p client client end From 32628efab67265f03669b5883b997e5ae9ae4998 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 19:00:36 -0400 Subject: [PATCH 50/98] think this is it --- lib/racoon/server.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb index 66463fd..87755cb 100644 --- a/lib/racoon/server.rb +++ b/lib/racoon/server.rb @@ -72,8 +72,7 @@ def handle_job(job) client = get_client(project[:name], project[:certificate], packet[:sandbox]) begin - p client - p notification + client.connect! unless client.connected? client.write(notification) # TODO: Listen for error responses from Apple From 1d13f920f3f43c4b9fe84981a3765a79b57db780 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 19:02:07 -0400 Subject: [PATCH 51/98] removed debugging code, fixed errors --- bin/racoon-send | 1 + lib/racoon/notification.rb | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/racoon-send b/bin/racoon-send index 15891ba..fd76b32 100755 --- a/bin/racoon-send +++ b/bin/racoon-send @@ -73,6 +73,7 @@ else bs.ignore("default") project = { :name => "test", :certificate => certificate } bs.yput({ :project => project, + :identifier => 1, :notification => notification.payload, :device_token => notification.device_token, :sandbox => true diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index d239e9b..a3021df 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -30,7 +30,6 @@ def json_payload def to_bytes j = json_payload - p [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j] [1, identifier, expiry.to_i, device_token.size, device_token, j.size, j].pack("cNNna*na*") end From 18be55604e8bcb39a7a4f29613fecf686b14595b Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Wed, 27 Apr 2011 19:03:02 -0400 Subject: [PATCH 52/98] version bump --- lib/racoon.rb | 2 +- racoon.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index 34608fe..4b049b8 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -6,5 +6,5 @@ require 'racoon/feedback_client' module Racoon - VERSION = "0.4.0" + VERSION = "0.4.1" end diff --git a/racoon.gemspec b/racoon.gemspec index f1a1e3b..62d695c 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "0.4.0" + s.version = "0.4.1" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] From b6aa3841dce85e8b694fe899d7575839cb2b4361 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 02:13:58 -0400 Subject: [PATCH 53/98] added firehose and worker -- first draft, probably doesn't work right. Also added associated documentation --- doc/firehose.mdown | 4 +++ doc/worker.mdown | 13 ++++++++ lib/racoon/firehose.rb | 58 +++++++++++++++++++++++++++++++++++ lib/racoon/worker.rb | 68 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 doc/firehose.mdown create mode 100644 doc/worker.mdown create mode 100644 lib/racoon/firehose.rb create mode 100644 lib/racoon/worker.rb 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/racoon/firehose.rb b/lib/racoon/firehose.rb new file mode 100644 index 0000000..c1972a4 --- /dev/null +++ b/lib/racoon/firehose.rb @@ -0,0 +1,58 @@ +# 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' + +module Racoon + class Firehose + def initialize(context = ZMQ::Context.new(1), address = "tcp://*:11555") + @connections = {} + @context = context + @firehose = context.socket(ZMQ::PULL) + @firehose.bind(address) + end + + def start! + EventMachine::run do + apns = EventMachine.spawn do |project, bytes| + uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" + hash = project_hash(project) + + should_fail_in_exception_handler = false + + begin + @connection[hash] ||= Racoon::Client.new(project[:certificate], uri) + + @connection[hash].connect! unless @connection[hash].connected? + @connection[hash].write(bytes) + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + @connection[hash].disconnect! + retry unless should_fail_in_exception_handler + should_fail_in_exception_handler = true + end + end + + EventMachine::PeriodicTimer.new(0.1) do + received_message = ZMQ::Message.new + @firehose.recv(received_message, ZMQ::NOBLOCK) + json_string = received_message.copy_out_string + + if json_string + packet = Yajl::Parser.parse(json_string) + + apns.notify(packet[:project], packet[:bytes]) + end + end + end + end + + private + + def project_hash(project) + Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") + end + end +end \ No newline at end of file diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb new file mode 100644 index 0000000..fa7d86a --- /dev/null +++ b/lib/racoon/worker.rb @@ -0,0 +1,68 @@ +# 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. + +module Racoon + class Worker + def initialize(beanstalk_uris, context = ZMQ::Context.new(1), address = "tcp://*:11555") + @beanstalk_uris = beanstalk_uris + @context = context + @firehose = context.socket(ZMQ::PUSH) + @firehose.connect(address) + # First packet, send something silly, the firehose ignores it + @send_batch = true + end + + def start! + EventMachine::run do + if @send_batch + @send_batch = false + @firehose.send_string("") + end + + EventMachine::PeriodicTimer.new(0.5) do + begin + if beanstalk.peek_ready + job = beanstalk.reserve(1) + process job + end + rescue Beanstalk::TimedOut + Config.logger.info "[Beanstalk] Unable to secure job, operation timed out." + end + end + end + end + + private + + def beanstalk + return @beanstalk if @beanstalk + @beanstalk ||= Beanstalk::Pool.new(@beanstalk_uris) + %w{use watch}.each { |s| @beanstalk.send(s, 'racoon') } + @beanstalk.ignore('default') + @beanstalk + end + + # Expects json ala: + # json = { + # "project":{ + # "name":"foobar", + # "certificate":"...", + # "sandbox":false + # }, + # "bytes":"..." + # } + def process(job) + json_string = job.body + packet = Yajl::Parser.parse(json_string) + project = packet[:project] + + notification = Notification.create_from_packet(packet) + + data = { :project => project, :bytes => notification.to_bytes } + @firehose.send_string(Yajl::Encoder.encode(data)) + end + end +end \ No newline at end of file From bb6d043aaa1a51ec84ad303cab603dcc5a138b82 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 13:59:19 -0400 Subject: [PATCH 54/98] removing send_at, doesn't make sense with the new pipeline --- README.mdown | 6 ++---- lib/racoon/notification.rb | 8 ++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.mdown b/README.mdown index 8da6c37..4865a10 100644 --- a/README.mdown +++ b/README.mdown @@ -121,15 +121,13 @@ Finally within we can send a push notification using the following code: ```ruby beanstalk.yput({ :project => project, :notification => notification, - :send_at => Time.mktime(2012, 12, 21, 4, 15) :device_token => "binary encoded token", :sandbox => true }) ``` -This will construct a notification which is to be scheduled to be delivered on December 21st, -2012 at 4:15 AM localtime to the server. That is, if the server's timezone is UTC, you must -account for that in your client application. +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. Note that the `sandbox` parameter is used to indicate whether or not we're using a development certificate, and as such, should contact Apple's sandbox APN service instead of the production diff --git a/lib/racoon/notification.rb b/lib/racoon/notification.rb index a3021df..90ffd26 100644 --- a/lib/racoon/notification.rb +++ b/lib/racoon/notification.rb @@ -1,3 +1,8 @@ +# 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' @@ -6,11 +11,10 @@ module Racoon class Notification include Racoon::Payload - attr_accessor :identifier, :expiry, :device_token, :alert, :badge, :sound, :custom, :send_at, :expiry + attr_accessor :identifier, :expiry, :device_token, :alert, :badge, :sound, :custom, :expiry def initialize @expiry = 0 - @send_at = Time.now end def payload From 92b9ab2880c90bc28c46bd0dd965fa7c0e8cf8d8 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 14:00:22 -0400 Subject: [PATCH 55/98] version bump, 0.5.0pre1 --- lib/racoon.rb | 4 +++- racoon.gemspec | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index 4b049b8..b9565e3 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -4,7 +4,9 @@ require 'racoon/notification' require 'racoon/client' require 'racoon/feedback_client' +require 'racoon/worker' +require 'racoon/firehose' module Racoon - VERSION = "0.4.1" + VERSION = "0.5.0pre1" end diff --git a/racoon.gemspec b/racoon.gemspec index 62d695c..ef45fe3 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "0.4.1" + s.version = "0.5.0pre1" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] From 1d353f357e8cfcc17f889d907354daff88189c33 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 14:00:37 -0400 Subject: [PATCH 56/98] just added header --- lib/racoon/config.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/racoon/config.rb b/lib/racoon/config.rb index 236b618..2ede23e 100644 --- a/lib/racoon/config.rb +++ b/lib/racoon/config.rb @@ -1,3 +1,8 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# Configuration settings. + module Racoon class Config class << self From 2eaafa78be0437a80e7f09325554759ac9bd3390 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 14:01:07 -0400 Subject: [PATCH 57/98] worker bugfix --- lib/racoon/worker.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index fa7d86a..d7dedcb 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -4,19 +4,24 @@ # This module contains the worker which processes notifications before sending them off # down to the firehose. +require 'eventmachine' +require 'ffi-rzmq' + module Racoon class Worker - def initialize(beanstalk_uris, context = ZMQ::Context.new(1), address = "tcp://*:11555") + def initialize(beanstalk_uris, address = "tcp://*:11555", context = ZMQ::Context.new(1)) @beanstalk_uris = beanstalk_uris @context = context @firehose = context.socket(ZMQ::PUSH) - @firehose.connect(address) + @address = address # First packet, send something silly, the firehose ignores it @send_batch = true end def start! EventMachine::run do + @firehose.connect(@address) + if @send_batch @send_batch = false @firehose.send_string("") From 1dbbfa8752c2b80c59f44d4180722480a8cca5c1 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 14:01:18 -0400 Subject: [PATCH 58/98] firehose bugfix --- lib/racoon/firehose.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index c1972a4..21f29b6 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -5,18 +5,22 @@ # connections to Apple, and sending data over the right ones. require 'digest/sha1' +require 'eventmachine' +require 'ffi-rzmq' module Racoon class Firehose - def initialize(context = ZMQ::Context.new(1), address = "tcp://*:11555") + def initialize(address = "tcp://*:11555", context = ZMQ::Context.new(1)) @connections = {} @context = context @firehose = context.socket(ZMQ::PULL) - @firehose.bind(address) + @address = address end def start! EventMachine::run do + @firehose.bind(@address) + apns = EventMachine.spawn do |project, bytes| uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" hash = project_hash(project) @@ -40,7 +44,7 @@ def start! @firehose.recv(received_message, ZMQ::NOBLOCK) json_string = received_message.copy_out_string - if json_string + if json_string and json_string != "" packet = Yajl::Parser.parse(json_string) apns.notify(packet[:project], packet[:bytes]) From 6913c3f91b22b706a3f308972b66010b34edc685 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 20:23:22 -0400 Subject: [PATCH 59/98] added worker and firehose scripts --- bin/racoon-firehosed | 59 ++++++++++++++++++++++++++++++++++++++++ bin/racoon-workerd | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100755 bin/racoon-firehosed create mode 100755 bin/racoon-workerd diff --git a/bin/racoon-firehosed b/bin/racoon-firehosed new file mode 100755 index 0000000..b480e9b --- /dev/null +++ b/bin/racoon-firehosed @@ -0,0 +1,59 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'getoptlong' +require 'rubygems' +require 'daemons' +require 'racoon' +require 'csv' + +def usage + puts "Usage: racoon-firehosed [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-firehosed') + 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-firehosed.pid' +@log_file = '/var/log/racoon-firehosed.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 worker." +end +Racoon::Firehose.new.start! diff --git a/bin/racoon-workerd b/bin/racoon-workerd new file mode 100755 index 0000000..48b6cee --- /dev/null +++ b/bin/racoon-workerd @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'getoptlong' +require 'rubygems' +require 'daemons' +require 'racoon' +require 'csv' + +def usage + puts "Usage: racoon-workerd [switches]" + puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers" + 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-workerd') + 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], + ["--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"] +@pid_file = '/var/run/racoon-workerd.pid' +@log_file = '/var/log/racoon-workerd.log' +daemon = false + +opts.each do |opt, arg| + case opt + when '--help' + usage + exit 1 + when '--beanstalk' + beanstalks = CSV.parse(arg)[0] + 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 worker." +end +Racoon::Worker.new(beanstalks).start! From c838b6cc041ebd66a43645c37bdd4fe94fd83415 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 20:23:36 -0400 Subject: [PATCH 60/98] changed to new packet format --- bin/racoon-send | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/racoon-send b/bin/racoon-send index fd76b32..e400f68 100755 --- a/bin/racoon-send +++ b/bin/racoon-send @@ -69,13 +69,12 @@ if notification.device_token.nil? exit else bs = Beanstalk::Pool.new beanstalks - %w{use watch}.each { |s| bs.send(s, "racoon-apns") } + %w{use watch}.each { |s| bs.send(s, "racoon") } bs.ignore("default") - project = { :name => "test", :certificate => certificate } + project = { :name => "test", :certificate => certificate, :sandbox => true } bs.yput({ :project => project, :identifier => 1, :notification => notification.payload, :device_token => notification.device_token, - :sandbox => true }) end From 9b3bf9658a06051b16d2a26876e15a8307d93674 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 20:57:08 -0400 Subject: [PATCH 61/98] renamed firehose and worker removing the 'd' --- bin/{racoon-firehosed => racoon-firehose} | 0 bin/{racoon-workerd => racoon-worker} | 0 racoon.gemspec | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename bin/{racoon-firehosed => racoon-firehose} (100%) rename bin/{racoon-workerd => racoon-worker} (100%) diff --git a/bin/racoon-firehosed b/bin/racoon-firehose similarity index 100% rename from bin/racoon-firehosed rename to bin/racoon-firehose diff --git a/bin/racoon-workerd b/bin/racoon-worker similarity index 100% rename from bin/racoon-workerd rename to bin/racoon-worker diff --git a/racoon.gemspec b/racoon.gemspec index ef45fe3..0e79015 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.date = %q{2011-04-24} s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment} s.email = %q{jeremy.tregunna@me.com} - s.executables = ["racoon-send", "racoond"] + 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} From 7b4e0217267e4d0b2fd2ff255e1ef8c2eb59177b Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 20:58:03 -0400 Subject: [PATCH 62/98] reflect new name in help messages --- bin/racoon-firehose | 12 ++++++------ bin/racoon-worker | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/racoon-firehose b/bin/racoon-firehose index b480e9b..49e3ef0 100755 --- a/bin/racoon-firehose +++ b/bin/racoon-firehose @@ -8,15 +8,15 @@ require 'racoon' require 'csv' def usage - puts "Usage: racoon-firehosed [switches]" - puts " --pid the path to store the pid" - puts " --log the path to store the log" + 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-firehosed') + 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) @@ -31,8 +31,8 @@ opts = GetoptLong.new( ["--daemon", "-d", GetoptLong::NO_ARGUMENT] ) -@pid_file = '/var/run/racoon-firehosed.pid' -@log_file = '/var/log/racoon-firehosed.log' +@pid_file = '/var/run/racoon-firehose.pid' +@log_file = '/var/log/racoon-firehose.log' daemon = false opts.each do |opt, arg| diff --git a/bin/racoon-worker b/bin/racoon-worker index 48b6cee..ed2ddfc 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -8,16 +8,16 @@ require 'racoon' require 'csv' def usage - puts "Usage: racoon-workerd [switches]" + puts "Usage: racoon-worker [switches]" puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers" - puts " --pid the path to store the pid" - puts " --log the path to store the log" + 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-workerd') + 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) @@ -34,8 +34,8 @@ opts = GetoptLong.new( ) beanstalks = ["127.0.0.1:11300"] -@pid_file = '/var/run/racoon-workerd.pid' -@log_file = '/var/log/racoon-workerd.log' +@pid_file = '/var/run/racoon-worker.pid' +@log_file = '/var/log/racoon-worker.log' daemon = false opts.each do |opt, arg| From 305626eee311c88ae2eb1994000ebf13d99af570 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 21:02:06 -0400 Subject: [PATCH 63/98] removed server, obsoleted by worker and firehose --- lib/racoon/server.rb | 141 ------------------------------------------- 1 file changed, 141 deletions(-) delete mode 100644 lib/racoon/server.rb diff --git a/lib/racoon/server.rb b/lib/racoon/server.rb deleted file mode 100644 index 87755cb..0000000 --- a/lib/racoon/server.rb +++ /dev/null @@ -1,141 +0,0 @@ -require 'beanstalk-client' - -module Racoon - class Server - attr_accessor :beanstalkd_uris, :feedback_callback - - def initialize(beanstalkd_uris = ["127.0.0.1:11300"], &feedback_blk) - @beanstalks = {} - @clients = {} - @feedback_callback = feedback_blk - @beanstalkd_uris = beanstalkd_uris - end - - def beanstalk(arg) - tube = "racoon-#{arg}" - return @beanstalks[tube] if @beanstalks[tube] - @beanstalks[tube] = Beanstalk::Pool.new @beanstalkd_uris - @beanstalks[tube] - end - - def start! - EventMachine::run do - EventMachine::PeriodicTimer.new(3600) do - begin - b = beanstalk('feedback') - %w{watch use}.each { |s| b.send(s, "racoon-feedback") } - b.ignore('default') - if b.peek_ready - item = b.reserve(1) - handle_feedback(item) - end - rescue Beanstalk::TimedOut - Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." - end - end - - EventMachine::PeriodicTimer.new(1) do - begin - b = beanstalk('apns') - %w{watch use}.each { |s| b.send(s, "racoon-apns") } - b.ignore('default') - if b.peek_ready - item = b.reserve(1) - handle_job item - end - rescue Beanstalk::TimedOut - Config.logger.info "(Beanstalkd) Unable to secure a job, operation timed out." - end - end - end - end - - private - - # Received a notification. job is YAML encoded hash in the following format: - # job = { - # :project => { - # :name => "Foo", - # :certificate => "contents of a certificate.pem" - # }, - # :device_token => "0f21ab...def", - # :notification => notification.payload, - # :sandbox => true # Development environment? - # } - def handle_job(job) - packet = job.ybody - project = packet[:project] - - notification = Notification.create_from_packet(packet) - - if notification - client = get_client(project[:name], project[:certificate], packet[:sandbox]) - - begin - client.connect! unless client.connected? - client.write(notification) - - # TODO: Listen for error responses from Apple - job.delete - rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - Config.logger.error "Caught error, closing connection and adding notification back to queue" - - client.disconnect! - - job.release - rescue RuntimeError => e - Config.logger.error "Unable to handle: #{e}" - - job.delete - end - end - end - - # Will be a hash with two keys: - # :certificate and :sandbox. - def handle_feedback(job) - begin - packet = job.ybody - uri = "feedback.#{packet[:sandbox] ? 'sandbox.' : ''}push.apple.com" - feedback_client = Racoon::FeedbackClient.new(packet[:certificate], uri) - feedback_client.connect! - feedback_client.read.each do |record| - feedback_callback.call record - end - feedback_client.disconnect! - job.delete - rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - Config.logger.error "(Feedback) Caught Error, closing connection" - feedback_client.disconnect! - job.release - rescue RuntimeError => e - Config.logger.error "(Feedback) Unable to handle: #{e}" - job.delete - end - end - - def get_client(project_name, certificate, sandbox = false) - uri = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com" - @clients[project_name] ||= Racoon::Client.new(certificate, uri) - client = @clients[project_name] - - # If the certificate has changed, but we still are connected using the old certificate, - # disconnect and reconnect. - unless client.pem.eql?(certificate) - client.disconnect! if client.connected? - @clients[project_name] = Racoon::Client.new(certificate, uri) - client = @clients[project_name] - end - - client - end - - def purge_client(job) - project_name = job.ybody - client = @clients[project_name] - client.disconnect! if client - @clients[project_name] = nil - job.delete - end - end -end From 7ae8592206deaebbab9307fc7733e8fdf87515cd Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 21:02:21 -0400 Subject: [PATCH 64/98] removed racoond, obsoleted by racoon-worker and racoon-firehose --- bin/racoond | 65 ----------------------------------------------------- 1 file changed, 65 deletions(-) delete mode 100755 bin/racoond diff --git a/bin/racoond b/bin/racoond deleted file mode 100755 index 64b93e8..0000000 --- a/bin/racoond +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env ruby -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) - -require 'getoptlong' -require 'rubygems' -require 'daemons' -require 'eventmachine' -require 'racoon' -require 'racoon/server' -require 'csv' - -def usage - puts "Usage: racoond [switches] --beanstalk a.b.c.d:11300,...,w.x.y.z:11300" - puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers" - 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, 'racoond') - 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], - ["--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"] -@pid_file = '/var/run/racoond.pid' -@log_file = '/var/log/racoond.log' -daemon = false - -opts.each do |opt, arg| - case opt - when '--help' - usage - exit 1 - when '--beanstalk' - beanstalks = CSV.parse(arg)[0] - when '--pid' - @pid_file = arg - when '--log' - @log_file = arg - when '--daemon' - daemon = true - end -end - -Racoon::Config.logger = Logger.new(@log_file) - -daemonize if daemon -server = Racoon::Server.new(beanstalks) do |feedback_record| - Racoon::Config.logger.info "Received feedback at #{feedback_record[:feedback_at]} (length: #{feedback_record[:length]}): #{feedback_record[:device_token]}" -end -server.start! From 59f2d74d9091b84e321a9182cfb28a7f48061cd5 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 21:07:47 -0400 Subject: [PATCH 65/98] moved client to apns/connection --- lib/racoon/apns/connection.rb | 51 +++++++++++++++++++++++++++++++++++ lib/racoon/client.rb | 44 ------------------------------ lib/racoon/firehose.rb | 4 +-- 3 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 lib/racoon/apns/connection.rb delete mode 100644 lib/racoon/client.rb diff --git a/lib/racoon/apns/connection.rb b/lib/racoon/apns/connection.rb new file mode 100644 index 0000000..1f680f8 --- /dev/null +++ b/lib/racoon/apns/connection.rb @@ -0,0 +1,51 @@ +# 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 write(notification) + if host.include? "sandbox" + 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 + end + end +end diff --git a/lib/racoon/client.rb b/lib/racoon/client.rb deleted file mode 100644 index 6351fd7..0000000 --- a/lib/racoon/client.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'openssl' -require 'socket' - -module Racoon - 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 "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 write(notification) - if host.include? "sandbox" - 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 - end -end diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index 21f29b6..f001f7a 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -28,7 +28,7 @@ def start! should_fail_in_exception_handler = false begin - @connection[hash] ||= Racoon::Client.new(project[:certificate], uri) + @connection[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) @connection[hash].connect! unless @connection[hash].connected? @connection[hash].write(bytes) @@ -59,4 +59,4 @@ def project_hash(project) Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") end end -end \ No newline at end of file +end From 5cfd2cb67e3c8eaf948ff2617f2aa845b7f9f49a Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 21:09:59 -0400 Subject: [PATCH 66/98] renamed feedback_client to apns/feedback_connection --- lib/racoon/apns/feedback_connection.rb | 29 ++++++++++++++++++++++++++ lib/racoon/feedback_client.rb | 24 --------------------- 2 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 lib/racoon/apns/feedback_connection.rb delete mode 100644 lib/racoon/feedback_client.rb 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/feedback_client.rb b/lib/racoon/feedback_client.rb deleted file mode 100644 index f1ad7aa..0000000 --- a/lib/racoon/feedback_client.rb +++ /dev/null @@ -1,24 +0,0 @@ -# Feedback service - -module Racoon - class FeedbackClient < Client - 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 \ No newline at end of file From b243815bcc9f4aefb5a4f8190ecb37759a63d262 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 21:19:56 -0400 Subject: [PATCH 67/98] just adding a comment --- lib/racoon/payload.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/racoon/payload.rb b/lib/racoon/payload.rb index b21bb2b..7d7c6e8 100644 --- a/lib/racoon/payload.rb +++ b/lib/racoon/payload.rb @@ -1,3 +1,8 @@ +# Racoon - A distributed APNs provider +# Copyright (c) 2011, Jeremy Tregunna, All Rights Reserved. +# +# APNs payload data + module Racoon module Payload PayloadInvalid = Class.new(RuntimeError) From 059b722828e3061dc6eaafdff6cb5c87d8f28f13 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 21:23:54 -0400 Subject: [PATCH 68/98] updated dependency information --- Gemfile | 1 + Gemfile.lock | 2 ++ racoon.gemspec | 1 + 3 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 8c47f4f..38b7309 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ gem "eventmachine" gem "daemons" gem "yajl-ruby" gem "beanstalk-client" +gem "ffi-rzmq" group :spec do gem "rspec" diff --git a/Gemfile.lock b/Gemfile.lock index 9420740..17d9248 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ GEM daemons (1.0.10) diff-lcs (1.1.2) eventmachine (0.12.10) + ffi-rzmq (0.8.0) rspec (2.3.0) rspec-core (~> 2.3.0) rspec-expectations (~> 2.3.0) @@ -22,5 +23,6 @@ DEPENDENCIES beanstalk-client daemons eventmachine + ffi-rzmq rspec yajl-ruby diff --git a/racoon.gemspec b/racoon.gemspec index 0e79015..2f250cb 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -21,6 +21,7 @@ Gem::Specification.new do |s| 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' s.add_development_dependency 'eventmachine', '>= 0.12.8' end From e841f1a7bdb911006a742151e34fe6d9bb3b3156 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 02:43:57 +0000 Subject: [PATCH 69/98] removed racoon.god, added two new godfiles -- one for worker, one for firehose --- racoon.god => racoon-firehose.god | 8 +++---- racoon-worker.god | 36 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) rename racoon.god => racoon-firehose.god (75%) create mode 100644 racoon-worker.god diff --git a/racoon.god b/racoon-firehose.god similarity index 75% rename from racoon.god rename to racoon-firehose.god index 37cdbaa..950fad9 100644 --- a/racoon.god +++ b/racoon-firehose.god @@ -1,14 +1,14 @@ -# Godfile for racoon +# Godfile for racoon-firehose [ 1 ].each do |instance| - name = "racoon-#{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/racoond -d --beanstalk 127.0.0.1:11300 --pid #{pid_file} --log /var/log/#{name}.log" - w.stop = "kill -15 `cat #{pid_file}`" + 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 diff --git a/racoon-worker.god b/racoon-worker.god new file mode 100644 index 0000000..ae16381 --- /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 --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 From 820055ea92013d8d8f398f0ed19505d9a0b1ea89 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 22:49:49 -0400 Subject: [PATCH 70/98] fixed paths --- lib/racoon.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index b9565e3..cf757a4 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -2,8 +2,8 @@ require 'racoon/config' require 'racoon/payload' require 'racoon/notification' -require 'racoon/client' -require 'racoon/feedback_client' +require 'racoon/apns/connection' +require 'racoon/apns/feedback_connection' require 'racoon/worker' require 'racoon/firehose' From cbb756221e24a44d0d55e9fecefe9a9512526025 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 03:05:05 +0000 Subject: [PATCH 71/98] fixed dep issues --- Gemfile | 1 + Gemfile.lock | 4 ++++ lib/racoon/worker.rb | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 38b7309..60dfb6f 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ gem "eventmachine" gem "daemons" gem "yajl-ruby" gem "beanstalk-client" +gem "ffi" gem "ffi-rzmq" group :spec do diff --git a/Gemfile.lock b/Gemfile.lock index 17d9248..385bbc0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,10 @@ GEM daemons (1.0.10) diff-lcs (1.1.2) eventmachine (0.12.10) + 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) @@ -23,6 +26,7 @@ DEPENDENCIES beanstalk-client daemons eventmachine + ffi ffi-rzmq rspec yajl-ruby diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index d7dedcb..9f0ded1 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -4,6 +4,7 @@ # This module contains the worker which processes notifications before sending them off # down to the firehose. +require 'beanstalk-client' require 'eventmachine' require 'ffi-rzmq' @@ -70,4 +71,4 @@ def process(job) @firehose.send_string(Yajl::Encoder.encode(data)) end end -end \ No newline at end of file +end From 8ad8c50e68a63f882fcebddef77895608ee23a6a Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 23:23:09 -0400 Subject: [PATCH 72/98] using yaml instead of json for beanstalk payload --- bin/racoon-send | 5 +++-- lib/racoon/worker.rb | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/racoon-send b/bin/racoon-send index e400f68..973cf98 100755 --- a/bin/racoon-send +++ b/bin/racoon-send @@ -72,9 +72,10 @@ else %w{use watch}.each { |s| bs.send(s, "racoon") } bs.ignore("default") project = { :name => "test", :certificate => certificate, :sandbox => true } - bs.yput({ :project => project, + notif = { :project => project, :identifier => 1, :notification => notification.payload, :device_token => notification.device_token, - }) + } + bs.yput(notif) end diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index 9f0ded1..02473da 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -61,8 +61,7 @@ def beanstalk # "bytes":"..." # } def process(job) - json_string = job.body - packet = Yajl::Parser.parse(json_string) + packet = job.ybody project = packet[:project] notification = Notification.create_from_packet(packet) From 887bfaf27995f50620584801ec2b40d2878b710e Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 28 Apr 2011 23:57:57 -0400 Subject: [PATCH 73/98] switching to yaml --- lib/racoon/firehose.rb | 11 ++++------- lib/racoon/worker.rb | 3 ++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index f001f7a..f7f81fa 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -25,8 +25,6 @@ def start! uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" hash = project_hash(project) - should_fail_in_exception_handler = false - begin @connection[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) @@ -34,18 +32,17 @@ def start! @connection[hash].write(bytes) rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET @connection[hash].disconnect! - retry unless should_fail_in_exception_handler - should_fail_in_exception_handler = true + retry end end EventMachine::PeriodicTimer.new(0.1) do received_message = ZMQ::Message.new @firehose.recv(received_message, ZMQ::NOBLOCK) - json_string = received_message.copy_out_string + yaml_string = received_message.copy_out_string - if json_string and json_string != "" - packet = Yajl::Parser.parse(json_string) + if yaml_string and yaml_string != "" + packet = YAML::load(yaml_string) apns.notify(packet[:project], packet[:bytes]) end diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index 02473da..8568900 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -7,6 +7,7 @@ require 'beanstalk-client' require 'eventmachine' require 'ffi-rzmq' +require 'yaml' module Racoon class Worker @@ -67,7 +68,7 @@ def process(job) notification = Notification.create_from_packet(packet) data = { :project => project, :bytes => notification.to_bytes } - @firehose.send_string(Yajl::Encoder.encode(data)) + @firehose.send_string(YAML::dump(data)) end end end From 75f4b5303b50de9b4af4d62d67bb6c65f11360cb Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 00:03:59 -0400 Subject: [PATCH 74/98] bugfix in firehose --- lib/racoon/firehose.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index f7f81fa..123d68f 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -21,7 +21,7 @@ def start! EventMachine::run do @firehose.bind(@address) - apns = EventMachine.spawn do |project, bytes| + apns = EventMachine.spawn do |project, bytes, retries| uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" hash = project_hash(project) @@ -32,7 +32,7 @@ def start! @connection[hash].write(bytes) rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET @connection[hash].disconnect! - retry + retry if (retries -= 1) > 0 end end @@ -44,14 +44,12 @@ def start! if yaml_string and yaml_string != "" packet = YAML::load(yaml_string) - apns.notify(packet[:project], packet[:bytes]) + apns.notify(packet[:project], packet[:bytes], 2) end end end end - private - def project_hash(project) Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") end From aa9deb9d2055dc437e5afc2cb20494bfda475a11 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 00:05:47 -0400 Subject: [PATCH 75/98] spawned process not finding project_hash, just inlining --- lib/racoon/firehose.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index 123d68f..fa9ef63 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -23,7 +23,7 @@ def start! apns = EventMachine.spawn do |project, bytes, retries| uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" - hash = project_hash(project) + hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") begin @connection[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) @@ -49,9 +49,5 @@ def start! end end end - - def project_hash(project) - Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") - end end end From d65113c629d872e10ec9398fbb5242bfb67416ce Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 00:07:47 -0400 Subject: [PATCH 76/98] getting rid of the spawned process, i just don't understand eventmachine as well as i thought i guess --- lib/racoon/firehose.rb | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index fa9ef63..3951ceb 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -21,21 +21,6 @@ def start! EventMachine::run do @firehose.bind(@address) - apns = EventMachine.spawn do |project, bytes, retries| - uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" - hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") - - begin - @connection[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) - - @connection[hash].connect! unless @connection[hash].connected? - @connection[hash].write(bytes) - rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - @connection[hash].disconnect! - retry if (retries -= 1) > 0 - end - end - EventMachine::PeriodicTimer.new(0.1) do received_message = ZMQ::Message.new @firehose.recv(received_message, ZMQ::NOBLOCK) @@ -44,10 +29,25 @@ def start! if yaml_string and yaml_string != "" packet = YAML::load(yaml_string) - apns.notify(packet[:project], packet[:bytes], 2) + apns(packet[:project], packet[:bytes]) end end end end + + def apns(project, bytes, retries=2) + uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" + hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") + + begin + @connection[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) + + @connection[hash].connect! unless @connection[hash].connected? + @connection[hash].write(bytes) + rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET + @connection[hash].disconnect! + retry if (retries -= 1) > 0 + end + end end end From 2d174c50c38052e6bd22e5c224f0c8223d8b5a02 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 00:08:58 -0400 Subject: [PATCH 77/98] typo --- lib/racoon/firehose.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index 3951ceb..bc7a46c 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -40,12 +40,12 @@ def apns(project, bytes, retries=2) hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") begin - @connection[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) + @connections[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) - @connection[hash].connect! unless @connection[hash].connected? - @connection[hash].write(bytes) + @connections[hash].connect! unless @connections[hash].connected? + @connections[hash].write(bytes) rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - @connection[hash].disconnect! + @connections[hash].disconnect! retry if (retries -= 1) > 0 end end From e8508ddb2b26c3aaa561c8da7f0c51baf7330ea1 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 00:11:27 -0400 Subject: [PATCH 78/98] fixed connection --- lib/racoon/apns/connection.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/racoon/apns/connection.rb b/lib/racoon/apns/connection.rb index 1f680f8..0e7500b 100644 --- a/lib/racoon/apns/connection.rb +++ b/lib/racoon/apns/connection.rb @@ -36,8 +36,9 @@ def disconnect! @sock = nil end - def write(notification) + 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) From 3283b7c078fcb72d372932f10771cdde960eea43 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 00:15:11 -0400 Subject: [PATCH 79/98] forgot to delete the job --- lib/racoon/worker.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index 8568900..3e1b8fb 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -34,6 +34,7 @@ def start! if beanstalk.peek_ready job = beanstalk.reserve(1) process job + job.delete end rescue Beanstalk::TimedOut Config.logger.info "[Beanstalk] Unable to secure job, operation timed out." From acc97611da23196f1d7989a1e22c33ece1f9fac4 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 01:05:39 -0400 Subject: [PATCH 80/98] version bump & documentation update --- README.mdown | 175 +++++++++++++++++++++++++++---------------------- lib/racoon.rb | 2 +- racoon.gemspec | 6 +- 3 files changed, 99 insertions(+), 84 deletions(-) diff --git a/README.mdown b/README.mdown index 4865a10..011e5df 100644 --- a/README.mdown +++ b/README.mdown @@ -6,25 +6,28 @@ has since taken on a different path. How does it differ from apnserver? By a few 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; and -5. Receives packets containing notifications from beanstalkd instead of a listening socket. +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 for an APNS provider to replace the current -provider used by [Diligent Street](http://www.diligentstreet.com/) with something more robust. As such, it needs to be -suitable for a hosted environment, where multiple—unrelated—users of the service will be -using it. +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). ## Description -racoon is a server and a set of command line programs to send push notifications to iOS devices. -Apple recommends to maintain an open connection to the push notification service, and refrain -from opening up and tearing down SSL connections repeatedly. As such, a separate daemon is -introduced that has messages queued up (beanstalkd) for consumption by this daemon. This -decouples the APNS server from your backend system. Those notifications are sent over a -persistent connection to Apple. +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 @@ -46,98 +49,110 @@ 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. -## APN Server Daemon - -
-Usage: racoond [options]
-  --beanstalk 
-	The comma-separated list of ip:port for beanstalk servers
-
-  --pid 
-	Path used for the PID file. Defaults to /var/run/racoon.pid
-
-  --log 
-	Path used for the log file. Defaults to /var/log/racoon.log
+## Firehose
 
-  --help
-    usage message
+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.
 
-  --daemon
-    Runs process as daemon, not available on Windows
+
+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
 
-## APN Server Client +## Worker -TODO: Document this +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. -## Sending Notifications from Ruby +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 need to set up a connection to the beanstalkd service. We can do this simply by defining -the following method: +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. -```ruby -def beanstalk - return @beanstalk if @beanstalk - @beanstalk = Beanstalk::Pool.new ["127.0.0.1:11300"] - @beanstalk.use "racoon-apns" - @beanstalk -end -``` +
+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
+
-In this way, whenever we need access to beanstalk, we'll make the connection and set up to use -the appropriate tube whether the connection is open yet or not. +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. -We will also need two pieces of information: A project, and a notification. +## Sender -**Note that the tube should always be "racoon-apns". The server won't look on any other tube for -push notifications.** +The sender is the program used to form a packet, place it on beanstalk for racoon to consume. +It is useful during testing. -### Project +
+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 -A project os comprised of a few pieces of information at a minimum (you may supply more if you -wish, but racoond will ignore them): +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 => "Awesome project", :certificate => "contents of the generated .pem file" } +{ + :project => { :name => "My awesome app", :certificate => "...", :sandbox => true }, + :identifier => 12345, + :notification => { :aps => { :alert => "text", + :sound => "default", + :badge => 1, + :custom => { ... } + } + }, + :device_token => "..." +} ``` -### Notification +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. -A notification is a ruby hash containing the things to be sent along, including the device token. -An example notification may look like this: - -```ruby -notification = { :aps => { :alert => "Some text", - :sound => "Audio_file", - :badge => 42, - :custom => { :field => "lala", - :stuff => 42 } - } - } -``` +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`. -Finally within we can send a push notification using the following code: +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. -```ruby -beanstalk.yput({ :project => project, - :notification => notification, - :device_token => "binary encoded token", - :sandbox => true -}) -``` +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. -Note that the `sandbox` parameter is used to indicate whether or not we're using a development -certificate, and as such, should contact Apple's sandbox APN service instead of the production -certificate. If left out, we assume production. - -This will schedule the push on beanstalkd. Racoon is constantly polling beanstalkd looking for -ready jobs it can pop off and process (send to Apple). Using beanstalkd however allows us to -queue up items, and during peak times, add another **N** more racoon servers to make up any -backlog, to ensure our messages are sent fast, and that we can scale. - ## Installation Racoon is hosted on [rubygems](https://rubygems.org/gems/racoon) diff --git a/lib/racoon.rb b/lib/racoon.rb index cf757a4..7f9a1b7 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -8,5 +8,5 @@ require 'racoon/firehose' module Racoon - VERSION = "0.5.0pre1" + VERSION = "0.5.0" end diff --git a/racoon.gemspec b/racoon.gemspec index 2f250cb..0adbe05 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,11 +1,11 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "0.5.0pre1" + s.version = "0.5.0" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] s.date = %q{2011-04-24} - s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment} + 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"] @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.rubyforge_project = %q{racoon} s.rubygems_version = %q{1.3.5} - s.summary = %q{Apple Push Notification Toolkit for hosted environments} + 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" From 83115be450f65f0b50d12bf590874462acd21a0c Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 01:26:37 -0400 Subject: [PATCH 81/98] added back integration code for feedback service --- lib/racoon/firehose.rb | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index bc7a46c..1ad25dd 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -10,17 +10,33 @@ module Racoon class Firehose - def initialize(address = "tcp://*:11555", context = ZMQ::Context.new(1)) + def initialize(address = "tcp://*:11555", context = ZMQ::Context.new(1), &feedback_callback) @connections = {} @context = context @firehose = context.socket(ZMQ::PULL) @address = address + @feedback_callback = feedback_callback end def start! EventMachine::run do @firehose.bind(@address) + EventMachine::PeriodicTimer.new(28800) do + @connections.each_pair do |key, data| + begin + 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 + EventMachine::PeriodicTimer.new(0.1) do received_message = ZMQ::Message.new @firehose.recv(received_message, ZMQ::NOBLOCK) @@ -40,12 +56,13 @@ def apns(project, bytes, retries=2) hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") begin - @connections[hash] ||= Racoon::APNS::Connection.new(project[:certificate], uri) + connection = Racoon::APNS::Connection.new(project[:certificate], uri) + @connections[hash] ||= { :connection => connection, :certificate => project[:certificate], :sandbox => project[:sandbox] } - @connections[hash].connect! unless @connections[hash].connected? - @connections[hash].write(bytes) + connection.connect! unless connection.connected? + connection.write(bytes) rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET - @connections[hash].disconnect! + connection.disconnect! retry if (retries -= 1) > 0 end end From 5bde233a4df706d1ca69d361aff4670dda3de126 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 01:46:44 -0400 Subject: [PATCH 82/98] added configurable tube arguments, defaulting to 'racoon' --- bin/racoon-send | 7 ++++++- bin/racoon-worker | 7 ++++++- lib/racoon/worker.rb | 5 +++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bin/racoon-send b/bin/racoon-send index 973cf98..8fbd10e 100755 --- a/bin/racoon-send +++ b/bin/racoon-send @@ -14,6 +14,7 @@ require 'beanstalk-client' def usage 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 " --alert the message to send" puts " --sound the sound to play, defaults to 'default'" @@ -26,6 +27,7 @@ end opts = GetoptLong.new( ["--beanstalk", "-b", GetoptLong::REQUIRED_ARGUMENT], + ["--tube", "-t", GetoptLong::REQUIRED_ARGUMENT], ["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT], ["--alert", "-a", GetoptLong::REQUIRED_ARGUMENT], ["--sound", "-S", GetoptLong::REQUIRED_ARGUMENT], @@ -37,6 +39,7 @@ opts = GetoptLong.new( ) beanstalks = ["127.0.0.1:11300"] +tube = 'racoon' certificate = nil notification = Racoon::Notification.new @@ -47,6 +50,8 @@ opts.each do |opt, arg| exit when '--beanstalk' beanstalks = CSV.parse(arg)[0] + when '--tube' + tube = arg when '--pem' certificate = File.read(arg) when '--alert' @@ -69,7 +74,7 @@ if notification.device_token.nil? exit else bs = Beanstalk::Pool.new beanstalks - %w{use watch}.each { |s| bs.send(s, "racoon") } + %w{use watch}.each { |s| bs.send(s, tube) } bs.ignore("default") project = { :name => "test", :certificate => certificate, :sandbox => true } notif = { :project => project, diff --git a/bin/racoon-worker b/bin/racoon-worker index ed2ddfc..6b896ab 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -10,6 +10,7 @@ require 'csv' 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 " --pid the path to store the pid" puts " --log the path to store the log" puts " --daemon to daemonize the server" @@ -27,6 +28,7 @@ end opts = GetoptLong.new( ["--beanstalk", "-b", GetoptLong::REQUIRED_ARGUMENT], + ["--tube", "-t", GetoptLong::REQUIRED_ARGUMENT], ["--pid", "-i", GetoptLong::REQUIRED_ARGUMENT], ["--log", "-l", GetoptLong::REQUIRED_ARGUMENT], ["--help", "-h", GetoptLong::NO_ARGUMENT], @@ -34,6 +36,7 @@ opts = GetoptLong.new( ) beanstalks = ["127.0.0.1:11300"] +tube = 'racoon' @pid_file = '/var/run/racoon-worker.pid' @log_file = '/var/log/racoon-worker.log' daemon = false @@ -45,6 +48,8 @@ opts.each do |opt, arg| exit 1 when '--beanstalk' beanstalks = CSV.parse(arg)[0] + when '--tube' + tube = arg when '--pid' @pid_file = arg when '--log' @@ -61,4 +66,4 @@ if daemon else puts "Starting racoon worker." end -Racoon::Worker.new(beanstalks).start! +Racoon::Worker.new(beanstalks, tube).start! diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index 3e1b8fb..de64d85 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -11,10 +11,11 @@ module Racoon class Worker - def initialize(beanstalk_uris, address = "tcp://*:11555", context = ZMQ::Context.new(1)) + def initialize(beanstalk_uris, tube = "racoon", address = "tcp://*:11555", context = ZMQ::Context.new(1)) @beanstalk_uris = beanstalk_uris @context = context @firehose = context.socket(ZMQ::PUSH) + @tube = tube @address = address # First packet, send something silly, the firehose ignores it @send_batch = true @@ -48,7 +49,7 @@ def start! def beanstalk return @beanstalk if @beanstalk @beanstalk ||= Beanstalk::Pool.new(@beanstalk_uris) - %w{use watch}.each { |s| @beanstalk.send(s, 'racoon') } + %w{use watch}.each { |s| @beanstalk.send(s, @tube) } @beanstalk.ignore('default') @beanstalk end From 1a54ba4d05cf095244f1e64fa2d30b338af032c6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:25:48 -0400 Subject: [PATCH 83/98] Optimizing architecture -- using ZMQMachine, dropping EventMachine. --- Gemfile | 1 + Gemfile.lock | 8 +++++ README.mdown | 8 ++++- bin/racoon-firehose | 26 ++++++++++++++- bin/racoon-worker | 57 +++++++++++++++++++++++++++++++-- lib/racoon.rb | 2 +- lib/racoon/firehose.rb | 55 +++++++++++--------------------- lib/racoon/worker.rb | 72 ++++++++++-------------------------------- racoon.gemspec | 3 +- 9 files changed, 133 insertions(+), 99 deletions(-) diff --git a/Gemfile b/Gemfile index 60dfb6f..dc74579 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ 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" diff --git a/Gemfile.lock b/Gemfile.lock index 385bbc0..d130d74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +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: @@ -30,3 +37,4 @@ DEPENDENCIES ffi-rzmq rspec yajl-ruby + zmqmachine! diff --git a/README.mdown b/README.mdown index 011e5df..9952c3c 100644 --- a/README.mdown +++ b/README.mdown @@ -8,7 +8,7 @@ has since taken on a different path. How does it differ from apnserver? By a few 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. +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: @@ -20,6 +20,12 @@ more robust, and better suited to scaling upwards. This APNs provider had a coup 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 diff --git a/bin/racoon-firehose b/bin/racoon-firehose index 49e3ef0..5de3c65 100755 --- a/bin/racoon-firehose +++ b/bin/racoon-firehose @@ -6,6 +6,8 @@ require 'rubygems' require 'daemons' require 'racoon' require 'csv' +require 'yaml' +require 'zmqmachine' def usage puts "Usage: racoon-firehose [switches]" @@ -56,4 +58,26 @@ if daemon else puts "Starting racoon worker." end -Racoon::Firehose.new.start! +#Racoon::Firehose.new.start! + +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 + 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 diff --git a/bin/racoon-worker b/bin/racoon-worker index 6b896ab..e2c7889 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -6,11 +6,15 @@ 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" @@ -29,6 +33,7 @@ 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], @@ -36,7 +41,8 @@ opts = GetoptLong.new( ) beanstalks = ["127.0.0.1:11300"] -tube = 'racoon' +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 @@ -49,7 +55,9 @@ opts.each do |opt, arg| when '--beanstalk' beanstalks = CSV.parse(arg)[0] when '--tube' - tube = arg + @tube = arg + when '--firehose' + firehose_address = arg.split(":") when '--pid' @pid_file = arg when '--log' @@ -61,9 +69,52 @@ end Racoon::Config.logger = Logger.new(@log_file) +def beanstalk + return @beanstalk if @beanstalk + @beanstalk = Beanstalk::Pool.new(beanstalk_uris) + %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 -Racoon::Worker.new(beanstalks, tube).start! + +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 + job.delete + end + rescue Beanstalk::TimedOut + Racoon::Config.logger.info "[Beanstalk] Unable to secure job, timed out." + end + end +end diff --git a/lib/racoon.rb b/lib/racoon.rb index 7f9a1b7..9f5e14e 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -8,5 +8,5 @@ require 'racoon/firehose' module Racoon - VERSION = "0.5.0" + VERSION = "0.6.0" end diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index 1ad25dd..3a1eb8a 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -5,59 +5,42 @@ # connections to Apple, and sending data over the right ones. require 'digest/sha1' -require 'eventmachine' -require 'ffi-rzmq' +require 'zmqmachine' +require 'yaml' module Racoon class Firehose - def initialize(address = "tcp://*:11555", context = ZMQ::Context.new(1), &feedback_callback) + attr_accessor :connections, :feedback_callback + + def initialize(reactor, address = ZM::Address.new('*', 11555, :tcp), &feedback_callback) @connections = {} - @context = context - @firehose = context.socket(ZMQ::PULL) + @reactor = reactor @address = address @feedback_callback = feedback_callback end - def start! - EventMachine::run do - @firehose.bind(@address) - - EventMachine::PeriodicTimer.new(28800) do - @connections.each_pair do |key, data| - begin - 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 - - EventMachine::PeriodicTimer.new(0.1) do - received_message = ZMQ::Message.new - @firehose.recv(received_message, ZMQ::NOBLOCK) - yaml_string = received_message.copy_out_string - - if yaml_string and yaml_string != "" - packet = YAML::load(yaml_string) + def on_attach(socket) + socket.bind(@address) + end - apns(packet[:project], packet[:bytes]) - end - 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 - connection = Racoon::APNS::Connection.new(project[:certificate], uri) - @connections[hash] ||= { :connection => connection, :certificate => project[:certificate], :sandbox => project[:sandbox] } + @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) diff --git a/lib/racoon/worker.rb b/lib/racoon/worker.rb index de64d85..3337c07 100644 --- a/lib/racoon/worker.rb +++ b/lib/racoon/worker.rb @@ -4,73 +4,35 @@ # This module contains the worker which processes notifications before sending them off # down to the firehose. -require 'beanstalk-client' -require 'eventmachine' require 'ffi-rzmq' -require 'yaml' +require 'zmqmachine' module Racoon class Worker - def initialize(beanstalk_uris, tube = "racoon", address = "tcp://*:11555", context = ZMQ::Context.new(1)) - @beanstalk_uris = beanstalk_uris - @context = context - @firehose = context.socket(ZMQ::PUSH) - @tube = tube + def initialize(reactor, address) + @reactor = reactor @address = address - # First packet, send something silly, the firehose ignores it - @send_batch = true + @send_queue = [] end - - def start! - EventMachine::run do - @firehose.connect(@address) - if @send_batch - @send_batch = false - @firehose.send_string("") - end + def on_attach(socket) + @socket = socket - EventMachine::PeriodicTimer.new(0.5) do - begin - if beanstalk.peek_ready - job = beanstalk.reserve(1) - process job - job.delete - end - rescue Beanstalk::TimedOut - Config.logger.info "[Beanstalk] Unable to secure job, operation timed out." - end - end - end + socket.connect(@address.to_s) end - private - - def beanstalk - return @beanstalk if @beanstalk - @beanstalk ||= Beanstalk::Pool.new(@beanstalk_uris) - %w{use watch}.each { |s| @beanstalk.send(s, @tube) } - @beanstalk.ignore('default') - @beanstalk + def on_writable(socket) + unless @send_queue.empty? + message = @send_queue.shift + socket.send_message_string(message) + else + @reactor.deregister_writable(socket) + end end - # Expects json ala: - # json = { - # "project":{ - # "name":"foobar", - # "certificate":"...", - # "sandbox":false - # }, - # "bytes":"..." - # } - def process(job) - packet = job.ybody - project = packet[:project] - - notification = Notification.create_from_packet(packet) - - data = { :project => project, :bytes => notification.to_bytes } - @firehose.send_string(YAML::dump(data)) + def send_message(message) + @send_queue.push(message) + @reactor.register_writable(@socket) end end end diff --git a/racoon.gemspec b/racoon.gemspec index 0adbe05..ac2e9ad 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "0.5.0" + s.version = "0.6.0" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] @@ -23,5 +23,4 @@ Gem::Specification.new do |s| s.add_dependency 'beanstalk-client', '>= 1.0.0' s.add_dependency 'ffi-rzmq', '~> 0.8.0' s.add_development_dependency 'bundler', '~> 1.0.0' - s.add_development_dependency 'eventmachine', '>= 0.12.8' end From a86d5b871640ba136b9e8bb63a14e5f8e6e0638b Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:31:46 -0400 Subject: [PATCH 84/98] removing eventmachine from gemfile --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index dc74579..9f06b0e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source "http://rubygems.org" -gem "eventmachine" gem "daemons" gem "yajl-ruby" gem "beanstalk-client" From 33229134984de7d200b06557df7fb099234f39e6 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:39:03 -0400 Subject: [PATCH 85/98] missing call to join --- bin/racoon-firehose | 2 +- bin/racoon-worker | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/racoon-firehose b/bin/racoon-firehose index 5de3c65..d138ada 100755 --- a/bin/racoon-firehose +++ b/bin/racoon-firehose @@ -80,4 +80,4 @@ ZM::Reactor.new(:firehose).run do |context| end end end -end +end.join diff --git a/bin/racoon-worker b/bin/racoon-worker index e2c7889..5e1d520 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -117,4 +117,4 @@ ZM::Reactor.new(:worker).run do |context| Racoon::Config.logger.info "[Beanstalk] Unable to secure job, timed out." end end -end +end.join From 1dfe373854ac8eb1cc198dcab92310cd6fee9294 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:39:44 -0400 Subject: [PATCH 86/98] typo --- bin/racoon-worker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/racoon-worker b/bin/racoon-worker index 5e1d520..215283b 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -71,7 +71,7 @@ Racoon::Config.logger = Logger.new(@log_file) def beanstalk return @beanstalk if @beanstalk - @beanstalk = Beanstalk::Pool.new(beanstalk_uris) + @beanstalk = Beanstalk::Pool.new(beanstalks) %w{use watch}.each { |s| @beanstalk.send(s, @tube) } @beanstalk.ignore('default') @beanstalk From 589925d6745e17d2a19374d5ad9aa08a34363ba0 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:40:28 -0400 Subject: [PATCH 87/98] made beanstalks an ivar --- bin/racoon-worker | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/racoon-worker b/bin/racoon-worker index 215283b..591719b 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -40,7 +40,7 @@ opts = GetoptLong.new( ["--daemon", "-d", GetoptLong::NO_ARGUMENT] ) -beanstalks = ["127.0.0.1:11300"] +@beanstalks = ["127.0.0.1:11300"] firehose_address = "127.0.0.1:11555".split(":") @tube = 'racoon' @pid_file = '/var/run/racoon-worker.pid' @@ -53,7 +53,7 @@ opts.each do |opt, arg| usage exit 1 when '--beanstalk' - beanstalks = CSV.parse(arg)[0] + @beanstalks = CSV.parse(arg)[0] when '--tube' @tube = arg when '--firehose' @@ -71,7 +71,7 @@ Racoon::Config.logger = Logger.new(@log_file) def beanstalk return @beanstalk if @beanstalk - @beanstalk = Beanstalk::Pool.new(beanstalks) + @beanstalk = Beanstalk::Pool.new(@beanstalks) %w{use watch}.each { |s| @beanstalk.send(s, @tube) } @beanstalk.ignore('default') @beanstalk From 5a73228187c4da7d48eaf68a4a6c99fb63894f74 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:41:15 -0400 Subject: [PATCH 88/98] startup message modification --- bin/racoon-firehose | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/racoon-firehose b/bin/racoon-firehose index d138ada..894c9ec 100755 --- a/bin/racoon-firehose +++ b/bin/racoon-firehose @@ -56,9 +56,8 @@ Racoon::Config.logger = Logger.new(@log_file) if daemon daemonize else - puts "Starting racoon worker." + puts "Starting racoon firehose." end -#Racoon::Firehose.new.start! ZM::Reactor.new(:firehose).run do |context| firehose = Racoon::Firehose.new(context) do |feedback_record| From 50f46e1d8cfa0fcc39cf96dae68dfb3a2bfd30b5 Mon Sep 17 00:00:00 2001 From: "jeremy.tregunna@emiair.ca" Date: Fri, 29 Apr 2011 22:43:00 +0000 Subject: [PATCH 89/98] Updated arguments in godfile --- Gemfile.lock | 2 -- racoon-worker.god | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d130d74..3af895c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,6 @@ GEM beanstalk-client (1.1.0) daemons (1.0.10) diff-lcs (1.1.2) - eventmachine (0.12.10) ffi (1.0.7) rake (>= 0.8.7) ffi-rzmq (0.8.0) @@ -32,7 +31,6 @@ PLATFORMS DEPENDENCIES beanstalk-client daemons - eventmachine ffi ffi-rzmq rspec diff --git a/racoon-worker.god b/racoon-worker.god index ae16381..bf79ee2 100644 --- a/racoon-worker.god +++ b/racoon-worker.god @@ -7,7 +7,7 @@ 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 --pid #{pid_file} --log /var/log/#{name}.log" + 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 From e4970cfb1e25b030d413dce8d26fdda4ae23c6b5 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:45:01 -0400 Subject: [PATCH 90/98] missing argument --- bin/racoon-worker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/racoon-worker b/bin/racoon-worker index 591719b..e4c8e1c 100755 --- a/bin/racoon-worker +++ b/bin/racoon-worker @@ -110,7 +110,7 @@ ZM::Reactor.new(:worker).run do |context| begin if beanstalk.peek_ready job = beanstalk.reserve(1) - process job + process(job, worker) job.delete end rescue Beanstalk::TimedOut From 6a169a327b6f2c4c8a3638a79cd84c6eba00cc74 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Fri, 29 Apr 2011 18:51:37 -0400 Subject: [PATCH 91/98] forgot one line of code --- bin/racoon-firehose | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/racoon-firehose b/bin/racoon-firehose index 894c9ec..9ab67bf 100755 --- a/bin/racoon-firehose +++ b/bin/racoon-firehose @@ -68,6 +68,7 @@ ZM::Reactor.new(:firehose).run do |context| 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! From 7343938a9e1b35ec7ae0e9e8d2445afc88a6f1f0 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:09:35 -0400 Subject: [PATCH 92/98] removing old startup scripts. use supplied godfiles instead, that's what i do. --- bin/apnserverd.fedora.init | 71 ----------------------- bin/apnserverd.ubuntu.init | 116 ------------------------------------- 2 files changed, 187 deletions(-) delete mode 100755 bin/apnserverd.fedora.init delete mode 100644 bin/apnserverd.ubuntu.init diff --git a/bin/apnserverd.fedora.init b/bin/apnserverd.fedora.init deleted file mode 100755 index 301ce9a..0000000 --- a/bin/apnserverd.fedora.init +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -# -# /etc/rc.d/init.d/apnserverd -# apnserverd This shell script takes care of starting and stopping -# the APN Server Proxy -# -# chkconfig: 345 20 80 -# Author: Ben Poweski bpoweski@gmail.com -# -# Source function library. -. /etc/init.d/functions - -NAME=apnserverd -APNSERVERD=/usr/bin/$NAME -PIDFILE=/var/run/$NAME.pid - -if [ -f /etc/sysconfig/$NAME ]; then - . /etc/sysconfig/$NAME -fi - - -start() { - echo -n "Starting APN Server: " - if [ -f $PIDFILE ]; then - PID=`cat $PIDFILE` - echo $NAME already running: $PID - exit 2; - elif [ -f $PIDFILE ]; then - PID=`cat $PIDFILE` - echo $NAME already running: $PID - exit 2; - else - daemon $APNSERVERD $OPTIONS - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$NAME - return $RETVAL - fi - -} - -stop() { - echo -n "Shutting down APN Server: " - echo - kill `cat $PIDFILE` - echo - rm -f /var/lock/subsys/$NAME - rm -f $PIDFILE - return 0 -} - -case "$1" in - start) - start - ;; - stop) - stop - ;; - status) - status $NAME - ;; - restart) - stop - start - ;; - *) - echo "Usage: {start|stop|status|restart}" - exit 1 - ;; -esac -exit $? diff --git a/bin/apnserverd.ubuntu.init b/bin/apnserverd.ubuntu.init deleted file mode 100644 index 9870cd7..0000000 --- a/bin/apnserverd.ubuntu.init +++ /dev/null @@ -1,116 +0,0 @@ -#! /bin/sh -### BEGIN INIT INFO -# Provides: apnserverd -# Required-Start: $remote_fs -# Required-Stop: $remote_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Apple Push Notification Server Daemon -### END INIT INFO - -# Author: Philipp Schmid - -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 - -: From ba49e319545ebfc9b7aada276370c47f15e62db2 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:19:17 -0400 Subject: [PATCH 93/98] now reading errors from socket and calling a block --- lib/racoon/apns/connection.rb | 15 +++++++++++++++ lib/racoon/firehose.rb | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/racoon/apns/connection.rb b/lib/racoon/apns/connection.rb index 0e7500b..72580a6 100644 --- a/lib/racoon/apns/connection.rb +++ b/lib/racoon/apns/connection.rb @@ -36,6 +36,14 @@ def disconnect! @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) @@ -47,6 +55,13 @@ def write(bytes) 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/firehose.rb b/lib/racoon/firehose.rb index 3a1eb8a..b65ecbd 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -32,7 +32,7 @@ def on_readable(socket, messages) private - def apns(project, bytes, retries=2) + def apns(project, bytes, retries=2, &error_callback) uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") @@ -44,6 +44,8 @@ def apns(project, bytes, retries=2) connection.connect! unless connection.connected? connection.write(bytes) + connection.read.each(&error_callback) + end rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET connection.disconnect! retry if (retries -= 1) > 0 From 3d5fe8ddcabe5e869ea64b26c25927cc2900f429 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:22:19 -0400 Subject: [PATCH 94/98] errant end --- lib/racoon/firehose.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index b65ecbd..75bcffa 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -45,7 +45,6 @@ def apns(project, bytes, retries=2, &error_callback) connection.connect! unless connection.connected? connection.write(bytes) connection.read.each(&error_callback) - end rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET connection.disconnect! retry if (retries -= 1) > 0 From 21d8d50c4627a16c52821c77ecde827fecfc405c Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:23:31 -0400 Subject: [PATCH 95/98] safety in case error_callback isn't supplied --- lib/racoon/firehose.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index 75bcffa..c8ce04d 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -44,7 +44,7 @@ def apns(project, bytes, retries=2, &error_callback) connection.connect! unless connection.connected? connection.write(bytes) - connection.read.each(&error_callback) + connection.read.each(&error_callback) unless error_callback.nil? rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET connection.disconnect! retry if (retries -= 1) > 0 From 668cf92de429b82898e39e96a9b6c933745d9b41 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:24:18 -0400 Subject: [PATCH 96/98] version bump --- lib/racoon.rb | 2 +- racoon.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index 9f5e14e..4286ee7 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -8,5 +8,5 @@ require 'racoon/firehose' module Racoon - VERSION = "0.6.0" + VERSION = "1.0.0" end diff --git a/racoon.gemspec b/racoon.gemspec index ac2e9ad..33cf01f 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "0.6.0" + s.version = "1.0.0" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"] From f4715845cda09f4beb30c0d26f4c260d05af0baf Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:36:45 -0400 Subject: [PATCH 97/98] changed api in a very minor way. making error_callback an ivar instead --- lib/racoon/firehose.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/racoon/firehose.rb b/lib/racoon/firehose.rb index c8ce04d..3641523 100644 --- a/lib/racoon/firehose.rb +++ b/lib/racoon/firehose.rb @@ -10,13 +10,14 @@ module Racoon class Firehose - attr_accessor :connections, :feedback_callback + 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) @@ -32,7 +33,7 @@ def on_readable(socket, messages) private - def apns(project, bytes, retries=2, &error_callback) + def apns(project, bytes, retries=2) uri = "gateway.#{project[:sandbox] ? 'sandbox.' : ''}push.apple.com" hash = Digest::SHA1.hexdigest("#{project[:name]}-#{project[:certificate]}") @@ -44,7 +45,7 @@ def apns(project, bytes, retries=2, &error_callback) connection.connect! unless connection.connected? connection.write(bytes) - connection.read.each(&error_callback) unless error_callback.nil? + connection.read.each(&@error_callback) if @error_callback rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET connection.disconnect! retry if (retries -= 1) > 0 From 5e1c435a9468e9d850e3aad553f0af19b3185260 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Sun, 1 May 2011 01:49:14 -0400 Subject: [PATCH 98/98] version bump --- lib/racoon.rb | 2 +- racoon.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/racoon.rb b/lib/racoon.rb index 4286ee7..9887807 100644 --- a/lib/racoon.rb +++ b/lib/racoon.rb @@ -8,5 +8,5 @@ require 'racoon/firehose' module Racoon - VERSION = "1.0.0" + VERSION = "1.0.1" end diff --git a/racoon.gemspec b/racoon.gemspec index 33cf01f..c80da74 100644 --- a/racoon.gemspec +++ b/racoon.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = %q{racoon} - s.version = "1.0.0" + s.version = "1.0.1" s.platform = Gem::Platform::RUBY s.authors = ["Jeremy Tregunna"]