rpush 2.4.0-java → 2.6.0-java

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/README.md +18 -8
  4. data/lib/generators/rpush_migration_generator.rb +1 -0
  5. data/lib/generators/templates/rpush.rb +8 -2
  6. data/lib/generators/templates/rpush_2_0_0_updates.rb +1 -1
  7. data/lib/generators/templates/rpush_2_6_0_updates.rb +10 -0
  8. data/lib/rpush/cli.rb +63 -27
  9. data/lib/rpush/client/active_model.rb +3 -0
  10. data/lib/rpush/client/active_model/apns/notification.rb +1 -1
  11. data/lib/rpush/client/active_model/gcm/notification.rb +1 -0
  12. data/lib/rpush/client/active_model/wns/app.rb +23 -0
  13. data/lib/rpush/client/active_model/wns/notification.rb +28 -0
  14. data/lib/rpush/client/active_model/wpns/notification.rb +11 -6
  15. data/lib/rpush/client/active_record.rb +3 -0
  16. data/lib/rpush/client/active_record/notification.rb +1 -1
  17. data/lib/rpush/client/active_record/wns/app.rb +11 -0
  18. data/lib/rpush/client/active_record/wns/notification.rb +11 -0
  19. data/lib/rpush/client/mongoid.rb +3 -0
  20. data/lib/rpush/client/mongoid/apns/feedback.rb +3 -0
  21. data/lib/rpush/client/mongoid/notification.rb +7 -0
  22. data/lib/rpush/client/mongoid/wns/app.rb +14 -0
  23. data/lib/rpush/client/mongoid/wns/notification.rb +11 -0
  24. data/lib/rpush/client/redis.rb +3 -0
  25. data/lib/rpush/client/redis/notification.rb +1 -0
  26. data/lib/rpush/client/redis/wns/app.rb +14 -0
  27. data/lib/rpush/client/redis/wns/notification.rb +11 -0
  28. data/lib/rpush/configuration.rb +3 -7
  29. data/lib/rpush/daemon.rb +9 -0
  30. data/lib/rpush/daemon/apns/feedback_receiver.rb +5 -0
  31. data/lib/rpush/daemon/app_runner.rb +4 -5
  32. data/lib/rpush/daemon/dispatcher/apns_tcp.rb +47 -12
  33. data/lib/rpush/daemon/dispatcher_loop.rb +5 -0
  34. data/lib/rpush/daemon/feeder.rb +11 -0
  35. data/lib/rpush/daemon/gcm/delivery.rb +2 -2
  36. data/lib/rpush/daemon/interruptible_sleep.rb +8 -3
  37. data/lib/rpush/daemon/loggable.rb +4 -0
  38. data/lib/rpush/daemon/rpc.rb +9 -0
  39. data/lib/rpush/daemon/rpc/client.rb +27 -0
  40. data/lib/rpush/daemon/rpc/server.rb +82 -0
  41. data/lib/rpush/daemon/signal_handler.rb +7 -0
  42. data/lib/rpush/daemon/store/active_record.rb +17 -3
  43. data/lib/rpush/daemon/store/mongoid.rb +2 -2
  44. data/lib/rpush/daemon/store/redis.rb +2 -2
  45. data/lib/rpush/daemon/tcp_connection.rb +2 -2
  46. data/lib/rpush/daemon/wns.rb +9 -0
  47. data/lib/rpush/daemon/wns/delivery.rb +204 -0
  48. data/lib/rpush/embed.rb +15 -13
  49. data/lib/rpush/logger.rb +4 -0
  50. data/lib/rpush/plugin.rb +1 -1
  51. data/lib/rpush/push.rb +2 -11
  52. data/lib/rpush/reflection_collection.rb +15 -17
  53. data/lib/rpush/reflection_public_methods.rb +6 -4
  54. data/lib/rpush/version.rb +1 -1
  55. data/spec/functional/apns_spec.rb +1 -11
  56. data/spec/functional/cli_spec.rb +36 -0
  57. data/spec/functional_spec_helper.rb +11 -1
  58. data/spec/spec_helper.rb +4 -3
  59. data/spec/support/active_record_setup.rb +3 -2
  60. data/spec/unit/client/active_record/apns/notification_spec.rb +1 -1
  61. data/spec/unit/client/active_record/gcm/notification_spec.rb +5 -0
  62. data/spec/unit/configuration_spec.rb +0 -7
  63. data/spec/unit/daemon/adm/delivery_spec.rb +2 -2
  64. data/spec/unit/daemon/app_runner_spec.rb +2 -3
  65. data/spec/unit/daemon/gcm/delivery_spec.rb +1 -1
  66. data/spec/unit/daemon/tcp_connection_spec.rb +1 -1
  67. data/spec/unit/daemon/wns/delivery_spec.rb +171 -0
  68. data/spec/unit/daemon/wpns/delivery_spec.rb +1 -1
  69. data/spec/unit/daemon_spec.rb +2 -0
  70. data/spec/unit/embed_spec.rb +4 -11
  71. data/spec/unit/logger_spec.rb +2 -2
  72. data/spec/unit/push_spec.rb +0 -7
  73. data/spec/unit_spec_helper.rb +1 -1
  74. metadata +20 -2
