apple_shove 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -37,6 +37,21 @@ Willing to give it a try? Onward...
37
37
 
38
38
  ## Usage
39
39
 
40
+ ### Quick Note on Certificate/Key Handling
41
+
42
+ AppleShove expects certificates and keys to be bundled together in PKCS#12 PEM format. If you want to test the validity of your PKCS#12 file, you can do something similar to this:
43
+
44
+ apns_p12 = File.read('my_cert.p12')
45
+ begin
46
+ AppleShove.try_p12(apns_p12)
47
+ rescue Exception => e
48
+ puts "the PKCS#12 file is invalid: #{e.message}"
49
+ else
50
+ puts "it's valid!"
51
+ end
52
+
53
+ If you send a notification with an invalid PKCS#12 file, the notification will fail downstream.
54
+
40
55
  ### Sending Notifications
41
56
 
42
57
  Sending a notification request looks like this:
@@ -81,6 +96,8 @@ We also have a feedback mechanism in place:
81
96
 
82
97
  ## Installation
83
98
 
99
+ If you haven't already, install redis. It's normally available via brew, apt-get, and yum, but you can also build from [source](http://redis.io/download).
100
+
84
101
  Add this line to your application's Gemfile:
85
102
 
86
103
  gem 'apple_shove'
@@ -6,10 +6,12 @@ module AppleShove
6
6
 
7
7
  attr_reader :last_used
8
8
 
9
- def initialize(opts = {})
10
- @host = opts[:host]
11
- @port = opts[:port]
12
- @certificate = opts[:certificate]
9
+ def initialize(host, port, p12_string)
10
+ @host = host
11
+ @port = port
12
+ @p12_string = p12_string
13
+
14
+ @p12 = nil
13
15
  end
14
16
 
15
17
  # lazy connect the socket
@@ -31,12 +33,13 @@ module AppleShove
31
33
  private
32
34
 
33
35
  def connect
34
- @sock = TCPSocket.new(@host, @port)
35
- @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
36
-
36
+ @p12 ||= OpenSSLHelper.pkcs12_from_pem(@p12_string)
37
37
  context = ::OpenSSL::SSL::SSLContext.new
38
- context.cert = ::OpenSSL::X509::Certificate.new(@certificate)
39
- context.key = ::OpenSSL::PKey::RSA.new(@certificate)
38
+ context.cert = @p12.certificate
39
+ context.key = @p12.key
40
+
41
+ @sock = TCPSocket.new(@host, @port)
42
+ @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
40
43
  @ssl_sock = ::OpenSSL::SSL::SSLSocket.new(@sock, context)
41
44
  @ssl_sock.sync = true
42
45
 
@@ -49,4 +52,4 @@ module AppleShove
49
52
 
50
53
  end
51
54
  end
52
- end
55
+ end
@@ -2,12 +2,10 @@ module AppleShove
2
2
  module APNS
3
3
  class FeedbackConnection < Connection
4
4
 
5
- def initialize(opts = {})
5
+ def initialize(p12, sandbox)
6
6
  host = "feedback.#{opts[:sandbox] ? 'sandbox.' : ''}push.apple.com"
7
7
 
8
- super certificate: opts[:certificate],
9
- host: host,
10
- port: 2196
8
+ super host, 2196, p12
11
9
  end
12
10
 
13
11
  def device_tokens
@@ -8,20 +8,18 @@ module AppleShove
8
8
  attr_accessor :pending_notifications
9
9
  attr_reader :name
10
10
 
11
- def initialize(opts = {})
12
- @name = self.class.generate_name(opts[:certificate], opts[:sandbox])
11
+ def initialize(p12, sandbox)
12
+ @name = self.class.generate_name(p12, sandbox)
13
13
  @last_message = nil
14
14
  @pending_notifications = 0
15
15
 
16
- host = "gateway.#{opts[:sandbox] ? 'sandbox.' : ''}push.apple.com"
16
+ host = "gateway.#{sandbox ? 'sandbox.' : ''}push.apple.com"
17
17
 
18
- super certificate: opts[:certificate],
19
- host: host,
20
- port: 2195
18
+ super host, 2195, p12
21
19
  end
22
20
 
23
- def self.generate_name(certificate, sandbox)
24
- Digest::SHA1.hexdigest("#{certificate}#{sandbox}")
21
+ def self.generate_name(p12, sandbox)
22
+ Digest::SHA1.hexdigest("#{p12}#{sandbox}")
25
23
  end
26
24
 
27
25
  exclusive
@@ -35,34 +33,37 @@ module AppleShove
35
33
  message = notification.binary_message
36
34
 
37
35
  if @last_used && Time.now - @last_used > CONFIG[:reconnect_timer] * 60
38
- Logger.info("#{@name}\trefreshing connection")
36
+ Logger.info("refreshing connection", self, notification)
39
37
  reconnect
40
38
  end
41
39
 
42
40
  begin
43
41
  socket.write message
44
42
  rescue Errno::EPIPE
45
- Logger.warn("#{@name}\tbroken pipe. reconnecting.")
43
+ Logger.warn("broken pipe. reconnecting.", self, notification)
46
44
  reconnect
47
45
  # EPIPE raises on the second write to a closed pipe. We need to resend
48
46
  # the previous notification that didn't make it through.
49
47
  socket.write @last_message if @last_message
50
48
  retry
51
49
  rescue Errno::ETIMEDOUT
52
- Logger.warn("#{@name}\ttimeout. reconnecting.")
50
+ Logger.warn("timeout. reconnecting.", self, notification)
53
51
  reconnect
54
52
  retry
53
+ rescue Exception => e
54
+ Logger.error("error sending notification: #{e.message}", self, notification)
55
+ else
56
+ Logger.info("delivered notification", self, notification)
55
57
  end
56
-
58
+
57
59
  @last_message = message
58
60
  @last_used = Time.now
59
61
  @pending_notifications -= 1
60
- Logger.info("#{@name}\tdelivered notification")
61
62
  end
62
63
 
63
64
  def shutdown
64
65
  while @pending_notifications > 0
65
- Logger.info("#{@name}\twaiting to shut down. #{@pending_notifications} job(s) remaining.")
66
+ Logger.info("waiting to shut down. #{@pending_notifications} job(s) remaining.", self)
66
67
  sleep 1
67
68
  end
68
69
 
@@ -72,4 +73,4 @@ module AppleShove
72
73
 
73
74
  end
74
75
  end
75
- end
76
+ end
@@ -1,7 +1,7 @@
1
1
  module AppleShove
2
2
 
3
- def self.notify(certificate, device_token, payload, sandbox = false)
4
- notification = Notification.new certificate: certificate,
3
+ def self.notify(p12, device_token, payload, sandbox = false)
4
+ notification = Notification.new p12: p12,
5
5
  device_token: device_token,
6
6
  payload: payload,
7
7
  sandbox: sandbox
@@ -12,9 +12,8 @@ module AppleShove
12
12
  true
13
13
  end
14
14
 
15
- def self.feedback_tokens(certificate, sandbox = false)
16
- conn = APNS::FeedbackConnection.new certificate: certificate,
17
- sandbox: sandbox
15
+ def self.feedback_tokens(p12, sandbox = false)
16
+ conn = APNS::FeedbackConnection.new p12, sandbox
18
17
 
19
18
  conn.device_tokens
20
19
  end
@@ -30,4 +29,10 @@ module AppleShove
30
29
  "queue size:\t#{size}"
31
30
  end
32
31
 
32
+ # raises an exception if the p12 string is invalid
33
+ def self.try_p12(p12)
34
+ OpenSSLHelper.pkcs12_from_pem(p12)
35
+ true
36
+ end
37
+
33
38
  end
@@ -31,16 +31,15 @@ module AppleShove
31
31
  private
32
32
 
33
33
  def get_connection(notification)
34
- key = APNS::NotifyConnection.generate_name(notification.certificate, notification.sandbox)
34
+ key = APNS::NotifyConnection.generate_name(notification.p12, notification.sandbox)
35
35
  connection = @connections[key]
36
36
 
37
37
  unless connection
38
38
  retire_oldest_connection if @connections.count >= @max_connections
39
39
 
40
- connection = APNS::NotifyConnection.new certificate: notification.certificate,
41
- sandbox: notification.sandbox
40
+ connection = APNS::NotifyConnection.new notification.p12, notification.sandbox
42
41
  @connections[key] = connection
43
- Logger.info "#{connection.name}\tcreated connection to APNS (#{@connections.count} total)"
42
+ Logger.info "created connection to APNS (#{@connections.count} total)", connection
44
43
  end
45
44
 
46
45
  connection
@@ -53,7 +52,7 @@ module AppleShove
53
52
  @connections.delete key
54
53
  conn.shutdown
55
54
 
56
- Logger.info "#{conn_name}\tdestroyed connection to APNS (#{@connections.count} total)"
55
+ Logger.info "destroyed connection to APNS (#{@connections.count} total)", connection
57
56
  end
58
57
  end
59
58
 
@@ -19,11 +19,35 @@ module AppleShove
19
19
  self
20
20
  end
21
21
 
22
- def self.error(msg); instance.error(msg) end
23
- def self.debug(msg); instance.debug(msg) end
24
- def self.fatal(msg); instance.fatal(msg) end
25
- def self.info(msg); instance.info(msg) end
26
- def self.warn(msg); instance.warn(msg) end
27
-
22
+ def self.error(msg, connection = nil, notification = nil)
23
+ log('error', msg, connection, notification)
24
+ end
25
+
26
+ def self.debug(msg, connection = nil, notification = nil)
27
+ log('debug', msg, connection, notification)
28
+ end
29
+
30
+ def self.fatal(msg, connection = nil, notification = nil)
31
+ log('fatal', msg, connection, notification)
32
+ end
33
+
34
+ def self.info(msg, connection = nil, notification = nil)
35
+ log('info', msg, connection, notification)
36
+ end
37
+
38
+ def self.warn(msg, connection = nil, notification = nil)
39
+ log('warn', msg, connection, notification)
40
+ end
41
+
42
+ private
43
+
44
+ def self.log(severity, msg, connection, notification)
45
+ output = ''
46
+ output += "#{connection.name}\t" if connection && connection.respond_to?("name")
47
+ output += "#{notification.device_token}\t" if notification
48
+ output += msg
49
+
50
+ instance.send(severity, output)
51
+ end
28
52
  end
29
53
  end
@@ -1,7 +1,7 @@
1
1
  module AppleShove
2
2
  class Notification
3
3
 
4
- attr_accessor :certificate, :sandbox, :device_token, :payload
4
+ attr_accessor :p12, :sandbox, :device_token, :payload
5
5
 
6
6
  def initialize(attributes = {})
7
7
  attributes.each { |k, v| self.send("#{k}=", v) }
@@ -31,4 +31,4 @@ module AppleShove
31
31
  end
32
32
 
33
33
  end
34
- end
34
+ end
@@ -0,0 +1,15 @@
1
+ require 'openssl'
2
+
3
+ module AppleShove
4
+ class OpenSSLHelper
5
+
6
+ def self.pkcs12_from_pem(p12_pem)
7
+ key = ::OpenSSL::PKey::RSA.new p12_pem
8
+ cert = ::OpenSSL::X509::Certificate.new p12_pem
9
+ p12 = ::OpenSSL::PKCS12.create nil, nil, key, cert
10
+
11
+ p12
12
+ end
13
+
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module AppleShove
2
- VERSION = "1.0.2"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -0,0 +1,10 @@
1
+ require 'apple_shove'
2
+
3
+ describe AppleShove do
4
+
5
+ it 'detects an invalid p12' do
6
+ p12 = 'this is an invalid p12'
7
+ expect { AppleShove.try_p12(p12) }.to raise_error
8
+ end
9
+
10
+ end
@@ -1,12 +1,12 @@
1
1
  module NotificationHelper
2
2
 
3
3
  def generate_notification
4
- certificate = "DummyCertificate"
4
+ p12 = "DummyP12"
5
5
  sandbox = false
6
6
  device_token = hex(64)
7
7
  payload = { mdm: "#{hex(8)}-#{hex(4)}-#{hex(4)}-#{hex(4)}-#{hex(12)}".downcase }
8
8
 
9
- AppleShove::Notification.new certificate: certificate,
9
+ AppleShove::Notification.new p12: p12,
10
10
  sandbox: sandbox,
11
11
  device_token: device_token,
12
12
  payload: payload
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apple_shove
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-12 00:00:00.000000000 Z
12
+ date: 2013-05-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -130,10 +130,12 @@ files:
130
130
  - lib/apple_shove/logger.rb
131
131
  - lib/apple_shove/notification.rb
132
132
  - lib/apple_shove/notification_queue.rb
133
+ - lib/apple_shove/openssl_helper.rb
133
134
  - lib/apple_shove/tasks.rb
134
135
  - lib/apple_shove/version.rb
135
136
  - lib/tasks/apple_shove.rake
136
137
  - script/daemon
138
+ - spec/apple_shove_spec.rb
137
139
  - spec/demultiplexer_spec.rb
138
140
  - spec/notification_helper.rb
139
141
  - spec/notification_queue_spec.rb
@@ -153,7 +155,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
153
155
  version: '0'
154
156
  segments:
155
157
  - 0
156
- hash: 3074046605150142324
158
+ hash: 2826984217209674541
157
159
  required_rubygems_version: !ruby/object:Gem::Requirement
158
160
  none: false
159
161
  requirements:
@@ -162,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
164
  version: '0'
163
165
  segments:
164
166
  - 0
165
- hash: 3074046605150142324
167
+ hash: 2826984217209674541
166
168
  requirements: []
167
169
  rubyforge_project:
168
170
  rubygems_version: 1.8.24
@@ -170,6 +172,7 @@ signing_key:
170
172
  specification_version: 3
171
173
  summary: ''
172
174
  test_files:
175
+ - spec/apple_shove_spec.rb
173
176
  - spec/demultiplexer_spec.rb
174
177
  - spec/notification_helper.rb
175
178
  - spec/notification_queue_spec.rb