kreftjs 0.0.20-beta → 0.0.22-beta
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/README.md +10 -1
- package/bin/artisan.js +4 -2
- package/bin/backend/cache.js +31 -5
- package/bin/backend/gc.js +193 -43
- package/bin/backend/http.js +6 -2
- package/bin/backend/index.js +6 -0
- package/bin/backend/mcp-client.js +133 -0
- package/bin/backend/mcp.js +17 -0
- package/bin/backend/model.js +15 -1
- package/bin/backend/shield.js +266 -0
- package/bin/backend/stream.js +74 -0
- package/bin/build.js +12 -5
- package/bin/db-api.js +9 -1
- package/bin/kreftjs.js +50 -12
- package/book/ar/16-virtual-list.md +131 -0
- package/book/ar/17-combobox.md +169 -0
- package/book/ar/18-tabs-offline.md +168 -0
- package/book/ar/19-wizard.md +196 -0
- package/book/ar/20-pipes-match-props.md +75 -0
- package/book/ar/README.md +12 -1
- package/book/kreft-book-ar.docx +0 -0
- package/docs/README.md +209 -13
- package/editors/sublime/Comments.tmPreferences +26 -0
- package/editors/sublime/Component.sublime-snippet +20 -0
- package/editors/sublime/Kreft.sublime-syntax +39 -0
- package/editors/sublime/LICENSE +95 -0
- package/editors/sublime/README.md +7 -0
- package/editors/sublime/action.sublime-snippet +10 -0
- package/editors/sublime/derive.sublime-snippet +8 -0
- package/editors/sublime/for.sublime-snippet +12 -0
- package/editors/sublime/k-case.sublime-snippet +8 -0
- package/editors/sublime/k-if.sublime-snippet +8 -0
- package/editors/sublime/k-match.sublime-snippet +12 -0
- package/editors/sublime/kreft-wait.sublime-snippet +12 -0
- package/editors/sublime/layout.sublime-snippet +8 -0
- package/editors/sublime/load.sublime-snippet +8 -0
- package/editors/sublime/on.sublime-snippet +10 -0
- package/editors/sublime/pipe.sublime-snippet +8 -0
- package/editors/sublime/prop-typed.sublime-snippet +8 -0
- package/editors/sublime/saved.sublime-snippet +8 -0
- package/editors/sublime/section.sublime-snippet +10 -0
- package/editors/sublime/state.sublime-snippet +8 -0
- package/editors/sublime/watch.sublime-snippet +10 -0
- package/editors/sublime/when.sublime-snippet +10 -0
- package/editors/sublime/yield.sublime-snippet +8 -0
- package/editors/vscode/snippets/kreftjs.json +143 -15
- package/editors/zed/LICENSE +95 -0
- package/editors/zed/README.md +17 -0
- package/editors/zed/extension.toml +11 -0
- package/editors/zed/languages/kreft/brackets.scm +4 -0
- package/editors/zed/languages/kreft/config.toml +17 -0
- package/editors/zed/languages/kreft/highlights.scm +18 -0
- package/editors/zed/languages/kreft/indents.scm +5 -0
- package/editors/zed/languages/kreft/injections.scm +14 -0
- package/editors/zed/languages/kreft/outline.scm +3 -0
- package/kreftjs.d.ts +1 -1
- package/kreftjs.js +219 -14
- package/package.json +3 -2
- package/server.d.ts +44 -0
- package/templates/app/backend/app/Auth/Session.js +8 -4
- package/templates/app/backend/app/Http/Middleware/Authenticate.js +7 -4
- package/templates/app/backend/database/seeders/DatabaseSeeder.js +4 -0
- package/templates/app/frontend/kreftjs.d.ts +1 -1
- package/templates/app/frontend/kreftjs.js +219 -14
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Kreft Shield: automatic protection against floods and abuse from a single address.
|
|
3
|
+
*
|
|
4
|
+
* SHIELD=true in .env turns it on (off by default). Changes to .env apply without a restart.
|
|
5
|
+
*
|
|
6
|
+
* What it does, per visitor address (the same req.ip() that throttle uses, so TRUSTED_PROXIES applies):
|
|
7
|
+
* 1. Rate limit: more than SHIELD_MAX_REQUESTS (300) in SHIELD_WINDOW_SEC (10) seconds → 429 + Retry-After.
|
|
8
|
+
* Responses that look like probing (401, 403, 404, 405) count SHIELD_ERROR_WEIGHT (5) times, so scanners
|
|
9
|
+
* and password guessers hit the limit sooner than normal visitors.
|
|
10
|
+
* 2. Concurrency: more than SHIELD_MAX_CONCURRENT (60) requests of one address in flight at once → 429.
|
|
11
|
+
* 3. Ban: SHIELD_STRIKES (3) rate-limit hits within SHIELD_STRIKE_WINDOW_MIN (10) minutes → the address is
|
|
12
|
+
* refused for SHIELD_BAN_MINUTES (15), doubling at every repeat (up to 24 hours). Banned requests are
|
|
13
|
+
* answered with a bare 429 before any work is done.
|
|
14
|
+
* 4. Connections: more than SHIELD_MAX_CONNECTIONS (100) open sockets from one address are closed at once, which
|
|
15
|
+
* stops slow-connection floods (slowloris) that never finish a request. Needs Shield.watch(server) (kreft does it).
|
|
16
|
+
* 5. Overload: when the event loop lags by more than SHIELD_MAX_LAG_MS (300), limits shrink to a quarter
|
|
17
|
+
* so the server keeps answering instead of falling over.
|
|
18
|
+
*
|
|
19
|
+
* Never limited: SHIELD_ALLOW="1.2.3.4,5.6.7.8" and, unless SHIELD_LOCAL=true, this computer (127.0.0.1, ::1).
|
|
20
|
+
* Bans live in memory: a restart clears them. For a flood bigger than one process can absorb, put a firewall
|
|
21
|
+
* or CDN in front; the Shield protects the application, it does not replace network-level DDoS protection.
|
|
22
|
+
*
|
|
23
|
+
* const { Shield } = require('kreftjs/server')
|
|
24
|
+
* Shield.bans() Shield.unban('1.2.3.4') Shield.ban('1.2.3.4', 60) Shield.status() Shield.on('ban', fn)
|
|
25
|
+
*/
|
|
26
|
+
const { EventEmitter } = require('events');
|
|
27
|
+
|
|
28
|
+
const events = new EventEmitter();
|
|
29
|
+
const ips = new Map(); // address → { windowStart, count, struck, strikes: [ms], level, lastBanAt, bannedUntil, inflight, last }
|
|
30
|
+
const MAX_ENTRIES = 100000;
|
|
31
|
+
const DAY = 86400000;
|
|
32
|
+
|
|
33
|
+
let cfgAt = 0;
|
|
34
|
+
let cfg = null;
|
|
35
|
+
let lagMonitor = null;
|
|
36
|
+
let lagMs = 0;
|
|
37
|
+
let timers = [];
|
|
38
|
+
let warnedProxy = false;
|
|
39
|
+
const conns = new Map(); // direct socket address → open connections
|
|
40
|
+
const stats = { blocked: 0, limited: 0, bans: 0 };
|
|
41
|
+
|
|
42
|
+
const num = (v, d) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : d);
|
|
43
|
+
const flag = (v) => ['true', '1', 'on', 'yes'].includes(String(v || '').trim().toLowerCase());
|
|
44
|
+
|
|
45
|
+
function config() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (cfg && now - cfgAt < 2000) return cfg;
|
|
48
|
+
const e = process.env;
|
|
49
|
+
cfg = {
|
|
50
|
+
enabled: flag(e.SHIELD),
|
|
51
|
+
maxRequests: num(e.SHIELD_MAX_REQUESTS, 300),
|
|
52
|
+
windowMs: num(e.SHIELD_WINDOW_SEC, 10) * 1000,
|
|
53
|
+
errorWeight: num(e.SHIELD_ERROR_WEIGHT, 5),
|
|
54
|
+
maxConcurrent: num(e.SHIELD_MAX_CONCURRENT, 60),
|
|
55
|
+
maxConnections: num(e.SHIELD_MAX_CONNECTIONS, 100),
|
|
56
|
+
strikes: num(e.SHIELD_STRIKES, 3),
|
|
57
|
+
strikeWindowMs: num(e.SHIELD_STRIKE_WINDOW_MIN, 10) * 60000,
|
|
58
|
+
banMs: num(e.SHIELD_BAN_MINUTES, 15) * 60000,
|
|
59
|
+
maxLagMs: num(e.SHIELD_MAX_LAG_MS, 300),
|
|
60
|
+
local: flag(e.SHIELD_LOCAL),
|
|
61
|
+
allow: new Set(String(e.SHIELD_ALLOW || '').split(',').map((x) => x.trim().replace(/^::ffff:/, '')).filter(Boolean)),
|
|
62
|
+
};
|
|
63
|
+
cfgAt = now;
|
|
64
|
+
return cfg;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isLoopback = (ip) => ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
68
|
+
const normalize = (ip) => String(ip || '').replace(/^::ffff:/, '');
|
|
69
|
+
|
|
70
|
+
// One IPv6 customer owns a whole /64 (billions of addresses): counting single addresses would let an attacker
|
|
71
|
+
// rotate through them and never be limited. IPv6 visitors are counted by their /64 prefix, IPv4 by address.
|
|
72
|
+
function keyOf(ip) {
|
|
73
|
+
if (!ip.includes(':')) return ip;
|
|
74
|
+
const s = ip.split('%')[0];
|
|
75
|
+
if (s.includes('.')) return ip; // an embedded IPv4 form: leave as is
|
|
76
|
+
const dbl = s.includes('::');
|
|
77
|
+
const [h, t] = s.split('::');
|
|
78
|
+
const head = h ? h.split(':') : [];
|
|
79
|
+
const tail = dbl && t ? t.split(':') : [];
|
|
80
|
+
const fill = dbl ? 8 - head.length - tail.length : 0;
|
|
81
|
+
if (fill < 0 || (!dbl && head.length !== 8)) return ip;
|
|
82
|
+
const groups = [...head, ...Array(fill).fill('0'), ...tail].map((x) => parseInt(x, 16));
|
|
83
|
+
if (groups.length !== 8 || groups.some((x) => Number.isNaN(x))) return ip;
|
|
84
|
+
return groups.slice(0, 4).map((x) => x.toString(16)).join(':') + '::/64';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function entry(ip, now) {
|
|
88
|
+
let e = ips.get(ip);
|
|
89
|
+
if (!e) {
|
|
90
|
+
if (ips.size >= MAX_ENTRIES) prune(true);
|
|
91
|
+
e = { windowStart: now, count: 0, struck: false, strikes: [], level: 0, lastBanAt: 0, bannedUntil: 0, inflight: 0, last: now };
|
|
92
|
+
ips.set(ip, e);
|
|
93
|
+
}
|
|
94
|
+
e.last = now;
|
|
95
|
+
return e;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Drops addresses that are idle and not banned. `hard` also drops the oldest when the table is full.
|
|
99
|
+
function prune(hard = false) {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const [ip, e] of ips) {
|
|
102
|
+
if (e.bannedUntil > now || e.inflight > 0) continue;
|
|
103
|
+
if (now - e.last > 10 * 60000 && (!e.lastBanAt || now - e.lastBanAt > DAY)) ips.delete(ip);
|
|
104
|
+
}
|
|
105
|
+
if (hard && ips.size >= MAX_ENTRIES) {
|
|
106
|
+
for (const [ip, e] of ips) { if (e.bannedUntil <= now && e.inflight === 0) { ips.delete(ip); if (ips.size < MAX_ENTRIES * 0.9) break; } }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureRunning() {
|
|
111
|
+
if (timers.length) return;
|
|
112
|
+
try {
|
|
113
|
+
const { monitorEventLoopDelay } = require('perf_hooks');
|
|
114
|
+
lagMonitor = monitorEventLoopDelay({ resolution: 20 });
|
|
115
|
+
lagMonitor.enable();
|
|
116
|
+
} catch (_) { lagMonitor = null; }
|
|
117
|
+
const t1 = setInterval(() => {
|
|
118
|
+
if (lagMonitor) { lagMs = lagMonitor.percentile(99) / 1e6; lagMonitor.reset(); }
|
|
119
|
+
const c = config();
|
|
120
|
+
if (c.enabled && lagMs > c.maxLagMs && !ensureRunning.warned) {
|
|
121
|
+
ensureRunning.warned = true;
|
|
122
|
+
console.warn(` [Kreft Shield] The server is overloaded (event loop lag ${Math.round(lagMs)} ms): per-address limits are cut to a quarter.`);
|
|
123
|
+
} else if (lagMs <= c.maxLagMs) ensureRunning.warned = false;
|
|
124
|
+
}, 5000);
|
|
125
|
+
const t2 = setInterval(() => prune(), 60000);
|
|
126
|
+
timers = [t1, t2];
|
|
127
|
+
timers.forEach((t) => t.unref && t.unref());
|
|
128
|
+
try { require('./gc.js').onMemoryPressure('shield', () => { const before = ips.size; prune(true); return before - ips.size; }); } catch (_) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function banEntry(ip, e, ms, reason) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
e.bannedUntil = now + ms;
|
|
134
|
+
e.lastBanAt = now;
|
|
135
|
+
e.level++;
|
|
136
|
+
e.strikes = [];
|
|
137
|
+
stats.bans++;
|
|
138
|
+
const info = { ip, minutes: Math.round(ms / 60000), reason, until: new Date(e.bannedUntil).toISOString(), level: e.level };
|
|
139
|
+
console.warn(` [Kreft Shield] Banned ${ip} for ${info.minutes} min (${reason}).`);
|
|
140
|
+
events.emit('ban', info);
|
|
141
|
+
return info;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function refuse(res, seconds, why) {
|
|
145
|
+
stats.blocked++;
|
|
146
|
+
try {
|
|
147
|
+
res.writeHead(429, { 'Retry-After': String(Math.max(1, Math.ceil(seconds))), 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store', Connection: 'close' });
|
|
148
|
+
res.end(why);
|
|
149
|
+
} catch (_) { try { res.destroy(); } catch (__) {} }
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Call first for every request. Returns true when it already answered (the request must stop there).
|
|
155
|
+
*/
|
|
156
|
+
function guard(req, res) {
|
|
157
|
+
const c = config();
|
|
158
|
+
if (!c.enabled) return false;
|
|
159
|
+
ensureRunning();
|
|
160
|
+
let ip;
|
|
161
|
+
try { ip = normalize(require('./http.js').Request.prototype.ip.call({ raw: req })); } catch (_) { ip = normalize(req.socket && req.socket.remoteAddress); }
|
|
162
|
+
if (!ip) return false;
|
|
163
|
+
// Behind a proxy that is not in TRUSTED_PROXIES every visitor looks like the proxy: one address (all users would
|
|
164
|
+
// share one limit and one ban) or, for a proxy on this computer, loopback (which is never limited). Say so once.
|
|
165
|
+
if (!warnedProxy && req.headers['x-forwarded-for'] && ip === normalize(req.socket && req.socket.remoteAddress)) {
|
|
166
|
+
warnedProxy = true;
|
|
167
|
+
console.warn(` [Kreft Shield] Requests carry X-Forwarded-For, but ${ip} is not in TRUSTED_PROXIES, so the real visitor address is unknown and the Shield cannot tell visitors apart. Behind nginx, Caddy or a CDN, set TRUSTED_PROXIES in .env.`);
|
|
168
|
+
}
|
|
169
|
+
if (c.allow.has(ip) || (isLoopback(ip) && !c.local)) return false;
|
|
170
|
+
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const key = keyOf(ip);
|
|
173
|
+
const e = entry(key, now);
|
|
174
|
+
|
|
175
|
+
if (e.bannedUntil > now) return refuse(res, (e.bannedUntil - now) / 1000, 'Too many requests. Try again later.');
|
|
176
|
+
if (e.bannedUntil && e.bannedUntil <= now) e.bannedUntil = 0;
|
|
177
|
+
if (e.level && e.lastBanAt && now - e.lastBanAt > DAY) e.level = 0;
|
|
178
|
+
|
|
179
|
+
if (now - e.windowStart >= c.windowMs) { e.windowStart = now; e.count = 0; e.struck = false; }
|
|
180
|
+
e.strikes = e.strikes.filter((t) => now - t < c.strikeWindowMs);
|
|
181
|
+
|
|
182
|
+
const overloaded = lagMs > c.maxLagMs;
|
|
183
|
+
const limit = Math.max(5, Math.floor(c.maxRequests / (overloaded ? 4 : 1)));
|
|
184
|
+
const concurrent = Math.max(3, Math.floor(c.maxConcurrent / (overloaded ? 4 : 1)));
|
|
185
|
+
e.count++;
|
|
186
|
+
|
|
187
|
+
const overRate = e.count > limit;
|
|
188
|
+
const overConc = e.inflight >= concurrent;
|
|
189
|
+
if (overRate || overConc) {
|
|
190
|
+
stats.limited++;
|
|
191
|
+
if (!e.struck) {
|
|
192
|
+
e.struck = true;
|
|
193
|
+
e.strikes.push(now);
|
|
194
|
+
if (e.strikes.length >= c.strikes) {
|
|
195
|
+
const ms = Math.min(DAY, c.banMs * 2 ** e.level);
|
|
196
|
+
banEntry(key, e, ms, `${e.strikes.length} rate-limit hits in ${Math.round(c.strikeWindowMs / 60000)} min`);
|
|
197
|
+
return refuse(res, ms / 1000, 'Too many requests. Try again later.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return refuse(res, (e.windowStart + c.windowMs - now) / 1000, 'Too many requests. Slow down.');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
e.inflight++;
|
|
204
|
+
let done = false;
|
|
205
|
+
const finish = () => { if (done) return; done = true; e.inflight = Math.max(0, e.inflight - 1); };
|
|
206
|
+
res.once('close', finish);
|
|
207
|
+
res.once('finish', () => {
|
|
208
|
+
// probing (wrong login, missing pages, forbidden) costs more, so scanners trip the limit early
|
|
209
|
+
const s = res.statusCode;
|
|
210
|
+
if (s === 401 || s === 403 || s === 404 || s === 405) e.count += c.errorWeight - 1;
|
|
211
|
+
finish();
|
|
212
|
+
});
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const Shield = {
|
|
217
|
+
guard,
|
|
218
|
+
enabled: () => config().enabled,
|
|
219
|
+
on: (event, fn) => { events.on(event, fn); return Shield; },
|
|
220
|
+
off: (event, fn) => { events.off(event, fn); return Shield; },
|
|
221
|
+
/**
|
|
222
|
+
* Caps open connections per direct address (call once on the http.Server). Connections from this computer or from a
|
|
223
|
+
* proxy in TRUSTED_PROXIES are not capped here: they carry many visitors, and the request limits tell those apart.
|
|
224
|
+
*/
|
|
225
|
+
watch(server) {
|
|
226
|
+
server.on('connection', (sock) => {
|
|
227
|
+
const c = config();
|
|
228
|
+
if (!c.enabled) return;
|
|
229
|
+
const ip = normalize(sock.remoteAddress);
|
|
230
|
+
const trusted = String(process.env.TRUSTED_PROXIES || '').split(',').map((x) => normalize(x.trim())).filter(Boolean);
|
|
231
|
+
if (!ip || c.allow.has(ip) || (isLoopback(ip) && !c.local) || trusted.includes('*') || trusted.includes(ip)) return;
|
|
232
|
+
const key = keyOf(ip);
|
|
233
|
+
const n = (conns.get(key) || 0) + 1;
|
|
234
|
+
if (n > c.maxConnections) { stats.blocked++; sock.destroy(); return; }
|
|
235
|
+
conns.set(key, n);
|
|
236
|
+
sock.once('close', () => { const m = (conns.get(key) || 1) - 1; if (m <= 0) conns.delete(key); else conns.set(key, m); });
|
|
237
|
+
});
|
|
238
|
+
return Shield;
|
|
239
|
+
},
|
|
240
|
+
/** Currently banned addresses. */
|
|
241
|
+
bans() {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const [ip, e] of ips) if (e.bannedUntil > now) out.push({ ip, until: new Date(e.bannedUntil).toISOString(), level: e.level, secondsLeft: Math.ceil((e.bannedUntil - now) / 1000) });
|
|
245
|
+
return out;
|
|
246
|
+
},
|
|
247
|
+
ban(ip, minutes = 60, reason = 'manual') { const a = keyOf(normalize(ip)); return banEntry(a, entry(a, Date.now()), minutes * 60000, reason); },
|
|
248
|
+
unban(ip) {
|
|
249
|
+
const e = ips.get(keyOf(normalize(ip)));
|
|
250
|
+
if (!e) return false;
|
|
251
|
+
e.bannedUntil = 0; e.level = 0; e.strikes = []; e.count = 0; e.struck = false;
|
|
252
|
+
return true;
|
|
253
|
+
},
|
|
254
|
+
status() {
|
|
255
|
+
const c = config();
|
|
256
|
+
return { enabled: c.enabled, tracked: ips.size, banned: Shield.bans().length, ...stats, eventLoopLagMs: Math.round(lagMs), overloaded: lagMs > c.maxLagMs, limits: { maxRequests: c.maxRequests, windowSec: c.windowMs / 1000, maxConcurrent: c.maxConcurrent, strikes: c.strikes, banMinutes: c.banMs / 60000 } };
|
|
257
|
+
},
|
|
258
|
+
/** For tests: forget everything and stop the timers. */
|
|
259
|
+
reset() {
|
|
260
|
+
ips.clear(); conns.clear(); warnedProxy = false; cfg = null; lagMs = 0; stats.blocked = stats.limited = stats.bans = 0;
|
|
261
|
+
timers.forEach(clearInterval); timers = [];
|
|
262
|
+
if (lagMonitor) { try { lagMonitor.disable(); } catch (_) {} lagMonitor = null; }
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
module.exports = { Shield, _keyOf: keyOf };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Sending a file the way a video player needs it: byte ranges (206), so the browser can seek and Safari plays at all.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const MEDIA_TYPES = {
|
|
6
|
+
'.mp4': 'video/mp4', '.m4v': 'video/mp4', '.webm': 'video/webm', '.ogv': 'video/ogg', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska',
|
|
7
|
+
'.mp3': 'audio/mpeg', '.m4a': 'audio/mp4', '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.oga': 'audio/ogg', '.wav': 'audio/wav', '.opus': 'audio/ogg', '.flac': 'audio/flac',
|
|
8
|
+
'.m3u8': 'application/vnd.apple.mpegurl', '.ts': 'video/mp2t', '.m4s': 'video/iso.segment', '.mpd': 'application/dash+xml', '.vtt': 'text/vtt',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// "bytes=0-99", "bytes=100-", "bytes=-500" -> { start, end } | null (no range) | false (cannot be satisfied)
|
|
12
|
+
function parseRange(header, size) {
|
|
13
|
+
if (!header) return null;
|
|
14
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(String(header).trim()); // one range only; several ranges are answered with the whole file
|
|
15
|
+
if (!m || (m[1] === '' && m[2] === '')) return null;
|
|
16
|
+
let start, end;
|
|
17
|
+
if (m[1] === '') { // the last N bytes
|
|
18
|
+
const n = Number(m[2]);
|
|
19
|
+
if (n === 0) return false;
|
|
20
|
+
start = Math.max(0, size - n); end = size - 1;
|
|
21
|
+
} else {
|
|
22
|
+
start = Number(m[1]);
|
|
23
|
+
end = m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1);
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= size) return false;
|
|
26
|
+
return { start, end };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// sendFile(req, res, file, { type, headers, status, download })
|
|
30
|
+
function sendFile(req, res, file, opts = {}) {
|
|
31
|
+
req = req || res.req;
|
|
32
|
+
let stat;
|
|
33
|
+
try { stat = fs.statSync(file); if (!stat.isFile()) throw new Error('not a file'); } catch (_) {
|
|
34
|
+
if (!res.headersSent) res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
35
|
+
return res.end('Not found');
|
|
36
|
+
}
|
|
37
|
+
const size = stat.size;
|
|
38
|
+
const type = opts.type || MEDIA_TYPES[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
39
|
+
const etag = `W/"${size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
|
|
40
|
+
const headers = {
|
|
41
|
+
'Content-Type': type,
|
|
42
|
+
'Accept-Ranges': 'bytes',
|
|
43
|
+
'Last-Modified': stat.mtime.toUTCString(),
|
|
44
|
+
ETag: etag,
|
|
45
|
+
'X-Content-Type-Options': 'nosniff',
|
|
46
|
+
...(opts.download ? { 'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(path.basename(file))}` } : {}),
|
|
47
|
+
...(opts.headers || {}),
|
|
48
|
+
};
|
|
49
|
+
const method = (req && req.method) || 'GET';
|
|
50
|
+
const h = (req && req.headers) || {};
|
|
51
|
+
|
|
52
|
+
if (h['if-none-match'] === etag && !h.range) { res.writeHead(304, headers); return res.end(); }
|
|
53
|
+
|
|
54
|
+
let range = parseRange(h.range, size);
|
|
55
|
+
// If-Range: only honor the range when the file is still the one the client started with
|
|
56
|
+
if (range && h['if-range'] && h['if-range'] !== etag && h['if-range'] !== headers['Last-Modified']) range = null;
|
|
57
|
+
if (range === false) {
|
|
58
|
+
res.writeHead(416, { 'Content-Range': `bytes */${size}`, 'Content-Type': 'text/plain; charset=utf-8' });
|
|
59
|
+
return res.end('Range not satisfiable');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let status = opts.status || 200, start = 0, end = size - 1;
|
|
63
|
+
if (range) { status = 206; ({ start, end } = range); headers['Content-Range'] = `bytes ${start}-${end}/${size}`; }
|
|
64
|
+
headers['Content-Length'] = size === 0 ? 0 : end - start + 1;
|
|
65
|
+
res.writeHead(status, headers);
|
|
66
|
+
if (method === 'HEAD' || size === 0) return res.end();
|
|
67
|
+
|
|
68
|
+
const stream = fs.createReadStream(file, { start, end });
|
|
69
|
+
stream.on('error', () => res.destroy());
|
|
70
|
+
res.on('close', () => stream.destroy()); // the viewer seeked or left: stop reading
|
|
71
|
+
stream.pipe(res);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { sendFile, parseRange, MEDIA_TYPES };
|
package/bin/build.js
CHANGED
|
@@ -25,7 +25,7 @@ const path = require('path');
|
|
|
25
25
|
const Kreftjs = require('../kreftjs.js');
|
|
26
26
|
|
|
27
27
|
const { expandBlocks, prepareTemplate, splitKreft, splitInterp, exactBrace, stripBrace, interpToExpr, quoteBraceAttrs, expandSelfClosing,
|
|
28
|
-
scopeCss, hashString, propType, simplePathRE, fnExpRE, forAliasRE, stripTypeScript } = Kreftjs.__internal;
|
|
28
|
+
scopeCss, hashString, propType, typeCheck, caseCond, simplePathRE, fnExpRE, forAliasRE, stripTypeScript } = Kreftjs.__internal;
|
|
29
29
|
|
|
30
30
|
const deps = require('./deps.js');
|
|
31
31
|
// component files: .kreft, or .kreftjs in a project from the 0.0.x betas
|
|
@@ -329,12 +329,16 @@ function compileFile(file, rel, root) {
|
|
|
329
329
|
|
|
330
330
|
// props: keep the default as a function so every instance gets its own copy
|
|
331
331
|
const props = Object.entries(sp.props).map(([p, expr]) => {
|
|
332
|
-
|
|
332
|
+
// `prop size: 'sm' | 'lg'`: the type text is kept, so the built app warns about a wrong value too
|
|
333
|
+
const ti = sp.propTypes && sp.propTypes[p];
|
|
334
|
+
const simple = ti && { string: 'String', number: 'Number', boolean: 'Boolean' }[ti.type.trim()];
|
|
335
|
+
const typed = ti ? `, typeText: ${JSON.stringify(ti.type)}, check: Kreftjs.__typeCheck(${JSON.stringify(ti.type)})${ti.required ? ', required: true' : ''}` : '';
|
|
336
|
+
if (expr == null) return `${JSON.stringify(p)}: { ${simple ? `type: ${simple}` : ''}${typed} }`.replace('{ , ', '{ ').replace('{ }', '{}');
|
|
333
337
|
let sample;
|
|
334
338
|
try { sample = new Function(`return (${expr})`)(); } catch (_) { /* uses something only known at runtime */ }
|
|
335
|
-
if (typeof sample === 'function') return `${JSON.stringify(p)}: { type: Function, default: (${expr}) }`;
|
|
339
|
+
if (typeof sample === 'function') return `${JSON.stringify(p)}: { type: Function, default: (${expr})${typed} }`;
|
|
336
340
|
const type = propType(sample);
|
|
337
|
-
return `${JSON.stringify(p)}: { ${type ? `type: ${type.name}, ` : ''}default: function () { return (${expr}); } }`;
|
|
341
|
+
return `${JSON.stringify(p)}: { ${type ? `type: ${type.name}, ` : ''}default: function () { return (${expr}); }${typed} }`;
|
|
338
342
|
});
|
|
339
343
|
|
|
340
344
|
// imports: other .kreft files become component references; .js modules load with import()
|
|
@@ -439,12 +443,15 @@ function addRouterTemplates(fns) {
|
|
|
439
443
|
addHandler(fns, 'navigate($event)');
|
|
440
444
|
}
|
|
441
445
|
|
|
442
|
-
const STRUCTURAL = new Set(['if', 'elif', 'else', 'each', 'key', 'ref', 'skip', 'cloak', 'slot', 'empty', 'once', 'animate', 'waiting', 'failed']);
|
|
446
|
+
const STRUCTURAL = new Set(['match', 'case', 'default', 'if', 'elif', 'else', 'each', 'key', 'ref', 'skip', 'cloak', 'slot', 'empty', 'once', 'animate', 'waiting', 'failed']);
|
|
443
447
|
|
|
444
448
|
function collectExpressions(template, fns) {
|
|
445
449
|
const html = prepareTemplate(template);
|
|
450
|
+
const subjects = []; // k-match subjects seen so far: a k-case is compared with one of them
|
|
446
451
|
scanHtml(html, {
|
|
447
452
|
attr(name, value) {
|
|
453
|
+
if (name === 'k-match') { const v = stripBrace(value); subjects.push(v); return addEval(fns, v); }
|
|
454
|
+
if (name === 'k-case') { const raw = stripBrace(value); for (const sub of subjects) addEval(fns, caseCond(sub, raw)); return; }
|
|
448
455
|
if (name[0] === ':' || name === 'k-bind' || name.startsWith('k-bind:')) return addEval(fns, value);
|
|
449
456
|
if (name[0] === '@' || name.startsWith('k-on:')) return addHandler(fns, stripBrace(value));
|
|
450
457
|
if (name.startsWith('k-')) {
|
package/bin/db-api.js
CHANGED
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
* Which tables, and what each allows, comes from `tables` (kreftjs.config.json):
|
|
17
17
|
* { "posts": true, "users": { "write": false, "hidden": ["password"] }, "logs": false }
|
|
18
18
|
* `kreft dev` shows every table unless you list them; `kreft serve` shows only the tables you list.
|
|
19
|
+
* Credential-looking columns (password, token, secret, hash, …) are hidden by default ("show": ["token"] opens one), and
|
|
20
|
+
* tables of sessions, tokens and jobs are only opened when named in the list.
|
|
19
21
|
*/
|
|
20
22
|
const { validate } = require('../kreftjs.js'); // the same rules as Kreftjs.validate in the browser
|
|
21
23
|
const MAX_BODY = 1024 * 1024;
|
|
22
24
|
const MAX_ROWS = 1000;
|
|
25
|
+
const SECRET_COL_RE = /(password|passwd|secret|token|api_?key|private_?key|recovery_?code|remember|two_factor|salt|hash)/i;
|
|
26
|
+
const SECRET_TABLE_RE = /^(personal_access_tokens|password_reset_tokens|password_resets|sessions|failed_jobs|jobs|job_batches|cache|cache_locks)$/i;
|
|
23
27
|
|
|
24
28
|
function createDbApi(db, { tables: allow = null, production = false, prefix = '/api', log = console } = {}) {
|
|
25
29
|
const isProd = Boolean(production || process.env.APP_ENV === 'production' || process.env.NODE_ENV === 'production');
|
|
@@ -29,13 +33,17 @@ function createDbApi(db, { tables: allow = null, production = false, prefix = '/
|
|
|
29
33
|
const out = new Map();
|
|
30
34
|
for (const t of all) {
|
|
31
35
|
if (t.name === 'kreft_migrations') continue;
|
|
36
|
+
// tables that hold logins, tokens or queued payloads are never shown by default, even by `kreft dev` with no list:
|
|
37
|
+
// only a table named in kreftjs.config.json is opened
|
|
38
|
+
if (SECRET_TABLE_RE.test(t.name) && !(allow && allow[t.name] != null && allow[t.name] !== false)) continue;
|
|
32
39
|
let rules = allow ? allow[t.name] : true;
|
|
33
40
|
if (rules == null || rules === false) continue;
|
|
34
41
|
if (rules === true) rules = {};
|
|
35
42
|
const write = rules.write === undefined ? true : rules.write;
|
|
36
43
|
out.set(t.name, {
|
|
37
44
|
...t,
|
|
38
|
-
hidden:
|
|
45
|
+
// columns that look like credentials are hidden unless "show": ["token"] lists them (a "hidden" list adds to them)
|
|
46
|
+
hidden: new Set([...t.columns.map((c) => c.name).filter((n) => SECRET_COL_RE.test(n) && !(rules.show || []).includes(n)), ...(rules.hidden || [])]),
|
|
39
47
|
rules: rules.rules || null, // { "title": "required|min:3", "email": "required|email" }
|
|
40
48
|
can: {
|
|
41
49
|
read: rules.read !== false,
|
package/bin/kreftjs.js
CHANGED
|
@@ -583,6 +583,30 @@ function copyFolder(from, to) {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
/* ------------------------------------------------------------------ kreft vscode */
|
|
586
|
+
/* VS Code only loads extensions listed in extensions.json, and after "Uninstall" it keeps the folder in .obsolete and
|
|
587
|
+
deletes it on the next start. Copying the folder is not enough after an uninstall: list it again and clear .obsolete. */
|
|
588
|
+
function registerExtension(dir, folder, version) {
|
|
589
|
+
const id = 'kreft.kreftjs';
|
|
590
|
+
const location = path.join(dir, folder);
|
|
591
|
+
try {
|
|
592
|
+
const file = path.join(dir, 'extensions.json');
|
|
593
|
+
let list = [];
|
|
594
|
+
if (fs.existsSync(file)) { try { list = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { list = []; } }
|
|
595
|
+
if (!Array.isArray(list)) list = [];
|
|
596
|
+
list = list.filter((e) => !(e && e.identifier && /^kreft\.kreft(js)?$/.test(e.identifier.id)));
|
|
597
|
+
list.push({ identifier: { id }, version, location: { $mid: 1, fsPath: location, external: 'file://' + location, path: location, scheme: 'file' }, relativeLocation: folder, metadata: { installedTimestamp: Date.now(), pinned: false, source: 'resource', isApplicationScoped: false, isMachineScoped: false, isBuiltin: false, targetPlatform: 'undefined' } });
|
|
598
|
+
fs.writeFileSync(file, JSON.stringify(list));
|
|
599
|
+
} catch {}
|
|
600
|
+
try {
|
|
601
|
+
const file = path.join(dir, '.obsolete');
|
|
602
|
+
if (fs.existsSync(file)) {
|
|
603
|
+
const o = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
604
|
+
for (const k of Object.keys(o)) if (k.startsWith('kreft.kreftjs-') || k.startsWith('kreft.kreft-')) delete o[k];
|
|
605
|
+
fs.writeFileSync(file, JSON.stringify(o));
|
|
606
|
+
}
|
|
607
|
+
} catch {}
|
|
608
|
+
}
|
|
609
|
+
|
|
586
610
|
function installVscode() {
|
|
587
611
|
const os = require('os');
|
|
588
612
|
const from = path.join(__dirname, '..', 'editors', 'vscode');
|
|
@@ -595,6 +619,7 @@ function installVscode() {
|
|
|
595
619
|
fs.mkdirSync(dir, { recursive: true });
|
|
596
620
|
fs.readdirSync(dir).filter((n) => n.startsWith('kreft.kreftjs-') || n.startsWith('kreft.kreft-')) /* the older extensions go too */.forEach((n) => fs.rmSync(path.join(dir, n), { recursive: true, force: true }));
|
|
597
621
|
copyFolder(from, path.join(dir, `kreft.kreftjs-${ext.version}`));
|
|
622
|
+
registerExtension(dir, `kreft.kreftjs-${ext.version}`, ext.version);
|
|
598
623
|
console.log(` Installed in ${path.join(dir, `kreft.kreftjs-${ext.version}`)}`);
|
|
599
624
|
}
|
|
600
625
|
// Ensure current project .vscode/settings.json maps .kreft files to Kreft and configures formatter
|
|
@@ -615,7 +640,7 @@ function installVscode() {
|
|
|
615
640
|
/* ------------------------------------------------------------------ kreft dev */
|
|
616
641
|
async function devServer(args, { production = false } = {}) {
|
|
617
642
|
const isProd = Boolean(production || process.env.APP_ENV === 'production' || process.env.NODE_ENV === 'production');
|
|
618
|
-
let port = production ? Number(process.env.PORT) || 8000 : 8000, folder = production ? 'dist' : '.', apiFile = null, delay = 0, saveApi = true, hot = !production, dbUrl = null, migrations = null, serverFolder = null, backendFolder = null, seedNew = false;
|
|
643
|
+
let port = production ? Number(process.env.PORT) || 8000 : 8000, folder = production ? 'dist' : '.', apiFile = null, delay = 0, saveApi = true, hot = !production, dbUrl = null, migrations = null, serverFolder = null, backendFolder = null, seedNew = false, force = false;
|
|
619
644
|
// kreft dev only answers this computer; --host 0.0.0.0 shares it on your network.
|
|
620
645
|
// kreft serve answers everyone (HOST in the environment can narrow it).
|
|
621
646
|
let host = production ? process.env.HOST || '0.0.0.0' : '127.0.0.1';
|
|
@@ -630,6 +655,7 @@ async function devServer(args, { production = false } = {}) {
|
|
|
630
655
|
else if (args[i] === '--server') serverFolder = path.resolve(args[++i]);
|
|
631
656
|
else if (args[i] === '--backend') backendFolder = path.resolve(args[++i]);
|
|
632
657
|
else if (args[i] === '--seed') seedNew = true;
|
|
658
|
+
else if (args[i] === '--force') force = true;
|
|
633
659
|
else if (args[i] === '--host') host = args[++i];
|
|
634
660
|
else if (/^\d+$/.test(args[i])) port = Number(args[i]);
|
|
635
661
|
else folder = args[i];
|
|
@@ -655,6 +681,16 @@ async function devServer(args, { production = false } = {}) {
|
|
|
655
681
|
const backendDir = backendFolder || (lay.split ? lay.backend : envDir);
|
|
656
682
|
const apiRoutesFile = require('./backend/typescript.js').findFile(path.join(backendDir, 'routes', 'api')); // api.js or api.ts
|
|
657
683
|
const hasBackend = Boolean(apiRoutesFile);
|
|
684
|
+
// Not a Kreft project and nothing to show: do not start a server on whatever folder this happens to be (an empty
|
|
685
|
+
// one, or your home folder). --force serves it anyway.
|
|
686
|
+
if (!force && !dbUrl && !apiFile && !serverFolder && !hasBackend && !lay.split) {
|
|
687
|
+
const names = (() => { try { return fs.readdirSync(root); } catch (_) { return []; } })();
|
|
688
|
+
const isSite = names.includes('index.html') || names.some((n) => /\.kreft$/.test(n)) || fs.existsSync(path.join(root, 'demo', 'index.html')) || fs.existsSync(path.join(config.dir || root, 'kreftjs.config.json'));
|
|
689
|
+
if (!isSite) {
|
|
690
|
+
console.error(` ${path.resolve(root)} is not a Kreft project: there is no index.html, no frontend/ folder and no .kreft pages.\n\n Make one: kreft new my-app (then: cd my-app, kreft dev)\n Or go into your project: cd path/to/your-app\n To serve this folder anyway: kreft ${production ? 'serve' : 'dev'} ${folder === '.' ? '' : folder + ' '}--force`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
658
694
|
// kreftjs serve: the backend must not sit inside the folder that is served, or its files could be downloaded
|
|
659
695
|
if (production && hasBackend && (backendDir === root || backendDir.startsWith(root + path.sep))) {
|
|
660
696
|
console.error(` The backend (${backendDir}) is inside the folder being served (${root}).\n Serve the built site instead: kreft build, then kreft serve dist`);
|
|
@@ -736,7 +772,7 @@ async function devServer(args, { production = false } = {}) {
|
|
|
736
772
|
'.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.kreft': 'text/plain', '.kreftjs': 'text/plain',
|
|
737
773
|
'.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png',
|
|
738
774
|
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp',
|
|
739
|
-
'.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff', '.txt': 'text/plain', '.md': 'text/markdown',
|
|
775
|
+
'.ico': 'image/x-icon', ...(({ '.ts': _ts, ...media }) => media)(require('./backend/stream.js').MEDIA_TYPES), '.woff2': 'font/woff2', '.woff': 'font/woff', '.txt': 'text/plain', '.md': 'text/markdown',
|
|
740
776
|
};
|
|
741
777
|
const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch (_) { return false; } };
|
|
742
778
|
|
|
@@ -754,12 +790,15 @@ async function devServer(args, { production = false } = {}) {
|
|
|
754
790
|
}
|
|
755
791
|
}
|
|
756
792
|
const type = types[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
757
|
-
|
|
793
|
+
const fileHeaders = {
|
|
758
794
|
'Content-Type': type + (type.startsWith('text/') || type.endsWith('json') ? '; charset=utf-8' : ''),
|
|
759
795
|
'Cache-Control': isProd ? 'no-cache' : 'no-store', // dev: always pick up your latest changes
|
|
760
796
|
'X-Content-Type-Options': 'nosniff',
|
|
761
797
|
...prodHeaders,
|
|
762
|
-
}
|
|
798
|
+
};
|
|
799
|
+
// everything but pages is sent with byte ranges (video and audio seek, Safari plays)
|
|
800
|
+
if (type !== 'text/html') return require('./backend/stream.js').sendFile(res.req, res, file, { status: opts.status, type: fileHeaders['Content-Type'], headers: fileHeaders });
|
|
801
|
+
res.writeHead(opts.status || 200, fileHeaders);
|
|
763
802
|
if (!production && type === 'text/html') {
|
|
764
803
|
let html = fs.readFileSync(file, 'utf8');
|
|
765
804
|
// PUBLIC_ values from .env, as Kreftjs.env (kreft build puts them in app.js)
|
|
@@ -794,12 +833,9 @@ async function devServer(args, { production = false } = {}) {
|
|
|
794
833
|
return res.end(opts.seo ? require('./backend/seo-pages.js').inject(html, opts.seo) : html);
|
|
795
834
|
}
|
|
796
835
|
if (type === 'text/html' && opts.seo) return res.end(require('./backend/seo-pages.js').inject(fs.readFileSync(file, 'utf8'), opts.seo));
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
res.end('Not found');
|
|
801
|
-
});
|
|
802
|
-
stream.pipe(res);
|
|
836
|
+
const page = fs.createReadStream(file); // a page in production: headers are written, send it whole
|
|
837
|
+
page.on('error', () => res.end('Not found'));
|
|
838
|
+
page.pipe(res);
|
|
803
839
|
}
|
|
804
840
|
|
|
805
841
|
// ----- hot reload: watch the folder, tell open pages what changed -----
|
|
@@ -1035,6 +1071,8 @@ async function devServer(args, { production = false } = {}) {
|
|
|
1035
1071
|
|
|
1036
1072
|
const server = http.createServer(async (req, res) => {
|
|
1037
1073
|
try {
|
|
1074
|
+
// SHIELD=true in .env: rate limits, bans and load shedding per visitor address (see backend/shield.js)
|
|
1075
|
+
if (require('./backend/shield.js').Shield.guard(req, res)) return;
|
|
1038
1076
|
if (loopbackBind && !isLocalName(hostOnly(req.headers.host)) && !allowedHosts.has(hostOnly(req.headers.host))) {
|
|
1039
1077
|
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1040
1078
|
return res.end(`Blocked: this server only answers localhost. To reach it by the name "${hostOnly(req.headers.host)}", start it with KREFT_ALLOWED_HOSTS=${hostOnly(req.headers.host)} (or --host 0.0.0.0 to share it on your network).`);
|
|
@@ -1109,8 +1147,7 @@ async function devServer(args, { production = false } = {}) {
|
|
|
1109
1147
|
const realPub = real(pub), realF = real(f);
|
|
1110
1148
|
if ((f.startsWith(pub + path.sep)) && !secret(urlPath) && isFile(f) && realPub && realF && realF.startsWith(realPub + path.sep)) {
|
|
1111
1149
|
const type = types[path.extname(f).toLowerCase()] || 'application/octet-stream';
|
|
1112
|
-
|
|
1113
|
-
return fs.createReadStream(f).pipe(res);
|
|
1150
|
+
return require('./backend/stream.js').sendFile(req, res, f, { type, headers: { 'Content-Security-Policy': "default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'; sandbox", 'Cache-Control': /\.(m3u8|mpd)$/i.test(f) ? 'no-cache' : 'public, max-age=3600' } }); // a live playlist changes every few seconds
|
|
1114
1151
|
}
|
|
1115
1152
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1116
1153
|
return res.end('Not found: ' + urlPath);
|
|
@@ -1153,6 +1190,7 @@ async function devServer(args, { production = false } = {}) {
|
|
|
1153
1190
|
|
|
1154
1191
|
// if the port is taken (another server is still running), try the next one
|
|
1155
1192
|
let tries = 0;
|
|
1193
|
+
require('./backend/shield.js').Shield.watch(server); // SHIELD=true: caps open connections per address
|
|
1156
1194
|
server.on('error', (err) => {
|
|
1157
1195
|
if (err.code === 'EADDRINUSE' && tries < 20) {
|
|
1158
1196
|
console.log(` Port ${port + tries} is already in use by another program, trying ${port + tries + 1}…`);
|