1 | 'use strict'
|
2 |
|
3 | const os = require('os')
|
4 | const crypto = require('crypto')
|
5 | const dayjs = require('dayjs')
|
6 | const url = require('url')
|
7 | const ProxyAgent = require('proxy-agent')
|
8 | const cst = require('../constants.js')
|
9 |
|
10 | const 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 |
|
23 |
|
24 |
|
25 | const 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 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | class 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 (dayjs().isAfter(value)) {
|
64 | delete this._cache[key]
|
65 | delete this._ttl[key]
|
66 | }
|
67 | }
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 |
|
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 |
|
90 |
|
91 |
|
92 |
|
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] = dayjs().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 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | class 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 |
|
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 |
|
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 |
|
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 |
|
150 | if (!callsite || type === 'core' || callsite.file_name.indexOf('node_modules') > -1 ||
|
151 | callsite.file_name.indexOf('vxx') > -1) {
|
152 | continue
|
153 | }
|
154 |
|
155 |
|
156 | var context = this._cache.get(callsite.file_name)
|
157 | var source = []
|
158 | if (context && context.length > 0) {
|
159 |
|
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 |
|
168 | if (context[callsite.line_number - 1]) {
|
169 | source.push(context[callsite.line_number - 1].replace(/\t/g, ' ').replace(' ', '>>'))
|
170 | }
|
171 |
|
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 |
|
192 | if (typeof (error.stackframes) === 'object') {
|
193 | let result = this.parse(error.stackframes)
|
194 |
|
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 |
|
204 |
|
205 | if (typeof error.stack === 'string' && !error.callsite) {
|
206 | let siteRegex = /(\/[^\\\n]*)/g
|
207 | let tmp
|
208 | let stack = []
|
209 |
|
210 |
|
211 | while ((tmp = siteRegex.exec(error.stack))) {
|
212 | stack.push(tmp[1])
|
213 | }
|
214 |
|
215 |
|
216 | stack = stack.map((callsite) => {
|
217 |
|
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 |
|
240 |
|
241 |
|
242 | class 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 |
|
270 | class Cipher {
|
271 | static get CIPHER_ALGORITHM () {
|
272 | return 'aes256'
|
273 | }
|
274 |
|
275 | |
276 |
|
277 |
|
278 |
|
279 |
|
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 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | static cipherMessage (data, key) {
|
300 | try {
|
301 |
|
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 |
|
318 |
|
319 | class HTTPClient {
|
320 | |
321 |
|
322 |
|
323 |
|
324 | getModule (url) {
|
325 | return url.match(/https:\/\//) ? require('https') : require('http')
|
326 | }
|
327 | |
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
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 |
|
382 | module.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 | }
|