push-apns 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ = PushApns
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/lib/push-apns.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'socket'
2
+ require 'pathname'
3
+ require 'push-apns/version'
4
+ require 'push/apns/binary_notification_validator'
5
+ require 'push/message_apns'
6
+ require 'push/feedback_apns'
7
+ require 'push/daemon/apns'
8
+ require 'push/daemon/interruptible_sleep'
9
+ require 'push/daemon/apns_support/certificate'
10
+ require 'push/daemon/apns_support/connection_apns'
11
+ require 'push/daemon/apns_support/feedback_receiver'
@@ -0,0 +1,3 @@
1
+ module PushApns
2
+ VERSION = "0.0.1.pre"
3
+ end
@@ -0,0 +1,12 @@
1
+ module Push
2
+ module Apns
3
+ class BinaryNotificationValidator < ActiveModel::Validator
4
+
5
+ def validate(record)
6
+ if record.payload_size > 256
7
+ record.errors[:base] << "APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes."
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ module Push
2
+ module Daemon
3
+ class Apns
4
+ attr_accessor :configuration, :certificate
5
+
6
+ def initialize(options)
7
+ self.configuration = options
8
+
9
+ self.certificate = ApnsSupport::Certificate.new(configuration[:certificate])
10
+ certificate.load
11
+
12
+ start_feedback
13
+ end
14
+
15
+ def pushconnections
16
+ self.configuration[:connections]
17
+ end
18
+
19
+ def totalconnections
20
+ # + feedback
21
+ pushconnections + 1
22
+ end
23
+
24
+ def connectiontype
25
+ ApnsSupport::ConnectionApns
26
+ end
27
+
28
+ def start_feedback
29
+ ApnsSupport::FeedbackReceiver.start(self)
30
+ end
31
+
32
+ def stop_feedback
33
+ ApnsSupport::FeedbackReceiver.stop
34
+ end
35
+
36
+ def stop
37
+ stop_feedback
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ module Push
2
+ class CertificateError < StandardError; end
3
+
4
+ module Daemon
5
+ module ApnsSupport
6
+ class Certificate
7
+ attr_accessor :certificate
8
+
9
+ def initialize(certificate_path)
10
+ @certificate_path = path(certificate_path)
11
+ end
12
+
13
+ def path(path)
14
+ if Pathname.new(path).absolute?
15
+ path
16
+ else
17
+ File.join(Rails.root, "config", "push", path)
18
+ end
19
+ end
20
+
21
+ def load
22
+ @certificate = read_certificate
23
+ end
24
+
25
+ protected
26
+
27
+ def read_certificate
28
+ if !File.exists?(@certificate_path)
29
+ raise CertificateError, "#{@certificate_path} does not exist. The certificate location can be configured in config/push/<<environment>>.rb"
30
+ else
31
+ File.read(@certificate_path)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,96 @@
1
+ module Push
2
+ module Daemon
3
+ module ApnsSupport
4
+ class ConnectionApns
5
+ attr_reader :name, :provider
6
+
7
+ def initialize(provider, i=nil)
8
+ @provider = provider
9
+ if i
10
+ # Apns push connection
11
+ @name = "ConnectionApns #{i}"
12
+ @host = "gateway.#{provider.configuration[:sandbox] ? 'sandbox.' : ''}push.apple.com"
13
+ @port = 2195
14
+ else
15
+ @name = "FeedbackReceiver"
16
+ @host = "feedback.#{provider.configuration[:sandbox] ? 'sandbox.' : ''}push.apple.com"
17
+ @port = 2196
18
+ end
19
+ end
20
+
21
+ def connect
22
+ @ssl_context = setup_ssl_context
23
+ @tcp_socket, @ssl_socket = connect_socket
24
+ end
25
+
26
+ def close
27
+ begin
28
+ @ssl_socket.close if @ssl_socket
29
+ @tcp_socket.close if @tcp_socket
30
+ rescue IOError
31
+ end
32
+ end
33
+
34
+ def read(num_bytes)
35
+ @ssl_socket.read(num_bytes)
36
+ end
37
+
38
+ def select(timeout)
39
+ IO.select([@ssl_socket], nil, nil, timeout)
40
+ end
41
+
42
+ def write(data)
43
+ retry_count = 0
44
+
45
+ begin
46
+ write_data(data)
47
+ rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
48
+ retry_count += 1;
49
+
50
+ if retry_count == 1
51
+ Push::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
52
+ end
53
+
54
+ if retry_count <= 3
55
+ reconnect
56
+ sleep 1
57
+ retry
58
+ else
59
+ raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
60
+ end
61
+ end
62
+ end
63
+
64
+ def reconnect
65
+ close
66
+ @tcp_socket, @ssl_socket = connect_socket
67
+ end
68
+
69
+ protected
70
+
71
+ def write_data(data)
72
+ @ssl_socket.write(data)
73
+ @ssl_socket.flush
74
+ end
75
+
76
+ def setup_ssl_context
77
+ ssl_context = OpenSSL::SSL::SSLContext.new
78
+ ssl_context.key = OpenSSL::PKey::RSA.new(provider.certificate.certificate, provider.configuration[:certificate_password])
79
+ ssl_context.cert = OpenSSL::X509::Certificate.new(provider.certificate.certificate)
80
+ ssl_context
81
+ end
82
+
83
+ def connect_socket
84
+ tcp_socket = TCPSocket.new(@host, @port)
85
+ tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
86
+ tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
87
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
88
+ ssl_socket.sync = true
89
+ ssl_socket.connect
90
+ Push::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
91
+ [tcp_socket, ssl_socket]
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,58 @@
1
+ module Push
2
+ module Daemon
3
+ module ApnsSupport
4
+ class FeedbackReceiver
5
+ extend Push::Daemon::InterruptibleSleep
6
+ attr_accessor :provider
7
+ FEEDBACK_TUPLE_BYTES = 38
8
+
9
+ def self.start(provider)
10
+ @provider = provider
11
+ @thread = Thread.new do
12
+ loop do
13
+ break if @stop
14
+ check_for_feedback
15
+ interruptible_sleep @provider.configuration[:feedback_poll]
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.stop
21
+ @stop = true
22
+ interrupt_sleep
23
+ @thread.join if @thread
24
+ end
25
+
26
+ def self.check_for_feedback
27
+ connection = nil
28
+ begin
29
+ connection = ApnsSupport::ConnectionApns.new(@provider)
30
+ connection.connect
31
+
32
+ while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
33
+ timestamp, device = parse_tuple(tuple)
34
+ create_feedback(timestamp, device)
35
+ end
36
+ rescue StandardError => e
37
+ Push::Daemon.logger.error(e)
38
+ ensure
39
+ connection.close if connection
40
+ end
41
+ end
42
+
43
+ protected
44
+
45
+ def self.parse_tuple(tuple)
46
+ failed_at, _, device = tuple.unpack("N1n1H*")
47
+ [Time.at(failed_at).utc, device]
48
+ end
49
+
50
+ def self.create_feedback(failed_at, device)
51
+ formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
52
+ Push::Daemon.logger.info("[FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device}")
53
+ Push::FeedbackApns.create!(:failed_at => failed_at, :device => device)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,5 @@
1
+ module Push
2
+ class FeedbackApns < Push::Feedback
3
+ validates :device, :format => { :with => /\A[a-z0-9]{64}\z/ }
4
+ end
5
+ end
@@ -0,0 +1,101 @@
1
+ module Push
2
+ class MessageApns < Push::Message
3
+ SELECT_TIMEOUT = 0.5
4
+ ERROR_TUPLE_BYTES = 6
5
+ APN_ERRORS = {
6
+ 1 => "Processing error",
7
+ 2 => "Missing device token",
8
+ 3 => "Missing topic",
9
+ 4 => "Missing payload",
10
+ 5 => "Missing token size",
11
+ 6 => "Missing topic size",
12
+ 7 => "Missing payload size",
13
+ 8 => "Invalid token",
14
+ 255 => "None (unknown error)"
15
+ }
16
+
17
+ store :properties, accessors: [:alert, :badge, :sound, :expiry, :attributes_for_device]
18
+
19
+ validates :badge, :numericality => true, :allow_nil => true
20
+ validates :expiry, :numericality => true, :presence => true
21
+ validates :device, :format => { :with => /\A[a-z0-9]{64}\z/ }
22
+ validates_with Push::Apns::BinaryNotificationValidator
23
+
24
+ # def attributes_for_device=(attrs)
25
+ # raise ArgumentError, "attributes_for_device must be a Hash" if !attrs.is_a?(Hash)
26
+ # write_attribute(:attributes_for_device, MultiJson.encode(attrs))
27
+ # end
28
+ #
29
+ # def attributes_for_device
30
+ # MultiJson.decode(read_attribute(:attributes_for_device)) if read_attribute(:attributes_for_device)
31
+ # end
32
+
33
+ def alert=(alert)
34
+ if alert.is_a?(Hash)
35
+ #write_attribute(:alert, MultiJson.encode(alert))
36
+ properties[:alert] = MultiJson.encode(alert)
37
+ else
38
+ #write_attribute(:alert, alert)
39
+ properties[:alert] = alert
40
+ end
41
+ end
42
+
43
+ def alert
44
+ string_or_json = read_attribute(:alert)
45
+ MultiJson.decode(string_or_json) rescue string_or_json
46
+ end
47
+
48
+ # This method conforms to the enhanced binary format.
49
+ # http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4
50
+ def to_message(options = {})
51
+ id_for_pack = options[:for_validation] ? 0 : id
52
+ [1, id_for_pack, expiry, 0, 32, device, 0, payload_size, payload].pack("cNNccH*cca*")
53
+ end
54
+
55
+ def use_connection
56
+ Push::Daemon::ApnsSupport::ConnectionApns
57
+ end
58
+
59
+ def payload
60
+ MultiJson.encode(as_json)
61
+ end
62
+
63
+ def payload_size
64
+ payload.bytesize
65
+ end
66
+
67
+ private
68
+
69
+ def as_json
70
+ json = ActiveSupport::OrderedHash.new
71
+ json['aps'] = ActiveSupport::OrderedHash.new
72
+ json['aps']['alert'] = alert if alert
73
+ json['aps']['badge'] = badge if badge
74
+ json['aps']['sound'] = sound if sound
75
+ attributes_for_device.each { |k, v| json[k.to_s] = v.to_s } if attributes_for_device
76
+ json
77
+ end
78
+
79
+ def check_for_error(connection)
80
+ if connection.select(SELECT_TIMEOUT)
81
+ error = nil
82
+
83
+ if tuple = connection.read(ERROR_TUPLE_BYTES)
84
+ cmd, code, notification_id = tuple.unpack("ccN")
85
+
86
+ description = APN_ERRORS[code.to_i] || "Unknown error. Possible push bug?"
87
+ error = Push::DeliveryError.new(code, notification_id, description, "APNS")
88
+ else
89
+ error = Push::DisconnectionError.new
90
+ end
91
+
92
+ begin
93
+ Push::Daemon.logger.error("[#{connection.name}] Error received, reconnecting...")
94
+ connection.reconnect
95
+ ensure
96
+ raise error if error
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: push-apns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Tom Pesman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: &70268841150140 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70268841150140
25
+ - !ruby/object:Gem::Dependency
26
+ name: push-core
27
+ requirement: &70268841149600 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - =
31
+ - !ruby/object:Gem::Version
32
+ version: 0.0.1.pre
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70268841149600
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3
38
+ requirement: &70268841149200 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70268841149200
47
+ description: Plugin with APNS specific push information.
48
+ email:
49
+ - tom@tnux.net
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/push-apns.rb
55
+ - lib/push-apns/version.rb
56
+ - lib/push/apns/binary_notification_validator.rb
57
+ - lib/push/daemon/apns.rb
58
+ - lib/push/daemon/apns_support/certificate.rb
59
+ - lib/push/daemon/apns_support/connection_apns.rb
60
+ - lib/push/daemon/apns_support/feedback_receiver.rb
61
+ - lib/push/feedback_apns.rb
62
+ - lib/push/message_apns.rb
63
+ - README.md
64
+ - MIT-LICENSE
65
+ homepage: https://github.com/tompesman/push-apns
66
+ licenses: []
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>'
81
+ - !ruby/object:Gem::Version
82
+ version: 1.3.1
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 1.8.5
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: APNS (iOS) part of the modular push daemon.
89
+ test_files: []