em-apn 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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