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.
Files changed (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +99 -0
  3. data/LICENSE +7 -0
  4. data/README.md +189 -0
  5. data/bin/rpush +36 -0
  6. data/config/database.yml +44 -0
  7. data/lib/generators/rpush_generator.rb +44 -0
  8. data/lib/generators/templates/add_adm.rb +23 -0
  9. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  10. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  11. data/lib/generators/templates/add_fail_after_to_rpush_notifications.rb +9 -0
  12. data/lib/generators/templates/add_gcm.rb +102 -0
  13. data/lib/generators/templates/add_rpush.rb +349 -0
  14. data/lib/generators/templates/add_wpns.rb +16 -0
  15. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  16. data/lib/generators/templates/create_rapns_feedback.rb +18 -0
  17. data/lib/generators/templates/create_rapns_notifications.rb +29 -0
  18. data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
  19. data/lib/generators/templates/rpush.rb +104 -0
  20. data/lib/rpush/TODO +3 -0
  21. data/lib/rpush/adm/app.rb +15 -0
  22. data/lib/rpush/adm/data_validator.rb +11 -0
  23. data/lib/rpush/adm/notification.rb +29 -0
  24. data/lib/rpush/apns/app.rb +29 -0
  25. data/lib/rpush/apns/binary_notification_validator.rb +12 -0
  26. data/lib/rpush/apns/device_token_format_validator.rb +12 -0
  27. data/lib/rpush/apns/feedback.rb +16 -0
  28. data/lib/rpush/apns/notification.rb +84 -0
  29. data/lib/rpush/apns_feedback.rb +13 -0
  30. data/lib/rpush/app.rb +18 -0
  31. data/lib/rpush/configuration.rb +75 -0
  32. data/lib/rpush/daemon/adm/delivery.rb +222 -0
  33. data/lib/rpush/daemon/adm.rb +9 -0
  34. data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
  35. data/lib/rpush/daemon/apns/delivery.rb +64 -0
  36. data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
  37. data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
  38. data/lib/rpush/daemon/apns.rb +16 -0
  39. data/lib/rpush/daemon/app_runner.rb +187 -0
  40. data/lib/rpush/daemon/batch.rb +115 -0
  41. data/lib/rpush/daemon/constants.rb +59 -0
  42. data/lib/rpush/daemon/delivery.rb +28 -0
  43. data/lib/rpush/daemon/delivery_error.rb +19 -0
  44. data/lib/rpush/daemon/dispatcher/http.rb +21 -0
  45. data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
  46. data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
  47. data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
  48. data/lib/rpush/daemon/feeder.rb +68 -0
  49. data/lib/rpush/daemon/gcm/delivery.rb +222 -0
  50. data/lib/rpush/daemon/gcm.rb +9 -0
  51. data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
  52. data/lib/rpush/daemon/loggable.rb +31 -0
  53. data/lib/rpush/daemon/reflectable.rb +13 -0
  54. data/lib/rpush/daemon/retry_header_parser.rb +23 -0
  55. data/lib/rpush/daemon/retryable_error.rb +20 -0
  56. data/lib/rpush/daemon/service_config_methods.rb +33 -0
  57. data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
  58. data/lib/rpush/daemon/store/active_record.rb +154 -0
  59. data/lib/rpush/daemon/tcp_connection.rb +143 -0
  60. data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
  61. data/lib/rpush/daemon/wpns/delivery.rb +132 -0
  62. data/lib/rpush/daemon/wpns.rb +9 -0
  63. data/lib/rpush/daemon.rb +140 -0
  64. data/lib/rpush/deprecatable.rb +23 -0
  65. data/lib/rpush/deprecation.rb +23 -0
  66. data/lib/rpush/embed.rb +28 -0
  67. data/lib/rpush/gcm/app.rb +11 -0
  68. data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  69. data/lib/rpush/gcm/notification.rb +30 -0
  70. data/lib/rpush/logger.rb +63 -0
  71. data/lib/rpush/multi_json_helper.rb +16 -0
  72. data/lib/rpush/notification.rb +69 -0
  73. data/lib/rpush/notifier.rb +52 -0
  74. data/lib/rpush/payload_data_size_validator.rb +10 -0
  75. data/lib/rpush/push.rb +16 -0
  76. data/lib/rpush/railtie.rb +11 -0
  77. data/lib/rpush/reflection.rb +58 -0
  78. data/lib/rpush/registration_ids_count_validator.rb +10 -0
  79. data/lib/rpush/version.rb +3 -0
  80. data/lib/rpush/wpns/app.rb +9 -0
  81. data/lib/rpush/wpns/notification.rb +26 -0
  82. data/lib/rpush.rb +62 -0
  83. data/lib/tasks/cane.rake +18 -0
  84. data/lib/tasks/rpush.rake +16 -0
  85. data/lib/tasks/test.rake +38 -0
  86. data/spec/functional/adm_spec.rb +43 -0
  87. data/spec/functional/apns_spec.rb +58 -0
  88. data/spec/functional/embed_spec.rb +49 -0
  89. data/spec/functional/gcm_spec.rb +42 -0
  90. data/spec/functional/wpns_spec.rb +41 -0
  91. data/spec/support/cert_with_password.pem +90 -0
  92. data/spec/support/cert_without_password.pem +59 -0
  93. data/spec/support/install.sh +32 -0
  94. data/spec/support/simplecov_helper.rb +20 -0
  95. data/spec/support/simplecov_quality_formatter.rb +8 -0
  96. data/spec/tmp/.gitkeep +0 -0
  97. data/spec/unit/adm/app_spec.rb +58 -0
  98. data/spec/unit/adm/notification_spec.rb +45 -0
  99. data/spec/unit/apns/app_spec.rb +29 -0
  100. data/spec/unit/apns/feedback_spec.rb +9 -0
  101. data/spec/unit/apns/notification_spec.rb +208 -0
  102. data/spec/unit/apns_feedback_spec.rb +21 -0
  103. data/spec/unit/app_spec.rb +30 -0
  104. data/spec/unit/configuration_spec.rb +45 -0
  105. data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
  106. data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
  107. data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
  108. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  109. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +117 -0
  110. data/spec/unit/daemon/app_runner_spec.rb +292 -0
  111. data/spec/unit/daemon/batch_spec.rb +232 -0
  112. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  113. data/spec/unit/daemon/delivery_spec.rb +38 -0
  114. data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
  115. data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
  116. data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
  117. data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
  118. data/spec/unit/daemon/feeder_spec.rb +98 -0
  119. data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
  120. data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
  121. data/spec/unit/daemon/reflectable_spec.rb +27 -0
  122. data/spec/unit/daemon/retryable_error_spec.rb +14 -0
  123. data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
  124. data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
  125. data/spec/unit/daemon/store/active_record_spec.rb +357 -0
  126. data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
  127. data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
  128. data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
  129. data/spec/unit/daemon_spec.rb +159 -0
  130. data/spec/unit/deprecatable_spec.rb +32 -0
  131. data/spec/unit/deprecation_spec.rb +15 -0
  132. data/spec/unit/embed_spec.rb +50 -0
  133. data/spec/unit/gcm/app_spec.rb +4 -0
  134. data/spec/unit/gcm/notification_spec.rb +36 -0
  135. data/spec/unit/logger_spec.rb +127 -0
  136. data/spec/unit/notification_shared.rb +105 -0
  137. data/spec/unit/notification_spec.rb +15 -0
  138. data/spec/unit/notifier_spec.rb +49 -0
  139. data/spec/unit/push_spec.rb +43 -0
  140. data/spec/unit/reflection_spec.rb +30 -0
  141. data/spec/unit/rpush_spec.rb +9 -0
  142. data/spec/unit/wpns/app_spec.rb +4 -0
  143. data/spec/unit/wpns/notification_spec.rb +30 -0
  144. data/spec/unit_spec_helper.rb +101 -0
  145. 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,12 @@
1
+ module Rpush
2
+ module Apns
3
+ class DeviceTokenFormatValidator < ActiveModel::Validator
4
+
5
+ def validate(record)
6
+ if record.device_token !~ /^[a-z0-9]{64}$/i
7
+ record.errors[:device_token] << "is invalid"
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,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Adm
4
+ extend ServiceConfigMethods
5
+
6
+ dispatcher :http
7
+ end
8
+ end
9
+ 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