From 5c508005cf8fcb4176ef0b2749c1344c4b0d89e0 Mon Sep 17 00:00:00 2001 From: Jeremy Tregunna Date: Thu, 17 Mar 2011 22:20:52 -0400 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 badcad37e953e418a71c7b46d98b7937b174ca27 Mon Sep 17 00:00:00 2001
From: Ed McManus 
Date: Thu, 21 Apr 2011 17:06:48 -0700
Subject: [PATCH 19/22] Some modifications to the expected job format, updated
 Notification.push

---
 lib/apnserver/client.rb       |  2 +-
 lib/apnserver/notification.rb | 25 ++++++++--------
 lib/apnserver/server.rb       | 54 ++++++++++++++++++++++++++++-------
 3 files changed, 58 insertions(+), 23 deletions(-)

diff --git a/lib/apnserver/client.rb b/lib/apnserver/client.rb
index 389a72f..967c2df 100644
--- a/lib/apnserver/client.rb
+++ b/lib/apnserver/client.rb
@@ -39,4 +39,4 @@ def connected?
       @ssl
     end
   end
-end
\ No newline at end of file
+end
diff --git a/lib/apnserver/notification.rb b/lib/apnserver/notification.rb
index 4fa12a4..54475b8 100644
--- a/lib/apnserver/notification.rb
+++ b/lib/apnserver/notification.rb
@@ -5,7 +5,7 @@
 module ApnServer
   class Config
     class << self
-      attr_accessor :logger
+      attr_accessor :logger, :project_name, :project_beanstalk_pool, :project_pem_location, :project_use_sandbox
     end
   end
 
@@ -30,17 +30,18 @@ def json_payload
       j
     end
 
-    def push
-      if Config.pem.nil?
-        socket = TCPSocket.new(Config.host || 'localhost', Config.port.to_i || 22195)
-        socket.write(to_bytes)
-        socket.close
-      else
-        client = ApnServer::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195)
-        client.connect!
-        client.write(self)
-        client.disconnect!
-      end
+    def push(b64_device_token)
+        job = { 
+          :project => { 
+            :name => Config.project_name,
+            :certificate => Config.project_pem_location,
+          },
+          :sandbox => Config.project_use_sandbox,
+          :notification => self.payload,
+          :device_token => b64_device_token,
+        }
+        beanstalk = Beanstalk::Pool.new(Config.project_beanstalk_pool)
+        beanstalk.yput(job)
     end
 
     def to_bytes
diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb
index be4405e..1d2c4aa 100644
--- a/lib/apnserver/server.rb
+++ b/lib/apnserver/server.rb
@@ -1,4 +1,6 @@
 require 'beanstalk-client'
+require 'yajl'
+require 'base64'
 
 module ApnServer
   class Server
@@ -48,31 +50,57 @@ 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.
+    # Jobs should be posted as a YAML encoded hash in the following format:
+    #
+    #   @job = { 
+    #     :project => { 
+    #       :name => "example",
+    #       :certificate => "/path/to/pem.pem",
+    #     },
+    #     :sandbox => Rails.env.development?,
+    #     :notification => notification.json_payload, 
+    #     :device_token => notification.device_token, # Base64 encoded
+    #   }
+    #
+    # Receipt UUID's may be added in later.
+
     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])
+
+      # Build new notification object from our hash
+      apn_data = packet[:notification][:aps]
+
+      notification = Notification.new
+      notification.device_token = Base64.decode64(packet[:device_token])
+
+      notification.badge = apn_data[:badge] if apn_data.has_key?(:badge)
+      notification.alert = apn_data[:alert] if apn_data.has_key?(:alert)
+      notification.sound = apn_data[:sound] if apn_data.has_key?(:sound)
+      notification.custom = apn_data[:custom] if apn_data.has_key?(:custom)
+
+      # TODO skip this file read unless necessary
+      certificate_data = File.read(project[:certificate])
+
+      if notification
+        client = get_client(project[:name], certificate_data, packet[:sandbox])
         begin
+          Config.logger.debug "Connection already open" if client.connected?
           client.connect! unless client.connected?
           client.write(notification)
           job.delete
+          Config.logger.info "Notification should've been deleted, keeping socket open."
           # 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"
+          Config.logger.error "Caught Error, closing connecting and adding notification back to queue."
           client.disconnect!
           # Queue back up the notification
           job.release
+          Config.logger.info "Notification should've been released."
         rescue RuntimeError => e
           Config.logger.error "Unable to handle: #{e}"
           # TODO: Find the receipt and write the failed_at property.
@@ -82,7 +110,13 @@ def handle_job(job)
           #end
           job.delete
         end
+      else
+        Config.logger.error "Unable to create payload, deleting message."
+        job.delete
       end
+    rescue Exception => e
+        Config.logger.error "#{$!} -- Printing Backtrace:\n #{e.backtrace.join "\n"}, deleting job"
+        job.delete
     end
 
     def get_client(project_name, certificate, sandbox = false)
