eq-apns 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+