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 +20 -0
- data/README.md +3 -0
- data/lib/push-apns.rb +11 -0
- data/lib/push-apns/version.rb +3 -0
- data/lib/push/apns/binary_notification_validator.rb +12 -0
- data/lib/push/daemon/apns.rb +41 -0
- data/lib/push/daemon/apns_support/certificate.rb +37 -0
- data/lib/push/daemon/apns_support/connection_apns.rb +96 -0
- data/lib/push/daemon/apns_support/feedback_receiver.rb +58 -0
- data/lib/push/feedback_apns.rb +5 -0
- data/lib/push/message_apns.rb +101 -0
- metadata +89 -0
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
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,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,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: []
|