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


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

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


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

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.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/imobile/push_notification.rb', line 191

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.sync = true
  socket.connect
end

.decode_push_certificate(certificate_blob) ⇒ Object

Decodes an APNs certificate.



99
100
101
102
103
104
105
106
107
108
109
# File 'lib/imobile/push_notification.rb', line 99

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.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/imobile/push_notification.rb', line 129

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.



117
118
119
120
121
122
123
124
# File 'lib/imobile/push_notification.rb', line 117

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).



172
173
174
175
176
177
178
179
180
181
# File 'lib/imobile/push_notification.rb', line 172

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.



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

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



261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/imobile/push_notification.rb', line 261

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



256
257
258
# File 'lib/imobile/push_notification.rb', line 256

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



241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/imobile/push_notification.rb', line 241

def self.push_notifications(certificate_or_path, notifications)
  socket = apns_socket read_certificate(certificate_or_path), :push
  notifications = [notifications] if notifications.kind_of? Hash
  notifications.each { |n| socket.write encode_notification(n) }
  if Kernel.block_given?
    loop do
      notifications = yield
      notifications = [notifications] if notifications.kind_of? Hash
      notifications.each { |n| socket.write encode_notification(n) }
    end
  end
  socket.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.



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/imobile/push_notification.rb', line 289

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.



87
88
89
90
91
92
93
94
95
96
# File 'lib/imobile/push_notification.rb', line 87

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.



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

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)


112
113
114
# File 'lib/imobile/push_notification.rb', line 112

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