topbit 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +128 -0
  2. package/README.cn.md +1519 -0
  3. package/README.md +1483 -0
  4. package/bin/app.js +17 -0
  5. package/bin/loadinfo.sh +18 -0
  6. package/bin/new-ctl.js +234 -0
  7. package/bin/newapp.js +22 -0
  8. package/demo/allow.js +98 -0
  9. package/demo/cert/localhost-cert.pem +19 -0
  10. package/demo/cert/localhost-privkey.pem +28 -0
  11. package/demo/controller/api.js +15 -0
  12. package/demo/extends.js +5 -0
  13. package/demo/group-api.js +161 -0
  14. package/demo/group-api2.js +109 -0
  15. package/demo/http2.js +34 -0
  16. package/demo/http2_proxy_backend.js +45 -0
  17. package/demo/http2proxy.js +48 -0
  18. package/demo/http_proxy_backend.js +44 -0
  19. package/demo/httpproxy.js +47 -0
  20. package/demo/loader.js +27 -0
  21. package/demo/log.js +118 -0
  22. package/demo/memlimit.js +31 -0
  23. package/demo/min.js +7 -0
  24. package/demo/serv.js +15 -0
  25. package/images/middleware.jpg +0 -0
  26. package/images/topbit-middleware.png +0 -0
  27. package/images/topbit.png +0 -0
  28. package/package.json +42 -11
  29. package/src/_loadExtends.js +21 -0
  30. package/src/bodyparser.js +420 -0
  31. package/src/connfilter.js +125 -0
  32. package/src/context1.js +166 -0
  33. package/src/context2.js +182 -0
  34. package/src/ctxpool.js +39 -0
  35. package/src/ext.js +318 -0
  36. package/src/extends/Http2Pool.js +365 -0
  37. package/src/extends/__randstring.js +24 -0
  38. package/src/extends/cookie.js +44 -0
  39. package/src/extends/cors.js +334 -0
  40. package/src/extends/errorlog.js +252 -0
  41. package/src/extends/http2limit.js +126 -0
  42. package/src/extends/http2proxy.js +691 -0
  43. package/src/extends/jwt.js +217 -0
  44. package/src/extends/mixlogger.js +63 -0
  45. package/src/extends/paramcheck.js +266 -0
  46. package/src/extends/proxy.js +662 -0
  47. package/src/extends/realip.js +34 -0
  48. package/src/extends/referer.js +68 -0
  49. package/src/extends/resource.js +398 -0
  50. package/src/extends/session.js +174 -0
  51. package/src/extends/setfinal.js +50 -0
  52. package/src/extends/sni.js +48 -0
  53. package/src/extends/sse.js +293 -0
  54. package/src/extends/timing.js +111 -0
  55. package/src/extends/tofile.js +123 -0
  56. package/src/fastParseUrl.js +426 -0
  57. package/src/headerLimit.js +18 -0
  58. package/src/http1.js +336 -0
  59. package/src/http2.js +337 -0
  60. package/src/httpc.js +251 -0
  61. package/src/lib/npargv.js +354 -0
  62. package/src/lib/zipdata.js +45 -0
  63. package/src/loader/loader.js +999 -0
  64. package/src/logger.js +32 -0
  65. package/src/loggermsg.js +349 -0
  66. package/src/makeId.js +200 -0
  67. package/src/midcore.js +213 -0
  68. package/src/middleware1.js +103 -0
  69. package/src/middleware2.js +116 -0
  70. package/src/monitor.js +380 -0
  71. package/src/movefile.js +30 -0
  72. package/src/optionsCheck.js +54 -0
  73. package/src/randstring.js +23 -0
  74. package/src/router.js +682 -0
  75. package/src/sendmsg.js +27 -0
  76. package/src/strong.js +72 -0
  77. package/src/token/token.js +461 -0
  78. package/src/topbit.js +1293 -0
  79. package/src/versionCheck.js +31 -0
  80. package/test/test-bigctx.js +29 -0
  81. package/test/test-daemon-args.js +7 -0
  82. package/test/test-ext.js +81 -0
  83. package/test/test-find.js +69 -0
  84. package/test/test-route-sort.js +71 -0
  85. package/test/test-route.js +49 -0
  86. package/test/test-route2.js +51 -0
  87. package/test/test-run-args.js +7 -0
  88. package/test/test-url.js +52 -0
  89. package/main.js +0 -0
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ class Referer {
4
+ constructor(options = {}) {
5
+
6
+ this.allow = [];
7
+
8
+ this.failedCode = 404;
9
+
10
+ if (typeof options !== 'object') {
11
+ options = {};
12
+ }
13
+
14
+ for (let k in options) {
15
+ switch (k) {
16
+ case 'allow':
17
+ if (options[k] === '*' || (options[k] instanceof Array)) {
18
+ this.allow = options[k];
19
+ }
20
+ break;
21
+
22
+ case 'failedCode':
23
+ if (typeof options[k] === 'number' && options[k] >=400 && options[k] < 500)
24
+ this.failedCode = options[k];
25
+ break;
26
+
27
+ }
28
+ }
29
+
30
+ }
31
+
32
+ mid() {
33
+ let self = this;
34
+
35
+ return async (c, next) => {
36
+
37
+ if (c.headers.referer === undefined) c.headers.referer = '';
38
+
39
+ let stat = false;
40
+
41
+ if (self.allow === '*' || self.allow.length === 0 ) {
42
+ stat = true;
43
+ }
44
+ else {
45
+ let refer = c.headers.referer;
46
+
47
+ for (let i = self.allow.length - 1; i >= 0; i--) {
48
+ if (refer.indexOf(self.allow[i]) === 0) {
49
+ stat = true;
50
+ break;
51
+ }
52
+ }
53
+
54
+ }
55
+
56
+ if (stat) {
57
+ await next(c);
58
+ } else {
59
+ c.status(self.failedCode).to('');
60
+ }
61
+
62
+ };
63
+
64
+ }
65
+
66
+ }
67
+
68
+ module.exports = Referer;
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ //const fs = require('fs');
4
+
5
+ const zlib = require('node:zlib')
6
+ const fs = require('node:fs')
7
+
8
+ const fsp = fs.promises
9
+
10
+ let _typemap = {
11
+ '.css' : 'text/css; charset=utf-8',
12
+ '.js' : 'text/javascript; charset=utf-8',
13
+ '.txt' : 'text/plain; charset=utf-8',
14
+ '.json' : 'application/json; charset=utf-8',
15
+ '.lrc' : 'text/plain; charset=utf-8',
16
+ '.md' : 'text/plain; charset=utf-8',
17
+ '.html' : 'text/html; charset=utf-8',
18
+ '.xml' : 'text/xml; charset=utf-8',
19
+
20
+ '.svg' : 'image/svg+xml',
21
+ '.jpg' : 'image/jpeg',
22
+ '.jpeg' : 'image/jpeg',
23
+ '.png' : 'image/png',
24
+ '.gif' : 'image/gif',
25
+ '.ico' : 'image/x-icon',
26
+ '.webp' : 'image/webp',
27
+ '.tif' : 'image/tiff',
28
+ '.tiff' : 'image/tiff',
29
+ '.avif' : 'image/avif',
30
+ '.apng' : 'image/apng',
31
+
32
+ '.mp3' : 'audio/mpeg',
33
+ '.flac' : 'audio/flac',
34
+ '.wav' : 'audio/x-wav',
35
+ '.mp4' : 'video/mp4',
36
+ '.webm' : 'video/webm',
37
+
38
+ '.otf' : 'font/otf',
39
+ '.ttf' : 'font/ttf',
40
+ '.wtf' : 'font/wtf',
41
+ '.woff' : 'font/woff',
42
+ '.ttc' : 'font/ttc',
43
+ '.woff2' : 'font/woff2',
44
+
45
+ '.pdf': 'application/pdf',
46
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
47
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
48
+ }
49
+
50
+ /**
51
+ * 处理静态资源的请求,需要把中间件挂载到一个分组下,否则会影响全局,如果一个只做静态分发的服务则可以全局启用。
52
+ */
53
+
54
+ class Resource {
55
+
56
+ constructor(options = {}) {
57
+
58
+ this.cache = new Map()
59
+
60
+ this.staticPath = ''
61
+
62
+ //最大缓存,单位为字节,0表示不限制。
63
+ this.maxCacheSize = 128_000_000
64
+
65
+ this.size = 0
66
+
67
+ //控制释放缓存的概率,1到100
68
+ this.prob = 6
69
+
70
+ //失败缓存统计,当失败缓存计数达到一个阈值,则会清空缓存。
71
+ this.cacheFailed = 0
72
+
73
+ this.failedLimit = 50
74
+
75
+ this.compress = true
76
+
77
+ this.cacheControl = null
78
+
79
+ this.routePath = '/static/*'
80
+
81
+ this.prepath = ''
82
+
83
+ this.routeGroup = `__static_${parseInt(Math.random()*10000)}_`
84
+
85
+ this.decodePath = false
86
+
87
+ this.maxFileSize = 10_000_000
88
+
89
+ if (typeof options !== 'object') {
90
+ options = {}
91
+ }
92
+
93
+ for (let k in options) {
94
+ switch(k) {
95
+ case 'staticPath':
96
+ this.staticPath = options[k]
97
+ break
98
+
99
+ case 'maxCacheSize':
100
+ case 'maxFileSize':
101
+ if (typeof options[k] === 'number') {
102
+ this[k] = options[k]
103
+ }
104
+ break
105
+
106
+ case 'failedLimit':
107
+ if (options[k] > 0) {
108
+ this.failedLimit = options[k]
109
+ }
110
+ break
111
+
112
+ case 'compress':
113
+ this.compress = options[k]
114
+ break
115
+
116
+ case 'cacheControl':
117
+ this.cacheControl = options[k]
118
+ break
119
+
120
+ case 'routePath':
121
+ if (typeof options[k] === 'string') {
122
+ this.routePath = options[k]
123
+ }
124
+ break
125
+
126
+ case 'routeGroup':
127
+ if (typeof options[k] === 'string') {
128
+ this.routeGroup = options[k]
129
+ }
130
+ break
131
+
132
+ case 'decodePath':
133
+ this.decodePath = options[k]
134
+ break
135
+
136
+ case 'prepath':
137
+ this.prepath = options[k]
138
+
139
+ if (this.prepath.length > 0 && this.prepath[0] !== '/') {
140
+ this.prepath = `/${this.prepath}`
141
+ }
142
+ break
143
+
144
+ case 'prob':
145
+ if (typeof options[k] === 'number' && options[k] >= 1 && options[k] <= 100) {
146
+ this.prob = options[k]
147
+ }
148
+ break
149
+
150
+ }
151
+ }
152
+
153
+ if (this.maxCacheSize < 10_000_000) {
154
+ this.maxCacheSize = 10_000_000
155
+ }
156
+
157
+ if (this.maxFileSize < 10000 || this.maxFileSize > 500_000_000) {
158
+ this.maxFileSize = 10_000_000
159
+ }
160
+
161
+ if (this.staticPath.length > 1 && this.staticPath[ this.staticPath.length-1 ] === '/') {
162
+ this.staticPath = this.staticPath.substring(0, this.staticPath.length-1)
163
+ }
164
+
165
+ this.ctypeMap = _typemap
166
+
167
+ for (let k in this.ctypeMap) {
168
+ this.ctypeMap[ k.toUpperCase() ] = _typemap[k]
169
+ }
170
+
171
+ }
172
+
173
+ addType(tobj) {
174
+ let lower_name, up_name;
175
+
176
+ for (let k in tobj) {
177
+ lower_name = k.toLowerCase()
178
+
179
+ up_name = k.toUpperCase()
180
+
181
+ this.ctypeMap[lower_name] = tobj[k]
182
+
183
+ this.ctypeMap[up_name] = tobj[k]
184
+ }
185
+ }
186
+
187
+ extName(filename) {
188
+ let extind = filename.length - 1
189
+ let extstart = filename.length - 5
190
+
191
+ while (extind > 0 && extind >= extstart) {
192
+ if (filename[extind] === '.') break
193
+
194
+ extind -= 1
195
+ }
196
+
197
+ return filename.substring(extind)
198
+ }
199
+
200
+ filetype(extname) {
201
+ if (this.ctypeMap[extname] !== undefined) {
202
+ return this.ctypeMap[extname]
203
+ }
204
+
205
+ return 'application/octet-stream'
206
+ }
207
+
208
+ removeGroupCache(filepre) {
209
+ let keys = this.cache.keys()
210
+ for (let k of keys) {
211
+ if (k.indexOf(filepre) === 0) this.cache.delete(k)
212
+ }
213
+ }
214
+
215
+ removeNameCache(name) {
216
+ let keys = this.cache.keys()
217
+ for (let k of keys) {
218
+ if (k.lastIndexOf(name) >= 0) this.cache.delete(k)
219
+ }
220
+ }
221
+
222
+ removeCache(filepath) {
223
+ if (this.cache.has(filepath)) {
224
+ this.cache.delete(filepath)
225
+ }
226
+ }
227
+
228
+ clearCache() {
229
+ this.size = 0
230
+ this.cacheFailed = 0
231
+ this.cache.clear()
232
+ }
233
+
234
+ async pipeData(pathfile, ctx, filesize) {
235
+ let stm = fs.createReadStream(pathfile)
236
+ let dataBuffer = []
237
+ let total = 0
238
+
239
+ if (ctx.major === 2) {
240
+ ctx.sendHeader()
241
+ }
242
+
243
+ return new Promise((rv, rj) => {
244
+ stm.on('data', data => {
245
+ if (filesize <= this.maxFileSize) {
246
+ total += data.length
247
+ dataBuffer.push(data)
248
+ }
249
+ })
250
+
251
+ stm.on('error', err => {
252
+ dataBuffer = null
253
+ !stm.destroyed && stm.destroy()
254
+ rj(err)
255
+ })
256
+
257
+ stm.on('end', () => {
258
+ if (dataBuffer && dataBuffer.length > 0) {
259
+ let retData = Buffer.concat(dataBuffer, total)
260
+ dataBuffer = null
261
+ rv(retData)
262
+ } else {
263
+ rv(null)
264
+ }
265
+ })
266
+
267
+ stm.pipe(ctx.res)
268
+ })
269
+
270
+ }
271
+
272
+ mid() {
273
+ let self = this
274
+
275
+ return async (c, next) => {
276
+ let rpath = c.param.starPath || c.path
277
+
278
+ if (rpath[0] !== '/') {
279
+ rpath = `/${rpath}`
280
+ }
281
+
282
+ let real_path = rpath
283
+
284
+ if (self.decodePath) {
285
+ try {
286
+ real_path = decodeURIComponent(rpath)
287
+ } catch (err) {
288
+ real_path = rpath
289
+ }
290
+ }
291
+
292
+ let pathfile = `${self.staticPath}${self.prepath}${real_path}`
293
+
294
+ if (self.cache.has(real_path)) {
295
+ let r = self.cache.get(real_path)
296
+
297
+ c.setHeader('content-type', r.type)
298
+ c.setHeader('content-length', r.data.length)
299
+
300
+ if (r.gzip) {
301
+ c.setHeader('content-encoding', 'gzip')
302
+ }
303
+
304
+ if (self.cacheControl) {
305
+ c.setHeader('cache-control', self.cacheControl)
306
+ }
307
+
308
+ return c.to(r.data)
309
+ }
310
+
311
+ let data = null
312
+
313
+ let extname = this.extName(pathfile)
314
+
315
+ let ctype = self.filetype(extname)
316
+
317
+ let zipdata = null
318
+
319
+ try {
320
+ if (ctype.indexOf('text/') === 0
321
+ || extname === '.json'
322
+ || ctype.indexOf('font/') === 0)
323
+ {
324
+ data = await fsp.readFile(pathfile)
325
+
326
+ //若文件很小,压缩后的数据很可能要比源文件还大,所以对超过1k的文件进行压缩,否则不进行压缩。
327
+ if (data && data.length > 1024) {
328
+ zipdata = await new Promise((rv, rj) => {
329
+ zlib.gzip(data, (err, d) => {
330
+ if (err) {
331
+ rj(err)
332
+ } else {
333
+ rv(d)
334
+ }
335
+ })
336
+ }).catch(err => {
337
+ zipdata = null
338
+ })
339
+ }
340
+
341
+ c.setHeader('content-type', ctype)
342
+ .setHeader('content-length', zipdata ? zipdata.length : data.length)
343
+
344
+ if (zipdata) {
345
+ c.setHeader('content-encoding', 'gzip')
346
+ }
347
+
348
+ self.cacheControl && c.setHeader('cache-control', self.cacheControl);
349
+
350
+ c.sendHeader().to(zipdata || data)
351
+ } else {
352
+ let fst = await fsp.stat(pathfile)
353
+
354
+ c.setHeader('content-type', ctype).setHeader('content-length', fst.size);
355
+
356
+ self.cacheControl && c.setHeader('cache-control', self.cacheControl);
357
+
358
+ data = await this.pipeData(pathfile, c, fst.size)
359
+ //说明数据太大,放弃了缓存
360
+ if (!data) return;
361
+ }
362
+
363
+ if (self.cacheFailed >= self.failedLimit) {
364
+ //以{self.prob}%概率决定是否释放缓存。
365
+ if (parseInt(Math.random() * 100) < self.prob) {
366
+ self.clearCache()
367
+ }
368
+
369
+ } else if (self.maxCacheSize > 0 && self.size >= self.maxCacheSize) {
370
+
371
+ if (self.cacheFailed < 1000_0000)
372
+ self.cacheFailed++
373
+
374
+ } else {
375
+ self.cache.set(real_path, {
376
+ data : zipdata || data,
377
+ type : ctype,
378
+ gzip : zipdata ? true : false,
379
+ })
380
+
381
+ self.size += zipdata ? zipdata.length : data.length
382
+ }
383
+ } catch (err) {
384
+ c.status(404).to('read file failed')
385
+ }
386
+
387
+ }
388
+
389
+ }
390
+
391
+ init(app, group = null) {
392
+ app.get(this.routePath, async c => {}, {group: group || this.routeGroup})
393
+ app.use(this.mid(), {group : group || this.routeGroup})
394
+ }
395
+
396
+ }
397
+
398
+ module.exports = Resource
@@ -0,0 +1,174 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const crypto = require('node:crypto')
5
+
6
+ /*
7
+ 这个模块用于titbit框架的登录会话,调用一定要在cookie中间件之后。
8
+ 整体的过程就是在基于cookie中间件的解析结果,如果检测到cookie中有会话ID
9
+ 则寻找文件并读取数据,解析成JSON对象添加到c.session;如果cookie中
10
+ 没有会话ID或者读取文件失败则创建会话文件并发送Set-Cookie头部信息保存会话ID。
11
+ */
12
+
13
+ class Session {
14
+
15
+ constructor() {
16
+ this.expires = false
17
+ this.domain = false
18
+ this.path = '/'
19
+ this.sessionDir = '/tmp'
20
+
21
+ this.ds = '/'
22
+
23
+ if (process.platform.indexOf('win') === 0) {
24
+ this.ds = '\\'
25
+ this.sessionDir = 'C:\\Users\\Public\\sess'
26
+ try {
27
+ fs.accessSync(this.sessionDir)
28
+ } catch (err) {
29
+ fs.mkdirSync(this.sessionDir)
30
+ }
31
+ }
32
+
33
+ this.prefix = 'titbit_sess_'
34
+
35
+ this.sessionKey = 'TITBIT_SESSID'
36
+
37
+ this.error = null
38
+
39
+ }
40
+
41
+ init(app) {
42
+ let proto = app.httpServ.Content.prototype
43
+
44
+ let self = this
45
+
46
+ proto.sessionError = function (options={}) {
47
+ let err = self.error
48
+ if (options.clear) self.error = null
49
+ return err
50
+ }
51
+
52
+ proto.setSession = function (key, data) {
53
+ this._session[key] = data
54
+ this._sessionState = true
55
+ }
56
+
57
+ proto.getSession = function (key = null) {
58
+ if (key === null) {
59
+ return this._session
60
+ }
61
+
62
+ return this._session[key] || null
63
+ }
64
+
65
+ proto.deleteSession = function (key) {
66
+ delete this._session[key]
67
+ this._sessionState = true
68
+ }
69
+
70
+ proto.clearSession = function () {
71
+ this._sessionState = false
72
+ this._session = {}
73
+ fs.unlink(this._sessFile, (err) => {
74
+ err && (self.error = err)
75
+ })
76
+ }
77
+
78
+ }
79
+
80
+ mid() {
81
+ let self = this
82
+
83
+ return async (c, next) => {
84
+ c._session = {}
85
+ c._sessionState = false
86
+ c._sessFile = ''
87
+
88
+ let sess_file = ''
89
+ let sessid = c.cookie[ self.sessionKey ]
90
+ let sess_state
91
+
92
+ if (sessid) {
93
+ sess_file = `${self.sessionDir}${self.ds}${self.prefix}${sessid}`
94
+ c._sessFile = sess_file
95
+
96
+ await new Promise((rv, rj) => {
97
+ fs.readFile(sess_file, (err, data) => {
98
+ if (err) {
99
+ rj(err)
100
+ } else {
101
+ sess_state = true
102
+ rv(data)
103
+ }
104
+ })
105
+ }).then(data => {
106
+ c._session = JSON.parse(data)
107
+ }, err => {
108
+ sess_state = false
109
+ }).catch(err => {
110
+ self.error = err
111
+ })
112
+ }
113
+
114
+ if (sessid === undefined || sess_state === false) {
115
+ let org_name = `${c.host}_${Date.now()}__${Math.random()}`
116
+
117
+ let hash = crypto.createHash('sha1')
118
+
119
+ hash.update(org_name)
120
+
121
+ sessid = hash.digest('hex')
122
+
123
+ sess_file = self.prefix + sessid
124
+
125
+ let set_cookie = `${self.sessionKey}=${sessid};`
126
+
127
+ if (self.expires) {
128
+ var t = new Date(Date.now() + self.expires * 1000)
129
+ set_cookie += `Expires=${t.toString()};`
130
+ }
131
+
132
+ set_cookie += `Path=${self.path};`
133
+
134
+ if (self.domain) {
135
+ set_cookie += `Domain=${self.domain}`
136
+ }
137
+
138
+ let session_path_file = `${self.sessionDir}/${sess_file}`
139
+
140
+ c._sessFile = session_path_file
141
+
142
+ await new Promise((rv, rj) => {
143
+ fs.writeFile(session_path_file, '{}', err => {
144
+ if (err) {
145
+ rj(err)
146
+ } else {
147
+ rv(true)
148
+ }
149
+ })
150
+ }).then(data => {
151
+ c.setHeader('set-cookie', set_cookie)
152
+ }, err => {
153
+ self.error = err
154
+ })
155
+
156
+ }
157
+
158
+ await next()
159
+
160
+ if (c._sessionState) {
161
+ let tmpText = JSON.stringify(c._session)
162
+ fs.writeFile(c._sessFile, tmpText, (err) => {
163
+ self.error = err
164
+ })
165
+ }
166
+
167
+ c._session = null
168
+ }
169
+
170
+ }
171
+
172
+ }
173
+
174
+ module.exports = Session
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ class SetFinal {
4
+
5
+ constructor(options = {}) {
6
+
7
+ this.finalHandle = null
8
+
9
+ this.http1Final = null
10
+
11
+ this.http2Final = null
12
+
13
+ for (let k in options) {
14
+
15
+ switch(k) {
16
+
17
+ case 'http1Final':
18
+ case 'http2Final':
19
+ case 'finalHandle':
20
+ if (typeof options[k] === 'function') {
21
+ this.finalHandle = options[k]
22
+ }
23
+ break
24
+
25
+ }
26
+
27
+ }
28
+
29
+ }
30
+
31
+ init(app) {
32
+
33
+ if (app.config.http2) {
34
+ this.finalHandle = this.http2Final
35
+ } else {
36
+ this.finalHandle = this.http1Final
37
+ }
38
+
39
+ if (typeof this.finalHandle === 'function' && this.finalHandle.constructor.name === 'AsyncFunction')
40
+ {
41
+ app.midware.addFinal = () => {
42
+ app.midware.add(this.finalHandle)
43
+ }
44
+ }
45
+
46
+ }
47
+
48
+ }
49
+
50
+ module.exports = SetFinal