suj-pusher 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +148 -0
- data/bin/pusher +41 -0
- data/lib/suj/pusher/apn_connection.rb +84 -0
- data/lib/suj/pusher/apn_notification.rb +38 -0
- data/lib/suj/pusher/configuration.rb +40 -0
- data/lib/suj/pusher/connection_pool.rb +46 -0
- data/lib/suj/pusher/daemon.rb +91 -0
- data/lib/suj/pusher/gcm_connection.rb +49 -0
- data/lib/suj/pusher/logger.rb +13 -0
- data/lib/suj/pusher/monkey/hash.rb +8 -0
- data/lib/suj/pusher/version.rb +5 -0
- data/lib/suj/pusher.rb +19 -0
- metadata +103 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 R&D SkillupJapan Corp.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# Suj Pusher Server
|
2
|
+
|
3
|
+
This is a simple but enterprise level pusher server that can push notifications to iOS and Android devices using the APN and GCM push services respectively.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- Support both APN and GCM push services with a simple unified API interface.
|
8
|
+
- Keep persistent connections to APN following Apple recommendation.
|
9
|
+
- Use redis pub/sub mechanism for real time push notifications. No polling.
|
10
|
+
- No need to set APN certificates or GCM api keys in configuration files or pusher startup. These are sent in a per request basis. This allows support for multiple APN certs and GCM api keys in a single Pusher instance.
|
11
|
+
- EventMachine based to handle in the order of thousands of push requests per second.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Via gems simply install the suj-pusher gem on your system:
|
16
|
+
|
17
|
+
```sh
|
18
|
+
gem install suj-pusher
|
19
|
+
```
|
20
|
+
|
21
|
+
or download the source via git:
|
22
|
+
|
23
|
+
```
|
24
|
+
git clone https://github.com/sujrd/suj-pusher.git
|
25
|
+
```
|
26
|
+
|
27
|
+
after cloning the gem make sure to run bundle install to install the dependencies:
|
28
|
+
|
29
|
+
```
|
30
|
+
cd suj-pusher
|
31
|
+
bundle install
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
To run the server simply run the pusher daemon:
|
37
|
+
|
38
|
+
```
|
39
|
+
pusher start|stop|restart|status <options>
|
40
|
+
```
|
41
|
+
|
42
|
+
options:
|
43
|
+
- -r <REDIS>: The redis server used to receive push notification messages. The default is localhost:6379.
|
44
|
+
|
45
|
+
Note that the daemon will run on your current folder and under the current user. Make sure you cd to a folder (e.g. /var/run/pusher) and that your current user has permissions to create folders/files inside that directory:
|
46
|
+
|
47
|
+
```
|
48
|
+
mkdir /var/run/pusher
|
49
|
+
chown pusher_user:pusher_group /var/run/pusher
|
50
|
+
cd /var/run/pusher
|
51
|
+
/usr/bin/pusher start -r redis://192.168.x.x:6379/namespace
|
52
|
+
```
|
53
|
+
|
54
|
+
The pusher daemon creates a logs, tmp and certs directory in the current path. To check the state of the daemon you may check the logs. We store the APN certs inside the certs directory temporarily during the connection to APN and delete them when the connection is closed. Still make sure this folder is only readable/writeable by the user under which the pusher process is running.
|
55
|
+
|
56
|
+
## Sending Notifications
|
57
|
+
|
58
|
+
Once the pusher daemon is running and connected to your redis server you can push notifications by publishing messages to redis. The message format is a simple JSON string.
|
59
|
+
|
60
|
+
|
61
|
+
### JSON string format
|
62
|
+
|
63
|
+
Example JSON message:
|
64
|
+
|
65
|
+
```json
|
66
|
+
{
|
67
|
+
'apn_ids': ["xxxxx"],
|
68
|
+
'gcm_ids': ["xxxxx", "yyyyyy"],
|
69
|
+
'development': true,
|
70
|
+
'cert': "cert string",
|
71
|
+
'api_key': "secret key",
|
72
|
+
'data': {
|
73
|
+
'aps': {
|
74
|
+
'alert': "This is a message"
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
- apn_ids: This is an array with the list of iOS client tokens to which the push notification is to be sent. These are the tokens you get from the iOS devices when they register for APN push notifications.
|
81
|
+
- gcm_ids: This is an array with the list of Android client ids to which the push notification is to be sent. These IDs are obtained on the devices when they register for GCM push notifications. You may only have up to 1000 ids in this array.
|
82
|
+
- 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
|
+
- 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
|
+
- 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.
|
85
|
+
- 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
|
+
|
87
|
+
#### Apple *aps* hash
|
88
|
+
|
89
|
+
When sending push notifications to iOS devices you must provide an aps hash inside the data hash that follows the format:
|
90
|
+
|
91
|
+
"aps": {
|
92
|
+
"alert": {
|
93
|
+
"action-loc-key": "Open",
|
94
|
+
"body": "Hello, world!"
|
95
|
+
},
|
96
|
+
"badge": 2,
|
97
|
+
"sound": "default"
|
98
|
+
}
|
99
|
+
|
100
|
+
Read the [official documentation](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) for details on the *aps* hash format. Note that this hash must not exceed the 256 bytes or it will be rejected by the APN service.
|
101
|
+
|
102
|
+
#### Sending one message to both APN and GCM
|
103
|
+
|
104
|
+
Normally you would send messages to either Android or iOS indenpendently. But the pusher daemon can send the same message to devices on both networks as long as you follow the Apple restrictions. This is because Apple push messages are more limited than GCM.
|
105
|
+
|
106
|
+
If your data hash is compatible with the APN standard as described above and you specify a APN cert, a GCM api_key, a list of apn_ids and a list of gcm_ids then the message will be delivered via push notification to all the devices in those lists. Apple will display the notifications using their own mechanisms and for Android you will receive the data hash in a Bundle object as usual. Is your responsibility to extract the data from that bundle and display/use it as you please.
|
107
|
+
|
108
|
+
## Examples
|
109
|
+
|
110
|
+
A simple example using ruby code to send a push notification to iOS devices.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
require 'multi_json'
|
114
|
+
require 'redis'
|
115
|
+
|
116
|
+
# Build a message hash
|
117
|
+
msg = {
|
118
|
+
apn_ids: ["xxxxx"],
|
119
|
+
development: true,
|
120
|
+
cert: File.read(pemfile),
|
121
|
+
data: {
|
122
|
+
aps: {
|
123
|
+
alert: "This is a message"
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
# Format the hash as a JSON string. We use multi_json gem for this but you are free to use any JSON encoder you want.
|
129
|
+
msg_json = MultiJson.dump(msg)
|
130
|
+
|
131
|
+
# Obtain a redis instance
|
132
|
+
redis = Redis.new({ host: "localhost", port: 6379})
|
133
|
+
|
134
|
+
# Push the message to the *suj_pusher_queue* in the redis server.
|
135
|
+
redis.publish "suj_pusher_queue", msg_json
|
136
|
+
```
|
137
|
+
|
138
|
+
You must push the messages to the *suj_pusher_queue* queue that is the one the Pusher daemon is listening to. Also make sure your message follows the format described on the previous sections.
|
139
|
+
|
140
|
+
## Issues
|
141
|
+
|
142
|
+
- We have no feedback mechanism. This is a fire and forget daemon that does not tell us if the message was sent or not.
|
143
|
+
- This daemon has no security at all. Anyone that can push to your redis server can use this daemon to spam your users. Make sure your redis server is only accessible to you and the pusher daemon.
|
144
|
+
|
145
|
+
## TODO
|
146
|
+
|
147
|
+
- Implement a feedback mechanism that exposes a simple API to allow users check if some tokens, ids, certs, or api_keys are no longer valid so they can take proper action.
|
148
|
+
- Find a way to register certificates and api_key only once so we do not need to send them for every request. Maybe add a cert/key registration api.
|
data/bin/pusher
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'suj/pusher'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'daemon_spawn'
|
7
|
+
|
8
|
+
|
9
|
+
BANNER = "Usage: pusher start|stop|restart|status [options]"
|
10
|
+
WORKDIR = Dir.pwd
|
11
|
+
|
12
|
+
class PusherDaemon < DaemonSpawn::Base
|
13
|
+
def start(args)
|
14
|
+
config = Suj::Pusher::Configuration.new
|
15
|
+
args.options do |opts|
|
16
|
+
opts.banner = BANNER
|
17
|
+
opts.on('-r REDIS', '--redis REDIS', String, 'Redis server to connect') { |redis| config.redis = redis }
|
18
|
+
opts.on('-v', '--version', 'Print this version of rapns.') { puts "rapns #{Suj::Pusher::VERSION}"; exit }
|
19
|
+
opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
|
20
|
+
opts.parse!
|
21
|
+
end
|
22
|
+
|
23
|
+
config.certs_path = File.join(WORKDIR, "certs")
|
24
|
+
FileUtils.mkdir_p(config.certs_path)
|
25
|
+
FileUtils.mkdir_p(File.join(WORKDIR, "logs"))
|
26
|
+
FileUtils.mkdir_p(File.join(WORKDIR, "tmp/pids"))
|
27
|
+
Suj::Pusher.config.update(config)
|
28
|
+
|
29
|
+
@daemon = Suj::Pusher::Daemon.new
|
30
|
+
@daemon.start
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@daemon.stop
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
PusherDaemon.spawn!(
|
39
|
+
sync_log: true,
|
40
|
+
working_dir: Dir.pwd
|
41
|
+
)
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
module Suj
|
5
|
+
module Pusher
|
6
|
+
class APNConnection < EM::Connection
|
7
|
+
include Suj::Pusher::Logger
|
8
|
+
|
9
|
+
ERRORS = {
|
10
|
+
0 => "No errors encountered",
|
11
|
+
1 => "Processing error",
|
12
|
+
2 => "Missing device token",
|
13
|
+
3 => "Missing topic",
|
14
|
+
4 => "Missing payload",
|
15
|
+
5 => "Invalid token size",
|
16
|
+
6 => "Invalid topic size",
|
17
|
+
7 => "Invalid payload size",
|
18
|
+
8 => "Invalid token",
|
19
|
+
255 => "Unknown error"
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(pool, options = {})
|
23
|
+
super
|
24
|
+
@disconnected = true
|
25
|
+
@pool = pool
|
26
|
+
@options = options
|
27
|
+
@cert_key = Digest::SHA1.hexdigest(@options[:cert])
|
28
|
+
@cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
|
29
|
+
File.open(@cert_file, "w") do |f|
|
30
|
+
f.write @options[:cert]
|
31
|
+
end
|
32
|
+
@ssl_options = {
|
33
|
+
private_key_file: @cert_file,
|
34
|
+
cert_chain_file: @cert_file,
|
35
|
+
verify_peer: false
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnected?
|
40
|
+
@disconnected
|
41
|
+
end
|
42
|
+
|
43
|
+
def deliver(data)
|
44
|
+
@notification = Suj::Pusher::ApnNotification.new(data)
|
45
|
+
if ! disconnected?
|
46
|
+
info "APN delivering data"
|
47
|
+
send_data(@notification.data)
|
48
|
+
@notification = nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def post_init
|
53
|
+
info "APN Connection init "
|
54
|
+
start_tls(@ssl_options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def receive_data(data)
|
58
|
+
cmd, status, id = data.unpack("ccN")
|
59
|
+
if status != 0
|
60
|
+
error "APN push error received: #{ERRORS[status]}"
|
61
|
+
else
|
62
|
+
info "APN push notification sent"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def connection_completed
|
67
|
+
info "APN Connection established..."
|
68
|
+
@disconnected = false
|
69
|
+
if ! @notification.nil?
|
70
|
+
info "APN delivering data"
|
71
|
+
send_data(@notification.data)
|
72
|
+
@notification = nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def unbind
|
77
|
+
info "APN Connection closed..."
|
78
|
+
@disconnected = true
|
79
|
+
FileUtils.rm_f(@cert_file)
|
80
|
+
@pool.remove_connection(@cert)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Suj
|
2
|
+
module Pusher
|
3
|
+
class ApnNotification
|
4
|
+
include Suj::Pusher::Logger
|
5
|
+
MAX_SIZE = 256
|
6
|
+
|
7
|
+
class InvalidToken < StandardError; end
|
8
|
+
class PayloadTooLarge < StandardError; end
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@token = options[:token]
|
12
|
+
@options = options
|
13
|
+
raise InvalidToken if @token.nil? || (@token.length != 64)
|
14
|
+
raise PayloadTooLarge if data.size > MAX_SIZE
|
15
|
+
end
|
16
|
+
|
17
|
+
def payload
|
18
|
+
@payload ||= MultiJson.dump(@options[:data] || {})
|
19
|
+
end
|
20
|
+
|
21
|
+
def data
|
22
|
+
@data ||= encode_data
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def encode_data
|
28
|
+
identifier = 0
|
29
|
+
expiry = 0
|
30
|
+
size = [payload].pack("a*").size
|
31
|
+
data_array = [1, identifier, expiry, 32, @token, size, payload]
|
32
|
+
info("PAYLOAD: #{data_array}")
|
33
|
+
data_array.pack("cNNnH*na*")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Suj
|
2
|
+
module Pusher
|
3
|
+
|
4
|
+
def self.config
|
5
|
+
@config ||= Suj::Pusher::Configuration.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.configure
|
9
|
+
yield config if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
CONFIG_ATTRS = [
|
13
|
+
:certs_path,
|
14
|
+
:workdir,
|
15
|
+
:logger,
|
16
|
+
:redis
|
17
|
+
]
|
18
|
+
|
19
|
+
class Configuration < Struct.new(*CONFIG_ATTRS)
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
super
|
23
|
+
set_defaults
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(other)
|
27
|
+
CONFIG_ATTRS.each do |attr|
|
28
|
+
other_value = other.send(attr)
|
29
|
+
send("#{attr}=", other_value) unless other_value.nil?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_defaults
|
34
|
+
self.redis = "redis://localhost:6379"
|
35
|
+
self.logger = ::Logger.new(STDOUT)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "base64"
|
2
|
+
require File.join File.dirname(File.expand_path(__FILE__)), "apn_connection.rb"
|
3
|
+
require File.join File.dirname(File.expand_path(__FILE__)), "gcm_connection.rb"
|
4
|
+
|
5
|
+
module Suj
|
6
|
+
module Pusher
|
7
|
+
class ConnectionPool
|
8
|
+
include Suj::Pusher::Logger
|
9
|
+
|
10
|
+
APN_SANDBOX = "gateway.sandbox.push.apple.com"
|
11
|
+
APN_GATEWAY = "gateway.push.apple.com"
|
12
|
+
APN_PORT = 2195
|
13
|
+
|
14
|
+
def initialize(daemon)
|
15
|
+
@pool = {}
|
16
|
+
@daemon = daemon
|
17
|
+
end
|
18
|
+
|
19
|
+
def apn_connection(options = {})
|
20
|
+
cert = Digest::SHA1.hexdigest options[:cert]
|
21
|
+
info "APN connection #{cert}"
|
22
|
+
@pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def apn_sandbox_connection(options = {})
|
26
|
+
cert = Digest::SHA1.hexdigest options[:cert]
|
27
|
+
info "APN connection #{cert}"
|
28
|
+
@pool[cert] ||= EM.connect(APN_SANDBOX, APN_PORT, APNConnection, self, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def gcm_connection(options = {})
|
32
|
+
# All GCM connections are unique, even if they are to the same app.
|
33
|
+
api_key = "#{options[:api_key]}#{rand * 100}"
|
34
|
+
info "GCM connection #{api_key}"
|
35
|
+
@pool[api_key] ||= Suj::Pusher::GCMConnection.new(self, api_key, options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_connection(key)
|
39
|
+
info "Removing connection #{key}"
|
40
|
+
@pool.delete(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'socket'
|
3
|
+
require 'pathname'
|
4
|
+
require 'openssl'
|
5
|
+
require 'em-hiredis'
|
6
|
+
require "multi_json"
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
module Suj
|
10
|
+
module Pusher
|
11
|
+
class Daemon
|
12
|
+
include Suj::Pusher::Logger
|
13
|
+
|
14
|
+
def start
|
15
|
+
info "Starting pusher daemon"
|
16
|
+
EM.run do
|
17
|
+
wait_msg do |msg|
|
18
|
+
begin
|
19
|
+
data = Hash.symbolize_keys(MultiJson.load(msg))
|
20
|
+
send_notification(data)
|
21
|
+
rescue MultiJson::LoadError
|
22
|
+
warn("Received invalid json data, discarding msg")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop
|
29
|
+
info "Stopping daemon process"
|
30
|
+
begin
|
31
|
+
EM.stop
|
32
|
+
rescue
|
33
|
+
end
|
34
|
+
info "Stopped daemon process"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def wait_msg
|
40
|
+
redis.pubsub.subscribe(Suj::Pusher::QUEUE) do |msg|
|
41
|
+
yield msg
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def send_notification(msg)
|
46
|
+
if msg.has_key?(:cert)
|
47
|
+
if msg.has_key?(:development) && msg[:development]
|
48
|
+
send_apn_sandbox_notification(msg)
|
49
|
+
else
|
50
|
+
send_apn_notification(msg)
|
51
|
+
end
|
52
|
+
elsif msg.has_key?(:api_key)
|
53
|
+
send_gcm_notification(msg)
|
54
|
+
else
|
55
|
+
warn "Could not determine push notification service."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def send_apn_notification(msg)
|
60
|
+
info "Sending APN notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
|
61
|
+
conn = pool.apn_connection(msg)
|
62
|
+
msg[:apn_ids].each do |apn_id|
|
63
|
+
conn.deliver(msg.merge({token: apn_id}))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_apn_sandbox_notification(msg)
|
68
|
+
info "Sending APN sandbox notification via connection #{Digest::SHA1.hexdigest(msg[:cert])}"
|
69
|
+
conn = pool.apn_sandbox_connection(msg)
|
70
|
+
msg[:apn_ids].each do |apn_id|
|
71
|
+
conn.deliver(msg.merge({token: apn_id}))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def send_gcm_notification(msg)
|
76
|
+
info "Sending GCM notification via connection #{msg[:api_key]}"
|
77
|
+
conn = pool.gcm_connection(msg)
|
78
|
+
conn.deliver(msg)
|
79
|
+
end
|
80
|
+
|
81
|
+
def redis
|
82
|
+
@redis || EM::Hiredis.connect(Suj::Pusher.config.redis)
|
83
|
+
end
|
84
|
+
|
85
|
+
def pool
|
86
|
+
@pool ||= Suj::Pusher::ConnectionPool.new(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "em-http"
|
2
|
+
|
3
|
+
module Suj
|
4
|
+
module Pusher
|
5
|
+
class GCMConnection
|
6
|
+
include Suj::Pusher::Logger
|
7
|
+
|
8
|
+
GATEWAY = "https://android.googleapis.com/gcm/send"
|
9
|
+
|
10
|
+
def initialize(pool, key, options = {})
|
11
|
+
@pool = pool
|
12
|
+
@options = options
|
13
|
+
@key = key
|
14
|
+
@headers = {
|
15
|
+
'Content-Type' => 'application/json',
|
16
|
+
'Authorization' => "key=#{options[:api_key]}"
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def deliver(msg)
|
21
|
+
|
22
|
+
return if msg[:gcm_ids].empty?
|
23
|
+
|
24
|
+
body = MultiJson.dump({
|
25
|
+
registration_ids: msg[:gcm_ids],
|
26
|
+
data: msg[:data] || {}
|
27
|
+
})
|
28
|
+
|
29
|
+
|
30
|
+
http = EventMachine::HttpRequest.new(GATEWAY).post( head: @headers, body: body )
|
31
|
+
|
32
|
+
http.errback do
|
33
|
+
error "GCM network error"
|
34
|
+
@pool.remove_connection(@key)
|
35
|
+
end
|
36
|
+
http.callback do
|
37
|
+
if http.response_header.status != 200
|
38
|
+
error "GCM push error #{http.response_header.status}"
|
39
|
+
error http.response
|
40
|
+
else
|
41
|
+
info "GCM push notification send"
|
42
|
+
info http.response
|
43
|
+
end
|
44
|
+
@pool.remove_connection(@key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Suj
|
2
|
+
module Pusher
|
3
|
+
module Logger
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def info(msg); Suj::Pusher.config.logger.info(msg); end
|
8
|
+
def warn(msg); Suj::Pusher.config.logger.warn(msg); end
|
9
|
+
def error(msg); Suj::Pusher.config.logger.error(msg); end
|
10
|
+
def fatal(msg); Suj::Pusher.config.logger.fatal(msg); end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/suj/pusher.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'suj/pusher/monkey/hash'
|
2
|
+
require 'suj/pusher/version'
|
3
|
+
require 'suj/pusher/configuration'
|
4
|
+
require 'suj/pusher/logger'
|
5
|
+
require 'suj/pusher/connection_pool'
|
6
|
+
require 'suj/pusher/apn_connection'
|
7
|
+
require 'suj/pusher/gcm_connection'
|
8
|
+
require 'suj/pusher/apn_notification'
|
9
|
+
require 'suj/pusher/daemon'
|
10
|
+
|
11
|
+
require 'logger'
|
12
|
+
|
13
|
+
module Suj
|
14
|
+
module Pusher
|
15
|
+
|
16
|
+
QUEUE = "suj_pusher_queue"
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: suj-pusher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Horacio Sanson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: em-http-request
|
16
|
+
requirement: &19868920 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *19868920
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: em-hiredis
|
27
|
+
requirement: &19868320 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *19868320
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: multi_json
|
38
|
+
requirement: &19867580 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *19867580
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: daemon-spawn
|
49
|
+
requirement: &19866460 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *19866460
|
58
|
+
description: Stand alone push notification server for APN and GCM.
|
59
|
+
email:
|
60
|
+
- rd@skillupjapan.co.jp
|
61
|
+
executables:
|
62
|
+
- pusher
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- lib/suj/pusher.rb
|
69
|
+
- lib/suj/pusher/apn_connection.rb
|
70
|
+
- lib/suj/pusher/apn_notification.rb
|
71
|
+
- lib/suj/pusher/configuration.rb
|
72
|
+
- lib/suj/pusher/connection_pool.rb
|
73
|
+
- lib/suj/pusher/daemon.rb
|
74
|
+
- lib/suj/pusher/gcm_connection.rb
|
75
|
+
- lib/suj/pusher/logger.rb
|
76
|
+
- lib/suj/pusher/monkey/hash.rb
|
77
|
+
- lib/suj/pusher/version.rb
|
78
|
+
- bin/pusher
|
79
|
+
homepage: https://github.com/sujrd/suj-pusher
|
80
|
+
licenses: []
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.8.12
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Stand alone push notification server.
|
103
|
+
test_files: []
|