Skip to content
This repository has been archived by the owner on Jan 29, 2024. It is now read-only.

Error Handling from SendNotifications #28

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
6 changes: 3 additions & 3 deletions MIT-LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Copyright (c) 2009 James Pozdena

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
Expand All @@ -8,10 +8,10 @@ copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Expand Down
20 changes: 10 additions & 10 deletions README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ After you have your .pem file. Set what host, port, certificate file location on

<pre>
<code>
APNS.host = 'gateway.push.apple.com'
APNS.host = 'gateway.push.apple.com'
# gateway.sandbox.push.apple.com is default

APNS.pem = '/path/to/pem/file'
# this is the file you just created
APNS.port = 2195

APNS.port = 2195
# this is also the default. Shouldn't ever have to set this, but just in case Apple goes crazy, you can.
</code>
</pre>
Expand Down Expand Up @@ -58,7 +58,7 @@ You can also send multiple notifications using the same connection to Apple:
n1 = APNS::Notification.new(device_token, 'Hello iPhone!' )

n2 = APNS::Notification.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default')

APNS.send_notifications([n1, n2])
</code>
</pre>
Expand Down Expand Up @@ -92,17 +92,17 @@ h3. ApplicationAppDelegate.m

<pre>
<code>
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
// Register with apple that this app will use push notification
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert |
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert |
UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];

// Your app startup logic...
return YES;
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
// Convert the binary data token into an NSString (see below for the implementation of this function)
NSString *deviceTokenAsString = stringFromDeviceTokenData(deviceToken);
Expand All @@ -122,11 +122,11 @@ This snippet comes from "this stackoverflow post's anwser":http://stackoverflow.
{
const char *data = [deviceToken bytes];
NSMutableString* token = [NSMutableString string];

for (int i = 0; i < [deviceToken length]; i++) {
[token appendFormat:@"%02.2hhX", data[i]];
}

return [[token copy] autorelease];
}
</code>
Expand Down
14 changes: 7 additions & 7 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ require 'rake/gempackagetask'
require 'rubygems/specification'
require 'date'
require 'spec/rake/spectask'

GEM = 'apns'
GEM_NAME = 'apns'
GEM_VERSION = '0.9.0'
AUTHORS = ['James Pozdena']
EMAIL = "[email protected]"
HOMEPAGE = "http://github.com/jpoz/apns"
SUMMARY = "Simple Apple push notification service gem"

spec = Gem::Specification.new do |s|
s.name = GEM
s.version = GEM_VERSION
Expand All @@ -27,24 +27,24 @@ spec = Gem::Specification.new do |s|
s.autorequire = GEM
s.files = %w(MIT-LICENSE README.textile Rakefile) + Dir.glob("{lib}/**/*")
end

task :default => :spec

desc "Run specs"
Spec::Rake::SpecTask.new do |t|
t.spec_files = FileList['spec/**/*_spec.rb']
t.spec_opts = %w(-fs --color)
end

Rake::GemPackageTask.new(spec) do |pkg|
pkg.gem_spec = spec
end

desc "install the gem locally"
task :install => [:package] do
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
end

desc "create a gemspec file"
task :make_spec do
File.open("#{GEM}.gemspec", "w") do |file|
Expand Down
50 changes: 38 additions & 12 deletions lib/apns/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,59 @@ module APNS
require 'openssl'
require 'json'

TIMEOUT = 0.2

@host = 'gateway.sandbox.push.apple.com'
@port = 2195
# openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
@pem = nil # this should be the path of the pem file not the contentes
@pass = nil
@pem_contents # alternative way to specify pem certificate, overrides pem file path

class << self
attr_accessor :host, :pem, :port, :pass
attr_accessor :host, :pem, :port, :pass, :pem_contents
end

def self.send_notification(device_token, message)
n = APNS::Notification.new(device_token, message)
self.send_notifications([n])
end

# improved send_notification to listen on errors
def self.send_notifications(notifications)
error = 0; idx = 0
sock, ssl = self.open_connection

# prepares the messages to be send
notifications.each_with_index{|apns_notf, idx| apns_notf.message_identifier = [idx].pack('N')}

# packs all notifications into a single pack
packed_nofications = self.packed_nofications(notifications)

notifications.each do |n|
ssl.write(packed_nofications)
end
# sends the notifications
ssl.write(packed_nofications)

# if we get and error
if IO.select([ssl], nil, nil, TIMEOUT)

if buffer = ssl.read(6)
_, error_code, idx = buffer.unpack('CCN')
error = error_code.to_i
end
end

ssl.close
sock.close

return error, idx
end

def self.packed_nofications(notifications)
bytes = ''

notifications.each do |notification|
# Each notification frame consists of
# 1. (e.g. protocol version) 2 (unsigned char [1 byte])
# 1. (e.g. protocol version) 2 (unsigned char [1 byte])
# 2. size of the full frame (unsigend int [4 byte], big endian)
pn = notification.packaged_notification
bytes << ([2, pn.bytesize].pack('CN') + pn)
Expand All @@ -63,13 +82,21 @@ def self.feedback

protected

def self.open_connection
def self.get_pem
return self.pem_contents unless self.pem_contents.nil?

raise "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
raise "The path to your pem file does not exist!" unless File.exist?(self.pem)

File.read(self.pem)
end

