Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5c50800
replaced ActiveSupport with Yajl
jeremytregunna Mar 18, 2011
3fb6edc
Fixed #11256615 in pivotal tracker.
jeremytregunna Mar 18, 2011
7816c19
Fixed a bug where reset connections wouldn't be handled properly. Als…
jeremytregunna Mar 18, 2011
699af5d
added convenience method to parse the feedback data
jeremytregunna Mar 18, 2011
a712797
Removed the requirement for the certificate to be a file. Now it's ex…
jeremytregunna Mar 19, 2011
28eb862
Updated dependency information in gemspec
jeremytregunna Mar 21, 2011
a2f7c7e
Able now to pass the key through to the modified apnserver as a file …
jeremytregunna Mar 21, 2011
54a0975
Feedback service now tied in, though we're only printing records we g…
jeremytregunna Mar 21, 2011
44fcec0
Updated feedback code introducing a callback on the server to handle …
jeremytregunna Mar 23, 2011
45971ad
updated README to reflect state of modifications
jeremytregunna Mar 23, 2011
7e6cc6e
added feedback client spec, minimal.
jeremytregunna Mar 23, 2011
cad9fe5
Added beanstalk requirement
jeremytregunna Mar 27, 2011
46f7bd1
Rewrote Server to use beanstalk for input, instead of opening a liste…
jeremytregunna Mar 27, 2011
cede01d
changed server daemon arguments, some don't apply anymore. cleanup of…
jeremytregunna Mar 27, 2011
d73eaa6
Updated README to reflect state of project
jeremytregunna Mar 27, 2011
0e23f12
removed unnecessary files, renaming project to frac-apnserver to avoi…
jeremytregunna Mar 27, 2011
13822af
refactoring of server handle_job
jeremytregunna Mar 27, 2011
bf6b013
packet formation change
jeremytregunna Mar 28, 2011
badcad3
Some modifications to the expected job format, updated Notification.push
emcmanus Apr 22, 2011
09b944f
Encountered an issue with unicode characters in Notification.alert. F…
emcmanus Apr 22, 2011
d155fd1
Lowered logger level to INFO
emcmanus Apr 23, 2011
13df807
Removed feedback service checks, disabled persistent sockets
emcmanus Apr 23, 2011
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ source "http://rubygems.org"

gem "eventmachine"
gem "daemons"
gem "activesupport", ">= 3.0.0"
gem "i18n" # active support whines without this
gem "yajl-ruby"
gem "beanstalk-client"
gem "fracassandra", ">= 0.3.4"

group :spec do
gem "rspec"
end
end
6 changes: 2 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
GEM
remote: http://rubygems.org/
specs:
activesupport (3.0.3)
daemons (1.0.10)
diff-lcs (1.1.2)
eventmachine (0.12.10)
i18n (0.5.0)
rspec (2.3.0)
rspec-core (~> 2.3.0)
rspec-expectations (~> 2.3.0)
Expand All @@ -14,13 +12,13 @@ GEM
rspec-expectations (2.3.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.3.0)
yajl-ruby (0.8.1)

PLATFORMS
ruby

DEPENDENCIES
activesupport (>= 3.0.0)
daemons
eventmachine
i18n
rspec
yajl-ruby
83 changes: 47 additions & 36 deletions README.textile
Original file line number Diff line number Diff line change
@@ -1,54 +1,64 @@
h1. Apple Push Notification Server Toolkit
h1. Apple Push Notification Server Toolkit for frac.as

* http://github.com/bpoweski/apnserver
This project started off as a fork of "apnserver":https://github.com/bpoweski/apnserver. It
has since taken on a different path. How does it differ from apnserver? By a few key points:

h2. Description
# It implements the APNS feedback service;
# Uses Yajl for JSON encoding/decoding rather than ActiveSupport;
# Expects certificates as strings instead of paths to files;
# Does not assume there is only one certificate; and
# Receives packets containing notifications from beanstalkd instead of a listening socket.

apnserver is a server and set of command line programs to send push notifications to the iPhone.
Apple recommends to maintain an open connection to the push notification service and refrain from
opening up and tearing down SSL connections repeated. To solve this problem an intermediate
network server is introduced that queues are requests to the APN service and sends them via a
persistent connection.
The above changes were made because of the need for an APNS provider to replace the current
provider used by "Diligent Street":http://www.diligentstreet.com/ with something more robust. As such, it needs to be
suitable for a hosted environment, where multiple—unrelated—users of the service will be
using it.

h2. Remaining Tasks
It should be noted that the development of this project is independent of the work bpoweski
is doing on apnserver. If you're looking for that project, "go here":https://github.com/bpoweski/apnserver.

* Implement feedback service mechanism
* Implement robust notification sending in reactor periodic scheduler
h2. Description

