From 9cdbb21a99379f7945cf48a39261fda597cd2273 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 16:31:18 +0100 Subject: [PATCH] luci-app-vhost-manager: migrate to vhosts config --- acl/luci-app-vhost-manager.json | 4 +- luci-app-vhost-manager/Makefile | 10 +- luci-app-vhost-manager/README.md | 90 +- .../resources/vhost-manager/api.js | 4 +- .../resources/view/vhost-manager/vhosts.js | 87 +- luci-app-vhost-manager/root/etc/config/vhosts | 17 + .../50-luci-app-vhost-manager-migrate | 96 ++ .../root/usr/libexec/rpcd/luci.vhost-manager | 843 ++++++++++-------- .../rpcd/acl.d/luci-app-vhost-manager.json | 4 +- secubox-tools/acl/luci-app-vhost-manager.json | 4 +- 10 files changed, 749 insertions(+), 410 deletions(-) create mode 100644 luci-app-vhost-manager/root/etc/config/vhosts create mode 100755 luci-app-vhost-manager/root/etc/uci-defaults/50-luci-app-vhost-manager-migrate diff --git a/acl/luci-app-vhost-manager.json b/acl/luci-app-vhost-manager.json index c5fdce2..5ff4341 100644 --- a/acl/luci-app-vhost-manager.json +++ b/acl/luci-app-vhost-manager.json @@ -11,7 +11,7 @@ "test_config" ] }, - "uci": ["vhost_manager", "nginx"], + "uci": ["vhosts", "vhost_manager", "nginx"], "file": { "/etc/nginx/conf.d/*": ["read"], "/etc/acme/*": ["read"] @@ -26,7 +26,7 @@ "reload_nginx" ] }, - "uci": ["vhost_manager", "nginx"], + "uci": ["vhosts", "vhost_manager", "nginx"], "file": { "/etc/nginx/conf.d/*": ["write"] } diff --git a/luci-app-vhost-manager/Makefile b/luci-app-vhost-manager/Makefile index 6cf0112..2a2c707 100644 --- a/luci-app-vhost-manager/Makefile +++ b/luci-app-vhost-manager/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-vhost-manager PKG_VERSION:=0.4.1 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=CyberMind @@ -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_PKGARCH:=all +define Package/$(PKG_NAME)/conffiles +/etc/config/vhost_manager +/etc/config/vhosts +endef + # File permissions (CRITICAL: RPCD scripts MUST be executable 755) # Format: path:owner:group:mode @@ -21,7 +26,8 @@ LUCI_PKGARCH:=all # - Helper scripts: 755 (if executable) # - Config files: 644 (readable by all, writable by root) # - 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 diff --git a/luci-app-vhost-manager/README.md b/luci-app-vhost-manager/README.md index 9fa65b9..ab5c33a 100644 --- a/luci-app-vhost-manager/README.md +++ b/luci-app-vhost-manager/README.md @@ -49,26 +49,30 @@ opkg install luci-app-vhost-manager ## 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 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 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 '1' option auth_user 'admin' option auth_pass 'secretpassword' 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 #### Global Section @@ -78,12 +82,13 @@ config vhost 'myapp' #### 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) +- `upstream`: Backend URL to proxy to (required, e.g., http://192.168.1.100:8080) +- `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_user`: Username for authentication (required if auth=1) -- `auth_pass`: Password for authentication (required if auth=1) -- `websocket`: Enable WebSocket support (default: 0) +- `auth_user` / `auth_pass`: Credentials used when `auth=1` +- `websocket`: Enable WebSocket headers (default: 0) +- `enabled`: Disable the vhost without deleting it (default: 1) ## Usage @@ -135,9 +140,12 @@ ubus call luci.vhost-manager status ubus call luci.vhost-manager add_vhost '{ "domain": "app.example.com", "backend": "http://192.168.1.100:8080", - "ssl": true, - "auth": false, - "websocket": true + "tls_mode": "acme", + "auth": 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 -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) @@ -307,10 +315,16 @@ List all configured virtual hosts. { "domain": "app.example.com", "backend": "http://192.168.1.100:8080", + "upstream": "http://192.168.1.100:8080", + "tls_mode": "acme", "ssl": true, - "ssl_expires": "2025-03-15", - "auth": false, - "websocket": true + "cert_file": "/etc/acme/app.example.com/fullchain.cer", + "cert_expires": "2025-03-15", + "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", "backend": "http://192.168.1.100:8080", + "tls_mode": "acme", "ssl": true, - "ssl_expires": "2025-03-15", + "cert_expires": "2025-03-15", + "cert_issuer": "R3", "auth": true, "auth_user": "admin", - "websocket": true + "websocket": true, + "enabled": true } ``` -### add_vhost(domain, backend, ssl, auth, websocket) +### add_vhost(payload) Add a new virtual host. **Parameters:** - `domain`: Domain name (required) - `backend`: Backend URL (required) -- `ssl`: Enable SSL (boolean) +- `tls_mode`: `off`, `acme`, or `manual` (required) - `auth`: Enable authentication (boolean) +- `auth_user` / `auth_pass`: Credentials when auth is enabled - `websocket`: Enable WebSocket (boolean) +- `enabled`: Disable the vhost without deleting (boolean) +- `cert_path` / `key_path`: Required when `tls_mode=manual` **Returns:** ```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. -**Parameters:** Same as add_vhost +**Parameters:** Same as `add_vhost`. Omitted fields retain their previous value. **Returns:** ```json @@ -394,8 +414,9 @@ Test connectivity to a backend server. **Returns:** ```json { + "backend": "http://192.168.1.100:8080", "reachable": true, - "response_time": 45 + "status": "Backend is reachable" } ``` @@ -411,7 +432,7 @@ Request a Let's Encrypt SSL certificate. ```json { "success": true, - "message": "Certificate obtained successfully" + "message": "Certificate requested" } ``` @@ -456,6 +477,7 @@ Get nginx access logs for a domain. **Returns:** ```json { + "domain": "app.example.com", "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" @@ -513,18 +535,24 @@ Ensure: Check htpasswd file exists: ```bash -ls -l /etc/nginx/htpasswd/{domain} +ls -l /etc/nginx/.luci-app-vhost-manager_{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 +# Update UCI entry +uci set vhosts.myapp.auth='1' +uci set vhosts.myapp.auth_user='newuser' +uci set vhosts.myapp.auth_pass='newpass' +uci commit vhosts # 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 diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/api.js b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/api.js index ed998b6..9c41009 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/api.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/api.js @@ -24,14 +24,14 @@ var callGetVHost = rpc.declare({ var callAddVHost = rpc.declare({ object: 'luci.vhost-manager', 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: { } }); var callUpdateVHost = rpc.declare({ object: 'luci.vhost-manager', 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: { } }); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js index 2f83e2f..9d76eaf 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js @@ -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({ load: function() { return Promise.all([ @@ -58,7 +74,7 @@ return L.view.extend({ }, 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')); s.anonymous = false; s.addremove = true; @@ -105,9 +121,20 @@ return L.view.extend({ return widget; }; - o = s.option(form.Flag, 'ssl', _('Enable SSL')); - o.default = o.disabled; - o.description = _('Serve HTTPS (requires certificate).'); + o = s.option(form.ListValue, 'tls_mode', _('TLS Mode')); + o.value('off', _('Disabled (HTTP only)')); + 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.default = o.disabled; @@ -124,19 +151,49 @@ return L.view.extend({ o.default = o.disabled; 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) { 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 tlsMode = this.section.formvalue(section_id, 'tls_mode') || 'off'; var auth = this.section.formvalue(section_id, 'auth') === '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) { ui.addNotification(null, E('p', _('Domain and backend are required')), 'error'); 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) { ui.addNotification(null, E('p', _('VHost created successfully')), 'info'); @@ -174,9 +231,10 @@ return L.view.extend({ }, renderHeader: function(vhosts) { - var sslEnabled = vhosts.filter(function(v) { return v.ssl; }).length; - var authEnabled = vhosts.filter(function(v) { return v.auth; }).length; - var websocketEnabled = vhosts.filter(function(v) { return v.websocket; }).length; + var active = vhosts.filter(isEnabled); + var sslEnabled = active.filter(function(v) { return v.ssl; }).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' }, [ E('div', {}, [ @@ -189,6 +247,7 @@ return L.view.extend({ ]), E('div', { 'class': 'sh-stats-grid' }, [ this.renderStatBadge(vhosts.length, _('Defined')), + this.renderStatBadge(active.length, _('Enabled')), this.renderStatBadge(sslEnabled, _('TLS')), this.renderStatBadge(authEnabled, _('Auth')), this.renderStatBadge(websocketEnabled, _('WebSocket')) @@ -225,14 +284,22 @@ return L.view.extend({ renderVhostCard: function(vhost, cert) { 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.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' }, [ E('div', { 'class': 'vhost-card-title' }, ['🌐', vhost.domain || _('Unnamed')]), E('div', { 'class': 'vhost-card-meta' }, vhost.backend || _('No backend defined')), 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' }, cert ? _('Certificate expires %s').format(formatDate(cert.expires)) : _('No certificate detected')) ]); diff --git a/luci-app-vhost-manager/root/etc/config/vhosts b/luci-app-vhost-manager/root/etc/config/vhosts new file mode 100644 index 0000000..ce4fc9c --- /dev/null +++ b/luci-app-vhost-manager/root/etc/config/vhosts @@ -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' diff --git a/luci-app-vhost-manager/root/etc/uci-defaults/50-luci-app-vhost-manager-migrate b/luci-app-vhost-manager/root/etc/uci-defaults/50-luci-app-vhost-manager-migrate new file mode 100755 index 0000000..a0d1203 --- /dev/null +++ b/luci-app-vhost-manager/root/etc/uci-defaults/50-luci-app-vhost-manager-migrate @@ -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 diff --git a/luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager b/luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager index da756cf..a104caf 100755 --- a/luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager +++ b/luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager @@ -1,6 +1,5 @@ #!/bin/sh -# RPCD backend for VHost Manager -# Provides ubus interface: luci.vhost-manager +# RPCD backend for VHost Manager (UCI-driven) . /lib/functions.sh . /usr/share/libubox/jshn.sh @@ -18,26 +17,228 @@ PKG_VERSION="$(get_pkg_version)" NGINX_VHOST_DIR="/etc/nginx/conf.d" 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() { - mkdir -p "$NGINX_VHOST_DIR" - mkdir -p "$ACME_STATE_DIR" - touch "$VHOST_CONFIG" + mkdir -p "$NGINX_VHOST_DIR" "$ACME_STATE_DIR" + [ -f "$VHOSTS_CONFIG" ] || cat <<'CFG' > "$VHOSTS_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() { 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 + local config_file="$NGINX_VHOST_DIR/${domain}.conf" + + cat > "$config_file" <> "$config_file" << NGINXEOF - - # Redirect to HTTPS + cat >> "$config_file" <> "$config_file" << NGINXEOF - - # Basic authentication + cat >> "$config_file" <> "$config_file" << NGINXEOF - + cat >> "$config_file" <> "$config_file" << NGINXEOF - - # WebSocket support + cat >> "$config_file" <> "$config_file" << NGINXEOF - - # Timeouts + cat >> "$config_file" </dev/null; then - return 0 - else - return 1 + if [ -n "$host" ]; then + nc -z -w 5 "$host" "$port" 2>/dev/null + return $? fi + return 1 } +add_or_update_vhost() { + 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_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 "tls_mode" "string" + 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 "enabled" "bool" + json_add_string "cert_path" "string" + json_add_string "key_path" "string" + json_close_object + json_add_object "update_vhost" + json_add_string "domain" "string" + json_add_string "backend" "string" + json_add_string "tls_mode" "string" + 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 "enabled" "bool" + json_add_string "cert_path" "string" + json_add_string "key_path" "string" + 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 +} + +init_dirs + case "$1" in 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 + list_response_schema ;; - call) case "$2" in status) init_dirs - json_init json_add_boolean "enabled" 1 json_add_string "module" "vhost-manager" - json_add_string "version" "$PKG_VERSION" - - # Check nginx status - if pgrep -x nginx > /dev/null 2>&1; then + json_add_string "version" "$PKG_VERSION" + 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" + json_add_string "nginx_version" "$(nginx -v 2>&1 | grep -o 'nginx/[0-9.]*' | cut -d'/' -f2)" 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 + 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" + json_add_string "acme_version" "$(acme.sh --version 2>/dev/null | head -1)" 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" - + count=0 + config_load vhosts + config_foreach _count_vhost vhost + json_add_int "vhost_count" "$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 - + config_load vhosts + config_foreach append_vhost_json vhost 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" - + local section=$(find_section "$domain") json_init - json_add_string "domain" "$domain" - - if [ -f "$config_file" ]; then + if [ -n "$section" ]; then + config_load vhosts + 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 - - # 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_string "domain" "$domain" + json_add_string "backend" "$upstream" + json_add_string "tls_mode" "${tls:-off}" + json_add_boolean "ssl" "$TLS_ACTIVE" json_add_boolean "auth" "$auth" + [ -n "$auth_user" ] && json_add_string "auth_user" "$auth_user" 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" + json_add_boolean "enabled" "$enabled" + json_add_string "config_file" "$NGINX_VHOST_DIR/${domain}.conf" + [ -n "$cert_path" ] && json_add_string "cert_path" "$cert_path" + [ -n "$key_path" ] && json_add_string "key_path" "$key_path" + 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" 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 tls_mode tls_mode + json_get_var ssl legacy_ssl json_get_var auth auth + json_get_var auth_user auth_user + json_get_var auth_pass auth_pass json_get_var websocket websocket - - init_dirs - - # Validate domain + json_get_var enabled enabled + json_get_var cert_path cert_path + json_get_var key_path key_path 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 + json_init; json_add_boolean "success" 0; json_add_string "message" "Domain and backend 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 + if ! 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 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 + local existing_section + existing_section=$(find_section "$domain") + if [ -n "$existing_section" ]; then + json_init; json_add_boolean "success" 0; json_add_string "message" "VHost already exists"; json_dump; exit 0 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) read -r input json_load "$input" json_get_var domain domain 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_user auth_user + json_get_var auth_pass auth_pass 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 + json_get_var enabled enabled + json_get_var cert_path cert_path + json_get_var key_path key_path + if [ -z "$domain" ]; then + json_init; json_add_boolean "success" 0; json_add_string "message" "Domain required"; 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 + local section + section=$(find_section "$domain") + if [ -z "$section" ]; then + json_init; json_add_boolean "success" 0; json_add_string "message" "VHost not found"; json_dump; exit 0 + fi + 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 + fi + config_load vhosts + if [ -z "$backend" ]; then + config_get backend "$section" upstream + fi + if [ -z "$enabled" ]; then + config_get_bool enabled "$section" enabled 1 + fi + 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 - # 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 + tls_mode="${current_tls:-off}" 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) 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 + local section=$(find_section "$domain") + local conf="$NGINX_VHOST_DIR/${domain}.conf" + if [ -n "$section" ]; then + uci delete vhosts.$section + uci commit vhosts fi + rm -f "$conf" + remove_htpasswd "$domain" + json_init; json_add_boolean "success" 1; json_add_boolean "reload_required" 1; json_dump ;; - 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 + if [ -n "$backend" ] && probe_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 + 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 + return fi - - # Request certificate using acme.sh - if acme.sh --issue -d "$domain" --standalone --force 2>&1 | grep -q "success"; then + if acme.sh --issue -d "$domain" --standalone --accountemail "$email" --force >/tmp/acme.log 2>&1; then json_add_boolean "success" 1 - json_add_string "message" "Certificate requested successfully" - json_add_string "domain" "$domain" + json_add_string "message" "Certificate requested" 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-) - + local domain + domain=$(basename "$(dirname "$cert_file")") + local expires issuer subject + 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_string "domain" "$domain" json_add_string "expires" "$expires" @@ -524,62 +677,34 @@ case "$1" in 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 + if nginx -t >/dev/null 2>&1; then + /etc/init.d/nginx reload + json_init; json_add_boolean "success" 1; json_dump else - json_add_boolean "success" 0 - json_add_string "message" "Invalid nginx configuration" + json_init; json_add_boolean "success" 0; json_add_string "message" "nginx config invalid"; json_dump 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" - + lines=${lines:-100} + local 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 + if [ -f "$file" ]; then + tail -n "$lines" "$file" | while read -r line; do json_add_string "" "$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 - ;; + *) json_init; json_add_string "error" "unknown method"; json_dump ;; esac ;; esac diff --git a/luci-app-vhost-manager/root/usr/share/rpcd/acl.d/luci-app-vhost-manager.json b/luci-app-vhost-manager/root/usr/share/rpcd/acl.d/luci-app-vhost-manager.json index 01dbb99..cf2f7c7 100644 --- a/luci-app-vhost-manager/root/usr/share/rpcd/acl.d/luci-app-vhost-manager.json +++ b/luci-app-vhost-manager/root/usr/share/rpcd/acl.d/luci-app-vhost-manager.json @@ -12,7 +12,7 @@ "get_access_logs" ] }, - "uci": ["vhost_manager", "nginx"], + "uci": ["vhosts", "vhost_manager", "nginx"], "file": { "/etc/nginx/conf.d/*": ["read"], "/etc/acme/*": ["read"], @@ -29,7 +29,7 @@ "reload_nginx" ] }, - "uci": ["vhost_manager", "nginx"], + "uci": ["vhosts", "vhost_manager", "nginx"], "file": { "/etc/nginx/conf.d/*": ["write"], "/etc/nginx/.htpasswd_*": ["write"] diff --git a/secubox-tools/acl/luci-app-vhost-manager.json b/secubox-tools/acl/luci-app-vhost-manager.json index c5fdce2..5ff4341 100644 --- a/secubox-tools/acl/luci-app-vhost-manager.json +++ b/secubox-tools/acl/luci-app-vhost-manager.json @@ -11,7 +11,7 @@ "test_config" ] }, - "uci": ["vhost_manager", "nginx"], + "uci": ["vhosts", "vhost_manager", "nginx"], "file": { "/etc/nginx/conf.d/*": ["read"], "/etc/acme/*": ["read"] @@ -26,7 +26,7 @@ "reload_nginx" ] }, - "uci": ["vhost_manager", "nginx"], + "uci": ["vhosts", "vhost_manager", "nginx"], "file": { "/etc/nginx/conf.d/*": ["write"] }