kw_apn 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ ====================================
2
+ Getting Started
3
+ ====================================
4
+ For Rails start the generator: kw_apn
5
+ This will copy the configuration example into your project.
6
+
7
+ If you not working with Rails you need to specify either Rails.root and Rails.env or RACK_ROOT and RACK_ENV for the gem to work.
8
+
9
+ Manual:
10
+ create the File
11
+ <project_root>/config/kw_apn.yml
12
+
13
+ structure of the config file should look like this:
14
+
15
+
16
+ environment:
17
+ cert_file: path to ssl certificate file
18
+ push_host: 'gateway.sandbox.push.apple.com' or 'gateway.push.apple.com' for live
19
+ push_port: 2195
20
+ feedback_host: 'feedback.sandbox.push.apple.com' or 'feedback.push.apple.com' for live
21
+ feedback_port: 2196
22
+
23
+
24
+ ====================================
25
+ Example
26
+ ====================================
27
+
28
+ users = MyAPNUsers.all
29
+ n = []
30
+ payload = {:aps => {:alert => "Something very important for everyone to read", :sound => 'annoying_beep'}}
31
+
32
+ users.each do |u|
33
+ n << KwAPN::Notification.create(u.token, payload, 0)
34
+ end
35
+
36
+ status, ret = KwAPN::Sender.push(n, 'TestSession')
37
+
38
+ if status == :ok
39
+ ret.each do |token|
40
+ MyAPNUsers.delete_all('token'=>token)
41
+ end
42
+ end
43
+
44
+
45
+ ====================================
46
+ Copyright
47
+ ====================================
48
+
49
+ Distributed under the MIT License.
50
+ Based in part on Apns4r by Leonid Ponomarev (http://rdoc.info/projects/thegeekbird/Apns4r)
51
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ task :gem do
5
+ `gem build *.gemspec`
6
+ end
7
+
8
+ task :install do
9
+ Rake::Task[:gem].invoke
10
+ `sudo gem install *.gem`
11
+ Rake::Task[:cleanup].invoke
12
+ end
13
+
14
+ task :cleanup do
15
+ `rm *.gem`
16
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2
data/kw_apn.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ version = File.read('VERSION')
2
+
3
+ File.open(File.join(File.dirname(__FILE__), 'VERSION'), 'w') do |f|
4
+ f.write(version)
5
+ end
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'kw_apn'
9
+ s.version = version
10
+ s.authors = ['Jonathan Cichon']
11
+ s.email = 'cichon@kupferwerk.com'
12
+ s.homepage = 'http://kupferwerk.com'
13
+ s.summary = 'APN Lib by Kupferwerk'
14
+ s.description = 'Apple Push Notification Library by Kupferwerk'
15
+ s.has_rdoc = true
16
+ s.extra_rdoc_files = ['README.rdoc']
17
+ s.require_path = 'lib'
18
+ s.files = Dir['lib/**/*'] + Dir['*.gemspec'] + ['Rakefile', 'README.rdoc', 'VERSION']
19
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,40 @@
1
+ module KwAPN
2
+ require 'erb'
3
+ class Config
4
+ class << self
5
+ def options
6
+ @@options ||= nil
7
+ unless @@options
8
+ p_root = if defined? Rails
9
+ Rails.root
10
+ elsif defined? RACK_ROOT
11
+ RACK_ROOT
12
+ else
13
+ puts "Warning (KwAPN): You need to specifiy either Rails.root or RACK_ROOT for apns to work!"
14
+ nil
15
+ end
16
+
17
+ p_env = if defined? Rails
18
+ Rails.env
19
+ elsif defined? RACK_ENV
20
+ RACK_ENV
21
+ else
22
+ puts "Warning (KwAPN): You need to specifiy either Rails.env or RACK_ENV for apns to work!"
23
+ nil
24
+ end
25
+
26
+ @@options = begin
27
+ raw_config = File.read(p_root.join("config", "kw_apn.yml"))
28
+ parsed_config = ERB.new(raw_config).result
29
+ YAML.load(parsed_config)[p_env].symbolize_keys
30
+ rescue => e
31
+ puts "Warning (KwAPN): Could not parse config file: #{e.message}"
32
+ {}
33
+ end
34
+ @@options[:root] = p_root
35
+ end
36
+ return @@options
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/connection.rb ADDED
@@ -0,0 +1,30 @@
1
+ module KwAPN
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+
6
+ class Connection
7
+
8
+ def connect(host, port, opts)
9
+ ctx = OpenSSL::SSL::SSLContext.new()
10
+ ctx.cert = OpenSSL::X509::Certificate.new(File::read(opts[:cert_file]))
11
+ ctx.key = OpenSSL::PKey::RSA.new(File::read(opts[:cert_file]))
12
+
13
+ s = TCPSocket.new(host, port)
14
+ ssl = OpenSSL::SSL::SSLSocket.new(s, ctx)
15
+ ssl.connect # start SSL session
16
+ ssl.sync_close = true # close underlying socket on SSLSocket#close
17
+ ssl
18
+ end
19
+
20
+ class << self
21
+ def log(s)
22
+ File.open(KwAPN::Config.options[:root].join("log", "kw_apn.log"), File::WRONLY|File::APPEND|File::CREAT, 0666) do |f|
23
+ f.write("#{s}\n")
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ end
data/lib/core.rb ADDED
@@ -0,0 +1,66 @@
1
+ begin
2
+ require 'json'
3
+ rescue
4
+ puts "Warning: you need the json gem for apns to work!"
5
+ end
6
+
7
+ class Hash
8
+ MAX_PAYLOAD_LEN = 256
9
+
10
+ # Converts hash into JSON String.
11
+ # When payload is too long but can be chopped, tries to cut self.[:aps][:alert].
12
+ # If payload still don't fit Apple's restrictions, returns nil
13
+ #
14
+ # @return [String, nil] the object converted into JSON or nil.
15
+ def to_apn_payload
16
+ # Payload too long
17
+ if (to_json.length > MAX_PAYLOAD_LEN)
18
+ alert = self[:aps][:alert]
19
+ self[:aps][:alert] = ''
20
+ # can be chopped?
21
+ if (to_json.length > MAX_PAYLOAD_LEN)
22
+ return nil
23
+ else # inefficient way, but payload may be full of unicode-escaped chars, so...
24
+ self[:aps][:alert] = alert
25
+ while (self.to_json.length > MAX_PAYLOAD_LEN)
26
+ self[:aps][:alert].chop!
27
+ end
28
+ end
29
+ end
30
+ to_json
31
+ end
32
+
33
+ # Invokes {Hash#to_payload} and returns it's length
34
+ # @return [Fixnum, nil] length of object converted into JSON or nil.
35
+ def apn_payload_length
36
+ p = to_apn_payload
37
+ p ? p.length : nil
38
+ end
39
+
40
+ end
41
+
42
+ module KwAPN
43
+
44
+ class Notification
45
+
46
+ attr_accessor :identifier, :token
47
+ def initialize(token, payload, timestamp=0)
48
+ @token, @payload, @timestamp = token, payload, timestamp
49
+ end
50
+
51
+ # Creates new notification with given token and payload
52
+ # @param [String, Fixnum] token APNs token of device to notify
53
+ # @param [Hash, String] payload attached payload
54
+ def Notification.create(token, payload, timestamp=0)
55
+ Notification.new(token.kind_of?(String) ? token.delete(' ') : token.to_s(16) , payload.kind_of?(Hash) ? payload.to_apn_payload : payload, timestamp)
56
+ end
57
+
58
+ # Converts to binary string wich can be writen directly into socket
59
+ # @return [String] binary string representation
60
+ def to_s
61
+ [1, @identifier, @timestamp, 32, @token, @payload.length, @payload].pack("CNNnH*na*")
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,26 @@
1
+ module KwAPN
2
+ class FeedbackReader < Connection
3
+
4
+ attr_accessor :host, :port
5
+ def initialize(host=nil, port=nil)
6
+ @host = host || KwAPN::Config.options[:feedback_host]
7
+ @port = port || KwAPN::Config.options[:feedback_port]
8
+ end
9
+
10
+ def read
11
+ records ||= []
12
+ begin
13
+ @ssl = connect(@host, @port, KwAPN::Config.options)
14
+ while record = @ssl.read(38)
15
+ feedback = record.strip.unpack('NnH*')
16
+ records << feedback[2].scan(/.{0,8}/).join(' ').strip
17
+ end
18
+ rescue => e
19
+ puts "Error reading feedback channel: #{e.message}"
20
+ ensure
21
+ @ssl.close
22
+ end
23
+ return records
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails/generators'
2
+
3
+ class KwApnGenerator < Rails::Generators::Base
4
+
5
+ def install_kw_apn
6
+ source_paths << File.join(File.dirname(__FILE__), 'templates')
7
+
8
+ copy_file "config/kw_apn.yml"
9
+ directory "config/cert"
10
+ directory "log"
11
+ copy_file "log/kw_apn.log"
12
+
13
+ gem 'kw_apn'
14
+ end
15
+
16
+
17
+
18
+ end
@@ -0,0 +1,18 @@
1
+ defaults: &defaults
2
+ cert_file: <%= Rails.root.join("config/cert", "apn_#{Rails.env}_cert.pem") %>
3
+ push_host: 'gateway.sandbox.push.apple.com'
4
+ push_port: 2195
5
+ feedback_host: 'feedback.sandbox.push.apple.com'
6
+ feedback_port: 2196
7
+
8
+ development:
9
+ <<: *defaults
10
+ profile: true
11
+
12
+ test:
13
+ <<: *defaults
14
+
15
+ production:
16
+ <<: *defaults
17
+ push_host: 'gateway.push.apple.com'
18
+ feedback_host: 'feedback.push.apple.com'
File without changes
@@ -0,0 +1 @@
1
+ Installs config templates to get you started with your project
data/lib/kw_apn.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'config'
2
+ require 'connection'
3
+ require 'sender'
4
+ require 'feedback_reader'
5
+ require 'core'
data/lib/sender.rb ADDED
@@ -0,0 +1,145 @@
1
+ module KwAPN
2
+
3
+ # maybe somebody knows why but Apple seems to have dificulties handling identifiers between 9 and ~ 20
4
+ # if there is a bug on our side feel free to fix it, but for now it seems to work with the offset workaround
5
+ ID_OFFSET = 333
6
+ class Sender < Connection
7
+ attr_accessor :host, :port, :count, :fail_count, :work_thread, :watch_thread, :failed_index_array, :session_id
8
+
9
+ # Creates new {Sender} object with given host and port
10
+ def initialize(session_id, host=nil, port=nil)
11
+ @session_id = session_id
12
+ @host = host || KwAPN::Config.options[:push_host] || 'gateway.sandbox.push.apple.com'
13
+ @port = port || KwAPN::Config.options[:push_port] || 2195
14
+ @count = 0
15
+ @fail_count = 0
16
+ @failed_index_array = []
17
+ self
18
+ end
19
+
20
+ def push_batch(notifications=[])
21
+ begin
22
+ if @ssl != nil
23
+ return [:nok, 'Start a new Connection for every batch you want to perform']
24
+ end
25
+ @count = notifications.length
26
+ start_threads(notifications)
27
+ return [:ok, @failed_index_array.collect{|a| notifications[a].token}]
28
+ rescue => e
29
+ failed
30
+ self.class.log("(#{session_id}) Exception: #{e.message}")
31
+ return [:nok, "Exception: #{e.message}"]
32
+ end
33
+ end
34
+
35
+ def close_connection
36
+ @ssl.close if @ssl
37
+ @ssl = nil
38
+ end
39
+
40
+ private
41
+
42
+ def start_threads(notifications, index=0)
43
+ @ssl = connect(@host, @port, KwAPN::Config.options)
44
+ if @ssl
45
+ @watch_thread = Thread.new do
46
+ perform_watch()
47
+ end
48
+
49
+ @work_thread = Thread.new do
50
+ perform_batch(notifications, index)
51
+ end
52
+
53
+ @work_thread.join
54
+
55
+ if @failed_index_array.last and index <= @failed_index_array.last and @failed_index_array.last < @count - 1 and @ssl.nil?
56
+ # wait for apple to respond errors
57
+ # sleep(1)
58
+ start_threads(notifications, @failed_index_array.last + 1)
59
+ end
60
+ else
61
+ failed
62
+ end
63
+ end
64
+
65
+ def perform_batch(notifications, index=0)
66
+ notifications[index..-1].each_with_index do |n, i|
67
+ begin
68
+ n.identifier = i + index + ID_OFFSET
69
+ bytes = @ssl.write(n.to_s)
70
+ if bytes <= 0
71
+ self.class.log("(#{session_id}) Warning at index #{i+index}: could not write to Socket")
72
+ # TODO?
73
+ # we do not realy want to respond to network errors, as we do not know how many apns might have been lost.
74
+ # At the moment we hope the watchthread does everything right and our connection holds.
75
+ end
76
+ rescue => e
77
+ # probably interrupted by watchthread, do nothing wait for restart
78
+ self.class.log("(#{session_id}) Exception at index #{i+index}: #{e.message}")
79
+
80
+ #if e.message == 'Broken pipe'
81
+ #end
82
+ end
83
+ end
84
+ # wait for apple to respond errors
85
+ sleep(5)
86
+ end
87
+
88
+ def perform_watch
89
+ ret = @ssl.read
90
+ err = ret.strip.unpack('CCN')
91
+ if err[1] != 0 and err[2]
92
+ @failed_index_array << (err[2] - ID_OFFSET)
93
+ failed
94
+ @work_thread.exit
95
+ else
96
+ perform_watch
97
+ end
98
+ end
99
+
100
+ def failed
101
+ @fail_count += 1
102
+ close_connection
103
+ end
104
+
105
+ def error_msg(status)
106
+ case status
107
+ when 1
108
+ "Processing error"
109
+ when 2
110
+ "Missing device token"
111
+ when 3
112
+ "Missing topic"
113
+ when 4
114
+ "Missing payload"
115
+ when 5
116
+ "Invalid token size"
117
+ when 6
118
+ "Invalid topic size"
119
+ when 7
120
+ "Invalid payload size"
121
+ when 8
122
+ "Invalid token"
123
+ when 255
124
+ "unknown"
125
+ end
126
+ end
127
+
128
+ class << self
129
+ # convenient way of connecting with apple and pushing the notifications
130
+ # @param [Array] notifications - An Array of Objects of Type KwAPN::Notification
131
+ # @param [String] session_id - A Identifier for Login purpose
132
+ #
133
+ # @returns [Symbol, Array/String] if no Problems occured :ok and an Array of Tokens failed to push is returned. The Caller should take care of those invalid Tokens.
134
+ def push(notifications, session_id=nil)
135
+ s = self.new(session_id)
136
+ startdate = Time.now
137
+ status, ret = s.push_batch(notifications)
138
+ log("(#{session_id}) #{startdate.to_s} SENT APN #{s.count - s.fail_count}/#{s.count} in #{Time.now.to_i - startdate.to_i} seconds")
139
+ s.close_connection
140
+ return [status, ret]
141
+ end
142
+ end
143
+ end
144
+ end
145
+
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kw_apn
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ version: "0.2"
10
+ platform: ruby
11
+ authors:
12
+ - Jonathan Cichon
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-08-05 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Apple Push Notification Library by Kupferwerk
22
+ email: cichon@kupferwerk.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.rdoc
29
+ files:
30
+ - lib/config.rb
31
+ - lib/connection.rb
32
+ - lib/core.rb
33
+ - lib/feedback_reader.rb
34
+ - lib/generators/kw_apn/kw_apn_generator.rb
35
+ - lib/generators/kw_apn/templates/config/cert/dummy.pem
36
+ - lib/generators/kw_apn/templates/config/kw_apn.yml
37
+ - lib/generators/kw_apn/templates/log/kw_apn.log
38
+ - lib/generators/kw_apn/usage
39
+ - lib/kw_apn.rb
40
+ - lib/sender.rb
41
+ - kw_apn.gemspec
42
+ - Rakefile
43
+ - README.rdoc
44
+ - VERSION
45
+ has_rdoc: true
46
+ homepage: http://kupferwerk.com
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: APN Lib by Kupferwerk
79
+ test_files: []
80
+