rpush 7.0.1 → 9.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -4
- data/README.md +24 -44
- data/lib/generators/rpush_migration_generator.rb +1 -0
- data/lib/generators/templates/rpush.rb +23 -13
- data/lib/generators/templates/rpush_7_1_0_updates.rb +12 -0
- data/lib/rpush/client/active_model/apns/notification.rb +0 -4
- data/lib/rpush/client/active_model/{gcm → fcm}/app.rb +4 -3
- data/lib/rpush/client/active_model/{gcm → fcm}/expiry_collapse_key_mutual_inclusion_validator.rb +1 -1
- data/lib/rpush/client/active_model/fcm/notification.rb +129 -0
- data/lib/rpush/client/active_model/fcm/notification_keys_in_allowed_list_validator.rb +20 -0
- data/lib/rpush/client/active_model.rb +4 -3
- data/lib/rpush/client/active_record/{gcm → fcm}/app.rb +2 -2
- data/lib/rpush/client/active_record/{gcm → fcm}/notification.rb +2 -2
- data/lib/rpush/client/active_record.rb +2 -2
- data/lib/rpush/client/redis/app.rb +2 -0
- data/lib/rpush/client/redis/{gcm → fcm}/app.rb +2 -2
- data/lib/rpush/client/redis/{gcm → fcm}/notification.rb +2 -2
- data/lib/rpush/client/redis.rb +2 -2
- data/lib/rpush/configuration.rb +2 -19
- data/lib/rpush/daemon/apns2/delivery.rb +0 -1
- data/lib/rpush/daemon/apnsp8/delivery.rb +0 -1
- data/lib/rpush/daemon/fcm/delivery.rb +162 -0
- data/lib/rpush/daemon/{gcm.rb → fcm.rb} +1 -1
- data/lib/rpush/daemon/google_credential_cache.rb +41 -0
- data/lib/rpush/daemon/service_config_methods.rb +0 -2
- data/lib/rpush/daemon/store/active_record.rb +15 -12
- data/lib/rpush/daemon/store/interface.rb +3 -3
- data/lib/rpush/daemon/store/redis.rb +13 -9
- data/lib/rpush/daemon/webpush/delivery.rb +2 -2
- data/lib/rpush/daemon.rb +3 -9
- data/lib/rpush/reflection_collection.rb +3 -3
- data/lib/rpush/version.rb +2 -2
- data/lib/rpush.rb +1 -1
- data/spec/functional/apns2_spec.rb +2 -6
- data/spec/functional/cli_spec.rb +41 -15
- data/spec/functional/embed_spec.rb +57 -26
- data/spec/functional/{gcm_priority_spec.rb → fcm_priority_spec.rb} +13 -7
- data/spec/functional/fcm_spec.rb +77 -0
- data/spec/functional/retry_spec.rb +21 -4
- data/spec/functional/synchronization_spec.rb +1 -1
- data/spec/functional_spec_helper.rb +1 -7
- data/spec/spec_helper.rb +4 -1
- data/spec/support/active_record_setup.rb +3 -1
- data/spec/unit/client/active_record/{gcm → fcm}/app_spec.rb +2 -2
- data/spec/unit/client/active_record/fcm/notification_spec.rb +10 -0
- data/spec/unit/client/active_record/shared/app.rb +1 -1
- data/spec/unit/client/redis/fcm/app_spec.rb +5 -0
- data/spec/unit/client/redis/fcm/notification_spec.rb +5 -0
- data/spec/unit/client/shared/apns/notification.rb +0 -15
- data/spec/unit/client/shared/fcm/app.rb +4 -0
- data/spec/unit/client/shared/fcm/notification.rb +92 -0
- data/spec/unit/configuration_spec.rb +1 -1
- data/spec/unit/daemon/apnsp8/delivery_spec.rb +1 -1
- data/spec/unit/daemon/fcm/delivery_spec.rb +127 -0
- data/spec/unit/daemon/service_config_methods_spec.rb +1 -1
- data/spec/unit/daemon/shared/store.rb +0 -42
- data/spec/unit/daemon/wns/delivery_spec.rb +1 -1
- data/spec/unit/logger_spec.rb +1 -1
- data/spec/unit_spec_helper.rb +1 -1
- metadata +127 -69
- data/lib/rpush/apns_feedback.rb +0 -18
- data/lib/rpush/client/active_model/gcm/notification.rb +0 -62
- data/lib/rpush/daemon/apns/delivery.rb +0 -43
- data/lib/rpush/daemon/apns/feedback_receiver.rb +0 -91
- data/lib/rpush/daemon/apns.rb +0 -17
- data/lib/rpush/daemon/dispatcher/apns_tcp.rb +0 -152
- data/lib/rpush/daemon/dispatcher/tcp.rb +0 -22
- data/lib/rpush/daemon/gcm/delivery.rb +0 -241
- data/lib/rpush/daemon/tcp_connection.rb +0 -190
- data/spec/functional/apns_spec.rb +0 -162
- data/spec/functional/gcm_spec.rb +0 -46
- data/spec/functional/new_app_spec.rb +0 -44
- data/spec/unit/apns_feedback_spec.rb +0 -39
- data/spec/unit/client/active_record/gcm/notification_spec.rb +0 -14
- data/spec/unit/client/redis/gcm/app_spec.rb +0 -5
- data/spec/unit/client/redis/gcm/notification_spec.rb +0 -5
- data/spec/unit/client/shared/gcm/app.rb +0 -4
- data/spec/unit/client/shared/gcm/notification.rb +0 -77
- data/spec/unit/daemon/apns/delivery_spec.rb +0 -108
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +0 -137
- data/spec/unit/daemon/dispatcher/tcp_spec.rb +0 -32
- data/spec/unit/daemon/gcm/delivery_spec.rb +0 -387
- data/spec/unit/daemon/tcp_connection_spec.rb +0 -292
data/lib/rpush/daemon/apns.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
module Rpush
|
2
|
-
module Daemon
|
3
|
-
module Apns
|
4
|
-
extend ServiceConfigMethods
|
5
|
-
|
6
|
-
HOSTS = {
|
7
|
-
production: ['gateway.push.apple.com', 2195],
|
8
|
-
development: ['gateway.sandbox.push.apple.com', 2195], # deprecated
|
9
|
-
sandbox: ['gateway.sandbox.push.apple.com', 2195]
|
10
|
-
}
|
11
|
-
|
12
|
-
batch_deliveries true
|
13
|
-
dispatcher :apns_tcp, host: proc { |app| HOSTS[app.environment.to_sym] }
|
14
|
-
loops Rpush::Daemon::Apns::FeedbackReceiver, if: -> { Rpush.config.apns.feedback_receiver.enabled && !Rpush.config.push }
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,152 +0,0 @@
|
|
1
|
-
module Rpush
|
2
|
-
module Daemon
|
3
|
-
module Dispatcher
|
4
|
-
class ApnsTcp < Rpush::Daemon::Dispatcher::Tcp
|
5
|
-
include Loggable
|
6
|
-
include Reflectable
|
7
|
-
|
8
|
-
SELECT_TIMEOUT = 10
|
9
|
-
ERROR_TUPLE_BYTES = 6
|
10
|
-
APNS_ERRORS = {
|
11
|
-
1 => 'Processing error',
|
12
|
-
2 => 'Missing device token',
|
13
|
-
3 => 'Missing topic',
|
14
|
-
4 => 'Missing payload',
|
15
|
-
5 => 'Missing token size',
|
16
|
-
6 => 'Missing topic size',
|
17
|
-
7 => 'Missing payload size',
|
18
|
-
8 => 'Invalid device token',
|
19
|
-
10 => 'APNs closed connection (possible maintenance)',
|
20
|
-
255 => 'None (unknown error)'
|
21
|
-
}
|
22
|
-
|
23
|
-
def initialize(*args)
|
24
|
-
super
|
25
|
-
@dispatch_mutex = Mutex.new
|
26
|
-
@stop_error_receiver = false
|
27
|
-
@connection.on_connect { start_error_receiver }
|
28
|
-
end
|
29
|
-
|
30
|
-
def dispatch(payload)
|
31
|
-
@dispatch_mutex.synchronize do
|
32
|
-
@delivery_class.new(@app, @connection, payload.batch).perform
|
33
|
-
record_batch(payload.batch)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def cleanup
|
38
|
-
if Rpush.config.push
|
39
|
-
# In push mode only a single batch is sent, followed by immediate shutdown.
|
40
|
-
# Allow the error receiver time to handle any errors.
|
41
|
-
@reconnect_disabled = true
|
42
|
-
sleep 1
|
43
|
-
end
|
44
|
-
|
45
|
-
@stop_error_receiver = true
|
46
|
-
super
|
47
|
-
@error_receiver_thread.join if @error_receiver_thread
|
48
|
-
rescue StandardError => e
|
49
|
-
log_error(e)
|
50
|
-
reflect(:error, e)
|
51
|
-
ensure
|
52
|
-
@error_receiver_thread = nil
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def start_error_receiver
|
58
|
-
@error_receiver_thread = Thread.new do
|
59
|
-
check_for_error until @stop_error_receiver
|
60
|
-
Rpush::Daemon.store.release_connection
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def delivered_buffer
|
65
|
-
@delivered_buffer ||= RingBuffer.new(Rpush.config.batch_size * 10)
|
66
|
-
end
|
67
|
-
|
68
|
-
def record_batch(batch)
|
69
|
-
batch.each_delivered do |notification|
|
70
|
-
delivered_buffer << notification.id
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def check_for_error
|
75
|
-
begin
|
76
|
-
# On Linux, select returns nil from a dropped connection.
|
77
|
-
# On OS X, Errno::EBADF is raised following a Errno::EADDRNOTAVAIL from the write call.
|
78
|
-
return unless @connection.select(SELECT_TIMEOUT)
|
79
|
-
tuple = @connection.read(ERROR_TUPLE_BYTES)
|
80
|
-
rescue *TcpConnection::TCP_ERRORS
|
81
|
-
reconnect unless @stop_error_receiver
|
82
|
-
return
|
83
|
-
end
|
84
|
-
|
85
|
-
@dispatch_mutex.synchronize { handle_error_response(tuple) }
|
86
|
-
rescue StandardError => e
|
87
|
-
log_error(e)
|
88
|
-
end
|
89
|
-
|
90
|
-
def handle_error_response(tuple)
|
91
|
-
if tuple
|
92
|
-
_, code, notification_id = tuple.unpack('ccN')
|
93
|
-
handle_error(code, notification_id)
|
94
|
-
else
|
95
|
-
handle_disconnect
|
96
|
-
end
|
97
|
-
|
98
|
-
if Rpush.config.push
|
99
|
-
# Only attempt to handle a single error in Push mode.
|
100
|
-
@stop_error_receiver = true
|
101
|
-
return
|
102
|
-
end
|
103
|
-
|
104
|
-
reconnect
|
105
|
-
ensure
|
106
|
-
delivered_buffer.clear
|
107
|
-
end
|
108
|
-
|
109
|
-
def reconnect
|
110
|
-
return if @reconnect_disabled
|
111
|
-
log_error("Lost connection to #{@connection.host}:#{@connection.port}, reconnecting...")
|
112
|
-
@connection.reconnect_with_rescue
|
113
|
-
end
|
114
|
-
|
115
|
-
def handle_disconnect
|
116
|
-
log_error("The APNs disconnected before any notifications could be delivered. This usually indicates you are using an invalid certificate.") if delivered_buffer.size == 0
|
117
|
-
end
|
118
|
-
|
119
|
-
def handle_error(code, notification_id)
|
120
|
-
notification_id = Rpush::Daemon.store.translate_integer_notification_id(notification_id)
|
121
|
-
failed_pos = delivered_buffer.index(notification_id)
|
122
|
-
description = description_for_code(code)
|
123
|
-
log_error("Notification #{notification_id} failed with error: " + description)
|
124
|
-
Rpush::Daemon.store.mark_ids_failed([notification_id], code, description, Time.now)
|
125
|
-
reflect(:notification_id_failed, @app, notification_id, code, description)
|
126
|
-
|
127
|
-
if failed_pos
|
128
|
-
retry_ids = delivered_buffer[(failed_pos + 1)..-1]
|
129
|
-
retry_notification_ids(retry_ids, notification_id)
|
130
|
-
elsif delivered_buffer.size > 0
|
131
|
-
log_error("Delivery sequence unknown for notifications following #{notification_id}.")
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def description_for_code(code)
|
136
|
-
APNS_ERRORS[code.to_i] ? "#{APNS_ERRORS[code.to_i]} (#{code})" : "Unknown error code #{code.inspect}. Possible Rpush bug?"
|
137
|
-
end
|
138
|
-
|
139
|
-
def retry_notification_ids(ids, notification_id)
|
140
|
-
return if ids.size == 0
|
141
|
-
|
142
|
-
now = Time.now
|
143
|
-
Rpush::Daemon.store.mark_ids_retryable(ids, now)
|
144
|
-
notifications_str = 'Notification'
|
145
|
-
notifications_str += 's' if ids.size > 1
|
146
|
-
log_warn("#{notifications_str} #{ids.join(', ')} will be retried due to the failure of notification #{notification_id}.")
|
147
|
-
ids.each { |id| reflect(:notification_id_will_retry, @app, id, now) }
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
module Rpush
|
2
|
-
module Daemon
|
3
|
-
module Dispatcher
|
4
|
-
class Tcp
|
5
|
-
def initialize(app, delivery_class, options = {})
|
6
|
-
@app = app
|
7
|
-
@delivery_class = delivery_class
|
8
|
-
@host, @port = options[:host].call(@app)
|
9
|
-
@connection = Rpush::Daemon::TcpConnection.new(@app, @host, @port)
|
10
|
-
end
|
11
|
-
|
12
|
-
def dispatch(payload)
|
13
|
-
@delivery_class.new(@app, @connection, payload.notification, payload.batch).perform
|
14
|
-
end
|
15
|
-
|
16
|
-
def cleanup
|
17
|
-
@connection.close if @connection
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,241 +0,0 @@
|
|
1
|
-
module Rpush
|
2
|
-
module Daemon
|
3
|
-
module Gcm
|
4
|
-
# https://firebase.google.com/docs/cloud-messaging/server
|
5
|
-
class Delivery < Rpush::Daemon::Delivery
|
6
|
-
include MultiJsonHelper
|
7
|
-
|
8
|
-
host = 'https://fcm.googleapis.com'
|
9
|
-
FCM_URI = URI.parse("#{host}/fcm/send")
|
10
|
-
UNAVAILABLE_STATES = %w(Unavailable BadGateway InternalServerError)
|
11
|
-
INVALID_REGISTRATION_ID_STATES = %w(InvalidRegistration MismatchSenderId NotRegistered InvalidPackageName)
|
12
|
-
|
13
|
-
def initialize(app, http, notification, batch)
|
14
|
-
@app = app
|
15
|
-
@http = http
|
16
|
-
@notification = notification
|
17
|
-
@batch = batch
|
18
|
-
end
|
19
|
-
|
20
|
-
def perform
|
21
|
-
handle_response(do_post)
|
22
|
-
rescue SocketError => error
|
23
|
-
mark_retryable(@notification, Time.now + 10.seconds, error)
|
24
|
-
raise
|
25
|
-
rescue StandardError => error
|
26
|
-
mark_failed(error)
|
27
|
-
raise
|
28
|
-
ensure
|
29
|
-
@batch.notification_processed
|
30
|
-
end
|
31
|
-
|
32
|
-
protected
|
33
|
-
|
34
|
-
def handle_response(response)
|
35
|
-
case response.code.to_i
|
36
|
-
when 200
|
37
|
-
ok(response)
|
38
|
-
when 400
|
39
|
-
bad_request
|
40
|
-
when 401
|
41
|
-
unauthorized
|
42
|
-
when 500
|
43
|
-
internal_server_error(response)
|
44
|
-
when 502
|
45
|
-
bad_gateway(response)
|
46
|
-
when 503
|
47
|
-
service_unavailable(response)
|
48
|
-
when 500..599
|
49
|
-
other_5xx_error(response)
|
50
|
-
else
|
51
|
-
fail Rpush::DeliveryError.new(response.code.to_i, @notification.id, Rpush::Daemon::HTTP_STATUS_CODES[response.code.to_i])
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def ok(response)
|
56
|
-
results = process_response(response)
|
57
|
-
handle_successes(results.successes)
|
58
|
-
|
59
|
-
if results.failures.any?
|
60
|
-
handle_failures(results.failures, response)
|
61
|
-
else
|
62
|
-
mark_delivered
|
63
|
-
log_info("#{@notification.id} sent to #{@notification.registration_ids.join(', ')}")
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def process_response(response)
|
68
|
-
body = multi_json_load(response.body)
|
69
|
-
results = Results.new(body['results'], @notification.registration_ids)
|
70
|
-
results.process(invalid: INVALID_REGISTRATION_ID_STATES, unavailable: UNAVAILABLE_STATES)
|
71
|
-
results
|
72
|
-
end
|
73
|
-
|
74
|
-
def handle_successes(successes)
|
75
|
-
successes.each do |result|
|
76
|
-
reflect(:gcm_delivered_to_recipient, @notification, result[:registration_id])
|
77
|
-
next unless result.key?(:canonical_id)
|
78
|
-
reflect(:gcm_canonical_id, result[:registration_id], result[:canonical_id])
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def handle_failures(failures, response)
|
83
|
-
if failures[:unavailable].count == @notification.registration_ids.count
|
84
|
-
retry_delivery(@notification, response)
|
85
|
-
log_warn("All recipients unavailable. #{retry_message}")
|
86
|
-
else
|
87
|
-
if failures[:unavailable].any?
|
88
|
-
unavailable_idxs = failures[:unavailable].map { |result| result[:index] }
|
89
|
-
new_notification = create_new_notification(response, unavailable_idxs)
|
90
|
-
failures.description += " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}."
|
91
|
-
end
|
92
|
-
handle_errors(failures)
|
93
|
-
fail Rpush::DeliveryError.new(nil, @notification.id, failures.description)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def handle_errors(failures)
|
98
|
-
failures.each do |result|
|
99
|
-
reflect(:gcm_failed_to_recipient, @notification, result[:error], result[:registration_id])
|
100
|
-
end
|
101
|
-
failures[:invalid].each do |result|
|
102
|
-
reflect(:gcm_invalid_registration_id, @app, result[:error], result[:registration_id])
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def create_new_notification(response, unavailable_idxs)
|
107
|
-
attrs = { 'app_id' => @notification.app_id, 'collapse_key' => @notification.collapse_key, 'delay_while_idle' => @notification.delay_while_idle }
|
108
|
-
registration_ids = @notification.registration_ids.values_at(*unavailable_idxs)
|
109
|
-
Rpush::Daemon.store.create_gcm_notification(attrs, @notification.data,
|
110
|
-
registration_ids, deliver_after_header(response), @app)
|
111
|
-
end
|
112
|
-
|
113
|
-
def bad_request
|
114
|
-
fail Rpush::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an Rpush bug, please open an issue.')
|
115
|
-
end
|
116
|
-
|
117
|
-
def unauthorized
|
118
|
-
fail Rpush::DeliveryError.new(401, @notification.id, 'Unauthorized, check your App auth_key.')
|
119
|
-
end
|
120
|
-
|
121
|
-
def internal_server_error(response)
|
122
|
-
retry_delivery(@notification, response)
|
123
|
-
log_warn("GCM responded with an Internal Error. " + retry_message)
|
124
|
-
end
|
125
|
-
|
126
|
-
def bad_gateway(response)
|
127
|
-
retry_delivery(@notification, response)
|
128
|
-
log_warn("GCM responded with a Bad Gateway Error. " + retry_message)
|
129
|
-
end
|
130
|
-
|
131
|
-
def service_unavailable(response)
|
132
|
-
retry_delivery(@notification, response)
|
133
|
-
log_warn("GCM responded with an Service Unavailable Error. " + retry_message)
|
134
|
-
end
|
135
|
-
|
136
|
-
def other_5xx_error(response)
|
137
|
-
retry_delivery(@notification, response)
|
138
|
-
log_warn("GCM responded with a 5xx Error. " + retry_message)
|
139
|
-
end
|
140
|
-
|
141
|
-
def deliver_after_header(response)
|
142
|
-
Rpush::Daemon::RetryHeaderParser.parse(response.header['retry-after'])
|
143
|
-
end
|
144
|
-
|
145
|
-
def retry_delivery(notification, response)
|
146
|
-
time = deliver_after_header(response)
|
147
|
-
if time
|
148
|
-
mark_retryable(notification, time)
|
149
|
-
else
|
150
|
-
mark_retryable_exponential(notification)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def retry_message
|
155
|
-
"Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime('%Y-%m-%d %H:%M:%S')} (retry #{@notification.retries})."
|
156
|
-
end
|
157
|
-
|
158
|
-
def do_post
|
159
|
-
post = Net::HTTP::Post.new(FCM_URI.path, 'Content-Type' => 'application/json',
|
160
|
-
'Authorization' => "key=#{@app.auth_key}")
|
161
|
-
post.body = @notification.as_json.to_json
|
162
|
-
@http.request(FCM_URI, post)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
class Results
|
167
|
-
attr_reader :successes, :failures
|
168
|
-
|
169
|
-
def initialize(results_data, registration_ids)
|
170
|
-
@results_data = results_data
|
171
|
-
@registration_ids = registration_ids
|
172
|
-
end
|
173
|
-
|
174
|
-
def process(failure_partitions = {}) # rubocop:disable Metrics/AbcSize
|
175
|
-
@successes = []
|
176
|
-
@failures = Failures.new
|
177
|
-
failure_partitions.each_key do |category|
|
178
|
-
failures[category] = []
|
179
|
-
end
|
180
|
-
|
181
|
-
@results_data.each_with_index do |result, index|
|
182
|
-
entry = {
|
183
|
-
registration_id: @registration_ids[index],
|
184
|
-
index: index
|
185
|
-
}
|
186
|
-
if result['message_id']
|
187
|
-
entry[:canonical_id] = result['registration_id'] if result['registration_id'].present?
|
188
|
-
successes << entry
|
189
|
-
elsif result['error']
|
190
|
-
entry[:error] = result['error']
|
191
|
-
failures << entry
|
192
|
-
failure_partitions.each do |category, error_states|
|
193
|
-
failures[category] << entry if error_states.include?(result['error'])
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
failures.all_failed = failures.count == @registration_ids.count
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
class Failures < Hash
|
202
|
-
include Enumerable
|
203
|
-
attr_writer :all_failed, :description
|
204
|
-
|
205
|
-
def initialize
|
206
|
-
super[:all] = []
|
207
|
-
end
|
208
|
-
|
209
|
-
def each
|
210
|
-
self[:all].each { |x| yield x }
|
211
|
-
end
|
212
|
-
|
213
|
-
def <<(item)
|
214
|
-
self[:all] << item
|
215
|
-
end
|
216
|
-
|
217
|
-
def description
|
218
|
-
@description ||= describe
|
219
|
-
end
|
220
|
-
|
221
|
-
def any?
|
222
|
-
self[:all].any?
|
223
|
-
end
|
224
|
-
|
225
|
-
private
|
226
|
-
|
227
|
-
def describe
|
228
|
-
if @all_failed
|
229
|
-
error_description = "Failed to deliver to all recipients."
|
230
|
-
else
|
231
|
-
index_list = map { |item| item[:index] }
|
232
|
-
error_description = "Failed to deliver to recipients #{index_list.join(', ')}."
|
233
|
-
end
|
234
|
-
|
235
|
-
error_list = map { |item| item[:error] }
|
236
|
-
error_description + " Errors: #{error_list.join(', ')}."
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
@@ -1,190 +0,0 @@
|
|
1
|
-
module Rpush
|
2
|
-
module Daemon
|
3
|
-
class TcpConnectionError < StandardError; end
|
4
|
-
|
5
|
-
class TcpConnection
|
6
|
-
include Reflectable
|
7
|
-
include Loggable
|
8
|
-
|
9
|
-
OSX_TCP_KEEPALIVE = 0x10 # Defined in <netinet/tcp.h>
|
10
|
-
KEEPALIVE_INTERVAL = 5
|
11
|
-
KEEPALIVE_IDLE = 5
|
12
|
-
KEEPALIVE_MAX_FAIL_PROBES = 1
|
13
|
-
TCP_ERRORS = [SystemCallError, OpenSSL::OpenSSLError, IOError]
|
14
|
-
|
15
|
-
attr_accessor :last_touch
|
16
|
-
attr_reader :host, :port
|
17
|
-
|
18
|
-
def self.idle_period
|
19
|
-
30.minutes
|
20
|
-
end
|
21
|
-
|
22
|
-
def initialize(app, host, port)
|
23
|
-
@app = app
|
24
|
-
@host = host
|
25
|
-
@port = port
|
26
|
-
@certificate = app.certificate
|
27
|
-
@password = app.password
|
28
|
-
@connected = false
|
29
|
-
@connection_callbacks = []
|
30
|
-
touch
|
31
|
-
end
|
32
|
-
|
33
|
-
def on_connect(&blk)
|
34
|
-
raise 'already connected' if @connected
|
35
|
-
@connection_callbacks << blk
|
36
|
-
end
|
37
|
-
|
38
|
-
def connect
|
39
|
-
@ssl_context = setup_ssl_context
|
40
|
-
@tcp_socket, @ssl_socket = connect_socket
|
41
|
-
@connected = true
|
42
|
-
|
43
|
-
@connection_callbacks.each do |blk|
|
44
|
-
begin
|
45
|
-
blk.call
|
46
|
-
rescue StandardError => e
|
47
|
-
log_error(e)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
@connection_callbacks.clear
|
52
|
-
end
|
53
|
-
|
54
|
-
def close
|
55
|
-
@ssl_socket.close if @ssl_socket
|
56
|
-
@tcp_socket.close if @tcp_socket
|
57
|
-
rescue IOError # rubocop:disable HandleExceptions
|
58
|
-
end
|
59
|
-
|
60
|
-
def read(num_bytes)
|
61
|
-
@ssl_socket.read(num_bytes) if @ssl_socket
|
62
|
-
end
|
63
|
-
|
64
|
-
def select(timeout)
|
65
|
-
IO.select([@ssl_socket], nil, nil, timeout) if @ssl_socket
|
66
|
-
end
|
67
|
-
|
68
|
-
def write(data)
|
69
|
-
connect unless @connected
|
70
|
-
reconnect_idle if idle_period_exceeded?
|
71
|
-
|
72
|
-
retry_count = 0
|
73
|
-
|
74
|
-
begin
|
75
|
-
write_data(data)
|
76
|
-
rescue *TCP_ERRORS => e
|
77
|
-
retry_count += 1
|
78
|
-
|
79
|
-
if retry_count == 1
|
80
|
-
log_error("Lost connection to #{@host}:#{@port} (#{e.class.name}, #{e.message}), reconnecting...")
|
81
|
-
reflect(:tcp_connection_lost, @app, e)
|
82
|
-
end
|
83
|
-
|
84
|
-
if retry_count <= 3
|
85
|
-
reconnect_with_rescue
|
86
|
-
sleep 1
|
87
|
-
retry
|
88
|
-
else
|
89
|
-
raise TcpConnectionError, "#{@app.name} tried #{retry_count - 1} times to reconnect but failed (#{e.class.name}, #{e.message})."
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def reconnect_with_rescue
|
95
|
-
reconnect
|
96
|
-
rescue StandardError => e
|
97
|
-
log_error(e)
|
98
|
-
end
|
99
|
-
|
100
|
-
def reconnect
|
101
|
-
close
|
102
|
-
@tcp_socket, @ssl_socket = connect_socket
|
103
|
-
end
|
104
|
-
|
105
|
-
protected
|
106
|
-
|
107
|
-
def reconnect_idle
|
108
|
-
log_info("Idle period exceeded, reconnecting...")
|
109
|
-
reconnect
|
110
|
-
end
|
111
|
-
|
112
|
-
def idle_period_exceeded?
|
113
|
-
Time.now - last_touch > self.class.idle_period
|
114
|
-
end
|
115
|
-
|
116
|
-
def write_data(data)
|
117
|
-
@ssl_socket.write(data)
|
118
|
-
@ssl_socket.flush
|
119
|
-
touch
|
120
|
-
end
|
121
|
-
|
122
|
-
def touch
|
123
|
-
self.last_touch = Time.now
|
124
|
-
end
|
125
|
-
|
126
|
-
def setup_ssl_context
|
127
|
-
ssl_context = OpenSSL::SSL::SSLContext.new
|
128
|
-
ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
|
129
|
-
ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
|
130
|
-
ssl_context
|
131
|
-
end
|
132
|
-
|
133
|
-
def connect_socket
|
134
|
-
touch
|
135
|
-
check_certificate_expiration
|
136
|
-
|
137
|
-
tcp_socket = TCPSocket.new(@host, @port)
|
138
|
-
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
139
|
-
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
|
140
|
-
|
141
|
-
# Linux
|
142
|
-
if [:SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all? { |c| Socket.const_defined?(c) }
|
143
|
-
tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEPALIVE_IDLE)
|
144
|
-
tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, KEEPALIVE_INTERVAL)
|
145
|
-
tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, KEEPALIVE_MAX_FAIL_PROBES)
|
146
|
-
end
|
147
|
-
|
148
|
-
# OSX
|
149
|
-
if RUBY_PLATFORM =~ /darwin/
|
150
|
-
tcp_socket.setsockopt(Socket::IPPROTO_TCP, OSX_TCP_KEEPALIVE, KEEPALIVE_IDLE)
|
151
|
-
end
|
152
|
-
|
153
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
154
|
-
ssl_socket.sync = true
|
155
|
-
ssl_socket.connect
|
156
|
-
[tcp_socket, ssl_socket]
|
157
|
-
rescue *TCP_ERRORS => error
|
158
|
-
if error.message =~ /certificate revoked/i
|
159
|
-
log_error('Certificate has been revoked.')
|
160
|
-
reflect(:ssl_certificate_revoked, @app, error)
|
161
|
-
end
|
162
|
-
raise TcpConnectionError, "#{error.class.name}, #{error.message}"
|
163
|
-
end
|
164
|
-
|
165
|
-
def check_certificate_expiration
|
166
|
-
cert = @ssl_context.cert
|
167
|
-
if certificate_expired?
|
168
|
-
log_error(certificate_msg('expired'))
|
169
|
-
raise Rpush::CertificateExpiredError.new(@app, cert.not_after)
|
170
|
-
elsif certificate_expires_soon?
|
171
|
-
log_warn(certificate_msg('will expire'))
|
172
|
-
reflect(:ssl_certificate_will_expire, @app, cert.not_after)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def certificate_msg(msg)
|
177
|
-
time = @ssl_context.cert.not_after.utc.strftime('%Y-%m-%d %H:%M:%S UTC')
|
178
|
-
"Certificate #{msg} at #{time}."
|
179
|
-
end
|
180
|
-
|
181
|
-
def certificate_expired?
|
182
|
-
@ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < Time.now.utc
|
183
|
-
end
|
184
|
-
|
185
|
-
def certificate_expires_soon?
|
186
|
-
@ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < (Time.now + 1.month).utc
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|