sentia_apn_sender 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Kali Donovan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,148 @@
1
+ == Sentia Modifications
2
+
3
+ This great gem was originally created by Kali Donovan. The project has been modified by Sentia to provide the ability to use custom resque queues and to allow sending via multiple certificates in the same application.
4
+
5
+ == Synopsis
6
+
7
+ Need to send background notifications to an iPhone application over a <em>persistent</em> connection in Ruby? Keep reading...
8
+
9
+ == The Story
10
+
11
+ So you're building the server component of an iPhone application in Ruby. And you want to send background notifications through the Apple Push Notification servers, which doesn't seem too bad at first. But then you read in the {Apple Documentation}[https://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/WhatAreRemoteNotif/WhatAreRemoteNotif.html#//apple_ref/doc/uid/TP40008194-CH102-SW7] that Apple's servers may treat non-persistent connections as a Denial of Service attack, and you realize that Rails has no easy way to maintain a persistent connection internally, and things started looking more complicated.
12
+
13
+ The apn_sender gem includes a background daemon which processes background messages from your application and sends them along to Apple <em>over a single, persistent socket</em>. It also includes the ability to query the Feedback service, helper methods for enqueueing your jobs, and a sample monit config to make sure the background worker is around when you need it.
14
+
15
+ == Yet another ApplePushNotification interface?
16
+
17
+ Yup. There's some great code out there already, but we didn't like the idea of getting banned from the APN gateway for establishing a new connection each time we needed to send a batch of messages, and none of the libraries I found handled maintaining a persistent connection.
18
+
19
+ == Current Status
20
+
21
+ This gem has been in production use since early May, 2010. There are no guarantees of complete functionality, of course, but it's working for us. :)
22
+
23
+
24
+ == Usage
25
+
26
+ === 1. Setup
27
+
28
+ You must set the queue name to be used in an initializer, the name you use must be in the format `apn_#{appname}_#{environment}`
29
+
30
+ Here is an example for an application named beans
31
+
32
+ if Rails.env.production?
33
+ APN::Config.queue_name = "apn_beans_production"
34
+ else
35
+ APN::Config.queue_name = "apn_beans_development"
36
+ end
37
+
38
+
39
+ === 2. Queueing Messages From Your Application
40
+
41
+ To queue a message for sending through Apple's Push Notification service from your Rails application:
42
+
43
+ APN.notify(token, opts_hash)
44
+
45
+ where +token+ is the unique identifier of the iPhone to receive the notification and +opts_hash+ can have any of the following keys:
46
+
47
+ # :alert #=> The alert to send
48
+ # :badge #=> The badge number to send
49
+ # :sound #=> The sound file to play on receipt, or true to play the default sound installed with your app
50
+
51
+ If any other keys are present they'll be be passed along as custom data to your application.
52
+
53
+ === 3. Sending Queued Messages
54
+
55
+ Put your <code>apn_development.pem</code> and <code>apn_production.pem</code> certificates from Apple in your <code>RAILS_ROOT/config/certs/APPNAME</code> directory.
56
+
57
+ Once this is done, you can fire off a background worker with
58
+
59
+ $ rake apn:sender
60
+
61
+ For production, you're probably better off running a dedicated daemon and setting up monit to watch over it for you. Luckily, that's pretty easy:
62
+
63
+ # To generate daemon
64
+ ./script/generate apn_sender
65
+
66
+ # To run daemon. Pass --help to print all options
67
+ ./script/apn_sender --environment=production --verbose --application appname start
68
+
69
+ Note the --environment must be explicitly set (separately from your <code>RAILS_ENV</code>) to production in order to send messages via the production APN servers. Any other environment sends messages through Apple's sandbox servers at <code>gateway.sandbox.push.apple.com</code>.
70
+
71
+ Check <code>RAILS_ROOT/logs/apn_sender.log</code> for debugging output. In addition to logging any major errors there, apn_sender hooks into the Resque::Worker logging to display any verbose or very_verbose worker output in apn_sender.log file as well.
72
+
73
+
74
+ === 3. Checking Apple's Feedback Service
75
+
76
+ Since push notifications are a fire-and-forget sorta deal, where you get no indication if your message was received (or if the specified recipient even exists), Apple needed to come up with some other way to ensure their network isn't clogged with thousands of bogus messages (e.g. from developers sending messages to phones where their application <em>used</em> to be installed, but where the user has since removed it). Hence, the Feedback Service.
77
+
78
+ It's actually really simple - you connect to them periodically and they give you a big dump of tokens you shouldn't send to anymore. The gem wraps this up nicely -- just call:
79
+
80
+ # APN::Feedback accepts the same optional :environment and :cert_path options as APN::Sender
81
+ feedback = APN::Feedback.new()
82
+
83
+ tokens = feedback.tokens # => Array of device tokens
84
+ tokens.each do |token|
85
+ # ... custom logic here to stop you app from
86
+ # sending further notifications to this token
87
+ end
88
+
89
+ If you're interested in knowing exactly <em>when</em> Apple determined each token was expired (which can be useful in determining if the application re-registered with your service since it first appeared in the expired queue):
90
+
91
+ items = feedback.data # => Array of APN::FeedbackItem elements
92
+ items.each do |item|
93
+ item.token
94
+ item.timestamp
95
+ # ... custom logic here
96
+ end
97
+
98
+ The Feedback Service works as a big queue. When you connect it pops off all its data and sends it over the wire at once, which means connecting a second time will return an empty array, so for ease of use a call to either +tokens+ or +data+ will connect once and cache the data. If you call either one again it'll continue to use its cached version (rather than connecting to Apple a second time to retrieve an empty array, which is probably not what you want).
99
+
100
+ Forcing a reconnect is as easy as calling either method with the single parameter +true+, but be sure you've already used the existing data because you'll never get it back.
101
+
102
+
103
+ ==== Warning: No really, check Apple's Feedback Service occasionally
104
+
105
+ If you're sending notifications, you should definitely call one of the <code>receive</code> methods periodically, as Apple's policies require it and they apparently monitors providers for compliance. I'd definitely recommend throwing together a quick rake task to take care of this for you (the {whenever library}[http://github.com/javan/whenever] provides a nice wrapper around scheduling tasks to run at certain times (for systems with cron enabled)).
106
+
107
+ Just for the record, this is essentially what you want to have whenever run periodically for you:
108
+
109
+ def self.clear_uninstalled_applications
110
+ feedback_data = APN::Feedback.new(:environment => :production).data
111
+
112
+ feedback_data.each do |item|
113
+ user = User.find_by_iphone_token( item.token )
114
+
115
+ if user.iphone_token_updated_at && user.iphone_token_updated_at > item.timestamp
116
+ return true # App has been reregistered since Apple determined it'd been uninstalled
117
+ else
118
+ user.update_attributes(:iphone_token => nil, :iphone_token_updated_at => Time.now)
119
+ end
120
+ end
121
+ end
122
+
123
+
124
+
125
+
126
+ === Keeping Your Workers Working
127
+
128
+ There's also an included sample <code>apn_sender.monitrc</code> file in the <code>contrib/</code> folder to help monit handle server restarts and unexpected disasters.
129
+
130
+
131
+ == Installation
132
+
133
+ APN is built on top of {Resque}[http://github.com/defunkt/resque] (an awesome {Redis}[http://code.google.com/p/redis/]-based background runner similar to {delayed_job}[http://github.com/collectiveidea/delayed_job]). Read through the {Resque README}[http://github.com/defunkt/resque#readme] to get a feel for what's going on, follow the installation instructions there, and then run:
134
+
135
+ $ sudo gem install apn_sender
136
+
137
+ In your Rails app, add
138
+
139
+ config.gem 'apn_sender', :lib => 'apn'
140
+
141
+ To add a few useful rake tasks for running workers, add the following line to your Rakefile:
142
+
143
+ require 'apn/tasks'
144
+
145
+
146
+ == Copyright
147
+
148
+ Copyright (c) 2010 Kali Donovan. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ load 'lib/apn/tasks.rb'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "sentia_apn_sender"
10
+ gem.summary = %Q{Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Original gem by Kali Donovan and modified by Sentia.}
11
+ gem.description = %Q{Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper methods for enqueueing APN notifications, and a background daemon to send them. Original gem by Kali Donovan and modified by Sentia.}
12
+ gem.email = "mario.visic@sentia.com.au"
13
+ gem.homepage = 'https://github.com/sentia/apn_sender'
14
+ gem.authors = ['Kali Donovan', 'Mario Visic']
15
+ gem.add_dependency 'resque'
16
+ gem.add_dependency 'resque-access_worker_from_job'
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rdoc/task'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "apple_push_notification #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.4
@@ -0,0 +1,22 @@
1
+ # An example Monit configuration file for running the apn_sender background daemon
2
+ #
3
+ # 1. Replace #{app_name} with your application name
4
+ # 2. Add any arguments between apn_sender the and start/stop command
5
+ # 3. Install as a monitrc file (paste to bottom of /etc/monit/monitrc, or save as a .monitrc file and include in the main config)
6
+
7
+ check process redis
8
+ with pidfile /var/run/redis.pid
9
+ group apn_sender
10
+ start program = "/usr/bin/redis-server /etc/redis/redis.conf"
11
+ stop program = "/bin/echo SHUTDOWN | nc localhost 6379"
12
+ if failed host 127.0.0.1 port 6379 then restart
13
+ if 5 restarts within 5 cycles then timeout
14
+
15
+
16
+ check process apn_sender
17
+ with pidfile /var/www/#{app_name}/shared/pids/apn_sender.pid
18
+ group apn_sender
19
+ start program = "/var/www/#{app_name}/current/script/apn_sender --environment=production --verbose start"
20
+ stop program = "/var/www/{#app_name}/current/script/apn_sender --environment=production --verbose stop"
21
+ if 2 restarts within 3 cycles then timeout
22
+ depends_on redis
@@ -0,0 +1,9 @@
1
+ class ApnSenderGenerator < Rails::Generator::Base
2
+
3
+ def manifest
4
+ record do |m|
5
+ m.template 'script', 'script/apn_sender', :chmod => 0755
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Daemons sets pwd to /, so we have to explicitly set RAILS_ROOT
4
+ RAILS_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
5
+
6
+ require 'rubygems'
7
+ require 'apn'
8
+ require 'apn/sender_daemon'
9
+
10
+ APN::SenderDaemon.new(ARGV).daemonize
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init.rb"
@@ -0,0 +1,121 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+ require 'resque'
4
+
5
+ module APN
6
+ module Connection
7
+ # APN::Connection::Base takes care of all the boring certificate loading, socket creating, and logging
8
+ # responsibilities so APN::Sender and APN::Feedback and focus on their respective specialties.
9
+ module Base
10
+ attr_accessor :opts, :logger
11
+
12
+ def initialize(opts = {})
13
+ @opts = opts
14
+
15
+ setup_logger
16
+ log(:info, "APN::Sender initializing. Establishing connections first...") if @opts[:verbose]
17
+ setup_paths
18
+
19
+ super( "apn_" + @opts[:app] + "_" + @opts[:environment] ) if self.class.ancestors.include?(Resque::Worker)
20
+ end
21
+
22
+ # Lazy-connect the socket once we try to access it in some way
23
+ def socket
24
+ setup_connection unless @socket
25
+ return @socket
26
+ end
27
+
28
+ protected
29
+
30
+ # Default to Rails or Merg logger, if available
31
+ def setup_logger
32
+ @logger = if defined?(Merb::Logger)
33
+ Merb.logger
34
+ elsif defined?(RAILS_DEFAULT_LOGGER)
35
+ RAILS_DEFAULT_LOGGER
36
+ end
37
+ end
38
+
39
+ # Log message to any logger provided by the user (e.g. the Rails logger).
40
+ # Accepts +log_level+, +message+, since that seems to make the most sense,
41
+ # and just +message+, to be compatible with Resque's log method and to enable
42
+ # sending verbose and very_verbose worker messages to e.g. the rails logger.#
43
+ #
44
+ # Perhaps a method definition of +message, +level+ would make more sense, but
45
+ # that's also the complete opposite of what anyone comming from rails would expect.
46
+ alias_method(:resque_log, :log) if defined?(log)
47
+ def log(level, message = nil)
48
+ level, message = 'info', level if message.nil? # Handle only one argument if called from Resque, which expects only message
49
+
50
+ resque_log(message) if defined?(resque_log)
51
+ return false unless self.logger && self.logger.respond_to?(level)
52
+ self.logger.send(level, "#{Time.now}: #{message}")
53
+ end
54
+
55
+ # Log the message first, to ensure it reports what went wrong if in daemon mode. Then die, because something went horribly wrong.
56
+ def log_and_die(msg)
57
+ log(:fatal, msg)
58
+ raise msg
59
+ end
60
+
61
+ def apn_production?
62
+ @opts[:environment] && @opts[:environment] != '' && (:dev != @opts[:environment].to_sym && :development != @opts[:environment].to_sym)
63
+ end
64
+
65
+ # Get a fix on the .pem certificate we'll be using for SSL
66
+ def setup_paths
67
+ # Set option defaults
68
+ @opts[:cert_path] ||= File.join(File.expand_path(RAILS_ROOT), "config", "certs") if defined?(RAILS_ROOT)
69
+ @opts[:environment] ||= RAILS_ENV if defined?(RAILS_ENV)
70
+
71
+ log_and_die("Missing certificate path. Please specify :cert_path when initializing class.") unless @opts[:cert_path]
72
+
73
+ cert_name = @opts[:environment] && @opts[:environment] != "" ? "apn_#{@opts[:environment]}.pem" : "apn_development.pem"
74
+ cert_path = File.join(@opts[:cert_path], @opts[:app], cert_name)
75
+
76
+ @apn_cert = File.exists?(cert_path) ? File.read(cert_path) : nil
77
+
78
+ log(:info, "Cert path is #{cert_path}")
79
+ log(:info, "Cert found") if @apn_cert
80
+
81
+ log_and_die("Missing apple push notification certificate in #{cert_path}") unless @apn_cert
82
+ end
83
+
84
+ # Open socket to Apple's servers
85
+ def setup_connection
86
+ log_and_die("Missing apple push notification certificate") unless @apn_cert
87
+ return true if @socket && @socket_tcp
88
+ log_and_die("Trying to open half-open connection") if @socket || @socket_tcp
89
+
90
+ ctx = OpenSSL::SSL::SSLContext.new
91
+ ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
92
+ ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
93
+
94
+ @socket_tcp = TCPSocket.new(apn_host, apn_port)
95
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket_tcp, ctx)
96
+ @socket.sync = true
97
+ @socket.connect
98
+ rescue SocketError => error
99
+ log_and_die("Error with connection to #{apn_host}: #{error}")
100
+ end
101
+
102
+ # Close open sockets
103
+ def teardown_connection
104
+ log(:info, "Closing connections...") if @opts[:verbose]
105
+
106
+ begin
107
+ @socket.close if @socket
108
+ rescue Exception => e
109
+ log(:error, "Error closing SSL Socket: #{e}")
110
+ end
111
+
112
+ begin
113
+ @socket_tcp.close if @socket_tcp
114
+ rescue Exception => e
115
+ log(:error, "Error closing TCP Socket: #{e}")
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,92 @@
1
+ require 'apn/connection/base'
2
+
3
+ module APN
4
+ # Encapsulates data returned from the {APN Feedback Service}[http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3].
5
+ # Possesses +timestamp+ and +token+ attributes.
6
+ class FeedbackItem
7
+ attr_accessor :timestamp, :token
8
+
9
+ def initialize(time, token)
10
+ @timestamp = time
11
+ @token = token
12
+ end
13
+
14
+ # For convenience, return the token on to_s
15
+ def to_s
16
+ token
17
+ end
18
+ end
19
+
20
+ # When supplied with the certificate path and the desired environment, connects to the {APN Feedback Service}[http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3]
21
+ # and returns any response as an array of APN::FeedbackItem elements.
22
+ #
23
+ # See README for usage and details.
24
+ class Feedback
25
+ include APN::Connection::Base
26
+
27
+ # Returns array of APN::FeedbackItem elements read from Apple. Connects to Apple once and caches the
28
+ # data, continues to returns cached data unless called with <code>data(true)</code>, which clears the
29
+ # existing feedback array. Note that once you force resetting the cache you loose all previous feedback,
30
+ # so be sure you've already processed it.
31
+ def data(force = nil)
32
+ @feedback = nil if force
33
+ @feedback ||= receive
34
+ end
35
+
36
+ # Wrapper around +data+ returning just an array of token strings.
37
+ def tokens(force = nil)
38
+ data(force).map(&:token)
39
+ end
40
+
41
+ # Prettify to return meaningful status information when printed. Can't add these directly to connection/base, because Resque depends on decoding to_s
42
+ def inspect
43
+ "#<#{self.class.name}: #{to_s}>"
44
+ end
45
+
46
+ # Prettify to return meaningful status information when printed. Can't add these directly to connection/base, because Resque depends on decoding to_s
47
+ def to_s
48
+ "#{@socket ? 'Connected' : 'Connection not currently established'} to #{apn_host} on #{apn_port}"
49
+ end
50
+
51
+ protected
52
+
53
+ # Connects to Apple's Feedback Service and checks if there's anything there for us.
54
+ # Returns an array of APN::FeedbackItem pairs
55
+ def receive
56
+ feedback = []
57
+
58
+ # Hi Apple
59
+ setup_connection
60
+
61
+ # Unpacking code borrowed from http://github.com/jpoz/APNS/blob/master/lib/apns/core.rb
62
+ while line = socket.gets # Read lines from the socket
63
+ line.strip!
64
+ f = line.unpack('N1n1H140')
65
+ feedback << APN::FeedbackItem.new(Time.at(f[0]), f[2])
66
+ end
67
+
68
+ # Bye Apple
69
+ teardown_connection
70
+
71
+ return feedback
72
+ end
73
+
74
+
75
+ def apn_host
76
+ @apn_host ||= apn_production? ? "feedback.push.apple.com" : "feedback.sandbox.push.apple.com"
77
+ end
78
+
79
+ def apn_port
80
+ 2196
81
+ end
82
+
83
+ end
84
+ end
85
+
86
+
87
+
88
+ __END__
89
+ # Testing from irb
90
+ irb -r 'lib/apn/feedback'
91
+
92
+ a=APN::Feedback.new(:cert_path => '/Users/kali/Code/insurrection/certs/', :environment => :production)
@@ -0,0 +1,86 @@
1
+ module APN
2
+ # Encapsulates the logic necessary to convert an iPhone token and an array of options into a string of the format required
3
+ # by Apple's servers to send the notification. Much of the processing code here copied with many thanks from
4
+ # http://github.com/samsoffes/apple_push_notification/blob/master/lib/apple_push_notification.rb
5
+ #
6
+ # APN::Notification.new's first argument is the token of the iPhone which should receive the notification. The second argument
7
+ # is a hash with any of :alert, :badge, and :sound keys. All three accept string arguments, while :sound can also be set to +true+
8
+ # to play the default sound installed with the application. At least one of these keys must exist. Any other keys are merged into
9
+ # the root of the hash payload ultimately sent to the iPhone:
10
+ #
11
+ # APN::Notification.new(token, {:alert => 'Stuff', :custom => {:code => 23}})
12
+ # # Writes this JSON to servers: {"aps" => {"alert" => "Stuff"}, "custom" => {"code" => 23}}
13
+ #
14
+ # As a shortcut, APN::Notification.new also accepts a string as the second argument, which it converts into the alert to send. The
15
+ # following two lines are equivalent:
16
+ #
17
+ # APN::Notification.new(token, 'Some Alert')
18
+ # APN::Notification.new(token, {:alert => 'Some Alert'})
19
+ #
20
+ class Notification
21
+ # Available to help clients determine before they create the notification if their message will be too large.
22
+ # Each iPhone Notification payload must be 256 or fewer characters. Encoding a null message has a 57
23
+ # character overhead, so there are 199 characters available for the alert string.
24
+ MAX_ALERT_LENGTH = 199
25
+
26
+ attr_accessor :options, :token
27
+ def initialize(token, opts)
28
+ @options = hash_as_symbols(opts.is_a?(Hash) ? opts : {:alert => opts})
29
+ @token = token
30
+
31
+ raise "The maximum size allowed for a notification payload is 256 bytes." if packaged_notification.size.to_i > 256
32
+ end
33
+
34
+ def to_s
35
+ packaged_notification
36
+ end
37
+
38
+ # Ensures at least one of <code>%w(alert badge sound)</code> is present
39
+ def valid?
40
+ return true if %w(alert badge sound).any?{|key| options.keys.include?(key.to_sym) }
41
+ false
42
+ end
43
+
44
+ protected
45
+
46
+ # Completed encoded notification, ready to send down the wire to Apple
47
+ def packaged_notification
48
+ pt = packaged_token
49
+ pm = packaged_message
50
+ [0, 0, 32, pt, 0, pm.size, pm].pack("ccca*cca*")
51
+ end
52
+
53
+ # Device token, compressed and hex-ified
54
+ def packaged_token
55
+ [@token.gsub(/[\s|<|>]/,'')].pack('H*')
56
+ end
57
+
58
+ # Converts the supplied options into the JSON needed for Apple's push notification servers.
59
+ # Extracts :alert, :badge, and :sound keys into the 'aps' hash, merges any other hash data
60
+ # into the root of the hash to encode and send to apple.
61
+ def packaged_message
62
+ opts = @options.clone # Don't destroy our pristine copy
63
+ hsh = {'aps' => {}}
64
+ hsh['aps']['alert'] = opts.delete(:alert).to_s if opts[:alert]
65
+ hsh['aps']['badge'] = opts.delete(:badge).to_i if opts[:badge]
66
+ if sound = opts.delete(:sound)
67
+ hsh['aps']['sound'] = sound.is_a?(TrueClass) ? 'default' : sound.to_s
68
+ end
69
+ hsh.merge!(opts)
70
+ ActiveSupport::JSON::encode(hsh)
71
+ end
72
+
73
+ # Symbolize keys, using ActiveSupport if available
74
+ def hash_as_symbols(hash)
75
+ if hash.respond_to?(:symbolize_keys)
76
+ return hash.symbolize_keys
77
+ else
78
+ hash.inject({}) do |opt, (key, value)|
79
+ opt[(key.to_sym rescue key) || key] = value
80
+ opt
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,25 @@
1
+ require 'apn/connection/base'
2
+
3
+ module APN
4
+ # This is the class that's actually enqueued via Resque when user calls +APN.notify+.
5
+ # It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
6
+ # workers of the +APN::Sender+ class.
7
+ class NotificationJob
8
+ # We don't need to set @queue here because we changed the enqueue method in queue_manager
9
+ # # Behind the scenes, this is the name of our Resque queue
10
+ # @queue = APN::QUEUE_NAME
11
+
12
+ # Build a notification from arguments and send to Apple
13
+ def self.perform(token, opts)
14
+ msg = APN::Notification.new(token, opts)
15
+ raise "Invalid notification options (did you provide :alert, :badge, or :sound?): #{opts.inspect}" unless msg.valid?
16
+
17
+ worker.send_to_apple( msg )
18
+ end
19
+
20
+ # Only execute this job in specialized APN::Sender workers, since
21
+ # standard Resque workers don't maintain the persistent TCP connection.
22
+ extend Resque::Plugins::AccessWorkerFromJob
23
+ self.required_worker_class = 'APN::Sender'
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # Extending Resque to respond to the +before_unregister_worker+ hook. Note this requires a matching
2
+ # monkeypatch in the Resque::Worker class. See +resque/hooks/before_unregister_worker.rb+ for an
3
+ # example implementation
4
+
5
+ module APN
6
+ # Enqueues a notification to be sent in the background via the persistent TCP socket, assuming apn_sender is running (or will be soon)
7
+ def self.notify(token, opts = {})
8
+ token = token.to_s.gsub(/\W/, '')
9
+ APN::QueueManager.enqueue(APN::Config.queue_name, APN::NotificationJob, token, opts)
10
+ end
11
+
12
+ # Extends Resque, allowing us to add all the callbacks to Resque we desire without affecting the expected
13
+ # functionality in the parent app, if we're included in e.g. a Rails application.
14
+ class QueueManager
15
+ extend Resque
16
+
17
+ def self.before_unregister_worker(&block)
18
+ block ? (@before_unregister_worker = block) : @before_unregister_worker
19
+ end
20
+
21
+ def self.before_unregister_worker=(before_unregister_worker)
22
+ @before_unregister_worker = before_unregister_worker
23
+ end
24
+
25
+ def self.to_s
26
+ "APN::QueueManager (Resque Client) connected to #{redis.server}"
27
+ end
28
+
29
+ # We define our own enqueue method so we can specify a dynamic queue
30
+ def self.enqueue(queue, klass, *args)
31
+ Resque::Job.create(queue, klass, *args)
32
+
33
+ Resque::Plugin.after_enqueue_hooks(klass).each do |hook|
34
+ klass.send(hook, *args)
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ # Ensures we close any open sockets when the worker exits
42
+ APN::QueueManager.before_unregister_worker do |worker|
43
+ worker.send(:teardown_connection) if worker.respond_to?(:teardown_connection)
44
+ end
45
+
46
+
47
+ # # Run N jobs per fork, rather than creating a new fork for each notification
48
+ # # By defunkt - http://gist.github.com/349376
49
+ # APN::QueueManager.after_fork do |job|
50
+ # # How many jobs should we process in each fork?
51
+ # jobs_per_fork = 10
52
+ #
53
+ # # Set hook to nil to prevent running this hook over
54
+ # # and over while processing more jobs in this fork.
55
+ # Resque.after_fork = nil
56
+ #
57
+ # # Make sure we process jobs in the right order.
58
+ # job.worker.process(job)
59
+ #
60
+ # # One less than specified because the child will run a
61
+ # # final job after exiting this hook.
62
+ # (jobs_per_fork.to_i - 1).times do
63
+ # job.worker.process
64
+ # end
65
+ # end
66
+
@@ -0,0 +1,14 @@
1
+ module APN
2
+ class Config
3
+ class << self
4
+ attr_accessor :queue_name
5
+
6
+ def queue_name_with_blank_check
7
+ queue_name_without_blank_check || raise("APN: No queue name set, please set APN::Config.queue_name in an initializer")
8
+ end
9
+
10
+ alias_method :queue_name_without_blank_check, :queue_name
11
+ alias_method :queue_name, :queue_name_with_blank_check
12
+ end
13
+ end
14
+ end
data/lib/apn/sender.rb ADDED
@@ -0,0 +1,47 @@
1
+ module APN
2
+ # Subclass of Resque::Worker which initializes a single TCP socket on creation to communicate with Apple's Push Notification servers.
3
+ # Shares this socket with each child process forked off by Resque to complete a job. Socket is closed in the before_unregister_worker
4
+ # callback, which gets called on normal or exceptional exits.
5
+ #
6
+ # End result: single persistent TCP connection to Apple, so they don't ban you for frequently opening and closing connections,
7
+ # which they apparently view as a DOS attack.
8
+ #
9
+ # Accepts <code>:environment</code> (production vs anything else) and <code>:cert_path</code> options on initialization. If called in a
10
+ # Rails context, will default to RAILS_ENV and RAILS_ROOT/config/certs. :environment will default to development.
11
+ # APN::Sender expects two files to exist in the specified <code>:cert_path</code> directory:
12
+ # <code>apn_production.pem</code> and <code>apn_development.pem</code>.
13
+ #
14
+ # If a socket error is encountered, will teardown the connection and retry again twice before admitting defeat.
15
+ class Sender < ::Resque::Worker
16
+ include APN::Connection::Base
17
+ TIMES_TO_RETRY_SOCKET_ERROR = 2
18
+
19
+ # Send a raw string over the socket to Apple's servers (presumably already formatted by APN::Notification)
20
+ def send_to_apple( notification, attempt = 0 )
21
+ if attempt > TIMES_TO_RETRY_SOCKET_ERROR
22
+ log_and_die("Error with connection to #{apn_host} (retried #{TIMES_TO_RETRY_SOCKET_ERROR} times): #{error}")
23
+ end
24
+
25
+ self.socket.write( notification.to_s )
26
+ rescue SocketError => error
27
+ log(:error, "Error with connection to #{apn_host} (attempt #{attempt}): #{error}")
28
+
29
+ # Try reestablishing the connection
30
+ teardown_connection
31
+ setup_connection
32
+ send_to_apple(notification, attempt + 1)
33
+ end
34
+
35
+ protected
36
+
37
+ def apn_host
38
+ @apn_host ||= apn_production? ? "gateway.push.apple.com" : "gateway.sandbox.push.apple.com"
39
+ end
40
+
41
+ def apn_port
42
+ 2195
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,78 @@
1
+ # Based roughly on delayed_job's delayed/command.rb
2
+ require 'rubygems'
3
+ require 'daemons'
4
+ require 'optparse'
5
+ require 'logger'
6
+
7
+ module APN
8
+ # A wrapper designed to daemonize an APN::Sender instance to keep in running in the background.
9
+ # Connects worker's output to a custom logger, if available. Creates a pid file suitable for
10
+ # monitoring with {monit}[http://mmonit.com/monit/].
11
+ #
12
+ # Based off delayed_job's great example, except we can be much lighter by not loading the entire
13
+ # Rails environment. To use in a Rails app, <code>script/generate apn_sender</code>.
14
+ class SenderDaemon
15
+
16
+ def initialize(args)
17
+ @options = {:worker_count => 1, :environment => :development, :delay => 5}
18
+
19
+ optparse = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
21
+
22
+ opts.on('-h', '--help', 'Show this message') do
23
+ puts opts
24
+ exit 1
25
+ end
26
+ opts.on('-e', '--environment=NAME', 'Specifies the environment to run this apn_sender under ([development]/production).') do |e|
27
+ @options[:environment] = e
28
+ end
29
+ opts.on('--cert-path=NAME', 'Path to directory containing apn .pem certificates.') do |path|
30
+ @options[:cert_path] = path
31
+ end
32
+ opts.on('-n', '--number-of-workers=WORKERS', "Number of unique workers to spawn") do |worker_count|
33
+ @options[:worker_count] = worker_count.to_i rescue 1
34
+ end
35
+ opts.on('-v', '--verbose', "Turn on verbose mode") do
36
+ @options[:verbose] = true
37
+ end
38
+ opts.on('-V', '--very-verbose', "Turn on very verbose mode") do
39
+ @options[:very_verbose] = true
40
+ end
41
+ opts.on('-d', '--delay=D', "Delay between rounds of work (seconds)") do |d|
42
+ @options[:delay] = d
43
+ end
44
+ opts.on('-a', '--app=NAME', 'Specifies the application for this apn_sender') do |a|
45
+ @options[:app] = a
46
+ end
47
+ end
48
+
49
+ # If no arguments, give help screen
50
+ @args = optparse.parse!(args.empty? ? ['-h'] : args)
51
+ @options[:verbose] = true if @options[:very_verbose]
52
+ end
53
+
54
+ def daemonize
55
+ @options[:worker_count].times do |worker_index|
56
+ process_name = @options[:worker_count] == 1 ? "apn_sender" : "apn_sender.#{worker_index}"
57
+ Daemons.run_proc(process_name, :dir => "#{::RAILS_ROOT}/tmp/pids", :dir_mode => :normal, :ARGV => @args) do |*args|
58
+ run process_name
59
+ end
60
+ end
61
+ end
62
+
63
+ def run(worker_name = nil)
64
+ logger = Logger.new(File.join(::RAILS_ROOT, 'log', 'apn_sender.log'))
65
+
66
+ worker = APN::Sender.new(@options)
67
+ worker.logger = logger
68
+ worker.verbose = @options[:verbose]
69
+ worker.very_verbose = @options[:very_verbose]
70
+ worker.work(@options[:delay])
71
+ rescue => e
72
+ STDERR.puts e.message
73
+ logger.fatal(e) if logger && logger.respond_to?(:fatal)
74
+ exit 1
75
+ end
76
+
77
+ end
78
+ end
data/lib/apn/tasks.rb ADDED
@@ -0,0 +1,41 @@
1
+ # Slight modifications from the default Resque tasks
2
+ namespace :apn do
3
+ task :setup
4
+ task :work => :sender
5
+ task :workers => :senders
6
+
7
+ desc "Start an APN worker"
8
+ task :sender => :setup do
9
+ require 'apn'
10
+
11
+ Resque.redis = ENV['REDIS'] if ENV['REDIS']
12
+
13
+ worker = nil
14
+
15
+ begin
16
+ worker = APN::Sender.new(:cert_path => ENV['CERT_PATH'], :environment => ENV['ENVIRONMENT'], :app => "#{ENV['APP']}_#{ENV['ENVIRONMENT']}")
17
+ worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
18
+ worker.very_verbose = ENV['VVERBOSE']
19
+ rescue Exception => e
20
+ raise e
21
+ # abort "set QUEUE env var, e.g. $ QUEUE=critical,high rake resque:work"
22
+ end
23
+
24
+ puts "*** Starting worker to send apple notifications in the background from #{worker}"
25
+
26
+ worker.work(ENV['INTERVAL'] || 5) # interval, will block
27
+ end
28
+
29
+ desc "Start multiple APN workers. Should only be used in dev mode."
30
+ task :senders do
31
+ threads = []
32
+
33
+ ENV['COUNT'].to_i.times do
34
+ threads << Thread.new do
35
+ system "rake apn:work"
36
+ end
37
+ end
38
+
39
+ threads.each { |thread| thread.join }
40
+ end
41
+ end
data/lib/apn.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'resque'
2
+ require 'resque/plugins/access_worker_from_job'
3
+ require 'resque/hooks/before_unregister_worker'
4
+ require 'json'
5
+ require 'active_support'
6
+
7
+ require 'apn/queue_name'
8
+ require 'apn/queue_manager'
9
+ require 'apn/notification'
10
+ require 'apn/notification_job'
11
+ require 'apn/connection/base'
12
+ require 'apn/sender'
13
+ require 'apn/feedback'
@@ -0,0 +1,30 @@
1
+ # Adding a +before_unregister_worker+ hook Resque::Worker. To be used, must be matched by a similar monkeypatch
2
+ # for Resque class itself, or else a class that extends Resque. See apple_push_notification/queue_manager.rb for
3
+ # an implementation.
4
+ module Resque
5
+ class Worker
6
+ alias_method :unregister_worker_without_before_hook, :unregister_worker
7
+
8
+ # Wrapper for original unregister_worker method which adds a before hook +before_unregister_worker+
9
+ # to be executed if present.
10
+ def unregister_worker
11
+ run_hook(:before_unregister_worker, self)
12
+ unregister_worker_without_before_hook
13
+ end
14
+
15
+
16
+ # Unforunately have to override Resque::Worker's +run_hook+ method to call hook on
17
+ # APN::QueueManager rather on Resque directly. Any suggestions on
18
+ # how to make this more flexible are more than welcome.
19
+ def run_hook(name, *args)
20
+ # return unless hook = Resque.send(name)
21
+ return unless hook = APN::QueueManager.send(name)
22
+ msg = "Running #{name} hook"
23
+ msg << " with #{args.inspect}" if args.any?
24
+ log msg
25
+
26
+ args.any? ? hook.call(*args) : hook.call
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1 @@
1
+ require 'apn'
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "apn"
@@ -0,0 +1,66 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "sentia_apn_sender"
8
+ s.version = "1.0.4"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Kali Donovan", "Mario Visic"]
12
+ s.date = "2012-05-04"
13
+ s.description = "Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper methods for enqueueing APN notifications, and a background daemon to send them. Original gem by Kali Donovan and modified by Sentia."
14
+ s.email = "mario.visic@sentia.com.au"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "contrib/apn_sender.monitrc",
26
+ "generators/apn_sender_generator.rb",
27
+ "generators/templates/script",
28
+ "init.rb",
29
+ "lib/apn.rb",
30
+ "lib/apn/connection/base.rb",
31
+ "lib/apn/feedback.rb",
32
+ "lib/apn/notification.rb",
33
+ "lib/apn/notification_job.rb",
34
+ "lib/apn/queue_manager.rb",
35
+ "lib/apn/queue_name.rb",
36
+ "lib/apn/sender.rb",
37
+ "lib/apn/sender_daemon.rb",
38
+ "lib/apn/tasks.rb",
39
+ "lib/resque/hooks/before_unregister_worker.rb",
40
+ "lib/sentia_apn_sender.rb",
41
+ "rails/init.rb",
42
+ "sentia_apn_sender.gemspec",
43
+ "test/helper.rb",
44
+ "test/test_apple_push_notification.rb"
45
+ ]
46
+ s.homepage = "https://github.com/sentia/apn_sender"
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = "1.8.16"
49
+ s.summary = "Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Original gem by Kali Donovan and modified by Sentia."
50
+
51
+ if s.respond_to? :specification_version then
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<resque>, [">= 0"])
56
+ s.add_runtime_dependency(%q<resque-access_worker_from_job>, [">= 0"])
57
+ else
58
+ s.add_dependency(%q<resque>, [">= 0"])
59
+ s.add_dependency(%q<resque-access_worker_from_job>, [">= 0"])
60
+ end
61
+ else
62
+ s.add_dependency(%q<resque>, [">= 0"])
63
+ s.add_dependency(%q<resque-access_worker_from_job>, [">= 0"])
64
+ end
65
+ end
66
+
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'apple_push_notification'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestAPN < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sentia_apn_sender
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kali Donovan
9
+ - Mario Visic
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-04 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: resque
17
+ requirement: &70289459606940 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *70289459606940
26
+ - !ruby/object:Gem::Dependency
27
+ name: resque-access_worker_from_job
28
+ requirement: &70289459606420 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *70289459606420
37
+ description: Resque-based background worker to send Apple Push Notifications over
38
+ a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between
39
+ jobs, helper methods for enqueueing APN notifications, and a background daemon to
40
+ send them. Original gem by Kali Donovan and modified by Sentia.
41
+ email: mario.visic@sentia.com.au
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files:
45
+ - LICENSE
46
+ - README.rdoc
47
+ files:
48
+ - .document
49
+ - LICENSE
50
+ - README.rdoc
51
+ - Rakefile
52
+ - VERSION
53
+ - contrib/apn_sender.monitrc
54
+ - generators/apn_sender_generator.rb
55
+ - generators/templates/script
56
+ - init.rb
57
+ - lib/apn.rb
58
+ - lib/apn/connection/base.rb
59
+ - lib/apn/feedback.rb
60
+ - lib/apn/notification.rb
61
+ - lib/apn/notification_job.rb
62
+ - lib/apn/queue_manager.rb
63
+ - lib/apn/queue_name.rb
64
+ - lib/apn/sender.rb
65
+ - lib/apn/sender_daemon.rb
66
+ - lib/apn/tasks.rb
67
+ - lib/resque/hooks/before_unregister_worker.rb
68
+ - lib/sentia_apn_sender.rb
69
+ - rails/init.rb
70
+ - sentia_apn_sender.gemspec
71
+ - test/helper.rb
72
+ - test/test_apple_push_notification.rb
73
+ homepage: https://github.com/sentia/apn_sender
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 1.8.16
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Resque-based background worker to send Apple Push Notifications over a persistent
97
+ TCP socket. Original gem by Kali Donovan and modified by Sentia.
98
+ test_files: []