UNPKG

10.2 kBJavaScriptView Raw
1'use strict'
2
3const os = require('os')
4const crypto = require('crypto')
5const moment = require('moment')
6const url = require('url')
7
8const interfaceType = {
9 v4: {
10 default: '127.0.0.1',
11 family: 'IPv4'
12 },
13 v6: {
14 default: '::1',
15 family: 'IPv6'
16 }
17}
18
19/**
20 * Search for public network adress
21 * @param {String} type the type of network interface, can be either 'v4' or 'v6'
22 */
23const retrieveAddress = (type) => {
24 let interfce = interfaceType[type]
25 let ret = interfce.default
26 let interfaces = os.networkInterfaces()
27
28 Object.keys(interfaces).forEach(function (el) {
29 interfaces[el].forEach(function (el2) {
30 if (!el2.internal && el2.family === interfce.family) {
31 ret = el2.address
32 }
33 })
34 })
35 return ret
36}
37
38/**
39 * Simple cache implementation
40 *
41 * @param {Object} opts cache options
42 * @param {Function} opts.miss function called when a key isn't found in the cache
43 */
44class Cache {
45 constructor (opts) {
46 this._cache = {}
47 this._miss = opts.miss
48 this._ttl_time = opts.ttl
49 this._ttl = {}
50
51 if (opts.ttl) {
52 this._worker = setInterval(this.worker.bind(this), 1000)
53 }
54 }
55
56 worker () {
57 let keys = Object.keys(this._ttl)
58 for (let i = 0; i < keys.length; i++) {
59 let key = keys[i]
60 let value = this._ttl[key]
61 if (moment().isAfter(value)) {
62 delete this._cache[key]
63 delete this._ttl[key]
64 }
65 }
66 }
67
68 /**
69 * Get a value from the cache
70 *
71 * @param {String} key
72 */
73 get (key) {
74 if (!key) return null
75 let value = this._cache[key]
76 if (value) return value
77
78 value = this._miss(key)
79
80 if (value) {
81 this.set(key, value)
82 }
83 return value
84 }
85
86 /**
87 * Set a value in the cache
88 *
89 * @param {String} key
90 * @param {Mixed} value
91 */
92 set (key, value) {
93 if (!key || !value) return false
94 this._cache[key] = value
95 if (this._ttl_time) {
96 this._ttl[key] = moment().add(this._ttl_time, 'seconds')
97 }
98 return true
99 }
100
101 reset () {
102 this._cache = null
103 this._cache = {}
104 this._ttl = null
105 this._ttl = {}
106 }
107}
108
109/**
110 * StackTraceParser is used to parse callsite from stacktrace
111 * and get from FS the context of the error (if available)
112 *
113 * @param {Cache} cache cache implementation used to query file from FS and get context
114 */
115class StackTraceParser {
116 constructor (opts) {
117 this._cache = opts.cache
118 this._context_size = opts.context
119 }
120
121 isAbsolute (path) {
122 if (process.platform === 'win32') {
123 // https://github.com/nodejs/node/blob/b3fcc245fb25539909ef1d5eaa01dbf92e168633/lib/path.js#L56
124 let splitDeviceRe = /^([a-zA-Z]:|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)?([\\/])?([\s\S]*?)$/
125 let result = splitDeviceRe.exec(path)
126 let device = result[1] || ''
127 let isUnc = Boolean(device && device.charAt(1) !== ':')
128 // UNC paths are always absolute
129 return Boolean(result[2] || isUnc)
130 } else {
131 return path.charAt(0) === '/'
132 }
133 }
134
135 parse (stack) {
136 if (!stack || stack.length === 0) return false
137
138 for (var i = 0, len = stack.length; i < len; i++) {
139 var callsite = stack[i]
140
141 // avoid null values
142 if (typeof callsite !== 'object') continue
143 if (!callsite.file_name || !callsite.line_number) continue
144
145 var type = this.isAbsolute(callsite.file_name) || callsite.file_name[0] === '.' ? 'user' : 'core'
146
147 // only use the callsite if its inside user space
148 if (!callsite || type === 'core' || callsite.file_name.indexOf('node_modules') > -1 ||
149 callsite.file_name.indexOf('vxx') > -1) {
150 continue
151 }
152
153 // get the whole context (all lines) and cache them if necessary
154 var context = this._cache.get(callsite.file_name)
155 var source = []
156 if (context && context.length > 0) {
157 // get line before the call
158 var preLine = callsite.line_number - this._context_size - 1
159 var pre = context.slice(preLine > 0 ? preLine : 0, callsite.line_number - 1)
160 if (pre && pre.length > 0) {
161 pre.forEach(function (line) {
162 source.push(line.replace(/\t/g, ' '))
163 })
164 }
165 // get the line where the call has been made
166 if (context[callsite.line_number - 1]) {
167 source.push(context[callsite.line_number - 1].replace(/\t/g, ' ').replace(' ', '>>'))
168 }
169 // and get the line after the call
170 var postLine = callsite.line_number + this._context_size
171 var post = context.slice(callsite.line_number, postLine)
172 if (post && post.length > 0) {
173 post.forEach(function (line) {
174 source.push(line.replace(/\t/g, ' '))
175 })
176 }
177 }
178 return {
179 context: source.length > 0 ? source.join('\n') : 'cannot retrieve source context',
180 callsite: [ callsite.file_name, callsite.line_number ].join(':')
181 }
182 }
183 return false
184 }
185
186 attachContext (error) {
187 if (!error) return error
188
189 // if pmx attached callsites we can parse them to retrieve the context
190 if (typeof (error.stackframes) === 'object') {
191 let result = this.parse(error.stackframes)
192 // no need to send it since there is already the stacktrace
193 delete error.stackframes
194 delete error.__error_callsites
195
196 if (result) {
197 error.callsite = result.callsite
198 error.context = result.context
199 }
200 }
201 // if the stack is here we can parse it directly from the stack string
202 // only if the context has been retrieved from elsewhere
203 if (typeof error.stack === 'string' && !error.callsite) {
204 let siteRegex = /(\/[^\\\n]*)/g
205 let tmp
206 let stack = []
207
208 // find matching groups
209 while ((tmp = siteRegex.exec(error.stack))) {
210 stack.push(tmp[1])
211 }
212
213 // parse each callsite to match the format used by the stackParser
214 stack = stack.map((callsite) => {
215 // remove the trailing ) if present
216 if (callsite[callsite.length - 1] === ')') {
217 callsite = callsite.substr(0, callsite.length - 1)
218 }
219 let location = callsite.split(':')
220
221 return location.length < 3 ? callsite : {
222 file_name: location[0],
223 line_number: parseInt(location[1])
224 }
225 })
226
227 let finalCallsite = this.parse(stack)
228 if (finalCallsite) {
229 error.callsite = finalCallsite.callsite
230 error.context = finalCallsite.context
231 }
232 }
233 return error
234 }
235}
236
237// EWMA = ExponentiallyWeightedMovingAverage from
238// https://github.com/felixge/node-measured/blob/master/lib/util/ExponentiallyMovingWeightedAverage.js
239// Copyright Felix Geisendörfer <felix@debuggable.com> under MIT license
240class EWMA {
241 constructor () {
242 this._timePeriod = 60000
243 this._tickInterval = 5000
244 this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod)
245 this._count = 0
246 this._rate = 0
247 this._interval = setInterval(_ => {
248 this.tick()
249 }, this._tickInterval)
250 this._interval.unref()
251 }
252
253 update (n) {
254 this._count += n || 1
255 }
256
257 tick () {
258 let instantRate = this._count / this._tickInterval
259 this._count = 0
260 this._rate += (this._alpha * (instantRate - this._rate))
261 }
262
263 rate (timeUnit) {
264 return (this._rate || 0) * timeUnit
265 }
266}
267
268class Cipher {
269 static get CIPHER_ALGORITHM () {
270 return 'aes256'
271 }
272
273 /**
274 * Decipher data using 256 bits key (AES)
275 * @param {Hex} data input data
276 * @param {String} key 256 bits key
277 * @return {Object} deciphered data parsed as json object
278 */
279 static decipherMessage (msg, key) {
280 try {
281 let decipher = crypto.createDecipher(Cipher.CIPHER_ALGORITHM, key)
282 let decipheredMessage = decipher.update(msg, 'hex', 'utf8')
283 decipheredMessage += decipher.final('utf8')
284 return JSON.parse(decipheredMessage)
285 } catch (err) {
286 console.error(err)
287 return null
288 }
289 }
290
291 /**
292 * Cipher data using 256 bits key (AES)
293 * @param {String} data input data
294 * @param {String} key 256 bits key
295 * @return {Hex} ciphered data
296 */
297 static cipherMessage (data, key) {
298 try {
299 // stringify if not already done (fail safe)
300 if (typeof data !== 'string') {
301 data = JSON.stringify(data)
302 }
303
304 let cipher = crypto.createCipher(Cipher.CIPHER_ALGORITHM, key)
305 let cipheredData = cipher.update(data, 'utf8', 'hex')
306 cipheredData += cipher.final('hex')
307 return cipheredData
308 } catch (err) {
309 console.error(err)
310 }
311 }
312}
313
314/**
315 * HTTP wrapper
316 */
317class HTTPClient {
318 /**
319 * Return native module (HTTP/HTTPS)
320 * @param {String} url
321 */
322 getModule (url) {
323 return url.match(/https:\/\//) ? require('https') : require('http')
324 }
325 /**
326 * Send an HTTP request and return data or error if status > 200
327 * @param {Object} opts
328 * @param {String} opts.url
329 * @param {String} opts.method
330 * @param {Object} [opts.data]
331 * @param {Object} [opts.headers]
332 * @param {Function} cb invoked with <err, body>
333 */
334 open (opts, cb) {
335 const http = this.getModule(opts.url)
336 const parsedUrl = url.parse(opts.url)
337 let data = null
338 const options = {
339 hostname: parsedUrl.hostname,
340 path: parsedUrl.path,
341 port: parsedUrl.port,
342 method: opts.method,
343 headers: opts.headers
344 }
345
346 if (opts.data) {
347 data = JSON.stringify(opts.data)
348 options.headers = Object.assign({
349 'Content-Type': 'application/json',
350 'Content-Length': data.length
351 }, opts.headers)
352 }
353
354 const req = http.request(options, (res) => {
355 let body = ''
356
357 res.setEncoding('utf8')
358 res.on('data', (chunk) => {
359 body += chunk.toString()
360 })
361 res.on('end', () => {
362 try {
363 let jsonData = JSON.parse(body)
364 return cb(null, jsonData)
365 } catch (err) {
366 return cb(err)
367 }
368 })
369 })
370 req.on('error', cb)
371
372 if (data) {
373 req.write(data)
374 }
375 req.end()
376 }
377}
378
379module.exports = {
380 EWMA: EWMA,
381 Cache: Cache,
382 StackTraceParser: StackTraceParser,
383 serialize: require('fclone'),
384 network: {
385 v4: retrieveAddress('v4'),
386 v6: retrieveAddress('v6')
387 },
388 HTTPClient: HTTPClient,
389 Cipher: Cipher,
390 clone: require('fclone')
391}