@@ -0,0 +1,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Rpc
4
+ def self.socket_path(pid = Process.pid)
5
+ "/tmp/rpush.#{pid}.sock"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Rpc
4
+ class Client
5
+ def initialize(pid)
6
+ @socket = UNIXSocket.open(Rpc.socket_path(pid))
7
+ end
8
+
9
+ def status
10
+ call(:status)
11
+ end
12
+
13
+ def close
14
+ @socket.close
15
+ rescue StandardError # rubocop:disable Lint/HandleExceptions
16
+ end
17
+
18
+ private
19
+
20
+ def call(cmd, args = {})
21
+ @socket.puts(JSON.dump([cmd, args]))
22
+ JSON.parse(@socket.gets)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,82 @@
1
+ require 'socket'
2
+ require 'singleton'
3
+
4
+ module Rpush
5
+ module Daemon
6
+ module Rpc
7
+ class Server
8
+ include Singleton
9
+ include Loggable
10
+ include Reflectable
11
+
12
+ def self.start
13
+ instance.start
14
+ end
15
+
16
+ def self.stop
17
+ instance.stop
18
+ end
19
+
20
+ def start
21
+ @stop = false
22
+
23
+ @thread = Thread.new(UNIXServer.open(Rpc.socket_path)) do |server|
24
+ begin
25
+ loop do
26
+ socket = server.accept
27
+ break if @stop
28
+ read_loop(socket)
29
+ end
30
+
31
+ server.close
32
+ rescue StandardError => e
33
+ log_error(e)
34
+ ensure
35
+ File.unlink(Rpc.socket_path) if File.exist?(Rpc.socket_path)
36
+ end
37
+ end
38
+ end
39
+
40
+ def stop
41
+ @stop = true
42
+ UNIXSocket.new(Rpc.socket_path)
43
+ @thread.join if @thread
44
+ rescue StandardError => e
45
+ log_error(e)
46
+ end
47
+
48
+ private
49
+
50
+ def read_loop(socket)
51
+ loop do
52
+ line = socket.gets
53
+ break unless line
54
+
55
+ begin
56
+ cmd, args = JSON.load(line)
57
+ log_debug("[rpc:server] #{cmd.to_sym.inspect}, args: #{args.inspect}")
58
+ response = process(cmd, args)
59
+ socket.puts(JSON.dump(response))
60
+ rescue StandardError => e
61
+ log_error(e)
62
+ reflect(:error, e)
63
+ end
64
+ end
65
+
66
+ socket.close
67
+ end
68
+
69
+ def process(cmd, args) # rubocop:disable Lint/UnusedMethodArgument
70
+ case cmd
71
+ when 'status'
72
+ status
73
+ end
74
+ end
75
+
76
+ def status
77
+ Rpush::Daemon::AppRunner.status
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,6 +1,8 @@
1
1
  module Rpush
2
2
  module Daemon
3
3
  class SignalHandler
4
+ extend Loggable
5
+
4
6
  class << self
5
7
  attr_reader :thread
6
8
  end
@@ -18,6 +20,11 @@ module Rpush
18
20
  def self.stop
19
21
  @write_io.puts('break') if @write_io
20
22
  @thread.join if @thread
23
+ rescue StandardError => e
24
+ log_error(e)
25
+ reflect(:error, e)
26
+ ensure
27
+ @thread = nil
21
28
  end
22
29
 
23
30
  def self.start_handler(read_io)
