rapns_rails_2 3.4.3

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