Compare commits
6
Commits
suggestions
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0617664d97 | ||
|
|
ce76f4fa8d | ||
|
|
5fc741ab02 | ||
|
|
dc72665610 | ||
|
|
1d345e072e | ||
|
|
d606e11de4 |
No files matched your search
@@ -4,7 +4,7 @@ const users = require('./users');
|
||||
const auth = require('./auth');
|
||||
const audit = require('./audit');
|
||||
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d => c.push(d)); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
const { readBody } = require('./body');
|
||||
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
||||
const row = u => ({ ...users.publicView(u), tabsChosen: u.tabs || [], passkeys: auth.passkeyCount(u.id) });
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ const rp = req => {
|
||||
};
|
||||
const equalStr = (a, b) => { const x = Buffer.from(String(a)), y = Buffer.from(String(b)); return x.length === y.length && crypto.timingSafeEqual(x, y); };
|
||||
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d => c.push(d)); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
const { readBody } = require('./body');
|
||||
const send = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json', ...(res.getHeader('Set-Cookie') ? { 'Set-Cookie': res.getHeader('Set-Cookie') } : {}) }); res.end(JSON.stringify(obj)); };
|
||||
const credsOf = uid => store.creds.filter(c => c.uid === uid);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
// Reads a request body as text, and refuses one that is too big, so a huge upload cannot use up the server's memory.
|
||||
function readBody(req, max = 2e6) {
|
||||
return new Promise((ok, no) => {
|
||||
const c = []; let n = 0, done = false;
|
||||
req.on('data', d => { if (done) return; n += d.length; if (n > max) { done = true; c.length = 0; const e = new Error('That is too large'); e.status = 413; no(e); return; } c.push(d); });
|
||||
req.on('end', () => { if (!done) ok(Buffer.concat(c).toString()); });
|
||||
req.on('error', e => { if (!done) no(e); });
|
||||
});
|
||||
}
|
||||
module.exports = { readBody };
|
||||
@@ -306,7 +306,7 @@ function feed(req, res) {
|
||||
}
|
||||
|
||||
// ---- HTTP API (called by server.js, behind the passkey gate)
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d => c.push(d)); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
const readBody = req => require('./body').readBody(req, 8e6);
|
||||
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
||||
|
||||
/** Every event in the window from all calendars (ICS links, iCloud, NE Hub, shared): { events, errors } */
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "for t in smoke tasks push users cal plex player lyrics signal mail mail-imap files podcast abapi abdirect books store librivox radio vbv savefiles bible lrclib visualizer; do node tools/$t-test.js || exit 1; done",
|
||||
"test": "for t in smoke tasks push users cal plex player lyrics signal mail mail-imap files podcast abapi abdirect books store librivox radio vbv savefiles bible lrclib visualizer body; do node tools/$t-test.js || exit 1; done",
|
||||
"audit": "npm audit --omit=dev --audit-level=high"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
+10
-1
@@ -33,6 +33,14 @@ function mailDoc(html, images) {
|
||||
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${csp}"><base target="_blank"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>body{margin:14px;font:15px/1.45 -apple-system,Segoe UI,Roboto,sans-serif;color:#111;background:#fff;overflow-wrap:anywhere}img{max-width:100%;height:auto}table{max-width:100%}a{color:#1a5fd1}blockquote{margin:0 0 0 6px;padding-left:10px;border-left:3px solid #ccc;color:#555}</style>${html}`;
|
||||
}
|
||||
/** On a phone the message is as tall as its text and the page scrolls, so there is one scroll, not a box inside a page. (The box keeps its own scroll on a wide screen.) No scripts run in it: only same-origin so its height can be read. */
|
||||
function mailFit(fr) {
|
||||
if (!fr) return;
|
||||
const fit = () => { if (!fr.isConnected) return; if (innerWidth >= 1024) { fr.style.height = ''; return; } try { const d = fr.contentDocument; d.documentElement.style.overflowY = 'hidden'; fr.style.height = Math.max(160, Math.ceil(d.documentElement.scrollHeight) + 2) + 'px'; } catch {} };
|
||||
fr.addEventListener('load', () => { fit(); try { fr.contentDocument.addEventListener('load', fit, true); } catch {} setTimeout(fit, 300); setTimeout(fit, 1200); });
|
||||
if (!mailFit.on) { mailFit.on = true; addEventListener('resize', () => { const f = document.querySelector('.mbody'); if (f) f.dispatchEvent(new Event('fit')); }); }
|
||||
fr.addEventListener('fit', fit);
|
||||
}
|
||||
const mailHasRemote = html => /<img[^>]+src=["']?https?:|url\(\s*["']?https?:/i.test(html || '');
|
||||
|
||||
// ---- unread badge and Home card
|
||||
@@ -462,8 +470,9 @@ function mailDrawReader() {
|
||||
<button class="btn ghost" data-a="archive">Archive</button><button class="btn ghost" data-a="unread">Mark unread</button><button class="btn ghost" data-a="flag">${m.flagged ? 'Unflag' : 'Flag'}</button><button class="btn danger" data-a="delete">Delete</button></div>
|
||||
${m.attachments.length ? `<div class="matt">${m.attachments.map(a => `<a class="btn ghost" href="/api/mail/attachment?id=${encodeURIComponent(m.id)}&aid=${encodeURIComponent(a.id)}" download>${icon('clip')}${esc(a.name)} <small>${esc(fmtSize(a.size || 0))}</small></a>`).join('')}</div>` : ''}
|
||||
${remote ? '<div class="mimg">Pictures are hidden to protect your privacy. <button class="btn ghost" id="mshow" style="min-height:32px">Show pictures</button></div>' : ''}</div>
|
||||
<iframe class="mbody" title="Message" sandbox="allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer" srcdoc="${esc(mailDoc(m.html, mail.images))}"></iframe>`;
|
||||
<iframe class="mbody" title="Message" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" referrerpolicy="no-referrer" srcdoc="${esc(mailDoc(m.html, mail.images))}"></iframe>`;
|
||||
if ($('#mshow')) $('#mshow').onclick = () => { mail.images = true; mailDrawReader(); };
|
||||
mailFit(rd.querySelector('.mbody'));
|
||||
rd.querySelectorAll('[data-a]').forEach(b => b.onclick = () => mailAction(b.dataset.a));
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -539,14 +539,14 @@ body.lyropen { overflow: hidden; }
|
||||
.mact, .matt { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
|
||||
.mact .btn, .matt .btn { min-height: 38px; }
|
||||
.mimg { background: var(--panel2); border-radius: 10px; padding: 8px 12px; font-size: 14px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.mbody { width: 100%; flex: 1; min-height: 55dvh; border: 1px solid var(--line); border-radius: 12px; background: #fff; }
|
||||
.mbody { width: 100%; flex: none; height: 60dvh; min-height: 0; border: 1px solid var(--line); border-radius: 12px; background: #fff; }
|
||||
@media (min-width: 1024px) {
|
||||
.mailapp { grid-template-columns: 340px minmax(0, 1fr); height: calc(100dvh - 40px); min-height: 0; }
|
||||
.mailapp[data-mode] .mlist, .mailapp[data-mode] .mread { display: block; }
|
||||
.mailapp[data-mode] .mread { display: flex; }
|
||||
.mlist, .mread { overflow-y: auto; min-height: 0; }
|
||||
.mback { display: none; }
|
||||
.mbody { min-height: 0; }
|
||||
.mbody { flex: 1; height: auto; }
|
||||
}
|
||||
@media (min-width: 1500px) {
|
||||
.mailapp { grid-template-columns: 190px 340px minmax(0, 1fr); }
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v49';
|
||||
const CACHE = 'hub-v50';
|
||||
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
|
||||
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ const sessions = new Map(); // callback id -> { token, repo, path, exp }; keeps
|
||||
setInterval(() => { for (const [k, v] of sessions) if (v.exp < Date.now()) sessions.delete(k); }, 3600e3).unref();
|
||||
const DOC_TYPE = { word: 'doc docx odt rtf txt', cell: 'xls xlsx ods csv', slide: 'ppt pptx odp' };
|
||||
const docType = e => Object.keys(DOC_TYPE).find(k => DOC_TYPE[k].split(' ').includes(e));
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d => c.push(d)); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
const { readBody } = require('./body');
|
||||
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
||||
const sfGet = async (p, token) => {
|
||||
const r = await fetch(SEAFILE_URL + p, { headers: { Authorization: 'Token ' + token } });
|
||||
@@ -661,9 +661,13 @@ async function handleRequest(req, res) {
|
||||
http.createServer(async (req, res) => {
|
||||
try { await handleRequest(req, res); }
|
||||
catch (e) {
|
||||
if (e && e.status === 413) { if (!res.headersSent) { res.writeHead(413, { 'content-type': 'application/json', connection: 'close' }); res.end('{"error":"That is too large"}'); } return; }
|
||||
console.error('[error]', req.method, req.url.split('?')[0], e && e.stack || e);
|
||||
if (!res.headersSent) { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'Server error: ' + (e && e.message || 'unknown') })); } else res.destroy();
|
||||
}
|
||||
}).listen(PORT, () => console.log(`Hub on :${PORT} (Seafile ${SEAFILE_URL})`));
|
||||
process.on('unhandledRejection', e => console.error('[unhandledRejection]', e && e.stack || e));
|
||||
process.on('uncaughtException', e => console.error('[uncaughtException]', e && e.stack || e));
|
||||
|
||||
// Docker sends SIGTERM on stop: leave at once instead of waiting to be killed.
|
||||
for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, () => { console.log('[stop] ' + sig); process.exit(0); });
|
||||
@@ -12,7 +12,7 @@ const users = require('./users');
|
||||
const file = () => path.join(users.dir(), 'tasks.json'); // each person has their own
|
||||
const load = () => { try { return JSON.parse(fs.readFileSync(file(), 'utf8')); } catch { return { lists: [{ id: 'inbox', name: 'Tasks' }], tasks: [] }; } };
|
||||
const save = d => atomicWrite(file(), JSON.stringify(d, null, 2), { mode: 0o600 });
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d => c.push(d)); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
const { readBody } = require('./body');
|
||||
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
||||
const isLocal = id => String(id || '').startsWith('local:');
|
||||
const COLORS = ['#4f7cff', '#2f9e6e', '#e0742b', '#d9453a', '#8a5cf0', '#e5a00d'];
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const http = require('http'), assert = require('assert'), { readBody } = require('../body');
|
||||
const s = http.createServer(async (req, res) => { try { const t = await readBody(req, 1000); res.end('ok:' + t.length); } catch (e) { res.writeHead(e.status || 500); res.end(e.message); } });
|
||||
s.listen(0, async () => {
|
||||
const P = s.address().port, post = b => fetch(`http://127.0.0.1:${P}/`, { method: 'POST', body: b }).then(async r => [r.status, await r.text()]);
|
||||
assert.deepStrictEqual(await post('x'.repeat(900)), [200, 'ok:900']);
|
||||
const big = await post('x'.repeat(5000)); assert.strictEqual(big[0], 413);
|
||||
console.log('body test ok'); s.close();
|
||||
});
|
||||
Reference in new issue
Block a user