rpush 1.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
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