apns-ruby 0.0.0
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 +7 -0
- data/README.md +39 -0
- data/lib/apns.rb +2 -0
- data/lib/apns/connection.rb +103 -0
- data/lib/apns/notification.rb +70 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bb1f035a12ec76366159c112eccd23dc2f319b8d
|
4
|
+
data.tar.gz: 2d6a9188eba06cb48683f10ba748a71069a46157
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 07fe76e8c32cd0a91d0bc76eb283896aa53a65b4ffc4b5caf960fbb018c1212c29880e13b9acb7a65d948ab8618497b70fe6da9f946d8595e528f87ed03c1987
|
7
|
+
data.tar.gz: 47499c4c0f549710d22a27e4d379d8642ebc0e2cd34fb18b9c3d9b2b8e7a65b6cfd4a76f599eec9c23853d00b078df8de61a43b3f6dbe9ecb3cebaa6385652fc
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
apns-ruby
|
2
|
+
-----
|
3
|
+
This gem sends APNS notifications with proper error handling. Most importantly, it does this without all the nonsense other gems provide:
|
4
|
+
* Storing notifications in a database, and have a separate process consume them. Worse, only a single consumer is ever allowed.
|
5
|
+
* Opening a new SSL connection for every notification, which typically takes several hundred milliseconds. Imagine how long it would take to send a million notifications.
|
6
|
+
|
7
|
+
Beware though, that this gem has it's own nonsense in that by default, it sets the notification buffer size to 1 million, which could potentially take up 256MB of memory (Apple caps the size of a single notification's payload at 256 bytes). If you cannot afford to spare that amount of memory, configure it to a lesser number `conn = APNS::Connection.new(notification_buffer_size: 1024) # 1024 bytes`.
|
8
|
+
|
9
|
+
Example usage:
|
10
|
+
```
|
11
|
+
pem = path_to_your_pem_file
|
12
|
+
host = 'gateway.sandbox.push.apple.com' # or 'gateway.push.apple.com' on production
|
13
|
+
token = a_valid_apns_device_token
|
14
|
+
conn = APNS::Connection.new(pem: pem, host: host)
|
15
|
+
conn.error_handler = ->(code, notification) {
|
16
|
+
case code
|
17
|
+
when 8
|
18
|
+
puts "Invalid token: #{notification.device_token}"
|
19
|
+
else
|
20
|
+
# Consult Apple's docs
|
21
|
+
end
|
22
|
+
}
|
23
|
+
n1 = APNS::Notification.new(token, alert: 'hello')
|
24
|
+
ne = APNS::Notification.new('bogustoken', alert: 'error')
|
25
|
+
n2 = APNS::Notification.new(token, alert: 'world')
|
26
|
+
conn.write([n1, ne, n2])
|
27
|
+
# Should receive only a 'hello' notification on your device
|
28
|
+
|
29
|
+
# Wait for Apple to report an error and close the connection
|
30
|
+
sleep(7)
|
31
|
+
conn.write([APNS::Notification.new(token, alert: 'hello world 0')])
|
32
|
+
sleep(7)
|
33
|
+
conn.write([APNS::Notification.new(token, alert: 'hello world 1')])
|
34
|
+
|
35
|
+
# 'Invalid token: bogustoken' is printed out
|
36
|
+
# We should be receiving the 'world', 'hello world 0', and 'hello world 1' notifications
|
37
|
+
```
|
38
|
+
|
39
|
+
A great amount of code in this gem is copied from https://github.com/jpoz/APNS , many thanks to his pioneering work. This work itself is licensed under the MIT license.
|
data/lib/apns.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'socket'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
module APNS
|
6
|
+
class Connection
|
7
|
+
attr_accessor :error_handler
|
8
|
+
|
9
|
+
def initialize(pem: ,
|
10
|
+
pass: nil,
|
11
|
+
host: 'gateway.sandbox.push.apple.com',
|
12
|
+
port: 2195,
|
13
|
+
notification_buffer_size: 1_000_000)
|
14
|
+
@notifications = []
|
15
|
+
@pem = pem
|
16
|
+
@pass = pass
|
17
|
+
@host = host
|
18
|
+
@port = port
|
19
|
+
@notification_buffer_size = notification_buffer_size
|
20
|
+
|
21
|
+
@sock, @ssl = open_connection
|
22
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@sock, @ssl))
|
23
|
+
end
|
24
|
+
def self.finalize sock, ssl
|
25
|
+
proc {
|
26
|
+
ssl.close
|
27
|
+
sock.close
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def write ns
|
32
|
+
if @notifications.size > @notification_buffer_size
|
33
|
+
ns = detect_failed_notifications(timeout: 0.5) + ns
|
34
|
+
@notifications = []
|
35
|
+
end
|
36
|
+
|
37
|
+
packed = pack_notifications(ns)
|
38
|
+
@ssl.write(packed)
|
39
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError
|
40
|
+
failed_notifications = detect_failed_notifications timeout: 3
|
41
|
+
@notifications = []
|
42
|
+
@ssl.close
|
43
|
+
@sock.close
|
44
|
+
@sock, @ssl = open_connection
|
45
|
+
|
46
|
+
ns = failed_notifications
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
|
50
|
+
def pack_notifications notifications
|
51
|
+
bytes = ''
|
52
|
+
|
53
|
+
notifications.each do |n|
|
54
|
+
n.message_identifier = [@notifications.size].pack('N')
|
55
|
+
@notifications << n
|
56
|
+
|
57
|
+
# Each notification frame consists of
|
58
|
+
# 1. (e.g. protocol version) 2 (unsigned char [1 byte])
|
59
|
+
# 2. size of the full frame (unsigend int [4 byte], big endian)
|
60
|
+
pn = n.packaged_notification
|
61
|
+
bytes << ([2, pn.bytesize].pack('CN') + pn)
|
62
|
+
end
|
63
|
+
|
64
|
+
bytes
|
65
|
+
end
|
66
|
+
|
67
|
+
def detect_failed_notifications(timeout:)
|
68
|
+
begin
|
69
|
+
tuple = Timeout::timeout(timeout){ @ssl.read(6) }
|
70
|
+
_, code, failed_id = tuple.unpack("ccN")
|
71
|
+
rescue Timeout::Error
|
72
|
+
end
|
73
|
+
failed_id ||= @notifications.size
|
74
|
+
|
75
|
+
# Report error to user
|
76
|
+
failed_notification = @notifications[failed_id]
|
77
|
+
if @error_handler && failed_notification
|
78
|
+
@error_handler.call(code, failed_notification)
|
79
|
+
end
|
80
|
+
|
81
|
+
@notifications[failed_id+1..-1] || []
|
82
|
+
end
|
83
|
+
|
84
|
+
def open_connection
|
85
|
+
context = OpenSSL::SSL::SSLContext.new
|
86
|
+
context.cert = OpenSSL::X509::Certificate.new(File.read(@pem))
|
87
|
+
context.key = OpenSSL::PKey::RSA.new(File.read(@pem), @pass)
|
88
|
+
|
89
|
+
sock = TCPSocket.new(@host, @port)
|
90
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock,context)
|
91
|
+
ssl.connect
|
92
|
+
|
93
|
+
return sock, ssl
|
94
|
+
end
|
95
|
+
|
96
|
+
# Override inspect since we do not want to print out the entire @notifications,
|
97
|
+
# whose size might be over a hundred thousand
|
98
|
+
def inspect
|
99
|
+
puts "#<#{self.class}:#{'0x%014x' % object_id} @pem=#{@pem} @pass=#{@pass} @host=#{@host} @port=#{@port} @notifications.size=#{@notifications.size} @error_handler=#{@error_handler}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module APNS
|
4
|
+
|
5
|
+
class Notification
|
6
|
+
attr_accessor :device_token, :alert, :badge, :sound, :other, :priority
|
7
|
+
attr_accessor :message_identifier, :expiration_date
|
8
|
+
attr_accessor :content_available
|
9
|
+
|
10
|
+
def initialize(device_token, message)
|
11
|
+
self.device_token = device_token
|
12
|
+
if message.is_a?(Hash)
|
13
|
+
self.alert = message[:alert]
|
14
|
+
self.badge = message[:badge]
|
15
|
+
self.sound = message[:sound]
|
16
|
+
self.other = message[:other]
|
17
|
+
self.message_identifier = message[:message_identifier]
|
18
|
+
self.content_available = !message[:content_available].nil?
|
19
|
+
self.expiration_date = message[:expiration_date]
|
20
|
+
self.priority = if self.content_available
|
21
|
+
message[:priority] || 5
|
22
|
+
else
|
23
|
+
message[:priority] || 10
|
24
|
+
end
|
25
|
+
elsif message.is_a?(String)
|
26
|
+
self.alert = message
|
27
|
+
else
|
28
|
+
raise "Notification needs to have either a hash or string"
|
29
|
+
end
|
30
|
+
|
31
|
+
self.message_identifier ||= "0000"
|
32
|
+
end
|
33
|
+
|
34
|
+
def packaged_notification
|
35
|
+
pt = self.packaged_token
|
36
|
+
pm = self.packaged_message
|
37
|
+
pi = self.message_identifier
|
38
|
+
pe = (self.expiration_date || 0).to_i
|
39
|
+
pr = (self.priority || 10).to_i
|
40
|
+
|
41
|
+
# Each item consist of
|
42
|
+
# 1. unsigned char [1 byte] is the item (type) number according to Apple's docs
|
43
|
+
# 2. short [big endian, 2 byte] is the size of this item
|
44
|
+
# 3. item data, depending on the type fixed or variable length
|
45
|
+
data = ''
|
46
|
+
data << [1, pt.bytesize, pt].pack("CnA*")
|
47
|
+
data << [2, pm.bytesize, pm].pack("CnA*")
|
48
|
+
data << [3, pi.bytesize, pi].pack("CnA*")
|
49
|
+
data << [4, 4, pe].pack("CnN")
|
50
|
+
data << [5, 1, pr].pack("CnC")
|
51
|
+
|
52
|
+
data
|
53
|
+
end
|
54
|
+
|
55
|
+
def packaged_token
|
56
|
+
[device_token.gsub(/[\s|<|>]/,'')].pack('H*')
|
57
|
+
end
|
58
|
+
|
59
|
+
def packaged_message
|
60
|
+
aps = {'aps'=> {} }
|
61
|
+
aps['aps']['alert'] = self.alert if self.alert
|
62
|
+
aps['aps']['badge'] = self.badge if self.badge
|
63
|
+
aps['aps']['sound'] = self.sound if self.sound
|
64
|
+
aps['aps']['content-available'] = 1 if self.content_available
|
65
|
+
|
66
|
+
aps.merge!(self.other) if self.other
|
67
|
+
JSON.generate(aps)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apns-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- awaw
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: APNS notifications sent properly with correct connection management and
|
14
|
+
error handling
|
15
|
+
email: awawfumin@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/apns.rb
|
22
|
+
- lib/apns/connection.rb
|
23
|
+
- lib/apns/notification.rb
|
24
|
+
homepage: https://github.com/fumin/apns-ruby
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.2.2
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: APNS notifications sent properly
|
48
|
+
test_files: []
|
49
|
+
has_rdoc:
|