UNPKG

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