Module: Imobile::PushNotifications

Defined in:
lib/imobile/push_notification.rb

Overview

Implementation details for push_notification.

Class Method Summary collapse

Class Method Details

.apns_host(server_type, service = :push) ⇒ Object

The host name for an Apple Push Notification Server.

Args:

server_type:: either :production or :sandbox
service:: either :push or :feedback


273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/imobile/push_notification.rb', line 273

def self.apns_host(server_type, service = :push)
  {
    :feedback => {
      :sandbox => 'feedback.sandbox.push.apple.com',
      :production => 'feedback.push.apple.com'
    },
    :push => {
      :sandbox => 'gateway.sandbox.push.apple.com',
      :production => 'gateway.push.apple.com'
    }
  }[service][server_type]
end

.apns_port(server_type, service = :push) ⇒ Object

The port for an Apple Push Notification Server.

Args:

server_type:: either :production or :sandbox
service:: either :push or :feedback


291
292
293
294
295
296
# File 'lib/imobile/push_notification.rb', line 291

def self.apns_port(server_type, service = :push)
  {
    :feedback => 2196,
    :push => 2195
  }[service]
end

.apns_socket(push_certificate, service = :push) ⇒ Object

Creates a socket to an Apple Push Notification Server.

Args:

push_certificate:: the APNs client certificate data, obtained by a call to
                   read_certificate
service:: either :feedback or :push

The returned socket is connected and ready for use.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/imobile/push_notification.rb', line 250

def self.apns_socket(push_certificate, service = :push)
  context = OpenSSL::SSL::SSLContext.new
  context.cert = push_certificate[:certificate]
  context.key = push_certificate[:key]
  
  server_type = push_certificate[:server_type]
  raw_socket = TCPSocket.new apns_host(server_type, service),
                             apns_port(server_type, service)
  
  socket = OpenSSL::SSL::SSLSocket.new raw_socket, context
  # Magic for closing the raw socket when the SSL socket is closed.
  (class <<socket; self; end).send :define_method, :close do
    super
    raw_socket.close
  end
  socket.connect
end

.decode_push_certificate(certificate_blob) ⇒ Object

Decodes an APNs certificate.



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/imobile/push_notification.rb', line 158

def self.decode_push_certificate(certificate_blob)
  if use_new_certificate_decoder?
    # Ruby 1.8.7 and above.
    data = decode_push_certificate_new certificate_blob
  else
    # Ruby 1.8.6.
    data = decode_push_certificate_heroku certificate_blob
  end
  data[:server_type] = server_type data[:certificate] 
  data
end

.decode_push_certificate_heroku(certificate_blob) ⇒ Object

Decodes an APNs certificate, using the openssl command-line tool.

This works on Heroku, which uses Ruby 1.8.6.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/imobile/push_notification.rb', line 188

def self.decode_push_certificate_heroku(certificate_blob)
  # Most of the filesystem on Heroku is read-only. On the other hand, not
  # everyone runs on Heroku. Find a reasonable temporary dir.
  if defined? RAILS_ROOT
    temp_dir = File.join RAILS_ROOT, 'tmp'
  elsif File.exists? '/tmp'
    temp_dir = '/tmp'
  else
    temp_dir = '.'
  end
  
  pkcs12_file_name = File.join temp_dir, "apns_#{Process.pid}.p12"
  pem_file_name = File.join temp_dir, "apns_#{Process.pid}.pem"
  out_file_name = File.join temp_dir, "apns_#{Process.pid}.err"
  
  # Use the command-line openssl tool to break up the pkcs12 file.
  File.open(pkcs12_file_name, 'wb') { |f| f.write certificate_blob }
  Kernel.system "openssl pkcs12 -in #{pkcs12_file_name} -clcerts -nodes " +
                "-out #{pem_file_name} -password pass: 2> #{out_file_name}"
  pem_blob = File.read pem_file_name    
  [pkcs12_file_name, pem_file_name, out_file_name].each { |f| File.delete f }
  
  certificate = OpenSSL::X509::Certificate.new pem_blob
  key = OpenSSL::PKey::RSA.new pem_blob
  { :certificate => certificate, :key => key }    
end

.decode_push_certificate_new(certificate_blob) ⇒ Object

Decodes an APNs certificate, using the new (1.8.7+) OpenSSL methods.



176
177
178
179
180
181
182
183
# File 'lib/imobile/push_notification.rb', line 176

def self.decode_push_certificate_new(certificate_blob)    
  pkcs12 = OpenSSL::PKCS12.new certificate_blob
  
  certificate = pkcs12.certificate
  key = pkcs12.key
  
  { :certificate => certificate, :key => key }
end

.encode_notification(notification) ⇒ Object

Encodes a push notification in a binary string for APNs consumption.

