@webqit/webflo 0.20.25 → 0.20.27
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 +7 -5
- 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 -95
- 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 -266
- 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
|
|
|
@@ -636,29 +524,34 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
636
524
|
const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
|
|
637
525
|
const isSameOriginRedirect = destinationURL.origin === httpEvent.url.origin;
|
|
638
526
|
let isSameSpaRedirect = true;
|
|
527
|
+
|
|
639
528
|
if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && $sparoots.length) {
|
|
640
529
|
// Longest-first sorting
|
|
641
530
|
const sparoots = $sparoots.sort((a, b) => a.length > b.length ? -1 : 1);
|
|
642
531
|
const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
|
|
643
532
|
isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
|
|
644
533
|
}
|
|
534
|
+
|
|
645
535
|
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',
|
|
536
|
+
response.headers.set('X-Redirect-Code', response.status);
|
|
647
537
|
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
648
538
|
response.headers.set('Cache-Control', 'no-store');
|
|
649
|
-
const responseMeta =
|
|
539
|
+
const responseMeta = _meta(response);
|
|
650
540
|
responseMeta.set('status', xRedirectCode);
|
|
651
541
|
}
|
|
652
542
|
}
|
|
653
543
|
|
|
654
544
|
async remoteFetch(request, ...args) {
|
|
655
545
|
let href = request;
|
|
546
|
+
|
|
656
547
|
if (request instanceof Request) {
|
|
657
548
|
href = request.url;
|
|
658
549
|
} else if (request instanceof URL) {
|
|
659
550
|
href = request.href;
|
|
660
551
|
}
|
|
552
|
+
|
|
661
553
|
const _response = fetch(request, ...args);
|
|
554
|
+
|
|
662
555
|
// Save a reference to this
|
|
663
556
|
return _response.then(async response => {
|
|
664
557
|
// Stop loading status
|
|
@@ -670,6 +563,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
670
563
|
const { flags: FLAGS } = this.cx;
|
|
671
564
|
const { RUNTIME_LAYOUT, LAYOUT } = this.config;
|
|
672
565
|
const scopeObj = {};
|
|
566
|
+
|
|
673
567
|
if (FLAGS['dev']) {
|
|
674
568
|
if (httpEvent.url.pathname === '/@hmr') {
|
|
675
569
|
const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
|
|
@@ -690,22 +584,28 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
690
584
|
} else {
|
|
691
585
|
scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
|
|
692
586
|
}
|
|
587
|
+
|
|
693
588
|
scopeObj.ext = Path.parse(scopeObj.filename).ext;
|
|
589
|
+
|
|
694
590
|
const finalizeResponse = (response) => {
|
|
695
591
|
// Qualify Service-Worker responses
|
|
696
592
|
if (httpEvent.request.headers.get('Service-Worker') === 'script') {
|
|
697
593
|
response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
|
|
698
594
|
}
|
|
699
|
-
|
|
595
|
+
|
|
596
|
+
const responseMeta = _meta(response);
|
|
700
597
|
responseMeta.set('filename', scopeObj.filename);
|
|
701
598
|
responseMeta.set('static', true);
|
|
702
599
|
responseMeta.set('index', scopeObj.index);
|
|
600
|
+
|
|
703
601
|
return response;
|
|
704
602
|
};
|
|
603
|
+
|
|
705
604
|
// Pre-encoding support?
|
|
706
605
|
if (scopeObj.preEncodingSupportLevel !== 0) {
|
|
707
606
|
scopeObj.acceptEncs = [];
|
|
708
607
|
scopeObj.supportedEncs = { gzip: '.gz', br: '.br' };
|
|
608
|
+
|
|
709
609
|
if ((scopeObj.acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map((e) => e.trim())).length
|
|
710
610
|
&& (scopeObj.enc = scopeObj.acceptEncs.reduce((prev, _enc) => prev || (scopeObj.supportedEncs[_enc] && Fs.existsSync(scopeObj.filename + scopeObj.supportedEncs[_enc]) && _enc), null))) {
|
|
711
611
|
// Route to a pre-compressed version of the file
|
|
@@ -715,6 +615,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
715
615
|
// TODO: Do dynamic encoding
|
|
716
616
|
}
|
|
717
617
|
}
|
|
618
|
+
|
|
718
619
|
// if is a directory, search for index file matching the extention
|
|
719
620
|
if (!scopeObj.ext && scopeObj.autoIndexFileSupport !== false && Fs.existsSync(scopeObj.filename) && (scopeObj.stats = Fs.lstatSync(scopeObj.filename)).isDirectory()) {
|
|
720
621
|
scopeObj.ext = '.html';
|
|
@@ -722,6 +623,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
722
623
|
scopeObj.filename = Path.join(scopeObj.filename, scopeObj.index);
|
|
723
624
|
scopeObj.stats = null;
|
|
724
625
|
}
|
|
626
|
+
|
|
725
627
|
// ------ If we get here, scopeObj.filename has been finalized ------
|
|
726
628
|
// Do file stats
|
|
727
629
|
if (!scopeObj.stats) {
|
|
@@ -730,9 +632,11 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
730
632
|
throw e; // Re-throw other errors
|
|
731
633
|
}
|
|
732
634
|
}
|
|
635
|
+
|
|
733
636
|
// ETag support
|
|
734
637
|
scopeObj.stats.etag = `W/"${scopeObj.stats.size}-${scopeObj.stats.mtimeMs}"`;
|
|
735
638
|
const ifNoneMatch = httpEvent.request.headers.get('If-None-Match');
|
|
639
|
+
|
|
736
640
|
if (scopeObj.stats.etag && ifNoneMatch === scopeObj.stats.etag) {
|
|
737
641
|
const response = new Response(null, { status: 304, statusText: 'Not Modified' });
|
|
738
642
|
response.headers.set('ETag', scopeObj.stats.etag);
|
|
@@ -740,29 +644,37 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
740
644
|
response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
|
|
741
645
|
return finalizeResponse(response);
|
|
742
646
|
}
|
|
743
|
-
|
|
647
|
+
|
|
648
|
+
scopeObj.stats.mime = scopeObj.ext && (Mime.lookup(scopeObj.ext) || null)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
|
|
649
|
+
|
|
744
650
|
// Range support
|
|
745
651
|
const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
|
|
746
652
|
scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
|
|
747
|
-
const statusCode =
|
|
653
|
+
const statusCode = scopeObj.response.status;
|
|
748
654
|
if (statusCode === 416) return finalizeResponse(scopeObj.response);
|
|
655
|
+
|
|
749
656
|
// ------ If we get here, it means we're good ------
|
|
750
657
|
if (scopeObj.enc) {
|
|
751
658
|
scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
|
|
752
659
|
}
|
|
660
|
+
|
|
753
661
|
// 1. Strong cache validators
|
|
754
662
|
scopeObj.response.headers.set('ETag', scopeObj.stats.etag);
|
|
755
663
|
scopeObj.response.headers.set('Last-Modified', scopeObj.stats.mtime.toUTCString());
|
|
664
|
+
|
|
756
665
|
// 2. Content presentation and policy
|
|
757
666
|
scopeObj.response.headers.set('Content-Disposition', `inline; filename="${Path.basename(scopeObj.filename)}"`);
|
|
758
667
|
scopeObj.response.headers.set('Referrer-Policy', 'no-referrer-when-downgrade');
|
|
668
|
+
|
|
759
669
|
// 3. Cache-Control
|
|
760
670
|
scopeObj.response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
|
|
761
671
|
scopeObj.response.headers.set('Vary', 'Accept-Encoding'); // The header that talks to our support for "Accept-Encoding"
|
|
672
|
+
|
|
762
673
|
// 4. Security headers
|
|
763
674
|
scopeObj.response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
764
675
|
scopeObj.response.headers.set('Access-Control-Allow-Origin', '*');
|
|
765
676
|
scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
|
677
|
+
|
|
766
678
|
// 5. Partial content support
|
|
767
679
|
scopeObj.response.headers.set('Accept-Ranges', 'bytes');
|
|
768
680
|
|
|
@@ -772,63 +684,110 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
772
684
|
async navigate(url, init = {}, detail = {}) {
|
|
773
685
|
const { HEADERS } = this.config;
|
|
774
686
|
const { flags: FLAGS } = this.cx;
|
|
775
|
-
|
|
687
|
+
|
|
688
|
+
// Scope object
|
|
689
|
+
const scopeObj = {
|
|
690
|
+
url,
|
|
691
|
+
init,
|
|
692
|
+
detail,
|
|
693
|
+
requestID: (0 | Math.random() * 9e6).toString(36),
|
|
694
|
+
sessionTTL: parseInt(this.env('SESSION_TTL')) || 2592000/*30days*/
|
|
695
|
+
};
|
|
776
696
|
if (typeof scopeObj.url === 'string') {
|
|
777
697
|
scopeObj.url = new URL(scopeObj.url, 'http://localhost');
|
|
778
698
|
}
|
|
779
|
-
|
|
699
|
+
|
|
700
|
+
// Request
|
|
780
701
|
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
|
-
|
|
702
|
+
scopeObj.request = scopeObj.init instanceof Request && scopeObj.init.url === scopeObj.url.href
|
|
703
|
+
? scopeObj.init
|
|
704
|
+
: this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
|
|
705
|
+
RequestPlus.upgradeInPlace(scopeObj.request);
|
|
706
|
+
scopeObj.tenantID = this.identifyIncoming(scopeObj.request, true);
|
|
707
|
+
|
|
708
|
+
// Origins
|
|
709
|
+
const origins = [scopeObj.requestID];
|
|
710
|
+
|
|
711
|
+
// Thread
|
|
712
|
+
scopeObj.thread = HttpThread111.create({
|
|
713
|
+
context: {},
|
|
714
|
+
store: this.#keyvals.create({ path: ['thread', scopeObj.tenantID], origins }),
|
|
789
715
|
threadID: scopeObj.url.searchParams.get('_thread'),
|
|
790
716
|
realm: 3
|
|
791
717
|
});
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
718
|
+
|
|
719
|
+
// Cookies
|
|
720
|
+
const entries = scopeObj.request.headers.get('Cookie', true).map((c) => [c.name, c]);
|
|
721
|
+
const store = InMemoryKV.create({ path: ['cookies', scopeObj.tenantID] });
|
|
722
|
+
entries.forEach(([key, value]) => store.set(key, { value }));
|
|
723
|
+
const initial = Object.fromEntries(entries);
|
|
724
|
+
scopeObj.cookies = HttpCookies101.create({
|
|
725
|
+
context: { handlersRegistry: this.#keyvals.getHandlers('cookies', true) },
|
|
726
|
+
store,
|
|
727
|
+
initial,
|
|
795
728
|
realm: 3
|
|
796
729
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
730
|
+
|
|
731
|
+
// Session
|
|
732
|
+
scopeObj.session = HttpSession001.create({
|
|
733
|
+
context: { handlersRegistry: this.#keyvals.getHandlers('session', true) },
|
|
734
|
+
store: this.#keyvals.create({ path: ['session', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
|
|
735
|
+
sessionID: scopeObj.tenantID,
|
|
802
736
|
ttl: scopeObj.sessionTTL,
|
|
803
737
|
realm: 3
|
|
804
738
|
});
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
739
|
+
|
|
740
|
+
// User
|
|
741
|
+
scopeObj.user = HttpUser111.create({
|
|
742
|
+
context: { handlersRegistry: this.#keyvals.getHandlers('user', true) },
|
|
743
|
+
store: this.#keyvals.create({ path: ['user', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
|
|
810
744
|
realm: 3
|
|
811
745
|
});
|
|
812
|
-
|
|
746
|
+
|
|
747
|
+
// Client
|
|
748
|
+
scopeObj.tenant = this.#tenancy.getTenant(scopeObj.tenantID, true);
|
|
749
|
+
scopeObj.clientRequestPort = scopeObj.tenant.createRequestPort(
|
|
750
|
+
crypto.randomUUID(),
|
|
751
|
+
scopeObj.request.url
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
// HttpEvent
|
|
755
|
+
scopeObj.httpEvent = HttpEvent111.create({
|
|
756
|
+
detail: scopeObj.detail,
|
|
757
|
+
signal: init.signal,
|
|
813
758
|
request: scopeObj.request,
|
|
814
759
|
thread: scopeObj.thread,
|
|
815
|
-
client: scopeObj.clientRequestRealtime,
|
|
816
760
|
cookies: scopeObj.cookies,
|
|
817
761
|
session: scopeObj.session,
|
|
818
762
|
user: scopeObj.user,
|
|
819
|
-
|
|
763
|
+
client: scopeObj.clientRequestPort,
|
|
820
764
|
realm: 3
|
|
821
765
|
});
|
|
766
|
+
|
|
822
767
|
// Dispatch for response
|
|
823
768
|
scopeObj.response = await this.dispatchNavigationEvent({
|
|
824
769
|
httpEvent: scopeObj.httpEvent,
|
|
825
770
|
crossLayerFetch: (event) => this.localFetch(event),
|
|
826
|
-
clientPortB: `
|
|
771
|
+
clientPortB: `socket:///${scopeObj.httpEvent.client.portID}?rel=background-messaging`
|
|
827
772
|
});
|
|
773
|
+
|
|
774
|
+
// Commit session - expires six months
|
|
775
|
+
if (!scopeObj.response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
|
|
776
|
+
scopeObj.response.headers.append('Set-Cookie', `__sessid=${scopeObj.tenantID}; Path=/; ${!FLAGS['dev'] ? 'Secure; ' : ''}HttpOnly; SameSite=Lax${scopeObj.sessionTTL ? `; Max-Age=${scopeObj.sessionTTL}` : ''}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Commit cookies
|
|
780
|
+
for (const cookieStr of await scopeObj.cookies.render()) {
|
|
781
|
+
scopeObj.response.headers.append('Set-Cookie', cookieStr);
|
|
782
|
+
}
|
|
783
|
+
await scopeObj.cookies._commit();
|
|
784
|
+
|
|
828
785
|
// Reponse handlers
|
|
829
786
|
if (FLAGS['dev']) {
|
|
830
787
|
scopeObj.response.headers.set('X-Webflo-Dev-Mode', 'true'); // Must come before satisfyRequestFormat() sp as to be rendered
|
|
831
788
|
}
|
|
789
|
+
|
|
790
|
+
// Write headers / satisfy request format
|
|
832
791
|
if (scopeObj.response.headers.get('Location')) {
|
|
833
792
|
this.writeRedirectHeaders(scopeObj.httpEvent, scopeObj.response);
|
|
834
793
|
} else {
|
|
@@ -838,74 +797,90 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
838
797
|
scopeObj.response.headers.set('Cache-Control', 'no-store');
|
|
839
798
|
}
|
|
840
799
|
}
|
|
800
|
+
|
|
841
801
|
return scopeObj.response;
|
|
842
802
|
}
|
|
843
803
|
|
|
844
804
|
async satisfyRequestFormat(httpEvent, response) {
|
|
845
|
-
const statusCode =
|
|
805
|
+
const statusCode = response.status;
|
|
806
|
+
|
|
846
807
|
if (statusCode === 206 || statusCode === 416) {
|
|
847
808
|
// If the response is a partial content, we don't need to do anything else
|
|
848
809
|
return response;
|
|
849
810
|
}
|
|
811
|
+
|
|
850
812
|
// Satisfy "Accept" header
|
|
851
|
-
const requestAccept =
|
|
813
|
+
const requestAccept = httpEvent.request.headers.get('Accept', true);
|
|
852
814
|
const asHTML = requestAccept?.match('text/html');
|
|
853
815
|
const asIs = requestAccept?.match(response.headers.get('Content-Type'));
|
|
854
|
-
const responseMeta =
|
|
816
|
+
const responseMeta = _meta(response);
|
|
817
|
+
|
|
855
818
|
if (requestAccept && asHTML > asIs && !responseMeta.get('static')) {
|
|
856
819
|
response = await this.render(httpEvent, response);
|
|
857
|
-
} else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
|
|
820
|
+
} else if (requestAccept && response.body && response.headers.get('Content-Type') && !asIs) {
|
|
858
821
|
return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
|
|
859
822
|
}
|
|
823
|
+
|
|
860
824
|
// ------- With "exception" responses out of the way,
|
|
861
825
|
// let's set the header that talks to our support for "Accept"
|
|
862
826
|
if (!responseMeta.get('static')) {
|
|
863
827
|
response.headers.append('Vary', 'Accept');
|
|
864
828
|
}
|
|
829
|
+
|
|
865
830
|
// Satisfy "Range" header
|
|
866
|
-
const requestRange =
|
|
867
|
-
if (requestRange.length && response.headers.get('Content-Length')) {
|
|
831
|
+
const requestRange = httpEvent.request.headers.get('Range', true);
|
|
832
|
+
if (requestRange.length && response.body && response.headers.get('Content-Length')) {
|
|
868
833
|
const stats = {
|
|
869
834
|
size: parseInt(response.headers.get('Content-Length')),
|
|
870
835
|
mime: response.headers.get('Content-Type') || 'application/octet-stream',
|
|
871
836
|
};
|
|
837
|
+
|
|
872
838
|
const headersBefore = response.headers;
|
|
873
839
|
response = this.createStreamingResponse(
|
|
874
840
|
httpEvent,
|
|
875
841
|
(params) => this.streamSlice(response.body, { ...params }),
|
|
876
842
|
stats
|
|
877
843
|
);
|
|
844
|
+
|
|
878
845
|
for (const [name, value] of headersBefore) {
|
|
879
846
|
if (/Content-Length|Content-Type/i.test(name)) continue;
|
|
880
847
|
response.headers.append(name, value);
|
|
881
848
|
}
|
|
882
849
|
}
|
|
850
|
+
|
|
883
851
|
return response;
|
|
884
852
|
}
|
|
885
853
|
|
|
886
854
|
async render(httpEvent, response) {
|
|
887
855
|
const { LAYOUT } = this.config;
|
|
888
856
|
const scopeObj = {};
|
|
889
|
-
|
|
857
|
+
|
|
858
|
+
scopeObj.router = new WebfloRouter111(this, httpEvent.url.pathname);
|
|
890
859
|
scopeObj.rendering = await scopeObj.router.route('render', httpEvent, async (httpEvent) => {
|
|
891
860
|
let renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
|
|
861
|
+
|
|
892
862
|
while ((renderFile = Path.join(LAYOUT.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
|
|
893
863
|
&& (this.#renderFileCache.get(renderFile) === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
|
|
894
864
|
this.#renderFileCache.set(renderFile, false);
|
|
895
865
|
pathnameSplit.pop();
|
|
896
866
|
}
|
|
867
|
+
|
|
897
868
|
const dirPublic = Url.pathToFileURL(Path.resolve(Path.join(LAYOUT.PUBLIC_DIR)));
|
|
898
869
|
const instanceParams = /*QueryString.stringify*/({
|
|
899
870
|
//file: renderFile,
|
|
900
871
|
url: dirPublic.href,// httpEvent.url.href,
|
|
901
872
|
});
|
|
873
|
+
|
|
902
874
|
const { window, document } = createWindow(renderFile, instanceParams);
|
|
875
|
+
|
|
903
876
|
//const { window, document } = await import('@webqit/oohtml-ssr/src/instance.js?' + instanceParams);
|
|
904
877
|
await new Promise((res) => {
|
|
905
878
|
if (document.readyState === 'complete') return res(1);
|
|
906
879
|
document.addEventListener('load', res);
|
|
907
880
|
});
|
|
908
|
-
|
|
881
|
+
|
|
882
|
+
const data = await response.any({ to: 'json' });
|
|
883
|
+
|
|
909
884
|
if (window.webqit?.oohtml?.config) {
|
|
910
885
|
// Await rendering engine
|
|
911
886
|
if (window.webqit?.$qCompilerWorker) {
|
|
@@ -915,14 +890,17 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
915
890
|
setTimeout(() => res(1), 1000);
|
|
916
891
|
});
|
|
917
892
|
}
|
|
893
|
+
|
|
918
894
|
const {
|
|
919
895
|
HTML_IMPORTS: { attr: modulesContextAttrs } = {},
|
|
920
896
|
BINDINGS_API: { api: bindingsConfig } = {},
|
|
921
897
|
} = window.webqit.oohtml.config;
|
|
898
|
+
|
|
922
899
|
if (modulesContextAttrs) {
|
|
923
900
|
const newRoute = '/' + `app/${httpEvent.url.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
|
|
924
901
|
document.body.setAttribute(modulesContextAttrs.importscontext, newRoute);
|
|
925
902
|
}
|
|
903
|
+
|
|
926
904
|
if (bindingsConfig) {
|
|
927
905
|
document[bindingsConfig.bind]({
|
|
928
906
|
state: {},
|
|
@@ -935,9 +913,11 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
935
913
|
background: null
|
|
936
914
|
}, { diff: true });
|
|
937
915
|
}
|
|
916
|
+
|
|
938
917
|
await new Promise(res => setTimeout(res, 300));
|
|
939
918
|
}
|
|
940
|
-
|
|
919
|
+
|
|
920
|
+
for (const name of ['X-Message-Port', 'X-Webflo-Dev-Mode']) {
|
|
941
921
|
document.querySelector(`meta[name="${name}"]`)?.remove();
|
|
942
922
|
if (!response.headers.get(name)) continue;
|
|
943
923
|
const metaElement = document.createElement('meta');
|
|
@@ -945,6 +925,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
945
925
|
metaElement.setAttribute('content', response.headers.get(name));
|
|
946
926
|
document.head.prepend(metaElement);
|
|
947
927
|
}
|
|
928
|
+
|
|
948
929
|
// Append hydration data
|
|
949
930
|
for (const [rel, content] of [['hydration', data]]) {
|
|
950
931
|
document.querySelector(`script[rel="${rel}"][type="application/json"]`)?.remove();
|
|
@@ -954,18 +935,23 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
954
935
|
dataScript.textContent = JSON.stringify(content);
|
|
955
936
|
document.body.append(dataScript);
|
|
956
937
|
}
|
|
938
|
+
|
|
957
939
|
const rendering = window.toString();
|
|
958
940
|
document.documentElement.remove();
|
|
959
941
|
document.writeln('');
|
|
942
|
+
|
|
960
943
|
try { window.close(); } catch (e) { }
|
|
944
|
+
|
|
961
945
|
return rendering;
|
|
962
946
|
});
|
|
947
|
+
|
|
963
948
|
// Validate rendering
|
|
964
949
|
if (typeof scopeObj.rendering !== 'string' && !(typeof scopeObj.rendering?.toString === 'function')) {
|
|
965
950
|
throw new Error('render() must return a string response or an object that implements toString()..');
|
|
966
951
|
}
|
|
952
|
+
|
|
967
953
|
// Convert back to response
|
|
968
|
-
const statusCode =
|
|
954
|
+
const statusCode = response.status;
|
|
969
955
|
scopeObj.response = new Response(scopeObj.rendering, {
|
|
970
956
|
headers: response.headers,
|
|
971
957
|
status: statusCode,
|
|
@@ -973,32 +959,42 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
973
959
|
});
|
|
974
960
|
scopeObj.response.headers.set('Content-Type', 'text/html');
|
|
975
961
|
scopeObj.response.headers.set('Content-Length', (new Blob([scopeObj.rendering])).size);
|
|
962
|
+
|
|
976
963
|
return scopeObj.response;
|
|
977
964
|
}
|
|
978
965
|
|
|
979
966
|
generateLog(request, response, isproxy = false) {
|
|
980
967
|
const { logger: LOGGER } = this.cx;
|
|
981
968
|
const log = [];
|
|
969
|
+
|
|
982
970
|
// ---------------
|
|
983
971
|
const style = LOGGER.style || { keyword: (str) => str, comment: (str) => str, url: (str) => str, val: (str) => str, err: (str) => str, };
|
|
984
|
-
const statusCode =
|
|
972
|
+
const statusCode = response.status;
|
|
985
973
|
const errorCode = statusCode >= 400 && statusCode < 500 ? statusCode : 0;
|
|
986
974
|
const xRedirectCode = response.headers.get('X-Redirect-Code');
|
|
987
975
|
const isRedirect = (xRedirectCode || statusCode + '').startsWith('3') && (xRedirectCode || statusCode) !== 304;
|
|
988
976
|
const _statusCode = xRedirectCode && `${xRedirectCode} (${statusCode})` || statusCode;
|
|
989
|
-
const responseMeta =
|
|
977
|
+
const responseMeta = _meta(response);
|
|
990
978
|
// ---------------
|
|
979
|
+
|
|
991
980
|
log.push(`[${style.comment((new Date).toUTCString())}]`);
|
|
992
981
|
log.push(style.keyword(request.method));
|
|
993
982
|
if (isproxy) log.push(style.keyword('>>'));
|
|
994
983
|
log.push(style.url(request.url));
|
|
984
|
+
|
|
995
985
|
if (responseMeta.has('hint')) log.push(`(${style.comment(responseMeta.get('hint'))})`);
|
|
996
986
|
const contentInfo = [response.headers.get('Content-Type'), response.headers.get('Content-Length') && this.formatBytes(response.headers.get('Content-Length'))].filter((x) => x);
|
|
987
|
+
|
|
997
988
|
if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
|
|
998
989
|
if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
|
|
999
990
|
if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
|
|
1000
991
|
else log.push(style.val(`${_statusCode} ${response.statusText}`));
|
|
992
|
+
if (response.headers.get('X-Message-Port')) {
|
|
993
|
+
log.push(style.keyword(`[${style.keyword('L')}]`));
|
|
994
|
+
}
|
|
995
|
+
|
|
1001
996
|
if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
|
|
997
|
+
|
|
1002
998
|
return log.join(' ');
|
|
1003
999
|
}
|
|
1004
1000
|
|