apnserver 0.1.10 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +31 -22
- data/bin/apnsend +10 -6
- data/bin/apnserverd +24 -6
- data/bin/apnserverd.fedora.init +0 -0
- data/bin/apnserverd.ubuntu.init +116 -0
- data/lib/apnserver.rb +0 -4
- data/lib/apnserver/client.rb +9 -10
- data/lib/apnserver/notification.rb +27 -28
- data/lib/apnserver/payload.rb +3 -7
- data/lib/apnserver/protocol.rb +6 -8
- data/lib/apnserver/server.rb +10 -11
- data/spec/models/client_spec.rb +21 -0
- data/spec/models/notification_spec.rb +85 -0
- data/spec/models/payload_spec.rb +57 -0
- data/spec/models/protocol_spec.rb +19 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/test_server.rb +10 -0
- metadata +74 -42
- data/Rakefile +0 -41
- data/VERSION +0 -1
- data/test/test_client.rb +0 -11
- data/test/test_helper.rb +0 -4
- data/test/test_notification.rb +0 -79
- data/test/test_payload.rb +0 -59
- data/test/test_protocol.rb +0 -29
@@ -1,91 +1,90 @@
|
|
1
1
|
require 'apnserver/payload'
|
2
|
-
require 'json'
|
3
|
-
require 'json/add/rails'
|
4
2
|
require 'base64'
|
3
|
+
require 'active_support/ordered_hash'
|
4
|
+
require 'active_support/json'
|
5
5
|
|
6
6
|
module ApnServer
|
7
|
-
|
8
7
|
class Config
|
9
8
|
class << self
|
10
|
-
attr_accessor :host, :port, :pem, :password
|
9
|
+
attr_accessor :host, :port, :pem, :password, :logger
|
11
10
|
end
|
12
11
|
end
|
13
|
-
|
14
|
-
|
12
|
+
|
13
|
+
Config.logger = Logger.new("/dev/null")
|
14
|
+
|
15
15
|
class Notification
|
16
16
|
include ApnServer::Payload
|
17
|
-
|
17
|
+
|
18
18
|
attr_accessor :device_token, :alert, :badge, :sound, :custom
|
19
|
-
|
20
|
-
|
19
|
+
|
21
20
|
def payload
|
22
21
|
p = Hash.new
|
23
22
|
[:badge, :alert, :sound, :custom].each do |k|
|
24
|
-
p[k] = send(k) if send(k)
|
23
|
+
p[k] = send(k) if send(k)
|
25
24
|
end
|
26
25
|
create_payload(p)
|
27
26
|
end
|
28
|
-
|
29
|
-
def json_payload
|
30
|
-
j =
|
27
|
+
|
28
|
+
def json_payload
|
29
|
+
j = ActiveSupport::JSON.encode(payload)
|
31
30
|
raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256
|
32
31
|
j
|
33
32
|
end
|
34
|
-
|
33
|
+
|
35
34
|
def push
|
36
35
|
if Config.pem.nil?
|
37
36
|
socket = TCPSocket.new(Config.host || 'localhost', Config.port.to_i || 22195)
|
38
|
-
socket.write(to_bytes)
|
37
|
+
socket.write(to_bytes)
|
39
38
|
socket.close
|
40
39
|
else
|
41
40
|
client = ApnServer::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195)
|
42
41
|
client.connect!
|
43
42
|
client.write(self)
|
44
43
|
client.disconnect!
|
45
|
-
end
|
44
|
+
end
|
46
45
|
end
|
47
|
-
|
46
|
+
|
48
47
|
def to_bytes
|
49
48
|
j = json_payload
|
50
49
|
[0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*")
|
51
50
|
end
|
52
|
-
|
51
|
+
|
53
52
|
def self.valid?(p)
|
54
53
|
begin
|
55
54
|
Notification.parse(p)
|
56
55
|
rescue PayloadInvalid => p
|
57
|
-
|
58
|
-
false
|
59
|
-
rescue JSON::ParserError => p
|
56
|
+
Config.logger.error "PayloadInvalid: #{p}"
|
60
57
|
false
|
61
58
|
rescue RuntimeError
|
62
59
|
false
|
60
|
+
rescue Exception => e
|
61
|
+
false
|
63
62
|
end
|
64
63
|
end
|
65
|
-
|
64
|
+
|
66
65
|
def self.parse(p)
|
67
66
|
buffer = p.dup
|
68
67
|
notification = Notification.new
|
69
|
-
|
68
|
+
|
70
69
|
header = buffer.slice!(0, 3).unpack('ccc')
|
71
70
|
if header[0] != 0
|
72
71
|
raise RuntimeError.new("Header of notification is invalid: #{header.inspect}")
|
73
72
|
end
|
74
|
-
|
73
|
+
|
75
74
|
# parse token
|
76
75
|
notification.device_token = buffer.slice!(0, 32).unpack('a*').first
|
77
|
-
|
76
|
+
|
78
77
|
# parse json payload
|
79
78
|
payload_len = buffer.slice!(0, 2).unpack('CC')
|
80
79
|
j = buffer.slice!(0, payload_len.last)
|
81
|
-
result = JSON.
|
82
|
-
|
80
|
+
result = ActiveSupport::JSON.decode(j)
|
81
|
+
|
83
82
|
['alert', 'badge', 'sound'].each do |k|
|
84
83
|
notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k]
|
85
84
|
end
|
86
85
|
result.delete('aps')
|
87
86
|
notification.custom = result
|
88
|
-
|
87
|
+
|
89
88
|
notification
|
90
89
|
end
|
91
90
|
end
|
data/lib/apnserver/payload.rb
CHANGED
@@ -1,17 +1,14 @@
|
|
1
1
|
module ApnServer
|
2
|
-
|
3
2
|
module Payload
|
4
|
-
|
5
|
-
|
6
|
-
end
|
7
|
-
|
3
|
+
PayloadInvalid = Class.new(RuntimeError)
|
4
|
+
|
8
5
|
def create_payload(payload)
|
9
6
|
case payload
|
10
7
|
when String then { :aps => { :alert => payload } }
|
11
8
|
when Hash then create_payload_from_hash(payload)
|
12
9
|
end
|
13
10
|
end
|
14
|
-
|
11
|
+
|
15
12
|
def create_payload_from_hash(payload)
|
16
13
|
custom = payload.delete(:custom)
|
17
14
|
aps = {:aps => payload }
|
@@ -19,5 +16,4 @@ module ApnServer
|
|
19
16
|
aps
|
20
17
|
end
|
21
18
|
end
|
22
|
-
|
23
19
|
end
|
data/lib/apnserver/protocol.rb
CHANGED
@@ -1,24 +1,22 @@
|
|
1
1
|
module ApnServer
|
2
2
|
module Protocol
|
3
|
-
|
4
3
|
def post_init
|
5
4
|
@address = Socket.unpack_sockaddr_in(self.get_peername)
|
6
|
-
|
5
|
+
Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] CONNECT"
|
7
6
|
end
|
8
|
-
|
7
|
+
|
9
8
|
def unbind
|
10
|
-
|
9
|
+
Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] DISCONNECT"
|
11
10
|
end
|
12
|
-
|
11
|
+
|
13
12
|
def receive_data(data)
|
14
13
|
(@buf ||= "") << data
|
15
14
|
if notification = ApnServer::Notification.valid?(@buf)
|
16
|
-
|
15
|
+
Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] found valid Notification: #{notification}"
|
17
16
|
queue.push(notification)
|
18
17
|
else
|
19
|
-
|
18
|
+
Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] invalid notification: #{@buf}"
|
20
19
|
end
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
24
22
|
end
|
data/lib/apnserver/server.rb
CHANGED
@@ -1,42 +1,41 @@
|
|
1
1
|
module ApnServer
|
2
|
-
|
3
2
|
class Server
|
4
3
|
attr_accessor :client, :bind_address, :port
|
5
|
-
|
4
|
+
|
6
5
|
def initialize(pem, bind_address = '0.0.0.0', port = 22195)
|
7
6
|
@queue = EM::Queue.new
|
8
7
|
@client = ApnServer::Client.new(pem)
|
9
8
|
@bind_address, @port = bind_address, port
|
10
9
|
end
|
11
|
-
|
10
|
+
|
12
11
|
def start!
|
13
12
|
EventMachine::run do
|
14
|
-
|
15
|
-
|
13
|
+
Config.logger.info "#{Time.now} Starting APN Server on #{bind_address}:#{port}"
|
14
|
+
|
16
15
|
EM.start_server(bind_address, port, ApnServer::ServerConnection) do |s|
|
17
16
|
s.queue = @queue
|
18
|
-
end
|
19
|
-
|
17
|
+
end
|
18
|
+
|
20
19
|
EventMachine::PeriodicTimer.new(1) do
|
21
20
|
unless @queue.empty?
|
22
21
|
size = @queue.size
|
23
|
-
size.times do
|
22
|
+
size.times do
|
24
23
|
@queue.pop do |notification|
|
25
24
|
begin
|
26
25
|
@client.connect! unless @client.connected?
|
27
26
|
@client.write(notification)
|
28
27
|
rescue Errno::EPIPE, OpenSSL::SSL::SSLError
|
29
|
-
|
28
|
+
Config.logger.error "Caught Error, closing connecting and adding notification back to queue"
|
30
29
|
@client.disconnect!
|
31
30
|
@queue.push(notification)
|
32
31
|
rescue RuntimeError => e
|
33
|
-
|
32
|
+
Config.logger.error "Unable to handle: #{e}"
|
34
33
|
end
|
35
34
|
end
|
36
35
|
end
|
37
36
|
end
|
38
37
|
end
|
39
|
-
end
|
38
|
+
end
|
40
39
|
end
|
41
40
|
end
|
42
41
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe Client do
|
5
|
+
describe "#new" do
|
6
|
+
let(:client) { ApnServer::Client.new('cert.pem', 'gateway.sandbox.push.apple.com', 2196) }
|
7
|
+
|
8
|
+
it "sets the pem path" do
|
9
|
+
client.pem.should == 'cert.pem'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sets the host" do
|
13
|
+
client.host.should == 'gateway.sandbox.push.apple.com'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "sets the port" do
|
17
|
+
client.port.should == 2196
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe Notification do
|
5
|
+
let(:notification) { Notification.new }
|
6
|
+
|
7
|
+
describe "#to_bytes" do
|
8
|
+
it "generates a byte array" do
|
9
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
10
|
+
device_token = "12345678123456781234567812345678"
|
11
|
+
notification.device_token = device_token
|
12
|
+
notification.alert = "You have not mail!"
|
13
|
+
expected = [0, 0, device_token.size, device_token, 0, payload.size, payload]
|
14
|
+
notification.to_bytes.should == expected.pack("ccca*CCa*")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#payload" do
|
19
|
+
it "generates the badge element" do
|
20
|
+
expected = { :aps => { :badge => 1 }}
|
21
|
+
notification.badge = 1
|
22
|
+
notification.payload.should == expected
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates the alert alement" do
|
26
|
+
expected = { :aps => { :alert => 'Hi' }}
|
27
|
+
notification.alert = 'Hi'
|
28
|
+
notification.payload.should == expected
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#json_payload" do
|
33
|
+
it "converts payload to json" do
|
34
|
+
expected = '{"aps":{"alert":"Hi"}}'
|
35
|
+
notification.alert = 'Hi'
|
36
|
+
notification.json_payload.should == expected
|
37
|
+
end
|
38
|
+
|
39
|
+
it "does not allow payloads larger than 256 chars" do
|
40
|
+
lambda {
|
41
|
+
alert = []
|
42
|
+
256.times { alert << 'Hi' }
|
43
|
+
notification.alert = alert.join
|
44
|
+
notification.json_payload
|
45
|
+
}.should raise_error(Payload::PayloadInvalid)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#valid?" do
|
50
|
+
it "recognizes a valid request" do
|
51
|
+
device_token = '12345678123456781234567812345678'
|
52
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
53
|
+
request = [0, 0, device_token.size, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
|
54
|
+
Notification.valid?(request).should be_true
|
55
|
+
notification = Notification.parse(request)
|
56
|
+
notification.device_token.should == device_token
|
57
|
+
notification.alert.should == "You have not mail!"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "recognizes an invalid request" do
|
61
|
+
device_token = '123456781234567812345678'
|
62
|
+
payload = '{"aps":{"alert":"You have not mail!"}}'
|
63
|
+
request = [0, 0, 32, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
|
64
|
+
Notification.valid?(request).should be_false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#parse" do
|
69
|
+
it "reads a byte array and constructs a notification" do
|
70
|
+
device_token = '12345678123456781234567812345678'
|
71
|
+
notification.device_token = device_token
|
72
|
+
notification.badge = 10
|
73
|
+
notification.alert = 'Hi'
|
74
|
+
notification.sound = 'default'
|
75
|
+
notification.custom = { 'acme1' => "bar", 'acme2' => 42}
|
76
|
+
|
77
|
+
parsed = Notification.parse(notification.to_bytes)
|
78
|
+
[:device_token, :badge, :alert, :sound, :custom].each do |k|
|
79
|
+
expected = notification.send(k)
|
80
|
+
parsed.send(k).should == expected
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApnServer
|
4
|
+
describe Payload do
|
5
|
+
describe "#payload.create_payload" do
|
6
|
+
let(:payload) { Class.new.send(:include, Payload).new }
|
7
|
+
|
8
|
+
it "creates a payload_with_simple_string" do
|
9
|
+
payload.create_payload('Hi').should == { :aps => { :alert => 'Hi' }}
|
10
|
+
end
|
11
|
+
|
12
|
+
it "creates a payload_with_alert_key" do
|
13
|
+
payload.create_payload(:alert => 'Hi').should == { :aps => { :alert => 'Hi' }}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "creates payload with badge_and_alert" do
|
17
|
+
payload.create_payload(:alert => 'Hi', :badge => 1).should == { :aps => { :alert => 'Hi', :badge => 1 }}
|
18
|
+
end
|
19
|
+
|
20
|
+
# example 1
|
21
|
+
it "test_should_payload.create_payload_with_custom_payload" do
|
22
|
+
alert = 'Message received from Bob'
|
23
|
+
payload.create_payload(:alert => alert, :custom => { :acme2 => ['bang', 'whiz']}).should == {
|
24
|
+
:aps => { :alert => alert },
|
25
|
+
:acme2 => [ "bang", "whiz" ]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# example 3
|
30
|
+
it "test_should_payload.create_payload_with_sound_and_multiple_custom" do
|
31
|
+
expected = {
|
32
|
+
:aps => {
|
33
|
+
:alert => "You got your emails.",
|
34
|
+
:badge => 9,
|
35
|
+
:sound => "bingbong.aiff"
|
36
|
+
},
|
37
|
+
:acme1 => "bar",
|
38
|
+
:acme2 => 42
|
39
|
+
}
|
40
|
+
payload.create_payload({
|
41
|
+
:alert => "You got your emails.",
|
42
|
+
:badge => 9,
|
43
|
+
:sound => "bingbong.aiff",
|
44
|
+
:custom => { :acme1 => "bar", :acme2 => 42}
|
45
|
+
}).should == expected
|
46
|
+
end
|
47
|
+
|
48
|
+
# example 5
|
49
|
+
it "test_should_payload.create_payload_with_empty_aps" do
|
50
|
+
payload.create_payload(:custom => { :acme2 => [ 5, 8 ] }).should == {
|
51
|
+
:aps => {},
|
52
|
+
:acme2 => [ 5, 8 ]
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "TestProtocol" do
|
4
|
+
before(:each) do
|
5
|
+
@server = TestServer.new
|
6
|
+
@server.queue = Array.new # fake out EM::Queue
|
7
|
+
end
|
8
|
+
|
9
|
+
it "adds_notification_to_queue" do
|
10
|
+
token = "12345678123456781234567812345678"
|
11
|
+
@server.receive_data("\0\0 #{token}\0#{22.chr}{\"aps\":{\"alert\":\"Hi\"}}")
|
12
|
+
@server.queue.size.should == 1
|
13
|
+
end
|
14
|
+
|
15
|
+
it "does_not_add_invalid_notification" do
|
16
|
+
@server.receive_data('fakedata')
|
17
|
+
@server.queue.should be_empty
|
18
|
+
end
|
19
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
# Set up gems listed in the Gemfile.
|
4
|
+
gemfile = File.expand_path('../Gemfile', File.dirname(__FILE__))
|
5
|
+
begin
|
6
|
+
ENV['BUNDLE_GEMFILE'] = gemfile
|
7
|
+
require 'bundler'
|
8
|
+
Bundler.setup
|
9
|
+
rescue Bundler::GemNotFound => e
|
10
|
+
STDERR.puts e.message
|
11
|
+
STDERR.puts "Try running `bundle install`."
|
12
|
+
exit!
|
13
|
+
end
|
14
|
+
|
15
|
+
Bundler.require(:spec)
|
16
|
+
|
17
|
+
Rspec.configure do |config|
|
18
|
+
config.mock_with :rspec
|
19
|
+
end
|
20
|
+
# Requires supporting files with custom matchers and macros, etc,
|
21
|
+
# in ./support/ and its subdirectories.
|
22
|
+
|
23
|
+
require "apnserver"
|
24
|
+
require 'base64'
|
25
|
+
|
26
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|