1 | 'use strict'
|
2 |
|
3 | const os = require('os')
|
4 | const crypto = require('crypto')
|
5 | const moment = require('moment')
|
6 | const url = require('url')
|
7 |
|
8 | const 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 |
|
21 |
|
22 |
|
23 | const 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 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | class 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 |
|
70 |
|
71 |
|
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 |
|
88 |
|
89 |
|
90 |
|
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 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | class 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 |
|
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 |
|
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 |
|
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 |
|
148 | if (!callsite || type === 'core' || callsite.file_name.indexOf('node_modules') > -1 ||
|
149 | callsite.file_name.indexOf('vxx') > -1) {
|
150 | continue
|
151 | }
|
152 |
|
153 |
|
154 | var context = this._cache.get(callsite.file_name)
|
155 | var source = []
|
156 | if (context && context.length > 0) {
|
157 |
|
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 |
|
166 | if (context[callsite.line_number - 1]) {
|
167 | source.push(context[callsite.line_number - 1].replace(/\t/g, ' ').replace(' ', '>>'))
|
168 | }
|
169 |
|
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 |
|
190 | if (typeof (error.stackframes) === 'object') {
|
191 | let result = this.parse(error.stackframes)
|
192 |
|
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 |
|
202 |
|
203 | if (typeof error.stack === 'string' && !error.callsite) {
|
204 | let siteRegex = /(\/[^\\\n]*)/g
|
205 | let tmp
|
206 | let stack = []
|
207 |
|
208 |
|
209 | while ((tmp = siteRegex.exec(error.stack))) {
|
210 | stack.push(tmp[1])
|
211 | }
|
212 |
|
213 |
|
214 | stack = stack.map((callsite) => {
|
215 |
|
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 |
|
238 |
|
239 |
|
240 | class 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 |
|
268 | class Cipher {
|
269 | static get CIPHER_ALGORITHM () {
|
270 | return 'aes256'
|
271 | }
|
272 |
|
273 | |
274 |
|
275 |
|
276 |
|
277 |
|
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 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 | static cipherMessage (data, key) {
|
298 | try {
|
299 |
|
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 |
|
316 |
|
317 | class HTTPClient {
|
318 | |
319 |
|
320 |
|
321 |
|
322 | getModule (url) {
|
323 | return url.match(/https:\/\//) ? require('https') : require('http')
|
324 | }
|
325 | |
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
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 |
|
379 | module.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 | }
|