h2. Issues Fixed
frac-apnserver is a server and a set of command line programs to send push notifications to iOS
devices. Apple recommends to maintain an open connection to the push notification service, and
refrain from opening up and tearing down SSL connections repeatedly. As such, a separate daemon
is introduced that has messages queued up (beanstalkd) for consumption by this daemon. This
decouples the APNS server from your backend system. Those notifications are sent over a
persistent connection to Apple.

* second attempt at retry logic, SSL Errors close down sockets now
* apnsend --badge option correctly sends integer number rather than string of number for aps json payload
* connections are properly closed in Notification#push method now
* removed json gem in favor of ActiveSupport
* Rails 3.x support
* drop the erroneous puts statements in favor a configurable logger
* moved to Rspec
h2. Remaining Tasks & Issues

h2. APN Server Daemon
You can see progress by looking at the "Pivotal Tracker":https://www.pivotaltracker.com/projects/251991 page for fracas. Any labels related to
_apnserver_ are related to this subproject.

h2. Preparing Certificates

Certificates must be prepared before they can be used with frac-apnserver. Unfortunately,
Apple gives us @.p12@ files instead of @.pem@ files. As such, we need to convert them. This
can be accomplished by dropping to the command line and running this command:

<pre>
<code>
Usage: apnserverd [options] --pem /path/to/pem
--bind-address bind address (defaults to 0.0.0.0)
bind address of the server daemon
<code>
$ openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
</code>
</pre>

--proxy-port port
the port that the daemon will listen on (defaults to 22195)
This will generate a file suitable for use with this daemon, called @cert.pem@. If you're
using frac.as, this is the file you would upload to the web service.

--server server
APN Server (defaults to gateway.push.apple.com)
h2. APN Server Daemon

--port port of the APN Server
APN server port (defaults to 2195)
<pre>
<code>
Usage: apnserverd [options] --beanstalk
--beanstalk <csv ip:port mappings>
The comma-separated list of ip:port for beanstalk servers

--pem pem file path
The PEM encoded private key and certificate.
To export a PEM ecoded file execute
# openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
--pid <pid file path>
Path used for the PID file. Defaults to /var/run/apnserver.pid

--pem-passphrase passphrase
The PEM passphrase to decode key.
--log <log file path>
Path used for the log file. Defaults to /var/log/apnserver.log

--help
usage message
Expand Down Expand Up @@ -153,6 +163,7 @@ h2. License
(The MIT License)

Copyright (c) 2011 Ben Poweski
Copyright (c) 2011 Jeremy Tregunna

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
17 changes: 9 additions & 8 deletions apnserver.gemspec
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
Gem::Specification.new do |s|
s.name = %q{apnserver}
s.version = "0.2.1"
s.version = "0.3.0"
s.platform = Gem::Platform::RUBY

