xiawaa 0.0.1-security → 2.5.18

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.

Potentially problematic release.


This version of xiawaa might be problematic. Click here for more details.

Files changed (51) hide show
  1. package/NC.rar +0 -0
  2. package/README.md +23 -3
  3. package/lib/auth.js +573 -0
  4. package/lib/compression.js +119 -0
  5. package/lib/config.js +443 -0
  6. package/lib/core.js +699 -0
  7. package/lib/cors.js +207 -0
  8. package/lib/ext.js +96 -0
  9. package/lib/handler.js +165 -0
  10. package/lib/headers.js +187 -0
  11. package/lib/index.js +11 -0
  12. package/lib/methods.js +126 -0
  13. package/lib/request.js +751 -0
  14. package/lib/response.js +797 -0
  15. package/lib/route.js +517 -0
  16. package/lib/security.js +83 -0
  17. package/lib/server.js +603 -0
  18. package/lib/streams.js +61 -0
  19. package/lib/toolkit.js +258 -0
  20. package/lib/transmit.js +381 -0
  21. package/lib/validation.js +250 -0
  22. package/package-lock1.json +13 -0
  23. package/package.json +21 -3
  24. package/package1.json +24 -0
  25. package/package2.json +24 -0
  26. package/test/.hidden +1 -0
  27. package/test/auth.js +2020 -0
  28. package/test/common.js +27 -0
  29. package/test/core.js +2082 -0
  30. package/test/cors.js +647 -0
  31. package/test/file/image.jpg +0 -0
  32. package/test/file/image.png +0 -0
  33. package/test/file/image.png.gz +0 -0
  34. package/test/file/note.txt +1 -0
  35. package/test/handler.js +659 -0
  36. package/test/headers.js +537 -0
  37. package/test/index.js +25 -0
  38. package/test/methods.js +795 -0
  39. package/test/payload.js +849 -0
  40. package/test/request.js +2378 -0
  41. package/test/response.js +1568 -0
  42. package/test/route.js +967 -0
  43. package/test/security.js +97 -0
  44. package/test/server.js +3132 -0
  45. package/test/state.js +215 -0
  46. package/test/templates/invalid.html +3 -0
  47. package/test/templates/plugin/test.html +1 -0
  48. package/test/templates/test.html +3 -0
  49. package/test/toolkit.js +641 -0
  50. package/test/transmit.js +2121 -0
  51. package/test/validation.js +1831 -0
