UNPKG

14.9 kBtext/coffeescriptView Raw
1_ = require 'lodash'
2url = require 'url'
3debug = require('debug')('meshblu-http')
4stableStringify = require 'json-stable-stringify'
5
6class MeshbluHttp
7 @SUBSCRIPTION_TYPES = [
8 'broadcast'
9 'sent'
10 'received'
11 'config'
12 "broadcast.received"
13 "broadcast.sent"
14 "configure.received"
15 "configure.sent"
16 "message.received"
17 "message.sent"
18 ]
19
20 constructor: (options={}, @dependencies={}) ->
21 options = _.cloneDeep options
22 {
23 uuid
24 token
25 hostname
26 port
27 protocol
28 domain
29 service
30 secure
31 resolveSrv
32 auth
33 @raw
34 @keepAlive
35 @gzip
36 @timeout
37 @serviceName
38 } = options
39 @keepAlive ?= true
40 @gzip ?= true
41
42 throw new Error 'a uuid is provided but the token is not' if uuid? && !token?
43 throw new Error 'a token is provided but the uuid is not' if token? && !uuid?
44
45 auth ?= {username: uuid, password: token} if uuid? && token?
46
47 {request, @MeshbluRequest, @NodeRSA} = @dependencies
48 @MeshbluRequest ?= require './meshblu-request'
49 @NodeRSA ?= require 'node-rsa'
50 @request = @_buildRequest {request, protocol, hostname, port, service, domain, secure, resolveSrv, auth}
51
52 authenticate: (callback) =>
53 options = @_getDefaultRequestOptions()
54
55 @request.post "/authenticate", options, (error, response, body) =>
56 debug "authenticate", error, body
57 @_handleResponse {error, response, body}, callback
58
59 createHook: (uuid, type, url, callback) =>
60 error = new Error "Hook type not supported. supported types are: #{MeshbluHttp.SUBSCRIPTION_TYPES.join ', '}"
61 return callback error unless type in MeshbluHttp.SUBSCRIPTION_TYPES
62
63 updateRequest =
64 $addToSet:
65 "meshblu.forwarders.#{type}":
66 type: 'webhook'
67 url: url
68 method: 'POST',
69 generateAndForwardMeshbluCredentials: true
70
71 @updateDangerously(uuid, updateRequest, callback)
72
73 createSubscription: ({subscriberUuid, emitterUuid, type}, callback) =>
74 url = @_subscriptionUrl {subscriberUuid, emitterUuid, type}
75 requestOptions = @_getDefaultRequestOptions()
76
77 @request.post url, requestOptions, (error, response, body) =>
78 @_handleResponse {error, response, body}, callback
79
80 deleteSubscription: (options, callback) =>
81 url = @_subscriptionUrl options
82 requestOptions = @_getDefaultRequestOptions()
83
84 @request.delete url, requestOptions, (error, response, body) =>
85 @_handleResponse {error, response, body}, callback
86
87 device: (uuid, rest...) =>
88 [callback] = rest
89 [metadata, callback] = rest if _.isPlainObject callback
90 metadata ?= {}
91 @_device uuid, metadata, callback
92
93 _device: (uuid, metadata, callback=->) =>
94 options = @_getDefaultRequestOptions()
95 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
96
97 @request.get "/v2/devices/#{uuid}", options, (error, response, body) =>
98 debug "device", error, body
99 @_handleResponse {error, response, body}, callback
100
101 devices: (query={}, rest...) =>
102 [callback] = rest
103 [metadata, callback] = rest if _.isPlainObject callback
104 metadata ?= {}
105
106 @_devices query, metadata, callback
107
108 findAndUpdate: (uuid, params, rest...) =>
109 [callback] = rest
110 [metadata, callback] = rest if _.isPlainObject callback
111 metadata ?= {}
112
113 options = @_getDefaultRequestOptions()
114 options.json = params
115 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
116
117 @request.put "/v2/devices/#{uuid}/find-and-update", options, (error, response, body) =>
118 debug "update", error, body
119 @_handleResponse {error, response, body}, callback
120
121 generateAndStoreToken: (uuid, rest...) =>
122 [callback] = rest
123 [metadata, callback] = rest if _.isPlainObject callback
124 metadata ?= {}
125 params = true
126 @_generateAndStoreToken uuid, params, metadata, callback
127
128 generateAndStoreTokenWithOptions: (uuid, params, rest...) =>
129 [callback] = rest
130 [metadata, callback] = rest if _.isPlainObject callback
131 metadata ?= {}
132
133 @_generateAndStoreToken uuid, params, metadata, callback
134
135 _generateAndStoreToken: (uuid, params, metadata, callback=->) =>
136 options = @_getDefaultRequestOptions()
137 options.json = params
138 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
139 @request.post "/devices/#{uuid}/tokens", options, (error, response, body) =>
140 debug "generateAndStoreToken", error, body
141 @_handleResponse {error, response, body}, callback
142
143 generateKeyPair: =>
144 key = new @NodeRSA()
145 key.generateKeyPair()
146
147 privateKey: key.exportKey('private'), publicKey: key.exportKey('public')
148
149 healthcheck: (callback=->) =>
150 options = @_getDefaultRequestOptions()
151 @request.get '/healthcheck', options, (error, response) =>
152 return callback error if error?
153 healthy = response.statusCode == 200
154 return callback null, healthy, response.statusCode
155
156 message: (message, rest...) =>
157 [callback] = rest
158 [metadata, callback] = rest if _.isPlainObject callback
159 metadata ?= {}
160
161 @_message message, metadata, callback
162
163 mydevices: (query={}, callback=->) =>
164 options = @_getDefaultRequestOptions()
165 options.qs = query
166
167 @request.get "/mydevices", options, (error, response, body) =>
168 debug "mydevices", error, body
169 @_handleResponse {error, response, body}, callback
170
171 publicKey: (deviceUuid, callback=->) =>
172 options = @_getDefaultRequestOptions()
173
174 @request.get "/devices/#{deviceUuid}/publickey", options, (error, response, body) =>
175 debug "publicKey", error, body
176 @_handleResponse {error, response, body}, callback
177
178 register: (device, callback=->) =>
179 options = @_getDefaultRequestOptions()
180 options.json = device
181
182 @request.post "/devices", options, (error, response, body={}) =>
183 debug "register", error, body
184 @_handleResponse {error, response, body}, callback
185
186 resetToken: (deviceUuid, callback=->) =>
187 options = @_getDefaultRequestOptions()
188 url = "/devices/#{deviceUuid}/token"
189 @request.post url, options, (error, response, body) =>
190 @_handleResponse {error, response, body}, callback
191
192 revokeToken: (deviceUuid, deviceToken, callback=->) =>
193 options = @_getDefaultRequestOptions()
194
195 @request.delete "/devices/#{deviceUuid}/tokens/#{deviceToken}", options, (error, response, body={}) =>
196 debug "revokeToken", error, body
197 @_handleResponse {error, response, body}, callback
198
199 revokeTokenByQuery: (deviceUuid, query, callback=->) =>
200 options = @_getDefaultRequestOptions()
201 options.qs = query
202
203 @request.delete "/devices/#{deviceUuid}/tokens", options, (error, response, body={}) =>
204 debug "revokeToken", error, body
205 @_handleResponse {error, response, body}, callback
206
207 search: (query, metadata, callback) =>
208 options = @_getDefaultRequestOptions()
209 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
210 options.json = query
211 @request.post "/search/devices", options, (error, response, body) =>
212 @_handleResponse {error, response, body}, callback
213
214 searchTokens: (query, metadata, callback) =>
215 options = @_getDefaultRequestOptions()
216 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
217 options.json = query
218 @request.post "/search/tokens", options, (error, response, body) =>
219 @_handleResponse {error, response, body}, callback
220
221 setPrivateKey: (privateKey) =>
222 @privateKey = new @NodeRSA privateKey
223
224 sign: (data) =>
225 @privateKey.sign(stableStringify(data)).toString('base64')
226
227 subscriptions: (uuid, rest...) =>
228 [callback] = rest
229 [metadata, callback] = rest if _.isPlainObject callback
230 metadata ?= {}
231 @_subscriptions uuid, metadata, callback
232
233 unregister: (device, callback=->) =>
234 options = @_getDefaultRequestOptions()
235
236 @request.delete "/devices/#{device.uuid}", options, (error, response, body) =>
237 debug "unregister", error, body
238 @_handleResponse {error, response, body}, callback
239
240 update: (uuid, params, rest...) =>
241 [callback] = rest
242 [metadata, callback] = rest if _.isPlainObject callback
243 metadata ?= {}
244
245 @_update uuid, params, metadata, callback
246
247 updateDangerously: (uuid, params, rest...) =>
248 [callback] = rest
249 [metadata, callback] = rest if _.isPlainObject callback
250 metadata ?= {}
251
252 @_updateDangerously uuid, params, metadata, callback
253
254 _updateDangerously: (uuid, params, metadata, callback=->) =>
255 options = @_getDefaultRequestOptions()
256 options.json = params
257 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
258
259 @request.put "/v2/devices/#{uuid}", options, (error, response, body) =>
260 debug "updated", uuid, params
261 debug "update", error, body
262 @_handleResponse {error, response, body}, callback
263
264 verify: (message, signature) =>
265 @privateKey.verify stableStringify(message), signature, 'utf8', 'base64'
266
267 whoami: (callback=->) =>
268 options = @_getDefaultRequestOptions()
269
270 @request.get "/v2/whoami", options, (error, response, body) =>
271 debug "whoami", error, body
272 @_handleResponse {error, response, body}, callback
273
274 _assertNoSrv: ({service, domain, secure}) =>
275 throw new Error('domain property only applies when resolveSrv is true') if domain?
276 throw new Error('service property only applies when resolveSrv is true') if service?
277 throw new Error('secure property only applies when resolveSrv is true') if secure?
278
279 _assertNoUrl: ({protocol, hostname, port}) =>
280 throw new Error('protocol property only applies when resolveSrv is false') if protocol?
281 throw new Error('hostname property only applies when resolveSrv is false') if hostname?
282 throw new Error('port property only applies when resolveSrv is false') if port?
283
284 _buildRequest: ({request, protocol, hostname, port, service, domain, secure, resolveSrv, auth}) =>
285 return request if request?
286
287 return @_buildSrvRequest({protocol, hostname, port, service, domain, secure, auth}) if resolveSrv
288 return @_buildUrlRequest({protocol, hostname, port, service, domain, secure, auth})
289
290 _buildSrvRequest: ({protocol, hostname, port, service, domain, secure, auth}) =>
291 @_assertNoUrl({protocol, hostname, port})
292 service ?= 'meshblu'
293 domain ?= 'octoblu.com'
294 secure ?= true
295 request = {}
296 request.auth = auth if auth?
297 request.timeout = @timeout if @timeout?
298 return new @MeshbluRequest {resolveSrv: true, service, domain, secure, request}
299
300 _buildUrlRequest: ({protocol, hostname, port, service, domain, secure, auth}) =>
301 @_assertNoSrv({service, domain, secure})
302 protocol ?= 'https'
303 hostname ?= 'meshblu.octoblu.com'
304 port ?= 443
305 try port = parseInt port
306 request = {}
307 request.auth = auth if auth?
308 request.timeout = @timeout if @timeout?
309 return new @MeshbluRequest {resolveSrv: false, protocol, hostname, port, request }
310
311 _devices: (query, metadata, callback=->) =>
312 options = @_getDefaultRequestOptions()
313 options.qs = query
314
315 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
316
317 @request.get "/v2/devices", options, (error, response, body) =>
318 debug "devices", error, body
319 @_handleResponse {error, response, body}, callback
320
321 _getDefaultHeaders: =>
322 return unless @serviceName?
323 return {
324 'x-meshblu-service-name': @serviceName
325 }
326
327 _getDefaultRequestOptions: =>
328 headers = @_getDefaultHeaders()
329 options = {
330 json: true
331 forever: @keepAlive
332 gzip: @gzip
333 }
334 options.headers = headers if headers?
335 return options
336
337 _getMetadataHeaders: (metadata) =>
338 _.transform metadata, (newMetadata, value, key) =>
339 kebabKey = _.kebabCase key
340 newMetadata["x-meshblu-#{kebabKey}"] = @_possiblySerializeHeaderValue value
341 return true
342 , {}
343
344 _getRawRequestOptions: =>
345 headers = @_getDefaultHeaders() ? {}
346 headers['content-type'] = 'application/json'
347 return {
348 json: false
349 headers
350 }
351
352 _handleError: ({message,code,response}, callback) =>
353 message ?= 'Unknown Error Occurred'
354 response = response?.toJSON?()
355 debug 'handling error', JSON.stringify({ message, code, response }, null, 2)
356 error = @_userError code, message, response
357 callback error
358
359 _handleResponse: ({error, response, body}, callback) =>
360 return @_handleError { message: error.message, code: error?.code, response }, callback if error?
361 code = response?.statusCode
362
363 if response?.headers?['x-meshblu-error']?
364 error = JSON.parse response.headers['x-meshblu-error']
365 return @_handleError { message: error.message, response, code }, callback
366
367 if body?.error?
368 return @_handleError { message: body.error, response, code }, callback
369
370 if code >= 400
371 message = body if _.isString body
372 message ?= "Invalid Response Code #{code}"
373 return @_handleError { message, response, code }, callback
374
375 callback null, body
376
377
378 _message: (message, metadata, callback=->) =>
379 if @raw
380 options = @_getRawRequestOptions()
381 options.body = message
382 else
383 options = @_getDefaultRequestOptions()
384 options.json = message
385
386 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
387
388 debug 'POST', "/messages", options
389
390 @request.post "/messages", options, (error, response, body) =>
391 debug "message", error, body
392 @_handleResponse {error, response, body}, callback
393
394 # because request doesn't serialize arrays correctly for headers.
395 _possiblySerializeHeaderValue: (value) =>
396 return value if _.isString value
397 return value if _.isBoolean value
398 return value if _.isNumber value
399 return JSON.stringify value
400
401 _subscriptions: (uuid, metadata, callback=->) =>
402 options = @_getDefaultRequestOptions()
403 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
404
405 @request.get "/v2/devices/#{uuid}/subscriptions", options, (error, response, body) =>
406 debug "subscriptions", error, body
407 @_handleResponse {error, response, body}, callback
408
409 _subscriptionUrl: (options) =>
410 {subscriberUuid, emitterUuid, type} = options
411 "/v2/devices/#{subscriberUuid}/subscriptions/#{emitterUuid}/#{type}"
412
413 _update: (uuid, params, metadata, callback=->) =>
414 options = @_getDefaultRequestOptions()
415 options.json = params
416 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
417
418 @request.patch "/v2/devices/#{uuid}", options, (error, response, body) =>
419 debug "update", error, body
420 @_handleResponse {error, response, body}, callback
421
422 _userError: (code, message, response) =>
423 error = new Error message
424 error.code = code
425 error.response = response if response?
426 error
427
428module.exports = MeshbluHttp