ultimate-express 2.0.8 → 2.0.10

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.
package/src/response.js CHANGED
@@ -1,869 +1,869 @@
1
- /*
2
- Copyright 2024 dimden.dev
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- const cookie = require("cookie");
18
- const mime = require("mime-types");
19
- const vary = require("vary");
20
- const encodeUrl = require("encodeurl");
21
- const {
22
- normalizeType, stringify, deprecated, UP_PATH_REGEXP, decode,
23
- containsDotFile, isPreconditionFailure, isRangeFresh, NullObject
24
- } = require("./utils.js");
25
- const { Writable } = require("stream");
26
- const { isAbsolute } = require("path");
27
- const fs = require("fs");
28
- const Path = require("path");
29
- const statuses = require("statuses");
30
- const { sign } = require("cookie-signature");
31
- // events is faster at init, tseep is faster at sending events
32
- // since we create a ton of objects and dont send a ton of events, its better to use events here
33
- const { EventEmitter } = require("events");
34
- const http = require("http");
35
- const ms = require('ms');
36
- const etag = require("etag");
37
-
38
- const outgoingMessage = new http.OutgoingMessage();
39
- const symbols = Object.getOwnPropertySymbols(outgoingMessage);
40
- const kOutHeaders = symbols.find(s => s.toString() === 'Symbol(kOutHeaders)');
41
- const HIGH_WATERMARK = 256 * 1024;
42
-
43
- class Socket extends EventEmitter {
44
- constructor(response) {
45
- super();
46
- this.response = response;
47
-
48
- this.on('error', (err) => {
49
- this.emit('close');
50
- });
51
- }
52
- get writable() {
53
- return !this.response.finished;
54
- }
55
-
56
- end(body) {
57
- this.response.end(body);
58
- }
59
-
60
- close() {
61
- if(this.response.finished) {
62
- return;
63
- }
64
- this.response.finished = true;
65
- this.emit('close');
66
- this.response._res.close();
67
- }
68
- }
69
-
70
- module.exports = class Response extends Writable {
71
- #socket = null;
72
- #ended = false;
73
- #pendingChunks = [];
74
- #lastWriteChunkTime = 0;
75
- #writeTimeout = null;
76
- constructor(res, req, app) {
77
- super();
78
- this._req = req;
79
- this._res = res;
80
- this.headersSent = false;
81
- this.app = app;
82
- this.locals = new NullObject();
83
- this.finished = false;
84
- this.aborted = false;
85
- this.statusCode = 200;
86
- this.statusText = undefined;
87
- this.chunkedTransfer = true;
88
- this.totalSize = 0;
89
- this.writingChunk = false;
90
- this.headers = {
91
- 'connection': 'keep-alive',
92
- 'keep-alive': 'timeout=10'
93
- };
94
- if(this.app.get('x-powered-by')) {
95
- this.headers['x-powered-by'] = 'UltimateExpress';
96
- }
97
-
98
- // support for node internal
99
- this[kOutHeaders] = new Proxy(this.headers, {
100
- set: (obj, prop, value) => {
101
- this.set(prop, value[1]);
102
- return true;
103
- },
104
- get: (obj, prop) => {
105
- return obj[prop];
106
- }
107
- });
108
- this.body = undefined;
109
- this.on('error', (err) => {
110
- if(this.finished) {
111
- return;
112
- }
113
- this._res.cork(() => {
114
- this._res.close();
115
- this.finished = true;
116
- this.#socket?.emit('close');
117
- });
118
- });
119
- this.once('close', () => {
120
- this.#ended = true
121
- })
122
- }
123
-
124
- get socket() {
125
- if(this.#ended) return null;
126
- if(!this.#socket) {
127
- this.#socket = new Socket(this);
128
- }
129
- return this.#socket;
130
- }
131
-
132
- _write(chunk, encoding, callback) {
133
- if (this.aborted) {
134
- const err = new Error('Request aborted');
135
- err.code = 'ECONNABORTED';
136
- return this.destroy(err);
137
- }
138
- if (this.finished) {
139
- const err = new Error('Response already finished');
140
- return this.destroy(err);
141
- }
142
-
143
- this.writingChunk = true;
144
- this._res.cork(() => {
145
- if (!this.headersSent) {
146
- this.writeHead(this.statusCode);
147
- const statusMessage = this.statusText ?? statuses.message[this.statusCode] ?? '';
148
- this._res.writeStatus(`${this.statusCode} ${statusMessage}`.trim());
149
- this.writeHeaders(typeof chunk === 'string');
150
- }
151
-
152
- if (!Buffer.isBuffer(chunk) && !(chunk instanceof ArrayBuffer)) {
153
- chunk = Buffer.from(chunk);
154
- chunk = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
155
- }
156
-
157
- if (this.chunkedTransfer) {
158
- this.#pendingChunks.push(chunk);
159
- const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
160
- const now = performance.now();
161
- // the first chunk is sent immediately (!this.#lastWriteChunkTime)
162
- // the other chunks are sent when watermark is reached (size >= HIGH_WATERMARK)
163
- // or if elapsed 50ms of last send (now - this.#lastWriteChunkTime > 50)
164
- if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime > 50) {
165
- this._res.write(Buffer.concat(this.#pendingChunks, size));
166
- this.#pendingChunks = [];
167
- this.#lastWriteChunkTime = now;
168
- if(this.#writeTimeout) {
169
- clearTimeout(this.#writeTimeout);
170
- this.#writeTimeout = null;
171
- }
172
- } else if(!this.#writeTimeout) {
173
- this.#writeTimeout = setTimeout(() => {
174
- this.#writeTimeout = null;
175
- if(!this.finished && !this.aborted) this._res.cork(() => {
176
- if(this.#pendingChunks.length) {
177
- const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
178
- this._res.write(Buffer.concat(this.#pendingChunks, size));
179
- this.#pendingChunks = [];
180
- this.#lastWriteChunkTime = performance.now();
181
- }
182
- });
183
- }, 50);
184
- this.#writeTimeout.unref();
185
- }
186
- this.writingChunk = false;
187
- callback(null);
188
- } else {
189
- const lastOffset = this._res.getWriteOffset();
190
- const [ok, done] = this._res.tryEnd(chunk, this.totalSize);
191
- if (done) {
192
- super.end();
193
- this.finished = true;
194
- this.writingChunk = false;
195
- this.#socket?.emit('close');
196
- callback(null);
197
- } else if (!ok) {
198
- this._res.ab = chunk;
199
- this._res.abOffset = lastOffset;
200
- this._res.onWritable((offset) => {
201
- if (this.finished) return true;
202
- const [ok, done] = this._res.tryEnd(this._res.ab.slice(offset - this._res.abOffset), this.totalSize);
203
- if (done) {
204
- this.finished = true;
205
- this.#socket?.emit('close');
206
- }
207
- if (ok) {
208
- this.writingChunk = false;
209
- callback(null);
210
- }
211
- return ok;
212
- });
213
- } else {
214
- this.writingChunk = false;
215
- callback(null);
216
- }
217
- }
218
- });
219
- }
220
- writeHead(statusCode, statusMessage, headers) {
221
- this.statusCode = statusCode;
222
- if(typeof statusMessage === 'string') {
223
- this.statusText = statusMessage;
224
- }
225
- if(!headers) {
226
- if(!statusMessage) return this;
227
- headers = statusMessage;
228
- }
229
- for(let header in headers) {
230
- this.set(header, headers[header]);
231
- }
232
- return this;
233
- }
234
- writeHeaders(utf8) {
235
- for(const header in this.headers) {
236
- const value = this.headers[header];
237
- if(header === 'content-length') {
238
- // if content-length is set, disable chunked transfer encoding, since size is known
239
- this.chunkedTransfer = false;
240
- this.totalSize = parseInt(value);
241
- continue;
242
- }
243
- if(Array.isArray(value)) {
244
- for(let val of value) {
245
- this._res.writeHeader(header, val);
246
- }
247
- } else {
248
- this._res.writeHeader(header, value);
249
- }
250
- }
251
- this.headersSent = true;
252
- }
253
- _implicitHeader() {
254
- // compatibility function
255
- // usually should send headers but this is useless for us
256
- this.writeHead(this.statusCode);
257
- }
258
- status(code) {
259
- this.statusCode = parseInt(code);
260
- return this;
261
- }
262
- sendStatus(code) {
263
- return this.status(code).send(statuses.message[+code] ?? code.toString());
264
- }
265
- end(data, cb) {
266
- if(typeof data === 'function') {
267
- cb = data;
268
- data = undefined;
269
- }
270
- if(typeof cb !== 'function') {
271
- cb = undefined; // silence the error?
272
- }
273
-
274
- if(this.writingChunk) {
275
- this.once('drain', () => {
276
- this.end(data, cb);
277
- });
278
- return;
279
- }
280
- if(this.finished) {
281
- return;
282
- }
283
- this.writeHead(this.statusCode);
284
- this._res.cork(() => {
285
- if(!this.headersSent) {
286
- const etagFn = this.app.get('etag fn');
287
- if(etagFn && data && !this.headers['etag'] && !this.req.noEtag) {
288
- this.headers['etag'] = etagFn(data);
289
- }
290
- const fresh = this.req.fresh;
291
- const statusMessage = this.statusText ?? statuses.message[this.statusCode] ?? '';
292
- this._res.writeStatus(fresh ? '304 Not Modified' : `${this.statusCode} ${statusMessage}`.trim());
293
- this.writeHeaders(true);
294
- if(fresh) {
295
- this._res.end();
296
- this.finished = true;
297
- this.#socket?.emit('close');
298
- this.emit('finish');
299
- this.emit('close');
300
- cb && queueMicrotask(() => {
301
- this.#ended = true;
302
- cb();
303
- });
304
- return;
305
- }
306
- }
307
- const contentLength = this.headers['content-length'];
308
- if(!data && contentLength) {
309
- this._res.endWithoutBody(contentLength.toString());
310
- } else {
311
- if(this.#pendingChunks.length) {
312
- this._res.write(Buffer.concat(this.#pendingChunks));
313
- this.#pendingChunks = [];
314
- this.lastWriteChunkTime = 0;
315
- }
316
- if(data instanceof Buffer) {
317
- data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
318
- }
319
- if(this.req.method === 'HEAD') {
320
- const length = Buffer.byteLength(data ?? '');
321
- this._res.endWithoutBody(length.toString());
322
- } else {
323
- this._res.end(data);
324
- }
325
- }
326
-
327
- this.finished = true;
328
- this.#socket?.emit('close');
329
- this.emit('finish');
330
- this.emit('close');
331
- cb && queueMicrotask(() => {
332
- this.#ended = true;
333
- cb();
334
- });
335
- });
336
- return this;
337
- }
338
-
339
- send(body) {
340
- if(this.headersSent) {
341
- throw new Error('Can\'t write body: Response was already sent');
342
- }
343
- const isBuffer = Buffer.isBuffer(body);
344
- if(body === null || body === undefined) {
345
- body = '';
346
- return this.end(body);
347
- } else if(typeof body === 'object' && !isBuffer) {
348
- return this.json(body);
349
- } else if(typeof body === 'number') {
350
- if(arguments[1]) {
351
- deprecated('res.send(status, body)', 'res.status(status).send(body)');
352
- return this.status(body).send(arguments[1]);
353
- } else {
354
- deprecated('res.send(status)', 'res.sendStatus(status)');
355
- if(!this.headers['content-type']) {
356
- this.headers['content-type'] = 'text/plain; charset=utf-8';
357
- }
358
- return this.sendStatus(body);
359
- }
360
- } else if(typeof body === 'boolean') {
361
- return this.json(body);
362
- } else if(!isBuffer) {
363
- body = String(body);
364
- }
365
- if(typeof body === 'string' && !isBuffer) {
366
- const contentType = this.headers['content-type'];
367
- if(!contentType) {
368
- this.headers['content-type'] = 'text/html; charset=utf-8';
369
- } else if(!contentType.includes(';')) {
370
- this.headers['content-type'] += '; charset=utf-8';
371
- }
372
- } else {
373
- if(!this.headers['content-type']) {
374
- this.headers['content-type'] = 'application/octet-stream';
375
- }
376
- }
377
- return this.end(body);
378
- }
379
-
380
- sendFile(path, options = new NullObject(), callback) {
381
- if(typeof path !== 'string') {
382
- throw new TypeError('path argument is required to res.sendFile');
383
- }
384
- if(typeof options === 'function') {
385
- callback = options;
386
- options = new NullObject();
387
- }
388
- if(!options) options = new NullObject();
389
- let done = callback;
390
- if(!done) done = this.req.next;
391
- // default options
392
- if(typeof options.maxAge === 'string') {
393
- options.maxAge = ms(options.maxAge);
394
- } else if(typeof options.maxAge === 'undefined') {
395
- options.maxAge = 0;
396
- }
397
- if(typeof options.lastModified === 'undefined') {
398
- options.lastModified = true;
399
- }
400
- if(typeof options.cacheControl === 'undefined') {
401
- options.cacheControl = true;
402
- }
403
- if(typeof options.acceptRanges === 'undefined') {
404
- options.acceptRanges = true;
405
- }
406
- if(typeof options.etag === 'undefined') {
407
- options.etag = this.app.get('etag') !== false;
408
- }
409
- let etagFn = this.app.get('etag fn');
410
- if(options.etag && !etagFn) {
411
- etagFn = stat => {
412
- return etag(stat, { weak: true });
413
- }
414
- }
415
-
416
- // path checks
417
- if(!options.root && !isAbsolute(path)) {
418
- this.status(500);
419
- return done(new Error('path must be absolute or specify root to res.sendFile'));
420
- }
421
- if(!options.skipEncodePath) {
422
- path = encodeURI(path);
423
- }
424
- path = decode(path);
425
- if(path === -1) {
426
- this.status(400);
427
- return done(new Error('Bad Request'));
428
- }
429
- if(~path.indexOf('\0')) {
430
- this.status(400);
431
- return done(new Error('Bad Request'));
432
- }
433
- if(UP_PATH_REGEXP.test(path)) {
434
- this.status(403);
435
- return done(new Error('Forbidden'));
436
- }
437
- const parts = Path.normalize(path).split(Path.sep);
438
- const fullpath = options.root ? Path.resolve(Path.join(options.root, path)) : path;
439
- if(options.root && !fullpath.startsWith(Path.resolve(options.root))) {
440
- this.status(403);
441
- return done(new Error('Forbidden'));
442
- }
443
-
444
- // dotfile checks
445
- if(containsDotFile(parts)) {
446
- switch(options.dotfiles) {
447
- case 'allow':
448
- break;
449
- case 'deny':
450
- this.status(403);
451
- return done(new Error('Forbidden'));
452
- case 'ignore_files':
453
- const len = parts.length;
454
- if(len > 1 && parts[len - 1].startsWith('.')) {
455
- this.status(404);
456
- return done(new Error('Not found'));
457
- }
458
- break;
459
- case 'ignore':
460
- default:
461
- this.status(404);
462
- return done(new Error('Not found'));
463
- }
464
- }
465
-
466
- let stat = options._stat;
467
- if(!stat) {
468
- try {
469
- stat = fs.statSync(fullpath);
470
- } catch(err) {
471
- return done(err);
472
- }
473
- if(stat.isDirectory()) {
474
- this.status(404);
475
- return done(new Error(`Not found`));
476
- }
477
- }
478
-
479
- // headers
480
- if(!this.headers['content-type']) {
481
- const m = mime.lookup(fullpath);
482
- if(m) this.type(m);
483
- else this.type('application/octet-stream');
484
- }
485
- if(options.cacheControl) {
486
- this.headers['cache-control'] = `public, max-age=${options.maxAge / 1000}` + (options.immutable ? ', immutable' : '');
487
- }
488
- if(options.lastModified) {
489
- this.headers['last-modified'] = stat.mtime.toUTCString();
490
- }
491
- if(options.headers) {
492
- for(const header in options.headers) {
493
- this.set(header, options.headers[header]);
494
- }
495
- }
496
- if(options.setHeaders) {
497
- options.setHeaders(this, fullpath, stat);
498
- }
499
-
500
- // etag
501
- if(options.etag && etagFn && !this.headers['etag']) {
502
- this.headers['etag'] = etagFn(stat);
503
- }
504
- if(!options.etag) {
505
- this.req.noEtag = true;
506
- }
507
-
508
- // conditional requests
509
- if(isPreconditionFailure(this.req, this)) {
510
- this.status(412);
511
- return done(new Error('Precondition Failed'));
512
- }
513
-
514
- // range requests
515
- let offset = 0, len = stat.size, ranged = false;
516
- if(options.acceptRanges) {
517
- this.headers['accept-ranges'] = 'bytes';
518
- if(this.req.headers.range) {
519
- let ranges = this.req.range(stat.size, {
520
- combine: true
521
- });
522
-
523
- // if-range
524
- if(!isRangeFresh(this.req, this)) {
525
- ranges = -2;
526
- }
527
-
528
- if(ranges === -1) {
529
- this.status(416);
530
- this.headers['content-range'] = `bytes */${stat.size}`;
531
- return done(new Error('Range Not Satisfiable'));
532
- }
533
- if(ranges !== -2 && ranges.length === 1) {
534
- this.status(206);
535
- const range = ranges[0];
536
- this.headers['content-range'] = `bytes ${range.start}-${range.end}/${stat.size}`;
537
- offset = range.start;
538
- len = range.end - range.start + 1;
539
- ranged = true;
540
- }
541
- }
542
- }
543
-
544
- // if-modified-since, if-none-match
545
- if(this.req.fresh) {
546
- return this.end();
547
- }
548
-
549
- if(this.req.method === 'HEAD') {
550
- this.set('Content-Length', stat.size);
551
- return this.end();
552
- }
553
-
554
- // serve smaller files using workers
555
- if(this.app.workers.length && stat.size < 768 * 1024 && !ranged) {
556
- this.app.readFileWithWorker(fullpath).then((data) => {
557
- if(this._res.finished) {
558
- return;
559
- }
560
- this.end(data);
561
- if(callback) callback();
562
- }).catch((err) => {
563
- if(callback) callback(err);
564
- });
565
- } else {
566
- // larger files or range requests are piped over response
567
- let opts = {
568
- highWaterMark: HIGH_WATERMARK
569
- };
570
- if(ranged) {
571
- opts.start = offset;
572
- opts.end = Math.max(offset, offset + len - 1);
573
- }
574
- const file = fs.createReadStream(fullpath, opts);
575
- this.set('Content-Length', len);
576
- file.pipe(this);
577
- }
578
- }
579
- download(path, filename, options, callback) {
580
- let done = callback;
581
- let name = filename;
582
- let opts = options || new NullObject();
583
-
584
- // support function as second or third arg
585
- if (typeof filename === 'function') {
586
- done = filename;
587
- name = null;
588
- opts = {};
589
- } else if (typeof options === 'function') {
590
- done = options;
591
- opts = {};
592
- }
593
-
594
- // support optional filename, where options may be in it's place
595
- if (typeof filename === 'object' &&
596
- (typeof options === 'function' || options === undefined)) {
597
- name = null;
598
- opts = filename;
599
- }
600
- if(!name) {
601
- name = Path.basename(path);
602
- }
603
- if(!opts.root && !isAbsolute(path)) {
604
- opts.root = process.cwd();
605
- }
606
-
607
- this.attachment(name);
608
- this.sendFile(path, opts, done);
609
- }
610
- setHeader(field, value) {
611
- if(this.headersSent) {
612
- throw new Error('Cannot set headers after they are sent to the client');
613
- }
614
- if(typeof field !== 'string') {
615
- throw new TypeError('Header name must be a valid HTTP token');
616
- } else {
617
- field = field.toLowerCase();
618
- if(Array.isArray(value)) {
619
- this.headers[field] = value;
620
- return this;
621
- }
622
- this.headers[field] = String(value);
623
- }
624
- return this;
625
- }
626
- header(field, value) {
627
- return this.set(field, value);
628
- }
629
- set(field, value) {
630
- if(typeof field === 'object') {
631
- for(const header in field) {
632
- this.setHeader(header, field[header]);
633
- }
634
- } else {
635
- field = field.toLowerCase();
636
- if(field === 'content-type') {
637
- if(!value.includes('charset=') && (value.startsWith('text/') || value === 'application/json' || value === 'application/javascript')) {
638
- value += '; charset=utf-8';
639
- }
640
- }
641
- this.setHeader(field, value);
642
- }
643
- return this;
644
- }
645
- get(field) {
646
- return this.headers[field.toLowerCase()];
647
- }
648
- getHeader(field) {
649
- return this.get(field);
650
- }
651
- getHeaders(){
652
- return this.headers;
653
- }
654
- removeHeader(field) {
655
- delete this.headers[field.toLowerCase()];
656
- return this;
657
- }
658
- append(field, value) {
659
- field = field.toLowerCase();
660
- const old = this.headers[field];
661
- if(old) {
662
- const newVal = [];
663
- if(Array.isArray(old)) {
664
- newVal.push(...old);
665
- } else {
666
- newVal.push(old);
667
- }
668
- if(Array.isArray(value)) {
669
- newVal.push(...value);
670
- } else {
671
- newVal.push(value);
672
- }
673
- this.headers[field] = newVal;
674
- } else {
675
- this.headers[field] = value;
676
- }
677
- return this;
678
- }
679
- render(view, options, callback) {
680
- if(typeof options === 'function') {
681
- callback = options;
682
- options = {};
683
- }
684
- if(!options) {
685
- options = {};
686
- } else {
687
- options = Object.assign({}, options);
688
- }
689
- options._locals = this.locals;
690
- const done = callback || ((err, str) => {
691
- if(err) return this.req.next(err);
692
- this.send(str);
693
- });
694
-
695
- this.app.render(view, options, done);
696
- }
697
- cookie(name, value, options) {
698
- const opt = {...(options ?? {})}; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
699
- let val = typeof value === 'object' ? "j:"+JSON.stringify(value) : String(value);
700
- if(opt.maxAge != null) {
701
- const maxAge = opt.maxAge - 0;
702
- if(!isNaN(maxAge)) {
703
- opt.expires = new Date(Date.now() + maxAge);
704
- opt.maxAge = Math.floor(maxAge / 1000);
705
- }
706
- }
707
- if(opt.signed) {
708
- val = 's:' + sign(val, this.req.secret);
709
- }
710
-
711
- if(opt.path == null) {
712
- opt.path = '/';
713
- }
714
-
715
- this.append('Set-Cookie', cookie.serialize(name, val, opt));
716
- return this;
717
- }
718
- clearCookie(name, options) {
719
- const opts = { path: '/', ...options, expires: new Date(1) };
720
- delete opts.maxAge;
721
- return this.cookie(name, '', opts);
722
- }
723
- attachment(filename) {
724
- this.headers['Content-Disposition'] = `attachment; filename="${filename}"`;
725
- this.type(filename.split('.').pop());
726
- return this;
727
- }
728
- format(object) {
729
- const keys = Object.keys(object).filter(v => v !== 'default');
730
- const key = keys.length > 0 ? this.req.accepts(keys) : false;
731
-
732
- this.vary('Accept');
733
-
734
- if(key) {
735
- this.set('Content-Type', normalizeType(key).value);
736
- object[key](this.req, this, this.req.next);
737
- } else if(object.default) {
738
- object.default(this.req, this, this.req.next);
739
- } else {
740
- this.status(406).send(this.app._generateErrorPage('Not Acceptable'));
741
- }
742
-
743
- return this;
744
- }
745
- json(body) {
746
- if(!this.headers['content-type']) {
747
- this.headers['content-type'] = 'application/json; charset=utf-8';
748
- }
749
- const escape = this.app.get('json escape');
750
- const replacer = this.app.get('json replacer');
751
- const spaces = this.app.get('json spaces');
752
- this.send(stringify(body, replacer, spaces, escape));
753
- }
754
- jsonp(object) {
755
- let callback = this.req.query[this.app.get('jsonp callback name')];
756
- let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
757
- let js = false;
758
-
759
- if(Array.isArray(callback)) {
760
- callback = callback[0];
761
- }
762
-
763
- if(typeof callback === 'string' && callback.length !== 0) {
764
- callback = callback.replace(/[^\[\]\w$.]/g, '');
765
-
766
- if(body === undefined) {
767
- body = '';
768
- } else if(typeof body === 'string') {
769
- // replace chars not allowed in JavaScript that are in JSON
770
- body = body
771
- .replace(/\u2028/g, '\\u2028')
772
- .replace(/\u2029/g, '\\u2029')
773
- }
774
- body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
775
- js = true;
776
- }
777
-
778
-
779
- if(!this.headers['content-type']) {
780
- this.headers['content-type'] = `${js ? 'text/javascript' : 'application/json'}; charset=utf-8`;
781
- if(js) this.headers['X-Content-Type-Options'] = 'nosniff';
782
- }
783
-
784
- return this.send(body);
785
- }
786
- links(links) {
787
- this.headers['link'] = Object.entries(links).map(([rel, url]) => `<${url}>; rel="${rel}"`).join(', ');
788
- return this;
789
- }
790
- location(path) {
791
- if(path === 'back') {
792
- path = this.req.get('Referrer');
793
- if(!path) path = this.req.get('Referer');
794
- if(!path) path = '/';
795
- }
796
- return this.headers['location'] = encodeUrl(path);
797
- }
798
- redirect(status, url, forceHtml = false) {
799
- if(typeof status !== 'number' && !url) {
800
- url = status;
801
- status = 302;
802
- }
803
- this.location(url);
804
- this.status(status);
805
- let body;
806
- // Support text/{plain,html} by default
807
- if(forceHtml) {
808
- this.set('Content-Type', 'text/html; charset=UTF-8');
809
- body =
810
- '<!DOCTYPE html>\n' +
811
- '<html lang="en">\n' +
812
- '<head>\n' +
813
- '<meta charset="utf-8">\n' +
814
- '<title>Redirecting</title>\n' +
815
- '</head>\n' +
816
- '<body>\n' +
817
- `<pre>Redirecting to ${url.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</pre>\n` +
818
- '</body>\n' +
819
- '</html>\n';
820
- } else {
821
- this.format({
822
- text: () => {
823
- this.set('Content-Type', 'text/plain; charset=UTF-8');
824
- body = statuses.message[status] + '. Redirecting to ' + url
825
- },
826
- html: () => {
827
- this.set('Content-Type', 'text/html; charset=UTF-8');
828
- body = `<p>${statuses.message[status]}. Redirecting to ${url.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</p>`;
829
- },
830
- default: () => {
831
- this.set('Content-Type', 'text/plain; charset=UTF-8');
832
- body = '';
833
- }
834
- });
835
- }
836
- if (this.req.method === 'HEAD') {
837
- this.end();
838
- } else {
839
- this.end(body);
840
- }
841
- }
842
-
843
- type(type) {
844
- let ct = type.indexOf('/') === -1
845
- ? (mime.contentType(type) || 'application/octet-stream')
846
- : type;
847
-
848
- return this.set('content-type', ct);
849
- }
850
- contentType = this.type;
851
-
852
- vary(field) {
853
- // checks for back-compat
854
- if (!field || (Array.isArray(field) && !field.length)) {
855
- deprecate('res.vary(): Provide a field name');
856
- return this;
857
- }
858
- vary(this, field);
859
- return this;
860
- }
861
-
862
- get connection() {
863
- return this.socket;
864
- }
865
-
866
- get writableFinished() {
867
- return this.finished;
868
- }
869
- }
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const cookie = require("cookie");
18
+ const mime = require("mime-types");
19
+ const vary = require("vary");
20
+ const encodeUrl = require("encodeurl");
21
+ const {
22
+ normalizeType, stringify, deprecated, UP_PATH_REGEXP, decode,
23
+ containsDotFile, isPreconditionFailure, isRangeFresh, NullObject
24
+ } = require("./utils.js");
25
+ const { Writable } = require("stream");
26
+ const { isAbsolute } = require("path");
27
+ const fs = require("fs");
28
+ const Path = require("path");
29
+ const statuses = require("statuses");
30
+ const { sign } = require("cookie-signature");
31
+ // events is faster at init, tseep is faster at sending events
32
+ // since we create a ton of objects and dont send a ton of events, its better to use events here
33
+ const { EventEmitter } = require("events");
34
+ const http = require("http");
35
+ const ms = require('ms');
36
+ const etag = require("etag");
37
+
38
+ const outgoingMessage = new http.OutgoingMessage();
39
+ const symbols = Object.getOwnPropertySymbols(outgoingMessage);
40
+ const kOutHeaders = symbols.find(s => s.toString() === 'Symbol(kOutHeaders)');
41
+ const HIGH_WATERMARK = 256 * 1024;
42
+
43
+ class Socket extends EventEmitter {
44
+ constructor(response) {
45
+ super();
46
+ this.response = response;
47
+
48
+ this.on('error', (err) => {
49
+ this.emit('close');
50
+ });
51
+ }
52
+ get writable() {
53
+ return !this.response.finished;
54
+ }
55
+
56
+ end(body) {
57
+ this.response.end(body);
58
+ }
59
+
60
+ close() {
61
+ if(this.response.finished) {
62
+ return;
63
+ }
64
+ this.response.finished = true;
65
+ this.emit('close');
66
+ this.response._res.close();
67
+ }
68
+ }
69
+
70
+ module.exports = class Response extends Writable {
71
+ #socket = null;
72
+ #ended = false;
73
+ #pendingChunks = [];
74
+ #lastWriteChunkTime = 0;
75
+ #writeTimeout = null;
76
+ constructor(res, req, app) {
77
+ super();
78
+ this._req = req;
79
+ this._res = res;
80
+ this.headersSent = false;
81
+ this.app = app;
82
+ this.locals = new NullObject();
83
+ this.finished = false;
84
+ this.aborted = false;
85
+ this.statusCode = 200;
86
+ this.statusText = undefined;
87
+ this.chunkedTransfer = true;
88
+ this.totalSize = 0;
89
+ this.writingChunk = false;
90
+ this.headers = {
91
+ 'connection': 'keep-alive',
92
+ 'keep-alive': 'timeout=10'
93
+ };
94
+ if(this.app.get('x-powered-by')) {
95
+ this.headers['x-powered-by'] = 'UltimateExpress';
96
+ }
97
+
98
+ // support for node internal
99
+ this[kOutHeaders] = new Proxy(this.headers, {
100
+ set: (obj, prop, value) => {
101
+ this.set(prop, value[1]);
102
+ return true;
103
+ },
104
+ get: (obj, prop) => {
105
+ return obj[prop];
106
+ }
107
+ });
108
+ this.body = undefined;
109
+ this.on('error', (err) => {
110
+ if(this.finished) {
111
+ return;
112
+ }
113
+ this._res.cork(() => {
114
+ this._res.close();
115
+ this.finished = true;
116
+ this.#socket?.emit('close');
117
+ });
118
+ });
119
+ this.once('close', () => {
120
+ this.#ended = true
121
+ })
122
+ }
123
+
124
+ get socket() {
125
+ if(this.#ended) return null;
126
+ if(!this.#socket) {
127
+ this.#socket = new Socket(this);
128
+ }
129
+ return this.#socket;
130
+ }
131
+
132
+ _write(chunk, encoding, callback) {
133
+ if (this.aborted) {
134
+ const err = new Error('Request aborted');
135
+ err.code = 'ECONNABORTED';
136
+ return this.destroy(err);
137
+ }
138
+ if (this.finished) {
139
+ const err = new Error('Response already finished');
140
+ return this.destroy(err);
141
+ }
142
+
143
+ this.writingChunk = true;
144
+ this._res.cork(() => {
145
+ if (!this.headersSent) {
146
+ this.writeHead(this.statusCode);
147
+ const statusMessage = this.statusText ?? statuses.message[this.statusCode] ?? '';
148
+ this._res.writeStatus(`${this.statusCode} ${statusMessage}`.trim());
149
+ this.writeHeaders(typeof chunk === 'string');
150
+ }
151
+
152
+ if (!Buffer.isBuffer(chunk) && !(chunk instanceof ArrayBuffer)) {
153
+ chunk = Buffer.from(chunk);
154
+ chunk = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
155
+ }
156
+
157
+ if (this.chunkedTransfer) {
158
+ this.#pendingChunks.push(chunk);
159
+ const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
160
+ const now = performance.now();
161
+ // the first chunk is sent immediately (!this.#lastWriteChunkTime)
162
+ // the other chunks are sent when watermark is reached (size >= HIGH_WATERMARK)
163
+ // or if elapsed 50ms of last send (now - this.#lastWriteChunkTime > 50)
164
+ if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime > 50) {
165
+ this._res.write(Buffer.concat(this.#pendingChunks, size));
166
+ this.#pendingChunks = [];
167
+ this.#lastWriteChunkTime = now;
168
+ if(this.#writeTimeout) {
169
+ clearTimeout(this.#writeTimeout);
170
+ this.#writeTimeout = null;
171
+ }
172
+ } else if(!this.#writeTimeout) {
173
+ this.#writeTimeout = setTimeout(() => {
174
+ this.#writeTimeout = null;
175
+ if(!this.finished && !this.aborted) this._res.cork(() => {
176
+ if(this.#pendingChunks.length) {
177
+ const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
178
+ this._res.write(Buffer.concat(this.#pendingChunks, size));
179
+ this.#pendingChunks = [];
180
+ this.#lastWriteChunkTime = performance.now();
181
+ }
182
+ });
183
+ }, 50);
184
+ this.#writeTimeout.unref();
185
+ }
186
+ this.writingChunk = false;
187
+ callback(null);
188
+ } else {
189
+ const lastOffset = this._res.getWriteOffset();
190
+ const [ok, done] = this._res.tryEnd(chunk, this.totalSize);
191
+ if (done) {
192
+ super.end();
193
+ this.finished = true;
194
+ this.writingChunk = false;
195
+ this.#socket?.emit('close');
196
+ callback(null);
197
+ } else if (!ok) {
198
+ this._res.ab = chunk;
199
+ this._res.abOffset = lastOffset;
200
+ this._res.onWritable((offset) => {
201
+ if (this.finished) return true;
202
+ const [ok, done] = this._res.tryEnd(this._res.ab.slice(offset - this._res.abOffset), this.totalSize);
203
+ if (done) {
204
+ this.finished = true;
205
+ this.#socket?.emit('close');
206
+ }
207
+ if (ok) {
208
+ this.writingChunk = false;
209
+ callback(null);
210
+ }
211
+ return ok;
212
+ });
213
+ } else {
214
+ this.writingChunk = false;
215
+ callback(null);
216
+ }
217
+ }
218
+ });
219
+ }
220
+ writeHead(statusCode, statusMessage, headers) {
221
+ this.statusCode = statusCode;
222
+ if(typeof statusMessage === 'string') {
223
+ this.statusText = statusMessage;
224
+ }
225
+ if(!headers) {
226
+ if(!statusMessage) return this;
227
+ headers = statusMessage;
228
+ }
229
+ for(let header in headers) {
230
+ this.set(header, headers[header]);
231
+ }
232
+ return this;
233
+ }
234
+ writeHeaders(utf8) {
235
+ for(const header in this.headers) {
236
+ const value = this.headers[header];
237
+ if(header === 'content-length') {
238
+ // if content-length is set, disable chunked transfer encoding, since size is known
239
+ this.chunkedTransfer = false;
240
+ this.totalSize = parseInt(value);
241
+ continue;
242
+ }
243
+ if(Array.isArray(value)) {
244
+ for(let val of value) {
245
+ this._res.writeHeader(header, val);
246
+ }
247
+ } else {
248
+ this._res.writeHeader(header, value);
249
+ }
250
+ }
251
+ this.headersSent = true;
252
+ }
253
+ _implicitHeader() {
254
+ // compatibility function
255
+ // usually should send headers but this is useless for us
256
+ this.writeHead(this.statusCode);
257
+ }
258
+ status(code) {
259
+ this.statusCode = parseInt(code);
260
+ return this;
261
+ }
262
+ sendStatus(code) {
263
+ return this.status(code).send(statuses.message[+code] ?? code.toString());
264
+ }
265
+ end(data, cb) {
266
+ if(typeof data === 'function') {
267
+ cb = data;
268
+ data = undefined;
269
+ }
270
+ if(typeof cb !== 'function') {
271
+ cb = undefined; // silence the error?
272
+ }
273
+
274
+ if(this.writingChunk) {
275
+ this.once('drain', () => {
276
+ this.end(data, cb);
277
+ });
278
+ return;
279
+ }
280
+ if(this.finished) {
281
+ return;
282
+ }
283
+ this.writeHead(this.statusCode);
284
+ this._res.cork(() => {
285
+ if(!this.headersSent) {
286
+ const etagFn = this.app.get('etag fn');
287
+ if(etagFn && data && !this.headers['etag'] && !this.req.noEtag) {
288
+ this.headers['etag'] = etagFn(data);
289
+ }
290
+ const fresh = this.req.fresh;
291
+ const statusMessage = this.statusText ?? statuses.message[this.statusCode] ?? '';
292
+ this._res.writeStatus(fresh ? '304 Not Modified' : `${this.statusCode} ${statusMessage}`.trim());
293
+ this.writeHeaders(true);
294
+ if(fresh) {
295
+ this._res.end();
296
+ this.finished = true;
297
+ this.#socket?.emit('close');
298
+ this.emit('finish');
299
+ this.emit('close');
300
+ cb && queueMicrotask(() => {
301
+ this.#ended = true;
302
+ cb();
303
+ });
304
+ return;
305
+ }
306
+ }
307
+ const contentLength = this.headers['content-length'];
308
+ if(!data && contentLength) {
309
+ this._res.endWithoutBody(contentLength.toString());
310
+ } else {
311
+ if(this.#pendingChunks.length) {
312
+ this._res.write(Buffer.concat(this.#pendingChunks));
313
+ this.#pendingChunks = [];
314
+ this.lastWriteChunkTime = 0;
315
+ }
316
+ if(data instanceof Buffer) {
317
+ data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
318
+ }
319
+ if(this.req.method === 'HEAD') {
320
+ const length = Buffer.byteLength(data ?? '');
321
+ this._res.endWithoutBody(length.toString());
322
+ } else {
323
+ this._res.end(data);
324
+ }
325
+ }
326
+
327
+ this.finished = true;
328
+ this.#socket?.emit('close');
329
+ this.emit('finish');
330
+ this.emit('close');
331
+ cb && queueMicrotask(() => {
332
+ this.#ended = true;
333
+ cb();
334
+ });
335
+ });
336
+ return this;
337
+ }
338
+
339
+ send(body) {
340
+ if(this.headersSent) {
341
+ throw new Error('Can\'t write body: Response was already sent');
342
+ }
343
+ const isBuffer = Buffer.isBuffer(body);
344
+ if(body === null || body === undefined) {
345
+ body = '';
346
+ return this.end(body);
347
+ } else if(typeof body === 'object' && !isBuffer) {
348
+ return this.json(body);
349
+ } else if(typeof body === 'number') {
350
+ if(arguments[1]) {
351
+ deprecated('res.send(status, body)', 'res.status(status).send(body)');
352
+ return this.status(body).send(arguments[1]);
353
+ } else {
354
+ deprecated('res.send(status)', 'res.sendStatus(status)');
355
+ if(!this.headers['content-type']) {
356
+ this.headers['content-type'] = 'text/plain; charset=utf-8';
357
+ }
358
+ return this.sendStatus(body);
359
+ }
360
+ } else if(typeof body === 'boolean') {
361
+ return this.json(body);
362
+ } else if(!isBuffer) {
363
+ body = String(body);
364
+ }
365
+ if(typeof body === 'string' && !isBuffer) {
366
+ const contentType = this.headers['content-type'];
367
+ if(!contentType) {
368
+ this.headers['content-type'] = 'text/html; charset=utf-8';
369
+ } else if(!contentType.includes(';')) {
370
+ this.headers['content-type'] += '; charset=utf-8';
371
+ }
372
+ } else {
373
+ if(!this.headers['content-type']) {
374
+ this.headers['content-type'] = 'application/octet-stream';
375
+ }
376
+ }
377
+ return this.end(body);
378
+ }
379
+
380
+ sendFile(path, options = new NullObject(), callback) {
381
+ if(typeof path !== 'string') {
382
+ throw new TypeError('path argument is required to res.sendFile');
383
+ }
384
+ if(typeof options === 'function') {
385
+ callback = options;
386
+ options = new NullObject();
387
+ }
388
+ if(!options) options = new NullObject();
389
+ let done = callback;
390
+ if(!done) done = this.req.next;
391
+ // default options
392
+ if(typeof options.maxAge === 'string') {
393
+ options.maxAge = ms(options.maxAge);
394
+ } else if(typeof options.maxAge === 'undefined') {
395
+ options.maxAge = 0;
396
+ }
397
+ if(typeof options.lastModified === 'undefined') {
398
+ options.lastModified = true;
399
+ }
400
+ if(typeof options.cacheControl === 'undefined') {
401
+ options.cacheControl = true;
402
+ }
403
+ if(typeof options.acceptRanges === 'undefined') {
404
+ options.acceptRanges = true;
405
+ }
406
+ if(typeof options.etag === 'undefined') {
407
+ options.etag = this.app.get('etag') !== false;
408
+ }
409
+ let etagFn = this.app.get('etag fn');
410
+ if(options.etag && !etagFn) {
411
+ etagFn = stat => {
412
+ return etag(stat, { weak: true });
413
+ }
414
+ }
415
+
416
+ // path checks
417
+ if(!options.root && !isAbsolute(path)) {
418
+ this.status(500);
419
+ return done(new Error('path must be absolute or specify root to res.sendFile'));
420
+ }
421
+ if(!options.skipEncodePath) {
422
+ path = encodeURI(path);
423
+ }
424
+ path = decode(path);
425
+ if(path === -1) {
426
+ this.status(400);
427
+ return done(new Error('Bad Request'));
428
+ }
429
+ if(~path.indexOf('\0')) {
430
+ this.status(400);
431
+ return done(new Error('Bad Request'));
432
+ }
433
+ if(UP_PATH_REGEXP.test(path)) {
434
+ this.status(403);
435
+ return done(new Error('Forbidden'));
436
+ }
437
+ const parts = Path.normalize(path).split(Path.sep);
438
+ const fullpath = options.root ? Path.resolve(Path.join(options.root, path)) : path;
439
+ if(options.root && !fullpath.startsWith(Path.resolve(options.root))) {
440
+ this.status(403);
441
+ return done(new Error('Forbidden'));
442
+ }
443
+
444
+ // dotfile checks
445
+ if(containsDotFile(parts)) {
446
+ switch(options.dotfiles) {
447
+ case 'allow':
448
+ break;
449
+ case 'deny':
450
+ this.status(403);
451
+ return done(new Error('Forbidden'));
452
+ case 'ignore_files':
453
+ const len = parts.length;
454
+ if(len > 1 && parts[len - 1].startsWith('.')) {
455
+ this.status(404);
456
+ return done(new Error('Not found'));
457
+ }
458
+ break;
459
+ case 'ignore':
460
+ default:
461
+ this.status(404);
462
+ return done(new Error('Not found'));
463
+ }
464
+ }
465
+
466
+ let stat = options._stat;
467
+ if(!stat) {
468
+ try {
469
+ stat = fs.statSync(fullpath);
470
+ } catch(err) {
471
+ return done(err);
472
+ }
473
+ if(stat.isDirectory()) {
474
+ this.status(404);
475
+ return done(new Error(`Not found`));
476
+ }
477
+ }
478
+
479
+ // headers
480
+ if(!this.headers['content-type']) {
481
+ const m = mime.lookup(fullpath);
482
+ if(m) this.type(m);
483
+ else this.type('application/octet-stream');
484
+ }
485
+ if(options.cacheControl) {
486
+ this.headers['cache-control'] = `public, max-age=${options.maxAge / 1000}` + (options.immutable ? ', immutable' : '');
487
+ }
488
+ if(options.lastModified) {
489
+ this.headers['last-modified'] = stat.mtime.toUTCString();
490
+ }
491
+ if(options.headers) {
492
+ for(const header in options.headers) {
493
+ this.set(header, options.headers[header]);
494
+ }
495
+ }
496
+ if(options.setHeaders) {
497
+ options.setHeaders(this, fullpath, stat);
498
+ }
499
+
500
+ // etag
501
+ if(options.etag && etagFn && !this.headers['etag']) {
502
+ this.headers['etag'] = etagFn(stat);
503
+ }
504
+ if(!options.etag) {
505
+ this.req.noEtag = true;
506
+ }
507
+
508
+ // conditional requests
509
+ if(isPreconditionFailure(this.req, this)) {
510
+ this.status(412);
511
+ return done(new Error('Precondition Failed'));
512
+ }
513
+
514
+ // range requests
515
+ let offset = 0, len = stat.size, ranged = false;
516
+ if(options.acceptRanges) {
517
+ this.headers['accept-ranges'] = 'bytes';
518
+ if(this.req.headers.range) {
519
+ let ranges = this.req.range(stat.size, {
520
+ combine: true
521
+ });
522
+
523
+ // if-range
524
+ if(!isRangeFresh(this.req, this)) {
525
+ ranges = -2;
526
+ }
527
+
528
+ if(ranges === -1) {
529
+ this.status(416);
530
+ this.headers['content-range'] = `bytes */${stat.size}`;
531
+ return done(new Error('Range Not Satisfiable'));
532
+ }
533
+ if(ranges !== -2 && ranges.length === 1) {
534
+ this.status(206);
535
+ const range = ranges[0];
536
+ this.headers['content-range'] = `bytes ${range.start}-${range.end}/${stat.size}`;
537
+ offset = range.start;
538
+ len = range.end - range.start + 1;
539
+ ranged = true;
540
+ }
541
+ }
542
+ }
543
+
544
+ // if-modified-since, if-none-match
545
+ if(this.req.fresh) {
546
+ return this.end();
547
+ }
548
+
549
+ if(this.req.method === 'HEAD') {
550
+ this.set('Content-Length', stat.size);
551
+ return this.end();
552
+ }
553
+
554
+ // serve smaller files using workers
555
+ if(this.app.workers.length && stat.size < 768 * 1024 && !ranged) {
556
+ this.app.readFileWithWorker(fullpath).then((data) => {
557
+ if(this._res.finished) {
558
+ return;
559
+ }
560
+ this.end(data);
561
+ if(callback) callback();
562
+ }).catch((err) => {
563
+ if(callback) callback(err);
564
+ });
565
+ } else {
566
+ // larger files or range requests are piped over response
567
+ let opts = {
568
+ highWaterMark: HIGH_WATERMARK
569
+ };
570
+ if(ranged) {
571
+ opts.start = offset;
572
+ opts.end = Math.max(offset, offset + len - 1);
573
+ }
574
+ const file = fs.createReadStream(fullpath, opts);
575
+ this.set('Content-Length', len);
576
+ file.pipe(this);
577
+ }
578
+ }
579
+ download(path, filename, options, callback) {
580
+ let done = callback;
581
+ let name = filename;
582
+ let opts = options || new NullObject();
583
+
584
+ // support function as second or third arg
585
+ if (typeof filename === 'function') {
586
+ done = filename;
587
+ name = null;
588
+ opts = {};
589
+ } else if (typeof options === 'function') {
590
+ done = options;
591
+ opts = {};
592
+ }
593
+
594
+ // support optional filename, where options may be in it's place
595
+ if (typeof filename === 'object' &&
596
+ (typeof options === 'function' || options === undefined)) {
597
+ name = null;
598
+ opts = filename;
599
+ }
600
+ if(!name) {
601
+ name = Path.basename(path);
602
+ }
603
+ if(!opts.root && !isAbsolute(path)) {
604
+ opts.root = process.cwd();
605
+ }
606
+
607
+ this.attachment(name);
608
+ this.sendFile(path, opts, done);
609
+ }
610
+ setHeader(field, value) {
611
+ if(this.headersSent) {
612
+ throw new Error('Cannot set headers after they are sent to the client');
613
+ }
614
+ if(typeof field !== 'string') {
615
+ throw new TypeError('Header name must be a valid HTTP token');
616
+ } else {
617
+ field = field.toLowerCase();
618
+ if(Array.isArray(value)) {
619
+ this.headers[field] = value;
620
+ return this;
621
+ }
622
+ this.headers[field] = String(value);
623
+ }
624
+ return this;
625
+ }
626
+ header(field, value) {
627
+ return this.set(field, value);
628
+ }
629
+ set(field, value) {
630
+ if(typeof field === 'object') {
631
+ for(const header in field) {
632
+ this.setHeader(header, field[header]);
633
+ }
634
+ } else {
635
+ field = field.toLowerCase();
636
+ if(field === 'content-type') {
637
+ if(!value.includes('charset=') && (value.startsWith('text/') || value === 'application/json' || value === 'application/javascript')) {
638
+ value += '; charset=utf-8';
639
+ }
640
+ }
641
+ this.setHeader(field, value);
642
+ }
643
+ return this;
644
+ }
645
+ get(field) {
646
+ return this.headers[field.toLowerCase()];
647
+ }
648
+ getHeader(field) {
649
+ return this.get(field);
650
+ }
651
+ getHeaders(){
652
+ return this.headers;
653
+ }
654
+ removeHeader(field) {
655
+ delete this.headers[field.toLowerCase()];
656
+ return this;
657
+ }
658
+ append(field, value) {
659
+ field = field.toLowerCase();
660
+ const old = this.headers[field];
661
+ if(old) {
662
+ const newVal = [];
663
+ if(Array.isArray(old)) {
664
+ newVal.push(...old);
665
+ } else {
666
+ newVal.push(old);
667
+ }
668
+ if(Array.isArray(value)) {
669
+ newVal.push(...value);
670
+ } else {
671
+ newVal.push(value);
672
+ }
673
+ this.headers[field] = newVal;
674
+ } else {
675
+ this.headers[field] = value;
676
+ }
677
+ return this;
678
+ }
679
+ render(view, options, callback) {
680
+ if(typeof options === 'function') {
681
+ callback = options;
682
+ options = {};
683
+ }
684
+ if(!options) {
685
+ options = {};
686
+ } else {
687
+ options = Object.assign({}, options);
688
+ }
689
+ options._locals = this.locals;
690
+ const done = callback || ((err, str) => {
691
+ if(err) return this.req.next(err);
692
+ this.send(str);
693
+ });
694
+
695
+ this.app.render(view, options, done);
696
+ }
697
+ cookie(name, value, options) {
698
+ const opt = {...(options ?? {})}; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
699
+ let val = typeof value === 'object' ? "j:"+JSON.stringify(value) : String(value);
700
+ if(opt.maxAge != null) {
701
+ const maxAge = opt.maxAge - 0;
702
+ if(!isNaN(maxAge)) {
703
+ opt.expires = new Date(Date.now() + maxAge);
704
+ opt.maxAge = Math.floor(maxAge / 1000);
705
+ }
706
+ }
707
+ if(opt.signed) {
708
+ val = 's:' + sign(val, this.req.secret);
709
+ }
710
+
711
+ if(opt.path == null) {
712
+ opt.path = '/';
713
+ }
714
+
715
+ this.append('Set-Cookie', cookie.serialize(name, val, opt));
716
+ return this;
717
+ }
718
+ clearCookie(name, options) {
719
+ const opts = { path: '/', ...options, expires: new Date(1) };
720
+ delete opts.maxAge;
721
+ return this.cookie(name, '', opts);
722
+ }
723
+ attachment(filename) {
724
+ this.headers['Content-Disposition'] = `attachment; filename="${filename}"`;
725
+ this.type(filename.split('.').pop());
726
+ return this;
727
+ }
728
+ format(object) {
729
+ const keys = Object.keys(object).filter(v => v !== 'default');
730
+ const key = keys.length > 0 ? this.req.accepts(keys) : false;
731
+
732
+ this.vary('Accept');
733
+
734
+ if(key) {
735
+ this.set('Content-Type', normalizeType(key).value);
736
+ object[key](this.req, this, this.req.next);
737
+ } else if(object.default) {
738
+ object.default(this.req, this, this.req.next);
739
+ } else {
740
+ this.status(406).send(this.app._generateErrorPage('Not Acceptable'));
741
+ }
742
+
743
+ return this;
744
+ }
745
+ json(body) {
746
+ if(!this.headers['content-type']) {
747
+ this.headers['content-type'] = 'application/json; charset=utf-8';
748
+ }
749
+ const escape = this.app.get('json escape');
750
+ const replacer = this.app.get('json replacer');
751
+ const spaces = this.app.get('json spaces');
752
+ this.send(stringify(body, replacer, spaces, escape));
753
+ }
754
+ jsonp(object) {
755
+ let callback = this.req.query[this.app.get('jsonp callback name')];
756
+ let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
757
+ let js = false;
758
+
759
+ if(Array.isArray(callback)) {
760
+ callback = callback[0];
761
+ }
762
+
763
+ if(typeof callback === 'string' && callback.length !== 0) {
764
+ callback = callback.replace(/[^\[\]\w$.]/g, '');
765
+
766
+ if(body === undefined) {
767
+ body = '';
768
+ } else if(typeof body === 'string') {
769
+ // replace chars not allowed in JavaScript that are in JSON
770
+ body = body
771
+ .replace(/\u2028/g, '\\u2028')
772
+ .replace(/\u2029/g, '\\u2029')
773
+ }
774
+ body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
775
+ js = true;
776
+ }
777
+
778
+
779
+ if(!this.headers['content-type']) {
780
+ this.headers['content-type'] = `${js ? 'text/javascript' : 'application/json'}; charset=utf-8`;
781
+ if(js) this.headers['X-Content-Type-Options'] = 'nosniff';
782
+ }
783
+
784
+ return this.send(body);
785
+ }
786
+ links(links) {
787
+ this.headers['link'] = Object.entries(links).map(([rel, url]) => `<${url}>; rel="${rel}"`).join(', ');
788
+ return this;
789
+ }
790
+ location(path) {
791
+ if(path === 'back') {
792
+ path = this.req.get('Referrer');
793
+ if(!path) path = this.req.get('Referer');
794
+ if(!path) path = '/';
795
+ }
796
+ return this.headers['location'] = encodeUrl(path);
797
+ }
798
+ redirect(status, url, forceHtml = false) {
799
+ if(typeof status !== 'number' && !url) {
800
+ url = status;
801
+ status = 302;
802
+ }
803
+ this.location(url);
804
+ this.status(status);
805
+ let body;
806
+ // Support text/{plain,html} by default
807
+ if(forceHtml) {
808
+ this.set('Content-Type', 'text/html; charset=UTF-8');
809
+ body =
810
+ '<!DOCTYPE html>\n' +
811
+ '<html lang="en">\n' +
812
+ '<head>\n' +
813
+ '<meta charset="utf-8">\n' +
814
+ '<title>Redirecting</title>\n' +
815
+ '</head>\n' +
816
+ '<body>\n' +
817
+ `<pre>Redirecting to ${url.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</pre>\n` +
818
+ '</body>\n' +
819
+ '</html>\n';
820
+ } else {
821
+ this.format({
822
+ text: () => {
823
+ this.set('Content-Type', 'text/plain; charset=UTF-8');
824
+ body = statuses.message[status] + '. Redirecting to ' + url
825
+ },
826
+ html: () => {
827
+ this.set('Content-Type', 'text/html; charset=UTF-8');
828
+ body = `<p>${statuses.message[status]}. Redirecting to ${url.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</p>`;
829
+ },
830
+ default: () => {
831
+ this.set('Content-Type', 'text/plain; charset=UTF-8');
832
+ body = '';
833
+ }
834
+ });
835
+ }
836
+ if (this.req.method === 'HEAD') {
837
+ this.end();
838
+ } else {
839
+ this.end(body);
840
+ }
841
+ }
842
+
843
+ type(type) {
844
+ let ct = type.indexOf('/') === -1
845
+ ? (mime.contentType(type) || 'application/octet-stream')
846
+ : type;
847
+
848
+ return this.set('content-type', ct);
849
+ }
850
+ contentType = this.type;
851
+
852
+ vary(field) {
853
+ // checks for back-compat
854
+ if (!field || (Array.isArray(field) && !field.length)) {
855
+ deprecate('res.vary(): Provide a field name');
856
+ return this;
857
+ }
858
+ vary(this, field);
859
+ return this;
860
+ }
861
+
862
+ get connection() {
863
+ return this.socket;
864
+ }
865
+
866
+ get writableFinished() {
867
+ return this.finished;
868
+ }
869
+ }