rapns_rails_2 3.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|