def self.open_connection
cert_contents = self.get_pem

context = OpenSSL::SSL::SSLContext.new
context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
context.cert = OpenSSL::X509::Certificate.new(cert_contents)
context.key = OpenSSL::PKey::RSA.new(cert_contents, self.pass)

sock = TCPSocket.new(self.host, self.port)
ssl = OpenSSL::SSL::SSLSocket.new(sock,context)
Expand All @@ -79,12 +106,11 @@ def self.open_connection
end

def self.feedback_connection
raise "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
cert_contents = self.get_pem

context = OpenSSL::SSL::SSLContext.new
context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
context.cert = OpenSSL::X509::Certificate.new(cert_contents)
context.key = OpenSSL::PKey::RSA.new(cert_contents, self.pass)

fhost = self.host.gsub('gateway','feedback')
puts fhost
Expand Down
19 changes: 12 additions & 7 deletions lib/apns/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ module APNS
require 'openssl'

class Notification
attr_accessor :device_token, :alert, :badge, :sound, :other, :priority
attr_accessor :device_token, :alert, :badge, :sound, :category, :other, :priority
attr_accessor :message_identifier, :expiration_date
attr_accessor :content_available

attr_accessor :mutable_content

def initialize(device_token, message)
self.device_token = device_token
if message.is_a?(Hash)
self.alert = message[:alert]
self.badge = message[:badge]
self.sound = message[:sound]
self.category = message[:category]
self.other = message[:other]
self.mutable_content = message[:mutable_content]
self.message_identifier = message[:message_identifier]
self.content_available = !message[:content_available].nil?
self.expiration_date = message[:expiration_date]
Expand All @@ -29,7 +32,7 @@ def initialize(device_token, message)

self.message_identifier ||= OpenSSL::Random.random_bytes(4)
end

def packaged_notification
pt = self.packaged_token
pm = self.packaged_message
Expand All @@ -47,23 +50,25 @@ def packaged_notification
data << [3, pi.bytesize, pi].pack("CnA*")
data << [4, 4, pe].pack("CnN")
data << [5, 1, pr].pack("CnC")

data
end

def packaged_token
[device_token.gsub(/[\s|<|>]/,'')].pack('H*')
end

def packaged_message
aps = {'aps'=> {} }
aps['aps']['alert'] = self.alert if self.alert
aps['aps']['badge'] = self.badge if self.badge
aps['aps']['sound'] = self.sound if self.sound
aps['aps']['category'] = self.category if self.category
aps['aps']['content-available'] = 1 if self.content_available
aps['aps']['mutable-content'] = 1 if self.mutable_content

aps.merge!(self.other) if self.other
aps.to_json
end
end
end
end
26 changes: 18 additions & 8 deletions spec/apns/notification_spec.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
require File.dirname(__FILE__) + '/../spec_helper'

describe APNS::Notification do

it "should take a string as the message" do
n = APNS::Notification.new('device_token', 'Hello')
n.alert.should == 'Hello'
end

it "should take a hash as the message" do
n = APNS::Notification.new('device_token', {:alert => 'Hello iPhone', :badge => 3})
n.alert.should == "Hello iPhone"
n.badge.should == 3
end

it "should have a priority if content_availible is set" do
n = APNS::Notification.new('device_token', {:content_available => true})
n.content_available.should be_true
n.priority.should eql(5)
end

describe '#packaged_message' do

it "should return JSON with notification information" do
n = APNS::Notification.new('device_token', {:alert => 'Hello iPhone', :badge => 3, :sound => 'awesome.caf'})
n.packaged_message.should == "{\"aps\":{\"alert\":\"Hello iPhone\",\"badge\":3,\"sound\":\"awesome.caf\"}}"
end


it "should support the iOS 8 category key" do
n = APNS::Notification.new('device_token', {:alert => 'Hello iPhone', :badge => 3, :category => 'CATEGORY_IDENTIFIER'})
n.packaged_message.should == "{\"aps\":{\"alert\":\"Hello iPhone\",\"badge\":3,\"category\":\"CATEGORY_IDENTIFIER\"}}"
end

it "should not include keys that are empty in the JSON" do
n = APNS::Notification.new('device_token', {:badge => 3})
n.packaged_message.should == "{\"aps\":{\"badge\":3}}"
Expand All @@ -35,9 +40,14 @@
n = APNS::Notification.new('device_token', {:content_available => true})
n.packaged_message.should == "{\"aps\":{\"content-available\":1}}"
end


it 'should accept mutable-content key only if mentionned' do
n = APNS::Notification.new('device_token', {:mutable_content => 1})
n.packaged_message.should == "{\"aps\":{\"mutable-content\":1}}"
end

end

describe '#package_token' do
it "should package the token" do
n = APNS::Notification.new('<5b51030d d5bad758 fbad5004 bad35c31 e4e0f550 f77f20d4 f737bf8d 3d5524c6>', 'a')
Expand All @@ -52,5 +62,5 @@
Base64.encode64(n.packaged_notification).should == "AQAG3vLO/YTnAgBAeyJhcHMiOnsiYWxlcnQiOiJIZWxsbyBpUGhvbmUiLCJi\nYWRnZSI6Mywic291bmQiOiJhd2Vzb21lLmNhZiJ9fQMABGFhYWEEAAQAAAAA\nBQABCg==\n"
end
end

end