@webqit/webflo 0.11.21 → 0.11.24

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 (86) hide show
  1. package/.gitignore +7 -7
  2. package/LICENSE +20 -20
  3. package/README.md +2074 -2071
  4. package/package.json +82 -82
  5. package/src/Context.js +79 -79
  6. package/src/config-pi/deployment/Env.js +69 -69
  7. package/src/config-pi/deployment/Layout.js +65 -65
  8. package/src/config-pi/deployment/Origins.js +133 -133
  9. package/src/config-pi/deployment/Virtualization.js +65 -65
  10. package/src/config-pi/deployment/index.js +17 -17
  11. package/src/config-pi/index.js +15 -15
  12. package/src/config-pi/runtime/Client.js +101 -101
  13. package/src/config-pi/runtime/Server.js +128 -128
  14. package/src/config-pi/runtime/client/Worker.js +135 -135
  15. package/src/config-pi/runtime/client/index.js +11 -11
  16. package/src/config-pi/runtime/index.js +17 -17
  17. package/src/config-pi/runtime/server/Headers.js +77 -77
  18. package/src/config-pi/runtime/server/Redirects.js +73 -73
  19. package/src/config-pi/runtime/server/index.js +13 -13
  20. package/src/config-pi/static/Manifest.js +321 -321
  21. package/src/config-pi/static/Ssg.js +51 -51
  22. package/src/config-pi/static/index.js +13 -13
  23. package/src/deployment-pi/index.js +10 -10
  24. package/src/deployment-pi/origins/index.js +215 -215
  25. package/src/index.js +19 -19
  26. package/src/runtime-pi/Router.js +131 -131
  27. package/src/runtime-pi/client/Context.js +6 -6
  28. package/src/runtime-pi/client/Router.js +47 -47
  29. package/src/runtime-pi/client/Runtime.js +357 -341
  30. package/src/runtime-pi/client/RuntimeClient.js +98 -98
  31. package/src/runtime-pi/client/Storage.js +56 -56
  32. package/src/runtime-pi/client/Url.js +205 -205
  33. package/src/runtime-pi/client/Workport.js +163 -163
  34. package/src/runtime-pi/client/generate.js +467 -467
  35. package/src/runtime-pi/client/index.js +23 -23
  36. package/src/runtime-pi/client/oohtml/full.js +6 -6
  37. package/src/runtime-pi/client/oohtml/namespacing.js +6 -6
  38. package/src/runtime-pi/client/oohtml/scripting.js +7 -7
  39. package/src/runtime-pi/client/oohtml/templating.js +7 -7
  40. package/src/runtime-pi/client/whatwag.js +27 -27
  41. package/src/runtime-pi/client/worker/Context.js +6 -6
  42. package/src/runtime-pi/client/worker/Worker.js +291 -291
  43. package/src/runtime-pi/client/worker/WorkerClient.js +46 -46
  44. package/src/runtime-pi/client/worker/Workport.js +79 -79
  45. package/src/runtime-pi/client/worker/index.js +23 -23
  46. package/src/runtime-pi/index.js +13 -13
  47. package/src/runtime-pi/server/Context.js +15 -15
  48. package/src/runtime-pi/server/Router.js +157 -157
  49. package/src/runtime-pi/server/Runtime.js +547 -547
  50. package/src/runtime-pi/server/RuntimeClient.js +112 -112
  51. package/src/runtime-pi/server/index.js +23 -23
  52. package/src/runtime-pi/server/whatwag.js +35 -35
  53. package/src/runtime-pi/util.js +162 -162
  54. package/src/runtime-pi/xFormData.js +59 -59
  55. package/src/runtime-pi/xHeaders.js +87 -87
  56. package/src/runtime-pi/xHttpEvent.js +92 -92
  57. package/src/runtime-pi/xHttpMessage.js +179 -179
  58. package/src/runtime-pi/xRequest.js +73 -73
  59. package/src/runtime-pi/xRequestHeaders.js +94 -94
  60. package/src/runtime-pi/xResponse.js +68 -68
  61. package/src/runtime-pi/xResponseHeaders.js +109 -109
  62. package/src/runtime-pi/xURL.js +110 -110
  63. package/src/runtime-pi/xfetch.js +6 -6
  64. package/src/services-pi/certbot/http-auth-hook.js +22 -22
  65. package/src/services-pi/certbot/http-cleanup-hook.js +22 -22
  66. package/src/services-pi/certbot/index.js +79 -79
  67. package/src/services-pi/index.js +8 -8
  68. package/src/static-pi/index.js +10 -10
  69. package/src/webflo.js +31 -31
  70. package/test/index.test.js +26 -25
  71. package/test/site/package.json +9 -9
  72. package/test/site/public/bundle.html +5 -5
  73. package/test/site/public/bundle.html.json +3 -3
  74. package/test/site/public/bundle.js +2 -2
  75. package/test/site/public/bundle.webflo.js +15 -15
  76. package/test/site/public/index.html +29 -29
  77. package/test/site/public/index1.html +34 -34
  78. package/test/site/public/page-2/bundle.html +4 -4
  79. package/test/site/public/page-2/bundle.js +2 -2
  80. package/test/site/public/page-2/index.html +45 -45
  81. package/test/site/public/page-2/main.html +2 -2
  82. package/test/site/public/page-4/subpage/bundle.js +2 -2
  83. package/test/site/public/page-4/subpage/index.html +30 -30
  84. package/test/site/public/sparoots.json +4 -4
  85. package/test/site/public/worker.js +3 -3
  86. package/test/site/server/index.js +15 -15
