Fixed ratelimiting and added docker support
This commit is contained in:
20
src/index.js
20
src/index.js
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { ensureBucket } = require('./lib/minio');
|
||||
const { createRequestLogger, logError } = require('./lib/logger');
|
||||
const pastesRouter = require('./routes/pastes');
|
||||
|
||||
const app = express();
|
||||
@@ -31,7 +32,20 @@ if (process.env.TRUST_PROXY === 'true') {
|
||||
}
|
||||
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false,
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", 'https://cdnjs.cloudflare.com'],
|
||||
styleSrc: ["'self'", 'https://cdnjs.cloudflare.com'],
|
||||
imgSrc: ["'self'", 'data:'],
|
||||
fontSrc: ["'self'", 'https://cdnjs.cloudflare.com', 'data:'],
|
||||
connectSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
baseUri: ["'self'"],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["'none'"],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false,
|
||||
}));
|
||||
|
||||
@@ -53,6 +67,7 @@ const readPasteLimiter = rateLimit({
|
||||
|
||||
app.use(express.json({ limit: '110kb' }));
|
||||
app.use(express.urlencoded({ extended: false, limit: '110kb' }));
|
||||
app.use(createRequestLogger());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/api/pastes', readPasteLimiter);
|
||||
@@ -71,7 +86,7 @@ app.get('/p/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// Short raw URL for plain-text retrieval.
|
||||
app.get('/raw/:id', (req, res) => {
|
||||
app.get('/raw/:id', readPasteLimiter, (req, res) => {
|
||||
req.url = `/${req.params.id}/raw`;
|
||||
pastesRouter.handle(req, res);
|
||||
});
|
||||
@@ -82,6 +97,7 @@ app.use((req, res) => res.status(404).json({ error: 'Not found.' }));
|
||||
// Global error handler
|
||||
app.use((err, req, res, _next) => {
|
||||
console.error(err);
|
||||
logError(err, req);
|
||||
res.status(500).json({ error: 'Internal server error.' });
|
||||
});
|
||||
|
||||
|
||||
70
src/lib/logger.js
Normal file
70
src/lib/logger.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOG_DIR = path.resolve(__dirname, '../../log');
|
||||
|
||||
function ensureLogDir() {
|
||||
if (!fs.existsSync(LOG_DIR)) {
|
||||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function dayStamp(date = new Date()) {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function logPath(kind, date = new Date()) {
|
||||
return path.join(LOG_DIR, `${kind}-${dayStamp(date)}.log`);
|
||||
}
|
||||
|
||||
function appendLine(filePath, line) {
|
||||
fs.appendFile(filePath, `${line}\n`, err => {
|
||||
if (err) {
|
||||
console.error('Failed to write log:', err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createRequestLogger() {
|
||||
ensureLogDir();
|
||||
|
||||
return (req, res, next) => {
|
||||
const started = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const durationMs = Date.now() - started;
|
||||
const line = [
|
||||
new Date().toISOString(),
|
||||
req.ip || '-',
|
||||
req.method,
|
||||
req.originalUrl,
|
||||
res.statusCode,
|
||||
`${durationMs}ms`,
|
||||
req.get('user-agent') || '-',
|
||||
].join(' | ');
|
||||
|
||||
appendLine(logPath('requests'), line);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function logError(err, req) {
|
||||
ensureLogDir();
|
||||
|
||||
const line = [
|
||||
new Date().toISOString(),
|
||||
req.ip || '-',
|
||||
req.method,
|
||||
req.originalUrl,
|
||||
err && err.stack ? err.stack.replace(/\n/g, ' \\n ') : String(err),
|
||||
].join(' | ');
|
||||
|
||||
appendLine(logPath('errors'), line);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRequestLogger,
|
||||
logError,
|
||||
};
|
||||
@@ -21,6 +21,8 @@
|
||||
<span id="paste-date" class="paste-date"></span>
|
||||
<span id="paste-expiry" class="paste-date"></span>
|
||||
<div class="paste-meta-actions">
|
||||
<button id="paste-url-btn" type="button" class="meta-action meta-url-btn" title="Click to copy paste URL"></button>
|
||||
<span id="paste-url-status" class="paste-url-status" aria-live="polite"></span>
|
||||
<a id="raw-btn" href="#" target="_blank" class="meta-action">Raw</a>
|
||||
<button id="download-btn" type="button" class="meta-action">Download</button>
|
||||
<button id="copy-btn" type="button" class="meta-action">Copy</button>
|
||||
|
||||
@@ -6,6 +6,12 @@ const pasteLangEl = document.getElementById('paste-lang');
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
const downloadBtn = document.getElementById('download-btn');
|
||||
const rawBtn = document.getElementById('raw-btn');
|
||||
const pasteUrlBtn = document.getElementById('paste-url-btn');
|
||||
const pasteUrlStatus = document.getElementById('paste-url-status');
|
||||
|
||||
const pasteUrl = window.location.href;
|
||||
|
||||
pasteUrlBtn.textContent = pasteUrl;
|
||||
|
||||
rawBtn.href = `/raw/${location.pathname.replace(/^\/p\//, '')}`;
|
||||
|
||||
@@ -136,6 +142,19 @@ copyBtn.addEventListener('click', () => {
|
||||
});
|
||||
});
|
||||
|
||||
pasteUrlBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(pasteUrl);
|
||||
pasteUrlStatus.textContent = 'Copied!';
|
||||
} catch {
|
||||
pasteUrlStatus.textContent = 'Unable to copy';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
pasteUrlStatus.textContent = '';
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
const lang = pasteLangEl.classList.contains('hidden') ? null : pasteLangEl.textContent.trim();
|
||||
const ext = extForLang(lang);
|
||||
|
||||
@@ -308,6 +308,27 @@ textarea#content::placeholder { color: var(--text-muted); }
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
.meta-url-btn {
|
||||
max-width: 360px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--accent);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.meta-url-btn:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.paste-url-status {
|
||||
font-size: 0.78rem;
|
||||
color: var(--accent);
|
||||
min-width: 3.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#paste-content {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user