pushlet 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/Gemfile +12 -0
- data/README.md +187 -0
- data/Rakefile +10 -0
- data/SPEC-OLD.md +238 -0
- data/lib/pushlet.rb +20 -0
- data/lib/pushlet/api.rb +122 -0
- data/lib/pushlet/apns_gateway.rb +101 -0
- data/lib/pushlet/auth_middleware.rb +29 -0
- data/lib/pushlet/gateway_pool.rb +30 -0
- data/lib/pushlet/version.rb +8 -0
- data/pushlet.gemspec +33 -0
- data/test/env.rb +21 -0
- data/test/files/example.cer +14 -0
- data/test/files/example.key +9 -0
- data/test/files/example.p12 +0 -0
- data/test/files/example.pem +23 -0
- data/test/files/example_with_pass.p12 +0 -0
- data/test/gcm_push_test.rb +32 -0
- data/test/register_application_test.rb +51 -0
- metadata +326 -0
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
tmp
|
14
|
+
.DS_Store
|
15
|
+
|
16
|
+
# YARD artifacts
|
17
|
+
.yardoc
|
18
|
+
_yardoc
|
19
|
+
doc/
|
20
|
+
config.ru
|
21
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
Pushlet
|
2
|
+
=======
|
3
|
+
|
4
|
+
Pushlet is a simple, stateless HTTP API for interacting with Apple and Google's push services for mobile devices. It's goal is to simplify your push notifications by providing a simple SoA API that does this one thing, and does it well.
|
5
|
+
|
6
|
+
Pushlet integrates [Grocer](http://github.com/highgroove/grocer) for APNS, and is designed to be thread safe.
|
7
|
+
|
8
|
+
Because it is containerized and requires no data store, it can be easily put on a hosted service like AppFog or Heroku.
|
9
|
+
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
* Ruby 1.9, JRuby 1.7, Rubinius 2.0 or later.
|
13
|
+
|
14
|
+
## Installation and Configuration
|
15
|
+
|
16
|
+
First, install the gem via Rubygems:
|
17
|
+
|
18
|
+
gem install pushlet
|
19
|
+
|
20
|
+
Then, setup a `config.ru` with the following. Setup an HTTP Auth so that you can control access (we provide a simple helper for you that returns JSON error messages, but you can use any Rack Middleware you like):
|
21
|
+
|
22
|
+
require 'pushlet'
|
23
|
+
|
24
|
+
map '/' do
|
25
|
+
Pushlet::API.http_basic_auth 'YOUR_USERNAME', 'YOUR_PASSWORD'
|
26
|
+
run Pushlet::API
|
27
|
+
end
|
28
|
+
|
29
|
+
Pushlet is **thread optimized**, so use a threaded web server. I highly recommend using [Puma](http://puma.io), which is designed for threaded applications. You can add it by putting `gem 'puma'` in your Gemfile:
|
30
|
+
|
31
|
+
$ bundle exec puma
|
32
|
+
|
33
|
+
You can also use `rackup`:
|
34
|
+
|
35
|
+
$ bundle exec rackup -s puma
|
36
|
+
|
37
|
+
If using Heroku, setup a `Procfile` to make it start with Puma by default:
|
38
|
+
|
39
|
+
web: bundle exec puma -p $PORT
|
40
|
+
|
41
|
+
## Apple Push Notifications (APNS)
|
42
|
+
|
43
|
+
Sending push notifications to apple is a two step process. First, you need to register the application via the `register_application` route:
|
44
|
+
|
45
|
+
$ curl http://127.0.0.1:9292/register_application -F "apns_p12=@/PATH/TO/YOUR/CERT.p12" -F "application_id=YOUR_APPLICATION_NAME"
|
46
|
+
|
47
|
+
Now you can send push notifications via the `send_notification` route, and the socket connection for that application will handle them:
|
48
|
+
|
49
|
+
$ curl http://127.0.0.1:9292/send_notification -d "device_token=YOUR_DEVICE_TOKEN&alert=YOUR+TEXT+MESSAGE&application_id=YOUR_APPLICATION_NAME"
|
50
|
+
|
51
|
+
## Google Cloud Messaging (GCM)
|
52
|
+
|
53
|
+
Google does not require a stateful socket connection, because it uses HTTPS. It's simple enough to implement the protocol that this project doesn't add a lot of value for this, but we've included it for convenience.
|
54
|
+
|
55
|
+
GCM does not have a concept of a "message" field. You are given a data input that can be JSON, and you need to program your application to send a message based on which column you send. For example, if your pre-defined field is `text`, encode with something like `Rack::Utils.escape({text: 'hello'}.to_json)`, and then send this way:
|
56
|
+
|
57
|
+
curl http://127.0.0.1:9292/send_notification -d "device_token=YOUR_GCM_DEVICE_KEY&gcm_api_key=YOUR_GCM_API_KEY&data=%7B%22text%22%3A%22hello%22%7D"
|
58
|
+
|
59
|
+
## POST /application_feedback
|
60
|
+
|
61
|
+
Returns the feedback information from Apple. Not required for GCM.
|
62
|
+
|
63
|
+
### Arguments
|
64
|
+
|
65
|
+
* application\_id - required
|
66
|
+
|
67
|
+
### Example Response
|
68
|
+
|
69
|
+
{
|
70
|
+
"response": [
|
71
|
+
{
|
72
|
+
"device_token": "abcd",
|
73
|
+
"timestamp": "2012-11-27T22:24:07Z"
|
74
|
+
},
|
75
|
+
{
|
76
|
+
"device_token": "efgh",
|
77
|
+
"timestamp": "2012-11-27T22:24:07Z"
|
78
|
+
}
|
79
|
+
]
|
80
|
+
}
|
81
|
+
|
82
|
+
### Possible errors
|
83
|
+
|
84
|
+
{
|
85
|
+
"error": "missing_application_id",
|
86
|
+
"message": "an application_id is required for this call"
|
87
|
+
}
|
88
|
+
|
89
|
+
{
|
90
|
+
"error": "application_not_found",
|
91
|
+
"message": "no application found for the provided application_id"
|
92
|
+
}
|
93
|
+
|
94
|
+
## POST /register_application
|
95
|
+
|
96
|
+
Registers the APNS application with a certificate. This will create a socket connection when the first message is sent, providing a persistent connection to the Apple push servers.
|
97
|
+
|
98
|
+
* application_id - a unique identifier for this application. If none is provided, a UUID-based one will be generated and returned to you, and you will need to store it somewhere to use the connection.
|
99
|
+
* apns\_p12 - Apple certificate in P12 format. Either this or apns\_pem are required.
|
100
|
+
* apns\_pem - Apple certificate in PEM format. Either this or apns\_p12 are required.
|
101
|
+
* apns_mode - toggles whether to use sandbox mode or not. If set to "development", the sandbox is used. Defaults to production.
|
102
|
+
* apns\_p12\_pass - password for P12. Optional.
|
103
|
+
|
104
|
+
### Example Response
|
105
|
+
|
106
|
+
{
|
107
|
+
"application_id": "id_you_supplied_or_uuid"
|
108
|
+
}
|
109
|
+
|
110
|
+
### Possible errors
|
111
|
+
|
112
|
+
{
|
113
|
+
"error": "missing_apns_certificate",
|
114
|
+
"message": "an apns_p12 or apns_pem certificate is required for this call"
|
115
|
+
}
|
116
|
+
|
117
|
+
{
|
118
|
+
"error": "invalid_apns_p12",
|
119
|
+
"message": "could not process apns_p12 certificate, check that it is in p12 format and is valid"
|
120
|
+
}
|
121
|
+
|
122
|
+
|
123
|
+
## POST /send_notification
|
124
|
+
|
125
|
+
Sends a message to either the GCM REST service or the APNS application socket.
|
126
|
+
|
127
|
+
* type - "apns" or "gcm". Required.
|
128
|
+
* application\_id - application\_id provided for registered APNS application. Either this or gcm\_api\_key are required.
|
129
|
+
* gcm\_api\_key - GCM secret key from Google. Either this or application_id are required.
|
130
|
+
* device\_token - the device push token provided by the phone for your application. Required.
|
131
|
+
* data - the generic data JSON payload to send to either APNS or GCM.
|
132
|
+
|
133
|
+
APNS specific:
|
134
|
+
|
135
|
+
* alert - the text to send as a popup message. Optional.
|
136
|
+
* badge - the number to show on the menu icon. Optional.
|
137
|
+
* sound - the application sound file to play on the phone. Optional.
|
138
|
+
* identifier - 4-byte unique identifier for the message. Optional.
|
139
|
+
|
140
|
+
### Example Response (APNS)
|
141
|
+
|
142
|
+
{
|
143
|
+
"response": "ok"
|
144
|
+
}
|
145
|
+
|
146
|
+
### Example Response (GCM)
|
147
|
+
|
148
|
+
{ "multicast_id": 108,
|
149
|
+
"success": 1,
|
150
|
+
"failure": 0,
|
151
|
+
"canonical_ids": 0,
|
152
|
+
"results": [
|
153
|
+
{ "message_id": "1:08" }
|
154
|
+
]
|
155
|
+
}
|
156
|
+
|
157
|
+
### Possible errors
|
158
|
+
|
159
|
+
{
|
160
|
+
"error": "missing_identifier",
|
161
|
+
"message": "an application_id or gcm_api_key are required for this call"
|
162
|
+
}
|
163
|
+
|
164
|
+
{
|
165
|
+
"error": "device_token_required",
|
166
|
+
"message": "device_token is required, which is registed with the application on your phone"
|
167
|
+
}
|
168
|
+
|
169
|
+
{
|
170
|
+
"error": "application_not_found",
|
171
|
+
"message": "no application found for the provided application_id"
|
172
|
+
}
|
173
|
+
|
174
|
+
{
|
175
|
+
"error": "invalid_data_json",
|
176
|
+
"message": "data could not be converted to json: ERROR_MESSAGE_FROM_JSON_PARSER"
|
177
|
+
}
|
178
|
+
|
179
|
+
{
|
180
|
+
"error": "invalid_data_json",
|
181
|
+
"message": "data could not be converted to json: ERROR_MESSAGE_FROM_JSON_PARSER"
|
182
|
+
}
|
183
|
+
|
184
|
+
{
|
185
|
+
"error": "invalid_gcm_credentials",
|
186
|
+
"message": "the provided credentials were rejected by google servers"
|
187
|
+
}
|
data/Rakefile
ADDED
data/SPEC-OLD.md
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
# `POST /register_application`
|
2
|
+
|
3
|
+
TODO: Update spec with updated parameter names.
|
4
|
+
|
5
|
+
TODO: feedback_url not yet implemented.
|
6
|
+
|
7
|
+
* app_id: Required. A unique identifier for this application.
|
8
|
+
* feedback\_url: http://yoursite.com/notification_errors
|
9
|
+
* mode: development or production. Only applies to apple.
|
10
|
+
|
11
|
+
Provide either a P12 certificate or a Google GCM API Key
|
12
|
+
|
13
|
+
* p12: The P12 certificate as a string
|
14
|
+
* gcm\_api\_key: sefsdjfsdlkfj
|
15
|
+
|
16
|
+
## Response
|
17
|
+
|
18
|
+
```
|
19
|
+
{
|
20
|
+
"application_id": "a9b20610187a0130d39614109feae54c"
|
21
|
+
}
|
22
|
+
```
|
23
|
+
|
24
|
+
## Errors
|
25
|
+
|
26
|
+
### /application_feedback
|
27
|
+
|
28
|
+
```
|
29
|
+
{
|
30
|
+
"error": "missing_identifier",
|
31
|
+
"message": "an application\_id or gcm\_api\_key are required for this call"
|
32
|
+
}
|
33
|
+
```
|
34
|
+
|
35
|
+
```
|
36
|
+
{
|
37
|
+
"error": "application\_not\_found",
|
38
|
+
"message": "no application found for the provided application_id"
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
```
|
43
|
+
{
|
44
|
+
"error": "expired_certificate",
|
45
|
+
"message": "The certificate provided has expired. Please generate a new certificate."
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
|
50
|
+
# `/send_notification?type=apns`
|
51
|
+
|
52
|
+
TODO: Explain why the "device_token" parameter implemented but not the plural version.
|
53
|
+
|
54
|
+
* device_tokens - array or string. Apple does not support multiple, so just loop for that case.x
|
55
|
+
* alert - "text message to send or see apple docs for other options"
|
56
|
+
* badge - example: 5
|
57
|
+
* sound - "soundfile.aiff"
|
58
|
+
* identifier - unique key for message. enforce as a 32 bit integer if grocer does not
|
59
|
+
* data - JSON to dump in the root with the aps payload.
|
60
|
+
|
61
|
+
## Errors
|
62
|
+
|
63
|
+
```
|
64
|
+
{
|
65
|
+
"error": "invalid_input",
|
66
|
+
"message": "badge must be an integer"
|
67
|
+
}
|
68
|
+
```
|
69
|
+
|
70
|
+
```
|
71
|
+
{
|
72
|
+
"error": "missing_application",
|
73
|
+
"message": "The application could not be found. Register the application first using POST /register\_application"
|
74
|
+
}
|
75
|
+
```
|
76
|
+
|
77
|
+
### APNS Error Codes
|
78
|
+
|
79
|
+
TODO: Error responses are not sent back from the send_notification endpoint. Why not?
|
80
|
+
|
81
|
+
* 1: processing error
|
82
|
+
* 2: missing device token
|
83
|
+
* 3: missing topic
|
84
|
+
* 4: missing payload
|
85
|
+
* 5: invalid token size
|
86
|
+
* 6: invalid topic size
|
87
|
+
* 7: invalid payload size
|
88
|
+
* 8: invalid token
|
89
|
+
* 255: unknown
|
90
|
+
|
91
|
+
```
|
92
|
+
{
|
93
|
+
"error": "missing_topic",
|
94
|
+
"code": 3,
|
95
|
+
"device_token": "b85a40425e91a68512259b40f480a2a645efd185106dc66a3e73f6c89e95c963",
|
96
|
+
"identifier": "asdjfhd"
|
97
|
+
}
|
98
|
+
```
|
99
|
+
|
100
|
+
## APNS Payload Examples (Internal use only)
|
101
|
+
|
102
|
+
### Send text:
|
103
|
+
```
|
104
|
+
{
|
105
|
+
"aps": {
|
106
|
+
"alert": "hello"
|
107
|
+
}
|
108
|
+
}
|
109
|
+
```
|
110
|
+
|
111
|
+
### Set number on badge:
|
112
|
+
```
|
113
|
+
{
|
114
|
+
"aps": {
|
115
|
+
"badge": 9
|
116
|
+
}
|
117
|
+
}
|
118
|
+
```
|
119
|
+
|
120
|
+
### Sound (plays sound)
|
121
|
+
```
|
122
|
+
{
|
123
|
+
"aps": {
|
124
|
+
"sound": "ping.aiff"
|
125
|
+
}
|
126
|
+
}
|
127
|
+
```
|
128
|
+
|
129
|
+
All three of these parameters can be combined. You can also send a push notification with custom data and leave out the standard "aps" keys.
|
130
|
+
|
131
|
+
Other keys can be sent outside of the aps root. Example:
|
132
|
+
|
133
|
+
```
|
134
|
+
{
|
135
|
+
"aps": {
|
136
|
+
"alert": "hello"
|
137
|
+
},
|
138
|
+
"hello": "kitty"
|
139
|
+
}
|
140
|
+
```
|
141
|
+
|
142
|
+
|
143
|
+
# `/send_notification?type=gcm`
|
144
|
+
|
145
|
+
* app_id OR gcm\_api\_key
|
146
|
+
* device_tokens - json array or string (one device)
|
147
|
+
* data - the notification payload
|
148
|
+
* collapse\_key - if you send multiple pushes with the same collapse\_key, only the last will be delivered when the phone comes back online
|
149
|
+
* delay\_while\_idle - this will send the message when the device comes back from idle. Default: false
|
150
|
+
* time\_to\_live - number of seconds the message should be kept if the device is offline. Default: 4 weeks
|
151
|
+
* dry\_run - default: false
|
152
|
+
|
153
|
+
## Input/Format Errors
|
154
|
+
|
155
|
+
```
|
156
|
+
{
|
157
|
+
"error": "missing_input",
|
158
|
+
"message": "data is required"
|
159
|
+
}
|
160
|
+
```
|
161
|
+
|
162
|
+
```
|
163
|
+
{
|
164
|
+
"error": "missing_input",
|
165
|
+
"message": "device_tokens is required"
|
166
|
+
}
|
167
|
+
```
|
168
|
+
|
169
|
+
```
|
170
|
+
{
|
171
|
+
"error": "missing_input",
|
172
|
+
"message": "app_id OR gcm_api_key is required"
|
173
|
+
}
|
174
|
+
```
|
175
|
+
|
176
|
+
These errors will come from the GCM service
|
177
|
+
|
178
|
+
`{"error": "message_too_big"}`
|
179
|
+
|
180
|
+
`{"error": "invalid_data_key"}`
|
181
|
+
|
182
|
+
`{"error": "invalid_ttl"}`
|
183
|
+
|
184
|
+
## Error format for multiple responses
|
185
|
+
|
186
|
+
If the input format is valid, the service will attempt to send the notification. The results of sending each notification will be returned in "results".
|
187
|
+
|
188
|
+
```
|
189
|
+
{
|
190
|
+
"results": [
|
191
|
+
{"device_token": "fsdsfegr", "error": "invalid_registration"},
|
192
|
+
{"device_token": "dsdfdgfg", "success": true}
|
193
|
+
]
|
194
|
+
}
|
195
|
+
```
|
196
|
+
|
197
|
+
## Other possbile errors from the GCM service
|
198
|
+
|
199
|
+
* 401: "Invalid API Key"
|
200
|
+
* MissingRegistration
|
201
|
+
* InvalidRegistration
|
202
|
+
* MismatchSenderId
|
203
|
+
* NotRegistered
|
204
|
+
* MessageTooBig
|
205
|
+
* InvalidDataKey
|
206
|
+
* InvalidTtl
|
207
|
+
* InternalServerError
|
208
|
+
|
209
|
+
{"type": "MissingRegistration", device_token: "sdfsdf"} <- maybe convert to undercase?
|
210
|
+
|
211
|
+
## GCM Documentation
|
212
|
+
|
213
|
+
* http://developer.android.com/guide/google/gcm/index.html
|
214
|
+
* http://developer.android.com/guide/google/gcm/adv.html
|
215
|
+
|
216
|
+
|
217
|
+
# `apns\_feedback`
|
218
|
+
|
219
|
+
TODO: Update this explanation with a reason why this was implemented as a GET API
|
220
|
+
method instead of as described below.
|
221
|
+
|
222
|
+
The Pushlet service will periodically check the APNS feedback service for errors. If any
|
223
|
+
errors are reported by Apple, Pushlet will deliver a POST to the registered `callback_url`
|
224
|
+
for the application.
|
225
|
+
|
226
|
+
The feedback payload will contain the device token and the timestamp of the error.
|
227
|
+
|
228
|
+
|
229
|
+
```
|
230
|
+
{
|
231
|
+
"app_id": "14002",
|
232
|
+
"type": "feedback",
|
233
|
+
"device_token": "b85a40425e91a68512259b40f480a2a645efd185106dc66a3e73f6c89e95c963",
|
234
|
+
"timestamp": 1352502272
|
235
|
+
}
|
236
|
+
```
|
237
|
+
|
238
|
+
|
data/lib/pushlet.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
libdir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
|
+
require 'json'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'escape'
|
6
|
+
require 'openssl'
|
7
|
+
require 'base64'
|
8
|
+
require 'tempfile'
|
9
|
+
require 'escape'
|
10
|
+
require 'uuid'
|
11
|
+
require 'running'
|
12
|
+
require 'sinatra/jsonapi'
|
13
|
+
require 'grocer'
|
14
|
+
require 'httpclient'
|
15
|
+
|
16
|
+
require 'pushlet/version'
|
17
|
+
require 'pushlet/apns_gateway'
|
18
|
+
require 'pushlet/gateway_pool'
|
19
|
+
require 'pushlet/auth_middleware'
|
20
|
+
require 'pushlet/api'
|
data/lib/pushlet/api.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
module Pushlet
|
2
|
+
class API < Sinatra::Base
|
3
|
+
register Sinatra::JSONAPI
|
4
|
+
set :show_exceptions, false
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def gateway_pool
|
8
|
+
@gateway_pool ||= GatewayPool.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def uuid
|
12
|
+
@uuid ||= UUID.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def http_client
|
16
|
+
@http_client ||= HTTPClient.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def http_basic_auth(user, pass)
|
20
|
+
use AuthMiddleware, user, pass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/application_feedback' do
|
25
|
+
if p[:application_id].nil?
|
26
|
+
api_error :missing_application_id, 'an application_id is required for this call'
|
27
|
+
end
|
28
|
+
|
29
|
+
app = self.class.gateway_pool.get p[:application_id]
|
30
|
+
api_error :application_not_found, 'no application found for the provided application_id' if app.nil?
|
31
|
+
|
32
|
+
api_response response: app.feedback.collect {|fda| {device_token: fda.device_token, timestamp: fda.timestamp.utc.iso8601}}
|
33
|
+
end
|
34
|
+
|
35
|
+
post '/register_application' do
|
36
|
+
id = p[:application_id] || self.class.uuid.generate(:compact)
|
37
|
+
|
38
|
+
if p[:apns_p12].nil? && p[:apns_pem].nil?
|
39
|
+
api_error :missing_apns_certificate, 'an apns_p12 or apns_pem certificate is required for this call'
|
40
|
+
end
|
41
|
+
|
42
|
+
api_error :apns_mode_required, 'specify apns_mode=production or apns_mode=development' unless ['production','development'].include? p[:apns_mode]
|
43
|
+
|
44
|
+
apns_p12 = p[:apns_p12].is_a?(Hash) ? p[:apns_p12][:tempfile].read : p[:apns_p12]
|
45
|
+
apns_pem = p[:apns_pem].is_a?(Hash) ? p[:apns_pem][:tempfile].read : p[:apns_pem]
|
46
|
+
|
47
|
+
begin
|
48
|
+
apns_gateway = APNSGateway.new({
|
49
|
+
mode: p[:apns_mode],
|
50
|
+
p12: apns_p12,
|
51
|
+
pem: apns_pem,
|
52
|
+
p12_pass: p[:apns_p12_pass]
|
53
|
+
})
|
54
|
+
rescue OpenSSL::PKCS12::PKCS12Error
|
55
|
+
api_error :invalid_apns_p12, 'could not process apns_p12 file, check that it is in p12 format and is valid'
|
56
|
+
end
|
57
|
+
|
58
|
+
self.class.gateway_pool.add id, apns_gateway
|
59
|
+
|
60
|
+
api_response application_id: id
|
61
|
+
end
|
62
|
+
|
63
|
+
post '/send_notification' do
|
64
|
+
if p[:application_id].nil? && p[:gcm_api_key].nil?
|
65
|
+
api_error :missing_identifier, 'an application_id or gcm_api_key are required for this call'
|
66
|
+
end
|
67
|
+
|
68
|
+
if p[:device_token].nil? || p[:device_token].empty?
|
69
|
+
api_error :device_token_required, 'device_token is required, which is registed with the application on your phone'
|
70
|
+
end
|
71
|
+
|
72
|
+
if p[:application_id]
|
73
|
+
app = self.class.gateway_pool.get p[:application_id]
|
74
|
+
api_error :application_not_found, 'no application found for the provided application_id' if app.nil?
|
75
|
+
|
76
|
+
api_error :missing_payload, 'at least one of alert, badge, sound or data is required' if p[:alert].nil? && p[:badge].nil? && p[:sound].nil? && p[:data].nil?
|
77
|
+
|
78
|
+
app.push({
|
79
|
+
device_token: p[:device_token],
|
80
|
+
alert: p[:alert],
|
81
|
+
badge: p[:badge],
|
82
|
+
sound: p[:sound],
|
83
|
+
identifier: p[:identifier],
|
84
|
+
custom: p[:data]
|
85
|
+
})
|
86
|
+
|
87
|
+
api_response response: 'ok'
|
88
|
+
else
|
89
|
+
api_error :gcm_api_key_required, 'must provide gcm_api_key' if p[:gcm_api_key].nil?
|
90
|
+
|
91
|
+
payload = {registration_ids: [p[:device_token]]}
|
92
|
+
|
93
|
+
[:collapse_key, :data, :delay_while_idle, :time_to_live, :restricted_package_name, :dry_run].each do |key|
|
94
|
+
if key == :data
|
95
|
+
begin
|
96
|
+
payload[key] = JSON.parse params[:data] if params[:data]
|
97
|
+
rescue JSON::ParserError => e
|
98
|
+
api_error :invalid_data_json, "data could not be converted to json: #{e.message}"
|
99
|
+
end
|
100
|
+
else
|
101
|
+
payload[key] = p[key] unless p[key].nil?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
headers = {
|
106
|
+
'Content-Type' => 'application/json',
|
107
|
+
'Authorization' => "key=#{p[:gcm_api_key]}"
|
108
|
+
}
|
109
|
+
|
110
|
+
resp = self.class.http_client.post 'https://android.googleapis.com/gcm/send', payload.to_json, headers
|
111
|
+
|
112
|
+
if resp.status == 401
|
113
|
+
api_error :invalid_gcm_credentials, 'the provided credentials were rejected by google servers'
|
114
|
+
else
|
115
|
+
api_response response: JSON.parse(resp.body)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def p; params end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Pushlet
|
5
|
+
class APNSGateway
|
6
|
+
def initialize(o={})
|
7
|
+
@mode = o[:mode]
|
8
|
+
@pass = o[:p12_pass]
|
9
|
+
@pem = o[:pem] || self.class.p12_to_pem_text(o[:p12], o[:p12_pass])
|
10
|
+
create_pusher
|
11
|
+
end
|
12
|
+
|
13
|
+
def feedback
|
14
|
+
feedback = Grocer.feedback(
|
15
|
+
certificate: StringIO.new(@pem),
|
16
|
+
passphrase: @pass,
|
17
|
+
gateway: (@mode == "development" ? gateway_sandbox : gateway_production),
|
18
|
+
port: feedback_port,
|
19
|
+
retries: retries
|
20
|
+
)
|
21
|
+
|
22
|
+
failed_delivery_attempts = []
|
23
|
+
|
24
|
+
feedback.each do |fda|
|
25
|
+
failed_delivery_attempts << fda
|
26
|
+
end
|
27
|
+
|
28
|
+
failed_delivery_attempts
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_pusher
|
32
|
+
@pusher = Grocer.pusher(
|
33
|
+
certificate: StringIO.new(@pem),
|
34
|
+
passphrase: @pass,
|
35
|
+
gateway: (@mode == "development" ? gateway_sandbox : gateway_production),
|
36
|
+
port: port,
|
37
|
+
retries: retries
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def gateway_sandbox; 'gateway.sandbox.push.apple.com' end
|
42
|
+
def gateway_production; 'gateway.push.apple.com' end
|
43
|
+
def port; 2195 end
|
44
|
+
def feedback_port; 2196 end
|
45
|
+
def retries; 3 end
|
46
|
+
|
47
|
+
def push(args={})
|
48
|
+
if args[:identifier]
|
49
|
+
identifier = args[:identifier]
|
50
|
+
else
|
51
|
+
# Seriously Apple, a 4 byte identifier? Really guys?
|
52
|
+
identifier = SecureRandom.random_bytes(4)
|
53
|
+
end
|
54
|
+
|
55
|
+
payload = {}
|
56
|
+
|
57
|
+
[:device_token, :alert, :badge, :sound, :expiry, :identifier].each do |a|
|
58
|
+
if a == :expiry
|
59
|
+
payload[a] = args[a] || Time.now + 60*10
|
60
|
+
else
|
61
|
+
payload[a] = args[a] if args[a]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
notification = Grocer::Notification.new payload
|
66
|
+
|
67
|
+
@pusher.push(notification)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.p12_to_pem_text(p12, p12_pass='')
|
71
|
+
p12_pass = '' if p12_pass.nil?
|
72
|
+
|
73
|
+
if Running.jruby?
|
74
|
+
tf_p12 = Tempfile.new 'tf_p12'
|
75
|
+
tf_p12.write p12
|
76
|
+
tf_p12.rewind
|
77
|
+
tf_p12.close
|
78
|
+
|
79
|
+
tf_pem = Tempfile.new 'tf_pem'
|
80
|
+
tf_pem.close
|
81
|
+
|
82
|
+
cmd = Escape.shell_command(['openssl', 'pkcs12', '-in', tf_p12.path, '-out', tf_pem.path,
|
83
|
+
'-nodes', '-clcerts', '-password', 'pass:'+p12_pass]).to_s + ' &> /dev/null'
|
84
|
+
|
85
|
+
begin
|
86
|
+
result = system cmd
|
87
|
+
raise CommandFailError if result.nil? || result == false
|
88
|
+
pem = File.read tf_pem.path
|
89
|
+
ensure
|
90
|
+
tf_pem.unlink
|
91
|
+
tf_p12.unlink
|
92
|
+
end
|
93
|
+
|
94
|
+
pem
|
95
|
+
else
|
96
|
+
pkcs12 = OpenSSL::PKCS12.new p12, p12_pass
|
97
|
+
pkcs12.certificate.to_s + pkcs12.key.to_s
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Pushlet
|
2
|
+
class AuthMiddleware
|
3
|
+
def initialize(app, username, password)
|
4
|
+
@app = app
|
5
|
+
@username = username
|
6
|
+
@password = password
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
return error_json if env['HTTP_AUTHORIZATION'].nil? || env['HTTP_AUTHORIZATION'] == ''
|
11
|
+
|
12
|
+
begin
|
13
|
+
username, password = Base64.strict_decode64(env['HTTP_AUTHORIZATION'].split('Basic ').last).split(':')
|
14
|
+
rescue
|
15
|
+
return error_json
|
16
|
+
end
|
17
|
+
|
18
|
+
return error_json if username != @username || password != @password
|
19
|
+
|
20
|
+
@app.call env
|
21
|
+
end
|
22
|
+
|
23
|
+
def error_json(type='access denied', message='invalid credentials')
|
24
|
+
payload = {error: {type: type, message: message}}.to_json
|
25
|
+
|
26
|
+
[200, {'Content-Type' => 'application/json', 'Content-Length' => payload.length.to_s}, [payload]]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Pushlet
|
2
|
+
class GatewayPool
|
3
|
+
def initialize
|
4
|
+
@mutex = Mutex.new
|
5
|
+
@pool = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(id)
|
9
|
+
@pool[id.to_sym]
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(id, gateway)
|
13
|
+
lock {
|
14
|
+
@pool[id.to_sym] = gateway
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def remove(id)
|
19
|
+
lock {
|
20
|
+
@pool.delete id.to_sym
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def lock(&block)
|
27
|
+
@mutex.synchronize { yield }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/pushlet.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require './lib/pushlet/version.rb'
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'pushlet'
|
4
|
+
s.version = Pushlet.version
|
5
|
+
s.authors = ['Kyle Drake', 'Aaron Parecki']
|
6
|
+
s.email = ['kyledrake@gmail.com', 'aaron@parecki.com']
|
7
|
+
s.homepage = 'http://github.com/geoloqi/pushlet'
|
8
|
+
s.summary = 'Powerful, simple, stateless, multi-threaded push notification service API for APNS and Google GCM'
|
9
|
+
s.description = 'Powerful, simple, stateless, multi-threaded push notification service API for APNS and Google GCM!'
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_paths = %w[lib]
|
13
|
+
s.rubyforge_project = s.name
|
14
|
+
s.required_rubygems_version = '>= 1.3.4'
|
15
|
+
|
16
|
+
s.add_dependency 'json'
|
17
|
+
s.add_dependency 'httpclient'
|
18
|
+
s.add_dependency 'sinatra'
|
19
|
+
s.add_dependency 'sinatra-jsonapi'
|
20
|
+
s.add_dependency 'escape'
|
21
|
+
s.add_dependency 'running'
|
22
|
+
s.add_dependency 'httpclient'
|
23
|
+
s.add_dependency 'uuid'
|
24
|
+
s.add_dependency 'grocer'
|
25
|
+
s.add_dependency 'puma'
|
26
|
+
|
27
|
+
s.add_development_dependency 'rake'
|
28
|
+
s.add_development_dependency 'minitest'
|
29
|
+
s.add_development_dependency 'rack-test'
|
30
|
+
s.add_development_dependency 'webmock'
|
31
|
+
s.add_development_dependency 'yard'
|
32
|
+
s.add_development_dependency 'mocha'
|
33
|
+
end
|
data/test/env.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'mocha/setup'
|
5
|
+
require 'rack/test'
|
6
|
+
require 'webmock'
|
7
|
+
require './lib/pushlet.rb'
|
8
|
+
|
9
|
+
include WebMock::API
|
10
|
+
include Rack::Test::Methods
|
11
|
+
|
12
|
+
# For rack-test
|
13
|
+
def app
|
14
|
+
@app ||= Sinatra.new Pushlet::API do
|
15
|
+
enable :raise_errors
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def resp
|
20
|
+
JSON.parse last_response.body, symbolize_names: true
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICKzCCAdWgAwIBAgIJAKDDs5zNJQVeMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
3
|
+
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
4
|
+
aWRnaXRzIFB0eSBMdGQwHhcNMTIwMzI5MTg1MDEwWhcNMTIwNDI4MTg1MDEwWjBF
|
5
|
+
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
6
|
+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANlO
|
7
|
+
UQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64gmRucz+DKmxfYr5kq
|
8
|
+
+XT6GIpEdw0wrBGsKlUCAwEAAaOBpzCBpDAdBgNVHQ4EFgQUVkHor75NOFIKInJN
|
9
|
+
VUpmqRbWfsgwdQYDVR0jBG4wbIAUVkHor75NOFIKInJNVUpmqRbWfsihSaRHMEUx
|
10
|
+
CzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl
|
11
|
+
cm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCgw7OczSUFXjAMBgNVHRMEBTADAQH/MA0G
|
12
|
+
CSqGSIb3DQEBBQUAA0EAzPoxFj8j1uUIEGGsViUXkAH9/uoZuCBy9PtHEJcjSkya
|
13
|
+
TlKHfzEaEsA8pfmHax3gMgYcIbbWuYVR/HTUEnog9Q==
|
14
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,9 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIBOwIBAAJBANlOUQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64g
|
3
|
+
mRucz+DKmxfYr5kq+XT6GIpEdw0wrBGsKlUCAwEAAQJBAISneW67Wrrl/WQXfWri
|
4
|
+
/IBeWvvDo9nEK6gwLdCrm3+UZ18Tr9scjdvWsRbHy6fiIYIIZY0KA0mIx1TlPxbD
|
5
|
+
ANUCIQDuc5KeAJZ5bVDWK/Qca6/kAtx6jKH0Q8A4M4vhEynHwwIhAOlMXJqUyt4p
|
6
|
+
As+jG3jRz820QQW5CkjQ4rNCvDgnEjwHAiArzn+5F1KNrE+ViS2nqwD9Wqk2um9m
|
7
|
+
eKvvp0ijaOncEQIgG7dwwQSwXVht9xEfsGjs0Tl7CB0FtcTrSfTBu8IYjn0CIQCh
|
8
|
+
tJEYZA1szHnTdHA2V8FvQ9ZDC3KW6pBvKDQJkqPIaw==
|
9
|
+
-----END RSA PRIVATE KEY-----
|
Binary file
|
@@ -0,0 +1,23 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIBOwIBAAJBANlOUQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64g
|
3
|
+
mRucz+DKmxfYr5kq+XT6GIpEdw0wrBGsKlUCAwEAAQJBAISneW67Wrrl/WQXfWri
|
4
|
+
/IBeWvvDo9nEK6gwLdCrm3+UZ18Tr9scjdvWsRbHy6fiIYIIZY0KA0mIx1TlPxbD
|
5
|
+
ANUCIQDuc5KeAJZ5bVDWK/Qca6/kAtx6jKH0Q8A4M4vhEynHwwIhAOlMXJqUyt4p
|
6
|
+
As+jG3jRz820QQW5CkjQ4rNCvDgnEjwHAiArzn+5F1KNrE+ViS2nqwD9Wqk2um9m
|
7
|
+
eKvvp0ijaOncEQIgG7dwwQSwXVht9xEfsGjs0Tl7CB0FtcTrSfTBu8IYjn0CIQCh
|
8
|
+
tJEYZA1szHnTdHA2V8FvQ9ZDC3KW6pBvKDQJkqPIaw==
|
9
|
+
-----END RSA PRIVATE KEY-----
|
10
|
+
-----BEGIN CERTIFICATE-----
|
11
|
+
MIICKzCCAdWgAwIBAgIJAKDDs5zNJQVeMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
12
|
+
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
13
|
+
aWRnaXRzIFB0eSBMdGQwHhcNMTIwMzI5MTg1MDEwWhcNMTIwNDI4MTg1MDEwWjBF
|
14
|
+
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
15
|
+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANlO
|
16
|
+
UQLaP+NWbniFq2I0yW7AOyOjrKuEBj9i4cVOR1EWN2wnf64gmRucz+DKmxfYr5kq
|
17
|
+
+XT6GIpEdw0wrBGsKlUCAwEAAaOBpzCBpDAdBgNVHQ4EFgQUVkHor75NOFIKInJN
|
18
|
+
VUpmqRbWfsgwdQYDVR0jBG4wbIAUVkHor75NOFIKInJNVUpmqRbWfsihSaRHMEUx
|
19
|
+
CzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl
|
20
|
+
cm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCgw7OczSUFXjAMBgNVHRMEBTADAQH/MA0G
|
21
|
+
CSqGSIb3DQEBBQUAA0EAzPoxFj8j1uUIEGGsViUXkAH9/uoZuCBy9PtHEJcjSkya
|
22
|
+
TlKHfzEaEsA8pfmHax3gMgYcIbbWuYVR/HTUEnog9Q==
|
23
|
+
-----END CERTIFICATE-----
|
Binary file
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require './test/env.rb'
|
2
|
+
|
3
|
+
describe 'send gcm push' do
|
4
|
+
it 'fails without key' do
|
5
|
+
post '/send_notification'
|
6
|
+
resp[:error][:type].must_equal 'missing_identifier'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'fails without one or more device tokens' do
|
10
|
+
post '/send_notification', gcm_api_key: 'ABCD', data: {text: 'hello'}
|
11
|
+
resp[:error][:type].must_equal 'device_token_required'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should fail gracefully for invalid key' do
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'works' do
|
19
|
+
|
20
|
+
stub_request(:post, "https://android.googleapis.com/gcm/send").
|
21
|
+
with(:body => "{\"registration_ids\":[\"1234\"],\"data\":{\"text\":\"hello\"},\"collapse_key\":null,\"delay_while_idle\":null,\"time_to_live\":null,\"dry_run\":null}",
|
22
|
+
:headers => {'Authorization'=>'key=ABCD', 'Content-Type'=>'application/json'}).
|
23
|
+
to_return(:status => 200, :body => %{{"multicast_id":6782339717028231855,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"InvalidRegistration"}]}})
|
24
|
+
|
25
|
+
post '/send_notification', gcm_api_key: 'ABCD', data: {text: 'hello'}, device_tokens: ['1234']
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
# binding.pry
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require './test/env.rb'
|
2
|
+
|
3
|
+
describe 'registering apns applications' do
|
4
|
+
it 'should fail without input' do
|
5
|
+
post '/register_application'
|
6
|
+
resp[:application_id].must_be_nil
|
7
|
+
resp[:error][:type].must_equal 'missing_apns_certificate'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'fails with bad cert' do
|
11
|
+
post '/register_application', apns_p12: File.read('./test/files/example.pem'), apns_mode: 'production'
|
12
|
+
resp[:application_id].must_be_nil
|
13
|
+
resp[:error][:type].must_equal 'invalid_apns_p12'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'works' do
|
17
|
+
post '/register_application', apns_p12: File.read('./test/files/example.p12'), apns_mode: 'production'
|
18
|
+
resp[:application_id].wont_be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'works with provided id' do
|
22
|
+
post '/register_application', apns_p12: File.read('./test/files/example.p12'), application_id: 'specialapp', apns_mode: 'production'
|
23
|
+
resp[:application_id].must_equal 'specialapp'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'fails for passworded p12 with bad pass' do
|
27
|
+
post '/register_application', apns_p12: File.read('./test/files/example_with_pass.p12'), apns_p12_pass: 'fail', apns_mode: 'production'
|
28
|
+
resp[:application_id].must_be_nil
|
29
|
+
resp[:error][:type].must_equal 'invalid_apns_p12'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'works with pass' do
|
33
|
+
post '/register_application', apns_p12: File.read('./test/files/example_with_pass.p12'), apns_p12_pass: 'test', apns_mode: 'production'
|
34
|
+
resp[:application_id].wont_be_nil
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'works with pem instead of p12' do
|
38
|
+
post '/register_application', apns_pem: File.read('./test/files/example.pem'), apns_mode: 'production'
|
39
|
+
resp[:application_id].wont_be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'fails if no apns_mode provided' do
|
43
|
+
post '/register_application', apns_pem: File.read('./test/files/example.pem')
|
44
|
+
resp[:error][:type].must_equal 'apns_mode_required'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'fails on invalid apns_mode' do
|
48
|
+
post '/register_application', apns_pem: File.read('./test/files/example.pem'), apns_mode: 'whatever'
|
49
|
+
resp[:error][:type].must_equal 'apns_mode_required'
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,326 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pushlet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kyle Drake
|
9
|
+
- Aaron Parecki
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-12-04 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: json
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: httpclient
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sinatra
|
49
|
+
requirement: !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: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: sinatra-jsonapi
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: escape
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: running
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: httpclient
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: uuid
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :runtime
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
- !ruby/object:Gem::Dependency
|
144
|
+
name: grocer
|
145
|
+
requirement: !ruby/object:Gem::Requirement
|
146
|
+
none: false
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
type: :runtime
|
152
|
+
prerelease: false
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ! '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: puma
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
none: false
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
type: :runtime
|
168
|
+
prerelease: false
|
169
|
+
version_requirements: !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ! '>='
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
- !ruby/object:Gem::Dependency
|
176
|
+
name: rake
|
177
|
+
requirement: !ruby/object:Gem::Requirement
|
178
|
+
none: false
|
179
|
+
requirements:
|
180
|
+
- - ! '>='
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
type: :development
|
184
|
+
prerelease: false
|
185
|
+
version_requirements: !ruby/object:Gem::Requirement
|
186
|
+
none: false
|
187
|
+
requirements:
|
188
|
+
- - ! '>='
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
- !ruby/object:Gem::Dependency
|
192
|
+
name: minitest
|
193
|
+
requirement: !ruby/object:Gem::Requirement
|
194
|
+
none: false
|
195
|
+
requirements:
|
196
|
+
- - ! '>='
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: '0'
|
199
|
+
type: :development
|
200
|
+
prerelease: false
|
201
|
+
version_requirements: !ruby/object:Gem::Requirement
|
202
|
+
none: false
|
203
|
+
requirements:
|
204
|
+
- - ! '>='
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
- !ruby/object:Gem::Dependency
|
208
|
+
name: rack-test
|
209
|
+
requirement: !ruby/object:Gem::Requirement
|
210
|
+
none: false
|
211
|
+
requirements:
|
212
|
+
- - ! '>='
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: '0'
|
215
|
+
type: :development
|
216
|
+
prerelease: false
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
218
|
+
none: false
|
219
|
+
requirements:
|
220
|
+
- - ! '>='
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: webmock
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
none: false
|
227
|
+
requirements:
|
228
|
+
- - ! '>='
|
229
|
+
- !ruby/object:Gem::Version
|
230
|
+
version: '0'
|
231
|
+
type: :development
|
232
|
+
prerelease: false
|
233
|
+
version_requirements: !ruby/object:Gem::Requirement
|
234
|
+
none: false
|
235
|
+
requirements:
|
236
|
+
- - ! '>='
|
237
|
+
- !ruby/object:Gem::Version
|
238
|
+
version: '0'
|
239
|
+
- !ruby/object:Gem::Dependency
|
240
|
+
name: yard
|
241
|
+
requirement: !ruby/object:Gem::Requirement
|
242
|
+
none: false
|
243
|
+
requirements:
|
244
|
+
- - ! '>='
|
245
|
+
- !ruby/object:Gem::Version
|
246
|
+
version: '0'
|
247
|
+
type: :development
|
248
|
+
prerelease: false
|
249
|
+
version_requirements: !ruby/object:Gem::Requirement
|
250
|
+
none: false
|
251
|
+
requirements:
|
252
|
+
- - ! '>='
|
253
|
+
- !ruby/object:Gem::Version
|
254
|
+
version: '0'
|
255
|
+
- !ruby/object:Gem::Dependency
|
256
|
+
name: mocha
|
257
|
+
requirement: !ruby/object:Gem::Requirement
|
258
|
+
none: false
|
259
|
+
requirements:
|
260
|
+
- - ! '>='
|
261
|
+
- !ruby/object:Gem::Version
|
262
|
+
version: '0'
|
263
|
+
type: :development
|
264
|
+
prerelease: false
|
265
|
+
version_requirements: !ruby/object:Gem::Requirement
|
266
|
+
none: false
|
267
|
+
requirements:
|
268
|
+
- - ! '>='
|
269
|
+
- !ruby/object:Gem::Version
|
270
|
+
version: '0'
|
271
|
+
description: Powerful, simple, stateless, multi-threaded push notification service
|
272
|
+
API for APNS and Google GCM!
|
273
|
+
email:
|
274
|
+
- kyledrake@gmail.com
|
275
|
+
- aaron@parecki.com
|
276
|
+
executables: []
|
277
|
+
extensions: []
|
278
|
+
extra_rdoc_files: []
|
279
|
+
files:
|
280
|
+
- .gitignore
|
281
|
+
- Gemfile
|
282
|
+
- README.md
|
283
|
+
- Rakefile
|
284
|
+
- SPEC-OLD.md
|
285
|
+
- lib/pushlet.rb
|
286
|
+
- lib/pushlet/api.rb
|
287
|
+
- lib/pushlet/apns_gateway.rb
|
288
|
+
- lib/pushlet/auth_middleware.rb
|
289
|
+
- lib/pushlet/gateway_pool.rb
|
290
|
+
- lib/pushlet/version.rb
|
291
|
+
- pushlet.gemspec
|
292
|
+
- test/env.rb
|
293
|
+
- test/files/example.cer
|
294
|
+
- test/files/example.key
|
295
|
+
- test/files/example.p12
|
296
|
+
- test/files/example.pem
|
297
|
+
- test/files/example_with_pass.p12
|
298
|
+
- test/gcm_push_test.rb
|
299
|
+
- test/register_application_test.rb
|
300
|
+
homepage: http://github.com/geoloqi/pushlet
|
301
|
+
licenses: []
|
302
|
+
post_install_message:
|
303
|
+
rdoc_options: []
|
304
|
+
require_paths:
|
305
|
+
- lib
|
306
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
307
|
+
none: false
|
308
|
+
requirements:
|
309
|
+
- - ! '>='
|
310
|
+
- !ruby/object:Gem::Version
|
311
|
+
version: '0'
|
312
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
313
|
+
none: false
|
314
|
+
requirements:
|
315
|
+
- - ! '>='
|
316
|
+
- !ruby/object:Gem::Version
|
317
|
+
version: 1.3.4
|
318
|
+
requirements: []
|
319
|
+
rubyforge_project: pushlet
|
320
|
+
rubygems_version: 1.8.24
|
321
|
+
signing_key:
|
322
|
+
specification_version: 3
|
323
|
+
summary: Powerful, simple, stateless, multi-threaded push notification service API
|
324
|
+
for APNS and Google GCM
|
325
|
+
test_files: []
|
326
|
+
has_rdoc:
|