eq-apns 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/MIT-LICENSE +22 -0
  2. data/README.textile +153 -0
  3. data/Rakefile +53 -0
  4. data/lib/apns.rb +24 -0
  5. data/lib/apns/core.rb +242 -0
  6. metadata +68 -0
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 James Pozdena, 2010 Justin.tv
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,153 @@
1
+ h1. JTV-APNS
2
+
3
+ Jtv-apns is a gem for accessing the Apple Push Notification Service that allows
4
+ both sending notifications and reading from apple's feedback service. This gem
5
+ is based heavily on the work of James Pozdena (http://github.com/jpoz/APNS).
6
+
7
+ h2. Install
8
+
9
+ <pre>
10
+ sudo gem install jtv-apns
11
+ </pre>
12
+
13
+ h2. Setup:
14
+
15
+ First, you will need to export your development/production iphone push service
16
+ certificate and key from your keychain. To do this select the private key and
17
+ certificate, and select File -> Export from your keychain. By default a .p12
18
+ file will be generated containing certificate and key.
19
+
20
+ Next, run the following command to convert this .p12 file into a .pem file.
21
+ <pre>
22
+ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
23
+ </pre>
24
+
25
+ This pem file should be stored somewhere secure that your application can access. Next, set the jtv-apns configuration parameters:
26
+
27
+ <pre>
28
+ ###################
29
+ # Hosts Config
30
+ ###################
31
+
32
+ # Push Notification Service:
33
+ #
34
+ # (default: gateway.sandbox.push.apple.com is)
35
+ # Set as below for a production install
36
+ APNS.host = 'gateway.push.apple.com'
37
+
38
+ # (default: 2195)
39
+ # APNS.port = 2195
40
+
41
+ # Feedback Service:
42
+ #
43
+ # (default: feedback.sandbox.push.apple.com)
44
+ APNS.feedback_host = 'feedback.push.apple.com'
45
+
46
+ # (default: 2196)
47
+ # APNS.feedback_port = 2196
48
+
49
+ ####################
50
+ # Certificate Setup
51
+ ####################
52
+
53
+ # Path to the .pem file created earlier
54
+ APNS.pem = '/path/to/pem/file'
55
+
56
+ # Password for decrypting the .pem file, if one was used
57
+ APNS.pass = 'xxxx'
58
+
59
+ ####################
60
+ # Connection Mgmt
61
+ ####################
62
+
63
+ # Cache open connections when sending push notifications
64
+ # this will force the gem to keep 1 connection open per
65
+ # host/port pair, and reuse it when sending notifications
66
+
67
+ # (default: false)
68
+ # APNS.cache_connections = true
69
+ </pre>
70
+
71
+ h2. Example (Single notification):
72
+
73
+ Then to send a push notification you can either just send a string as the alert or give it a hash for the alert, badge and sound.
74
+
75
+ <pre>
76
+ device_token = '123abc456def'
77
+
78
+ APNS.send_notification(device_token, 'Hello iPhone!')
79
+ APNS.send_notification(device_token, :aps => {:alert => 'Hello iPhone!', :badge => 1, :sound => 'default'})
80
+ </pre>
81
+
82
+ h2. Example (Multiple notifications):
83
+
84
+ You can also send multiple notifications using the same connection to Apple:
85
+
86
+ <pre>
87
+ device_token = '123abc456def'
88
+
89
+ n1 = [device_token, :aps => { :alert => 'Hello...', :badge => 1, :sound => 'default' }
90
+ n2 = [device_token, :aps => { :alert => '... iPhone!', :badge => 1, :sound => 'default' }]
91
+
92
+ APNS.send_notifications([n1, n2])
93
+ </pre>
94
+
95
+
96
+ h2. Send other info along with aps
97
+
98
+ Application-specific information can be included along with the alert by
99
+ passing it along with the "aps" key in the message hash.
100
+
101
+ <pre>
102
+ APNS.send_notification(device_token, :aps => { :alert => 'Hello iPhone!', :badge => 1, :sound => 'default'},
103
+ :sent_by => 'Justin.tv')
104
+ </pre>
105
+
106
+ h2. Pre-establishing connections
107
+
108
+ If connection caching is enabled, you can tell the gem to establish connections before sending any notifications.
109
+
110
+ <pre>
111
+ APNS.establish_notification_connection
112
+ # ...
113
+ if APNS.has_notification_connection?
114
+ APNS.send_notification(device_token, "It works!")
115
+ end
116
+ </pre>
117
+
118
+ h2. Accessing the feedback service
119
+
120
+ jtv-apns provides a simple api to access Apple's feedback service. Below is an example for setting the feedback time on an ActiveRecord object corresponding to a device token.
121
+
122
+ <pre>
123
+ # APNS.feedback_each returns an array of Hash objects with the following keys
124
+ # :feedback_on => (Time) Time Apple considers app unregistered from device
125
+ # :length => (Fixnum) Length of :device_token, currently always 32 (bytes)
126
+ # :device_token => (String) hex-encoded device token
127
+ APNS.feedback.each do |feedback|
128
+ d = RegisteredDevices.find(:first, :conditions => { :device_token = feedback.device_token })
129
+ unless d.nil?
130
+ d.feedback_on = feedback.feedback_on
131
+ end
132
+ end
133
+ </pre>
134
+
135
+ h2. Getting your iPhone's device token
136
+
137
+ After you setup push notification for your application with Apple. You need to ask Apple for you application specific device token.
138
+
139
+ ApplicationAppDelegate.m
140
+ <pre>
141
+ - (void)applicationDidFinishLaunching:(UIApplication *)application {
142
+ // Register with apple that this app will use push notification
143
+ [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert |
144
+ UIRemoteNotificationTypeSound | UIRemoteNotificationTypeBadge)];
145
+ }
146
+
147
+ - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
148
+ // Show the device token obtained from apple to the log
149
+ NSLog(@"deviceToken: %@", deviceToken);
150
+ }
151
+ </pre>
152
+
153
+
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+
7
+ GEM = 'apns'
8
+ GEM_NAME = 'apns'
9
+ GEM_VERSION = '0.9.0'
10
+ AUTHORS = ['James Pozdena']
11
+ EMAIL = "jpoz@jpoz.net"
12
+ HOMEPAGE = "http://github.com/jpoz/apns"
13
+ SUMMARY = "Simple Apple push notification service gem"
14
+
15
+ spec = Gem::Specification.new do |s|
16
+ s.name = GEM
17
+ s.version = GEM_VERSION
18
+ s.platform = Gem::Platform::RUBY
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["MIT-LICENSE"]
21
+ s.summary = SUMMARY
22
+ s.description = s.summary
23
+ s.authors = AUTHORS
24
+ s.email = EMAIL
25
+ s.homepage = HOMEPAGE
26
+ s.require_path = 'lib'
27
+ s.autorequire = GEM
28
+ s.files = %w(MIT-LICENSE README.textile Rakefile) + Dir.glob("{lib}/**/*")
29
+ end
30
+
31
+ task :default => :spec
32
+
33
+ desc "Run specs"
34
+ Spec::Rake::SpecTask.new do |t|
35
+ t.spec_files = FileList['spec/**/*_spec.rb']
36
+ t.spec_opts = %w(-fs --color)
37
+ end
38
+
39
+ Rake::GemPackageTask.new(spec) do |pkg|
40
+ pkg.gem_spec = spec
41
+ end
42
+
43
+ desc "install the gem locally"
44
+ task :install => [:package] do
45
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
46
+ end
47
+
48
+ desc "create a gemspec file"
49
+ task :make_spec do
50
+ File.open("#{GEM}.gemspec", "w") do |file|
51
+ file.puts spec.to_ruby
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ # Copyright (c) 2009 James Pozdena, 2010 Justin.tv
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'apns/core'
@@ -0,0 +1,242 @@
1
+ # Copyright (c) 2009 James Pozdena, 2010 Justin.tv
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module APNS
25
+ require 'socket'
26
+ require 'openssl'
27
+ require 'json'
28
+
29
+ # Host for push notification service
30
+ # production: gateway.push.apple.com
31
+ # development: gateway.sandbox.apple.com
32
+ @host = 'gateway.sandbox.push.apple.com'
33
+ @port = 2195
34
+
35
+ # Host for feedback service
36
+ # production: feedback.push.apple.com
37
+ # development: feedback.sandbox.apple.com
38
+ @feedback_host = 'feedback.sandbox.push.apple.com'
39
+ @feedback_port = 2196
40
+
41
+ # openssl pkcs12 -in mycert.p12 -out client-cert.pem -nodes -clcerts
42
+ @pem = nil # this should be the contents of the pem file not the path
43
+ @pass = nil
44
+
45
+ @max_connection_age = 60 * 60 # 1 hour
46
+ @cache_connections = false
47
+ @connections = {}
48
+
49
+
50
+
51
+ class << self
52
+ attr_accessor :host, :port, :feedback_host, :feedback_port, :pem, :pass, :cache_connections, :max_connection_age
53
+ end
54
+
55
+ def self.establish_notification_connection
56
+ if @cache_connections
57
+ begin
58
+ self.get_connection(self.host, self.port, self.pem)
59
+ return true
60
+ rescue
61
+ end
62
+ end
63
+ return false
64
+ end
65
+
66
+ def self.has_notification_connection?
67
+ return self.has_connection?(self.host, self.port, self.pem)
68
+ end
69
+
70
+ def self.send_notification(device_token, message)
71
+ self.with_notification_connection do |conn|
72
+ conn.write(self.packaged_notification(device_token, message))
73
+ conn.flush
74
+ end
75
+ end
76
+
77
+ def self.send_notifications(notifications)
78
+ self.with_notification_connection do |conn|
79
+ notifications.each do |n|
80
+ conn.write(self.packaged_notification(n[0], n[1]))
81
+ end
82
+ conn.flush
83
+ end
84
+ end
85
+
86
+ def self.feedback
87
+ apns_feedback = []
88
+ self.with_feedback_connection do |conn|
89
+ # Read buffers data from the OS, so it's probably not
90
+ # too inefficient to do the small reads
91
+ while data = conn.read(38)
92
+ apns_feedback << self.parse_feedback_tuple(data)
93
+ end
94
+ end
95
+
96
+ return apns_feedback
97
+ end
98
+
99
+ protected
100
+
101
+ # Each tuple is in the following format:
102
+ #
103
+ # timestamp | token_length (32) | token
104
+ # bytes: 4 (big-endian) 2 (big-endian) | 32
105
+ #
106
+ # timestamp - seconds since the epoch, in UTC
107
+ # token_length - Always 32 for now
108
+ # token - 32 bytes of binary data specifying the device token
109
+ #
110
+ def self.parse_feedback_tuple(data)
111
+ feedback = data.unpack('N1n1H64')
112
+ {:feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] }
113
+ end
114
+
115
+ def self.packaged_notification(device_token, message)
116
+ pt = self.packaged_token(device_token)
117
+ pm = self.packaged_message(message)
118
+ [0, 0, 32, pt, 0, pm.size, pm].pack("ccca*cca*")
119
+ end
120
+
121
+ def self.packaged_token(device_token)
122
+ [device_token.gsub(/[\s|<|>]/,'')].pack('H*')
123
+ end
124
+
125
+ def self.packaged_message(message)
126
+ if message.is_a?(Hash)
127
+ message.to_json
128
+ elsif message.is_a?(String)
129
+ '{"aps":{"alert":"'+ message + '"}}'
130
+ else
131
+ raise "Message needs to be either a hash or string"
132
+ end
133
+ end
134
+
135
+ def self.with_notification_connection(&block)
136
+ self.with_connection(self.host, self.port, self.pem, &block)
137
+ end
138
+
139
+ def self.with_feedback_connection(&block)
140
+ # Explicitly disable the connection cache for feedback
141
+ cache_temp = @cache_connections
142
+ @cache_connections = false
143
+
144
+ self.with_connection(self.feedback_host, self.feedback_port, self.pem, &block)
145
+
146
+ ensure
147
+ @cache_connections = cache_temp
148
+ end
149
+
150
+ private
151
+
152
+ def self.open_connection(host, port, pem)
153
+ raise "The PEM data is not set" unless self.pem
154
+
155
+ context = OpenSSL::SSL::SSLContext.new
156
+ context.cert = OpenSSL::X509::Certificate.new(pem)
157
+ context.key = OpenSSL::PKey::RSA.new(pem, self.pass)
158
+
159
+ retries = 0
160
+ begin
161
+ sock = TCPSocket.new(host, port)
162
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, context)
163
+ ssl.connect
164
+ return ssl, sock
165
+ rescue SystemCallError
166
+ if (retries += 1) < 5
167
+ sleep 1
168
+ retry
169
+ else
170
+ # Too many retries, re-raise this exception
171
+ raise
172
+ end
173
+ end
174
+ end
175
+
176
+ def self.has_connection?(host, port, pem)
177
+ @connections.has_key?([host,port, pem])
178
+ end
179
+
180
+ def self.create_connection(host, port, pem)
181
+ ssl, sock = self.open_connection(host, port, pem)
182
+ @connections[[host, port, pem]] = [ssl, sock, Time.now]
183
+ end
184
+
185
+ def self.find_connection(host, port, pem)
186
+ @connections[[host, port, pem]]
187
+ end
188
+
189
+ def self.remove_connection(host, port, pem)
190
+ if self.has_connection?(host, port, pem)
191
+ ssl, sock = @connections.delete([host, port, pem])
192
+ ssl.close
193
+ sock.close
194
+ end
195
+ end
196
+
197
+ def self.reconnect_connection(host, port, pem)
198
+ self.remove_connection(host, port, pem)
199
+ self.create_connection(host, port, pem)
200
+ end
201
+
202
+ def self.get_connection(host, port, pem)
203
+ if @cache_connections
204
+ # Create a new connection if we don't have one
205
+ unless self.has_connection?(host, port, pem)
206
+ self.create_connection(host, port, pem)
207
+ end
208
+
209
+ ssl, sock, connection_opened_at = self.find_connection(host, port, pem)
210
+ # If we're closed, reconnect
211
+ if ssl.closed? || Time.now - connection_opened_at > @max_connection_age
212
+ self.reconnect_connection(host, port, pem)
213
+ self.find_connection(host, port, pem)
214
+ else
215
+ return [ssl, sock]
216
+ end
217
+ else
218
+ self.open_connection(host, port, pem)
219
+ end
220
+ end
221
+
222
+ def self.with_connection(host, port, pem, &block)
223
+ retries = 0
224
+ begin
225
+ ssl, sock = self.get_connection(host, port, pem)
226
+ yield ssl if block_given?
227
+
228
+ unless @cache_connections
229
+ ssl.close
230
+ sock.close
231
+ end
232
+ rescue Errno::ECONNABORTED, Errno::EPIPE, Errno::ECONNRESET
233
+ if (retries += 1) < 5
234
+ self.remove_connection(host, port, pem)
235
+ retry
236
+ else
237
+ # too-many retries, re-raise
238
+ raise
239
+ end
240
+ end
241
+ end
242
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eq-apns
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Eric Mason
13
+ - Paul Gebheim
14
+ - James Pozdena
15
+ autorequire: apns
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2013-02-21 00:00:00 -05:00
20
+ default_executable:
21
+ dependencies: []
22
+
23
+ description: Simple Apple push notification service gem
24
+ email: eric@equisolve.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files:
30
+ - MIT-LICENSE
31
+ files:
32
+ - MIT-LICENSE
33
+ - README.textile
34
+ - Rakefile
35
+ - lib/apns/core.rb
36
+ - lib/apns.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/ericmason/APNS
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.6
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Simple Apple push notification service gem
67
+ test_files: []
68
+