rpush 1.0.0-java
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 +7 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE +7 -0
- data/README.md +189 -0
- data/bin/rpush +36 -0
- data/config/database.yml +44 -0
- data/lib/generators/rpush_generator.rb +44 -0
- data/lib/generators/templates/add_adm.rb +23 -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_fail_after_to_rpush_notifications.rb +9 -0
- data/lib/generators/templates/add_gcm.rb +102 -0
- data/lib/generators/templates/add_rpush.rb +349 -0
- data/lib/generators/templates/add_wpns.rb +16 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +18 -0
- data/lib/generators/templates/create_rapns_notifications.rb +29 -0
- data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
- data/lib/generators/templates/rpush.rb +104 -0
- data/lib/rpush/TODO +3 -0
- data/lib/rpush/adm/app.rb +15 -0
- data/lib/rpush/adm/data_validator.rb +11 -0
- data/lib/rpush/adm/notification.rb +29 -0
- data/lib/rpush/apns/app.rb +29 -0
- data/lib/rpush/apns/binary_notification_validator.rb +12 -0
- data/lib/rpush/apns/device_token_format_validator.rb +12 -0
- data/lib/rpush/apns/feedback.rb +16 -0
- data/lib/rpush/apns/notification.rb +84 -0
- data/lib/rpush/apns_feedback.rb +13 -0
- data/lib/rpush/app.rb +18 -0
- data/lib/rpush/configuration.rb +75 -0
- data/lib/rpush/daemon/adm/delivery.rb +222 -0
- data/lib/rpush/daemon/adm.rb +9 -0
- data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rpush/daemon/apns/delivery.rb +64 -0
- data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
- data/lib/rpush/daemon/apns.rb +16 -0
- data/lib/rpush/daemon/app_runner.rb +187 -0
- data/lib/rpush/daemon/batch.rb +115 -0
- data/lib/rpush/daemon/constants.rb +59 -0
- data/lib/rpush/daemon/delivery.rb +28 -0
- data/lib/rpush/daemon/delivery_error.rb +19 -0
- data/lib/rpush/daemon/dispatcher/http.rb +21 -0
- data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
- data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
- data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
- data/lib/rpush/daemon/feeder.rb +68 -0
- data/lib/rpush/daemon/gcm/delivery.rb +222 -0
- data/lib/rpush/daemon/gcm.rb +9 -0
- data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
- data/lib/rpush/daemon/loggable.rb +31 -0
- data/lib/rpush/daemon/reflectable.rb +13 -0
- data/lib/rpush/daemon/retry_header_parser.rb +23 -0
- data/lib/rpush/daemon/retryable_error.rb +20 -0
- data/lib/rpush/daemon/service_config_methods.rb +33 -0
- data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
- data/lib/rpush/daemon/store/active_record.rb +154 -0
- data/lib/rpush/daemon/tcp_connection.rb +143 -0
- data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
- data/lib/rpush/daemon/wpns/delivery.rb +132 -0
- data/lib/rpush/daemon/wpns.rb +9 -0
- data/lib/rpush/daemon.rb +140 -0
- data/lib/rpush/deprecatable.rb +23 -0
- data/lib/rpush/deprecation.rb +23 -0
- data/lib/rpush/embed.rb +28 -0
- data/lib/rpush/gcm/app.rb +11 -0
- data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rpush/gcm/notification.rb +30 -0
- data/lib/rpush/logger.rb +63 -0
- data/lib/rpush/multi_json_helper.rb +16 -0
- data/lib/rpush/notification.rb +69 -0
- data/lib/rpush/notifier.rb +52 -0
- data/lib/rpush/payload_data_size_validator.rb +10 -0
- data/lib/rpush/push.rb +16 -0
- data/lib/rpush/railtie.rb +11 -0
- data/lib/rpush/reflection.rb +58 -0
- data/lib/rpush/registration_ids_count_validator.rb +10 -0
- data/lib/rpush/version.rb +3 -0
- data/lib/rpush/wpns/app.rb +9 -0
- data/lib/rpush/wpns/notification.rb +26 -0
- data/lib/rpush.rb +62 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/rpush.rake +16 -0
- data/lib/tasks/test.rake +38 -0
- data/spec/functional/adm_spec.rb +43 -0
- data/spec/functional/apns_spec.rb +58 -0
- data/spec/functional/embed_spec.rb +49 -0
- data/spec/functional/gcm_spec.rb +42 -0
- data/spec/functional/wpns_spec.rb +41 -0
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/support/install.sh +32 -0
- data/spec/support/simplecov_helper.rb +20 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/tmp/.gitkeep +0 -0
- data/spec/unit/adm/app_spec.rb +58 -0
- data/spec/unit/adm/notification_spec.rb +45 -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 +208 -0
- data/spec/unit/apns_feedback_spec.rb +21 -0
- data/spec/unit/app_spec.rb +30 -0
- data/spec/unit/configuration_spec.rb +45 -0
- data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -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 +117 -0
- data/spec/unit/daemon/app_runner_spec.rb +292 -0
- data/spec/unit/daemon/batch_spec.rb +232 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_spec.rb +38 -0
- data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
- data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
- data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
- data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
- data/spec/unit/daemon/feeder_spec.rb +98 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon/retryable_error_spec.rb +14 -0
- data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
- data/spec/unit/daemon/store/active_record_spec.rb +357 -0
- data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
- data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
- data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
- data/spec/unit/daemon_spec.rb +159 -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 +36 -0
- data/spec/unit/logger_spec.rb +127 -0
- data/spec/unit/notification_shared.rb +105 -0
- data/spec/unit/notification_spec.rb +15 -0
- data/spec/unit/notifier_spec.rb +49 -0
- data/spec/unit/push_spec.rb +43 -0
- data/spec/unit/reflection_spec.rb +30 -0
- data/spec/unit/rpush_spec.rb +9 -0
- data/spec/unit/wpns/app_spec.rb +4 -0
- data/spec/unit/wpns/notification_spec.rb +30 -0
- data/spec/unit_spec_helper.rb +101 -0
- metadata +304 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rpush
|
2
|
+
module Apns
|
3
|
+
class App < Rpush::App
|
4
|
+
validates :environment, :presence => true, :inclusion => { :in => %w(development production sandbox) }
|
5
|
+
validates :certificate, :presence => true
|
6
|
+
validate :certificate_has_matching_private_key
|
7
|
+
|
8
|
+
def service_name
|
9
|
+
'apns'
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def certificate_has_matching_private_key
|
15
|
+
result = false
|
16
|
+
if certificate.present?
|
17
|
+
begin
|
18
|
+
x509 = OpenSSL::X509::Certificate.new(certificate)
|
19
|
+
pkey = OpenSSL::PKey::RSA.new(certificate, password)
|
20
|
+
result = !x509.nil? && !pkey.nil?
|
21
|
+
rescue OpenSSL::OpenSSLError
|
22
|
+
errors.add :certificate, 'Certificate value must contain a certificate and a private key.'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Rpush
|
2
|
+
module Apns
|
3
|
+
class BinaryNotificationValidator < ActiveModel::Validator
|
4
|
+
|
5
|
+
def validate(record)
|
6
|
+
if record.payload_size > 256
|
7
|
+
record.errors[: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 Rpush
|
2
|
+
module Apns
|
3
|
+
class Feedback < ActiveRecord::Base
|
4
|
+
self.table_name = 'rpush_feedback'
|
5
|
+
|
6
|
+
if Rpush.attr_accessible_available?
|
7
|
+
attr_accessible :device_token, :failed_at, :app
|
8
|
+
end
|
9
|
+
|
10
|
+
validates :device_token, :presence => true
|
11
|
+
validates :failed_at, :presence => true
|
12
|
+
|
13
|
+
validates_with Rpush::Apns::DeviceTokenFormatValidator
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rpush
|
2
|
+
module Apns
|
3
|
+
class Notification < Rpush::Notification
|
4
|
+
class MultipleAppAssignmentError < StandardError; end
|
5
|
+
|
6
|
+
validates :device_token, :presence => true
|
7
|
+
validates :badge, :numericality => true, :allow_nil => true
|
8
|
+
|
9
|
+
validates_with Rpush::Apns::DeviceTokenFormatValidator
|
10
|
+
validates_with Rpush::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 = '__rpush_mdm__'
|
44
|
+
def mdm=(magic)
|
45
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ MDM_KEY => magic })
|
46
|
+
end
|
47
|
+
|
48
|
+
CONTENT_AVAILABLE_KEY = '__rpush_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
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Rpush
|
2
|
+
def self.apns_feedback
|
3
|
+
Rpush.require_for_daemon
|
4
|
+
Rpush::Daemon.initialize_store
|
5
|
+
|
6
|
+
Rpush::Apns::App.all.each do |app|
|
7
|
+
receiver = Rpush::Daemon::Apns::FeedbackReceiver.new(app)
|
8
|
+
receiver.check_for_feedback
|
9
|
+
end
|
10
|
+
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
data/lib/rpush/app.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rpush
|
2
|
+
class App < ActiveRecord::Base
|
3
|
+
self.table_name = 'rpush_apps'
|
4
|
+
|
5
|
+
if Rpush.attr_accessible_available?
|
6
|
+
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key, :client_id, :client_secret
|
7
|
+
end
|
8
|
+
|
9
|
+
has_many :notifications, :class_name => 'Rpush::Notification', :dependent => :destroy
|
10
|
+
|
11
|
+
validates :name, :presence => true, :uniqueness => { :scope => [:type, :environment] }
|
12
|
+
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
13
|
+
|
14
|
+
def service_name
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Rpush
|
2
|
+
def self.config
|
3
|
+
@config ||= Rpush::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
|
+
:check_for_errors, :pid_file, :batch_size, :push, :store, :logger,
|
12
|
+
:batch_storage_updates, :wakeup]
|
13
|
+
|
14
|
+
class ConfigurationWithoutDefaults < Struct.new(*CONFIG_ATTRS)
|
15
|
+
end
|
16
|
+
|
17
|
+
class Configuration < Struct.new(*CONFIG_ATTRS)
|
18
|
+
include Deprecatable
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
super
|
22
|
+
set_defaults
|
23
|
+
end
|
24
|
+
|
25
|
+
def update(other)
|
26
|
+
CONFIG_ATTRS.each do |attr|
|
27
|
+
other_value = other.send(attr)
|
28
|
+
send("#{attr}=", other_value) unless other_value.nil?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def pid_file=(path)
|
33
|
+
if path && !Pathname.new(path).absolute?
|
34
|
+
super(File.join(Rails.root, path))
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def logger=(logger)
|
41
|
+
super(logger)
|
42
|
+
end
|
43
|
+
|
44
|
+
def foreground=(bool)
|
45
|
+
if Rpush.jruby?
|
46
|
+
# The JVM does not support fork().
|
47
|
+
super(true)
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_defaults
|
54
|
+
if Rpush.jruby?
|
55
|
+
# The JVM does not support fork().
|
56
|
+
self.foreground = true
|
57
|
+
else
|
58
|
+
self.foreground = false
|
59
|
+
end
|
60
|
+
|
61
|
+
self.push_poll = 2
|
62
|
+
self.feedback_poll = 60
|
63
|
+
self.check_for_errors = true
|
64
|
+
self.batch_size = 100
|
65
|
+
self.pid_file = nil
|
66
|
+
self.store = :active_record
|
67
|
+
self.logger = nil
|
68
|
+
self.batch_storage_updates = true
|
69
|
+
|
70
|
+
# Internal options.
|
71
|
+
self.embedded = false
|
72
|
+
self.push = false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module Rpush
|
2
|
+
module Daemon
|
3
|
+
module Adm
|
4
|
+
# https://developer.amazon.com/sdk/adm/sending-message.html
|
5
|
+
class Delivery < Rpush::Daemon::Delivery
|
6
|
+
include MultiJsonHelper
|
7
|
+
|
8
|
+
# Oauth2.0 token endpoint. This endpoint is used to request authorization tokens.
|
9
|
+
AMAZON_TOKEN_URI = URI.parse('https://api.amazon.com/auth/O2/token')
|
10
|
+
|
11
|
+
# ADM services endpoint. This endpoint is used to perform ADM requests.
|
12
|
+
AMAZON_ADM_URL = 'https://api.amazon.com/messaging/registrations/%s/messages'
|
13
|
+
|
14
|
+
# Data used to request authorization tokens.
|
15
|
+
ACCESS_TOKEN_REQUEST_DATA = {"grant_type" => "client_credentials", "scope" => "messaging:push"}
|
16
|
+
|
17
|
+
def initialize(app, http, notification, batch)
|
18
|
+
@app = app
|
19
|
+
@http = http
|
20
|
+
@notification = notification
|
21
|
+
@batch = batch
|
22
|
+
@sent_registration_ids = []
|
23
|
+
@failed_registration_ids = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform
|
27
|
+
begin
|
28
|
+
@notification.registration_ids.each do |registration_id|
|
29
|
+
handle_response(do_post(registration_id), registration_id)
|
30
|
+
end
|
31
|
+
|
32
|
+
if(@sent_registration_ids.empty?)
|
33
|
+
raise Rpush::DeliveryError.new(nil, @notification.id, describe_errors)
|
34
|
+
else
|
35
|
+
unless(@failed_registration_ids.empty?)
|
36
|
+
@notification.error_description = describe_errors
|
37
|
+
Rpush::Daemon.store.update_notification(@notification)
|
38
|
+
end
|
39
|
+
|
40
|
+
mark_delivered
|
41
|
+
end
|
42
|
+
rescue Rpush::RetryableError => error
|
43
|
+
handle_retryable(error)
|
44
|
+
rescue Rpush::TooManyRequestsError => error
|
45
|
+
handle_too_many_requests(error)
|
46
|
+
rescue Rpush::DeliveryError => error
|
47
|
+
mark_failed(error.code, error.description)
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def handle_response(response, current_registration_id)
|
55
|
+
case response.code.to_i
|
56
|
+
when 200
|
57
|
+
ok(response, current_registration_id)
|
58
|
+
when 400
|
59
|
+
bad_request(response, current_registration_id)
|
60
|
+
when 401
|
61
|
+
unauthorized(response)
|
62
|
+
when 429
|
63
|
+
too_many_requests(response)
|
64
|
+
when 500
|
65
|
+
internal_server_error(response, current_registration_id)
|
66
|
+
when 503
|
67
|
+
service_unavailable(response)
|
68
|
+
else
|
69
|
+
raise Rpush::DeliveryError.new(response.code, @notification.id, Rpush::Daemon::HTTP_STATUS_CODES[response.code.to_i])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def ok(response, current_registration_id)
|
74
|
+
response_body = multi_json_load(response.body)
|
75
|
+
|
76
|
+
if(response_body.has_key?('registrationID'))
|
77
|
+
@sent_registration_ids << response_body['registrationID']
|
78
|
+
log_info("#{@notification.id} sent to #{response_body['registrationID']}")
|
79
|
+
end
|
80
|
+
|
81
|
+
if(current_registration_id != response_body['registrationID'])
|
82
|
+
reflect(:adm_canonical_id, current_registration_id, response_body['registrationID'])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_retryable(error)
|
87
|
+
case error.code
|
88
|
+
when 401
|
89
|
+
# clear app access_token so a new one is fetched
|
90
|
+
@notification.app.access_token = nil
|
91
|
+
get_access_token
|
92
|
+
mark_retryable(@notification, Time.now) if @notification.app.access_token
|
93
|
+
when 503
|
94
|
+
retry_delivery(@notification, error.response)
|
95
|
+
log_warn("ADM responded with an Service Unavailable Error. " + retry_message)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def handle_too_many_requests(error)
|
100
|
+
if(@sent_registration_ids.empty?)
|
101
|
+
# none sent yet, just resend after the specified retry-after response.header
|
102
|
+
retry_delivery(@notification, error.response)
|
103
|
+
else
|
104
|
+
# save unsent registration ids
|
105
|
+
unsent_registration_ids = @notification.registration_ids.select { |reg_id| !@sent_registration_ids.include?(reg_id) }
|
106
|
+
|
107
|
+
# update the current notification so it only contains the sent reg ids
|
108
|
+
@notification.registration_ids.reject! { |reg_id| !@sent_registration_ids.include?(reg_id) }
|
109
|
+
|
110
|
+
Rpush::Daemon.store.update_notification(@notification)
|
111
|
+
|
112
|
+
# create a new notification with the remaining unsent reg ids
|
113
|
+
create_new_notification(error.response, unsent_registration_ids)
|
114
|
+
|
115
|
+
# mark the current notification as sent
|
116
|
+
mark_delivered
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def bad_request(response, current_registration_id)
|
121
|
+
response_body = multi_json_load(response.body)
|
122
|
+
|
123
|
+
if(response_body.has_key?('reason'))
|
124
|
+
log_warn("bad_request: #{current_registration_id} (#{response_body['reason']})")
|
125
|
+
@failed_registration_ids[current_registration_id] = response_body['reason']
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def unauthorized(response)
|
130
|
+
# Indicate a notification is retryable. Because ADM requires separate request for each push token, this will safely mark the entire notification to retry delivery.
|
131
|
+
raise Rpush::RetryableError.new(response.code.to_i, @notification.id, 'ADM responded with an Unauthorized Error.', response)
|
132
|
+
end
|
133
|
+
|
134
|
+
def too_many_requests(response)
|
135
|
+
# raise error so the current notification stops sending messages to remaining reg ids
|
136
|
+
raise Rpush::TooManyRequestsError.new(response.code.to_i, @notification.id, 'Exceeded maximum allowable rate of messages.', response)
|
137
|
+
end
|
138
|
+
|
139
|
+
def internal_server_error(response, current_registration_id)
|
140
|
+
@failed_registration_ids[current_registration_id] = "Internal Server Error"
|
141
|
+
log_warn("internal_server_error: #{current_registration_id} (Internal Server Error)")
|
142
|
+
end
|
143
|
+
|
144
|
+
def service_unavailable(response)
|
145
|
+
# Indicate a notification is retryable. Because ADM requires separate request for each push token, this will safely mark the entire notification to retry delivery.
|
146
|
+
raise Rpush::RetryableError.new(response.code.to_i, @notification.id, 'ADM responded with an Service Unavailable Error.', response)
|
147
|
+
end
|
148
|
+
|
149
|
+
def create_new_notification(response, registration_ids)
|
150
|
+
attrs = @notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle')
|
151
|
+
Rpush::Daemon.store.create_adm_notification(attrs, @notification.data, registration_ids, deliver_after_header(response), @notification.app)
|
152
|
+
end
|
153
|
+
|
154
|
+
def deliver_after_header(response)
|
155
|
+
Rpush::Daemon::RetryHeaderParser.parse(response.header['retry-after'])
|
156
|
+
end
|
157
|
+
|
158
|
+
def retry_delivery(notification, response)
|
159
|
+
if time = deliver_after_header(response)
|
160
|
+
mark_retryable(notification, time)
|
161
|
+
else
|
162
|
+
mark_retryable_exponential(notification)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def describe_errors
|
167
|
+
if @failed_registration_ids.size == @notification.registration_ids.size
|
168
|
+
"Failed to deliver to all recipients."
|
169
|
+
else
|
170
|
+
error_msgs = []
|
171
|
+
@failed_registration_ids.each_pair { |regId, reason| error_msgs.push("#{regId}: #{reason}") }
|
172
|
+
"Failed to deliver to recipients: \n#{error_msgs.join("\n")}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def retry_message
|
177
|
+
"Notification #{@notification.id} will be retired after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
|
178
|
+
end
|
179
|
+
|
180
|
+
def do_post(registration_id)
|
181
|
+
adm_uri = URI.parse(AMAZON_ADM_URL % [registration_id])
|
182
|
+
post = Net::HTTP::Post.new(adm_uri.path, initheader = {
|
183
|
+
'Content-Type' => 'application/json',
|
184
|
+
'Accept' => 'application/json',
|
185
|
+
'x-amzn-type-version' => 'com.amazon.device.messaging.ADMMessage@1.0',
|
186
|
+
'x-amzn-accept-type' => 'com.amazon.device.messaging.ADMSendResult@1.0',
|
187
|
+
'Authorization' => "Bearer #{get_access_token}"
|
188
|
+
})
|
189
|
+
post.body = @notification.as_json.to_json
|
190
|
+
|
191
|
+
@http.request(adm_uri, post)
|
192
|
+
end
|
193
|
+
|
194
|
+
def get_access_token
|
195
|
+
if(@notification.app.access_token.nil? || @notification.app.access_token_expired?)
|
196
|
+
post = Net::HTTP::Post.new(AMAZON_TOKEN_URI.path, initheader = {'Content-Type' => 'application/x-www-form-urlencoded'})
|
197
|
+
post.set_form_data(ACCESS_TOKEN_REQUEST_DATA.merge({'client_id' => @notification.app.client_id, 'client_secret' => @notification.app.client_secret}))
|
198
|
+
|
199
|
+
handle_access_token(@http.request(AMAZON_TOKEN_URI, post))
|
200
|
+
end
|
201
|
+
|
202
|
+
@notification.app.access_token
|
203
|
+
end
|
204
|
+
|
205
|
+
def handle_access_token(response)
|
206
|
+
if(response.code.to_i == 200)
|
207
|
+
update_access_token(JSON.parse(response.body))
|
208
|
+
Rpush::Daemon.store.update_app(@notification.app)
|
209
|
+
log_info("ADM access token updated: token = #{@notification.app.access_token}, expires = #{@notification.app.access_token_expiration}")
|
210
|
+
else
|
211
|
+
log_warn("Could not retrieve access token from ADM: #{response.body}")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def update_access_token(data)
|
216
|
+
@notification.app.access_token = data['access_token']
|
217
|
+
@notification.app.access_token_expiration = Time.now + data['expires_in'].to_i
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rpush
|
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,64 @@
|
|
1
|
+
module Rpush
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class Delivery < Rpush::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 Rpush.config.check_for_errors
|
30
|
+
mark_delivered
|
31
|
+
log_info("#{@notification.id} sent to #{@notification.device_token}")
|
32
|
+
rescue Rpush::DeliveryError, Rpush::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
|
+
_, code, notification_id = tuple.unpack("ccN")
|
46
|
+
|
47
|
+
description = APN_ERRORS[code.to_i] || "Unknown error. Possible Rpush bug?"
|
48
|
+
error = Rpush::DeliveryError.new(code, notification_id, description)
|
49
|
+
else
|
50
|
+
error = Rpush::Apns::DisconnectionError.new
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
log_error("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,20 @@
|
|
1
|
+
module Rpush
|
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,79 @@
|
|
1
|
+
module Rpush
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class FeedbackReceiver
|
5
|
+
include Reflectable
|
6
|
+
include Loggable
|
7
|
+
|
8
|
+
TUPLE_BYTES = 38
|
9
|
+
HOSTS = {
|
10
|
+
:production => ['feedback.push.apple.com', 2196],
|
11
|
+
:development => ['feedback.sandbox.push.apple.com', 2196], # deprecated
|
12
|
+
:sandbox => ['feedback.sandbox.push.apple.com', 2196]
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(app)
|
16
|
+
@app = app
|
17
|
+
@host, @port = HOSTS[@app.environment.to_sym]
|
18
|
+
@poll = Rpush.config.feedback_poll
|
19
|
+
@certificate = app.certificate
|
20
|
+
@password = app.password
|
21
|
+
@interruptible_sleep = InterruptibleSleep.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
return if Rpush.config.push
|
26
|
+
|
27
|
+
@thread = Thread.new do
|
28
|
+
loop do
|
29
|
+
break if @stop
|
30
|
+
check_for_feedback
|
31
|
+
@interruptible_sleep.sleep @poll
|
32
|
+
end
|
33
|
+
|
34
|
+
Rpush::Daemon.store.release_connection
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop
|
39
|
+
@stop = true
|
40
|
+
@interruptible_sleep.interrupt_sleep
|
41
|
+
@thread.join if @thread
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_for_feedback
|
45
|
+
connection = nil
|
46
|
+
begin
|
47
|
+
connection = Rpush::Daemon::TcpConnection.new(@app, @host, @port)
|
48
|
+
connection.connect
|
49
|
+
|
50
|
+
while tuple = connection.read(TUPLE_BYTES)
|
51
|
+
timestamp, device_token = parse_tuple(tuple)
|
52
|
+
create_feedback(timestamp, device_token)
|
53
|
+
end
|
54
|
+
rescue StandardError => e
|
55
|
+
log_error(e)
|
56
|
+
reflect(:error, e)
|
57
|
+
ensure
|
58
|
+
connection.close if connection
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def parse_tuple(tuple)
|
65
|
+
failed_at, _, device_token = tuple.unpack("N1n1H*")
|
66
|
+
[Time.at(failed_at).utc, device_token]
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_feedback(failed_at, device_token)
|
70
|
+
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
71
|
+
log_info("[FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
|
72
|
+
|
73
|
+
feedback = Rpush::Daemon.store.create_apns_feedback(failed_at, device_token, @app)
|
74
|
+
reflect(:apns_feedback, feedback)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|