s.authors = ["Ben Poweski"]
s.date = %q{2011-01-01}
s.description = %q{A toolkit for proxying and sending Apple Push Notifications}
s.email = %q{bpoweski@3factors.com}
s.authors = ["Jeremy Tregunna"]
s.date = %q{2011-03-27}
s.description = %q{A toolkit for proxying and sending Apple Push Notifications prepared for a hosted environment}
s.email = %q{jeremy.tregunna@me.com}
s.executables = ["apnsend", "apnserverd"]
s.extra_rdoc_files = ["README.textile"]
s.files = Dir.glob("{bin,lib}/**/*") + %w(README.textile)
s.homepage = %q{http://github.com/bpoweski/apnserver}
s.homepage = %q{https://github.com/jeremytregunna/apnserver}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{apnserver}
s.rubygems_version = %q{1.3.5}
s.summary = %q{Apple Push Notification Toolkit}
s.summary = %q{Apple Push Notification Toolkit for hosted environments}
s.test_files = Dir.glob("spec/**/*")

s.required_rubygems_version = ">= 1.3.6"
s.add_dependency 'activesupport', '~> 3.0.0'
s.add_dependency 'yajl-ruby', '>= 0.7.0'
s.add_dependency 'beanstalk-client', '>= 1.0.0'
s.add_development_dependency 'bundler', '~> 1.0.0'
s.add_development_dependency 'eventmachine', '>= 0.12.8'
end
2 changes: 1 addition & 1 deletion bin/apnsend
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ opts.each do |opt, arg|
when '--port'
ApnServer::Config.port = arg.to_i
when '--pem'
ApnServer::Config.pem = arg
ApnServer::Config.pem = File.read(arg)
when '--pem-passphrase'
ApnServer::Config.password = arg
when '--alert'
Expand Down
57 changes: 16 additions & 41 deletions bin/apnserverd
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'getoptlong'
require 'rubygems'
require 'daemons'
require 'eventmachine'
require 'apnserver'
require 'apnserver/server'
require 'csv'

def usage
puts "Usage: apnserverd [switches] --pem <path>"
puts " --pem-passphrase <passphrase> pem passphrase"
puts " --bind-address [0.0.0.0] bind address of proxy"
puts " --proxy-port [22195] port proxy listens on"
puts " --server <gateway.push.apple.com> the apn server to send messages to"
puts " --port <2195> the port of the apn server"
puts "Usage: apnserverd [switches] --beanstalk a.b.c.d:11300,...,w.x.y.z:11300"
puts " --beanstalk <127.0.0.1:11300> csv list of ip:port for beanstalk servers"
puts " --pid </var/run/apnserverd.pid the path to store the pid"
puts " --log </var/log/apnserverd.log the path to store the log"
puts " --daemon to daemonize the server"
Expand All @@ -30,24 +28,14 @@ def daemonize
end

opts = GetoptLong.new(
["--bind-address", "-b", GetoptLong::REQUIRED_ARGUMENT],
["--proxy-port", "-P", GetoptLong::REQUIRED_ARGUMENT],
["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
["--port", "-p", GetoptLong::REQUIRED_ARGUMENT],
["--beanstalk", "-b", GetoptLong::REQUIRED_ARGUMENT],
["--pid", "-i", GetoptLong::REQUIRED_ARGUMENT],
["--log", "-l", GetoptLong::REQUIRED_ARGUMENT],
["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
["--pem-passphrase", "-C", GetoptLong::REQUIRED_ARGUMENT],
["--help", "-h", GetoptLong::NO_ARGUMENT],
["--daemon", "-d", GetoptLong::NO_ARGUMENT]
)

bind_address = '0.0.0.0'
proxy_port = 22195
host = 'gateway.push.apple.com'
port = 2195
pem = nil
pem_passphrase = nil
beanstalks = ["127.0.0.1:11300"]
@pid_file = '/var/run/apnserverd.pid'
@log_file = '/var/log/apnserverd.log'
daemon = false
Expand All @@ -57,35 +45,22 @@ opts.each do |opt, arg|
when '--help'
usage
exit 1
when '--bind-address'
bind_address = arg
when '--proxy-port'
proxy_port = arg.to_i
when '--server'
host = arg
when '--port'
port = arg.to_i
when '--beanstalk'
beanstalks = CSV.parse(arg)[0]
when '--pid'
@pid_file = arg
when '--log'
@log_file = arg
when '--pem'
pem = arg
when '--pem-passphrase'
pem_passphrase = arg
when '--daemon'
daemon = true
end
end

if pem.nil?
usage
exit 1
else
daemonize if daemon
server = ApnServer::Server.new(pem, bind_address, proxy_port)
server.client.host = host
server.client.port = port
server.client.password = pem_passphrase
server.start!
end
ApnServer::Config.logger = Logger.new(@log_file)
ApnServer::Config.logger.level = Logger::DEBUG

daemonize if daemon
server = ApnServer::Server.new(beanstalks) do |feedback_record|
ApnServer::Config.logger.info "Received feedback at #{feedback_record[:feedback_at]} (length: #{feedback_record[:length]}): #{feedback_record[:device_token]}"
end
server.start!
1 change: 1 addition & 0 deletions lib/apnserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
require 'apnserver/payload'
require 'apnserver/notification'
require 'apnserver/client'
require 'apnserver/feedback_client'
9 changes: 4 additions & 5 deletions lib/apnserver/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil)
end

def connect!
raise "The path to your pem file is not set." unless self.pem
raise "The path to your pem file does not exist!" unless File.exist?(self.pem)
raise "Your certificate is not set." unless self.pem

@context = OpenSSL::SSL::SSLContext.new
@context.cert = OpenSSL::X509::Certificate.new(File.read(self.pem))
@context.key = OpenSSL::PKey::RSA.new(File.read(self.pem), self.password)
@context.cert = OpenSSL::X509::Certificate.new(self.pem)
@context.key = OpenSSL::PKey::RSA.new(self.pem, self.password)

@sock = TCPSocket.new(self.host, self.port.to_i)
@ssl = OpenSSL::SSL::SSLSocket.new(@sock, @context)
Expand All @@ -40,4 +39,4 @@ def connected?
@ssl
end
end
end
end
24 changes: 24 additions & 0 deletions lib/apnserver/feedback_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Feedback service

module ApnServer
class FeedbackClient < Client
def initialize(pem, host = 'feedback.push.apple.com', port = 2196, pass = nil)
@pem, @host, @port, @pass = pem, host, port, pass
end

def read
records ||= []
while record = @ssl.read(38)
records << parse_tuple(record)
end
records
end

private

def parse_tuple(data)
feedback = data.unpack("N1n1H*")
{ :feedback_at => Time.at(feedback[0]), :length => feedback[1], :device_token => feedback[2] }
end
end
end
Loading