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.
@@ -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
@@ -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.
@@ -0,0 +1,2 @@
1
+ require 'apns/connection'
2
+ require 'apns/notification'
@@ -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: