luci-app-vhost-manager: migrate to vhosts config
This commit is contained in:
parent
40e937a919
commit
9cdbb21a99
@ -11,7 +11,7 @@
|
|||||||
"test_config"
|
"test_config"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": ["vhost_manager", "nginx"],
|
"uci": ["vhosts", "vhost_manager", "nginx"],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/nginx/conf.d/*": ["read"],
|
"/etc/nginx/conf.d/*": ["read"],
|
||||||
"/etc/acme/*": ["read"]
|
"/etc/acme/*": ["read"]
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"reload_nginx"
|
"reload_nginx"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": ["vhost_manager", "nginx"],
|
"uci": ["vhosts", "vhost_manager", "nginx"],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/nginx/conf.d/*": ["write"]
|
"/etc/nginx/conf.d/*": ["write"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-vhost-manager
|
PKG_NAME:=luci-app-vhost-manager
|
||||||
PKG_VERSION:=0.4.1
|
PKG_VERSION:=0.4.1
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
@ -14,6 +14,11 @@ LUCI_DESCRIPTION:=Nginx reverse proxy manager with Let's Encrypt SSL certificate
|
|||||||
LUCI_DEPENDS:=+luci-base +rpcd +nginx-ssl +acme +curl
|
LUCI_DEPENDS:=+luci-base +rpcd +nginx-ssl +acme +curl
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/conffiles
|
||||||
|
/etc/config/vhost_manager
|
||||||
|
/etc/config/vhosts
|
||||||
|
endef
|
||||||
|
|
||||||
|
|
||||||
# File permissions (CRITICAL: RPCD scripts MUST be executable 755)
|
# File permissions (CRITICAL: RPCD scripts MUST be executable 755)
|
||||||
# Format: path:owner:group:mode
|
# Format: path:owner:group:mode
|
||||||
@ -21,7 +26,8 @@ LUCI_PKGARCH:=all
|
|||||||
# - Helper scripts: 755 (if executable)
|
# - Helper scripts: 755 (if executable)
|
||||||
# - Config files: 644 (readable by all, writable by root)
|
# - Config files: 644 (readable by all, writable by root)
|
||||||
# - CSS/JS files: 644 (set automatically by luci.mk)
|
# - CSS/JS files: 644 (set automatically by luci.mk)
|
||||||
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.vhost-manager:root:root:755
|
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.vhost-manager:root:root:755 \
|
||||||
|
/etc/uci-defaults/50-luci-app-vhost-manager-migrate:root:root:755
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
|||||||
@ -49,26 +49,30 @@ opkg install luci-app-vhost-manager
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### UCI Configuration
|
### UCI Configuration (`/etc/config/vhosts`)
|
||||||
|
|
||||||
Edit `/etc/config/vhost_manager`:
|
Virtual hosts now live in `/etc/config/vhosts`, allowing other SecuBox components to declaratively install proxies. A default file is dropped during install; edit it like any other UCI config:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
config global 'global'
|
config global 'global'
|
||||||
option enabled '1'
|
option enabled '1'
|
||||||
option auto_reload '1'
|
option auto_reload '1'
|
||||||
option log_retention '30'
|
|
||||||
|
|
||||||
config vhost 'myapp'
|
config vhost 'myapp'
|
||||||
option domain 'app.example.com'
|
option domain 'app.example.com'
|
||||||
option backend 'http://192.168.1.100:8080'
|
option upstream 'http://127.0.0.1:8080'
|
||||||
option ssl '1'
|
option tls 'acme' # off|acme|manual
|
||||||
|
option cert_path '/etc/custom/fullchain.pem' # used when tls=manual
|
||||||
|
option key_path '/etc/custom/privkey.pem'
|
||||||
option auth '1'
|
option auth '1'
|
||||||
option auth_user 'admin'
|
option auth_user 'admin'
|
||||||
option auth_pass 'secretpassword'
|
option auth_pass 'secretpassword'
|
||||||
option websocket '1'
|
option websocket '1'
|
||||||
|
option enabled '1'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Legacy installations may still ship `/etc/config/vhost_manager` for backwards compatibility, but the RPC backend now generates `/etc/nginx/conf.d/*.conf` exclusively from `/etc/config/vhosts`.
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
#### Global Section
|
#### Global Section
|
||||||
@ -78,12 +82,13 @@ config vhost 'myapp'
|
|||||||
|
|
||||||
#### VHost Section
|
#### VHost Section
|
||||||
- `domain`: Domain name for this virtual host (required)
|
- `domain`: Domain name for this virtual host (required)
|
||||||
- `backend`: Backend URL to proxy to (required, e.g., http://192.168.1.100:8080)
|
- `upstream`: Backend URL to proxy to (required, e.g., http://192.168.1.100:8080)
|
||||||
- `ssl`: Enable HTTPS (default: 0, requires valid SSL certificate)
|
- `tls`: TLS strategy (`off`, `acme`, or `manual`)
|
||||||
|
- `cert_path` / `key_path`: Required when `tls=manual` to point to PEM files
|
||||||
- `auth`: Enable HTTP Basic Authentication (default: 0)
|
- `auth`: Enable HTTP Basic Authentication (default: 0)
|
||||||
- `auth_user`: Username for authentication (required if auth=1)
|
- `auth_user` / `auth_pass`: Credentials used when `auth=1`
|
||||||
- `auth_pass`: Password for authentication (required if auth=1)
|
- `websocket`: Enable WebSocket headers (default: 0)
|
||||||
- `websocket`: Enable WebSocket support (default: 0)
|
- `enabled`: Disable the vhost without deleting it (default: 1)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -135,9 +140,12 @@ ubus call luci.vhost-manager status
|
|||||||
ubus call luci.vhost-manager add_vhost '{
|
ubus call luci.vhost-manager add_vhost '{
|
||||||
"domain": "app.example.com",
|
"domain": "app.example.com",
|
||||||
"backend": "http://192.168.1.100:8080",
|
"backend": "http://192.168.1.100:8080",
|
||||||
"ssl": true,
|
"tls_mode": "acme",
|
||||||
"auth": false,
|
"auth": true,
|
||||||
"websocket": true
|
"auth_user": "admin",
|
||||||
|
"auth_pass": "secret",
|
||||||
|
"websocket": true,
|
||||||
|
"enabled": true
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -181,7 +189,7 @@ ubus call luci.vhost-manager get_access_logs '{
|
|||||||
|
|
||||||
## Nginx Configuration
|
## Nginx Configuration
|
||||||
|
|
||||||
VHost Manager generates nginx configuration files in `/etc/nginx/conf.d/vhosts/`.
|
VHost Manager generates nginx configuration files in `/etc/nginx/conf.d/`.
|
||||||
|
|
||||||
### Example Generated Configuration (HTTP Only)
|
### Example Generated Configuration (HTTP Only)
|
||||||
|
|
||||||
@ -307,10 +315,16 @@ List all configured virtual hosts.
|
|||||||
{
|
{
|
||||||
"domain": "app.example.com",
|
"domain": "app.example.com",
|
||||||
"backend": "http://192.168.1.100:8080",
|
"backend": "http://192.168.1.100:8080",
|
||||||
|
"upstream": "http://192.168.1.100:8080",
|
||||||
|
"tls_mode": "acme",
|
||||||
"ssl": true,
|
"ssl": true,
|
||||||
"ssl_expires": "2025-03-15",
|
"cert_file": "/etc/acme/app.example.com/fullchain.cer",
|
||||||
"auth": false,
|
"cert_expires": "2025-03-15",
|
||||||
"websocket": true
|
"auth": true,
|
||||||
|
"auth_user": "admin",
|
||||||
|
"websocket": true,
|
||||||
|
"enabled": true,
|
||||||
|
"config_file": "/etc/nginx/conf.d/app.example.com.conf"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -328,24 +342,30 @@ Get details for a specific virtual host.
|
|||||||
{
|
{
|
||||||
"domain": "app.example.com",
|
"domain": "app.example.com",
|
||||||
"backend": "http://192.168.1.100:8080",
|
"backend": "http://192.168.1.100:8080",
|
||||||
|
"tls_mode": "acme",
|
||||||
"ssl": true,
|
"ssl": true,
|
||||||
"ssl_expires": "2025-03-15",
|
"cert_expires": "2025-03-15",
|
||||||
|
"cert_issuer": "R3",
|
||||||
"auth": true,
|
"auth": true,
|
||||||
"auth_user": "admin",
|
"auth_user": "admin",
|
||||||
"websocket": true
|
"websocket": true,
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### add_vhost(domain, backend, ssl, auth, websocket)
|
### add_vhost(payload)
|
||||||
|
|
||||||
Add a new virtual host.
|
Add a new virtual host.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `domain`: Domain name (required)
|
- `domain`: Domain name (required)
|
||||||
- `backend`: Backend URL (required)
|
- `backend`: Backend URL (required)
|
||||||
- `ssl`: Enable SSL (boolean)
|
- `tls_mode`: `off`, `acme`, or `manual` (required)
|
||||||
- `auth`: Enable authentication (boolean)
|
- `auth`: Enable authentication (boolean)
|
||||||
|
- `auth_user` / `auth_pass`: Credentials when auth is enabled
|
||||||
- `websocket`: Enable WebSocket (boolean)
|
- `websocket`: Enable WebSocket (boolean)
|
||||||
|
- `enabled`: Disable the vhost without deleting (boolean)
|
||||||
|
- `cert_path` / `key_path`: Required when `tls_mode=manual`
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
```json
|
```json
|
||||||
@ -355,11 +375,11 @@ Add a new virtual host.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### update_vhost(domain, backend, ssl, auth, websocket)
|
### update_vhost(payload)
|
||||||
|
|
||||||
Update an existing virtual host.
|
Update an existing virtual host.
|
||||||
|
|
||||||
**Parameters:** Same as add_vhost
|
**Parameters:** Same as `add_vhost`. Omitted fields retain their previous value.
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
```json
|
```json
|
||||||
@ -394,8 +414,9 @@ Test connectivity to a backend server.
|
|||||||
**Returns:**
|
**Returns:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"backend": "http://192.168.1.100:8080",
|
||||||
"reachable": true,
|
"reachable": true,
|
||||||
"response_time": 45
|
"status": "Backend is reachable"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -411,7 +432,7 @@ Request a Let's Encrypt SSL certificate.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Certificate obtained successfully"
|
"message": "Certificate requested"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -456,6 +477,7 @@ Get nginx access logs for a domain.
|
|||||||
**Returns:**
|
**Returns:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"domain": "app.example.com",
|
||||||
"logs": [
|
"logs": [
|
||||||
"192.168.1.50 - - [24/Dec/2025:10:30:15 +0000] \"GET / HTTP/1.1\" 200 1234",
|
"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"
|
"192.168.1.51 - - [24/Dec/2025:10:30:16 +0000] \"GET /api HTTP/1.1\" 200 5678"
|
||||||
@ -513,18 +535,24 @@ Ensure:
|
|||||||
|
|
||||||
Check htpasswd file exists:
|
Check htpasswd file exists:
|
||||||
```bash
|
```bash
|
||||||
ls -l /etc/nginx/htpasswd/{domain}
|
ls -l /etc/nginx/.luci-app-vhost-manager_{domain}
|
||||||
```
|
```
|
||||||
|
|
||||||
Regenerate htpasswd file:
|
Regenerate htpasswd file:
|
||||||
```bash
|
```bash
|
||||||
# Via UCI
|
# Update UCI entry
|
||||||
uci set vhost_manager.myapp.auth_user='newuser'
|
uci set vhosts.myapp.auth='1'
|
||||||
uci set vhost_manager.myapp.auth_pass='newpass'
|
uci set vhosts.myapp.auth_user='newuser'
|
||||||
uci commit vhost_manager
|
uci set vhosts.myapp.auth_pass='newpass'
|
||||||
|
uci commit vhosts
|
||||||
|
|
||||||
# Trigger config regeneration
|
# Trigger config regeneration
|
||||||
ubus call luci.vhost-manager update_vhost '{...}'
|
ubus call luci.vhost-manager update_vhost '{
|
||||||
|
"domain": "myapp.example.com",
|
||||||
|
"auth": true,
|
||||||
|
"auth_user": "newuser",
|
||||||
|
"auth_pass": "newpass"
|
||||||
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|||||||
@ -24,14 +24,14 @@ var callGetVHost = rpc.declare({
|
|||||||
var callAddVHost = rpc.declare({
|
var callAddVHost = rpc.declare({
|
||||||
object: 'luci.vhost-manager',
|
object: 'luci.vhost-manager',
|
||||||
method: 'add_vhost',
|
method: 'add_vhost',
|
||||||
params: ['domain', 'backend', 'ssl', 'auth', 'websocket'],
|
params: ['domain', 'backend', 'tls_mode', 'auth', 'auth_user', 'auth_pass', 'websocket', 'enabled', 'cert_path', 'key_path'],
|
||||||
expect: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
var callUpdateVHost = rpc.declare({
|
var callUpdateVHost = rpc.declare({
|
||||||
object: 'luci.vhost-manager',
|
object: 'luci.vhost-manager',
|
||||||
method: 'update_vhost',
|
method: 'update_vhost',
|
||||||
params: ['domain', 'backend', 'ssl', 'auth', 'websocket'],
|
params: ['domain', 'backend', 'tls_mode', 'auth', 'auth_user', 'auth_pass', 'websocket', 'enabled', 'cert_path', 'key_path'],
|
||||||
expect: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,22 @@ function formatDate(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEnabled(vhost) {
|
||||||
|
return !vhost || vhost.enabled !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTlsMode(vhost) {
|
||||||
|
var mode = (vhost && vhost.tls_mode) || (vhost && vhost.ssl ? 'acme' : 'off');
|
||||||
|
switch (mode) {
|
||||||
|
case 'acme':
|
||||||
|
return _('ACME (auto)');
|
||||||
|
case 'manual':
|
||||||
|
return _('Manual cert');
|
||||||
|
default:
|
||||||
|
return _('Disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return L.view.extend({
|
return L.view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@ -58,7 +74,7 @@ return L.view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
buildForm: function() {
|
buildForm: function() {
|
||||||
var m = new form.Map('vhost_manager', null, null);
|
var m = new form.Map('vhosts', null, null);
|
||||||
var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts'));
|
var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts'));
|
||||||
s.anonymous = false;
|
s.anonymous = false;
|
||||||
s.addremove = true;
|
s.addremove = true;
|
||||||
@ -105,9 +121,20 @@ return L.view.extend({
|
|||||||
return widget;
|
return widget;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.option(form.Flag, 'ssl', _('Enable SSL'));
|
o = s.option(form.ListValue, 'tls_mode', _('TLS Mode'));
|
||||||
o.default = o.disabled;
|
o.value('off', _('Disabled (HTTP only)'));
|
||||||
o.description = _('Serve HTTPS (requires certificate).');
|
o.value('acme', _('Automatic (acme.sh)'));
|
||||||
|
o.value('manual', _('Manual certificate'));
|
||||||
|
o.default = 'acme';
|
||||||
|
o.description = _('Select how nginx obtains TLS certificates.');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'cert_path', _('Certificate Path'));
|
||||||
|
o.placeholder = '/etc/custom/fullchain.pem';
|
||||||
|
o.depends('tls_mode', 'manual');
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'key_path', _('Private Key Path'));
|
||||||
|
o.placeholder = '/etc/custom/privkey.pem';
|
||||||
|
o.depends('tls_mode', 'manual');
|
||||||
|
|
||||||
o = s.option(form.Flag, 'auth', _('Enable Authentication'));
|
o = s.option(form.Flag, 'auth', _('Enable Authentication'));
|
||||||
o.default = o.disabled;
|
o.default = o.disabled;
|
||||||
@ -124,19 +151,49 @@ return L.view.extend({
|
|||||||
o.default = o.disabled;
|
o.default = o.disabled;
|
||||||
o.description = _('Forward upgrade headers for WS backends.');
|
o.description = _('Forward upgrade headers for WS backends.');
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', _('Enable Virtual Host'));
|
||||||
|
o.default = '1';
|
||||||
|
o.description = _('Toggle to disable without deleting configuration.');
|
||||||
|
|
||||||
s.addModalOptions = function(s, section_id) {
|
s.addModalOptions = function(s, section_id) {
|
||||||
var domain = this.section.formvalue(section_id, 'domain');
|
var domain = this.section.formvalue(section_id, 'domain');
|
||||||
var backend = this.section.formvalue(section_id, 'backend');
|
var backend = this.section.formvalue(section_id, 'backend');
|
||||||
var ssl = this.section.formvalue(section_id, 'ssl') === '1';
|
var tlsMode = this.section.formvalue(section_id, 'tls_mode') || 'off';
|
||||||
var auth = this.section.formvalue(section_id, 'auth') === '1';
|
var auth = this.section.formvalue(section_id, 'auth') === '1';
|
||||||
var websocket = this.section.formvalue(section_id, 'websocket') === '1';
|
var websocket = this.section.formvalue(section_id, 'websocket') === '1';
|
||||||
|
var enabled = this.section.formvalue(section_id, 'enabled') !== '0';
|
||||||
|
var certPath = this.section.formvalue(section_id, 'cert_path') || '';
|
||||||
|
var keyPath = this.section.formvalue(section_id, 'key_path') || '';
|
||||||
|
var authUser = this.section.formvalue(section_id, 'auth_user') || '';
|
||||||
|
var authPass = this.section.formvalue(section_id, 'auth_pass') || '';
|
||||||
|
|
||||||
if (!domain || !backend) {
|
if (!domain || !backend) {
|
||||||
ui.addNotification(null, E('p', _('Domain and backend are required')), 'error');
|
ui.addNotification(null, E('p', _('Domain and backend are required')), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
API.addVHost(domain, backend, ssl, auth, websocket).then(function(result) {
|
if (auth && (!authUser || !authPass)) {
|
||||||
|
ui.addNotification(null, E('p', _('Username and password required for authentication')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tlsMode === 'manual' && (!certPath || !keyPath)) {
|
||||||
|
ui.addNotification(null, E('p', _('Manual TLS requires certificate and key paths')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
API.addVHost(
|
||||||
|
domain,
|
||||||
|
backend,
|
||||||
|
tlsMode,
|
||||||
|
auth,
|
||||||
|
auth ? authUser : null,
|
||||||
|
auth ? authPass : null,
|
||||||
|
websocket,
|
||||||
|
enabled,
|
||||||
|
tlsMode === 'manual' ? certPath : null,
|
||||||
|
tlsMode === 'manual' ? keyPath : null
|
||||||
|
).then(function(result) {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
ui.addNotification(null, E('p', _('VHost created successfully')), 'info');
|
ui.addNotification(null, E('p', _('VHost created successfully')), 'info');
|
||||||
|
|
||||||
@ -174,9 +231,10 @@ return L.view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderHeader: function(vhosts) {
|
renderHeader: function(vhosts) {
|
||||||
var sslEnabled = vhosts.filter(function(v) { return v.ssl; }).length;
|
var active = vhosts.filter(isEnabled);
|
||||||
var authEnabled = vhosts.filter(function(v) { return v.auth; }).length;
|
var sslEnabled = active.filter(function(v) { return v.ssl; }).length;
|
||||||
var websocketEnabled = vhosts.filter(function(v) { return v.websocket; }).length;
|
var authEnabled = active.filter(function(v) { return v.auth; }).length;
|
||||||
|
var websocketEnabled = active.filter(function(v) { return v.websocket; }).length;
|
||||||
|
|
||||||
return E('div', { 'class': 'sh-page-header' }, [
|
return E('div', { 'class': 'sh-page-header' }, [
|
||||||
E('div', {}, [
|
E('div', {}, [
|
||||||
@ -189,6 +247,7 @@ return L.view.extend({
|
|||||||
]),
|
]),
|
||||||
E('div', { 'class': 'sh-stats-grid' }, [
|
E('div', { 'class': 'sh-stats-grid' }, [
|
||||||
this.renderStatBadge(vhosts.length, _('Defined')),
|
this.renderStatBadge(vhosts.length, _('Defined')),
|
||||||
|
this.renderStatBadge(active.length, _('Enabled')),
|
||||||
this.renderStatBadge(sslEnabled, _('TLS')),
|
this.renderStatBadge(sslEnabled, _('TLS')),
|
||||||
this.renderStatBadge(authEnabled, _('Auth')),
|
this.renderStatBadge(authEnabled, _('Auth')),
|
||||||
this.renderStatBadge(websocketEnabled, _('WebSocket'))
|
this.renderStatBadge(websocketEnabled, _('WebSocket'))
|
||||||
@ -225,14 +284,22 @@ return L.view.extend({
|
|||||||
|
|
||||||
renderVhostCard: function(vhost, cert) {
|
renderVhostCard: function(vhost, cert) {
|
||||||
var pills = [];
|
var pills = [];
|
||||||
if (vhost.ssl) pills.push(E('span', { 'class': 'vhost-pill success' }, _('SSL')));
|
if (!isEnabled(vhost)) {
|
||||||
|
pills.push(E('span', { 'class': 'vhost-pill danger' }, _('Disabled')));
|
||||||
|
} else if (vhost.ssl) {
|
||||||
|
pills.push(E('span', { 'class': 'vhost-pill success' }, _('TLS')));
|
||||||
|
}
|
||||||
if (vhost.auth) pills.push(E('span', { 'class': 'vhost-pill warn' }, _('Auth')));
|
if (vhost.auth) pills.push(E('span', { 'class': 'vhost-pill warn' }, _('Auth')));
|
||||||
if (vhost.websocket) pills.push(E('span', { 'class': 'vhost-pill' }, _('WebSocket')));
|
if (vhost.websocket) pills.push(E('span', { 'class': 'vhost-pill' }, _('WebSocket')));
|
||||||
|
if (vhost.tls_mode === 'manual') {
|
||||||
|
pills.push(E('span', { 'class': 'vhost-pill' }, _('Manual cert')));
|
||||||
|
}
|
||||||
|
|
||||||
return E('div', { 'class': 'vhost-card' }, [
|
return E('div', { 'class': 'vhost-card' }, [
|
||||||
E('div', { 'class': 'vhost-card-title' }, ['🌐', vhost.domain || _('Unnamed')]),
|
E('div', { 'class': 'vhost-card-title' }, ['🌐', vhost.domain || _('Unnamed')]),
|
||||||
E('div', { 'class': 'vhost-card-meta' }, vhost.backend || _('No backend defined')),
|
E('div', { 'class': 'vhost-card-meta' }, vhost.backend || _('No backend defined')),
|
||||||
pills.length ? E('div', { 'class': 'vhost-filter-tags' }, pills) : '',
|
pills.length ? E('div', { 'class': 'vhost-filter-tags' }, pills) : '',
|
||||||
|
E('div', { 'class': 'vhost-card-meta' }, _('TLS Mode: %s').format(formatTlsMode(vhost))),
|
||||||
E('div', { 'class': 'vhost-card-meta' },
|
E('div', { 'class': 'vhost-card-meta' },
|
||||||
cert ? _('Certificate expires %s').format(formatDate(cert.expires)) : _('No certificate detected'))
|
cert ? _('Certificate expires %s').format(formatDate(cert.expires)) : _('No certificate detected'))
|
||||||
]);
|
]);
|
||||||
|
|||||||
17
luci-app-vhost-manager/root/etc/config/vhosts
Normal file
17
luci-app-vhost-manager/root/etc/config/vhosts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
config global 'global'
|
||||||
|
option enabled '1'
|
||||||
|
option auto_reload '1'
|
||||||
|
|
||||||
|
# Example VHost entry
|
||||||
|
# config vhost 'zigbeeui'
|
||||||
|
# option domain 'zigbee.example.com'
|
||||||
|
# option upstream 'http://127.0.0.1:8080'
|
||||||
|
# option tls 'acme' # off|acme|manual
|
||||||
|
# option cert_path '/etc/custom/fullchain.pem' # used when tls=manual
|
||||||
|
# option key_path '/etc/custom/privkey.pem'
|
||||||
|
# option auth '0'
|
||||||
|
# option auth_user 'admin'
|
||||||
|
# option auth_pass 'secret'
|
||||||
|
# option websocket '1'
|
||||||
|
# option enabled '1'
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
|
||||||
|
LEGACY_CFG="/etc/config/vhost_manager"
|
||||||
|
TARGET_CFG="/etc/config/vhosts"
|
||||||
|
|
||||||
|
[ -f "$LEGACY_CFG" ] || exit 0
|
||||||
|
|
||||||
|
# If new config already contains vhost entries, assume migration already done.
|
||||||
|
if uci -q show vhosts 2>/dev/null | grep -q '=vhost'; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure target config exists
|
||||||
|
if [ ! -f "$TARGET_CFG" ]; then
|
||||||
|
cat <<'CFG' > "$TARGET_CFG"
|
||||||
|
config global 'global'
|
||||||
|
option enabled '1'
|
||||||
|
option auto_reload '1'
|
||||||
|
CFG
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_global_section() {
|
||||||
|
if ! uci -q get vhosts.global >/dev/null; then
|
||||||
|
local g
|
||||||
|
g=$(uci add vhosts global)
|
||||||
|
uci rename vhosts.$g='global'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_bool() {
|
||||||
|
case "$1" in
|
||||||
|
1|true|on|yes|enabled) echo "1" ;;
|
||||||
|
*) echo "0" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_global() {
|
||||||
|
local section="$1"
|
||||||
|
local enabled auto_reload log_retention
|
||||||
|
config_get enabled "$section" enabled
|
||||||
|
config_get auto_reload "$section" auto_reload
|
||||||
|
config_get log_retention "$section" log_retention
|
||||||
|
|
||||||
|
ensure_global_section
|
||||||
|
[ -n "$enabled" ] && uci set vhosts.global.enabled="$enabled"
|
||||||
|
[ -n "$auto_reload" ] && uci set vhosts.global.auto_reload="$auto_reload"
|
||||||
|
[ -n "$log_retention" ] && uci set vhosts.global.log_retention="$log_retention"
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_vhost() {
|
||||||
|
local section="$1"
|
||||||
|
local domain backend ssl auth auth_user auth_pass websocket
|
||||||
|
config_get domain "$section" domain
|
||||||
|
config_get backend "$section" backend
|
||||||
|
config_get ssl "$section" ssl
|
||||||
|
config_get auth "$section" auth
|
||||||
|
config_get auth_user "$section" auth_user
|
||||||
|
config_get auth_pass "$section" auth_pass
|
||||||
|
config_get websocket "$section" websocket
|
||||||
|
|
||||||
|
[ -n "$domain" ] || return
|
||||||
|
[ -n "$backend" ] || return
|
||||||
|
|
||||||
|
local tls_mode="off"
|
||||||
|
if [ "$(normalize_bool "$ssl")" = "1" ]; then
|
||||||
|
tls_mode="acme"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local s
|
||||||
|
s=$(uci add vhosts vhost)
|
||||||
|
uci set vhosts.$s.domain="$domain"
|
||||||
|
uci set vhosts.$s.upstream="$backend"
|
||||||
|
uci set vhosts.$s.tls="$tls_mode"
|
||||||
|
|
||||||
|
if [ "$(normalize_bool "$auth")" = "1" ]; then
|
||||||
|
uci set vhosts.$s.auth="1"
|
||||||
|
[ -n "$auth_user" ] && uci set vhosts.$s.auth_user="$auth_user"
|
||||||
|
[ -n "$auth_pass" ] && uci set vhosts.$s.auth_pass="$auth_pass"
|
||||||
|
else
|
||||||
|
uci set vhosts.$s.auth="0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci set vhosts.$s.websocket="$(normalize_bool "$websocket")"
|
||||||
|
uci set vhosts.$s.enabled="1"
|
||||||
|
}
|
||||||
|
|
||||||
|
config_load vhost_manager
|
||||||
|
config_foreach migrate_global global
|
||||||
|
config_foreach migrate_vhost vhost
|
||||||
|
uci commit vhosts
|
||||||
|
|
||||||
|
mv "$LEGACY_CFG" "${LEGACY_CFG}.legacy" 2>/dev/null
|
||||||
|
|
||||||
|
exit 0
|
||||||
@ -1,6 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# RPCD backend for VHost Manager
|
# RPCD backend for VHost Manager (UCI-driven)
|
||||||
# Provides ubus interface: luci.vhost-manager
|
|
||||||
|
|
||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
. /usr/share/libubox/jshn.sh
|
. /usr/share/libubox/jshn.sh
|
||||||
@ -18,24 +17,226 @@ PKG_VERSION="$(get_pkg_version)"
|
|||||||
|
|
||||||
NGINX_VHOST_DIR="/etc/nginx/conf.d"
|
NGINX_VHOST_DIR="/etc/nginx/conf.d"
|
||||||
ACME_STATE_DIR="/etc/acme"
|
ACME_STATE_DIR="/etc/acme"
|
||||||
VHOST_CONFIG="/etc/config/vhost_manager"
|
VHOSTS_CONFIG="/etc/config/vhosts"
|
||||||
|
|
||||||
|
TLS_CERT_PATH=""
|
||||||
|
TLS_KEY_PATH=""
|
||||||
|
TLS_ACTIVE=0
|
||||||
|
CERT_EXPIRES=""
|
||||||
|
CERT_ISSUER=""
|
||||||
|
CERT_SUBJECT=""
|
||||||
|
|
||||||
# Initialize directories
|
|
||||||
init_dirs() {
|
init_dirs() {
|
||||||
mkdir -p "$NGINX_VHOST_DIR"
|
mkdir -p "$NGINX_VHOST_DIR" "$ACME_STATE_DIR"
|
||||||
mkdir -p "$ACME_STATE_DIR"
|
[ -f "$VHOSTS_CONFIG" ] || cat <<'CFG' > "$VHOSTS_CONFIG"
|
||||||
touch "$VHOST_CONFIG"
|
config global 'global'
|
||||||
|
option enabled '1'
|
||||||
|
option auto_reload '1'
|
||||||
|
CFG
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_bool() {
|
||||||
|
local value="$1"
|
||||||
|
case "$value" in
|
||||||
|
1|true|on|yes|enabled) echo 1 ;;
|
||||||
|
*) echo 0 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_tls_mode() {
|
||||||
|
local tls_mode="$1"
|
||||||
|
local legacy_ssl="$2"
|
||||||
|
if [ -n "$tls_mode" ]; then
|
||||||
|
echo "$tls_mode"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
legacy_ssl=$(normalize_bool "$legacy_ssl")
|
||||||
|
if [ "$legacy_ssl" = "1" ]; then
|
||||||
|
echo "acme"
|
||||||
|
else
|
||||||
|
echo "off"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
find_section() {
|
||||||
|
__target="$1"
|
||||||
|
__found_section=""
|
||||||
|
config_load vhosts
|
||||||
|
config_foreach __match_section vhost
|
||||||
|
echo "$__found_section"
|
||||||
|
}
|
||||||
|
|
||||||
|
__match_section() {
|
||||||
|
local section="$1"
|
||||||
|
config_get domain "$section" domain
|
||||||
|
if [ "$domain" = "$__target" ]; then
|
||||||
|
__found_section="$section"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_htpasswd() {
|
||||||
|
local domain="$1"
|
||||||
|
local user="$2"
|
||||||
|
local pass="$3"
|
||||||
|
local file="/etc/nginx/.luci-app-vhost-manager_${domain}"
|
||||||
|
mkdir -p /etc/nginx
|
||||||
|
local hash
|
||||||
|
hash=$(openssl passwd -apr1 "$pass")
|
||||||
|
printf '%s:%s\n' "$user" "$hash" > "$file"
|
||||||
|
chmod 600 "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_htpasswd() {
|
||||||
|
local domain="$1"
|
||||||
|
rm -f "/etc/nginx/.luci-app-vhost-manager_${domain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize_section_name() {
|
||||||
|
local domain="$1"
|
||||||
|
local safe
|
||||||
|
safe=$(echo "$domain" | tr 'A-Z' 'a-z' | tr -cd 'a-z0-9_')
|
||||||
|
[ -z "$safe" ] && safe="vh$(date +%s)"
|
||||||
|
echo "vh_${safe}"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_tls_context() {
|
||||||
|
local domain="$1"
|
||||||
|
local tls_mode="$2"
|
||||||
|
local cert_path="$3"
|
||||||
|
local key_path="$4"
|
||||||
|
|
||||||
|
TLS_ACTIVE=0
|
||||||
|
TLS_CERT_PATH=""
|
||||||
|
TLS_KEY_PATH=""
|
||||||
|
|
||||||
|
case "$tls_mode" in
|
||||||
|
acme)
|
||||||
|
TLS_CERT_PATH="/etc/acme/${domain}/fullchain.cer"
|
||||||
|
TLS_KEY_PATH="/etc/acme/${domain}/${domain}.key"
|
||||||
|
;;
|
||||||
|
manual)
|
||||||
|
TLS_CERT_PATH="$cert_path"
|
||||||
|
TLS_KEY_PATH="$key_path"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
TLS_CERT_PATH=""
|
||||||
|
TLS_KEY_PATH=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "$TLS_CERT_PATH" ] && [ -f "$TLS_CERT_PATH" ] && \
|
||||||
|
[ -n "$TLS_KEY_PATH" ] && [ -f "$TLS_KEY_PATH" ]; then
|
||||||
|
TLS_ACTIVE=1
|
||||||
|
else
|
||||||
|
TLS_CERT_PATH=""
|
||||||
|
TLS_KEY_PATH=""
|
||||||
|
TLS_ACTIVE=0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
read_cert_metadata() {
|
||||||
|
local file="$1"
|
||||||
|
CERT_EXPIRES=""
|
||||||
|
CERT_ISSUER=""
|
||||||
|
CERT_SUBJECT=""
|
||||||
|
[ -f "$file" ] || return 1
|
||||||
|
CERT_EXPIRES="$(openssl x509 -in "$file" -noout -enddate 2>/dev/null | cut -d'=' -f2)"
|
||||||
|
CERT_ISSUER="$(openssl x509 -in "$file" -noout -issuer 2>/dev/null | cut -d'=' -f2-)"
|
||||||
|
CERT_SUBJECT="$(openssl x509 -in "$file" -noout -subject 2>/dev/null | cut -d'=' -f2-)"
|
||||||
|
}
|
||||||
|
|
||||||
|
_count_vhost() {
|
||||||
|
local section="$1"
|
||||||
|
config_get_bool enabled "$section" enabled 1
|
||||||
|
[ "$enabled" = "1" ] || return
|
||||||
|
count=$((count + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
append_vhost_json() {
|
||||||
|
local section="$1"
|
||||||
|
config_get domain "$section" domain
|
||||||
|
[ -n "$domain" ] || return
|
||||||
|
|
||||||
|
config_get upstream "$section" upstream
|
||||||
|
config_get tls "$section" tls
|
||||||
|
config_get cert_path "$section" cert_path
|
||||||
|
config_get key_path "$section" key_path
|
||||||
|
config_get_bool auth "$section" auth 0
|
||||||
|
config_get auth_user "$section" auth_user
|
||||||
|
config_get_bool websocket "$section" websocket 0
|
||||||
|
config_get_bool enabled "$section" enabled 1
|
||||||
|
|
||||||
|
set_tls_context "$domain" "${tls:-off}" "$cert_path" "$key_path"
|
||||||
|
[ "$TLS_ACTIVE" = "1" ] && read_cert_metadata "$TLS_CERT_PATH"
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "section" "$section"
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
|
json_add_string "backend" "$upstream"
|
||||||
|
json_add_string "upstream" "$upstream"
|
||||||
|
json_add_string "tls_mode" "${tls:-off}"
|
||||||
|
json_add_boolean "ssl" "$TLS_ACTIVE"
|
||||||
|
json_add_boolean "auth" "$auth"
|
||||||
|
json_add_string "auth_user" "${auth_user:-}"
|
||||||
|
json_add_boolean "websocket" "$websocket"
|
||||||
|
json_add_boolean "enabled" "$enabled"
|
||||||
|
json_add_string "config_file" "$NGINX_VHOST_DIR/${domain}.conf"
|
||||||
|
|
||||||
|
if [ "$TLS_ACTIVE" = "1" ]; then
|
||||||
|
json_add_string "cert_file" "$TLS_CERT_PATH"
|
||||||
|
[ -n "$CERT_EXPIRES" ] && json_add_string "cert_expires" "$CERT_EXPIRES"
|
||||||
|
[ -n "$CERT_ISSUER" ] && json_add_string "cert_issuer" "$CERT_ISSUER"
|
||||||
|
[ -n "$CERT_SUBJECT" ] && json_add_string "cert_subject" "$CERT_SUBJECT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render_vhost_section() {
|
||||||
|
local section="$1"
|
||||||
|
config_get domain "$section" domain
|
||||||
|
config_get upstream "$section" upstream
|
||||||
|
config_get tls "$section" tls
|
||||||
|
config_get cert_path "$section" cert_path
|
||||||
|
config_get key_path "$section" key_path
|
||||||
|
config_get_bool auth "$section" auth 0
|
||||||
|
config_get auth_user "$section" auth_user
|
||||||
|
config_get auth_pass "$section" auth_pass
|
||||||
|
config_get_bool websocket "$section" websocket 0
|
||||||
|
config_get_bool enabled "$section" enabled 1
|
||||||
|
[ -z "$domain" ] && return
|
||||||
|
[ -z "$upstream" ] && return
|
||||||
|
local conf="$NGINX_VHOST_DIR/${domain}.conf"
|
||||||
|
|
||||||
|
if [ "$enabled" != "1" ]; then
|
||||||
|
rm -f "$conf"
|
||||||
|
remove_htpasswd "$domain"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$auth" = "1" ] && [ -n "$auth_user" ] && [ -n "$auth_pass" ]; then
|
||||||
|
write_htpasswd "$domain" "$auth_user" "$auth_pass"
|
||||||
|
else
|
||||||
|
remove_htpasswd "$domain"
|
||||||
|
fi
|
||||||
|
|
||||||
|
set_tls_context "$domain" "${tls:-off}" "$cert_path" "$key_path"
|
||||||
|
generate_vhost_config "$domain" "$upstream" "$TLS_ACTIVE" "$auth" "$websocket"
|
||||||
|
}
|
||||||
|
|
||||||
|
render_all_vhosts() {
|
||||||
|
config_load vhosts
|
||||||
|
config_foreach render_vhost_section vhost
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate nginx vhost configuration
|
|
||||||
generate_vhost_config() {
|
generate_vhost_config() {
|
||||||
local domain="$1"
|
local domain="$1"
|
||||||
local backend="$2"
|
local backend="$2"
|
||||||
local ssl="$3"
|
local ssl="$3"
|
||||||
local auth="$4"
|
local auth="$4"
|
||||||
local websocket="$5"
|
local websocket="$5"
|
||||||
|
local config_file="$NGINX_VHOST_DIR/${domain}.conf"
|
||||||
local config_file="${NGINX_VHOST_DIR}/${domain}.conf"
|
|
||||||
|
|
||||||
cat > "$config_file" <<NGINXEOF
|
cat > "$config_file" <<NGINXEOF
|
||||||
# VHost for ${domain}
|
# VHost for ${domain}
|
||||||
@ -46,107 +247,150 @@ server {
|
|||||||
server_name ${domain};
|
server_name ${domain};
|
||||||
NGINXEOF
|
NGINXEOF
|
||||||
|
|
||||||
# Add SSL redirect if enabled
|
|
||||||
if [ "$ssl" = "1" ]; then
|
if [ "$ssl" = "1" ]; then
|
||||||
cat >> "$config_file" <<NGINXEOF
|
cat >> "$config_file" <<NGINXEOF
|
||||||
|
|
||||||
# Redirect to HTTPS
|
|
||||||
return 301 https://\$host\$request_uri;
|
return 301 https://\$host\$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
server_name ${domain};
|
server_name ${domain};
|
||||||
|
ssl_certificate ${TLS_CERT_PATH};
|
||||||
# SSL certificates
|
ssl_certificate_key ${TLS_KEY_PATH};
|
||||||
ssl_certificate /etc/acme/${domain}/fullchain.cer;
|
|
||||||
ssl_certificate_key /etc/acme/${domain}/${domain}.key;
|
|
||||||
|
|
||||||
# SSL settings
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
NGINXEOF
|
NGINXEOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add authentication if enabled
|
|
||||||
if [ "$auth" = "1" ]; then
|
if [ "$auth" = "1" ]; then
|
||||||
cat >> "$config_file" <<NGINXEOF
|
cat >> "$config_file" <<NGINXEOF
|
||||||
|
|
||||||
# Basic authentication
|
|
||||||
auth_basic "Restricted Access";
|
auth_basic "Restricted Access";
|
||||||
auth_basic_user_file /etc/nginx/.htpasswd_${domain};
|
auth_basic_user_file /etc/nginx/.luci-app-vhost-manager_${domain};
|
||||||
NGINXEOF
|
NGINXEOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add proxy configuration
|
|
||||||
cat >> "$config_file" <<NGINXEOF
|
cat >> "$config_file" <<NGINXEOF
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass ${backend};
|
proxy_pass ${backend};
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
# Proxy headers
|
|
||||||
proxy_set_header Host \$host;
|
proxy_set_header Host \$host;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
NGINXEOF
|
NGINXEOF
|
||||||
|
|
||||||
# Add WebSocket support if enabled
|
|
||||||
if [ "$websocket" = "1" ]; then
|
if [ "$websocket" = "1" ]; then
|
||||||
cat >> "$config_file" <<NGINXEOF
|
cat >> "$config_file" <<NGINXEOF
|
||||||
|
|
||||||
# WebSocket support
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
NGINXEOF
|
NGINXEOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat >> "$config_file" <<NGINXEOF
|
cat >> "$config_file" <<NGINXEOF
|
||||||
|
|
||||||
# Timeouts
|
|
||||||
proxy_connect_timeout 60s;
|
proxy_connect_timeout 60s;
|
||||||
proxy_send_timeout 60s;
|
proxy_send_timeout 60s;
|
||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Access logs
|
|
||||||
access_log /var/log/nginx/${domain}_access.log;
|
access_log /var/log/nginx/${domain}_access.log;
|
||||||
error_log /var/log/nginx/${domain}_error.log;
|
error_log /var/log/nginx/${domain}_error.log;
|
||||||
}
|
}
|
||||||
NGINXEOF
|
NGINXEOF
|
||||||
|
|
||||||
echo "$config_file"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test backend connectivity
|
validate_backend() {
|
||||||
test_backend() {
|
|
||||||
local backend="$1"
|
local backend="$1"
|
||||||
|
echo "$backend" | grep -Eq '^https?://' || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Extract host and port from backend URL
|
probe_backend() {
|
||||||
local host=$(echo "$backend" | sed 's|https\?://||' | cut -d':' -f1 | cut -d'/' -f1)
|
local backend="$1"
|
||||||
local port=$(echo "$backend" | sed 's|https\?://||' | cut -d':' -f2 | cut -d'/' -f1)
|
local host port
|
||||||
|
host=$(echo "$backend" | sed 's|https\?://||' | cut -d'/' -f1 | cut -d':' -f1)
|
||||||
# Default port if not specified
|
port=$(echo "$backend" | sed 's|https\?://||' | cut -d'/' -f1 | cut -d':' -f2)
|
||||||
if [ "$port" = "$host" ]; then
|
if [ -z "$port" ]; then
|
||||||
if echo "$backend" | grep -q "^https"; then
|
if echo "$backend" | grep -q '^https://'; then
|
||||||
port=443
|
port=443
|
||||||
else
|
else
|
||||||
port=80
|
port=80
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [ -n "$host" ]; then
|
||||||
# Test connection
|
nc -z -w 5 "$host" "$port" 2>/dev/null
|
||||||
if nc -z -w 5 "$host" "$port" 2>/dev/null; then
|
return $?
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$1" in
|
add_or_update_vhost() {
|
||||||
list)
|
local domain="$1"
|
||||||
|
local backend="$2"
|
||||||
|
local tls_mode="$3"
|
||||||
|
local auth="$4"
|
||||||
|
local auth_user="$5"
|
||||||
|
local auth_pass="$6"
|
||||||
|
local websocket="$7"
|
||||||
|
local enabled="$8"
|
||||||
|
local cert_path="$9"
|
||||||
|
local key_path="${10}"
|
||||||
|
|
||||||
|
local section
|
||||||
|
section=$(find_section "$domain")
|
||||||
|
if [ -z "$section" ]; then
|
||||||
|
local safe
|
||||||
|
safe=$(sanitize_section_name "$domain")
|
||||||
|
section=$(uci add vhosts vhost)
|
||||||
|
uci rename vhosts.$section="$safe"
|
||||||
|
section="$safe"
|
||||||
|
uci set vhosts.$section.domain="$domain"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local normalized_tls="${tls_mode:-off}"
|
||||||
|
local normalized_auth
|
||||||
|
local normalized_websocket
|
||||||
|
local normalized_enabled
|
||||||
|
|
||||||
|
normalized_auth=$(normalize_bool "$auth")
|
||||||
|
normalized_websocket=$(normalize_bool "$websocket")
|
||||||
|
if [ -n "$enabled" ]; then
|
||||||
|
normalized_enabled=$(normalize_bool "$enabled")
|
||||||
|
else
|
||||||
|
normalized_enabled=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci set vhosts.$section.upstream="$backend"
|
||||||
|
uci set vhosts.$section.tls="$normalized_tls"
|
||||||
|
if [ -n "$cert_path" ]; then
|
||||||
|
uci set vhosts.$section.cert_path="$cert_path"
|
||||||
|
else
|
||||||
|
uci -q delete vhosts.$section.cert_path
|
||||||
|
fi
|
||||||
|
if [ -n "$key_path" ]; then
|
||||||
|
uci set vhosts.$section.key_path="$key_path"
|
||||||
|
else
|
||||||
|
uci -q delete vhosts.$section.key_path
|
||||||
|
fi
|
||||||
|
uci set vhosts.$section.auth="$normalized_auth"
|
||||||
|
if [ -n "$auth_user" ]; then
|
||||||
|
uci set vhosts.$section.auth_user="$auth_user"
|
||||||
|
else
|
||||||
|
uci -q delete vhosts.$section.auth_user
|
||||||
|
fi
|
||||||
|
if [ -n "$auth_pass" ]; then
|
||||||
|
uci set vhosts.$section.auth_pass="$auth_pass"
|
||||||
|
else
|
||||||
|
uci -q delete vhosts.$section.auth_pass
|
||||||
|
fi
|
||||||
|
uci set vhosts.$section.websocket="$normalized_websocket"
|
||||||
|
uci set vhosts.$section.enabled="$normalized_enabled"
|
||||||
|
|
||||||
|
uci commit vhosts
|
||||||
|
config_load vhosts
|
||||||
|
render_vhost_section "$section"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_response_schema() {
|
||||||
json_init
|
json_init
|
||||||
json_add_object "status"
|
json_add_object "status"
|
||||||
json_close_object
|
json_close_object
|
||||||
@ -158,16 +402,26 @@ case "$1" in
|
|||||||
json_add_object "add_vhost"
|
json_add_object "add_vhost"
|
||||||
json_add_string "domain" "string"
|
json_add_string "domain" "string"
|
||||||
json_add_string "backend" "string"
|
json_add_string "backend" "string"
|
||||||
json_add_string "ssl" "bool"
|
json_add_string "tls_mode" "string"
|
||||||
json_add_string "auth" "bool"
|
json_add_string "auth" "bool"
|
||||||
|
json_add_string "auth_user" "string"
|
||||||
|
json_add_string "auth_pass" "string"
|
||||||
json_add_string "websocket" "bool"
|
json_add_string "websocket" "bool"
|
||||||
|
json_add_string "enabled" "bool"
|
||||||
|
json_add_string "cert_path" "string"
|
||||||
|
json_add_string "key_path" "string"
|
||||||
json_close_object
|
json_close_object
|
||||||
json_add_object "update_vhost"
|
json_add_object "update_vhost"
|
||||||
json_add_string "domain" "string"
|
json_add_string "domain" "string"
|
||||||
json_add_string "backend" "string"
|
json_add_string "backend" "string"
|
||||||
json_add_string "ssl" "bool"
|
json_add_string "tls_mode" "string"
|
||||||
json_add_string "auth" "bool"
|
json_add_string "auth" "bool"
|
||||||
|
json_add_string "auth_user" "string"
|
||||||
|
json_add_string "auth_pass" "string"
|
||||||
json_add_string "websocket" "bool"
|
json_add_string "websocket" "bool"
|
||||||
|
json_add_string "enabled" "bool"
|
||||||
|
json_add_string "cert_path" "string"
|
||||||
|
json_add_string "key_path" "string"
|
||||||
json_close_object
|
json_close_object
|
||||||
json_add_object "delete_vhost"
|
json_add_object "delete_vhost"
|
||||||
json_add_string "domain" "string"
|
json_add_string "domain" "string"
|
||||||
@ -188,333 +442,232 @@ case "$1" in
|
|||||||
json_add_string "lines" "int"
|
json_add_string "lines" "int"
|
||||||
json_close_object
|
json_close_object
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
}
|
||||||
|
|
||||||
|
init_dirs
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
list_response_schema
|
||||||
|
;;
|
||||||
call)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
status)
|
status)
|
||||||
init_dirs
|
init_dirs
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
json_add_boolean "enabled" 1
|
json_add_boolean "enabled" 1
|
||||||
json_add_string "module" "vhost-manager"
|
json_add_string "module" "vhost-manager"
|
||||||
json_add_string "version" "$PKG_VERSION"
|
json_add_string "version" "$PKG_VERSION"
|
||||||
|
|
||||||
# Check nginx status
|
|
||||||
if pgrep -x nginx >/dev/null 2>&1; then
|
if pgrep -x nginx >/dev/null 2>&1; then
|
||||||
json_add_boolean "nginx_running" 1
|
json_add_boolean "nginx_running" 1
|
||||||
|
json_add_string "nginx_version" "$(nginx -v 2>&1 | grep -o 'nginx/[0-9.]*' | cut -d'/' -f2)"
|
||||||
# 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
|
else
|
||||||
json_add_boolean "nginx_running" 0
|
json_add_boolean "nginx_running" 0
|
||||||
json_add_string "nginx_version" "unknown"
|
json_add_string "nginx_version" "unknown"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check acme.sh availability
|
|
||||||
if command -v acme.sh >/dev/null 2>&1; then
|
if command -v acme.sh >/dev/null 2>&1; then
|
||||||
json_add_boolean "acme_available" 1
|
json_add_boolean "acme_available" 1
|
||||||
local acme_version=$(acme.sh --version 2>/dev/null | head -1)
|
json_add_string "acme_version" "$(acme.sh --version 2>/dev/null | head -1)"
|
||||||
json_add_string "acme_version" "$acme_version"
|
|
||||||
else
|
else
|
||||||
json_add_boolean "acme_available" 0
|
json_add_boolean "acme_available" 0
|
||||||
json_add_string "acme_version" "not installed"
|
json_add_string "acme_version" "not installed"
|
||||||
fi
|
fi
|
||||||
|
count=0
|
||||||
# Count vhosts
|
config_load vhosts
|
||||||
local vhost_count=0
|
config_foreach _count_vhost vhost
|
||||||
if [ -d "$NGINX_VHOST_DIR" ]; then
|
json_add_int "vhost_count" "$count"
|
||||||
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
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
list_vhosts)
|
list_vhosts)
|
||||||
init_dirs
|
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
json_add_array "vhosts"
|
json_add_array "vhosts"
|
||||||
|
config_load vhosts
|
||||||
if [ -d "$NGINX_VHOST_DIR" ]; then
|
config_foreach append_vhost_json vhost
|
||||||
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_close_array
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
get_vhost)
|
get_vhost)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var domain domain
|
json_get_var domain domain
|
||||||
|
local section=$(find_section "$domain")
|
||||||
local config_file="${NGINX_VHOST_DIR}/${domain}.conf"
|
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
json_add_string "domain" "$domain"
|
if [ -n "$section" ]; then
|
||||||
|
config_load vhosts
|
||||||
if [ -f "$config_file" ]; then
|
config_get upstream "$section" upstream
|
||||||
|
config_get tls "$section" tls
|
||||||
|
config_get_bool auth "$section" auth 0
|
||||||
|
config_get auth_user "$section" auth_user
|
||||||
|
config_get_bool websocket "$section" websocket 0
|
||||||
|
config_get_bool enabled "$section" enabled 1
|
||||||
|
config_get cert_path "$section" cert_path
|
||||||
|
config_get key_path "$section" key_path
|
||||||
|
set_tls_context "$domain" "${tls:-off}" "$cert_path" "$key_path"
|
||||||
|
[ "$TLS_ACTIVE" = "1" ] && read_cert_metadata "$TLS_CERT_PATH"
|
||||||
json_add_boolean "exists" 1
|
json_add_boolean "exists" 1
|
||||||
|
json_add_string "domain" "$domain"
|
||||||
# Parse configuration
|
json_add_string "backend" "$upstream"
|
||||||
local ssl=0
|
json_add_string "tls_mode" "${tls:-off}"
|
||||||
local auth=0
|
json_add_boolean "ssl" "$TLS_ACTIVE"
|
||||||
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 "auth" "$auth"
|
||||||
|
[ -n "$auth_user" ] && json_add_string "auth_user" "$auth_user"
|
||||||
json_add_boolean "websocket" "$websocket"
|
json_add_boolean "websocket" "$websocket"
|
||||||
json_add_string "config_file" "$config_file"
|
json_add_boolean "enabled" "$enabled"
|
||||||
|
json_add_string "config_file" "$NGINX_VHOST_DIR/${domain}.conf"
|
||||||
# SSL certificate info
|
[ -n "$cert_path" ] && json_add_string "cert_path" "$cert_path"
|
||||||
if [ "$ssl" = "1" ] && [ -f "/etc/acme/${domain}/fullchain.cer" ]; then
|
[ -n "$key_path" ] && json_add_string "key_path" "$key_path"
|
||||||
local ssl_expires=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -enddate 2>/dev/null | cut -d'=' -f2)
|
if [ "$TLS_ACTIVE" = "1" ]; then
|
||||||
local ssl_issuer=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -issuer 2>/dev/null | cut -d'=' -f2-)
|
json_add_string "cert_file" "$TLS_CERT_PATH"
|
||||||
|
[ -n "$CERT_EXPIRES" ] && json_add_string "cert_expires" "$CERT_EXPIRES"
|
||||||
json_add_string "ssl_expires" "$ssl_expires"
|
[ -n "$CERT_ISSUER" ] && json_add_string "cert_issuer" "$CERT_ISSUER"
|
||||||
json_add_string "ssl_issuer" "$ssl_issuer"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
json_add_boolean "exists" 0
|
json_add_boolean "exists" 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
add_vhost)
|
add_vhost)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var domain domain
|
json_get_var domain domain
|
||||||
json_get_var backend backend
|
json_get_var backend backend
|
||||||
json_get_var ssl ssl
|
json_get_var tls_mode tls_mode
|
||||||
|
json_get_var ssl legacy_ssl
|
||||||
json_get_var auth auth
|
json_get_var auth auth
|
||||||
|
json_get_var auth_user auth_user
|
||||||
|
json_get_var auth_pass auth_pass
|
||||||
json_get_var websocket websocket
|
json_get_var websocket websocket
|
||||||
|
json_get_var enabled enabled
|
||||||
init_dirs
|
json_get_var cert_path cert_path
|
||||||
|
json_get_var key_path key_path
|
||||||
# Validate domain
|
|
||||||
if [ -z "$domain" ] || [ -z "$backend" ]; then
|
if [ -z "$domain" ] || [ -z "$backend" ]; then
|
||||||
json_init
|
json_init; json_add_boolean "success" 0; json_add_string "message" "Domain and backend required"; json_dump; exit 0
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "message" "Domain and backend are required"
|
|
||||||
json_dump
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
if ! validate_backend "$backend"; then
|
||||||
# Check if vhost already exists
|
json_init; json_add_boolean "success" 0; json_add_string "message" "Backend must be http(s) URL"; json_dump; exit 0
|
||||||
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
|
fi
|
||||||
|
local existing_section
|
||||||
# Generate nginx config
|
existing_section=$(find_section "$domain")
|
||||||
local config_file=$(generate_vhost_config "$domain" "$backend" "$ssl" "$auth" "$websocket")
|
if [ -n "$existing_section" ]; then
|
||||||
|
json_init; json_add_boolean "success" 0; json_add_string "message" "VHost already exists"; json_dump; exit 0
|
||||||
# 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
|
fi
|
||||||
|
tls_mode=$(resolve_tls_mode "$tls_mode" "$legacy_ssl")
|
||||||
|
if [ "$tls_mode" = "manual" ] && { [ -z "$cert_path" ] || [ -z "$key_path" ]; }; then
|
||||||
|
json_init; json_add_boolean "success" 0; json_add_string "message" "Manual TLS requires cert_path and key_path"; json_dump; exit 0
|
||||||
|
fi
|
||||||
|
add_or_update_vhost "$domain" "$backend" "$tls_mode" "$auth" "$auth_user" "$auth_pass" "$websocket" "$enabled" "$cert_path" "$key_path"
|
||||||
|
json_init; json_add_boolean "success" 1; json_add_boolean "reload_required" 1; json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
update_vhost)
|
update_vhost)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var domain domain
|
json_get_var domain domain
|
||||||
json_get_var backend backend
|
json_get_var backend backend
|
||||||
json_get_var ssl ssl
|
json_get_var tls_mode tls_mode
|
||||||
|
json_get_var ssl legacy_ssl
|
||||||
json_get_var auth auth
|
json_get_var auth auth
|
||||||
|
json_get_var auth_user auth_user
|
||||||
|
json_get_var auth_pass auth_pass
|
||||||
json_get_var websocket websocket
|
json_get_var websocket websocket
|
||||||
|
json_get_var enabled enabled
|
||||||
# Check if vhost exists
|
json_get_var cert_path cert_path
|
||||||
if [ ! -f "${NGINX_VHOST_DIR}/${domain}.conf" ]; then
|
json_get_var key_path key_path
|
||||||
json_init
|
if [ -z "$domain" ]; then
|
||||||
json_add_boolean "success" 0
|
json_init; json_add_boolean "success" 0; json_add_string "message" "Domain required"; json_dump; exit 0
|
||||||
json_add_string "message" "VHost not found: $domain"
|
|
||||||
json_dump
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
local section
|
||||||
# Backup old config
|
section=$(find_section "$domain")
|
||||||
cp "${NGINX_VHOST_DIR}/${domain}.conf" "${NGINX_VHOST_DIR}/${domain}.conf.bak"
|
if [ -z "$section" ]; then
|
||||||
|
json_init; json_add_boolean "success" 0; json_add_string "message" "VHost not found"; json_dump; exit 0
|
||||||
# Generate new config
|
fi
|
||||||
local config_file=$(generate_vhost_config "$domain" "$backend" "$ssl" "$auth" "$websocket")
|
if [ -n "$backend" ] && ! validate_backend "$backend"; then
|
||||||
|
json_init; json_add_boolean "success" 0; json_add_string "message" "Backend must be http(s) URL"; json_dump; exit 0
|
||||||
# Test nginx config
|
fi
|
||||||
if nginx -t 2>&1 | grep -q "successful"; then
|
config_load vhosts
|
||||||
rm -f "${NGINX_VHOST_DIR}/${domain}.conf.bak"
|
if [ -z "$backend" ]; then
|
||||||
|
config_get backend "$section" upstream
|
||||||
json_init
|
fi
|
||||||
json_add_boolean "success" 1
|
if [ -z "$enabled" ]; then
|
||||||
json_add_string "message" "VHost updated successfully"
|
config_get_bool enabled "$section" enabled 1
|
||||||
json_add_boolean "reload_required" 1
|
fi
|
||||||
json_dump
|
local current_tls current_cert_path current_key_path
|
||||||
|
config_get current_tls "$section" tls
|
||||||
|
config_get current_cert_path "$section" cert_path
|
||||||
|
config_get current_key_path "$section" key_path
|
||||||
|
if [ -n "$tls_mode" ] || [ -n "$legacy_ssl" ]; then
|
||||||
|
tls_mode=$(resolve_tls_mode "$tls_mode" "$legacy_ssl")
|
||||||
else
|
else
|
||||||
# Restore backup
|
tls_mode="${current_tls:-off}"
|
||||||
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
|
fi
|
||||||
|
[ -n "$cert_path" ] || cert_path="$current_cert_path"
|
||||||
|
[ -n "$key_path" ] || key_path="$current_key_path"
|
||||||
|
if [ "$tls_mode" = "manual" ] && { [ -z "$cert_path" ] || [ -z "$key_path" ]; }; then
|
||||||
|
json_init; json_add_boolean "success" 0; json_add_string "message" "Manual TLS requires cert_path and key_path"; json_dump; exit 0
|
||||||
|
fi
|
||||||
|
add_or_update_vhost "$domain" "$backend" "$tls_mode" "$auth" "$auth_user" "$auth_pass" "$websocket" "$enabled" "$cert_path" "$key_path"
|
||||||
|
json_init; json_add_boolean "success" 1; json_add_boolean "reload_required" 1; json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
delete_vhost)
|
delete_vhost)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var domain domain
|
json_get_var domain domain
|
||||||
|
local section=$(find_section "$domain")
|
||||||
local config_file="${NGINX_VHOST_DIR}/${domain}.conf"
|
local conf="$NGINX_VHOST_DIR/${domain}.conf"
|
||||||
|
if [ -n "$section" ]; then
|
||||||
if [ -f "$config_file" ]; then
|
uci delete vhosts.$section
|
||||||
rm -f "$config_file"
|
uci commit vhosts
|
||||||
|
|
||||||
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
|
fi
|
||||||
|
rm -f "$conf"
|
||||||
|
remove_htpasswd "$domain"
|
||||||
|
json_init; json_add_boolean "success" 1; json_add_boolean "reload_required" 1; json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
test_backend)
|
test_backend)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var backend backend
|
json_get_var backend backend
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
json_add_string "backend" "$backend"
|
json_add_string "backend" "$backend"
|
||||||
|
if [ -n "$backend" ] && probe_backend "$backend"; then
|
||||||
if test_backend "$backend"; then
|
|
||||||
json_add_boolean "reachable" 1
|
json_add_boolean "reachable" 1
|
||||||
json_add_string "status" "Backend is reachable"
|
json_add_string "status" "Backend is reachable"
|
||||||
else
|
else
|
||||||
json_add_boolean "reachable" 0
|
json_add_boolean "reachable" 0
|
||||||
json_add_string "status" "Backend is unreachable"
|
json_add_string "status" "Backend is unreachable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
request_cert)
|
request_cert)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var domain domain
|
json_get_var domain domain
|
||||||
json_get_var email email
|
json_get_var email email
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
|
|
||||||
if ! command -v acme.sh >/dev/null 2>&1; then
|
if ! command -v acme.sh >/dev/null 2>&1; then
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "message" "acme.sh not installed"
|
json_add_string "message" "acme.sh not installed"
|
||||||
json_dump
|
json_dump
|
||||||
exit 0
|
return
|
||||||
fi
|
fi
|
||||||
|
if acme.sh --issue -d "$domain" --standalone --accountemail "$email" --force >/tmp/acme.log 2>&1; then
|
||||||
# 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_boolean "success" 1
|
||||||
json_add_string "message" "Certificate requested successfully"
|
json_add_string "message" "Certificate requested"
|
||||||
json_add_string "domain" "$domain"
|
|
||||||
else
|
else
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "message" "Certificate request failed"
|
json_add_string "message" "Certificate request failed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
list_certs)
|
list_certs)
|
||||||
json_init
|
json_init
|
||||||
json_add_array "certificates"
|
json_add_array "certificates"
|
||||||
|
|
||||||
if [ -d "$ACME_STATE_DIR" ]; then
|
if [ -d "$ACME_STATE_DIR" ]; then
|
||||||
find "$ACME_STATE_DIR" -name "fullchain.cer" -type f 2>/dev/null | while read -r cert_file; do
|
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 domain
|
||||||
local expires=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d'=' -f2)
|
domain=$(basename "$(dirname "$cert_file")")
|
||||||
local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | cut -d'=' -f2-)
|
local expires issuer subject
|
||||||
local subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | cut -d'=' -f2-)
|
expires=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d'=' -f2)
|
||||||
|
issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | cut -d'=' -f2-)
|
||||||
|
subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | cut -d'=' -f2-)
|
||||||
json_add_object
|
json_add_object
|
||||||
json_add_string "domain" "$domain"
|
json_add_string "domain" "$domain"
|
||||||
json_add_string "expires" "$expires"
|
json_add_string "expires" "$expires"
|
||||||
@ -524,62 +677,34 @@ case "$1" in
|
|||||||
json_close_object
|
json_close_object
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
json_close_array
|
json_close_array
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
reload_nginx)
|
reload_nginx)
|
||||||
json_init
|
if nginx -t >/dev/null 2>&1; then
|
||||||
|
/etc/init.d/nginx reload
|
||||||
# Test configuration first
|
json_init; json_add_boolean "success" 1; json_dump
|
||||||
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
|
else
|
||||||
json_add_boolean "success" 0
|
json_init; json_add_boolean "success" 0; json_add_string "message" "nginx config invalid"; json_dump
|
||||||
json_add_string "message" "Failed to reload nginx"
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "message" "Invalid nginx configuration"
|
|
||||||
fi
|
|
||||||
|
|
||||||
json_dump
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
get_access_logs)
|
get_access_logs)
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var domain domain
|
json_get_var domain domain
|
||||||
json_get_var lines lines
|
json_get_var lines lines
|
||||||
|
lines=${lines:-100}
|
||||||
lines=${lines:-50}
|
local file="/var/log/nginx/${domain}_access.log"
|
||||||
|
|
||||||
local log_file="/var/log/nginx/${domain}_access.log"
|
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
json_add_string "domain" "$domain"
|
json_add_string "domain" "$domain"
|
||||||
json_add_array "logs"
|
json_add_array "logs"
|
||||||
|
if [ -f "$file" ]; then
|
||||||
if [ -f "$log_file" ]; then
|
tail -n "$lines" "$file" | while read -r line; do json_add_string "" "$line"; done
|
||||||
tail -n "$lines" "$log_file" | while read -r log_line; do
|
|
||||||
json_add_string "" "$log_line"
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
json_close_array
|
json_close_array
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
*) json_init; json_add_string "error" "unknown method"; json_dump ;;
|
||||||
*)
|
|
||||||
json_init
|
|
||||||
json_add_int "error" -32601
|
|
||||||
json_add_string "message" "Method not found: $2"
|
|
||||||
json_dump
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"get_access_logs"
|
"get_access_logs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": ["vhost_manager", "nginx"],
|
"uci": ["vhosts", "vhost_manager", "nginx"],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/nginx/conf.d/*": ["read"],
|
"/etc/nginx/conf.d/*": ["read"],
|
||||||
"/etc/acme/*": ["read"],
|
"/etc/acme/*": ["read"],
|
||||||
@ -29,7 +29,7 @@
|
|||||||
"reload_nginx"
|
"reload_nginx"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": ["vhost_manager", "nginx"],
|
"uci": ["vhosts", "vhost_manager", "nginx"],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/nginx/conf.d/*": ["write"],
|
"/etc/nginx/conf.d/*": ["write"],
|
||||||
"/etc/nginx/.htpasswd_*": ["write"]
|
"/etc/nginx/.htpasswd_*": ["write"]
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"test_config"
|
"test_config"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": ["vhost_manager", "nginx"],
|
"uci": ["vhosts", "vhost_manager", "nginx"],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/nginx/conf.d/*": ["read"],
|
"/etc/nginx/conf.d/*": ["read"],
|
||||||
"/etc/acme/*": ["read"]
|
"/etc/acme/*": ["read"]
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"reload_nginx"
|
"reload_nginx"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": ["vhost_manager", "nginx"],
|
"uci": ["vhosts", "vhost_manager", "nginx"],
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/nginx/conf.d/*": ["write"]
|
"/etc/nginx/conf.d/*": ["write"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user