apnserver 0.1.10 → 0.2.1

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