Initial commit.

This commit is contained in:
retoor 2025-10-03 02:09:53 +02:00
commit ce8012779b
14 changed files with 4514 additions and 0 deletions

183
.env.example Normal file
View File

@ -0,0 +1,183 @@
# ============================================================================
# WebDAV Server Configuration
# Copy this file to .env and modify as needed
# ============================================================================
# ----------------------------------------------------------------------------
# Server Configuration
# ----------------------------------------------------------------------------
HOST=0.0.0.0
PORT=8080
# SSL/TLS Configuration (recommended for production)
SSL_ENABLED=false
SSL_CERT_PATH=/path/to/cert.pem
SSL_KEY_PATH=/path/to/key.pem
# ----------------------------------------------------------------------------
# Database Configuration
# ----------------------------------------------------------------------------
DB_PATH=./webdav.db
DB_BACKUP_INTERVAL=3600
DB_VACUUM_INTERVAL=86400
# ----------------------------------------------------------------------------
# Authentication Configuration
# ----------------------------------------------------------------------------
# Supported methods: basic, digest, token (comma-separated)
AUTH_METHODS=basic,digest
# Secret key for JWT and session encryption (generate with: python -c "import secrets; print(secrets.token_hex(32))")
JWT_SECRET_KEY=your-secret-key-here-change-this-in-production
# Session timeout in seconds (1 hour default)
SESSION_TIMEOUT=3600
# Password requirements
PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_NUMBERS=true
PASSWORD_REQUIRE_SPECIAL=true
# ----------------------------------------------------------------------------
# WebDAV Configuration
# ----------------------------------------------------------------------------
# WebDAV root directory (where user files are stored)
WEBDAV_ROOT=./webdav
# Maximum file size in bytes (100MB default)
MAX_FILE_SIZE=104857600
# Maximum PROPFIND depth (3 default, use lower value for better performance)
MAX_PROPFIND_DEPTH=3
# Default lock timeout in seconds (1 hour default)
LOCK_TIMEOUT_DEFAULT=3600
# Windows compatibility mode (recommended: true)
ENABLE_WINDOWS_COMPATIBILITY=true
# ----------------------------------------------------------------------------
# Performance Configuration
# ----------------------------------------------------------------------------
# Maximum concurrent connections
MAX_CONNECTIONS=1000
# Keep-alive timeout in seconds
KEEPALIVE_TIMEOUT=30
# Client timeout in seconds
CLIENT_TIMEOUT=60
# Worker connections (for production deployment)
WORKER_CONNECTIONS=1024
# Number of worker processes (for gunicorn)
WORKERS=4
# ----------------------------------------------------------------------------
# Logging Configuration
# ----------------------------------------------------------------------------
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_LEVEL=INFO
# Log file paths
LOG_FILE=./logs/webdav.log
ACCESS_LOG_FILE=./logs/access.log
ERROR_LOG_FILE=./logs/error.log
# Enable/disable specific logs
ACCESS_LOG_ENABLED=true
ERROR_LOG_ENABLED=true
DEBUG_LOG_ENABLED=false
# ----------------------------------------------------------------------------
# Security Configuration
# ----------------------------------------------------------------------------
# CORS settings
CORS_ENABLED=false
CORS_ORIGINS=*
CORS_METHODS=GET,POST,PUT,DELETE,OPTIONS,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK
# Rate limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=60
# Brute force protection
MAX_LOGIN_ATTEMPTS=5
LOGIN_ATTEMPT_WINDOW=300
ACCOUNT_LOCKOUT_DURATION=900
# IP whitelist/blacklist (comma-separated)
IP_WHITELIST=
IP_BLACKLIST=
# ----------------------------------------------------------------------------
# Storage Configuration
# ----------------------------------------------------------------------------
# User storage quotas in bytes (0 = unlimited)
DEFAULT_USER_QUOTA=10737418240 # 10GB
# Shared folder configuration
SHARED_FOLDER_ENABLED=true
PUBLIC_FOLDER_ENABLED=false
# File versioning
ENABLE_FILE_VERSIONING=false
MAX_FILE_VERSIONS=5
# ----------------------------------------------------------------------------
# Cache Configuration (Optional - requires Redis)
# ----------------------------------------------------------------------------
CACHE_ENABLED=false
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=
# Cache TTL in seconds
CACHE_TTL_SESSION=3600
CACHE_TTL_LOCKS=1800
CACHE_TTL_PROPERTIES=300
# ----------------------------------------------------------------------------
# Monitoring and Metrics (Optional)
# ----------------------------------------------------------------------------
METRICS_ENABLED=false
PROMETHEUS_PORT=9090
# Health check endpoint
HEALTHCHECK_ENABLED=true
HEALTHCHECK_PATH=/health
# ----------------------------------------------------------------------------
# Email Configuration (for notifications)
# ----------------------------------------------------------------------------
EMAIL_ENABLED=false
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_FROM=webdav@example.com
SMTP_USE_TLS=true
# ----------------------------------------------------------------------------
# Backup Configuration
# ----------------------------------------------------------------------------
BACKUP_ENABLED=true
BACKUP_PATH=./backups
BACKUP_SCHEDULE=0 2 * * * # Daily at 2 AM (cron format)
BACKUP_RETENTION_DAYS=30
# ----------------------------------------------------------------------------
# Development Settings
# ----------------------------------------------------------------------------
# Enable debug mode (DO NOT use in production)
DEBUG=false
# Auto-reload on code changes (development only)
AUTO_RELOAD=false
# Enable detailed error messages
DETAILED_ERRORS=false

78
Dockerfile Normal file
View File

@ -0,0 +1,78 @@
# Multi-stage Dockerfile for WebDAV Server
# Optimized for production deployment
# ============================================================================
# Stage 1: Builder - Install dependencies and prepare environment
# ============================================================================
FROM python:3.11-slim as builder
# Set working directory
WORKDIR /build
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
libxml2-dev \
libxslt-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies to a target directory
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ============================================================================
# Stage 2: Runtime - Create minimal production image
# ============================================================================
FROM python:3.11-slim
# Set labels for metadata
LABEL maintainer="WebDAV Server <webdav@example.com>"
LABEL description="Production-ready WebDAV Server with aiohttp"
LABEL version="1.0.0"
# Create app user for security (don't run as root)
RUN groupadd -r webdav && useradd -r -g webdav webdav
# Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \
libxml2 \
libxslt1.1 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy Python packages from builder
COPY --from=builder /install /usr/local
# Copy application files
COPY main.py .
COPY config.py .
COPY .env.example .env
# Create necessary directories
RUN mkdir -p /app/webdav /app/logs /app/backups && \
chown -R webdav:webdav /app
# Switch to non-root user
USER webdav
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/ || exit 1
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV HOST=0.0.0.0
ENV PORT=8080
# Run application
CMD ["python", "main.py"]

450
QUICKSTART.md Normal file
View File

