mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-11-02 04:53:23 +00:00
Merge branch 'DumbWareio:main' into progress-bar
This commit is contained in:
17
.env.example
17
.env.example
@@ -1,14 +1,15 @@
|
|||||||
# Server Configuration
|
# Server Configuration
|
||||||
PORT=3000 # The port the server will listen on
|
PORT=3000 # The port the server will listen on
|
||||||
DUMBDROP_TITLE=DumbDrop # Site title displayed in header (default: DumbDrop)
|
|
||||||
|
|
||||||
# Upload Limits
|
# Upload Settings
|
||||||
MAX_FILE_SIZE=1024 # Maximum file size in MB (default: 1024 MB / 1 GB)
|
MAX_FILE_SIZE=1024 # Maximum file size in MB
|
||||||
|
AUTO_UPLOAD=false # Enable automatic upload on file selection
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
DUMBDROP_PIN= # Optional PIN protection (4-10 digits, leave empty to disable)
|
DUMBDROP_PIN= # Optional PIN protection (4-10 digits)
|
||||||
|
DUMBDROP_TITLE=DumbDrop # Site title displayed in header
|
||||||
|
|
||||||
# Notifications
|
# Notifications (Optional)
|
||||||
APPRISE_URL= # Apprise URL for notifications (leave empty to disable)
|
APPRISE_URL= # Apprise URL for notifications (e.g., tgram://bottoken/ChatID)
|
||||||
APPRISE_MESSAGE= # Custom message for notifications (default: "New file uploaded: {filename} ({size}), Storage used: {storage}")
|
APPRISE_MESSAGE=New file uploaded - {filename} ({size}), Storage used {storage}
|
||||||
APPRISE_SIZE_UNIT= # Size unit for notifications (B, KB, MB, GB, TB). Leave empty for auto
|
APPRISE_SIZE_UNIT=auto # Size unit for notifications (auto, B, KB, MB, GB, TB)
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -32,6 +32,15 @@ No auth (unless you want it now!), no storage, no nothing. Just a simple file up
|
|||||||
| APPRISE_URL | Apprise URL for notifications | None | No |
|
| APPRISE_URL | Apprise URL for notifications | None | No |
|
||||||
| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No |
|
| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No |
|
||||||
| APPRISE_SIZE_UNIT| Size unit for notifications | Auto | No |
|
| APPRISE_SIZE_UNIT| Size unit for notifications | Auto | No |
|
||||||
|
| AUTO_UPLOAD | Enable automatic upload on file selection | false | No |
|
||||||
|
| ALLOWED_EXTENSIONS| Comma-separated list of allowed file extensions | None | No |
|
||||||
|
|
||||||
|
## File Extension Filtering
|
||||||
|
To restrict which file types can be uploaded, set the `ALLOWED_EXTENSIONS` environment variable. For example:
|
||||||
|
```env
|
||||||
|
ALLOWED_EXTENSIONS=.jpg,.jpeg,.png,.pdf,.doc,.docx,.txt
|
||||||
|
```
|
||||||
|
If not set, all file extensions will be allowed.
|
||||||
|
|
||||||
## Notification Templates
|
## Notification Templates
|
||||||
The notification message supports the following placeholders:
|
The notification message supports the following placeholders:
|
||||||
@@ -57,6 +66,8 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN
|
|||||||
- Automatic input sanitization
|
- Automatic input sanitization
|
||||||
- Secure PIN validation middleware
|
- Secure PIN validation middleware
|
||||||
- No PIN storage in browser (memory only)
|
- No PIN storage in browser (memory only)
|
||||||
|
- Rate Limiting to prevent brute force attacks
|
||||||
|
- Optional file extension filtering
|
||||||
|
|
||||||
## Notification Support
|
## Notification Support
|
||||||
- Integration with [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#supported-notifications) for flexible notifications
|
- Integration with [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#supported-notifications) for flexible notifications
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
name: Dumb Drop
|
|
||||||
services:
|
services:
|
||||||
dumbdrop:
|
dumbdrop:
|
||||||
ports:
|
ports:
|
||||||
@@ -10,7 +9,9 @@ services:
|
|||||||
MAX_FILE_SIZE: 1024
|
MAX_FILE_SIZE: 1024
|
||||||
DUMBDROP_PIN: 123456
|
DUMBDROP_PIN: 123456
|
||||||
# APPRISE_URL: ntfys://
|
# APPRISE_URL: ntfys://
|
||||||
|
# APPRISE_MESSAGE: New file uploaded - {filename} ({size}), Storage used {storage}
|
||||||
|
# AUTO_UPLOAD: false
|
||||||
APPRISE_MESSAGE: New file uploaded - {filename} ({size}), Storage used {storage}
|
APPRISE_MESSAGE: New file uploaded - {filename} ({size}), Storage used {storage}
|
||||||
APPRISE_SIZE_UNIT: auto
|
APPRISE_SIZE_UNIT: auto
|
||||||
image: abite3/dumbdrop:latest
|
image: dumbwareio/dumbdrop:latest
|
||||||
# build: .
|
# build: .
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-rate-limit": "^7.1.5",
|
||||||
"multer": "^1.4.5-lts.1"
|
"multer": "^1.4.5-lts.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
|
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const RETRY_DELAY = 1000;
|
const RETRY_DELAY = 1000;
|
||||||
|
const AUTO_UPLOAD = ['true', '1', 'yes'].includes('{{AUTO_UPLOAD}}'.toLowerCase());
|
||||||
|
|
||||||
// Utility function to generate a unique batch ID
|
// Utility function to generate a unique batch ID
|
||||||
function generateBatchId() {
|
function generateBatchId() {
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
getAllFileEntries(items).then(newFiles => {
|
getAllFileEntries(items).then(newFiles => {
|
||||||
files = newFiles;
|
files = newFiles;
|
||||||
updateFileList();
|
updateFileList();
|
||||||
|
if (AUTO_UPLOAD) startUploads();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Handle single file drop
|
// Handle single file drop
|
||||||
@@ -529,6 +531,7 @@
|
|||||||
file.batchId = batchId;
|
file.batchId = batchId;
|
||||||
});
|
});
|
||||||
updateFileList();
|
updateFileList();
|
||||||
|
if (AUTO_UPLOAD) startUploads();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,6 +543,7 @@
|
|||||||
file.batchId = batchId;
|
file.batchId = batchId;
|
||||||
});
|
});
|
||||||
updateFileList();
|
updateFileList();
|
||||||
|
if (AUTO_UPLOAD) startUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFolders(e) {
|
function handleFolders(e) {
|
||||||
@@ -552,6 +556,7 @@
|
|||||||
file.batchId = batchId;
|
file.batchId = batchId;
|
||||||
});
|
});
|
||||||
updateFileList();
|
updateFileList();
|
||||||
|
if (AUTO_UPLOAD) startUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileList() {
|
function updateFileList() {
|
||||||
@@ -571,7 +576,7 @@
|
|||||||
fileList.appendChild(fileItem);
|
fileList.appendChild(fileItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadButton.style.display = files.length > 0 ? 'block' : 'none';
|
uploadButton.style.display = (!AUTO_UPLOAD && files.length > 0) ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startUploads() {
|
async function startUploads() {
|
||||||
|
|||||||
65
server.js
65
server.js
@@ -10,14 +10,30 @@ const util = require('util');
|
|||||||
const execAsync = util.promisify(exec);
|
const execAsync = util.promisify(exec);
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// Rate limiting setup
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
const uploadDir = './uploads'; // Local development
|
const uploadDir = './uploads'; // Local development
|
||||||
const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '1024') * 1024 * 1024; // Convert MB to bytes
|
const maxFileSize = parseInt(process.env.MAX_FILE_SIZE || '1024') * 1024 * 1024; // Convert MB to bytes
|
||||||
const APPRISE_URL = process.env.APPRISE_URL;
|
const APPRISE_URL = process.env.APPRISE_URL;
|
||||||
const APPRISE_MESSAGE = process.env.APPRISE_MESSAGE || 'New file uploaded - {filename} ({size}), Storage used: {storage}';
|
const APPRISE_MESSAGE = process.env.APPRISE_MESSAGE || 'New file uploaded - {filename} ({size}), Storage used {storage}';
|
||||||
const siteTitle = process.env.DUMBDROP_TITLE || 'DumbDrop';
|
const siteTitle = process.env.DUMBDROP_TITLE || 'DumbDrop';
|
||||||
const APPRISE_SIZE_UNIT = process.env.APPRISE_SIZE_UNIT;
|
const APPRISE_SIZE_UNIT = process.env.APPRISE_SIZE_UNIT;
|
||||||
|
const AUTO_UPLOAD = process.env.AUTO_UPLOAD === 'true';
|
||||||
|
|
||||||
|
// Update the chunk size and rate limits
|
||||||
|
const CHUNK_SIZE = 5 * 1024 * 1024; // Increase to 5MB chunks
|
||||||
|
|
||||||
|
// Update rate limiters for large files
|
||||||
|
const initUploadLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute window
|
||||||
|
max: 30, // 30 new upload initializations per minute
|
||||||
|
message: { error: 'Too many upload attempts. Please wait before starting new uploads.' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
});
|
||||||
|
|
||||||
// Brute force protection setup
|
// Brute force protection setup
|
||||||
const loginAttempts = new Map(); // Stores IP addresses and their attempt counts
|
const loginAttempts = new Map(); // Stores IP addresses and their attempt counts
|
||||||
@@ -113,6 +129,29 @@ app.use(cors());
|
|||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Security headers middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// Content Security Policy
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Security-Policy',
|
||||||
|
"default-src 'self'; " +
|
||||||
|
"style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
|
||||||
|
"script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
|
||||||
|
"img-src 'self' data: blob:;"
|
||||||
|
);
|
||||||
|
// X-Content-Type-Options
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
// X-Frame-Options
|
||||||
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||||
|
// X-XSS-Protection
|
||||||
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||||
|
// Strict Transport Security (when in production)
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Helper function for constant-time string comparison
|
// Helper function for constant-time string comparison
|
||||||
function safeCompare(a, b) {
|
function safeCompare(a, b) {
|
||||||
if (typeof a !== 'string' || typeof b !== 'string') {
|
if (typeof a !== 'string' || typeof b !== 'string') {
|
||||||
@@ -200,7 +239,8 @@ app.get('/', (req, res) => {
|
|||||||
}
|
}
|
||||||
// Read the file and replace the title
|
// Read the file and replace the title
|
||||||
let html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
|
let html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
|
||||||
html = html.replace(/{{SITE_TITLE}}/g, siteTitle); // Use global replace
|
html = html.replace(/{{SITE_TITLE}}/g, siteTitle);
|
||||||
|
html = html.replace('{{AUTO_UPLOAD}}', AUTO_UPLOAD.toString());
|
||||||
res.send(html);
|
res.send(html);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,7 +341,7 @@ function isValidBatchId(batchId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.post('/upload/init', async (req, res) => {
|
app.post('/upload/init', initUploadLimiter, async (req, res) => {
|
||||||
const { filename, fileSize } = req.body;
|
const { filename, fileSize } = req.body;
|
||||||
let batchId = req.headers['x-batch-id'];
|
let batchId = req.headers['x-batch-id'];
|
||||||
|
|
||||||
@@ -320,6 +360,22 @@ app.post('/upload/init', async (req, res) => {
|
|||||||
|
|
||||||
const safeFilename = path.normalize(filename).replace(/^(\.\.(\/|\\|$))+/, '');
|
const safeFilename = path.normalize(filename).replace(/^(\.\.(\/|\\|$))+/, '');
|
||||||
|
|
||||||
|
// Validate file extension
|
||||||
|
const allowedExtensions = process.env.ALLOWED_EXTENSIONS ?
|
||||||
|
process.env.ALLOWED_EXTENSIONS.split(',').map(ext => ext.trim().toLowerCase()) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (allowedExtensions) {
|
||||||
|
const fileExt = path.extname(safeFilename).toLowerCase();
|
||||||
|
if (!allowedExtensions.includes(fileExt)) {
|
||||||
|
log.error(`File type ${fileExt} not allowed`);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'File type not allowed',
|
||||||
|
allowedExtensions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check file size limit
|
// Check file size limit
|
||||||
if (fileSize > maxFileSize) {
|
if (fileSize > maxFileSize) {
|
||||||
log.error(`File size ${fileSize} bytes exceeds limit of ${maxFileSize} bytes`);
|
log.error(`File size ${fileSize} bytes exceeds limit of ${maxFileSize} bytes`);
|
||||||
@@ -479,6 +535,9 @@ app.listen(port, () => {
|
|||||||
log.info(`Custom title set to: ${siteTitle}`);
|
log.info(`Custom title set to: ${siteTitle}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add auto upload status logging
|
||||||
|
log.info(`Auto upload is ${AUTO_UPLOAD ? 'enabled' : 'disabled'}`);
|
||||||
|
|
||||||
// Add Apprise configuration logging
|
// Add Apprise configuration logging
|
||||||
if (APPRISE_URL) {
|
if (APPRISE_URL) {
|
||||||
log.info('Apprise notifications enabled');
|
log.info('Apprise notifications enabled');
|
||||||
|
|||||||
Reference in New Issue
Block a user