Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 94a5b96b58c279b624de8ead610b14f1ece4dc26 @tompesman committed Jun 15, 2012
@@ -0,0 +1,4 @@
+.bundle/
+log/*.log
+pkg/
+Gemfile.lock
16 Gemfile
@@ -0,0 +1,16 @@
+source "http://rubygems.org"
+
+# Declare your gem's dependencies in push-apns.gemspec.
+# Bundler will treat runtime dependencies like base dependencies, and
+# development dependencies will be added by default to the :development group.
+gemspec
+
+# Declare any dependencies that are still in development here instead of in
+# your gemspec. These might include edge Rails or gems from your path or
+# Git. Remember to move these dependencies to your gemspec before releasing
+# your gem to rubygems.org.
+
+# To use debugger
+# gem 'ruby-debug19', :require => 'ruby-debug'
+
+gem 'push-core', :path => "~/code/push/push-core"
@@ -0,0 +1,20 @@
+Copyright 2012 YOURNAME
+
+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.
@@ -0,0 +1,3 @@
+= PushApns
+
+This project rocks and uses MIT-LICENSE.
@@ -0,0 +1,11 @@
+require 'socket'
+require 'pathname'
+require 'push-apns/version'
+require 'push/apns/binary_notification_validator'
+require 'push/message_apns'
+require 'push/feedback_apns'
+require 'push/daemon/apns'
+require 'push/daemon/interruptible_sleep'
+require 'push/daemon/apns_support/certificate'
+require 'push/daemon/apns_support/connection_apns'
+require 'push/daemon/apns_support/feedback_receiver'
@@ -0,0 +1,3 @@
+module PushApns
+ VERSION = "0.0.1.pre"
+end
@@ -0,0 +1,12 @@
+module Push
+ module Apns
+ class BinaryNotificationValidator < ActiveModel::Validator
+
+ def validate(record)
+ if record.payload_size > 256
+ record.errors[:base] << "APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes."
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,41 @@
+module Push
+ module Daemon
+ class Apns
+ attr_accessor :configuration, :certificate
+
+ def initialize(options)
+ self.configuration = options
+
+ self.certificate = ApnsSupport::Certificate.new(configuration[:certificate])
+ certificate.load
+
+ start_feedback
+ end
+
+ def pushconnections
+ self.configuration[:connections]
+ end
+
+ def totalconnections
+ # + feedback
+ pushconnections + 1
+ end
+
+ def connectiontype
+ ApnsSupport::ConnectionApns
+ end
+
+ def start_feedback
+ ApnsSupport::FeedbackReceiver.start(self)
+ end
+
+ def stop_feedback
+ ApnsSupport::FeedbackReceiver.stop
+ end
+
+ def stop
+ stop_feedback
+ end
+ end
+ end
+end
@@ -0,0 +1,37 @@
+module Push
+ class CertificateError < StandardError; end
+
+ module Daemon
+ module ApnsSupport
+ class Certificate
+ attr_accessor :certificate
+
+ def initialize(certificate_path)
+ @certificate_path = path(certificate_path)
+ end
+
+ def path(path)
+ if Pathname.new(path).absolute?
+ path
+ else
+ File.join(Rails.root, "config", "push", path)
+ end
+ end
+
+ def load
+ @certificate = read_certificate
+ end
+
+ protected
+
+ def read_certificate
+ if !File.exists?(@certificate_path)
+ raise CertificateError, "#{@certificate_path} does not exist. The certificate location can be configured in config/push/<<environment>>.rb"
+ else
+ File.read(@certificate_path)
+ end
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,96 @@
+module Push
+ module Daemon
+ module ApnsSupport
+ class ConnectionApns
+ attr_reader :name, :provider
+
+ def initialize(provider, i=nil)
+ @provider = provider
+ if i
+ # Apns push connection
+ @name = "ConnectionApns #{i}"
+ @host = "gateway.#{provider.configuration[:sandbox] ? 'sandbox.' : ''}push.apple.com"
+ @port = 2195
+ else
+ @name = "FeedbackReceiver"
+ @host = "feedback.#{provider.configuration[:sandbox] ? 'sandbox.' : ''}push.apple.com"
+ @port = 2196
+ end
+ end
+
+ def connect
+ @ssl_context = setup_ssl_context
+ @tcp_socket, @ssl_socket = connect_socket
+ end
+
+ def close
+ begin
+ @ssl_socket.close if @ssl_socket
+ @tcp_socket.close if @tcp_socket
+ rescue IOError
+ end
+ end
+
+ def read(num_bytes)
+ @ssl_socket.read(num_bytes)
+ end
+
+ def select(timeout)
+ IO.select([@ssl_socket], nil, nil, timeout)
+ end
+
+ def write(data)
+ retry_count = 0
+
+ begin
+ write_data(data)
+ rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
+ retry_count += 1;
+
+ if retry_count == 1
+ Push::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
+ end
+
+ if retry_count <= 3
+ reconnect
+ sleep 1
+ retry
+ else
+ raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
+ end
+ end
+ end
+
+ def reconnect
+ close
+ @tcp_socket, @ssl_socket = connect_socket
+ end
+
+ protected
+
+ def write_data(data)
+ @ssl_socket.write(data)
+ @ssl_socket.flush
+ end
+
+ def setup_ssl_context
+ ssl_context = OpenSSL::SSL::SSLContext.new
+ ssl_context.key = OpenSSL::PKey::RSA.new(provider.certificate.certificate, provider.configuration[:certificate_password])
+ ssl_context.cert = OpenSSL::X509::Certificate.new(provider.certificate.certificate)
+ ssl_context
+ end
+
+ def connect_socket
+ tcp_socket = TCPSocket.new(@host, @port)
+ tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
+ tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
+ ssl_socket.sync = true
+ ssl_socket.connect
+ Push::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
+ [tcp_socket, ssl_socket]
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,58 @@
+module Push
+ module Daemon
+ module ApnsSupport
+ class FeedbackReceiver
+ extend Push::Daemon::InterruptibleSleep
+ attr_accessor :provider
+ FEEDBACK_TUPLE_BYTES = 38
+
+ def self.start(provider)
+ @provider = provider
+ @thread = Thread.new do
+ loop do
+ break if @stop
+ check_for_feedback
+ interruptible_sleep @provider.configuration[:feedback_poll]
+ end
+ end
+ end
+
+ def self.stop
+ @stop = true
+ interrupt_sleep
+ @thread.join if @thread
+ end
+
+ def self.check_for_feedback
+ connection = nil
+ begin
+ connection = ApnsSupport::ConnectionApns.new(@provider)
+ connection.connect
+
+ while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
+ timestamp, device = parse_tuple(tuple)
+ create_feedback(timestamp, device)
+ end
+ rescue StandardError => e
+ Push::Daemon.logger.error(e)
+ ensure
+ connection.close if connection
+ end
+ end
+
+ protected
+
+ def self.parse_tuple(tuple)
+ failed_at, _, device = tuple.unpack("N1n1H*")
+ [Time.at(failed_at).utc, device]
+ end
+
+ def self.create_feedback(failed_at, device)
+ formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
+ Push::Daemon.logger.info("[FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device}")
+ Push::FeedbackApns.create!(:failed_at => failed_at, :device => device)
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,5 @@
+module Push
+ class FeedbackApns < Push::Feedback
+ validates :device, :format => { :with => /\A[a-z0-9]{64}\z/ }
+ end
+end
Oops, something went wrong.

0 comments on commit 94a5b96

Please sign in to comment.