From f02358e2207dfdca6a861ccc157be42ef2fa6a4e Mon Sep 17 00:00:00 2001 From: Aaron Gotwalt Date: Wed, 19 Dec 2012 08:41:38 -0800 Subject: [PATCH] Added support for apple feedback service. --- lib/em-apn.rb | 2 ++ lib/em-apn/client.rb | 20 +++++++++++++ lib/em-apn/failed_delivery_attempt.rb | 18 +++++++++++ lib/em-apn/feedback_connection.rb | 43 +++++++++++++++++++++++++++ spec/em-apn/client_spec.rb | 41 +++++++++++++++++++++++++ 5 files changed, 124 insertions(+) create mode 100644 lib/em-apn/failed_delivery_attempt.rb create mode 100644 lib/em-apn/feedback_connection.rb diff --git a/lib/em-apn.rb b/lib/em-apn.rb index cdafe21..e9eddea 100644 --- a/lib/em-apn.rb +++ b/lib/em-apn.rb @@ -4,6 +4,8 @@ require "yajl" require "logger" require "em-apn/client" +require "em-apn/feedback_connection" +require 'em-apn/failed_delivery_attempt' require "em-apn/connection" require "em-apn/notification" require "em-apn/log_message" diff --git a/lib/em-apn/client.rb b/lib/em-apn/client.rb index dfaa7ca..82896c4 100644 --- a/lib/em-apn/client.rb +++ b/lib/em-apn/client.rb @@ -6,13 +6,19 @@ class Client SANDBOX_GATEWAY = "gateway.sandbox.push.apple.com" PRODUCTION_GATEWAY = "gateway.push.apple.com" PORT = 2195 + SANDBOX_FEEDBACK_GATEWAY = "feedback.sandbox.push.apple.com" + PRODUCTION_FEEDBACK_GATEWAY = "feedback.push.apple.com" + FEEDBACK_PORT = 2196 + attr_reader :gateway, :port, :key, :cert, :connection, :error_callback, :close_callback, :open_callback + attr_reader :feedback_connection, :feedback_gateway, :feedback_port, :feedback_callback # A convenience method for creating and connecting. def self.connect(options = {}) new(options).tap do |client| client.connect + client.connect_feedback end end @@ -24,13 +30,23 @@ def initialize(options = {}) @gateway = options[:gateway] || ENV["APN_GATEWAY"] @gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_GATEWAY : SANDBOX_GATEWAY + + @feedback_gateway = options[:feedback_gateway] || ENV["APN_FEEDBACK_GATEWAY"] + @feedback_gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_FEEDBACK_GATEWAY : SANDBOX_FEEDBACK_GATEWAY + @feedback_port = options[:feedback_port] || FEEDBACK_PORT + @connection = nil + @feedback_connection = nil end def connect @connection = EM.connect(gateway, port, Connection, self) end + def connect_feedback + @feedback_connection = EM.connect(feedback_gateway, feedback_port, FeedbackConnection, self) + end + def deliver(notification) notification.validate! connect if connection.nil? || connection.disconnected? @@ -50,6 +66,10 @@ def on_open(&block) @open_callback = block end + def on_feedback(&block) + @feedback_callback = block + end + def log(notification) EM::APN.logger.info("TOKEN=#{notification.token} PAYLOAD=#{notification.payload.inspect}") end diff --git a/lib/em-apn/failed_delivery_attempt.rb b/lib/em-apn/failed_delivery_attempt.rb new file mode 100644 index 0000000..b69ba20 --- /dev/null +++ b/lib/em-apn/failed_delivery_attempt.rb @@ -0,0 +1,18 @@ +module EventMachine + module APN + class FailedDeliveryAttempt + LENGTH = 38 + + attr_accessor :timestamp, :device_token + + def initialize(binary_tuple) + # N => 4 byte timestamp + # n => 2 byte token_length + # H64 => 32 byte device_token + seconds, _, @device_token = binary_tuple.unpack('NnH64') + raise ArgumentError('invalid format') unless seconds && @device_token + @timestamp = Time.at(seconds) + end + end + end +end diff --git a/lib/em-apn/feedback_connection.rb b/lib/em-apn/feedback_connection.rb new file mode 100644 index 0000000..c01dae3 --- /dev/null +++ b/lib/em-apn/feedback_connection.rb @@ -0,0 +1,43 @@ +module EventMachine + module APN + class FeedbackConnection < EM::Connection + attr_reader :client + + def initialize(*args) + super + @client = args.last + @disconnected = false + end + + def disconnected? + @disconnected + end + + def post_init + start_tls( + :private_key_file => client.key, + :cert_chain_file => client.cert, + :verify_peer => false + ) + end + + def connection_completed + EM::APN.logger.info("Feedback connection completed") + end + + def receive_data(data) + attempt = FailedDeliveryAttempt.new(data) + EM::APN.logger.warn(attempt.to_s) + + if client.feedback_callback + client.feedback_callback.call(attempt) + end + end + + def unbind + @disconnected = true + EM::APN.logger.info("Feedback connection closed") + end + end + end +end diff --git a/spec/em-apn/client_spec.rb b/spec/em-apn/client_spec.rb index bd9235b..d90fbaa 100644 --- a/spec/em-apn/client_spec.rb +++ b/spec/em-apn/client_spec.rb @@ -13,6 +13,7 @@ def new_client(*args) it "creates a new client without a connection" do client = EM::APN::Client.new client.connection.should be_nil + client.feedback_connection.should be_nil end context "configuring the gateway" do @@ -191,4 +192,44 @@ def new_client(*args) called.should be_true end end + + describe "#connect_feedback" do + it "creates a connection to the feedback service" do + client = EM::APN::Client.new + client.feedback_connection.should be_nil + + EM.run_block { client.connect_feedback } + client.feedback_connection.should be_an_instance_of(EM::APN::FeedbackConnection) + end + + it "passes the client to the new connection" do + client = EM::APN::Client.new + connection = double(EM::APN::FeedbackConnection).as_null_object + + EM::APN::FeedbackConnection.should_receive(:new).with(instance_of(Fixnum), client).and_return(connection) + EM.run_block { client.connect_feedback } + end + end + + describe "#on_feedback" do + it "sets a callback that is invoked when we receive data from Apple" do + failed_attempt = nil + + + timestamp = Time.utc(1995, 12, 21) + device_token = 'fe15a27d5df3c34778defb1f4f3980265cc52c0c047682223be59fb68500a9a2' + tuple = [timestamp.to_i, 32, device_token].pack('NnH64') + + EM.run_block do + client = EM::APN::Client.new + client.connect_feedback + client.on_feedback { |data | failed_attempt = data } + client.feedback_connection.receive_data(tuple) + end + + failed_attempt.should be_an_instance_of(EM::APN::FailedDeliveryAttempt) + failed_attempt.device_token.should == device_token + failed_attempt.timestamp.should == timestamp + end + end end