em-apn 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ certs/*.pem
6
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in em-apn.gemspec
4
+ gemspec
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.
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ desc "Run specs"
4
+ task :spec do
5
+ spec_files = Dir["spec/**/*_spec.rb"]
6
+ ruby("-S bundle exec rspec #{spec_files.join(" ")}")
7
+ end
8
+
9
+ task :default => :spec
File without changes
@@ -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
@@ -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,13 @@
1
+ module EventMachine
2
+ module APN
3
+ class LogMessage
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+
8
+ def log
9
+ EM::APN.logger.info(@response.to_s)
10
+ end
11
+ end
12
+ end
13
+ 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,13 @@
1
+ module EventMachine
2
+ module APN
3
+ class Response
4
+ def initialize(notification)
5
+ @notification = notification
6
+ end
7
+
8
+ def to_s
9
+ "TOKEN=#{@notification.token}"
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module APN
3
+ VERSION = "0.0.3"
4
+ end
5
+ end
@@ -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
@@ -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
@@ -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
@@ -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