dr-apns 0.1.3

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/core.rb +239 -0
  5. data/lib/apns.rb +24 -0
  6. metadata +50 -0
data/MIT-LICENSE ADDED
@@ -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.
data/README.textile ADDED
@@ -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
+
data/Rakefile ADDED
@@ -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
data/lib/apns/core.rb ADDED
@@ -0,0 +1,239 @@
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 path of the pem file not the contentes
43
+ @pass = nil
44
+
45
+ @cache_connections = false
46
+ @connections = {}
47
+
48
+ class << self
49
+ attr_accessor :host, :port, :feedback_host, :feedback_port, :pem, :pass, :cache_connections
50
+ end
51
+
52
+ def self.establish_notification_connection
53
+ if @cache_connections
54
+ begin
55
+ self.get_connection(self.host, self.port, self.pem)
56
+ return true
57
+ rescue
58
+ end
59
+ end
60
+ return false
61
+ end
62
+
63
+ def self.has_notification_connection?
64
+ return self.has_connection?(self.host, self.port)
65
+ end
66
+
67
+ def self.send_notification(device_token, message)
68
+ self.with_notification_connection do |conn|
69
+ conn.write(self.packaged_notification(device_token, message))
70
+ conn.flush
71
+ end
72
+ end
73
+
74
+ def self.send_notifications(notifications)
75
+ self.with_notification_connection do |conn|
76
+ notifications.each do |n|
77
+ conn.write(self.packaged_notification(n[0], n[1]))
78
+ end
79
+ conn.flush
80
+ end
81
+ end
82
+
83
+ def self.feedback
84
+ apns_feedback = []
85
+ self.with_feedback_connection do |conn|
86
+ # Read buffers data from the OS, so it's probably not
87
+ # too inefficient to do the small reads
88
+ while data = conn.read(38)
89
+ apns_feedback << self.parse_feedback_tuple(data)
90
+ end
91
+ end
92
+
93
+ return apns_feedback
94
+ end
95
+
96
+ protected
97
+
98
+ # Each tuple is in the following format:
99
+ #
100
+ # timestamp | token_length (32) | token
101
+ # bytes: 4 (big-endian) 2 (big-endian) | 32
102
+ #
103
+ # timestamp - seconds since the epoch, in UTC
104
+ # token_length - Always 32 for now
105
+ # token - 32 bytes of binary data specifying the device token
106
+ #
107
+ def self.parse_feedback_tuple(data)
108
+ feedback = data.unpack('N1n1H64')
109
+ {:feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] }
110
+ end
111
+
112
+ def self.packaged_notification(device_token, message)
113
+ pt = self.packaged_token(device_token)
114
+ pm = self.packaged_message(message)
115
+ [0, 0, 32, pt, 0, pm.size, pm].pack("ccca*cca*")
116
+ end
117
+
118
+ def self.packaged_token(device_token)
119
+ [device_token.gsub(/[\s|<|>]/,'')].pack('H*')
120
+ end
121
+
122
+ def self.packaged_message(message)
123
+ if message.is_a?(Hash)
124
+ message.to_json
125
+ elsif message.is_a?(String)
126
+ '{"aps":{"alert":"'+ message + '"}}'
127
+ else
128
+ raise "Message needs to be either a hash or string"
129
+ end
130
+ end
131
+
132
+ def self.with_notification_connection(&block)
133
+ self.with_connection(self.host, self.port, self.pem, &block)
134
+ end
135
+
136
+ def self.with_feedback_connection(&block)
137
+ # Explicitly disable the connection cache for feedback
138
+ cache_temp = @cache_connections
139
+ @cache_connections = false
140
+
141
+ self.with_connection(self.feedback_host, self.feedback_port, self.pem, &block)
142
+
143
+ ensure
144
+ @cache_connections = cache_temp
145
+ end
146
+
147
+ private
148
+
149
+ def self.open_connection(host, port, pem)
150
+ raise "The path to your pem file is not set. (APNS.pem = /path/to/cert.pem)" unless self.pem
151
+ raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
152
+
153
+ context = OpenSSL::SSL::SSLContext.new
154
+ context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
155
+ context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.pass)
156
+
157
+ retries = 0
158
+ begin
159
+ sock = TCPSocket.new(host, port)
160
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, context)
161
+ ssl.connect
162
+ return ssl, sock
163
+ rescue SystemCallError
164
+ if (retries += 1) < 5
165
+ sleep 1
166
+ retry
167
+ else
168
+ # Too many retries, re-raise this exception
169
+ raise
170
+ end
171
+ end
172
+ end
173
+
174
+ def self.has_connection?(host, port, pem)
175
+ @connections.has_key?([host,port,pem])
176
+ end
177
+
178
+ def self.create_connection(host, port, pem)
179
+ @connections[[host, port, pem]] = self.open_connection(host, port, pem)
180
+ end
181
+
182
+ def self.find_connection(host, port, pem)
183
+ @connections[[host, port, pem]]
184
+ end
185
+
186
+ def self.remove_connection(host, port, pem)
187
+ if self.has_connection?(host, port, pem)
188
+ ssl, sock = @connections.delete([host, port, pem])
189
+ ssl.close
190
+ sock.close
191
+ end
192
+ end
193
+
194
+ def self.reconnect_connection(host, port, pem)
195
+ self.remove_connection(host, port, pem)
196
+ self.create_connection(host, port, pem)
197
+ end
198
+
199
+ def self.get_connection(host, port, pem)
200
+ if @cache_connections
201
+ # Create a new connection if we don't have one
202
+ unless self.has_connection?(host, port, pem)
203
+ self.create_connection(host, port, pem)
204
+ end
205
+
206
+ ssl, sock = self.find_connection(host, port, pem)
207
+ # If we're closed, reconnect
208
+ if ssl.closed?
209
+ self.reconnect_connection(host, port, pem)
210
+ self.find_connection(host, port, pem)
211
+ else
212
+ return [ssl, sock]
213
+ end
214
+ else
215
+ self.open_connection(host, port, pem)
216
+ end
217
+ end
218
+
219
+ def self.with_connection(host, port, pem, &block)
220
+ retries = 0
221
+ begin
222
+ ssl, sock = self.get_connection(host, port, pem)
223
+ yield ssl if block_given?
224
+
225
+ unless @cache_connections
226
+ ssl.close
227
+ sock.close
228
+ end
229
+ rescue Errno::ECONNABORTED, Errno::EPIPE, Errno::ECONNRESET
230
+ if (retries += 1) < 5
231
+ self.remove_connection(host, port, pem)
232
+ retry
233
+ else
234
+ # too-many retries, re-raise
235
+ raise
236
+ end
237
+ end
238
+ end
239
+ end
data/lib/apns.rb ADDED
@@ -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'
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dr-apns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dongri Jin
9
+ autorequire: apns
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-10 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Simple Apple push notification service gem
15
+ email: dongriab@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - MIT-LICENSE
20
+ files:
21
+ - MIT-LICENSE
22
+ - README.textile
23
+ - Rakefile
24
+ - lib/apns/core.rb
25
+ - lib/apns.rb
26
+ homepage: http://github.com/dongriab/APNS
27
+ licenses: []
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.24
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: Simple Apple push notification service gem
50
+ test_files: []