apnmachine 0.1.0

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