Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Now handles multiple pem streams to be able to push notifications to …

…multiple applications from only one worker process
  • Loading branch information...
commit e9404728661a6f6d3bd9dbf75f2dd295326019c2 1 parent 352ed47
Thierry Passeron authored
184 README.textile
Source Rendered
... ... @@ -1,11 +1,11 @@
1 1 h1. APNS
2 2
3   -a gem for the Apple Push Notification Service.
  3 +A plugin/Gem for the Apple Push Notification Service.
4 4
5   -The connection to Apple is done as needed and last until it is either closed by the system or is timed out.
  5 +The connections to Apple are done on demand (per process) and last until they are either closed by the system or are timed out.
6 6 This is the prefered way for communicating with Apple's push servers.
7 7
8   -This is tested to work in Rails 3.
  8 +Works in Ruby on Rails 3.
9 9
10 10 h2. Install
11 11
@@ -19,9 +19,9 @@ rails plugin install git://...
19 19
20 20 h2. Setup:
21 21
22   -Convert your certificate
  22 +Convert your certificates
23 23
24   -In Keychain access export your certificate as a p12. Then run the following command to convert it to a .pem
  24 +In Keychain access export your push certificate(s) as a .p12. Then run the following command to convert each .p12 to a .pem
25 25
26 26 <pre>
27 27 <code>
@@ -29,97 +29,49 @@ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
29 29 </code>
30 30 </pre>
31 31
32   -After you have your .pem file. Set what host, port, certificate file location on the APNS class:
  32 +After you have your .pem files, copy them to a place where APNS can access them.
  33 +You will need to register them with APNS before being able to send Notifications
33 34
34   -<pre>
35   - <code>
36   -APNS.host = 'gateway.push.apple.com'
37   -# gateway.sandbox.push.apple.com is default
38   -
39   -APNS.pem = '/path/to/pem/file'
40   -# this is the file you just created
41   -
42   -APNS.port = 2195
43   -# this is also the default. Shouldn't ever have to set this, but just in case Apple goes crazy, you can.
44   - </code>
45   -</pre>
46   -
47   -In a Rails project, you can add an initializer to configure the gem, with for example:
  35 +In a Rails project, you can add an initializer to configure the pem(s), with for example:
48 36
49 37 <pre>
50 38 <code>
51 39 In file ./config/initializers/APNS.rb:
52 40
53 41 # Initialize the APNS environment
54   -
55   -APNS.pem = case Rails.env
56   - when 'development'
57   - Rails.root.join("config", "development.pem")
58   - when 'production'
59   - Rails.root.join("config", "production.pem")
60   -end
  42 +APNS.pem = Rails.root.join("config", Rails.env + ".pem") # => ./config/{development,production}.pem
61 43
62 44 </code>
63 45 </pre>
64 46
65   -h2. Example (Single notification):
  47 +h2. Creating a Payload:
66 48
67   -Sending a push notification is sending a payload to Apple's servers.
  49 +Sending a push notification is sending a Payload to Apple's servers.
68 50
69   -You may create payloads with APNS::Payload.new(<device-token>, <message>)
70   -The payload is composed of a device-token and a message all mixed and encoded together.
  51 +You may create payloads with APNS::Payload.new(<device-token> [,<message>])
  52 +A payload is composed of a device-token and a message all mixed and encoded together.
71 53 Payload message can either just be a alert string or a hash that lets you specify the alert, badge, sound and any custom field.
72 54
73 55 <pre>
74 56 <code>
75 57 device_token = '123abc456def'
76   -
77   -APNS.send_payloads(APNS::Payload.new(device_token, 'Hello iPhone!'))
78   -
79   -APNS.send_payloads(APNS::Payload.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default'))
80   - </code>
81   -</pre>
82   -
83   -h2. Example (Multiple notifications):
84   -
85   -You can also send multiple payloads at once
86   -
87   -<pre>
88   - <code>
89   -device_token = '123abc456def'
90   -
91   -p1 = APNS::Payload.new(device_token, 'Hello iPhone!' )
92   -
  58 +p1 = APNS::Payload.new(device_token, 'Hello iPhone!')
93 59 p2 = APNS::Payload.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default')
  60 +p3 = APNS::Payload.new(device_token).badge(4).alert("Hello iPhone!").sound('bird.aiff')
94 61
95   -p3 = APNS::Payload.new(device_token).alert("Hello from APNS").badge(2).sound("bipbip.aiff")
96   -
97   -APNS.send_payloads([p1, p2, p3])
  62 +# with custom data:
  63 +p4 = APNS::Payload.new(device_token, :badge => 2, :my_custom_field => 'blah')
  64 +p5 = APNS::Payload.new(device_token, :badge => 2).custom(:my_custom_field => 'blah')
98 65 </code>
99 66 </pre>
100 67
101 68
102   -h2. Send other info along with aps
103   -
104   -You can send other application specific information as well.
105   -
106   -<pre>
107   - <code>
108   -APNS.send_payload(APNS::Payload.new(device_token, :alert => 'Hello iPhone!', :badge => 1, :sound => 'default', :sent => 'with apns gem'))
109   - </code>
110   -</pre>
111   -
112   -This will add the :sent key to the same level as the "aps" key:
113   -
114   -<pre>
115   - <code>
116   -{"aps":{"alert":"Hello iPhone!","badge":1,"sound":"default"},"sent":"with apns gem"}
117   - </code>
118   -</pre>
119   -
120 69 h2. Truncating payload informations
121 70
122   -Only valid Payloads will be sent to Apple. A valid payload has a size lesser or equal to 256 bytes.
  71 +Only valid Payloads will be sent to Apple. From APNS point of view, a valid payload has a size lesser or equal to 256 bytes.
  72 +REM: Apple may find a APNS valid payload invalid because it doesn't contain mandatory fields in the message part.
  73 +For instance, a message must at least contain a non empty badge or sound or alert.
  74 +
123 75 You can check whether a payload is valid with the Payload#valid? method.
124 76 In case you know a payload message field is too large and wish to have it truncated you can either use Payload#payload_with_truncated_alert or a more generic Payload#payload_with_truncated_string_at_keypath. These two method will try to truncate the value of the alert field or any custom field at a keypath.
125 77
@@ -128,13 +80,10 @@ Truncate the alert field:
128 80 <pre>
129 81 <code>
130 82 p = APNS::Payload.new("a-device-token", "A very long message "*15)
131   -=> #<APNS::Payload:0x103192ba8 @device=#<APNS::Device:0x103192298 @token="a-device-token">, @message={:alert=>"A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message"}>
132 83 p.valid?
133 84 => false
134 85 p.size
135 86 => 331
136   -p.payload_with_truncated_alert
137   -=> #<APNS::Payload:0x1031d9468 @device=#<APNS::Device:0x1031d91e8 @token="a-device-token">, @message={:alert=>"A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A very long message A..."}>
138 87 p.payload_with_truncated_alert.size
139 88 => 256
140 89 p.payload_with_truncated_alert.valid?
@@ -147,13 +96,10 @@ Truncate a custom field:
147 96 <pre>
148 97 <code>
149 98 p = APNS::Payload.new("a-device-token", :alert => "Hello from APNS", :custom => {:foo => "Bar "*80})
150   -=> #<APNS::Payload:0x10331cb90 @device=#<APNS::Device:0x103481530 @token="a-device-token">, @message={:alert=>"Hello from APNS", :custom=>{:foo=>"Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar "}}>
151 99 p.valid?
152 100 => false
153 101 p.size
154 102 => 387
155   -p.payload_with_truncated_string_at_keypath "custom.foo"
156   -=> #<APNS::Payload:0x1031cbb88 @device=#<APNS::Device:0x1031cb908 @token="a-device-token">, @message={:alert=>"Hello from APNS", :custom=>{:foo=>"Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Bar Ba..."}}>
157 103 p.payload_with_truncated_string_at_keypath("custom.foo").size
158 104 => 256
159 105 p.payload_with_truncated_string_at_keypath("custom.foo").valid?
@@ -162,34 +108,92 @@ p.payload_with_truncated_string_at_keypath("custom.foo").valid?
162 108 </pre>
163 109
164 110
165   -h2. Getting your iPhone's device token
  111 +h2. Sending Notifications to a single application:
166 112
167   -After you setup push notification for your application with Apple. You need to ask Apple for you application specific device token.
  113 +Before sending notifications, you _must_ have setup the pem file(s) so that Apple knows which application you are sending a notification to.
168 114
169   -ApplicationAppDelegate.m
170 115 <pre>
171 116 <code>
172   -- (void)applicationDidFinishLaunching:(UIApplication *)application {
173   - // Register with apple that this app will use push notification
174   - [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];
175   -}
  117 +APNS.pem = "/path/to/my/development.pem"
  118 + </code>
  119 +</pre>
176 120
177   -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
178   - // Show the device token obtained from apple to the log
179   - NSLog(@"deviceToken: %@", deviceToken);
180   -}
  121 +Now we can send some payloads either with:
  122 +
  123 + * APNS.send_payloads(<payloads>)
  124 + * APNS.send(<payloads>) # same as APNS.send_payloads
  125 +
  126 +<pre>
  127 + <code>
  128 +APNS.send(p1, p2, p3)
  129 + </code>
  130 +</pre>
  131 +
  132 +h2. Sending Notifications to multiple applications:
  133 +
  134 +You may want to handle push notifications for many applications at once. In this case you have to setup multiple pem streams:
  135 +
  136 +<pre>
  137 + <code>
  138 +@streams = [:voodoo, :child]
  139 +
  140 +@streams.each do |stream|
  141 + APNS.pem(stream, "/path/to/#{stream}/development.pem"
  142 +end
  143 + </code>
  144 +</pre>
  145 +
  146 +Now you can send the notifications to any stream with:
  147 +
  148 + * APNS.send_stream(<stream>, <payloads>)
  149 +
  150 +<pre>
  151 + <code>
  152 +APNS.send_stream(@streams.first, p1, p2, p3)
  153 +APNS.send_stream(@streams.last, p4, p5)
181 154 </code>
182 155 </pre>
183 156
184 157
185   -h2. Feedback:
  158 +h2. Feedback queue:
186 159
187   -You should check the feedback queue of your application on Apple's servers to avoid sending notifications for obsolete devices
  160 +You should check the feedback queue of your application on Apple's servers to avoid sending notifications to obsolete devices
  161 +
  162 +For single pem:
188 163
189 164 <pre>
190 165 <code>
191 166 APNS.feedback.each do |time, token|
192   - ... do stuff with token
  167 + # remove the device registered with this token ?
  168 +end
  169 + </code>
  170 +</pre>
  171 +
  172 +For multiple pems:
  173 +
  174 +<pre>
  175 + <code>
  176 +APNS.feedback(@streams.first).each do |time, token|
  177 + # remove the device registered with this token ?
193 178 end
194 179 </code>
195 180 </pre>
  181 +
  182 +
  183 +h2. Getting your iPhone's device token
  184 +
  185 +After you setup push notification for your application with Apple. You need to ask Apple for you application specific device token.
  186 +
  187 +In the UIApplicationDelegate
  188 +<pre>
  189 + <code>
  190 +- (void)applicationDidFinishLaunching:(UIApplication *)application {
  191 + [[UIApplication sharedApplication] registerForRemoteNotificationTypes:
  192 + (UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];
  193 +}
  194 +
  195 +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  196 + // Do something with the device token
  197 +}
  198 + </code>
  199 +</pre>
2  lib/apns.rb
... ... @@ -1,3 +1,3 @@
1 1 require 'apns/core'
2 2 require 'apns/device'
3   -require 'apns/payload'
  3 +require 'apns/payload'
79 lib/apns/core.rb
@@ -27,15 +27,26 @@ class PemFileError < RuntimeError;end
27 27 @feedback_port = 2196
28 28
29 29 # openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
30   - @pem = nil # this should be the path of the pem file not the contents
31   - @pass = nil
  30 + @pem = {} # this should be the path of the pem file not the contents
  31 + @pass = {}
32 32
33 33 # Persistent connection
34   - @@ssl = nil
35   - @@sock = nil
  34 + @@ssl = {}
  35 + @@sock = {}
36 36
37 37 class << self
38   - attr_accessor :host, :port, :feedback_host, :feedback_port, :pem, :pass
  38 + attr_accessor :host, :port, :feedback_host, :feedback_port
  39 + def pem(stream = :_global, new_pem = nil)
  40 + @pem[stream] = new_pem if new_pem
  41 + @pem[stream]
  42 + end
  43 + def pem=(new_pem); @pem[:_global] = new_pem; end
  44 +
  45 + def pass(stream = :_global, new_pass = nil)
  46 + @pass[stream] = new_pass if new_pass
  47 + @pass[stream]
  48 + end
  49 + def pass=(new_pass); @pass[:_global] = new_pass; end
39 50 end
40 51
41 52 # send one or many payloads
@@ -55,9 +66,10 @@ class << self
55 66 # or with multiple payloads
56 67 # APNS.send_payloads([payload1, payload2])
57 68
58   - def self.send_payloads(payloads)
59   - # accept Array or single payload
60   - payloads = payloads.is_a?(Array) ? payloads : [payloads]
  69 +
  70 + # Send to a pem stream
  71 + def self.send_stream(stream, *payloads)
  72 + payloads.flatten!
61 73
62 74 # retain valid payloads only
63 75 payloads.reject!{ |p| !(p.is_a?(APNS::Payload) && p.valid?) }
@@ -67,16 +79,17 @@ def self.send_payloads(payloads)
67 79 # loop through each payloads
68 80 payloads.each do |payload|
69 81 retry_delay = 2
  82 +
  83 + # !ToDo! do a better job by using a select to poll the socket for a possible response from apple to inform us about an error in the sent payload
  84 + #
70 85 begin
71   - if @@ssl.nil?
72   - @@sock, @@ssl = self.push_connection
73   - end
74   - @@ssl.write(payload.to_ssl); @@ssl.flush
  86 + @@sock[stream], @@ssl[stream] = self.push_connection(stream) if @@ssl[stream].nil?
  87 + @@ssl[stream].write(payload.to_ssl); @@ssl[stream].flush
75 88 rescue PemPathError, PemFileError => e
76 89 raise e
77 90 rescue
78   - @@ssl.close; @@sock.close
79   - @@ssl = nil; @@sock = nil # cleanup
  91 + @@ssl[stream].close; @@sock[stream].close
  92 + @@ssl[stream] = nil; @@sock[stream] = nil # cleanup
80 93
81 94 retry_delay *= 2
82 95 if retry_delay <= 8
@@ -86,13 +99,21 @@ def self.send_payloads(payloads)
86 99 raise
87 100 end
88 101 end # begin block
89   -
  102 +
90 103 end # each payloads
91 104 end
92 105
93   -
94   - def self.feedback
95   - sock, ssl = self.feedback_connection
  106 + def self.send_payloads(*payloads)
  107 + self.send(payloads)
  108 + end
  109 +
  110 + def self.send(*payloads)
  111 + self.send_stream(:_global, payloads)
  112 + end
  113 +
  114 +
  115 + def self.feedback(stream = :_global)
  116 + sock, ssl = self.feedback_connection(stream)
96 117
97 118 apns_feedback = []
98 119
@@ -111,18 +132,18 @@ def self.feedback
111 132
112 133 protected
113 134
114   - def self.ssl_context
115   - raise PemPathError, "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
116   - raise PemFileError, "The path to your pem file does not exist!" unless File.exist?(self.pem)
  135 + def self.ssl_context(stream = :_global)
  136 + raise PemPathError, "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem(stream)
  137 + raise PemFileError, "The path to your pem file does not exist!" unless File.exist?(self.pem(stream))
117 138
118 139 context = OpenSSL::SSL::SSLContext.new
119   - context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
120   - context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
  140 + context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem(stream)))
  141 + context.key = OpenSSL::PKey::RSA.new(File.read(self.pem(stream)), self.pass(stream))
