apple_shove 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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