suj-pusher 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ #take keys of hash and transform those to a symbols
3
+ def self.symbolize_keys(value)
4
+ return value if not value.is_a?(Hash)
5
+ hash = value.inject({}){|memo,(k,v)| memo[k.to_sym] = Hash.symbolize_keys(v); memo}
6
+ return hash
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Suj
2
+ module Pusher
3
+ VERSION = '0.0.1'
4
+ end
5
+ 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: []