@@ -101,4 +135,4 @@ def get_client(project_name, certificate, sandbox = false)
       client
     end
   end
-end
\ No newline at end of file
+end

From 09b944fdb716bfb85dd99de8506cb08bad897d13 Mon Sep 17 00:00:00 2001
From: Ed McManus 
Date: Thu, 21 Apr 2011 18:37:54 -0700
Subject: [PATCH 20/22] Encountered an issue with unicode characters in
 Notification.alert. For now all alert strings are mapped to ASCII.

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

diff --git a/lib/apnserver/notification.rb b/lib/apnserver/notification.rb
index 54475b8..9fa4e03 100644
--- a/lib/apnserver/notification.rb
+++ b/lib/apnserver/notification.rb
@@ -31,15 +31,25 @@ def json_payload
     end
 
     def push(b64_device_token)
+        safe_payload = self.payload
+
+        # Force conversion to lower 127 bits - non-ascii characters appear to
+        # be cause EXPECTED_CRLF. Haven't looked further into the issue. FIXME.
+        if safe_payload[:aps].has_key? :alert
+          converter = Iconv.new('ASCII//IGNORE//TRANSLIT', 'UTF-8') 
+          safe_payload[:aps][:alert] = converter.iconv(safe_payload[:aps][:alert]).unpack('U*').select{ |cp| cp < 127 }.pack('U*')
+        end
+        
         job = { 
           :project => { 
             :name => Config.project_name,
             :certificate => Config.project_pem_location,
           },
           :sandbox => Config.project_use_sandbox,
-          :notification => self.payload,
+          :notification => safe_payload,
           :device_token => b64_device_token,
         }
+
         beanstalk = Beanstalk::Pool.new(Config.project_beanstalk_pool)
         beanstalk.yput(job)
     end
diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb
index 1d2c4aa..3166e8b 100644
--- a/lib/apnserver/server.rb
+++ b/lib/apnserver/server.rb
@@ -81,7 +81,7 @@ def handle_job(job)
 
       # TODO skip this file read unless necessary
       certificate_data = File.read(project[:certificate])
-
+      
       if notification
         client = get_client(project[:name], certificate_data, packet[:sandbox])
         begin

From d155fd11b20abc65289f7fc36e360cba31bd3b96 Mon Sep 17 00:00:00 2001
From: Ed McManus 
Date: Sat, 23 Apr 2011 00:07:51 -0700
Subject: [PATCH 21/22] Lowered logger level to INFO

---
 bin/apnserverd | 1 +
 1 file changed, 1 insertion(+)

diff --git a/bin/apnserverd b/bin/apnserverd
index 21b77da..ed2a96b 100755
--- a/bin/apnserverd
+++ b/bin/apnserverd
@@ -57,6 +57,7 @@ opts.each do |opt, arg|
 end
 
 ApnServer::Config.logger = Logger.new(@log_file)
+ApnServer::Config.logger.level = Logger::DEBUG
 
 daemonize if daemon
 server = ApnServer::Server.new(beanstalks) do |feedback_record|

From 13df807350e63ae02552de0dd50ae3bbac2bf2a7 Mon Sep 17 00:00:00 2001
From: Ed McManus 
Date: Sat, 23 Apr 2011 01:29:09 -0700
Subject: [PATCH 22/22] Removed feedback service checks, disabled persistent
 sockets

---
 lib/apnserver/server.rb | 57 +++++++++++++++++++++++++++++------------
 1 file changed, 40 insertions(+), 17 deletions(-)

diff --git a/lib/apnserver/server.rb b/lib/apnserver/server.rb
index 3166e8b..cde6957 100644
--- a/lib/apnserver/server.rb
+++ b/lib/apnserver/server.rb
@@ -19,21 +19,21 @@ def beanstalk
 
     def start!
       EventMachine::run 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
-            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(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
+            #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
           begin
@@ -83,25 +83,48 @@ def handle_job(job)
       certificate_data = File.read(project[:certificate])
       
       if notification
+
         client = get_client(project[:name], certificate_data, packet[:sandbox])
+
         begin
+
           Config.logger.debug "Connection already open" if client.connected?
-          client.connect! unless client.connected?
+
+          # TODO - FIXME - WTF Does Apple want us to do? Persist or tear-down?
+          # We can't seem to keep a server instance running for more than 1 day
+          # when we try to persist. UGH!
+
+          # Old - allow socket to persist
+          #client.connect! unless client.connected?
+          #client.write(notification)
+          #job.delete
+
+          # New - create and tear down a socket for each notification
+          client.connect!
           client.write(notification)
+          client.disconnect!
+
+          # Finished!
           job.delete
+
           Config.logger.info "Notification should've been deleted, keeping socket open."
+
           # 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!
+          client.disconnect! if client.connected?
           # Queue back up the notification
           job.release
           Config.logger.info "Notification should've been released."
+
         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]]