em-apn 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +78 -0
- data/Rakefile +9 -0
- data/certs/.gitignore +0 -0
- data/em-apn.gemspec +24 -0
- data/lib/em-apn.rb +23 -0
- data/lib/em-apn/client.rb +53 -0
- data/lib/em-apn/connection.rb +46 -0
- data/lib/em-apn/error_response.rb +34 -0
- data/lib/em-apn/log_message.rb +13 -0
- data/lib/em-apn/notification.rb +63 -0
- data/lib/em-apn/response.rb +13 -0
- data/lib/em-apn/server.rb +58 -0
- data/lib/em-apn/test_helper.rb +37 -0
- data/lib/em-apn/version.rb +5 -0
- data/script/push +29 -0
- data/script/server +17 -0
- data/script/test +36 -0
- data/spec/em-apn/client_spec.rb +166 -0
- data/spec/em-apn/notification_spec.rb +137 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/certs/cert.pem +17 -0
- data/spec/support/certs/key.pem +15 -0
- metadata +109 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT-License)
|
2
|
+
|
3
|
+
Copyright (c) 2011 GroupMe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# EM-APN - EventMachine'd Apple Push Notifications #
|
2
|
+
|
3
|
+
We want:
|
4
|
+
|
5
|
+
* Streamlined for a persistent connection use-case
|
6
|
+
* Support for the enhanced protocol, with receipts
|
7
|
+
|
8
|
+
## Usage ##
|
9
|
+
|
10
|
+
In a nutshell:
|
11
|
+
|
12
|
+
require "em-apn"
|
13
|
+
|
14
|
+
# Inside a reactor...
|
15
|
+
notification = EM::APN::Notification.new(token, :alert => alert)
|
16
|
+
client = EM::APN::Client.connect
|
17
|
+
client.deliver(notification)
|
18
|
+
|
19
|
+
Using this interface, the easiest way to configure the connection is by setting
|
20
|
+
some environment variables so that EM::APN can find your SSL certificates:
|
21
|
+
|
22
|
+
ENV["APN_KEY"] = "/path/to/key.pem"
|
23
|
+
ENV["APN_CERT"] = "/path/to/cert.pem"
|
24
|
+
|
25
|
+
Also, by default, the library connects to Apple's sandbox push server. If you
|
26
|
+
want to connect to the production server, simply set the `APN_ENV`
|
27
|
+
environment variable to `production`:
|
28
|
+
|
29
|
+
ENV["APN_ENV"] = "production"
|
30
|
+
|
31
|
+
The gateway and SSL certs can also be set directly when instantiating the object:
|
32
|
+
|
33
|
+
client = EM::APN::Client.connect(
|
34
|
+
:gateway => "some.host",
|
35
|
+
:key => "/path/to/key.pem",
|
36
|
+
:cert => "/path/to/cert.pem"
|
37
|
+
)
|
38
|
+
|
39
|
+
The client manages an underlying `EM::Connection`, and it will automatically
|
40
|
+
reconnect to the gateway when the connection is closed. Callbacks can be set
|
41
|
+
on the client to handle error responses from the gateway and connection close
|
42
|
+
events:
|
43
|
+
|
44
|
+
client = EM::APN::Client.connect
|
45
|
+
client.on_error do |response|
|
46
|
+
# See EM::APN::ErrorResponse
|
47
|
+
end
|
48
|
+
|
49
|
+
client.on_close do
|
50
|
+
# Do something.
|
51
|
+
end
|
52
|
+
|
53
|
+
In our experience, we've found that Apple immediately closes the connection
|
54
|
+
whenever an error is detected, so the error and close callbacks are nearly
|
55
|
+
always called one-to-one. These methods exist as a convenience, and the
|
56
|
+
callbacks can also be set directly to anything that responds to `#call`:
|
57
|
+
|
58
|
+
client.error_callback = Proc.new { |response| ... }
|
59
|
+
client.close_callback = Proc.new { ... }
|
60
|
+
|
61
|
+
### Max Payload Size ###
|
62
|
+
|
63
|
+
Apple enforces a limit of __256 bytes__ for the __entire payload__.
|
64
|
+
|
65
|
+
We raise an `EM::APN::Notification::PayloadTooLarge` exception.
|
66
|
+
|
67
|
+
How you truncate your payloads is up to you. Be especially careful when dealing with multi-byte data.
|
68
|
+
|
69
|
+
## TODO ##
|
70
|
+
|
71
|
+
* Support the feedback API for dead tokens
|
72
|
+
|
73
|
+
## Inspiration ##
|
74
|
+
|
75
|
+
Much thanks to:
|
76
|
+
|
77
|
+
* https://github.com/kdonovan/apn_sender
|
78
|
+
* http://blog.technopathllc.com/2010/12/apples-push-notification-with-ruby.html
|
data/Rakefile
ADDED
data/certs/.gitignore
ADDED
File without changes
|
data/em-apn.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "em-apn/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "em-apn"
|
7
|
+
s.version = EventMachine::APN::VERSION
|
8
|
+
s.authors = ["Dave Yeu"]
|
9
|
+
s.email = ["daveyeu@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{EventMachine-driven Apple Push Notifications}
|
12
|
+
|
13
|
+
s.rubyforge_project = "em-apn"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency "eventmachine", ">= 1.0.0.beta.3"
|
21
|
+
s.add_dependency "yajl-ruby", ">= 0.8.2"
|
22
|
+
|
23
|
+
s.add_development_dependency "rspec", "~> 2.6.0"
|
24
|
+
end
|
data/lib/em-apn.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "eventmachine"
|
4
|
+
require "yajl"
|
5
|
+
require "logger"
|
6
|
+
require "em-apn/client"
|
7
|
+
require "em-apn/connection"
|
8
|
+
require "em-apn/notification"
|
9
|
+
require "em-apn/log_message"
|
10
|
+
require "em-apn/response"
|
11
|
+
require "em-apn/error_response"
|
12
|
+
|
13
|
+
module EventMachine
|
14
|
+
module APN
|
15
|
+
def self.logger
|
16
|
+
@logger ||= Logger.new(STDOUT)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.logger=(new_logger)
|
20
|
+
@logger = new_logger
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module APN
|
5
|
+
class Client
|
6
|
+
SANDBOX_GATEWAY = "gateway.sandbox.push.apple.com"
|
7
|
+
PRODUCTION_GATEWAY = "gateway.push.apple.com"
|
8
|
+
PORT = 2195
|
9
|
+
|
10
|
+
attr_reader :gateway, :port, :key, :cert, :connection, :error_callback, :close_callback
|
11
|
+
|
12
|
+
# A convenience method for creating and connecting.
|
13
|
+
def self.connect(options = {})
|
14
|
+
new(options).tap do |client|
|
15
|
+
client.connect
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
@key = options[:key] || ENV["APN_KEY"]
|
21
|
+
@cert = options[:cert] || ENV["APN_CERT"]
|
22
|
+
@port = options[:port] || PORT
|
23
|
+
|
24
|
+
@gateway = options[:gateway] || ENV["APN_GATEWAY"]
|
25
|
+
@gateway ||= (ENV["APN_ENV"] == "production") ? PRODUCTION_GATEWAY : SANDBOX_GATEWAY
|
26
|
+
|
27
|
+
@connection = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def connect
|
31
|
+
@connection = EM.connect(gateway, port, Connection, self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def deliver(notification)
|
35
|
+
connect if connection.nil? || connection.disconnected?
|
36
|
+
log(notification)
|
37
|
+
connection.send_data(notification.data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_error(&block)
|
41
|
+
@error_callback = block
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_close(&block)
|
45
|
+
@close_callback = block
|
46
|
+
end
|
47
|
+
|
48
|
+
def log(notification)
|
49
|
+
EM::APN.logger.info("TOKEN=#{notification.token} ALERT=#{notification.alert}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module APN
|
3
|
+
class Connection < EM::Connection
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@client = args.last
|
9
|
+
@disconnected = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def disconnected?
|
13
|
+
@disconnected
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
start_tls(
|
18
|
+
:private_key_file => client.key,
|
19
|
+
:cert_chain_file => client.cert,
|
20
|
+
:verify_peer => false
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection_completed
|
25
|
+
EM::APN.logger.info("Connection completed")
|
26
|
+
end
|
27
|
+
|
28
|
+
def receive_data(data)
|
29
|
+
data_array = data.unpack("ccN")
|
30
|
+
error_response = ErrorResponse.new(*data_array)
|
31
|
+
EM::APN.logger.warn(error_response.to_s)
|
32
|
+
|
33
|
+
if client.error_callback
|
34
|
+
client.error_callback.call(error_response)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def unbind
|
39
|
+
@disconnected = true
|
40
|
+
|
41
|
+
EM::APN.logger.info("Connection closed")
|
42
|
+
client.close_callback.call if client.close_callback
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module APN
|
3
|
+
class ErrorResponse
|
4
|
+
DESCRIPTION = {
|
5
|
+
0 => "No errors encountered",
|
6
|
+
1 => "Processing error",
|
7
|
+
2 => "Missing device token",
|
8
|
+
3 => "Missing topic",
|
9
|
+
4 => "Missing payload",
|
10
|
+
5 => "Invalid token size",
|
11
|
+
6 => "Invalid topic size",
|
12
|
+
7 => "Invalid payload size",
|
13
|
+
8 => "Invalid token",
|
14
|
+
255 => "None (unknown)"
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_reader :command, :status_code, :identifier
|
18
|
+
|
19
|
+
def initialize(command, status_code, identifier)
|
20
|
+
@command = command
|
21
|
+
@status_code = status_code
|
22
|
+
@identifier = identifier
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"CODE=#{@status_code} ID=#{@identifier} DESC=#{description}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def description
|
30
|
+
DESCRIPTION[@status_code] || "Missing description"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module APN
|
5
|
+
class Notification
|
6
|
+
PAYLOAD_MAX_BYTES = 256
|
7
|
+
class PayloadTooLarge < StandardError;end
|
8
|
+
|
9
|
+
attr_reader :token
|
10
|
+
attr_accessor :identifier, :expiry
|
11
|
+
|
12
|
+
def initialize(token, aps = {}, custom = {}, options = {})
|
13
|
+
raise "Bad push token: #{token}" if token.nil? || (token.length != 64)
|
14
|
+
|
15
|
+
@token = token
|
16
|
+
@aps = aps
|
17
|
+
@custom = custom
|
18
|
+
|
19
|
+
self.identifier = options[:identifier] if options[:identifier]
|
20
|
+
self.expiry = options[:expiry] if options[:expiry]
|
21
|
+
end
|
22
|
+
|
23
|
+
def payload
|
24
|
+
@payload ||= build_payload
|
25
|
+
end
|
26
|
+
|
27
|
+
def data
|
28
|
+
@data ||= build_data
|
29
|
+
end
|
30
|
+
|
31
|
+
def identifier=(new_identifier)
|
32
|
+
@identifier = new_identifier.to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
def alert
|
36
|
+
@aps["alert"][0..49] if @aps.include?("alert")
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_payload
|
42
|
+
payload = @custom.merge(:aps => @aps)
|
43
|
+
Yajl::Encoder.encode(payload)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Documentation about this format is here:
|
47
|
+
# http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
|
48
|
+
def build_data
|
49
|
+
identifier = @identifier || 0
|
50
|
+
expiry = @expiry || 0
|
51
|
+
|
52
|
+
size = [payload].pack("a*").size
|
53
|
+
data_array = [1, identifier, expiry, 32, token, size, payload]
|
54
|
+
data = data_array.pack("cNNnH*na*")
|
55
|
+
if data.size > PAYLOAD_MAX_BYTES
|
56
|
+
error = "max is #{PAYLOAD_MAX_BYTES} bytes (got #{data.size})"
|
57
|
+
raise PayloadTooLarge.new(error)
|
58
|
+
end
|
59
|
+
data
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Mock Apple push server... because we love to test
|
3
|
+
module EventMachine
|
4
|
+
module APN
|
5
|
+
module Server
|
6
|
+
def post_init
|
7
|
+
EM::APN.logger.info("Received a new connection")
|
8
|
+
@data = ""
|
9
|
+
|
10
|
+
start_tls(
|
11
|
+
:cert_chain_file => ENV["APN_CERT"],
|
12
|
+
:private_key_file => ENV["APN_KEY"],
|
13
|
+
:verify_peer => false
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ssl_handshake_completed
|
18
|
+
EM::APN.logger.info("SSL handshake completed")
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_data(data)
|
22
|
+
@data << data
|
23
|
+
|
24
|
+
# Try to extract the payload header
|
25
|
+
headers = @data.unpack("cNNnH64n")
|
26
|
+
return if headers.last.nil?
|
27
|
+
|
28
|
+
# Try to grab the payload
|
29
|
+
payload_size = headers.last
|
30
|
+
payload = @data[45, payload_size]
|
31
|
+
return if payload.length != payload_size
|
32
|
+
|
33
|
+
@data = @data[45 + payload_size, -1] || ""
|
34
|
+
|
35
|
+
process(headers, payload)
|
36
|
+
end
|
37
|
+
|
38
|
+
def process(headers, payload)
|
39
|
+
message = "APN RECV #{headers[4]} #{payload}"
|
40
|
+
EM::APN.logger.info(message)
|
41
|
+
|
42
|
+
args = Yajl::Parser.parse(payload)
|
43
|
+
|
44
|
+
# If the alert is 'DISCONNECT', then we fake a bad payload by replying
|
45
|
+
# with an error and disconnecting.
|
46
|
+
if args["aps"]["alert"] == "DISCONNECT"
|
47
|
+
EM::APN.logger.info("Disconnecting")
|
48
|
+
send_data([8, 1, 0].pack("ccN"))
|
49
|
+
close_connection_after_writing
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def unbind
|
54
|
+
EM::APN.logger.info("Connection closed")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Test helper for EM::APN
|
2
|
+
#
|
3
|
+
# To use this, start by simply requiring this file after EM::APN has already
|
4
|
+
# been loaded
|
5
|
+
#
|
6
|
+
# require "em-apn"
|
7
|
+
# require "em-apn/test_helper"
|
8
|
+
#
|
9
|
+
# This will nullify actual deliveries and instead, push them onto an accessible
|
10
|
+
# list:
|
11
|
+
#
|
12
|
+
# expect {
|
13
|
+
# client.deliver('notification)
|
14
|
+
# }.to change { EM::APN.deliveries.size }.by(1)
|
15
|
+
#
|
16
|
+
# notification = EM::APN.deliveries.first
|
17
|
+
# notification.should be_an_instance_of(EM::APN::Notification)
|
18
|
+
# notification.payload.should == ...
|
19
|
+
#
|
20
|
+
module EventMachine
|
21
|
+
module APN
|
22
|
+
def self.deliveries
|
23
|
+
@deliveries ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
Client.class_eval do
|
27
|
+
def connect
|
28
|
+
# No-op
|
29
|
+
end
|
30
|
+
|
31
|
+
def deliver(notification)
|
32
|
+
log(notification)
|
33
|
+
EM::APN.deliveries << notification
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/script/push
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Send a single push notification
|
4
|
+
#
|
5
|
+
# $ script/push <TOKEN> "<ALERT>"
|
6
|
+
#
|
7
|
+
|
8
|
+
require "rubygems"
|
9
|
+
require "bundler/setup"
|
10
|
+
require "em-apn"
|
11
|
+
|
12
|
+
ENV["APN_KEY"] ||= File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
|
13
|
+
ENV["APN_CERT"] ||= File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
|
14
|
+
|
15
|
+
token, alert = ARGV
|
16
|
+
|
17
|
+
if token.nil? || alert.nil?
|
18
|
+
puts "Usage: script/push <TOKEN> \"<ALERT>\""
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
EM.run do
|
23
|
+
client = EM::APN::Client.connect
|
24
|
+
client.deliver(EM::APN::Notification.new(token, :alert => alert))
|
25
|
+
|
26
|
+
# Hopefully give ourselves enough time to receive a response on failure.
|
27
|
+
# Wish there was a better way to do this. Or at least a more timely way.
|
28
|
+
EM.add_timer(1) { EM.stop_event_loop }
|
29
|
+
end
|
data/script/server
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Run a mock Apple push server
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "bundler/setup"
|
7
|
+
require "em-apn"
|
8
|
+
require "em-apn/server"
|
9
|
+
|
10
|
+
ENV["APN_KEY"] = File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
|
11
|
+
ENV["APN_CERT"] = File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
|
12
|
+
|
13
|
+
EM::APN.logger.info("Starting push server")
|
14
|
+
|
15
|
+
EM.run do
|
16
|
+
EM.start_server("127.0.0.1", 2195, EM::APN::Server)
|
17
|
+
end
|
data/script/test
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "bundler/setup"
|
5
|
+
require "em-apn"
|
6
|
+
|
7
|
+
ENV["APN_KEY"] = File.join(File.dirname(__FILE__), "..", "certs", "key.pem")
|
8
|
+
ENV["APN_CERT"] = File.join(File.dirname(__FILE__), "..", "certs", "cert.pem")
|
9
|
+
|
10
|
+
TOKEN = "fe9515ba7556cfdcfca6530235c95dff682fac765838e749a201a7f6cf3792e6"
|
11
|
+
|
12
|
+
def notify(client, queue)
|
13
|
+
if queue.empty?
|
14
|
+
EM.add_periodic_timer(1) { EM.stop }
|
15
|
+
else
|
16
|
+
queue.pop do |alert|
|
17
|
+
notification = EM::APN::Notification.new(TOKEN, :alert => alert)
|
18
|
+
client.deliver(notification)
|
19
|
+
|
20
|
+
interval = rand(20).to_f / 100.0
|
21
|
+
EM.add_timer(interval) { notify(client, queue) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
EM.run do
|
27
|
+
client = EM::APN::Client.new(:gateway => "127.0.0.1")
|
28
|
+
queue = EM::Queue.new
|
29
|
+
queue.push *((1..5).to_a)
|
30
|
+
queue.push "DISCONNECT"
|
31
|
+
queue.push *((6..10).to_a)
|
32
|
+
queue.push "DISCONNECT"
|
33
|
+
queue.push *((11..15).to_a)
|
34
|
+
|
35
|
+
EM.next_tick { notify(client, queue) }
|
36
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe EventMachine::APN::Client do
|
4
|
+
def new_client(*args)
|
5
|
+
client = nil
|
6
|
+
EM.run_block {
|
7
|
+
client = EM.connect("localhost", 8888, EventMachine::APN::Client, *args)
|
8
|
+
}
|
9
|
+
client
|
10
|
+
end
|
11
|
+
|
12
|
+
describe ".new" do
|
13
|
+
it "creates a new client without a connection" do
|
14
|
+
client = EM::APN::Client.new
|
15
|
+
client.connection.should be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
context "configuring the gateway" do
|
19
|
+
before do
|
20
|
+
ENV["APN_GATEWAY"] = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:options) { {:key => ENV["APN_KEY"], :cert => ENV["APN_CERT"]} }
|
24
|
+
|
25
|
+
it "defaults to Apple's sandbox (gateway.sandbox.push.apple.com)" do
|
26
|
+
client = EM::APN::Client.new
|
27
|
+
client.gateway.should == "gateway.sandbox.push.apple.com"
|
28
|
+
client.port.should == 2195
|
29
|
+
end
|
30
|
+
|
31
|
+
it "uses an environment variable for the gateway host (APN_GATEWAY) if specified" do
|
32
|
+
ENV["APN_GATEWAY"] = "localhost"
|
33
|
+
|
34
|
+
client = EM::APN::Client.new
|
35
|
+
client.gateway.should == "localhost"
|
36
|
+
client.port.should == 2195
|
37
|
+
end
|
38
|
+
|
39
|
+
it "switches to the production gateway if APN_ENV is set to 'production'" do
|
40
|
+
ENV["APN_ENV"] = "production"
|
41
|
+
|
42
|
+
client = EM::APN::Client.new
|
43
|
+
client.gateway.should == "gateway.push.apple.com"
|
44
|
+
client.port.should == 2195
|
45
|
+
|
46
|
+
ENV["APN_ENV"] = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "takes arguments for the gateway and port" do
|
50
|
+
client = EM::APN::Client.new(:gateway => "localhost", :port => 3333)
|
51
|
+
client.gateway.should == "localhost"
|
52
|
+
client.port.should == 3333
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "configuring SSL" do
|
57
|
+
it "falls back to environment variables for key and cert (APN_KEY and APN_CERT) if they are unspecified" do
|
58
|
+
ENV["APN_KEY"] = "path/to/key.pem"
|
59
|
+
ENV["APN_CERT"] = "path/to/cert.pem"
|
60
|
+
|
61
|
+
client = EM::APN::Client.new
|
62
|
+
client.key.should == "path/to/key.pem"
|
63
|
+
client.cert.should == "path/to/cert.pem"
|
64
|
+
end
|
65
|
+
|
66
|
+
it "takes arguments for the key and cert" do
|
67
|
+
client = EM::APN::Client.new(:key => "key.pem", :cert => "cert.pem")
|
68
|
+
client.key.should == "key.pem"
|
69
|
+
client.cert.should == "cert.pem"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#connect" do
|
75
|
+
it "creates a connection to the gateway" do
|
76
|
+
client = EM::APN::Client.new
|
77
|
+
client.connection.should be_nil
|
78
|
+
|
79
|
+
EM.run_block { client.connect }
|
80
|
+
client.connection.should be_an_instance_of(EM::APN::Connection)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "passes the client to the new connection" do
|
84
|
+
client = EM::APN::Client.new
|
85
|
+
connection = double(EM::APN::Connection).as_null_object
|
86
|
+
|
87
|
+
EM::APN::Connection.should_receive(:new).with(instance_of(Fixnum), client).and_return(connection)
|
88
|
+
EM.run_block { client.connect }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#deliver" do
|
93
|
+
let(:token) { "fe9515ba7556cfdcfca6530235c95dff682fac765838e749a201a7f6cf3792e6" }
|
94
|
+
|
95
|
+
it "sends a Notification object" do
|
96
|
+
notification = EM::APN::Notification.new(token, :alert => "Hello world")
|
97
|
+
delivered = nil
|
98
|
+
|
99
|
+
EM.run_block do
|
100
|
+
client = EM::APN::Client.new
|
101
|
+
client.connect
|
102
|
+
client.connection.stub(:send_data).and_return do |data|
|
103
|
+
delivered = data.unpack("cNNnH64na*")
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
client.deliver(notification)
|
107
|
+
end
|
108
|
+
|
109
|
+
delivered[4].should == token
|
110
|
+
delivered[6].should == Yajl::Encoder.encode({:aps => {:alert => "Hello world"}})
|
111
|
+
end
|
112
|
+
|
113
|
+
it "logs a message" do
|
114
|
+
alert = "Hello world this is a long push notification to you"
|
115
|
+
|
116
|
+
test_log = StringIO.new
|
117
|
+
EM::APN.logger = Logger.new(test_log)
|
118
|
+
|
119
|
+
notification = EM::APN::Notification.new(token, "alert" => alert)
|
120
|
+
|
121
|
+
EM.run_block do
|
122
|
+
client = EM::APN::Client.new
|
123
|
+
client.deliver(notification)
|
124
|
+
end
|
125
|
+
|
126
|
+
test_log.rewind
|
127
|
+
test_log.read.should include("TOKEN=#{token} ALERT=#{alert[0..49]}")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "#on_error" do
|
132
|
+
it "sets a callback that is invoked when we receive data from Apple" do
|
133
|
+
error = nil
|
134
|
+
|
135
|
+
EM.run_block do
|
136
|
+
client = EM::APN::Client.new
|
137
|
+
client.connect
|
138
|
+
client.on_error { |e| error = e }
|
139
|
+
client.connection.receive_data([8, 8, 0].pack("ccN"))
|
140
|
+
end
|
141
|
+
|
142
|
+
error.should be
|
143
|
+
|
144
|
+
error.command.should == 8
|
145
|
+
error.status_code.should == 8
|
146
|
+
error.identifier.should == 0
|
147
|
+
|
148
|
+
error.description.should == "Invalid token"
|
149
|
+
error.to_s.should == "CODE=8 ID=0 DESC=Invalid token"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "#on_close" do
|
154
|
+
it "sets a callback that is invoked when the connection closes" do
|
155
|
+
called = false
|
156
|
+
|
157
|
+
EM.run_block do
|
158
|
+
client = EM::APN::Client.new
|
159
|
+
client.on_close { called = true }
|
160
|
+
client.connect # This should unbind immediately.
|
161
|
+
end
|
162
|
+
|
163
|
+
called.should be_true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe EventMachine::APN::Notification do
|
5
|
+
let(:token) { "fe9515ba7556cfdcfca6530235c95dff682fac765838e749a201a7f6cf3792e6" }
|
6
|
+
|
7
|
+
describe "#initialize" do
|
8
|
+
it "raises an exception if the token is blank" do
|
9
|
+
expect { EM::APN::Notification.new(nil) }.to raise_error
|
10
|
+
expect { EM::APN::Notification.new("") }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises an exception if the token is less than or greater than 32 bytes" do
|
14
|
+
expect { EM::APN::Notification.new("0" * 63) }.to raise_error
|
15
|
+
expect { EM::APN::Notification.new("0" * 65) }.to raise_error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#token" do
|
20
|
+
it "returns the token" do
|
21
|
+
notification = EM::APN::Notification.new(token)
|
22
|
+
notification.token.should == token
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#payload" do
|
27
|
+
it "returns aps properties encoded as JSON" do
|
28
|
+
notification = EM::APN::Notification.new(token, {
|
29
|
+
:alert => "Hello world",
|
30
|
+
:badge => 10,
|
31
|
+
:sound => "ding.aiff"
|
32
|
+
})
|
33
|
+
payload = Yajl::Parser.parse(notification.payload)
|
34
|
+
payload["aps"]["alert"].should == "Hello world"
|
35
|
+
payload["aps"]["badge"].should == 10
|
36
|
+
payload["aps"]["sound"].should == "ding.aiff"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns custom properties as well" do
|
40
|
+
notification = EM::APN::Notification.new(token, {}, {:line => "I'm super bad"})
|
41
|
+
payload = Yajl::Parser.parse(notification.payload)
|
42
|
+
payload["line"].should == "I'm super bad"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "handles string keys" do
|
46
|
+
notification = EM::APN::Notification.new(token,
|
47
|
+
{
|
48
|
+
"alert" => "Hello world",
|
49
|
+
"badge" => 10,
|
50
|
+
"sound" => "ding.aiff"
|
51
|
+
},
|
52
|
+
{
|
53
|
+
"custom" => "param"
|
54
|
+
}
|
55
|
+
)
|
56
|
+
payload = Yajl::Parser.parse(notification.payload)
|
57
|
+
payload["aps"]["alert"].should == "Hello world"
|
58
|
+
payload["aps"]["badge"].should == 10
|
59
|
+
payload["aps"]["sound"].should == "ding.aiff"
|
60
|
+
payload["custom"].should == "param"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#data" do
|
65
|
+
it "returns the enhanced notification in the supported binary format" do
|
66
|
+
notification = EM::APN::Notification.new(token, {:alert => "Hello world"})
|
67
|
+
data = notification.data.unpack("cNNnH64na*")
|
68
|
+
data[4].should == token
|
69
|
+
data[5].should == notification.payload.length
|
70
|
+
data[6].should == notification.payload
|
71
|
+
end
|
72
|
+
|
73
|
+
it "handles UTF-8 payloads" do
|
74
|
+
string = "✓ Please"
|
75
|
+
notification = EM::APN::Notification.new(token, {:alert => string})
|
76
|
+
data = notification.data.unpack("cNNnH64na*")
|
77
|
+
data[4].should == token
|
78
|
+
data[5].should == [notification.payload].pack("a*").size
|
79
|
+
data[6].force_encoding("UTF-8").should == notification.payload
|
80
|
+
end
|
81
|
+
|
82
|
+
it "defaults the identifier and expiry to 0" do
|
83
|
+
notification = EM::APN::Notification.new(token, {:alert => "Hello world"})
|
84
|
+
data = notification.data.unpack("cNNnH64na*")
|
85
|
+
data[1].should == 0 # Identifier
|
86
|
+
data[2].should == 0 # Expiry
|
87
|
+
end
|
88
|
+
|
89
|
+
it "raises PayloadTooLarge error if PAYLOAD_MAX_BYTES exceeded" do
|
90
|
+
notification = EM::APN::Notification.new(token, {:alert => "X" * 512})
|
91
|
+
|
92
|
+
lambda {
|
93
|
+
notification.data
|
94
|
+
}.should raise_error(EM::APN::Notification::PayloadTooLarge)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#identifier=" do
|
99
|
+
it "sets the identifier, which is returned in the binary data" do
|
100
|
+
notification = EM::APN::Notification.new(token)
|
101
|
+
notification.identifier = 12345
|
102
|
+
notification.identifier.should == 12345
|
103
|
+
|
104
|
+
data = notification.data.unpack("cNNnH64na*")
|
105
|
+
data[1].should == notification.identifier
|
106
|
+
end
|
107
|
+
|
108
|
+
it "converts everything to an integer" do
|
109
|
+
notification = EM::APN::Notification.new(token)
|
110
|
+
notification.identifier = "12345"
|
111
|
+
notification.identifier.should == 12345
|
112
|
+
end
|
113
|
+
|
114
|
+
it "can be set in the initializer" do
|
115
|
+
notification = EM::APN::Notification.new(token, {}, {}, {:identifier => 12345})
|
116
|
+
notification.identifier.should == 12345
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe "#expiry=" do
|
121
|
+
it "sets the expiry, which is returned in the binary data" do
|
122
|
+
epoch = Time.now.to_i
|
123
|
+
notification = EM::APN::Notification.new(token)
|
124
|
+
notification.expiry = epoch
|
125
|
+
notification.expiry.should == epoch
|
126
|
+
|
127
|
+
data = notification.data.unpack("cNNnH64na*")
|
128
|
+
data[2].should == epoch
|
129
|
+
end
|
130
|
+
|
131
|
+
it "can be set in the initializer" do
|
132
|
+
epoch = Time.now.to_i
|
133
|
+
notification = EM::APN::Notification.new(token, {}, {}, {:expiry => epoch})
|
134
|
+
notification.expiry.should == epoch
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
Bundler.require :default, :development
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.before(:each) do
|
7
|
+
ENV["APN_KEY"] = "spec/support/certs/key.pem"
|
8
|
+
ENV["APN_CERT"] = "spec/support/certs/cert.pem"
|
9
|
+
ENV["APN_GATEWAY"] = "127.0.0.1"
|
10
|
+
|
11
|
+
EM::APN.logger = Logger.new("/dev/null")
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICqjCCAhOgAwIBAgIJAO3gD8Fv66MhMA0GCSqGSIb3DQEBBQUAMEMxCzAJBgNV
|
3
|
+
BAYTAlVTMREwDwYDVQQIEwhOZXcgWW9yazERMA8GA1UEBxMITmV3IFlvcmsxDjAM
|
4
|
+
BgNVBAoTBUR1bW15MB4XDTExMDYyMTE5MDAyOFoXDTIxMDYxODE5MDAyOFowQzEL
|
5
|
+
MAkGA1UEBhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9y
|
6
|
+
azEOMAwGA1UEChMFRHVtbXkwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMsR
|
7
|
+
05wNdOiFXil3/r4xGLXc7MYwU2LiZbnXAqaoGnLv3W8cN7i/0i63y8YYITNWm3ji
|
8
|
+
lMiOkj2+TNVE/PiFizFBqhT28V/AsfOsiJrYQ4GXwLwJTsgDGTwAJgov0yPIffhK
|
9
|
+
n9jU7NLNJjhrj0rVD++eNOT8dunvYRlI5S8lJWCNAgMBAAGjgaUwgaIwHQYDVR0O
|
10
|
+
BBYEFISS0qC22So8BtQYTT4n/iLdYnZnMHMGA1UdIwRsMGqAFISS0qC22So8BtQY
|
11
|
+
TT4n/iLdYnZnoUekRTBDMQswCQYDVQQGEwJVUzERMA8GA1UECBMITmV3IFlvcmsx
|
12
|
+
ETAPBgNVBAcTCE5ldyBZb3JrMQ4wDAYDVQQKEwVEdW1teYIJAO3gD8Fv66MhMAwG
|
13
|
+
A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAFBGom218pNCB1zYxhDDtOiYC
|
14
|
+
DyU9rZkLXxMVjSHCvyBtQ3EC/21Zog05LSBMvOHFmfzPbyX/IYxSglJLJlQBzGtP
|
15
|
+
trVx1slmva3/QoKYwAKdAe5oSY8eDqejBpKO12maSGsPwbw3oREmudeWkrFFVAlP
|
16
|
+
hlkmwpq7901kIg4CCe8=
|
17
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,15 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIICXQIBAAKBgQDLEdOcDXTohV4pd/6+MRi13OzGMFNi4mW51wKmqBpy791vHDe4
|
3
|
+
v9Iut8vGGCEzVpt44pTIjpI9vkzVRPz4hYsxQaoU9vFfwLHzrIia2EOBl8C8CU7I
|
4
|
+
Axk8ACYKL9MjyH34Sp/Y1OzSzSY4a49K1Q/vnjTk/Hbp72EZSOUvJSVgjQIDAQAB
|
5
|
+
AoGBAIHk8Ef076BAlz/Naty7yQOjwqzvgpdRHCLo3uA9zVVSC4GkOhxqTwblOGqJ
|
6
|
+
SsttDdwgi21SjUcDcGBHVc2elq6SNx9A0mwhb9uPBUI74pJdbrN3/Ue1UnaKy7KZ
|
7
|
+
1PGlMi8GtBL5cn5+NCs/IJUZIIp3oNxURJTF54tun6ygOeddAkEA74rcrXRvFAab
|
8
|
+
mMwQuGlFCNU1ns6Zm19tBFBadTUJQl0cB0MAS9BKveXGOsxXVChR0JvDTnTnx5a4
|
9
|
+
SwKz3t/irwJBANkFeJdZy9Ve5mzzY3rt6sCorOPo9+lER4K01HB6h24vWB1DIFRE
|
10
|
+
YNNiK7YBVHrnb4mcKSdP93QXlYoAugP974MCQQDrVPACVI5ADVHV5j1S/tC8ocJg
|
11
|
+
9zW/iBtxDoQf++/Ry+maVL+4u7SCJXf/EfuFiWr/V9ejf4Sp96+sucX+YtOvAkBs
|
12
|
+
cVts5aYBHMavsn8HMlOXqbGawRMAMOo62fk9qzx5RpcVKDHDadeoSOnmrIt2Tqdh
|
13
|
+
b/Lwffj8vbwvlWVeEUnZAkBwGrhn+BzcfsPLIQfyfC13c1oLJNTZ/ktQdiZIp9Sn
|
14
|
+
Zmaf4yBl3NekmgdNQUGdbPBEUl32ukVhOkfOhI2qZcbA
|
15
|
+
-----END RSA PRIVATE KEY-----
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-apn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dave Yeu
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-19 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &70281393623880 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0.beta.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70281393623880
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yajl-ruby
|
27
|
+
requirement: &70281393622200 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.8.2
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70281393622200
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &70281393609200 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.6.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70281393609200
|
47
|
+
description:
|
48
|
+
email:
|
49
|
+
- daveyeu@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- .rspec
|
56
|
+
- Gemfile
|
57
|
+
- LICENSE
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- certs/.gitignore
|
61
|
+
- em-apn.gemspec
|
62
|
+
- lib/em-apn.rb
|
63
|
+
- lib/em-apn/client.rb
|
64
|
+
- lib/em-apn/connection.rb
|
65
|
+
- lib/em-apn/error_response.rb
|
66
|
+
- lib/em-apn/log_message.rb
|
67
|
+
- lib/em-apn/notification.rb
|
68
|
+
- lib/em-apn/response.rb
|
69
|
+
- lib/em-apn/server.rb
|
70
|
+
- lib/em-apn/test_helper.rb
|
71
|
+
- lib/em-apn/version.rb
|
72
|
+
- script/push
|
73
|
+
- script/server
|
74
|
+
- script/test
|
75
|
+
- spec/em-apn/client_spec.rb
|
76
|
+
- spec/em-apn/notification_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- spec/support/certs/cert.pem
|
79
|
+
- spec/support/certs/key.pem
|
80
|
+
homepage: ''
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project: em-apn
|
100
|
+
rubygems_version: 1.8.10
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: EventMachine-driven Apple Push Notifications
|
104
|
+
test_files:
|
105
|
+
- spec/em-apn/client_spec.rb
|
106
|
+
- spec/em-apn/notification_spec.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- spec/support/certs/cert.pem
|
109
|
+
- spec/support/certs/key.pem
|