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.
@@ -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 = defined?(Rails) ? payload.to_json : JSON.generate(payload)
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
- puts "PayloadInvalid: #{p}"
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.parse(j)
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
@@ -1,17 +1,14 @@
1
1
  module ApnServer
2
-
3
2
  module Payload
4
-
5
- class PayloadInvalid < RuntimeError
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
@@ -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
- puts "#{Time.now} [#{address.last}:#{address.first}] CONNECT"
5
+ Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] CONNECT"
7
6
  end
8
-
7
+
9
8
  def unbind
10
- puts "#{Time.now} [#{address.last}:#{address.first}] DISCONNECT"
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
- puts "#{Time.now} [#{address.last}:#{address.first}] found valid Notification: #{notification}"
15
+ Config.logger.debug "#{Time.now} [#{address.last}:#{address.first}] found valid Notification: #{notification}"
17
16
  queue.push(notification)
18
17
  else
19
- puts "#{Time.now} [#{address.last}:#{address.first}] invalid notification: #{@buf}"
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
@@ -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
- puts "#{Time.now} Starting APN Server on #{bind_address}:#{port}"
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
- puts "Caught Error, closing connecting and adding notification back to queue"
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
- puts "Unable to handle: #{e}"
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
@@ -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}