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}, 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 |
|
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 |
|
428 | module.exports = MeshbluHttp
|