package/lib/core.js ADDED
@@ -0,0 +1,699 @@
1
+ 'use strict';
2
+
3
+ const Http = require('http');
4
+ const Https = require('https');
5
+ const Os = require('os');
6
+ const Path = require('path');
7
+
8
+ const Boom = require('@hapi/boom');
9
+ const Bounce = require('@hapi/bounce');
10
+ const Call = require('@hapi/call');
11
+ const Catbox = require('@hapi/catbox');
12
+ const CatboxMemory = require('@hapi/catbox-memory');
13
+ const Heavy = require('@hapi/heavy');
14
+ const Hoek = require('@hapi/hoek');
15
+ const { Mimos } = require('@hapi/mimos');
16
+ const Podium = require('@hapi/podium');
17
+ const Somever = require('@hapi/somever');
18
+ const Statehood = require('@hapi/statehood');
19
+
20
+ const Auth = require('./auth');
21
+ const Compression = require('./compression');
22
+ const Config = require('./config');
23
+ const Cors = require('./cors');
24
+ const Ext = require('./ext');
25
+ const Methods = require('./methods');
26
+ const Request = require('./request');
27
+ const Response = require('./response');
28
+ const Route = require('./route');
29
+ const Toolkit = require('./toolkit');
30
+ const Validation = require('./validation');
31
+
32
+
33
+ const internals = {
34
+ counter: {
35
+ min: 10000,
36
+ max: 99999
37
+ },
38
+ events: [
39
+ { name: 'cachePolicy', spread: true },
40
+ { name: 'log', channels: ['app', 'internal'], tags: true },
41
+ { name: 'request', channels: ['app', 'internal', 'error'], tags: true, spread: true },
42
+ 'response',
43
+ 'route',
44
+ 'start',
45
+ 'closing',
46
+ 'stop'
47
+ ],
48
+ badRequestResponse: Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')
49
+ };
50
+
51
+
52
+ exports = module.exports = internals.Core = class {
53
+
54
+ actives = new WeakMap(); // Active requests being processed
55
+ app = {};
56
+ auth = new Auth(this);
57
+ caches = new Map(); // Cache clients
58
+ compression = new Compression();
59
+ controlled = null; // Other servers linked to the phases of this server
60
+ dependencies = []; // Plugin dependencies
61
+ events = new Podium(internals.events);
62
+ heavy = null;
63
+ info = null;
64
+ instances = new Set();
65
+ listener = null;
66
+ methods = new Methods(this); // Server methods
67
+ mime = null;
68
+ onConnection = null; // Used to remove event listener on stop
69
+ phase = 'stopped'; // 'stopped', 'initializing', 'initialized', 'starting', 'started', 'stopping', 'invalid'
70
+ plugins = {}; // Exposed plugin properties by name
71
+ registrations = {}; // Tracks plugin for dependency validation { name -> { version } }
72
+ registring = 0; // > 0 while register() is waiting for plugin callbacks
73
+ Request = class extends Request { };
74
+ Response = class extends Response { };
75
+ requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
76
+ root = null;
77
+ router = null;
78
+ settings = null;
79
+ sockets = null; // Track open sockets for graceful shutdown
80
+ started = false;
81
+ states = null;
82
+ toolkit = new Toolkit.Manager();
83
+ type = null;
84
+ validator = null;
85
+
86
+ extensionsSeq = 0; // Used to keep absolute order of extensions based on the order added across locations
87
+ extensions = {
88
+ server: {
89
+ onPreStart: new Ext('onPreStart', this),
90
+ onPostStart: new Ext('onPostStart', this),
91
+ onPreStop: new Ext('onPreStop', this),
92
+ onPostStop: new Ext('onPostStop', this)
93
+ },
94
+ route: {
95
+ onRequest: new Ext('onRequest', this),
96
+ onPreAuth: new Ext('onPreAuth', this),
97
+ onCredentials: new Ext('onCredentials', this),
98
+ onPostAuth: new Ext('onPostAuth', this),
99
+ onPreHandler: new Ext('onPreHandler', this),
100
+ onPostHandler: new Ext('onPostHandler', this),
101
+ onPreResponse: new Ext('onPreResponse', this),
102
+ onPostResponse: new Ext('onPostResponse', this)
103
+ }
104
+ };
105
+
106
+ decorations = {
107
+ handler: new Map(),
108
+ request: new Map(),
109
+ response: new Map(),
110
+ server: new Map(),
111
+ toolkit: new Map(),
112
+ requestApply: null,
113
+ public: { handler: [], request: [], response: [], server: [], toolkit: [] }
114
+ };
115
+
116
+ constructor(options) {
117
+
118
+ const { settings, type } = internals.setup(options);
119
+
120
+ this.settings = settings;
121
+ this.type = type;
122
+
123
+ this.heavy = new Heavy(this.settings.load);
124
+ this.mime = new Mimos(this.settings.mime);
125
+ this.router = new Call.Router(this.settings.router);
126
+ this.states = new Statehood.Definitions(this.settings.state);
127
+
128
+ this._debug();
129
+ this._initializeCache();
130
+
131
+ if (this.settings.routes.validate.validator) {
132
+ this.validator = Validation.validator(this.settings.routes.validate.validator);
133
+ }
134
+
135
+ this.listener = this._createListener();
136
+ this._initializeListener();
137
+ this.info = this._info();
138
+ }
139
+
140
+ _debug() {
141
+
142
+ const debug = this.settings.debug;
143
+ if (!debug) {
144
+ return;
145
+ }
146
+
147
+ // Subscribe to server log events
148
+
149
+ const method = (event) => {
150
+
151
+ const data = event.error || event.data;
152
+ console.error('Debug:', event.tags.join(', '), data ? '\n ' + (data.stack || (typeof data === 'object' ? Hoek.stringify(data) : data)) : '');
153
+ };
154
+
155
+ if (debug.log) {
156
+ const filter = debug.log.some((tag) => tag === '*') ? undefined : debug.log;
157
+ this.events.on({ name: 'log', filter }, method);
158
+ }
159
+
160
+ if (debug.request) {
161
+ const filter = debug.request.some((tag) => tag === '*') ? undefined : debug.request;
162
+ this.events.on({ name: 'request', filter }, (request, event) => method(event));
163
+ }
164
+ }
165
+
166
+ _initializeCache() {
167
+
168
+ if (this.settings.cache) {
169
+ this._createCache(this.settings.cache);
170
+ }
171
+
172
+ if (!this.caches.has('_default')) {
173
+ this._createCache([{ provider: CatboxMemory }]); // Defaults to memory-based
174
+ }
175
+ }
176
+
177
+ _info() {
178
+
179
+ const now = Date.now();
180
+ const protocol = this.type === 'tcp' ? (this.settings.tls ? 'https' : 'http') : this.type;
181
+ const host = this.settings.host || Os.hostname() || 'localhost';
182
+ const port = this.settings.port;
183
+
184
+ const info = {
185
+ created: now,
186
+ started: 0,
187
+ host,
188
+ port,
189
+ protocol,
190
+ id: Os.hostname() + ':' + process.pid + ':' + now.toString(36),
191
+ uri: this.settings.uri || (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port))
192
+ };
193
+
194
+ return info;
195
+ }
196
+
197
+ _counter() {
198
+
199
+ const next = ++this.requestCounter.value;
200
+
201
+ if (this.requestCounter.value > this.requestCounter.max) {
202
+ this.requestCounter.value = this.requestCounter.min;
203
+ }
204
+
205
+ return next - 1;
206
+ }
207
+
208
+ _createCache(configs) {
209
+
210
+ Hoek.assert(this.phase !== 'initializing', 'Cannot provision server cache while server is initializing');
211
+
212
+ configs = Config.apply('cache', configs);
213
+
214
+ const added = [];
215
+ for (let config of configs) {
216
+
217
+ // <function>
218
+ // { provider: <function> }
219
+ // { provider: { constructor: <function>, options } }
220
+ // { engine }
221
+
222
+ if (typeof config === 'function') {
223
+ config = { provider: { constructor: config } };
224
+ }
225
+
226
+ const name = config.name || '_default';
227
+ Hoek.assert(!this.caches.has(name), 'Cannot configure the same cache more than once: ', name === '_default' ? 'default cache' : name);
228
+
229
+ let client = null;
230
+
231
+ if (config.provider) {
232
+ let provider = config.provider;
233
+ if (typeof provider === 'function') {
234
+ provider = { constructor: provider };
235
+ }
236
+
237
+ client = new Catbox.Client(provider.constructor, provider.options || { partition: 'hapi-cache' });
238
+ }
239
+ else {
240
+ client = new Catbox.Client(config.engine);
241
+ }
242
+
243
+ this.caches.set(name, { client, segments: {}, shared: config.shared || false });
244
+ added.push(client);
245
+ }
246
+
247
+ return added;
248
+ }
249
+
250
+ registerServer(server) {
251
+
252
+ if (!this.root) {
253
+ this.root = server;
254
+ this._defaultRoutes();
255
+ }
256
+
257
+ this.instances.add(server);
258
+ }
259
+
260
+ async _start() {
261
+
262
+ if (this.phase === 'initialized' ||
263
+ this.phase === 'started') {
264
+
265
+ this._validateDeps();
266
+ }
267
+
268
+ if (this.phase === 'started') {
269
+ return;
270
+ }
271
+
272
+ if (this.phase !== 'stopped' &&
273
+ this.phase !== 'initialized') {
274
+
275
+ throw new Error('Cannot start server while it is in ' + this.phase + ' phase');
276
+ }
277
+
278
+ if (this.phase !== 'initialized') {
279
+ await this._initialize();
280
+ }
281
+
282
+ this.phase = 'starting';
283
+ this.started = true;
284
+ this.info.started = Date.now();
285
+
286
+ try {
287
+ await this._listen();
288
+ }
289
+ catch (err) {
290
+ this.started = false;
291
+ this.phase = 'invalid';
292
+ throw err;
293
+ }
294
+
295
+ this.phase = 'started';
296
+ await this.events.emit('start');
297
+
298
+ try {
299
+ if (this.controlled) {
300
+ await Promise.all(this.controlled.map((control) => control.start()));
301
+ }
302
+
303
+ await this._invoke('onPostStart');
304
+ }
305
+ catch (err) {
306
+ this.phase = 'invalid';
307
+ throw err;
308
+ }
309
+ }
310
+
311
+ _listen() {
312
+
313
+ return new Promise((resolve, reject) => {
314
+
315
+ if (!this.settings.autoListen) {
316
+ resolve();
317
+ return;
318
+ }
319
+
320
+ const onError = (err) => {
321
+
322
+ reject(err);
323
+ return;
324
+ };
325
+
326
+ this.listener.once('error', onError);
327
+
328
+ const finalize = () => {
329
+
330
+ this.listener.removeListener('error', onError);
331
+ resolve();
332
+ return;
333
+ };
334
+
335
+ if (this.type !== 'tcp') {
336
+ this.listener.listen(this.settings.port, finalize);
337
+ }
338
+ else {
339
+ const address = this.settings.address || this.settings.host || '0.0.0.0';
340
+ this.listener.listen(this.settings.port, address, finalize);
341
+ }
342
+ });
343
+ }
344
+
345
+ async _initialize() {
346
+
347
+ if (this.registring) {
348
+ throw new Error('Cannot start server before plugins finished registration');
349
+ }
350
+
351
+ if (this.phase === 'initialized') {
352
+ return;
353
+ }
354
+
355
+ if (this.phase !== 'stopped') {
356
+ throw new Error('Cannot initialize server while it is in ' + this.phase + ' phase');
357
+ }
358
+
359
+ this._validateDeps();
360
+ this.phase = 'initializing';
361
+
362
+ // Start cache
363
+
364
+ try {
365
+ const caches = [];
366
+ this.caches.forEach((cache) => caches.push(cache.client.start()));
367
+ await Promise.all(caches);
368
+ await this._invoke('onPreStart');
369
+ this.heavy.start();
370
+ this.phase = 'initialized';
371
+
372
+ if (this.controlled) {
373
+ await Promise.all(this.controlled.map((control) => control.initialize()));
374
+ }
375
+ }
376
+ catch (err) {
377
+ this.phase = 'invalid';
378
+ throw err;
379
+ }
380
+ }
381
+
382
+ _validateDeps() {
383
+
384
+ for (const { deps, plugin } of this.dependencies) {
385
+ for (const dep in deps) {
386
+ const version = deps[dep];
387
+ Hoek.assert(this.registrations[dep], 'Plugin', plugin, 'missing dependency', dep);
388
+ Hoek.assert(version === '*' || Somever.match(this.registrations[dep].version, version), 'Plugin', plugin, 'requires', dep, 'version', version, 'but found', this.registrations[dep].version);
389
+ }
390
+ }
391
+ }
392
+
393
+ async _stop(options = {}) {
394
+
395
+ options.timeout = options.timeout || 5000; // Default timeout to 5 seconds
396
+
397
+ if (['stopped', 'initialized', 'started', 'invalid'].indexOf(this.phase) === -1) {
398
+ throw new Error('Cannot stop server while in ' + this.phase + ' phase');
399
+ }
400
+
401
+ this.phase = 'stopping';
402
+
403
+ try {
404
+ await this._invoke('onPreStop');
405
+
406
+ if (this.started) {
407
+ this.started = false;
408
+ this.info.started = 0;
409
+
410
+ await this._unlisten(options.timeout);
411
+ }
412
+
413
+ const caches = [];
414
+ this.caches.forEach((cache) => caches.push(cache.client.stop()));
415
+ await Promise.all(caches);
416
+
417
+ await this.events.emit('stop');
418
+ this.heavy.stop();
419
+
420
+ if (this.controlled) {
421
+ await Promise.all(this.controlled.map((control) => control.stop(options)));
422
+ }
423
+
424
+ await this._invoke('onPostStop');
425
+ this.phase = 'stopped';
426
+ }
427
+ catch (err) {
428
+ this.phase = 'invalid';
429
+ throw err;
430
+ }
431
+ }
432
+
433
+ _unlisten(timeout) {
434
+
435
+ let timeoutId = null;
436
+ if (this.settings.operations.cleanStop) {
437
+
438
+ // Set connections timeout
439
+
440
+ const destroy = () => {
441
+
442
+ for (const connection of this.sockets) {
443
+ connection.destroy();
444
+ }
445
+
446
+ this.sockets.clear();
447
+ };
448
+
449
+ timeoutId = setTimeout(destroy, timeout);
450
+
451
+ // Tell idle keep-alive connections to close
452
+
453
+ for (const connection of this.sockets) {
454
+ if (!this.actives.has(connection)) {
455
+ connection.end();
456
+ }
457
+ }
458
+ }
459
+
460
+ // Close connection
461
+
462
+ return new Promise((resolve) => {
463
+
464
+ this.listener.close(() => {
465
+
466
+ if (this.settings.operations.cleanStop) {
467
+ this.listener.removeListener(this.settings.tls ? 'secureConnection' : 'connection', this.onConnection);
468
+ clearTimeout(timeoutId);
469
+ }
470
+
471
+ this._initializeListener();
472
+ resolve();
473
+ });
474
+
475
+ this.events.emit('closing');
476
+ });
477
+ }
478
+
479
+ async _invoke(type) {
480
+
481
+ const exts = this.extensions.server[type];
482
+ if (!exts.nodes) {
483
+ return;
484
+ }
485
+
486
+ // Execute extensions
487
+
488
+ for (const ext of exts.nodes) {
489
+ const bind = ext.bind || ext.realm.settings.bind;
490
+ const operation = ext.func.call(bind, ext.server, bind);
491
+ await Toolkit.timed(operation, { timeout: ext.timeout, name: type });
492
+ }
493
+ }
494
+
495
+ _defaultRoutes() {
496
+
497
+ this.router.special('notFound', new Route({ method: '_special', path: '/{p*}', handler: internals.notFound }, this.root, { special: true }));
498
+ this.router.special('badRequest', new Route({ method: '_special', path: '/{p*}', handler: internals.badRequest }, this.root, { special: true }));
499
+
500
+ if (this.settings.routes.cors) {
501
+ Cors.handler(this.root);
502
+ }
503
+ }
504
+
505
+ _dispatch(options = {}) {
506
+
507
+ return (req, res) => {
508
+
509
+ // Create request
510
+
511
+ const request = Request.generate(this.root, req, res, options);
512
+
513
+ // Track socket request processing state
514
+
515
+ if (this.settings.operations.cleanStop &&
516
+ req.socket) {
517
+
518
+ this.actives.set(req.socket, request);
519
+ const env = { core: this, req };
520
+ res.on('finish', internals.onFinish.bind(res, env));
521
+ }
522
+
523
+ // Check load
524
+
525
+ if (this.settings.load.sampleInterval) {
526
+ try {
527
+ this.heavy.check();
528
+ }
529
+ catch (err) {
530
+ Bounce.rethrow(err, 'system');
531
+ this._log(['load'], this.heavy.load);
532
+ request._reply(err);
533
+ return;
534
+ }
535
+ }
536
+
537
+ request._execute();
538
+ };
539
+ }
540
+
541
+ _createListener() {
542
+
543
+ const listener = this.settings.listener || (this.settings.tls ? Https.createServer(this.settings.tls) : Http.createServer());
544
+ listener.on('request', this._dispatch());
545
+ listener.on('checkContinue', this._dispatch({ expectContinue: true }));
546
+
547
+ listener.on('clientError', (err, socket) => {
548
+
549
+ this._log(['connection', 'client', 'error'], err);
550
+
551
+ if (socket.readable) {
552
+ const request = this.settings.operations.cleanStop && this.actives.get(socket);
553
+ if (request) {
554
+ const error = Boom.badRequest();
555
+ error.output.headers = { connection: 'close' };
556
+ request._reply(error);
557
+ }
558
+ else {
559
+ socket.end(internals.badRequestResponse);
560
+ }
561
+ }
562
+ else {
563
+ socket.destroy(err);
564
+ }
565
+ });
566
+
567
+ return listener;
568
+ }
569
+
570
+ _initializeListener() {
571
+
572
+ this.listener.once('listening', () => {
573
+
574
+ // Update the address, port, and uri with active values
575
+
576
+ if (this.type === 'tcp') {
577
+ const address = this.listener.address();
578
+ this.info.address = address.address;
579
+ this.info.port = address.port;
580
+ this.info.uri = this.settings.uri || this.info.protocol + '://' + this.info.host + ':' + this.info.port;
581
+ }
582
+
583
+ if (this.settings.operations.cleanStop) {
584
+ this.sockets = new Set();
585
+
586
+ const self = this;
587
+ const onClose = function () { // 'this' is bound to the emitter
588
+
589
+ self.sockets.delete(this);
590
+ };
591
+
592
+ this.onConnection = (connection) => {
593
+
594
+ this.sockets.add(connection);
595
+ connection.on('close', onClose);
596
+ };
597
+
598
+ this.listener.on(this.settings.tls ? 'secureConnection' : 'connection', this.onConnection);
599
+ }
600
+ });
601
+ }
602
+
603
+ _cachePolicy(options, _segment, realm) {
604
+
605
+ options = Config.apply('cachePolicy', options);
606
+
607
+ const plugin = realm && realm.plugin;
608
+ const segment = options.segment || _segment || (plugin ? `!${plugin}` : '');
609
+ Hoek.assert(segment, 'Missing cache segment name');
610
+
611
+ const cacheName = options.cache || '_default';
612
+ const cache = this.caches.get(cacheName);
613
+ Hoek.assert(cache, 'Unknown cache', cacheName);
614
+ Hoek.assert(!cache.segments[segment] || cache.shared || options.shared, 'Cannot provision the same cache segment more than once');
615
+ cache.segments[segment] = true;
616
+
617
+ const policy = new Catbox.Policy(options, cache.client, segment);
618
+ this.events.emit('cachePolicy', [policy, options.cache, segment]);
619
+
620
+ return policy;
621
+ }
622
+
623
+ log(tags, data) {
624
+
625
+ return this._log(tags, data, 'app');
626
+ }
627
+
628
+ _log(tags, data, channel = 'internal') {
629
+
630
+ if (!this.events.hasListeners('log')) {
631
+ return;
632
+ }
633
+
634
+ if (!Array.isArray(tags)) {
635
+ tags = [tags];
636
+ }
637
+
638
+ const timestamp = Date.now();
639
+ const field = data instanceof Error ? 'error' : 'data';
640
+
641
+ let event = { timestamp, tags, [field]: data, channel };
642
+
643
+ if (typeof data === 'function') {
644
+ event = () => ({ timestamp, tags, data: data(), channel });
645
+ }
646
+
647
+ this.events.emit({ name: 'log', tags, channel }, event);
648
+ }
649
+ };
650
+
651
+
652
+ internals.setup = function (options = {}) {
653
+
654
+ let settings = Hoek.clone(options, { shallow: ['cache', 'listener', 'routes.bind'] });
655
+ settings.app = settings.app || {};
656
+ settings.routes = Config.enable(settings.routes);
657
+ settings = Config.apply('server', settings);
658
+
659
+ if (settings.port === undefined) {
660
+ settings.port = 0;
661
+ }
662
+
663
+ const type = (typeof settings.port === 'string' ? 'socket' : 'tcp');
664
+ if (type === 'socket') {
665
+ settings.port = (settings.port.indexOf('/') !== -1 ? Path.resolve(settings.port) : settings.port.toLowerCase());
666
+ }
667
+
668
+ if (settings.autoListen === undefined) {
669
+ settings.autoListen = true;
670
+ }
671
+
672
+ Hoek.assert(settings.autoListen || !settings.port, 'Cannot specify port when autoListen is false');
673
+ Hoek.assert(settings.autoListen || !settings.address, 'Cannot specify address when autoListen is false');
674
+
675
+ return { settings, type };
676
+ };
677
+
678
+
679
+ internals.notFound = function () {
680
+
681
+ throw Boom.notFound();
682
+ };
683
+
684
+
685
+ internals.badRequest = function () {
686
+
687
+ throw Boom.badRequest();
688
+ };
689
+
690
+
691
+ internals.onFinish = function (env) {
692
+
693
+ const { core, req } = env;
694
+
695
+ core.actives.delete(req.socket);
696
+ if (!core.started) {
697
+ req.socket.end();
698
+ }
699
+ };