lead_zeppelin 0.1.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,18 @@
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
+ test.rb
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Geoloqi
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,71 @@
1
+ # LeadZeppelin
2
+
3
+ Fast, threaded client for the Apple Push Notification Service.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'lead_zeppelin'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install lead_zeppelin
18
+
19
+ ## Usage
20
+
21
+ Require the gem, set threads to throw exceptions (optional, but recommended for now):
22
+
23
+ require 'lead_zeppelin'
24
+ Thread.abort_on_exception = true
25
+
26
+ Instantiate a new client and configure it by adding the block of code to handle error responses, and . Provide client.on\_error _before adding applications_.
27
+
28
+ client = LeadZeppelin::APNS::Client.new do |c|
29
+ c.on_error do |error_response|
30
+ puts "Apple sent back an error response: #{error_response.inspect}"
31
+ end
32
+
33
+ # You can provide .p12 files too! p12: File.read('./yourapp.p12')
34
+ c.add_application :your_app_identifier, pem: File.read('./yourapp.pem')
35
+ end
36
+
37
+ # Add a poller to read messages via a method of your choosing:
38
+
39
+ # Poll every second, join parent (main) thread so it doesn't close
40
+
41
+ client.poll(1, join_parent_thread: true) do |c|
42
+ c.message :demoapp, 'f80d44bc73b4a856d9bcd63c2285e5190f8a7dcd8af34cfdf1f4a23cfd66423d', "testing!"
43
+ end
44
+
45
+ # Logging
46
+
47
+ LeadZeppelin#logger takes a Logger class:
48
+
49
+ require 'logger'
50
+ LeadZeppelin.logger = Logger.new(STDERR)
51
+
52
+ To watch the thread flow, pass an IO to LeadZeppelin#thread_logger (but not a Logger):
53
+
54
+ LeadZeppelin.thread_logger = STDERR
55
+
56
+ ## Contributing
57
+
58
+ 1. Fork it
59
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
60
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
61
+ 4. Push to the branch (`git push origin my-new-feature`)
62
+ 5. Create new Pull Request
63
+
64
+ ## TODO
65
+
66
+ * *TESTING*
67
+ * Way to handle errors asynchronously using a pool
68
+ * Performance and concurrency speedups
69
+ * Edge cases
70
+ * Documentation (code level and regular)
71
+ * Length checking for payload, possibly an auto truncating feature
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/lead_zeppelin/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Kyle Drake"]
6
+ gem.email = ["kyledrake@gmail.com"]
7
+ gem.description = %q{Thread-safe, multi-application APNS client}
8
+ gem.summary = %q{Thread-safe, multi-application APNS client that makes it easier to develop notification software for the APNS service.}
9
+ gem.homepage = "https://github.com/geoloqi/lead_zeppelin"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "lead_zeppelin"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = LeadZeppelin::VERSION
17
+ gem.add_dependency 'multi_json'
18
+ gem.add_dependency 'json'
19
+ gem.add_dependency 'connection_pool'
20
+ end
@@ -0,0 +1,9 @@
1
+ require_relative './lead_zeppelin/apns'
2
+ require_relative './lead_zeppelin/version'
3
+
4
+ module LeadZeppelin
5
+ class << self
6
+ attr_accessor :logger
7
+ attr_accessor :thread_logger
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'openssl'
4
+ require 'multi_json'
5
+ require 'connection_pool'
6
+ require 'timeout'
7
+ require 'securerandom'
8
+ require_relative './apns/application'
9
+ require_relative './apns/client'
10
+ require_relative './apns/gateway'
11
+ require_relative './apns/logger'
12
+ require_relative './apns/notification'
13
+ require_relative './apns/error'
@@ -0,0 +1,45 @@
1
+ module LeadZeppelin
2
+ module APNS
3
+ class Application
4
+ CONNECTION_POOL_SIZE = 5
5
+ CONNECTION_POOL_TIMEOUT = 5
6
+
7
+ def initialize(name, opts={})
8
+ @name = name
9
+ @opts = opts
10
+
11
+ @ssl_context = OpenSSL::SSL::SSLContext.new
12
+
13
+ if opts[:p12]
14
+ pem = OpenSSL::PKCS12.new opts[:p12], opts[:p12_pass]
15
+ @ssl_context.cert = pem.certificate
16
+ @ssl_context.key = pem.key
17
+ elsif opts[:pem]
18
+ @ssl_context.cert = OpenSSL::X509::Certificate.new opts[:pem]
19
+ @ssl_context.key = OpenSSL::PKey::RSA.new opts[:pem], opts[:pem_pass]
20
+ else
21
+ raise ArgumentError, 'opts[:p12] or opts[:pem] required'
22
+ end
23
+ end
24
+
25
+ def connect
26
+ cp_args = {size: (@opts[:connection_pool_size] || CONNECTION_POOL_SIZE),
27
+ timeout: (@opts[:connection_pool_timeout] || CONNECTION_POOL_TIMEOUT)}
28
+
29
+ @gateway_connection_pool = ConnectionPool.new(cp_args) do
30
+ Gateway.new @ssl_context, (@opts[:gateway_opts] || {}).merge(error_block: @opts[:error_block])
31
+ end
32
+ end
33
+
34
+ def message(device_id, message, opts={})
35
+ connect if @gateway_connection_pool.nil?
36
+
37
+ @gateway_connection_pool.with_connection do |gateway|
38
+ gateway.write Notification.new(device_id, message, opts)
39
+ end
40
+
41
+ true
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,75 @@
1
+ module LeadZeppelin
2
+ module APNS
3
+ class Client
4
+ CLIENT_THREADS = 10
5
+ DEFAULT_POLL_FREQUENCY = 1
6
+
7
+ def initialize(opts={}, &configure)
8
+ Logger.info "instantiating client with options: #{opts.inspect}"
9
+ Logger.thread 'c'
10
+ @semaphore = Mutex.new
11
+ @opts = opts
12
+
13
+ self.instance_eval &configure
14
+
15
+ # FIXME
16
+ @thread_count = Queue.new
17
+ (opts[:client_threads] || CLIENT_THREADS).times {|t| @thread_count << t}
18
+ end
19
+
20
+ attr_accessor :applications
21
+
22
+ def on_error(&block)
23
+ @error_block = block
24
+ end
25
+
26
+ def poll(frequency=DEFAULT_POLL_FREQUENCY, opts={}, &block)
27
+ Logger.info 'creating polling thread'
28
+ Logger.thread 'p'
29
+ @polling_thread = Thread.new {
30
+ loop do
31
+ self.instance_eval &block
32
+ sleep frequency
33
+ end
34
+
35
+ Logger.thread 'polling thread running'
36
+ }
37
+
38
+ @polling_thread.join if opts[:join_parent_thread] == true
39
+ end
40
+
41
+ def hold_open_poll
42
+ Logger.info 'attaching current thread to polling thread'
43
+ @polling_thread.join
44
+ end
45
+
46
+ def add_application(name, opts={})
47
+ Logger.info "adding application \"#{name}\""
48
+ Logger.thread 'a'
49
+ @semaphore.synchronize do
50
+ @applications ||= {}
51
+ @applications[name] = Application.new name, opts.merge(error_block: @error_block)
52
+ end
53
+ end
54
+
55
+ def remove_application(name)
56
+ Logger.info "removing application \"#{name}\""
57
+ Logger.thread 'r'
58
+ @semaphore.synchronize do
59
+ deleted = @applications.delete name
60
+ Logger.warn "removing application \"#{name}\" failed! Name may be invalid." if deleted.nil?
61
+ end
62
+ end
63
+
64
+ def message(app_name, device_id, message, opts={})
65
+ Logger.debug "message: \"#{app_name}\", \"#{device_id}\", \"#{message}\""
66
+ Logger.thread 'm'
67
+
68
+ # FIXME
69
+ t = @thread_count
70
+ @applications[app_name].message device_id, message, opts
71
+ @thread_count << t
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,26 @@
1
+ module LeadZeppelin
2
+ module APNS
3
+ class ErrorResponse
4
+ CODES = {
5
+ 0 => 'No errors encountered',
6
+ 1 => 'Processing error',
7
+ 2 => 'Missing device token',
8
+ 3 => 'Missing topic',
9
+ 4 => 'Missing payload',
10
+ 5 => 'Invalid token size',
11
+ 6 => 'Invalid topic size',
12
+ 7 => 'Invalid payload size',
13
+ 8 => 'Invalid token',
14
+ 255 => 'None (unknown)'
15
+ }
16
+
17
+ attr_reader :code, :identifier, :message, :notification
18
+
19
+ def initialize(packet, notification=nil)
20
+ command, @code, @identifier = packet.unpack 'ccA4'
21
+ @message = CODES[@code]
22
+ @notification = notification
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,108 @@
1
+ module LeadZeppelin
2
+ module APNS
3
+ class Gateway
4
+ HOST = 'gateway.push.apple.com'
5
+ PORT = 2195
6
+ DEFAULT_TIMEOUT = 10
7
+ DEFAULT_SELECT_WAIT = 0.5
8
+
9
+ def initialize(ssl_context, opts={})
10
+ Logger.thread 'g'
11
+ @semaphore = Mutex.new
12
+ @ssl_context = ssl_context
13
+ @opts = opts
14
+
15
+ connect
16
+ end
17
+
18
+ def connect
19
+ Logger.thread 's'
20
+ begin
21
+ timeout(@opts[:timeout] || DEFAULT_TIMEOUT) do
22
+ socket = TCPSocket.new((@opts[:apns_host] || HOST), (@opts[:apns_port] || PORT))
23
+ ssl_socket = OpenSSL::SSL::SSLSocket.new socket, @ssl_context
24
+ ssl_socket.sync_close = true # when ssl_socket is closed, make sure the regular socket closes too.
25
+ ssl_socket.connect
26
+
27
+ @semaphore.synchronize do
28
+ @socket = socket
29
+ @ssl_socket = ssl_socket
30
+ end
31
+ end
32
+
33
+ Logger.debug "gateway connection established"
34
+
35
+ rescue Errno::ETIMEDOUT, Timeout::Error
36
+ Logger.warn "gateway connection timeout, retrying"
37
+ retry
38
+ end
39
+ end
40
+
41
+ def reconnect
42
+ Logger.info "reconnecting to gateway"
43
+ Logger.thread 'r'
44
+ disconnect
45
+ connect
46
+ end
47
+
48
+ def disconnect
49
+ Logger.info "disconnecting from gateway"
50
+ Logger.thread 'd'
51
+ @ssl_socket.close
52
+ end
53
+
54
+ def process_error(notification=nil)
55
+ begin
56
+ error_response = @ssl_socket.read_nonblock 6
57
+ error = ErrorResponse.new error_response, notification
58
+
59
+ Logger.warn "error: #{error.code}, #{error.identifier.to_s}, #{error.message}"
60
+ Logger.thread 'e'
61
+
62
+ reconnect
63
+
64
+ if @opts[:error_block].nil? || !@opts[:error_block].respond_to?(:call)
65
+ Logger.error "You have not implemented an on_error block. This could lead to your account being banned from APNS. See the APNS docs"
66
+ else
67
+ @opts[:error_block].call(error)
68
+ end
69
+
70
+ rescue IO::WaitReadable
71
+ Logger.thread 'g'
72
+ end
73
+ end
74
+
75
+ def write(notification)
76
+ Logger.thread 'w'
77
+
78
+ begin
79
+ process_error
80
+
81
+ @ssl_socket.write notification.payload
82
+
83
+ read, write, error = IO.select [@ssl_socket], [], [@ssl_socket], (@opts[:select_wait] || DEFAULT_SELECT_WAIT)
84
+
85
+ if !error.nil? && !error.first.nil?
86
+ Logger.error "IO.select has reported an unexpected error. Reconnecting, sleeping a bit and retrying"
87
+ sleep 1
88
+ reconnect
89
+ end
90
+
91
+ if !read.nil? && !read.first.nil?
92
+ process_error(notification)
93
+ return false
94
+ end
95
+
96
+ rescue Errno::EPIPE => e
97
+ Logger.warn 'gateway connection returned broken pipe, attempting reconnect and retrying'
98
+ Logger.thread 'f'
99
+ reconnect
100
+ retry
101
+ end
102
+
103
+ true
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,29 @@
1
+ module LeadZeppelin
2
+ module APNS
3
+ module Logger
4
+
5
+ module_function
6
+
7
+ def thread(string)
8
+ LeadZeppelin.thread_logger.print string.upcase if LeadZeppelin.thread_logger
9
+ end
10
+
11
+ def debug(string)
12
+ LeadZeppelin.logger.debug(string) if LeadZeppelin.logger
13
+ end
14
+
15
+ def info(string)
16
+ LeadZeppelin.logger.info(string) if LeadZeppelin.logger
17
+ end
18
+
19
+ def warn(string)
20
+ LeadZeppelin.logger.warn(string) if LeadZeppelin.logger
21
+ end
22
+
23
+ def error(string)
24
+ LeadZeppelin.logger.error(string) if LeadZeppelin.logger
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module LeadZeppelin
2
+ module APNS
3
+ class Notification
4
+ attr_reader :device_token, :identifier, :expiry
5
+
6
+ def initialize(device_token, message, opts={})
7
+ @device_token = device_token
8
+ @opts = opts
9
+
10
+ @identifier = @opts[:identifier] || SecureRandom.random_bytes(4)
11
+ @identifier = @identifier.to_s
12
+
13
+ @expiry = @opts[:expiry].nil? ? 1 : @opts[:expiry].to_i
14
+
15
+ if message.is_a?(Hash)
16
+ other = message.delete(:other)
17
+ @message = {aps: message}
18
+ @message.merge!(other) if other
19
+ elsif message.is_a?(String)
20
+ @message = {aps: {alert: message}}
21
+ else
22
+ raise ArgumentError, "notification message must be hash or string"
23
+ end
24
+ end
25
+
26
+ def payload
27
+ [1, @identifier, @expiry, 0, 32, packaged_token, 0, message_json.bytesize, message_json].pack("cA4Ncca*cca*")
28
+ end
29
+
30
+ def packaged_token
31
+ [device_token.gsub(/[\s|<|>]/,'')].pack('H*')
32
+ end
33
+
34
+ def message_json
35
+ @message_json ||= MultiJson.encode @message
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module LeadZeppelin
2
+ VERSION = '0.1.1'
3
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lead_zeppelin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kyle Drake
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
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: connection_pool
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
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
+ description: Thread-safe, multi-application APNS client
63
+ email:
64
+ - kyledrake@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE
72
+ - README.md
73
+ - Rakefile
74
+ - lead_zeppelin.gemspec
75
+ - lib/lead_zeppelin.rb
76
+ - lib/lead_zeppelin/apns.rb
77
+ - lib/lead_zeppelin/apns/application.rb
78
+ - lib/lead_zeppelin/apns/client.rb
79
+ - lib/lead_zeppelin/apns/error.rb
80
+ - lib/lead_zeppelin/apns/gateway.rb
81
+ - lib/lead_zeppelin/apns/logger.rb
82
+ - lib/lead_zeppelin/apns/notification.rb
83
+ - lib/lead_zeppelin/version.rb
84
+ homepage: https://github.com/geoloqi/lead_zeppelin
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.24
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Thread-safe, multi-application APNS client that makes it easier to develop
108
+ notification software for the APNS service.
109
+ test_files: []