Returns a string suitable for transmission over an APNs, or nil if the notification is invalid (i.e. the json encoding exceeds 256 bytes).



231
232
233
234
235
236
237
238
239
240
# File 'lib/imobile/push_notification.rb', line 231

def self.encode_notification(notification)
  push_token = notification[:push_token] || ''
  notification = notification.dup
  notification.delete :push_token
  json_notification = notification.to_json
  return nil if json_notification.length > 256
  
  ["\0", [push_token.length].pack('n'), push_token,
   [json_notification.length].pack('n'), json_notification].join
end

.fixed_socket_read(socket, num_bytes) ⇒ Object

Reads a fixed number of bytes from a socket.



362
363
364
365
366
367
368
369
370
# File 'lib/imobile/push_notification.rb', line 362

def self.fixed_socket_read(socket, num_bytes)
  data = ''
  while data.length < num_bytes
    new_data = socket.read(num_bytes - data.length)
    return nil if new_data.nil? or new_data.empty?  # Socket closed.
    data += new_data
  end
  data
end

.push_feedback(certificate_or_path, &block) ⇒ Object

Real implementation of Imobile.push_feedback



320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/imobile/push_notification.rb', line 320

def self.push_feedback(certificate_or_path, &block)
  if Kernel.block_given?
    raw_push_feedback certificate_or_path, &block
    nil
  else
    feedback = []
    raw_push_feedback certificate_or_path do |feedback_item|
      feedback << feedback_item
    end
    feedback
  end
end

.push_notification(notification, certificate_or_path) ⇒ Object

Real implementation of Imobile.push_notification



315
316
317
# File 'lib/imobile/push_notification.rb', line 315

def self.push_notification(notification, certificate_or_path)
  push_notifications certificate_or_path, [notification]
end

.push_notifications(certificate_or_path, notifications) ⇒ Object

Real implementation of Imobile.push_notifications



299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/imobile/push_notification.rb', line 299

def self.push_notifications(certificate_or_path, notifications)
  context = PushNotificationsContext.new certificate_or_path
  notifications = [notifications] if notifications.kind_of? Hash
  notifications.each { |notification| context.push notification }
  if Kernel.block_given?
    loop do
      notifications = yield
      notifications = [notifications] if notifications.kind_of? Hash
      notifications.each { |notification| context.push notification }
    end
  end
  context.flush
  context.close
end

.raw_push_feedback(certificate_or_path) ⇒ Object

Reads the available feedback from Apple’s Push Notification service.

Args:

certificate_or_path:: see Imobile.push_notification

The currently provided feedback is the tokens for the devices which rejected notifications. Each piece of feedback is a hash with the following keys:

:push_token:: the device's token for push notifications, in binary
              (not hexadecimal) format
:time:: the last time when the device rejected notifications; according to
        Apple, the rejection can be discarded if the device sent a
        token after this time

The method reads all the feedback available from the Push Notification service, and yields each piece of feedback to the method’s block.



348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/imobile/push_notification.rb', line 348

def self.raw_push_feedback(certificate_or_path)
  socket = apns_socket read_certificate(certificate_or_path), :feedback
  loop do      
    break unless header = fixed_socket_read(socket, 6)
    time = Time.at header[0, 4].unpack('N').first
    push_token = fixed_socket_read(socket, header[4, 2].unpack('n').first)
    break unless push_token
    feedback_item = { :push_token => push_token, :time => time }
    yield feedback_item
  end
  socket.close
end

.read_certificate(certificate_blob_or_path) ⇒ Object

Reads an APNs certificate from a string or a file.



146
147
148
149
150
151
152
153
154
155
# File 'lib/imobile/push_notification.rb', line 146

def self.read_certificate(certificate_blob_or_path)
  unless certificate_blob_or_path.respond_to? :to_str
    return certificate_blob_or_path
  end    
  begin
    decode_push_certificate File.read(certificate_blob_or_path)
  rescue
    decode_push_certificate certificate_blob_or_path
  end
end

.server_type(certificate) ⇒ Object

The Apple Push Notification server type that a certificate works with.



216
217
218
219
220
221
222
223
224
225
# File 'lib/imobile/push_notification.rb', line 216

def self.server_type(certificate)
  case certificate.subject.to_s
  when /Apple Development Push/
    return :sandbox
  when /Apple Production Push/
    return :production
  else
    raise "Invalid push certificate - #{certificate.inspect}"
  end        
end

.use_new_certificate_decoder?Boolean

Checks whether the new certificate decoding code is supported.

Returns:

  • (Boolean)


171
172
173
# File 'lib/imobile/push_notification.rb', line 171

def self.use_new_certificate_decoder?
  OpenSSL::PKCS12.respond_to? :new    
end