rapns_rails_2 3.4.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +7 -0
- data/README.md +168 -0
- data/bin/rapns +37 -0
- data/config/database.yml +44 -0
- data/lib/generators/rapns_generator.rb +25 -0
- data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/add_gcm.rb +95 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +15 -0
- data/lib/generators/templates/create_rapns_notifications.rb +26 -0
- data/lib/generators/templates/rapns.rb +87 -0
- data/lib/rapns/TODO +3 -0
- data/lib/rapns/apns/app.rb +25 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +16 -0
- data/lib/rapns/apns/notification.rb +91 -0
- data/lib/rapns/apns_feedback.rb +13 -0
- data/lib/rapns/app.rb +16 -0
- data/lib/rapns/configuration.rb +89 -0
- data/lib/rapns/daemon/apns/app_runner.rb +26 -0
- data/lib/rapns/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rapns/daemon/apns/connection.rb +142 -0
- data/lib/rapns/daemon/apns/delivery.rb +64 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +35 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +89 -0
- data/lib/rapns/daemon/app_runner.rb +179 -0
- data/lib/rapns/daemon/batch.rb +112 -0
- data/lib/rapns/daemon/delivery.rb +23 -0
- data/lib/rapns/daemon/delivery_error.rb +19 -0
- data/lib/rapns/daemon/delivery_handler.rb +52 -0
- data/lib/rapns/daemon/delivery_handler_collection.rb +33 -0
- data/lib/rapns/daemon/feeder.rb +65 -0
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +228 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon/interruptible_sleep.rb +65 -0
- data/lib/rapns/daemon/reflectable.rb +13 -0
- data/lib/rapns/daemon/store/active_record/reconnectable.rb +66 -0
- data/lib/rapns/daemon/store/active_record.rb +128 -0
- data/lib/rapns/daemon.rb +129 -0
- data/lib/rapns/deprecatable.rb +23 -0
- data/lib/rapns/deprecation.rb +23 -0
- data/lib/rapns/embed.rb +28 -0
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +37 -0
- data/lib/rapns/gcm/payload_data_size_validator.rb +13 -0
- data/lib/rapns/gcm/registration_ids_count_validator.rb +13 -0
- data/lib/rapns/logger.rb +76 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +62 -0
- data/lib/rapns/notifier.rb +35 -0
- data/lib/rapns/push.rb +17 -0
- data/lib/rapns/rails-2-compatibility.rb +34 -0
- data/lib/rapns/reflection.rb +44 -0
- data/lib/rapns/upgraded.rb +31 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns_rails_2.rb +67 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/test.rake +38 -0
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/tmp/.gitkeep +0 -0
- data/spec/unit/apns/app_spec.rb +29 -0
- data/spec/unit/apns/feedback_spec.rb +9 -0
- data/spec/unit/apns/notification_spec.rb +215 -0
- data/spec/unit/apns_feedback_spec.rb +21 -0
- data/spec/unit/app_spec.rb +16 -0
- data/spec/unit/configuration_spec.rb +55 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +45 -0
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
- data/spec/unit/daemon/apns/connection_spec.rb +287 -0
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +59 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +134 -0
- data/spec/unit/daemon/app_runner_shared.rb +83 -0
- data/spec/unit/daemon/app_runner_spec.rb +170 -0
- data/spec/unit/daemon/batch_spec.rb +219 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_handler_collection_spec.rb +37 -0
- data/spec/unit/daemon/delivery_handler_shared.rb +45 -0
- data/spec/unit/daemon/feeder_spec.rb +81 -0
- data/spec/unit/daemon/gcm/app_runner_spec.rb +19 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +44 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +289 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
- data/spec/unit/daemon/store/active_record_spec.rb +281 -0
- data/spec/unit/daemon_spec.rb +157 -0
- data/spec/unit/deprecatable_spec.rb +32 -0
- data/spec/unit/deprecation_spec.rb +15 -0
- data/spec/unit/embed_spec.rb +50 -0
- data/spec/unit/gcm/app_spec.rb +4 -0
- data/spec/unit/gcm/notification_spec.rb +52 -0
- data/spec/unit/logger_spec.rb +180 -0
- data/spec/unit/notification_shared.rb +45 -0
- data/spec/unit/notification_spec.rb +4 -0
- data/spec/unit/notifier_spec.rb +32 -0
- data/spec/unit/push_spec.rb +44 -0
- data/spec/unit/rapns_spec.rb +9 -0
- data/spec/unit/reflection_spec.rb +30 -0
- data/spec/unit/upgraded_spec.rb +40 -0
- data/spec/unit_spec_helper.rb +137 -0
- metadata +232 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class App < Rapns::App
|
4
|
+
validates_presence_of :environment
|
5
|
+
validates_inclusion_of :environment, :in => %w(development production sandbox)
|
6
|
+
validates_presence_of :certificate
|
7
|
+
validate :certificate_has_matching_private_key
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def certificate_has_matching_private_key
|
12
|
+
result = false
|
13
|
+
if certificate.present?
|
14
|
+
x509 = OpenSSL::X509::Certificate.new(certificate) rescue nil
|
15
|
+
pkey = OpenSSL::PKey::RSA.new(certificate, password) rescue nil
|
16
|
+
result = !x509.nil? && !pkey.nil?
|
17
|
+
unless result
|
18
|
+
errors.add :certificate, 'Certificate value must contain a certificate and a private key.'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class BinaryNotificationValidator < ActiveModel::Validator
|
4
|
+
|
5
|
+
def validate(record)
|
6
|
+
if record.payload_size > 256
|
7
|
+
record.errors.add(:base, "APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes.")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class Feedback < ActiveRecord::Base
|
4
|
+
self.table_name = 'rapns_feedback'
|
5
|
+
|
6
|
+
if Rapns.attr_accessible_available?
|
7
|
+
attr_accessible :device_token, :failed_at, :app
|
8
|
+
end
|
9
|
+
|
10
|
+
validates_presence_of :device_token
|
11
|
+
validates_presence_of :failed_at
|
12
|
+
|
13
|
+
validates_with Rapns::Apns::DeviceTokenFormatValidator
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class Notification < Rapns::Notification
|
4
|
+
class MultipleAppAssignmentError < StandardError; end
|
5
|
+
|
6
|
+
validates_presence_of :device_token
|
7
|
+
validates_numericality_of :badge, :allow_nil => true
|
8
|
+
|
9
|
+
validates_with Rapns::Apns::DeviceTokenFormatValidator
|
10
|
+
validates_with Rapns::Apns::BinaryNotificationValidator
|
11
|
+
|
12
|
+
alias_method :attributes_for_device=, :data=
|
13
|
+
alias_method :attributes_for_device, :data
|
14
|
+
|
15
|
+
def device_token=(token)
|
16
|
+
write_attribute(:device_token, token.delete(" <>")) if !token.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def alert=(alert)
|
20
|
+
if alert.is_a?(Hash)
|
21
|
+
write_attribute(:alert, multi_json_dump(alert))
|
22
|
+
self.alert_is_json = true if has_attribute?(:alert_is_json)
|
23
|
+
else
|
24
|
+
write_attribute(:alert, alert)
|
25
|
+
self.alert_is_json = false if has_attribute?(:alert_is_json)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def alert
|
30
|
+
string_or_json = read_attribute(:alert)
|
31
|
+
|
32
|
+
if has_attribute?(:alert_is_json)
|
33
|
+
if alert_is_json?
|
34
|
+
multi_json_load(string_or_json)
|
35
|
+
else
|
36
|
+
string_or_json
|
37
|
+
end
|
38
|
+
else
|
39
|
+
multi_json_load(string_or_json) rescue string_or_json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
MDM_KEY = '__rapns_mdm__'
|
44
|
+
def mdm=(magic)
|
45
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ MDM_KEY => magic })
|
46
|
+
end
|
47
|
+
|
48
|
+
CONTENT_AVAILABLE_KEY = '__rapns_content_available__'
|
49
|
+
def content_available=(bool)
|
50
|
+
return unless bool
|
51
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ CONTENT_AVAILABLE_KEY => true })
|
52
|
+
end
|
53
|
+
|
54
|
+
def as_json
|
55
|
+
json = ActiveSupport::OrderedHash.new
|
56
|
+
|
57
|
+
if attributes_for_device && attributes_for_device.key?(MDM_KEY)
|
58
|
+
json['mdm'] = attributes_for_device[MDM_KEY]
|
59
|
+
else
|
60
|
+
json['aps'] = ActiveSupport::OrderedHash.new
|
61
|
+
json['aps']['alert'] = alert if alert
|
62
|
+
json['aps']['badge'] = badge if badge
|
63
|
+
json['aps']['sound'] = sound if sound
|
64
|
+
|
65
|
+
if attributes_for_device && attributes_for_device[CONTENT_AVAILABLE_KEY]
|
66
|
+
json['aps']['content-available'] = 1
|
67
|
+
end
|
68
|
+
|
69
|
+
if attributes_for_device
|
70
|
+
non_aps_attributes = attributes_for_device.reject { |k, v| k == CONTENT_AVAILABLE_KEY }
|
71
|
+
non_aps_attributes.each { |k, v| json[k.to_s] = v }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
json
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_binary(options = {})
|
79
|
+
id_for_pack = options[:for_validation] ? 0 : id
|
80
|
+
[1, id_for_pack, expiry, 0, 32, device_token, payload_size, payload].pack("cNNccH*na*")
|
81
|
+
end
|
82
|
+
|
83
|
+
def data=(attrs)
|
84
|
+
return unless attrs
|
85
|
+
raise ArgumentError, "must be a Hash" if !attrs.is_a?(Hash)
|
86
|
+
super attrs.merge(data || {})
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Rapns
|
2
|
+
def self.apns_feedback
|
3
|
+
Rapns.require_for_daemon
|
4
|
+
Rapns::Daemon.initialize_store
|
5
|
+
|
6
|
+
Rapns::Apns::App.all.each do |app|
|
7
|
+
receiver = Rapns::Daemon::Apns::FeedbackReceiver.new(app, 0)
|
8
|
+
receiver.check_for_feedback
|
9
|
+
end
|
10
|
+
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
data/lib/rapns/app.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rapns
|
2
|
+
class App < ActiveRecord::Base
|
3
|
+
self.table_name = 'rapns_apps'
|
4
|
+
self.store_full_sti_class = true
|
5
|
+
|
6
|
+
if Rapns.attr_accessible_available?
|
7
|
+
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key
|
8
|
+
end
|
9
|
+
|
10
|
+
has_many :notifications, :class_name => 'Rapns::Notification', :dependent => :destroy
|
11
|
+
|
12
|
+
validates_presence_of :name
|
13
|
+
validates_uniqueness_of :name, :scope => [:type, :environment]
|
14
|
+
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Rapns
|
2
|
+
def self.config
|
3
|
+
@config ||= Rapns::Configuration.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configure
|
7
|
+
yield config if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll, :embedded,
|
11
|
+
:airbrake_notify, :check_for_errors, :pid_file, :batch_size,
|
12
|
+
:push, :store, :logger, :batch_storage_updates, :udp_wake_host, :udp_wake_port]
|
13
|
+
|
14
|
+
class ConfigurationWithoutDefaults < Struct.new(*CONFIG_ATTRS)
|
15
|
+
end
|
16
|
+
|
17
|
+
class Configuration < Struct.new(*CONFIG_ATTRS)
|
18
|
+
include Deprecatable
|
19
|
+
|
20
|
+
attr_accessor :apns_feedback_callback
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
super
|
24
|
+
set_defaults
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(other)
|
28
|
+
CONFIG_ATTRS.each do |attr|
|
29
|
+
other_value = other.send(attr)
|
30
|
+
send("#{attr}=", other_value) unless other_value.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def airbrake_notify=(bool)
|
35
|
+
Rapns::Deprecation.warn("airbrake_notify is deprecated. Please use the Rapns.reflect API instead.")
|
36
|
+
super(bool)
|
37
|
+
end
|
38
|
+
|
39
|
+
def pid_file=(path)
|
40
|
+
if path && !Pathname.new(path).absolute?
|
41
|
+
super(File.join(Rails.root, path))
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def logger=(logger)
|
48
|
+
super(logger)
|
49
|
+
end
|
50
|
+
|
51
|
+
def foreground=(bool)
|
52
|
+
if Rapns.jruby?
|
53
|
+
# The JVM does not support fork().
|
54
|
+
super(true)
|
55
|
+
else
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_apns_feedback(&block)
|
61
|
+
self.apns_feedback_callback = block
|
62
|
+
end
|
63
|
+
deprecated(:on_apns_feedback, 3.2, "Please use the Rapns.reflect API instead.")
|
64
|
+
|
65
|
+
def set_defaults
|
66
|
+
if Rapns.jruby?
|
67
|
+
# The JVM does not support fork().
|
68
|
+
self.foreground = true
|
69
|
+
else
|
70
|
+
self.foreground = false
|
71
|
+
end
|
72
|
+
|
73
|
+
self.push_poll = 2
|
74
|
+
self.feedback_poll = 60
|
75
|
+
Rapns::Deprecation.muted { self.airbrake_notify = true }
|
76
|
+
self.check_for_errors = true
|
77
|
+
self.batch_size = 5000
|
78
|
+
self.pid_file = nil
|
79
|
+
self.apns_feedback_callback = nil
|
80
|
+
self.store = :active_record
|
81
|
+
self.logger = nil
|
82
|
+
self.batch_storage_updates = true
|
83
|
+
|
84
|
+
# Internal options.
|
85
|
+
self.embedded = false
|
86
|
+
self.push = false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class AppRunner < Rapns::Daemon::AppRunner
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def after_start
|
9
|
+
unless Rapns.config.push
|
10
|
+
poll = Rapns.config.feedback_poll
|
11
|
+
@feedback_receiver = FeedbackReceiver.new(app, poll)
|
12
|
+
@feedback_receiver.start
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def after_stop
|
17
|
+
@feedback_receiver.stop if @feedback_receiver
|
18
|
+
end
|
19
|
+
|
20
|
+
def new_delivery_handler
|
21
|
+
DeliveryHandler.new(app)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class CertificateExpiredError < StandardError
|
4
|
+
attr_reader :app, :time
|
5
|
+
|
6
|
+
def initialize(app, time)
|
7
|
+
@app = app
|
8
|
+
@time = time
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
message
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
"#{app.name} certificate expired at #{time}."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class ConnectionError < StandardError; end
|
5
|
+
|
6
|
+
class Connection
|
7
|
+
include Reflectable
|
8
|
+
|
9
|
+
attr_accessor :last_write
|
10
|
+
|
11
|
+
def self.idle_period
|
12
|
+
30.minutes
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(app, host, port)
|
16
|
+
@app = app
|
17
|
+
@host = host
|
18
|
+
@port = port
|
19
|
+
@certificate = app.certificate
|
20
|
+
@password = app.password
|
21
|
+
written
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect
|
25
|
+
@ssl_context = setup_ssl_context
|
26
|
+
@tcp_socket, @ssl_socket = connect_socket
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
begin
|
31
|
+
@ssl_socket.close if @ssl_socket
|
32
|
+
@tcp_socket.close if @tcp_socket
|
33
|
+
rescue IOError
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def read(num_bytes)
|
38
|
+
@ssl_socket.read(num_bytes)
|
39
|
+
end
|
40
|
+
|
41
|
+
def select(timeout)
|
42
|
+
IO.select([@ssl_socket], nil, nil, timeout)
|
43
|
+
end
|
44
|
+
|
45
|
+
def write(data)
|
46
|
+
reconnect_idle if idle_period_exceeded?
|
47
|
+
|
48
|
+
retry_count = 0
|
49
|
+
|
50
|
+
begin
|
51
|
+
write_data(data)
|
52
|
+
rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError, IOError => e
|
53
|
+
retry_count += 1;
|
54
|
+
|
55
|
+
if retry_count == 1
|
56
|
+
Rapns.logger.error("[#{@app.name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
|
57
|
+
reflect(:apns_connection_lost, @app, e)
|
58
|
+
end
|
59
|
+
|
60
|
+
if retry_count <= 3
|
61
|
+
reconnect
|
62
|
+
sleep 1
|
63
|
+
retry
|
64
|
+
else
|
65
|
+
raise ConnectionError, "#{@app.name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def reconnect
|
71
|
+
close
|
72
|
+
@tcp_socket, @ssl_socket = connect_socket
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def reconnect_idle
|
78
|
+
Rapns.logger.info("[#{@app.name}] Idle period exceeded, reconnecting...")
|
79
|
+
reconnect
|
80
|
+
end
|
81
|
+
|
82
|
+
def idle_period_exceeded?
|
83
|
+
Time.now - last_write > self.class.idle_period
|
84
|
+
end
|
85
|
+
|
86
|
+
def write_data(data)
|
87
|
+
@ssl_socket.write(data)
|
88
|
+
@ssl_socket.flush
|
89
|
+
written
|
90
|
+
end
|
91
|
+
|
92
|
+
def written
|
93
|
+
self.last_write = Time.now
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_ssl_context
|
97
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
98
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
|
99
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
|
100
|
+
ssl_context
|
101
|
+
end
|
102
|
+
|
103
|
+
def connect_socket
|
104
|
+
check_certificate_expiration
|
105
|
+
|
106
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
107
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
108
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
109
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
110
|
+
ssl_socket.sync = true
|
111
|
+
ssl_socket.connect
|
112
|
+
Rapns.logger.info("[#{@app.name}] Connected to #{@host}:#{@port}")
|
113
|
+
[tcp_socket, ssl_socket]
|
114
|
+
end
|
115
|
+
|
116
|
+
def check_certificate_expiration
|
117
|
+
cert = @ssl_context.cert
|
118
|
+
if certificate_expired?
|
119
|
+
Rapns.logger.error(certificate_msg('expired'))
|
120
|
+
raise Rapns::Apns::CertificateExpiredError.new(@app, cert.not_after)
|
121
|
+
elsif certificate_expires_soon?
|
122
|
+
Rapns.logger.warn(certificate_msg('will expire'))
|
123
|
+
reflect(:apns_certificate_will_expire, @app, cert.not_after)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def certificate_msg(msg)
|
128
|
+
time = @ssl_context.cert.not_after.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
129
|
+
"[#{@app.name}] Certificate #{msg} at #{time}."
|
130
|
+
end
|
131
|
+
|
132
|
+
def certificate_expired?
|
133
|
+
@ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < Time.now.utc
|
134
|
+
end
|
135
|
+
|
136
|
+
def certificate_expires_soon?
|
137
|
+
@ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < (Time.now + 1.month).utc
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class Delivery < Rapns::Daemon::Delivery
|
5
|
+
SELECT_TIMEOUT = 0.2
|
6
|
+
ERROR_TUPLE_BYTES = 6
|
7
|
+
APN_ERRORS = {
|
8
|
+
1 => "Processing error",
|
9
|
+
2 => "Missing device token",
|
10
|
+
3 => "Missing topic",
|
11
|
+
4 => "Missing payload",
|
12
|
+
5 => "Missing token size",
|
13
|
+
6 => "Missing topic size",
|
14
|
+
7 => "Missing payload size",
|
15
|
+
8 => "Invalid token",
|
16
|
+
255 => "None (unknown error)"
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(app, conneciton, notification, batch)
|
20
|
+
@app = app
|
21
|
+
@connection = conneciton
|
22
|
+
@notification = notification
|
23
|
+
@batch = batch
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform
|
27
|
+
begin
|
28
|
+
@connection.write(@notification.to_binary)
|
29
|
+
check_for_error if Rapns.config.check_for_errors
|
30
|
+
mark_delivered
|
31
|
+
Rapns.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.device_token}")
|
32
|
+
rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError => error
|
33
|
+
mark_failed(error.code, error.description)
|
34
|
+
raise
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def check_for_error
|
41
|
+
if @connection.select(SELECT_TIMEOUT)
|
42
|
+
error = nil
|
43
|
+
|
44
|
+
if tuple = @connection.read(ERROR_TUPLE_BYTES)
|
45
|
+
cmd, code, notification_id = tuple.unpack("ccN")
|
46
|
+
|
47
|
+
description = APN_ERRORS[code.to_i] || "Unknown error. Possible rapns bug?"
|
48
|
+
error = Rapns::DeliveryError.new(code, notification_id, description)
|
49
|
+
else
|
50
|
+
error = Rapns::Apns::DisconnectionError.new
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
Rapns.logger.error("[#{@app.name}] Error received, reconnecting...")
|
55
|
+
@connection.reconnect
|
56
|
+
ensure
|
57
|
+
raise error if error
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
|
5
|
+
HOSTS = {
|
6
|
+
:production => ['gateway.push.apple.com', 2195],
|
7
|
+
:development => ['gateway.sandbox.push.apple.com', 2195], # deprecated
|
8
|
+
:sandbox => ['gateway.sandbox.push.apple.com', 2195]
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
@host, @port = HOSTS[@app.environment.to_sym]
|
14
|
+
end
|
15
|
+
|
16
|
+
def deliver(notification, batch)
|
17
|
+
Rapns::Daemon::Apns::Delivery.new(@app, connection, notification, batch).perform
|
18
|
+
end
|
19
|
+
|
20
|
+
def stopped
|
21
|
+
@connection.close if @connection
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def connection
|
27
|
+
return @connection if defined? @connection
|
28
|
+
connection = Connection.new(@app, @host, @port)
|
29
|
+
connection.connect
|
30
|
+
@connection = connection
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class DisconnectionError < StandardError
|
4
|
+
attr_reader :code, :description
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@code = nil
|
8
|
+
@description = "APNs disconnected without returning an error."
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
message
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
"The APNs disconnected without returning an error. This may indicate you are using an invalid certificate for the host."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class FeedbackReceiver
|
5
|
+
include Reflectable
|
6
|
+
|
7
|
+
FEEDBACK_TUPLE_BYTES = 38
|
8
|
+
HOSTS = {
|
9
|
+
:production => ['feedback.push.apple.com', 2196],
|
10
|
+
:development => ['feedback.sandbox.push.apple.com', 2196], # deprecated
|
11
|
+
:sandbox => ['feedback.sandbox.push.apple.com', 2196]
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(app, poll)
|
15
|
+
@app = app
|
16
|
+
@host, @port = HOSTS[@app.environment.to_sym]
|
17
|
+
@poll = poll
|
18
|
+
@certificate = app.certificate
|
19
|
+
@password = app.password
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
@thread = Thread.new do
|
24
|
+
loop do
|
25
|
+
break if @stop
|
26
|
+
check_for_feedback
|
27
|
+
interruptible_sleep.sleep @poll
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop
|
33
|
+
@stop = true
|
34
|
+
interruptible_sleep.interrupt_sleep
|
35
|
+
@thread.join if @thread
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_for_feedback
|
39
|
+
connection = nil
|
40
|
+
begin
|
41
|
+
connection = Connection.new(@app, @host, @port)
|
42
|
+
connection.connect
|
43
|
+
|
44
|
+
while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
|
45
|
+
timestamp, device_token = parse_tuple(tuple)
|
46
|
+
create_feedback(timestamp, device_token)
|
47
|
+
end
|
48
|
+
rescue StandardError => e
|
49
|
+
Rapns.logger.error(e)
|
50
|
+
ensure
|
51
|
+
connection.close if connection
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def interrupt_sleep
|
56
|
+
interruptible_sleep.interrupt_sleep
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def parse_tuple(tuple)
|
62
|
+
failed_at, _, device_token = tuple.unpack("N1n1H*")
|
63
|
+
[Time.at(failed_at).utc, device_token]
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_feedback(failed_at, device_token)
|
67
|
+
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
68
|
+
Rapns.logger.info("[#{@app.name}] [FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
|
69
|
+
|
70
|
+
feedback = Rapns::Daemon.store.create_apns_feedback(failed_at, device_token, @app)
|
71
|
+
reflect(:apns_feedback, feedback)
|
72
|
+
|
73
|
+
# Deprecated.
|
74
|
+
begin
|
75
|
+
Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
|
76
|
+
rescue StandardError => e
|
77
|
+
Rapns.logger.error(e)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def interruptible_sleep
|
82
|
+
@interruptible_sleep ||= InterruptibleSleep.new
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|