apnmachine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ gem "em-synchrony"
4
+ gem "daemons"
5
+ gem "activesupport", ">= 3.0.0"
6
+ gem "redis", ">= 2.2.0"
7
+ gem "i18n"
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "shoulda", ">= 0"
13
+ gem "rdoc", "~> 3.12"
14
+ gem "bundler", "~> 1.0.0"
15
+ gem "jeweler", "~> 1.8.3"
16
+ gem 'simplecov', :require => false
17
+ end
@@ -0,0 +1,41 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.0.9)
5
+ daemons (1.1.4)
6
+ em-synchrony (1.0.0)
7
+ eventmachine (>= 1.0.0.beta.1)
8
+ eventmachine (1.0.0.beta.4)
9
+ git (1.2.5)
10
+ i18n (0.6.0)
11
+ jeweler (1.8.3)
12
+ bundler (~> 1.0)
13
+ git (>= 1.2.5)
14
+ rake
15
+ rdoc
16
+ json (1.6.5)
17
+ multi_json (1.0.4)
18
+ rake (0.9.2.2)
19
+ rdoc (3.12)
20
+ json (~> 1.4)
21
+ redis (2.2.2)
22
+ shoulda (2.11.3)
23
+ simplecov (0.5.4)
24
+ multi_json (~> 1.0.3)
25
+ simplecov-html (~> 0.5.3)
26
+ simplecov-html (0.5.3)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ activesupport (>= 3.0.0)
33
+ bundler (~> 1.0.0)
34
+ daemons
35
+ em-synchrony
36
+ i18n
37
+ jeweler (~> 1.8.3)
38
+ rdoc (~> 3.12)
39
+ redis (>= 2.2.0)
40
+ shoulda
41
+ simplecov
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Julien Nakache
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,132 @@
1
+ h1. Apple Push Notification Server Toolkit
2
+
3
+ * http://github.com/jnak/apnmachine
4
+
5
+ h2. Description
6
+
7
+ I want:
8
+ - persistent connection to APN Servers (as Apple recommends)
9
+ - real-time notifications (no regular polling a la Resque)
10
+ - super easy to use in Ruby and any languages (as easy as enqueuing a serialized JSON hash in Redis)
11
+ - persist and queue messages when server is down
12
+ - horizontal scalability and out-of-the-box load-balancing
13
+ - fast daemons
14
+
15
+ So I built ApnMachine. We're running it in production at zapkast.com and find it very reliable.
16
+
17
+
18
+ h2. Remaining Tasks
19
+
20
+ * Implement feedback service mechanism
21
+ * Write real tests
22
+
23
+ h2. APN Server Daemon
24
+
25
+ To start ApnMachine, tell it where is your redis server and the complete path to your PEM file.
26
+ That's it.
27
+
28
+ <pre>
29
+ <code>
30
+ Usage: apnmachined [options] --pem /path/to/pem
31
+ --address bind address (defaults to 127.0.0.1)
32
+ address of your redis server
33
+
34
+ --port port
35
+ the port of your redis server (defaults to)
36
+
37
+ --apn-address server
38
+ APN Server (defaults to gateway.push.apple.com)
39
+ Use 'sandbox' to connect to gateway.sandbox.push.apple.com
40
+
41
+ --apn-port port of the APN Server
42
+ APN server port (defaults to 2195)
43
+
44
+ --pem pem file path
45
+ The PEM encoded private key and certificate.
46
+ To export a PEM ecoded file execute
47
+ # openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts
48
+
49
+ --pem-passphrase passphrase
50
+ The PEM passphrase to decode key.
51
+ Default to nil
52
+
53
+ --help
54
+ usage message
55
+
56
+ --daemon or -d
57
+ Runs process as daemon, not available on Windows
58
+ </code>
59
+ </pre>
60
+
61
+ h2. Sending Notifications from Ruby
62
+
63
+ To send a notification, you just need a working Redis client that responds to rpush. It doesn't matter if you're
64
+ in an EventMachine program or a plain vanilla Rails app.
65
+
66
+ <pre>
67
+ <code>
68
+ ApnMachine::Config.port = @a_redis_client
69
+ ApnMachine::Config.logger = Rails.logger
70
+ </code>
71
+ </pre>
72
+
73
+ Finally:
74
+
75
+ <pre>
76
+ <code>
77
+ notification = ApnMachine::Notification.new
78
+ notification.device_token = apns_token
79
+ notification.alert = message
80
+ notification.badge = 1
81
+ notification.sound = 'default'
82
+ notification.push
83
+ </code>
84
+ </pre>
85
+
86
+
87
+ h2. Installation
88
+
89
+ Apnserver is hosted on "rubygems":https://rubygems.org/gems/apnmachine
90
+
91
+ <pre>
92
+ <code>
93
+ $ gem install apnmachine
94
+ </code>
95
+ </pre>
96
+
97
+ Adding apnmachine to your Rails application
98
+
99
+ <pre>
100
+ <code>
101
+ gem 'apnmachine'
102
+ </code>
103
+ </pre>
104
+
105
+
106
+ h2. License
107
+
108
+ Widely Inspired from groupme/em-apn and bpoweski/apnserver
109
+
110
+ (The MIT License)
111
+
112
+ Copyright (c) 2012 Julien Nakache
113
+
114
+ Permission is hereby granted, free of charge, to any person obtaining
115
+ a copy of this software and associated documentation files (the
116
+ 'Software'), to deal in the Software without restriction, including
117
+ without limitation the rights to use, copy, modify, merge, publish,
118
+ distribute, sublicense, and/or sell copies of the Software, and to
119
+ permit persons to whom the Software is furnished to do so, subject to
120
+ the following conditions:
121
+
122
+ The above copyright notice and this permission notice shall be
123
+ included in all copies or substantial portions of the Software.
124
+
125
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
126
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
127
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
128
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
129
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
130
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
131
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
132
+
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "apnmachine"
18
+ gem.homepage = "http://github.com/jnak/apnmachine"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{An APN server & library built on top of Redis and EventMachine}
21
+ gem.description = %Q{An APN server & library in which EventMachine daemons maintain a persistent connection to Apple servers and Redis acts as the glue with your Apps. See Readme for more info :)}
22
+ gem.email = "julien.nakache@gmail.com"
23
+ gem.authors = ["Julien Nakache"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ # require 'simplecov'
36
+ # Simplecov::SimplecovTask.new do |test|
37
+ # test.libs << 'test'
38
+ # test.pattern = 'test/**/test_*.rb'
39
+ # test.verbose = true
40
+ # test.rcov_opts << '--exclude "gems/*"'
41
+ # end
42
+
43
+ task :default => :test
44
+
45
+ require 'rdoc/task'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "apnmachine #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,92 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{apnmachine}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Julien Nakache}]
12
+ s.date = %q{2012-02-21}
13
+ s.description = %q{An APN server & library in which EventMachine daemons maintain a persistent connection to Apple servers and Redis acts as the glue with your Apps. See Readme for more info :)}
14
+ s.email = %q{julien.nakache@gmail.com}
15
+ s.executables = [%q{apnmachined}]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.textile"
19
+ ]
20
+ s.files = [
21
+ ".DS_Store",
22
+ ".document",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE.txt",
26
+ "README.textile",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "apnmachine.gemspec",
30
+ "bin/apnmachined",
31
+ "lib/.DS_Store",
32
+ "lib/apnmachine.rb",
33
+ "lib/apnmachine/.DS_Store",
34
+ "lib/apnmachine/config.rb",
35
+ "lib/apnmachine/notification.rb",
36
+ "lib/apnmachine/server.rb",
37
+ "lib/apnmachine/server/.DS_Store",
38
+ "lib/apnmachine/server/client.rb",
39
+ "lib/apnmachine/server/error_response.rb",
40
+ "lib/apnmachine/server/response.rb",
41
+ "lib/apnmachine/server/server.rb",
42
+ "lib/apnmachine/server/server_connection.rb",
43
+ "lib/apnmachine/version.rb",
44
+ "test/helper.rb",
45
+ "test/test_apnmachine.rb"
46
+ ]
47
+ s.homepage = %q{http://github.com/jnak/apnmachine}
48
+ s.licenses = [%q{MIT}]
49
+ s.require_paths = [%q{lib}]
50
+ s.rubygems_version = %q{1.8.5}
51
+ s.summary = %q{An APN server & library built on top of Redis and EventMachine}
52
+
53
+ if s.respond_to? :specification_version then
54
+ s.specification_version = 3
55
+
56
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
+ s.add_runtime_dependency(%q<em-synchrony>, [">= 0"])
58
+ s.add_runtime_dependency(%q<daemons>, [">= 0"])
59
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0"])
60
+ s.add_runtime_dependency(%q<redis>, [">= 2.2.0"])
61
+ s.add_runtime_dependency(%q<i18n>, [">= 0"])
62
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
63
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
64
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
65
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
66
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
67
+ else
68
+ s.add_dependency(%q<em-synchrony>, [">= 0"])
69
+ s.add_dependency(%q<daemons>, [">= 0"])
70
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
71
+ s.add_dependency(%q<redis>, [">= 2.2.0"])
72
+ s.add_dependency(%q<i18n>, [">= 0"])
73
+ s.add_dependency(%q<shoulda>, [">= 0"])
74
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
75
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
76
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
77
+ s.add_dependency(%q<simplecov>, [">= 0"])
78
+ end
79
+ else
80
+ s.add_dependency(%q<em-synchrony>, [">= 0"])
81
+ s.add_dependency(%q<daemons>, [">= 0"])
82
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
83
+ s.add_dependency(%q<redis>, [">= 2.2.0"])
84
+ s.add_dependency(%q<i18n>, [">= 0"])
85
+ s.add_dependency(%q<shoulda>, [">= 0"])
86
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
87
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
88
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
89
+ s.add_dependency(%q<simplecov>, [">= 0"])
90
+ end
91
+ end
92
+
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'getoptlong'
4
+ require 'daemons'
5
+ require 'apnmachine'
6
+ require 'apnmachine/server'
7
+
8
+ def usage
9
+ puts "Usage: apnmchined [switches] --pem <path>"
10
+ puts " --redis-address [127.0.0.1] bind address of proxy"
11
+ puts " --redis-port [6379] port proxy listens on"
12
+ puts " --server <gateway.push.apple.com> the apn server to send messages to"
13
+ puts " --log </var/log/apnmachined.log the path to store the log"
14
+ puts " --daemon to daemonize the server"
15
+ puts " --help this message"
16
+ end
17
+
18
+ def daemonize
19
+ options = {
20
+ :backtrace => true,
21
+ :ontop => false,
22
+ :log_output => true,
23
+ :app_name => 'apnmachined'
24
+ }
25
+ Daemons.daemonize(options)
26
+ end
27
+
28
+ opts = GetoptLong.new(
29
+ ["--redis-address", "-b", GetoptLong::REQUIRED_ARGUMENT],
30
+ ["--redis-port", "-p", GetoptLong::REQUIRED_ARGUMENT],
31
+ ["--server", "-s", GetoptLong::REQUIRED_ARGUMENT],
32
+ ["--log", "-l", GetoptLong::REQUIRED_ARGUMENT],
33
+ ["--pem", "-c", GetoptLong::REQUIRED_ARGUMENT],
34
+ ["--daemon", "-d", GetoptLong::NO_ARGUMENT],
35
+ ["--help", "-h", GetoptLong::NO_ARGUMENT]
36
+ )
37
+
38
+ redis_address = '127.0.0.1'
39
+ redis_port = 6379
40
+ host = 'gateway.push.apple.com'
41
+ pem = nil
42
+ daemon = false
43
+ log = STDOUT
44
+
45
+ opts.each do |opt, arg|
46
+ case opt
47
+ when '--help'
48
+ usage
49
+ exit 1
50
+ when '--redis-address'
51
+ redis_address = arg
52
+ when '--port'
53
+ redis_port = arg.to_i
54
+ when '--server'
55
+ if arg == 'sandbox'
56
+ host = 'gateway.sandbox.push.apple.com'
57
+ else
58
+ raise 'Wrong --server option. Pass "sandbox" as option to override production servers'
59
+ end
60
+ when '--pem'
61
+ pem = arg
62
+ when '--daemon'
63
+ daemon = true
64
+ when '--log'
65
+ log = arg
66
+ end
67
+ end
68
+
69
+ if pem.nil?
70
+ usage
71
+ exit 1
72
+ else
73
+ daemonize if daemon
74
+ server = ApnMachine::Server::Server.new(pem, redis_address, redis_port, log)
75
+ server.start!
76
+ end
Binary file
@@ -0,0 +1,7 @@
1
+ require 'base64'
2
+ require 'active_support/ordered_hash'
3
+ require 'active_support/json'
4
+ require 'logger'
5
+ require 'apnmachine/config'
6
+ require 'apnmachine/notification'
7
+
Binary file
@@ -0,0 +1,7 @@
1
+ module ApnMachine
2
+ class Config
3
+ class << self
4
+ attr_accessor :redis, :logger
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ module ApnMachine
2
+ class Notification
3
+
4
+ attr_accessor :device_token, :alert, :badge, :sound, :custom
5
+
6
+ PAYLOAD_MAX_BYTES = 256
7
+ class PayloadTooLarge < StandardError;end
8
+ class NoDeviceToken < StandardError;end
9
+
10
+ def encode_payload
11
+ p = {:aps => Hash.new}
12
+ [:badge, :alert, :sound].each do |k|
13
+ p[:aps][k] = send(k) if send(k)
14
+ end
15
+ p.merge!(custom) if send(:custom)
16
+
17
+ j = ActiveSupport::JSON.encode(p)
18
+ raise PayloadTooLarge.new("The payload is larger than allowed: #{j.length}") if j.size > PAYLOAD_MAX_BYTES
19
+
20
+ p[:device_token] = device_token
21
+ raise NoDeviceToken.new("No device token") unless device_token
22
+
23
+ ActiveSupport::JSON.encode(p)
24
+ end
25
+
26
+ def push
27
+ raise 'No Redis client' if Config.redis.nil?
28
+ socket = Config.redis.rpush "apnmachine.queue", encode_payload
29
+ end
30
+
31
+ def self.to_bytes(encoded_payload)
32
+ notif_hash = ActiveSupport::JSON.decode(encoded_payload)
33
+
34
+ device_token = notif_hash.delete('device_token')
35
+ bin_token = [device_token].pack('H*')
36
+ raise NoDeviceToken.new("No device token") unless device_token
37
+
38
+ j = ActiveSupport::JSON.encode(notif_hash)
39
+ raise PayloadTooLarge.new("The payload is larger than allowed: #{j.length}") if j.size > PAYLOAD_MAX_BYTES
40
+
41
+ Config.logger.debug "TOKEN:#{device_token} | ALERT:#{notif_hash.inspect}"
42
+
43
+ [0, 0, bin_token.size, bin_token, 0, j.size, j].pack("ccca*cca*")
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,11 @@
1
+ require 'openssl'
2
+ require 'em-synchrony'
3
+ require 'redis/connection/synchrony'
4
+ require 'redis'
5
+ require 'logger'
6
+
7
+ require 'apnmachine/server/client'
8
+ require 'apnmachine/server/server_connection'
9
+ require 'apnmachine/server/error_response'
10
+ require 'apnmachine/server/response'
11
+ require 'apnmachine/server/server'
@@ -0,0 +1,40 @@
1
+ module ApnMachine
2
+ module Server
3
+ class Client
4
+ attr_accessor :pem, :host, :port, :password, :key, :cert, :close_callback
5
+
6
+ def initialize(pem, host = 'gateway.push.apple.com', port = 2195, pass = nil)
7
+ @pem, @host, @port, @password = pem, host, port, pass
8
+ end
9
+
10
+ def connect!
11
+ raise "The path to your pem file is not set." unless @pem
12
+ raise "The path to your pem file does not exist!" unless File.exist?(@pem)
13
+ @key, @cert = @pem, @pem
14
+ @connection = EM.connect(host, port, ApnMachine::Server::ServerConnection, self)
15
+ end
16
+
17
+ def disconnect!
18
+ @connection.close_connection
19
+ end
20
+
21
+ def write(notif_bin)
22
+ Config.logger.debug "#{Time.now} [#{host}:#{port}] New notif"
23
+ @connection.send_data(notif_bin)
24
+ end
25
+
26
+ def connected?
27
+ @connection.connected?
28
+ end
29
+
30
+ def on_error(&block)
31
+ @error_callback = block
32
+ end
33
+
34
+ def on_close(&block)
35
+ @close_callback = block
36
+ end
37
+
38
+ end #client
39
+ end #server
40
+ end #apnmachine
@@ -0,0 +1,35 @@
1
+ module ApnMachine
2
+ module Server
3
+ class ErrorResponse
4
+
5
+ DESCRIPTION = {
6
+ 0 => "No errors encountered",
7
+ 1 => "Processing error",
8
+ 2 => "Missing device token",
9
+ 3 => "Missing topic",
10
+ 4 => "Missing payload",
11
+ 5 => "Invalid token size",
12
+ 6 => "Invalid topic size",
13
+ 7 => "Invalid payload size",
14
+ 8 => "Invalid token",
15
+ 255 => "None (unknown)"
16
+ }
17
+
18
+ attr_reader :command, :status_code, :identifier
19
+
20
+ def initialize(command, status_code, identifier)
21
+ @command = command
22
+ @status_code = status_code
23
+ @identifier = identifier
24
+ end
25
+
26
+ def to_s
27
+ "CODE=#{@status_code} ID=#{@identifier} DESC=#{description}"
28
+ end
29
+
30
+ def description
31
+ DESCRIPTION[@status_code] || "Missing description"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ module ApnMachine
2
+ module Server
3
+ class Response
4
+ def initialize(notification)
5
+ @notification = notification
6
+ end
7
+
8
+ def to_s
9
+ "TOKEN=#{@notification.token}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,77 @@
1
+ module ApnMachine
2
+ module Server
3
+ class Server
4
+ attr_accessor :client, :bind_address, :port, :redis
5
+
6
+ def initialize(pem, redis_address, redis_port, log = '/apnmachined.log')
7
+ @client = ApnMachine::Server::Client.new(pem)
8
+ @bind_address, @port = bind_address, redis_port
9
+ @redis = Redis.new
10
+
11
+ #set logging options
12
+ if log == STDOUT
13
+ Config.logger = Logger.new STDOUT
14
+ elsif File.exist?(log)
15
+ @flog = File.open(log, File::WRONLY | File::APPEND)
16
+ Config.logger = Logger.new(@flog, 'daily')
17
+ else
18
+ FileUtils.mkdir_p(File.dirname(log))
19
+ @flog = File.open(log, File::WRONLY | File::APPEND | File::CREAT)
20
+ Config.logger = Logger.new(@flog, 'daily')
21
+ end
22
+
23
+ end
24
+
25
+ def start!
26
+ EM.synchrony do
27
+ EM::Synchrony.add_periodic_timer(5) { @flog.flush if @flog }
28
+ Config.logger.info "Connecting to Apple Servers"
29
+ @client.connect!
30
+ @last_conn_time = Time.now.to_i
31
+
32
+ Config.logger.info "Starting APN Server on Redis"
33
+ loop do
34
+ notification = @redis.blpop("apnmachine.queue", 0)[1]
35
+ retries = 2
36
+
37
+ begin
38
+ #prepare notification
39
+ #next if Notification.valid?(notification)
40
+ notif_bin = Notification.to_bytes(notification)
41
+
42
+ #force deconnection/reconnection after 10 min
43
+ if (@last_conn_time + 1000) < Time.now.to_i || !@client.connected?
44
+ Config.logger.error 'Reconnecting connection to APN'
45
+ @client.disconnect!
46
+ @client.connect!
47
+ @last_conn_time = Time.now.to_i
48
+ end
49
+
50
+ #sending notification
51
+ Config.logger.debug 'Sending notification to APN'
52
+ @client.write(notif_bin)
53
+ Config.logger.debug 'Notif sent'
54
+
55
+ rescue Errno::EPIPE, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ETIMEDOUT
56
+ if retries > 1
57
+ Config.logger.error "Connection to APN servers idle for too long. Trying to reconnect"
58
+ @client.disconnect!
59
+ @client.connect!
60
+ @last_conn_time = Time.now
61
+ retries -= 1
62
+ retry
63
+ else
64
+ Config.logger.error "Can't reconnect to APN Servers! Ignoring notification #{notification.to_s}"
65
+ @client.disconnect!
66
+ @redis.rpush(notification)
67
+ end
68
+ rescue Exception => e
69
+ Config.logger.error "Unable to handle: #{e}"
70
+ end #end of begin
71
+
72
+ end #end of loop
73
+ end # synchrony
74
+ end # def start!
75
+ end #class Server
76
+ end #module Server
77
+ end #module ApnMachine
@@ -0,0 +1,46 @@
1
+ module ApnMachine
2
+ module Server
3
+ class ServerConnection < EM::Connection
4
+ attr_reader :client
5
+
6
+ def initialize(*args)
7
+ super
8
+ @client = args.last
9
+ @disconnected = false
10
+ end
11
+
12
+ def connected?
13
+ !@disconnected
14
+ end
15
+
16
+ def post_init
17
+ start_tls(
18
+ :private_key_file => client.key,
19
+ :cert_chain_file => client.cert,
20
+ :verify_peer => false
21
+ )
22
+ end
23
+
24
+ def connection_completed
25
+ Config.logger.info "Connection to Apple Servers completed"
26
+ end
27
+
28
+ def receive_data(data)
29
+ data_array = data.unpack("ccN")
30
+ Config.logger.info "Error"
31
+ error_response = ErrorResponse.new(*data_array)
32
+ Config.logger.warn(error_response.to_s)
33
+ if client.error_callback
34
+ client.error_callback.call(error_response)
35
+ end
36
+ end
37
+
38
+ def unbind
39
+ @disconnected = true
40
+ Config.logger.info "Connection closed"
41
+ client.close_callback.call if client.close_callback
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module ApnMachine
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'apnmachine'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestApnmachine < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apnmachine
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Julien Nakache
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-02-21 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: em-synchrony
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: daemons
28
+ requirement: &id002 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: activesupport
39
+ requirement: &id003 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 3.0.0
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: redis
50
+ requirement: &id004 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.2.0
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: i18n
61
+ requirement: &id005 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: *id005
70
+ - !ruby/object:Gem::Dependency
71
+ name: shoulda
72
+ requirement: &id006 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: *id006
81
+ - !ruby/object:Gem::Dependency
82
+ name: rdoc
83
+ requirement: &id007 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ version: "3.12"
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: *id007
92
+ - !ruby/object:Gem::Dependency
93
+ name: bundler
94
+ requirement: &id008 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ~>
98
+ - !ruby/object:Gem::Version
99
+ version: 1.0.0
100
+ type: :development
101
+ prerelease: false
102
+ version_requirements: *id008
103
+ - !ruby/object:Gem::Dependency
104
+ name: jeweler
105
+ requirement: &id009 !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 1.8.3
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: *id009
114
+ - !ruby/object:Gem::Dependency
115
+ name: simplecov
116
+ requirement: &id010 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: "0"
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: *id010
125
+ description: An APN server & library in which EventMachine daemons maintain a persistent connection to Apple servers and Redis acts as the glue with your Apps. See Readme for more info :)
126
+ email: julien.nakache@gmail.com
127
+ executables:
128
+ - apnmachined
129
+ extensions: []
130
+
131
+ extra_rdoc_files:
132
+ - LICENSE.txt
133
+ - README.textile
134
+ files:
135
+ - .DS_Store
136
+ - .document
137
+ - Gemfile
138
+ - Gemfile.lock
139
+ - LICENSE.txt
140
+ - README.textile
141
+ - Rakefile
142
+ - VERSION
143
+ - apnmachine.gemspec
144
+ - bin/apnmachined
145
+ - lib/.DS_Store
146
+ - lib/apnmachine.rb
147
+ - lib/apnmachine/.DS_Store
148
+ - lib/apnmachine/config.rb
149
+ - lib/apnmachine/notification.rb
150
+ - lib/apnmachine/server.rb
151
+ - lib/apnmachine/server/.DS_Store
152
+ - lib/apnmachine/server/client.rb
153
+ - lib/apnmachine/server/error_response.rb
154
+ - lib/apnmachine/server/response.rb
155
+ - lib/apnmachine/server/server.rb
156
+ - lib/apnmachine/server/server_connection.rb
157
+ - lib/apnmachine/version.rb
158
+ - test/helper.rb
159
+ - test/test_apnmachine.rb
160
+ homepage: http://github.com/jnak/apnmachine
161
+ licenses:
162
+ - MIT
163
+ post_install_message:
164
+ rdoc_options: []
165
+
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ hash: -105431761613938155
174
+ segments:
175
+ - 0
176
+ version: "0"
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: "0"
183
+ requirements: []
184
+
185
+ rubyforge_project:
186
+ rubygems_version: 1.8.5
187
+ signing_key:
188
+ specification_version: 3
189
+ summary: An APN server & library built on top of Redis and EventMachine
190
+ test_files: []
191
+