UNPKG

15.5 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}, rest...) =>
74 [callback] = rest
75 [metadata, callback] = rest if _.isPlainObject callback
76
77 url = @_subscriptionUrl {subscriberUuid, emitterUuid, type}
78 options = @_getDefaultRequestOptions()
79 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
80
81 @request.post url, options, (error, response, body) =>
82 @_handleResponse {error, response, body}, callback
83
84 deleteSubscription: ({subscriberUuid, emitterUuid, type}, rest...) =>
85 [callback] = rest
86 [metadata, callback] = rest if _.isPlainObject callback
87
88 url = @_subscriptionUrl {subscriberUuid, emitterUuid, type}
89 options = @_getDefaultRequestOptions()
90 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
91
92 @request.delete url, options, (error, response, body) =>
93 @_handleResponse {error, response, body}, callback
94
95 device: (uuid, rest...) =>
96 [callback] = rest
97 [metadata, callback] = rest if _.isPlainObject callback
98 metadata ?= {}
99 @_device uuid, metadata, callback
100
101 _device: (uuid, metadata, callback=->) =>
102 options = @_getDefaultRequestOptions()
103 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
104
105 @request.get "/v2/devices/#{uuid}", options, (error, response, body) =>
106 debug "device", error, body
107 @_handleResponse {error, response, body}, callback
108
109 devices: (query={}, rest...) =>
110 [callback] = rest
111 [metadata, callback] = rest if _.isPlainObject callback
112 metadata ?= {}
113
114 @_devices query, metadata, callback
115
116 findAndUpdate: (uuid, params, rest...) =>
117 [callback] = rest
118 [metadata, callback] = rest if _.isPlainObject callback
119 metadata ?= {}
120
121 options = @_getDefaultRequestOptions()
122 options.json = params
123 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
124
125 @request.put "/v2/devices/#{uuid}/find-and-update", options, (error, response, body) =>
126 debug "update", error, body
127 @_handleResponse {error, response, body}, callback
128
129 generateAndStoreToken: (uuid, rest...) =>
130 [callback] = rest
131 [metadata, callback] = rest if _.isPlainObject callback
132 metadata ?= {}
133 params = true
134 @_generateAndStoreToken uuid, params, metadata, callback
135
136 generateAndStoreTokenWithOptions: (uuid, params, rest...) =>
137 [callback] = rest
138 [metadata, callback] = rest if _.isPlainObject callback
139 metadata ?= {}
140
141 @_generateAndStoreToken uuid, params, metadata, callback
142
143 _generateAndStoreToken: (uuid, params, metadata, callback=->) =>
144 options = @_getDefaultRequestOptions()
145 options.json = params
146 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
147 @request.post "/devices/#{uuid}/tokens", options, (error, response, body) =>
148 debug "generateAndStoreToken", error, body
149 @_handleResponse {error, response, body}, callback
150
151 generateKeyPair: =>
152 key = new @NodeRSA()
153 key.generateKeyPair()
154
155 privateKey: key.exportKey('private'), publicKey: key.exportKey('public')
156
157 getServerPublicKey: (callback) =>
158 options = @_getDefaultRequestOptions()
159 @request.get '/publickey', options, (error, response, body) =>
160 body = _.get body, 'publicKey'
161 @_handleResponse {error, response, body}, callback
162
163 healthcheck: (callback=->) =>
164 options = @_getDefaultRequestOptions()
165 @request.get '/healthcheck', options, (error, response) =>
166 return callback error if error?
167 healthy = response.statusCode == 200
168 return callback null, healthy, response.statusCode
169
170 message: (message, rest...) =>
171 [callback] = rest
172 [metadata, callback] = rest if _.isPlainObject callback
173 metadata ?= {}
174
175 @_message message, metadata, callback
176
177 mydevices: (query={}, callback=->) =>
178 options = @_getDefaultRequestOptions()
179 options.qs = query
180
181 @request.get "/mydevices", options, (error, response, body) =>
182 debug "mydevices", error, body
183 @_handleResponse {error, response, body}, callback
184
185 publicKey: (deviceUuid, callback=->) =>
186 options = @_getDefaultRequestOptions()
187
188 @request.get "/devices/#{deviceUuid}/publickey", options, (error, response, body) =>
189 debug "publicKey", error, body
190 @_handleResponse {error, response, body}, callback
191
192 register: (device, callback=->) =>
193 options = @_getDefaultRequestOptions()
194 options.json = device
195
196 @request.post "/devices", options, (error, response, body={}) =>
197 debug "register", error, body
198 @_handleResponse {error, response, body}, callback
199
200 resetToken: (deviceUuid, callback=->) =>
201 options = @_getDefaultRequestOptions()
202 url = "/devices/#{deviceUuid}/token"
203 @request.post url, options, (error, response, body) =>
204 @_handleResponse {error, response, body}, callback
205
206 revokeToken: (deviceUuid, deviceToken, callback=->) =>
207 options = @_getDefaultRequestOptions()
208
209 @request.delete "/devices/#{deviceUuid}/tokens/#{deviceToken}", options, (error, response, body={}) =>
210 debug "revokeToken", error, body
211 @_handleResponse {error, response, body}, callback
212
213 revokeTokenByQuery: (deviceUuid, query, callback=->) =>
214 options = @_getDefaultRequestOptions()
215 options.qs = query
216
217 @request.delete "/devices/#{deviceUuid}/tokens", options, (error, response, body={}) =>
218 debug "revokeToken", error, body
219 @_handleResponse {error, response, body}, callback
220
221 search: (query, metadata, callback) =>
222 options = @_getDefaultRequestOptions()
223 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
224 options.json = query
225 @request.post "/search/devices", options, (error, response, body) =>
226 @_handleResponse {error, response, body}, callback
227
228 searchTokens: (query, metadata, callback) =>
229 options = @_getDefaultRequestOptions()
230 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
231 options.json = query
232 @request.post "/search/tokens", options, (error, response, body) =>
233 @_handleResponse {error, response, body}, callback
234
235 setPrivateKey: (privateKey) =>
236 @privateKey = new @NodeRSA privateKey
237
238 sign: (data) =>
239 @privateKey.sign(stableStringify(data)).toString('base64')
240
241 subscriptions: (uuid, rest...) =>
242 [callback] = rest
243 [metadata, callback] = rest if _.isPlainObject callback
244 metadata ?= {}
245 @_subscriptions uuid, metadata, callback
246
247 unregister: (device, callback=->) =>
248 options = @_getDefaultRequestOptions()
249
250 @request.delete "/devices/#{device.uuid}", options, (error, response, body) =>
251 debug "unregister", error, body
252 @_handleResponse {error, response, body}, callback
253
254 update: (uuid, params, rest...) =>
255 [callback] = rest
256 [metadata, callback] = rest if _.isPlainObject callback
257 metadata ?= {}
258
259 @_update uuid, params, metadata, callback
260
261 updateDangerously: (uuid, params, rest...) =>
262 [callback] = rest
263 [metadata, callback] = rest if _.isPlainObject callback
264 metadata ?= {}
265
266 @_updateDangerously uuid, params, metadata, callback
267
268 _updateDangerously: (uuid, params, metadata, callback=->) =>
269 options = @_getDefaultRequestOptions()
270 options.json = params
271 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
272
273 @request.put "/v2/devices/#{uuid}", options, (error, response, body) =>
274 debug "updated", uuid, params
275 debug "update", error, body
276 @_handleResponse {error, response, body}, callback
277
278 verify: (message, signature) =>
279 @privateKey.verify stableStringify(message), signature, 'utf8', 'base64'
280
281 whoami: (callback=->) =>
282 options = @_getDefaultRequestOptions()
283
284 @request.get "/v2/whoami", options, (error, response, body) =>
285 debug "whoami", error, body
286 @_handleResponse {error, response, body}, callback
287
288 _assertNoSrv: ({service, domain, secure}) =>
289 throw new Error('domain property only applies when resolveSrv is true') if domain?
290 throw new Error('service property only applies when resolveSrv is true') if service?
291 throw new Error('secure property only applies when resolveSrv is true') if secure?
292
293 _assertNoUrl: ({protocol, hostname, port}) =>
294 throw new Error('protocol property only applies when resolveSrv is false') if protocol?
295 throw new Error('hostname property only applies when resolveSrv is false') if hostname?
296 throw new Error('port property only applies when resolveSrv is false') if port?
297
298 _buildRequest: ({request, protocol, hostname, port, service, domain, secure, resolveSrv, auth}) =>
299 return request if request?
300
301 return @_buildSrvRequest({protocol, hostname, port, service, domain, secure, auth}) if resolveSrv
302 return @_buildUrlRequest({protocol, hostname, port, service, domain, secure, auth})
303
304 _buildSrvRequest: ({protocol, hostname, port, service, domain, secure, auth}) =>
305 @_assertNoUrl({protocol, hostname, port})
306 service ?= 'meshblu'
307 domain ?= 'octoblu.com'
308 secure ?= true
309 request = {}
310 request.auth = auth if auth?
311 request.timeout = @timeout if @timeout?
312 return new @MeshbluRequest {resolveSrv: true, service, domain, secure, request}
313
314 _buildUrlRequest: ({protocol, hostname, port, service, domain, secure, auth}) =>
315 @_assertNoSrv({service, domain, secure})
316 protocol ?= 'https'
317 hostname ?= 'meshblu.octoblu.com'
318 port ?= 443
319 try port = parseInt port
320 request = {}
321 request.auth = auth if auth?
322 request.timeout = @timeout if @timeout?
323 return new @MeshbluRequest {resolveSrv: false, protocol, hostname, port, request }
324
325 _devices: (query, metadata, callback=->) =>
326 options = @_getDefaultRequestOptions()
327 options.qs = query
328
329 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
330
331 @request.get "/v2/devices", options, (error, response, body) =>
332 debug "devices", error, body
333 @_handleResponse {error, response, body}, callback
334
335 _getDefaultHeaders: =>
336 return unless @serviceName?
337 return {
338 'x-meshblu-service-name': @serviceName
339 }
340
341 _getDefaultRequestOptions: =>
342 headers = @_getDefaultHeaders()
343 options = {
344 json: true
345 forever: @keepAlive
346 gzip: @gzip
347 }
348 options.headers = headers if headers?
349 return options
350
351 _getMetadataHeaders: (metadata) =>
352 _.transform metadata, (newMetadata, value, key) =>
353 kebabKey = _.kebabCase key
354 newMetadata["x-meshblu-#{kebabKey}"] = @_possiblySerializeHeaderValue value
355 return true
356 , {}
357
358 _getRawRequestOptions: =>
359 headers = @_getDefaultHeaders() ? {}
360 headers['content-type'] = 'application/json'
361 return {
362 json: false
363 headers
364 }
365
366 _handleError: ({message,code,response}, callback) =>
367 message ?= 'Unknown Error Occurred'
368 response = response?.toJSON?()
369 debug 'handling error', JSON.stringify({ message, code, response }, null, 2)
370 error = @_userError code, message, response
371 callback error
372
373 _handleResponse: ({error, response, body}, callback) =>
374 return @_handleError { message: error.message, code: error?.code, response }, callback if error?
375 code = response?.statusCode
376
377 if response?.headers?['x-meshblu-error']?
378 error = JSON.parse response.headers['x-meshblu-error']
379 return @_handleError { message: error.message, response, code }, callback
380
381 if body?.error?
382 return @_handleError { message: body.error, response, code }, callback
383
384 if code >= 400
385 message = body if _.isString body
386 message ?= "Invalid Response Code #{code}"
387 return @_handleError { message, response, code }, callback
388
389 callback null, body
390
391
392 _message: (message, metadata, callback=->) =>
393 if @raw
394 options = @_getRawRequestOptions()
395 options.body = message
396 else
397 options = @_getDefaultRequestOptions()
398 options.json = message
399
400 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
401
402 debug 'POST', "/messages", options
403
404 @request.post "/messages", options, (error, response, body) =>
405 debug "message", error, body
406 @_handleResponse {error, response, body}, callback
407
408 # because request doesn't serialize arrays correctly for headers.
409 _possiblySerializeHeaderValue: (value) =>
410 return value if _.isString value
411 return value if _.isBoolean value
412 return value if _.isNumber value
413 return JSON.stringify value
414
415 _subscriptions: (uuid, metadata, callback=->) =>
416 options = @_getDefaultRequestOptions()
417 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
418
419 @request.get "/v2/devices/#{uuid}/subscriptions", options, (error, response, body) =>
420 debug "subscriptions", error, body
421 @_handleResponse {error, response, body}, callback
422
423 _subscriptionUrl: (options) =>
424 {subscriberUuid, emitterUuid, type} = options
425 "/v2/devices/#{subscriberUuid}/subscriptions/#{emitterUuid}/#{type}"
426
427 _update: (uuid, params, metadata, callback=->) =>
428 options = @_getDefaultRequestOptions()
429 options.json = params
430 options.headers = _.extend {}, @_getMetadataHeaders(metadata), options.headers
431
432 @request.patch "/v2/devices/#{uuid}", options, (error, response, body) =>
433 debug "update", error, body
434 @_handleResponse {error, response, body}, callback
435
436 _userError: (code, message, response) =>
437 error = new Error message
438 error.code = code
439 error.response = response if response?
440 error
441
442module.exports = MeshbluHttp