@ -0,0 +1,450 @@
# WebDAV Server - Quick Start Guide
Get your WebDAV server up and running in 5 minutes!
## 🚀 Instant Setup
### Option 1: Automated Installation (Recommended)
```bash
# Download and run the setup script
chmod +x setup.sh
./setup.sh
```
The script will:
- ✅ Check system requirements
- ✅ Install dependencies
- ✅ Create virtual environment
- ✅ Set up configuration
- ✅ Initialize database
- ✅ Create default admin user
### Option 2: Manual Installation
```bash
# 1. Create project directory
mkdir webdav-server && cd webdav-server
# 2. Create virtual environment
python3 -m venv venv
source venv/bin/activate
# 3. Install dependencies
pip install aiohttp aiofiles aiohttp-session cryptography python-dotenv lxml gunicorn
# 4. Copy the main.py file from the artifacts
# 5. Create .env file
cat > .env << 'EOF'
HOST=0.0.0.0
PORT=8080
DB_PATH=./webdav.db
WEBDAV_ROOT=./webdav
AUTH_METHODS=basic,digest
JWT_SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
SESSION_TIMEOUT=3600
EOF
# 6. Create directory structure
mkdir -p webdav/users logs backups
# 7. Run the server
python main.py
```
### Option 3: Docker (Fastest)
```bash
# 1. Create docker-compose.yml and Dockerfile from artifacts
# 2. Start with Docker Compose
docker-compose up -d
# 3. Check logs
docker-compose logs -f webdav
```
## 🎯 First Steps
### 1. Access the Server
**Default Credentials:**
- Username: `admin`
- Password: `admin123`
- URL: `http://localhost:8080/`
⚠️ **IMPORTANT**: Change the default password immediately!
### 2. Connect with Windows Explorer
**Method 1: Map Network Drive**
1. Open File Explorer
2. Right-click "This PC" → "Map network drive"
3. Enter: `http://localhost:8080/`
4. Enter credentials: `admin` / `admin123`
**Method 2: Add Network Location**
1. Right-click "This PC" → "Add a network location"
2. Choose custom network location
3. Enter: `http://localhost:8080/`
4. Enter credentials when prompted
**Windows Troubleshooting:**
- If connection fails, restart WebClient service:
```powershell
net stop webclient
net start webclient
```
### 3. Connect with macOS Finder
1. Press `Cmd+K` (Go → Connect to Server)
2. Enter: `http://localhost:8080/`
3. Click "Connect"
4. Enter credentials: `admin` / `admin123`
### 4. Connect with Linux
```bash
# Install davfs2
sudo apt-get install davfs2
# Mount WebDAV share
sudo mount -t davfs http://localhost:8080/ /mnt/webdav
# Enter credentials when prompted
```
## 👥 User Management
### Create Users via CLI
```bash
# Activate virtual environment
source venv/bin/activate
# Create a new user (interactive)
python webdav_cli.py user create john
# Create with all details
python webdav_cli.py user create jane \
--email jane@example.com \
--role user
# List all users
python webdav_cli.py user list
# Change password
python webdav_cli.py user password john
# Deactivate user
python webdav_cli.py user deactivate john
# Delete user
python webdav_cli.py user delete john --force
```
### Create Users Programmatically
```python
import asyncio
from main import Database
async def create_user():
db = Database('./webdav.db')
user_id = await db.create_user(
username='john',
password='SecurePass123!',
email='john@example.com',
role='user'
)
print(f"Created user ID: {user_id}")
asyncio.run(create_user())
```
## 🔧 Common Operations
### Upload Files
**Using curl:**
```bash
curl -u admin:admin123 -T myfile.txt http://localhost:8080/myfile.txt
```
**Using Python:**
```python
import requests
with open('myfile.txt', 'rb') as f:
response = requests.put(
'http://localhost:8080/myfile.txt',
data=f,
auth=('admin', 'admin123')
)
print(response.status_code)
```
### Download Files
```bash
curl -u admin:admin123 http://localhost:8080/myfile.txt -o downloaded.txt
```
### Create Directories
```bash
curl -u admin:admin123 -X MKCOL http://localhost:8080/newfolder/
```
### List Directory Contents
```bash
curl -u admin:admin123 -X PROPFIND http://localhost:8080/ \
-H "Depth: 1" \
-H "Content-Type: application/xml" \
--data '<?xml version="1.0"?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>'
```
### Copy Files
```bash
curl -u admin:admin123 -X COPY \
http://localhost:8080/source.txt \
-H "Destination: http://localhost:8080/destination.txt"
```
### Move Files
```bash
curl -u admin:admin123 -X MOVE \
http://localhost:8080/old.txt \
-H "Destination: http://localhost:8080/new.txt"
```
### Delete Files
```bash
curl -u admin:admin123 -X DELETE http://localhost:8080/myfile.txt
```
## 🔒 Security Setup
### Change Default Password
```bash
python webdav_cli.py user password admin
# Enter new password when prompted
```
### Enable HTTPS (Production)
1. **Get SSL Certificate (Let's Encrypt):**
```bash
sudo certbot certonly --standalone -d webdav.example.com
```
2. **Update .env:**
```env
SSL_ENABLED=true
SSL_CERT_PATH=/etc/letsencrypt/live/webdav.example.com/fullchain.pem
SSL_KEY_PATH=/etc/letsencrypt/live/webdav.example.com/privkey.pem
```
3. **Restart server**
### Use Nginx Reverse Proxy (Recommended)
1. Install Nginx:
```bash
sudo apt-get install nginx
```
2. Copy nginx.conf from artifacts to `/etc/nginx/conf.d/webdav.conf`
3. Update domain name in config
4. Reload Nginx:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
## 📊 Monitoring
### View Statistics
```bash
python webdav_cli.py stats
```
### Check Active Locks
```bash
python webdav_cli.py lock list
```
### View Logs
```bash
# Application logs
tail -f logs/webdav.log
# Access logs
tail -f logs/access.log
# If using systemd
sudo journalctl -u webdav -f
```
## 🐳 Docker Deployment
### Start Service
```bash
docker-compose up -d
```
### View Logs
```bash
docker-compose logs -f
```
### Stop Service
```bash
docker-compose down
```
### Update and Restart
```bash
docker-compose pull
docker-compose up -d --build
```
### Backup Data
```bash
# Backup database
docker exec webdav-server python webdav_cli.py backup
# Backup files
docker cp webdav-server:/app/webdav ./backup-webdav/
```
## ⚙️ Production Deployment
### Using Gunicorn (Recommended)
```bash
# Start with Gunicorn
gunicorn main:init_app \
--config gunicorn_config.py \
--bind 0.0.0.0:8080 \
--workers 4
# With systemd (after setup)
sudo systemctl enable webdav
sudo systemctl start webdav
sudo systemctl status webdav
```
### Environment Configuration
**Development:**
```env
DEBUG=true
LOG_LEVEL=DEBUG
WORKERS=1
```
**Production:**
```env
DEBUG=false
LOG_LEVEL=INFO
WORKERS=4
SSL_ENABLED=true
RATE_LIMIT_ENABLED=true
```
## 🔥 Common Issues
### Issue: Connection Refused
**Solution:**
```bash
# Check if server is running
ps aux | grep python
# Check port
sudo netstat -tlnp | grep 8080
# Check firewall
sudo ufw allow 8080/tcp
```
### Issue: Permission Denied
**Solution:**
```bash
# Fix directory permissions
chmod -R 755 webdav/
chown -R $USER:$USER webdav/
```
### Issue: Database Locked
**Solution:**
```bash
# Stop all instances
pkill -f "python main.py"
# Check for locks
lsof webdav.db
# Restart server
python main.py
```
### Issue: Windows Explorer Won't Connect
**Solutions:**
1. Restart WebClient service
2. Enable Basic Auth in registry:
```
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
BasicAuthLevel = 2
```
3. Use HTTPS instead of HTTP
4. Ensure URL ends with `/`
## 📚 Next Steps
1. **Read the full README.md** for comprehensive documentation
2. **Configure SSL/TLS** for production use
3. **Set up automated backups**
4. **Configure monitoring** and alerts
5. **Review security settings**
6. **Set up user quotas** and permissions
7. **Configure rate limiting**
8. **Enable caching** with Redis
## 🆘 Getting Help
- **Documentation**: Check README.md and inline comments
- **Logs**: Always check logs first (`logs/webdav.log`)
- **CLI Help**: `python webdav_cli.py --help`
- **Test Connection**: `curl -v http://localhost:8080/`
## 🎉 You're All Set!
Your WebDAV server is now ready to use. Start by:
1. Changing the default admin password
2. Creating user accounts
3. Connecting with your favorite WebDAV client
4. Uploading some files
Happy file sharing! 🚀

641
README.md Normal file
View File

@ -0,0 +1,641 @@
# WebDAV Server with aiohttp
A comprehensive, production-ready WebDAV server implementation with full RFC 4918 compliance, Windows Explorer compatibility, and enterprise-grade features.
## Features
### Core WebDAV Protocol
- ✅ **Full RFC 4918 Compliance**: All standard WebDAV methods implemented
- ✅ **Windows Explorer Integration**: Seamless compatibility with Windows WebDAV Mini-Redirector
- ✅ **Multiple Authentication Methods**: Basic, Digest, and Token-based authentication
- ✅ **Resource Locking**: Exclusive and shared locks with timeout management
- ✅ **Custom Properties**: Full property management (PROPFIND/PROPPATCH)
- ✅ **Collection Operations**: MKCOL, COPY, MOVE with depth support
### Enterprise Features
- 🔒 **Multi-User Support**: Isolated user directories with permissions
- 💾 **SQLite Database**: Robust backend for users, locks, and properties
- 🌐 **Web Management Interface**: Admin dashboard and user portal
- 🚀 **High Performance**: Async I/O with aiohttp and aiofiles
- 🔐 **Security Focused**: Input validation, path traversal prevention, SQL injection protection
- 📊 **Monitoring Ready**: Structured logging and metrics collection
- 🐳 **Docker Support**: Container-ready with docker-compose
- 📈 **Production Ready**: Gunicorn integration, health checks, graceful shutdown
## Quick Start
### Prerequisites
- Python 3.8 or higher
- pip (Python package manager)
- Optional: Redis for caching
- Optional: Docker for containerized deployment
### Installation
1. **Clone or create the project directory:**
```bash
mkdir webdav-server
cd webdav-server
```
2. **Install dependencies:**
```bash
pip install -r requirements.txt
```
3. **Configure environment:**
```bash
cp .env.example .env
# Edit .env with your configuration
nano .env
```
4. **Generate a secure secret key:**
```bash
python -c "import secrets; print(secrets.token_hex(32))"
# Copy the output to JWT_SECRET_KEY in .env
```
5. **Run the server:**
```bash
python main.py
```
The server will start on `http://0.0.0.0:8080` by default.
### First Run
On first run, the server automatically creates:
- A default admin user: `admin` / `admin123`
- The WebDAV root directory structure
- SQLite database with all required tables
**⚠️ Important**: Change the default admin password immediately!
## Configuration
### Environment Variables
All configuration is done through the `.env` file. Key settings:
#### Server Configuration
```env
HOST=0.0.0.0 # Listen address
PORT=8080 # Listen port
SSL_ENABLED=false # Enable HTTPS
```
#### Authentication
```env
AUTH_METHODS=basic,digest # Supported auth methods
JWT_SECRET_KEY=... # Secret for sessions (required)
SESSION_TIMEOUT=3600 # Session duration in seconds
```
#### WebDAV Settings
```env
WEBDAV_ROOT=./webdav # Root directory for files
MAX_FILE_SIZE=104857600 # Max file size (100MB)
MAX_PROPFIND_DEPTH=3 # Depth limit for PROPFIND
LOCK_TIMEOUT_DEFAULT=3600 # Default lock timeout
```
See `.env.example` for complete configuration options.
## Usage
### Connecting with Windows Explorer
#### Method 1: Map Network Drive
1. Open File Explorer
2. Right-click "This PC" → "Map network drive"
3. Choose a drive letter
4. Enter the WebDAV URL:
```
http://your-server:8080/
```
5. Check "Connect using different credentials"
6. Enter your username and password
7. Click "Finish"
#### Method 2: Add Network Location
1. Open File Explorer
2. Right-click "This PC" → "Add a network location"
3. Choose "Choose a custom network location"
4. Enter the WebDAV URL:
```
http://your-server:8080/
```
5. Enter credentials when prompted
#### Windows with SSL (Recommended)
For HTTPS connections:
```
https://your-server:8443/
```
**Note**: Windows requires port 443 for HTTPS WebDAV by default. To use other ports, modify Windows registry:
```
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
BasicAuthLevel = 2
```
### Connecting with macOS Finder
1. Open Finder
2. Press `Cmd+K` (Go → Connect to Server)
3. Enter the WebDAV URL:
```
http://your-server:8080/
```
4. Click "Connect"
5. Enter your credentials
### Connecting with Linux (davfs2)
1. Install davfs2:
```bash
sudo apt-get install davfs2 # Ubuntu/Debian
```
2. Mount the WebDAV share:
```bash
sudo mount -t davfs http://your-server:8080/ /mnt/webdav
```
3. Enter credentials when prompted
### Command Line Tools
#### Using curl
**Upload a file:**
```bash
curl -u username:password -T file.txt http://localhost:8080/file.txt
```
**Download a file:**
```bash
curl -u username:password http://localhost:8080/file.txt -o file.txt
```
**Create a directory:**
```bash
curl -u username:password -X MKCOL http://localhost:8080/newdir/
```
**Delete a resource:**
```bash
curl -u username:password -X DELETE http://localhost:8080/file.txt
```
#### Using cadaver
```bash
cadaver http://localhost:8080/
# Enter credentials
dav:/> ls
dav:/> put localfile.txt
dav:/> get remotefile.txt
dav:/> mkcol newfolder
```
## User Management
### Creating Users Programmatically
```python
import asyncio
from main import Database
async def create_user():
db = Database('./webdav.db')
user_id = await db.create_user(
username='john',
password='SecurePass123!',
email='john@example.com',
role='user'
)
print(f"Created user with ID: {user_id}")
asyncio.run(create_user())
```
### User Roles
- **admin**: Full access to all features and user management
- **user**: Standard user with access to their own directory
- **readonly**: Read-only access (future implementation)
### Directory Structure
```
webdav/
├── users/
│ ├── admin/ # Admin user directory
│ ├── john/ # John's private directory
│ └── jane/ # Jane's private directory
├── shared/ # Shared between all users (optional)
└── public/ # Public access (optional)
```
## Production Deployment
### Using Gunicorn
Create `gunicorn_config.py`:
```python
import multiprocessing
bind = "0.0.0.0:8080"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "aiohttp.GunicornWebWorker"
keepalive = 30
timeout = 60
accesslog = "./logs/access.log"
errorlog = "./logs/error.log"
loglevel = "info"
```
Run with Gunicorn:
```bash
gunicorn main:init_app --config gunicorn_config.py
```
### Using Docker
Create `Dockerfile`:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Create directories
RUN mkdir -p /app/webdav /app/logs
# Expose port
EXPOSE 8080
# Run application
CMD ["python", "main.py"]
```
Create `docker-compose.yml`:
```yaml
version: '3.8'
services:
webdav:
build: .
ports:
- "8080:8080"
volumes:
- ./webdav:/app/webdav
- ./logs:/app/logs
- ./webdav.db:/app/webdav.db
environment:
- HOST=0.0.0.0
- PORT=8080
- DB_PATH=/app/webdav.db
- WEBDAV_ROOT=/app/webdav
restart: unless-stopped
# Optional: Redis for caching
redis:
image: redis:alpine
ports:
- "6379:6379"
restart: unless-stopped
```
Build and run:
```bash
docker-compose up -d
```
### Nginx Reverse Proxy
Create `/etc/nginx/sites-available/webdav`:
```nginx
server {
listen 80;
server_name webdav.example.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name webdav.example.com;
ssl_certificate /etc/letsencrypt/live/webdav.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/webdav.example.com/privkey.pem;
# WebDAV specific settings
client_max_body_size 100M;
client_body_buffer_size 128k;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebDAV headers
proxy_set_header Destination $http_destination;
proxy_set_header Depth $http_depth;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
```
Enable and reload:
```bash
sudo ln -s /etc/nginx/sites-available/webdav /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
## Security Best Practices
### SSL/TLS Configuration
1. **Always use HTTPS in production**
2. Generate SSL certificates with Let's Encrypt:
```bash
sudo certbot certonly --nginx -d webdav.example.com
```
3. Update `.env`:
```env
SSL_ENABLED=true
SSL_CERT_PATH=/etc/letsencrypt/live/webdav.example.com/fullchain.pem
SSL_KEY_PATH=/etc/letsencrypt/live/webdav.example.com/privkey.pem
```
### Authentication
- Use **Digest authentication** over Basic when possible
- Enforce strong passwords (min length, complexity)
- Enable rate limiting to prevent brute force attacks
- Implement account lockout after failed attempts
### Firewall Rules
```bash
# Allow only necessary ports
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
### Regular Updates
```bash
# Update dependencies
pip install --upgrade -r requirements.txt
# Backup database before updates
cp webdav.db webdav.db.backup
```
## Monitoring and Logging
### Log Files
Logs are stored in `./logs/` directory:
- `webdav.log` - Application logs
- `access.log` - Access logs (requests)
- `error.log` - Error logs
### Log Format
JSON-structured logs for easy parsing:
```json
{
"timestamp": "2025-01-15T10:30:45Z",
"level": "INFO",
"user": "john",
"method": "PROPFIND",
"path": "/documents/",
"status": 207,
"duration_ms": 45
}
```
### Health Check
Access the health check endpoint:
```bash
curl http://localhost:8080/health
```
## Troubleshooting
### Windows Explorer Connection Issues
**Problem**: "The network folder is no longer available"
**Solutions**:
1. Increase Windows WebClient timeout:
```
Registry: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
FileSizeLimitInBytes = DWORD: 0xFFFFFFFF
```
2. Enable Basic Authentication over HTTP (insecure - use only for testing):
```
Registry: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
BasicAuthLevel = 2
```
3. Restart WebClient service:
```powershell
net stop webclient
net start webclient
```
**Problem**: "The specified server cannot perform the requested operation"
**Solution**: Ensure the URL ends with a slash and doesn't include port 80:
- ❌ `http://server:80/path`
- ✅ `http://server/path/`
### macOS Finder Issues
**Problem**: Connection refused or timeout
**Solutions**:
1. Use HTTP URL with explicit protocol:
```
http://server:8080/
```
2. Check firewall settings on server
3. Try connecting via IP address instead of hostname
### Performance Issues
**Problem**: Slow PROPFIND responses
**Solutions**:
1. Reduce `MAX_PROPFIND_DEPTH` in `.env`:
```env
MAX_PROPFIND_DEPTH=1
```
2. Enable caching with Redis:
```env
CACHE_ENABLED=true
REDIS_HOST=localhost
```
3. Increase worker processes:
```env
WORKERS=8
```
### Database Locked Errors
**Problem**: "database is locked" errors
**Solutions**:
1. Enable WAL mode (automatic in code)
2. Ensure only one process accesses the database
3. Use separate databases for multiple instances
## API Documentation
### WebDAV Methods
#### PROPFIND - Property Discovery
```http
PROPFIND /documents/ HTTP/1.1
Depth: 1
Content-Type: application/xml
<?xml version="1.0"?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>
```
#### PROPPATCH - Property Modification
```http
PROPPATCH /file.txt HTTP/1.1
Content-Type: application/xml
<?xml version="1.0"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<D:displayname>My Document</D:displayname>
</D:prop>
</D:set>
</D:propertyupdate>
```
#### LOCK - Resource Locking
```http
LOCK /file.txt HTTP/1.1
Timeout: Second-3600
Content-Type: application/xml
<?xml version="1.0"?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href>mailto:john@example.com</D:href>
</D:owner>
</D:lockinfo>
```
## Development
### Running Tests
```bash
# Install test dependencies
pip install pytest pytest-asyncio pytest-aiohttp pytest-cov
# Run tests
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=. --cov-report=html
```
### Code Style
```bash
# Format code
black main.py
# Lint code
flake8 main.py
# Type checking
mypy main.py
```
### Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request
## License
MIT License - See LICENSE file for details
## Support
For issues, questions, or contributions:
- GitHub Issues: [Create an issue]
- Documentation: [Wiki]
- Community: [Discussions]
## Changelog
### Version 1.0.0 (2025-01-15)
- Initial release
- Full RFC 4918 compliance
- Windows Explorer compatibility
- Multi-user support with SQLite backend
- Basic and Digest authentication
- Resource locking
- Custom properties
- Production-ready with Gunicorn support
## Acknowledgments
- RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)
- aiohttp - Async HTTP client/server framework
- Python community
---
**Made with ❤️ for the WebDAV community**

145
docker-compose.yml Normal file
View File

@ -0,0 +1,145 @@
version: '3.8'
# ============================================================================
# WebDAV Server - Docker Compose Configuration
# ============================================================================
services:
# --------------------------------------------------------------------------
# WebDAV Server
# --------------------------------------------------------------------------
webdav:
build:
context: .
dockerfile: Dockerfile
container_name: webdav-server
restart: unless-stopped
ports:
- "8080:8080"
volumes:
# Persistent storage for user files
- ./webdav:/app/webdav
# Database persistence
- ./webdav.db:/app/webdav.db
# Logs
- ./logs:/app/logs
# Backups
- ./backups:/app/backups
# Configuration (optional - uncomment to override)
# - ./.env:/app/.env:ro
environment:
# Server Configuration
- HOST=0.0.0.0
- PORT=8080
# Database
- DB_PATH=/app/webdav.db
# WebDAV Root
- WEBDAV_ROOT=/app/webdav
# Authentication
- AUTH_METHODS=basic,digest
- SESSION_TIMEOUT=3600
# Performance
- MAX_CONNECTIONS=1000
- WORKERS=4
# Logging
- LOG_LEVEL=INFO
- LOG_FILE=/app/logs/webdav.log
# Cache (if Redis enabled)
- CACHE_ENABLED=false
- REDIS_HOST=redis
- REDIS_PORT=6379
networks:
- webdav-network
depends_on:
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# --------------------------------------------------------------------------
# Redis Cache (Optional but recommended for production)
# --------------------------------------------------------------------------
redis:
image: redis:7-alpine
container_name: webdav-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
networks:
- webdav-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# --------------------------------------------------------------------------
# Nginx Reverse Proxy with SSL (Production)
# --------------------------------------------------------------------------
nginx:
image: nginx:alpine
container_name: webdav-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
networks:
- webdav-network
depends_on:
- webdav
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
# ============================================================================
# Networks
# ============================================================================
networks:
webdav-network:
driver: bridge
# ============================================================================
# Volumes
# ============================================================================
volumes:
redis-data:
driver: local

223
gunicorn_config.py Normal file
View File

@ -0,0 +1,223 @@
"""
Gunicorn Configuration for WebDAV Server
Production-grade WSGI server configuration with aiohttp worker
"""
import os
import multiprocessing
# ============================================================================
# Server Socket
# ============================================================================
bind = f"{os.getenv('HOST', '0.0.0.0')}:{os.getenv('PORT', '8080')}"
backlog = 2048
# ============================================================================
# Worker Processes
# ============================================================================
# Number of worker processes
workers = int(os.getenv('WORKERS', multiprocessing.cpu_count() * 2 + 1))
# Worker class - MUST be aiohttp.GunicornWebWorker for aiohttp
worker_class = 'aiohttp.GunicornWebWorker'
# Worker connections (only for async workers)
worker_connections = int(os.getenv('WORKER_CONNECTIONS', 1024))
# Maximum requests a worker will process before restarting (prevents memory leaks)
max_requests = int(os.getenv('MAX_REQUESTS', 10000))
max_requests_jitter = int(os.getenv('MAX_REQUESTS_JITTER', 1000))
# Worker timeout in seconds
timeout = int(os.getenv('WORKER_TIMEOUT', 60))
# Graceful timeout for workers
graceful_timeout = int(os.getenv('GRACEFUL_TIMEOUT', 30))
# Keep-alive timeout
keepalive = int(os.getenv('KEEPALIVE_TIMEOUT', 30))
# ============================================================================
# Logging
# ============================================================================
# Access log
accesslog = os.getenv('ACCESS_LOG_FILE', './logs/gunicorn_access.log')
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Error log
errorlog = os.getenv('ERROR_LOG_FILE', './logs/gunicorn_error.log')
# Log level
loglevel = os.getenv('LOG_LEVEL', 'info').lower()
# Capture output from workers
capture_output = True
# Enable log rotation
logconfig_dict = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'generic': {
'format': '%(asctime)s [%(process)d] [%(levelname)s] %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
'class': 'logging.Formatter'
},
'access': {
'format': '%(message)s',
'class': 'logging.Formatter'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'generic',
'stream': 'ext://sys.stdout'
},
'error_file': {
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'generic',
'filename': errorlog,
'maxBytes': 10485760, # 10MB
'backupCount': 5
},
'access_file': {
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'access',
'filename': accesslog,
'maxBytes': 10485760, # 10MB
'backupCount': 5
}
},
'loggers': {
'gunicorn.error': {
'handlers': ['console', 'error_file'],
'level': loglevel.upper(),
'propagate': False
},
'gunicorn.access': {
'handlers': ['access_file'],
'level': 'INFO',
'propagate': False
}
},
'root': {
'level': loglevel.upper(),
'handlers': ['console', 'error_file']
}
}
# ============================================================================
# Process Naming
# ============================================================================
proc_name = 'webdav_server'
# ============================================================================
# Server Mechanics
# ============================================================================
# Daemon mode (run in background)
daemon = os.getenv('DAEMON', 'false').lower() == 'true'
# PID file
pidfile = os.getenv('PID_FILE', './gunicorn.pid')
# User and group to run workers
user = os.getenv('USER', None)
group = os.getenv('GROUP', None)
# Directory to switch to before loading apps
chdir = os.getenv('CHDIR', '.')
# Environment variables to set for workers
raw_env = [
f"WEBDAV_ENV={os.getenv('WEBDAV_ENV', 'production')}"
]
# ============================================================================
# Server Hooks
# ============================================================================
def on_starting(server):
"""Called just before the master process is initialized."""
server.log.info("Starting WebDAV Server...")
# Create necessary directories
os.makedirs('./logs', exist_ok=True)
os.makedirs('./webdav', exist_ok=True)
os.makedirs('./backups', exist_ok=True)
def on_reload(server):
"""Called to recycle workers during a reload."""
server.log.info("Reloading WebDAV Server...")
def when_ready(server):
"""Called just after the server is started."""
server.log.info(f"WebDAV Server is ready. Listening on: {bind}")
server.log.info(f"Workers: {workers}")
server.log.info(f"Worker class: {worker_class}")
def pre_fork(server, worker):
"""Called just before a worker is forked."""
pass
def post_fork(server, worker):
"""Called just after a worker has been forked."""
server.log.info(f"Worker spawned (pid: {worker.pid})")
def pre_exec(server):
"""Called just before a new master process is forked."""
server.log.info("Forked child, re-executing.")
def worker_int(worker):
"""Called when a worker receives the SIGINT or SIGQUIT signal."""
worker.log.info(f"Worker received INT or QUIT signal (pid: {worker.pid})")
def worker_abort(worker):
"""Called when a worker receives the SIGABRT signal."""
worker.log.info(f"Worker received SIGABRT signal (pid: {worker.pid})")
def pre_request(worker, req):
"""Called just before a worker processes the request."""
worker.log.debug(f"{req.method} {req.path}")
def post_request(worker, req, environ, resp):
"""Called after a worker processes the request."""
pass
def worker_exit(server, worker):
"""Called just after a worker has been exited."""
server.log.info(f"Worker exited (pid: {worker.pid})")
def on_exit(server):
"""Called just before exiting Gunicorn."""
server.log.info("Shutting down WebDAV Server...")
# ============================================================================
# SSL Configuration (if needed)
# ============================================================================
# Uncomment and configure for SSL support
# keyfile = '/path/to/key.pem'
# certfile = '/path/to/cert.pem'
# ssl_version = 'TLSv1_2'
# cert_reqs = 0 # ssl.CERT_NONE
# ca_certs = None
# suppress_ragged_eofs = True
# do_handshake_on_connect = False
# ciphers = 'TLSv1'