@@ -11,7 +11,8 @@ module Rpush
11
11
  DEFAULT_MARK_OPTIONS = { persist: true }
12
12
 
13
13
  def initialize
14
- reopen_log
14
+ @using_oracle = adapter_name =~ /oracle/
15
+ reopen_log unless Rpush.config.embedded
15
16
  end
16
17
 
17
18
  def reopen_log
@@ -31,7 +32,7 @@ module Rpush
31
32
  Rpush::Client::ActiveRecord::Notification.transaction do
32
33
  relation = ready_for_delivery
33
34
  relation = relation.limit(limit)
34
- notifications = relation.lock(true).to_a
35
+ notifications = claim(relation)
35
36
  mark_processing(notifications)
36
37
  notifications
37
38
  end
@@ -188,7 +189,8 @@ module Rpush
188
189
  end
189
190
 
190
191
  def ready_for_delivery
191
- Rpush::Client::ActiveRecord::Notification.where('processing = ? AND delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, false, Time.now).order('created_at ASC')
192
+ relation = Rpush::Client::ActiveRecord::Notification.where('processing = ? AND delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, false, Time.now)
193
+ @using_oracle ? relation : relation.order('created_at ASC')
192
194
  end
193
195
 
194
196
  def mark_processing(notifications)
@@ -201,6 +203,18 @@ module Rpush
201
203
  end
202
204
  Rpush::Client::ActiveRecord::Notification.where(id: ids).update_all(['processing = ?', true])
203
205
  end
206
+
207
+ def claim(relation)
208
+ notifications = relation.lock(true).to_a
209
+ @using_oracle ? notifications.sort_by(&:created_at) : notifications
210
+ end
211
+
212
+ def adapter_name
213
+ env = (defined?(Rails) && Rails.env) ? Rails.env : 'development'
214
+ config = ::ActiveRecord::Base.configurations[env]
215
+ return '' unless config
216
+ Hash[config.map { |k, v| [k.to_sym, v] }][:adapter]
217
+ end
204
218
  end
205
219
  end
206
220
  end
@@ -96,12 +96,12 @@ module Rpush
96
96
  Rpush::Client::Mongoid::Apns::Feedback.create!(failed_at: failed_at, device_token: device_token, app: app)
97
97
  end
98
98
 
99
- def create_gcm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
99
+ def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
100
100
  notification = Rpush::Client::Mongoid::Gcm::Notification.new
101
101
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
102
102
  end
103
103
 
104
- def create_adm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
104
+ def create_adm_notification(attrs, data, registration_ids, deliver_after, app)
105
105
  notification = Rpush::Client::Mongoid::Adm::Notification.new
106
106
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
107
107
  end
@@ -82,12 +82,12 @@ module Rpush
82
82
  Rpush::Client::Redis::Apns::Feedback.create!(failed_at: failed_at, device_token: device_token, app_id: app.id)
83
83
  end
84
84
 
85
- def create_gcm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
85
+ def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
86
86
  notification = Rpush::Client::Redis::Gcm::Notification.new
87
87
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
88
88
  end
89
89
 
90
- def create_adm_notification(attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
90
+ def create_adm_notification(attrs, data, registration_ids, deliver_after, app)
91
91
  notification = Rpush::Client::Redis::Adm::Notification.new
92
92
  create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
93
93
  end
@@ -156,7 +156,7 @@ module Rpush
156
156
  [tcp_socket, ssl_socket]
157
157
  rescue *TCP_ERRORS => error
158
158
  if error.message =~ /certificate revoked/i
159
- log_warn('Certificate has been revoked.')
159
+ log_error('Certificate has been revoked.')
160
160
  reflect(:ssl_certificate_revoked, @app, error)
161
161
  end
162
162
  raise TcpConnectionError, "#{error.class.name}, #{error.message}"
@@ -166,7 +166,7 @@ module Rpush
166
166
  cert = @ssl_context.cert
167
167
  if certificate_expired?
168
168
  log_error(certificate_msg('expired'))
169
- fail Rpush::CertificateExpiredError.new(@app, cert.not_after)
169
+ raise Rpush::CertificateExpiredError.new(@app, cert.not_after)
170
170
  elsif certificate_expires_soon?
171
171
  log_warn(certificate_msg('will expire'))
172
172
  reflect(:ssl_certificate_will_expire, @app, cert.not_after)
@@ -0,0 +1,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Wns
4
+ extend ServiceConfigMethods
5
+
6
+ dispatcher :http
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,204 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Wns
4
+ # https://msdn.microsoft.com/en-us/library/windows/apps/hh465435.aspx
5
+ class Delivery < Rpush::Daemon::Delivery
6
+ # Oauth2.0 token endpoint. This endpoint is used to request authorization tokens.
7
+ WPN_TOKEN_URI = URI.parse('https://login.live.com/accesstoken.srf')
8
+
9
+ # Data used to request authorization tokens.
10
+ ACCESS_TOKEN_REQUEST_DATA = { "grant_type" => "client_credentials", "scope" => "notify.windows.com" }
11
+
12
+ MAX_RETRIES = 14
13
+
14
+ FAILURE_MESSAGES = {
15
+ 400 => 'One or more headers were specified incorrectly or conflict with another header.',
16
+ 401 => 'The cloud service did not present a valid authentication ticket. The OAuth ticket may be invalid.',
17
+ 403 => 'The cloud service is not authorized to send a notification to this URI even though they are authenticated.',
18
+ 404 => 'The channel URI is not valid or is not recognized by WNS.',
19
+ 405 => 'Invalid method (GET, CREATE); only POST (Windows or Windows Phone) or DELETE (Windows Phone only) is allowed.',
20
+ 406 => 'The cloud service exceeded its throttle limit.',
21
+ 410 => 'The channel expired.',
22
+ 413 => 'The notification payload exceeds the 5000 byte size limit.',
23
+ 500 => 'An internal failure caused notification delivery to fail.',
24
+ 503 => 'The server is currently unavailable.'
25
+ }
26
+
27
+ def initialize(app, http, notification, batch)
28
+ @app = app
29
+ @http = http
30
+ @notification = notification
31
+ @batch = batch
32
+ end
33
+
34
+ def perform
35
+ handle_response(do_post)
36
+ rescue SocketError => error
37
+ mark_retryable(@notification, Time.now + 10.seconds, error)
38
+ raise
39
+ rescue StandardError => error
40
+ mark_failed(error)
41
+ raise
42
+ ensure
43
+ @batch.notification_processed
44
+ end
45
+
46
+ private
47
+
48
+ def handle_response(response)
49
+ code = response.code.to_i
50
+ case code
51
+ when 200
52
+ ok(response)
53
+ when 401
54
+ unauthorized
55
+ when 404
56
+ invalid_channel(code)
57
+ when 406
58
+ not_acceptable
59
+ when 410
60
+ invalid_channel(code)
61
+ when 412
62
+ precondition_failed
63
+ when 503
64
+ service_unavailable
65
+ else
66
+ handle_failure(code)
67
+ end
68
+ end
69
+
70
+ def handle_failure(code, msg = nil)
71
+ unless msg
72
+ msg = FAILURE_MESSAGES.key?(code) ? FAILURE_MESSAGES[code] : Rpush::Daemon::HTTP_STATUS_CODES[code]
73
+ end
74
+ fail Rpush::DeliveryError.new(code, @notification.id, msg)
75
+ end
76
+
77
+ def ok(response)
78
+ status = status_from_response(response)
79
+ case status[:notification]
80
+ when ["received"]
81
+ mark_delivered
82
+ log_info("#{@notification.id} sent successfully")
83
+ when ["channelthrottled"]
84
+ mark_retryable(@notification, Time.now + (60 * 10))
85
+ log_warn("#{@notification.id} cannot be sent. The Queue is full.")
86
+ when ["dropped"]
87
+ log_error("#{@notification.id} was dropped. Headers: #{status}")
88
+ handle_failure(200, "Notification was received but suppressed by the service (#{status[:error_description]}).")
89
+ end
90
+ end
91
+
92
+ def unauthorized
93
+ @notification.app.access_token = nil
94
+ Rpush::Daemon.store.update_app(@notification.app)
95
+ if @notification.retries < MAX_RETRIES
96
+ retry_notification("Token invalid.")
97
+ else
98
+ msg = "Notification failed to be delivered in #{MAX_RETRIES} retries."
99
+ mark_failed(Rpush::DeliveryError.new(nil, @notification.id, msg))
100
+ end
101
+ end
102
+
103
+ def invalid_channel(code, msg = nil)
104
+ unless msg
105
+ msg = FAILURE_MESSAGES.key?(code) ? FAILURE_MESSAGES[code] : Rpush::Daemon::HTTP_STATUS_CODES[code]
106
+ end
107
+ reflect(:wns_invalid_channel, @notification, @notification.uri, "#{code}. #{msg}")
108
+ handle_failure(code, msg)
109
+ end
110
+
111
+ def not_acceptable
112
+ retry_notification("Per-day throttling limit reached.")
113
+ end
114
+
115
+ def precondition_failed
116
+ retry_notification("Device unreachable.")
117
+ end
118
+
119
+ def service_unavailable
120
+ mark_retryable_exponential(@notification)
121
+ log_warn("Service Unavailable. " + retry_message)
122
+ end
123
+
124
+ def retry_message
125
+ "Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime('%Y-%m-%d %H:%M:%S')} (retry #{@notification.retries})."
126
+ end
127
+
128
+ def retry_notification(reason)
129
+ deliver_after = Time.now + (60 * 60)
130
+ mark_retryable(@notification, deliver_after)
131
+ log_warn("#{reason} " + retry_message)
132
+ end
133
+
134
+ def do_post
135
+ body = notification_to_xml
136
+ uri = URI.parse(@notification.uri)
137
+ post = Net::HTTP::Post.new(uri.request_uri,
138
+ "Content-Length" => body.length.to_s,
139
+ "Content-Type" => "text/xml",
140
+ "X-WNS-Type" => "wns/toast",
141
+ "X-WNS-RequestForStatus" => "true",
142
+ "Authorization" => "Bearer #{access_token}")
143
+ post.body = body
144
+ @http.request(URI.parse(@notification.uri), post)
145
+ end
146
+
147
+ def status_from_response(response)
148
+ headers = response.to_hash.inject({}) {|h, v| h[v[0].downcase] = v[1]; h}
149
+ {
150
+ notification: headers["x-wns-status"],
151
+ device_connection: headers["x-wns-deviceconnectionstatus"],
152
+ msg_id: headers["x-wns-msg-id"],
153
+ error_description: headers["x-wns-error-description"],
154
+ debug_trace: headers["x-wns-debug-trace"]
155
+ }
156
+ end
157
+
158
+ def notification_to_xml
159
+ title = clean_param_string(@notification.data['title']) if @notification.data['title'].present?
160
+ body = clean_param_string(@notification.data['body']) if @notification.data['body'].present?
161
+ "<toast>
162
+ <visual version='1' lang='en-US'>
163
+ <binding template='ToastText02'>
164
+ <text id='1'>#{title}</text>
165
+ <text id='2'>#{body}</text>
166
+ </binding>
167
+ </visual>
168
+ </toast>"
169
+ end
170
+
171
+ def clean_param_string(string)
172
+ string.gsub(/&/, "&amp;").gsub(/</, "&lt;") \
173
+ .gsub(/>/, "&gt;").gsub(/'/, "&apos;").gsub(/"/, "&quot;")
174
+ end
175
+
176
+ def access_token
177
+ if @notification.app.access_token.nil? || @notification.app.access_token_expired?
178
+ post = Net::HTTP::Post.new(WPN_TOKEN_URI.path, 'Content-Type' => 'application/x-www-form-urlencoded')
179
+ post.set_form_data(ACCESS_TOKEN_REQUEST_DATA.merge('client_id' => @notification.app.client_id, 'client_secret' => @notification.app.client_secret))
180
+
181
+ handle_access_token(@http.request(WPN_TOKEN_URI, post))
182
+ end
183
+
184
+ @notification.app.access_token
185
+ end
186
+
187
+ def handle_access_token(response)
188
+ if response.code.to_i == 200
189
+ update_access_token(JSON.parse(response.body))
190
+ Rpush::Daemon.store.update_app(@notification.app)
191
+ log_info("WNS access token updated: token = #{@notification.app.access_token}, expires = #{@notification.app.access_token_expiration}")
192
+ else
193
+ log_warn("Could not retrieve access token from WNS: #{response.body}")
194
+ end
195
+ end
196
+
197
+ def update_access_token(data)
198
+ @notification.app.access_token = data['access_token']
199
+ @notification.app.access_token_expiration = Time.now + data['expires_in'].to_i
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end