apnserver 0.1.10 → 0.2.1
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.
- 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}
|