1 | _ = require 'lodash'
|
2 | url = require 'url'
|
3 | debug = require('debug')('meshblu-http')
|
4 | stableStringify = require 'json-stable-stringify'
|
5 |
|
6 | class 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 |
|
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 |
|
442 | module.exports = MeshbluHttp
|