suj-pusher 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +3 -1
- data/lib/suj/pusher/apn_connection.rb +18 -7
- data/lib/suj/pusher/apn_feedback_connection.rb +50 -0
- data/lib/suj/pusher/apn_notification.rb +10 -2
- data/lib/suj/pusher/connection_pool.rb +14 -1
- data/lib/suj/pusher/daemon.rb +60 -2
- data/lib/suj/pusher/gcm_connection.rb +1 -1
- data/lib/suj/pusher/version.rb +1 -1
- data/lib/suj/pusher.rb +3 -2
- metadata +33 -11
data/README.md
CHANGED
@@ -62,13 +62,14 @@ Once the pusher daemon is running and connected to your redis server you can pus
|
|
62
62
|
|
63
63
|
Example JSON message:
|
64
64
|
|
65
|
-
```
|
65
|
+
```
|
66
66
|
{
|
67
67
|
'apn_ids': ["xxxxx"],
|
68
68
|
'gcm_ids': ["xxxxx", "yyyyyy"],
|
69
69
|
'development': true,
|
70
70
|
'cert': "cert string",
|
71
71
|
'api_key': "secret key",
|
72
|
+
'time_to_live': 0,
|
72
73
|
'data': {
|
73
74
|
'aps': {
|
74
75
|
'alert': "This is a message"
|
@@ -82,6 +83,7 @@ Example JSON message:
|
|
82
83
|
- development: This can be true or false and indicates if the push notification is to be sent using the APN sandbox gateway (yes) or the APN production gateway (no). This option only affects push notifications to iOS devices and is assumed yes if not provided.
|
83
84
|
- cert: This is a string representation of the certificate used to send push notifications via the APN network. Simply read the cert.pem file as string and plug it in this field.
|
84
85
|
- api_key: This is the secret api_key used to send push notifications via the GCM network. This is the key you get from the Google API console.
|
86
|
+
- time_to_live: Time in seconds the message would be stored on the cloud in case the destination device is not available at the moment. The default value is zero that means the message is discarded if the destination is not reachable at the moment the notification is sent. Note that even if you set this value larger than zero there are limitations that may prevent the message from arriving. For example Google allows to store up to 4 sync messages or 100 payload messages for up to time_to_live messages or max 4 weeks while Apple only stores the last message up to to time_to_live seconds.
|
85
87
|
- data: This is a custom hash that is sent as push notification to the devices. For GCM this hash may contain anything you want as long as its size do not exceed 4096. For APN this data hash MUST contain an *aps* hash that follows Apple push notification format.
|
86
88
|
|
87
89
|
#### Apple *aps* hash
|
@@ -41,11 +41,21 @@ module Suj
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def deliver(data)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
44
|
+
begin
|
45
|
+
@notification = Suj::Pusher::ApnNotification.new(data)
|
46
|
+
if ! disconnected?
|
47
|
+
info "APN delivering data"
|
48
|
+
send_data(@notification.data)
|
49
|
+
@notification = nil
|
50
|
+
info "APN delivered data"
|
51
|
+
else
|
52
|
+
info "APN connection unavailable"
|
53
|
+
end
|
54
|
+
rescue Suj::Pusher::ApnNotification::PayloadTooLarge => e
|
55
|
+
error "APN notification payload too large."
|
56
|
+
debug @notification.data.inspect
|
57
|
+
rescue => ex
|
58
|
+
error "APN notification error : #{ex}"
|
49
59
|
end
|
50
60
|
end
|
51
61
|
|
@@ -67,9 +77,10 @@ module Suj
|
|
67
77
|
info "APN Connection established..."
|
68
78
|
@disconnected = false
|
69
79
|
if ! @notification.nil?
|
70
|
-
info "APN delivering data"
|
80
|
+
info "EST - APN delivering data"
|
71
81
|
send_data(@notification.data)
|
72
82
|
@notification = nil
|
83
|
+
info "EST - APN delivered data"
|
73
84
|
end
|
74
85
|
end
|
75
86
|
|
@@ -77,7 +88,7 @@ module Suj
|
|
77
88
|
info "APN Connection closed..."
|
78
89
|
@disconnected = true
|
79
90
|
FileUtils.rm_f(@cert_file)
|
80
|
-
@pool.remove_connection(@
|
91
|
+
@pool.remove_connection(@cert_key)
|
81
92
|
end
|
82
93
|
end
|
83
94
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
module Suj
|
5
|
+
module Pusher
|
6
|
+
class APNFeedbackConnection < EM::Connection
|
7
|
+
include Suj::Pusher::Logger
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
super
|
11
|
+
@disconnected = true
|
12
|
+
@options = options
|
13
|
+
@cert_key = Digest::SHA1.hexdigest(@options[:cert])
|
14
|
+
@cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
|
15
|
+
File.open(@cert_file, "w") do |f|
|
16
|
+
f.write @options[:cert]
|
17
|
+
end
|
18
|
+
@ssl_options = {
|
19
|
+
private_key_file: @cert_file,
|
20
|
+
cert_chain_file: @cert_file,
|
21
|
+
verify_peer: false
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def disconnected?
|
26
|
+
@disconnected
|
27
|
+
end
|
28
|
+
|
29
|
+
def post_init
|
30
|
+
info "APN Feedback Connection init "
|
31
|
+
start_tls(@ssl_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def receive_data(data)
|
35
|
+
timestamp, size, token = data.unpack("QnN")
|
36
|
+
info "APN Feedback invalid token #{token}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def connection_completed
|
40
|
+
info "APN Feedback Connection established..."
|
41
|
+
@disconnected = false
|
42
|
+
end
|
43
|
+
|
44
|
+
def unbind
|
45
|
+
info "APN Feedback Connection closed..."
|
46
|
+
@disconnected = true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -9,6 +9,7 @@ module Suj
|
|
9
9
|
|
10
10
|
def initialize(options = {})
|
11
11
|
@token = options[:token]
|
12
|
+
@ttl = options[:time_to_live] || 0
|
12
13
|
@options = options
|
13
14
|
raise InvalidToken if @token.nil? || (@token.length != 64)
|
14
15
|
raise PayloadTooLarge if data.size > MAX_SIZE
|
@@ -24,15 +25,22 @@ module Suj
|
|
24
25
|
|
25
26
|
private
|
26
27
|
|
28
|
+
def get_expiry
|
29
|
+
if @ttl.to_i == 0
|
30
|
+
return 0
|
31
|
+
else
|
32
|
+
return Time.now.to_i + @ttl.to_i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
27
36
|
def encode_data
|
28
37
|
identifier = 0
|
29
|
-
expiry =
|
38
|
+
expiry = get_expiry
|
30
39
|
size = [payload].pack("a*").size
|
31
40
|
data_array = [1, identifier, expiry, 32, @token, size, payload]
|
32
41
|
info("PAYLOAD: #{data_array}")
|
33
42
|
data_array.pack("cNNnH*na*")
|
34
43
|
end
|
35
|
-
|
36
44
|
end
|
37
45
|
end
|
38
46
|
end
|
@@ -9,7 +9,10 @@ module Suj
|
|
9
9
|
|
10
10
|
APN_SANDBOX = "gateway.sandbox.push.apple.com"
|
11
11
|
APN_GATEWAY = "gateway.push.apple.com"
|
12
|
+
FEEDBACK_SANDBOX = "feedback.sandbox.push.apple.com"
|
13
|
+
FEEDBACK_GATEWAY = "feedback.push.apple.com"
|
12
14
|
APN_PORT = 2195
|
15
|
+
FEEDBACK_PORT = 2196
|
13
16
|
|
14
17
|
def initialize(daemon)
|
15
18
|
@pool = {}
|
@@ -28,6 +31,16 @@ module Suj
|
|
28
31
|
@pool[cert] ||= EM.connect(APN_SANDBOX, APN_PORT, APNConnection, self, options)
|
29
32
|
end
|
30
33
|
|
34
|
+
def feedback_connection(options = {})
|
35
|
+
info "Feedback connection"
|
36
|
+
EM.connect(FEEDBACK_GATEWAY, FEEDBACK_PORT, APNFeedbackConnection, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def feedback_sandbox_connection(options = {})
|
40
|
+
info "Feedback sandbox connection"
|
41
|
+
EM.connect(FEEDBACK_SANDBOX, FEEDBACK_PORT, APNFeedbackConnection, options)
|
42
|
+
end
|
43
|
+
|
31
44
|
def gcm_connection(options = {})
|
32
45
|
# All GCM connections are unique, even if they are to the same app.
|
33
46
|
api_key = "#{options[:api_key]}#{rand * 100}"
|
@@ -37,7 +50,7 @@ module Suj
|
|
37
50
|
|
38
51
|
def remove_connection(key)
|
39
52
|
info "Removing connection #{key}"
|
40
|
-
@pool.delete(key)
|
53
|
+
info "Connection not found" unless @pool.delete(key)
|
41
54
|
end
|
42
55
|
|
43
56
|
end
|
data/lib/suj/pusher/daemon.rb
CHANGED
@@ -11,15 +11,24 @@ module Suj
|
|
11
11
|
class Daemon
|
12
12
|
include Suj::Pusher::Logger
|
13
13
|
|
14
|
+
FEEDBACK_TIME = 43200 # 12H
|
15
|
+
|
14
16
|
def start
|
15
17
|
info "Starting pusher daemon"
|
16
18
|
EM.run do
|
17
19
|
wait_msg do |msg|
|
18
20
|
begin
|
21
|
+
info "RECEIVED MESSAGE"
|
19
22
|
data = Hash.symbolize_keys(MultiJson.load(msg))
|
20
23
|
send_notification(data)
|
24
|
+
info "SENT MESSAGE"
|
25
|
+
retrieve_feedback(data)
|
26
|
+
info "FINISHED FEEDBACK RETRIEVAL"
|
21
27
|
rescue MultiJson::LoadError
|
22
28
|
warn("Received invalid json data, discarding msg")
|
29
|
+
rescue => e
|
30
|
+
error("Error sending notification : #{e}")
|
31
|
+
error e.backtrace
|
23
32
|
end
|
24
33
|
end
|
25
34
|
end
|
@@ -37,8 +46,28 @@ module Suj
|
|
37
46
|
private
|
38
47
|
|
39
48
|
def wait_msg
|
49
|
+
redis.on(:connected) { info "REDIS - Connected to Redis server" }
|
50
|
+
redis.on(:closed) { info "REDIS - Closed connection to Redis server" }
|
51
|
+
redis.on(:failed) { info "REDIS - redis connection FAILED" }
|
52
|
+
redis.on(:reconnected) { info "REDIS - Reconnected to Redis server" }
|
53
|
+
redis.on(:disconnected) { info "REDIS - Disconnected from Redis server" }
|
54
|
+
redis.on(:reconnect_failed) { info "REDIS - Reconnection attempt to Redis server FAILED" }
|
55
|
+
# EM.add_periodic_timer(30) { redis.publish Suj::Pusher::QUEUE, "ECHO" }
|
40
56
|
redis.pubsub.subscribe(Suj::Pusher::QUEUE) do |msg|
|
41
|
-
|
57
|
+
if msg == "ECHO"
|
58
|
+
info "REDIS - ECHO Received"
|
59
|
+
elsif msg == "PUSH_MSG"
|
60
|
+
info "REDIS - PUSH_MSG Received"
|
61
|
+
get_message.callback do |message|
|
62
|
+
if message
|
63
|
+
yield message
|
64
|
+
else
|
65
|
+
info "REDIS - PUSH_MSG Queue was empty"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
else
|
69
|
+
yield msg
|
70
|
+
end
|
42
71
|
end
|
43
72
|
end
|
44
73
|
|
@@ -56,6 +85,16 @@ module Suj
|
|
56
85
|
end
|
57
86
|
end
|
58
87
|
|
88
|
+
def retrieve_feedback(msg)
|
89
|
+
if msg.has_key?(:cert)
|
90
|
+
if msg.has_key?(:development) && msg[:development]
|
91
|
+
feedback_sandbox_connection(msg)
|
92
|
+
else
|
93
|
+
feedback_connection(msg)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
59
98
|
def send_apn_notification(msg)
|
60
99
|
info "Sending APN notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
|
61
100
|
conn = pool.apn_connection(msg)
|
@@ -72,6 +111,20 @@ module Suj
|
|
72
111
|
end
|
73
112
|
end
|
74
113
|
|
114
|
+
def feedback_connection(msg)
|
115
|
+
return if @last_feedback and (Time.now - @last_feedback < FEEDBACK_TIME)
|
116
|
+
info "Get feedback information"
|
117
|
+
conn = pool.feedback_connection(msg)
|
118
|
+
@last_feedback = Time.now
|
119
|
+
end
|
120
|
+
|
121
|
+
def feedback_sandbox_connection(msg)
|
122
|
+
return if @last_sandbox_feedback and (Time.now - @last_sandbox_feedback < FEEDBACK_TIME)
|
123
|
+
info "Get feedback sandbox information"
|
124
|
+
conn = pool.feedback_sandbox_connection(msg)
|
125
|
+
@last_sandbox_feedback = Time.now
|
126
|
+
end
|
127
|
+
|
75
128
|
def send_gcm_notification(msg)
|
76
129
|
info "Sending GCM notification via connection #{msg[:api_key]}"
|
77
130
|
conn = pool.gcm_connection(msg)
|
@@ -79,7 +132,12 @@ module Suj
|
|
79
132
|
end
|
80
133
|
|
81
134
|
def redis
|
82
|
-
@redis
|
135
|
+
@redis ||= EM::Hiredis.connect(Suj::Pusher.config.redis)
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_message
|
139
|
+
@redis_connection ||= EM::Hiredis.connect(Suj::Pusher.config.redis)
|
140
|
+
@redis_connection.rpop MSG_QUEUE
|
83
141
|
end
|
84
142
|
|
85
143
|
def pool
|
@@ -23,10 +23,10 @@ module Suj
|
|
23
23
|
|
24
24
|
body = MultiJson.dump({
|
25
25
|
registration_ids: msg[:gcm_ids],
|
26
|
+
time_to_live: msg[:time_to_live] || 0,
|
26
27
|
data: msg[:data] || {}
|
27
28
|
})
|
28
29
|
|
29
|
-
|
30
30
|
http = EventMachine::HttpRequest.new(GATEWAY).post( head: @headers, body: body )
|
31
31
|
|
32
32
|
http.errback do
|
data/lib/suj/pusher/version.rb
CHANGED
data/lib/suj/pusher.rb
CHANGED
@@ -3,6 +3,7 @@ require 'suj/pusher/version'
|
|
3
3
|
require 'suj/pusher/configuration'
|
4
4
|
require 'suj/pusher/logger'
|
5
5
|
require 'suj/pusher/connection_pool'
|
6
|
+
require 'suj/pusher/apn_feedback_connection'
|
6
7
|
require 'suj/pusher/apn_connection'
|
7
8
|
require 'suj/pusher/gcm_connection'
|
8
9
|
require 'suj/pusher/apn_notification'
|
@@ -13,7 +14,7 @@ require 'logger'
|
|
13
14
|
module Suj
|
14
15
|
module Pusher
|
15
16
|
|
16
|
-
QUEUE
|
17
|
-
|
17
|
+
QUEUE = "suj_pusher_queue"
|
18
|
+
MSG_QUEUE = "suj_pusher_msgs"
|
18
19
|
end
|
19
20
|
end
|
metadata
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: suj-pusher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Horacio Sanson
|
9
|
+
- Fernando Wong
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2013-
|
13
|
+
date: 2013-10-10 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: em-http-request
|
16
|
-
requirement:
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
17
18
|
none: false
|
18
19
|
requirements:
|
19
20
|
- - ! '>='
|
@@ -21,10 +22,15 @@ dependencies:
|
|
21
22
|
version: '0'
|
22
23
|
type: :runtime
|
23
24
|
prerelease: false
|
24
|
-
version_requirements:
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
25
31
|
- !ruby/object:Gem::Dependency
|
26
32
|
name: em-hiredis
|
27
|
-
requirement:
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
28
34
|
none: false
|
29
35
|
requirements:
|
30
36
|
- - ! '>='
|
@@ -32,10 +38,15 @@ dependencies:
|
|
32
38
|
version: '0'
|
33
39
|
type: :runtime
|
34
40
|
prerelease: false
|
35
|
-
version_requirements:
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
36
47
|
- !ruby/object:Gem::Dependency
|
37
48
|
name: multi_json
|
38
|
-
requirement:
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
39
50
|
none: false
|
40
51
|
requirements:
|
41
52
|
- - ! '>='
|
@@ -43,10 +54,15 @@ dependencies:
|
|
43
54
|
version: '0'
|
44
55
|
type: :runtime
|
45
56
|
prerelease: false
|
46
|
-
version_requirements:
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
47
63
|
- !ruby/object:Gem::Dependency
|
48
64
|
name: daemon-spawn
|
49
|
-
requirement:
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
50
66
|
none: false
|
51
67
|
requirements:
|
52
68
|
- - ! '>='
|
@@ -54,7 +70,12 @@ dependencies:
|
|
54
70
|
version: '0'
|
55
71
|
type: :runtime
|
56
72
|
prerelease: false
|
57
|
-
version_requirements:
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
58
79
|
description: Stand alone push notification server for APN and GCM.
|
59
80
|
email:
|
60
81
|
- rd@skillupjapan.co.jp
|
@@ -67,6 +88,7 @@ files:
|
|
67
88
|
- README.md
|
68
89
|
- lib/suj/pusher.rb
|
69
90
|
- lib/suj/pusher/apn_connection.rb
|
91
|
+
- lib/suj/pusher/apn_feedback_connection.rb
|
70
92
|
- lib/suj/pusher/apn_notification.rb
|
71
93
|
- lib/suj/pusher/configuration.rb
|
72
94
|
- lib/suj/pusher/connection_pool.rb
|
@@ -96,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
118
|
version: '0'
|
97
119
|
requirements: []
|
98
120
|
rubyforge_project:
|
99
|
-
rubygems_version: 1.8.
|
121
|
+
rubygems_version: 1.8.23
|
100
122
|
signing_key:
|
101
123
|
specification_version: 3
|
102
124
|
summary: Stand alone push notification server.
|