lead_zeppelin 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []