apnserver 0.1.9

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 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