@webqit/webflo 0.11.61 → 1.0.1
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/package.json +1 -1
- package/src/{Context.js → AbstractContext.js} +1 -9
- package/src/deployment-pi/origins/index.js +1 -1
- package/src/index.js +1 -9
- package/src/runtime-pi/HttpEvent.js +101 -81
- package/src/runtime-pi/HttpUser.js +126 -0
- package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
- package/src/runtime-pi/MessagingOverChannel.js +85 -0
- package/src/runtime-pi/MessagingOverSocket.js +106 -0
- package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
- package/src/runtime-pi/WebfloCookieStorage.js +27 -0
- package/src/runtime-pi/WebfloEventTarget.js +39 -0
- package/src/runtime-pi/WebfloMessageEvent.js +58 -0
- package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
- package/src/runtime-pi/{Router.js → WebfloRouter.js} +3 -34
- package/src/runtime-pi/WebfloRuntime.js +52 -0
- package/src/runtime-pi/WebfloStorage.js +109 -0
- package/src/runtime-pi/client/ClientMessaging.js +5 -0
- package/src/runtime-pi/client/Context.js +2 -6
- package/src/runtime-pi/client/CookieStorage.js +17 -0
- package/src/runtime-pi/client/Router.js +3 -13
- package/src/runtime-pi/client/SessionStorage.js +33 -0
- package/src/runtime-pi/client/Url.js +24 -72
- package/src/runtime-pi/client/WebfloClient.js +544 -0
- package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
- package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
- package/src/runtime-pi/client/WebfloSubClient.js +165 -0
- package/src/runtime-pi/client/Workport.js +89 -161
- package/src/runtime-pi/client/generate.js +3 -3
- package/src/runtime-pi/client/index.js +13 -18
- package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
- package/src/runtime-pi/client/worker/Context.js +2 -6
- package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
- package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
- package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
- package/src/runtime-pi/client/worker/Workport.js +13 -73
- package/src/runtime-pi/client/worker/index.js +7 -18
- package/src/runtime-pi/index.js +1 -8
- package/src/runtime-pi/server/ClientMessaging.js +18 -0
- package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
- package/src/runtime-pi/server/Context.js +2 -6
- package/src/runtime-pi/server/CookieStorage.js +17 -0
- package/src/runtime-pi/server/Router.js +2 -68
- package/src/runtime-pi/server/SessionStorage.js +53 -0
- package/src/runtime-pi/server/WebfloServer.js +755 -0
- package/src/runtime-pi/server/index.js +7 -18
- package/src/runtime-pi/util-http.js +268 -32
- package/src/runtime-pi/xURL.js +25 -22
- package/src/runtime-pi/xfetch.js +2 -2
- package/src/runtime-pi/Application.js +0 -29
- package/src/runtime-pi/Cookies.js +0 -82
- package/src/runtime-pi/Runtime.js +0 -21
- package/src/runtime-pi/client/Application.js +0 -76
- package/src/runtime-pi/client/Runtime.js +0 -525
- package/src/runtime-pi/client/createStorage.js +0 -58
- package/src/runtime-pi/client/worker/Application.js +0 -44
- package/src/runtime-pi/client/worker/Runtime.js +0 -275
- package/src/runtime-pi/server/Application.js +0 -101
- package/src/runtime-pi/server/Runtime.js +0 -558
- package/src/runtime-pi/xFormData.js +0 -24
- package/src/runtime-pi/xHeaders.js +0 -146
- package/src/runtime-pi/xRequest.js +0 -46
- package/src/runtime-pi/xRequestHeaders.js +0 -109
- package/src/runtime-pi/xResponse.js +0 -33
- package/src/runtime-pi/xResponseHeaders.js +0 -117
- package/src/runtime-pi/xxHttpMessage.js +0 -102
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import Fs from 'fs';
|
|
2
|
+
import Url from 'url';
|
|
3
|
+
import Path from 'path';
|
|
4
|
+
import Http from 'http';
|
|
5
|
+
import Https from 'https';
|
|
6
|
+
import WebSocket from 'ws';
|
|
7
|
+
import Mime from 'mime-types';
|
|
8
|
+
import QueryString from 'querystring';
|
|
9
|
+
import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
|
|
10
|
+
import { _isEmpty, _isObject } from '@webqit/util/js/index.js';
|
|
11
|
+
import { _each } from '@webqit/util/obj/index.js';
|
|
12
|
+
import { slice as _streamSlice } from 'stream-slice';
|
|
13
|
+
import { Readable as _ReadableStream } from 'stream';
|
|
14
|
+
import { WebfloRuntime } from '../WebfloRuntime.js';
|
|
15
|
+
import { Context } from './Context.js';
|
|
16
|
+
import { CookieStorage } from './CookieStorage.js';
|
|
17
|
+
import { SessionStorage } from './SessionStorage.js';
|
|
18
|
+
import { MessagingOverSocket } from '../MessagingOverSocket.js';
|
|
19
|
+
import { ClientMessagingRegistry } from './ClientMessagingRegistry.js';
|
|
20
|
+
import { HttpEvent } from '../HttpEvent.js';
|
|
21
|
+
import { HttpUser } from '../HttpUser.js';
|
|
22
|
+
import { Router } from './Router.js';
|
|
23
|
+
import { pattern } from '../util-url.js';
|
|
24
|
+
import xfetch from '../xfetch.js';
|
|
25
|
+
import '../util-http.js';
|
|
26
|
+
|
|
27
|
+
const parseDomains = (domains) => _arrFrom(domains).reduce((arr, str) => arr.concat(str.split(',')), []).map(str => str.trim()).filter(str => str);
|
|
28
|
+
const selectDomains = (serverDefs, matchingPort = null) => serverDefs.reduce((doms, def) => doms.length ? doms : (((!matchingPort || def.port === matchingPort) && parseDomains(def.domains || def.hostnames)) || []), []);
|
|
29
|
+
|
|
30
|
+
export class WebfloServer extends WebfloRuntime {
|
|
31
|
+
|
|
32
|
+
static get Context() { return Context; }
|
|
33
|
+
|
|
34
|
+
static get Router() { return Router; }
|
|
35
|
+
|
|
36
|
+
static get HttpEvent() { return HttpEvent; }
|
|
37
|
+
|
|
38
|
+
static get CookieStorage() { return CookieStorage; }
|
|
39
|
+
|
|
40
|
+
static get SessionStorage() { return SessionStorage; }
|
|
41
|
+
|
|
42
|
+
static get HttpUser() { return HttpUser; }
|
|
43
|
+
|
|
44
|
+
static create(cx) {
|
|
45
|
+
return new this(this.Context.create(cx));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#cx;
|
|
49
|
+
#servers = new Map;
|
|
50
|
+
#proxies = new Map;
|
|
51
|
+
#sockets = new Map;
|
|
52
|
+
|
|
53
|
+
// Typically for access by Router
|
|
54
|
+
get cx() { return this.#cx; }
|
|
55
|
+
|
|
56
|
+
constructor(cx) {
|
|
57
|
+
super();
|
|
58
|
+
if (!(cx instanceof this.constructor.Context)) {
|
|
59
|
+
throw new Error('Argument #2 must be a Webflo Context instance');
|
|
60
|
+
}
|
|
61
|
+
this.#cx = cx;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async initialize() {
|
|
65
|
+
const resolveContextObj = async (cx, force = false) => {
|
|
66
|
+
if (_isEmpty(cx.layout) || force) { cx.layout = await (new cx.config.deployment.Layout(cx)).read(); }
|
|
67
|
+
if (_isEmpty(cx.server) || force) { cx.server = await (new cx.config.runtime.Server(cx)).read(); }
|
|
68
|
+
if (_isEmpty(cx.env) || force) { cx.env = await (new cx.config.deployment.Env(cx)).read(); }
|
|
69
|
+
};
|
|
70
|
+
await resolveContextObj(this.#cx);
|
|
71
|
+
if (this.#cx.env.autoload !== false) {
|
|
72
|
+
Object.keys(this.#cx.env.entries).forEach(key => {
|
|
73
|
+
if (!(key in process.env)) {
|
|
74
|
+
process.env[key] = this.#cx.env.entries[key];
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ---------------
|
|
79
|
+
if (this.#cx.config.deployment.Proxy) {
|
|
80
|
+
const proxied = await (new this.#cx.config.deployment.Proxy(this.#cx)).read();
|
|
81
|
+
await Promise.all((proxied.entries || []).map(async vhost => {
|
|
82
|
+
let cx, hostnames = parseDomains(vhost.hostnames), port = vhost.port, proto = vhost.proto;
|
|
83
|
+
if (vhost.path) {
|
|
84
|
+
cx = this.#cx.constructor.create(this.#cx, Path.join(this.#cx.CWD, vhost.path));
|
|
85
|
+
await resolveContextObj(cx, true);
|
|
86
|
+
cx.dict.key = true;
|
|
87
|
+
// From the server that's most likely to be active
|
|
88
|
+
port || (port = cx.server.https.port || cx.server.port);
|
|
89
|
+
// The domain list that corresponds to the specified resolved port
|
|
90
|
+
hostnames.length || (hostnames = selectDomains([cx.server.https, cx.server], port));
|
|
91
|
+
// Or anyone available... hoping that the remote configs can eventually be in sync
|
|
92
|
+
hostnames.length || (hostnames = selectDomains([cx.server.https, cx.server]));
|
|
93
|
+
// The corresponding proto
|
|
94
|
+
proto || (proto = port === cx.server.https.port ? 'https' : 'http');
|
|
95
|
+
}
|
|
96
|
+
hostnames.length || (hostnames = ['*']);
|
|
97
|
+
this.#proxies.set(hostnames.sort().join('|'), { cx, hostnames, port, proto });
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
// ---------------
|
|
101
|
+
this.control();
|
|
102
|
+
if (this.#cx.logger) {
|
|
103
|
+
if (this.#servers.size) {
|
|
104
|
+
this.#cx.logger.info(`> Server running! (${this.#cx.app.title || ''})`);
|
|
105
|
+
for (let [proto, def] of this.#servers) {
|
|
106
|
+
this.#cx.logger.info(`> ${proto.toUpperCase()} / ${def.domains.concat('').join(`:${def.port} / `)}`);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this.#cx.logger.info(`> Server not running! No port specified.`);
|
|
110
|
+
}
|
|
111
|
+
if (this.#proxies.size) {
|
|
112
|
+
this.#cx.logger.info(`> Reverse proxy active.`);
|
|
113
|
+
for (let [id, def] of this.#proxies) {
|
|
114
|
+
this.#cx.logger.info(`> ${id} >>> ${def.port}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.#cx.logger.info(``);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
control() {
|
|
122
|
+
// ---------------
|
|
123
|
+
if (!this.#cx.flags['test-only'] && !this.#cx.flags['https-only'] && this.#cx.server.port) {
|
|
124
|
+
const httpServer = Http.createServer((request, response) => this.handleNodeHttpRequest('http', request, response));
|
|
125
|
+
httpServer.listen(this.#cx.server.port);
|
|
126
|
+
// -------
|
|
127
|
+
let domains = parseDomains(this.#cx.server.domains);
|
|
128
|
+
if (!domains.length) { domains = ['*']; }
|
|
129
|
+
this.#servers.set('http', {
|
|
130
|
+
instance: httpServer,
|
|
131
|
+
port: this.#cx.server.port,
|
|
132
|
+
domains,
|
|
133
|
+
});
|
|
134
|
+
// Handle WebSocket connections
|
|
135
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
136
|
+
this.handleNodeWsRequest(wss, 'ws', request, socket, head);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// ---------------
|
|
140
|
+
if (!this.#cx.flags['test-only'] && !this.#cx.flags['http-only'] && this.#cx.server.https.port) {
|
|
141
|
+
const httpsServer = Https.createServer((request, response) => this.handleNodeHttpRequest('https', request, response));
|
|
142
|
+
httpsServer.listen(this.#cx.server.https.port);
|
|
143
|
+
// -------
|
|
144
|
+
const addSSLContext = (serverConfig, domains) => {
|
|
145
|
+
if (!Fs.existsSync(serverConfig.https.keyfile)) return;
|
|
146
|
+
const cert = {
|
|
147
|
+
key: Fs.readFileSync(serverConfig.https.keyfile),
|
|
148
|
+
cert: Fs.readFileSync(serverConfig.https.certfile),
|
|
149
|
+
};
|
|
150
|
+
domains.forEach(domain => { httpsServer.addContext(domain, cert); });
|
|
151
|
+
}
|
|
152
|
+
// -------
|
|
153
|
+
let domains = parseDomains(this.#cx.server.https.domains);
|
|
154
|
+
if (!domains.length) { domains = ['*']; }
|
|
155
|
+
this.#servers.set('https', {
|
|
156
|
+
instance: httpsServer,
|
|
157
|
+
port: this.#cx.server.https.port,
|
|
158
|
+
domains,
|
|
159
|
+
});
|
|
160
|
+
// -------
|
|
161
|
+
addSSLContext(this.#cx.server, domains);
|
|
162
|
+
for (const [ /*id*/, vhost] of this.#proxies) {
|
|
163
|
+
vhost.cx && addSSLContext(vhost.cx.server, vhost.hostnames);
|
|
164
|
+
}
|
|
165
|
+
// Handle WebSocket connections
|
|
166
|
+
httpsServer.on('upgrade', (request, socket, head) => {
|
|
167
|
+
this.handleNodeWsRequest(wss, 'wss', request, socket, head);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// ---------------
|
|
171
|
+
const wss = new WebSocket.Server({ noServer: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#globalMessagingRegistry = new Map;
|
|
175
|
+
async handleNodeWsRequest(wss, proto, nodeRequest, socket, head) {
|
|
176
|
+
const [fullUrl, requestInit] = this.parseNodeRequest(proto, nodeRequest, false);
|
|
177
|
+
const scope = {};
|
|
178
|
+
scope.url = new URL(fullUrl);
|
|
179
|
+
// -----------------
|
|
180
|
+
// Level 1 validation
|
|
181
|
+
const hosts = [...this.#servers.values()].reduce((_hosts, server) => _hosts.concat(server.domains), []);
|
|
182
|
+
for (const [ /*id*/, vhost] of this.#proxies) {
|
|
183
|
+
if (vhost.hostnames.includes(scope.url.hostname) || (vhost.hostnames.includes('*') && !hosts.includes('*'))) {
|
|
184
|
+
scope.error = `Web sockets not supported over Webflo reverse proxies`;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// -----------------
|
|
189
|
+
// Level 2 validation
|
|
190
|
+
if (!scope.error) {
|
|
191
|
+
if (!hosts.includes(scope.url.hostname) && !hosts.includes('*')) {
|
|
192
|
+
scope.error = 'Unrecognized host';
|
|
193
|
+
} else if (scope.url.protocol === 'ws:' && this.#cx.server.https.force) {
|
|
194
|
+
scope.error = `Only secure connections allowed (wss:)`;
|
|
195
|
+
} else if (scope.url.hostname.startsWith('www.') && this.#cx.server.force_www === 'remove') {
|
|
196
|
+
scope.error = `Connections not allowed over the www subdomain`;
|
|
197
|
+
} else if (!scope.url.hostname.startsWith('www.') && this.#cx.server.force_www === 'add') {
|
|
198
|
+
scope.error = `Connections only allowed over the www subdomain`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// -----------------
|
|
202
|
+
// Level 3 validation
|
|
203
|
+
// and actual processing
|
|
204
|
+
scope.request = this.createRequest(scope.url.href, requestInit);
|
|
205
|
+
scope.session = this.constructor.SessionStorage.create(scope.request, { secret: this.#cx.env.entries.SESSION_KEY });
|
|
206
|
+
if (!scope.error) {
|
|
207
|
+
if (!(scope.clientMessagingRegistry = this.#globalMessagingRegistry.get(scope.session.sessionID))) {
|
|
208
|
+
scope.error = `Lost or invalid clientID`;
|
|
209
|
+
} else if (!(scope.clientMessaging = scope.clientMessagingRegistry.get(scope.url.pathname.split('/').pop()))) {
|
|
210
|
+
scope.error = `Lost or invalid portID`;
|
|
211
|
+
} else {
|
|
212
|
+
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
|
|
213
|
+
wss.emit('connection', ws, nodeRequest);
|
|
214
|
+
const wsw = new MessagingOverSocket(null, ws);
|
|
215
|
+
scope.clientMessaging.add(wsw);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// -----------------
|
|
220
|
+
// Errors?
|
|
221
|
+
if (scope.error) {
|
|
222
|
+
socket.write(
|
|
223
|
+
`HTTP/1.1 400 Bad Request\r\n` +
|
|
224
|
+
`Content-Type: text/plain\r\n` +
|
|
225
|
+
`Connection: close\r\n` +
|
|
226
|
+
`\r\n` +
|
|
227
|
+
`${scope.error}\r\n`
|
|
228
|
+
);
|
|
229
|
+
socket.destroy();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async handleNodeHttpRequest(proto, nodeRequest, nodeResponse) {
|
|
235
|
+
const [fullUrl, requestInit] = this.parseNodeRequest(proto, nodeRequest);
|
|
236
|
+
const scope = {};
|
|
237
|
+
scope.url = new URL(fullUrl);
|
|
238
|
+
// -----------------
|
|
239
|
+
// Level 1 handling
|
|
240
|
+
const hosts = [...this.#servers.values()].reduce((_hosts, server) => _hosts.concat(server.domains), []);
|
|
241
|
+
for (const [ /*id*/, vhost] of this.#proxies) {
|
|
242
|
+
if (vhost.hostnames.includes(scope.url.hostname) || (vhost.hostnames.includes('*') && !hosts.includes('*'))) {
|
|
243
|
+
scope.response = await this.proxyFetch(vhost, scope.url, scope.init);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// -----------------
|
|
248
|
+
// Level 2 handling
|
|
249
|
+
if (!scope.response) {
|
|
250
|
+
if (!hosts.includes(scope.url.hostname) && !hosts.includes('*')) {
|
|
251
|
+
scope.exit = { status: 500 };
|
|
252
|
+
scope.exitMessage = 'Unrecognized host';
|
|
253
|
+
} else if (scope.url.protocol === 'http:' && this.#cx.server.https.force) {
|
|
254
|
+
scope.exit = {
|
|
255
|
+
status: 302,
|
|
256
|
+
headers: { Location: (scope.url.protocol = 'https:', scope.url.href) }
|
|
257
|
+
};
|
|
258
|
+
} else if (scope.url.hostname.startsWith('www.') && this.#cx.server.force_www === 'remove') {
|
|
259
|
+
scope.exit = {
|
|
260
|
+
status: 302,
|
|
261
|
+
headers: { Location: (scope.url.hostname = scope.url.hostname.substr(4), scope.url.href) }
|
|
262
|
+
};
|
|
263
|
+
} else if (!scope.url.hostname.startsWith('www.') && this.#cx.server.force_www === 'add') {
|
|
264
|
+
scope.exit = {
|
|
265
|
+
status: 302,
|
|
266
|
+
headers: { Location: (scope.url.hostname = `www.${scope.url.hostname}`, scope.url.href) }
|
|
267
|
+
};
|
|
268
|
+
} else if (this.#cx.config.runtime.server.Redirects) {
|
|
269
|
+
scope.exit = ((await (new this.#cx.config.runtime.server.Redirects(this.#cx)).read()).entries || []).reduce((_rdr, entry) => {
|
|
270
|
+
return _rdr || ((_rdr = pattern(entry.from, scope.url.origin).exec(scope.url.href)) && {
|
|
271
|
+
status: entry.code || 302,
|
|
272
|
+
headers: { Location: _rdr.render(entry.to) }
|
|
273
|
+
});
|
|
274
|
+
}, null);
|
|
275
|
+
}
|
|
276
|
+
if (scope.exit) {
|
|
277
|
+
scope.response = new Response(scope.exitMessage, scope.exit);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// -----------------
|
|
281
|
+
// Level 3 handling
|
|
282
|
+
if (!scope.response) {
|
|
283
|
+
scope.response = await this.navigate(fullUrl, requestInit, {
|
|
284
|
+
request: nodeRequest,
|
|
285
|
+
response: nodeResponse
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// -----------------
|
|
289
|
+
// For when response was sent during this.navigate()
|
|
290
|
+
if (!nodeResponse.headersSent) {
|
|
291
|
+
for (const [name, value] of scope.response.headers) {
|
|
292
|
+
const existing = nodeResponse.getHeader(name);
|
|
293
|
+
if (existing) nodeResponse.setHeader(name, [].concat(existing).concat(value));
|
|
294
|
+
else nodeResponse.setHeader(name, value);
|
|
295
|
+
}
|
|
296
|
+
// --------
|
|
297
|
+
nodeResponse.statusCode = scope.response.status;
|
|
298
|
+
nodeResponse.statusMessage = scope.response.statusText;
|
|
299
|
+
if (scope.response.headers.has('Location')) {
|
|
300
|
+
nodeResponse.end();
|
|
301
|
+
} else if (scope.response.body instanceof _ReadableStream) {
|
|
302
|
+
scope.response.body.pipe(nodeResponse);
|
|
303
|
+
} else if (scope.response.body instanceof ReadableStream) {
|
|
304
|
+
_ReadableStream.from(scope.response.body).pipe(nodeResponse);
|
|
305
|
+
} else {
|
|
306
|
+
let body = scope.response.body;
|
|
307
|
+
if (scope.response.headers.get('Content-Type') === 'application/json') {
|
|
308
|
+
body += '';
|
|
309
|
+
}
|
|
310
|
+
nodeResponse.end(body);
|
|
311
|
+
}
|
|
312
|
+
// -----------------
|
|
313
|
+
// Logging
|
|
314
|
+
if (this.#cx.logger) {
|
|
315
|
+
const log = this.generateLog({ url: fullUrl, method: nodeRequest.method }, scope.response);
|
|
316
|
+
this.#cx.logger.log(log);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
parseNodeRequest(proto, nodeRequest, withBody = true) {
|
|
322
|
+
// Detected when using manual proxy setting in a browser
|
|
323
|
+
if (nodeRequest.url.startsWith(`${proto}://${nodeRequest.headers.host}`)) {
|
|
324
|
+
nodeRequest.url = nodeRequest.url.split(nodeRequest.headers.host)[1];
|
|
325
|
+
}
|
|
326
|
+
const fullUrl = proto + '://' + nodeRequest.headers.host + nodeRequest.url;
|
|
327
|
+
const requestInit = { method: nodeRequest.method, headers: nodeRequest.headers };
|
|
328
|
+
if (withBody && !['GET', 'HEAD'].includes(nodeRequest.method)) {
|
|
329
|
+
nodeRequest[Symbol.toStringTag] = 'ReadableStream';
|
|
330
|
+
requestInit.body = nodeRequest;
|
|
331
|
+
requestInit.duplex = 'half'; // See https://github.com/nodejs/node/issues/46221
|
|
332
|
+
}
|
|
333
|
+
return [fullUrl, requestInit];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
writeAutoHeaders(headers, autoHeaders) {
|
|
337
|
+
autoHeaders.forEach(header => {
|
|
338
|
+
var headerName = header.name.toLowerCase(),
|
|
339
|
+
headerValue = header.value,
|
|
340
|
+
isAppend = headerName.startsWith('+') ? (headerName = headerName.substr(1), true) : false,
|
|
341
|
+
isPrepend = headerName.endsWith('+') ? (headerName = headerName.substr(0, headerName.length - 1), true) : false;
|
|
342
|
+
if (isAppend || isPrepend) {
|
|
343
|
+
headerValue = [headers.get(headerName) || '', headerValue].filter(v => v);
|
|
344
|
+
headerValue = isPrepend ? headerValue.reverse().join(',') : headerValue.join(',');
|
|
345
|
+
}
|
|
346
|
+
headers.set(headerName, headerValue);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
writeRedirectHeaders(httpEvent, response) {
|
|
351
|
+
const xRedirectPolicy = httpEvent.request.headers.get('X-Redirect-Policy');
|
|
352
|
+
const xRedirectCode = httpEvent.request.headers.get('X-Redirect-Code') || 300;
|
|
353
|
+
const destinationUrl = new URL(response.headers.get('Location'), httpEvent.url.origin);
|
|
354
|
+
const isSameOriginRedirect = destinationUrl.origin === httpEvent.url.origin;
|
|
355
|
+
let isSameSpaRedirect, sparootsFile = Path.join(this.#cx.CWD, this.#cx.layout.PUBLIC_DIR, 'sparoots.json');
|
|
356
|
+
if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && Fs.existsSync(sparootsFile)) {
|
|
357
|
+
// Longest-first sorting
|
|
358
|
+
const sparoots = _arrFrom(JSON.parse(Fs.readFileSync(sparootsFile))).sort((a, b) => a.length > b.length ? -1 : 1);
|
|
359
|
+
const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
|
|
360
|
+
isSameSpaRedirect = matchRoot(destinationUrl.pathname) === matchRoot(httpEvent.url.pathname);
|
|
361
|
+
}
|
|
362
|
+
if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
|
|
363
|
+
response.headers.set('X-Redirect-Code', response.status);
|
|
364
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
365
|
+
response.headers.set('Cache-Control', 'no-store');
|
|
366
|
+
response.meta.status = xRedirectCode;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
createRequest(href, init = {}, autoHeaders = []) {
|
|
371
|
+
const request = new Request(href, init);
|
|
372
|
+
this.writeAutoHeaders(request.headers, autoHeaders);
|
|
373
|
+
return request;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async proxyFetch(vhost, url, init) {
|
|
377
|
+
const scope = {};
|
|
378
|
+
scope.url = new URL(url);
|
|
379
|
+
scope.url.port = vhost.port;
|
|
380
|
+
if (vhost.proto) {
|
|
381
|
+
scope.url.protocol = vhost.proto;
|
|
382
|
+
}
|
|
383
|
+
// ---------
|
|
384
|
+
if (init instanceof Request) {
|
|
385
|
+
scope.init = init.clone();
|
|
386
|
+
scope.init.headers.set('Host', scope.url.host);
|
|
387
|
+
} else {
|
|
388
|
+
scope.init = { ...init, decompress: false/* honoured in xfetch() */ };
|
|
389
|
+
if (!scope.init.headers) scope.init.headers = {};
|
|
390
|
+
scope.init.headers.host = scope.url.host;
|
|
391
|
+
delete scope.init.headers.connection;
|
|
392
|
+
}
|
|
393
|
+
// ---------
|
|
394
|
+
try {
|
|
395
|
+
scope.response = await this.remoteFetch(scope.url, scope.init);
|
|
396
|
+
} catch (e) {
|
|
397
|
+
scope.response = new Response(`Reverse Proxy Error: ${e.message}`, { status: 500 });
|
|
398
|
+
console.error(e);
|
|
399
|
+
}
|
|
400
|
+
return scope.response;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async remoteFetch(request, ...args) {
|
|
404
|
+
let href = request;
|
|
405
|
+
if (request instanceof Request) {
|
|
406
|
+
href = request.url;
|
|
407
|
+
} else if (request instanceof URL) {
|
|
408
|
+
href = request.href;
|
|
409
|
+
}
|
|
410
|
+
const _response = xfetch(request, ...args);
|
|
411
|
+
// Save a reference to this
|
|
412
|
+
return _response.then(async response => {
|
|
413
|
+
// Stop loading status
|
|
414
|
+
return response;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async localFetch(httpEvent) {
|
|
419
|
+
const scope = {};
|
|
420
|
+
scope.filename = Path.join(this.#cx.CWD, this.#cx.layout.PUBLIC_DIR, decodeURIComponent(httpEvent.url.pathname));
|
|
421
|
+
scope.ext = Path.parse(httpEvent.url.pathname).ext;
|
|
422
|
+
// if is a directory search for index file matching the extention
|
|
423
|
+
if (!scope.ext && Fs.existsSync(scope.filename) && Fs.lstatSync(scope.filename).isDirectory()) {
|
|
424
|
+
scope.ext = '.html';
|
|
425
|
+
scope.index = `index${scope.ext}`;
|
|
426
|
+
scope.filename = Path.join(scope.filename, scope.index);
|
|
427
|
+
}
|
|
428
|
+
scope.acceptEncs = [];
|
|
429
|
+
scope.supportedEncs = { gzip: '.gz', br: '.br' };
|
|
430
|
+
// based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
|
431
|
+
// and process encoding
|
|
432
|
+
if ((scope.acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map((e) => e.trim())).length
|
|
433
|
+
&& (scope.enc = scope.acceptEncs.reduce((prev, _enc) => prev || (Fs.existsSync(scope.filename + scope.supportedEncs[_enc]) && _enc), null))) {
|
|
434
|
+
scope.filename = scope.filename + scope.supportedEncs[scope.enc];
|
|
435
|
+
} else {
|
|
436
|
+
if (!Fs.existsSync(scope.filename)) return;
|
|
437
|
+
if (Object.values(scope.supportedEncs).includes(scope.ext)) {
|
|
438
|
+
scope.enc = Object.keys(scope.supportedEncs).reduce((prev, _enc) => prev || (scope.supportedEncs[_enc] === ext && _enc), null);
|
|
439
|
+
scope.ext = Path.parse(scope.filename.substring(0, scope.filename.length - scope.ext.length)).ext;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// read file from file system
|
|
443
|
+
return new Promise(resolve => {
|
|
444
|
+
Fs.readFile(scope.filename, function (err, data) {
|
|
445
|
+
if (err) {
|
|
446
|
+
scope.response = new Response(null, { status: 500, statusText: `Error reading static file: ${scope.filename}` });
|
|
447
|
+
} else {
|
|
448
|
+
// if the file is found, set Content-type and send data
|
|
449
|
+
const mime = Mime.lookup(scope.ext);
|
|
450
|
+
scope.response = new Response(data, {
|
|
451
|
+
headers: {
|
|
452
|
+
'Content-Type': mime === 'application/javascript' ? 'text/javascript' : mime,
|
|
453
|
+
'Content-Length': Buffer.byteLength(data)
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
if (scope.enc) {
|
|
457
|
+
scope.response.headers.set('Content-Encoding', scope.enc);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
scope.response.meta.filename = scope.filename;
|
|
461
|
+
scope.response.meta.static = true;
|
|
462
|
+
scope.response.meta.index = scope.index;
|
|
463
|
+
resolve(scope.response);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async navigate(url, init = {}, detail = {}) {
|
|
469
|
+
const scope = { url, init, detail };
|
|
470
|
+
if (typeof scope.url === 'string') {
|
|
471
|
+
scope.url = new URL(scope.url, 'http://localhost');
|
|
472
|
+
}
|
|
473
|
+
scope.response = await new Promise(async (resolveResponse) => {
|
|
474
|
+
scope.handleRespondWith = async (response) => {
|
|
475
|
+
if (scope.finalResponseSeen) {
|
|
476
|
+
throw new Error('Final response already sent');
|
|
477
|
+
}
|
|
478
|
+
if (scope.initialResponseSeen) {
|
|
479
|
+
return await this.execPush(scope.clientMessaging, response);
|
|
480
|
+
}
|
|
481
|
+
resolveResponse(response);
|
|
482
|
+
};
|
|
483
|
+
// ---------------
|
|
484
|
+
// Request processing
|
|
485
|
+
scope.autoHeaders = this.#cx.config.runtime.server.Headers
|
|
486
|
+
? ((await (new this.#cx.config.runtime.server.Headers(this.#cx)).read()).entries || []).filter(entry => pattern(entry.url, url.origin).exec(url.href))
|
|
487
|
+
: [];
|
|
488
|
+
scope.request = this.createRequest(scope.url.href, scope.init, scope.autoHeaders.filter((header) => header.type === 'request'));
|
|
489
|
+
scope.cookies = this.constructor.CookieStorage.create(scope.request);
|
|
490
|
+
scope.session = this.constructor.SessionStorage.create(scope.request, { secret: this.#cx.env.entries.SESSION_KEY });
|
|
491
|
+
const sessionID = scope.session.sessionID;
|
|
492
|
+
if (!this.#globalMessagingRegistry.has(sessionID)) {
|
|
493
|
+
this.#globalMessagingRegistry.set(sessionID, new ClientMessagingRegistry(this, sessionID));
|
|
494
|
+
}
|
|
495
|
+
scope.clientMessagingRegistry = this.#globalMessagingRegistry.get(sessionID);
|
|
496
|
+
scope.clientMessaging = scope.clientMessagingRegistry.createPort();
|
|
497
|
+
scope.user = this.constructor.HttpUser.create(
|
|
498
|
+
scope.request,
|
|
499
|
+
scope.session,
|
|
500
|
+
scope.clientMessaging
|
|
501
|
+
);
|
|
502
|
+
scope.httpEvent = this.constructor.HttpEvent.create(scope.handleRespondWith, {
|
|
503
|
+
request: scope.request,
|
|
504
|
+
detail: scope.detail,
|
|
505
|
+
cookies: scope.cookies,
|
|
506
|
+
session: scope.session,
|
|
507
|
+
user: scope.user,
|
|
508
|
+
client: scope.clientMessaging
|
|
509
|
+
});
|
|
510
|
+
// Restore session before dispatching
|
|
511
|
+
if (scope.request.method === 'GET'
|
|
512
|
+
&& (scope.redirectMessageID = scope.httpEvent.url.query['redirect-message'])
|
|
513
|
+
&& (scope.redirectMessage = scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
|
|
514
|
+
scope.session.delete(`redirect-message:${scope.redirectMessageID}`);
|
|
515
|
+
}
|
|
516
|
+
// Dispatch for response
|
|
517
|
+
scope.$response = await this.dispatch(scope.httpEvent, {}, async (event) => {
|
|
518
|
+
return await this.localFetch(event);
|
|
519
|
+
});
|
|
520
|
+
// Final reponse!!!
|
|
521
|
+
scope.finalResponseSeen = true;
|
|
522
|
+
if (scope.initialResponseSeen) {
|
|
523
|
+
// Send via background port
|
|
524
|
+
if (typeof scope.$response !== 'undefined') {
|
|
525
|
+
await this.execPush(scope.clientMessaging, scope.$response);
|
|
526
|
+
}
|
|
527
|
+
scope.clientMessaging.close();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Send normally
|
|
531
|
+
resolveResponse(scope.$response);
|
|
532
|
+
});
|
|
533
|
+
scope.initialResponseSeen = true;
|
|
534
|
+
scope.hasBackgroundActivity = !scope.finalResponseSeen || (scope.redirectMessage && !(scope.response instanceof Response && scope.response.headers.get('Location')));
|
|
535
|
+
scope.response = await this.normalizeResponse(scope.httpEvent, scope.response, scope.hasBackgroundActivity);
|
|
536
|
+
if (scope.hasBackgroundActivity) {
|
|
537
|
+
scope.response.headers.set('X-Background-Messaging', `ws:${scope.clientMessaging.portID}`);
|
|
538
|
+
}
|
|
539
|
+
// Reponse handlers
|
|
540
|
+
if (scope.response instanceof Response && scope.response.headers.get('Location')) {
|
|
541
|
+
this.writeRedirectHeaders(scope.httpEvent, scope.response);
|
|
542
|
+
if (scope.redirectMessage) {
|
|
543
|
+
scope.session.set(`redirect-message:${scope.redirectMessageID}`, scope.redirectMessage);
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
this.writeAutoHeaders(scope.response.headers, scope.autoHeaders.filter((header) => header.type === 'response'));
|
|
547
|
+
if (scope.httpEvent.request.method !== 'GET' && !scope.response.headers.get('Cache-Control')) {
|
|
548
|
+
scope.response.headers.set('Cache-Control', 'no-store');
|
|
549
|
+
}
|
|
550
|
+
scope.response.headers.set('Accept-Ranges', 'bytes');
|
|
551
|
+
scope.response = await this.satisfyRequestFormat(scope.httpEvent, scope.response);
|
|
552
|
+
if (scope.redirectMessage) {
|
|
553
|
+
this.execPush(scope.clientMessaging, scope.redirectMessage);
|
|
554
|
+
if (scope.finalResponseSeen) {
|
|
555
|
+
scope.clientMessaging.on('connected', () => {
|
|
556
|
+
setTimeout(() => {
|
|
557
|
+
scope.clientMessaging.close();
|
|
558
|
+
}, 100);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
} else if (scope.finalResponseSeen) {
|
|
562
|
+
scope.clientMessaging.close();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return scope.response;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async satisfyRequestFormat(httpEvent, response) {
|
|
569
|
+
// Satisfy "Accept" header
|
|
570
|
+
const is404 = response.status === 404;
|
|
571
|
+
if (is404) return response;
|
|
572
|
+
const acceptedOrUnchanged = [202/*Accepted*/, 304/*Not Modified*/].includes(response.status);
|
|
573
|
+
const t = response.status === 404;
|
|
574
|
+
if (httpEvent.request.headers.get('Accept')) {
|
|
575
|
+
const requestAccept = httpEvent.request.headers.get('Accept', true);
|
|
576
|
+
if (requestAccept.match('text/html') && !response.meta.static) {
|
|
577
|
+
const data = acceptedOrUnchanged ? {} : await response.parse();
|
|
578
|
+
response = await this.render(httpEvent, data, response);
|
|
579
|
+
} else if (acceptedOrUnchanged) {
|
|
580
|
+
return response;
|
|
581
|
+
} else if (response.headers.get('Content-Type') && !requestAccept.match(response.headers.get('Content-Type'))) {
|
|
582
|
+
return new Response(response.body, { status: 406, headers: response.headers });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Satisfy "Range" header
|
|
586
|
+
if (httpEvent.request.headers.get('Range') && !response.headers.get('Content-Range')
|
|
587
|
+
&& (response.body instanceof ReadableStream || ArrayBuffer.isView(response.body))) {
|
|
588
|
+
if (t) {
|
|
589
|
+
console.log(httpEvent.request.url, response.body);
|
|
590
|
+
}
|
|
591
|
+
const rangeRequest = httpEvent.request.headers.get('Range', true);
|
|
592
|
+
const body = _ReadableStream.from(response.body);
|
|
593
|
+
// ...in partials
|
|
594
|
+
const totalLength = parseInt(response.headers.get('Content-Length') || 0);
|
|
595
|
+
const ranges = await rangeRequest.reduce(async (_ranges, range) => {
|
|
596
|
+
_ranges = await _ranges;
|
|
597
|
+
if (range[0] < 0 || (totalLength && range[0] > totalLength)
|
|
598
|
+
|| (range[1] > -1 && (range[1] <= range[0] || (totalLength && range[1] >= totalLength)))) {
|
|
599
|
+
// The range is beyond upper/lower limits
|
|
600
|
+
_ranges.error = true;
|
|
601
|
+
}
|
|
602
|
+
if (!totalLength && range[0] === undefined) {
|
|
603
|
+
// totalLength is unknown and we cant read the trailing size specified in range[1]
|
|
604
|
+
_ranges.error = true;
|
|
605
|
+
}
|
|
606
|
+
if (_ranges.error) return _ranges;
|
|
607
|
+
if (totalLength) { range.clamp(totalLength); }
|
|
608
|
+
const partLength = range[1] - range[0] + 1;
|
|
609
|
+
_ranges.parts.push({
|
|
610
|
+
body: body.pipe(_streamSlice(range[0], range[1] + 1)),
|
|
611
|
+
range: range = `bytes ${range[0]}-${range[1]}/${totalLength || '*'}`,
|
|
612
|
+
length: partLength,
|
|
613
|
+
});
|
|
614
|
+
_ranges.totalLength += partLength;
|
|
615
|
+
return _ranges;
|
|
616
|
+
}, { parts: [], totalLength: 0 });
|
|
617
|
+
if (ranges.error) {
|
|
618
|
+
response.meta.status = 416;
|
|
619
|
+
response.headers.set('Content-Range', `bytes */${totalLength || '*'}`);
|
|
620
|
+
response.headers.set('Content-Length', 0);
|
|
621
|
+
} else {
|
|
622
|
+
// TODO: of ranges.parts is more than one, return multipart/byteranges
|
|
623
|
+
response = new Response(ranges.parts[0].body, {
|
|
624
|
+
status: 206,
|
|
625
|
+
statusText: response.statusText,
|
|
626
|
+
headers: response.headers,
|
|
627
|
+
});
|
|
628
|
+
response.headers.set('Content-Range', ranges.parts[0].range);
|
|
629
|
+
response.headers.set('Content-Length', ranges.totalLength);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return response;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#renderFileCache = new Map;
|
|
636
|
+
async render(httpEvent, data, response) {
|
|
637
|
+
const scope = {};
|
|
638
|
+
scope.router = new this.constructor.Router(this.#cx, httpEvent.url.pathname);
|
|
639
|
+
scope.rendering = await scope.router.route('render', httpEvent, data, async (httpEvent, data) => {
|
|
640
|
+
let renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
|
|
641
|
+
while ((renderFile = Path.join(this.#cx.CWD, this.#cx.layout.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
|
|
642
|
+
&& (this.#renderFileCache.get(renderFile) === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
|
|
643
|
+
this.#renderFileCache.set(renderFile, false);
|
|
644
|
+
pathnameSplit.pop();
|
|
645
|
+
}
|
|
646
|
+
const dirPublic = Url.pathToFileURL(Path.resolve(Path.join(this.#cx.CWD, this.#cx.layout.PUBLIC_DIR)));
|
|
647
|
+
const instanceParams = QueryString.stringify({
|
|
648
|
+
file: renderFile,
|
|
649
|
+
url: dirPublic.href,// httpEvent.url.href,
|
|
650
|
+
root: this.#cx.CWD,
|
|
651
|
+
});
|
|
652
|
+
const { window, document } = await import('@webqit/oohtml-ssr/src/instance.js?' + instanceParams);
|
|
653
|
+
await new Promise(res => {
|
|
654
|
+
if (document.readyState === 'complete') return res();
|
|
655
|
+
document.addEventListener('load', res);
|
|
656
|
+
});
|
|
657
|
+
if (window.webqit?.oohtml?.configs) {
|
|
658
|
+
const {
|
|
659
|
+
BINDINGS_API: { api: bindingsConfig } = {},
|
|
660
|
+
HTML_IMPORTS: { attr: modulesContextAttrs } = {},
|
|
661
|
+
} = window.webqit.oohtml.configs;
|
|
662
|
+
if (bindingsConfig) {
|
|
663
|
+
document[bindingsConfig.bind]({
|
|
664
|
+
state: {},
|
|
665
|
+
...(!_isObject(data) ? {} : data),
|
|
666
|
+
env: 'server',
|
|
667
|
+
navigator: null,
|
|
668
|
+
location: this.location,
|
|
669
|
+
network: null,
|
|
670
|
+
transition: null,
|
|
671
|
+
background: null
|
|
672
|
+
}, { diff: true });
|
|
673
|
+
let overridenKeys;
|
|
674
|
+
if (_isObject(data) && (overridenKeys = ['env', 'navigator', 'location', 'network', 'transition', 'background'].filter((k) => k in data)).length) {
|
|
675
|
+
console.error(`The following data properties were overridden: ${overridenKeys.join(', ')}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (modulesContextAttrs) {
|
|
679
|
+
const newRoute = '/' + `routes/${httpEvent.url.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
|
|
680
|
+
document.body.setAttribute(modulesContextAttrs.importscontext, newRoute);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Append background-activity meta
|
|
684
|
+
let backgroundActivityMeta = document.querySelector('meta[name="X-Background-Messaging"]');
|
|
685
|
+
if (response.headers.has('X-Background-Messaging')) {
|
|
686
|
+
if (!backgroundActivityMeta) {
|
|
687
|
+
backgroundActivityMeta = document.createElement('meta');
|
|
688
|
+
backgroundActivityMeta.setAttribute('name', 'X-Background-Messaging');
|
|
689
|
+
document.head.prepend(backgroundActivityMeta);
|
|
690
|
+
}
|
|
691
|
+
backgroundActivityMeta.setAttribute('content', response.headers.get('X-Background-Messaging'));
|
|
692
|
+
} else if (backgroundActivityMeta) {
|
|
693
|
+
backgroundActivityMeta.remove();
|
|
694
|
+
}
|
|
695
|
+
// Append hydration data
|
|
696
|
+
const hydrationData = document.querySelector('script[rel="hydration"][type="application/json"]') || document.createElement('script');
|
|
697
|
+
hydrationData.setAttribute('type', 'application/json');
|
|
698
|
+
hydrationData.setAttribute('rel', 'hydration');
|
|
699
|
+
hydrationData.textContent = JSON.stringify(data);
|
|
700
|
+
document.body.append(hydrationData);
|
|
701
|
+
// Await rendering engine
|
|
702
|
+
if (window.webqit.$qCompilerImport) {
|
|
703
|
+
await new Promise(res => {
|
|
704
|
+
window.webqit.$qCompilerImport.then(res);
|
|
705
|
+
setTimeout(res, 300);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
await new Promise(res => setTimeout(res, 50));
|
|
709
|
+
return window;
|
|
710
|
+
});
|
|
711
|
+
// Validate rendering
|
|
712
|
+
if (typeof scope.rendering !== 'string' && !(typeof scope.rendering?.toString === 'function')) {
|
|
713
|
+
throw new Error('render() must return a string response or an object that implements toString()..');
|
|
714
|
+
}
|
|
715
|
+
// Convert back to response
|
|
716
|
+
scope.response = new Response(scope.rendering, {
|
|
717
|
+
headers: response.headers,
|
|
718
|
+
status: response.status,
|
|
719
|
+
});
|
|
720
|
+
scope.response.headers.set('Content-Type', 'text/html');
|
|
721
|
+
scope.response.headers.set('Content-Length', (new Blob([scope.rendering])).size);
|
|
722
|
+
return scope.response;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
generateLog(request, response, isproxy = false) {
|
|
726
|
+
let log = [];
|
|
727
|
+
// ---------------
|
|
728
|
+
const style = this.#cx.logger.style || { keyword: str => str, comment: str => str, url: str => str, val: str => str, err: str => str, };
|
|
729
|
+
const errorCode = [404, 500].includes(response.status) ? response.status : 0;
|
|
730
|
+
const xRedirectCode = response.headers.get('X-Redirect-Code');
|
|
731
|
+
const isRedirect = xRedirectCode || (response.status + '').startsWith('3');
|
|
732
|
+
const statusCode = xRedirectCode && `${xRedirectCode} (${response.status})` || response.status;
|
|
733
|
+
// ---------------
|
|
734
|
+
log.push(`[${style.comment((new Date).toUTCString())}]`);
|
|
735
|
+
log.push(style.keyword(request.method));
|
|
736
|
+
if (isproxy) log.push(style.keyword('>>'));
|
|
737
|
+
log.push(style.url(request.url));
|
|
738
|
+
if (response.meta.hint) log.push(`(${style.comment(response.meta.hint)})`);
|
|
739
|
+
const contentInfo = [response.headers.get('Content-Type'), response.headers.get('Content-Length')].filter(x => x);
|
|
740
|
+
if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
|
|
741
|
+
if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
|
|
742
|
+
if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
|
|
743
|
+
else log.push(style.val(`${statusCode} ${response.statusText}`));
|
|
744
|
+
if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
|
|
745
|
+
|
|
746
|
+
return log.join(' ');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const _streamRead = stream => new Promise(res => {
|
|
752
|
+
let data = '';
|
|
753
|
+
stream.on('data', chunk => data += chunk);
|
|
754
|
+
stream.on('end', () => res(data));
|
|
755
|
+
});
|