security: replace live tunnel URL with placeholder API key and URL in public repo

This commit is contained in:
katR 2026-09-12 10:16:23 +05:30
parent 7f3c84763e
commit f13eaa279f
4 changed files with 75 additions and 15 deletions

View file

@ -1,16 +1,42 @@
# katdrop # katdrop
a minimal, smooth file transfer service. a minimal, smooth file transfer service with 10-minute auto-deletion.
> **Note**: This repository uses a placeholder backend URL (`https://YOUR_BACKEND_URL.trycloudflare.com`) and placeholder API key (`YOUR_API_KEY_HERE`) so no private endpoints or keys are exposed publicly.
## features ## features
- minimal dark interface with smooth interactive particle background - minimal dark interface with smooth interactive particle background
- real-time backend status indicator (`online` / `offline`)
- live upload byte progress and percentage
- drag & drop files anywhere to upload instantly - drag & drop files anywhere to upload instantly
- 10-minute auto-deletion timer per file with live countdown - 10-minute auto-deletion timer per file with live countdown
- sleek upload progress bar
- safe path verification and collision-free file naming - safe path verification and collision-free file naming
- optional API key authorization (`x-katdrop-key`)
- zero external CDN dependencies (works completely offline) - zero external CDN dependencies (works completely offline)
## configuration
### connecting your frontend (Netlify) to your backend
To connect your deployed frontend to your private backend (e.g. Cloudflare Tunnel or local server), pass your API URL and optional key via URL parameters once:
```
https://your-site.netlify.app/?api=https://your-tunnel.trycloudflare.com&key=YOUR_SECRET_KEY
```
KatDrop will automatically save these to `localStorage` in your browser.
### backend API key (optional)
To require an API key on your backend, set `KATDROP_KEY`:
```bash
KATDROP_KEY=mysecretkey npm start
```
If not set, the backend runs without key verification.
## usage ## usage
```bash ```bash
@ -25,8 +51,9 @@ Runs by default on `http://localhost:3000` (or falls back to an available port i
## endpoints ## endpoints
- `GET /health` - health check & uptime
- `POST /upload` - upload files (`file`) - `POST /upload` - upload files (`file`)
- `GET /files` - list files - `GET /files` - list active files with remaining TTL
- `GET /uploads/:filename` - download file - `GET /uploads/:filename` - download file
- `DELETE /delete/:filename` - delete file - `DELETE /delete/:filename` - delete file

View file

@ -1,5 +1,7 @@
/files https://feof-signals-fix-chevy.trycloudflare.com/files 200 # Netlify API Proxying
/upload https://feof-signals-fix-chevy.trycloudflare.com/upload 200 # Replace YOUR_BACKEND_URL with your live Cloudflare Tunnel URL:
/uploads/* https://feof-signals-fix-chevy.trycloudflare.com/uploads/:splat 200 # /files https://YOUR_BACKEND_URL.trycloudflare.com/files 200
/delete/* https://feof-signals-fix-chevy.trycloudflare.com/delete/:splat 200 # /upload https://YOUR_BACKEND_URL.trycloudflare.com/upload 200
/* /index.html 200 # /uploads/* https://YOUR_BACKEND_URL.trycloudflare.com/uploads/:splat 200
# /delete/* https://YOUR_BACKEND_URL.trycloudflare.com/delete/:splat 200
/* /index.html 200

View file

@ -20,15 +20,29 @@
let selectedFiles = []; let selectedFiles = [];
// API Base configuration for remote frontend (Netlify) or local hosting // API & Key Configuration (configurable via URL param ?api=...&key=... or localStorage)
const DEFAULT_REMOTE_API = 'https://feof-signals-fix-chevy.trycloudflare.com'; // Public repo uses placeholder values for security
const DEFAULT_REMOTE_API = 'https://YOUR_BACKEND_URL.trycloudflare.com';
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('api')) { if (urlParams.has('api')) {
const customApi = urlParams.get('api').replace(/\/+$/, ''); const customApi = urlParams.get('api').replace(/\/+$/, '');
localStorage.setItem('katdrop_api', customApi); localStorage.setItem('katdrop_api', customApi);
} }
if (urlParams.has('key')) {
localStorage.setItem('katdrop_key', urlParams.get('key'));
}
const isLocalOrigin = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.'); const isLocalOrigin = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.');
const API_BASE = window.KATDROP_API || localStorage.getItem('katdrop_api') || (isLocalOrigin ? '' : DEFAULT_REMOTE_API); const API_BASE = window.KATDROP_API || localStorage.getItem('katdrop_api') || (isLocalOrigin ? '' : DEFAULT_REMOTE_API);
const API_KEY = window.KATDROP_KEY || localStorage.getItem('katdrop_key') || 'YOUR_API_KEY_HERE';
// Authenticated fetch helper
function apiFetch(url, options = {}) {
options.headers = options.headers || {};
if (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') {
options.headers['x-katdrop-key'] = API_KEY;
}
return fetch(url, options);
}
// Toast // Toast
let toastTimeout; let toastTimeout;
@ -59,7 +73,7 @@
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 4000); const timeoutId = setTimeout(() => controller.abort(), 4000);
const res = await fetch(`${API_BASE}/health`, { signal: controller.signal }); const res = await apiFetch(`${API_BASE}/health`, { signal: controller.signal });
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (res.ok) { if (res.ok) {
@ -267,7 +281,7 @@
// Fetch and display files // Fetch and display files
async function fetchFiles() { async function fetchFiles() {
try { try {
const res = await fetch(`${API_BASE}/files`); const res = await apiFetch(`${API_BASE}/files`);
const files = await res.json(); const files = await res.json();
fileList.innerHTML = ''; fileList.innerHTML = '';
@ -288,6 +302,7 @@
const size = typeof file === 'object' && file.sizeFormatted ? file.sizeFormatted : ''; const size = typeof file === 'object' && file.sizeFormatted ? file.sizeFormatted : '';
const expiresMs = file.expiresAt ? new Date(file.expiresAt).getTime() : (Date.now() + 600000); const expiresMs = file.expiresAt ? new Date(file.expiresAt).getTime() : (Date.now() + 600000);
const remainingSec = Math.max(0, Math.ceil((expiresMs - Date.now()) / 1000)); const remainingSec = Math.max(0, Math.ceil((expiresMs - Date.now()) / 1000));
const keyParam = (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') ? `?key=${encodeURIComponent(API_KEY)}` : '';
li.innerHTML = ` li.innerHTML = `
<div class="file-info"> <div class="file-info">
@ -298,7 +313,7 @@
</div> </div>
</div> </div>
<div class="file-actions"> <div class="file-actions">
<a href="${API_BASE}/uploads/${encodeURIComponent(name)}" class="action-btn action-download" download>download</a> <a href="${API_BASE}/uploads/${encodeURIComponent(name)}${keyParam}" class="action-btn action-download" download>download</a>
<button class="action-btn action-delete" onclick="deleteFile('${escapeHtml(name)}', this.closest('li'))">delete</button> <button class="action-btn action-delete" onclick="deleteFile('${escapeHtml(name)}', this.closest('li'))">delete</button>
</div> </div>
`; `;
@ -317,7 +332,7 @@
if (cardElement) cardElement.classList.add('deleting'); if (cardElement) cardElement.classList.add('deleting');
try { try {
const res = await fetch(`${API_BASE}/delete/${encodeURIComponent(filename)}`, { method: 'DELETE' }); const res = await apiFetch(`${API_BASE}/delete/${encodeURIComponent(filename)}`, { method: 'DELETE' });
if (res.ok) { if (res.ok) {
showToast('file deleted'); showToast('file deleted');
setTimeout(() => { setTimeout(() => {
@ -397,6 +412,9 @@
}; };
xhr.open('POST', `${API_BASE}/upload`); xhr.open('POST', `${API_BASE}/upload`);
if (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') {
xhr.setRequestHeader('x-katdrop-key', API_KEY);
}
xhr.send(formData); xhr.send(formData);
} }

View file

@ -95,7 +95,7 @@ const upload = multer({
app.use((req, res, next) => { app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-katdrop-key');
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return res.sendStatus(200); return res.sendStatus(200);
} }
@ -104,6 +104,19 @@ app.use((req, res, next) => {
app.use(express.json()); app.use(express.json());
app.use(express.static(publicDir)); app.use(express.static(publicDir));
// Optional API Key Protection (if KATDROP_KEY environment variable is set)
const REQUIRED_API_KEY = process.env.KATDROP_KEY || '';
if (REQUIRED_API_KEY) {
app.use((req, res, next) => {
if (req.method === 'OPTIONS' || req.path === '/health' || req.path === '/') return next();
const clientKey = req.headers['x-katdrop-key'] || req.query.key;
if (clientKey !== REQUIRED_API_KEY) {
return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key' });
}
next();
});
}
// Health check endpoint // Health check endpoint
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: Math.floor(process.uptime()) }); res.json({ status: 'ok', uptime: Math.floor(process.uptime()) });