mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-10-22 23:31:57 +00:00
Fix/cors csp (#64)
* CORS/CSP fix * deprecate ALLOWED_IFRAME_ORIGINS * Revert "deprecate ALLOWED_IFRAME_ORIGINS" This reverts commit9792f06691
. * Reapply "deprecate ALLOWED_IFRAME_ORIGINS" This reverts commit683ee93036
. * Add helmet config and deprecate previous ALLOWED_IFRAME_ORIGINS * add build to docker compose for local builds * set server to listen on 0.0.0.0 and control with cors * Remove hsts from helmet and apply new pin status check limits * add back allowed_iframe_origins env as a fallback for allowed_origins * update readme for allowed_iframe_origins
This commit is contained in:
19
.env.example
19
.env.example
@@ -9,8 +9,17 @@ PORT=3000
|
||||
# You must update this to the url you use to access your site
|
||||
BASE_URL=http://localhost:3000/
|
||||
|
||||
# Node environment (default: development)
|
||||
NODE_ENV=development
|
||||
#ALLOWED_IFRAME_ORIGINS= #DEPRECATED and will be used as ALLOWED_ORIGINS if SET
|
||||
|
||||
# Comma-separated list of allowed origins for CORS
|
||||
# (default: '*' if empty, add your base_url if you want to restrict only to base_url)
|
||||
# When adding multiple origins, base_url will be included by default
|
||||
# ALLOWED_ORIGINS: http://internalip:port,https://subdomain.example.com
|
||||
ALLOWED_ORIGINS=
|
||||
|
||||
# Node environment (default: production)
|
||||
# When set to 'development', ALLOWED_ORIGINS will default to '*'
|
||||
NODE_ENV=production
|
||||
|
||||
#########################################
|
||||
# FILE UPLOAD SETTINGS
|
||||
@@ -62,8 +71,4 @@ APPRISE_SIZE_UNIT=Auto
|
||||
#########################################
|
||||
|
||||
# Enable automatic upload on file selection (true/false, default: false)
|
||||
AUTO_UPLOAD=false
|
||||
|
||||
# Comma-separated list of origins allowed to embed the app in an iframe (optional)
|
||||
# ALLOWED_IFRAME_ORIGINS=https://example.com,https://another.com
|
||||
ALLOWED_IFRAME_ORIGINS=
|
||||
AUTO_UPLOAD=false
|
@@ -1,5 +1,5 @@
|
||||
# Base stage for shared configurations
|
||||
FROM node:20-alpine as base
|
||||
FROM node:22-alpine as base
|
||||
|
||||
# Install python and create virtual environment with minimal dependencies
|
||||
RUN apk add --no-cache python3 py3-pip && \
|
||||
|
124
README.md
124
README.md
@@ -7,6 +7,7 @@ A stupid simple file upload application that provides a clean, modern interface
|
||||
No auth (unless you want it now!), no storage, no nothing. Just a simple file uploader to drop dumb files into a dumb folder.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Production Deployment with Docker](#production-deployment-with-docker)
|
||||
- [Local Development (Recommended Quick Start)](LOCAL_DEVELOPMENT.md)
|
||||
@@ -21,44 +22,51 @@ No auth (unless you want it now!), no storage, no nothing. Just a simple file up
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Docker (For Dummies)
|
||||
|
||||
```bash
|
||||
# Pull and run with one command
|
||||
docker run -p 3000:3000 -v ./uploads:/app/uploads dumbwareio/dumbdrop:latest
|
||||
```
|
||||
|
||||
1. Go to http://localhost:3000
|
||||
2. Upload a File - It'll show up in ./uploads
|
||||
3. Celebrate on how dumb easy this was
|
||||
|
||||
### Option 2: Docker Compose (For Dummies who like customizing)
|
||||
|
||||
Create a `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
dumbdrop:
|
||||
image: dumbwareio/dumbdrop:latest
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
# Where your uploaded files will land
|
||||
- ./uploads:/app/uploads
|
||||
environment:
|
||||
# Explicitly set upload directory inside the container
|
||||
UPLOAD_DIR: /app/uploads
|
||||
# The title shown in the web interface
|
||||
DUMBDROP_TITLE: DumbDrop
|
||||
# Maximum file size in MB
|
||||
MAX_FILE_SIZE: 1024
|
||||
# Optional PIN protection (leave empty to disable)
|
||||
DUMBDROP_PIN: 123456
|
||||
# Upload without clicking button
|
||||
AUTO_UPLOAD: false
|
||||
# The base URL for the application
|
||||
# You must update this to the url you use to access your site
|
||||
BASE_URL: http://localhost:3000
|
||||
dumbdrop:
|
||||
image: dumbwareio/dumbdrop:latest
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
# Where your uploaded files will land
|
||||
- ./uploads:/app/uploads
|
||||
environment:
|
||||
# Explicitly set upload directory inside the container
|
||||
UPLOAD_DIR: /app/uploads
|
||||
# The title shown in the web interface
|
||||
DUMBDROP_TITLE: DumbDrop
|
||||
# Maximum file size in MB
|
||||
MAX_FILE_SIZE: 1024
|
||||
# Optional PIN protection (leave empty to disable)
|
||||
DUMBDROP_PIN: 123456
|
||||
# Upload without clicking button
|
||||
AUTO_UPLOAD: false
|
||||
# The base URL for the application
|
||||
# You must update this to the url you use to access your site
|
||||
BASE_URL: http://localhost:3000
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
1. Go to http://localhost:3000
|
||||
2. Upload a File - It'll show up in ./uploads
|
||||
3. Rejoice in the glory of your dumb uploads
|
||||
@@ -90,21 +98,22 @@ For local development setup, troubleshooting, and advanced usage, see the dedica
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|------------------------|------------------------------------------------------------------|-----------------------------------------|----------|
|
||||
| PORT | Server port | 3000 | No |
|
||||
| BASE_URL | Base URL for the application | http://localhost:PORT | No |
|
||||
| MAX_FILE_SIZE | Maximum file size in MB | 1024 | No |
|
||||
| DUMBDROP_PIN | PIN protection (4-10 digits) | None | No |
|
||||
| DUMBDROP_TITLE | Site title displayed in header | DumbDrop | No |
|
||||
| APPRISE_URL | Apprise URL for notifications | None | No |
|
||||
| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No |
|
||||
| APPRISE_SIZE_UNIT | Size unit for notifications (B, KB, MB, GB, TB, or Auto) | Auto | No |
|
||||
| AUTO_UPLOAD | Enable automatic upload on file selection | false | No |
|
||||
| ALLOWED_EXTENSIONS | Comma-separated list of allowed file extensions | None | No |
|
||||
| ALLOWED_IFRAME_ORIGINS | Comma-separated list of origins allowed to embed the app in an iframe | None | No |
|
||||
| UPLOAD_DIR | Directory for uploads (Docker/production; should be `/app/uploads` in container) | None (see LOCAL_UPLOAD_DIR fallback) | No |
|
||||
| LOCAL_UPLOAD_DIR | Directory for uploads (local dev, fallback: './local_uploads') | ./local_uploads | No |
|
||||
| Variable | Description | Default | Required |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------- |
|
||||
| PORT | Server port | 3000 | No |
|
||||
| BASE_URL | Base URL for the application | http://localhost:PORT | No |
|
||||
| MAX_FILE_SIZE | Maximum file size in MB | 1024 | No |
|
||||
| DUMBDROP_PIN | PIN protection (4-10 digits) | None | No |
|
||||
| DUMBDROP_TITLE | Site title displayed in header | DumbDrop | No |
|
||||
| APPRISE_URL | Apprise URL for notifications | None | No |
|
||||
| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No |
|
||||
| APPRISE_SIZE_UNIT | Size unit for notifications (B, KB, MB, GB, TB, or Auto) | Auto | No |
|
||||
| AUTO_UPLOAD | Enable automatic upload on file selection | false | No |
|
||||
| ALLOWED_EXTENSIONS | Comma-separated list of allowed file extensions | None | No |
|
||||
| ALLOWED_IFRAME_ORIGINS (deprecated: see ALLOWED_ORIGINS) | Comma-separated list of origins allowed to embed the app in an iframe | None | No |
|
||||
| ALLOWED_ORIGINS | You can restrict CORS to your BASE_URL or a comma-separated list of specified origins, which will automatically include your base_url | '\*' | No |
|
||||
| UPLOAD_DIR | Directory for uploads (Docker/production; should be `/app/uploads` in container) | None (see LOCAL_UPLOAD_DIR fallback) | No |
|
||||
| LOCAL_UPLOAD_DIR | Directory for uploads (local dev, fallback: './local_uploads') | ./local_uploads | No |
|
||||
|
||||
- **UPLOAD_DIR** is used in Docker/production. If not set, LOCAL_UPLOAD_DIR is used for local development. If neither is set, the default is `./local_uploads`.
|
||||
- **Docker Note:** The Dockerfile now only creates the `uploads` directory inside the container. The host's `./local_uploads` is mounted to `/app/uploads` and should be managed on the host system.
|
||||
@@ -114,50 +123,76 @@ For local development setup, troubleshooting, and advanced usage, see the dedica
|
||||
See `.env.example` for a template and more details.
|
||||
|
||||
<details>
|
||||
<summary>ALLOWED_IFRAME_ORIGINS</summary>
|
||||
<summary>ALLOWED_IFRAME_ORIGINS (DEPRECATED: see ALLOWED_ORIGINS)</summary>
|
||||
|
||||
To allow this app to be embedded in an iframe on specific origins (such as Organizr), set the `ALLOWED_IFRAME_ORIGINS` environment variable. For example:
|
||||
- This is now deprecated but still works for backwards compatibility
|
||||
- ALLOWED_IFRAME_ORIGINS will be used as a fallback if ALLOWED_ORIGINS is not set
|
||||
- Please update to ALLOWED_ORIGINS for future compatibility
|
||||
|
||||
~~To allow this app to be embedded in an iframe on specific origins (such as Organizr), set the `ALLOWED_IFRAME_ORIGINS` environment variable. For example:~~
|
||||
|
||||
```env
|
||||
ALLOWED_IFRAME_ORIGINS=https://organizr.example.com,https://myportal.com
|
||||
```
|
||||
|
||||
- If not set, the app will only allow itself to be embedded in an iframe on the same origin (default security).
|
||||
- If set, the app will allow embedding in iframes on the specified origins and itself.
|
||||
- **Security Note:** Only add trusted origins. Allowing arbitrary origins can expose your app to clickjacking and other attacks.
|
||||
- ~~If not set, the app will only allow itself to be embedded in an iframe on the same origin (default security).~~
|
||||
- ~~If set, the app will allow embedding in iframes on the specified origins and itself.~~
|
||||
- ~~**Security Note:** Only add trusted origins. Allowing arbitrary origins can expose your app to clickjacking and other attacks.~~
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ALLOWED_ORIGINS</summary>
|
||||
|
||||
By default `ALLOWED_ORIGINS` is set to '\*'
|
||||
|
||||
```env
|
||||
ALLOWED_ORIGINS=https://organizr.example.com,https://myportal.com,http://internalip:port
|
||||
```
|
||||
|
||||
- If you would like to restrict CORS to your BASE_URL, you can set it like this: `ALLOWED_ORIGINS=http://localhost:3000`
|
||||
- If you would like to allow multiple origins, you can set it like this: `ALLOWED_ORIGINS=http://internalip:port,https://subdomain.domain.tld`
|
||||
- This will automatically include your BASE_URL in the list of allowed origins.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>File Extension Filtering</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Notification Setup</summary>
|
||||
|
||||
#### Message Templates
|
||||
|
||||
The notification message supports the following placeholders:
|
||||
|
||||
- `{filename}`: Name of the uploaded file
|
||||
- `{size}`: Size of the file (formatted according to APPRISE_SIZE_UNIT)
|
||||
- `{storage}`: Total size of all files in upload directory
|
||||
|
||||
Example message template:
|
||||
|
||||
```env
|
||||
APPRISE_MESSAGE: New file uploaded {filename} ({size}), Storage used {storage}
|
||||
```
|
||||
|
||||
Size formatting examples:
|
||||
|
||||
- Auto (default): Chooses nearest unit (e.g., "1.44MB", "256KB")
|
||||
- Fixed unit: Set APPRISE_SIZE_UNIT to B, KB, MB, GB, or TB
|
||||
|
||||
Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UNIT.
|
||||
|
||||
#### Notification Support
|
||||
|
||||
- Integration with [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#supported-notifications) for flexible notifications
|
||||
- Support for all Apprise notification services
|
||||
- Customizable notification messages with filename templating
|
||||
@@ -167,6 +202,7 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN
|
||||
## Security
|
||||
|
||||
### Features
|
||||
|
||||
- Variable-length PIN support (4-10 digits)
|
||||
- Constant-time PIN comparison
|
||||
- Input sanitization
|
||||
@@ -178,6 +214,7 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN
|
||||
## Technical Details
|
||||
|
||||
### Stack
|
||||
|
||||
- **Backend**: Node.js (>=20.0.0) with Express
|
||||
- **Frontend**: Vanilla JavaScript (ES6+)
|
||||
- **Container**: Docker with multi-stage builds
|
||||
@@ -186,6 +223,7 @@ Both {size} and {storage} use the same formatting rules based on APPRISE_SIZE_UN
|
||||
- **Notifications**: Apprise integration
|
||||
|
||||
### Dependencies
|
||||
|
||||
- express: Web framework
|
||||
- multer: File upload handling
|
||||
- apprise: Notification system
|
||||
@@ -210,8 +248,10 @@ See [Local Development (Recommended Quick Start)](LOCAL_DEVELOPMENT.md) for loca
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by [DumbWare.io](https://dumbware.io)
|
||||
|
||||
## Future Features
|
||||
|
||||
- Camera Upload for Mobile
|
||||
> Got an idea? [Open an issue](https://github.com/dumbwareio/dumbdrop/issues) or [submit a PR](https://github.com/dumbwareio/dumbdrop/pulls)
|
||||
> Got an idea? [Open an issue](https://github.com/dumbwareio/dumbdrop/issues) or [submit a PR](https://github.com/dumbwareio/dumbdrop/pulls)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
services:
|
||||
dumbdrop:
|
||||
image: dumbwareio/dumbdrop:latest
|
||||
# build: .
|
||||
container_name: dumbdrop
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
@@ -15,9 +18,15 @@ services:
|
||||
AUTO_UPLOAD: true # Upload without clicking button
|
||||
BASE_URL: http://localhost:3000 # The base URL for the application, You must update this to the url you use to access your site
|
||||
|
||||
# Comma-separated list of allowed origins for CORS
|
||||
# (default: '*' if empty, replace with your base_url if you want to restrict only to base_url)
|
||||
# When adding multiple origins, base_url will be included by default and does not need to the list
|
||||
# ALLOWED_IFRAME_ORIGINS: #DEPRECATED and will be used as ALLOWED_ORIGINS if SET
|
||||
# ALLOWED_ORIGINS: http://internalip:port,https://subdomain.example.com
|
||||
|
||||
# Additional available environment variables (commented out with defaults)
|
||||
# PORT: 3000 # Server port (default: 3000)
|
||||
# NODE_ENV: production # Node environment (development/production)
|
||||
# NODE_ENV: production # Node environment (development/production) - when not using production ALLOWED_ORIGINS will be set to '*' by default
|
||||
# DEBUG: false # Debug mode for verbose logging (default: false in production, true in development)
|
||||
# APPRISE_URL: "" # Apprise notification URL for upload notifications (default: none)
|
||||
# APPRISE_MESSAGE: "New file uploaded - {filename} ({size}), Storage used {storage}" # Notification message template with placeholders: {filename}, {size}, {storage}
|
||||
|
38
package-lock.json
generated
38
package-lock.json
generated
@@ -15,7 +15,9 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
"helmet": "^8.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"toastify-js": "^1.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
@@ -188,9 +190,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -356,9 +358,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -625,9 +627,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1423,6 +1425,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -1801,6 +1812,7 @@
|
||||
"version": "1.4.5-lts.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
||||
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
@@ -2658,6 +2670,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toastify-js": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
|
||||
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
@@ -21,7 +21,9 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
"helmet": "^8.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"toastify-js": "^1.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
|
@@ -4,12 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{SITE_TITLE}} - Simple File Upload</title>
|
||||
<link rel="stylesheet" href="{{BASE_URL}}styles.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
<link rel="manifest" href="{{BASE_URL}}manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="{{BASE_URL}}assets/icon.svg">
|
||||
<script>window.BASE_URL = '{{BASE_URL}}';</script>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="toastify/toastify.css">
|
||||
<script src="toastify/toastify.js"></script>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="assets/icon.svg">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -156,7 +155,7 @@
|
||||
|
||||
// Remove leading slash from API path before concatenating
|
||||
const apiUrl = '/api/upload/init'.startsWith('/') ? '/api/upload/init'.substring(1) : '/api/upload/init';
|
||||
const response = await fetch(window.BASE_URL + apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
@@ -219,7 +218,7 @@
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30-second timeout per attempt
|
||||
|
||||
const response = await fetch(window.BASE_URL + chunkApiUrl, {
|
||||
const response = await fetch(chunkApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -395,7 +394,7 @@
|
||||
const cancelApiUrlPath = `/api/upload/cancel/${this.uploadId}`;
|
||||
const cancelApiUrl = cancelApiUrlPath.startsWith('/') ? cancelApiUrlPath.substring(1) : cancelApiUrlPath;
|
||||
// No need to wait for response here, just fire and forget
|
||||
fetch(window.BASE_URL + cancelApiUrl, { method: 'POST' }).catch(err => {
|
||||
fetch(cancelApiUrl, { method: 'POST' }).catch(err => {
|
||||
console.warn(`Sending cancel request failed for upload ${this.uploadId}:`, err);
|
||||
});
|
||||
} catch (cancelError) {
|
||||
|
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{SITE_TITLE}} - Login</title>
|
||||
<link rel="stylesheet" href="{{BASE_URL}}styles.css">
|
||||
<link rel="icon" type="image/svg+xml" href="{{BASE_URL}}assets/icon.svg">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" type="image/svg+xml" href="assets/icon.svg">
|
||||
<style>
|
||||
.login-container {
|
||||
display: flex;
|
||||
@@ -54,7 +54,6 @@
|
||||
background-color: var(--textarea-bg);
|
||||
}
|
||||
</style>
|
||||
<script>window.BASE_URL = '{{BASE_URL}}';</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
@@ -126,10 +125,12 @@
|
||||
// Handle form submission
|
||||
const verifyPin = async (pin) => {
|
||||
try {
|
||||
const response = await fetch(window.BASE_URL + 'api/auth/verify-pin', {
|
||||
const response = await fetch('/api/auth/verify-pin', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pin })
|
||||
body: JSON.stringify({ pin }),
|
||||
credentials: 'include', // Ensure cookies are sent
|
||||
// redirect: 'follow' // Follow server redirects
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
@@ -212,7 +213,7 @@
|
||||
};
|
||||
|
||||
// Check PIN length and initialize
|
||||
fetch(window.BASE_URL + 'api/auth/pin-required')
|
||||
fetch('/api/auth/pin-required')
|
||||
.then(response => {
|
||||
if (response.status === 429) {
|
||||
throw new Error('Too many attempts. Please wait before trying again.');
|
||||
@@ -241,17 +242,6 @@
|
||||
pinContainer.style.pointerEvents = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Rewrite asset URLs to use BASE_URL as prefix if not absolute
|
||||
const baseUrl = window.BASE_URL;
|
||||
document.querySelectorAll('link[rel="stylesheet"], link[rel="icon"]').forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && !href.startsWith('http') && !href.startsWith('data:') && !href.startsWith(baseUrl)) {
|
||||
link.setAttribute('href', baseUrl + href.replace(/^\//, ''));
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
57
src/app.js
57
src/app.js
@@ -6,6 +6,7 @@
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -14,32 +15,66 @@ const fsPromises = require('fs').promises;
|
||||
const { config, validateConfig } = require('./config');
|
||||
const logger = require('./utils/logger');
|
||||
const { ensureDirectoryExists } = require('./utils/fileUtils');
|
||||
const { securityHeaders, requirePin } = require('./middleware/security');
|
||||
const { getHelmetConfig, requirePin } = require('./middleware/security');
|
||||
const { safeCompare } = require('./utils/security');
|
||||
const { initUploadLimiter, pinVerifyLimiter, downloadLimiter } = require('./middleware/rateLimiter');
|
||||
const { initUploadLimiter, pinVerifyLimiter, pinStatusLimiter, downloadLimiter } = require('./middleware/rateLimiter');
|
||||
const { injectDemoBanner, demoMiddleware } = require('./utils/demoMode');
|
||||
const { originValidationMiddleware, getCorsOptions } = require('./middleware/cors');
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
|
||||
|
||||
// Add this line to trust the first proxy
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// Middleware setup
|
||||
app.use(cors());
|
||||
app.use(cors(getCorsOptions(BASE_URL)));
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
app.use(securityHeaders);
|
||||
app.use(helmet(getHelmetConfig()));
|
||||
|
||||
// --- AUTHENTICATION MIDDLEWARE FOR ALL PROTECTED ROUTES ---
|
||||
app.use((req, res, next) => {
|
||||
// List of paths that should be publicly accessible
|
||||
const publicPaths = [
|
||||
'/login',
|
||||
'/login.html',
|
||||
'/api/auth/logout',
|
||||
'/api/auth/verify-pin',
|
||||
'/api/auth/pin-required',
|
||||
'/api/auth/pin-length',
|
||||
'/pin-length',
|
||||
'/verify-pin',
|
||||
'/config.js',
|
||||
'/assets/',
|
||||
'/styles.css',
|
||||
'/manifest.json',
|
||||
'/asset-manifest.json',
|
||||
'/toastify',
|
||||
];
|
||||
|
||||
// Check if the current path matches any of the public paths
|
||||
if (publicPaths.some(path => req.path.startsWith(path))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// For all other paths, apply both origin validation and auth middleware
|
||||
originValidationMiddleware(req, res, () => {
|
||||
demoMiddleware(req, res, next);
|
||||
});
|
||||
});
|
||||
|
||||
// Import routes
|
||||
const { router: uploadRouter } = require('./routes/upload');
|
||||
const fileRoutes = require('./routes/files');
|
||||
const authRoutes = require('./routes/auth');
|
||||
|
||||
// Add demo middleware before your routes
|
||||
app.use(demoMiddleware);
|
||||
|
||||
// Use routes with appropriate middleware
|
||||
// Apply strict rate limiting to PIN verification, but more permissive to status checks
|
||||
app.use('/api/auth/pin-required', pinStatusLimiter);
|
||||
app.use('/api/auth/logout', pinStatusLimiter);
|
||||
app.use('/api/auth', pinVerifyLimiter, authRoutes);
|
||||
app.use('/api/upload', requirePin(config.pin), initUploadLimiter, uploadRouter);
|
||||
app.use('/api/files', requirePin(config.pin), downloadLimiter, fileRoutes);
|
||||
@@ -55,9 +90,6 @@ app.get('/', (req, res) => {
|
||||
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
||||
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
|
||||
html = html.replace('{{MAX_RETRIES}}', config.clientMaxRetries.toString());
|
||||
// Ensure baseUrl has a trailing slash for correct asset linking
|
||||
const baseUrlWithSlash = config.baseUrl.endsWith('/') ? config.baseUrl : config.baseUrl + '/';
|
||||
html = html.replace(/{{BASE_URL}}/g, baseUrlWithSlash);
|
||||
html = injectDemoBanner(html);
|
||||
res.send(html);
|
||||
});
|
||||
@@ -71,9 +103,6 @@ app.get('/login.html', (req, res) => {
|
||||
|
||||
let html = fs.readFileSync(path.join(__dirname, '../public', 'login.html'), 'utf8');
|
||||
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
||||
// Ensure baseUrl has a trailing slash
|
||||
const baseUrlWithSlash = config.baseUrl.endsWith('/') ? config.baseUrl : config.baseUrl + '/';
|
||||
html = html.replace(/{{BASE_URL}}/g, baseUrlWithSlash);
|
||||
html = injectDemoBanner(html);
|
||||
res.send(html);
|
||||
});
|
||||
@@ -104,6 +133,8 @@ app.use((req, res, next) => {
|
||||
|
||||
// Serve remaining static files
|
||||
app.use(express.static('public'));
|
||||
// Serve Toastify assets under /toastify
|
||||
app.use('/toastify', express.static(path.join(__dirname, '../node_modules/toastify-js/src')));
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
|
||||
|
@@ -1,16 +1,5 @@
|
||||
require('dotenv').config();
|
||||
console.log('Loaded ENV:', {
|
||||
PORT: process.env.PORT,
|
||||
UPLOAD_DIR: process.env.UPLOAD_DIR,
|
||||
LOCAL_UPLOAD_DIR: process.env.LOCAL_UPLOAD_DIR,
|
||||
NODE_ENV: process.env.NODE_ENV
|
||||
});
|
||||
console.log('Loaded ENV:', {
|
||||
PORT: process.env.PORT,
|
||||
UPLOAD_DIR: process.env.UPLOAD_DIR,
|
||||
LOCAL_UPLOAD_DIR: process.env.LOCAL_UPLOAD_DIR,
|
||||
NODE_ENV: process.env.NODE_ENV
|
||||
});
|
||||
|
||||
const { validatePin } = require('../utils/security');
|
||||
const logger = require('../utils/logger');
|
||||
const fs = require('fs');
|
||||
@@ -33,7 +22,6 @@ const { version } = require('../../package.json'); // Get version from package.j
|
||||
* APPRISE_MESSAGE - Notification message template (default provided)
|
||||
* APPRISE_SIZE_UNIT - Size unit for notifications (optional)
|
||||
* ALLOWED_EXTENSIONS - Comma-separated list of allowed file extensions (optional)
|
||||
* ALLOWED_IFRAME_ORIGINS - Comma-separated list of allowed iframe origins (optional)
|
||||
*/
|
||||
|
||||
// Helper for clear configuration logging
|
||||
@@ -43,12 +31,20 @@ const logConfig = (message, level = 'info') => {
|
||||
};
|
||||
|
||||
// Default configurations
|
||||
const DEFAULT_PORT = 3000;
|
||||
const DEFAULT_CHUNK_SIZE = 1024 * 1024 * 100; // 100MB
|
||||
const DEFAULT_SITE_TITLE = 'DumbDrop';
|
||||
const DEFAULT_BASE_URL = 'http://localhost:3000';
|
||||
const NODE_ENV = process.env.NODE_ENV || 'production';
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
|
||||
const DEFAULT_CLIENT_MAX_RETRIES = 5; // Default retry count
|
||||
|
||||
console.log('Loaded ENV:', {
|
||||
PORT,
|
||||
UPLOAD_DIR: process.env.UPLOAD_DIR,
|
||||
LOCAL_UPLOAD_DIR: process.env.LOCAL_UPLOAD_DIR,
|
||||
NODE_ENV,
|
||||
BASE_URL,
|
||||
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*',
|
||||
});
|
||||
const logAndReturn = (key, value, isDefault = false) => {
|
||||
logConfig(`${key}: ${value}${isDefault ? ' (default)' : ''}`);
|
||||
return value;
|
||||
@@ -83,7 +79,7 @@ function determineUploadDirectory() {
|
||||
* Returns true if NODE_ENV is not 'production' and UPLOAD_DIR is not set (i.e., not Docker)
|
||||
*/
|
||||
function isLocalDevelopment() {
|
||||
return process.env.NODE_ENV !== 'production' && !process.env.UPLOAD_DIR;
|
||||
return process.env.NODE_ENV !== 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,17 +117,17 @@ const config = {
|
||||
* Port for the server (default: 3000)
|
||||
* Set via PORT in .env
|
||||
*/
|
||||
port: process.env.PORT || DEFAULT_PORT,
|
||||
port: PORT,
|
||||
/**
|
||||
* Node environment (default: 'development')
|
||||
* Node environment (default: 'production')
|
||||
* Set via NODE_ENV in .env
|
||||
*/
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
nodeEnv: NODE_ENV,
|
||||
/**
|
||||
* Base URL for the app (default: http://localhost:${PORT})
|
||||
* Set via BASE_URL in .env
|
||||
*/
|
||||
baseUrl: process.env.BASE_URL || DEFAULT_BASE_URL,
|
||||
baseUrl: BASE_URL,
|
||||
|
||||
// =====================
|
||||
// =====================
|
||||
@@ -211,10 +207,6 @@ const config = {
|
||||
process.env.ALLOWED_EXTENSIONS.split(',').map(ext => ext.trim().toLowerCase()) :
|
||||
null,
|
||||
|
||||
allowedIframeOrigins: process.env.ALLOWED_IFRAME_ORIGINS
|
||||
? process.env.ALLOWED_IFRAME_ORIGINS.split(',').map(origin => origin.trim()).filter(Boolean)
|
||||
: null,
|
||||
|
||||
/**
|
||||
* Max number of retries for client-side chunk uploads (default: 5)
|
||||
* Set via CLIENT_MAX_RETRIES in .env
|
||||
@@ -251,7 +243,6 @@ function validateConfig() {
|
||||
|
||||
// Validate BASE_URL format
|
||||
try {
|
||||
let url = new URL(config.baseUrl);
|
||||
// Ensure BASE_URL ends with a slash
|
||||
if (!config.baseUrl.endsWith('/')) {
|
||||
logger.warn('BASE_URL did not end with a trailing slash. Automatically appending "/".');
|
||||
|
84
src/middleware/cors.js
Normal file
84
src/middleware/cors.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS || process.env.ALLOWED_IFRAME_ORIGINS || '*';
|
||||
const NODE_ENV = process.env.NODE_ENV || 'production';
|
||||
let allowedOrigins = [];
|
||||
|
||||
function setupOrigins(baseUrl) {
|
||||
const normalizedBaseUrl = normalizeOrigin(baseUrl);
|
||||
allowedOrigins = [ normalizedBaseUrl ];
|
||||
|
||||
if (NODE_ENV === 'development' || ALLOWED_ORIGINS === '*') allowedOrigins = '*';
|
||||
else if (ALLOWED_ORIGINS && typeof ALLOWED_ORIGINS === 'string') {
|
||||
try {
|
||||
const allowed = ALLOWED_ORIGINS.split(',').map(origin => origin.trim());
|
||||
allowed.forEach(origin => {
|
||||
const normalizedOrigin = normalizeOrigin(origin);
|
||||
if (normalizedOrigin !== baseUrl) allowedOrigins.push(normalizedOrigin);
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error setting up ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}:`, error);
|
||||
}
|
||||
}
|
||||
console.log("ALLOWED ORIGINS:", allowedOrigins);
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
function normalizeOrigin(origin) {
|
||||
if (origin) {
|
||||
try {
|
||||
const normalizedOrigin = new URL(origin).origin;
|
||||
return normalizedOrigin;
|
||||
} catch (error) {
|
||||
console.error("Error parsing referer URL:", error);
|
||||
throw new Error("Error parsing referer URL:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateOrigin(origin) {
|
||||
if (NODE_ENV === 'development' || allowedOrigins === '*') return true;
|
||||
|
||||
try {
|
||||
if (origin) origin = normalizeOrigin(origin);
|
||||
else {
|
||||
console.warn("No origin to validate.");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Validating Origin:", origin);
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
console.log("Allowed request from origin:", origin);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
console.warn("Blocked request from origin:", origin);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function originValidationMiddleware(req, res, next) {
|
||||
const origin = req.headers.origin || req.headers.referer || `${req.protocol}://${req.headers.host}`;
|
||||
const isOriginValid = validateOrigin(origin);
|
||||
if (isOriginValid) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
}
|
||||
|
||||
function getCorsOptions(baseUrl) {
|
||||
const allowedOrigins = setupOrigins(baseUrl);
|
||||
const corsOptions = {
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Pin', 'X-Batch-Id'],
|
||||
};
|
||||
return corsOptions;
|
||||
}
|
||||
|
||||
module.exports = { getCorsOptions, originValidationMiddleware, validateOrigin, allowedOrigins };
|
@@ -48,7 +48,7 @@ const chunkUploadLimiter = createLimiter({
|
||||
|
||||
/**
|
||||
* Rate limiter for PIN verification attempts
|
||||
* Prevents brute force attacks
|
||||
* Prevents brute force attacks on actual PIN verification
|
||||
*/
|
||||
const pinVerifyLimiter = createLimiter({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
@@ -57,6 +57,24 @@ const pinVerifyLimiter = createLimiter({
|
||||
error: 'Too many PIN verification attempts. Please try again later.'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
// Apply strict rate limiting only to PIN verification, not PIN status checks
|
||||
skip: (req) => {
|
||||
return req.path === '/pin-required'; // Skip rate limiting for PIN requirement checks
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter for PIN status checks
|
||||
* More permissive for checking if PIN is required
|
||||
*/
|
||||
const pinStatusLimiter = createLimiter({
|
||||
windowMs: 60 * 1000, // 1 minute window
|
||||
max: 30, // 30 requests per minute
|
||||
message: {
|
||||
error: 'Too many requests. Please wait before trying again.'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
});
|
||||
|
||||
@@ -78,5 +96,6 @@ module.exports = {
|
||||
initUploadLimiter,
|
||||
chunkUploadLimiter,
|
||||
pinVerifyLimiter,
|
||||
pinStatusLimiter,
|
||||
downloadLimiter
|
||||
};
|
@@ -6,41 +6,67 @@
|
||||
|
||||
const { safeCompare } = require('../utils/security');
|
||||
const logger = require('../utils/logger');
|
||||
const { config } = require('../config');
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const NODE_ENV = process.env.NODE_ENV || 'production';
|
||||
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
|
||||
|
||||
// const { config } = require('../config');
|
||||
/**
|
||||
* Security headers middleware
|
||||
* DEPRECATED: Use helmet middleware instead for security headers
|
||||
*/
|
||||
function securityHeaders(req, res, next) {
|
||||
// Content Security Policy
|
||||
let csp =
|
||||
"default-src 'self'; " +
|
||||
"connect-src 'self'; " +
|
||||
"style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
|
||||
"script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
|
||||
"img-src 'self' data: blob:;";
|
||||
// function securityHeaders(req, res, next) {
|
||||
// // Content Security Policy
|
||||
// let csp =
|
||||
// "default-src 'self'; " +
|
||||
// "connect-src 'self'; " +
|
||||
// "style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
|
||||
// "script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; " +
|
||||
// "img-src 'self' data: blob:;";
|
||||
|
||||
// If allowedIframeOrigins is set, allow those origins to embed via iframe
|
||||
if (config.allowedIframeOrigins && config.allowedIframeOrigins.length > 0) {
|
||||
// Remove X-Frame-Options header (do not set it)
|
||||
// Add frame-ancestors directive to CSP
|
||||
const frameAncestors = ["'self'", ...config.allowedIframeOrigins].join(' ');
|
||||
csp += ` frame-ancestors ${frameAncestors};`;
|
||||
} else {
|
||||
// Default: only allow same origin if not configured
|
||||
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
// // If allowedIframeOrigins is set, allow those origins to embed via iframe
|
||||
// if (config.allowedIframeOrigins && config.allowedIframeOrigins.length > 0) {
|
||||
// // Remove X-Frame-Options header (do not set it)
|
||||
// // Add frame-ancestors directive to CSP
|
||||
// const frameAncestors = ["'self'", ...config.allowedIframeOrigins].join(' ');
|
||||
// csp += ` frame-ancestors ${frameAncestors};`;
|
||||
// } else {
|
||||
// // Default: only allow same origin if not configured
|
||||
// res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
// }
|
||||
|
||||
res.setHeader('Content-Security-Policy', csp);
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
// res.setHeader('Content-Security-Policy', csp);
|
||||
// res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
// 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');
|
||||
}
|
||||
// // Strict Transport Security (when in production)
|
||||
// if (process.env.NODE_ENV === 'production') {
|
||||
// res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
// }
|
||||
|
||||
next();
|
||||
// next();
|
||||
// }
|
||||
|
||||
function getHelmetConfig() {
|
||||
// const isSecure = BASE_URL.startsWith('https://');
|
||||
|
||||
return {
|
||||
noSniff: true, // Prevent MIME type sniffing
|
||||
frameguard: { action: 'deny' }, // Prevent clickjacking
|
||||
crossOriginEmbedderPolicy: false, // Disable for local network access
|
||||
crossOriginOpenerPolicy: false, // Disable to prevent warnings on HTTP
|
||||
crossOriginResourcePolicy: { policy: 'cross-origin' }, // Allow cross-origin for local network
|
||||
referrerPolicy: { policy: 'no-referrer-when-downgrade' }, // Set referrer policy
|
||||
ieNoOpen: true, // Prevent IE from executing downloads
|
||||
// hsts: isSecure ? { maxAge: 31536000, includeSubDomains: true } : false, // Only enforce HTTPS if using HTTPS
|
||||
// Disabled Helmet middlewares:
|
||||
hsts: false,
|
||||
contentSecurityPolicy: false, // Disable CSP for now
|
||||
dnsPrefetchControl: true, // Disable DNS prefetching
|
||||
permittedCrossDomainPolicies: false,
|
||||
originAgentCluster: false,
|
||||
xssFilter: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +92,7 @@ function requirePin(PIN) {
|
||||
// Set cookie for subsequent requests with enhanced security
|
||||
const cookieOptions = {
|
||||
httpOnly: true, // Always enable HttpOnly
|
||||
secure: req.secure || req.headers['x-forwarded-proto'] === 'https', // Enable secure flag only if the request is over HTTPS
|
||||
secure: req.secure || (BASE_URL.startsWith('https') && NODE_ENV === 'production'),
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hour expiry
|
||||
@@ -82,6 +108,7 @@ function requirePin(PIN) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
securityHeaders,
|
||||
// securityHeaders, // Deprecated, use helmet instead
|
||||
getHelmetConfig,
|
||||
requirePin
|
||||
};
|
@@ -11,7 +11,9 @@ const {
|
||||
MAX_ATTEMPTS,
|
||||
LOCKOUT_DURATION
|
||||
} = require('../utils/security');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const NODE_ENV = process.env.NODE_ENV || 'production';
|
||||
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
|
||||
/**
|
||||
* Verify PIN
|
||||
*/
|
||||
@@ -22,13 +24,14 @@ router.post('/verify-pin', (req, res) => {
|
||||
try {
|
||||
// If no PIN is set in config, always return success
|
||||
if (!config.pin) {
|
||||
res.cookie('DUMBDROP_PIN', '', {
|
||||
httpOnly: true,
|
||||
secure: req.secure || (process.env.NODE_ENV === 'production' && config.baseUrl.startsWith('https')),
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
return res.json({ success: true, error: null });
|
||||
// res.cookie('DUMBDROP_PIN', '', {
|
||||
// httpOnly: true,
|
||||
// secure: req.secure || (BASE_URL.startsWith('https') && NODE_ENV === 'production'),
|
||||
// sameSite: 'strict',
|
||||
// path: '/'
|
||||
// });
|
||||
res.clearCookie('DUMBDROP_PIN', { path: '/' });
|
||||
return res.json({ success: true, error: null, path: '/' });
|
||||
}
|
||||
|
||||
// Validate PIN format
|
||||
@@ -63,7 +66,7 @@ router.post('/verify-pin', (req, res) => {
|
||||
// Set secure cookie with cleaned PIN
|
||||
res.cookie('DUMBDROP_PIN', cleanedPin, {
|
||||
httpOnly: true,
|
||||
secure: req.secure || (process.env.NODE_ENV === 'production' && config.baseUrl.startsWith('https')),
|
||||
secure: req.secure || (BASE_URL.startsWith('https') && NODE_ENV === 'production'),
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
|
@@ -22,10 +22,11 @@ async function startServer() {
|
||||
// Initialize the application
|
||||
await initialize();
|
||||
|
||||
// Start the server
|
||||
const server = app.listen(config.port, () => {
|
||||
// Start the server - bind to 0.0.0.0 for Docker compatibility
|
||||
const server = app.listen(config.port, '0.0.0.0', () => {
|
||||
logger.info(`Server running at ${config.baseUrl}`);
|
||||
logger.info(`Upload directory: ${config.uploadDisplayPath}`);
|
||||
logger.info(`Server listening on 0.0.0.0:${config.port}`);
|
||||
logger.info(`Upload directory: ${config.uploadDir}`);
|
||||
|
||||
// List directory contents in development
|
||||
if (config.nodeEnv === 'development') {
|
||||
|
Reference in New Issue
Block a user