pushlet 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|