apple_shove 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
19
+ *.output
20
+ *.pid
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in apple_shove.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Taylor Boyko
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # AppleShove [![Code Climate](https://codeclimate.com/github/tboyko/apple_shove.png)](https://codeclimate.com/github/tboyko/apple_shove)
2
+
3
+ APN Service Provider. More powerful than a push...
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'apple_shove'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install apple_shove
18
+
19
+ ## Usage
20
+
21
+ # bundle exec rake -T
22
+ bundle exec rake apple_shove:run
23
+ bundle exec rake apple_shove:start
24
+ bundle exec rake apple_shove:stop
25
+ bundle exec rake apple_shove:status
26
+ bundle exec rake apple_shove:stats
27
+
28
+ ### Optional Command Line Arguments
29
+
30
+ log_dir: specify an absolute path if you want to log
31
+ pid_dir: specify an absolute or relative path where the PID file
32
+ is to be stored. Defaults to the current directory.
33
+ connection_limit: maximum number of simultaneous connections to Apple
34
+ allowed.
35
+
36
+ Example usage:
37
+
38
+ bundle exec rake apple_shove:start connection_limit=100 log_dir=log
39
+
40
+ ## TCP Keep-Alives
41
+
42
+ Apple Shove has the ability to maintain connections to Apple for long durations of time without sending a notification. These connections will generally stay open, however, intermediate NATs and firewalls may expire and close the connection prematurely.
43
+
44
+ To combat this, Apple Shove enables keep-alive on all connections to Apple. Apple Shove is not able to set the interval between keep-alives, however, as this is generally managed by the operating system. If you are aware of a relatively short NAT or firewall timer, you can either manually shorten your OS's keep-alive timer to be shorter than the timer. As this likely breaks the portability of your code, you can alternatively change the `AppleShove::CONFIG[:reconnect_timer]` to a value less than the NAT/firewall timer. This will force Apple Shove to re-establish the SSL connection after enough idle time has passed.
45
+
46
+ For reference, we have observed the following keep-alive timeout values:
47
+
48
+ * OS X: 4 minutes, 45 seconds
49
+ * Linux: 2 hours
50
+ * WIndows: 2 hours
51
+
52
+ Apple also seems to send a keep-alive packet if it sees the connection as idle for 10 minutes.
53
+
54
+ ## Gotchas
55
+
56
+ Due to the TCP/IP stack, AppleShove will only know about a broken pipe to APNS after it writes two notifications to the socket. When this occurs, AppleShove will re-transmit the first as well as the second notification. Because time may have elapsed between the first and second notification writes, a non-trivial delay in the delivery of the first notification may occur.
57
+
58
+ ## Contributing
59
+
60
+ 1. Fork it
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+
3
+ require "bundler/gem_tasks"
4
+ require 'apple_shove/tasks'
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'apple_shove/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "apple_shove"
8
+ spec.version = AppleShove::VERSION
9
+ spec.authors = ["Taylor Boyko"]
10
+ spec.email = ["tboyko@unwiredrevolution.com"]
11
+ spec.description = %q{APN Service Provider. More powerful than a push...}
12
+ spec.summary = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency 'rspec'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_dependency 'redis', '~> 3.0'
25
+ spec.add_dependency 'daemons', '~> 1.1'
26
+ spec.add_dependency 'celluloid', '~> 0.13'
27
+ end
@@ -0,0 +1,52 @@
1
+ require 'openssl'
2
+
3
+ module AppleShove
4
+ module APNS
5
+ class Connection
6
+
7
+ attr_reader :last_used
8
+
9
+ def initialize(opts = {})
10
+ @host = opts[:host]
11
+ @port = opts[:port]
12
+ @certificate = opts[:certificate]
13
+ end
14
+
15
+ # lazy connect the socket
16
+ def socket
17
+ connect unless connected?
18
+ @ssl_sock
19
+ end
20
+
21
+ def disconnect
22
+ @ssl_sock.close if @ssl_sock
23
+ @sock.close if @sock
24
+ end
25
+
26
+ def reconnect
27
+ disconnect
28
+ connect
29
+ end
30
+
31
+ private
32
+
33
+ def connect
34
+ @sock = TCPSocket.new(@host, @port)
35
+ @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
36
+
37
+ context = ::OpenSSL::SSL::SSLContext.new
38
+ context.cert = ::OpenSSL::X509::Certificate.new(@certificate)
39
+ context.key = ::OpenSSL::PKey::RSA.new(@certificate)
40
+ @ssl_sock = ::OpenSSL::SSL::SSLSocket.new(@sock, context)
41
+ @ssl_sock.sync = true
42
+
43
+ @ssl_sock.connect
44
+ end
45
+
46
+ def connected?
47
+ @sock && @ssl_sock
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ module AppleShove
2
+ module APNS
3
+ class FeedbackConnection < Connection
4
+
5
+ def initialize(opts = {})
6
+ host = "feedback.#{opts[:sandbox] ? 'sandbox.' : ''}push.apple.com"
7
+
8
+ super certificate: opts[:certificate],
9
+ host: host,
10
+ port: 2196
11
+ end
12
+
13
+ def device_tokens
14
+ tokens = []
15
+ while response = socket.read(38)
16
+ timestamp, token_length, device_token = response.unpack('N1n1H*')
17
+ tokens << device_token
18
+ end
19
+
20
+ tokens
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ require 'celluloid'
2
+
3
+ module AppleShove
4
+ module APNS
5
+ class NotifyConnection < Connection
6
+ include Celluloid
7
+
8
+ attr_accessor :pending_notifications
9
+ attr_reader :name
10
+
11
+ def initialize(opts = {})
12
+ @name = self.class.generate_name(opts[:certificate], opts[:sandbox])
13
+ @last_message = nil
14
+ @pending_notifications = 0
15
+
16
+ host = "gateway.#{opts[:sandbox] ? 'sandbox.' : ''}push.apple.com"
17
+
18
+ super certificate: opts[:certificate],
19
+ host: host,
20
+ port: 2195
21
+ end
22
+
23
+ def self.generate_name(certificate, sandbox)
24
+ Digest::SHA1.hexdigest("#{certificate}#{sandbox}")
25
+ end
26
+
27
+ exclusive
28
+
29
+ def connect
30
+ super
31
+ @last_used = Time.now
32
+ end
33
+
34
+ def send(notification)
35
+ message = notification.binary_message
36
+
37
+ if @last_used && Time.now - @last_used > CONFIG[:reconnect_timer] * 60
38
+ Logger.info("#{@name}\trefreshing connection")
39
+ reconnect
40
+ end
41
+
42
+ begin
43
+ socket.write message
44
+ rescue Errno::EPIPE
45
+ Logger.warn("#{@name}\tbroken pipe. reconnecting.")
46
+ reconnect
47
+ # EPIPE raises on the second write to a closed pipe. We need to resend
48
+ # the previous notification that didn't make it through.
49
+ socket.write @last_message if @last_message
50
+ retry
51
+ rescue Errno::ETIMEDOUT
52
+ Logger.warn("#{@name}\ttimeout. reconnecting.")
53
+ reconnect
54
+ retry
55
+ end
56
+
57
+ @last_message = message
58
+ @last_used = Time.now
59
+ @pending_notifications -= 1
60
+ Logger.info("#{@name}\tdelivered notification")
61
+ end
62
+
63
+ def shutdown
64
+ while @pending_notifications > 0
65
+ Logger.info("#{@name}\twaiting to shut down. #{@pending_notifications} job(s) remaining.")
66
+ sleep 1
67
+ end
68
+
69
+ self.disconnect
70
+ self.terminate
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,33 @@
1
+ module AppleShove
2
+
3
+ def self.notify(certificate, device_token, payload, sandbox = false)
4
+ notification = Notification.new certificate: certificate,
5
+ device_token: device_token,
6
+ payload: payload,
7
+ sandbox: sandbox
8
+
9
+ queue = NotificationQueue.new(CONFIG[:redis_key])
10
+ queue.add(notification)
11
+
12
+ true
13
+ end
14
+
15
+ def self.feedback_tokens(certificate, sandbox = false)
16
+ conn = APNS::FeedbackConnection.new certificate: certificate,
17
+ sandbox: sandbox
18
+
19
+ conn.device_tokens
20
+ end
21
+
22
+ def self.stats
23
+ redis = ::Redis.new
24
+ queue = NotificationQueue.new(CONFIG[:redis_key], redis)
25
+
26
+ size = queue.size
27
+
28
+ redis.quit
29
+
30
+ "queue size:\t#{size}"
31
+ end
32
+
33
+ end
@@ -0,0 +1,6 @@
1
+ module AppleShove
2
+ CONFIG = {
3
+ :redis_key => 'apple_shove',
4
+ :reconnect_timer => 5 # timeout in minutes to re-establish APNS connection
5
+ }
6
+ end
@@ -0,0 +1,61 @@
1
+ module AppleShove
2
+ class Demultiplexer
3
+
4
+ def initialize(opts = {})
5
+ unless opts[:max_apns_connections]
6
+ raise ArgumentError, 'max_apns_connections must be specified'
7
+ end
8
+
9
+ @max_connections = opts[:max_apns_connections].to_i
10
+
11
+ @connections = {}
12
+ @queue = NotificationQueue.new(CONFIG[:redis_key])
13
+ end
14
+
15
+ def start
16
+
17
+ while true
18
+
19
+ if notification = @queue.get
20
+ conn = get_connection(notification)
21
+ conn.pending_notifications += 1
22
+ conn.async.send(notification)
23
+ else
24
+ sleep 1
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ private
32
+
33
+ def get_connection(notification)
34
+ key = APNS::NotifyConnection.generate_name(notification.certificate, notification.sandbox)
35
+ connection = @connections[key]
36
+
37
+ unless connection
38
+ retire_oldest_connection if @connections.count >= @max_connections
39
+
40
+ connection = APNS::NotifyConnection.new certificate: notification.certificate,
41
+ sandbox: notification.sandbox
42
+ @connections[key] = connection
43
+ Logger.info "#{connection.name}\tcreated connection to APNS (#{@connections.count} total)"
44
+ end
45
+
46
+ connection
47
+ end
48
+
49
+ def retire_oldest_connection
50
+ if oldest = @connections.min_by { |_k, v| v.last_used }
51
+ key, conn = oldest[0], oldest[1]
52
+ conn_name = conn.name
53
+ @connections.delete key
54
+ conn.shutdown
55
+
56
+ Logger.info "#{conn_name}\tdestroyed connection to APNS (#{@connections.count} total)"
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ require 'logger'
2
+ require 'singleton'
3
+
4
+ module AppleShove
5
+ class Logger < ::Logger
6
+ include Singleton
7
+
8
+ class Formatter
9
+ def call(severity, time, progname, msg)
10
+ formatted_severity = sprintf("%-5s",severity.to_s)
11
+ formatted_time = time.strftime("%Y-%m-%d %H:%M:%S")
12
+ "[#{formatted_severity} #{formatted_time}] #{msg.strip}\n"
13
+ end
14
+ end
15
+
16
+ def initialize(output_stream = STDOUT)
17
+ super(output_stream)
18
+ self.formatter = Formatter.new
19
+ self
20
+ end
21
+
22
+ def self.error(msg); instance.error(msg) end
23
+ def self.debug(msg); instance.debug(msg) end
24
+ def self.fatal(msg); instance.fatal(msg) end
25
+ def self.info(msg); instance.info(msg) end
26
+ def self.warn(msg); instance.warn(msg) end
27
+
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ module AppleShove
2
+ class Notification
3
+
4
+ attr_accessor :certificate, :sandbox, :device_token, :payload
5
+
6
+ def initialize(attributes = {})
7
+ attributes.each { |k, v| self.send("#{k}=", v) }
8
+ end
9
+
10
+ def self.parse(json)
11
+ self.new(JSON.parse(json))
12
+ end
13
+
14
+ def to_json(*a)
15
+ hash = {}
16
+ clean_instance_variables.each { |k| hash[k] = self.send(k) }
17
+ hash.to_json(*a)
18
+ end
19
+
20
+ # Apple APNS format
21
+ def binary_message
22
+ payload_json = @payload.to_json
23
+ message = [0, 32, @device_token, payload_json.length, payload_json]
24
+ message.pack('CnH*na*')
25
+ end
26
+
27
+ private
28
+
29
+ def clean_instance_variables
30
+ self.instance_variables.collect { |i| i[1..-1] }
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+ require 'redis'
3
+
4
+ module AppleShove
5
+ class NotificationQueue
6
+
7
+ def initialize(key, redis = Redis.new)
8
+ @redis = redis
9
+ @key = key
10
+ end
11
+
12
+ def add(notification)
13
+ @redis.rpush @key, notification.to_json
14
+ end
15
+
16
+ def get
17
+ element = @redis.lpop @key
18
+ element ? Notification.parse(element) : nil
19
+ end
20
+
21
+ def size
22
+ @redis.llen @key
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ require 'rake'
2
+
3
+ include Rake::DSL
4
+
5
+ namespace :apple_shove do
6
+
7
+ desc 'Display service statistics every second'
8
+ task :stats do
9
+ require 'apple_shove'
10
+
11
+ begin
12
+ puts AppleShove.stats
13
+ sleep 1
14
+ end while true
15
+ end
16
+
17
+ desc 'Start the daemon in the foreground'
18
+ task :run do
19
+ exec "ruby #{path_to_daemon} run#{argument_string}"
20
+ end
21
+
22
+ desc 'Start the daemon'
23
+ task :start do
24
+ exec "ruby #{path_to_daemon} start#{argument_string}"
25
+ end
26
+
27
+ desc 'Stop the daemon'
28
+ task :stop do
29
+ exec "ruby #{path_to_daemon} stop#{argument_string}"
30
+ end
31
+
32
+ desc 'Restart the daemon'
33
+ task :restart do
34
+ exec "ruby #{path_to_daemon} restart#{argument_string}"
35
+ end
36
+
37
+ desc 'Show the status (PID) of the daemon'
38
+ task :status do
39
+ exec "ruby #{path_to_daemon} status"
40
+ end
41
+
42
+ private
43
+
44
+ def path_to_daemon
45
+ File.join(File.dirname(__FILE__), '..', '..', 'script', 'daemon')
46
+ end
47
+
48
+ def argument_string
49
+ watched_args = ['log_dir', 'pid_dir', 'connection_limit']
50
+ arg_str = watched_args.collect { |a| ENV[a] ? "--#{a}=#{ENV[a]}" : nil }.compact.join(' ')
51
+
52
+ arg_str.empty? ? nil : " -- #{arg_str}"
53
+ end
54
+
55
+
56
+ end
@@ -0,0 +1,3 @@
1
+ module AppleShove
2
+ VERSION = "1.0.1"
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'apple_shove/apns/connection'
2
+ Dir[File.join(File.dirname(__FILE__), 'apple_shove', '**', '*.rb')].each {|file| require file }
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib'
2
+
3
+ require 'apple_shove/tasks'
data/script/daemon ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'daemons'
4
+ require 'apple_shove'
5
+
6
+ # process command line arguments
7
+
8
+ args = {}
9
+ ARGV.each do |arg|
10
+ if m = arg.match(/^--(?<key>[^=]+)=(?<val>.+)$/)
11
+ key = m[:key].gsub('-','_').to_sym
12
+ args[key] = m[:val]
13
+ end
14
+ end
15
+
16
+ options = {
17
+ stop_proc: Proc.new { puts "Stopping daemon" },
18
+ dir_mode: :script
19
+ }
20
+
21
+ options[:dir] = args[:pid_dir] if args[:pid_dir]
22
+
23
+ if args[:log_dir]
24
+ options[:log_output] = true
25
+ options[:log_dir] = args[:log_dir]
26
+ end
27
+
28
+ Daemons.run_proc('apple_shove', options) do
29
+ # max of 15 connections recommended by Apple: http://bit.ly/YNHTfE
30
+ # note: this may be per-certificate, in which case we can crank this number
31
+ # up much higher.
32
+ conn_limit = args[:connection_limit] || 100
33
+
34
+ puts "Starting daemon with a APNS connection limit of #{conn_limit}"
35
+
36
+ dmp = AppleShove::Demultiplexer.new max_apns_connections: conn_limit
37
+ dmp.start
38
+ end
@@ -0,0 +1,14 @@
1
+ require 'apple_shove'
2
+
3
+ describe AppleShove::Demultiplexer do
4
+
5
+ it 'initializes without error' do
6
+ dmp = AppleShove::Demultiplexer.new max_apns_connections: 10
7
+ dmp.should be_an_instance_of(AppleShove::Demultiplexer)
8
+ end
9
+
10
+ it 'raises an error when a connection limit is omitted' do
11
+ expect { AppleShove::Demultiplexer.new }.to raise_error
12
+ end
13
+
14
+ end
@@ -0,0 +1,21 @@
1
+ module NotificationHelper
2
+
3
+ def generate_notification
4
+ certificate = "DummyCertificate"
5
+ sandbox = false
6
+ device_token = hex(64)
7
+ payload = { mdm: "#{hex(8)}-#{hex(4)}-#{hex(4)}-#{hex(4)}-#{hex(12)}".downcase }
8
+
9
+ AppleShove::Notification.new certificate: certificate,
10
+ sandbox: sandbox,
11
+ device_token: device_token,
12
+ payload: payload
13
+ end
14
+
15
+ private
16
+
17
+ def hex(length)
18
+ length.times.map { ((0..9).to_a + ('a'..'f').to_a)[rand(16)] }.join
19
+ end
20
+
21
+ end
@@ -0,0 +1,34 @@
1
+ require 'apple_shove'
2
+ require './spec/notification_helper'
3
+
4
+ describe AppleShove::NotificationQueue do
5
+ include NotificationHelper
6
+
7
+ before do
8
+ @q = AppleShove::NotificationQueue.new('dummy_key')
9
+ end
10
+
11
+ it 'should initialize without error' do
12
+ @q.should_not eql(nil)
13
+ end
14
+
15
+ it 'should add notifications to the queue' do
16
+ n = generate_notification
17
+ @q.add(n)
18
+ end
19
+
20
+ it 'should count notification on the queue when they are there' do
21
+ @q.size.should_not eql(0)
22
+ end
23
+
24
+ it 'should get notifications from the queue' do
25
+ while n = @q.get
26
+ n.should be_an_instance_of(AppleShove::Notification)
27
+ end
28
+ end
29
+
30
+ it 'should count 0 notifications when the queue is empty' do
31
+ @q.size.should eql(0)
32
+ end
33
+
34
+ end
@@ -0,0 +1,28 @@
1
+ require 'apple_shove'
2
+ require './spec/notification_helper'
3
+
4
+ describe AppleShove::Notification do
5
+ include NotificationHelper
6
+
7
+ before do
8
+ @n = generate_notification
9
+ end
10
+
11
+ it "converts to and from json" do
12
+ json = @n.to_json
13
+
14
+ json.should be_an_instance_of(String)
15
+
16
+ n2 = AppleShove::Notification.parse(json)
17
+
18
+ @n.to_json.should == n2.to_json
19
+ end
20
+
21
+ it "creates a binary message for apns" do
22
+ m = @n.binary_message
23
+
24
+ m.should be_an_instance_of(String)
25
+ m.length.should > 0
26
+ end
27
+
28
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apple_shove
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Taylor Boyko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: redis
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '3.0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '3.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: daemons
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.1'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.1'
94
+ - !ruby/object:Gem::Dependency
95
+ name: celluloid
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '0.13'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '0.13'
110
+ description: APN Service Provider. More powerful than a push...
111
+ email:
112
+ - tboyko@unwiredrevolution.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - .gitignore
118
+ - Gemfile
119
+ - LICENSE.txt
120
+ - README.md
121
+ - Rakefile
122
+ - apple_shove.gemspec
123
+ - lib/apple_shove.rb
124
+ - lib/apple_shove/apns/connection.rb
125
+ - lib/apple_shove/apns/feedback_connection.rb
126
+ - lib/apple_shove/apns/notify_connection.rb
127
+ - lib/apple_shove/apple_shove.rb
128
+ - lib/apple_shove/config.rb
129
+ - lib/apple_shove/demultiplexer.rb
130
+ - lib/apple_shove/logger.rb
131
+ - lib/apple_shove/notification.rb
132
+ - lib/apple_shove/notification_queue.rb
133
+ - lib/apple_shove/tasks.rb
134
+ - lib/apple_shove/version.rb
135
+ - lib/tasks/apple_shove.rake
136
+ - script/daemon
137
+ - spec/demultiplexer_spec.rb
138
+ - spec/notification_helper.rb
139
+ - spec/notification_queue_spec.rb
140
+ - spec/notification_spec.rb
141
+ homepage: ''
142
+ licenses:
143
+ - MIT
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ segments:
155
+ - 0
156
+ hash: -1646100289206665010
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ none: false
159
+ requirements:
160
+ - - ! '>='
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ segments:
164
+ - 0
165
+ hash: -1646100289206665010
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 1.8.24
169
+ signing_key:
170
+ specification_version: 3
171
+ summary: ''
172
+ test_files:
173
+ - spec/demultiplexer_spec.rb
174
+ - spec/notification_helper.rb
175
+ - spec/notification_queue_spec.rb
176
+ - spec/notification_spec.rb