1131
main.py Normal file

File diff suppressed because it is too large Load Diff

153
nginx.conf Normal file
View File

@ -0,0 +1,153 @@
# ============================================================================
# Nginx Configuration for WebDAV Server
# Place this in: /etc/nginx/conf.d/webdav.conf
# ============================================================================
# Upstream backend
upstream webdav_backend {
server webdav:8080 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# HTTP Server - Redirect to HTTPS
server {
listen 80;
listen [::]:80;
server_name webdav.example.com; # Change to your domain
# Let's Encrypt ACME challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect all other traffic to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS Server - Main WebDAV
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name webdav.example.com; # Change to your domain
# SSL Configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# SSL Security Settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# WebDAV Specific Settings
client_max_body_size 1G; # Maximum file size
client_body_buffer_size 128k;
client_body_timeout 300s; # Timeout for client body
client_header_timeout 60s;
send_timeout 300s; # Timeout for sending response
# Proxy buffering (disable for large files)
proxy_buffering off;
proxy_request_buffering off;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Logging
access_log /var/log/nginx/webdav_access.log combined;
error_log /var/log/nginx/webdav_error.log warn;
# Root location - WebDAV
location / {
# Proxy to backend
proxy_pass http://webdav_backend;
# Standard proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebDAV specific headers
proxy_set_header Destination $http_destination;
proxy_set_header Depth $http_depth;
proxy_set_header Overwrite $http_overwrite;
proxy_set_header Lock-Token $http_lock_token;
proxy_set_header Timeout $http_timeout;
proxy_set_header If $http_if;
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_set_header Connection "";
# Disable redirects
proxy_redirect off;
# Handle errors
proxy_intercept_errors off;
}
# Health check endpoint
location /health {
proxy_pass http://webdav_backend/health;
access_log off;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
# ============================================================================
# HTTP Configuration without SSL (for development only)
# ============================================================================
# Uncomment this section for development without SSL
# server {
# listen 80;
# listen [::]:80;
# server_name webdav.example.com;
#
# client_max_body_size 1G;
# client_body_buffer_size 128k;
# client_body_timeout 300s;
# send_timeout 300s;
#
# location / {
# proxy_pass http://webdav_backend;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# # WebDAV headers
# proxy_set_header Destination $http_destination;
# proxy_set_header Depth $http_depth;
# proxy_set_header Overwrite $http_overwrite;
# proxy_set_header Lock-Token $http_lock_token;
#
# proxy_http_version 1.1;
# proxy_set_header Connection "";
# proxy_redirect off;
# proxy_buffering off;
# }
# }

40
requirements.txt Normal file
View File

@ -0,0 +1,40 @@
# Core Web Framework
aiohttp>=3.9.0
aiofiles>=23.2.0
aiohttp-session>=2.12.0
aiohttp-security>=0.4.0
# Security and Cryptography
cryptography>=41.0.0
PyJWT>=2.8.0
# Environment and Configuration
python-dotenv>=1.0.0
# XML Processing
lxml>=4.9.0
# Template Engine (for web interface)
Jinja2>=3.1.2
MarkupSafe>=2.1.3
# Production Server
gunicorn>=21.2.0
# Testing
pytest>=7.4.0
pytest-asyncio>=0.21.0
pytest-aiohttp>=1.0.5
pytest-cov>=4.1.0
# Development Tools
black>=23.7.0
flake8>=6.1.0
mypy>=1.5.0
# Optional: Redis for caching
redis>=5.0.0
aioredis>=2.0.1
# Optional: Performance monitoring
prometheus-client>=0.17.1

362
setup.sh Executable file
View File

@ -0,0 +1,362 @@
#!/bin/bash
# ============================================================================
# WebDAV Server Installation and Setup Script
# ============================================================================
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Functions
print_info() {
echo -e "${BLUE} ${1}${NC}"
}
print_success() {
echo -e "${GREEN}${1}${NC}"
}
print_warning() {
echo -e "${YELLOW}${1}${NC}"
}
print_error() {
echo -e "${RED}${1}${NC}"
}
print_header() {
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ WebDAV Server Installation Script ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
# Check if running as root
check_root() {
if [ "$EUID" -eq 0 ]; then
print_warning "Running as root. It's recommended to run as a regular user."
read -p "Continue? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
}
# Check system requirements
check_requirements() {
print_info "Checking system requirements..."
# Check Python version
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
print_success "Python $PYTHON_VERSION found"
else
print_error "Python 3 not found. Please install Python 3.8 or higher."
exit 1
fi
# Check pip
if command -v pip3 &> /dev/null; then
print_success "pip3 found"
else
print_warning "pip3 not found. Installing..."
python3 -m ensurepip --upgrade
fi
# Check git (optional)
if command -v git &> /dev/null; then
print_success "git found"
else
print_warning "git not found. Some features may be limited."
fi
}
# Install system dependencies
install_dependencies() {
print_info "Installing system dependencies..."
if [ -f /etc/debian_version ]; then
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y python3-dev python3-venv build-essential libxml2-dev libxslt-dev
print_success "Dependencies installed (Debian/Ubuntu)"
elif [ -f /etc/redhat-release ]; then
# RHEL/CentOS/Fedora
sudo yum install -y python3-devel gcc gcc-c++ libxml2-devel libxslt-devel
print_success "Dependencies installed (RHEL/CentOS/Fedora)"
else
print_warning "Unknown distribution. Please install python3-dev, build-essential manually."
fi
}
# Setup installation directory
setup_directory() {
print_info "Setting up installation directory..."
read -p "Installation directory [./webdav-server]: " INSTALL_DIR
INSTALL_DIR=${INSTALL_DIR:-./webdav-server}
if [ -d "$INSTALL_DIR" ]; then
print_warning "Directory $INSTALL_DIR already exists."
read -p "Continue and potentially overwrite files? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
else
mkdir -p "$INSTALL_DIR"
fi
cd "$INSTALL_DIR"
print_success "Using directory: $(pwd)"
}
# Create virtual environment
setup_venv() {
print_info "Creating Python virtual environment..."
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip setuptools wheel
print_success "Virtual environment created"
}
# Install Python packages
install_packages() {
print_info "Installing Python packages..."
source venv/bin/activate
if [ -f requirements.txt ]; then
pip install -r requirements.txt
print_success "Packages installed from requirements.txt"
else
print_warning "requirements.txt not found. Installing core packages..."
pip install aiohttp aiofiles aiohttp-session cryptography python-dotenv lxml gunicorn
print_success "Core packages installed"
fi
}
# Setup configuration
setup_config() {
print_info "Setting up configuration..."
if [ ! -f .env ]; then
if [ -f .env.example ]; then
cp .env.example .env
print_success "Created .env from .env.example"
else
print_warning ".env.example not found. Creating basic .env..."
cat > .env << EOF
HOST=0.0.0.0
PORT=8080
DB_PATH=./webdav.db
WEBDAV_ROOT=./webdav
AUTH_METHODS=basic,digest
JWT_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
SESSION_TIMEOUT=3600
MAX_FILE_SIZE=104857600
LOG_LEVEL=INFO
EOF
print_success "Created basic .env file"
fi
else
print_warning ".env already exists. Skipping..."
fi
# Generate secret key if needed
if grep -q "your-secret-key-here" .env 2>/dev/null; then
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s/your-secret-key-here-change-this-in-production/$SECRET_KEY/" .env
else
# Linux
sed -i "s/your-secret-key-here-change-this-in-production/$SECRET_KEY/" .env
fi
print_success "Generated new JWT secret key"
fi
}
# Create directory structure
create_directories() {
print_info "Creating directory structure..."
mkdir -p webdav/users
mkdir -p webdav/shared
mkdir -p logs
mkdir -p backups
print_success "Directory structure created"
}
# Initialize database
init_database() {
print_info "Initializing database..."
source venv/bin/activate
python3 << EOF
import asyncio
from main import Database, create_default_user
async def init():
db = Database('./webdav.db')
await create_default_user(db)
asyncio.run(init())
EOF
print_success "Database initialized"
}
# Setup systemd service (optional)
setup_systemd() {
print_info "Would you like to set up systemd service? (requires sudo)"
read -p "Setup systemd service? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Create webdav user if doesn't exist
if ! id -u webdav &>/dev/null; then
sudo useradd -r -s /bin/false webdav
print_success "Created webdav user"
fi
# Copy service file
if [ -f webdav.service ]; then
sudo cp webdav.service /etc/systemd/system/
sudo systemctl daemon-reload
print_success "Systemd service installed"
print_info "To enable and start the service:"
echo " sudo systemctl enable webdav"
echo " sudo systemctl start webdav"
echo " sudo systemctl status webdav"
else
print_warning "webdav.service not found. Skipping..."
fi
fi
}
# Setup nginx (optional)
setup_nginx() {
print_info "Would you like to set up Nginx reverse proxy?"
read -p "Setup Nginx? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
if command -v nginx &> /dev/null; then
read -p "Enter your domain name: " DOMAIN_NAME
if [ -f nginx.conf ]; then
mkdir -p nginx/conf.d nginx/ssl
sed "s/webdav.example.com/$DOMAIN_NAME/g" nginx.conf > nginx/conf.d/webdav.conf
print_success "Nginx configuration created"
print_info "Configuration saved to: nginx/conf.d/webdav.conf"
print_warning "Don't forget to configure SSL certificates!"
else
print_warning "nginx.conf template not found"
fi
else
print_warning "Nginx not installed. Skipping..."
fi
fi
}
# Setup Docker (optional)
setup_docker() {
print_info "Would you like to set up Docker deployment?"
read -p "Setup Docker? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
if command -v docker &> /dev/null; then
if [ -f Dockerfile ] && [ -f docker-compose.yml ]; then
print_info "Building Docker image..."
docker-compose build
print_success "Docker image built"
print_info "To start the service:"
echo " docker-compose up -d"
echo " docker-compose logs -f"
else
print_warning "Dockerfile or docker-compose.yml not found"
fi
else
print_warning "Docker not installed. Skipping..."
fi
fi
}
# Final instructions
print_instructions() {
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Installation Complete! ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
print_info "Getting Started:"
echo ""
echo "1. Activate virtual environment:"
echo " source venv/bin/activate"
echo ""
echo "2. Start the server:"
echo " python main.py"
echo " OR with Gunicorn (production):"
echo " gunicorn main:init_app --config gunicorn_config.py"
echo ""
echo "3. Access WebDAV server:"
echo " http://localhost:8080/"
echo ""
echo "4. Default credentials:"
echo " Username: admin"
echo " Password: admin123"
echo " ⚠️ CHANGE THIS PASSWORD IMMEDIATELY!"
echo ""
echo "5. Manage users with CLI:"
echo " python webdav_cli.py user create newuser"
echo " python webdav_cli.py user list"
echo " python webdav_cli.py stats"
echo ""
print_warning "Remember to:"
echo " • Change the default admin password"
echo " • Configure SSL/TLS for production"
echo " • Set up firewall rules"
echo " • Configure backups"
echo ""
}
# Main installation process
main() {
print_header
check_root
check_requirements
install_dependencies
setup_directory
setup_venv
install_packages
setup_config
create_directories
init_database
setup_systemd
setup_nginx
setup_docker
print_instructions
print_success "Installation complete!"
}
# Run main function
main

565
test_webdav.py Normal file
View File

@ -0,0 +1,565 @@
"""
Comprehensive Test Suite for WebDAV Server
Tests all WebDAV methods, authentication, and edge cases
"""
import pytest
import asyncio
import tempfile
import shutil
from pathlib import Path
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
import base64
# Import the main application
import sys
sys.path.insert(0, '.')
from main import Database, AuthHandler, WebDAVHandler, init_app, Config
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
async def temp_dir():
"""Create temporary directory for tests"""
temp_path = Path(tempfile.mkdtemp())
yield temp_path
shutil.rmtree(temp_path, ignore_errors=True)
@pytest.fixture
async def test_db(temp_dir):
"""Create test database"""
db_path = temp_dir / 'test.db'
db = Database(str(db_path))
# Create test users
await db.create_user('testuser', 'testpass123', 'test@example.com', 'user')
await db.create_user('admin', 'adminpass123', 'admin@example.com', 'admin')
yield db
@pytest.fixture
async def test_app(test_db, temp_dir, monkeypatch):
"""Create test application"""
# Override config for testing
monkeypatch.setattr(Config, 'DB_PATH', str(temp_dir / 'test.db'))
monkeypatch.setattr(Config, 'WEBDAV_ROOT', str(temp_dir / 'webdav'))
# Create webdav root
(temp_dir / 'webdav' / 'users' / 'testuser').mkdir(parents=True, exist_ok=True)
(temp_dir / 'webdav' / 'users' / 'admin').mkdir(parents=True, exist_ok=True)
app = await init_app()
app['db'] = test_db
yield app
@pytest.fixture
async def client(test_app):
"""Create test client"""
server = TestServer(test_app)
client = TestClient(server)
await client.start_server()
yield client
await client.close()
@pytest.fixture
def basic_auth_header():
"""Create basic auth header"""
credentials = base64.b64encode(b'testuser:testpass123').decode('utf-8')
return {'Authorization': f'Basic {credentials}'}
# ============================================================================
# Authentication Tests
# ============================================================================
class TestAuthentication:
"""Test authentication mechanisms"""
@pytest.mark.asyncio
async def test_no_auth_returns_401(self, client):
"""Test that requests without auth return 401"""
response = await client.get('/')
assert response.status == 401
assert 'WWW-Authenticate' in response.headers
@pytest.mark.asyncio
async def test_basic_auth_success(self, client, basic_auth_header):
"""Test successful basic authentication"""
response = await client.get('/', headers=basic_auth_header)
assert response.status in [200, 207] # 207 for PROPFIND
@pytest.mark.asyncio
async def test_basic_auth_invalid_credentials(self, client):
"""Test basic auth with invalid credentials"""
credentials = base64.b64encode(b'testuser:wrongpass').decode('utf-8')
headers = {'Authorization': f'Basic {credentials}'}
response = await client.get('/', headers=headers)
assert response.status == 401
@pytest.mark.asyncio
async def test_basic_auth_malformed(self, client):
"""Test basic auth with malformed header"""
headers = {'Authorization': 'Basic invalid-base64'}
response = await client.get('/', headers=headers)
assert response.status == 401
# ============================================================================
# HTTP Methods Tests
# ============================================================================
class TestHTTPMethods:
"""Test standard HTTP methods"""
@pytest.mark.asyncio
async def test_options(self, client, basic_auth_header):
"""Test OPTIONS method"""
response = await client.options('/', headers=basic_auth_header)
assert response.status == 200
assert 'DAV' in response.headers
assert 'Allow' in response.headers
allow = response.headers['Allow']
assert 'PROPFIND' in allow
assert 'PROPPATCH' in allow
assert 'MKCOL' in allow
assert 'LOCK' in allow
assert 'UNLOCK' in allow
@pytest.mark.asyncio
async def test_get_nonexistent_file(self, client, basic_auth_header):
"""Test GET on nonexistent file"""
response = await client.get('/nonexistent.txt', headers=basic_auth_header)
assert response.status == 404
@pytest.mark.asyncio
async def test_put_and_get_file(self, client, basic_auth_header):
"""Test PUT and GET file"""
content = b'Hello, WebDAV!'
# PUT file
put_response = await client.put(
'/test.txt',
data=content,
headers=basic_auth_header
)
assert put_response.status in [201, 204]
# GET file
get_response = await client.get('/test.txt', headers=basic_auth_header)
assert get_response.status == 200
body = await get_response.read()
assert body == content
@pytest.mark.asyncio
async def test_head_file(self, client, basic_auth_header):
"""Test HEAD method"""
# Create a file first
content = b'Test content'
await client.put('/test.txt', data=content, headers=basic_auth_header)
# HEAD request
response = await client.head('/test.txt', headers=basic_auth_header)
assert response.status == 200
assert 'Content-Length' in response.headers
assert int(response.headers['Content-Length']) == len(content)
# Body should be empty
body = await response.read()
assert body == b''
@pytest.mark.asyncio
async def test_delete_file(self, client, basic_auth_header):
"""Test DELETE method"""
# Create a file
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
# Delete it
response = await client.delete('/test.txt', headers=basic_auth_header)
assert response.status == 204
# Verify it's gone
get_response = await client.get('/test.txt', headers=basic_auth_header)
assert get_response.status == 404
# ============================================================================
# WebDAV Methods Tests
# ============================================================================
class TestWebDAVMethods:
"""Test WebDAV-specific methods"""
@pytest.mark.asyncio
async def test_mkcol(self, client, basic_auth_header):
"""Test MKCOL (create collection)"""
response = await client.request(
'MKCOL',
'/newdir/',
headers=basic_auth_header
)
assert response.status == 201
# Verify directory was created with PROPFIND
propfind_response = await client.request(
'PROPFIND',
'/newdir/',
headers={**basic_auth_header, 'Depth': '0'}
)
assert propfind_response.status == 207
@pytest.mark.asyncio
async def test_mkcol_already_exists(self, client, basic_auth_header):
"""Test MKCOL on existing directory"""
# Create directory
await client.request('MKCOL', '/testdir/', headers=basic_auth_header)
# Try to create again
response = await client.request('MKCOL', '/testdir/', headers=basic_auth_header)
assert response.status == 405 # Method Not Allowed
@pytest.mark.asyncio
async def test_propfind_depth_0(self, client, basic_auth_header):
"""Test PROPFIND with depth 0"""
# Create a file
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
# PROPFIND with depth 0
propfind_body = b'''<?xml version="1.0"?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>'''
response = await client.request(
'PROPFIND',
'/test.txt',
data=propfind_body,
headers={**basic_auth_header, 'Depth': '0', 'Content-Type': 'application/xml'}
)
assert response.status == 207
body = await response.text()
assert 'multistatus' in body
assert 'test.txt' in body
@pytest.mark.asyncio
async def test_propfind_depth_1(self, client, basic_auth_header):
"""Test PROPFIND with depth 1"""
# Create directory with files
await client.request('MKCOL', '/testdir/', headers=basic_auth_header)
await client.put('/testdir/file1.txt', data=b'content1', headers=basic_auth_header)
await client.put('/testdir/file2.txt', data=b'content2', headers=basic_auth_header)
# PROPFIND with depth 1
response = await client.request(
'PROPFIND',
'/testdir/',
headers={**basic_auth_header, 'Depth': '1'}
)
assert response.status == 207
body = await response.text()
assert 'file1.txt' in body
assert 'file2.txt' in body
@pytest.mark.asyncio
async def test_proppatch(self, client, basic_auth_header):
"""Test PROPPATCH (set properties)"""
# Create a file
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
# Set custom property
proppatch_body = b'''<?xml version="1.0"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<D:displayname>My Document</D:displayname>
</D:prop>
</D:set>
</D:propertyupdate>'''
response = await client.request(
'PROPPATCH',
'/test.txt',
data=proppatch_body,
headers={**basic_auth_header, 'Content-Type': 'application/xml'}
)
assert response.status == 207
@pytest.mark.asyncio
async def test_copy(self, client, basic_auth_header):
"""Test COPY method"""
# Create source file
await client.put('/source.txt', data=b'content', headers=basic_auth_header)
# Copy to destination
response = await client.request(
'COPY',
'/source.txt',
headers={
**basic_auth_header,
'Destination': '/dest.txt'
}
)
assert response.status in [201, 204]
# Verify both files exist
source_response = await client.get('/source.txt', headers=basic_auth_header)
assert source_response.status == 200
dest_response = await client.get('/dest.txt', headers=basic_auth_header)
assert dest_response.status == 200
# Verify content is the same
source_content = await source_response.read()
dest_content = await dest_response.read()
assert source_content == dest_content
@pytest.mark.asyncio
async def test_move(self, client, basic_auth_header):
"""Test MOVE method"""
# Create source file
await client.put('/source.txt', data=b'content', headers=basic_auth_header)
# Move to destination
response = await client.request(
'MOVE',
'/source.txt',
headers={
**basic_auth_header,
'Destination': '/dest.txt'
}
)
assert response.status in [201, 204]
# Verify source is gone
source_response = await client.get('/source.txt', headers=basic_auth_header)
assert source_response.status == 404
# Verify destination exists
dest_response = await client.get('/dest.txt', headers=basic_auth_header)
assert dest_response.status == 200
@pytest.mark.asyncio
async def test_lock_and_unlock(self, client, basic_auth_header):
"""Test LOCK and UNLOCK methods"""
# Create a file
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
# Lock the file
lock_body = b'''<?xml version="1.0"?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href>mailto:test@example.com</D:href>
</D:owner>
</D:lockinfo>'''
lock_response = await client.request(
'LOCK',
'/test.txt',
data=lock_body,
headers={**basic_auth_header, 'Content-Type': 'application/xml', 'Timeout': 'Second-3600'}
)
assert lock_response.status == 200
assert 'Lock-Token' in lock_response.headers
lock_token = lock_response.headers['Lock-Token'].strip('<>')
# Unlock the file
unlock_response = await client.request(
'UNLOCK',
'/test.txt',
headers={**basic_auth_header, 'Lock-Token': f'<{lock_token}>'}
)
assert unlock_response.status == 204
# ============================================================================
# Security Tests
# ============================================================================
class TestSecurity:
"""Test security features"""
@pytest.mark.asyncio
async def test_path_traversal_prevention(self, client, basic_auth_header):
"""Test that path traversal is prevented"""
# Try to access parent directory
response = await client.get('/../../../etc/passwd', headers=basic_auth_header)
assert response.status in [403, 404]
@pytest.mark.asyncio
async def test_user_isolation(self, client):
"""Test that users can't access other users' files"""
# Create file as testuser
testuser_auth = base64.b64encode(b'testuser:testpass123').decode('utf-8')
testuser_headers = {'Authorization': f'Basic {testuser_auth}'}
await client.put('/myfile.txt', data=b'secret', headers=testuser_headers)
# Try to access as admin
admin_auth = base64.b64encode(b'admin:adminpass123').decode('utf-8')
admin_headers = {'Authorization': f'Basic {admin_auth}'}
# This should fail because each user has their own isolated directory
# The path /myfile.txt for admin is different from /myfile.txt for testuser
response = await client.get('/myfile.txt', headers=admin_headers)
assert response.status == 404
# ============================================================================
# Database Tests
# ============================================================================
class TestDatabase:
"""Test database operations"""
@pytest.mark.asyncio
async def test_create_user(self, test_db):
"""Test user creation"""
user_id = await test_db.create_user('newuser', 'password123', 'new@example.com')
assert user_id > 0
# Verify user can be retrieved
user = await test_db.verify_user('newuser', 'password123')
assert user is not None
assert user['username'] == 'newuser'
@pytest.mark.asyncio
async def test_verify_user_wrong_password(self, test_db):
"""Test user verification with wrong password"""
user = await test_db.verify_user('testuser', 'wrongpassword')
assert user is None
@pytest.mark.asyncio
async def test_lock_creation(self, test_db):
"""Test lock creation"""
lock_token = await test_db.create_lock(
'/test.txt',
1,
'write',
'exclusive',
0,
3600,
'test@example.com'
)
assert lock_token.startswith('opaquelocktoken:')
# Retrieve lock
lock = await test_db.get_lock('/test.txt')
assert lock is not None
assert lock['lock_token'] == lock_token
@pytest.mark.asyncio
async def test_lock_removal(self, test_db):
"""Test lock removal"""
lock_token = await test_db.create_lock('/test.txt', 1)
removed = await test_db.remove_lock(lock_token, 1)
assert removed is True
# Verify lock is gone
lock = await test_db.get_lock('/test.txt')
assert lock is None
@pytest.mark.asyncio
async def test_property_management(self, test_db):
"""Test property set and get"""
await test_db.set_property('/test.txt', 'DAV:', 'displayname', 'My File')
props = await test_db.get_properties('/test.txt')
assert len(props) > 0
assert any(p['property_name'] == 'displayname' for p in props)
# ============================================================================
# Integration Tests
# ============================================================================
class TestIntegration:
"""Integration tests for complex workflows"""
@pytest.mark.asyncio
async def test_complete_workflow(self, client, basic_auth_header):
"""Test complete file management workflow"""
# 1. Create directory
await client.request('MKCOL', '/docs/', headers=basic_auth_header)
# 2. Upload files
await client.put('/docs/file1.txt', data=b'content1', headers=basic_auth_header)
await client.put('/docs/file2.txt', data=b'content2', headers=basic_auth_header)
# 3. List directory
response = await client.request(
'PROPFIND',
'/docs/',
headers={**basic_auth_header, 'Depth': '1'}
)
assert response.status == 207
body = await response.text()
assert 'file1.txt' in body
assert 'file2.txt' in body
# 4. Copy file
await client.request(
'COPY',
'/docs/file1.txt',
headers={**basic_auth_header, 'Destination': '/docs/file1_copy.txt'}
)
# 5. Move file
await client.request(
'MOVE',
'/docs/file2.txt',
headers={**basic_auth_header, 'Destination': '/docs/renamed.txt'}
)
# 6. Verify final state
response = await client.request(
'PROPFIND',
'/docs/',
headers={**basic_auth_header, 'Depth': '1'}
)
body = await response.text()
assert 'file1.txt' in body
assert 'file1_copy.txt' in body
assert 'renamed.txt' in body
assert 'file2.txt' not in body
# 7. Delete directory
await client.delete('/docs/', headers=basic_auth_header)
# 8. Verify deletion
response = await client.request(
'PROPFIND',
'/docs/',
headers={**basic_auth_header, 'Depth': '0'}
)
assert response.status == 404
# ============================================================================
# Run Tests
# ============================================================================
if __name__ == '__main__':
pytest.main([__file__, '-v', '--tb=short'])

BIN
webdav.db Normal file

Binary file not shown.

85
webdav.service Normal file
View File

@ -0,0 +1,85 @@
# ============================================================================
# WebDAV Server Systemd Service
# Installation: sudo cp webdav.service /etc/systemd/system/
# Enable: sudo systemctl enable webdav
# Start: sudo systemctl start webdav
# Status: sudo systemctl status webdav
# Logs: sudo journalctl -u webdav -f
# ============================================================================
[Unit]
Description=WebDAV Server with aiohttp
Documentation=https://github.com/yourusername/webdav-server
After=network.target
[Service]
Type=notify
# User and group to run the service (create with: sudo useradd -r -s /bin/false webdav)
User=webdav
Group=webdav
# Working directory
WorkingDirectory=/opt/webdav-server
# Environment file
EnvironmentFile=/opt/webdav-server/.env
# Command to start the service (using Gunicorn for production)
ExecStart=/opt/webdav-server/venv/bin/gunicorn main:init_app \
--config /opt/webdav-server/gunicorn_config.py \
--bind 0.0.0.0:8080 \
--worker-class aiohttp.GunicornWebWorker \
--workers 4 \
--access-logfile /var/log/webdav/access.log \
--error-logfile /var/log/webdav/error.log \
--log-level info
# Alternative: Run with Python directly (for development)
# ExecStart=/opt/webdav-server/venv/bin/python /opt/webdav-server/main.py
# Restart policy
Restart=always
RestartSec=10
# Resource limits
LimitNOFILE=65536
LimitNPROC=4096
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/webdav-server/webdav /opt/webdav-server/logs /opt/webdav-server/backups /opt/webdav-server/webdav.db
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
# Process properties
Nice=0
IOSchedulingClass=best-effort
IOSchedulingPriority=4
# Standard output and error
StandardOutput=journal
StandardError=journal
SyslogIdentifier=webdav-server
# Watchdog (for monitoring)
WatchdogSec=60s
# Kill mode
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30s
[Install]
WantedBy=multi-user.target

458
webdav_cli.py Normal file
View File

@ -0,0 +1,458 @@
#!/usr/bin/env python3
"""
WebDAV Server CLI Management Tool
Command-line interface for managing users, permissions, and server configuration
"""
import sys
import asyncio
import argparse
from pathlib import Path
from getpass import getpass
from datetime import datetime
import sqlite3
# Import from main application
from main import Database, Config
class WebDAVCLI:
"""Command-line interface for WebDAV server management"""
def __init__(self):
self.db = Database(Config.DB_PATH)
# ========================================================================
# User Management
# ========================================================================
async def create_user(self, username: str, password: str = None,
email: str = None, role: str = 'user'):
"""Create a new user"""
if not password:
password = getpass(f"Enter password for {username}: ")
password_confirm = getpass("Confirm password: ")
if password != password_confirm:
print("❌ Passwords do not match!")
return False
if len(password) < Config.PASSWORD_MIN_LENGTH:
print(f"❌ Password must be at least {Config.PASSWORD_MIN_LENGTH} characters!")
return False
try:
user_id = await self.db.create_user(username, password, email, role)
print(f"✅ User created successfully!")
print(f" ID: {user_id}")
print(f" Username: {username}")
print(f" Email: {email or 'N/A'}")
print(f" Role: {role}")
return True
except Exception as e:
print(f"❌ Error creating user: {e}")
return False
async def list_users(self):
"""List all users"""
conn = self.db.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT id, username, email, role, is_active, created_at, last_login FROM users')
users = cursor.fetchall()
conn.close()
if not users:
print("No users found.")
return
print(f"\n{'ID':<5} {'Username':<20} {'Email':<30} {'Role':<10} {'Active':<8} {'Created':<20}")
print("=" * 100)
for user in users:
active = "" if user['is_active'] else ""
created = user['created_at'][:19] if user['created_at'] else 'N/A'
email = user['email'] or 'N/A'
print(f"{user['id']:<5} {user['username']:<20} {email:<30} {user['role']:<10} {active:<8} {created:<20}")
async def delete_user(self, username: str, force: bool = False):
"""Delete a user"""
if not force:
confirm = input(f"Are you sure you want to delete user '{username}'? (yes/no): ")
if confirm.lower() != 'yes':
print("Deletion cancelled.")
return False
try:
conn = self.db.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM users WHERE username = ?', (username,))
deleted = cursor.rowcount > 0
conn.commit()
conn.close()
if deleted:
# Remove user directory
user_dir = Path(Config.WEBDAV_ROOT) / 'users' / username
if user_dir.exists():
import shutil
shutil.rmtree(user_dir)
print(f"✅ User '{username}' deleted successfully!")
return True
else:
print(f"❌ User '{username}' not found!")
return False
except Exception as e:
print(f"❌ Error deleting user: {e}")
return False
async def change_password(self, username: str, new_password: str = None):
"""Change user password"""
if not new_password:
new_password = getpass(f"Enter new password for {username}: ")
password_confirm = getpass("Confirm new password: ")
if new_password != password_confirm:
print("❌ Passwords do not match!")
return False
if len(new_password) < Config.PASSWORD_MIN_LENGTH:
print(f"❌ Password must be at least {Config.PASSWORD_MIN_LENGTH} characters!")
return False
try:
import hashlib
import secrets
salt = secrets.token_hex(16)
password_hash = hashlib.pbkdf2_hmac('sha256', new_password.encode(), salt.encode(), 100000).hex()
conn = self.db.get_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET password_hash = ?, salt = ? WHERE username = ?',
(password_hash, salt, username))
updated = cursor.rowcount > 0
conn.commit()
conn.close()
if updated:
print(f"✅ Password changed successfully for user '{username}'!")
return True
else:
print(f"❌ User '{username}' not found!")
return False
except Exception as e:
print(f"❌ Error changing password: {e}")
return False
async def activate_user(self, username: str, active: bool = True):
"""Activate or deactivate a user"""
try:
conn = self.db.get_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET is_active = ? WHERE username = ?',
(1 if active else 0, username))
updated = cursor.rowcount > 0
conn.commit()
conn.close()
if updated:
status = "activated" if active else "deactivated"
print(f"✅ User '{username}' {status} successfully!")
return True
else:
print(f"❌ User '{username}' not found!")
return False
except Exception as e:
print(f"❌ Error updating user: {e}")
return False
# ========================================================================
# Lock Management
# ========================================================================
async def list_locks(self):
"""List all active locks"""
conn = self.db.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT l.lock_token, l.resource_path, l.lock_type, l.lock_scope,
l.created_at, l.timeout_seconds, u.username
FROM locks l
LEFT JOIN users u ON l.user_id = u.id
WHERE datetime(l.created_at, '+' || l.timeout_seconds || ' seconds') > datetime('now')
''')
locks = cursor.fetchall()
conn.close()
if not locks:
print("No active locks found.")
return
print(f"\n{'Resource':<40} {'Type':<10} {'Scope':<12} {'Owner':<15} {'Created':<20}")
print("=" * 100)
for lock in locks:
created = lock['created_at'][:19] if lock['created_at'] else 'N/A'
owner = lock['username'] or 'Unknown'
print(f"{lock['resource_path']:<40} {lock['lock_type']:<10} {lock['lock_scope']:<12} {owner:<15} {created:<20}")
async def remove_lock(self, resource_path: str):
"""Remove a lock from a resource"""
try:
conn = self.db.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM locks WHERE resource_path = ?', (resource_path,))
deleted = cursor.rowcount > 0
conn.commit()
conn.close()
if deleted:
print(f"✅ Lock removed from '{resource_path}'!")
return True
else:
print(f"❌ No lock found on '{resource_path}'!")
return False
except Exception as e:
print(f"❌ Error removing lock: {e}")
return False
# ========================================================================
# Statistics and Info
# ========================================================================
async def show_stats(self):
"""Show server statistics"""
conn = self.db.get_connection()
cursor = conn.cursor()
# User statistics
cursor.execute('SELECT COUNT(*) as total, SUM(is_active) as active FROM users')
user_stats = cursor.fetchone()
# Lock statistics
cursor.execute('''
SELECT COUNT(*) as total FROM locks
WHERE datetime(created_at, '+' || timeout_seconds || ' seconds') > datetime('now')
''')
lock_stats = cursor.fetchone()
# Session statistics
cursor.execute('''
SELECT COUNT(*) as total FROM sessions
WHERE datetime(expires_at) > datetime('now')
''')
session_stats = cursor.fetchone()
# Properties statistics
cursor.execute('SELECT COUNT(*) as total FROM properties')
prop_stats = cursor.fetchone()
conn.close()
# Storage statistics
webdav_root = Path(Config.WEBDAV_ROOT)
if webdav_root.exists():
total_size = sum(f.stat().st_size for f in webdav_root.rglob('*') if f.is_file())
total_files = sum(1 for f in webdav_root.rglob('*') if f.is_file())
else:
total_size = 0
total_files = 0
print("\n" + "=" * 60)
print("WebDAV Server Statistics")
print("=" * 60)
print(f"\n📊 Users:")
print(f" Total: {user_stats['total']}")
print(f" Active: {user_stats['active']}")
print(f" Inactive: {user_stats['total'] - user_stats['active']}")
print(f"\n🔒 Locks:")
print(f" Active: {lock_stats['total']}")
print(f"\n🔑 Sessions:")
print(f" Active: {session_stats['total']}")
print(f"\n📝 Properties:")
print(f" Total: {prop_stats['total']}")
print(f"\n💾 Storage:")
print(f" Total files: {total_files:,}")
print(f" Total size: {total_size:,} bytes ({total_size / 1024 / 1024:.2f} MB)")
print("\n" + "=" * 60)
# ========================================================================
# Database Management
# ========================================================================
async def backup_database(self, output_path: str = None):
"""Backup the database"""
if not output_path:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_path = f"./backups/webdav_backup_{timestamp}.db"
try:
import shutil
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(Config.DB_PATH, output_path)
print(f"✅ Database backed up to: {output_path}")
return True
except Exception as e:
print(f"❌ Error backing up database: {e}")
return False
async def restore_database(self, backup_path: str):
"""Restore database from backup"""
if not Path(backup_path).exists():
print(f"❌ Backup file not found: {backup_path}")
return False
confirm = input("⚠️ This will overwrite the current database. Continue? (yes/no): ")
if confirm.lower() != 'yes':
print("Restore cancelled.")
return False
try:
import shutil
shutil.copy2(backup_path, Config.DB_PATH)
print(f"✅ Database restored from: {backup_path}")
return True
except Exception as e:
print(f"❌ Error restoring database: {e}")
return False
# ============================================================================
# CLI Command Handlers
# ============================================================================
async def main():
"""Main CLI entry point"""
parser = argparse.ArgumentParser(
description='WebDAV Server Management CLI',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s user create john --email john@example.com
%(prog)s user list
%(prog)s user delete john
%(prog)s user password john
%(prog)s lock list
%(prog)s stats
%(prog)s backup
"""
)
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# User commands
user_parser = subparsers.add_parser('user', help='User management')
user_subparsers = user_parser.add_subparsers(dest='user_command')
# Create user
create_parser = user_subparsers.add_parser('create', help='Create a new user')
create_parser.add_argument('username', help='Username')
create_parser.add_argument('--password', help='Password (will prompt if not provided)')
create_parser.add_argument('--email', help='Email address')
create_parser.add_argument('--role', default='user', choices=['user', 'admin'], help='User role')
# List users
user_subparsers.add_parser('list', help='List all users')
# Delete user
delete_parser = user_subparsers.add_parser('delete', help='Delete a user')
delete_parser.add_argument('username', help='Username to delete')
delete_parser.add_argument('--force', action='store_true', help='Skip confirmation')
# Change password
password_parser = user_subparsers.add_parser('password', help='Change user password')
password_parser.add_argument('username', help='Username')
password_parser.add_argument('--new-password', help='New password (will prompt if not provided)')
# Activate/deactivate user
activate_parser = user_subparsers.add_parser('activate', help='Activate a user')
activate_parser.add_argument('username', help='Username')
deactivate_parser = user_subparsers.add_parser('deactivate', help='Deactivate a user')
deactivate_parser.add_argument('username', help='Username')
# Lock commands
lock_parser = subparsers.add_parser('lock', help='Lock management')
lock_subparsers = lock_parser.add_subparsers(dest='lock_command')
lock_subparsers.add_parser('list', help='List all active locks')
remove_parser = lock_subparsers.add_parser('remove', help='Remove a lock')
remove_parser.add_argument('resource', help='Resource path')
# Statistics
subparsers.add_parser('stats', help='Show server statistics')
# Backup
backup_parser = subparsers.add_parser('backup', help='Backup database')
backup_parser.add_argument('--output', help='Output path for backup')
# Restore
restore_parser = subparsers.add_parser('restore', help='Restore database from backup')
restore_parser.add_argument('backup', help='Path to backup file')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
cli = WebDAVCLI()
# Execute command
try:
if args.command == 'user':
if args.user_command == 'create':
await cli.create_user(args.username, args.password, args.email, args.role)
elif args.user_command == 'list':
await cli.list_users()
elif args.user_command == 'delete':
await cli.delete_user(args.username, args.force)
elif args.user_command == 'password':
await cli.change_password(args.username, getattr(args, 'new_password', None))
elif args.user_command == 'activate':
await cli.activate_user(args.username, True)
elif args.user_command == 'deactivate':
await cli.activate_user(args.username, False)
else:
user_parser.print_help()
elif args.command == 'lock':
if args.lock_command == 'list':
await cli.list_locks()
elif args.lock_command == 'remove':
await cli.remove_lock(args.resource)
else:
lock_parser.print_help()
elif args.command == 'stats':
await cli.show_stats()
elif args.command == 'backup':
await cli.backup_database(args.output)
elif args.command == 'restore':
await cli.restore_database(args.backup)
except KeyboardInterrupt:
print("\n\nOperation cancelled.")
sys.exit(1)
except Exception as e:
print(f"\n❌ Error: {e}")
sys.exit(1)
if __name__ == '__main__':
asyncio.run(main())