ultimate-express 1.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 (200) hide show
  1. package/.github/workflows/test.yml +27 -0
  2. package/EXPRESS_LICENSE +26 -0
  3. package/README.md +269 -0
  4. package/package.json +76 -0
  5. package/src/application.js +295 -0
  6. package/src/index.js +23 -0
  7. package/src/middlewares.js +95 -0
  8. package/src/request.js +324 -0
  9. package/src/response.js +686 -0
  10. package/src/router.js +490 -0
  11. package/src/utils.js +285 -0
  12. package/src/view.js +103 -0
  13. package/src/workers/fs.js +14 -0
  14. package/tests/index.js +64 -0
  15. package/tests/parts/.test/index.html +11 -0
  16. package/tests/parts/big.jpg +0 -0
  17. package/tests/parts/index.art +12 -0
  18. package/tests/parts/index.dot +12 -0
  19. package/tests/parts/index.ejs +12 -0
  20. package/tests/parts/index.handlebars +2 -0
  21. package/tests/parts/index.html +12 -0
  22. package/tests/parts/index.mustache +12 -0
  23. package/tests/parts/index.pug +6 -0
  24. package/tests/parts/index.swig +12 -0
  25. package/tests/parts/layouts/main.handlebars +12 -0
  26. package/tests/preload.cjs +5 -0
  27. package/tests/singular.js +41 -0
  28. package/tests/tests/app/app-engine.js +8 -0
  29. package/tests/tests/app/app-on-mount.js +24 -0
  30. package/tests/tests/app/app-path.js +21 -0
  31. package/tests/tests/app/app-route.js +30 -0
  32. package/tests/tests/app/options.js +16 -0
  33. package/tests/tests/app/setting-inheritance.js +16 -0
  34. package/tests/tests/engines/art-template.js +33 -0
  35. package/tests/tests/engines/dot.js +28 -0
  36. package/tests/tests/engines/ejs.js +25 -0
  37. package/tests/tests/engines/handlebars.js +28 -0
  38. package/tests/tests/engines/mustache.js +28 -0
  39. package/tests/tests/engines/pug.js +25 -0
  40. package/tests/tests/engines/swig.js +28 -0
  41. package/tests/tests/errors/error-handling-middleware.js +22 -0
  42. package/tests/tests/errors/next-error-optimized.js +26 -0
  43. package/tests/tests/errors/next-error.js +26 -0
  44. package/tests/tests/errors/unexpected-error-handling.js +18 -0
  45. package/tests/tests/listen/listen-random.js +11 -0
  46. package/tests/tests/listen/listen-specific.js +17 -0
  47. package/tests/tests/middlewares/body-json.js +31 -0
  48. package/tests/tests/middlewares/body-raw-deflate.js +43 -0
  49. package/tests/tests/middlewares/body-raw-gzip.js +43 -0
  50. package/tests/tests/middlewares/body-raw.js +41 -0
  51. package/tests/tests/middlewares/body-text.js +27 -0
  52. package/tests/tests/middlewares/body-urlencoded.js +30 -0
  53. package/tests/tests/middlewares/cookie-parser-signed.js +31 -0
  54. package/tests/tests/middlewares/cookie-parser.js +28 -0
  55. package/tests/tests/middlewares/cookie-session.js +40 -0
  56. package/tests/tests/middlewares/cors.js +29 -0
  57. package/tests/tests/middlewares/errorhandler.js +26 -0
  58. package/tests/tests/middlewares/express-fileupload-temp.js +46 -0
  59. package/tests/tests/middlewares/express-fileupload.js +28 -0
  60. package/tests/tests/middlewares/express-rate-limit.js +33 -0
  61. package/tests/tests/middlewares/express-session.js +37 -0
  62. package/tests/tests/middlewares/express-static-options.js +72 -0
  63. package/tests/tests/middlewares/express-static.js +40 -0
  64. package/tests/tests/middlewares/method-override.js +33 -0
  65. package/tests/tests/middlewares/multer.js +43 -0
  66. package/tests/tests/middlewares/multiple-middlewares.js +37 -0
  67. package/tests/tests/middlewares/response-time.js +29 -0
  68. package/tests/tests/middlewares/serve-index.js +38 -0
  69. package/tests/tests/middlewares/serve-static.js +38 -0
  70. package/tests/tests/middlewares/vhost.js +50 -0
  71. package/tests/tests/params/array-param.js +30 -0
  72. package/tests/tests/params/nested-params.js +24 -0
  73. package/tests/tests/params/param-errors.js +56 -0
  74. package/tests/tests/params/param-function.js +49 -0
  75. package/tests/tests/params/param-next-route.js +48 -0
  76. package/tests/tests/params/param-optimized.js +38 -0
  77. package/tests/tests/params/param-use.js +39 -0
  78. package/tests/tests/params/param.js +68 -0
  79. package/tests/tests/params/params-regex.js +26 -0
  80. package/tests/tests/params/params-use.js +20 -0
  81. package/tests/tests/params/params.js +35 -0
  82. package/tests/tests/req/req-accepts-charsets.js +40 -0
  83. package/tests/tests/req/req-accepts-encodings.js +36 -0
  84. package/tests/tests/req/req-accepts-languages.js +41 -0
  85. package/tests/tests/req/req-accepts.js +41 -0
  86. package/tests/tests/req/req-app.js +17 -0
  87. package/tests/tests/req/req-baseurl.js +38 -0
  88. package/tests/tests/req/req-connection.js +19 -0
  89. package/tests/tests/req/req-fresh.js +59 -0
  90. package/tests/tests/req/req-get.js +78 -0
  91. package/tests/tests/req/req-headers-distinct.js +72 -0
  92. package/tests/tests/req/req-headers.js +72 -0
  93. package/tests/tests/req/req-host.js +45 -0
  94. package/tests/tests/req/req-hostname.js +45 -0
  95. package/tests/tests/req/req-ip.js +19 -0
  96. package/tests/tests/req/req-is.js +44 -0
  97. package/tests/tests/req/req-original-url.js +29 -0
  98. package/tests/tests/req/req-param.js +29 -0
  99. package/tests/tests/req/req-protocol.js +20 -0
  100. package/tests/tests/req/req-query.js +23 -0
  101. package/tests/tests/req/req-range.js +48 -0
  102. package/tests/tests/req/req-raw-headers.js +72 -0
  103. package/tests/tests/req/req-subdomains.js +48 -0
  104. package/tests/tests/req/req-url-nested.js +27 -0
  105. package/tests/tests/req/req-url-optimized-router.js +26 -0
  106. package/tests/tests/req/req-url-optimized.js +23 -0
  107. package/tests/tests/req/req-url.js +36 -0
  108. package/tests/tests/req/req-xhr.js +23 -0
  109. package/tests/tests/res/head-content-length.js +18 -0
  110. package/tests/tests/res/head.js +47 -0
  111. package/tests/tests/res/injecting.js +25 -0
  112. package/tests/tests/res/piping.js +23 -0
  113. package/tests/tests/res/res-app.js +17 -0
  114. package/tests/tests/res/res-append.js +24 -0
  115. package/tests/tests/res/res-attachment.js +19 -0
  116. package/tests/tests/res/res-clear-cookie.js +18 -0
  117. package/tests/tests/res/res-cookie.js +22 -0
  118. package/tests/tests/res/res-download.js +36 -0
  119. package/tests/tests/res/res-format.js +57 -0
  120. package/tests/tests/res/res-get.js +18 -0
  121. package/tests/tests/res/res-headers-sent.js +18 -0
  122. package/tests/tests/res/res-json.js +17 -0
  123. package/tests/tests/res/res-jsonp.js +25 -0
  124. package/tests/tests/res/res-links.js +21 -0
  125. package/tests/tests/res/res-location.js +34 -0
  126. package/tests/tests/res/res-redirect.js +46 -0
  127. package/tests/tests/res/res-remove-header.js +19 -0
  128. package/tests/tests/res/res-send-file.js +17 -0
  129. package/tests/tests/res/res-send-status.js +17 -0
  130. package/tests/tests/res/res-send.js +69 -0
  131. package/tests/tests/res/res-set.js +28 -0
  132. package/tests/tests/res/res-status.js +18 -0
  133. package/tests/tests/res/res-type.js +19 -0
  134. package/tests/tests/res/res-vary.js +19 -0
  135. package/tests/tests/res/res-write.js +29 -0
  136. package/tests/tests/routers/complex-routers.js +34 -0
  137. package/tests/tests/routers/empty-router.js +25 -0
  138. package/tests/tests/routers/lot-of-routes.js +38 -0
  139. package/tests/tests/routers/mergeparams.js +42 -0
  140. package/tests/tests/routers/nested-routers.js +52 -0
  141. package/tests/tests/routers/router-options.js +68 -0
  142. package/tests/tests/routers/routers.js +45 -0
  143. package/tests/tests/routers/simple-routers.js +35 -0
  144. package/tests/tests/routing/all.js +47 -0
  145. package/tests/tests/routing/array-arguments.js +35 -0
  146. package/tests/tests/routing/array-use.js +33 -0
  147. package/tests/tests/routing/async-use.js +25 -0
  148. package/tests/tests/routing/complex-routes.js +50 -0
  149. package/tests/tests/routing/lot-of-param-routes.js +26 -0
  150. package/tests/tests/routing/lot-of-routes.js +59 -0
  151. package/tests/tests/routing/next-existent-optimized-route.js +29 -0
  152. package/tests/tests/routing/next-existent-route.js +29 -0
  153. package/tests/tests/routing/next-nonexistent-optimized-route.js +19 -0
  154. package/tests/tests/routing/next-nonexistent-route.js +19 -0
  155. package/tests/tests/routing/next-special-cases.js +54 -0
  156. package/tests/tests/routing/next-unoptimized.js +39 -0
  157. package/tests/tests/routing/no-path-use.js +29 -0
  158. package/tests/tests/routing/non-string-routes.js +27 -0
  159. package/tests/tests/routing/req-multiple-mountpaths.js +34 -0
  160. package/tests/tests/routing/simple-routes.js +29 -0
  161. package/tests/tests/routing/simple-use.js +52 -0
  162. package/tests/tests/routing/some-middlewares.js +27 -0
  163. package/tests/tests/routing/special-characters.js +28 -0
  164. package/tests/tests/routing/star.js +31 -0
  165. package/tests/tests/routing/sub-apps.js +32 -0
  166. package/tests/tests/routing/trailing-slash.js +37 -0
  167. package/tests/tests/routing/weird-route-start.js +18 -0
  168. package/tests/tests/send-file/accept-ranges.js +26 -0
  169. package/tests/tests/send-file/callback.js +23 -0
  170. package/tests/tests/send-file/default-error-routing.js +21 -0
  171. package/tests/tests/send-file/dotfiles.js +24 -0
  172. package/tests/tests/send-file/etag.js +19 -0
  173. package/tests/tests/send-file/fs-threads.js +39 -0
  174. package/tests/tests/send-file/head.js +49 -0
  175. package/tests/tests/send-file/headers.js +23 -0
  176. package/tests/tests/send-file/if-match.js +31 -0
  177. package/tests/tests/send-file/if-range.js +37 -0
  178. package/tests/tests/send-file/if-unmodified-since.js +32 -0
  179. package/tests/tests/send-file/immutable.js +22 -0
  180. package/tests/tests/send-file/large-file.js +19 -0
  181. package/tests/tests/send-file/last-modified.js +21 -0
  182. package/tests/tests/send-file/max-age.js +21 -0
  183. package/tests/tests/send-file/path-traversal.js +35 -0
  184. package/tests/tests/send-file/range.js +57 -0
  185. package/tests/tests/send-file/simple.js +18 -0
  186. package/tests/tests/settings/case-sensitive-routing.js +48 -0
  187. package/tests/tests/settings/env-errors.js +38 -0
  188. package/tests/tests/settings/etag.js +36 -0
  189. package/tests/tests/settings/json-escape.js +45 -0
  190. package/tests/tests/settings/json-replacer.js +50 -0
  191. package/tests/tests/settings/json-spaces.js +44 -0
  192. package/tests/tests/settings/query-parser.js +45 -0
  193. package/tests/tests/settings/strict-routing.js +64 -0
  194. package/tests/tests/settings/subdomain-offset.js +38 -0
  195. package/tests/tests/settings/trust-proxy-host.js +58 -0
  196. package/tests/tests/settings/trust-proxy-ip.js +58 -0
  197. package/tests/tests/settings/trust-proxy-ips.js +58 -0
  198. package/tests/tests/settings/trust-proxy-protocol.js +46 -0
  199. package/tests/tests/settings/x-powered-by.js +32 -0
  200. package/tests/uws.js +14 -0
