@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.
- package/.gitignore +7 -7
- package/LICENSE +20 -20
- package/README.md +2074 -2071
- package/package.json +82 -82
- package/src/Context.js +79 -79
- package/src/config-pi/deployment/Env.js +69 -69
- package/src/config-pi/deployment/Layout.js +65 -65
- package/src/config-pi/deployment/Origins.js +133 -133
- package/src/config-pi/deployment/Virtualization.js +65 -65
- package/src/config-pi/deployment/index.js +17 -17
- package/src/config-pi/index.js +15 -15
- package/src/config-pi/runtime/Client.js +101 -101
- package/src/config-pi/runtime/Server.js +128 -128
- package/src/config-pi/runtime/client/Worker.js +135 -135
- package/src/config-pi/runtime/client/index.js +11 -11
- package/src/config-pi/runtime/index.js +17 -17
- package/src/config-pi/runtime/server/Headers.js +77 -77
- package/src/config-pi/runtime/server/Redirects.js +73 -73
- package/src/config-pi/runtime/server/index.js +13 -13
- package/src/config-pi/static/Manifest.js +321 -321
- package/src/config-pi/static/Ssg.js +51 -51
- package/src/config-pi/static/index.js +13 -13
- package/src/deployment-pi/index.js +10 -10
- package/src/deployment-pi/origins/index.js +215 -215
- package/src/index.js +19 -19
- package/src/runtime-pi/Router.js +131 -131
- package/src/runtime-pi/client/Context.js +6 -6
- package/src/runtime-pi/client/Router.js +47 -47
- package/src/runtime-pi/client/Runtime.js +357 -341
- package/src/runtime-pi/client/RuntimeClient.js +98 -98
- package/src/runtime-pi/client/Storage.js +56 -56
- package/src/runtime-pi/client/Url.js +205 -205
- package/src/runtime-pi/client/Workport.js +163 -163
- package/src/runtime-pi/client/generate.js +467 -467
- package/src/runtime-pi/client/index.js +23 -23
- package/src/runtime-pi/client/oohtml/full.js +6 -6
- package/src/runtime-pi/client/oohtml/namespacing.js +6 -6
- package/src/runtime-pi/client/oohtml/scripting.js +7 -7
- package/src/runtime-pi/client/oohtml/templating.js +7 -7
- package/src/runtime-pi/client/whatwag.js +27 -27
- package/src/runtime-pi/client/worker/Context.js +6 -6
- package/src/runtime-pi/client/worker/Worker.js +291 -291
- package/src/runtime-pi/client/worker/WorkerClient.js +46 -46
- package/src/runtime-pi/client/worker/Workport.js +79 -79
- package/src/runtime-pi/client/worker/index.js +23 -23
- package/src/runtime-pi/index.js +13 -13
- package/src/runtime-pi/server/Context.js +15 -15
- package/src/runtime-pi/server/Router.js +157 -157
- package/src/runtime-pi/server/Runtime.js +547 -547
- package/src/runtime-pi/server/RuntimeClient.js +112 -112
- package/src/runtime-pi/server/index.js +23 -23
- package/src/runtime-pi/server/whatwag.js +35 -35
- package/src/runtime-pi/util.js +162 -162
- package/src/runtime-pi/xFormData.js +59 -59
- package/src/runtime-pi/xHeaders.js +87 -87
- package/src/runtime-pi/xHttpEvent.js +92 -92
- package/src/runtime-pi/xHttpMessage.js +179 -179
- package/src/runtime-pi/xRequest.js +73 -73
- package/src/runtime-pi/xRequestHeaders.js +94 -94
- package/src/runtime-pi/xResponse.js +68 -68
- package/src/runtime-pi/xResponseHeaders.js +109 -109
- package/src/runtime-pi/xURL.js +110 -110
- package/src/runtime-pi/xfetch.js +6 -6
- package/src/services-pi/certbot/http-auth-hook.js +22 -22
- package/src/services-pi/certbot/http-cleanup-hook.js +22 -22
- package/src/services-pi/certbot/index.js +79 -79
- package/src/services-pi/index.js +8 -8
- package/src/static-pi/index.js +10 -10
- package/src/webflo.js +31 -31
- package/test/index.test.js +26 -25
- package/test/site/package.json +9 -9
- package/test/site/public/bundle.html +5 -5
- package/test/site/public/bundle.html.json +3 -3
- package/test/site/public/bundle.js +2 -2
- package/test/site/public/bundle.webflo.js +15 -15
- package/test/site/public/index.html +29 -29
- package/test/site/public/index1.html +34 -34
- package/test/site/public/page-2/bundle.html +4 -4
- package/test/site/public/page-2/bundle.js +2 -2
- package/test/site/public/page-2/index.html +45 -45
- package/test/site/public/page-2/main.html +2 -2
- package/test/site/public/page-4/subpage/bundle.js +2 -2
- package/test/site/public/page-4/subpage/index.html +30 -30
- package/test/site/public/sparoots.json +4 -4
- package/test/site/public/worker.js +3 -3
- 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
|
});
|