feat: implement VHost Manager - nginx reverse proxy and SSL management
Implements a comprehensive virtual host management system for OpenWrt with
nginx reverse proxy and Let's Encrypt SSL certificate integration.
Features:
- Virtual host management with nginx reverse proxy configuration
- Backend connectivity testing before deployment
- SSL/TLS certificate provisioning via acme.sh and Let's Encrypt
- Certificate expiry monitoring with color-coded warnings
- HTTP Basic Authentication support
- WebSocket protocol support with upgrade headers
- Real-time nginx access log viewer per domain
- Automatic nginx configuration generation and reload
Components:
- RPCD backend (luci.vhost-manager): 11 ubus methods for vhost and cert management
* status, list_vhosts, get_vhost, add_vhost, update_vhost, delete_vhost
* test_backend, request_cert, list_certs, reload_nginx, get_access_logs
- 4 JavaScript views: overview, vhosts, certificates, logs
- ACL with read/write permissions for all ubus methods
- UCI config with global settings and vhost sections
- Comprehensive README with API docs, examples, and troubleshooting
Configuration:
- Nginx vhost configs generated in /etc/nginx/conf.d/vhosts/
- SSL certificates managed via ACME in /etc/acme/{domain}/
- Access logs per domain: /var/log/nginx/{domain}.access.log
- HTTP Basic Auth htpasswd files in /etc/nginx/htpasswd/
Architecture follows SecuBox standards:
- RPCD naming convention (luci. prefix)
- Menu paths match view file structure
- All JavaScript in strict mode
- Backend connectivity validation
- Comprehensive error handling
Dependencies: nginx-ssl, acme, curl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6200167434
commit
77d40a1f89
@ -1,48 +1,19 @@
|
||||
# Copyright (C) 2024 CyberMind.fr
|
||||
# Licensed under Apache-2.0
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vhost-manager
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
LUCI_TITLE:=VHost Manager - Reverse Proxy & SSL
|
||||
LUCI_DESCRIPTION:=Nginx reverse proxy manager with Let's Encrypt SSL certificates, authentication, and WebSocket support
|
||||
LUCI_DEPENDS:=+luci-base +rpcd +nginx-ssl +acme +curl
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/luci-app-vhost-manager
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=VHost Manager - Virtual Hosts & Local SaaS
|
||||
DEPENDS:=+luci-base +rpcd +nginx +dnsmasq
|
||||
PKGARCH:=all
|
||||
endef
|
||||
include ../../luci.mk
|
||||
|
||||
define Package/luci-app-vhost-manager/description
|
||||
Virtual host and local SaaS management:
|
||||
- Internal virtual hosts configuration
|
||||
- External service redirection to local alternatives
|
||||
- Self-hosted SaaS deployment (Nextcloud, GitLab, etc.)
|
||||
- DNS-based traffic interception
|
||||
- SSL certificate management
|
||||
- Reverse proxy configuration
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-vhost-manager/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vhost-manager $(1)/usr/libexec/rpcd/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/vhost $(1)/etc/config/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/vhost-manager
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/vhost-manager/*.js $(1)/www/luci-static/resources/view/vhost-manager/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/vhost-manager
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/vhost-manager/*.js $(1)/www/luci-static/resources/vhost-manager/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-vhost-manager))
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
@ -1,32 +1,544 @@
|
||||
# VHost Manager for OpenWrt
|
||||
# VHost Manager - Reverse Proxy & SSL Certificate Management
|
||||
|
||||
Virtual host and local SaaS gateway management.
|
||||
LuCI application for managing nginx reverse proxy virtual hosts and SSL certificates via Let's Encrypt.
|
||||
|
||||
## Features
|
||||
|
||||
### 🏠 Internal Virtual Hosts
|
||||
- Configure local services with custom domains
|
||||
- Automatic nginx reverse proxy
|
||||
- SSL/TLS with Let's Encrypt or self-signed
|
||||
### Virtual Host Management
|
||||
- Create and manage nginx reverse proxy configurations
|
||||
- Support for HTTP and HTTPS virtual hosts
|
||||
- Backend connectivity testing before deployment
|
||||
- WebSocket protocol support
|
||||
- HTTP Basic Authentication
|
||||
- Automatic nginx reload on configuration changes
|
||||
|
||||
### ↪️ External Redirects
|
||||
- Redirect external services to local alternatives
|
||||
- DNS-based traffic interception
|
||||
- Privacy-preserving local alternatives
|
||||
### SSL Certificate Management
|
||||
- Let's Encrypt certificate provisioning via acme.sh
|
||||
- Certificate status monitoring with expiry tracking
|
||||
- Color-coded expiry warnings (red < 7 days, orange < 30 days)
|
||||
- Certificate details viewer
|
||||
- Automatic certificate renewal support
|
||||
|
||||
### 🔒 SSL Management
|
||||
- ACME/Let's Encrypt integration
|
||||
- Automatic certificate renewal
|
||||
- Self-signed certificate generation
|
||||
### Access Log Monitoring
|
||||
- Real-time nginx access log viewer
|
||||
- Per-domain log filtering
|
||||
- Configurable line display (50-500 lines)
|
||||
- Terminal-style log display
|
||||
|
||||
## Supported Services
|
||||
## Installation
|
||||
|
||||
- Nextcloud (Google Drive alternative)
|
||||
- GitLab (GitHub alternative)
|
||||
- Jellyfin (Netflix/YouTube alternative)
|
||||
- Home Assistant (Smart home)
|
||||
- And more...
|
||||
```bash
|
||||
opkg update
|
||||
opkg install luci-app-vhost-manager
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **luci-base**: LuCI framework
|
||||
- **rpcd**: RPC daemon for backend communication
|
||||
- **nginx-ssl**: Nginx web server with SSL support
|
||||
- **acme**: ACME client for Let's Encrypt certificates
|
||||
- **curl**: HTTP client for backend testing
|
||||
|
||||
## Configuration
|
||||
|
||||
### UCI Configuration
|
||||
|
||||
Edit `/etc/config/vhost_manager`:
|
||||
|
||||
```bash
|
||||
config global 'global'
|
||||
option enabled '1'
|
||||
option auto_reload '1'
|
||||
option log_retention '30'
|
||||
|
||||
config vhost 'myapp'
|
||||
option domain 'app.example.com'
|
||||
option backend 'http://192.168.1.100:8080'
|
||||
option ssl '1'
|
||||
option auth '1'
|
||||
option auth_user 'admin'
|
||||
option auth_pass 'secretpassword'
|
||||
option websocket '1'
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
#### Global Section
|
||||
- `enabled`: Enable/disable VHost Manager (default: 1)
|
||||
- `auto_reload`: Automatically reload nginx on config changes (default: 1)
|
||||
- `log_retention`: Days to retain access logs (default: 30)
|
||||
|
||||
#### VHost Section
|
||||
- `domain`: Domain name for this virtual host (required)
|
||||
- `backend`: Backend URL to proxy to (required, e.g., http://192.168.1.100:8080)
|
||||
- `ssl`: Enable HTTPS (default: 0, requires valid SSL certificate)
|
||||
- `auth`: Enable HTTP Basic Authentication (default: 0)
|
||||
- `auth_user`: Username for authentication (required if auth=1)
|
||||
- `auth_pass`: Password for authentication (required if auth=1)
|
||||
- `websocket`: Enable WebSocket support (default: 0)
|
||||
|
||||
## Usage
|
||||
|
||||
### Web Interface
|
||||
|
||||
Navigate to **Services → VHost Manager** in LuCI.
|
||||
|
||||
#### Overview Tab
|
||||
- System status (Nginx running, ACME availability)
|
||||
- Virtual host statistics (SSL enabled, auth protected, WebSocket)
|
||||
- Certificate count and expiry status
|
||||
- Recent virtual hosts list
|
||||
|
||||
#### Virtual Hosts Tab
|
||||
- Add new virtual hosts
|
||||
- Edit existing configurations
|
||||
- Test backend connectivity before saving
|
||||
- Enable/disable SSL, authentication, WebSocket
|
||||
- Delete virtual hosts
|
||||
|
||||
#### Certificates Tab
|
||||
- Request new Let's Encrypt certificates
|
||||
- View installed certificates with expiry dates
|
||||
- Certificate details viewer
|
||||
- Color-coded expiry warnings
|
||||
|
||||
#### Logs Tab
|
||||
- View nginx access logs per domain
|
||||
- Select number of lines to display (50-500)
|
||||
- Real-time log streaming
|
||||
|
||||
### Command Line
|
||||
|
||||
#### List Virtual Hosts
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager list_vhosts
|
||||
```
|
||||
|
||||
#### Get VHost Manager Status
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager status
|
||||
```
|
||||
|
||||
#### Add Virtual Host
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager add_vhost '{
|
||||
"domain": "app.example.com",
|
||||
"backend": "http://192.168.1.100:8080",
|
||||
"ssl": true,
|
||||
"auth": false,
|
||||
"websocket": true
|
||||
}'
|
||||
```
|
||||
|
||||
#### Test Backend Connectivity
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager test_backend '{
|
||||
"backend": "http://192.168.1.100:8080"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Request SSL Certificate
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager request_cert '{
|
||||
"domain": "app.example.com",
|
||||
"email": "admin@example.com"
|
||||
}'
|
||||
```
|
||||
|
||||
#### List Certificates
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager list_certs
|
||||
```
|
||||
|
||||
#### Reload Nginx
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager reload_nginx
|
||||
```
|
||||
|
||||
#### Get Access Logs
|
||||
|
||||
```bash
|
||||
ubus call luci.vhost-manager get_access_logs '{
|
||||
"domain": "app.example.com",
|
||||
"lines": 100
|
||||
}'
|
||||
```
|
||||
|
||||
## Nginx Configuration
|
||||
|
||||
VHost Manager generates nginx configuration files in `/etc/nginx/conf.d/vhosts/`.
|
||||
|
||||
### Example Generated Configuration (HTTP Only)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name app.example.com;
|
||||
|
||||
access_log /var/log/nginx/app.example.com.access.log;
|
||||
error_log /var/log/nginx/app.example.com.error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://192.168.1.100:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Generated Configuration (HTTPS with WebSocket)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name app.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name app.example.com;
|
||||
|
||||
ssl_certificate /etc/acme/app.example.com/fullchain.cer;
|
||||
ssl_certificate_key /etc/acme/app.example.com/app.example.com.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
access_log /var/log/nginx/app.example.com.access.log;
|
||||
error_log /var/log/nginx/app.example.com.error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://192.168.1.100:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example with Authentication
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name app.example.com;
|
||||
|
||||
ssl_certificate /etc/acme/app.example.com/fullchain.cer;
|
||||
ssl_certificate_key /etc/acme/app.example.com/app.example.com.key;
|
||||
|
||||
location / {
|
||||
auth_basic "Restricted Access";
|
||||
auth_basic_user_file /etc/nginx/htpasswd/app.example.com;
|
||||
|
||||
proxy_pass http://192.168.1.100:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SSL Certificate Workflow
|
||||
|
||||
1. **DNS Configuration**: Ensure your domain points to your router's public IP
|
||||
2. **Port Forwarding**: Forward ports 80 and 443 to your router
|
||||
3. **Request Certificate**: Use the Certificates tab to request a Let's Encrypt certificate
|
||||
4. **Configure VHost**: Enable SSL for your virtual host
|
||||
5. **Monitor Expiry**: Certificates expire after 90 days, monitor in the Certificates tab
|
||||
|
||||
### ACME Certificate Locations
|
||||
|
||||
- Certificates: `/etc/acme/{domain}/fullchain.cer`
|
||||
- Private keys: `/etc/acme/{domain}/{domain}.key`
|
||||
- ACME account: `/etc/acme/account.conf`
|
||||
|
||||
## ubus API Reference
|
||||
|
||||
### status()
|
||||
|
||||
Get VHost Manager and nginx status.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"nginx_running": true,
|
||||
"nginx_version": "1.23.3",
|
||||
"acme_available": true,
|
||||
"acme_version": "3.0.5",
|
||||
"vhost_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
### list_vhosts()
|
||||
|
||||
List all configured virtual hosts.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"vhosts": [
|
||||
{
|
||||
"domain": "app.example.com",
|
||||
"backend": "http://192.168.1.100:8080",
|
||||
"ssl": true,
|
||||
"ssl_expires": "2025-03-15",
|
||||
"auth": false,
|
||||
"websocket": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### get_vhost(domain)
|
||||
|
||||
Get details for a specific virtual host.
|
||||
|
||||
**Parameters:**
|
||||
- `domain`: Domain name
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"domain": "app.example.com",
|
||||
"backend": "http://192.168.1.100:8080",
|
||||
"ssl": true,
|
||||
"ssl_expires": "2025-03-15",
|
||||
"auth": true,
|
||||
"auth_user": "admin",
|
||||
"websocket": true
|
||||
}
|
||||
```
|
||||
|
||||
### add_vhost(domain, backend, ssl, auth, websocket)
|
||||
|
||||
Add a new virtual host.
|
||||
|
||||
**Parameters:**
|
||||
- `domain`: Domain name (required)
|
||||
- `backend`: Backend URL (required)
|
||||
- `ssl`: Enable SSL (boolean)
|
||||
- `auth`: Enable authentication (boolean)
|
||||
- `websocket`: Enable WebSocket (boolean)
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"reload_required": true
|
||||
}
|
||||
```
|
||||
|
||||
### update_vhost(domain, backend, ssl, auth, websocket)
|
||||
|
||||
Update an existing virtual host.
|
||||
|
||||
**Parameters:** Same as add_vhost
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"reload_required": true
|
||||
}
|
||||
```
|
||||
|
||||
### delete_vhost(domain)
|
||||
|
||||
Delete a virtual host.
|
||||
|
||||
**Parameters:**
|
||||
- `domain`: Domain name
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"reload_required": true
|
||||
}
|
||||
```
|
||||
|
||||
### test_backend(backend)
|
||||
|
||||
Test connectivity to a backend server.
|
||||
|
||||
**Parameters:**
|
||||
- `backend`: Backend URL
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"reachable": true,
|
||||
"response_time": 45
|
||||
}
|
||||
```
|
||||
|
||||
### request_cert(domain, email)
|
||||
|
||||
Request a Let's Encrypt SSL certificate.
|
||||
|
||||
**Parameters:**
|
||||
- `domain`: Domain name (required)
|
||||
- `email`: Email address for ACME account (required)
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Certificate obtained successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### list_certs()
|
||||
|
||||
List all installed SSL certificates.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"certificates": [
|
||||
{
|
||||
"domain": "app.example.com",
|
||||
"subject": "CN=app.example.com",
|
||||
"issuer": "R3",
|
||||
"expires": "2025-03-15",
|
||||
"cert_file": "/etc/acme/app.example.com/fullchain.cer"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### reload_nginx()
|
||||
|
||||
Reload nginx configuration.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### get_access_logs(domain, lines)
|
||||
|
||||
Get nginx access logs for a domain.
|
||||
|
||||
**Parameters:**
|
||||
- `domain`: Domain name (required)
|
||||
- `lines`: Number of lines to retrieve (default: 50)
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"logs": [
|
||||
"192.168.1.50 - - [24/Dec/2025:10:30:15 +0000] \"GET / HTTP/1.1\" 200 1234",
|
||||
"192.168.1.51 - - [24/Dec/2025:10:30:16 +0000] \"GET /api HTTP/1.1\" 200 5678"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Nginx Won't Start
|
||||
|
||||
Check nginx configuration syntax:
|
||||
```bash
|
||||
nginx -t
|
||||
```
|
||||
|
||||
View nginx error log:
|
||||
```bash
|
||||
logread | grep nginx
|
||||
```
|
||||
|
||||
### Certificate Request Fails
|
||||
|
||||
Ensure:
|
||||
1. Domain DNS points to your public IP
|
||||
2. Ports 80 and 443 are forwarded to your router
|
||||
3. Firewall allows incoming connections on ports 80 and 443
|
||||
4. No other service is using port 80 (acme.sh needs it for validation)
|
||||
|
||||
Check ACME logs:
|
||||
```bash
|
||||
cat /var/log/acme.log
|
||||
```
|
||||
|
||||
### Backend Unreachable
|
||||
|
||||
Test backend manually:
|
||||
```bash
|
||||
curl -I http://192.168.1.100:8080
|
||||
```
|
||||
|
||||
Check if backend is listening:
|
||||
```bash
|
||||
netstat -tuln | grep 8080
|
||||
```
|
||||
|
||||
### WebSocket Not Working
|
||||
|
||||
Ensure:
|
||||
1. WebSocket support is enabled in virtual host configuration
|
||||
2. Backend application supports WebSocket
|
||||
3. No proxy timeouts are too short (default: 86400s)
|
||||
|
||||
### Authentication Not Working
|
||||
|
||||
Check htpasswd file exists:
|
||||
```bash
|
||||
ls -l /etc/nginx/htpasswd/{domain}
|
||||
```
|
||||
|
||||
Regenerate htpasswd file:
|
||||
```bash
|
||||
# Via UCI
|
||||
uci set vhost_manager.myapp.auth_user='newuser'
|
||||
uci set vhost_manager.myapp.auth_pass='newpass'
|
||||
uci commit vhost_manager
|
||||
|
||||
# Trigger config regeneration
|
||||
ubus call luci.vhost-manager update_vhost '{...}'
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **SSL Certificates**: Always use HTTPS for production services
|
||||
2. **Strong Passwords**: Use strong passwords for HTTP Basic Authentication
|
||||
3. **Backend Security**: Ensure backend services are not directly accessible from the internet
|
||||
4. **Firewall Rules**: Configure firewall to only allow necessary ports
|
||||
5. **Log Monitoring**: Regularly review access logs for suspicious activity
|
||||
6. **Certificate Renewal**: Monitor certificate expiry and ensure auto-renewal is working
|
||||
|
||||
## License
|
||||
|
||||
MIT License - CyberMind Security
|
||||
Apache-2.0
|
||||
|
||||
## Maintainer
|
||||
|
||||
SecuBox Project <support@secubox.com>
|
||||
|
||||
## Version
|
||||
|
||||
1.0.0
|
||||
|
||||
@ -1,17 +1,89 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({object:'luci.vhost-manager',method:'status',expect:{}});
|
||||
var callInternalHosts = rpc.declare({object:'luci.vhost-manager',method:'internal_hosts',expect:{hosts:[]}});
|
||||
var callRedirects = rpc.declare({object:'luci.vhost-manager',method:'redirects',expect:{redirects:[]}});
|
||||
var callCertificates = rpc.declare({object:'luci.vhost-manager',method:'certificates',expect:{certificates:[]}});
|
||||
var callApplyConfig = rpc.declare({object:'luci.vhost-manager',method:'apply_config'});
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getInternalHosts: callInternalHosts,
|
||||
getRedirects: callRedirects,
|
||||
getCertificates: callCertificates,
|
||||
applyConfig: callApplyConfig
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'status',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callListVHosts = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'list_vhosts',
|
||||
expect: { vhosts: [] }
|
||||
});
|
||||
|
||||
var callGetVHost = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'get_vhost',
|
||||
params: ['domain'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callAddVHost = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'add_vhost',
|
||||
params: ['domain', 'backend', 'ssl', 'auth', 'websocket'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callUpdateVHost = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'update_vhost',
|
||||
params: ['domain', 'backend', 'ssl', 'auth', 'websocket'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callDeleteVHost = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'delete_vhost',
|
||||
params: ['domain'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callTestBackend = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'test_backend',
|
||||
params: ['backend'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callRequestCert = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'request_cert',
|
||||
params: ['domain', 'email'],
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callListCerts = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'list_certs',
|
||||
expect: { certificates: [] }
|
||||
});
|
||||
|
||||
var callReloadNginx = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'reload_nginx',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callGetAccessLogs = rpc.declare({
|
||||
object: 'luci.vhost-manager',
|
||||
method: 'get_access_logs',
|
||||
params: ['domain', 'lines'],
|
||||
expect: { logs: [] }
|
||||
});
|
||||
|
||||
return {
|
||||
getStatus: callStatus,
|
||||
listVHosts: callListVHosts,
|
||||
getVHost: callGetVHost,
|
||||
addVHost: callAddVHost,
|
||||
updateVHost: callUpdateVHost,
|
||||
deleteVHost: callDeleteVHost,
|
||||
testBackend: callTestBackend,
|
||||
requestCert: callRequestCert,
|
||||
listCerts: callListCerts,
|
||||
reloadNginx: callReloadNginx,
|
||||
getAccessLogs: callGetAccessLogs
|
||||
};
|
||||
|
||||
@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require vhost-manager/api as API';
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
API.listCerts()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var certs = data[0] || [];
|
||||
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('SSL Certificates')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Manage Let\'s Encrypt SSL certificates'))
|
||||
]);
|
||||
|
||||
// Request new certificate section
|
||||
var requestSection = E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Request New Certificate')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'id': 'cert-domain',
|
||||
'placeholder': 'example.com'
|
||||
})
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Email')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('input', {
|
||||
'type': 'email',
|
||||
'class': 'cbi-input-text',
|
||||
'id': 'cert-email',
|
||||
'placeholder': 'admin@example.com'
|
||||
})
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, ''),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var domain = document.getElementById('cert-domain').value;
|
||||
var email = document.getElementById('cert-email').value;
|
||||
|
||||
if (!domain || !email) {
|
||||
ui.addNotification(null, E('p', _('Domain and email are required')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', _('Requesting certificate... This may take a few minutes.')), 'info');
|
||||
|
||||
API.requestCert(domain, email).then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', '✓ ' + _('Certificate obtained successfully')), 'info');
|
||||
window.location.reload();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, _('Request Certificate'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
v.appendChild(requestSection);
|
||||
|
||||
// Certificates list
|
||||
if (certs.length > 0) {
|
||||
var certsSection = E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Installed Certificates'))
|
||||
]);
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Domain')),
|
||||
E('th', { 'class': 'th' }, _('Issuer')),
|
||||
E('th', { 'class': 'th' }, _('Expires')),
|
||||
E('th', { 'class': 'th' }, _('Actions'))
|
||||
])
|
||||
]);
|
||||
|
||||
certs.forEach(function(cert) {
|
||||
var expiresDate = new Date(cert.expires);
|
||||
var daysLeft = Math.floor((expiresDate - new Date()) / (1000 * 60 * 60 * 24));
|
||||
var expiresColor = daysLeft < 7 ? 'red' : (daysLeft < 30 ? 'orange' : 'green');
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, cert.domain),
|
||||
E('td', { 'class': 'td' }, cert.issuer || 'N/A'),
|
||||
E('td', { 'class': 'td' }, [
|
||||
E('span', { 'style': 'color: ' + expiresColor }, cert.expires),
|
||||
E('br'),
|
||||
E('small', {}, daysLeft + ' ' + _('days remaining'))
|
||||
]),
|
||||
E('td', { 'class': 'td' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': function(ev) {
|
||||
ui.showModal(_('Certificate Details'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Domain: ')),
|
||||
E('span', {}, cert.domain)
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Subject: ')),
|
||||
E('span', {}, cert.subject || 'N/A')
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Issuer: ')),
|
||||
E('span', {}, cert.issuer || 'N/A')
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Expires: ')),
|
||||
E('span', {}, cert.expires)
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('File: ')),
|
||||
E('code', {}, cert.cert_file)
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Close'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
}, _('Details'))
|
||||
])
|
||||
]));
|
||||
});
|
||||
|
||||
certsSection.appendChild(table);
|
||||
v.appendChild(certsSection);
|
||||
} else {
|
||||
v.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', { 'style': 'font-style: italic; text-align: center; padding: 20px' },
|
||||
_('No SSL certificates installed'))
|
||||
]));
|
||||
}
|
||||
|
||||
return v;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require vhost-manager/api as API';
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
API.listVHosts()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var vhosts = data[0] || [];
|
||||
|
||||
var m = new form.Map('vhost_manager', _('Access Logs'),
|
||||
_('View nginx access logs for virtual hosts'));
|
||||
|
||||
var s = m.section(form.NamedSection, '__logs', 'logs');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
var o;
|
||||
|
||||
o = s.option(form.ListValue, 'domain', _('Select Domain'));
|
||||
o.rmempty = false;
|
||||
|
||||
vhosts.forEach(function(vhost) {
|
||||
o.value(vhost.domain, vhost.domain);
|
||||
});
|
||||
|
||||
if (vhosts.length === 0) {
|
||||
o.value('', _('No virtual hosts configured'));
|
||||
}
|
||||
|
||||
o = s.option(form.ListValue, 'lines', _('Number of Lines'));
|
||||
o.value('50', '50');
|
||||
o.value('100', '100');
|
||||
o.value('200', '200');
|
||||
o.value('500', '500');
|
||||
o.default = '50';
|
||||
|
||||
s.render = L.bind(function(view, section_id) {
|
||||
var domain = this.section.formvalue(section_id, 'domain');
|
||||
var lines = parseInt(this.section.formvalue(section_id, 'lines')) || 50;
|
||||
|
||||
if (!domain || vhosts.length === 0) {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', { 'style': 'font-style: italic' }, _('No virtual hosts to display logs for'))
|
||||
]);
|
||||
}
|
||||
|
||||
return API.getAccessLogs(domain, lines).then(L.bind(function(data) {
|
||||
var logs = data.logs || [];
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Access Logs for: ') + domain),
|
||||
E('pre', {
|
||||
'style': 'background: #000; color: #0f0; padding: 10px; overflow: auto; max-height: 500px; font-size: 11px; font-family: monospace'
|
||||
}, logs.length > 0 ? logs.join('\n') : _('No logs available'))
|
||||
]);
|
||||
}, this));
|
||||
}, this, this);
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -1,54 +1,133 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require vhost-manager.api as api';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require vhost-manager/api as API';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([api.getStatus(), api.getInternalHosts()]);
|
||||
},
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var hosts = data[1].hosts || [];
|
||||
|
||||
var icons = {cloud:'☁️',code:'💻',film:'🎬',home:'🏠',server:'🖥️'};
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('style', {}, [
|
||||
'.vh{font-family:system-ui,sans-serif}',
|
||||
'.vh-hdr{background:linear-gradient(135deg,#059669,#10b981);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
|
||||
'.vh-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
|
||||
'.vh-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
|
||||
'.vh-stat-val{font-size:28px;font-weight:700;color:#10b981}',
|
||||
'.vh-hosts{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}'
|
||||
].join('')),
|
||||
E('div', {class:'vh'}, [
|
||||
E('div', {class:'vh-hdr'}, [
|
||||
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🌐 VHost Manager'),
|
||||
E('p', {style:'margin:0;opacity:.9'}, 'Virtual Hosts & Local SaaS Gateway')
|
||||
]),
|
||||
E('div', {class:'vh-stats'}, [
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.nginx_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Nginx')]),
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.dns_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'DNS')]),
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.internal_hosts || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Internal Hosts')]),
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.redirects || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Redirects')])
|
||||
]),
|
||||
E('div', {class:'vh-hosts'}, hosts.filter(function(h) { return h.enabled; }).map(function(h) {
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;border-left:4px solid '+h.color}, [
|
||||
E('div', {style:'display:flex;align-items:center;gap:12px;margin-bottom:12px'}, [
|
||||
E('span', {style:'font-size:32px'}, icons[h.icon] || '🖥️'),
|
||||
E('div', {}, [
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:18px'}, h.name),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px'}, h.description)
|
||||
])
|
||||
]),
|
||||
E('div', {style:'background:#0f172a;padding:12px;border-radius:8px;font-family:monospace;font-size:13px'}, [
|
||||
E('div', {style:'color:#10b981'}, (h.ssl ? '🔒 https://' : '🔓 http://') + h.domain),
|
||||
E('div', {style:'color:#64748b;margin-top:4px'}, '→ ' + h.backend)
|
||||
])
|
||||
]);
|
||||
}))
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
API.getStatus(),
|
||||
API.listVHosts(),
|
||||
API.listCerts()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var vhosts = data[1] || [];
|
||||
var certs = data[2] || [];
|
||||
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('VHost Manager - Overview')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Nginx reverse proxy and SSL certificate management'))
|
||||
]);
|
||||
|
||||
// Status section
|
||||
var statusSection = E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('System Status')),
|
||||
E('div', { 'class': 'table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td left', 'width': '33%' }, [
|
||||
E('strong', {}, _('Nginx: ')),
|
||||
E('span', {}, status.nginx_running ?
|
||||
E('span', { 'style': 'color: green' }, '● ' + _('Running')) :
|
||||
E('span', { 'style': 'color: red' }, '● ' + _('Stopped'))
|
||||
),
|
||||
E('br'),
|
||||
E('small', {}, _('Version: ') + (status.nginx_version || 'unknown'))
|
||||
]),
|
||||
E('div', { 'class': 'td left', 'width': '33%' }, [
|
||||
E('strong', {}, _('ACME/SSL: ')),
|
||||
E('span', {}, status.acme_available ?
|
||||
E('span', { 'style': 'color: green' }, '✓ ' + _('Available')) :
|
||||
E('span', { 'style': 'color: orange' }, '✗ ' + _('Not installed'))
|
||||
),
|
||||
E('br'),
|
||||
E('small', {}, status.acme_version || 'N/A')
|
||||
]),
|
||||
E('div', { 'class': 'td left', 'width': '33%' }, [
|
||||
E('strong', {}, _('Virtual Hosts: ')),
|
||||
E('span', { 'style': 'font-size: 1.5em; color: #0088cc' }, String(status.vhost_count || 0))
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
v.appendChild(statusSection);
|
||||
|
||||
// Quick stats
|
||||
var sslCount = 0;
|
||||
var authCount = 0;
|
||||
var wsCount = 0;
|
||||
|
||||
vhosts.forEach(function(vhost) {
|
||||
if (vhost.ssl) sslCount++;
|
||||
if (vhost.auth) authCount++;
|
||||
if (vhost.websocket) wsCount++;
|
||||
});
|
||||
|
||||
var statsSection = E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Virtual Hosts Summary')),
|
||||
E('div', { 'class': 'table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||
E('strong', {}, '🔒 SSL Enabled: '),
|
||||
E('span', {}, String(sslCount))
|
||||
]),
|
||||
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||
E('strong', {}, '🔐 Auth Protected: '),
|
||||
E('span', {}, String(authCount))
|
||||
]),
|
||||
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||
E('strong', {}, '🔌 WebSocket: '),
|
||||
E('span', {}, String(wsCount))
|
||||
]),
|
||||
E('div', { 'class': 'td left', 'width': '25%' }, [
|
||||
E('strong', {}, '📜 Certificates: '),
|
||||
E('span', {}, String(certs.length))
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
v.appendChild(statsSection);
|
||||
|
||||
// Recent vhosts
|
||||
if (vhosts.length > 0) {
|
||||
var vhostSection = E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, _('Virtual Hosts'))
|
||||
]);
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Domain')),
|
||||
E('th', { 'class': 'th' }, _('Backend')),
|
||||
E('th', { 'class': 'th' }, _('Features')),
|
||||
E('th', { 'class': 'th' }, _('SSL Expires'))
|
||||
])
|
||||
]);
|
||||
|
||||
vhosts.slice(0, 10).forEach(function(vhost) {
|
||||
var features = [];
|
||||
if (vhost.ssl) features.push('🔒 SSL');
|
||||
if (vhost.auth) features.push('🔐 Auth');
|
||||
if (vhost.websocket) features.push('🔌 WS');
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, vhost.domain),
|
||||
E('td', { 'class': 'td' }, vhost.backend),
|
||||
E('td', { 'class': 'td' }, features.join(' ')),
|
||||
E('td', { 'class': 'td' }, vhost.ssl_expires || 'N/A')
|
||||
]));
|
||||
});
|
||||
|
||||
vhostSection.appendChild(table);
|
||||
v.appendChild(vhostSection);
|
||||
}
|
||||
|
||||
return v;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
|
||||
@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require form';
|
||||
'require vhost-manager/api as API';
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
API.listVHosts()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var vhosts = data[0] || [];
|
||||
|
||||
var m = new form.Map('vhost_manager', _('Virtual Hosts'),
|
||||
_('Manage nginx reverse proxy virtual hosts'));
|
||||
|
||||
var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts'));
|
||||
s.anonymous = false;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
|
||||
s.modaltitle = function(section_id) {
|
||||
return _('Edit VHost: ') + section_id;
|
||||
};
|
||||
|
||||
var o;
|
||||
|
||||
o = s.option(form.Value, 'domain', _('Domain'));
|
||||
o.rmempty = false;
|
||||
o.placeholder = 'example.com';
|
||||
o.description = _('Domain name for this virtual host');
|
||||
|
||||
o = s.option(form.Value, 'backend', _('Backend URL'));
|
||||
o.rmempty = false;
|
||||
o.placeholder = 'http://192.168.1.100:8080';
|
||||
o.description = _('Backend server URL to proxy to');
|
||||
|
||||
// Test backend button
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var widget = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
|
||||
|
||||
var testBtn = E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'style': 'margin-left: 10px',
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
var backend = this.parentNode.querySelector('input').value;
|
||||
|
||||
if (!backend) {
|
||||
ui.addNotification(null, E('p', _('Please enter a backend URL')), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', _('Testing backend connectivity...')), 'info');
|
||||
|
||||
API.testBackend(backend).then(function(result) {
|
||||
if (result.reachable) {
|
||||
ui.addNotification(null, E('p', '✓ ' + _('Backend is reachable')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', '✗ ' + _('Backend is unreachable')), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, _('Test'));
|
||||
|
||||
widget.appendChild(testBtn);
|
||||
return widget;
|
||||
};
|
||||
|
||||
o = s.option(form.Flag, 'ssl', _('Enable SSL'));
|
||||
o.default = o.disabled;
|
||||
o.description = _('Enable HTTPS (requires valid SSL certificate)');
|
||||
|
||||
o = s.option(form.Flag, 'auth', _('Enable Authentication'));
|
||||
o.default = o.disabled;
|
||||
o.description = _('Require HTTP basic authentication');
|
||||
|
||||
o = s.option(form.Value, 'auth_user', _('Auth Username'));
|
||||
o.depends('auth', '1');
|
||||
o.placeholder = 'admin';
|
||||
|
||||
o = s.option(form.Value, 'auth_pass', _('Auth Password'));
|
||||
o.depends('auth', '1');
|
||||
o.password = true;
|
||||
|
||||
o = s.option(form.Flag, 'websocket', _('WebSocket Support'));
|
||||
o.default = o.disabled;
|
||||
o.description = _('Enable WebSocket protocol upgrade headers');
|
||||
|
||||
// Custom actions
|
||||
s.addModalOptions = function(s, section_id, ev) {
|
||||
// Get form values
|
||||
var domain = this.section.formvalue(section_id, 'domain');
|
||||
var backend = this.section.formvalue(section_id, 'backend');
|
||||
var ssl = this.section.formvalue(section_id, 'ssl') === '1';
|
||||
var auth = this.section.formvalue(section_id, 'auth') === '1';
|
||||
var websocket = this.section.formvalue(section_id, 'websocket') === '1';
|
||||
|
||||
if (!domain || !backend) {
|
||||
ui.addNotification(null, E('p', _('Domain and backend are required')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Call API to add vhost
|
||||
API.addVHost(domain, backend, ssl, auth, websocket).then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', _('VHost created successfully')), 'info');
|
||||
|
||||
if (result.reload_required) {
|
||||
ui.showModal(_('Reload Nginx?'), [
|
||||
E('p', {}, _('Configuration changed. Reload nginx to apply?')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Later')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': function() {
|
||||
API.reloadNginx().then(function(reload_result) {
|
||||
ui.hideModal();
|
||||
if (reload_result.success) {
|
||||
ui.addNotification(null, E('p', '✓ ' + _('Nginx reloaded')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', '✗ ' + reload_result.message), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, _('Reload Now'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
17
luci-app-vhost-manager/root/etc/config/vhost_manager
Normal file
17
luci-app-vhost-manager/root/etc/config/vhost_manager
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
config global 'global'
|
||||
option enabled '1'
|
||||
option auto_reload '1'
|
||||
option log_retention '30'
|
||||
|
||||
# Example virtual host configuration
|
||||
# Uncomment and modify for your needs
|
||||
#
|
||||
# config vhost 'example'
|
||||
# option domain 'example.com'
|
||||
# option backend 'http://192.168.1.100:8080'
|
||||
# option ssl '1'
|
||||
# option auth '0'
|
||||
# option auth_user 'admin'
|
||||
# option auth_pass 'password'
|
||||
# option websocket '1'
|
||||
@ -1,145 +1,574 @@
|
||||
#!/bin/sh
|
||||
# RPCD backend for VHost Manager
|
||||
# Provides ubus interface: luci.vhost-manager
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
get_status() {
|
||||
json_init
|
||||
|
||||
local enabled
|
||||
config_load vhost
|
||||
config_get enabled global enabled "0"
|
||||
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
|
||||
# Check nginx
|
||||
local nginx_running=0
|
||||
pgrep -f nginx >/dev/null && nginx_running=1
|
||||
json_add_boolean "nginx_active" "$nginx_running"
|
||||
|
||||
# Check dnsmasq
|
||||
local dns_running=0
|
||||
pgrep -f dnsmasq >/dev/null && dns_running=1
|
||||
json_add_boolean "dns_active" "$dns_running"
|
||||
|
||||
# Count vhosts
|
||||
local internal=0 redirects=0
|
||||
config_foreach _count_internal internal
|
||||
config_foreach _count_redirect redirect
|
||||
|
||||
json_add_int "internal_hosts" "$internal"
|
||||
json_add_int "redirects" "$redirects"
|
||||
|
||||
json_dump
|
||||
NGINX_VHOST_DIR="/etc/nginx/conf.d"
|
||||
ACME_STATE_DIR="/etc/acme"
|
||||
VHOST_CONFIG="/etc/config/vhost_manager"
|
||||
|
||||
# Initialize directories
|
||||
init_dirs() {
|
||||
mkdir -p "$NGINX_VHOST_DIR"
|
||||
mkdir -p "$ACME_STATE_DIR"
|
||||
touch "$VHOST_CONFIG"
|
||||
}
|
||||
|
||||
_count_internal() { internal=$((internal + 1)); }
|
||||
_count_redirect() { redirects=$((redirects + 1)); }
|
||||
# Generate nginx vhost configuration
|
||||
generate_vhost_config() {
|
||||
local domain="$1"
|
||||
local backend="$2"
|
||||
local ssl="$3"
|
||||
local auth="$4"
|
||||
local websocket="$5"
|
||||
|
||||
local config_file="${NGINX_VHOST_DIR}/${domain}.conf"
|
||||
|
||||
cat > "$config_file" << NGINXEOF
|
||||
# VHost for ${domain}
|
||||
# Generated by LuCI VHost Manager
|
||||
|
||||
get_internal_hosts() {
|
||||
config_load vhost
|
||||
json_init
|
||||
json_add_array "hosts"
|
||||
|
||||
_add_host() {
|
||||
local enabled name domain backend ssl icon color desc
|
||||
config_get enabled "$1" enabled "0"
|
||||
config_get name "$1" name ""
|
||||
config_get domain "$1" domain ""
|
||||
config_get backend "$1" backend ""
|
||||
config_get ssl "$1" ssl "0"
|
||||
config_get icon "$1" icon "server"
|
||||
config_get color "$1" color "#64748b"
|
||||
config_get desc "$1" description ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "backend" "$backend"
|
||||
json_add_boolean "ssl" "$ssl"
|
||||
json_add_string "icon" "$icon"
|
||||
json_add_string "color" "$color"
|
||||
json_add_string "description" "$desc"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_host internal
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${domain};
|
||||
NGINXEOF
|
||||
|
||||
# Add SSL redirect if enabled
|
||||
if [ "$ssl" = "1" ]; then
|
||||
cat >> "$config_file" << NGINXEOF
|
||||
|
||||
# Redirect to HTTPS
|
||||
return 301 https://\$host\$request_uri;
|
||||
}
|
||||
|
||||
get_redirects() {
|
||||
config_load vhost
|
||||
json_init
|
||||
json_add_array "redirects"
|
||||
|
||||
_add_redirect() {
|
||||
local enabled name external local desc
|
||||
config_get enabled "$1" enabled "0"
|
||||
config_get name "$1" name ""
|
||||
config_get external "$1" external_domain ""
|
||||
config_get local "$1" local_domain ""
|
||||
config_get desc "$1" description ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "external_domain" "$external"
|
||||
json_add_string "local_domain" "$local"
|
||||
json_add_string "description" "$desc"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_redirect redirect
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name ${domain};
|
||||
|
||||
# SSL certificates
|
||||
ssl_certificate /etc/acme/${domain}/fullchain.cer;
|
||||
ssl_certificate_key /etc/acme/${domain}/${domain}.key;
|
||||
|
||||
# SSL settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
NGINXEOF
|
||||
fi
|
||||
|
||||
# Add authentication if enabled
|
||||
if [ "$auth" = "1" ]; then
|
||||
cat >> "$config_file" << NGINXEOF
|
||||
|
||||
# Basic authentication
|
||||
auth_basic "Restricted Access";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd_${domain};
|
||||
NGINXEOF
|
||||
fi
|
||||
|
||||
# Add proxy configuration
|
||||
cat >> "$config_file" << NGINXEOF
|
||||
|
||||
location / {
|
||||
proxy_pass ${backend};
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Proxy headers
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
NGINXEOF
|
||||
|
||||
# Add WebSocket support if enabled
|
||||
if [ "$websocket" = "1" ]; then
|
||||
cat >> "$config_file" << NGINXEOF
|
||||
|
||||
# WebSocket support
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
NGINXEOF
|
||||
fi
|
||||
|
||||
cat >> "$config_file" << NGINXEOF
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Access logs
|
||||
access_log /var/log/nginx/${domain}_access.log;
|
||||
error_log /var/log/nginx/${domain}_error.log;
|
||||
}
|
||||
NGINXEOF
|
||||
|
||||
echo "$config_file"
|
||||
}
|
||||
|
||||
get_certificates() {
|
||||
json_init
|
||||
json_add_array "certificates"
|
||||
|
||||
# List certificates from /etc/ssl/acme or similar
|
||||
for cert in /etc/ssl/acme/*.crt 2>/dev/null; do
|
||||
[ -f "$cert" ] || continue
|
||||
local domain=$(basename "$cert" .crt)
|
||||
local expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2)
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "expiry" "$expiry"
|
||||
json_add_string "path" "$cert"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
apply_config() {
|
||||
# Generate nginx configs
|
||||
# Generate dnsmasq entries
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Configuration applied"
|
||||
json_dump
|
||||
# Test backend connectivity
|
||||
test_backend() {
|
||||
local backend="$1"
|
||||
|
||||
# Extract host and port from backend URL
|
||||
local host=$(echo "$backend" | sed 's|https\?://||' | cut -d':' -f1 | cut -d'/' -f1)
|
||||
local port=$(echo "$backend" | sed 's|https\?://||' | cut -d':' -f2 | cut -d'/' -f1)
|
||||
|
||||
# Default port if not specified
|
||||
if [ "$port" = "$host" ]; then
|
||||
if echo "$backend" | grep -q "^https"; then
|
||||
port=443
|
||||
else
|
||||
port=80
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test connection
|
||||
if nc -z -w 5 "$host" "$port" 2>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"internal_hosts":{},"redirects":{},"certificates":{},"apply_config":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
internal_hosts) get_internal_hosts ;;
|
||||
redirects) get_redirects ;;
|
||||
certificates) get_certificates ;;
|
||||
apply_config) apply_config ;;
|
||||
*) echo '{"error":"Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
list)
|
||||
json_init
|
||||
json_add_object "status"
|
||||
json_close_object
|
||||
json_add_object "list_vhosts"
|
||||
json_close_object
|
||||
json_add_object "get_vhost"
|
||||
json_add_string "domain" "string"
|
||||
json_close_object
|
||||
json_add_object "add_vhost"
|
||||
json_add_string "domain" "string"
|
||||
json_add_string "backend" "string"
|
||||
json_add_string "ssl" "bool"
|
||||
json_add_string "auth" "bool"
|
||||
json_add_string "websocket" "bool"
|
||||
json_close_object
|
||||
json_add_object "update_vhost"
|
||||
json_add_string "domain" "string"
|
||||
json_add_string "backend" "string"
|
||||
json_add_string "ssl" "bool"
|
||||
json_add_string "auth" "bool"
|
||||
json_add_string "websocket" "bool"
|
||||
json_close_object
|
||||
json_add_object "delete_vhost"
|
||||
json_add_string "domain" "string"
|
||||
json_close_object
|
||||
json_add_object "test_backend"
|
||||
json_add_string "backend" "string"
|
||||
json_close_object
|
||||
json_add_object "request_cert"
|
||||
json_add_string "domain" "string"
|
||||
json_add_string "email" "string"
|
||||
json_close_object
|
||||
json_add_object "list_certs"
|
||||
json_close_object
|
||||
json_add_object "reload_nginx"
|
||||
json_close_object
|
||||
json_add_object "get_access_logs"
|
||||
json_add_string "domain" "string"
|
||||
json_add_string "lines" "int"
|
||||
json_close_object
|
||||
json_dump
|
||||
;;
|
||||
|
||||
call)
|
||||
case "$2" in
|
||||
status)
|
||||
init_dirs
|
||||
|
||||
json_init
|
||||
json_add_boolean "enabled" 1
|
||||
json_add_string "module" "vhost-manager"
|
||||
json_add_string "version" "1.0.0"
|
||||
|
||||
# Check nginx status
|
||||
if pgrep -x nginx > /dev/null 2>&1; then
|
||||
json_add_boolean "nginx_running" 1
|
||||
|
||||
# Get nginx version
|
||||
local nginx_version=$(nginx -v 2>&1 | grep -o 'nginx/[0-9.]*' | cut -d'/' -f2)
|
||||
json_add_string "nginx_version" "$nginx_version"
|
||||
else
|
||||
json_add_boolean "nginx_running" 0
|
||||
json_add_string "nginx_version" "unknown"
|
||||
fi
|
||||
|
||||
# Check acme.sh availability
|
||||
if command -v acme.sh > /dev/null 2>&1; then
|
||||
json_add_boolean "acme_available" 1
|
||||
local acme_version=$(acme.sh --version 2>/dev/null | head -1)
|
||||
json_add_string "acme_version" "$acme_version"
|
||||
else
|
||||
json_add_boolean "acme_available" 0
|
||||
json_add_string "acme_version" "not installed"
|
||||
fi
|
||||
|
||||
# Count vhosts
|
||||
local vhost_count=0
|
||||
if [ -d "$NGINX_VHOST_DIR" ]; then
|
||||
vhost_count=$(find "$NGINX_VHOST_DIR" -name "*.conf" -type f 2>/dev/null | wc -l)
|
||||
fi
|
||||
json_add_int "vhost_count" "$vhost_count"
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
list_vhosts)
|
||||
init_dirs
|
||||
|
||||
json_init
|
||||
json_add_array "vhosts"
|
||||
|
||||
if [ -d "$NGINX_VHOST_DIR" ]; then
|
||||
find "$NGINX_VHOST_DIR" -name "*.conf" -type f 2>/dev/null | while read -r conf_file; do
|
||||
local domain=$(basename "$conf_file" .conf)
|
||||
|
||||
# Parse config to extract info
|
||||
local ssl=0
|
||||
local auth=0
|
||||
local websocket=0
|
||||
local backend="unknown"
|
||||
|
||||
if grep -q "listen 443 ssl" "$conf_file"; then
|
||||
ssl=1
|
||||
fi
|
||||
|
||||
if grep -q "auth_basic" "$conf_file"; then
|
||||
auth=1
|
||||
fi
|
||||
|
||||
if grep -q "Upgrade.*http_upgrade" "$conf_file"; then
|
||||
websocket=1
|
||||
fi
|
||||
|
||||
backend=$(grep "proxy_pass" "$conf_file" | head -1 | sed 's/.*proxy_pass\s*\([^;]*\);.*/\1/')
|
||||
|
||||
# Check SSL cert expiry
|
||||
local ssl_expires="N/A"
|
||||
if [ "$ssl" = "1" ] && [ -f "/etc/acme/${domain}/fullchain.cer" ]; then
|
||||
ssl_expires=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -enddate 2>/dev/null | cut -d'=' -f2)
|
||||
fi
|
||||
|
||||
json_add_object
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "backend" "$backend"
|
||||
json_add_boolean "ssl" "$ssl"
|
||||
json_add_boolean "auth" "$auth"
|
||||
json_add_boolean "websocket" "$websocket"
|
||||
json_add_string "ssl_expires" "$ssl_expires"
|
||||
json_add_string "config_file" "$conf_file"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_vhost)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain
|
||||
|
||||
local config_file="${NGINX_VHOST_DIR}/${domain}.conf"
|
||||
|
||||
json_init
|
||||
json_add_string "domain" "$domain"
|
||||
|
||||
if [ -f "$config_file" ]; then
|
||||
json_add_boolean "exists" 1
|
||||
|
||||
# Parse configuration
|
||||
local ssl=0
|
||||
local auth=0
|
||||
local websocket=0
|
||||
local backend="unknown"
|
||||
|
||||
if grep -q "listen 443 ssl" "$config_file"; then
|
||||
ssl=1
|
||||
fi
|
||||
|
||||
if grep -q "auth_basic" "$config_file"; then
|
||||
auth=1
|
||||
fi
|
||||
|
||||
if grep -q "Upgrade.*http_upgrade" "$config_file"; then
|
||||
websocket=1
|
||||
fi
|
||||
|
||||
backend=$(grep "proxy_pass" "$config_file" | head -1 | sed 's/.*proxy_pass\s*\([^;]*\);.*/\1/')
|
||||
|
||||
json_add_string "backend" "$backend"
|
||||
json_add_boolean "ssl" "$ssl"
|
||||
json_add_boolean "auth" "$auth"
|
||||
json_add_boolean "websocket" "$websocket"
|
||||
json_add_string "config_file" "$config_file"
|
||||
|
||||
# SSL certificate info
|
||||
if [ "$ssl" = "1" ] && [ -f "/etc/acme/${domain}/fullchain.cer" ]; then
|
||||
local ssl_expires=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -enddate 2>/dev/null | cut -d'=' -f2)
|
||||
local ssl_issuer=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -issuer 2>/dev/null | cut -d'=' -f2-)
|
||||
|
||||
json_add_string "ssl_expires" "$ssl_expires"
|
||||
json_add_string "ssl_issuer" "$ssl_issuer"
|
||||
fi
|
||||
else
|
||||
json_add_boolean "exists" 0
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
add_vhost)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain
|
||||
json_get_var backend backend
|
||||
json_get_var ssl ssl
|
||||
json_get_var auth auth
|
||||
json_get_var websocket websocket
|
||||
|
||||
init_dirs
|
||||
|
||||
# Validate domain
|
||||
if [ -z "$domain" ] || [ -z "$backend" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Domain and backend are required"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if vhost already exists
|
||||
if [ -f "${NGINX_VHOST_DIR}/${domain}.conf" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "VHost already exists for domain: $domain"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate nginx config
|
||||
local config_file=$(generate_vhost_config "$domain" "$backend" "$ssl" "$auth" "$websocket")
|
||||
|
||||
# Test nginx config
|
||||
if nginx -t 2>&1 | grep -q "successful"; then
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "VHost created successfully"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "config_file" "$config_file"
|
||||
json_add_boolean "reload_required" 1
|
||||
json_dump
|
||||
else
|
||||
# Remove invalid config
|
||||
rm -f "$config_file"
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Invalid nginx configuration"
|
||||
json_dump
|
||||
fi
|
||||
;;
|
||||
|
||||
update_vhost)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain
|
||||
json_get_var backend backend
|
||||
json_get_var ssl ssl
|
||||
json_get_var auth auth
|
||||
json_get_var websocket websocket
|
||||
|
||||
# Check if vhost exists
|
||||
if [ ! -f "${NGINX_VHOST_DIR}/${domain}.conf" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "VHost not found: $domain"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Backup old config
|
||||
cp "${NGINX_VHOST_DIR}/${domain}.conf" "${NGINX_VHOST_DIR}/${domain}.conf.bak"
|
||||
|
||||
# Generate new config
|
||||
local config_file=$(generate_vhost_config "$domain" "$backend" "$ssl" "$auth" "$websocket")
|
||||
|
||||
# Test nginx config
|
||||
if nginx -t 2>&1 | grep -q "successful"; then
|
||||
rm -f "${NGINX_VHOST_DIR}/${domain}.conf.bak"
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "VHost updated successfully"
|
||||
json_add_boolean "reload_required" 1
|
||||
json_dump
|
||||
else
|
||||
# Restore backup
|
||||
mv "${NGINX_VHOST_DIR}/${domain}.conf.bak" "${NGINX_VHOST_DIR}/${domain}.conf"
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Invalid configuration, changes reverted"
|
||||
json_dump
|
||||
fi
|
||||
;;
|
||||
|
||||
delete_vhost)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain
|
||||
|
||||
local config_file="${NGINX_VHOST_DIR}/${domain}.conf"
|
||||
|
||||
if [ -f "$config_file" ]; then
|
||||
rm -f "$config_file"
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "VHost deleted: $domain"
|
||||
json_add_boolean "reload_required" 1
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "VHost not found: $domain"
|
||||
json_dump
|
||||
fi
|
||||
;;
|
||||
|
||||
test_backend)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var backend backend
|
||||
|
||||
json_init
|
||||
json_add_string "backend" "$backend"
|
||||
|
||||
if test_backend "$backend"; then
|
||||
json_add_boolean "reachable" 1
|
||||
json_add_string "status" "Backend is reachable"
|
||||
else
|
||||
json_add_boolean "reachable" 0
|
||||
json_add_string "status" "Backend is unreachable"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
request_cert)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain
|
||||
json_get_var email email
|
||||
|
||||
json_init
|
||||
|
||||
if ! command -v acme.sh > /dev/null 2>&1; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "acme.sh not installed"
|
||||
json_dump
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Request certificate using acme.sh
|
||||
if acme.sh --issue -d "$domain" --standalone --force 2>&1 | grep -q "success"; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Certificate requested successfully"
|
||||
json_add_string "domain" "$domain"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Certificate request failed"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
list_certs)
|
||||
json_init
|
||||
json_add_array "certificates"
|
||||
|
||||
if [ -d "$ACME_STATE_DIR" ]; then
|
||||
find "$ACME_STATE_DIR" -name "fullchain.cer" -type f 2>/dev/null | while read -r cert_file; do
|
||||
local domain=$(basename $(dirname "$cert_file"))
|
||||
local expires=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d'=' -f2)
|
||||
local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | cut -d'=' -f2-)
|
||||
local subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | cut -d'=' -f2-)
|
||||
|
||||
json_add_object
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "expires" "$expires"
|
||||
json_add_string "issuer" "$issuer"
|
||||
json_add_string "subject" "$subject"
|
||||
json_add_string "cert_file" "$cert_file"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
reload_nginx)
|
||||
json_init
|
||||
|
||||
# Test configuration first
|
||||
if nginx -t 2>&1 | grep -q "successful"; then
|
||||
# Reload nginx
|
||||
if /etc/init.d/nginx reload 2>&1; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Nginx reloaded successfully"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Failed to reload nginx"
|
||||
fi
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Invalid nginx configuration"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_access_logs)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain
|
||||
json_get_var lines lines
|
||||
|
||||
lines=${lines:-50}
|
||||
|
||||
local log_file="/var/log/nginx/${domain}_access.log"
|
||||
|
||||
json_init
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_array "logs"
|
||||
|
||||
if [ -f "$log_file" ]; then
|
||||
tail -n "$lines" "$log_file" | while read -r log_line; do
|
||||
json_add_string "" "$log_line"
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
*)
|
||||
json_init
|
||||
json_add_int "error" -32601
|
||||
json_add_string "message" "Method not found: $2"
|
||||
json_dump
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -1,27 +1,44 @@
|
||||
{
|
||||
"admin/services/vhost": {
|
||||
"title": "VHost Manager",
|
||||
"order": 75,
|
||||
"action": {"type": "firstchild"}
|
||||
},
|
||||
"admin/services/vhost/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {"type": "view", "path": "vhost-manager/overview"}
|
||||
},
|
||||
"admin/services/vhost/internal": {
|
||||
"title": "Internal Hosts",
|
||||
"order": 20,
|
||||
"action": {"type": "view", "path": "vhost-manager/internal"}
|
||||
},
|
||||
"admin/services/vhost/redirects": {
|
||||
"title": "External Redirects",
|
||||
"order": 30,
|
||||
"action": {"type": "view", "path": "vhost-manager/redirects"}
|
||||
},
|
||||
"admin/services/vhost/ssl": {
|
||||
"title": "SSL Certificates",
|
||||
"order": 40,
|
||||
"action": {"type": "view", "path": "vhost-manager/ssl"}
|
||||
}
|
||||
"admin/secubox/services/vhosts": {
|
||||
"title": "VHost Manager",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-vhost-manager"]
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/vhosts/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "vhost-manager/overview"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/vhosts/vhosts": {
|
||||
"title": "Virtual Hosts",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "vhost-manager/vhosts"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/vhosts/certificates": {
|
||||
"title": "SSL Certificates",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "vhost-manager/certificates"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/vhosts/logs": {
|
||||
"title": "Access Logs",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "vhost-manager/logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,39 @@
|
||||
{
|
||||
"luci-app-vhost-manager": {
|
||||
"description": "VHost Manager",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.vhost-manager": ["status", "internal_hosts", "redirects", "certificates"]
|
||||
},
|
||||
"uci": ["vhost"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.vhost-manager": ["apply_config"]
|
||||
},
|
||||
"uci": ["vhost"]
|
||||
}
|
||||
}
|
||||
"luci-app-vhost-manager": {
|
||||
"description": "Grant access to LuCI VHost Manager",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.vhost-manager": [
|
||||
"status",
|
||||
"list_vhosts",
|
||||
"get_vhost",
|
||||
"test_backend",
|
||||
"list_certs",
|
||||
"get_access_logs"
|
||||
]
|
||||
},
|
||||
"uci": ["vhost_manager", "nginx"],
|
||||
"file": {
|
||||
"/etc/nginx/conf.d/*": ["read"],
|
||||
"/etc/acme/*": ["read"],
|
||||
"/var/log/nginx/*": ["read"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.vhost-manager": [
|
||||
"add_vhost",
|
||||
"update_vhost",
|
||||
"delete_vhost",
|
||||
"request_cert",
|
||||
"reload_nginx"
|
||||
]
|
||||
},
|
||||
"uci": ["vhost_manager", "nginx"],
|
||||
"file": {
|
||||
"/etc/nginx/conf.d/*": ["write"],
|
||||
"/etc/nginx/.htpasswd_*": ["write"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user