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.
Files changed (113) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.md +83 -0
  3. data/LICENSE +7 -0
  4. data/README.md +168 -0
  5. data/bin/rapns +37 -0
  6. data/config/database.yml +44 -0
  7. data/lib/generators/rapns_generator.rb +25 -0
  8. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  9. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  10. data/lib/generators/templates/add_gcm.rb +95 -0
  11. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  12. data/lib/generators/templates/create_rapns_feedback.rb +15 -0
  13. data/lib/generators/templates/create_rapns_notifications.rb +26 -0
  14. data/lib/generators/templates/rapns.rb +87 -0
  15. data/lib/rapns/TODO +3 -0
  16. data/lib/rapns/apns/app.rb +25 -0
  17. data/lib/rapns/apns/binary_notification_validator.rb +12 -0
  18. data/lib/rapns/apns/device_token_format_validator.rb +12 -0
  19. data/lib/rapns/apns/feedback.rb +16 -0
  20. data/lib/rapns/apns/notification.rb +91 -0
  21. data/lib/rapns/apns_feedback.rb +13 -0
  22. data/lib/rapns/app.rb +16 -0
  23. data/lib/rapns/configuration.rb +89 -0
  24. data/lib/rapns/daemon/apns/app_runner.rb +26 -0
  25. data/lib/rapns/daemon/apns/certificate_expired_error.rb +20 -0
  26. data/lib/rapns/daemon/apns/connection.rb +142 -0
  27. data/lib/rapns/daemon/apns/delivery.rb +64 -0
  28. data/lib/rapns/daemon/apns/delivery_handler.rb +35 -0
  29. data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
  30. data/lib/rapns/daemon/apns/feedback_receiver.rb +89 -0
  31. data/lib/rapns/daemon/app_runner.rb +179 -0
  32. data/lib/rapns/daemon/batch.rb +112 -0
  33. data/lib/rapns/daemon/delivery.rb +23 -0
  34. data/lib/rapns/daemon/delivery_error.rb +19 -0
  35. data/lib/rapns/daemon/delivery_handler.rb +52 -0
  36. data/lib/rapns/daemon/delivery_handler_collection.rb +33 -0
  37. data/lib/rapns/daemon/feeder.rb +65 -0
  38. data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
  39. data/lib/rapns/daemon/gcm/delivery.rb +228 -0
  40. data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
  41. data/lib/rapns/daemon/interruptible_sleep.rb +65 -0
  42. data/lib/rapns/daemon/reflectable.rb +13 -0
  43. data/lib/rapns/daemon/store/active_record/reconnectable.rb +66 -0
  44. data/lib/rapns/daemon/store/active_record.rb +128 -0
  45. data/lib/rapns/daemon.rb +129 -0
  46. data/lib/rapns/deprecatable.rb +23 -0
  47. data/lib/rapns/deprecation.rb +23 -0
  48. data/lib/rapns/embed.rb +28 -0
  49. data/lib/rapns/gcm/app.rb +7 -0
  50. data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  51. data/lib/rapns/gcm/notification.rb +37 -0
  52. data/lib/rapns/gcm/payload_data_size_validator.rb +13 -0
  53. data/lib/rapns/gcm/registration_ids_count_validator.rb +13 -0
  54. data/lib/rapns/logger.rb +76 -0
  55. data/lib/rapns/multi_json_helper.rb +16 -0
  56. data/lib/rapns/notification.rb +62 -0
  57. data/lib/rapns/notifier.rb +35 -0
  58. data/lib/rapns/push.rb +17 -0
  59. data/lib/rapns/rails-2-compatibility.rb +34 -0
  60. data/lib/rapns/reflection.rb +44 -0
  61. data/lib/rapns/upgraded.rb +31 -0
  62. data/lib/rapns/version.rb +3 -0
  63. data/lib/rapns_rails_2.rb +67 -0
  64. data/lib/tasks/cane.rake +18 -0
  65. data/lib/tasks/test.rake +38 -0
  66. data/spec/support/cert_with_password.pem +90 -0
  67. data/spec/support/cert_without_password.pem +59 -0
  68. data/spec/support/simplecov_helper.rb +13 -0
  69. data/spec/support/simplecov_quality_formatter.rb +8 -0
  70. data/spec/tmp/.gitkeep +0 -0
  71. data/spec/unit/apns/app_spec.rb +29 -0
  72. data/spec/unit/apns/feedback_spec.rb +9 -0
  73. data/spec/unit/apns/notification_spec.rb +215 -0
  74. data/spec/unit/apns_feedback_spec.rb +21 -0
  75. data/spec/unit/app_spec.rb +16 -0
  76. data/spec/unit/configuration_spec.rb +55 -0
  77. data/spec/unit/daemon/apns/app_runner_spec.rb +45 -0
  78. data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
  79. data/spec/unit/daemon/apns/connection_spec.rb +287 -0
  80. data/spec/unit/daemon/apns/delivery_handler_spec.rb +59 -0
  81. data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
  82. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  83. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +134 -0
  84. data/spec/unit/daemon/app_runner_shared.rb +83 -0
  85. data/spec/unit/daemon/app_runner_spec.rb +170 -0
  86. data/spec/unit/daemon/batch_spec.rb +219 -0
  87. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  88. data/spec/unit/daemon/delivery_handler_collection_spec.rb +37 -0
  89. data/spec/unit/daemon/delivery_handler_shared.rb +45 -0
  90. data/spec/unit/daemon/feeder_spec.rb +81 -0
  91. data/spec/unit/daemon/gcm/app_runner_spec.rb +19 -0
  92. data/spec/unit/daemon/gcm/delivery_handler_spec.rb +44 -0
  93. data/spec/unit/daemon/gcm/delivery_spec.rb +289 -0
  94. data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
  95. data/spec/unit/daemon/reflectable_spec.rb +27 -0
  96. data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
  97. data/spec/unit/daemon/store/active_record_spec.rb +281 -0
  98. data/spec/unit/daemon_spec.rb +157 -0
  99. data/spec/unit/deprecatable_spec.rb +32 -0
  100. data/spec/unit/deprecation_spec.rb +15 -0
  101. data/spec/unit/embed_spec.rb +50 -0
  102. data/spec/unit/gcm/app_spec.rb +4 -0
  103. data/spec/unit/gcm/notification_spec.rb +52 -0
  104. data/spec/unit/logger_spec.rb +180 -0
  105. data/spec/unit/notification_shared.rb +45 -0
  106. data/spec/unit/notification_spec.rb +4 -0
  107. data/spec/unit/notifier_spec.rb +32 -0
  108. data/spec/unit/push_spec.rb +44 -0
  109. data/spec/unit/rapns_spec.rb +9 -0
  110. data/spec/unit/reflection_spec.rb +30 -0
  111. data/spec/unit/upgraded_spec.rb +40 -0
  112. data/spec/unit_spec_helper.rb +137 -0
  113. 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,12 @@
1
+ module Rapns
2
+ module Apns
3
+ class DeviceTokenFormatValidator < ActiveModel::Validator
4
+
5
+ def validate(record)
6
+ if record.device_token !~ /^[a-z0-9]{64}$/
7
+ record.errors.add(:device_token, "is invalid")
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