@@ -1,548 +1,548 @@
1
-
2
- /**
3
- * @imports
4
- */
5
- import Fs from 'fs';
6
- import Path from 'path';
7
- import Http from 'http';
8
- import Https from 'https';
9
- import Formidable from 'formidable';
10
- import Sessions from 'client-sessions';
11
- import { Observer } from '@webqit/oohtml-ssr/apis.js';
12
- import { _each } from '@webqit/util/obj/index.js';
13
- import { _isEmpty } from '@webqit/util/js/index.js';
14
- import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
15
- import { slice as _streamSlice } from 'stream-slice';
16
- import { urlPattern } from '../util.js';
17
- import * as whatwag from './whatwag.js';
18
- import xURL from '../xURL.js';
19
- import xFormData from "../xFormData.js";
20
- import xRequestHeaders from "../xRequestHeaders.js";
21
- import xResponseHeaders from "../xResponseHeaders.js";
22
- import xRequest from "../xRequest.js";
23
- import xResponse from "../xResponse.js";
24
- import xfetch from '../xfetch.js';
25
- import xHttpEvent from '../xHttpEvent.js';
26
-
27
- const URL = xURL(whatwag.URL);
28
- const FormData = xFormData(whatwag.FormData);
29
- const ReadableStream = whatwag.ReadableStream;
30
- const RequestHeaders = xRequestHeaders(whatwag.Headers);
31
- const ResponseHeaders = xResponseHeaders(whatwag.Headers);
32
- const Request = xRequest(whatwag.Request, RequestHeaders, FormData, whatwag.Blob);
33
- const Response = xResponse(whatwag.Response, ResponseHeaders, FormData, whatwag.Blob);
34
- const fetch = xfetch(whatwag.fetch);
35
- const HttpEvent = xHttpEvent(Request, Response, URL);
36
-
37
- export {
38
- URL,
39
- FormData,
40
- ReadableStream,
41
- RequestHeaders,
42
- ResponseHeaders,
43
- Request,
44
- Response,
45
- fetch,
46
- HttpEvent,
47
- Observer,
48
- }
49
-
50
- export default class Runtime {
51
-
52
- /**
53
- * Runtime
54
- *
55
- * @param Object cx
56
- * @param Function clientCallback
57
- *
58
- * @return void
59
- */
60
- constructor(cx, clientCallback) {
61
- // ---------------
62
- this.cx = cx;
63
- this.clients = new Map;
64
- this.mockSessionStore = {};
65
- this.ready = (async () => {
66
-
67
- // ---------------
68
-
69
- const execClientCallback = (cx, hostname) => {
70
- let client = clientCallback(cx, hostname);
71
- if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
72
- return client;
73
- };
74
- const loadContextObj = async cx => {
75
- const meta = {};
76
- if (_isEmpty(cx.server)) { cx.server = await (new this.cx.config.runtime.Server(cx)).read(); }
77
- if (_isEmpty(cx.layout)) { cx.layout = await (new this.cx.config.deployment.Layout(cx)).read(); }
78
- if (_isEmpty(cx.env)) {
79
- let env = await (new this.cx.config.deployment.Env(cx)).read();
80
- cx.env = env.entries;
81
- meta.envAutoloading = env.autoload;
82
- }
83
- return meta;
84
- };
85
- let loadMeta = await loadContextObj(this.cx);
86
- if (this.cx.server.shared && (this.cx.config.deployment.Virtualization || !_isEmpty(this.cx.vcontexts))) {
87
- if (_isEmpty(this.cx.vcontexts)) {
88
- this.cx.vcontexts = {};
89
- const vhosts = await (new this.cx.config.deployment.Virtualization(cx)).read();
90
- await Promise.all((vhosts.entries || []).map(vhost => async () => {
91
- this.cx.vcontexts[vhost.host] = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
92
- await loadContextObj(this.cx.vcontexts[vhost.host]);
93
- }));
94
- }
95
- _each(this.cx.vcontexts, (host, vcontext) => {
96
- this.clients.set(vhost.host, execClientCallback(vcontext, host));
97
- });
98
- } else {
99
- this.clients.set('*', execClientCallback(this.cx, '*'));
100
- }
101
- // Always populate... regardless whether shared setup
102
- if (loadMeta.envAutoloading !== false) {
103
- Object.keys(this.cx.env).forEach(key => {
104
- if (!(key in process.env)) {
105
- process.env[key] = this.cx.env[key];
106
- }
107
- });
108
- }
109
-
110
- // ---------------
111
-
112
- if (!this.cx.flags['test-only'] && !this.cx.flags['https-only']) {
113
- Http.createServer((request, response) => handleRequest('http', request, response)).listen(process.env.PORT || this.cx.server.port);
114
- }
115
-
116
- // ---------------
117
-
118
- if (!this.cx.flags['test-only'] && !this.cx.flags['http-only'] && this.cx.server.https.port) {
119
- const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
120
- if (this.cx.server.shared) {
121
- _each(this.cx.vcontexts, (host, vcontext) => {
122
- if (Fs.existsSync(vcontext.server.https.keyfile)) {
123
- const cert = {
124
- key: Fs.readFileSync(vcontext.server.https.keyfile),
125
- cert: Fs.readFileSync(vcontext.server.https.certfile),
126
- };
127
- var domains = _arrFrom(vcontext.server.https.certdoms);
128
- if (!domains[0] || domains[0].trim() === '*') {
129
- httpsServer.addContext(host, cert);
130
- if (vcontext.server.force_www) {
131
- httpsServer.addContext(host.startsWith('www.') ? host.substr(4) : 'www.' + host, cert);
132
- }
133
- } else {
134
- domains.forEach(domain => {
135
- httpsServer.addContext(domain, cert);
136
- });
137
- }
138
- }
139
- });
140
- } else {
141
- if (Fs.existsSync(this.cx.server.https.keyfile)) {
142
- var domains = _arrFrom(this.cx.server.https.certdoms);
143
- var cert = {
144
- key: Fs.readFileSync(this.cx.server.https.keyfile),
145
- cert: Fs.readFileSync(this.cx.server.https.certfile),
146
- };
147
- if (!domains[0]) {
148
- domains = ['*'];
149
- }
150
- domains.forEach(domain => {
151
- httpsServer.addContext(domain, cert);
152
- });
153
- }
154
- }
155
- httpsServer.listen(process.env.PORT2 || this.cx.server.https.port);
156
- }
157
-
158
- // ---------------
159
-
160
- const handleRequest = async (protocol, request, response) => {
161
- // --------
162
- // Parse request
163
- const fullUrl = protocol + '://' + request.headers.host + request.url;
164
- const requestInit = { method: request.method, headers: request.headers };
165
- if (request.method !== 'GET' && request.method !== 'HEAD') {
166
- requestInit.body = await new Promise((resolve, reject) => {
167
- var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: false, keepExtensions: true });
168
- formidable.parse(request, (error, fields, files) => {
169
- if (error) {
170
- reject(error);
171
- return;
172
- }
173
- if (request.headers['content-type'] === 'application/json') {
174
- return resolve(fields);
175
- }
176
- const formData = new FormData;
177
- Object.keys(fields).forEach(name => {
178
- if (Array.isArray(fields[name])) {
179
- const values = Array.isArray(fields[name][0])
180
- ? fields[name][0]/* bugly a nested array when there are actually more than entry */
181
- : fields[name];
182
- values.forEach(value => {
183
- formData.append(!name.endsWith(']') ? name + '[]' : name, value);
184
- });
185
- } else {
186
- formData.append(name, fields[name]);
187
- }
188
- });
189
- Object.keys(files).forEach(name => {
190
- const fileCompat = file => {
191
- // IMPORTANT
192
- // Path up the "formidable" file in a way that "formdata-node"
193
- // to can translate it into its own file instance
194
- file[Symbol.toStringTag] = 'File';
195
- file.stream = () => Fs.createReadStream(file.path);
196
- // Done pathcing
197
- return file;
198
- }
199
- if (Array.isArray(files[name])) {
200
- files[name].forEach(value => {
201
- formData.append(name, fileCompat(value));
202
- });
203
- } else {
204
- formData.append(name, fileCompat(files[name]));
205
- }
206
- });
207
- resolve(formData);
208
- });
209
- });
210
- }
211
-
212
- // --------
213
- // Run Application
214
- let clientResponse = await this.go(fullUrl, requestInit, { request, response });
215
- if (response.headersSent) return;
216
-
217
- // --------
218
- // Set headers
219
- _each(clientResponse.headers.json(), (name, value) => {
220
- response.setHeader(name, value);
221
- });
222
-
223
- // --------
224
- // Send
225
- response.statusCode = clientResponse.status;
226
- response.statusMessage = clientResponse.statusText;
227
- if (clientResponse.headers.redirect) {
228
- response.end();
229
- } else {
230
- var body = clientResponse.body;
231
- if ((body instanceof ReadableStream)) {
232
- body.pipe(response);
233
- } else {
234
- // The default
235
- if (clientResponse.headers.contentType === 'application/json') {
236
- body += '';
237
- }
238
- response.end(body);
239
- }
240
- }
241
- };
242
-
243
- })();
244
- // ---------------
245
- Observer.set(this, 'location', {});
246
- Observer.set(this, 'network', {});
247
- // ---------------
248
- this.ready.then(() => {
249
- if (!this.cx.logger) return;
250
- if (this.cx.server.shared) {
251
- this.cx.logger.info(`> Server running (shared)`);
252
- } else {
253
- this.cx.logger.info(`> Server running (${this.cx.app.title || ''})::${this.cx.server.port}`);
254
- }
255
- });
256
- }
257
-
258
- /**
259
- * Performs a request.
260
- *
261
- * @param object|string url
262
- * @param object init
263
- * @param object detail
264
- *
265
- * @return Response
266
- */
267
- async go(url, init = {}, detail = {}) {
268
- await this.ready;
269
-
270
- // ------------
271
- url = typeof url === 'string' ? new whatwag.URL(url) : url;
272
- init = { referrer: this.location.href, ...init };
273
- // ------------
274
- let _context = this.cx, rdr;
275
- if (this.cx.server.shared && !(_context = this.cx.vcontexts[url.hostname])
276
- && !(url.hostname.startsWith('www.') && (_context = this.cx.vcontexts[url.hostname.substr(4)]) && _context.server.force_www)
277
- && !(!url.hostname.startsWith('www.') && (_context = this.cx.vcontexts['www.' + url.hostname]) && _context.server.force_www)) {
278
- rdr = { status: 500, statusText: 'Unrecognized host' };
279
- } else if (url.protocol === 'http:' && _context.server.https.force && !this.cx.flags['http-only'] && /** main server */this.cx.server.https.port) {
280
- rdr = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
281
- } else if (url.hostname.startsWith('www.') && _context.server.force_www === 'remove') {
282
- rdr = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
283
- } else if (!url.hostname.startsWith('www.') && _context.server.force_www === 'add') {
284
- rdr = { status: 302, headers: { Location: ( url.hostname = `www.${url.hostname}`, url.href ) } };
285
- } else if (_context.config.runtime.server.Redirects) {
286
- rdr = ((await (new _context.config.runtime.server.Redirects(_context)).read()).entries || []).reduce((_rdr, entry) => {
287
- return _rdr || ((_rdr = urlPattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
288
- }, null);
289
- }
290
- if (rdr) {
291
- return new Response(null, rdr);
292
- }
293
- const autoHeaders = _context.config.runtime.server.Headers
294
- ? ((await (new _context.config.runtime.server.Headers(_context)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
295
- : [];
296
- // ------------
297
-
298
- // ------------
299
- Observer.set(this.location, url, { detail: { ...init, ...detail, } });
300
- Observer.set(this.network, 'redirecting', null);
301
- // ------------
302
-
303
- // The request object
304
- let request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
305
- // The navigation event
306
- let httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
307
- return this.getSession(_context, httpEvent, id, options, callback);
308
- });
309
- // Response
310
- let client = this.clients.get('*'), response, finalResponse;
311
- if (this.cx.server.shared) {
312
- client = this.clients.get(url.hostname);
313
- }
314
- try {
315
- response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
316
- finalResponse = await this.handleResponse(_context, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
317
- } catch(e) {
318
- finalResponse = new Response(null, { status: 500, statusText: e.message });
319
- console.error(e);
320
- }
321
- // Logging
322
- if (this.cx.logger) {
323
- let log = this.generateLog(httpEvent, finalResponse);
324
- this.cx.logger.log(log);
325
- }
326
- // ------------
327
- // Return value
328
- return finalResponse;
329
- }
330
-
331
- // Generates request object
332
- generateRequest(href, init, autoHeaders = []) {
333
- let request = new Request(href, init);
334
- this._autoHeaders(request.headers, autoHeaders);
335
- return request;
336
- }
337
-
338
- // Generates session object
339
- getSession(cx, e, id, options = {}, callback = null) {
340
- let baseObject;
341
- if (!(e.detail.request && e.detail.response)) {
342
- baseObject = this.mockSessionStore;
343
- let cookieAvailability = e.request.headers.cookies[id]; // We just want to know availability... not validity, as this is understood to be for testing purposes only
344
- if (!(this.mockSessionStore[id] && cookieAvailability)) {
345
- let cookieObj = {};
346
- Object.defineProperty(this.mockSessionStore, id, {
347
- get: () => cookieObj,
348
- set: value => (cookieObj = value),
349
- });
350
- }
351
- } else {
352
- Sessions({
353
- duration: 0, // how long the session will stay valid in ms
354
- activeDuration: 0, // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
355
- ...options,
356
- cookieName: id, // cookie name dictates the key name added to the request object
357
- secret: cx.env.SESSION_KEY, // should be a large unguessable string
358
- })(e.detail.request, e.detail.response, e => {
359
- if (e) {
360
- if (!callback) throw e;
361
- callback(e);
362
- }
363
- });
364
- baseObject = e.detail.request;
365
- }
366
- // Where theres no error, instance is available
367
- let instance = Object.getOwnPropertyDescriptor(baseObject, id);
368
- if (!callback) return instance;
369
- if (instance) callback(null, instance);
370
- }
371
-
372
- // Initiates remote fetch and sets the status
373
- remoteFetch(request, ...args) {
374
- Observer.set(this.network, 'remote', true);
375
- let _response = fetch(request, ...args);
376
- // This catch() is NOT intended to handle failure of the fetch
377
- _response.catch(e => Observer.set(this.network, 'error', e.message));
378
- // Save a reference to this
379
- return _response.then(async response => {
380
- // Stop loading status
381
- Observer.set(this.network, 'remote', false);
382
- return new Response(response);
383
- });
384
- }
385
-
386
- // Handles response object
387
- async handleResponse(cx, e, response, autoHeaders = []) {
388
- if (!(response instanceof Response)) { response = new Response(response); }
389
- Observer.set(this.network, 'remote', false);
390
- Observer.set(this.network, 'error', null);
391
-
392
- // ----------------
393
- // Mock-Cookies?
394
- if (!(e.detail.request && e.detail.response)) {
395
- for (let cookieName of Object.getOwnPropertyNames(this.mockSessionStore)) {
396
- response.headers.set('Set-Cookie', `${cookieName}=1`); // We just want to know availability... not validity, as this is understood to be for testing purposes only
397
- }
398
- }
399
-
400
- // ----------------
401
- // Auto-Headers
402
- response.headers.set('Accept-Ranges', 'bytes');
403
- this._autoHeaders(response.headers, autoHeaders);
404
-
405
- // ----------------
406
- // Redirects
407
- if (response.headers.redirect) {
408
- let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
409
- let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
410
- let destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
411
- let isSameOriginRedirect = destinationUrl.origin === e.url.origin;
412
- let isSameSpaRedirect, sparootsFile = Path.join(cx.CWD, cx.layout.PUBLIC_DIR, 'sparoots.json');
413
- if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
414
- // Longest-first sorting
415
- let sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
416
- let matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
417
- isSameSpaRedirect = matchRoot(destinationUrl.pathname) === matchRoot(e.url.pathname);
418
- }
419
- if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
420
- response.headers.json({
421
- 'X-Redirect-Code': response.status,
422
- 'Access-Control-Allow-Origin': '*',
423
- 'Cache-Control': 'no-store',
424
- });
425
- response.attrs.status = xRedirectCode;
426
- }
427
- return response;
428
- }
429
-
430
- // ----------------
431
- // 404
432
- if (response.bodyAttrs.input === undefined || response.bodyAttrs.input === null) {
433
- response.attrs.status = response.status !== 200 ? response.status : 404;
434
- response.attrs.statusText = `${e.request.url} not found!`;
435
- return response;
436
- }
437
-
438
- // ----------------
439
- // Not acceptable
440
- if (e.request.headers.get('Accept') && !e.request.headers.accept.match(response.headers.contentType)) {
441
- response.attrs.status = 406;
442
- return response;
443
- }
444
-
445
- // ----------------
446
- // Important no-caching
447
- // for non-"get" requests
448
- if (e.request.method !== 'GET' && !response.headers.get('Cache-Control')) {
449
- response.headers.set('Cache-Control', 'no-store');
450
- }
451
-
452
- // ----------------
453
- // Body
454
- let rangeRequest, body = response.body;
455
- if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
456
- && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
457
- // ...in partials
458
- let totalLength = response.headers.contentLength || 0;
459
- let ranges = await rangeRequest.reduce(async (_ranges, range) => {
460
- _ranges = await _ranges;
461
- if (range[0] < 0 || (totalLength && range[0] > totalLength)
462
- || (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
463
- // The range is beyond upper/lower limits
464
- _ranges.error = true;
465
- }
466
- if (!totalLength && range[0] === undefined) {
467
- // totalLength is unknown and we cant read the trailing size specified in range[1]
468
- _ranges.error = true;
469
- }
470
- if (_ranges.error) return _ranges;
471
- if (totalLength) { range.clamp(totalLength); }
472
- let partLength = range[1] - range[0] + 1;
473
- _ranges.parts.push({
474
- body: body.pipe(_streamSlice(range[0], range[1] + 1)),
475
- range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
476
- length: partLength,
477
- });
478
- _ranges.totalLength += partLength;
479
- return _ranges;
480
- }, { parts: [], totalLength: 0 });
481
- if (ranges.error) {
482
- response.attrs.status = 416;
483
- response.headers.json({
484
- 'Content-Range': `bytes */${totalLength || '*'}`,
485
- 'Content-Length': 0,
486
- });
487
- } else {
488
- // TODO: of ranges.parts is more than one, return multipart/byteranges
489
- response = new Response(ranges.parts[0].body, {
490
- status: 206,
491
- statusText: response.statusText,
492
- headers: response.headers,
493
- });
494
- response.headers.json({
495
- 'Content-Range': ranges.parts[0].range,
496
- 'Content-Length': ranges.totalLength,
497
- });
498
- }
499
- }
500
-
501
- return response;
502
- }
503
-
504
- // Generates log
505
- generateLog(e, response) {
506
- let log = [];
507
- // ---------------
508
- let style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
509
- let errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
510
- let xRedirectCode = response.headers.get('X-Redirect-Code');
511
- let redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
512
- let statusCode = xRedirectCode || response.status;
513
- // ---------------
514
- log.push(`[${style.comment((new Date).toUTCString())}]`);
515
- log.push(style.keyword(e.request.method));
516
- log.push(style.url(e.request.url));
517
- if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
518
- if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
519
- if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
520
- if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
521
- else log.push(style.val(`${statusCode} ${response.statusText}`));
522
- if (redirectCode) log.push(`- ${style.url(response.headers.redirect)}`);
523
-
524
- return log.join(' ');
525
- }
526
-
527
- // Applies auto headers
528
- _autoHeaders(headers, autoHeaders) {
529
- autoHeaders.forEach(header => {
530
- var headerName = header.name.toLowerCase(),
531
- headerValue = header.value,
532
- isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
533
- isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
534
- if (isAppend || isPrepend) {
535
- headerValue = [ headers.get(headerName) || '' , headerValue].filter(v => v);
536
- headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
537
- }
538
- headers.set(headerName, headerValue);
539
- });
540
- }
541
-
542
- }
543
-
544
- const _streamRead = stream => new Promise(res => {
545
- let data = '';
546
- stream.on('data', chunk => data += chunk);
547
- stream.on('end', () => res(data));
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Fs from 'fs';
6
+ import Path from 'path';
7
+ import Http from 'http';
8
+ import Https from 'https';
9
+ import Formidable from 'formidable';
10
+ import Sessions from 'client-sessions';
11
+ import { Observer } from '@webqit/oohtml-ssr/apis.js';
12
+ import { _each } from '@webqit/util/obj/index.js';
13
+ import { _isEmpty } from '@webqit/util/js/index.js';
14
+ import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
15
+ import { slice as _streamSlice } from 'stream-slice';
16
+ import { urlPattern } from '../util.js';
17
+ import * as whatwag from './whatwag.js';
18
+ import xURL from '../xURL.js';
19
+ import xFormData from "../xFormData.js";
20
+ import xRequestHeaders from "../xRequestHeaders.js";
21
+ import xResponseHeaders from "../xResponseHeaders.js";
22
+ import xRequest from "../xRequest.js";
23
+ import xResponse from "../xResponse.js";
24
+ import xfetch from '../xfetch.js';
25
+ import xHttpEvent from '../xHttpEvent.js';
26
+
27
+ const URL = xURL(whatwag.URL);
28
+ const FormData = xFormData(whatwag.FormData);
29
+ const ReadableStream = whatwag.ReadableStream;
30
+ const RequestHeaders = xRequestHeaders(whatwag.Headers);
31
+ const ResponseHeaders = xResponseHeaders(whatwag.Headers);
32
+ const Request = xRequest(whatwag.Request, RequestHeaders, FormData, whatwag.Blob);
33
+ const Response = xResponse(whatwag.Response, ResponseHeaders, FormData, whatwag.Blob);
34
+ const fetch = xfetch(whatwag.fetch);
35
+ const HttpEvent = xHttpEvent(Request, Response, URL);
36
+
37
+ export {
38
+ URL,
39
+ FormData,
40
+ ReadableStream,
41
+ RequestHeaders,
42
+ ResponseHeaders,
43
+ Request,
44
+ Response,
45
+ fetch,
46
+ HttpEvent,
47
+ Observer,
48
+ }
49
+
50
+ export default class Runtime {
51
+
52
+ /**
53
+ * Runtime
54
+ *
55
+ * @param Object cx
56
+ * @param Function clientCallback
57
+ *
58
+ * @return void
59
+ */
60
+ constructor(cx, clientCallback) {
61
+ // ---------------
62
+ this.cx = cx;
63
+ this.clients = new Map;
64
+ this.mockSessionStore = {};
65
+ this.ready = (async () => {
66
+
67
+ // ---------------
68
+
69
+ const execClientCallback = (cx, hostname) => {
70
+ let client = clientCallback(cx, hostname);
71
+ if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
72
+ return client;
73
+ };
74
+ const loadContextObj = async cx => {
75
+ const meta = {};
76
+ if (_isEmpty(cx.server)) { cx.server = await (new this.cx.config.runtime.Server(cx)).read(); }
77
+ if (_isEmpty(cx.layout)) { cx.layout = await (new this.cx.config.deployment.Layout(cx)).read(); }
78
+ if (_isEmpty(cx.env)) {
79
+ let env = await (new this.cx.config.deployment.Env(cx)).read();
80
+ cx.env = env.entries;
81
+ meta.envAutoloading = env.autoload;
82
+ }
83
+ return meta;
84
+ };
85
+ let loadMeta = await loadContextObj(this.cx);
86
+ if (this.cx.server.shared && (this.cx.config.deployment.Virtualization || !_isEmpty(this.cx.vcontexts))) {
87
+ if (_isEmpty(this.cx.vcontexts)) {
88
+ this.cx.vcontexts = {};
89
+ const vhosts = await (new this.cx.config.deployment.Virtualization(cx)).read();
90
+ await Promise.all((vhosts.entries || []).map(vhost => async () => {
91
+ this.cx.vcontexts[vhost.host] = this.cx.constructor.create(this.cx, Path.join(this.cx.CWD, vhost.path));
92
+ await loadContextObj(this.cx.vcontexts[vhost.host]);
93
+ }));
94
+ }
95
+ _each(this.cx.vcontexts, (host, vcontext) => {
96
+ this.clients.set(vhost.host, execClientCallback(vcontext, host));
97
+ });
98
+ } else {
99
+ this.clients.set('*', execClientCallback(this.cx, '*'));
100
+ }
101
+ // Always populate... regardless whether shared setup
102
+ if (loadMeta.envAutoloading !== false) {
103
+ Object.keys(this.cx.env).forEach(key => {
104
+ if (!(key in process.env)) {
105
+ process.env[key] = this.cx.env[key];
106
+ }
107
+ });
108
+ }
109
+
110
+ // ---------------
111
+
112
+ if (!this.cx.flags['test-only'] && !this.cx.flags['https-only']) {
113
+ Http.createServer((request, response) => handleRequest('http', request, response)).listen(process.env.PORT || this.cx.server.port);
114
+ }
115
+
116
+ // ---------------
117
+
118
+ if (!this.cx.flags['test-only'] && !this.cx.flags['http-only'] && this.cx.server.https.port) {
119
+ const httpsServer = Https.createServer((request, response) => handleRequest('https', request, response));
120
+ if (this.cx.server.shared) {
121
+ _each(this.cx.vcontexts, (host, vcontext) => {
122
+ if (Fs.existsSync(vcontext.server.https.keyfile)) {
123
+ const cert = {
124
+ key: Fs.readFileSync(vcontext.server.https.keyfile),
125
+ cert: Fs.readFileSync(vcontext.server.https.certfile),
126
+ };
127
+ var domains = _arrFrom(vcontext.server.https.certdoms);
128
+ if (!domains[0] || domains[0].trim() === '*') {
129
+ httpsServer.addContext(host, cert);
130
+ if (vcontext.server.force_www) {
131
+ httpsServer.addContext(host.startsWith('www.') ? host.substr(4) : 'www.' + host, cert);
132
+ }
133
+ } else {
134
+ domains.forEach(domain => {
135
+ httpsServer.addContext(domain, cert);
136
+ });
137
+ }
138
+ }
139
+ });
140
+ } else {
141
+ if (Fs.existsSync(this.cx.server.https.keyfile)) {
142
+ var domains = _arrFrom(this.cx.server.https.certdoms);
143
+ var cert = {
144
+ key: Fs.readFileSync(this.cx.server.https.keyfile),
145
+ cert: Fs.readFileSync(this.cx.server.https.certfile),
146
+ };
147
+ if (!domains[0]) {
148
+ domains = ['*'];
149
+ }
150
+ domains.forEach(domain => {
151
+ httpsServer.addContext(domain, cert);
152
+ });
153
+ }
154
+ }
155
+ httpsServer.listen(process.env.PORT2 || this.cx.server.https.port);
156
+ }
157
+
158
+ // ---------------
159
+
160
+ const handleRequest = async (protocol, request, response) => {
161
+ // --------
162
+ // Parse request
163
+ const fullUrl = protocol + '://' + request.headers.host + request.url;
164
+ const requestInit = { method: request.method, headers: request.headers };
165
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
166
+ requestInit.body = await new Promise((resolve, reject) => {
167
+ var formidable = new Formidable.IncomingForm({ multiples: true, allowEmptyFiles: false, keepExtensions: true });
168
+ formidable.parse(request, (error, fields, files) => {
169
+ if (error) {
170
+ reject(error);
171
+ return;
172
+ }
173
+ if (request.headers['content-type'] === 'application/json') {
174
+ return resolve(fields);
175
+ }
176
+ const formData = new FormData;
177
+ Object.keys(fields).forEach(name => {
178
+ if (Array.isArray(fields[name])) {
179
+ const values = Array.isArray(fields[name][0])
180
+ ? fields[name][0]/* bugly a nested array when there are actually more than entry */
181
+ : fields[name];
182
+ values.forEach(value => {
183
+ formData.append(!name.endsWith(']') ? name + '[]' : name, value);
184
+ });
185
+ } else {
186
+ formData.append(name, fields[name]);
187
+ }
188
+ });
189
+ Object.keys(files).forEach(name => {
190
+ const fileCompat = file => {
191
+ // IMPORTANT
192
+ // Path up the "formidable" file in a way that "formdata-node"
193
+ // to can translate it into its own file instance
194
+ file[Symbol.toStringTag] = 'File';
195
+ file.stream = () => Fs.createReadStream(file.path);
196
+ // Done pathcing
197
+ return file;
198
+ }
199
+ if (Array.isArray(files[name])) {
200
+ files[name].forEach(value => {
201
+ formData.append(name, fileCompat(value));
202
+ });
203
+ } else {
204
+ formData.append(name, fileCompat(files[name]));
205
+ }
206
+ });
207
+ resolve(formData);
208
+ });
209
+ });
210
+ }
211
+
212
+ // --------
213
+ // Run Application
214
+ let clientResponse = await this.go(fullUrl, requestInit, { request, response });
215
+ if (response.headersSent) return;
216
+
217
+ // --------
218
+ // Set headers
219
+ _each(clientResponse.headers.json(), (name, value) => {
220
+ response.setHeader(name, value);
221
+ });
222
+
223
+ // --------
224
+ // Send
225
+ response.statusCode = clientResponse.status;
226
+ response.statusMessage = clientResponse.statusText;
227
+ if (clientResponse.headers.redirect) {
228
+ response.end();
229
+ } else {
230
+ var body = clientResponse.body;
231
+ if ((body instanceof ReadableStream)) {
232
+ body.pipe(response);
233
+ } else {
234
+ // The default
235
+ if (clientResponse.headers.contentType === 'application/json') {
236
+ body += '';
237
+ }
238
+ response.end(body);
239
+ }
240
+ }
241
+ };
242
+
243
+ })();
244
+ // ---------------
245
+ Observer.set(this, 'location', {});
246
+ Observer.set(this, 'network', {});
247
+ // ---------------
248
+ this.ready.then(() => {
249
+ if (!this.cx.logger) return;
250
+ if (this.cx.server.shared) {
251
+ this.cx.logger.info(`> Server running (shared)`);
252
+ } else {
253
+ this.cx.logger.info(`> Server running (${this.cx.app.title || ''})::${this.cx.server.port}`);
254
+ }
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Performs a request.
260
+ *
261
+ * @param object|string url
262
+ * @param object init
263
+ * @param object detail
264
+ *
265
+ * @return Response
266
+ */
267
+ async go(url, init = {}, detail = {}) {
268
+ await this.ready;
269
+
270
+ // ------------
271
+ url = typeof url === 'string' ? new whatwag.URL(url) : url;
272
+ init = { referrer: this.location.href, ...init };
273
+ // ------------
274
+ let _context = this.cx, rdr;
275
+ if (this.cx.server.shared && !(_context = this.cx.vcontexts[url.hostname])
276
+ && !(url.hostname.startsWith('www.') && (_context = this.cx.vcontexts[url.hostname.substr(4)]) && _context.server.force_www)
277
+ && !(!url.hostname.startsWith('www.') && (_context = this.cx.vcontexts['www.' + url.hostname]) && _context.server.force_www)) {
278
+ rdr = { status: 500, statusText: 'Unrecognized host' };
279
+ } else if (url.protocol === 'http:' && _context.server.https.force && !this.cx.flags['http-only'] && /** main server */this.cx.server.https.port) {
280
+ rdr = { status: 302, headers: { Location: ( url.protocol = 'https:', url.href ) } };
281
+ } else if (url.hostname.startsWith('www.') && _context.server.force_www === 'remove') {
282
+ rdr = { status: 302, headers: { Location: ( url.hostname = url.hostname.substr(4), url.href ) } };
283
+ } else if (!url.hostname.startsWith('www.') && _context.server.force_www === 'add') {
284
+ rdr = { status: 302, headers: { Location: ( url.hostname = `www.${url.hostname}`, url.href ) } };
285
+ } else if (_context.config.runtime.server.Redirects) {
286
+ rdr = ((await (new _context.config.runtime.server.Redirects(_context)).read()).entries || []).reduce((_rdr, entry) => {
287
+ return _rdr || ((_rdr = urlPattern(entry.from, url.origin).exec(url.href)) && { status: entry.code || 302, headers: { Location: _rdr.render(entry.to) } });
288
+ }, null);
289
+ }
290
+ if (rdr) {
291
+ return new Response(null, rdr);
292
+ }
293
+ const autoHeaders = _context.config.runtime.server.Headers
294
+ ? ((await (new _context.config.runtime.server.Headers(_context)).read()).entries || []).filter(entry => urlPattern(entry.url, url.origin).exec(url.href))
295
+ : [];
296
+ // ------------
297
+
298
+ // ------------
299
+ Observer.set(this.location, url, { detail: { ...init, ...detail, } });
300
+ Observer.set(this.network, 'redirecting', null);
301
+ // ------------
302
+
303
+ // The request object
304
+ let request = this.generateRequest(url.href, init, autoHeaders.filter(header => header.type === 'request'));
305
+ // The navigation event
306
+ let httpEvent = new HttpEvent(request, detail, (id = 'session', options = { duration: 60 * 60 * 24, activeDuration: 60 * 60 * 24 }, callback = null) => {
307
+ return this.getSession(_context, httpEvent, id, options, callback);
308
+ });
309
+ // Response
310
+ let client = this.clients.get('*'), response, finalResponse;
311
+ if (this.cx.server.shared) {
312
+ client = this.clients.get(url.hostname);
313
+ }
314
+ try {
315
+ response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
316
+ finalResponse = await this.handleResponse(_context, httpEvent, response, autoHeaders.filter(header => header.type === 'response'));
317
+ } catch(e) {
318
+ finalResponse = new Response(null, { status: 500, statusText: e.message });
319
+ console.error(e);
320
+ }
321
+ // Logging
322
+ if (this.cx.logger) {
323
+ let log = this.generateLog(httpEvent, finalResponse);
324
+ this.cx.logger.log(log);
325
+ }
326
+ // ------------
327
+ // Return value
328
+ return finalResponse;
329
+ }
330
+
331
+ // Generates request object
332
+ generateRequest(href, init, autoHeaders = []) {
333
+ let request = new Request(href, init);
334
+ this._autoHeaders(request.headers, autoHeaders);
335
+ return request;
336
+ }
337
+
338
+ // Generates session object
339
+ getSession(cx, e, id, options = {}, callback = null) {
340
+ let baseObject;
341
+ if (!(e.detail.request && e.detail.response)) {
342
+ baseObject = this.mockSessionStore;
343
+ let cookieAvailability = e.request.headers.cookies[id]; // We just want to know availability... not validity, as this is understood to be for testing purposes only
344
+ if (!(this.mockSessionStore[id] && cookieAvailability)) {
345
+ let cookieObj = {};
346
+ Object.defineProperty(this.mockSessionStore, id, {
347
+ get: () => cookieObj,
348
+ set: value => (cookieObj = value),
349
+ });
350
+ }
351
+ } else {
352
+ Sessions({
353
+ duration: 0, // how long the session will stay valid in ms
354
+ activeDuration: 0, // if expiresIn < activeDuration, the session will be extended by activeDuration milliseconds
355
+ ...options,
356
+ cookieName: id, // cookie name dictates the key name added to the request object
357
+ secret: cx.env.SESSION_KEY, // should be a large unguessable string
358
+ })(e.detail.request, e.detail.response, e => {
359
+ if (e) {
360
+ if (!callback) throw e;
361
+ callback(e);
362
+ }
363
+ });
364
+ baseObject = e.detail.request;
365
+ }
366
+ // Where theres no error, instance is available
367
+ let instance = Object.getOwnPropertyDescriptor(baseObject, id);
368
+ if (!callback) return instance;
369
+ if (instance) callback(null, instance);
370
+ }
371
+
372
+ // Initiates remote fetch and sets the status
373
+ remoteFetch(request, ...args) {
374
+ Observer.set(this.network, 'remote', true);
375
+ let _response = fetch(request, ...args);
376
+ // This catch() is NOT intended to handle failure of the fetch
377
+ _response.catch(e => Observer.set(this.network, 'error', e.message));
378
+ // Save a reference to this
379
+ return _response.then(async response => {
380
+ // Stop loading status
381
+ Observer.set(this.network, 'remote', false);
382
+ return new Response(response);
383
+ });
384
+ }
385
+
386
+ // Handles response object
387
+ async handleResponse(cx, e, response, autoHeaders = []) {
388
+ if (!(response instanceof Response)) { response = new Response(response); }
389
+ Observer.set(this.network, 'remote', false);
390
+ Observer.set(this.network, 'error', null);
391
+
392
+ // ----------------
393
+ // Mock-Cookies?
394
+ if (!(e.detail.request && e.detail.response)) {
395
+ for (let cookieName of Object.getOwnPropertyNames(this.mockSessionStore)) {
396
+ response.headers.set('Set-Cookie', `${cookieName}=1`); // We just want to know availability... not validity, as this is understood to be for testing purposes only
397
+ }
398
+ }
399
+
400
+ // ----------------
401
+ // Auto-Headers
402
+ response.headers.set('Accept-Ranges', 'bytes');
403
+ this._autoHeaders(response.headers, autoHeaders);
404
+
405
+ // ----------------
406
+ // Redirects
407
+ if (response.headers.redirect) {
408
+ let xRedirectPolicy = e.request.headers.get('X-Redirect-Policy');
409
+ let xRedirectCode = e.request.headers.get('X-Redirect-Code') || 300;
410
+ let destinationUrl = new whatwag.URL(response.headers.location, e.url.origin);
411
+ let isSameOriginRedirect = destinationUrl.origin === e.url.origin;
412
+ let isSameSpaRedirect, sparootsFile = Path.join(cx.CWD, cx.layout.PUBLIC_DIR, 'sparoots.json');
413
+ if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
414
+ // Longest-first sorting
415
+ let sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
416
+ let matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
417
+ isSameSpaRedirect = matchRoot(destinationUrl.pathname) === matchRoot(e.url.pathname);
418
+ }
419
+ if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
420
+ response.headers.json({
421
+ 'X-Redirect-Code': response.status,
422
+ 'Access-Control-Allow-Origin': '*',
423
+ 'Cache-Control': 'no-store',
424
+ });
425
+ response.attrs.status = xRedirectCode;
426
+ }
427
+ return response;
428
+ }
429
+
430
+ // ----------------
431
+ // 404
432
+ if (response.bodyAttrs.input === undefined || response.bodyAttrs.input === null) {
433
+ response.attrs.status = response.status !== 200 ? response.status : 404;
434
+ response.attrs.statusText = `${e.request.url} not found!`;
435
+ return response;
436
+ }
437
+
438
+ // ----------------
439
+ // Not acceptable
440
+ if (e.request.headers.get('Accept') && !e.request.headers.accept.match(response.headers.contentType)) {
441
+ response.attrs.status = 406;
442
+ return response;
443
+ }
444
+
445
+ // ----------------
446
+ // Important no-caching
447
+ // for non-"get" requests
448
+ if (e.request.method !== 'GET' && !response.headers.get('Cache-Control')) {
449
+ response.headers.set('Cache-Control', 'no-store');
450
+ }
451
+
452
+ // ----------------
453
+ // Body
454
+ let rangeRequest, body = response.body;
455
+ if ((rangeRequest = e.request.headers.range) && !response.headers.get('Content-Range')
456
+ && ((body instanceof ReadableStream) || (ArrayBuffer.isView(body) && (body = ReadableStream.from(body))))) {
457
+ // ...in partials
458
+ let totalLength = response.headers.contentLength || 0;
459
+ let ranges = await rangeRequest.reduce(async (_ranges, range) => {
460
+ _ranges = await _ranges;
461
+ if (range[0] < 0 || (totalLength && range[0] > totalLength)
462
+ || (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
463
+ // The range is beyond upper/lower limits
464
+ _ranges.error = true;
465
+ }
466
+ if (!totalLength && range[0] === undefined) {
467
+ // totalLength is unknown and we cant read the trailing size specified in range[1]
468
+ _ranges.error = true;
469
+ }
470
+ if (_ranges.error) return _ranges;
471
+ if (totalLength) { range.clamp(totalLength); }
472
+ let partLength = range[1] - range[0] + 1;
473
+ _ranges.parts.push({
474
+ body: body.pipe(_streamSlice(range[0], range[1] + 1)),
475
+ range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
476
+ length: partLength,
477
+ });
478
+ _ranges.totalLength += partLength;
479
+ return _ranges;
480
+ }, { parts: [], totalLength: 0 });
481
+ if (ranges.error) {
482
+ response.attrs.status = 416;
483
+ response.headers.json({
484
+ 'Content-Range': `bytes */${totalLength || '*'}`,
485
+ 'Content-Length': 0,
486
+ });
487
+ } else {
488
+ // TODO: of ranges.parts is more than one, return multipart/byteranges
489
+ response = new Response(ranges.parts[0].body, {
490
+ status: 206,
491
+ statusText: response.statusText,
492
+ headers: response.headers,
493
+ });
494
+ response.headers.json({
495
+ 'Content-Range': ranges.parts[0].range,
496
+ 'Content-Length': ranges.totalLength,
497
+ });
498
+ }
499
+ }
500
+
501
+ return response;
502
+ }
503
+
504
+ // Generates log
505
+ generateLog(e, response) {
506
+ let log = [];
507
+ // ---------------
508
+ let style = this.cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
509
+ let errorCode = [ 404, 500 ].includes(response.status) ? response.status : 0;
510
+ let xRedirectCode = response.headers.get('X-Redirect-Code');
511
+ let redirectCode = xRedirectCode || ((response.status + '').startsWith('3') ? response.status : 0);
512
+ let statusCode = xRedirectCode || response.status;
513
+ // ---------------
514
+ log.push(`[${style.comment((new Date).toUTCString())}]`);
515
+ log.push(style.keyword(e.request.method));
516
+ log.push(style.url(e.request.url));
517
+ if (response.attrs.hint) log.push(`(${style.comment(response.attrs.hint)})`);
518
+ if (response.headers.contentType) log.push(`(${style.comment(response.headers.contentType)})`);
519
+ if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
520
+ if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
521
+ else log.push(style.val(`${statusCode} ${response.statusText}`));
522
+ if (redirectCode) log.push(`- ${style.url(response.headers.redirect)}`);
523
+
524
+ return log.join(' ');
525
+ }
526
+
527
+ // Applies auto headers
528
+ _autoHeaders(headers, autoHeaders) {
529
+ autoHeaders.forEach(header => {
530
+ var headerName = header.name.toLowerCase(),
531
+ headerValue = header.value,
532
+ isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
533
+ isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
534
+ if (isAppend || isPrepend) {
535
+ headerValue = [ headers.get(headerName) || '' , headerValue].filter(v => v);
536
+ headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
537
+ }
538
+ headers.set(headerName, headerValue);
539
+ });
540
+ }
541
+
542
+ }
543
+
544
+ const _streamRead = stream => new Promise(res => {
545
+ let data = '';
546
+ stream.on('data', chunk => data += chunk);
547
+ stream.on('end', () => res(data));
548
548
  });