121 142 context
122 143 end
123 144
124   - def self.connect_to(aps_host, aps_port)
125   - context = self.ssl_context
  145 + def self.connect_to(aps_host, aps_port, stream = :_global)
  146 + context = self.ssl_context(stream)
126 147 sock = TCPSocket.new(aps_host, aps_port)
127 148 ssl = OpenSSL::SSL::SSLSocket.new(sock, context)
128 149 ssl.connect
@@ -130,12 +151,12 @@ def self.connect_to(aps_host, aps_port)
130 151 return sock, ssl
131 152 end
132 153
133   - def self.push_connection
134   - self.connect_to(self.host, self.port)
  154 + def self.push_connection(stream = :_global)
  155 + self.connect_to(self.host, self.port, stream)
135 156 end
136 157
137   - def self.feedback_connection
138   - self.connect_to(self.feedback_host, self.feedback_port)
  158 + def self.feedback_connection(stream = :_global)
  159 + self.connect_to(self.feedback_host, self.feedback_port, stream)
139 160 end
140 161
141 162 end
28 lib/apns/payload.rb
@@ -20,10 +20,28 @@ def initialize(device_token, message_string_or_hash = {})
20 20 self
21 21 end
22 22
  23 +
  24 + # Batch payloads
  25 + # Ex: APNS::Payload.batch(Device.all.collect{|d|d.token}, :alert => "Hello")
  26 + # or with a block
  27 + # APNS::Payload.batch(Device.all.collect{|d|d.token}, :alert => custom_big_alert) do |payload|
  28 + # payload.payload_with_truncated_alert
  29 + # end
  30 + def self.batch(device_tokens, message_string_or_hash = {})
  31 + raise unless device_tokens.is_a?(Array)
  32 + payloads = []
  33 + device_tokens.each do |device|
  34 + payload = self.new(device, message_string_or_hash)
  35 + payload = yield(payload) if block_given?
  36 + payloads << payload
  37 + end
  38 + payloads
  39 + end
  40 +
23 41 #
24 42 # Handy chainable setters
25 43 #
26   - # Ex: APNS::Payload.new(token).badge(3).sound("bipbip").alert("Roadrunner!")
  44 + # Ex: APNS::Payload.new(token).badge(3).sound("bipbip").alert("Roadrunner!").custom(:foo => :bar)
27 45 #
28 46 def badge(number)
29 47 message[:badge] = number
@@ -40,6 +58,13 @@ def alert(string)
40 58 self
41 59 end
42 60
  61 + def custom(hash)
  62 + return nil unless hash.is_a? Hash
  63 + return nil if hash.any?{|k,v| APS_KEYS.include?(k.to_sym) || (k.to_sym == APS_ROOT)}
  64 + message.merge!(hash)
  65 + self
  66 + end
  67 +
43 68
44 69 #
45 70 def to_ssl
@@ -51,6 +76,7 @@ def size
51 76 self.to_ssl.size
52 77 end
53 78
  79 + # Validity checking only checks that the payload size is valid. We do not check the message content.
54 80 def valid?
55 81 self.size <= PAYLOAD_MAX_SIZE
56 82 end

0 comments on commit e940472

Please sign in to comment.
Something went wrong with that request. Please try again.