@@ -0,0 +1,686 @@
1
+ const cookie = require("cookie");
2
+ const mime = require("mime-types");
3
+ const vary = require("vary");
4
+ const {
5
+ normalizeType, stringify, deprecated, UP_PATH_REGEXP, decode,
6
+ containsDotFile, isPreconditionFailure, isRangeFresh, parseHttpDate
7
+ } = require("./utils.js");
8
+ const { Writable } = require("stream");
9
+ const { isAbsolute } = require("path");
10
+ const fs = require("fs");
11
+ const Path = require("path");
12
+ const statuses = require("statuses");
13
+ const { sign } = require("cookie-signature");
14
+ const { EventEmitter } = require("tseep");
15
+ const http = require("http");
16
+ const ms = require('ms');
17
+ const etag = require("etag");
18
+
19
+ const outgoingMessage = new http.OutgoingMessage();
20
+ const symbols = Object.getOwnPropertySymbols(outgoingMessage);
21
+ const kOutHeaders = symbols.find(s => s.toString() === 'Symbol(kOutHeaders)');
22
+
23
+ class Socket extends EventEmitter {
24
+ constructor(response) {
25
+ super();
26
+ this.response = response;
27
+ this.writable = true;
28
+
29
+ this.on('error', (err) => {
30
+ this.emit('close');
31
+ });
32
+ this.on('close', () => {
33
+ this.writable = false;
34
+ });
35
+ }
36
+ }
37
+
38
+ module.exports = class Response extends Writable {
39
+ constructor(res, req, app) {
40
+ super();
41
+ this._req = req;
42
+ this._res = res;
43
+ this.headersSent = false;
44
+ this.aborted = false;
45
+ this.socket = new Socket(this);
46
+ this.app = app;
47
+ this.locals = {};
48
+ this.aborted = false;
49
+ this.statusCode = 200;
50
+ this.headers = {
51
+ 'keep-alive': 'timeout=10'
52
+ };
53
+ // support for node internal
54
+ this[kOutHeaders] = new Proxy(this.headers, {
55
+ set: (obj, prop, value) => {
56
+ this.set(prop, value[1]);
57
+ return true;
58
+ },
59
+ get: (obj, prop) => {
60
+ return obj[prop];
61
+ }
62
+ });
63
+ this.body = undefined;
64
+ if(this.app.get('x-powered-by')) {
65
+ this.set('x-powered-by', 'uExpress');
66
+ }
67
+ this.on('error', (err) => {
68
+ if(this.aborted) {
69
+ return;
70
+ }
71
+ this._res.cork(() => {
72
+ this._res.close();
73
+ this.socket.emit('close');
74
+ });
75
+ });
76
+ this.emit('socket', this.socket);
77
+ }
78
+ _write(chunk, encoding, callback) {
79
+ if(this.aborted) {
80
+ const err = new Error('Request aborted');
81
+ err.code = 'ECONNABORTED';
82
+ return this.destroy(err);
83
+ }
84
+ if(!Buffer.isBuffer(chunk)) {
85
+ chunk = Buffer.from(chunk);
86
+ }
87
+ this._res.cork(() => {
88
+ if(!this.headersSent) {
89
+ this.writeHead(this.statusCode);
90
+ this._res.writeStatus(this.statusCode.toString());
91
+ for(const header in this.headers) {
92
+ if(header === 'content-length') {
93
+ continue;
94
+ }
95
+ this._res.writeHeader(header, this.headers[header]);
96
+ }
97
+ if(!this.headers['content-type']) {
98
+ this._res.writeHeader('content-type', 'text/html');
99
+ }
100
+ this.headersSent = true;
101
+ }
102
+ const ab = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
103
+ this._res.write(ab);
104
+ callback();
105
+ });
106
+ }
107
+ writeHead(statusCode, statusMessage, headers) {
108
+ this.statusCode = statusCode;
109
+ if(!headers) {
110
+ if(!statusMessage) return;
111
+ headers = statusMessage;
112
+ }
113
+ for(let header in headers) {
114
+ this.set(header, headers[header]);
115
+ }
116
+ }
117
+ _implicitHeader() {
118
+ // compatibility function
119
+ // usually should send headers but this is useless for us
120
+ return;
121
+ }
122
+ status(code) {
123
+ if(this.headersSent) {
124
+ throw new Error('Can\'t set status: Response was already sent');
125
+ }
126
+ this.statusCode = parseInt(code);
127
+ return this;
128
+ }
129
+ sendStatus(code) {
130
+ return this.status(code).send(statuses.message[+code] ?? code.toString());
131
+ }
132
+ end(data) {
133
+ if(this.finished) {
134
+ return;
135
+ }
136
+
137
+ this._res.cork(() => {
138
+ if(!this.headersSent) {
139
+ const etagFn = this.app.get('etag fn');
140
+ if(data && !this.headers['etag'] && etagFn && !this.req.noEtag) {
141
+ this.set('etag', etagFn(data, this.req));
142
+ }
143
+ if(this.req.fresh) {
144
+ if(!this.headersSent) {
145
+ this._res.writeStatus('304');
146
+ }
147
+ this.headersSent = true;
148
+ this.socket.emit('close');
149
+ return this._res.end();
150
+ }
151
+ this._res.writeStatus(this.statusCode.toString());
152
+ for(const header in this.headers) {
153
+ if(header === 'content-length') {
154
+ continue;
155
+ }
156
+ this._res.writeHeader(header, this.headers[header]);
157
+ }
158
+ if(!this.headers['content-type']) {
159
+ this._res.writeHeader('content-type', 'text/html');
160
+ }
161
+ this.headersSent = true;
162
+ }
163
+ if(!data && this.headers['content-length']) {
164
+ this._res.endWithoutBody(this.headers['content-length'].toString());
165
+ } else {
166
+ if(this.req.method === 'HEAD') {
167
+ const length = Buffer.byteLength(data ?? '');
168
+ this._res.endWithoutBody(length.toString());
169
+ } else {
170
+ this._res.end(data);
171
+ }
172
+ }
173
+ this.socket.emit('close');
174
+ });
175
+
176
+ return this;
177
+ }
178
+ send(body) {
179
+ if(this.headersSent) {
180
+ throw new Error('Can\'t write body: Response was already sent');
181
+ }
182
+ if(Buffer.isBuffer(body)) {
183
+ body = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
184
+ } else if(body === null || body === undefined) {
185
+ body = '';
186
+ } else if(typeof body === 'object') {
187
+ if(!(body instanceof ArrayBuffer)) {
188
+ body = stringify(body);
189
+ }
190
+ } else if(typeof body === 'number') {
191
+ if(arguments[1]) {
192
+ deprecated('res.send(status, body)', 'res.status(status).send(body)');
193
+ return this.status(body).send(arguments[1]);
194
+ } else {
195
+ deprecated('res.send(status)', 'res.sendStatus(status)');
196
+ return this.sendStatus(body);
197
+ }
198
+ } else {
199
+ body = String(body);
200
+ }
201
+ this.writeHead(this.statusCode);
202
+ return this.end(body);
203
+ }
204
+ sendFile(path, options = {}, callback) {
205
+ if(typeof path !== 'string') {
206
+ throw new TypeError('path argument is required to res.sendFile');
207
+ }
208
+ if(typeof options === 'function') {
209
+ callback = options;
210
+ options = {};
211
+ }
212
+ if(!options) options = {};
213
+ let done = callback;
214
+ if(!done) done = this.req.next;
215
+ // default options
216
+ if(typeof options.maxAge === 'string') {
217
+ options.maxAge = ms(options.maxAge);
218
+ }
219
+ if(typeof options.maxAge === 'undefined') {
220
+ options.maxAge = 0;
221
+ }
222
+ if(typeof options.lastModified === 'undefined') {
223
+ options.lastModified = true;
224
+ }
225
+ if(typeof options.cacheControl === 'undefined') {
226
+ options.cacheControl = true;
227
+ }
228
+ if(typeof options.acceptRanges === 'undefined') {
229
+ options.acceptRanges = true;
230
+ }
231
+ if(typeof options.etag === 'undefined') {
232
+ options.etag = this.app.get('etag') !== false;
233
+ }
234
+ let etagFn = this.app.get('etag fn');
235
+ if(options.etag && !etagFn) {
236
+ etagFn = stat => {
237
+ return etag(stat, { weak: true });
238
+ }
239
+ }
240
+
241
+ // path checks
242
+ if(!options.root && !isAbsolute(path)) {
243
+ this.status(500);
244
+ return done(new Error('path must be absolute or specify root to res.sendFile'));
245
+ }
246
+ path = decode(path);
247
+ if(path === -1) {
248
+ this.status(400);
249
+ return done(new Error('Bad Request'));
250
+ }
251
+ if(~path.indexOf('\0')) {
252
+ this.status(400);
253
+ return done(new Error('Bad Request'));
254
+ }
255
+ if(UP_PATH_REGEXP.test(path)) {
256
+ this.status(403);
257
+ return done(new Error('Forbidden'));
258
+ }
259
+ const parts = Path.normalize(path).split(Path.sep);
260
+ const fullpath = options.root ? Path.resolve(Path.join(options.root, path)) : path;
261
+ if(options.root && !fullpath.startsWith(Path.resolve(options.root))) {
262
+ this.status(403);
263
+ return done(new Error('Forbidden'));
264
+ }
265
+
266
+ // dotfile checks
267
+ if(containsDotFile(parts)) {
268
+ switch(options.dotfiles) {
269
+ case 'allow':
270
+ break;
271
+ case 'deny':
272
+ this.status(403);
273
+ return done(new Error('Forbidden'));
274
+ case 'ignore':
275
+ default:
276
+ this.status(404);
277
+ return done(new Error('Not found'));
278
+ }
279
+ }
280
+
281
+ let stat = options._stat;
282
+ if(!stat) {
283
+ try {
284
+ stat = fs.statSync(fullpath);
285
+ } catch(err) {
286
+ return done(err);
287
+ }
288
+ if(stat.isDirectory()) {
289
+ this.status(404);
290
+ return done(new Error(`Not found`));
291
+ }
292
+ }
293
+
294
+ // headers
295
+ if(!this.get('Content-Type')) {
296
+ const m = mime.lookup(fullpath);
297
+ if(m) this.type(m);
298
+ }
299
+ if(options.cacheControl) {
300
+ this.set('Cache-Control', `public, max-age=${options.maxAge / 1000}` + (options.immutable ? ', immutable' : ''));
301
+ }
302
+ if(options.lastModified) {
303
+ this.set('Last-Modified', stat.mtime.toUTCString());
304
+ }
305
+ if(options.headers) {
306
+ for(const header in options.headers) {
307
+ this.set(header, options.headers[header]);
308
+ }
309
+ }
310
+ if(options.setHeaders) {
311
+ options.setHeaders(this, fullpath, stat);
312
+ }
313
+
314
+ // etag
315
+ if(options.etag && !this.headers['etag'] && etagFn) {
316
+ this.set('etag', etagFn(stat));
317
+ }
318
+ if(!options.etag) {
319
+ this.req.noEtag = true;
320
+ }
321
+
322
+ // conditional requests
323
+ if(isPreconditionFailure(this.req, this)) {
324
+ this.status(412);
325
+ return done(new Error('Precondition Failed'));
326
+ }
327
+
328
+ // if-modified-since
329
+ let modifiedSince = this.req.headers['if-modified-since'];
330
+ let lastModified = this.headers['last-modified'];
331
+ if(options.lastModified && lastModified && modifiedSince) {
332
+ modifiedSince = parseHttpDate(modifiedSince);
333
+ lastModified = parseHttpDate(lastModified);
334
+
335
+ if(!isNaN(lastModified) && !isNaN(modifiedSince) && lastModified <= modifiedSince) {
336
+ this.status(304);
337
+ return this.end();
338
+ };
339
+ }
340
+
341
+ // range requests
342
+ let offset = 0, len = stat.size, ranged = false;
343
+ if(options.acceptRanges && this.req.headers.range) {
344
+ let ranges = this.req.range(stat.size, {
345
+ combine: true
346
+ });
347
+
348
+ // if-range
349
+ if(!isRangeFresh(this.req, this)) {
350
+ ranges = -2;
351
+ }
352
+
353
+ if(ranges === -1) {
354
+ this.status(416);
355
+ this.set('Content-Range', `bytes */${stat.size}`);
356
+ return done(new Error('Range Not Satisfiable'));
357
+ }
358
+ if(ranges !== -2 && ranges.length === 1) {
359
+ this.status(206);
360
+ this.set('Content-Range', `bytes ${ranges[0].start}-${ranges[0].end}/${stat.size}`);
361
+ offset = ranges[0].start;
362
+ len = ranges[0].end - ranges[0].start + 1;
363
+ ranged = true;
364
+ }
365
+ }
366
+
367
+ if(this.req.method === 'HEAD') {
368
+ this.set('Content-Length', stat.size);
369
+ return this.end();
370
+ }
371
+
372
+ // serve smaller files using workers
373
+ if(this.app.fsWorkers.length && stat.size < 1024 * 1024 && !ranged) {
374
+ this.app.readFileWithWorker(fullpath).then((data) => {
375
+ if(this._res.aborted) {
376
+ return;
377
+ }
378
+ this.send(data);
379
+ if(callback) callback();
380
+ }).catch((err) => {
381
+ if(callback) callback(err);
382
+ });
383
+ } else {
384
+ // larger files or range requests are piped over response
385
+ let opts = {};
386
+ if(ranged) {
387
+ opts.start = offset;
388
+ opts.end = Math.max(offset, offset + len - 1);
389
+ }
390
+ const file = fs.createReadStream(fullpath, opts);
391
+ pipeStreamOverResponse(this, file, len, callback);
392
+ }
393
+ }
394
+ download(path, filename, options, callback) {
395
+ let done = callback;
396
+ let name = filename;
397
+ let opts = options || {};
398
+
399
+ // support function as second or third arg
400
+ if (typeof filename === 'function') {
401
+ done = filename;
402
+ name = null;
403
+ opts = {};
404
+ } else if (typeof options === 'function') {
405
+ done = options;
406
+ opts = {};
407
+ }
408
+
409
+ // support optional filename, where options may be in it's place
410
+ if (typeof filename === 'object' &&
411
+ (typeof options === 'function' || options === undefined)) {
412
+ name = null;
413
+ opts = filename;
414
+ }
415
+ if(!name) {
416
+ name = Path.basename(path);
417
+ }
418
+ if(!opts.root) {
419
+ opts.root = process.cwd();
420
+ }
421
+
422
+ this.attachment(name);
423
+ this.sendFile(path, opts, done);
424
+ }
425
+ set(field, value) {
426
+ if(this.headersSent) {
427
+ throw new Error('Can\'t write headers: Response was already sent');
428
+ }
429
+ if(typeof field === 'object') {
430
+ for(const header in field) {
431
+ this.set(header, field[header]);
432
+ }
433
+ } else {
434
+ if(field === 'set-cookie' && Array.isArray(value)) {
435
+ value = value.join('; ');
436
+ }
437
+ this.headers[field.toLowerCase()] = String(value);
438
+ }
439
+ return this;
440
+ }
441
+ header(field, value) {
442
+ return this.set(field, value);
443
+ }
444
+ setHeader(field, value) {
445
+ return this.set(field, value);
446
+ }
447
+ get(field) {
448
+ return this.headers[field.toLowerCase()];
449
+ }
450
+ getHeader(field) {
451
+ return this.get(field);
452
+ }
453
+ removeHeader(field) {
454
+ delete this.headers[field.toLowerCase()];
455
+ return this;
456
+ }
457
+ append(field, value) {
458
+ field = field.toLowerCase();
459
+ if(this.headers[field]) {
460
+ if(typeof value === 'string' || typeof value === 'number') {
461
+ this.headers[field] += ', ' + value;
462
+ } else if(Array.isArray(value)) {
463
+ this.headers[field] += ', ' + value.join(', ');
464
+ }
465
+ } else {
466
+ if(typeof value === 'string' || typeof value === 'number') {
467
+ this.headers[field] = value.toString();
468
+ } else if(Array.isArray(value)) {
469
+ this.headers[field] = value.join(', ');
470
+ }
471
+ }
472
+ return this;
473
+ }
474
+ render(view, options, callback) {
475
+ if(typeof options === 'function') {
476
+ callback = options;
477
+ options = {};
478
+ }
479
+ if(!options) {
480
+ options = {};
481
+ } else {
482
+ options = Object.assign({}, options);
483
+ }
484
+ options._locals = this.locals;
485
+ const done = callback || ((err, str) => {
486
+ if(err) return this.req.next(err);
487
+ this.send(str);
488
+ });
489
+
490
+ this.app.render(view, options, done);
491
+ }
492
+ cookie(name, value, options) {
493
+ if(!options) {
494
+ options = {};
495
+ }
496
+ // TODO: signed cookies
497
+ let val = typeof value === 'object' ? "j:"+JSON.stringify(value) : String(value);
498
+ if(options.maxAge != null) {
499
+ const maxAge = options.maxAge - 0;
500
+ if(!isNaN(maxAge)) {
501
+ options.expires = new Date(Date.now() + maxAge);
502
+ options.maxAge = Math.floor(maxAge / 1000);
503
+ }
504
+ }
505
+ if(options.signed) {
506
+ val = 's:' + sign(val, this.req.secret);
507
+ }
508
+
509
+ if(options.path == null) {
510
+ options.path = '/';
511
+ }
512
+
513
+ this.append('Set-Cookie', cookie.serialize(name, val, options));
514
+ return this;
515
+ }
516
+ clearCookie(name, options) {
517
+ const opts = { path: '/', ...options, expires: new Date(1) };
518
+ delete opts.maxAge;
519
+ return this.cookie(name, '', opts);
520
+ }
521
+ attachment(filename) {
522
+ this.set('Content-Disposition', `attachment; filename="${filename}"`);
523
+ this.type(filename.split('.').pop());
524
+ return this;
525
+ }
526
+ format(object) {
527
+ const keys = Object.keys(object).filter(v => v !== 'default');
528
+ const key = keys.length > 0 ? this.req.accepts(keys) : false;
529
+
530
+ this.vary('Accept');
531
+
532
+ if(key) {
533
+ this.set('Content-Type', normalizeType(key).value);
534
+ object[key](this.req, this, this.req.next);
535
+ } else if(object.default) {
536
+ object.default(this.req, this, this.req.next);
537
+ } else {
538
+ this.status(406).send(this.app._generateErrorPage('Not Acceptable'));
539
+ }
540
+
541
+ return this;
542
+ }
543
+ json(body) {
544
+ if(!this.get('Content-Type')) {
545
+ this.set('Content-Type', 'application/json');
546
+ }
547
+ const escape = this.app.get('json escape');
548
+ const replacer = this.app.get('json replacer');
549
+ const spaces = this.app.get('json spaces');
550
+ this.send(stringify(body, replacer, spaces, escape));
551
+ }
552
+ jsonp(object) {
553
+ let callback = this.req.query[this.app.get('jsonp callback name')];
554
+ let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
555
+
556
+ if(!this.get('Content-Type')) {
557
+ this.set('Content-Type', 'application/javascript');
558
+ this.set('X-Content-Type-Options', 'nosniff');
559
+ }
560
+
561
+ if(Array.isArray(callback)) {
562
+ callback = callback[0];
563
+ }
564
+
565
+ if(typeof callback === 'string' && callback.length !== 0) {
566
+ this.set('Content-Type', 'application/javascript');
567
+ this.set('X-Content-Type-Options', 'nosniff');
568
+ callback = callback.replace(/[^\[\]\w$.]/g, '');
569
+
570
+ if(body === undefined) {
571
+ body = '';
572
+ } else if(typeof body === 'string') {
573
+ // replace chars not allowed in JavaScript that are in JSON
574
+ body = body
575
+ .replace(/\u2028/g, '\\u2028')
576
+ .replace(/\u2029/g, '\\u2029')
577
+ }
578
+ body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
579
+ }
580
+
581
+ return this.send(body);
582
+ }
583
+ links(links) {
584
+ this.set('Link', Object.entries(links).map(([rel, url]) => `<${url}>; rel="${rel}"`).join(', '));
585
+ return this;
586
+ }
587
+ location(path) {
588
+ if(path === 'back') {
589
+ path = this.req.get('Referrer');
590
+ if(!path) path = this.req.get('Referer');
591
+ if(!path) path = '/';
592
+ }
593
+ return this.set('Location', encodeURI(path));
594
+ }
595
+ redirect(status, url) {
596
+ if(typeof status !== 'number' && !url) {
597
+ url = status;
598
+ status = 302;
599
+ }
600
+ this.location(url);
601
+ this.status(status);
602
+ this.set('Content-Type', 'text/plain');
603
+ return this.send(`${statuses.message[status] ?? status}. Redirecting to ${url}`);
604
+ }
605
+
606
+ type(type) {
607
+ const ct = type.indexOf('/') === -1
608
+ ? (mime.contentType(type) || 'application/octet-stream')
609
+ : type;
610
+ return this.set('Content-Type', ct);
611
+ }
612
+ contentType(type) {
613
+ return this.type(type);
614
+ }
615
+
616
+ vary(field) {
617
+ vary(this, field);
618
+ return this;
619
+ }
620
+
621
+ get finished() {
622
+ return !this.socket.writable;
623
+ }
624
+
625
+ get writableFinished() {
626
+ return !this.socket.writable;
627
+ }
628
+ }
629
+
630
+ function pipeStreamOverResponse(res, readStream, totalSize, callback) {
631
+ readStream.on('data', (chunk) => {
632
+ if(res.aborted) {
633
+ return readStream.destroy();
634
+ }
635
+ res._res.cork(() => {
636
+ if(!res.headersSent) {
637
+ res.writeHead(res.statusCode);
638
+ res._res.writeStatus(res.statusCode.toString());
639
+ for(const header in res.headers) {
640
+ if(header === 'content-length') {
641
+ continue;
642
+ }
643
+ res._res.writeHeader(header, res.headers[header]);
644
+ }
645
+ if(!res.headers['content-type']) {
646
+ res._res.writeHeader('content-type', 'text/html');
647
+ }
648
+ res.headersSent = true;
649
+ }
650
+ const ab = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
651
+
652
+ const lastOffset = res._res.getWriteOffset();
653
+ const [ok, done] = res._res.tryEnd(ab, totalSize);
654
+
655
+ if (done) {
656
+ readStream.destroy();
657
+ res.socket.emit('close');
658
+ if(callback) callback();
659
+ } else if (!ok) {
660
+ readStream.pause();
661
+
662
+ res._res.ab = ab;
663
+ res._res.abOffset = lastOffset;
664
+
665
+ res._res.onWritable((offset) => {
666
+ const [ok, done] = res._res.tryEnd(res._res.ab.slice(offset - res._res.abOffset), totalSize);
667
+ if (done) {
668
+ readStream.destroy();
669
+ res.socket.emit('close');
670
+ if(callback) callback();
671
+ } else if (ok) {
672
+ readStream.resume();
673
+ }
674
+
675
+ return ok;
676
+ });
677
+ }
678
+ });
679
+ }).on('error', e => {
680
+ if(callback) callback(e);
681
+ if(!res.finished) {
682
+ res._res.close();
683
+ res.socket.emit('error', e);
684
+ }
685
+ });
686
+ }