apn_sender 1.0.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d85dcde79eb0f21533547c631b827e73081d5167
4
+ data.tar.gz: a3bd028152b5f7ee6960b1516bf87b2fa7abdce8
5
+ SHA512:
6
+ metadata.gz: 96a448c4a6d1ed2fcc9807617eea4171e876ef45f7dd94b41925ca84c81f656404ef526ab590f7d30612571bd3ecab4f47621aabe44aeb66c5c3909b89794da5
7
+ data.tar.gz: 12b386591581bdcb234d0dd90de44f0836ad27705aedeeefcc6e0333d5225207fdabd0e085c0cf2675abfbbed1b725d716611ca859ea06c60a6ffe9cb1385a46
@@ -0,0 +1,15 @@
1
+ # Version 2.0
2
+ ## 2.0.0
3
+ - adding connection_pool for handle apple sockets
4
+ - removing resque hard dependency
5
+ - adding support for sending sync messages
6
+ - adding Thread support
7
+ - adding support to sidekiq (Caue Guerra)
8
+ - truncation messages when payload is greater than 256 option (Caue Guerra)
9
+
10
+ # Version 1.0
11
+ ## 1.0.6
12
+ - Added support for password-protected .pem files
13
+ - Read feedback data in 38-byte chunks
14
+ - Support passing dictionary as :alert key
15
+ - Logging to STDOUT if no other loggers present
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
+ [![Code Climate](https://codeclimate.com/github/arthurnn/apn_sender.png)](https://codeclimate.com/github/arthurnn/apn_sender)
2
+ [![Build Status](https://travis-ci.org/arthurnn/apn_sender.png)](https://travis-ci.org/arthurnn/apn_sender)
3
+
1
4
  ## UPDATE May 3, 2013: current status
2
5
 
3
- This project was not supported for a while, but we are going to support it again.
6
+ This project was not supported for a while, but we are going to support it again.
4
7
 
5
8
  -----
6
9
 
@@ -19,7 +22,7 @@ The apn_sender gem includes a background daemon which processes background messa
19
22
  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.
20
23
 
21
24
  ## Current Status
22
- This gem has been in production on 500px,sending million of notifications.
25
+ This gem has been used in production, on 500px, sending millions of notifications.
23
26
 
24
27
  ## Usage
25
28
 
@@ -28,7 +31,7 @@ This gem has been in production on 500px,sending million of notifications.
28
31
  To queue a message for sending through Apple's Push Notification service from your Rails application:
29
32
 
30
33
  ```
31
- APN.notify(token, opts_hash)
34
+ APN.notify_async(token, opts_hash)
32
35
  ```
33
36
 
34
37
  where ```token``` is the unique identifier of the iPhone to receive the notification and ```opts_hash``` can have any of the following keys:
@@ -59,10 +62,9 @@ For production, you're probably better off running a dedicated daemon and settin
59
62
  ./script/generate apn_sender
60
63
 
61
64
  # To run daemon. Pass --help to print all options
62
- ./script/apn_sender --environment#production --verbose start
65
+ ./script/apn_sender start
63
66
  ```
64
67
 
65
- 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>.
66
68
 
67
69
  Also, there are two similar options: ```:cert_path``` and ```:full_cert_path```. The former specifies the directory in which to find the .pem file (either apn_production.pem or apn_development.pem, depending on the environment). The latter specifies a .pem file explicitly, allowing customized certificate names if needed.
68
70
 
@@ -70,7 +72,7 @@ Check ```logs/apn_sender.log``` for debugging output. In addition to logging an
70
72
 
71
73
 
72
74
  ### 3. Checking Apple's Feedback Service
73
-
75
+
74
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.
75
77
 
76
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:
@@ -78,9 +80,9 @@ It's actually really simple - you connect to them periodically and they give you
78
80
  ```
79
81
  # APN::Feedback accepts the same optional :environment
80
82
  # and :cert_path / :full_cert_path options as APN::Sender
81
- feedback # APN::Feedback.new()
83
+ feedback = APN::Feedback.new()
82
84
 
83
- tokens # feedback.tokens # #> Array of device tokens
85
+ tokens = feedback.tokens # Array of device tokens
84
86
  tokens.each do |token|
85
87
  # ... custom logic here to stop you app from
86
88
  # sending further notifications to this token
@@ -90,7 +92,7 @@ It's actually really simple - you connect to them periodically and they give you
90
92
  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):
91
93
 
92
94
  ```
93
- items # feedback.data # #> Array of APN::FeedbackItem elements
95
+ items = feedback.data # Array of APN::FeedbackItem elements
94
96
  items.each do |item|
95
97
  item.token
96
98
  item.timestamp
@@ -110,15 +112,15 @@ If you're sending notifications, you should definitely call one of the ```receiv
110
112
  Just for the record, this is essentially what you want to have whenever run periodically for you:
111
113
  ```
112
114
  def self.clear_uninstalled_applications
113
- feedback_data # APN::Feedback.new(:environment #> :production).data
114
-
115
+ feedback_data = APN::Feedback.new(:environment #> :production).data
116
+
115
117
  feedback_data.each do |item|
116
- user # User.find_by_iphone_token( item.token )
117
-
118
+ user = User.find_by_iphone_token( item.token )
119
+
118
120
  if user.iphone_token_updated_at && user.iphone_token_updated_at > item.timestamp
119
121
  return true # App has been reregistered since Apple determined it'd been uninstalled
120
122
  else
121
- user.update_attributes(:iphone_token #> nil, :iphone_token_updated_at #> Time.now)
123
+ user.update_attributes(iphone_token: nil, iphone_token_updated_at: Time.now)
122
124
  end
123
125
  end
124
126
  end
@@ -141,12 +143,12 @@ In your Rails app, add (2.3.x):
141
143
 
142
144
  ```
143
145
  config.gem 'apn_sender', :lib => 'apn'
144
- ```
146
+ ```
145
147
  or (3.x) to your Gemfile:
146
148
 
147
- ```
149
+ ```
148
150
  gem 'apn_sender', require: 'apn'
149
- ```
151
+ ```
150
152
  To add a few useful rake tasks for running workers, add the following line to your Rakefile:
151
153
 
152
154
  ```
@@ -155,4 +157,4 @@ To add a few useful rake tasks for running workers, add the following line to yo
155
157
 
156
158
  ## Copyright
157
159
 
158
- Copyright (c) 2010 Kali Donovan. See LICENSE for details.
160
+ Copyright (c) 2013 Arthur Nogueira Neves. See LICENSE for details.
data/lib/apn.rb CHANGED
@@ -1,13 +1,96 @@
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'
1
+ require "openssl"
2
+ require "socket"
3
+ require "active_support/core_ext"
4
+ require "active_support/json"
5
+ require 'connection_pool'
6
+
7
+ require "apn/version"
8
+ require 'apn/connection'
9
+
10
+ module APN
11
+
12
+ class << self
13
+ include APN::Connection
14
+
15
+ def notify_async(token, opts = {})
16
+ token = token.to_s.gsub(/\W/, '')
17
+ backend.notify(token, opts)
18
+ end
19
+
20
+ def notify_sync(token, opts)
21
+ token = token.to_s.gsub(/\W/, '')
22
+ msg = APN::Notification.new(token, opts)
23
+ raise "Invalid notification options (did you provide :alert, :badge, or :sound?): #{opts.inspect}" unless msg.valid?
24
+
25
+ APN.with_connection do |client|
26
+ client.push(msg)
27
+ end
28
+ end
29
+
30
+ def backend=(backend)
31
+ @backend = backend
32
+ end
33
+
34
+ def backend
35
+ @backend ||=
36
+ if defined?(Sidekiq)
37
+ APN::Backend::Sidekiq.new
38
+ elsif defined?(Resque)
39
+ APN::Backend::Resque.new
40
+ else
41
+ APN::Backend::Simple.new
42
+ end
43
+ end
44
+
45
+ def logger=(logger)
46
+ @logger = logger
47
+ end
48
+
49
+ def logger
50
+ @logger ||= Logger.new(STDOUT)
51
+ end
52
+
53
+ def truncate_alert
54
+ @truncate_alert ||= false
55
+ end
56
+
57
+ def truncate_alert=(truncate)
58
+ @truncate_alert = truncate
59
+ end
60
+
61
+ # Log message to any logger provided by the user (e.g. the Rails logger).
62
+ # Accepts +log_level+, +message+, since that seems to make the most sense,
63
+ # and just +message+, to be compatible with Resque's log method and to enable
64
+ # sending verbose and very_verbose worker messages to e.g. the rails logger.
65
+ #
66
+ # Perhaps a method definition of +message, +level+ would make more sense, but
67
+ # that's also the complete opposite of what anyone comming from rails would expect.
68
+ def log(level, message = nil)
69
+ level, message = 'info', level if message.nil? # Handle only one argument if called from Resque, which expects only message
70
+
71
+ return false unless logger && logger.respond_to?(level)
72
+ logger.send(level, "#{Time.now}: #{message}")
73
+ end
74
+
75
+ # Log the message first, to ensure it reports what went wrong if in daemon mode.
76
+ # Then die, because something went horribly wrong.
77
+ def log_and_die(msg)
78
+ logger.fatal(msg)
79
+ raise msg
80
+ end
81
+ end
82
+ end
83
+
9
84
  require 'apn/notification'
10
- require 'apn/notification_job'
11
- require 'apn/connection/base'
12
- require 'apn/sender'
13
- require 'apn/feedback'
85
+ require 'apn/client'
86
+ require 'apn/feedback'
87
+
88
+ module APN::Jobs
89
+ QUEUE_NAME = :apple_push_notifications
90
+ end
91
+
92
+ require 'apn/jobs/sidekiq_notification_job' if defined?(Sidekiq)
93
+ require 'apn/jobs/resque_notification_job' if defined?(Resque)
94
+ require "apn/railtie" if defined?(Rails)
95
+
96
+ require 'apn/backend'
@@ -0,0 +1,34 @@
1
+ module APN
2
+ module Backend
3
+
4
+ class Sidekiq
5
+
6
+ def notify(token, opts)
7
+ ::Sidekiq::Client.enqueue(APN::Jobs::SidekiqNotificationJob, token, opts)
8
+ end
9
+ end
10
+
11
+ class Resque
12
+
13
+ def notify(token, opts)
14
+ ::Resque.enqueue(APN::Jobs::ResqueNotificationJob, token, opts)
15
+ end
16
+ end
17
+
18
+ class Simple
19
+
20
+ def notify(token, opts)
21
+ Thread.new do
22
+ APN.notify_sync(token, opts)
23
+ end
24
+ end
25
+ end
26
+
27
+ class Null
28
+
29
+ def notify(token, opts)
30
+ APN.log("Null Backend sending message to #{token}")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,65 @@
1
+ module APN
2
+ class Client
3
+
4
+ DEFAULTS = {port: 2195, host: "gateway.push.apple.com"}
5
+
6
+ def initialize(options = {})
7
+ options = DEFAULTS.merge options.reject{|k,v| v.nil?}
8
+ @apn_cert, @cert_pass = options[:certificate], options[:password]
9
+ @host, @port = options[:host], options[:port]
10
+ self
11
+ end
12
+
13
+ def push(message)
14
+ socket.write(message.to_s)
15
+ socket.flush
16
+ if IO.select([socket], nil, nil, 1) && error = socket.read(6)
17
+ error = error.unpack("ccN")
18
+ APN.log(:error, "Error on message: #{error}")
19
+ return false
20
+ end
21
+
22
+ APN.log(:debug, "Message sent.")
23
+ true
24
+ end
25
+
26
+ def feedback
27
+ if bunch = socket.read(38)
28
+ f = bunch.strip.unpack('N1n1H140')
29
+ APN::FeedbackItem.new(Time.at(f[0]), f[2])
30
+ end
31
+ end
32
+
33
+ def socket
34
+ @socket ||= setup_socket
35
+ end
36
+
37
+ private
38
+ # Open socket to Apple's servers
39
+ def setup_socket
40
+ ctx = setup_certificate
41
+
42
+ APN.log(:debug, "Connecting to #{@host}:#{@port}...")
43
+
44
+ socket_tcp = TCPSocket.new(@host, @port)
45
+ OpenSSL::SSL::SSLSocket.new(socket_tcp, ctx).tap do |s|
46
+ s.sync = true
47
+ s.connect
48
+ end
49
+ end
50
+
51
+ def setup_certificate
52
+ ctx = OpenSSL::SSL::SSLContext.new
53
+ ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
54
+ if @cert_pass
55
+ ctx.key = OpenSSL::PKey::RSA.new(@apn_cert, @cert_pass)
56
+ APN.log(:debug, "Setting up certificate using a password.")
57
+
58
+ else
59
+ ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
60
+ end
61
+ ctx
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ module APN
2
+ module Connection
3
+ # APN::Connection::Base takes care of all the boring certificate loading, socket creating, and logging
4
+ # responsibilities so APN::Sender and APN::Feedback and focus on their respective specialties.
5
+ def connection_pool
6
+ @pool ||= ConnectionPool.new(size: (pool_size || 1), timeout: (pool_timeout || 5)) do
7
+ APN::Client.new(host: host,
8
+ port: port,
9
+ certificate: certificate,
10
+ password: password)
11
+ end
12
+ end
13
+
14
+ def with_connection(&block)
15
+ connection_pool.with(&block)
16
+ end
17
+
18
+ # pool config
19
+ attr_accessor :pool_size, :pool_timeout
20
+
21
+ attr_accessor :host, :port, :root, :full_certificate_path, :password
22
+
23
+ def certificate
24
+ @apn_cert ||= File.read(certificate_path)
25
+ end
26
+
27
+ def certificate_path
28
+ full_certificate_path || File.join(root, certificate_name)
29
+ end
30
+
31
+ def certificate_name
32
+ @cert_name || "apn_production.pem"
33
+ end
34
+
35
+ def certificate_name=(name)
36
+ @cert_name = name
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,3 @@
1
- require 'apn/connection/base'
2
-
3
1
  module APN
4
2
  # 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
3
  # Possesses +timestamp+ and +token+ attributes.
@@ -22,7 +20,10 @@ module APN
22
20
  #
23
21
  # See README for usage and details.
24
22
  class Feedback
25
- include APN::Connection::Base
23
+
24
+ def initialize(options = {})
25
+ @apn_host, @apn_port = options[:host], options[:port]
26
+ end
26
27
 
27
28
  # Returns array of APN::FeedbackItem elements read from Apple. Connects to Apple once and caches the
28
29
  # data, continues to returns cached data unless called with <code>data(true)</code>, which clears the
@@ -45,7 +46,7 @@ module APN
45
46
 
46
47
  # Prettify to return meaningful status information when printed. Can't add these directly to connection/base, because Resque depends on decoding to_s
47
48
  def to_s
48
- "#{@socket ? 'Connected' : 'Connection not currently established'} to #{apn_host} on #{apn_port}"
49
+ "#{@socket ? 'Connected' : 'Connection not currently established'} to #{host} on #{port}"
49
50
  end
50
51
 
51
52
  protected
@@ -53,31 +54,26 @@ module APN
53
54
  # Connects to Apple's Feedback Service and checks if there's anything there for us.
54
55
  # Returns an array of APN::FeedbackItem pairs
55
56
  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 bunch = socket.read(38) # Read data from the socket
63
- f = bunch.strip.unpack('N1n1H140')
64
- feedback << APN::FeedbackItem.new(Time.at(f[0]), f[2])
57
+ feedbacks = []
58
+ while f = client.feedback
59
+ feedbacks << f
65
60
  end
66
-
67
- # Bye Apple
68
- teardown_connection
69
-
70
- return feedback
61
+ return feedbacks
71
62
  end
72
63
 
73
-
74
- def apn_host
75
- @apn_host ||= apn_production? ? "feedback.push.apple.com" : "feedback.sandbox.push.apple.com"
64
+ def client
65
+ @client ||= APN::Client.new(host: host,
66
+ port: port,
67
+ certificate: APN.certificate,
68
+ password: APN.password)
76
69
  end
77
70
 
78
- def apn_port
79
- 2196
71
+ def host
72
+ @apn_host || "feedback.push.apple.com"
80
73
  end
81
74
 
75
+ def port
76
+ @apn_port || 2196
77
+ end
82
78
  end
83
79
  end
@@ -0,0 +1,15 @@
1
+ module APN::Jobs
2
+ # This is the class that's actually enqueued via Resque when user calls +APN.notify+.
3
+ # It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
4
+ # workers of the +APN::Sender+ class.
5
+ class ResqueNotificationJob
6
+
7
+ # Behind the scenes, this is the name of our Resque queue
8
+ @queue = QUEUE_NAME
9
+
10
+ # Build a notification from arguments and send to Apple
11
+ def self.perform(token, opts)
12
+ APN.notify_sync(token, opts)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module APN::Jobs
2
+ # This is the class that's actually enqueued via Resque when user calls +APN.notify+.
3
+ # It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
4
+ # workers of the +APN::Sender+ class.
5
+ class SidekiqNotificationJob
6
+ include Sidekiq::Worker
7
+ # Behind the scenes, this is the name of our Sidekiq queue
8
+ @queue = QUEUE_NAME
9
+
10
+ # Build a notification from arguments and send to Apple
11
+ def perform(token, opts)
12
+ APN.notify_sync(token, opts)
13
+ end
14
+ end
15
+ end
@@ -22,12 +22,15 @@ module APN
22
22
  # Each iPhone Notification payload must be 256 or fewer characters. Encoding a null message has a 57
23
23
  # character overhead, so there are 199 characters available for the alert string.
24
24
  MAX_ALERT_LENGTH = 199
25
+ DATA_MAX_BYTES = 256
25
26
 
26
27
  attr_accessor :options, :token
27
28
  def initialize(token, opts)
28
29
  @options = opts.is_a?(Hash) ? opts.symbolize_keys : {:alert => opts}
29
30
  @token = token
30
31
 
32
+ truncate_alert! if APN.truncate_alert
33
+
31
34
  raise "The maximum size allowed for a notification payload is 256 bytes." if packaged_notification.size.to_i > 256
32
35
  end
33
36
 
@@ -41,8 +44,6 @@ module APN
41
44
  false
42
45
  end
43
46
 
44
- protected
45
-
46
47
  # Completed encoded notification, ready to send down the wire to Apple
47
48
  def packaged_notification
48
49
  pt = packaged_token
@@ -73,5 +74,15 @@ module APN
73
74
  ActiveSupport::JSON::encode(hsh)
74
75
  end
75
76
 
77
+ def truncate_alert!
78
+ while packaged_notification.size.to_i > DATA_MAX_BYTES
79
+ if @options[:alert].is_a? Hash
80
+ last = @options[:alert]['loc-args'].pop
81
+ @options[:alert]['loc-args'] << last[0..-2]
82
+ else
83
+ @options[:alert] = @options[:alert][0..-2]
84
+ end
85
+ end
86
+ end
76
87
  end
77
88
  end
@@ -0,0 +1,16 @@
1
+ module APN
2
+ class Railtie < Rails::Railtie
3
+ initializer "apn.setup" do |app|
4
+
5
+ APN.root = File.join(Rails.root, "config", "certs")
6
+ if Rails.env.development?
7
+ APN.certificate_name = "apn_development.pem"
8
+ APN.host = "gateway.sandbox.push.apple.com"
9
+ end
10
+
11
+ logger = Logger.new(File.join(Rails.root, 'log', 'apn_sender.log'))
12
+ APN.logger = logger
13
+
14
+ end
15
+ end
16
+ end
@@ -3,6 +3,7 @@ require 'rubygems'
3
3
  require 'daemons'
4
4
  require 'optparse'
5
5
  require 'logger'
6
+ require 'resque'
6
7
 
7
8
  module APN
8
9
  # A wrapper designed to daemonize an APN::Sender instance to keep in running in the background.
@@ -12,10 +13,10 @@ module APN
12
13
  # Based off delayed_job's great example, except we can be much lighter by not loading the entire
13
14
  # Rails environment. To use in a Rails app, <code>script/generate apn_sender</code>.
14
15
  class SenderDaemon
15
-
16
+
16
17
  def initialize(args)
17
- @options = {:worker_count => 1, :environment => :development, :delay => 5}
18
-
18
+ @options = {worker_count: 1, delay: 5}
19
+
19
20
  optparse = OptionParser.new do |opts|
20
21
  opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
21
22
 
@@ -23,59 +24,52 @@ module APN
23
24
  puts opts
24
25
  exit 1
25
26
  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
27
+ opts.on('--cert-path=PATH', 'Path to directory containing apn .pem certificates.') do |path|
28
+ @options[:cert_root] = path
31
29
  end
32
- opts.on('c', '--full-cert-path=NAME', 'Full path to desired .pem certificate (overrides environment selector).') do |path|
30
+ opts.on('c', '--full-cert-path=PATH', 'Full path to desired .pem certificate.') do |path|
33
31
  @options[:full_cert_path] = path
34
32
  end
35
33
  opts.on('--cert-pass=PASSWORD', 'Password for the apn .pem certificates.') do |pass|
36
34
  @options[:cert_pass] = pass
37
35
  end
36
+ opts.on('--cert-name=NAME', 'Certificate file name. Default: apn_production.pem') do |certificate_name|
37
+ @options[:certificate_name] = certificate_name
38
+ end
38
39
  opts.on('-n', '--number-of-workers=WORKERS', "Number of unique workers to spawn") do |worker_count|
39
40
  @options[:worker_count] = worker_count.to_i rescue 1
40
41
  end
41
- opts.on('-v', '--verbose', "Turn on verbose mode") do
42
- @options[:verbose] = true
43
- end
44
- opts.on('-V', '--very-verbose', "Turn on very verbose mode") do
45
- @options[:very_verbose] = true
46
- end
47
42
  opts.on('-d', '--delay=D', "Delay between rounds of work (seconds)") do |d|
48
43
  @options[:delay] = d
49
44
  end
50
45
  end
51
-
46
+
52
47
  # If no arguments, give help screen
53
48
  @args = optparse.parse!(args.empty? ? ['-h'] : args)
54
- @options[:verbose] = true if @options[:very_verbose]
55
49
  end
56
-
50
+
57
51
  def daemonize
58
52
  @options[:worker_count].times do |worker_index|
59
53
  process_name = @options[:worker_count] == 1 ? "apn_sender" : "apn_sender.#{worker_index}"
60
- Daemons.run_proc(process_name, :dir => "#{::RAILS_ROOT}/tmp/pids", :dir_mode => :normal, :ARGV => @args) do |*args|
61
- run process_name
54
+ pids_dir = defined?(Rails) ? "#{::RAILS_ROOT}/tmp/pids" : "tmp/pids"
55
+ Daemons.run_proc(process_name, :dir => pids_dir, :dir_mode => :normal, :ARGV => @args) do |*args|
56
+ run(process_name)
62
57
  end
63
58
  end
64
59
  end
65
-
60
+
66
61
  def run(worker_name = nil)
67
- logger = Logger.new(File.join(::RAILS_ROOT, 'log', 'apn_sender.log'))
68
-
69
- worker = APN::Sender.new(@options)
70
- worker.logger = logger
71
- worker.verbose = @options[:verbose]
72
- worker.very_verbose = @options[:very_verbose]
62
+ APN.password = @options[:cert_pass]
63
+ APN.full_certificate_path = @options[:full_cert_path]
64
+ APN.root = @options[:cert_root]
65
+ APN.certificate_name = @options[:certificate_name]
66
+
67
+ worker = ::Resque::Worker.new(APN::Jobs::QUEUE_NAME)
73
68
  worker.work(@options[:delay])
74
69
  rescue => e
75
70
  STDERR.puts e.message
76
- logger.fatal(e) if logger && logger.respond_to?(:fatal)
77
71
  exit 1
78
72
  end
79
-
73
+
80
74
  end
81
75
  end
@@ -8,9 +8,16 @@ namespace :apn do
8
8
  task :sender => :setup do
9
9
  require 'apn'
10
10
 
11
- worker = APN::Sender.new(:full_cert_path => ENV['FULL_CERT_PATH'], :cert_path => ENV['CERT_PATH'], :environment => ENV['ENVIRONMENT'], :cert_pass => ENV['CERT_PASS'])
12
- worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
13
- worker.very_verbose = ENV['VVERBOSE']
11
+ unless defined?(Resque)
12
+ puts "This rake task is only for resque workers"
13
+ return
14
+ end
15
+
16
+ APN.password = ENV['CERT_PASS']
17
+ APN.full_certificate_path = ENV['FULL_CERT_PATH']
18
+ APN.logger = Rails.logger
19
+
20
+ worker = ::Resque::Worker.new(APN::Jobs::QUEUE_NAME)
14
21
 
15
22
  puts "*** Starting worker to send apple notifications in the background from #{worker}"
16
23
 
@@ -29,4 +36,4 @@ namespace :apn do
29
36
 
30
37
  threads.each { |thread| thread.join }
31
38
  end
32
- end
39
+ end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module APN
3
- VERSION = "1.0.6"
3
+ VERSION = "2.0.0"
4
4
  end
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apn_sender
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
5
- prerelease:
4
+ version: 2.0.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Kali Donovan
@@ -13,102 +12,94 @@ cert_chain: []
13
12
  date: 2011-05-15 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
- name: resque
15
+ name: connection_pool
17
16
  requirement: !ruby/object:Gem::Requirement
18
- none: false
19
17
  requirements:
20
- - - ! '>='
18
+ - - '>='
21
19
  - !ruby/object:Gem::Version
22
20
  version: '0'
23
21
  type: :runtime
24
22
  prerelease: false
25
23
  version_requirements: !ruby/object:Gem::Requirement
26
- none: false
27
24
  requirements:
28
- - - ! '>='
25
+ - - '>='
29
26
  - !ruby/object:Gem::Version
30
27
  version: '0'
31
28
  - !ruby/object:Gem::Dependency
32
- name: resque-access_worker_from_job
29
+ name: activesupport
33
30
  requirement: !ruby/object:Gem::Requirement
34
- none: false
35
31
  requirements:
36
- - - ! '>='
32
+ - - '>='
37
33
  - !ruby/object:Gem::Version
38
- version: '0'
34
+ version: '3.1'
39
35
  type: :runtime
40
36
  prerelease: false
41
37
  version_requirements: !ruby/object:Gem::Requirement
42
- none: false
43
38
  requirements:
44
- - - ! '>='
39
+ - - '>='
45
40
  - !ruby/object:Gem::Version
46
- version: '0'
41
+ version: '3.1'
47
42
  - !ruby/object:Gem::Dependency
48
- name: active_support
43
+ name: daemons
49
44
  requirement: !ruby/object:Gem::Requirement
50
- none: false
51
45
  requirements:
52
- - - ! '>='
46
+ - - '>='
53
47
  - !ruby/object:Gem::Version
54
48
  version: '0'
55
49
  type: :runtime
56
50
  prerelease: false
57
51
  version_requirements: !ruby/object:Gem::Requirement
58
- none: false
59
52
  requirements:
60
- - - ! '>='
53
+ - - '>='
61
54
  - !ruby/object:Gem::Version
62
55
  version: '0'
63
- description: Resque-based background worker to send Apple Push Notifications over
64
- a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between
65
- jobs, helper methods for enqueueing APN notifications, and a background daemon to
66
- send them.
56
+ description: Background worker to send Apple Push Notifications over a persistent
57
+ TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper
58
+ methods for enqueueing APN notifications, and a background daemon to send them.
67
59
  email: arthurnn@gmail.com
68
60
  executables: []
69
61
  extensions: []
70
62
  extra_rdoc_files: []
71
63
  files:
72
- - lib/apn/connection/base.rb
64
+ - lib/apn/backend.rb
65
+ - lib/apn/client.rb
66
+ - lib/apn/connection.rb
73
67
  - lib/apn/feedback.rb
68
+ - lib/apn/jobs/resque_notification_job.rb
69
+ - lib/apn/jobs/sidekiq_notification_job.rb
74
70
  - lib/apn/notification.rb
75
- - lib/apn/notification_job.rb
76
- - lib/apn/queue_manager.rb
77
- - lib/apn/queue_name.rb
78
- - lib/apn/sender.rb
71
+ - lib/apn/railtie.rb
79
72
  - lib/apn/sender_daemon.rb
80
73
  - lib/apn/tasks.rb
81
74
  - lib/apn/version.rb
82
75
  - lib/apn.rb
83
- - lib/resque/hooks/before_unregister_worker.rb
84
- - CHANGELOG
76
+ - CHANGELOG.md
85
77
  - LICENSE
86
78
  - README.md
87
79
  - Rakefile
88
80
  homepage: http://github.com/arthurnn/apn_sender
89
81
  licenses:
90
82
  - MIT
83
+ metadata: {}
91
84
  post_install_message:
92
85
  rdoc_options: []
93
86
  require_paths:
94
87
  - lib
95
88
  required_ruby_version: !ruby/object:Gem::Requirement
96
- none: false
97
89
  requirements:
98
- - - ! '>='
90
+ - - '>='
99
91
  - !ruby/object:Gem::Version
100
92
  version: '1.9'
101
93
  required_rubygems_version: !ruby/object:Gem::Requirement
102
- none: false
103
94
  requirements:
104
- - - ! '>='
95
+ - - '>='
105
96
  - !ruby/object:Gem::Version
106
97
  version: 1.3.6
107
98
  requirements: []
108
99
  rubyforge_project:
109
- rubygems_version: 1.8.24
100
+ rubygems_version: 2.0.3
110
101
  signing_key:
111
- specification_version: 3
112
- summary: Resque-based background worker to send Apple Push Notifications over a persistent
113
- TCP socket.
102
+ specification_version: 4
103
+ summary: Background worker to send Apple Push Notifications over a persistent TCP
104
+ socket.
114
105
  test_files: []
data/CHANGELOG DELETED
@@ -1,5 +0,0 @@
1
- * 1.0.6
2
- - Added support for password-protected .pem files
3
- - Read feedback data in 38-byte chunks
4
- - Support passing dictionary as :alert key
5
- - Logging to STDOUT if no other loggers present
@@ -1,127 +0,0 @@
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::QUEUE_NAME ) 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.logger)
35
- ::Rails.logger
36
- end
37
- @logger ||= Logger.new(STDOUT)
38
- end
39
-
40
- # Log message to any logger provided by the user (e.g. the Rails logger).
41
- # Accepts +log_level+, +message+, since that seems to make the most sense,
42
- # and just +message+, to be compatible with Resque's log method and to enable
43
- # sending verbose and very_verbose worker messages to e.g. the rails logger.
44
- #
45
- # Perhaps a method definition of +message, +level+ would make more sense, but
46
- # that's also the complete opposite of what anyone comming from rails would expect.
47
- alias_method(:resque_log, :log) if defined?(log)
48
- def log(level, message = nil)
49
- level, message = 'info', level if message.nil? # Handle only one argument if called from Resque, which expects only message
50
-
51
- resque_log(message) if defined?(resque_log)
52
- return false unless self.logger && self.logger.respond_to?(level)
53
- self.logger.send(level, "#{Time.now}: #{message}")
54
- end
55
-
56
- # Log the message first, to ensure it reports what went wrong if in daemon mode.
57
- # Then die, because something went horribly wrong.
58
- def log_and_die(msg)
59
- log(:fatal, msg)
60
- raise msg
61
- end
62
-
63
- def apn_production?
64
- @opts[:environment] && @opts[:environment] != '' && :production == @opts[:environment].to_sym
65
- end
66
-
67
- # Get a fix on the .pem certificate we'll be using for SSL
68
- def setup_paths
69
- @opts[:environment] ||= ::Rails.env if defined?(::Rails.env)
70
-
71
- # Accept a complete :full_cert_path allowing arbitrary certificate names, or create a default from the Rails env
72
- cert_path = @opts[:full_cert_path] || begin
73
- # Note that RAILS_ROOT is still here not from Rails, but to handle passing in root from sender_daemon
74
- @opts[:root_path] ||= defined?(::Rails.root) ? ::Rails.root.to_s : (defined?(RAILS_ROOT) ? RAILS_ROOT : '/')
75
- @opts[:cert_path] ||= File.join(File.expand_path(@opts[:root_path]), "config", "certs")
76
- @opts[:cert_name] ||= apn_production? ? "apn_production.pem" : "apn_development.pem"
77
-
78
- File.join(@opts[:cert_path], @opts[:cert_name])
79
- end
80
-
81
- @apn_cert = File.read(cert_path) if File.exists?(cert_path)
82
- log_and_die("Please specify correct :full_cert_path. No apple push notification certificate found in: #{cert_path}") unless @apn_cert
83
- end
84
-
85
- # Open socket to Apple's servers
86
- def setup_connection
87
- log_and_die("Missing apple push notification certificate") unless @apn_cert
88
- return true if @socket && @socket_tcp
89
- log_and_die("Trying to open half-open connection") if @socket || @socket_tcp
90
-
91
- ctx = OpenSSL::SSL::SSLContext.new
92
- ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
93
-
94
- if @opts[:cert_pass]
95
- ctx.key = OpenSSL::PKey::RSA.new(@apn_cert, @opts[:cert_pass])
96
- else
97
- ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
98
- end
99
-
100
- @socket_tcp = TCPSocket.new(apn_host, apn_port)
101
- @socket = OpenSSL::SSL::SSLSocket.new(@socket_tcp, ctx)
102
- @socket.sync = true
103
- @socket.connect
104
- rescue SocketError => error
105
- log_and_die("Error with connection to #{apn_host}: #{error}")
106
- end
107
-
108
- # Close open sockets
109
- def teardown_connection
110
- log(:info, "Closing connections...") if @opts[:verbose]
111
-
112
- begin
113
- @socket.close if @socket
114
- rescue Exception => e
115
- log(:error, "Error closing SSL Socket: #{e}")
116
- end
117
-
118
- begin
119
- @socket_tcp.close if @socket_tcp
120
- rescue Exception => e
121
- log(:error, "Error closing TCP Socket: #{e}")
122
- end
123
- end
124
-
125
- end
126
- end
127
- end
@@ -1,24 +0,0 @@
1
- module APN
2
- # This is the class that's actually enqueued via Resque when user calls +APN.notify+.
3
- # It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
4
- # workers of the +APN::Sender+ class.
5
- class NotificationJob
6
- # Behind the scenes, this is the name of our Resque queue
7
- @queue = APN::QUEUE_NAME
8
-
9
- # Build a notification from arguments and send to Apple
10
- def self.perform(token, opts)
11
- msg = APN::Notification.new(token, opts)
12
- raise "Invalid notification options (did you provide :alert, :badge, or :sound?): #{opts.inspect}" unless msg.valid?
13
-
14
- raise "APN::NotificationJob was picked up by a non-APN:Sender resque worker. Aborting." unless worker
15
- worker.send_to_apple(msg)
16
- end
17
-
18
-
19
- # Only execute this job in specialized APN::Sender workers, since
20
- # standard Resque workers don't maintain the persistent TCP connection.
21
- extend Resque::Plugins::AccessWorkerFromJob
22
- self.required_worker_class = 'APN::Sender'
23
- end
24
- end
@@ -1,57 +0,0 @@
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::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
- end
29
-
30
- end
31
-
32
- # Ensures we close any open sockets when the worker exits
33
- APN::QueueManager.before_unregister_worker do |worker|
34
- worker.send(:teardown_connection) if worker.respond_to?(:teardown_connection)
35
- end
36
-
37
-
38
- # # Run N jobs per fork, rather than creating a new fork for each notification
39
- # # By defunkt - http://gist.github.com/349376
40
- # APN::QueueManager.after_fork do |job|
41
- # # How many jobs should we process in each fork?
42
- # jobs_per_fork = 10
43
- #
44
- # # Set hook to nil to prevent running this hook over
45
- # # and over while processing more jobs in this fork.
46
- # Resque.after_fork = nil
47
- #
48
- # # Make sure we process jobs in the right order.
49
- # job.worker.process(job)
50
- #
51
- # # One less than specified because the child will run a
52
- # # final job after exiting this hook.
53
- # (jobs_per_fork.to_i - 1).times do
54
- # job.worker.process
55
- # end
56
- # end
57
-
@@ -1,4 +0,0 @@
1
- module APN
2
- # Change this to modify the queue from which notification jobs are pushed and pulled
3
- QUEUE_NAME = :apple_push_notifications
4
- end
@@ -1,49 +0,0 @@
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), <code>:cert_pass</code> 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
- # Use the <code>:cert_pass</code> option if your certificates require a password
15
- #
16
- # If a socket error is encountered, will teardown the connection and retry again twice before admitting defeat.
17
- class Sender < ::Resque::Worker
18
- include APN::Connection::Base
19
- TIMES_TO_RETRY_SOCKET_ERROR = 2
20
-
21
- # Send a raw string over the socket to Apple's servers (presumably already formatted by APN::Notification)
22
- def send_to_apple( notification, attempt = 0 )
23
- if attempt > TIMES_TO_RETRY_SOCKET_ERROR
24
- log_and_die("Error with connection to #{apn_host} (retried #{TIMES_TO_RETRY_SOCKET_ERROR} times): #{error}")
25
- end
26
-
27
- self.socket.write( notification.to_s )
28
- rescue SocketError => error
29
- log(:error, "Error with connection to #{apn_host} (attempt #{attempt}): #{error}")
30
-
31
- # Try reestablishing the connection
32
- teardown_connection
33
- setup_connection
34
- send_to_apple(notification, attempt + 1)
35
- end
36
-
37
- protected
38
-
39
- def apn_host
40
- @apn_host ||= apn_production? ? "gateway.push.apple.com" : "gateway.sandbox.push.apple.com"
41
- end
42
-
43
- def apn_port
44
- 2195
45
- end
46
-
47
- end
48
-
49
- end
@@ -1,30 +0,0 @@
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