apnserver 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,166 @@
1
+ h1. Apple Push Notification Server Toolkit
2
+
3
+ * http://github.com/bpoweski/apnserver
4
+
5
+ h2. Description
6
+
7
+ apnserver is a server and set of command line programs to send push notifications to the iPhone.
8
+ Apple recomends to maintain an open connection to the push notification service and refrain from
9
+ opening up and tearing down SSL connections reapeated. To solve this problem an intermediate
10
+ network server is introduced that queues are requests to the APN service and sends them via a
11
+ persistent connection.
12
+
13
+ h2. Remaining Tasks
14
+
15
+ * Implement feedback service mechanism
16
+ * Implement robust notification sending in reactor periodic scheduler
17
+
18
+ h2. Issues Fixed
19
+
20
+ * second attempt at retry logic, SSL Errors close down sockets now
21
+ * apnsend --badge option correctly sends integer number rather than string of number for aps json payload
22
+ * connections are properly closed in Notification#push method now
23
+ * better compatibility with Rails
24
+
25
+ h2. APN Server Daemon
26
+
27
+ <pre>
28
+ <code>
29
+ Usage: apnserverd [options] --pem /path/to/pem
30
+ --bind-address bind address (defaults to 0.0.0.0)
31
+ bind address of the server daemon
32
+
33
+ --proxy-port port
34
+ the port that the daemon will listen on (defaults to 22195)
35
+
36
+ --server server
37
+ APN Server (defaults to gateway.push.apple.com)
38
+
39
+ --port port of the APN Server
40
+ APN server port (defaults to 2195)
41
+
42
+ --pem pem file path
43
+ The PEM encoded private key and certificate.
44
+ To export a PEM ecoded file execute
45
+ # openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
46
+
47
+ --help
48
+ usage message
49
+
50
+ --daemon
51
+ Runs process as daemon, not available on Windows
52
+ </code>
53
+ </pre>
54
+
55
+ h2. APN Server Client
56
+
57
+ With the APN server client script you can send push notifications directly to
58
+ Apple's APN server over an SSL connection or to the above daemon using a plain socket.
59
+ To send a notification to Apple's APN server using SSL the *--pem* option must be used.
60
+
61
+ <pre>
62
+ <code>
63
+ Usage: apnsend [switches] (--b64-token | --hex-token) <token>
64
+ --server <localhost> the apn server defaults to a locally running apnserverd
65
+ --port <2195> the port of the apn server
66
+ --pem <path> the path to the pem file, if a pem is supplied the server
67
+ defaults to gateway.push.apple.com:2195
68
+ --alert <message> the message to send"
69
+ --sound <default> the sound to play, defaults to 'default'
70
+ --badge <number> the badge number
71
+ --custom <json string> a custom json string to be added to the main object
72
+ --b64-token <token> a base 64 encoded device token
73
+ --hex-token <token> a hex encoded device token
74
+ --help this message
75
+ </code>
76
+ </pre>
77
+
78
+ To send a base64 encoded push notification via the command line execute the following:
79
+
80
+ <pre>
81
+ <code>
82
+ $ apnsend --server gateway.push.apple.com --port 2195 --pem key.pem \
83
+ --b64-token j92f12jh8lqcAwcOVeSIrsBxibaJ0xyCi8/AkmzNlk8= --sound default \
84
+ --alert Hello
85
+ </code>
86
+ </pre>
87
+
88
+ h2. Sending Notifications from Ruby
89
+
90
+ To configure the client to send to the local apnserverd process configure the ApnServer client with the following.
91
+
92
+ <pre>
93
+ <code>
94
+ # configured for a using the apnserverd proxy
95
+ ApnServer::Config.host = 'localhost'
96
+ ApnServer::Config.port = 22195
97
+ </code>
98
+ </pre>
99
+
100
+ To configure the client to send directly to Apple's push notification server, bypassing the apnserverd process configure the following.
101
+
102
+ <pre>
103
+ <code>
104
+ ApnServer::Config.pem = '/path/to/pem'
105
+ ApnServer::Config.host = 'gateway.push.apple.com'
106
+ ApnServer::Config.port = 2195
107
+ </code>
108
+ </pre>
109
+
110
+ Finally within we can send a push notification using the following code
111
+
112
+ <pre>
113
+ <code>
114
+ notification = ApnServer::Notification.new
115
+ notification.device_token = Base64.decode64(apns_token) # if base64 encoded
116
+ notification.alert = message
117
+ notification.badge = 1
118
+ notification.sound = 'default'
119
+ notification.push
120
+ </code>
121
+ </pre>
122
+
123
+
124
+ h2. Installation
125
+
126
+ To install apnserver execute the following gem command:
127
+
128
+ <pre>
129
+ <code>
130
+ $ gem install bpoweski-apnserver --source http://gems.github.com
131
+ </code>
132
+ </pre>
133
+
134
+ Adding apnserver to your Rails application
135
+
136
+ <pre>
137
+ <code>
138
+ config.gem "bpoweski-apnserver", :lib => 'apnserver', :source => "http://gems.github.com"
139
+ </code>
140
+ </pre>
141
+
142
+ h2. License
143
+
144
+ (The MIT License)
145
+
146
+ Copyright (c) 2009 Ben Poweski
147
+
148
+ Permission is hereby granted, free of charge, to any person obtaining
149
+ a copy of this software and associated documentation files (the
150
+ 'Software'), to deal in the Software without restriction, including
151
+ without limitation the rights to use, copy, modify, merge, publish,
152
+ distribute, sublicense, and/or sell copies of the Software, and to
153
+ permit persons to whom the Software is furnished to do so, subject to
154
+ the following conditions:
155
+
156
+ The above copyright notice and this permission notice shall be
157
+ included in all copies or substantial portions of the Software.
158
+
159
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
160
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
161
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
162
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
163
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
164
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
165
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
166
+
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "apnserver"
7
+ gemspec.summary = "Apple Push Notification Server"
8
+ gemspec.description = "A toolkit for proxying and sending Apple Push Notifications"
9
+ gemspec.email = "bpoweski@3factors.com"
10
+ gemspec.homepage = "http://github.com/bpoweski/apnserver"
11
+ gemspec.authors = ["Ben Poweski"]
12
+ gemspec.add_dependency 'eventmachine'
13
+ gemspec.add_dependency 'daemons'
14
+ gemspec.add_dependency 'json'
15
+ gemspec.rubyforge_project = 'apnserver'
16
+ gemspec.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*'].to_a
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.test_files = FileList.new('test/**/test_*.rb') do |list|
25
+ list.exclude 'test/test_helper.rb'
26
+ end
27
+ test.libs << 'test'
28
+ test.verbose = true
29
+ end
30
+
31
+ Jeweler::RubyforgeTasks.new do |rubyforge|
32
+ end
33
+
34
+ task :default => [:test]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.9
data/bin/apnsend ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'logger'
5
+ require 'getoptlong'
6
+ require 'rubygems'
7
+ require 'apnserver'
8
+ require 'base64'
9
+ require 'socket'
10
+
11
+ def usage
12
+ puts "Usage: apnsend [switches] (--b64-token | --hex-token) <token>"
13
+ puts " --server <localhost> the apn server defaults to a locally running apnserverd"
14
+ puts " --port <2195> the port of the apn server"
15
+ puts " --pem <path> the path to the pem file, if a pem is supplied the server defaults to gateway.push.apple.com:2195"
16
+ puts " --alert <message> the message to send"
17
+ puts " --sound <default> the sound to play, defaults to 'default'"
18
+ puts " --badge <number> the badge number"
19
+ puts " --custom <json string> a custom json string to be added to the main object"
20
+ puts " --b64-token <token> a base 64 encoded device token"
21
+ puts " --hex-token <token> a hex encoded device token"
22
+ puts " --help this message"
23
+ end
24
+
25
+ opts = GetoptLong.new(
26
+ ["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
27
+ ["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
28
+ ["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
29
+ ["--alert", "-a", GetoptLong::REQUIRED_ARGUMENT],
30
+ ["--sound", "-S", GetoptLong::REQUIRED_ARGUMENT],
31
+ ["--badge", "-b", GetoptLong::REQUIRED_ARGUMENT],
32
+ ["--custom", "-j", GetoptLong::REQUIRED_ARGUMENT],
33
+ ["--b64-token", "-B", GetoptLong::REQUIRED_ARGUMENT],
34
+ ["--hex-token", "-H", GetoptLong::REQUIRED_ARGUMENT],
35
+ ["--help", "-h", GetoptLong::NO_ARGUMENT]
36
+ )
37
+
38
+ notification = ApnServer::Notification.new
39
+
40
+ opts.each do |opt, arg|
41
+ case opt
42
+ when '--help'
43
+ usage
44
+ exit
45
+ when '--server'
46
+ ApnServer::Config.host = arg
47
+ when '--port'
48
+ ApnServer::Config.port = arg.to_i
49
+ when '--pem'
50
+ ApnServer::Config.pem = arg
51
+ when '--alert'
52
+ notification.alert = arg
53
+ when '--sound'
54
+ notification.sound = arg
55
+ when '--badge'
56
+ notification.badge = arg.to_i
57
+ when '--custom'
58
+ notification.custom = arg
59
+ when '--b64-token'
60
+ notification.device_token = Base64::decode64(arg)
61
+ when '--hex-token'
62
+ notification.device_token = arg.unpack('H*')
63
+ end
64
+ end
65
+
66
+ if notification.device_token.nil?
67
+ usage
68
+ exit
69
+ else
70
+ notification.push
71
+ end
data/bin/apnserverd ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'getoptlong'
5
+ require 'rubygems'
6
+ require 'daemons'
7
+ require 'apnserver'
8
+
9
+ def usage
10
+ puts "Usage: apnserverd [switches] --pem <path>"
11
+ puts " --bind-address [0.0.0.0] bind address of proxy"
12
+ puts " --proxy-port [22195] port proxy listens on"
13
+ puts " --server <gateway.push.apple.com> the apn server to send messages to"
14
+ puts " --port <2195> the port of the apn server"
15
+ puts " --help this message"
16
+ end
17
+
18
+ def daemonize
19
+ Daemonize.daemonize('/var/log/apnserverd.log', 'apnserverd')
20
+ @pid_file = '/var/run/apnserverd.pid'
21
+ open(@pid_file,"w") {|f| f.write(Process.pid) } # copied from mongrel
22
+ open(@pid_file,"w") do |f|
23
+ f.write(Process.pid)
24
+ File.chmod(0644, @pid_file)
25
+ end
26
+ end
27
+
28
+ opts = GetoptLong.new(
29
+ ["--bind-address", "-b", GetoptLong::REQUIRED_ARGUMENT],
30
+ ["--proxy-port", "-P", GetoptLong::REQUIRED_ARGUMENT],
31
+ ["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
32
+ ["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
33
+ ["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
34
+ ["--help", "-h", GetoptLong::NO_ARGUMENT],
35
+ ["--daemon", "-d", GetoptLong::NO_ARGUMENT]
36
+ )
37
+
38
+ bind_address = '0.0.0.0'
39
+ proxy_port = 22195
40
+ host = 'gateway.push.apple.com'
41
+ port = 2195
42
+ pem = nil
43
+ daemon = false
44
+
45
+ opts.each do |opt, arg|
46
+ case opt
47
+ when '--help'
48
+ usage
49
+ when '--bind-address'
50
+ bind_address = arg
51
+ when '--proxy-port'
52
+ proxy_port = arg.to_i
53
+ when '--server'
54
+ host = arg
55
+ when '--port'
56
+ port = arg.to_i
57
+ when '--pem'
58
+ pem = arg
59
+ when '--daemon'
60
+ daemon = true
61
+ end
62
+ end
63
+
64
+ if pem.nil?
65
+ usage
66
+ exit 1
67
+ else
68
+ daemonize if daemon
69
+ server = ApnServer::Server.new(pem, bind_address, proxy_port)
70
+ server.client.host = host
71
+ server.client.port = port
72
+ server.start!
73
+ end
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ #
3
+ # /etc/rc.d/init.d/apnserverd
4
+ # apnserverd This shell script takes care of starting and stopping
5
+ # the APN Server Proxy
6
+ #
7
+ # chkconfig: 345 20 80
8
+ # Author: Ben Poweski bpoweski@gmail.com
9
+ #
10
+ # Source function library.
11
+ . /etc/init.d/functions
12
+
13
+ NAME=apnserverd
14
+ APNSERVERD=/usr/bin/$NAME
15
+ PIDFILE=/var/run/$NAME.pid
16
+
17
+ if [ -f /etc/sysconfig/$NAME ]; then
18
+ . /etc/sysconfig/$NAME
19
+ fi
20
+
21
+
22
+ start() {
23
+ echo -n "Starting APN Server: "
24
+ if [ -f $PIDFILE ]; then
25
+ PID=`cat $PIDFILE`
26
+ echo $NAME already running: $PID
27
+ exit 2;
28
+ elif [ -f $PIDFILE ]; then
29
+ PID=`cat $PIDFILE`
30
+ echo $NAME already running: $PID
31
+ exit 2;
32
+ else
33
+ daemon $APNSERVERD $OPTIONS
34
+ RETVAL=$?
35
+ echo
36
+ [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$NAME
37
+ return $RETVAL
38
+ fi
39
+
40
+ }
41
+
42
+ stop() {
43
+ echo -n "Shutting down APN Server: "
44
+ echo
45
+ kill `cat $PIDFILE`
46
+ echo
47
+ rm -f /var/lock/subsys/$NAME
48
+ rm -f $PIDFILE
49
+ return 0
50
+ }
51
+
52
+ case "$1" in
53
+ start)
54
+ start
55
+ ;;
56
+ stop)
57
+ stop
58
+ ;;
59
+ status)
60
+ status $NAME
61
+ ;;
62
+ restart)
63
+ stop
64
+ start
65
+ ;;
66
+ *)
67
+ echo "Usage: {start|stop|status|restart}"
68
+ exit 1
69
+ ;;
70
+ esac
71
+ exit $?
data/lib/apnserver.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'logger'
2
+ require 'eventmachine'
3
+ require 'apnserver/payload'
4
+ require 'apnserver/notification'
5
+ require 'apnserver/protocol'
6
+ require 'apnserver/client'
7
+ require 'apnserver/server_connection'
8
+ require 'apnserver/server'
@@ -0,0 +1,44 @@
1
+ require 'openssl'
2
+ require 'socket'
3
+
4
+ module ApnServer
5
+ class Client
6
+
7
+ attr_accessor :pem, :host, :port, :password
8
+
9
+ def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil)
10
+ @pem, @host, @port, @password = pem, host, port, pass
11
+ end
12
+
13
+ def connect!
14
+ raise "The path to your pem file is not set." unless self.pem
15
+ raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
16
+
17
+ @context = OpenSSL::SSL::SSLContext.new
18
+ @context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
19
+ @context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.password)
20
+
21
+ @sock = TCPSocket.new(self.host, self.port.to_i)
22
+ @ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context)
23
+ @ssl.connect
24
+
25
+ return @sock, @ssl
26
+ end
27
+
28
+ def disconnect!
29
+ @ssl.close
30
+ @sock.close
31
+ @ssl = nil
32
+ @sock = nil
33
+ end
34
+
35
+ def write(notification)
36
+ puts "#{Time.now} [#{host}:#{port}] Device: #{notification.device_token.unpack('H*')} sending #{notification.json_payload}"
37
+ @ssl.write(notification.to_bytes)
38
+ end
39
+
40
+ def connected?
41
+ @ssl
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,92 @@
1
+ require 'apnserver/payload'
2
+ require 'json'
3
+ require 'json/add/rails'
4
+ require 'base64'
5
+
6
+ module ApnServer
7
+
8
+ class Config
9
+ class << self
10
+ attr_accessor :host, :port, :pem, :password
11
+ end
12
+ end
13
+
14
+
15
+ class Notification
16
+ include ApnServer::Payload
17
+
18
+ attr_accessor :device_token, :alert, :badge, :sound, :custom
19
+
20
+
21
+ def payload
22
+ p = Hash.new
23
+ [:badge, :alert, :sound, :custom].each do |k|
24
+ p[k] = send(k) if send(k)
25
+ end
26
+ create_payload(p)
27
+ end
28
+
29
+ def json_payload
30
+ j = defined?(Rails) ? payload.to_json : JSON.generate(payload)
31
+ raise PayloadInvalid.new("The payload is larger than allowed: #{j.length}") if j.size > 256
32
+ j
33
+ end
34
+
35
+ def push
36
+ if Config.pem.nil?
37
+ socket = TCPSocket.new(Config.host || 'localhost', Config.port.to_i || 22195)
38
+ socket.write(to_bytes)
39
+ socket.close
40
+ else
41
+ client = ApnServer::Client.new(Config.pem, Config.host || 'gateway.push.apple.com', Config.port.to_i || 2195)
42
+ client.connect!
43
+ client.write(self)
44
+ client.disconnect!
45
+ end
46
+ end
47
+
48
+ def to_bytes
49
+ j = json_payload
50
+ [0, 0, device_token.size, device_token, 0, j.size, j].pack("ccca*cca*")
51
+ end
52
+
53
+ def self.valid?(p)
54
+ begin
55
+ Notification.parse(p)
56
+ rescue PayloadInvalid => p
57
+ puts "PayloadInvalid: #{p}"
58
+ false
59
+ rescue JSON::ParserError => p
60
+ false
61
+ rescue RuntimeError
62
+ false
63
+ end
64
+ end
65
+
66
+ def self.parse(p)
67
+ buffer = p.dup
68
+ notification = Notification.new
69
+
70
+ header = buffer.slice!(0, 3).unpack('ccc')
71
+ if header[0] != 0
72
+ raise RuntimeError.new("Header of notification is invalid: #{header.inspect}")
73
+ end
74
+
75
+ # parse token
76
+ notification.device_token = buffer.slice!(0, 32).unpack('a*').first
77
+
78
+ # parse json payload
79
+ payload_len = buffer.slice!(0, 2).unpack('CC')
80
+ j = buffer.slice!(0, payload_len.last)
81
+ result = JSON.parse(j)
82
+
83
+ ['alert', 'badge', 'sound'].each do |k|
84
+ notification.send("#{k}=", result['aps'][k]) if result['aps'] && result['aps'][k]
85
+ end
86
+ result.delete('aps')
87
+ notification.custom = result
88
+
89
+ notification
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,23 @@
1
+ module ApnServer
2
+
3
+ module Payload
4
+
5
+ class PayloadInvalid < RuntimeError
6
+ end
7
+
8
+ def create_payload(payload)
9
+ case payload
10
+ when String then { :aps => { :alert => payload } }
11
+ when Hash then create_payload_from_hash(payload)
12
+ end
13
+ end
14
+
15
+ def create_payload_from_hash(payload)
16
+ custom = payload.delete(:custom)
17
+ aps = {:aps => payload }
18
+ aps.merge!(custom) if custom
19
+ aps
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,24 @@
1
+ module ApnServer
2
+ module Protocol
3
+
4
+ def post_init
5
+ @address = Socket.unpack_sockaddr_in(self.get_peername)
6
+ puts "#{Time.now} [#{address.last}:#{address.first}] CONNECT"
7
+ end
8
+
9
+ def unbind
10
+ puts "#{Time.now} [#{address.last}:#{address.first}] DISCONNECT"
11
+ end
12
+
13
+ def receive_data(data)
14
+ (@buf ||= "") << data
15
+ if notification = ApnServer::Notification.valid?(@buf)
16
+ puts "#{Time.now} [#{address.last}:#{address.first}] found valid Notification: #{notification}"
17
+ queue.push(notification)
18
+ else
19
+ puts "#{Time.now} [#{address.last}:#{address.first}] invalid notification: #{@buf}"
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module ApnServer
2
+
3
+ class Server
4
+ attr_accessor :client, :bind_address, :port
5
+
6
+ def initialize(pem, bind_address = '0.0.0.0', port = 22195)
7
+ @queue = EM::Queue.new
8
+ @client = ApnServer::Client.new(pem)
9
+ @bind_address, @port = bind_address, port
10
+ end
11
+
12
+ def start!
13
+ EventMachine::run do
14
+ puts "#{Time.now} Starting APN Server on #{bind_address}:#{port}"
15
+
16
+ EM.start_server(bind_address, port, ApnServer::ServerConnection) do |s|
17
+ s.queue = @queue
18
+ end
19
+
20
+ EventMachine::PeriodicTimer.new(1) do
21
+ unless @queue.empty?
22
+ size = @queue.size
23
+ size.times do
24
+ @queue.pop do |notification|
25
+ begin
26
+ @client.connect! unless @client.connected?
27
+ @client.write(notification)
28
+ rescue Errno::EPIPE
29
+ puts "Caught Errno::EPIPE adding notification back to queue"
30
+ @queue.push(notification)
31
+ rescue OpenSSL::SSL::SSLError
32
+ puts "Caught OpenSSL Error, closing connecting and adding notification back to queue"
33
+ @client.disconnect!
34
+ @queue.push(notification)
35
+ rescue RuntimeError => e
36
+ puts "Unable to handle: #{e}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ require 'socket'
2
+ require 'apnserver/protocol'
3
+
4
+ module ApnServer
5
+ class ServerConnection < EventMachine::Connection
6
+ include Protocol
7
+ attr_accessor :queue, :address
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class TestClient < Test::Unit::TestCase
4
+
5
+ def test_creates_client
6
+ client = ApnServer::Client.new('cert.pem', 'gateway.sandbox.push.apple.com', 2196)
7
+ assert_equal 'cert.pem', client.pem
8
+ assert_equal 'gateway.sandbox.push.apple.com', client.host
9
+ assert_equal 2196, client.port
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ require 'stringio'
2
+ require 'test/unit'
3
+ require 'rubygems'
4
+ require File.dirname(__FILE__) + '/../lib/apnserver'
@@ -0,0 +1,79 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+ require 'base64'
3
+
4
+ class NotificationTest < Test::Unit::TestCase
5
+ include ApnServer
6
+
7
+ def setup
8
+ @notification = Notification.new
9
+ end
10
+
11
+ def test_should_generate_byte_array
12
+ payload = '{"aps":{"alert":"You have not mail!"}}'
13
+ device_token = "12345678123456781234567812345678"
14
+ @notification.device_token = device_token
15
+ @notification.alert = "You have not mail!"
16
+ expected = [0, 0, device_token.size, device_token, 0, payload.size, payload]
17
+ assert_equal expected.pack("ccca*CCa*"), @notification.to_bytes
18
+ end
19
+
20
+ def test_should_create_payload_with_badge_attribute
21
+ expected = { :aps => { :badge => 1 }}
22
+ @notification.badge = 1
23
+ assert_equal expected, @notification.payload
24
+ end
25
+
26
+ def test_should_create_payload_with_alert_attribute
27
+ expected = { :aps => { :alert => 'Hi' }}
28
+ @notification.alert = 'Hi'
29
+ assert_equal expected, @notification.payload
30
+ end
31
+
32
+ def test_should_create_json_payload
33
+ expected = '{"aps":{"alert":"Hi"}}'
34
+ @notification.alert = 'Hi'
35
+ assert_equal expected, @notification.json_payload
36
+ end
37
+
38
+ def test_should_not_allow_for_payloads_larger_than_256_chars
39
+ assert_raise Payload::PayloadInvalid do
40
+ alert = []
41
+ 256.times { alert << 'Hi' }
42
+ @notification.alert = alert.join
43
+ @notification.json_payload
44
+ end
45
+ end
46
+
47
+ def test_should_recognize_valid_request
48
+ device_token = '12345678123456781234567812345678'
49
+ payload = '{"aps":{"alert":"You have not mail!"}}'
50
+ request = [0, 0, device_token.size, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
51
+ assert Notification.valid?(request)
52
+ notification = Notification.parse(request)
53
+ assert_equal device_token, notification.device_token
54
+ assert_equal "You have not mail!", notification.alert
55
+ end
56
+
57
+ def test_should_not_recognize_invalid_request
58
+ device_token = '123456781234567812345678'
59
+ payload = '{"aps":{"alert":"You have not mail!"}}'
60
+ request = [0, 0, 32, device_token, 0, payload.size, payload].pack("CCCa*CCa*")
61
+ assert !Notification.valid?(request), request
62
+ end
63
+
64
+ def test_should_pack_and_unpack_json
65
+ device_token = '12345678123456781234567812345678'
66
+ notification = Notification.new
67
+ notification.device_token = device_token
68
+ notification.badge = 10
69
+ notification.alert = 'Hi'
70
+ notification.sound = 'default'
71
+ notification.custom = { 'acme1' => "bar", 'acme2' => 42}
72
+
73
+ parsed = Notification.parse(notification.to_bytes)
74
+ [:device_token, :badge, :alert, :sound, :custom].each do |k|
75
+ expected = notification.send(k)
76
+ assert_equal expected, parsed.send(k), "Expected #{k} to be #{expected}"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,59 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class NotificationTest < Test::Unit::TestCase
4
+ include ApnServer::Payload
5
+
6
+ def test_should_create_payload_with_simple_string
7
+ expected = { :aps => { :alert => 'Hi' }}
8
+ assert_equal expected, create_payload('Hi')
9
+ end
10
+
11
+ def test_should_create_payload_with_alert_key
12
+ expected = { :aps => { :alert => 'Hi' }}
13
+ assert_equal expected, create_payload(:alert => 'Hi')
14
+ end
15
+
16
+ def test_should_create_payload_with_badge_and_alert
17
+ expected = { :aps => { :alert => 'Hi', :badge => 1 }}
18
+ assert_equal expected, create_payload(:alert => 'Hi', :badge => 1)
19
+ end
20
+
21
+ # example 1
22
+ def test_should_create_payload_with_custom_payload
23
+ alert = 'Message received from Bob'
24
+ expected = {
25
+ :aps => { :alert => alert },
26
+ :acme2 => [ "bang", "whiz" ]
27
+ }
28
+ assert_equal expected, create_payload(:alert => alert, :custom => { :acme2 => ['bang', 'whiz']})
29
+ end
30
+
31
+ # example 3
32
+ def test_should_create_payload_with_sound_and_multiple_custom
33
+ expected = {
34
+ :aps => {
35
+ :alert => "You got your emails.",
36
+ :badge => 9,
37
+ :sound => "bingbong.aiff"
38
+ },
39
+ :acme1 => "bar",
40
+ :acme2 => 42
41
+ }
42
+ assert_equal expected, create_payload({
43
+ :alert => "You got your emails.",
44
+ :badge => 9,
45
+ :sound => "bingbong.aiff",
46
+ :custom => { :acme1 => "bar", :acme2 => 42}
47
+ })
48
+ end
49
+
50
+ # example 5
51
+ def test_should_create_payload_with_empty_aps
52
+ expected = {
53
+ :aps => {},
54
+ :acme2 => [ 5, 8 ]
55
+ }
56
+ assert_equal expected, create_payload(:custom => { :acme2 => [ 5, 8 ] })
57
+ end
58
+
59
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestServer
4
+ attr_accessor :queue
5
+ include ApnServer::Protocol
6
+
7
+ def address
8
+ [12345, '127.0.0.1']
9
+ end
10
+ end
11
+
12
+ class TestProtocol < Test::Unit::TestCase
13
+
14
+ def setup
15
+ @server = TestServer.new
16
+ @server.queue = Array.new # fake out EM::Queue
17
+ end
18
+
19
+ def test_adds_notification_to_queue
20
+ token = "12345678123456781234567812345678"
21
+ @server.receive_data("\0\0 #{token}\0#{22.chr}{\"aps\":{\"alert\":\"Hi\"}}")
22
+ assert_equal 1, @server.queue.size
23
+ end
24
+
25
+ def test_does_not_add_invalid_notification
26
+ @server.receive_data('fakedata')
27
+ assert @server.queue.empty?
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apnserver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.9
5
+ platform: ruby
6
+ authors:
7
+ - Ben Poweski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-25 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: daemons
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: json
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ description: A toolkit for proxying and sending Apple Push Notifications
46
+ email: bpoweski@3factors.com
47
+ executables:
48
+ - apnsend
49
+ - apnserverd
50
+ - apnserverd.fedora.init
51
+ extensions: []
52
+
53
+ extra_rdoc_files:
54
+ - README.textile
55
+ files:
56
+ - README.textile
57
+ - Rakefile
58
+ - VERSION
59
+ - bin/apnsend
60
+ - bin/apnserverd
61
+ - bin/apnserverd.fedora.init
62
+ - lib/apnserver.rb
63
+ - lib/apnserver/client.rb
64
+ - lib/apnserver/notification.rb
65
+ - lib/apnserver/payload.rb
66
+ - lib/apnserver/protocol.rb
67
+ - lib/apnserver/server.rb
68
+ - lib/apnserver/server_connection.rb
69
+ - test/test_client.rb
70
+ - test/test_helper.rb
71
+ - test/test_notification.rb
72
+ - test/test_payload.rb
73
+ - test/test_protocol.rb
74
+ has_rdoc: true
75
+ homepage: http://github.com/bpoweski/apnserver
76
+ post_install_message:
77
+ rdoc_options:
78
+ - --charset=UTF-8
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ version:
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ version:
93
+ requirements: []
94
+
95
+ rubyforge_project: apnserver
96
+ rubygems_version: 1.3.1
97
+ signing_key:
98
+ specification_version: 2
99
+ summary: Apple Push Notification Server
100
+ test_files:
101
+ - test/test_client.rb
102
+ - test/test_helper.rb
103
+ - test/test_notification.rb
104
+ - test/test_payload.rb
105
+ - test/test_protocol.rb