lowdown 0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 596c18a9090845d35a4395a928c71c3b0c493f63
4
+ data.tar.gz: 2003c1226c6d4f1831452f1b83c5a2caeb0d38cc
5
+ SHA512:
6
+ metadata.gz: 11c94276f939853a735caae9b90a50ca288c6a9d77a070d5b5be8d5c7f401afefc0ce8a94ec249266bf10ad6d8cf92065fd91d3e8dfbd3e7295d3ffec741755a
7
+ data.tar.gz: 0652004863baf52a637f207243e1b2b891da389e6995565717378015efaf9eb12e40e83b7a73ff7e4a715c057e0e1599b4580f3e77b5e6d3b941434a27707180
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.4
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ #gem 'http-2', :git => 'https://github.com/alloy/http-2.git', :branch => "apns"
4
+
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Artsy, Eloy Durán <eloy.de.enige@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Lowdown
2
+
3
+ Lowdown is a Ruby client for the HTTP/2 version of the Apple Push Notification Service.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'lowdown'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install lowdown
20
+
21
+ ## Usage
22
+
23
+ You can use the `lowdown` bin that comes with this gem or in code at it’s simplest:
24
+
25
+ ```ruby
26
+ notification = Lowdown::Notification.new(:token => "device-token", :payload => { :alert => "Hello World!" })
27
+
28
+ Lowdown::Client.production(true, File.read("path/to/certificate.pem")).connect do |client|
29
+ client.send_notification(notification) do |response|
30
+ if response.success?
31
+ puts "Notification sent"
32
+ else
33
+ puts "Notification failed: #{response}"
34
+ end
35
+ end
36
+ end
37
+ ```
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alloy/lowdown.
42
+
43
+ ## License
44
+
45
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
46
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/lowdown ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "lowdown"
4
+ include Lowdown
5
+
6
+ require "json"
7
+ require "optparse"
8
+
9
+ options = { :payload => {}, :custom_data => {} }
10
+
11
+ OPTION_PARSER = OptionParser.new do |opts|
12
+ opts.banner = "Usage: lowdown [options] <tokens …>"
13
+
14
+ opts.on("-v", "--version", "Print version") do |v|
15
+ puts VERSION
16
+ exit
17
+ end
18
+
19
+ opts.on("-m", "--alert ALERT", "Body of the alert to send in the push notification") do |alert|
20
+ options[:alert] = alert
21
+ end
22
+
23
+ opts.on("-b", "--badge NUMBER", "Badge number to set with the push notification") do |badge|
24
+ options[:badge] = badge.to_i
25
+ end
26
+
27
+ opts.on("-s", "--sound SOUND", "Sound to play with the notification") do |sound|
28
+ options[:sound] = sound
29
+ end
30
+
31
+ opts.on("-x", "--[no-]content-available", "Indicates to the app that new content is available") do |available|
32
+ options[:content_available] = available
33
+ end
34
+
35
+ opts.on("-d", "--data KEY=VALUE", "Passes custom data to payload") do |custom_data|
36
+ key, value = custom_data.split("=", 2)
37
+ options[:custom_data][key] = value
38
+ end
39
+
40
+ opts.on("-P", "--payload PAYLOAD", "JSON payload for notifications, merged with --alert, --badge, --sound, and --data") do |payload|
41
+ options[:payload] = JSON.parse(payload)
42
+ end
43
+
44
+ opts.on("-t", "--topic TOPIC", "The topic for the notifications") do |topic|
45
+ options[:topic] = topic
46
+ end
47
+
48
+ opts.on("-e", "--environment ENV", "Environment to send push notification (production or development), defaults to certificate purpose or development") do |env|
49
+ options[:env] = env
50
+ end
51
+
52
+ opts.on("-c", "--certificate CERTIFICATE", "Path to certificate (.pem) file") do |file|
53
+ options[:certificate_file] = file
54
+ end
55
+
56
+ opts.on("-p", "--passphrase PASSPHRASE", "Certificate passphrase") do |passphrase|
57
+ options[:certificate_passphrase] = passphrase
58
+ end
59
+ end
60
+
61
+ OPTION_PARSER.parse!
62
+ tokens = ARGV
63
+
64
+ def help!(message)
65
+ puts message
66
+ puts
67
+ puts OPTION_PARSER
68
+ exit 1
69
+ end
70
+
71
+ certificate = nil
72
+ file, passphrase = options.values_at(:certificate_file, :certificate_passphrase)
73
+ unless file && File.exist?(file) && certificate = (Certificate.from_pem_data(File.read(file), passphrase) rescue nil)
74
+ help! "A valid certificate path is required."
75
+ end
76
+
77
+ production = false
78
+ if options[:env]
79
+ unless %w{ production development }.include?(options[:env])
80
+ help! "Invalid environment specified."
81
+ end
82
+ production = options[:env] == "production"
83
+ else
84
+ if certificate.development?
85
+ production = false
86
+ elsif certificate.production?
87
+ production = true
88
+ end
89
+ end
90
+
91
+ begin
92
+ client = Client.production(production, certificate)
93
+ rescue ArgumentError => e
94
+ help! e.message
95
+ end
96
+
97
+ payload = options[:payload]
98
+ payload.merge!(options[:custom_data])
99
+ payload["alert"] = options[:alert] if options[:alert]
100
+ payload["badge"] = options[:badge] if options[:badge]
101
+ payload["sound"] = options[:sound] if options[:sound]
102
+ payload["content-available"] = options[:content_available] ? 1 : 0 if options.has_key?(:content_available)
103
+ if payload.empty?
104
+ help! "No payload data specified."
105
+ end
106
+
107
+ if tokens.empty?
108
+ help! "No device tokens specified."
109
+ end
110
+
111
+ notifications = tokens.map.with_index do |token, index|
112
+ Notification.new(:token => token, :id => index+1, :payload => payload, :topic => options[:topic])
113
+ end
114
+
115
+ client.connect do
116
+ notifications.each do |notification|
117
+ client.send_notification(notification) do |response|
118
+ puts "[#{notification.token} ##{notification.id}] #{response}"
119
+ end
120
+ end
121
+ end
data/lib/lowdown.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "lowdown/client"
2
+ require "lowdown/version"
3
+
4
+ module Lowdown
5
+ end
@@ -0,0 +1,72 @@
1
+ require "openssl"
2
+
3
+ module Lowdown
4
+ def self.Certificate(certificate_or_data)
5
+ if certificate_or_data.is_a?(Certificate)
6
+ certificate_or_data
7
+ else
8
+ Certificate.from_pem_data(certificate_or_data)
9
+ end
10
+ end
11
+
12
+ class Certificate
13
+ # http://images.apple.com/certificateauthority/pdf/Apple_WWDR_CPS_v1.13.pdf
14
+ DEVELOPMENT_ENV_EXTENSION = "1.2.840.113635.100.6.3.1".freeze
15
+ PRODUCTION_ENV_EXTENSION = "1.2.840.113635.100.6.3.2".freeze
16
+ UNIVERSAL_CERTIFICATE_EXTENSION = "1.2.840.113635.100.6.3.6".freeze
17
+
18
+ def self.from_pem_data(data, passphrase = nil)
19
+ key = OpenSSL::PKey::RSA.new(data, passphrase)
20
+ certificate = OpenSSL::X509::Certificate.new(data)
21
+ new(key, certificate)
22
+ end
23
+
24
+ attr_reader :key, :certificate
25
+
26
+ def initialize(key, certificate)
27
+ @key, @certificate = key, certificate
28
+ end
29
+
30
+ def to_pem
31
+ "#{@key.to_pem}\n#{@certificate.to_pem}"
32
+ end
33
+
34
+ def ssl_context
35
+ @ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |context|
36
+ context.key = @key
37
+ context.cert = @certificate
38
+ end
39
+ end
40
+
41
+ def universal?
42
+ !extension(UNIVERSAL_CERTIFICATE_EXTENSION).nil?
43
+ end
44
+
45
+ def development?
46
+ !extension(DEVELOPMENT_ENV_EXTENSION).nil?
47
+ end
48
+
49
+ def production?
50
+ !extension(PRODUCTION_ENV_EXTENSION).nil?
51
+ end
52
+
53
+ def topics
54
+ if universal?
55
+ components = extension(UNIVERSAL_CERTIFICATE_EXTENSION).value.split(/0?\.{2,}/)
56
+ components.select.with_index { |_, index| index.odd? }
57
+ else
58
+ [app_bundle_id]
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def extension(oid)
65
+ @certificate.extensions.find { |ext| ext.oid == oid }
66
+ end
67
+
68
+ def app_bundle_id
69
+ @certificate.subject.to_a.find { |key, *_| key == 'UID' }[1]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,73 @@
1
+ require "lowdown/certificate"
2
+ require "lowdown/connection"
3
+ require "lowdown/notification"
4
+
5
+ require "uri"
6
+ require "json"
7
+
8
+ module Lowdown
9
+ class Client
10
+ DEVELOPMENT_URI = URI.parse("https://api.development.push.apple.com:443")
11
+ PRODUCTION_URI = URI.parse("https://api.push.apple.com:443")
12
+
13
+ def self.production(production, certificate_or_data)
14
+ certificate = Lowdown.Certificate(certificate_or_data)
15
+ if production
16
+ unless certificate.production?
17
+ raise ArgumentError, "The specified certificate is not usable with the production environment."
18
+ end
19
+ else
20
+ unless certificate.development?
21
+ raise ArgumentError, "The specified certificate is not usable with the development environment."
22
+ end
23
+ end
24
+ client(production ? PRODUCTION_URI : DEVELOPMENT_URI, certificate)
25
+ end
26
+
27
+ def self.client(uri, certificate_or_data)
28
+ certificate = Lowdown.Certificate(certificate_or_data)
29
+ default_topic = certificate.topics.first if certificate.universal?
30
+ new(Connection.new(uri, certificate.ssl_context), default_topic)
31
+ end
32
+
33
+ attr_reader :connection, :default_topic
34
+
35
+ def initialize(connection, default_topic = nil)
36
+ @connection, @default_topic = connection, default_topic
37
+ end
38
+
39
+ def connect
40
+ @connection.open
41
+ if block_given?
42
+ begin
43
+ yield self
44
+ ensure
45
+ close
46
+ end
47
+ end
48
+ end
49
+
50
+ def flush
51
+ @connection.flush
52
+ end
53
+
54
+ def close
55
+ @connection.close
56
+ end
57
+
58
+ def send_notification(notification, &callback)
59
+ raise ArgumentError, "Invalid notification: #{notification.inspect}" unless notification.valid?
60
+
61
+ topic = notification.topic || @default_topic
62
+ headers = {}
63
+ headers["apns-expiration"] = (notification.expiration || 0).to_i
64
+ headers["apns-id"] = notification.formatted_id if notification.id
65
+ headers["apns-priority"] = notification.priority if notification.priority
66
+ headers["apns-topic"] = topic if topic
67
+
68
+ body = { :aps => notification.payload }.to_json
69
+
70
+ @connection.post("/3/device/#{notification.token}", headers, body, &callback)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,164 @@
1
+ require "lowdown/threading"
2
+ require "lowdown/response"
3
+
4
+ require "http/2"
5
+ require "openssl"
6
+ require "uri"
7
+ require "socket"
8
+
9
+ # Monkey-patch http-2 gem until this PR is merged: https://github.com/igrigorik/http-2/pull/44
10
+ if HTTP2::VERSION == "0.8.0"
11
+ class HTTP2::Client
12
+ def connection_management(frame)
13
+ if @state == :waiting_connection_preface
14
+ send_connection_preface
15
+ connection_settings(frame)
16
+ else
17
+ super(frame)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module Lowdown
24
+ class Connection
25
+ attr_reader :uri, :ssl_context
26
+
27
+ def initialize(uri, ssl_context)
28
+ @uri, @ssl_context = URI(uri), ssl_context
29
+ end
30
+
31
+ def open
32
+ @socket = TCPSocket.new(@uri.host, @uri.port)
33
+
34
+ @ssl = OpenSSL::SSL::SSLSocket.new(@socket, @ssl_context)
35
+ @ssl.sync_close = true
36
+ @ssl.hostname = @uri.hostname
37
+ @ssl.connect
38
+
39
+ @http = HTTP2::Client.new
40
+ @http.on(:frame) do |bytes|
41
+ @ssl.print(bytes)
42
+ @ssl.flush
43
+ end
44
+
45
+ @main_queue = Threading::DispatchQueue.new
46
+ @work_queue = Threading::DispatchQueue.new
47
+ @requests = Threading::Counter.new
48
+ @exceptions = Queue.new
49
+ @worker_thread = start_worker_thread!
50
+ end
51
+
52
+ def open?
53
+ !@ssl.nil? && !@ssl.closed?
54
+ end
55
+
56
+ # Terminates the worker thread and closes the socket. Finally it peforms one more check for pending jobs dispatched
57
+ # onto the main thread.
58
+ #
59
+ def close
60
+ flush
61
+
62
+ @worker_thread[:should_exit] = true
63
+ @worker_thread.join
64
+
65
+ @ssl.close
66
+
67
+ sleep 0.1
68
+ @main_queue.drain!
69
+
70
+ @socket = @ssl = @http = @main_queue = @work_queue = @requests = @exceptions = @worker_thread = nil
71
+ end
72
+
73
+ def flush
74
+ until @work_queue.empty? && @requests.zero?
75
+ @main_queue.drain!
76
+ sleep 0.1
77
+ end
78
+ end
79
+
80
+ def post(path, headers, body, &callback)
81
+ request('POST', path, headers, body, &callback)
82
+ end
83
+
84
+ private
85
+
86
+ def request(method, path, custom_headers, body, &callback)
87
+ @requests.increment!
88
+ @work_queue.dispatch do
89
+ headers = { ":method" => method.to_s, ":path" => path.to_s, "content-length" => body.bytesize.to_s }
90
+ custom_headers.each { |k, v| headers[k] = v.to_s }
91
+
92
+ stream = @http.new_stream
93
+ response = Response.new
94
+
95
+ stream.on(:headers) do |response_headers|
96
+ response.headers = Hash[*response_headers.flatten]
97
+ end
98
+
99
+ stream.on(:data) do |response_data|
100
+ response.raw_body ||= ''
101
+ response.raw_body << response_data
102
+ end
103
+
104
+ stream.on(:close) do
105
+ @main_queue.dispatch do
106
+ callback.call(response)
107
+ @requests.decrement!
108
+ end
109
+ end
110
+
111
+ stream.headers(headers, end_stream: false)
112
+ stream.data(body, end_stream: true)
113
+ end
114
+
115
+ # The caller might be posting many notifications, so use this time to also dispatch work onto the main thread.
116
+ @main_queue.drain!
117
+ end
118
+
119
+ def start_worker_thread!
120
+ Thread.new do
121
+ until Thread.current[:should_exit] || @ssl.closed?
122
+ # Run any dispatched jobs that add new requests.
123
+ #
124
+ # Re-raising a worker exception aids the development process. In production there’s no reason why this should
125
+ # raise at all.
126
+ if exception = @work_queue.drain!
127
+ exception_occurred_in_worker(exception)
128
+ end
129
+
130
+ # Try to read data from the SSL socket without blocking. If it would block, catch the exception and restart
131
+ # the loop.
132
+ begin
133
+ data = @ssl.read_nonblock(1024)
134
+ rescue IO::WaitReadable
135
+ data = nil
136
+ rescue EOFError => exception
137
+ exception_occurred_in_worker(exception)
138
+ Thread.current[:should_exit] = true
139
+ data = nil
140
+ end
141
+
142
+ # Process incoming HTTP data. If any processing exception occurs, fail the whole process.
143
+ if data
144
+ begin
145
+ @http << data
146
+ rescue Exception => exception
147
+ @ssl.close
148
+ exception_occurred_in_worker(exception)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ # Raise the exception on the main thread and reset the number of in-flight requests so that a potential blocked
156
+ # caller of `Connection#flush` will continue.
157
+ #
158
+ def exception_occurred_in_worker(exception)
159
+ @exceptions << exception
160
+ @main_queue.dispatch { raise @exceptions.pop }
161
+ @requests.value = @http.active_stream_count
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,22 @@
1
+ module Lowdown
2
+ # For payload documentation see: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW1
3
+ #
4
+ class Notification
5
+ attr_accessor :token, :id, :expiration, :priority, :topic, :payload
6
+
7
+ def initialize(params)
8
+ params.each { |key, value| send("#{key}=", value) }
9
+ end
10
+
11
+ def valid?
12
+ !!(@token && @payload)
13
+ end
14
+
15
+ def formatted_id
16
+ if @id
17
+ padded = @id.to_s.rjust(32, "0")
18
+ [padded[0,8], padded[8,4], padded[12,4], padded[16,4], padded[20,12]].join("-")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,65 @@
1
+ module Lowdown
2
+ class Response < Struct.new(:headers, :raw_body)
3
+ # https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH101-SW3
4
+ STATUS_CODES = {
5
+ 200 => "Success",
6
+ 400 => "Bad request",
7
+ 403 => "There was an error with the certificate",
8
+ 405 => "The request used a bad :method value. Only POST requests are supported",
9
+ 410 => "The device token is no longer active for the topic",
10
+ 413 => "The notification payload was too large",
11
+ 429 => "The server received too many requests for the same device token",
12
+ 500 => "Internal server error",
13
+ 503 => "The server is shutting down and unavailable"
14
+ }
15
+
16
+ def id
17
+ headers["apns-id"]
18
+ end
19
+
20
+ def unformatted_id(length = nil)
21
+ id = self.id.tr('-', '')
22
+ length ? id[32-length,length] : id.gsub(/\A0*/, '')
23
+ end
24
+
25
+ def status
26
+ headers[":status"].to_i
27
+ end
28
+
29
+ def message
30
+ STATUS_CODES[status]
31
+ end
32
+
33
+ def success?
34
+ status == 200
35
+ end
36
+
37
+ def body
38
+ JSON.parse(raw_body) if raw_body
39
+ end
40
+
41
+ def failure_reason
42
+ body["reason"] unless success?
43
+ end
44
+
45
+ def invalid_token?
46
+ status == 410
47
+ end
48
+
49
+ # Only available when using an invalid token.
50
+ def validity_last_checked_at
51
+ Time.at(body["timestamp"].to_i) if invalid_token?
52
+ end
53
+
54
+ def to_s
55
+ s = "#{status} (#{message})"
56
+ s << ": #{failure_reason}" unless success?
57
+ s << " last checked at #{validity_last_checked_at}" if invalid_token?
58
+ s
59
+ end
60
+
61
+ def inspect
62
+ "#<Lowdown::Connection::Response #{to_s}>"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,63 @@
1
+ require "thread"
2
+
3
+ module Lowdown
4
+ module Threading
5
+ class DispatchQueue
6
+ def initialize
7
+ @queue = Queue.new
8
+ end
9
+
10
+ def dispatch(&block)
11
+ @queue << block
12
+ end
13
+
14
+ def empty?
15
+ @queue.empty?
16
+ end
17
+
18
+ # Performs the number of dispatched blocks that were on the queue at the moment of calling #drain!. Unlike
19
+ # performing blocks _until the queue is empty_, this ensures that it doesn’t block the calling thread too long if
20
+ # another thread is dispatching more work at the same time.
21
+ #
22
+ # By default this will let any exceptions bubble up on the main thread or catch and return them on other threads.
23
+ #
24
+ def drain!(rescue_exceptions = (Thread.current != Thread.main))
25
+ @queue.size.times { @queue.pop.call }
26
+ nil
27
+ rescue Exception => exception
28
+ raise unless rescue_exceptions
29
+ exception
30
+ end
31
+ end
32
+
33
+ class Counter
34
+ def initialize(value = 0)
35
+ @value = value
36
+ @mutex = Mutex.new
37
+ end
38
+
39
+ def value
40
+ value = nil
41
+ @mutex.synchronize { value = @value }
42
+ value
43
+ end
44
+
45
+ def zero?
46
+ value.zero?
47
+ end
48
+
49
+ def value=(value)
50
+ @mutex.synchronize { @value = value }
51
+ value
52
+ end
53
+
54
+ def increment!
55
+ @mutex.synchronize { @value += 1 }
56
+ end
57
+
58
+ def decrement!
59
+ @mutex.synchronize { @value -= 1 }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Lowdown
2
+ VERSION = "0.0.1"
3
+ end
data/lowdown.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'lowdown/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "lowdown"
8
+ spec.version = Lowdown::VERSION
9
+ spec.authors = ["Eloy Durán"]
10
+ spec.email = ["eloy.de.enige@gmail.com"]
11
+
12
+ spec.summary = "A Ruby client for the HTTP/2 version of the Apple Push Notification Service."
13
+ spec.homepage = "https://github.com/alloy/lowdown"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| File.dirname(f) == "test" }
17
+ spec.bindir = "bin"
18
+ spec.executables = "lowdown"
19
+ spec.require_paths = ["lib"]
20
+
21
+ # This is currently set to >= 2.0.0 in the http-2 gemspec, which is incorrect, as it uses required keyword arguments.
22
+ spec.required_ruby_version = '>= 2.1.0'
23
+
24
+ spec.add_runtime_dependency "http-2", ">= 0.8"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.11"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "minitest", "~> 5.0"
29
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lowdown
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Eloy Durán
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: http-2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ description:
70
+ email:
71
+ - eloy.de.enige@gmail.com
72
+ executables:
73
+ - lowdown
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".ruby-version"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - bin/lowdown
85
+ - lib/lowdown.rb
86
+ - lib/lowdown/certificate.rb
87
+ - lib/lowdown/client.rb
88
+ - lib/lowdown/connection.rb
89
+ - lib/lowdown/notification.rb
90
+ - lib/lowdown/response.rb
91
+ - lib/lowdown/threading.rb
92
+ - lib/lowdown/version.rb
93
+ - lowdown.gemspec
94
+ homepage: https://github.com/alloy/lowdown
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 2.1.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.4.5.1
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: A Ruby client for the HTTP/2 version of the Apple Push Notification Service.
118
+ test_files: []