@webqit/webflo 0.20.26 → 0.20.28
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 +8 -5
- package/src/build-pi/index.js +6 -4
- package/src/init-pi/index.js +0 -1
- package/src/runtime-pi/{WebfloRuntime.js → AppRuntime.js} +57 -113
- package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
- package/src/runtime-pi/webflo-client/WebfloClient.js +163 -103
- package/src/runtime-pi/webflo-client/{WebfloRootClient1.js → WebfloRootClientA.js} +39 -56
- package/src/runtime-pi/webflo-client/{WebfloRootClient2.js → WebfloRootClientB.js} +3 -3
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +28 -15
- package/src/runtime-pi/webflo-client/index.js +3 -3
- package/src/runtime-pi/webflo-messaging/ClientPortMixin.js +13 -0
- package/src/runtime-pi/{webflo-server/messaging/ClientRequestRealtime.js → webflo-messaging/ClientRequestPort001.js} +13 -9
- package/src/runtime-pi/webflo-messaging/ClientRequestPort010.js +4 -0
- package/src/runtime-pi/webflo-messaging/ClientRequestPort100.js +17 -0
- package/src/runtime-pi/webflo-messaging/WebfloTenancy001.js +27 -0
- package/src/runtime-pi/webflo-messaging/WebfloTenant001.js +27 -0
- package/src/runtime-pi/webflo-routing/HttpCookies101.js +53 -0
- package/src/runtime-pi/webflo-routing/HttpCookies110.js +3 -0
- package/src/runtime-pi/webflo-routing/{HttpEvent.js → HttpEvent111.js} +95 -73
- package/src/runtime-pi/webflo-routing/HttpKeyvalInterface.js +120 -0
- package/src/runtime-pi/webflo-routing/HttpSession001.js +24 -0
- package/src/runtime-pi/webflo-routing/HttpSession110.js +3 -0
- package/src/runtime-pi/webflo-routing/{HttpThread.js → HttpThread111.js} +54 -13
- package/src/runtime-pi/webflo-routing/{HttpUser.js → HttpUser111.js} +10 -23
- package/src/runtime-pi/webflo-routing/KeyvalsFactory001.js +53 -0
- package/src/runtime-pi/webflo-routing/KeyvalsFactory110.js +48 -0
- package/src/runtime-pi/webflo-routing/KeyvalsFactoryInterface.js +56 -0
- package/src/runtime-pi/webflo-routing/{WebfloRouter.js → WebfloRouter111.js} +5 -6
- package/src/runtime-pi/webflo-server/WebfloServer.js +262 -269
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +97 -44
- package/src/util.js +3 -2
- package/src/runtime-pi/apis.js +0 -9
- package/src/runtime-pi/webflo-client/ClientSideCookies.js +0 -18
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +0 -476
- package/src/runtime-pi/webflo-fetch/index.js +0 -419
- package/src/runtime-pi/webflo-fetch/util.js +0 -28
- package/src/runtime-pi/webflo-messaging/WQBroadcastChannel.js +0 -10
- package/src/runtime-pi/webflo-messaging/WQMessageChannel.js +0 -26
- package/src/runtime-pi/webflo-messaging/WQMessageEvent.js +0 -87
- package/src/runtime-pi/webflo-messaging/WQMessagePort.js +0 -38
- package/src/runtime-pi/webflo-messaging/WQRelayPort.js +0 -47
- package/src/runtime-pi/webflo-messaging/WQSockPort.js +0 -111
- package/src/runtime-pi/webflo-messaging/WQStarPort.js +0 -112
- package/src/runtime-pi/webflo-messaging/wq-message-port.js +0 -413
- package/src/runtime-pi/webflo-routing/HttpCookies.js +0 -43
- package/src/runtime-pi/webflo-routing/HttpSession.js +0 -11
- package/src/runtime-pi/webflo-routing/HttpState.js +0 -182
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +0 -22
- package/src/runtime-pi/webflo-server/ServerSideSession.js +0 -40
- package/src/runtime-pi/webflo-server/messaging/Client.js +0 -27
- package/src/runtime-pi/webflo-server/messaging/Clients.js +0 -25
- package/src/runtime-pi/webflo-url/Url.js +0 -156
- package/src/runtime-pi/webflo-url/index.js +0 -1
- package/src/runtime-pi/webflo-url/urlpattern.js +0 -38
- package/src/runtime-pi/webflo-url/util.js +0 -109
- package/src/runtime-pi/webflo-url/xURL.js +0 -94
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +0 -21
|
@@ -3,42 +3,43 @@ import Url from 'url';
|
|
|
3
3
|
import Path from 'path';
|
|
4
4
|
import Http from 'http';
|
|
5
5
|
import Https from 'https';
|
|
6
|
-
import { WebSocketServer } from 'ws';
|
|
7
6
|
import Mime from 'mime-types';
|
|
8
7
|
import crypto from 'crypto';
|
|
9
|
-
import 'dotenv/config';
|
|
10
8
|
import $glob from 'fast-glob';
|
|
11
9
|
import EsBuild from 'esbuild';
|
|
12
10
|
import { Readable } from 'stream';
|
|
11
|
+
import { WebSocketServer } from 'ws';
|
|
13
12
|
import { spawn } from 'child_process';
|
|
14
|
-
import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
|
|
15
|
-
import { _isEmpty, _isObject } from '@webqit/util/js/index.js';
|
|
16
|
-
import { _each } from '@webqit/util/obj/index.js';
|
|
17
|
-
import { WebfloHMR, openBrowser } from './webflo-devmode.js';
|
|
18
|
-
import { Clients } from './messaging/Clients.js';
|
|
19
|
-
import { WebfloRuntime } from '../WebfloRuntime.js';
|
|
20
|
-
import { WQSockPort } from '../webflo-messaging/WQSockPort.js';
|
|
21
|
-
import { ServerSideCookies } from './ServerSideCookies.js';
|
|
22
|
-
import { ServerSideSession } from './ServerSideSession.js';
|
|
23
|
-
import { response as responseShim, headers as headersShim } from '../webflo-fetch/index.js';
|
|
24
|
-
import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
|
|
25
13
|
import { createWindow } from '@webqit/oohtml-ssr';
|
|
26
|
-
import {
|
|
27
|
-
import '../webflo-
|
|
28
|
-
import '../webflo-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
import { RequestPlus } from '@webqit/fetch-plus';
|
|
15
|
+
import { HttpThread111 } from '../webflo-routing/HttpThread111.js';
|
|
16
|
+
import { HttpCookies101 } from '../webflo-routing/HttpCookies101.js';
|
|
17
|
+
import { HttpSession001 } from '../webflo-routing/HttpSession001.js';
|
|
18
|
+
import { HttpUser111 } from '../webflo-routing/HttpUser111.js';
|
|
19
|
+
import { HttpEvent111 } from '../webflo-routing/HttpEvent111.js';
|
|
20
|
+
import { WebfloRouter111 } from '../webflo-routing/WebfloRouter111.js';
|
|
21
|
+
import { KeyvalsFactory001 } from '../webflo-routing/KeyvalsFactory001.js';
|
|
22
|
+
import { WebfloTenancy001 } from '../webflo-messaging/WebfloTenancy001.js';
|
|
23
|
+
import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
|
|
24
|
+
import { WebfloHMR, openBrowser } from './webflo-devmode.js';
|
|
25
|
+
import { InMemoryKV } from '@webqit/keyval/inmemory';
|
|
26
|
+
import { URLPatternPlus } from '@webqit/url-plus';
|
|
27
|
+
import { WebSocketPort } from '@webqit/port-plus';
|
|
28
|
+
import { AppRuntime } from '../AppRuntime.js';
|
|
29
|
+
import { _meta } from '../../util.js';
|
|
30
|
+
import 'dotenv/config';
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
export class WebfloServer extends AppRuntime {
|
|
35
33
|
|
|
36
34
|
static create(bootstrap) {
|
|
37
35
|
return new this(bootstrap);
|
|
38
36
|
}
|
|
39
37
|
|
|
38
|
+
#keyvals;
|
|
39
|
+
get keyvals() { return this.#keyvals; }
|
|
40
|
+
|
|
40
41
|
#servers = new Map;
|
|
41
|
-
#
|
|
42
|
+
#tenancy = new WebfloTenancy001({ handshake: 1, postAwaitsOpen: true, autoClose: false });
|
|
42
43
|
#hmr;
|
|
43
44
|
|
|
44
45
|
#renderFileCache = new Map;
|
|
@@ -61,6 +62,14 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
61
62
|
await this.buildRoutes({ server: true });
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// ----------
|
|
66
|
+
// The keyvals API
|
|
67
|
+
this.#keyvals = new KeyvalsFactory001({
|
|
68
|
+
localDir: this.env('KEYVALS_DIR'),
|
|
69
|
+
redisUrl: this.env('REDIS_URL'),
|
|
70
|
+
redisNamespace: APP_META.name
|
|
71
|
+
});
|
|
72
|
+
|
|
64
73
|
// ----------
|
|
65
74
|
// Call default-init
|
|
66
75
|
const instanceController = await super.initialize();
|
|
@@ -72,11 +81,14 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
72
81
|
// ----------
|
|
73
82
|
// Show proxies
|
|
74
83
|
const { PROXY } = this.config;
|
|
84
|
+
|
|
75
85
|
if (PROXY.entries.length) {
|
|
76
86
|
// Show active proxies
|
|
77
87
|
LOGGER.info(`> Reverse proxies active.`);
|
|
88
|
+
|
|
78
89
|
for (const proxy of PROXY.entries) {
|
|
79
90
|
let desc = `> ${proxy.hostnames.join('|')} >>> ${proxy.port || proxy.path}`;
|
|
91
|
+
|
|
80
92
|
// Start a proxy recursively?
|
|
81
93
|
if (proxy.path && FLAGS['recursive']) {
|
|
82
94
|
desc += ` ✅`;
|
|
@@ -87,6 +99,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
87
99
|
shell: true // for Windows compatibility
|
|
88
100
|
});
|
|
89
101
|
}
|
|
102
|
+
|
|
90
103
|
LOGGER.info(desc);
|
|
91
104
|
}
|
|
92
105
|
}
|
|
@@ -95,6 +108,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
95
108
|
// Show server details
|
|
96
109
|
if (this.#servers.size) {
|
|
97
110
|
LOGGER.info(`> Server running! (${APP_META.title || ''}) ✅`);
|
|
111
|
+
|
|
98
112
|
for (let [proto, def] of this.#servers) {
|
|
99
113
|
LOGGER.info(`> ${proto.toUpperCase()} / ${def.hostnames.concat('').join(`:${def.port} / `)}`);
|
|
100
114
|
}
|
|
@@ -109,7 +123,9 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
109
123
|
const routeDirs = [...new Set([this.config.LAYOUT.CLIENT_DIR, this.config.LAYOUT.WORKER_DIR, this.config.LAYOUT.SERVER_DIR])];
|
|
110
124
|
const entryPoints = await $glob(routeDirs.map((d) => `${d}/**/handler{${client ? ',.client' : ''}${worker ? ',.worker' : ''}${server ? ',.server' : ''}}.js`), { absolute: true })
|
|
111
125
|
.then((files) => files.map((file) => file.replace(/\\/g, '/')));
|
|
126
|
+
|
|
112
127
|
const initFiles = await $glob(`${process.cwd()}/init.server.js`);
|
|
128
|
+
|
|
113
129
|
const bundlingConfig = {
|
|
114
130
|
entryPoints: entryPoints.concat(initFiles),
|
|
115
131
|
outdir: this.config.RUNTIME_DIR,
|
|
@@ -123,11 +139,13 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
123
139
|
plugins: [UseLiveTransform()],
|
|
124
140
|
...options,
|
|
125
141
|
};
|
|
142
|
+
|
|
126
143
|
return await EsBuild.build(bundlingConfig);
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
async enterDevMode() {
|
|
130
147
|
const { appMeta, flags: FLAGS } = this.cx;
|
|
148
|
+
|
|
131
149
|
this.#hmr = WebfloHMR.manage(this, {
|
|
132
150
|
appMeta,
|
|
133
151
|
buildScripts: {
|
|
@@ -137,8 +155,10 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
137
155
|
},
|
|
138
156
|
buildSensitivity: parseInt(FLAGS['build-sensitivity'] || 0),
|
|
139
157
|
});
|
|
158
|
+
|
|
140
159
|
await this.#hmr.buildRoutes(true);
|
|
141
160
|
await this.#hmr.bundleAssetsIfPending(true);
|
|
161
|
+
|
|
142
162
|
if (FLAGS['open']) {
|
|
143
163
|
for (let [proto, def] of this.#servers) {
|
|
144
164
|
const url = `${proto}://${def.hostnames.find((h) => h !== '*') || 'localhost'}:${def.port}`;
|
|
@@ -147,194 +167,41 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
147
167
|
}
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
async initCreateStorage() {
|
|
151
|
-
if (this.bootstrap.init.createStorage
|
|
152
|
-
|| !this.bootstrap.init.redis) {
|
|
153
|
-
return super.initCreateStorage();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ns -> field -> { value, subscriptions: Set<fn> }
|
|
157
|
-
const local = new Map();
|
|
158
|
-
const fire = (state, value, key, namespace, scopeReference = false) => {
|
|
159
|
-
const returnValues = [];
|
|
160
|
-
for (const subscription of state.subscriptions) {
|
|
161
|
-
const { callback, options } = subscription;
|
|
162
|
-
|
|
163
|
-
const scope = subscription.scopeReference === scopeReference ? 0 : scopeReference ? 1 : 2;
|
|
164
|
-
// For options.scope === 0, only include same-request cycle mutations
|
|
165
|
-
// For options.scope === 1, only include local mutations
|
|
166
|
-
// For options.scope === 2, include remote mutations
|
|
167
|
-
if ((options.scope || 0) !== scope) continue;
|
|
168
|
-
|
|
169
|
-
returnValues.push(callback(value, scope));
|
|
170
|
-
if (options.once) state.subscriptions.delete(subscription);
|
|
171
|
-
const ns = local.get(namespace);
|
|
172
|
-
if (!state.subscriptions.size) ns.delete(key);
|
|
173
|
-
if (!ns.size) local.delete(namespace);
|
|
174
|
-
}
|
|
175
|
-
return Promise.all(returnValues);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// Initialize storage
|
|
179
|
-
const redis = this.bootstrap.init.redis;
|
|
180
|
-
this.bootstrap.init.createStorage = (namespace, ttl = null, scopeReference = {}) => {
|
|
181
|
-
const cleanups = [];
|
|
182
|
-
return {
|
|
183
|
-
async has(key) { return await redis.hexists(namespace, key); },
|
|
184
|
-
async get(key) {
|
|
185
|
-
const jsonValue = await redis.hget(namespace, key);
|
|
186
|
-
return jsonValue === null ? undefined : JSON.parse(jsonValue);
|
|
187
|
-
},
|
|
188
|
-
async set(key, value) {
|
|
189
|
-
const jsonValue = JSON.stringify(value);
|
|
190
|
-
const returnValue = await redis.hset(namespace, key, jsonValue);
|
|
191
|
-
if (!this.ttlApplied && ttl) {
|
|
192
|
-
await redis.expire(namespace, ttl);
|
|
193
|
-
this.ttlApplied = true;
|
|
194
|
-
}
|
|
195
|
-
const state = local.get(namespace)?.get(key);
|
|
196
|
-
if (state) {
|
|
197
|
-
state.jsonValue = jsonValue;
|
|
198
|
-
state.initialized = true;
|
|
199
|
-
await fire(state, value, key, namespace, scopeReference);
|
|
200
|
-
}
|
|
201
|
-
return returnValue;
|
|
202
|
-
},
|
|
203
|
-
async delete(key) {
|
|
204
|
-
const returnValue = await redis.hdel(namespace, key);
|
|
205
|
-
const state = local.get(namespace)?.get(key);
|
|
206
|
-
if (state) {
|
|
207
|
-
state.jsonValue = null;
|
|
208
|
-
state.initialized = true;
|
|
209
|
-
await fire(state, undefined, key, namespace, scopeReference);
|
|
210
|
-
}
|
|
211
|
-
return returnValue;
|
|
212
|
-
},
|
|
213
|
-
async clear() {
|
|
214
|
-
const returnValue = await redis.del(namespace);
|
|
215
|
-
const nsLocal = local.get(namespace);
|
|
216
|
-
if (nsLocal) {
|
|
217
|
-
for (const [key, state] of nsLocal.entries()) {
|
|
218
|
-
state.jsonValue = null;
|
|
219
|
-
state.initialized = true;
|
|
220
|
-
await fire(state, undefined, key, namespace, scopeReference);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return returnValue;
|
|
224
|
-
},
|
|
225
|
-
async keys() { return await redis.hkeys(namespace); },
|
|
226
|
-
async values() { return (await redis.hvals(namespace) || []).map((value) => value === null ? undefined : JSON.parse(value)); },
|
|
227
|
-
async entries() { return Object.entries(await redis.hgetall(namespace) || {}).map(([key, value]) => [key, value === null ? undefined : JSON.parse(value)]); },
|
|
228
|
-
get size() { return redis.hlen(namespace); },
|
|
229
|
-
observe(key, callback, options = {}) {
|
|
230
|
-
// Prepare local data structure
|
|
231
|
-
let ns = local.get(namespace);
|
|
232
|
-
if (!ns) {
|
|
233
|
-
ns = new Map();
|
|
234
|
-
local.set(namespace, ns);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let state = ns.get(key);
|
|
238
|
-
if (!state) {
|
|
239
|
-
state = { jsonValue: null, initialized: false, subscriptions: new Set() };
|
|
240
|
-
ns.set(key, state);
|
|
241
|
-
// Prime initial value only once
|
|
242
|
-
redis.hget(namespace, key).then((jsonValue) => {
|
|
243
|
-
if (state.initialized) return;
|
|
244
|
-
state.jsonValue = jsonValue;
|
|
245
|
-
state.initialized = true;
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const subscription = { callback, options, scopeReference };
|
|
250
|
-
state.subscriptions.add(subscription);
|
|
251
|
-
if (options.signal) {
|
|
252
|
-
options.signal.addEventListener('abort', () => {
|
|
253
|
-
state.subscriptions.delete(subscription);
|
|
254
|
-
if (state.subscriptions.size === 0) ns.delete(key);
|
|
255
|
-
if (ns.size === 0) local.delete(namespace);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Unsubscribe logic
|
|
260
|
-
const cleanup = () => {
|
|
261
|
-
state.subscriptions.delete(subscription);
|
|
262
|
-
if (state.subscriptions.size === 0) ns.delete(key);
|
|
263
|
-
if (ns.size === 0) local.delete(namespace);
|
|
264
|
-
};
|
|
265
|
-
cleanups.push(cleanup);
|
|
266
|
-
return cleanup;
|
|
267
|
-
},
|
|
268
|
-
cleanup() {
|
|
269
|
-
for (const cleanup of cleanups) cleanup();
|
|
270
|
-
cleanups.length = 0;
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// Watch for changes
|
|
276
|
-
const redisWatch = this.bootstrap.init.redisWatch;
|
|
277
|
-
if (redisWatch) {
|
|
278
|
-
// Subscribe to all events
|
|
279
|
-
redisWatch.psubscribe('__keyevent@0__:*');
|
|
280
|
-
redisWatch.on('pmessage', async (pattern, channel, redisKey) => {
|
|
281
|
-
const [, , event] = channel.split(':'); // -> "hset", "hdel", "expire", "del"
|
|
282
|
-
const namespace = redisKey;
|
|
283
|
-
|
|
284
|
-
const nsLocal = local.get(namespace);
|
|
285
|
-
if (!nsLocal) return;
|
|
286
|
-
|
|
287
|
-
const emitDiff = async () => {
|
|
288
|
-
for (const [field, state] of nsLocal) {
|
|
289
|
-
const jsonValue = await redis.hget(namespace, field);
|
|
290
|
-
if (jsonValue !== state.jsonValue) {
|
|
291
|
-
state.jsonValue = jsonValue;
|
|
292
|
-
const value = jsonValue === null ? undefined : JSON.parse(jsonValue);
|
|
293
|
-
fire(state, value, field, namespace);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Field updates
|
|
299
|
-
if (event === 'hset') await emitDiff();
|
|
300
|
-
if (event === 'hdel') await emitDiff();
|
|
301
|
-
|
|
302
|
-
// Namespace removal (expire or del)
|
|
303
|
-
if (event === 'expire' || event === 'del') {
|
|
304
|
-
for (const [key, state] of nsLocal) {
|
|
305
|
-
if (state.jsonValue !== null) {
|
|
306
|
-
state.jsonValue = null;
|
|
307
|
-
fire(state, undefined, key, namespace);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
local.delete(namespace);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
170
|
control() {
|
|
317
|
-
const { flags: FLAGS } = this.cx;
|
|
171
|
+
const { flags: FLAGS, logger: LOGGER } = this.cx;
|
|
318
172
|
const { SERVER, PROXY } = this.config;
|
|
319
173
|
const instanceController = super.control();
|
|
320
174
|
|
|
321
175
|
if (!FLAGS['test-only'] && !FLAGS['https-only'] && SERVER.port) {
|
|
322
|
-
const httpServer = Http.createServer((request, response) =>
|
|
176
|
+
const httpServer = Http.createServer((request, response) => {
|
|
177
|
+
this.handleNodeHttpRequest(request, response).catch((e) => {
|
|
178
|
+
LOGGER.error(e);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
323
181
|
httpServer.listen(FLAGS['port'] || SERVER.port);
|
|
182
|
+
|
|
324
183
|
this.#servers.set('http', {
|
|
325
184
|
instance: httpServer,
|
|
326
185
|
hostnames: SERVER.hostnames,
|
|
327
186
|
port: FLAGS['port'] || SERVER.port,
|
|
328
187
|
});
|
|
188
|
+
|
|
329
189
|
// Handle WebSocket connections
|
|
330
190
|
httpServer.on('upgrade', (request, socket, head) => {
|
|
331
|
-
this.handleNodeWsRequest(wss, request, socket, head)
|
|
191
|
+
this.handleNodeWsRequest(wss, request, socket, head).catch((e) => {
|
|
192
|
+
LOGGER.error(e);
|
|
193
|
+
});
|
|
332
194
|
});
|
|
333
195
|
}
|
|
334
196
|
|
|
335
197
|
if (!FLAGS['test-only'] && !FLAGS['http-only'] && SERVER.https.port) {
|
|
336
|
-
const httpsServer = Https.createServer((request, response) =>
|
|
198
|
+
const httpsServer = Https.createServer((request, response) => {
|
|
199
|
+
this.handleNodeHttpRequest(request, response).catch((e) => {
|
|
200
|
+
LOGGER.error(e);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
337
203
|
httpsServer.listen(SERVER.https.port);
|
|
204
|
+
|
|
338
205
|
const addSSLContext = (SERVER) => {
|
|
339
206
|
if (!Fs.existsSync(SERVER.https.keyfile)) return;
|
|
340
207
|
const cert = {
|
|
@@ -344,32 +211,37 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
344
211
|
SERVER.https.hostnames.forEach((hostname) => {
|
|
345
212
|
httpsServer.addContext(hostname, cert);
|
|
346
213
|
});
|
|
347
|
-
}
|
|
214
|
+
};
|
|
215
|
+
|
|
348
216
|
this.#servers.set('https', {
|
|
349
217
|
instance: httpsServer,
|
|
350
218
|
hostnames: SERVER.https.hostnames,
|
|
351
219
|
port: SERVER.https.port,
|
|
352
220
|
});
|
|
353
|
-
|
|
221
|
+
|
|
354
222
|
addSSLContext(SERVER);
|
|
223
|
+
|
|
355
224
|
for (const proxy of PROXY.entries) {
|
|
356
225
|
if (proxy.SERVER) {
|
|
357
226
|
addSSLContext(proxy.SERVER);
|
|
358
227
|
}
|
|
359
228
|
}
|
|
229
|
+
|
|
360
230
|
// Handle WebSocket connections
|
|
361
231
|
httpsServer.on('upgrade', (request, socket, head) => {
|
|
362
|
-
this.handleNodeWsRequest(wss, request, socket, head)
|
|
232
|
+
this.handleNodeWsRequest(wss, request, socket, head).catch((e) => {
|
|
233
|
+
LOGGER.error(e);
|
|
234
|
+
});
|
|
363
235
|
});
|
|
364
236
|
}
|
|
365
237
|
|
|
366
238
|
const wss = new WebSocketServer({ noServer: true });
|
|
367
239
|
|
|
368
240
|
process.on('uncaughtException', (err) => {
|
|
369
|
-
|
|
241
|
+
LOGGER.error('Uncaught Exception:', err);
|
|
370
242
|
});
|
|
371
243
|
process.on('unhandledRejection', (reason, promise) => {
|
|
372
|
-
|
|
244
|
+
LOGGER.log('Unhandled Rejection', reason, promise);
|
|
373
245
|
});
|
|
374
246
|
|
|
375
247
|
return instanceController;
|
|
@@ -377,32 +249,35 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
377
249
|
|
|
378
250
|
identifyIncoming(request, autoGenerateID = false) {
|
|
379
251
|
const secret = this.env('SESSION_KEY');
|
|
380
|
-
let
|
|
381
|
-
if (
|
|
252
|
+
let tenantID = request.headers.get('Cookie', true).find((c) => c.name === '__sessid')?.value;
|
|
253
|
+
if (tenantID?.includes('.')) {
|
|
382
254
|
if (secret) {
|
|
383
|
-
const [rand, signature] =
|
|
255
|
+
const [rand, signature] = tenantID.split('.');
|
|
384
256
|
const expectedSignature = crypto.createHmac('sha256', secret)
|
|
385
257
|
.update(rand)
|
|
386
258
|
.digest('hex');
|
|
387
259
|
if (signature !== expectedSignature) {
|
|
388
|
-
|
|
260
|
+
tenantID = null;
|
|
389
261
|
}
|
|
390
262
|
} else {
|
|
391
|
-
|
|
263
|
+
tenantID = null;
|
|
392
264
|
}
|
|
393
265
|
}
|
|
394
|
-
if (!
|
|
266
|
+
if (!tenantID) {
|
|
267
|
+
tenantID = request.headers.get('Authorization')?.replace(/\s+/, '_');
|
|
268
|
+
}
|
|
269
|
+
if (!tenantID && autoGenerateID) {
|
|
395
270
|
if (secret) {
|
|
396
271
|
const rand = `${(0 | Math.random() * 9e6).toString(36)}`;
|
|
397
272
|
const signature = crypto.createHmac('sha256', secret)
|
|
398
273
|
.update(rand)
|
|
399
274
|
.digest('hex');
|
|
400
|
-
|
|
275
|
+
tenantID = `${rand}.${signature}`
|
|
401
276
|
} else {
|
|
402
|
-
|
|
277
|
+
tenantID = crypto.randomUUID();
|
|
403
278
|
}
|
|
404
279
|
}
|
|
405
|
-
return
|
|
280
|
+
return tenantID;
|
|
406
281
|
}
|
|
407
282
|
|
|
408
283
|
async preResolveIncoming({ type, nodeRequest, proxy, reject, handle }) {
|
|
@@ -459,7 +334,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
459
334
|
}
|
|
460
335
|
if (REDIRECTS) {
|
|
461
336
|
const rejection = REDIRECTS.entries.reduce((_rdr, entry) => {
|
|
462
|
-
return _rdr || ((_rdr =
|
|
337
|
+
return _rdr || ((_rdr = new URLPatternPlus(entry.from, url.origin).exec(url.href)) && {
|
|
463
338
|
status: entry.code || 302,
|
|
464
339
|
statusText: entry.code === 301 ? 'Moved Permanently' : 'Found',
|
|
465
340
|
headers: { Location: _rdr.render(entry.to) }
|
|
@@ -469,10 +344,11 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
469
344
|
return reject(rejection);
|
|
470
345
|
}
|
|
471
346
|
}
|
|
472
|
-
return handle(url);
|
|
347
|
+
return await handle(url);
|
|
473
348
|
}
|
|
474
349
|
|
|
475
350
|
async handleNodeWsRequest(wss, nodeRequest, socket, head) {
|
|
351
|
+
const { logger: LOGGER } = this.cx;
|
|
476
352
|
const reject = (rejection) => {
|
|
477
353
|
const status = rejection.status || 400;
|
|
478
354
|
const statusText = rejection.statusText || 'Bad Request';
|
|
@@ -487,7 +363,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
487
363
|
`\r\n` +
|
|
488
364
|
body + `\r\n`
|
|
489
365
|
);
|
|
490
|
-
socket.
|
|
366
|
+
socket.end();
|
|
491
367
|
};
|
|
492
368
|
const proxy = async (destinationURL) => {
|
|
493
369
|
const isSecure = destinationURL.protocol === 'wss:';
|
|
@@ -513,11 +389,11 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
513
389
|
});
|
|
514
390
|
// Handle errors
|
|
515
391
|
proxySocket.on('error', err => {
|
|
516
|
-
|
|
517
|
-
socket.
|
|
392
|
+
LOGGER.error('Proxy socket error:', err);
|
|
393
|
+
socket.end();
|
|
518
394
|
});
|
|
519
395
|
socket.on('error', () => {
|
|
520
|
-
proxySocket.
|
|
396
|
+
proxySocket.end();
|
|
521
397
|
});
|
|
522
398
|
};
|
|
523
399
|
const handle = (requestURL) => {
|
|
@@ -527,24 +403,29 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
527
403
|
this.#hmr.clients.add(ws);
|
|
528
404
|
});
|
|
529
405
|
}
|
|
406
|
+
|
|
530
407
|
if (requestURL.searchParams.get('rel') === 'background-messaging') {
|
|
531
|
-
const request = new
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
408
|
+
const request = new RequestPlus(requestURL.href, { headers: nodeRequest.headers });
|
|
409
|
+
const tenantID = this.identifyIncoming(request);
|
|
410
|
+
|
|
411
|
+
const tenant = tenantID && this.#tenancy.getTenant(tenantID);
|
|
412
|
+
if (!tenant) {
|
|
413
|
+
return reject({ body: `Lost or invalid tenantID` });
|
|
536
414
|
}
|
|
537
|
-
|
|
538
|
-
|
|
415
|
+
|
|
416
|
+
const clientRequestPort = tenant?.getRequestPort(requestURL.pathname.split('/').pop());
|
|
417
|
+
if (!clientRequestPort) {
|
|
539
418
|
return reject({ body: `Lost or invalid portID` });
|
|
540
419
|
}
|
|
420
|
+
|
|
541
421
|
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
|
|
542
422
|
wss.emit('connection', ws, nodeRequest);
|
|
543
|
-
const wsw = new
|
|
544
|
-
|
|
423
|
+
const wsw = new WebSocketPort(ws, { handshake: 1, postAwaitsOpen: true });
|
|
424
|
+
clientRequestPort.addPort(wsw);
|
|
545
425
|
});
|
|
546
426
|
}
|
|
547
427
|
};
|
|
428
|
+
|
|
548
429
|
return await this.preResolveIncoming({ type: 'ws', nodeRequest, proxy, reject, handle });
|
|
549
430
|
}
|
|
550
431
|
|
|
@@ -556,8 +437,10 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
556
437
|
if (existing) nodeResponse.setHeader(name, [].concat(existing).concat(value));
|
|
557
438
|
else nodeResponse.setHeader(name, value);
|
|
558
439
|
}
|
|
559
|
-
|
|
440
|
+
|
|
441
|
+
nodeResponse.statusCode = response.status;
|
|
560
442
|
nodeResponse.statusMessage = response.statusText;
|
|
443
|
+
|
|
561
444
|
if (response.body instanceof Readable) {
|
|
562
445
|
response.body.pipe(nodeResponse);
|
|
563
446
|
} else if (response.body instanceof ReadableStream) {
|
|
@@ -567,6 +450,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
567
450
|
} else {
|
|
568
451
|
nodeResponse.end();
|
|
569
452
|
}
|
|
453
|
+
|
|
570
454
|
// Logging
|
|
571
455
|
const { logger: LOGGER } = this.cx;
|
|
572
456
|
if (LOGGER && requestURL) {
|
|
@@ -574,10 +458,12 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
574
458
|
LOGGER.log(log);
|
|
575
459
|
}
|
|
576
460
|
};
|
|
461
|
+
|
|
577
462
|
// Reject with error status
|
|
578
463
|
const reject = async (rejection) => {
|
|
579
464
|
respondWith(new Response(null, rejection));
|
|
580
465
|
};
|
|
466
|
+
|
|
581
467
|
// Proxy request to a remote/local host
|
|
582
468
|
const proxy = async (destinationURL) => {
|
|
583
469
|
const requestInit = this.parseNodeRequest(nodeRequest);
|
|
@@ -586,6 +472,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
586
472
|
const response = await fetch(destinationURL, requestInit);
|
|
587
473
|
respondWith(response, destinationURL);
|
|
588
474
|
};
|
|
475
|
+
|
|
589
476
|
// Handle
|
|
590
477
|
const handle = async (requestURL) => {
|
|
591
478
|
const requestInit = this.parseNodeRequest(nodeRequest);
|
|
@@ -596,6 +483,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
596
483
|
});
|
|
597
484
|
respondWith(response, requestURL);
|
|
598
485
|
};
|
|
486
|
+
|
|
599
487
|
return await this.preResolveIncoming({ typr: 'http', nodeRequest, reject, proxy, handle });
|
|
600
488
|
}
|
|
601
489
|
|
|
@@ -632,33 +520,35 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
632
520
|
writeRedirectHeaders(httpEvent, response) {
|
|
633
521
|
const $sparoots = this.bootstrap.$sparoots;
|
|
634
522
|
const xRedirectPolicy = httpEvent.request.headers.get('X-Redirect-Policy');
|
|
635
|
-
const xRedirectCode = httpEvent.request.headers.get('X-Redirect-Code') || 300;
|
|
636
523
|
const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
|
|
637
524
|
const isSameOriginRedirect = destinationURL.origin === httpEvent.url.origin;
|
|
638
525
|
let isSameSpaRedirect = true;
|
|
526
|
+
|
|
639
527
|
if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && $sparoots.length) {
|
|
640
528
|
// Longest-first sorting
|
|
641
529
|
const sparoots = $sparoots.sort((a, b) => a.length > b.length ? -1 : 1);
|
|
642
530
|
const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
|
|
643
531
|
isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
|
|
644
532
|
}
|
|
533
|
+
|
|
645
534
|
if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && (xRedirectPolicy === 'manual-when-cross-origin' || xRedirectPolicy === 'manual-when-cross-spa')) || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
|
|
646
|
-
response.headers.set('X-Redirect-Code',
|
|
535
|
+
response.headers.set('X-Redirect-Code', response.status);
|
|
647
536
|
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
648
537
|
response.headers.set('Cache-Control', 'no-store');
|
|
649
|
-
const responseMeta = _wq(response, 'meta');
|
|
650
|
-
responseMeta.set('status', xRedirectCode);
|
|
651
538
|
}
|
|
652
539
|
}
|
|
653
540
|
|
|
654
541
|
async remoteFetch(request, ...args) {
|
|
655
542
|
let href = request;
|
|
543
|
+
|
|
656
544
|
if (request instanceof Request) {
|
|
657
545
|
href = request.url;
|
|
658
546
|
} else if (request instanceof URL) {
|
|
659
547
|
href = request.href;
|
|
660
548
|
}
|
|
549
|
+
|
|
661
550
|
const _response = fetch(request, ...args);
|
|
551
|
+
|
|
662
552
|
// Save a reference to this
|
|
663
553
|
return _response.then(async response => {
|
|
664
554
|
// Stop loading status
|
|
@@ -670,6 +560,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
670
560
|
const { flags: FLAGS } = this.cx;
|
|
671
561
|
const { RUNTIME_LAYOUT, LAYOUT } = this.config;
|
|
672
562
|
const scopeObj = {};
|
|
563
|
+
|
|
673
564
|
if (FLAGS['dev']) {
|
|
674
565
|
if (httpEvent.url.pathname === '/@hmr') {
|
|
675
566
|
const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
|
|
@@ -690,22 +581,28 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
690
581
|
} else {
|
|
691
582
|
scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
|
|
692
583
|
}
|
|
584
|
+
|
|
693
585
|
scopeObj.ext = Path.parse(scopeObj.filename).ext;
|
|
586
|
+
|
|
694
587
|
const finalizeResponse = (response) => {
|
|
695
588
|
// Qualify Service-Worker responses
|
|
696
589
|
if (httpEvent.request.headers.get('Service-Worker') === 'script') {
|
|
697
590
|
response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
|
|
698
591
|
}
|
|
699
|
-
|
|
592
|
+
|
|
593
|
+
const responseMeta = _meta(response);
|
|
700
594
|
responseMeta.set('filename', scopeObj.filename);
|
|
701
595
|
responseMeta.set('static', true);
|
|
702
596
|
responseMeta.set('index', scopeObj.index);
|
|
597
|
+
|
|
703
598
|
return response;
|
|
704
599
|
};
|
|
600
|
+
|
|
705
601
|
// Pre-encoding support?
|
|
706
602
|
if (scopeObj.preEncodingSupportLevel !== 0) {
|
|
707
603
|
scopeObj.acceptEncs = [];
|
|
708
604
|
scopeObj.supportedEncs = { gzip: '.gz', br: '.br' };
|
|
605
|
+
|
|
709
606
|
if ((scopeObj.acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map((e) => e.trim())).length
|
|
710
607
|
&& (scopeObj.enc = scopeObj.acceptEncs.reduce((prev, _enc) => prev || (scopeObj.supportedEncs[_enc] && Fs.existsSync(scopeObj.filename + scopeObj.supportedEncs[_enc]) && _enc), null))) {
|
|
711
608
|
// Route to a pre-compressed version of the file
|
|
@@ -715,6 +612,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
715
612
|
// TODO: Do dynamic encoding
|
|
716
613
|
}
|
|
717
614
|
}
|
|
615
|
+
|
|
718
616
|
// if is a directory, search for index file matching the extention
|
|
719
617
|
if (!scopeObj.ext && scopeObj.autoIndexFileSupport !== false && Fs.existsSync(scopeObj.filename) && (scopeObj.stats = Fs.lstatSync(scopeObj.filename)).isDirectory()) {
|
|
720
618
|
scopeObj.ext = '.html';
|
|
@@ -722,6 +620,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
722
620
|
scopeObj.filename = Path.join(scopeObj.filename, scopeObj.index);
|
|
723
621
|
scopeObj.stats = null;
|
|
724
622
|
}
|
|
623
|
+
|
|
725
624
|
// ------ If we get here, scopeObj.filename has been finalized ------
|
|
726
625
|
// Do file stats
|
|
727
626
|
if (!scopeObj.stats) {
|
|
@@ -730,9 +629,11 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
730
629
|
throw e; // Re-throw other errors
|
|
731
630
|
}
|
|
732
631
|
}
|
|
632
|
+
|
|
733
633
|
// ETag support
|
|
734
634
|
scopeObj.stats.etag = `W/"${scopeObj.stats.size}-${scopeObj.stats.mtimeMs}"`;
|
|
735
635
|
const ifNoneMatch = httpEvent.request.headers.get('If-None-Match');
|
|
636
|
+
|
|
736
637
|
if (scopeObj.stats.etag && ifNoneMatch === scopeObj.stats.etag) {
|
|
737
638
|
const response = new Response(null, { status: 304, statusText: 'Not Modified' });
|
|
738
639
|
response.headers.set('ETag', scopeObj.stats.etag);
|
|
@@ -740,29 +641,37 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
740
641
|
response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
|
|
741
642
|
return finalizeResponse(response);
|
|
742
643
|
}
|
|
743
|
-
|
|
644
|
+
|
|
645
|
+
scopeObj.stats.mime = scopeObj.ext && (Mime.lookup(scopeObj.ext) || null)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
|
|
646
|
+
|
|
744
647
|
// Range support
|
|
745
648
|
const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
|
|
746
649
|
scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
|
|
747
|
-
const statusCode =
|
|
650
|
+
const statusCode = scopeObj.response.status;
|
|
748
651
|
if (statusCode === 416) return finalizeResponse(scopeObj.response);
|
|
652
|
+
|
|
749
653
|
// ------ If we get here, it means we're good ------
|
|
750
654
|
if (scopeObj.enc) {
|
|
751
655
|
scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
|
|
752
656
|
}
|
|
657
|
+
|
|
753
658
|
// 1. Strong cache validators
|
|
754
659
|
scopeObj.response.headers.set('ETag', scopeObj.stats.etag);
|
|
755
660
|
scopeObj.response.headers.set('Last-Modified', scopeObj.stats.mtime.toUTCString());
|
|
661
|
+
|
|
756
662
|
// 2. Content presentation and policy
|
|
757
663
|
scopeObj.response.headers.set('Content-Disposition', `inline; filename="${Path.basename(scopeObj.filename)}"`);
|
|
758
664
|
scopeObj.response.headers.set('Referrer-Policy', 'no-referrer-when-downgrade');
|
|
665
|
+
|
|
759
666
|
// 3. Cache-Control
|
|
760
667
|
scopeObj.response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
|
|
761
668
|
scopeObj.response.headers.set('Vary', 'Accept-Encoding'); // The header that talks to our support for "Accept-Encoding"
|
|
669
|
+
|
|
762
670
|
// 4. Security headers
|
|
763
671
|
scopeObj.response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
764
672
|
scopeObj.response.headers.set('Access-Control-Allow-Origin', '*');
|
|
765
673
|
scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
|
674
|
+
|
|
766
675
|
// 5. Partial content support
|
|
767
676
|
scopeObj.response.headers.set('Accept-Ranges', 'bytes');
|
|
768
677
|
|
|
@@ -772,63 +681,110 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
772
681
|
async navigate(url, init = {}, detail = {}) {
|
|
773
682
|
const { HEADERS } = this.config;
|
|
774
683
|
const { flags: FLAGS } = this.cx;
|
|
775
|
-
|
|
684
|
+
|
|
685
|
+
// Scope object
|
|
686
|
+
const scopeObj = {
|
|
687
|
+
url,
|
|
688
|
+
init,
|
|
689
|
+
detail,
|
|
690
|
+
requestID: (0 | Math.random() * 9e6).toString(36),
|
|
691
|
+
sessionTTL: parseInt(this.env('SESSION_TTL')) || 2592000/*30days*/
|
|
692
|
+
};
|
|
776
693
|
if (typeof scopeObj.url === 'string') {
|
|
777
694
|
scopeObj.url = new URL(scopeObj.url, 'http://localhost');
|
|
778
695
|
}
|
|
779
|
-
|
|
696
|
+
|
|
697
|
+
// Request
|
|
780
698
|
scopeObj.autoHeaders = HEADERS.entries.filter((entry) => (new URLPattern(entry.url, url.origin)).exec(url.href)) || [];
|
|
781
|
-
scopeObj.request =
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
scopeObj.
|
|
785
|
-
scopeObj.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
699
|
+
scopeObj.request = scopeObj.init instanceof Request && scopeObj.init.url === scopeObj.url.href
|
|
700
|
+
? scopeObj.init
|
|
701
|
+
: this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
|
|
702
|
+
RequestPlus.upgradeInPlace(scopeObj.request);
|
|
703
|
+
scopeObj.tenantID = this.identifyIncoming(scopeObj.request, true);
|
|
704
|
+
|
|
705
|
+
// Origins
|
|
706
|
+
const origins = [scopeObj.requestID];
|
|
707
|
+
|
|
708
|
+
// Thread
|
|
709
|
+
scopeObj.thread = HttpThread111.create({
|
|
710
|
+
context: {},
|
|
711
|
+
store: this.#keyvals.create({ path: ['thread', scopeObj.tenantID], origins }),
|
|
789
712
|
threadID: scopeObj.url.searchParams.get('_thread'),
|
|
790
713
|
realm: 3
|
|
791
714
|
});
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
715
|
+
|
|
716
|
+
// Cookies
|
|
717
|
+
const entries = scopeObj.request.headers.get('Cookie', true).map((c) => [c.name, c]);
|
|
718
|
+
const store = InMemoryKV.create({ path: ['cookies', scopeObj.tenantID] });
|
|
719
|
+
entries.forEach(([key, value]) => store.set(key, { value }));
|
|
720
|
+
const initial = Object.fromEntries(entries);
|
|
721
|
+
scopeObj.cookies = HttpCookies101.create({
|
|
722
|
+
context: { handlersRegistry: this.#keyvals.getHandlers('cookies', true) },
|
|
723
|
+
store,
|
|
724
|
+
initial,
|
|
795
725
|
realm: 3
|
|
796
726
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
727
|
+
|
|
728
|
+
// Session
|
|
729
|
+
scopeObj.session = HttpSession001.create({
|
|
730
|
+
context: { handlersRegistry: this.#keyvals.getHandlers('session', true) },
|
|
731
|
+
store: this.#keyvals.create({ path: ['session', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
|
|
732
|
+
sessionID: scopeObj.tenantID,
|
|
802
733
|
ttl: scopeObj.sessionTTL,
|
|
803
734
|
realm: 3
|
|
804
735
|
});
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
736
|
+
|
|
737
|
+
// User
|
|
738
|
+
scopeObj.user = HttpUser111.create({
|
|
739
|
+
context: { handlersRegistry: this.#keyvals.getHandlers('user', true) },
|
|
740
|
+
store: this.#keyvals.create({ path: ['user', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
|
|
810
741
|
realm: 3
|
|
811
742
|
});
|
|
812
|
-
|
|
743
|
+
|
|
744
|
+
// Client
|
|
745
|
+
scopeObj.tenant = this.#tenancy.getTenant(scopeObj.tenantID, true);
|
|
746
|
+
scopeObj.clientRequestPort = scopeObj.tenant.createRequestPort(
|
|
747
|
+
crypto.randomUUID(),
|
|
748
|
+
scopeObj.request.url
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// HttpEvent
|
|
752
|
+
scopeObj.httpEvent = HttpEvent111.create({
|
|
753
|
+
detail: scopeObj.detail,
|
|
754
|
+
signal: init.signal,
|
|
813
755
|
request: scopeObj.request,
|
|
814
756
|
thread: scopeObj.thread,
|
|
815
|
-
client: scopeObj.clientRequestRealtime,
|
|
816
757
|
cookies: scopeObj.cookies,
|
|
817
758
|
session: scopeObj.session,
|
|
818
759
|
user: scopeObj.user,
|
|
819
|
-
|
|
760
|
+
client: scopeObj.clientRequestPort,
|
|
820
761
|
realm: 3
|
|
821
762
|
});
|
|
763
|
+
|
|
822
764
|
// Dispatch for response
|
|
823
765
|
scopeObj.response = await this.dispatchNavigationEvent({
|
|
824
766
|
httpEvent: scopeObj.httpEvent,
|
|
825
767
|
crossLayerFetch: (event) => this.localFetch(event),
|
|
826
|
-
clientPortB: `
|
|
768
|
+
clientPortB: `socket:///${scopeObj.httpEvent.client.portID}?rel=background-messaging`
|
|
827
769
|
});
|
|
770
|
+
|
|
771
|
+
// Commit session - expires six months
|
|
772
|
+
if (!scopeObj.response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
|
|
773
|
+
scopeObj.response.headers.append('Set-Cookie', `__sessid=${scopeObj.tenantID}; Path=/; ${!FLAGS['dev'] ? 'Secure; ' : ''}HttpOnly; SameSite=Lax${scopeObj.sessionTTL ? `; Max-Age=${scopeObj.sessionTTL}` : ''}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Commit cookies
|
|
777
|
+
for (const cookieStr of await scopeObj.cookies.render()) {
|
|
778
|
+
scopeObj.response.headers.append('Set-Cookie', cookieStr);
|
|
779
|
+
}
|
|
780
|
+
await scopeObj.cookies._commit();
|
|
781
|
+
|
|
828
782
|
// Reponse handlers
|
|
829
783
|
if (FLAGS['dev']) {
|
|
830
784
|
scopeObj.response.headers.set('X-Webflo-Dev-Mode', 'true'); // Must come before satisfyRequestFormat() sp as to be rendered
|
|
831
785
|
}
|
|
786
|
+
|
|
787
|
+
// Write headers / satisfy request format
|
|
832
788
|
if (scopeObj.response.headers.get('Location')) {
|
|
833
789
|
this.writeRedirectHeaders(scopeObj.httpEvent, scopeObj.response);
|
|
834
790
|
} else {
|
|
@@ -838,74 +794,90 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
838
794
|
scopeObj.response.headers.set('Cache-Control', 'no-store');
|
|
839
795
|
}
|
|
840
796
|
}
|
|
797
|
+
|
|
841
798
|
return scopeObj.response;
|
|
842
799
|
}
|
|
843
800
|
|
|
844
801
|
async satisfyRequestFormat(httpEvent, response) {
|
|
845
|
-
const statusCode =
|
|
802
|
+
const statusCode = response.status;
|
|
803
|
+
|
|
846
804
|
if (statusCode === 206 || statusCode === 416) {
|
|
847
805
|
// If the response is a partial content, we don't need to do anything else
|
|
848
806
|
return response;
|
|
849
807
|
}
|
|
808
|
+
|
|
850
809
|
// Satisfy "Accept" header
|
|
851
|
-
const requestAccept =
|
|
810
|
+
const requestAccept = httpEvent.request.headers.get('Accept', true);
|
|
852
811
|
const asHTML = requestAccept?.match('text/html');
|
|
853
812
|
const asIs = requestAccept?.match(response.headers.get('Content-Type'));
|
|
854
|
-
const responseMeta =
|
|
813
|
+
const responseMeta = _meta(response);
|
|
814
|
+
|
|
855
815
|
if (requestAccept && asHTML > asIs && !responseMeta.get('static')) {
|
|
856
816
|
response = await this.render(httpEvent, response);
|
|
857
|
-
} else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
|
|
817
|
+
} else if (requestAccept && response.body && response.headers.get('Content-Type') && !asIs) {
|
|
858
818
|
return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
|
|
859
819
|
}
|
|
820
|
+
|
|
860
821
|
// ------- With "exception" responses out of the way,
|
|
861
822
|
// let's set the header that talks to our support for "Accept"
|
|
862
823
|
if (!responseMeta.get('static')) {
|
|
863
824
|
response.headers.append('Vary', 'Accept');
|
|
864
825
|
}
|
|
826
|
+
|
|
865
827
|
// Satisfy "Range" header
|
|
866
|
-
const requestRange =
|
|
867
|
-
if (requestRange.length && response.headers.get('Content-Length')) {
|
|
828
|
+
const requestRange = httpEvent.request.headers.get('Range', true);
|
|
829
|
+
if (requestRange.length && response.body && response.headers.get('Content-Length')) {
|
|
868
830
|
const stats = {
|
|
869
831
|
size: parseInt(response.headers.get('Content-Length')),
|
|
870
832
|
mime: response.headers.get('Content-Type') || 'application/octet-stream',
|
|
871
833
|
};
|
|
834
|
+
|
|
872
835
|
const headersBefore = response.headers;
|
|
873
836
|
response = this.createStreamingResponse(
|
|
874
837
|
httpEvent,
|
|
875
838
|
(params) => this.streamSlice(response.body, { ...params }),
|
|
876
839
|
stats
|
|
877
840
|
);
|
|
841
|
+
|
|
878
842
|
for (const [name, value] of headersBefore) {
|
|
879
843
|
if (/Content-Length|Content-Type/i.test(name)) continue;
|
|
880
844
|
response.headers.append(name, value);
|
|
881
845
|
}
|
|
882
846
|
}
|
|
847
|
+
|
|
883
848
|
return response;
|
|
884
849
|
}
|
|
885
850
|
|
|
886
851
|
async render(httpEvent, response) {
|
|
887
852
|
const { LAYOUT } = this.config;
|
|
888
853
|
const scopeObj = {};
|
|
889
|
-
|
|
854
|
+
|
|
855
|
+
scopeObj.router = new WebfloRouter111(this, httpEvent.url.pathname);
|
|
890
856
|
scopeObj.rendering = await scopeObj.router.route('render', httpEvent, async (httpEvent) => {
|
|
891
857
|
let renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
|
|
858
|
+
|
|
892
859
|
while ((renderFile = Path.join(LAYOUT.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
|
|
893
860
|
&& (this.#renderFileCache.get(renderFile) === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
|
|
894
861
|
this.#renderFileCache.set(renderFile, false);
|
|
895
862
|
pathnameSplit.pop();
|
|
896
863
|
}
|
|
864
|
+
|
|
897
865
|
const dirPublic = Url.pathToFileURL(Path.resolve(Path.join(LAYOUT.PUBLIC_DIR)));
|
|
898
866
|
const instanceParams = /*QueryString.stringify*/({
|
|
899
867
|
//file: renderFile,
|
|
900
868
|
url: dirPublic.href,// httpEvent.url.href,
|
|
901
869
|
});
|
|
870
|
+
|
|
902
871
|
const { window, document } = createWindow(renderFile, instanceParams);
|
|
872
|
+
|
|
903
873
|
//const { window, document } = await import('@webqit/oohtml-ssr/src/instance.js?' + instanceParams);
|
|
904
874
|
await new Promise((res) => {
|
|
905
875
|
if (document.readyState === 'complete') return res(1);
|
|
906
876
|
document.addEventListener('load', res);
|
|
907
877
|
});
|
|
908
|
-
|
|
878
|
+
|
|
879
|
+
const data = await response.any({ to: 'json' });
|
|
880
|
+
|
|
909
881
|
if (window.webqit?.oohtml?.config) {
|
|
910
882
|
// Await rendering engine
|
|
911
883
|
if (window.webqit?.$qCompilerWorker) {
|
|
@@ -915,14 +887,17 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
915
887
|
setTimeout(() => res(1), 1000);
|
|
916
888
|
});
|
|
917
889
|
}
|
|
890
|
+
|
|
918
891
|
const {
|
|
919
892
|
HTML_IMPORTS: { attr: modulesContextAttrs } = {},
|
|
920
893
|
BINDINGS_API: { api: bindingsConfig } = {},
|
|
921
894
|
} = window.webqit.oohtml.config;
|
|
895
|
+
|
|
922
896
|
if (modulesContextAttrs) {
|
|
923
897
|
const newRoute = '/' + `app/${httpEvent.url.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
|
|
924
898
|
document.body.setAttribute(modulesContextAttrs.importscontext, newRoute);
|
|
925
899
|
}
|
|
900
|
+
|
|
926
901
|
if (bindingsConfig) {
|
|
927
902
|
document[bindingsConfig.bind]({
|
|
928
903
|
state: {},
|
|
@@ -935,9 +910,11 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
935
910
|
background: null
|
|
936
911
|
}, { diff: true });
|
|
937
912
|
}
|
|
913
|
+
|
|
938
914
|
await new Promise(res => setTimeout(res, 300));
|
|
939
915
|
}
|
|
940
|
-
|
|
916
|
+
|
|
917
|
+
for (const name of ['X-Message-Port', 'X-Webflo-Dev-Mode']) {
|
|
941
918
|
document.querySelector(`meta[name="${name}"]`)?.remove();
|
|
942
919
|
if (!response.headers.get(name)) continue;
|
|
943
920
|
const metaElement = document.createElement('meta');
|
|
@@ -945,6 +922,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
945
922
|
metaElement.setAttribute('content', response.headers.get(name));
|
|
946
923
|
document.head.prepend(metaElement);
|
|
947
924
|
}
|
|
925
|
+
|
|
948
926
|
// Append hydration data
|
|
949
927
|
for (const [rel, content] of [['hydration', data]]) {
|
|
950
928
|
document.querySelector(`script[rel="${rel}"][type="application/json"]`)?.remove();
|
|
@@ -954,18 +932,23 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
954
932
|
dataScript.textContent = JSON.stringify(content);
|
|
955
933
|
document.body.append(dataScript);
|
|
956
934
|
}
|
|
935
|
+
|
|
957
936
|
const rendering = window.toString();
|
|
958
937
|
document.documentElement.remove();
|
|
959
938
|
document.writeln('');
|
|
939
|
+
|
|
960
940
|
try { window.close(); } catch (e) { }
|
|
941
|
+
|
|
961
942
|
return rendering;
|
|
962
943
|
});
|
|
944
|
+
|
|
963
945
|
// Validate rendering
|
|
964
946
|
if (typeof scopeObj.rendering !== 'string' && !(typeof scopeObj.rendering?.toString === 'function')) {
|
|
965
947
|
throw new Error('render() must return a string response or an object that implements toString()..');
|
|
966
948
|
}
|
|
949
|
+
|
|
967
950
|
// Convert back to response
|
|
968
|
-
const statusCode =
|
|
951
|
+
const statusCode = response.status;
|
|
969
952
|
scopeObj.response = new Response(scopeObj.rendering, {
|
|
970
953
|
headers: response.headers,
|
|
971
954
|
status: statusCode,
|
|
@@ -973,32 +956,42 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
973
956
|
});
|
|
974
957
|
scopeObj.response.headers.set('Content-Type', 'text/html');
|
|
975
958
|
scopeObj.response.headers.set('Content-Length', (new Blob([scopeObj.rendering])).size);
|
|
959
|
+
|
|
976
960
|
return scopeObj.response;
|
|
977
961
|
}
|
|
978
962
|
|
|
979
963
|
generateLog(request, response, isproxy = false) {
|
|
980
964
|
const { logger: LOGGER } = this.cx;
|
|
981
965
|
const log = [];
|
|
966
|
+
|
|
982
967
|
// ---------------
|
|
983
968
|
const style = LOGGER.style || { keyword: (str) => str, comment: (str) => str, url: (str) => str, val: (str) => str, err: (str) => str, };
|
|
984
|
-
const statusCode =
|
|
969
|
+
const statusCode = response.status;
|
|
985
970
|
const errorCode = statusCode >= 400 && statusCode < 500 ? statusCode : 0;
|
|
986
971
|
const xRedirectCode = response.headers.get('X-Redirect-Code');
|
|
987
972
|
const isRedirect = (xRedirectCode || statusCode + '').startsWith('3') && (xRedirectCode || statusCode) !== 304;
|
|
988
|
-
const _statusCode = xRedirectCode && `${
|
|
989
|
-
const responseMeta =
|
|
973
|
+
const _statusCode = xRedirectCode && `${statusCode} (${xRedirectCode})` || statusCode;
|
|
974
|
+
const responseMeta = _meta(response);
|
|
990
975
|
// ---------------
|
|
976
|
+
|
|
991
977
|
log.push(`[${style.comment((new Date).toUTCString())}]`);
|
|
992
978
|
log.push(style.keyword(request.method));
|
|
993
979
|
if (isproxy) log.push(style.keyword('>>'));
|
|
994
980
|
log.push(style.url(request.url));
|
|
981
|
+
|
|
995
982
|
if (responseMeta.has('hint')) log.push(`(${style.comment(responseMeta.get('hint'))})`);
|
|
996
983
|
const contentInfo = [response.headers.get('Content-Type'), response.headers.get('Content-Length') && this.formatBytes(response.headers.get('Content-Length'))].filter((x) => x);
|
|
984
|
+
|
|
997
985
|
if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
|
|
998
986
|
if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
|
|
999
987
|
if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
|
|
1000
988
|
else log.push(style.val(`${_statusCode} ${response.statusText}`));
|
|
989
|
+
if (response.headers.get('X-Message-Port')) {
|
|
990
|
+
log.push(style.keyword(`[${style.keyword('L')}]`));
|
|
991
|
+
}
|
|
992
|
+
|
|
1001
993
|
if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
|
|
994
|
+
|
|
1002
995
|
return log.join(' ');
|
|
1003
996
|
}
|
|
1004
997
|
|