feat: HAProxy IPv6, ACME fixes, deploy command, docs
HAProxy: - Add IPv6 dual-stack binding (*:port,[::]:port) - Exclude ACME challenges from HTTPS redirects - Fix certificate path detection for multiple locations Service Registry: - Fix certificate expiry check paths (HAProxy, ACME, Let's Encrypt) - BusyBox-compatible date parsing local-build.sh: - Add deploy command for automated package deployment - Sync packages to router feed with index generation Documentation: - Add README for luci-app-haproxy - Add README for luci-app-hexojs - Add README for luci-app-metablogizer - Add README for luci-app-mitmproxy - Add README for luci-app-tor-shield Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b69b8de65
commit
8e7a5b1bb9
449
package/secubox/luci-app-haproxy/README.md
Normal file
449
package/secubox/luci-app-haproxy/README.md
Normal file
@ -0,0 +1,449 @@
|
||||
# ⚡ HAProxy Manager - Reverse Proxy Dashboard
|
||||
|
||||
Enterprise-grade reverse proxy management with automatic SSL certificates, vhost configuration, and backend health monitoring.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🌐 **Vhost Management** | Create and manage virtual hosts |
|
||||
| 🔒 **ACME SSL** | Automatic Let's Encrypt certificates |
|
||||
| ⚖️ **Load Balancing** | Round-robin, least-conn, source |
|
||||
| 🏥 **Health Checks** | Backend server monitoring |
|
||||
| 📊 **Statistics** | Real-time traffic dashboard |
|
||||
| 🔧 **Config Generator** | Auto-generate HAProxy config |
|
||||
| 🐳 **LXC Container** | Runs isolated in container |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Create a Vhost
|
||||
|
||||
1. Go to **Services → HAProxy → Vhosts**
|
||||
2. Click **+ Add Vhost**
|
||||
3. Fill in:
|
||||
- **Domain**: `app.example.com`
|
||||
- **Backend**: Select or create
|
||||
- **SSL**: ✅ Enable
|
||||
- **ACME**: ✅ Auto-certificate
|
||||
4. Click **Save & Apply**
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
Internet │ HAProxy Container │
|
||||
│ │ ┌─────────────────────────┐ │
|
||||
▼ │ │ Frontend │ │
|
||||
┌─────────┐ │ │ :80 → :443 redirect │ │
|
||||
│ Port 80 │──────►│ │ :443 → SSL terminate │ │
|
||||
│Port 443 │ │ └──────────┬──────────────┘ │
|
||||
└─────────┘ │ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ Backends │ │
|
||||
│ │ app.example.com →:8080 │ │
|
||||
│ │ api.example.com →:3000 │ │
|
||||
│ │ blog.example.com→:4000 │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📊 Dashboard
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ ⚡ HAProxy 🟢 Running │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📊 Statistics │
|
||||
│ ├─ 🌐 Vhosts: 5 active │
|
||||
│ ├─ ⚙️ Backends: 8 configured │
|
||||
│ ├─ 🔒 Certificates: 5 valid │
|
||||
│ └─ 📈 Requests: 12.5K/min │
|
||||
│ │
|
||||
│ 🏥 Backend Health │
|
||||
│ ┌────────────┬────────┬────────┬─────────┐ │
|
||||
│ │ Backend │ Status │ Server │ Latency │ │
|
||||
│ ├────────────┼────────┼────────┼─────────┤ │
|
||||
│ │ webapp │ 🟢 UP │ 2/2 │ 12ms │ │
|
||||
│ │ api │ 🟢 UP │ 1/1 │ 8ms │ │
|
||||
│ │ blog │ 🟡 DEG │ 1/2 │ 45ms │ │
|
||||
│ └────────────┴────────┴────────┴─────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🌐 Vhost Configuration
|
||||
|
||||
### Create Vhost
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy create_vhost '{
|
||||
"domain": "app.example.com",
|
||||
"backend": "webapp",
|
||||
"ssl": 1,
|
||||
"ssl_redirect": 1,
|
||||
"acme": 1,
|
||||
"enabled": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### Vhost Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `domain` | - | Domain name (required) |
|
||||
| `backend` | - | Backend name to route to |
|
||||
| `ssl` | 1 | Enable SSL/TLS |
|
||||
| `ssl_redirect` | 1 | Redirect HTTP to HTTPS |
|
||||
| `acme` | 1 | Auto-request Let's Encrypt cert |
|
||||
| `enabled` | 1 | Vhost active |
|
||||
|
||||
### List Vhosts
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy list_vhosts
|
||||
|
||||
# Response:
|
||||
{
|
||||
"vhosts": [{
|
||||
"id": "app_example_com",
|
||||
"domain": "app.example.com",
|
||||
"backend": "webapp",
|
||||
"ssl": true,
|
||||
"ssl_redirect": true,
|
||||
"acme": true,
|
||||
"enabled": true,
|
||||
"cert_status": "valid",
|
||||
"cert_expiry": "2025-03-15"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## ⚙️ Backend Configuration
|
||||
|
||||
### Create Backend
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy create_backend '{
|
||||
"name": "webapp",
|
||||
"mode": "http",
|
||||
"balance": "roundrobin"
|
||||
}'
|
||||
```
|
||||
|
||||
### Add Server to Backend
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy create_server '{
|
||||
"backend": "webapp",
|
||||
"name": "srv1",
|
||||
"address": "192.168.255.10",
|
||||
"port": 8080,
|
||||
"weight": 100,
|
||||
"check": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### Backend Modes
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `http` | Layer 7 HTTP proxy |
|
||||
| `tcp` | Layer 4 TCP proxy |
|
||||
|
||||
### Load Balancing
|
||||
|
||||
| Algorithm | Description |
|
||||
|-----------|-------------|
|
||||
| `roundrobin` | Rotate through servers |
|
||||
| `leastconn` | Least active connections |
|
||||
| `source` | Sticky by client IP |
|
||||
| `uri` | Sticky by URI hash |
|
||||
|
||||
## 🔒 SSL Certificates
|
||||
|
||||
### ACME Auto-Certificates
|
||||
|
||||
When `acme: 1` is set:
|
||||
1. HAProxy serves ACME challenge on port 80
|
||||
2. Let's Encrypt validates domain ownership
|
||||
3. Certificate stored in `/srv/haproxy/certs/`
|
||||
4. Auto-renewal before expiry
|
||||
|
||||
### Manual Certificate
|
||||
|
||||
```bash
|
||||
# Upload certificate
|
||||
ubus call luci.haproxy upload_certificate '{
|
||||
"domain": "app.example.com",
|
||||
"cert": "<PEM certificate>",
|
||||
"key": "<PEM private key>"
|
||||
}'
|
||||
```
|
||||
|
||||
### Certificate Status
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy list_certificates
|
||||
|
||||
# Response:
|
||||
{
|
||||
"certificates": [{
|
||||
"domain": "app.example.com",
|
||||
"status": "valid",
|
||||
"issuer": "Let's Encrypt",
|
||||
"expiry": "2025-03-15",
|
||||
"days_left": 45
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Request Certificate Manually
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy request_certificate '{"domain":"app.example.com"}'
|
||||
```
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
### Get Stats
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy get_stats
|
||||
|
||||
# Response:
|
||||
{
|
||||
"frontend": {
|
||||
"requests": 125000,
|
||||
"bytes_in": 1234567890,
|
||||
"bytes_out": 9876543210,
|
||||
"rate": 150
|
||||
},
|
||||
"backends": [{
|
||||
"name": "webapp",
|
||||
"status": "UP",
|
||||
"servers_up": 2,
|
||||
"servers_total": 2,
|
||||
"requests": 45000,
|
||||
"response_time_avg": 12
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Stats Page
|
||||
|
||||
Access HAProxy stats at:
|
||||
```
|
||||
http://192.168.255.1:8404/stats
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### UCI Structure
|
||||
|
||||
```bash
|
||||
# /etc/config/haproxy
|
||||
|
||||
config haproxy 'main'
|
||||
option enabled '1'
|
||||
option stats_port '8404'
|
||||
|
||||
config backend 'webapp'
|
||||
option name 'webapp'
|
||||
option mode 'http'
|
||||
option balance 'roundrobin'
|
||||
option enabled '1'
|
||||
|
||||
config server 'webapp_srv1'
|
||||
option backend 'webapp'
|
||||
option name 'srv1'
|
||||
option address '192.168.255.10'
|
||||
option port '8080'
|
||||
option weight '100'
|
||||
option check '1'
|
||||
option enabled '1'
|
||||
|
||||
config vhost 'app_example_com'
|
||||
option domain 'app.example.com'
|
||||
option backend 'webapp'
|
||||
option ssl '1'
|
||||
option ssl_redirect '1'
|
||||
option acme '1'
|
||||
option enabled '1'
|
||||
```
|
||||
|
||||
### Generate Config
|
||||
|
||||
```bash
|
||||
# Regenerate haproxy.cfg from UCI
|
||||
ubus call luci.haproxy generate
|
||||
|
||||
# Reload HAProxy
|
||||
ubus call luci.haproxy reload
|
||||
```
|
||||
|
||||
### Validate Config
|
||||
|
||||
```bash
|
||||
ubus call luci.haproxy validate
|
||||
|
||||
# Response:
|
||||
{
|
||||
"valid": true,
|
||||
"message": "Configuration is valid"
|
||||
}
|
||||
```
|
||||
|
||||
## 📡 RPCD API
|
||||
|
||||
### Service Control
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `status` | Get HAProxy status |
|
||||
| `start` | Start HAProxy service |
|
||||
| `stop` | Stop HAProxy service |
|
||||
| `restart` | Restart HAProxy |
|
||||
| `reload` | Reload configuration |
|
||||
| `generate` | Generate config file |
|
||||
| `validate` | Validate configuration |
|
||||
|
||||
### Vhost Management
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `list_vhosts` | List all vhosts |
|
||||
| `create_vhost` | Create new vhost |
|
||||
| `update_vhost` | Update vhost |
|
||||
| `delete_vhost` | Delete vhost |
|
||||
|
||||
### Backend Management
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `list_backends` | List all backends |
|
||||
| `create_backend` | Create backend |
|
||||
| `delete_backend` | Delete backend |
|
||||
| `create_server` | Add server to backend |
|
||||
| `delete_server` | Remove server |
|
||||
|
||||
### Certificates
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `list_certificates` | List all certs |
|
||||
| `request_certificate` | Request ACME cert |
|
||||
| `upload_certificate` | Upload manual cert |
|
||||
| `delete_certificate` | Delete certificate |
|
||||
|
||||
## 📁 File Locations
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/etc/config/haproxy` | UCI configuration |
|
||||
| `/var/lib/lxc/haproxy/` | LXC container root |
|
||||
| `/srv/haproxy/haproxy.cfg` | Generated config |
|
||||
| `/srv/haproxy/certs/` | SSL certificates |
|
||||
| `/srv/haproxy/acme/` | ACME challenges |
|
||||
| `/usr/libexec/rpcd/luci.haproxy` | RPCD backend |
|
||||
| `/usr/sbin/haproxyctl` | CLI tool |
|
||||
|
||||
## 🛠️ CLI Tool
|
||||
|
||||
### haproxyctl Commands
|
||||
|
||||
```bash
|
||||
# Status
|
||||
haproxyctl status
|
||||
|
||||
# List vhosts
|
||||
haproxyctl vhosts
|
||||
|
||||
# Add vhost
|
||||
haproxyctl vhost add app.example.com --backend webapp --ssl --acme
|
||||
|
||||
# Remove vhost
|
||||
haproxyctl vhost del app.example.com
|
||||
|
||||
# List certificates
|
||||
haproxyctl cert list
|
||||
|
||||
# Request certificate
|
||||
haproxyctl cert add app.example.com
|
||||
|
||||
# Generate config
|
||||
haproxyctl generate
|
||||
|
||||
# Reload
|
||||
haproxyctl reload
|
||||
|
||||
# Validate
|
||||
haproxyctl validate
|
||||
```
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### HAProxy Won't Start
|
||||
|
||||
```bash
|
||||
# Check container
|
||||
lxc-info -n haproxy
|
||||
|
||||
# Start container
|
||||
lxc-start -n haproxy
|
||||
|
||||
# Check logs
|
||||
lxc-attach -n haproxy -- cat /var/log/haproxy.log
|
||||
```
|
||||
|
||||
### 503 Service Unavailable
|
||||
|
||||
1. Check backend is configured:
|
||||
```bash
|
||||
ubus call luci.haproxy list_backends
|
||||
```
|
||||
2. Verify server is reachable:
|
||||
```bash
|
||||
curl http://192.168.255.10:8080
|
||||
```
|
||||
3. Check HAProxy logs
|
||||
|
||||
### Certificate Not Working
|
||||
|
||||
1. Ensure DNS resolves to your public IP
|
||||
2. Ensure ports 80/443 accessible from internet
|
||||
3. Check ACME challenge:
|
||||
```bash
|
||||
curl http://app.example.com/.well-known/acme-challenge/test
|
||||
```
|
||||
|
||||
### Config Validation Fails
|
||||
|
||||
```bash
|
||||
# Show validation errors
|
||||
lxc-attach -n haproxy -- haproxy -c -f /etc/haproxy/haproxy.cfg
|
||||
```
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
### Firewall Rules
|
||||
|
||||
HAProxy needs ports 80/443 open from WAN:
|
||||
|
||||
```bash
|
||||
# Auto-created when vhost uses SSL
|
||||
uci show firewall | grep HAProxy
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Add to backend config:
|
||||
```
|
||||
stick-table type ip size 100k expire 30s store http_req_rate(10s)
|
||||
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
|
||||
```
|
||||
|
||||
## 📜 License
|
||||
|
||||
MIT License - Copyright (C) 2025 CyberMind.fr
|
||||
405
package/secubox/luci-app-hexojs/README.md
Normal file
405
package/secubox/luci-app-hexojs/README.md
Normal file
@ -0,0 +1,405 @@
|
||||
# 📰 Hexo CMS - Blog Publishing Platform
|
||||
|
||||
Full-featured Hexo blog management with multi-instance support, Gitea integration, HAProxy publishing, and Tor hidden services.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 📝 **Post Editor** | Create, edit, publish posts with markdown |
|
||||
| 📁 **Categories/Tags** | Organize content hierarchically |
|
||||
| 🖼️ **Media Library** | Manage images and assets |
|
||||
| 🎨 **Theme Config** | Edit Hexo theme settings |
|
||||
| 🚀 **One-Click Deploy** | Generate and deploy with single click |
|
||||
| 🔗 **HAProxy Integration** | Auto-publish to clearnet with SSL |
|
||||
| 🧅 **Tor Hidden Services** | Publish to .onion addresses |
|
||||
| 📦 **Gitea Sync** | Push/pull from Git repositories |
|
||||
| 🧙 **Publishing Profiles** | Wizard presets for common setups |
|
||||
| 📊 **Health Monitoring** | Pipeline status and diagnostics |
|
||||
|
||||
## 🚀 Quick Start Wizard
|
||||
|
||||
### Publishing Profiles
|
||||
|
||||
Choose a preset to configure your blog:
|
||||
|
||||
| Profile | Icon | HAProxy | Tor | Use Case |
|
||||
|---------|------|---------|-----|----------|
|
||||
| 🌐 **Blog** | 📰 | ✅ SSL | ❌ | Public blog with custom domain |
|
||||
| 🎨 **Portfolio** | 🖼️ | ✅ SSL | ❌ | Creative showcase |
|
||||
| 🔒 **Privacy** | 🧅 | ❌ | ✅ | Anonymous .onion blog |
|
||||
| 🌍 **Dual** | 🌐🧅 | ✅ | ✅ | Clearnet + Tor access |
|
||||
| 📚 **Documentation** | 📖 | ✅ SSL | ❌ | Technical docs site |
|
||||
|
||||
### Apply a Profile
|
||||
|
||||
```bash
|
||||
# Via LuCI: Services → Hexo CMS → Profiles → Apply
|
||||
|
||||
# Via CLI
|
||||
ubus call luci.hexojs apply_profile '{
|
||||
"instance": "default",
|
||||
"profile": "blog",
|
||||
"domain": "blog.example.com"
|
||||
}'
|
||||
```
|
||||
|
||||
## 📊 Dashboard
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 📰 Hexo CMS 🟢 Running │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📊 Site Stats │
|
||||
│ ├─ 📝 Posts: 134 │
|
||||
│ ├─ 📁 Categories: 12 │
|
||||
│ ├─ 🏷️ Tags: 45 │
|
||||
│ └─ 🖼️ Media: 89 files │
|
||||
│ │
|
||||
│ 🔗 Endpoints │
|
||||
│ ├─ 🏠 Local: http://192.168.255.1:4000 │
|
||||
│ ├─ 🌐 Clearnet: https://blog.example.com │
|
||||
│ └─ 🧅 Tor: http://abc123xyz.onion │
|
||||
│ │
|
||||
│ 📈 Pipeline Health: 95/100 │
|
||||
│ ├─ ✅ Hexo Server: Running │
|
||||
│ ├─ ✅ HAProxy: Published │
|
||||
│ ├─ ✅ Certificate: Valid (45 days) │
|
||||
│ └─ ✅ Gitea: Synced │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📝 Content Management
|
||||
|
||||
### Create a Post
|
||||
|
||||
1. Go to **Services → Hexo CMS → Posts**
|
||||
2. Click **+ New Post**
|
||||
3. Fill in:
|
||||
- **Title**: My First Post
|
||||
- **Category**: tech/tutorials
|
||||
- **Tags**: hexo, blog
|
||||
- **Content**: Your markdown here
|
||||
4. Click **Save Draft** or **Publish**
|
||||
|
||||
### Post Front Matter
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: My First Post
|
||||
date: 2025-01-28 10:30:00
|
||||
categories:
|
||||
- tech
|
||||
- tutorials
|
||||
tags:
|
||||
- hexo
|
||||
- blog
|
||||
---
|
||||
|
||||
Your content here...
|
||||
```
|
||||
|
||||
### List Posts via CLI
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs list_posts '{"instance":"default","limit":10}'
|
||||
```
|
||||
|
||||
## 🚀 Publishing Pipeline
|
||||
|
||||
### Full Publish Flow
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Edit │ → │ Generate│ → │ Deploy │ → │ Live │
|
||||
│ Posts │ │ HTML │ │ HAProxy │ │ Online │
|
||||
└─────────┘ └─────────┘ │ Tor │ └─────────┘
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Generate static files
|
||||
ubus call luci.hexojs generate '{"instance":"default"}'
|
||||
|
||||
# Deploy to HAProxy (clearnet)
|
||||
ubus call luci.hexojs publish_to_haproxy '{
|
||||
"instance": "default",
|
||||
"domain": "blog.example.com"
|
||||
}'
|
||||
|
||||
# Deploy to Tor (.onion)
|
||||
ubus call luci.hexojs publish_to_tor '{"instance":"default"}'
|
||||
|
||||
# Full pipeline (generate + deploy all)
|
||||
ubus call luci.hexojs full_publish '{
|
||||
"instance": "default",
|
||||
"domain": "blog.example.com",
|
||||
"tor": true
|
||||
}'
|
||||
```
|
||||
|
||||
## 🔗 HAProxy Integration
|
||||
|
||||
### Publish to Clearnet
|
||||
|
||||
1. Go to **Hexo CMS → Publishing**
|
||||
2. Enter domain: `blog.example.com`
|
||||
3. Check **Enable SSL**
|
||||
4. Click **Publish to HAProxy**
|
||||
|
||||
### What Happens
|
||||
|
||||
1. ✅ Creates HAProxy backend → `hexo_default`
|
||||
2. ✅ Creates HAProxy server → `127.0.0.1:4000`
|
||||
3. ✅ Creates vhost → `blog.example.com`
|
||||
4. ✅ Requests ACME certificate
|
||||
5. ✅ Reloads HAProxy
|
||||
|
||||
### Check HAProxy Status
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs get_haproxy_status '{"instance":"default"}'
|
||||
|
||||
# Response:
|
||||
{
|
||||
"published": true,
|
||||
"domain": "blog.example.com",
|
||||
"ssl": true,
|
||||
"cert_status": "valid",
|
||||
"cert_days": 45,
|
||||
"dns_status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
## 🧅 Tor Hidden Services
|
||||
|
||||
### Create .onion Site
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs publish_to_tor '{"instance":"default"}'
|
||||
```
|
||||
|
||||
### Get Onion Address
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs get_tor_status '{"instance":"default"}'
|
||||
|
||||
# Response:
|
||||
{
|
||||
"enabled": true,
|
||||
"onion_address": "abc123xyz...def.onion",
|
||||
"virtual_port": 80,
|
||||
"status": "active"
|
||||
}
|
||||
```
|
||||
|
||||
### Access via Tor Browser
|
||||
|
||||
```
|
||||
http://abc123xyz...def.onion
|
||||
```
|
||||
|
||||
## 📦 Gitea Integration
|
||||
|
||||
### Setup Gitea Sync
|
||||
|
||||
1. Go to **Hexo CMS → Git**
|
||||
2. Enter repository: `user/myblog`
|
||||
3. Configure credentials (optional)
|
||||
4. Click **Clone** or **Pull**
|
||||
|
||||
### Webhook Auto-Deploy
|
||||
|
||||
Enable automatic deployment when you push to Gitea:
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs setup_webhook '{
|
||||
"instance": "default",
|
||||
"auto_build": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Git Operations
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
ubus call luci.hexojs git_clone '{
|
||||
"instance": "default",
|
||||
"url": "http://192.168.255.1:3000/user/myblog.git"
|
||||
}'
|
||||
|
||||
# Pull latest
|
||||
ubus call luci.hexojs git_pull '{"instance":"default"}'
|
||||
|
||||
# Push changes
|
||||
ubus call luci.hexojs git_push '{"instance":"default"}'
|
||||
|
||||
# View log
|
||||
ubus call luci.hexojs git_log '{"instance":"default","limit":10}'
|
||||
```
|
||||
|
||||
## 📊 Health Monitoring
|
||||
|
||||
### Instance Health Score
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs get_instance_health '{"instance":"default"}'
|
||||
|
||||
# Response:
|
||||
{
|
||||
"instance": "default",
|
||||
"score": 95,
|
||||
"status": "healthy",
|
||||
"checks": {
|
||||
"hexo_running": true,
|
||||
"content_exists": true,
|
||||
"haproxy_published": true,
|
||||
"ssl_valid": true,
|
||||
"dns_resolves": true,
|
||||
"git_clean": true
|
||||
},
|
||||
"issues": []
|
||||
}
|
||||
```
|
||||
|
||||
### Health Score Breakdown
|
||||
|
||||
| Check | Points | Description |
|
||||
|-------|--------|-------------|
|
||||
| Hexo Running | 20 | Server process active |
|
||||
| Content Exists | 15 | Posts directory has content |
|
||||
| HAProxy Published | 20 | Vhost configured |
|
||||
| SSL Valid | 15 | Certificate not expiring |
|
||||
| DNS Resolves | 15 | Domain points to server |
|
||||
| Git Clean | 15 | No uncommitted changes |
|
||||
|
||||
### Pipeline Status
|
||||
|
||||
```bash
|
||||
ubus call luci.hexojs get_pipeline_status
|
||||
|
||||
# Returns status of all instances
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### UCI Settings
|
||||
|
||||
```bash
|
||||
# /etc/config/hexojs
|
||||
|
||||
config hexojs 'main'
|
||||
option enabled '1'
|
||||
option instances_root '/srv/hexojs/instances'
|
||||
option content_root '/srv/hexojs/content'
|
||||
|
||||
config instance 'default'
|
||||
option name 'default'
|
||||
option enabled '1'
|
||||
option port '4000'
|
||||
option theme 'landscape'
|
||||
# HAProxy
|
||||
option haproxy_enabled '1'
|
||||
option haproxy_domain 'blog.example.com'
|
||||
option haproxy_ssl '1'
|
||||
# Tor
|
||||
option tor_enabled '1'
|
||||
option tor_onion 'abc123...onion'
|
||||
# Gitea
|
||||
option gitea_repo 'user/myblog'
|
||||
option gitea_auto_build '1'
|
||||
```
|
||||
|
||||
## 📁 File Locations
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/etc/config/hexojs` | UCI configuration |
|
||||
| `/srv/hexojs/instances/` | Instance directories |
|
||||
| `/srv/hexojs/content/` | Shared content (posts, media) |
|
||||
| `/srv/hexojs/content/source/_posts/` | Blog posts |
|
||||
| `/srv/hexojs/content/source/images/` | Media files |
|
||||
| `/usr/libexec/rpcd/luci.hexojs` | RPCD backend |
|
||||
|
||||
## 📡 RPCD Methods
|
||||
|
||||
### Content Management
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `list_posts` | List all posts |
|
||||
| `get_post` | Get single post content |
|
||||
| `create_post` | Create new post |
|
||||
| `update_post` | Update post content |
|
||||
| `delete_post` | Delete a post |
|
||||
| `publish_post` | Move draft to published |
|
||||
| `search_posts` | Search posts by query |
|
||||
|
||||
### Site Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `generate` | Generate static HTML |
|
||||
| `clean` | Clean generated files |
|
||||
| `deploy` | Deploy to configured targets |
|
||||
| `preview_start` | Start preview server |
|
||||
| `preview_status` | Check preview server |
|
||||
|
||||
### Publishing
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `publish_to_haproxy` | Publish to clearnet |
|
||||
| `unpublish_from_haproxy` | Remove from HAProxy |
|
||||
| `publish_to_tor` | Create Tor hidden service |
|
||||
| `unpublish_from_tor` | Remove Tor service |
|
||||
| `full_publish` | Complete pipeline |
|
||||
|
||||
### Monitoring
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `get_instance_health` | Health score & checks |
|
||||
| `get_pipeline_status` | All instances status |
|
||||
| `get_instance_endpoints` | All URLs for instance |
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Hexo Server Won't Start
|
||||
|
||||
```bash
|
||||
# Check if port is in use
|
||||
netstat -tln | grep 4000
|
||||
|
||||
# Check logs
|
||||
logread | grep hexo
|
||||
|
||||
# Restart manually
|
||||
/etc/init.d/hexojs restart
|
||||
```
|
||||
|
||||
### Posts Not Showing
|
||||
|
||||
1. Check posts are in `/srv/hexojs/content/source/_posts/`
|
||||
2. Verify front matter format is correct
|
||||
3. Run `hexo clean && hexo generate`
|
||||
|
||||
### HAProxy 503 Error
|
||||
|
||||
1. Verify Hexo is running on expected port
|
||||
2. Check HAProxy backend configuration
|
||||
3. Test local access: `curl http://127.0.0.1:4000`
|
||||
|
||||
### Git Push Fails
|
||||
|
||||
1. Check credentials: `ubus call luci.hexojs git_get_credentials`
|
||||
2. Verify remote URL is correct
|
||||
3. Check Gitea is accessible
|
||||
|
||||
## 📜 License
|
||||
|
||||
MIT License - Copyright (C) 2025 CyberMind.fr
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,14 @@
|
||||
"git_status",
|
||||
"git_log",
|
||||
"git_get_credentials",
|
||||
"gitea_status"
|
||||
"gitea_status",
|
||||
"get_workflow_status",
|
||||
"list_profiles",
|
||||
"get_haproxy_status",
|
||||
"get_tor_status",
|
||||
"get_instance_endpoints",
|
||||
"get_instance_health",
|
||||
"get_pipeline_status"
|
||||
]
|
||||
},
|
||||
"uci": ["hexojs"]
|
||||
@ -57,7 +64,15 @@
|
||||
"gitea_clone",
|
||||
"gitea_sync",
|
||||
"gitea_save_config",
|
||||
"publish_to_www"
|
||||
"publish_to_www",
|
||||
"apply_profile",
|
||||
"publish_to_haproxy",
|
||||
"unpublish_from_haproxy",
|
||||
"publish_to_tor",
|
||||
"unpublish_from_tor",
|
||||
"full_publish",
|
||||
"handle_webhook",
|
||||
"setup_webhook"
|
||||
]
|
||||
},
|
||||
"uci": ["hexojs"]
|
||||
|
||||
259
package/secubox/luci-app-metablogizer/README.md
Normal file
259
package/secubox/luci-app-metablogizer/README.md
Normal file
@ -0,0 +1,259 @@
|
||||
# 📝 MetaBlogizer - Static Site Publisher
|
||||
|
||||
One-click static website hosting with automatic HAProxy vhosts, SSL certificates, and Gitea sync.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🌐 **Auto Vhost** | Creates HAProxy vhost + backend automatically |
|
||||
| 🔒 **ACME SSL** | Automatic Let's Encrypt certificates |
|
||||
| 📦 **Gitea Sync** | Pull from Gitea repositories |
|
||||
| 📤 **File Upload** | Drag & drop file uploads |
|
||||
| 📊 **Health Status** | DNS, certificate, and publish monitoring |
|
||||
| 🔗 **QR Codes** | Share sites with QR codes |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Create a Site via LuCI
|
||||
|
||||
1. Go to **Services → MetaBlogizer**
|
||||
2. Click **+ New Site**
|
||||
3. Fill in:
|
||||
- **Site Name**: `myblog`
|
||||
- **Domain**: `blog.example.com`
|
||||
- **Gitea Repo**: `user/repo` (optional)
|
||||
4. Click **Create**
|
||||
|
||||
### What Happens Automatically
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 📝 Create Site "myblog" @ blog.example.com │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 1. 📁 Create /srv/metablogizer/sites/myblog/ │
|
||||
│ 2. 🌐 Create HAProxy backend (metablog_myblog) │
|
||||
│ 3. 🔗 Create HAProxy vhost (blog.example.com) │
|
||||
│ 4. 🔒 Request ACME certificate │
|
||||
│ 5. 📄 Generate default index.html │
|
||||
│ 6. ✅ Site live at https://blog.example.com │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📊 Dashboard
|
||||
|
||||
### Web Hosting Status Panel
|
||||
|
||||
The dashboard shows real-time health for all sites:
|
||||
|
||||
| Site | Domain | DNS | Resolved IP | Certificate | Status |
|
||||
|------|--------|-----|-------------|-------------|--------|
|
||||
| myblog | blog.example.com | 🌐 ok | 185.220.x.x | 🔒 45d | ✅ published |
|
||||
| docs | docs.example.com | ❌ failed | - | ⚪ missing | 🕐 pending |
|
||||
|
||||
### Status Indicators
|
||||
|
||||
| Icon | DNS Status | Meaning |
|
||||
|------|------------|---------|
|
||||
| 🌐 | ok | DNS resolves to your public IP |
|
||||
| ⚠️ | private | DNS points to private IP (192.168.x.x) |
|
||||
| ❗ | mismatch | DNS points to different public IP |
|
||||
| ❌ | failed | DNS resolution failed |
|
||||
|
||||
| Icon | Cert Status | Meaning |
|
||||
|------|-------------|---------|
|
||||
| 🔒 | ok | Certificate valid (30+ days) |
|
||||
| ⚠️ | warning | Certificate expiring (7-30 days) |
|
||||
| 🔴 | critical | Certificate critical (<7 days) |
|
||||
| 💀 | expired | Certificate expired |
|
||||
| ⚪ | missing | No certificate |
|
||||
|
||||
| Icon | Publish Status | Meaning |
|
||||
|------|----------------|---------|
|
||||
| ✅ | published | Site enabled with content |
|
||||
| 🕐 | pending | Site enabled, no content yet |
|
||||
| 📝 | draft | Site disabled |
|
||||
|
||||
## 📁 File Management
|
||||
|
||||
### Upload Files
|
||||
|
||||
1. Click **Upload** on a site card
|
||||
2. Drag & drop files or click to browse
|
||||
3. Check "Set first HTML as homepage" to use as index.html
|
||||
4. Click **Upload**
|
||||
|
||||
### Manage Files
|
||||
|
||||
1. Click **Files** on a site card
|
||||
2. View all uploaded files
|
||||
3. 🏠 Set any HTML file as homepage
|
||||
4. 🗑️ Delete files
|
||||
|
||||
## 🔄 Gitea Sync
|
||||
|
||||
### Setup
|
||||
|
||||
1. Create/edit a site
|
||||
2. Enter Gitea repository: `username/repo`
|
||||
3. Click **Sync** to pull latest
|
||||
|
||||
### Auto-Sync
|
||||
|
||||
The site syncs from Gitea on:
|
||||
- Manual sync button click
|
||||
- Webhook push (if configured)
|
||||
|
||||
```bash
|
||||
# Manual sync via CLI
|
||||
ubus call luci.metablogizer sync_site '{"id":"site_myblog"}'
|
||||
```
|
||||
|
||||
## 📤 Share & QR
|
||||
|
||||
Click **Share** on any site to get:
|
||||
- 📋 Copy URL to clipboard
|
||||
- 📱 QR code for mobile access
|
||||
- 🐦 Twitter share
|
||||
- 💼 LinkedIn share
|
||||
- 📘 Facebook share
|
||||
- ✈️ Telegram share
|
||||
- 📱 WhatsApp share
|
||||
- ✉️ Email share
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### UCI Settings
|
||||
|
||||
```bash
|
||||
# /etc/config/metablogizer
|
||||
|
||||
config metablogizer 'main'
|
||||
option enabled '1'
|
||||
option runtime 'auto' # auto | uhttpd | nginx
|
||||
option sites_root '/srv/metablogizer/sites'
|
||||
option nginx_container 'nginx'
|
||||
option gitea_url 'http://192.168.255.1:3000'
|
||||
|
||||
config site 'site_myblog'
|
||||
option name 'myblog'
|
||||
option domain 'blog.example.com'
|
||||
option gitea_repo 'user/myblog'
|
||||
option ssl '1'
|
||||
option enabled '1'
|
||||
option description 'My personal blog'
|
||||
option port '8901'
|
||||
option runtime 'uhttpd'
|
||||
```
|
||||
|
||||
### Runtime Modes
|
||||
|
||||
| Mode | Description | Use Case |
|
||||
|------|-------------|----------|
|
||||
| **uhttpd** | OpenWrt's built-in web server | Default, lightweight |
|
||||
| **nginx** | Nginx in LXC container | Advanced features |
|
||||
| **auto** | Auto-detect available runtime | Recommended |
|
||||
|
||||
## 📡 RPCD API
|
||||
|
||||
### Site Management
|
||||
|
||||
```bash
|
||||
# List all sites
|
||||
ubus call luci.metablogizer list_sites
|
||||
|
||||
# Create site
|
||||
ubus call luci.metablogizer create_site '{
|
||||
"name": "myblog",
|
||||
"domain": "blog.example.com",
|
||||
"gitea_repo": "user/myblog",
|
||||
"ssl": "1",
|
||||
"description": "My blog"
|
||||
}'
|
||||
|
||||
# Sync from Gitea
|
||||
ubus call luci.metablogizer sync_site '{"id":"site_myblog"}'
|
||||
|
||||
# Delete site
|
||||
ubus call luci.metablogizer delete_site '{"id":"site_myblog"}'
|
||||
```
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
```bash
|
||||
# Get hosting status for all sites
|
||||
ubus call luci.metablogizer get_hosting_status
|
||||
|
||||
# Response:
|
||||
{
|
||||
"success": true,
|
||||
"public_ip": "185.220.101.12",
|
||||
"haproxy_status": "running",
|
||||
"sites": [{
|
||||
"id": "site_myblog",
|
||||
"name": "myblog",
|
||||
"domain": "blog.example.com",
|
||||
"dns_status": "ok",
|
||||
"dns_ip": "185.220.101.12",
|
||||
"cert_status": "ok",
|
||||
"cert_days": 45,
|
||||
"publish_status": "published"
|
||||
}]
|
||||
}
|
||||
|
||||
# Check single site health
|
||||
ubus call luci.metablogizer check_site_health '{"id":"site_myblog"}'
|
||||
```
|
||||
|
||||
### File Operations
|
||||
|
||||
```bash
|
||||
# List files in site
|
||||
ubus call luci.metablogizer list_files '{"id":"site_myblog"}'
|
||||
|
||||
# Upload file (base64 content)
|
||||
ubus call luci.metablogizer upload_file '{
|
||||
"id": "site_myblog",
|
||||
"filename": "style.css",
|
||||
"content": "Ym9keSB7IGJhY2tncm91bmQ6ICNmZmY7IH0="
|
||||
}'
|
||||
```
|
||||
|
||||
## 📁 File Locations
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/etc/config/metablogizer` | UCI configuration |
|
||||
| `/srv/metablogizer/sites/` | Site content directories |
|
||||
| `/srv/metablogizer/sites/<name>/index.html` | Site homepage |
|
||||
| `/usr/libexec/rpcd/luci.metablogizer` | RPCD backend |
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Site Shows 503
|
||||
|
||||
1. Check HAProxy is running: `lxc-info -n haproxy`
|
||||
2. Check backend port is listening
|
||||
3. Verify uhttpd instance: `uci show uhttpd | grep metablog`
|
||||
|
||||
### DNS Not Resolving
|
||||
|
||||
1. Verify A record points to your public IP
|
||||
2. Check with: `nslookup blog.example.com`
|
||||
3. Wait for DNS propagation (up to 48h)
|
||||
|
||||
### Certificate Missing
|
||||
|
||||
1. Ensure DNS resolves correctly first
|
||||
2. Ensure ports 80/443 accessible from internet
|
||||
3. Check ACME logs: `logread | grep acme`
|
||||
|
||||
### Gitea Sync Fails
|
||||
|
||||
1. Verify Gitea URL: `uci get metablogizer.main.gitea_url`
|
||||
2. Check repository exists and is public
|
||||
3. Test manually: `git clone http://192.168.255.1:3000/user/repo.git`
|
||||
|
||||
## 📜 License
|
||||
|
||||
MIT License - Copyright (C) 2025 CyberMind.fr
|
||||
File diff suppressed because one or more lines are too long
@ -755,6 +755,302 @@ method_get_settings() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Helper: Check DNS resolution for a domain
|
||||
check_dns_resolution() {
|
||||
local domain="$1"
|
||||
local resolved_ip=""
|
||||
if command -v nslookup >/dev/null 2>&1; then
|
||||
resolved_ip=$(nslookup "$domain" 2>/dev/null | grep -A1 "Name:" | grep "Address" | head -1 | awk '{print $2}')
|
||||
[ -z "$resolved_ip" ] && resolved_ip=$(nslookup "$domain" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}' | grep -v "^$")
|
||||
elif command -v host >/dev/null 2>&1; then
|
||||
resolved_ip=$(host "$domain" 2>/dev/null | grep "has address" | head -1 | awk '{print $4}')
|
||||
fi
|
||||
echo "$resolved_ip"
|
||||
}
|
||||
|
||||
# Helper: Get public IPv4 address
|
||||
get_public_ipv4() {
|
||||
local ip=""
|
||||
ip=$(wget -qO- -T 5 "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n')
|
||||
[ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://api.ipify.org" 2>/dev/null | tr -d '\n')
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
# Helper: Check certificate expiry
|
||||
check_cert_expiry() {
|
||||
local domain="$1"
|
||||
local cert_file="/srv/haproxy/certs/${domain}.pem"
|
||||
|
||||
if [ ! -f "$cert_file" ]; then
|
||||
cert_file="/etc/acme/${domain}_ecc/${domain}.cer"
|
||||
[ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer"
|
||||
fi
|
||||
|
||||
if [ -f "$cert_file" ]; then
|
||||
local expiry_date
|
||||
expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$expiry_date" ]; then
|
||||
local expiry_epoch now_epoch days_left
|
||||
expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
|
||||
now_epoch=$(date +%s)
|
||||
if [ -n "$expiry_epoch" ]; then
|
||||
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
echo "$days_left"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get hosting status for all sites with DNS and cert health
|
||||
method_get_hosting_status() {
|
||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||
|
||||
# Get public IP once
|
||||
local public_ip
|
||||
public_ip=$(get_public_ipv4)
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "public_ip" "$public_ip"
|
||||
|
||||
# HAProxy status
|
||||
local haproxy_running="stopped"
|
||||
if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
haproxy_running="running"
|
||||
fi
|
||||
json_add_string "haproxy_status" "$haproxy_running"
|
||||
|
||||
json_add_array "sites"
|
||||
|
||||
config_load "$UCI_CONFIG"
|
||||
config_foreach _add_site_health site "$public_ip"
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
_add_site_health() {
|
||||
local section="$1"
|
||||
local public_ip="$2"
|
||||
local name domain ssl enabled has_content port runtime
|
||||
|
||||
config_get name "$section" name ""
|
||||
config_get domain "$section" domain ""
|
||||
config_get ssl "$section" ssl "1"
|
||||
config_get enabled "$section" enabled "1"
|
||||
config_get port "$section" port ""
|
||||
config_get runtime "$section" runtime ""
|
||||
|
||||
[ -z "$name" ] && return
|
||||
|
||||
# Check content
|
||||
has_content="0"
|
||||
if [ -d "$SITES_ROOT/$name" ] && [ -f "$SITES_ROOT/$name/index.html" ]; then
|
||||
has_content="1"
|
||||
fi
|
||||
|
||||
json_add_object
|
||||
json_add_string "id" "$section"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_boolean "has_content" "$has_content"
|
||||
[ -n "$port" ] && json_add_int "port" "$port"
|
||||
json_add_string "runtime" "$runtime"
|
||||
|
||||
# DNS check
|
||||
if [ -n "$domain" ]; then
|
||||
local resolved_ip
|
||||
resolved_ip=$(check_dns_resolution "$domain")
|
||||
if [ -n "$resolved_ip" ]; then
|
||||
json_add_string "dns_ip" "$resolved_ip"
|
||||
# Check if resolves to public IP
|
||||
case "$resolved_ip" in
|
||||
10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2*|172.30.*|172.31.*|192.168.*)
|
||||
json_add_string "dns_status" "private"
|
||||
;;
|
||||
*)
|
||||
if [ "$resolved_ip" = "$public_ip" ]; then
|
||||
json_add_string "dns_status" "ok"
|
||||
else
|
||||
json_add_string "dns_status" "mismatch"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
json_add_string "dns_status" "failed"
|
||||
fi
|
||||
|
||||
# Certificate check
|
||||
if [ "$ssl" = "1" ]; then
|
||||
local days_left
|
||||
days_left=$(check_cert_expiry "$domain")
|
||||
if [ -n "$days_left" ]; then
|
||||
if [ "$days_left" -lt 0 ]; then
|
||||
json_add_string "cert_status" "expired"
|
||||
elif [ "$days_left" -lt 7 ]; then
|
||||
json_add_string "cert_status" "critical"
|
||||
elif [ "$days_left" -lt 30 ]; then
|
||||
json_add_string "cert_status" "warning"
|
||||
else
|
||||
json_add_string "cert_status" "ok"
|
||||
fi
|
||||
json_add_int "cert_days" "$days_left"
|
||||
else
|
||||
json_add_string "cert_status" "missing"
|
||||
fi
|
||||
else
|
||||
json_add_string "cert_status" "none"
|
||||
fi
|
||||
else
|
||||
json_add_string "dns_status" "none"
|
||||
json_add_string "cert_status" "none"
|
||||
fi
|
||||
|
||||
# Publish status
|
||||
local publish_status="draft"
|
||||
if [ "$enabled" = "1" ] && [ "$has_content" = "1" ]; then
|
||||
publish_status="published"
|
||||
elif [ "$enabled" = "1" ]; then
|
||||
publish_status="pending"
|
||||
fi
|
||||
json_add_string "publish_status" "$publish_status"
|
||||
|
||||
# URL
|
||||
local protocol="http"
|
||||
[ "$ssl" = "1" ] && protocol="https"
|
||||
json_add_string "url" "${protocol}://${domain}"
|
||||
|
||||
json_close_object
|
||||
}
|
||||
|
||||
# Check health for single site
|
||||
method_check_site_health() {
|
||||
local id
|
||||
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var id id
|
||||
|
||||
if [ -z "$id" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Missing site id"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
local name domain ssl
|
||||
name=$(get_uci "$id" name "")
|
||||
domain=$(get_uci "$id" domain "")
|
||||
ssl=$(get_uci "$id" ssl "1")
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Site not found"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||
|
||||
# Get public IP
|
||||
local public_ip
|
||||
public_ip=$(get_public_ipv4)
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "id" "$id"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "public_ip" "$public_ip"
|
||||
|
||||
# DNS check
|
||||
json_add_object "dns"
|
||||
if [ -n "$domain" ]; then
|
||||
local resolved_ip
|
||||
resolved_ip=$(check_dns_resolution "$domain")
|
||||
if [ -n "$resolved_ip" ]; then
|
||||
json_add_string "resolved_ip" "$resolved_ip"
|
||||
case "$resolved_ip" in
|
||||
10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2*|172.30.*|172.31.*|192.168.*)
|
||||
json_add_string "status" "private"
|
||||
json_add_string "message" "DNS points to private IP"
|
||||
;;
|
||||
*)
|
||||
if [ "$resolved_ip" = "$public_ip" ]; then
|
||||
json_add_string "status" "ok"
|
||||
else
|
||||
json_add_string "status" "mismatch"
|
||||
json_add_string "expected" "$public_ip"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
json_add_string "status" "failed"
|
||||
json_add_string "message" "DNS resolution failed"
|
||||
fi
|
||||
else
|
||||
json_add_string "status" "none"
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
# Certificate check
|
||||
json_add_object "certificate"
|
||||
if [ -n "$domain" ] && [ "$ssl" = "1" ]; then
|
||||
local days_left
|
||||
days_left=$(check_cert_expiry "$domain")
|
||||
if [ -n "$days_left" ]; then
|
||||
json_add_int "days_left" "$days_left"
|
||||
if [ "$days_left" -lt 0 ]; then
|
||||
json_add_string "status" "expired"
|
||||
elif [ "$days_left" -lt 7 ]; then
|
||||
json_add_string "status" "critical"
|
||||
elif [ "$days_left" -lt 30 ]; then
|
||||
json_add_string "status" "warning"
|
||||
else
|
||||
json_add_string "status" "ok"
|
||||
fi
|
||||
else
|
||||
json_add_string "status" "missing"
|
||||
fi
|
||||
else
|
||||
json_add_string "status" "none"
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
# Content check
|
||||
json_add_object "content"
|
||||
if [ -d "$SITES_ROOT/$name" ]; then
|
||||
json_add_boolean "exists" 1
|
||||
local file_count
|
||||
file_count=$(find "$SITES_ROOT/$name" -type f 2>/dev/null | wc -l)
|
||||
json_add_int "file_count" "$file_count"
|
||||
if [ -f "$SITES_ROOT/$name/index.html" ]; then
|
||||
json_add_boolean "has_index" 1
|
||||
else
|
||||
json_add_boolean "has_index" 0
|
||||
fi
|
||||
else
|
||||
json_add_boolean "exists" 0
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
# HAProxy status
|
||||
json_add_object "haproxy"
|
||||
if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
json_add_string "status" "running"
|
||||
else
|
||||
json_add_string "status" "stopped"
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Save global settings
|
||||
method_save_settings() {
|
||||
local enabled runtime nginx_container sites_root gitea_url
|
||||
@ -798,25 +1094,29 @@ case "$1" in
|
||||
"upload_file": { "id": "string", "filename": "string", "content": "string" },
|
||||
"list_files": { "id": "string" },
|
||||
"get_settings": {},
|
||||
"save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" }
|
||||
"save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" },
|
||||
"get_hosting_status": {},
|
||||
"check_site_health": { "id": "string" }
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) method_status ;;
|
||||
list_sites) method_list_sites ;;
|
||||
get_site) method_get_site ;;
|
||||
create_site) method_create_site ;;
|
||||
update_site) method_update_site ;;
|
||||
delete_site) method_delete_site ;;
|
||||
sync_site) method_sync_site ;;
|
||||
get_publish_info) method_get_publish_info ;;
|
||||
upload_file) method_upload_file ;;
|
||||
list_files) method_list_files ;;
|
||||
get_settings) method_get_settings ;;
|
||||
save_settings) method_save_settings ;;
|
||||
*) echo '{"error": "unknown method"}' ;;
|
||||
status) method_status ;;
|
||||
list_sites) method_list_sites ;;
|
||||
get_site) method_get_site ;;
|
||||
create_site) method_create_site ;;
|
||||
update_site) method_update_site ;;
|
||||
delete_site) method_delete_site ;;
|
||||
sync_site) method_sync_site ;;
|
||||
get_publish_info) method_get_publish_info ;;
|
||||
upload_file) method_upload_file ;;
|
||||
list_files) method_list_files ;;
|
||||
get_settings) method_get_settings ;;
|
||||
save_settings) method_save_settings ;;
|
||||
get_hosting_status) method_get_hosting_status ;;
|
||||
check_site_health) method_check_site_health ;;
|
||||
*) echo '{"error": "unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
"list_sites",
|
||||
"get_site",
|
||||
"get_publish_info",
|
||||
"get_settings"
|
||||
"get_settings",
|
||||
"get_hosting_status",
|
||||
"check_site_health"
|
||||
],
|
||||
"file": ["read", "list", "stat"]
|
||||
},
|
||||
|
||||
377
package/secubox/luci-app-mitmproxy/README.md
Normal file
377
package/secubox/luci-app-mitmproxy/README.md
Normal file
@ -0,0 +1,377 @@
|
||||
# 🔐 mitmproxy - HTTPS Interception Proxy
|
||||
|
||||
Interactive HTTPS proxy for debugging, testing, and security analysis with transparent mode support and web-based traffic inspection.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔍 **Traffic Inspection** | View and analyze HTTP/HTTPS requests in real-time |
|
||||
| 🖥️ **Web UI** | Built-in mitmweb interface for visual traffic analysis |
|
||||
| 🎭 **Transparent Mode** | Intercept traffic automatically via nftables |
|
||||
| 📜 **CA Certificate** | Generate and manage SSL interception certificates |
|
||||
| 📊 **Statistics** | Track requests, unique hosts, and flow data |
|
||||
| 🔄 **Request Replay** | Replay captured requests for testing |
|
||||
| ⚙️ **Filtering** | Filter and track CDN, media, ads, and trackers |
|
||||
| 🛡️ **Whitelist** | Bypass interception for specific IPs/domains |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Proxy Modes
|
||||
|
||||
| Mode | Icon | Description | Use Case |
|
||||
|------|------|-------------|----------|
|
||||
| 🎯 **Regular** | Configure clients manually | Testing specific apps |
|
||||
| 🎭 **Transparent** | Auto-intercept via firewall | Network-wide inspection |
|
||||
| ⬆️ **Upstream** | Forward to another proxy | Proxy chaining |
|
||||
| ⬇️ **Reverse** | Reverse proxy mode | Backend analysis |
|
||||
|
||||
### Enable Transparent Mode
|
||||
|
||||
1. Go to **Security → mitmproxy → Settings**
|
||||
2. Set **Proxy Mode** to `Transparent`
|
||||
3. Enable **Transparent Firewall**
|
||||
4. Click **Save & Apply**
|
||||
|
||||
### Install CA Certificate
|
||||
|
||||
For HTTPS interception, install the mitmproxy CA on client devices:
|
||||
|
||||
1. Configure device to use proxy (or use transparent mode)
|
||||
2. Navigate to `http://mitm.it` from the device
|
||||
3. Download and install the certificate for your OS
|
||||
4. Trust the certificate in system settings
|
||||
|
||||
## 📊 Dashboard
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 🔐 mitmproxy 🟢 Running │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────┐ │
|
||||
│ │ 📊 12.5K │ │ 🌐 245 │ │ 💾 45 MB │ │ 🔌 8080│ │
|
||||
│ │ Requests │ │ Hosts │ │ Flow Data │ │ Port │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ └────────┘ │
|
||||
│ │
|
||||
│ 🌐 Top Hosts 🔒 CA Certificate │
|
||||
│ ┌──────────────────────────────┐ ┌─────────────────────────┐│
|
||||
│ │ 🔗 api.example.com 1,234 │ │ 📜 mitmproxy CA ││
|
||||
│ │ 🔗 cdn.cloudflare.com 890 │ │ ✅ Certificate installed ││
|
||||
│ │ 🔗 www.google.com 567 │ │ Expires: 2026-01-28 ││
|
||||
│ │ 🔗 analytics.google.com 432 │ │ [⬇ Download] ││
|
||||
│ └──────────────────────────────┘ └─────────────────────────┘│
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔍 Request Capture
|
||||
|
||||
### Live Request Viewer
|
||||
|
||||
The Requests tab shows captured HTTP traffic in real-time:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 🔍 Captured Requests ⏸ Pause │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [GET] api.example.com/users 200 application/json │
|
||||
│ [POST] auth.example.com/login 201 application/json │
|
||||
│ [GET] cdn.cloudflare.com/script.js 200 text/javascript │
|
||||
│ [GET] www.google.com/search 200 text/html │
|
||||
│ [PUT] api.example.com/user/123 204 - │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### View Request Details
|
||||
|
||||
Click on any request to see:
|
||||
- Full request headers
|
||||
- Response headers
|
||||
- Cookies
|
||||
- Request/response body (if captured)
|
||||
|
||||
## 🎭 Transparent Mode
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Client Device SecuBox Router
|
||||
┌──────────────┐ ┌────────────────────────┐
|
||||
│ │ │ │
|
||||
│ Browser │◀── HTTP/S ──▶│ nftables REDIRECT │
|
||||
│ │ │ │ │
|
||||
└──────────────┘ │ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ mitmproxy │ │
|
||||
│ │ (port 8080) │ │
|
||||
│ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Internet │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### Firewall Setup
|
||||
|
||||
When transparent mode is enabled, mitmproxy automatically creates nftables rules:
|
||||
|
||||
```bash
|
||||
# HTTP redirect (port 80 → 8080)
|
||||
nft add rule inet fw4 prerouting tcp dport 80 redirect to :8080
|
||||
|
||||
# HTTPS redirect (port 443 → 8080)
|
||||
nft add rule inet fw4 prerouting tcp dport 443 redirect to :8080
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### UCI Settings
|
||||
|
||||
```bash
|
||||
# /etc/config/mitmproxy
|
||||
|
||||
config mitmproxy 'main'
|
||||
option enabled '1'
|
||||
option mode 'transparent' # regular | transparent | upstream | reverse
|
||||
option proxy_port '8080'
|
||||
option web_host '0.0.0.0'
|
||||
option web_port '8081'
|
||||
option data_path '/srv/mitmproxy'
|
||||
option memory_limit '256M'
|
||||
option ssl_insecure '0' # Accept invalid upstream certs
|
||||
option anticache '0' # Strip cache headers
|
||||
option anticomp '0' # Disable compression
|
||||
option flow_detail '1' # Log detail level (0-4)
|
||||
|
||||
config transparent 'transparent'
|
||||
option enabled '1'
|
||||
option interface 'br-lan'
|
||||
option redirect_http '1'
|
||||
option redirect_https '1'
|
||||
option http_port '80'
|
||||
option https_port '443'
|
||||
|
||||
config whitelist 'whitelist'
|
||||
option enabled '1'
|
||||
list bypass_ip '192.168.255.0/24'
|
||||
list bypass_domain 'banking.com'
|
||||
|
||||
config filtering 'filtering'
|
||||
option enabled '0'
|
||||
option log_requests '1'
|
||||
option filter_cdn '0'
|
||||
option filter_media '0'
|
||||
option block_ads '0'
|
||||
|
||||
config capture 'capture'
|
||||
option save_flows '0'
|
||||
option capture_request_headers '1'
|
||||
option capture_response_headers '1'
|
||||
option capture_request_body '0'
|
||||
option capture_response_body '0'
|
||||
```
|
||||
|
||||
## 📡 RPCD API
|
||||
|
||||
### Service Control
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `get_status` | Get service status |
|
||||
| `service_start` | Start mitmproxy |
|
||||
| `service_stop` | Stop mitmproxy |
|
||||
| `service_restart` | Restart service |
|
||||
| `install` | Install mitmproxy container |
|
||||
|
||||
### Configuration
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `get_config` | Get main configuration |
|
||||
| `get_all_config` | Get all configuration sections |
|
||||
| `get_transparent_config` | Get transparent mode settings |
|
||||
| `get_whitelist_config` | Get whitelist settings |
|
||||
| `get_filtering_config` | Get filtering settings |
|
||||
| `set_config` | Set configuration value |
|
||||
|
||||
### Statistics & Data
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `get_stats` | Get traffic statistics |
|
||||
| `get_requests` | Get captured requests |
|
||||
| `get_top_hosts` | Get most requested hosts |
|
||||
| `get_ca_info` | Get CA certificate info |
|
||||
| `clear_data` | Clear captured data |
|
||||
|
||||
### Firewall
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `firewall_setup` | Setup transparent mode rules |
|
||||
| `firewall_clear` | Remove firewall rules |
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
# Get status
|
||||
ubus call luci.mitmproxy get_status
|
||||
|
||||
# Response:
|
||||
{
|
||||
"enabled": true,
|
||||
"running": true,
|
||||
"installed": true,
|
||||
"docker_available": true,
|
||||
"web_port": 8081,
|
||||
"proxy_port": 8080,
|
||||
"listen_port": 8080,
|
||||
"web_url": "http://192.168.255.1:8081"
|
||||
}
|
||||
|
||||
# Get statistics
|
||||
ubus call luci.mitmproxy get_stats
|
||||
|
||||
# Response:
|
||||
{
|
||||
"total_requests": 12500,
|
||||
"unique_hosts": 245,
|
||||
"flow_file_size": 47185920,
|
||||
"cdn_requests": 3200,
|
||||
"media_requests": 890,
|
||||
"blocked_ads": 156
|
||||
}
|
||||
|
||||
# Get top hosts
|
||||
ubus call luci.mitmproxy get_top_hosts '{"limit":10}'
|
||||
|
||||
# Response:
|
||||
{
|
||||
"hosts": [
|
||||
{ "host": "api.example.com", "count": 1234 },
|
||||
{ "host": "cdn.cloudflare.com", "count": 890 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 CA Certificate
|
||||
|
||||
### Generate New Certificate
|
||||
|
||||
```bash
|
||||
# Certificate is auto-generated on first start
|
||||
# Located at: /srv/mitmproxy/certs/mitmproxy-ca-cert.pem
|
||||
```
|
||||
|
||||
### Download Certificate
|
||||
|
||||
1. Access mitmweb UI at `http://192.168.255.1:8081`
|
||||
2. Or navigate to `http://mitm.it` from a proxied device
|
||||
3. Download certificate for your platform
|
||||
|
||||
### Certificate Locations
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/srv/mitmproxy/certs/mitmproxy-ca.pem` | CA private key + certificate |
|
||||
| `/srv/mitmproxy/certs/mitmproxy-ca-cert.pem` | CA certificate only |
|
||||
| `/srv/mitmproxy/certs/mitmproxy-ca-cert.cer` | Certificate (DER format) |
|
||||
|
||||
## 🛡️ Filtering & Analytics
|
||||
|
||||
### CDN Tracking
|
||||
|
||||
Track traffic to major CDN providers:
|
||||
- Cloudflare
|
||||
- Akamai
|
||||
- Fastly
|
||||
- AWS CloudFront
|
||||
- Google Cloud CDN
|
||||
|
||||
### Media Streaming Tracking
|
||||
|
||||
Track streaming services:
|
||||
- YouTube
|
||||
- Netflix
|
||||
- Spotify
|
||||
- Twitch
|
||||
- Amazon Prime Video
|
||||
|
||||
### Ad Blocking
|
||||
|
||||
Block known advertising and tracking domains with the built-in filter addon.
|
||||
|
||||
## 📁 File Locations
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/etc/config/mitmproxy` | UCI configuration |
|
||||
| `/srv/mitmproxy/` | Data directory |
|
||||
| `/srv/mitmproxy/certs/` | CA certificates |
|
||||
| `/srv/mitmproxy/flows/` | Captured flow files |
|
||||
| `/var/lib/lxc/mitmproxy/` | LXC container root |
|
||||
| `/usr/libexec/rpcd/luci.mitmproxy` | RPCD backend |
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Service Won't Start
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
lxc-info -n mitmproxy
|
||||
|
||||
# Check logs
|
||||
logread | grep mitmproxy
|
||||
|
||||
# Verify Docker is available
|
||||
docker ps
|
||||
```
|
||||
|
||||
### No Traffic Being Captured
|
||||
|
||||
1. **Regular mode**: Verify client proxy settings point to `192.168.255.1:8080`
|
||||
2. **Transparent mode**: Check firewall rules with `nft list ruleset | grep redirect`
|
||||
3. Verify mitmproxy is listening: `netstat -tln | grep 8080`
|
||||
|
||||
### HTTPS Interception Not Working
|
||||
|
||||
1. Install CA certificate on client device
|
||||
2. Trust the certificate in system settings
|
||||
3. Some apps use certificate pinning and cannot be intercepted
|
||||
|
||||
### Web UI Not Accessible
|
||||
|
||||
```bash
|
||||
# Check web port is listening
|
||||
netstat -tln | grep 8081
|
||||
|
||||
# Verify from router
|
||||
curl -I http://127.0.0.1:8081
|
||||
|
||||
# Check firewall allows access
|
||||
uci show firewall | grep mitmproxy
|
||||
```
|
||||
|
||||
### Memory Issues
|
||||
|
||||
```bash
|
||||
# Increase memory limit
|
||||
uci set mitmproxy.main.memory_limit='512M'
|
||||
uci commit mitmproxy
|
||||
/etc/init.d/mitmproxy restart
|
||||
```
|
||||
|
||||
## 🔒 Security Notes
|
||||
|
||||
1. **Sensitive Tool** - mitmproxy can intercept all network traffic including passwords. Use responsibly.
|
||||
2. **CA Certificate** - Protect the CA private key. Anyone with access can intercept traffic.
|
||||
3. **Whitelist Banking** - Add banking and financial sites to the bypass list.
|
||||
4. **Disable When Not Needed** - Turn off transparent mode when not actively debugging.
|
||||
5. **Audit Trail** - All captured requests may contain sensitive data.
|
||||
|
||||
## 📜 License
|
||||
|
||||
MIT License - Copyright (C) 2025 CyberMind.fr
|
||||
@ -9,88 +9,367 @@ var callStart = rpc.declare({ object: 'luci.mitmproxy', method: 'start', expect:
|
||||
var callStop = rpc.declare({ object: 'luci.mitmproxy', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.mitmproxy', method: 'restart', expect: {} });
|
||||
|
||||
var css = '.mp-container{max-width:900px;margin:0 auto}.mp-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#f97316 0%,#ea580c 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.mp-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.mp-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.mp-status.running{background:rgba(16,185,129,.2)}.mp-status.stopped{background:rgba(239,68,68,.2)}.mp-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.mp-status.running .mp-dot{background:#10b981}.mp-status.stopped .mp-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.mp-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.mp-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.mp-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem}.mp-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.mp-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.mp-info-value{font-size:1rem;font-weight:500}.mp-actions{display:flex;gap:.75rem;flex-wrap:wrap}.mp-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.mp-btn-primary{background:linear-gradient(135deg,#f97316,#ea580c);color:#fff}.mp-btn-success{background:#10b981;color:#fff}.mp-btn-danger{background:#ef4444;color:#fff}.mp-btn:disabled{opacity:.5;cursor:not-allowed}.mp-not-installed{text-align:center;padding:3rem}.mp-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin:1.5rem 0}.mp-feature{padding:.75rem;background:#fff7ed;border-radius:8px;font-size:.9rem}.mp-warning{background:#fef3c7;border:1px solid #f59e0b;border-radius:8px;padding:1rem;margin-top:1rem;font-size:.9rem;color:#92400e}';
|
||||
var css = [
|
||||
':root { --mp-primary: #e74c3c; --mp-primary-light: #ec7063; --mp-secondary: #3498db; --mp-success: #27ae60; --mp-warning: #f39c12; --mp-danger: #c0392b; --mp-bg: #0d0d12; --mp-card: #141419; --mp-border: rgba(255,255,255,0.08); --mp-text: #e0e0e8; --mp-muted: #8a8a9a; }',
|
||||
'.mp-overview { max-width: 1000px; margin: 0 auto; padding: 20px; font-family: system-ui, -apple-system, sans-serif; color: var(--mp-text); }',
|
||||
|
||||
/* Header */
|
||||
'.mp-header { display: flex; justify-content: space-between; align-items: center; padding: 24px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); border-radius: 16px; color: #fff; margin-bottom: 24px; }',
|
||||
'.mp-header-left { display: flex; align-items: center; gap: 16px; }',
|
||||
'.mp-logo { font-size: 48px; }',
|
||||
'.mp-title { font-size: 28px; font-weight: 700; margin: 0; }',
|
||||
'.mp-subtitle { font-size: 14px; opacity: 0.9; margin-top: 4px; }',
|
||||
'.mp-status { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; }',
|
||||
'.mp-status.running { background: rgba(39,174,96,0.3); }',
|
||||
'.mp-status.stopped { background: rgba(239,68,68,0.3); }',
|
||||
'.mp-dot { width: 10px; height: 10px; border-radius: 50%; animation: pulse 2s infinite; }',
|
||||
'.mp-status.running .mp-dot { background: #27ae60; }',
|
||||
'.mp-status.stopped .mp-dot { background: #ef4444; }',
|
||||
'@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }',
|
||||
|
||||
/* Welcome Banner - shown when not installed/running */
|
||||
'.mp-welcome { text-align: center; padding: 60px 40px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; margin-bottom: 24px; }',
|
||||
'.mp-welcome-icon { font-size: 80px; margin-bottom: 20px; }',
|
||||
'.mp-welcome h2 { font-size: 28px; margin: 0 0 12px 0; color: #fff; }',
|
||||
'.mp-welcome p { font-size: 16px; color: var(--mp-muted); margin: 0 0 30px 0; max-width: 600px; margin-left: auto; margin-right: auto; }',
|
||||
'.mp-welcome-note { background: rgba(231,76,60,0.1); border: 1px solid rgba(231,76,60,0.3); border-radius: 12px; padding: 16px; margin-top: 24px; font-size: 14px; color: #ec7063; }',
|
||||
|
||||
/* Mode Cards */
|
||||
'.mp-modes { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }',
|
||||
'.mp-mode-card { background: var(--mp-card); border: 2px solid var(--mp-border); border-radius: 16px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.3s; }',
|
||||
'.mp-mode-card:hover { border-color: var(--mp-primary); transform: translateY(-4px); box-shadow: 0 8px 32px rgba(231,76,60,0.2); }',
|
||||
'.mp-mode-card.recommended { border-color: var(--mp-primary); background: linear-gradient(180deg, rgba(231,76,60,0.1) 0%, transparent 100%); }',
|
||||
'.mp-mode-icon { font-size: 48px; margin-bottom: 16px; }',
|
||||
'.mp-mode-title { font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; }',
|
||||
'.mp-mode-desc { font-size: 13px; color: var(--mp-muted); line-height: 1.5; }',
|
||||
'.mp-mode-badge { display: inline-block; background: var(--mp-primary); color: #fff; font-size: 11px; padding: 4px 10px; border-radius: 12px; margin-top: 12px; text-transform: uppercase; font-weight: 600; }',
|
||||
|
||||
/* Feature Grid */
|
||||
'.mp-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }',
|
||||
'.mp-feature { display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 12px; }',
|
||||
'.mp-feature-icon { font-size: 24px; }',
|
||||
'.mp-feature-text { font-size: 14px; color: var(--mp-text); }',
|
||||
|
||||
/* Quick Actions */
|
||||
'.mp-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-bottom: 24px; }',
|
||||
'.mp-btn { display: inline-flex; align-items: center; gap: 8px; padding: 14px 28px; border-radius: 12px; border: none; cursor: pointer; font-size: 15px; font-weight: 600; transition: all 0.2s; text-decoration: none; }',
|
||||
'.mp-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }',
|
||||
'.mp-btn:disabled { opacity: 0.5; cursor: not-allowed; }',
|
||||
'.mp-btn-primary { background: linear-gradient(135deg, #e74c3c, #c0392b); color: #fff; }',
|
||||
'.mp-btn-success { background: linear-gradient(135deg, #27ae60, #1e8449); color: #fff; }',
|
||||
'.mp-btn-danger { background: linear-gradient(135deg, #e74c3c, #c0392b); color: #fff; }',
|
||||
'.mp-btn-secondary { background: rgba(255,255,255,0.1); color: var(--mp-text); border: 1px solid var(--mp-border); }',
|
||||
|
||||
/* Quick Start Card */
|
||||
'.mp-quickstart { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; padding: 24px; margin-bottom: 24px; }',
|
||||
'.mp-quickstart-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }',
|
||||
'.mp-quickstart-icon { font-size: 28px; }',
|
||||
'.mp-quickstart-title { font-size: 20px; font-weight: 600; color: #fff; }',
|
||||
'.mp-quickstart-steps { display: flex; flex-direction: column; gap: 16px; }',
|
||||
'.mp-step { display: flex; gap: 16px; align-items: flex-start; }',
|
||||
'.mp-step-num { width: 32px; height: 32px; background: var(--mp-primary); color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0; }',
|
||||
'.mp-step-content h4 { margin: 0 0 4px 0; font-size: 15px; color: #fff; }',
|
||||
'.mp-step-content p { margin: 0; font-size: 13px; color: var(--mp-muted); }',
|
||||
|
||||
/* Info Cards */
|
||||
'.mp-info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }',
|
||||
'.mp-info-card { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 12px; padding: 20px; }',
|
||||
'.mp-info-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }',
|
||||
'.mp-info-icon { font-size: 24px; }',
|
||||
'.mp-info-title { font-size: 16px; font-weight: 600; color: #fff; }',
|
||||
'.mp-info-value { font-size: 24px; font-weight: 700; color: var(--mp-primary); }',
|
||||
'.mp-info-label { font-size: 13px; color: var(--mp-muted); }',
|
||||
|
||||
/* How It Works */
|
||||
'.mp-howto { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; padding: 24px; }',
|
||||
'.mp-howto-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }',
|
||||
'.mp-howto-icon { font-size: 28px; }',
|
||||
'.mp-howto-title { font-size: 20px; font-weight: 600; color: #fff; }',
|
||||
'.mp-howto-diagram { background: rgba(0,0,0,0.3); border-radius: 12px; padding: 20px; font-family: monospace; font-size: 13px; line-height: 1.6; overflow-x: auto; }',
|
||||
'.mp-howto-diagram pre { margin: 0; color: var(--mp-text); }'
|
||||
].join('\n');
|
||||
|
||||
return view.extend({
|
||||
load: function() { return callStatus(); },
|
||||
|
||||
handleInstall: function() {
|
||||
ui.showModal(_('Installing mitmproxy'), [E('p', { 'class': 'spinning' }, _('Installing...'))]);
|
||||
ui.showModal(_('Installing mitmproxy'), [
|
||||
E('p', { 'class': 'spinning' }, _('Downloading and setting up mitmproxy container...')),
|
||||
E('p', { 'style': 'color: #888; font-size: 13px;' }, _('This may take a few minutes on first install.'))
|
||||
]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started. Please wait and refresh the page.')));
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting...'))]);
|
||||
ui.showModal(_('Starting mitmproxy'), [E('p', { 'class': 'spinning' }, _('Starting proxy service...'))]);
|
||||
callStart().then(function() { ui.hideModal(); location.reload(); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping...'))]);
|
||||
ui.showModal(_('Stopping mitmproxy'), [E('p', { 'class': 'spinning' }, _('Stopping proxy service...'))]);
|
||||
callStop().then(function() { ui.hideModal(); location.reload(); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
if (!document.getElementById('mp-styles')) {
|
||||
var s = document.createElement('style'); s.id = 'mp-styles'; s.textContent = css; document.head.appendChild(s);
|
||||
if (!document.getElementById('mp-overview-styles')) {
|
||||
var s = document.createElement('style');
|
||||
s.id = 'mp-overview-styles';
|
||||
s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
if (!status.installed || !status.docker_available) {
|
||||
return E('div', { 'class': 'mp-container' }, [
|
||||
var isInstalled = status.installed && status.docker_available;
|
||||
var isRunning = status.running;
|
||||
|
||||
// Not installed - show welcome wizard
|
||||
if (!isInstalled) {
|
||||
return E('div', { 'class': 'mp-overview' }, [
|
||||
// Header
|
||||
E('div', { 'class': 'mp-header' }, [
|
||||
E('h2', {}, ['\uD83D\uDD0D ', _('mitmproxy')]),
|
||||
E('div', { 'class': 'mp-status stopped' }, [E('span', { 'class': 'mp-dot' }), _('Not Installed')])
|
||||
E('div', { 'class': 'mp-header-left' }, [
|
||||
E('div', { 'class': 'mp-logo' }, '🔐'),
|
||||
E('div', {}, [
|
||||
E('h1', { 'class': 'mp-title' }, 'mitmproxy'),
|
||||
E('div', { 'class': 'mp-subtitle' }, _('HTTPS Interception Proxy'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-status stopped' }, [
|
||||
E('span', { 'class': 'mp-dot' }),
|
||||
_('Not Installed')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-card' }, [
|
||||
E('div', { 'class': 'mp-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\uD83D\uDD0D'),
|
||||
E('h3', {}, _('mitmproxy')),
|
||||
E('p', {}, _('Interactive HTTPS proxy for debugging, testing, and security analysis.')),
|
||||
E('div', { 'class': 'mp-features' }, [
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDCCA Web UI'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDD12 HTTPS'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDCDD Logging'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDD04 Replay'),
|
||||
E('div', { 'class': 'mp-feature' }, '\u2699 Scripting'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDCE6 Export')
|
||||
|
||||
// Welcome Banner
|
||||
E('div', { 'class': 'mp-welcome' }, [
|
||||
E('div', { 'class': 'mp-welcome-icon' }, '🔍'),
|
||||
E('h2', {}, _('Intercept & Analyze Network Traffic')),
|
||||
E('p', {}, _('mitmproxy is a powerful interactive HTTPS proxy that lets you inspect, modify, and replay HTTP/HTTPS traffic. Perfect for debugging APIs, testing applications, and security analysis.')),
|
||||
|
||||
// Features Grid
|
||||
E('div', { 'class': 'mp-features', 'style': 'margin-bottom: 30px;' }, [
|
||||
E('div', { 'class': 'mp-feature' }, [
|
||||
E('span', { 'class': 'mp-feature-icon' }, '📊'),
|
||||
E('span', { 'class': 'mp-feature-text' }, _('Real-time inspection'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-warning' }, _('Note: This is a security analysis tool. Only use for legitimate debugging and testing purposes.')),
|
||||
!status.docker_available ? E('div', { 'style': 'color:#ef4444;margin:1rem 0' }, _('Docker required')) : '',
|
||||
E('button', { 'class': 'mp-btn mp-btn-primary', 'style': 'margin-top:1rem', 'click': ui.createHandlerFn(this, 'handleInstall'), 'disabled': !status.docker_available }, _('Install mitmproxy'))
|
||||
E('div', { 'class': 'mp-feature' }, [
|
||||
E('span', { 'class': 'mp-feature-icon' }, '🔒'),
|
||||
E('span', { 'class': 'mp-feature-text' }, _('HTTPS decryption'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-feature' }, [
|
||||
E('span', { 'class': 'mp-feature-icon' }, '🖥️'),
|
||||
E('span', { 'class': 'mp-feature-text' }, _('Web-based UI'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-feature' }, [
|
||||
E('span', { 'class': 'mp-feature-icon' }, '🎭'),
|
||||
E('span', { 'class': 'mp-feature-text' }, _('Transparent mode'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-feature' }, [
|
||||
E('span', { 'class': 'mp-feature-icon' }, '🔄'),
|
||||
E('span', { 'class': 'mp-feature-text' }, _('Request replay'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-feature' }, [
|
||||
E('span', { 'class': 'mp-feature-icon' }, '📝'),
|
||||
E('span', { 'class': 'mp-feature-text' }, _('Flow logging'))
|
||||
])
|
||||
]),
|
||||
|
||||
!status.docker_available ?
|
||||
E('div', { 'style': 'color: #ef4444; margin-bottom: 20px;' }, [
|
||||
E('span', { 'style': 'font-size: 24px;' }, '⚠️ '),
|
||||
_('Docker is required but not available')
|
||||
]) : null,
|
||||
|
||||
E('button', {
|
||||
'class': 'mp-btn mp-btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'handleInstall'),
|
||||
'disabled': !status.docker_available
|
||||
}, ['📦 ', _('Install mitmproxy')]),
|
||||
|
||||
E('div', { 'class': 'mp-welcome-note' }, [
|
||||
'⚠️ ',
|
||||
_('Security Note: mitmproxy is a powerful security analysis tool. Only use for legitimate debugging, testing, and security research purposes.')
|
||||
])
|
||||
]),
|
||||
|
||||
// Proxy Modes
|
||||
E('h3', { 'style': 'margin: 0 0 16px 0; font-size: 18px; color: #fff;' }, '🎯 ' + _('Proxy Modes')),
|
||||
E('div', { 'class': 'mp-modes' }, [
|
||||
E('div', { 'class': 'mp-mode-card' }, [
|
||||
E('div', { 'class': 'mp-mode-icon' }, '🎯'),
|
||||
E('div', { 'class': 'mp-mode-title' }, _('Regular Proxy')),
|
||||
E('div', { 'class': 'mp-mode-desc' }, _('Configure clients to use the proxy manually. Best for testing specific applications.'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-mode-card recommended' }, [
|
||||
E('div', { 'class': 'mp-mode-icon' }, '🎭'),
|
||||
E('div', { 'class': 'mp-mode-title' }, _('Transparent Mode')),
|
||||
E('div', { 'class': 'mp-mode-desc' }, _('Intercept all network traffic automatically via firewall rules.')),
|
||||
E('span', { 'class': 'mp-mode-badge' }, _('Recommended'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-mode-card' }, [
|
||||
E('div', { 'class': 'mp-mode-icon' }, '⬆️'),
|
||||
E('div', { 'class': 'mp-mode-title' }, _('Upstream Proxy')),
|
||||
E('div', { 'class': 'mp-mode-desc' }, _('Forward traffic to another proxy server for proxy chaining.'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-mode-card' }, [
|
||||
E('div', { 'class': 'mp-mode-icon' }, '⬇️'),
|
||||
E('div', { 'class': 'mp-mode-title' }, _('Reverse Proxy')),
|
||||
E('div', { 'class': 'mp-mode-desc' }, _('Act as a reverse proxy to inspect backend server traffic.'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'mp-container' }, [
|
||||
// Installed - show dashboard overview
|
||||
return E('div', { 'class': 'mp-overview' }, [
|
||||
// Header
|
||||
E('div', { 'class': 'mp-header' }, [
|
||||
E('h2', {}, ['\uD83D\uDD0D ', _('mitmproxy')]),
|
||||
E('div', { 'class': 'mp-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('div', { 'class': 'mp-header-left' }, [
|
||||
E('div', { 'class': 'mp-logo' }, '🔐'),
|
||||
E('div', {}, [
|
||||
E('h1', { 'class': 'mp-title' }, 'mitmproxy'),
|
||||
E('div', { 'class': 'mp-subtitle' }, _('HTTPS Interception Proxy'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-status ' + (isRunning ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'mp-dot' }),
|
||||
status.running ? _('Running') : _('Stopped')
|
||||
isRunning ? _('Running') : _('Stopped')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-card' }, [
|
||||
E('div', { 'class': 'mp-card-title' }, ['\u2139\uFE0F ', _('Configuration')]),
|
||||
E('div', { 'class': 'mp-info-grid' }, [
|
||||
E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Proxy Port')), E('div', { 'class': 'mp-info-value' }, String(status.proxy_port))]),
|
||||
E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Web UI Port')), E('div', { 'class': 'mp-info-value' }, String(status.web_port))]),
|
||||
E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Web UI')), E('div', { 'class': 'mp-info-value' }, [E('a', { 'href': 'http://' + window.location.hostname + ':' + status.web_port, 'target': '_blank' }, _('Open UI'))])])
|
||||
|
||||
// Quick Actions
|
||||
E('div', { 'class': 'mp-actions' }, [
|
||||
isRunning ? E('button', {
|
||||
'class': 'mp-btn mp-btn-danger',
|
||||
'click': ui.createHandlerFn(this, 'handleStop')
|
||||
}, ['⏹ ', _('Stop Proxy')]) :
|
||||
E('button', {
|
||||
'class': 'mp-btn mp-btn-success',
|
||||
'click': ui.createHandlerFn(this, 'handleStart')
|
||||
}, ['▶️ ', _('Start Proxy')]),
|
||||
|
||||
E('a', {
|
||||
'class': 'mp-btn mp-btn-primary',
|
||||
'href': L.url('admin', 'secubox', 'security', 'mitmproxy', 'dashboard')
|
||||
}, ['📊 ', _('Dashboard')]),
|
||||
|
||||
E('a', {
|
||||
'class': 'mp-btn mp-btn-secondary',
|
||||
'href': 'http://' + window.location.hostname + ':' + (status.web_port || 8081),
|
||||
'target': '_blank'
|
||||
}, ['🖥️ ', _('Web UI')]),
|
||||
|
||||
E('a', {
|
||||
'class': 'mp-btn mp-btn-secondary',
|
||||
'href': L.url('admin', 'secubox', 'security', 'mitmproxy', 'settings')
|
||||
}, ['⚙️ ', _('Settings')])
|
||||
]),
|
||||
|
||||
// Info Cards
|
||||
E('div', { 'class': 'mp-info-grid' }, [
|
||||
E('div', { 'class': 'mp-info-card' }, [
|
||||
E('div', { 'class': 'mp-info-header' }, [
|
||||
E('span', { 'class': 'mp-info-icon' }, '🔌'),
|
||||
E('span', { 'class': 'mp-info-title' }, _('Proxy Port'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-info-value' }, String(status.proxy_port || 8080)),
|
||||
E('div', { 'class': 'mp-info-label' }, _('HTTP/HTTPS interception'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-info-card' }, [
|
||||
E('div', { 'class': 'mp-info-header' }, [
|
||||
E('span', { 'class': 'mp-info-icon' }, '🖥️'),
|
||||
E('span', { 'class': 'mp-info-title' }, _('Web UI Port'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-info-value' }, String(status.web_port || 8081)),
|
||||
E('div', { 'class': 'mp-info-label' }, _('mitmweb interface'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-info-card' }, [
|
||||
E('div', { 'class': 'mp-info-header' }, [
|
||||
E('span', { 'class': 'mp-info-icon' }, '💾'),
|
||||
E('span', { 'class': 'mp-info-title' }, _('Data Path'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-info-value', 'style': 'font-size: 14px; word-break: break-all;' }, status.data_path || '/srv/mitmproxy'),
|
||||
E('div', { 'class': 'mp-info-label' }, _('Certificates & flows'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-card' }, [
|
||||
E('div', { 'class': 'mp-card-title' }, ['\u26A1 ', _('Actions')]),
|
||||
E('div', { 'class': 'mp-actions' }, [
|
||||
E('button', { 'class': 'mp-btn mp-btn-success', 'click': ui.createHandlerFn(this, 'handleStart'), 'disabled': status.running }, _('Start')),
|
||||
E('button', { 'class': 'mp-btn mp-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop'), 'disabled': !status.running }, _('Stop'))
|
||||
|
||||
// Quick Start Guide
|
||||
E('div', { 'class': 'mp-quickstart' }, [
|
||||
E('div', { 'class': 'mp-quickstart-header' }, [
|
||||
E('span', { 'class': 'mp-quickstart-icon' }, '🚀'),
|
||||
E('span', { 'class': 'mp-quickstart-title' }, _('Quick Start Guide'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-quickstart-steps' }, [
|
||||
E('div', { 'class': 'mp-step' }, [
|
||||
E('div', { 'class': 'mp-step-num' }, '1'),
|
||||
E('div', { 'class': 'mp-step-content' }, [
|
||||
E('h4', {}, _('Start the Proxy')),
|
||||
E('p', {}, _('Click the Start button above to begin intercepting traffic.'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-step' }, [
|
||||
E('div', { 'class': 'mp-step-num' }, '2'),
|
||||
E('div', { 'class': 'mp-step-content' }, [
|
||||
E('h4', {}, _('Install CA Certificate')),
|
||||
E('p', {}, [
|
||||
_('Navigate to '),
|
||||
E('code', { 'style': 'background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;' }, 'http://mitm.it'),
|
||||
_(' from a proxied device to download and install the CA certificate.')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-step' }, [
|
||||
E('div', { 'class': 'mp-step-num' }, '3'),
|
||||
E('div', { 'class': 'mp-step-content' }, [
|
||||
E('h4', {}, _('Configure Clients')),
|
||||
E('p', {}, [
|
||||
_('Set proxy to '),
|
||||
E('code', { 'style': 'background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;' }, window.location.hostname + ':' + (status.proxy_port || 8080)),
|
||||
_(' or enable transparent mode in Settings.')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-step' }, [
|
||||
E('div', { 'class': 'mp-step-num' }, '4'),
|
||||
E('div', { 'class': 'mp-step-content' }, [
|
||||
E('h4', {}, _('View Traffic')),
|
||||
E('p', {}, _('Open the Dashboard or Web UI to see captured requests in real-time.'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// How It Works Diagram
|
||||
E('div', { 'class': 'mp-howto' }, [
|
||||
E('div', { 'class': 'mp-howto-header' }, [
|
||||
E('span', { 'class': 'mp-howto-icon' }, '📖'),
|
||||
E('span', { 'class': 'mp-howto-title' }, _('How mitmproxy Works'))
|
||||
]),
|
||||
E('div', { 'class': 'mp-howto-diagram' }, [
|
||||
E('pre', {}, [
|
||||
' Client Device SecuBox Router Internet\n',
|
||||
' ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐\n',
|
||||
' │ Browser │─────▶│ mitmproxy │─────▶│ Server │\n',
|
||||
' │ │◀─────│ │◀─────│ │\n',
|
||||
' └─────────────┘ │ 🔍 Inspect │ └─────────────┘\n',
|
||||
' │ ✏️ Modify │\n',
|
||||
' │ 📊 Log │\n',
|
||||
' │ 🔄 Replay │\n',
|
||||
' └──────────────────┘\n',
|
||||
'\n',
|
||||
' Port ' + (status.proxy_port || 8080) + ': HTTP/HTTPS interception\n',
|
||||
' Port ' + (status.web_port || 8081) + ': Web UI (mitmweb)'
|
||||
].join(''))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null, handleSave: null, handleReset: null
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
|
||||
@ -976,26 +976,62 @@ get_reverse_dns() {
|
||||
# Helper: Check certificate expiry
|
||||
check_cert_expiry() {
|
||||
local domain="$1"
|
||||
local cert_file="/srv/haproxy/certs/${domain}.pem"
|
||||
local cert_file=""
|
||||
|
||||
if [ ! -f "$cert_file" ]; then
|
||||
# Try the ACME path
|
||||
# Try multiple possible certificate locations
|
||||
# 1. HAProxy certs directory (combined pem files)
|
||||
if [ -f "/srv/haproxy/certs/${domain}.pem" ]; then
|
||||
cert_file="/srv/haproxy/certs/${domain}.pem"
|
||||
# 2. ACME standard path
|
||||
elif [ -f "/etc/acme/${domain}/${domain}.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}/${domain}.cer"
|
||||
# 3. ACME fullchain (some setups use this)
|
||||
elif [ -f "/etc/acme/${domain}/fullchain.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}/fullchain.cer"
|
||||
# 4. ACME with _ecc suffix (ECC certs)
|
||||
elif [ -f "/etc/acme/${domain}_ecc/${domain}.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}_ecc/${domain}.cer"
|
||||
[ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer"
|
||||
# 5. Let's Encrypt standard path
|
||||
elif [ -f "/etc/letsencrypt/live/${domain}/cert.pem" ]; then
|
||||
cert_file="/etc/letsencrypt/live/${domain}/cert.pem"
|
||||
fi
|
||||
|
||||
if [ -f "$cert_file" ]; then
|
||||
if [ -n "$cert_file" ] && [ -f "$cert_file" ]; then
|
||||
# Get expiry date using openssl
|
||||
local expiry_date
|
||||
expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$expiry_date" ]; then
|
||||
# Convert to epoch
|
||||
local expiry_epoch
|
||||
# Convert to epoch - try multiple date formats for compatibility
|
||||
local expiry_epoch now_epoch days_left
|
||||
|
||||
# BusyBox date may not support -d with GMT format
|
||||
# Try direct parsing first
|
||||
expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
|
||||
local now_epoch
|
||||
|
||||
# If that fails, try converting the format
|
||||
if [ -z "$expiry_epoch" ]; then
|
||||
# Parse "Apr 27 04:05:21 2026 GMT" format manually
|
||||
local month day time year
|
||||
month=$(echo "$expiry_date" | awk '{print $1}')
|
||||
day=$(echo "$expiry_date" | awk '{print $2}')
|
||||
time=$(echo "$expiry_date" | awk '{print $3}')
|
||||
year=$(echo "$expiry_date" | awk '{print $4}')
|
||||
|
||||
# Convert month name to number
|
||||
case "$month" in
|
||||
Jan) month="01" ;; Feb) month="02" ;; Mar) month="03" ;;
|
||||
Apr) month="04" ;; May) month="05" ;; Jun) month="06" ;;
|
||||
Jul) month="07" ;; Aug) month="08" ;; Sep) month="09" ;;
|
||||
Oct) month="10" ;; Nov) month="11" ;; Dec) month="12" ;;
|
||||
esac
|
||||
|
||||
# Try with reformatted date
|
||||
expiry_epoch=$(date -d "${year}-${month}-${day}" +%s 2>/dev/null)
|
||||
fi
|
||||
|
||||
now_epoch=$(date +%s)
|
||||
local days_left
|
||||
if [ -n "$expiry_epoch" ]; then
|
||||
|
||||
if [ -n "$expiry_epoch" ] && [ -n "$now_epoch" ]; then
|
||||
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
echo "$days_left"
|
||||
return 0
|
||||
|
||||
281
package/secubox/luci-app-tor-shield/README.md
Normal file
281
package/secubox/luci-app-tor-shield/README.md
Normal file
@ -0,0 +1,281 @@
|
||||
# 🧅 Tor Shield - Anonymous Routing Made Simple
|
||||
|
||||
Network-wide privacy protection through the Tor network with one-click activation.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🛡️ Protection Modes
|
||||
|
||||
| Mode | Description | Use Case |
|
||||
|------|-------------|----------|
|
||||
| 🌐 **Transparent Proxy** | All network traffic routed through Tor automatically | Full network anonymity |
|
||||
| 🎯 **SOCKS Proxy** | Apps connect via SOCKS5 (127.0.0.1:9050) | Selective app protection |
|
||||
| 🔓 **Bridge Mode** | Uses obfs4/meek bridges to bypass censorship | Restrictive networks |
|
||||
|
||||
### 🚀 Quick Start Presets
|
||||
|
||||
| Preset | Icon | Configuration |
|
||||
|--------|------|---------------|
|
||||
| **Full Anonymity** | 🛡️ | Transparent + DNS over Tor + Kill Switch |
|
||||
| **Selective Apps** | 🎯 | SOCKS only, no kill switch |
|
||||
| **Bypass Censorship** | 🔓 | Bridges enabled + obfs4 |
|
||||
|
||||
### 🔒 Security Features
|
||||
|
||||
- **🔐 Kill Switch** - Blocks all traffic if Tor disconnects
|
||||
- **🌍 DNS over Tor** - Prevents DNS leaks
|
||||
- **🔄 New Identity** - Request fresh circuits instantly
|
||||
- **🔍 Leak Test** - Verify your protection is working
|
||||
- **🧅 Hidden Services** - Host .onion sites
|
||||
|
||||
## 📊 Dashboard
|
||||
|
||||
The dashboard provides real-time monitoring:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 🧅 Tor Shield 🟢 Protected │
|
||||
├──────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────┐ Your Protection Status │
|
||||
│ │ 🧅 │ ───────────────────────── │
|
||||
│ │ Toggle │ Real IP: 192.168.x.x │
|
||||
│ │ │ Tor Exit: 185.220.x.x 🇩🇪 │
|
||||
│ └────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ 🛡️ Full │ 🎯 Selective │ 🔓 Censored │ │
|
||||
│ │ Anonymity │ Apps │ Bypass │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 🔄 Circuits: 5 │ 📊 45 KB/s │ ⏱ 2h 15m │
|
||||
│ 📥 125 MB │ 📤 45 MB │ │
|
||||
│ │
|
||||
│ ┌─────────┬─────────┬─────────┬─────────┐ │
|
||||
│ │🟢Service│🟢Boot │🟢DNS │🟢Kill │ │
|
||||
│ │ Running │ 100% │Protected│ Active │ │
|
||||
│ └─────────┴─────────┴─────────┴─────────┘ │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🧅 Hidden Services
|
||||
|
||||
Host your services on the Tor network with .onion addresses:
|
||||
|
||||
```bash
|
||||
# Via LuCI
|
||||
Services → Tor Shield → Hidden Services → Add
|
||||
|
||||
# Via CLI
|
||||
ubus call luci.tor-shield add_hidden_service '{"name":"mysite","local_port":80,"virtual_port":80}'
|
||||
|
||||
# Get onion address
|
||||
cat /var/lib/tor/hidden_service_mysite/hostname
|
||||
```
|
||||
|
||||
### Example Hidden Services
|
||||
|
||||
| Service | Local Port | Onion Port | Use Case |
|
||||
|---------|-----------|------------|----------|
|
||||
| Web Server | 80 | 80 | Anonymous website |
|
||||
| SSH | 22 | 22 | Secure remote access |
|
||||
| API | 8080 | 80 | Anonymous API endpoint |
|
||||
|
||||
## 🌉 Bridges
|
||||
|
||||
Bypass network censorship using Tor bridges:
|
||||
|
||||
### Bridge Types
|
||||
|
||||
| Type | Description | When to Use |
|
||||
|------|-------------|-------------|
|
||||
| **obfs4** | Obfuscated protocol | Most censored networks |
|
||||
| **meek-azure** | Domain fronting via Azure | Highly restrictive networks |
|
||||
| **snowflake** | WebRTC-based | Dynamic bridge discovery |
|
||||
|
||||
### Auto-Bridge Detection
|
||||
|
||||
```bash
|
||||
# Enable automatic bridge selection
|
||||
uci set tor-shield.main.auto_bridges=1
|
||||
uci commit tor-shield
|
||||
/etc/init.d/tor-shield restart
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### UCI Settings
|
||||
|
||||
```bash
|
||||
# /etc/config/tor-shield
|
||||
|
||||
config tor-shield 'main'
|
||||
option enabled '1'
|
||||
option mode 'transparent' # transparent | socks
|
||||
option dns_over_tor '1' # Route DNS through Tor
|
||||
option kill_switch '1' # Block traffic if Tor fails
|
||||
option auto_bridges '0' # Auto-detect censorship
|
||||
|
||||
config socks 'socks'
|
||||
option port '9050'
|
||||
option address '127.0.0.1'
|
||||
|
||||
config trans 'trans'
|
||||
option port '9040'
|
||||
option dns_port '9053'
|
||||
list excluded_ips '192.168.255.0/24' # LAN bypass
|
||||
|
||||
config bridges 'bridges'
|
||||
option enabled '0'
|
||||
option type 'obfs4'
|
||||
|
||||
config security 'security'
|
||||
option exit_nodes '' # Country codes: {us},{de}
|
||||
option exclude_exit_nodes '' # Avoid: {ru},{cn}
|
||||
option strict_nodes '0'
|
||||
|
||||
config hidden_service 'hs_mysite'
|
||||
option enabled '1'
|
||||
option name 'mysite'
|
||||
option local_port '80'
|
||||
option virtual_port '80'
|
||||
```
|
||||
|
||||
## 📡 RPCD API
|
||||
|
||||
### Status & Control
|
||||
|
||||
```bash
|
||||
# Get status
|
||||
ubus call luci.tor-shield status
|
||||
|
||||
# Enable with preset
|
||||
ubus call luci.tor-shield enable '{"preset":"anonymous"}'
|
||||
|
||||
# Disable
|
||||
ubus call luci.tor-shield disable
|
||||
|
||||
# Restart
|
||||
ubus call luci.tor-shield restart
|
||||
|
||||
# Request new identity
|
||||
ubus call luci.tor-shield new_identity
|
||||
|
||||
# Check for leaks
|
||||
ubus call luci.tor-shield check_leaks
|
||||
```
|
||||
|
||||
### Circuit Management
|
||||
|
||||
```bash
|
||||
# Get active circuits
|
||||
ubus call luci.tor-shield circuits
|
||||
|
||||
# Response:
|
||||
{
|
||||
"circuits": [{
|
||||
"id": "123",
|
||||
"status": "BUILT",
|
||||
"path": "$A~Guard,$B~Middle,$C~Exit",
|
||||
"purpose": "GENERAL",
|
||||
"nodes": [
|
||||
{"fingerprint": "ABC123", "name": "Guard"},
|
||||
{"fingerprint": "DEF456", "name": "Middle"},
|
||||
{"fingerprint": "GHI789", "name": "Exit"}
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Hidden Services
|
||||
|
||||
```bash
|
||||
# List hidden services
|
||||
ubus call luci.tor-shield hidden_services
|
||||
|
||||
# Add hidden service
|
||||
ubus call luci.tor-shield add_hidden_service '{"name":"web","local_port":80,"virtual_port":80}'
|
||||
|
||||
# Remove hidden service
|
||||
ubus call luci.tor-shield remove_hidden_service '{"name":"web"}'
|
||||
```
|
||||
|
||||
### Bandwidth Stats
|
||||
|
||||
```bash
|
||||
# Get bandwidth
|
||||
ubus call luci.tor-shield bandwidth
|
||||
|
||||
# Response:
|
||||
{
|
||||
"read": 125000000, # Total bytes downloaded
|
||||
"written": 45000000, # Total bytes uploaded
|
||||
"read_rate": 45000, # Current download rate (bytes/sec)
|
||||
"write_rate": 12000 # Current upload rate (bytes/sec)
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Tor Won't Start
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
logread | grep -i tor
|
||||
|
||||
# Verify config
|
||||
tor --verify-config -f /var/run/tor/torrc
|
||||
|
||||
# Check control socket
|
||||
ls -la /var/run/tor/control
|
||||
```
|
||||
|
||||
### Slow Connections
|
||||
|
||||
1. **Check bootstrap** - Wait for 100% completion
|
||||
2. **Try bridges** - Network may be throttling Tor
|
||||
3. **Change circuits** - Click "New Identity"
|
||||
4. **Check exit nodes** - Some exits are slow
|
||||
|
||||
### DNS Leaks
|
||||
|
||||
```bash
|
||||
# Verify DNS is routed through Tor
|
||||
nslookup check.torproject.org
|
||||
|
||||
# Should resolve via Tor DNS (127.0.0.1:9053)
|
||||
```
|
||||
|
||||
### Kill Switch Issues
|
||||
|
||||
```bash
|
||||
# Check firewall rules
|
||||
iptables -L -n | grep -i tor
|
||||
|
||||
# Verify kill switch config
|
||||
uci get tor-shield.main.kill_switch
|
||||
```
|
||||
|
||||
## 📁 File Locations
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `/etc/config/tor-shield` | UCI configuration |
|
||||
| `/var/run/tor/torrc` | Generated Tor config |
|
||||
| `/var/run/tor/control` | Control socket |
|
||||
| `/var/lib/tor/` | Tor data directory |
|
||||
| `/var/lib/tor/hidden_service_*/` | Hidden service keys |
|
||||
| `/tmp/tor_exit_ip` | Cached exit IP |
|
||||
| `/tmp/tor_real_ip` | Cached real IP |
|
||||
|
||||
## 🔐 Security Notes
|
||||
|
||||
1. **Kill Switch** - Always enable for maximum protection
|
||||
2. **DNS Leaks** - Enable DNS over Tor to prevent leaks
|
||||
3. **Hidden Services** - Keys in `/var/lib/tor/` are sensitive - back them up securely
|
||||
4. **Exit Nodes** - Consider excluding certain countries for sensitive use
|
||||
5. **Bridges** - Use if your ISP blocks or throttles Tor
|
||||
|
||||
## 📜 License
|
||||
|
||||
MIT License - Copyright (C) 2025 CyberMind.fr
|
||||
@ -250,20 +250,49 @@ return view.extend({
|
||||
|
||||
var statusClass = 'disabled';
|
||||
var statusText = _('Disabled');
|
||||
var statusEmoji = '\u{26AA}';
|
||||
if (isConnecting) {
|
||||
statusClass = 'connecting';
|
||||
statusText = _('Connecting %d%%').format(status.bootstrap);
|
||||
statusEmoji = '\u{1F7E1}';
|
||||
} else if (isProtected) {
|
||||
statusClass = 'protected';
|
||||
statusText = _('Protected');
|
||||
statusEmoji = '\u{1F7E2}';
|
||||
} else if (isActive) {
|
||||
statusClass = 'exposed';
|
||||
statusText = _('Exposed');
|
||||
statusEmoji = '\u{1F534}';
|
||||
}
|
||||
|
||||
var view = E('div', { 'class': 'tor-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }),
|
||||
|
||||
// Wizard Welcome Banner (shown when disabled)
|
||||
!isActive ? E('div', { 'class': 'tor-wizard-banner', 'style': 'background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 50%, #06b6d4 100%); border-radius: 16px; padding: 24px; margin-bottom: 20px; color: #fff; text-align: center;' }, [
|
||||
E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '\u{1F9D9}\u200D\u2642\uFE0F'),
|
||||
E('h2', { 'style': 'margin: 0 0 8px 0; font-size: 24px;' }, '\u{2728} ' + _('Welcome to Tor Shield') + ' \u{2728}'),
|
||||
E('p', { 'style': 'margin: 0 0 16px 0; opacity: 0.9; font-size: 14px;' }, _('Your gateway to anonymous browsing. Choose a protection level below to get started.')),
|
||||
E('div', { 'style': 'display: flex; justify-content: center; gap: 24px; flex-wrap: wrap; margin-top: 16px;' }, [
|
||||
E('div', { 'style': 'text-align: center;' }, [
|
||||
E('div', { 'style': 'font-size: 32px;' }, '\u{1F512}'),
|
||||
E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Encrypted'))
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center;' }, [
|
||||
E('div', { 'style': 'font-size: 32px;' }, '\u{1F310}'),
|
||||
E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Anonymous'))
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center;' }, [
|
||||
E('div', { 'style': 'font-size: 32px;' }, '\u{1F6E1}'),
|
||||
E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Protected'))
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center;' }, [
|
||||
E('div', { 'style': 'font-size: 32px;' }, '\u{1F30D}'),
|
||||
E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Worldwide'))
|
||||
])
|
||||
])
|
||||
]) : '',
|
||||
|
||||
// Header
|
||||
E('div', { 'class': 'tor-header' }, [
|
||||
E('div', { 'class': 'tor-logo' }, [
|
||||
@ -271,7 +300,7 @@ return view.extend({
|
||||
E('div', { 'class': 'tor-logo-text' }, ['Tor ', E('span', {}, 'Shield')])
|
||||
]),
|
||||
E('div', { 'class': 'tor-status-badge ' + statusClass }, [
|
||||
E('span', { 'class': 'tor-status-dot' }),
|
||||
E('span', { 'style': 'margin-right: 6px;' }, statusEmoji),
|
||||
statusText
|
||||
])
|
||||
]),
|
||||
@ -350,20 +379,40 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
|
||||
// Presets
|
||||
E('div', { 'class': 'tor-presets' },
|
||||
presets.map(function(preset) {
|
||||
return E('div', {
|
||||
'class': 'tor-preset' + (self.currentPreset === preset.id ? ' active' : ''),
|
||||
'data-preset': preset.id,
|
||||
'click': L.bind(function() { this.handlePresetSelect(preset.id); }, self)
|
||||
}, [
|
||||
E('div', { 'class': 'tor-preset-icon' }, api.getPresetIcon(preset.icon)),
|
||||
E('div', { 'class': 'tor-preset-name' }, preset.name),
|
||||
E('div', { 'class': 'tor-preset-desc' }, preset.description)
|
||||
]);
|
||||
})
|
||||
),
|
||||
// Presets - Wizard Style
|
||||
E('div', { 'class': 'tor-presets-wizard', 'style': 'margin-bottom: 24px;' }, [
|
||||
E('div', { 'style': 'text-align: center; margin-bottom: 16px;' }, [
|
||||
E('span', { 'style': 'font-size: 20px;' }, '\u{1F9D9}\u200D\u2642\uFE0F'),
|
||||
E('span', { 'style': 'font-weight: 600; margin-left: 8px;' }, _('Choose Your Protection Level'))
|
||||
]),
|
||||
E('div', { 'class': 'tor-presets', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px;' },
|
||||
[
|
||||
{ id: 'anonymous', name: _('Full Anonymity'), icon: '\u{1F6E1}', emoji: '\u{1F9D9}', desc: _('All traffic through Tor'), features: ['\u{2705} ' + _('Kill Switch'), '\u{2705} ' + _('DNS Protection'), '\u{2705} ' + _('Full Routing')] },
|
||||
{ id: 'selective', name: _('Selective Apps'), icon: '\u{1F3AF}', emoji: '\u{1F50D}', desc: _('SOCKS proxy mode'), features: ['\u{26AA} ' + _('No Kill Switch'), '\u{26AA} ' + _('Manual Config'), '\u{2705} ' + _('App Control')] },
|
||||
{ id: 'censored', name: _('Bypass Censorship'), icon: '\u{1F513}', emoji: '\u{1F30D}', desc: _('Bridge connections'), features: ['\u{2705} ' + _('obfs4 Bridges'), '\u{2705} ' + _('Anti-Censorship'), '\u{2705} ' + _('Stealth Mode')] }
|
||||
].map(function(preset) {
|
||||
var isSelected = self.currentPreset === preset.id;
|
||||
return E('div', {
|
||||
'class': 'tor-preset' + (isSelected ? ' active' : ''),
|
||||
'data-preset': preset.id,
|
||||
'style': 'background: var(--tor-bg-card, #1a1a24); border-radius: 12px; padding: 16px; cursor: pointer; border: 2px solid ' + (isSelected ? '#7c3aed' : 'transparent') + '; transition: all 0.2s;',
|
||||
'click': L.bind(function() { this.handlePresetSelect(preset.id); }, self)
|
||||
}, [
|
||||
E('div', { 'style': 'text-align: center; margin-bottom: 8px;' }, [
|
||||
E('span', { 'style': 'font-size: 32px;' }, preset.emoji),
|
||||
isSelected ? E('span', { 'style': 'position: absolute; margin-left: -8px; font-size: 14px;' }, '\u{2714}\uFE0F') : ''
|
||||
]),
|
||||
E('div', { 'style': 'font-weight: 600; text-align: center; margin-bottom: 4px;' }, preset.name),
|
||||
E('div', { 'style': 'font-size: 11px; color: var(--tor-text-muted, #a0a0b0); text-align: center; margin-bottom: 8px;' }, preset.desc),
|
||||
E('div', { 'style': 'font-size: 10px; color: var(--tor-text-muted, #a0a0b0);' },
|
||||
preset.features.map(function(f) {
|
||||
return E('div', { 'style': 'margin: 2px 0;' }, f);
|
||||
})
|
||||
)
|
||||
]);
|
||||
})
|
||||
)
|
||||
]),
|
||||
|
||||
// Quick Stats
|
||||
E('div', { 'class': 'tor-quick-stats' }, [
|
||||
@ -442,42 +491,109 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
|
||||
// Actions Card
|
||||
// Actions Card - Enhanced Wizard Style
|
||||
E('div', { 'class': 'tor-card' }, [
|
||||
E('div', { 'class': 'tor-card-header' }, [
|
||||
E('div', { 'class': 'tor-card-title' }, [
|
||||
E('span', { 'class': 'tor-card-title-icon' }, '\u26A1'),
|
||||
E('span', { 'class': 'tor-card-title-icon' }, '\u{26A1}'),
|
||||
_('Quick Actions')
|
||||
]),
|
||||
E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, '\u{1F9D9} ' + _('Wizard Tools'))
|
||||
]),
|
||||
E('div', { 'class': 'tor-card-body' }, [
|
||||
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px;' }, [
|
||||
E('button', {
|
||||
'class': 'tor-btn tor-btn-primary',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px;',
|
||||
'click': L.bind(this.handleNewIdentity, this),
|
||||
'disabled': !isActive
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F504}'),
|
||||
E('span', {}, _('New Identity'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'tor-btn',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px;',
|
||||
'click': L.bind(this.handleLeakTest, this),
|
||||
'disabled': !isActive
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F50D}'),
|
||||
E('span', {}, _('Leak Test'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'tor-btn tor-btn-warning',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px;',
|
||||
'click': L.bind(this.handleRestart, this)
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F504}'),
|
||||
E('span', {}, _('Restart'))
|
||||
]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'circuits')
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F5FA}'),
|
||||
E('span', {}, _('Circuits'))
|
||||
]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'hidden-services')
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F9C5}'),
|
||||
E('span', {}, _('.onion Sites'))
|
||||
]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'bridges')
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F309}'),
|
||||
E('span', {}, _('Bridges'))
|
||||
]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'settings')
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{2699}\uFE0F'),
|
||||
E('span', {}, _('Settings'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Features Guide Card
|
||||
E('div', { 'class': 'tor-card', 'style': 'background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(6, 182, 212, 0.1) 100%);' }, [
|
||||
E('div', { 'class': 'tor-card-header' }, [
|
||||
E('div', { 'class': 'tor-card-title' }, [
|
||||
E('span', { 'class': 'tor-card-title-icon' }, '\u{1F4D6}'),
|
||||
_('How Tor Shield Works')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'tor-card-body' }, [
|
||||
E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [
|
||||
E('button', {
|
||||
'class': 'tor-btn tor-btn-primary',
|
||||
'click': L.bind(this.handleNewIdentity, this),
|
||||
'disabled': !isActive
|
||||
}, ['\uD83D\uDD04 ', _('New Identity')]),
|
||||
E('button', {
|
||||
'class': 'tor-btn',
|
||||
'click': L.bind(this.handleLeakTest, this),
|
||||
'disabled': !isActive
|
||||
}, ['\uD83D\uDD0D ', _('Leak Test')]),
|
||||
E('button', {
|
||||
'class': 'tor-btn tor-btn-warning',
|
||||
'click': L.bind(this.handleRestart, this)
|
||||
}, ['\u21BB ', _('Restart')]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'circuits')
|
||||
}, ['\uD83D\uDDFA ', _('View Circuits')]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'hidden-services')
|
||||
}, ['\uD83E\uDDC5 ', _('Hidden Services')]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
'href': L.url('admin', 'services', 'tor-shield', 'settings')
|
||||
}, ['\u2699 ', _('Settings')])
|
||||
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;' }, [
|
||||
E('div', { 'style': 'text-align: center; padding: 12px;' }, [
|
||||
E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F512}'),
|
||||
E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Encrypted')),
|
||||
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('3 layers of encryption protect your data'))
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center; padding: 12px;' }, [
|
||||
E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F465}'),
|
||||
E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Anonymous')),
|
||||
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Your real IP is hidden from websites'))
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center; padding: 12px;' }, [
|
||||
E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F310}'),
|
||||
E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Decentralized')),
|
||||
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Traffic routed through volunteer relays'))
|
||||
]),
|
||||
E('div', { 'style': 'text-align: center; padding: 12px;' }, [
|
||||
E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F6E1}'),
|
||||
E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Kill Switch')),
|
||||
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Blocks traffic if Tor disconnects'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
@ -484,7 +484,7 @@ _generate_frontends() {
|
||||
# HTTP Frontend
|
||||
cat << EOF
|
||||
frontend http-in
|
||||
bind *:$http_port
|
||||
bind *:$http_port,[::]:$http_port
|
||||
mode http
|
||||
|
||||
# ACME challenge routing (no HAProxy restart needed for cert issuance)
|
||||
@ -509,7 +509,7 @@ EOF
|
||||
if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
|
||||
cat << EOF
|
||||
frontend https-in
|
||||
bind *:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
|
||||
bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
|
||||
mode http
|
||||
http-request set-header X-Forwarded-Proto https
|
||||
http-request set-header X-Real-IP %[src]
|
||||
@ -537,7 +537,7 @@ _add_ssl_redirect() {
|
||||
|
||||
local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_')
|
||||
echo " acl host_${acl_name} hdr(host) -i $domain"
|
||||
echo " http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc }"
|
||||
echo " http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc } !is_acme_challenge"
|
||||
}
|
||||
|
||||
_add_vhost_acl() {
|
||||
|
||||
@ -625,6 +625,85 @@ sync_packages_to_local_feed() {
|
||||
print_success "Synchronized $pkg_count packages to local-feed"
|
||||
}
|
||||
|
||||
# Deploy packages to router
|
||||
deploy_packages() {
|
||||
local router="$1"
|
||||
local packages="$2"
|
||||
local ssh_opts="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
|
||||
|
||||
# Test connectivity
|
||||
print_info "Testing connection to router..."
|
||||
if ! ssh $ssh_opts root@$router "echo 'Connected'" 2>/dev/null; then
|
||||
print_error "Cannot connect to router at $router"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find packages to deploy
|
||||
local pkg_dir="$SDK_DIR/bin/packages/$ARCH_NAME/secubox"
|
||||
local target_pkg_dir="$SDK_DIR/bin/targets/$SDK_PATH/packages"
|
||||
|
||||
if [[ -n "$packages" ]]; then
|
||||
# Deploy specific packages
|
||||
print_info "Deploying specific packages: $packages"
|
||||
for pkg in $packages; do
|
||||
local ipk=$(find "$pkg_dir" "$target_pkg_dir" -name "${pkg}*.ipk" 2>/dev/null | head -1)
|
||||
if [[ -n "$ipk" ]]; then
|
||||
print_info "Deploying $(basename "$ipk")..."
|
||||
scp $ssh_opts "$ipk" root@$router:/tmp/
|
||||
ssh $ssh_opts root@$router "opkg install /tmp/$(basename "$ipk") --force-reinstall 2>&1"
|
||||
else
|
||||
print_warning "Package not found: $pkg"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Deploy all recently built packages
|
||||
print_info "Deploying all packages from SDK..."
|
||||
|
||||
# Find all IPK files built today
|
||||
local today=$(date +%Y%m%d)
|
||||
local ipks=$(find "$pkg_dir" -name "*.ipk" -mtime 0 2>/dev/null)
|
||||
|
||||
if [[ -z "$ipks" ]]; then
|
||||
print_warning "No recently built packages found"
|
||||
print_info "Run 'local-build.sh build <package>' first"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Copy packages
|
||||
print_info "Copying packages to router..."
|
||||
for ipk in $ipks; do
|
||||
scp $ssh_opts "$ipk" root@$router:/tmp/
|
||||
done
|
||||
|
||||
# Install packages
|
||||
print_info "Installing packages..."
|
||||
ssh $ssh_opts root@$router "opkg install /tmp/*.ipk --force-reinstall 2>&1" || true
|
||||
fi
|
||||
|
||||
# Sync feed to router
|
||||
print_info "Syncing package feed to router..."
|
||||
local feed_pkg="$SDK_DIR/bin/packages/$ARCH_NAME/secubox"
|
||||
if [[ -d "$feed_pkg" ]]; then
|
||||
ssh $ssh_opts root@$router "mkdir -p /www/secubox-feed"
|
||||
scp $ssh_opts "$feed_pkg"/*.ipk root@$router:/www/secubox-feed/ 2>/dev/null || true
|
||||
|
||||
# Generate Packages index
|
||||
ssh $ssh_opts root@$router "cd /www/secubox-feed && \
|
||||
rm -f Packages Packages.gz && \
|
||||
for ipk in *.ipk; do \
|
||||
[ -f \"\$ipk\" ] && tar -xzf \"\$ipk\" ./control.tar.gz && \
|
||||
tar -xzf control.tar.gz ./control && \
|
||||
cat control >> Packages && echo '' >> Packages && \
|
||||
rm -f control control.tar.gz; \
|
||||
done && \
|
||||
gzip -k Packages 2>/dev/null || true"
|
||||
|
||||
print_success "Feed synced to /www/secubox-feed"
|
||||
fi
|
||||
|
||||
print_success "Deployment complete"
|
||||
}
|
||||
|
||||
# Copy packages to SDK feed
|
||||
copy_packages() {
|
||||
local single_package="$1"
|
||||
@ -2249,6 +2328,7 @@ COMMANDS:
|
||||
clean Clean build directories
|
||||
clean-all Clean all build directories including OpenWrt source and local-feed
|
||||
sync Sync packages from package/secubox to local-feed
|
||||
deploy [router] [packages] Deploy packages to router (default: 192.168.255.1)
|
||||
help Show this help message
|
||||
|
||||
PACKAGES:
|
||||
@ -2429,6 +2509,13 @@ main() {
|
||||
print_success "Packages synchronized to local-feed"
|
||||
;;
|
||||
|
||||
deploy)
|
||||
local router="${1:-192.168.255.1}"
|
||||
local packages="$2"
|
||||
print_header "Deploying Packages to Router ($router)"
|
||||
deploy_packages "$router" "$packages"
|
||||
;;
|
||||
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user