rapns 2.0.5 → 3.0.0.beta.1
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.
- data/lib/generators/rapns_generator.rb +1 -0
- data/lib/generators/templates/add_gcm.rb +86 -0
- data/lib/generators/templates/create_rapns_notifications.rb +1 -1
- data/lib/rapns/apns/app.rb +8 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +14 -0
- data/lib/rapns/apns/notification.rb +84 -0
- data/lib/rapns/app.rb +5 -6
- data/lib/rapns/{config.rb → configuration.rb} +5 -5
- data/lib/rapns/daemon/apns/app_runner.rb +36 -0
- data/lib/rapns/daemon/apns/connection.rb +113 -0
- data/lib/rapns/daemon/apns/delivery.rb +63 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
- data/lib/rapns/daemon/app_runner.rb +76 -77
- data/lib/rapns/daemon/database_reconnectable.rb +3 -3
- data/lib/rapns/daemon/delivery.rb +43 -0
- data/lib/rapns/daemon/delivery_error.rb +6 -2
- data/lib/rapns/daemon/delivery_handler.rb +13 -79
- data/lib/rapns/daemon/delivery_queue_18.rb +2 -2
- data/lib/rapns/daemon/delivery_queue_19.rb +3 -3
- data/lib/rapns/daemon/feeder.rb +5 -5
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +206 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon.rb +31 -20
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +31 -0
- data/lib/rapns/gcm/payload_size_validator.rb +13 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +28 -95
- data/lib/rapns/version.rb +1 -1
- data/lib/rapns.rb +14 -4
- data/lib/tasks/cane.rake +19 -0
- data/lib/tasks/test.rake +34 -0
- data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
- data/spec/acceptance_spec_helper.rb +85 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/unit/apns/app_spec.rb +15 -0
- data/spec/unit/apns/feedback_spec.rb +12 -0
- data/spec/{rapns → unit/apns}/notification_spec.rb +44 -72
- data/spec/unit/app_spec.rb +18 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +37 -0
- data/spec/{rapns/daemon → unit/daemon/apns}/connection_spec.rb +9 -9
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +154 -0
- data/spec/{rapns/daemon → unit/daemon/apns}/feedback_receiver_spec.rb +14 -14
- data/spec/unit/daemon/app_runner_shared.rb +66 -0
- data/spec/unit/daemon/app_runner_spec.rb +78 -0
- data/spec/{rapns → unit}/daemon/database_reconnectable_spec.rb +4 -5
- data/spec/{rapns → unit}/daemon/delivery_error_spec.rb +2 -2
- data/spec/unit/daemon/delivery_handler_shared.rb +19 -0
- data/spec/{rapns → unit}/daemon/delivery_queue_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon/feeder_spec.rb +33 -33
- data/spec/unit/daemon/gcm/app_runner_spec.rb +15 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
- data/spec/{rapns → unit}/daemon/interruptible_sleep_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon/logger_spec.rb +1 -1
- data/spec/{rapns → unit}/daemon_spec.rb +1 -1
- data/spec/unit/gcm/app_spec.rb +5 -0
- data/spec/unit/gcm/notification_spec.rb +55 -0
- data/spec/unit/notification_shared.rb +38 -0
- data/spec/unit/notification_spec.rb +6 -0
- data/spec/{rapns/app_spec.rb → unit_spec_helper.rb} +76 -16
- metadata +107 -45
- data/lib/rapns/binary_notification_validator.rb +0 -10
- data/lib/rapns/daemon/connection.rb +0 -114
- data/lib/rapns/daemon/delivery_handler_pool.rb +0 -18
- data/lib/rapns/daemon/disconnection_error.rb +0 -14
- data/lib/rapns/daemon/feedback_receiver.rb +0 -82
- data/lib/rapns/device_token_format_validator.rb +0 -10
- data/lib/rapns/feedback.rb +0 -12
- data/spec/rapns/daemon/app_runner_spec.rb +0 -193
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +0 -17
- data/spec/rapns/daemon/delivery_handler_spec.rb +0 -206
- data/spec/rapns/feedback_spec.rb +0 -12
- data/spec/spec_helper.rb +0 -78
@@ -0,0 +1,86 @@
|
|
1
|
+
class AddGcm < ActiveRecord::Migration
|
2
|
+
module Rapns
|
3
|
+
class App < ActiveRecord::Base
|
4
|
+
self.table_name = 'rapns_apps'
|
5
|
+
end
|
6
|
+
|
7
|
+
class Notification < ActiveRecord::Base
|
8
|
+
belongs_to :app
|
9
|
+
self.table_name = 'rapns_notifications'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.up
|
14
|
+
add_column :rapns_notifications, :type, :string, :null => true
|
15
|
+
add_column :rapns_apps, :type, :string, :null => true
|
16
|
+
|
17
|
+
AddGcm::Rapns::Notification.update_all :type => 'Rapns::Apns::Notification'
|
18
|
+
AddGcm::Rapns::App.update_all :type => 'Rapns::Apns::App'
|
19
|
+
|
20
|
+
change_column_null :rapns_notifications, :type, false
|
21
|
+
change_column_null :rapns_apps, :type, false
|
22
|
+
change_column_null :rapns_notifications, :device_token, true
|
23
|
+
change_column_null :rapns_notifications, :expiry, true
|
24
|
+
change_column_null :rapns_apps, :environment, true
|
25
|
+
change_column_null :rapns_apps, :certificate, true
|
26
|
+
|
27
|
+
change_column :rapns_notifications, :error_description, :text
|
28
|
+
|
29
|
+
rename_column :rapns_notifications, :attributes_for_device, :data
|
30
|
+
rename_column :rapns_apps, :key, :name
|
31
|
+
|
32
|
+
add_column :rapns_apps, :auth_key, :string, :null => true
|
33
|
+
|
34
|
+
add_column :rapns_notifications, :collapse_key, :string, :null => true
|
35
|
+
add_column :rapns_notifications, :delay_while_idle, :boolean, :null => false, :default => false
|
36
|
+
add_column :rapns_notifications, :registration_ids, :text, :null => true
|
37
|
+
add_column :rapns_notifications, :app_id, :integer, :null => true
|
38
|
+
add_column :rapns_notifications, :retries, :integer, :null => true, :default => 0
|
39
|
+
|
40
|
+
Rapns::Notification.reset_column_information
|
41
|
+
Rapns::App.reset_column_information
|
42
|
+
|
43
|
+
Rapns::App.all.each do |app|
|
44
|
+
Rapns::Notification.update_all(['app_id = ?', app.id], ['app = ?', app.name])
|
45
|
+
end
|
46
|
+
|
47
|
+
change_column_null :rapns_notifications, :app_id, false
|
48
|
+
remove_column :rapns_notifications, :app
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.down
|
52
|
+
AddGcm::Rapns::Notification.where(:type => 'Rapns::Gcm::Notification').delete_all
|
53
|
+
|
54
|
+
remove_column :rapns_notifications, :type
|
55
|
+
remove_column :rapns_apps, :type
|
56
|
+
|
57
|
+
change_column_null :rapns_notifications, :device_token, false
|
58
|
+
change_column_null :rapns_notifications, :expiry, false
|
59
|
+
change_column_null :rapns_apps, :environment, false
|
60
|
+
change_column_null :rapns_apps, :certificate, false
|
61
|
+
|
62
|
+
change_column :rapns_notifications, :error_description, :string
|
63
|
+
|
64
|
+
rename_column :rapns_notifications, :data, :attributes_for_device
|
65
|
+
rename_column :rapns_apps, :name, :key
|
66
|
+
|
67
|
+
remove_column :rapns_apps, :auth_key
|
68
|
+
|
69
|
+
remove_column :rapns_notifications, :collapse_key
|
70
|
+
remove_column :rapns_notifications, :delay_while_idle
|
71
|
+
remove_column :rapns_notifications, :registration_ids
|
72
|
+
remove_column :rapns_notifications, :retries
|
73
|
+
|
74
|
+
add_column :rapns_notifications, :app, :string, :null => true
|
75
|
+
|
76
|
+
Rapns::Notification.reset_column_information
|
77
|
+
Rapns::App.reset_column_information
|
78
|
+
|
79
|
+
Rapns::App.all.each do |app|
|
80
|
+
Rapns::Notification.update_all(['app = ?', app.key], ['app_id = ?', app.id])
|
81
|
+
end
|
82
|
+
|
83
|
+
change_column_null :rapns_notifications, :key, false
|
84
|
+
remove_column :rapns_notifications, :app_id
|
85
|
+
end
|
86
|
+
end
|
@@ -17,7 +17,7 @@ class CreateRapnsNotifications < ActiveRecord::Migration
|
|
17
17
|
t.timestamps
|
18
18
|
end
|
19
19
|
|
20
|
-
add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "
|
20
|
+
add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "index_rapns_notifications_multi"
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.down
|
@@ -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[: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,14 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class Feedback < ActiveRecord::Base
|
4
|
+
self.table_name = 'rapns_feedback'
|
5
|
+
|
6
|
+
attr_accessible :device_token, :failed_at, :app
|
7
|
+
|
8
|
+
validates :device_token, :presence => true
|
9
|
+
validates :failed_at, :presence => true
|
10
|
+
|
11
|
+
validates_with Rapns::Apns::DeviceTokenFormatValidator
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Apns
|
3
|
+
class Notification < Rapns::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 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 = { 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 = { 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.to_s }
|
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
|
data/lib/rapns/app.rb
CHANGED
@@ -2,11 +2,11 @@ module Rapns
|
|
2
2
|
class App < ActiveRecord::Base
|
3
3
|
self.table_name = 'rapns_apps'
|
4
4
|
|
5
|
-
attr_accessible :
|
5
|
+
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
validates :
|
7
|
+
has_many :notifications
|
8
|
+
|
9
|
+
validates :name, :presence => true, :uniqueness => { :scope => [:type, :environment] }
|
10
10
|
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
11
11
|
|
12
12
|
validate :certificate_has_matching_private_key
|
@@ -20,11 +20,10 @@ module Rapns
|
|
20
20
|
pkey = OpenSSL::PKey::RSA.new certificate rescue nil
|
21
21
|
result = !x509.nil? && !pkey.nil?
|
22
22
|
unless result
|
23
|
-
errors.add :certificate,
|
23
|
+
errors.add :certificate, 'Certificate value must contain a certificate and a private key.'
|
24
24
|
end
|
25
25
|
end
|
26
26
|
result
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module Rapns
|
2
2
|
|
3
|
-
# A globally accessible instance of Rapns::
|
3
|
+
# A globally accessible instance of Rapns::Configuration
|
4
4
|
def self.configuration
|
5
|
-
@configuration ||= Rapns::
|
5
|
+
@configuration ||= Rapns::Configuration.new
|
6
6
|
end
|
7
7
|
|
8
|
-
# Call the given block yielding to it the global Rapns::
|
8
|
+
# Call the given block yielding to it the global Rapns::Configuration instance for setting
|
9
9
|
# configuration values / callbacks.
|
10
10
|
#
|
11
11
|
# Typically this would be used in your Rails application's config/initializers/rapns.rb file
|
@@ -14,7 +14,7 @@ module Rapns
|
|
14
14
|
end
|
15
15
|
|
16
16
|
# A class to hold Rapns configuration settings and callbacks.
|
17
|
-
class
|
17
|
+
class Configuration < Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size)
|
18
18
|
|
19
19
|
attr_accessor :feedback_callback
|
20
20
|
|
@@ -52,4 +52,4 @@ module Rapns
|
|
52
52
|
self.feedback_callback = block
|
53
53
|
end
|
54
54
|
end
|
55
|
-
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class AppRunner < Rapns::Daemon::AppRunner
|
5
|
+
ENVIRONMENTS = {
|
6
|
+
:production => {
|
7
|
+
:push => ['gateway.push.apple.com', 2195],
|
8
|
+
:feedback => ['feedback.push.apple.com', 2196]
|
9
|
+
},
|
10
|
+
:development => {
|
11
|
+
:push => ['gateway.sandbox.push.apple.com', 2195],
|
12
|
+
:feedback => ['feedback.sandbox.push.apple.com', 2196]
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def started
|
19
|
+
poll = Rapns::Daemon.config[:feedback_poll]
|
20
|
+
host, port = ENVIRONMENTS[app.environment.to_sym][:feedback]
|
21
|
+
@feedback_receiver = FeedbackReceiver.new(app, host, port, poll)
|
22
|
+
@feedback_receiver.start
|
23
|
+
end
|
24
|
+
|
25
|
+
def stopped
|
26
|
+
@feedback_receiver.stop if @feedback_receiver
|
27
|
+
end
|
28
|
+
|
29
|
+
def new_delivery_handler
|
30
|
+
push_host, push_port = ENVIRONMENTS[app.environment.to_sym][:push]
|
31
|
+
DeliveryHandler.new(app, push_host, push_port)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class ConnectionError < StandardError; end
|
5
|
+
|
6
|
+
class Connection
|
7
|
+
attr_accessor :last_write
|
8
|
+
|
9
|
+
def self.idle_period
|
10
|
+
30.minutes
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(name, host, port, certificate, password)
|
14
|
+
@name = name
|
15
|
+
@host = host
|
16
|
+
@port = port
|
17
|
+
@certificate = certificate
|
18
|
+
@password = password
|
19
|
+
written
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect
|
23
|
+
@ssl_context = setup_ssl_context
|
24
|
+
@tcp_socket, @ssl_socket = connect_socket
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
begin
|
29
|
+
@ssl_socket.close if @ssl_socket
|
30
|
+
@tcp_socket.close if @tcp_socket
|
31
|
+
rescue IOError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def read(num_bytes)
|
36
|
+
@ssl_socket.read(num_bytes)
|
37
|
+
end
|
38
|
+
|
39
|
+
def select(timeout)
|
40
|
+
IO.select([@ssl_socket], nil, nil, timeout)
|
41
|
+
end
|
42
|
+
|
43
|
+
def write(data)
|
44
|
+
reconnect_idle if idle_period_exceeded?
|
45
|
+
|
46
|
+
retry_count = 0
|
47
|
+
|
48
|
+
begin
|
49
|
+
write_data(data)
|
50
|
+
rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
51
|
+
retry_count += 1;
|
52
|
+
|
53
|
+
if retry_count == 1
|
54
|
+
Rapns::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
|
55
|
+
end
|
56
|
+
|
57
|
+
if retry_count <= 3
|
58
|
+
reconnect
|
59
|
+
sleep 1
|
60
|
+
retry
|
61
|
+
else
|
62
|
+
raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def reconnect
|
68
|
+
close
|
69
|
+
@tcp_socket, @ssl_socket = connect_socket
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def reconnect_idle
|
75
|
+
Rapns::Daemon.logger.info("[#{@name}] Idle period exceeded, reconnecting...")
|
76
|
+
reconnect
|
77
|
+
end
|
78
|
+
|
79
|
+
def idle_period_exceeded?
|
80
|
+
Time.now - last_write > self.class.idle_period
|
81
|
+
end
|
82
|
+
|
83
|
+
def write_data(data)
|
84
|
+
@ssl_socket.write(data)
|
85
|
+
@ssl_socket.flush
|
86
|
+
written
|
87
|
+
end
|
88
|
+
|
89
|
+
def written
|
90
|
+
self.last_write = Time.now
|
91
|
+
end
|
92
|
+
|
93
|
+
def setup_ssl_context
|
94
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
95
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
|
96
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
|
97
|
+
ssl_context
|
98
|
+
end
|
99
|
+
|
100
|
+
def connect_socket
|
101
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
102
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
103
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
104
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
105
|
+
ssl_socket.sync = true
|
106
|
+
ssl_socket.connect
|
107
|
+
Rapns::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
|
108
|
+
[tcp_socket, ssl_socket]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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)
|
20
|
+
@app = app
|
21
|
+
@connection = conneciton
|
22
|
+
@notification = notification
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform
|
26
|
+
begin
|
27
|
+
@connection.write(@notification.to_binary)
|
28
|
+
check_for_error if Rapns::Daemon.config.check_for_errors
|
29
|
+
mark_delivered
|
30
|
+
Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.device_token}")
|
31
|
+
rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError => error
|
32
|
+
mark_failed(error.code, error.description)
|
33
|
+
raise
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def check_for_error
|
40
|
+
if @connection.select(SELECT_TIMEOUT)
|
41
|
+
error = nil
|
42
|
+
|
43
|
+
if tuple = @connection.read(ERROR_TUPLE_BYTES)
|
44
|
+
cmd, code, notification_id = tuple.unpack("ccN")
|
45
|
+
|
46
|
+
description = APN_ERRORS[code.to_i] || "Unknown error. Possible rapns bug?"
|
47
|
+
error = Rapns::DeliveryError.new(code, notification_id, description)
|
48
|
+
else
|
49
|
+
error = Rapns::Apns::DisconnectionError.new
|
50
|
+
end
|
51
|
+
|
52
|
+
begin
|
53
|
+
Rapns::Daemon.logger.error("[#{@app.name}] Error received, reconnecting...")
|
54
|
+
@connection.reconnect
|
55
|
+
ensure
|
56
|
+
raise error if error
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
|
5
|
+
def initialize(app, host, port)
|
6
|
+
@app = app
|
7
|
+
@connection = Connection.new(@app.name, host, port, @app.certificate, @app.password)
|
8
|
+
@connection.connect
|
9
|
+
end
|
10
|
+
|
11
|
+
def deliver(notification)
|
12
|
+
Rapns::Daemon::Apns::Delivery.perform(@app, @connection, notification)
|
13
|
+
end
|
14
|
+
|
15
|
+
def stopped
|
16
|
+
@connection.close
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
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,74 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
module Apns
|
4
|
+
class FeedbackReceiver
|
5
|
+
include InterruptibleSleep
|
6
|
+
include DatabaseReconnectable
|
7
|
+
|
8
|
+
FEEDBACK_TUPLE_BYTES = 38
|
9
|
+
|
10
|
+
def initialize(app, host, port, poll)
|
11
|
+
@app = app
|
12
|
+
@host = host
|
13
|
+
@port = port
|
14
|
+
@poll = poll
|
15
|
+
@certificate = app.certificate
|
16
|
+
@password = app.password
|
17
|
+
end
|
18
|
+
|
19
|
+
def start
|
20
|
+
@thread = Thread.new do
|
21
|
+
loop do
|
22
|
+
break if @stop
|
23
|
+
check_for_feedback
|
24
|
+
interruptible_sleep @poll
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop
|
30
|
+
@stop = true
|
31
|
+
interrupt_sleep
|
32
|
+
@thread.join if @thread
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_for_feedback
|
36
|
+
connection = nil
|
37
|
+
begin
|
38
|
+
connection = Connection.new("FeedbackReceiver:#{@app.name}", @host, @port, @certificate, @password)
|
39
|
+
connection.connect
|
40
|
+
|
41
|
+
while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
|
42
|
+
timestamp, device_token = parse_tuple(tuple)
|
43
|
+
create_feedback(timestamp, device_token)
|
44
|
+
end
|
45
|
+
rescue StandardError => e
|
46
|
+
Rapns::Daemon.logger.error(e)
|
47
|
+
ensure
|
48
|
+
connection.close if connection
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def parse_tuple(tuple)
|
55
|
+
failed_at, _, device_token = tuple.unpack("N1n1H*")
|
56
|
+
[Time.at(failed_at).utc, device_token]
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_feedback(failed_at, device_token)
|
60
|
+
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
61
|
+
with_database_reconnect_and_retry do
|
62
|
+
Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app.name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
|
63
|
+
feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
|
64
|
+
begin
|
65
|
+
Rapns.configuration.feedback_callback.call(feedback) if Rapns.configuration.feedback_callback
|
66
|
+
rescue StandardError => e
|
67
|
+
Rapns::Daemon.logger.error(e)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|