feat(multi): New LuCI apps, MetaBlogizer dual-runtime, service watchdog
- Add luci-app-lyrion: Music server dashboard - Add luci-app-mailinabox: Email server management - Add luci-app-nextcloud: Cloud storage dashboard - Add luci-app-mitmproxy: Security proxy in security section - Add luci-app-magicmirror2: Smart display dashboard - Add secubox-app-metablogizer: CLI tool with uhttpd/nginx support - Update luci-app-metablogizer: Runtime selection, QR codes, social share - Update secubox-core v0.8.1: Service watchdog (auto-restart crashed services) - Update haproxyctl: Hostname validation to prevent config errors - Fix portal.js app discovery Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
585a5d0f6c
commit
fa5d573755
32
package/secubox/luci-app-lyrion/Makefile
Normal file
32
package/secubox/luci-app-lyrion/Makefile
Normal file
@ -0,0 +1,32 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Lyrion Music Server
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-lyrion
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/luci-app-lyrion/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.lyrion $(1)/usr/libexec/rpcd/luci.lyrion
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-lyrion.json $(1)/usr/share/luci/menu.d/luci-app-lyrion.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-lyrion.json $(1)/usr/share/rpcd/acl.d/luci-app-lyrion.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/lyrion
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/lyrion/*.js $(1)/www/luci-static/resources/view/lyrion/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@ -0,0 +1,234 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.lyrion', method: 'status', expect: {} });
|
||||
var callInstall = rpc.declare({ object: 'luci.lyrion', method: 'install', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.lyrion', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.lyrion', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.lyrion', method: 'restart', expect: {} });
|
||||
|
||||
var css = '.ly-container{max-width:900px;margin:0 auto}.ly-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#ec4899 0%,#8b5cf6 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.ly-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.ly-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.ly-status.running{background:rgba(16,185,129,.2)}.ly-status.stopped{background:rgba(239,68,68,.2)}.ly-status.installing{background:rgba(245,158,11,.2)}.ly-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.ly-status.running .ly-dot{background:#10b981}.ly-status.stopped .ly-dot{background:#ef4444}.ly-status.installing .ly-dot{background:#f59e0b}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.ly-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.ly-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.ly-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}.ly-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.ly-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.ly-info-value{font-size:1.1rem;font-weight:500}.ly-actions{display:flex;gap:.75rem;flex-wrap:wrap}.ly-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.ly-btn-primary{background:linear-gradient(135deg,#ec4899,#8b5cf6);color:#fff}.ly-btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(139,92,246,.3)}.ly-btn-success{background:#10b981;color:#fff}.ly-btn-danger{background:#ef4444;color:#fff}.ly-btn-secondary{background:#6b7280;color:#fff}.ly-btn:disabled{opacity:.5;cursor:not-allowed}.ly-webui{display:flex;align-items:center;gap:1rem;padding:1rem;background:linear-gradient(135deg,rgba(236,72,153,.1),rgba(139,92,246,.1));border-radius:12px;margin-top:1rem}.ly-webui-icon{font-size:2rem}.ly-webui-info{flex:1}.ly-webui-url{font-family:monospace;color:#8b5cf6}.ly-not-installed{text-align:center;padding:3rem}.ly-not-installed h3{margin-bottom:1rem;color:#333}.ly-not-installed p{color:#666;margin-bottom:1.5rem}';
|
||||
|
||||
return view.extend({
|
||||
pollActive: true,
|
||||
|
||||
load: function() {
|
||||
return callStatus();
|
||||
},
|
||||
|
||||
startPolling: function() {
|
||||
var self = this;
|
||||
this.pollActive = true;
|
||||
poll.add(L.bind(function() {
|
||||
if (!this.pollActive) return Promise.resolve();
|
||||
return callStatus().then(L.bind(function(status) {
|
||||
this.updateStatus(status);
|
||||
}, this));
|
||||
}, this), 5);
|
||||
},
|
||||
|
||||
updateStatus: function(status) {
|
||||
var badge = document.querySelector('.ly-status');
|
||||
var dot = document.querySelector('.ly-dot');
|
||||
var statusText = document.querySelector('.ly-status-text');
|
||||
|
||||
if (badge && statusText) {
|
||||
badge.className = 'ly-status ' + (status.running ? 'running' : 'stopped');
|
||||
statusText.textContent = status.running ? _('Running') : _('Stopped');
|
||||
}
|
||||
|
||||
// Update info values
|
||||
var updates = {
|
||||
'.ly-val-runtime': status.detected_runtime || 'none',
|
||||
'.ly-val-port': status.port || '9000',
|
||||
'.ly-val-memory': status.memory_limit || '256M'
|
||||
};
|
||||
Object.keys(updates).forEach(function(sel) {
|
||||
var el = document.querySelector(sel);
|
||||
if (el) el.textContent = updates[sel];
|
||||
});
|
||||
},
|
||||
|
||||
handleInstall: function() {
|
||||
var self = this;
|
||||
ui.showModal(_('Installing Lyrion'), [
|
||||
E('p', { 'class': 'spinning' }, _('Installing Lyrion Music Server. This may take several minutes...'))
|
||||
]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
||||
self.startPolling();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown error')), 'error');
|
||||
}
|
||||
}).catch(function(e) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Lyrion...'))]);
|
||||
callStart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Lyrion started')));
|
||||
else ui.addNotification(null, E('p', _('Failed to start')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping Lyrion...'))]);
|
||||
callStop().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Lyrion stopped')));
|
||||
else ui.addNotification(null, E('p', _('Failed to stop')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleRestart: function() {
|
||||
ui.showModal(_('Restarting...'), [E('p', { 'class': 'spinning' }, _('Restarting Lyrion...'))]);
|
||||
callRestart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Lyrion restarted')));
|
||||
else ui.addNotification(null, E('p', _('Failed to restart')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
var self = this;
|
||||
|
||||
if (!document.getElementById('ly-styles')) {
|
||||
var s = document.createElement('style');
|
||||
s.id = 'ly-styles';
|
||||
s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
// Not installed view
|
||||
if (!status.installed) {
|
||||
return E('div', { 'class': 'ly-container' }, [
|
||||
E('div', { 'class': 'ly-header' }, [
|
||||
E('h2', {}, ['\ud83c\udfb5 ', _('Lyrion Music Server')]),
|
||||
E('div', { 'class': 'ly-status stopped' }, [
|
||||
E('span', { 'class': 'ly-dot' }),
|
||||
E('span', { 'class': 'ly-status-text' }, _('Not Installed'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'ly-card' }, [
|
||||
E('div', { 'class': 'ly-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\ud83c\udfb5'),
|
||||
E('h3', {}, _('Lyrion Music Server')),
|
||||
E('p', {}, _('Self-hosted music streaming with Squeezebox/Logitech Media Server compatibility. Stream your music library to any device.')),
|
||||
E('div', { 'class': 'ly-info-grid', 'style': 'margin-bottom:1.5rem;text-align:left' }, [
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Runtime')),
|
||||
E('div', { 'class': 'ly-info-value' }, status.detected_runtime === 'lxc' ? 'LXC Container' : status.detected_runtime === 'docker' ? 'Docker' : _('None detected'))
|
||||
]),
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Data Path')),
|
||||
E('div', { 'class': 'ly-info-value' }, status.data_path || '/srv/lyrion')
|
||||
]),
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Media Path')),
|
||||
E('div', { 'class': 'ly-info-value' }, status.media_path || '/srv/media')
|
||||
])
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'ly-btn ly-btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'handleInstall'),
|
||||
'disabled': status.detected_runtime === 'none'
|
||||
}, _('Install Lyrion'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// Installed view
|
||||
this.startPolling();
|
||||
|
||||
return E('div', { 'class': 'ly-container' }, [
|
||||
E('div', { 'class': 'ly-header' }, [
|
||||
E('h2', {}, ['\ud83c\udfb5 ', _('Lyrion Music Server')]),
|
||||
E('div', { 'class': 'ly-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'ly-dot' }),
|
||||
E('span', { 'class': 'ly-status-text' }, status.running ? _('Running') : _('Stopped'))
|
||||
])
|
||||
]),
|
||||
|
||||
// Info Card
|
||||
E('div', { 'class': 'ly-card' }, [
|
||||
E('div', { 'class': 'ly-card-title' }, ['\u2139\ufe0f ', _('Service Information')]),
|
||||
E('div', { 'class': 'ly-info-grid' }, [
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Runtime')),
|
||||
E('div', { 'class': 'ly-info-value ly-val-runtime' }, status.detected_runtime || 'auto')
|
||||
]),
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Port')),
|
||||
E('div', { 'class': 'ly-info-value ly-val-port' }, status.port || '9000')
|
||||
]),
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Memory Limit')),
|
||||
E('div', { 'class': 'ly-info-value ly-val-memory' }, status.memory_limit || '256M')
|
||||
]),
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Data Path')),
|
||||
E('div', { 'class': 'ly-info-value' }, status.data_path || '/srv/lyrion')
|
||||
]),
|
||||
E('div', { 'class': 'ly-info-item' }, [
|
||||
E('div', { 'class': 'ly-info-label' }, _('Media Path')),
|
||||
E('div', { 'class': 'ly-info-value' }, status.media_path || '/srv/media')
|
||||
])
|
||||
]),
|
||||
|
||||
// Web UI Link
|
||||
status.running && status.web_accessible ? E('div', { 'class': 'ly-webui' }, [
|
||||
E('div', { 'class': 'ly-webui-icon' }, '\ud83c\udf10'),
|
||||
E('div', { 'class': 'ly-webui-info' }, [
|
||||
E('div', { 'style': 'font-weight:600' }, _('Web Interface')),
|
||||
E('div', { 'class': 'ly-webui-url' }, status.web_url)
|
||||
]),
|
||||
E('a', {
|
||||
'href': status.web_url,
|
||||
'target': '_blank',
|
||||
'class': 'ly-btn ly-btn-primary'
|
||||
}, _('Open'))
|
||||
]) : ''
|
||||
]),
|
||||
|
||||
// Actions Card
|
||||
E('div', { 'class': 'ly-card' }, [
|
||||
E('div', { 'class': 'ly-card-title' }, ['\u26a1 ', _('Actions')]),
|
||||
E('div', { 'class': 'ly-actions' }, [
|
||||
E('button', {
|
||||
'class': 'ly-btn ly-btn-success',
|
||||
'click': ui.createHandlerFn(this, 'handleStart'),
|
||||
'disabled': status.running
|
||||
}, _('Start')),
|
||||
E('button', {
|
||||
'class': 'ly-btn ly-btn-danger',
|
||||
'click': ui.createHandlerFn(this, 'handleStop'),
|
||||
'disabled': !status.running
|
||||
}, _('Stop')),
|
||||
E('button', {
|
||||
'class': 'ly-btn ly-btn-secondary',
|
||||
'click': ui.createHandlerFn(this, 'handleRestart'),
|
||||
'disabled': !status.running
|
||||
}, _('Restart')),
|
||||
E('a', {
|
||||
'href': L.url('admin', 'secubox', 'services', 'lyrion', 'settings'),
|
||||
'class': 'ly-btn ly-btn-secondary'
|
||||
}, _('Settings'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.load('lyrion');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('lyrion', _('Lyrion Settings'),
|
||||
_('Configure Lyrion Music Server settings. Changes require service restart to take effect.'));
|
||||
|
||||
s = m.section(form.TypedSection, 'lyrion', _('General Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'),
|
||||
_('Enable Lyrion Music Server'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'runtime', _('Container Runtime'),
|
||||
_('Select the container runtime to use'));
|
||||
o.value('auto', _('Auto-detect (LXC preferred)'));
|
||||
o.value('lxc', _('LXC Container'));
|
||||
o.value('docker', _('Docker'));
|
||||
o.default = 'auto';
|
||||
|
||||
o = s.option(form.Value, 'port', _('Web UI Port'),
|
||||
_('Port for the Lyrion web interface'));
|
||||
o.datatype = 'port';
|
||||
o.default = '9000';
|
||||
o.placeholder = '9000';
|
||||
|
||||
o = s.option(form.Value, 'data_path', _('Data Path'),
|
||||
_('Path to store Lyrion configuration and cache'));
|
||||
o.default = '/srv/lyrion';
|
||||
o.placeholder = '/srv/lyrion';
|
||||
|
||||
o = s.option(form.Value, 'media_path', _('Media Path'),
|
||||
_('Path to your music library'));
|
||||
o.default = '/srv/media';
|
||||
o.placeholder = '/srv/media';
|
||||
|
||||
o = s.option(form.Value, 'memory_limit', _('Memory Limit'),
|
||||
_('Maximum memory for the container (e.g., 256M, 512M, 1G)'));
|
||||
o.default = '256M';
|
||||
o.placeholder = '256M';
|
||||
|
||||
o = s.option(form.Value, 'timezone', _('Timezone'),
|
||||
_('Timezone for the container'));
|
||||
o.default = 'UTC';
|
||||
o.placeholder = 'UTC';
|
||||
|
||||
o = s.option(form.Value, 'image', _('Docker Image'),
|
||||
_('Docker image to use (only for Docker runtime)'));
|
||||
o.default = 'ghcr.io/lms-community/lyrionmusicserver:stable';
|
||||
o.depends('runtime', 'docker');
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,235 @@
|
||||
#!/bin/sh
|
||||
# RPCD backend for Lyrion Music Server LuCI app
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
CONFIG="lyrion"
|
||||
|
||||
json_init() { echo "{"; }
|
||||
json_close() { echo "}"; }
|
||||
json_add_string() { echo "\"$1\": \"$2\""; }
|
||||
json_add_int() { echo "\"$1\": $2"; }
|
||||
json_add_bool() { [ "$2" = "1" ] && echo "\"$1\": true" || echo "\"$1\": false"; }
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local runtime=$(uci_get runtime)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
local media_path=$(uci_get media_path)
|
||||
local memory_limit=$(uci_get memory_limit)
|
||||
local image=$(uci_get image)
|
||||
|
||||
# Check if service is running
|
||||
local running=0
|
||||
local container_status="stopped"
|
||||
|
||||
if command -v lxc-info >/dev/null 2>&1; then
|
||||
if lxc-info -n lyrion -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
running=1
|
||||
container_status="running"
|
||||
fi
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
if docker ps --filter "name=secbx-lyrion" --format "{{.Names}}" 2>/dev/null | grep -q "secbx-lyrion"; then
|
||||
running=1
|
||||
container_status="running"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if installed (LXC rootfs or Docker image exists)
|
||||
local installed=0
|
||||
if [ -d "/srv/lxc/lyrion/rootfs" ] && [ -f "/srv/lxc/lyrion/rootfs/opt/lyrion/slimserver.pl" ]; then
|
||||
installed=1
|
||||
elif command -v docker >/dev/null 2>&1 && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "lyrionmusicserver"; then
|
||||
installed=1
|
||||
fi
|
||||
|
||||
# Detect runtime
|
||||
local detected_runtime="none"
|
||||
if command -v lxc-start >/dev/null 2>&1; then
|
||||
detected_runtime="lxc"
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
detected_runtime="docker"
|
||||
fi
|
||||
|
||||
# Check web UI accessibility
|
||||
local web_accessible=0
|
||||
if [ "$running" = "1" ]; then
|
||||
wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${port:-9000}/" 2>/dev/null && web_accessible=1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||
"container_status": "$container_status",
|
||||
"runtime": "${runtime:-auto}",
|
||||
"detected_runtime": "$detected_runtime",
|
||||
"port": ${port:-9000},
|
||||
"data_path": "${data_path:-/srv/lyrion}",
|
||||
"media_path": "${media_path:-/srv/media}",
|
||||
"memory_limit": "${memory_limit:-256M}",
|
||||
"image": "${image:-ghcr.io/lms-community/lyrionmusicserver:stable}",
|
||||
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"),
|
||||
"web_url": "http://192.168.255.1:${port:-9000}"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get configuration
|
||||
get_config() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local runtime=$(uci_get runtime)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
local media_path=$(uci_get media_path)
|
||||
local memory_limit=$(uci_get memory_limit)
|
||||
local timezone=$(uci_get timezone)
|
||||
local image=$(uci_get image)
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": "${enabled:-0}",
|
||||
"runtime": "${runtime:-auto}",
|
||||
"port": "${port:-9000}",
|
||||
"data_path": "${data_path:-/srv/lyrion}",
|
||||
"media_path": "${media_path:-/srv/media}",
|
||||
"memory_limit": "${memory_limit:-256M}",
|
||||
"timezone": "${timezone:-UTC}",
|
||||
"image": "${image:-ghcr.io/lms-community/lyrionmusicserver:stable}"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Save configuration
|
||||
save_config() {
|
||||
local input
|
||||
read -r input
|
||||
|
||||
local runtime=$(echo "$input" | jsonfilter -e '@.runtime' 2>/dev/null)
|
||||
local port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)
|
||||
local data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null)
|
||||
local media_path=$(echo "$input" | jsonfilter -e '@.media_path' 2>/dev/null)
|
||||
local memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null)
|
||||
local timezone=$(echo "$input" | jsonfilter -e '@.timezone' 2>/dev/null)
|
||||
|
||||
[ -n "$runtime" ] && uci_set runtime "$runtime"
|
||||
[ -n "$port" ] && uci_set port "$port"
|
||||
[ -n "$data_path" ] && uci_set data_path "$data_path"
|
||||
[ -n "$media_path" ] && uci_set media_path "$media_path"
|
||||
[ -n "$memory_limit" ] && uci_set memory_limit "$memory_limit"
|
||||
[ -n "$timezone" ] && uci_set timezone "$timezone"
|
||||
|
||||
echo '{"success": true}'
|
||||
}
|
||||
|
||||
# Install Lyrion
|
||||
do_install() {
|
||||
if command -v lyrionctl >/dev/null 2>&1; then
|
||||
lyrionctl install >/tmp/lyrion-install.log 2>&1 &
|
||||
echo '{"success": true, "message": "Installation started in background"}'
|
||||
else
|
||||
echo '{"success": false, "error": "lyrionctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Start service
|
||||
do_start() {
|
||||
if [ -x /etc/init.d/lyrion ]; then
|
||||
/etc/init.d/lyrion start >/dev/null 2>&1
|
||||
uci_set enabled '1'
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop service
|
||||
do_stop() {
|
||||
if [ -x /etc/init.d/lyrion ]; then
|
||||
/etc/init.d/lyrion stop >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Restart service
|
||||
do_restart() {
|
||||
if [ -x /etc/init.d/lyrion ]; then
|
||||
/etc/init.d/lyrion restart >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Update container
|
||||
do_update() {
|
||||
if command -v lyrionctl >/dev/null 2>&1; then
|
||||
lyrionctl update >/tmp/lyrion-update.log 2>&1 &
|
||||
echo '{"success": true, "message": "Update started in background"}'
|
||||
else
|
||||
echo '{"success": false, "error": "lyrionctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Get logs
|
||||
get_logs() {
|
||||
local lines=50
|
||||
local log_content=""
|
||||
|
||||
if [ -f /srv/lxc/lyrion/rootfs/var/log/lyrion/server.log ]; then
|
||||
log_content=$(tail -n $lines /srv/lxc/lyrion/rootfs/var/log/lyrion/server.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
||||
elif [ -f /tmp/lyrion-install.log ]; then
|
||||
log_content=$(tail -n $lines /tmp/lyrion-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
||||
fi
|
||||
|
||||
echo "{\"logs\": \"$log_content\"}"
|
||||
}
|
||||
|
||||
# RPCD list method
|
||||
list_methods() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
"status": {},
|
||||
"get_config": {},
|
||||
"save_config": {"runtime": "string", "port": "string", "data_path": "string", "media_path": "string", "memory_limit": "string", "timezone": "string"},
|
||||
"install": {},
|
||||
"start": {},
|
||||
"stop": {},
|
||||
"restart": {},
|
||||
"update": {},
|
||||
"logs": {}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main entry point
|
||||
case "$1" in
|
||||
list)
|
||||
list_methods
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
get_config) get_config ;;
|
||||
save_config) save_config ;;
|
||||
install) do_install ;;
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
restart) do_restart ;;
|
||||
update) do_update ;;
|
||||
logs) get_logs ;;
|
||||
*) echo '{"error": "Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Unknown command"}'
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
"admin/secubox/services/lyrion": {
|
||||
"title": "Lyrion",
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-lyrion"]
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/lyrion/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "lyrion/overview"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/lyrion/settings": {
|
||||
"title": "Settings",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "lyrion/settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-lyrion": {
|
||||
"description": "Grant access to Lyrion Music Server",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.lyrion": ["status", "get_config", "logs"]
|
||||
},
|
||||
"uci": ["lyrion"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.lyrion": ["install", "start", "stop", "restart", "update", "save_config"]
|
||||
},
|
||||
"uci": ["lyrion"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.magicmirror2', method: 'status', expect: {} });
|
||||
var callInstall = rpc.declare({ object: 'luci.magicmirror2', method: 'install', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.magicmirror2', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.magicmirror2', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.magicmirror2', method: 'restart', expect: {} });
|
||||
|
||||
var css = '.mm-container{max-width:900px;margin:0 auto}.mm-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.mm-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.mm-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.mm-status.running{background:rgba(16,185,129,.2)}.mm-status.stopped{background:rgba(239,68,68,.2)}.mm-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.mm-status.running .mm-dot{background:#10b981}.mm-status.stopped .mm-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.mm-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.mm-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.mm-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem}.mm-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.mm-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.mm-info-value{font-size:1rem;font-weight:500}.mm-actions{display:flex;gap:.75rem;flex-wrap:wrap}.mm-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.mm-btn-primary{background:linear-gradient(135deg,#1a1a2e,#16213e);color:#fff}.mm-btn-success{background:#10b981;color:#fff}.mm-btn-danger{background:#ef4444;color:#fff}.mm-btn:disabled{opacity:.5;cursor:not-allowed}.mm-not-installed{text-align:center;padding:3rem}.mm-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin:1.5rem 0}.mm-feature{padding:.75rem;background:#e0e7ff;border-radius:8px;font-size:.9rem}';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return callStatus(); },
|
||||
|
||||
handleInstall: function() {
|
||||
ui.showModal(_('Installing MagicMirror'), [E('p', { 'class': 'spinning' }, _('Installing...'))]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting...'))]);
|
||||
callStart().then(function() { ui.hideModal(); location.reload(); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping...'))]);
|
||||
callStop().then(function() { ui.hideModal(); location.reload(); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
if (!document.getElementById('mm-styles')) {
|
||||
var s = document.createElement('style'); s.id = 'mm-styles'; s.textContent = css; document.head.appendChild(s);
|
||||
}
|
||||
|
||||
if (!status.installed || !status.docker_available) {
|
||||
return E('div', { 'class': 'mm-container' }, [
|
||||
E('div', { 'class': 'mm-header' }, [
|
||||
E('h2', {}, ['\uD83E\uDE9E ', _('MagicMirror')]),
|
||||
E('div', { 'class': 'mm-status stopped' }, [E('span', { 'class': 'mm-dot' }), _('Not Installed')])
|
||||
]),
|
||||
E('div', { 'class': 'mm-card' }, [
|
||||
E('div', { 'class': 'mm-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\uD83E\uDE9E'),
|
||||
E('h3', {}, _('MagicMirror\u00B2')),
|
||||
E('p', {}, _('Open source modular smart mirror platform with customizable widgets.')),
|
||||
E('div', { 'class': 'mm-features' }, [
|
||||
E('div', { 'class': 'mm-feature' }, '\u2600 Weather'),
|
||||
E('div', { 'class': 'mm-feature' }, '\uD83D\uDCC5 Calendar'),
|
||||
E('div', { 'class': 'mm-feature' }, '\uD83D\uDCF0 News'),
|
||||
E('div', { 'class': 'mm-feature' }, '\u23F0 Clock'),
|
||||
E('div', { 'class': 'mm-feature' }, '\uD83D\uDDE3 Compliments'),
|
||||
E('div', { 'class': 'mm-feature' }, '\uD83D\uDD0C Modules')
|
||||
]),
|
||||
!status.docker_available ? E('div', { 'style': 'color:#ef4444;margin-bottom:1rem' }, _('Docker required')) : '',
|
||||
E('button', { 'class': 'mm-btn mm-btn-primary', 'click': ui.createHandlerFn(this, 'handleInstall'), 'disabled': !status.docker_available }, _('Install MagicMirror'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'mm-container' }, [
|
||||
E('div', { 'class': 'mm-header' }, [
|
||||
E('h2', {}, ['\uD83E\uDE9E ', _('MagicMirror')]),
|
||||
E('div', { 'class': 'mm-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'mm-dot' }),
|
||||
status.running ? _('Running') : _('Stopped')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mm-card' }, [
|
||||
E('div', { 'class': 'mm-card-title' }, ['\u2139\uFE0F ', _('Configuration')]),
|
||||
E('div', { 'class': 'mm-info-grid' }, [
|
||||
E('div', { 'class': 'mm-info-item' }, [E('div', { 'class': 'mm-info-label' }, _('Port')), E('div', { 'class': 'mm-info-value' }, String(status.port))]),
|
||||
E('div', { 'class': 'mm-info-item' }, [E('div', { 'class': 'mm-info-label' }, _('Data Path')), E('div', { 'class': 'mm-info-value' }, status.data_path)]),
|
||||
E('div', { 'class': 'mm-info-item' }, [E('div', { 'class': 'mm-info-label' }, _('Access')), E('div', { 'class': 'mm-info-value' }, [E('a', { 'href': 'http://' + window.location.hostname + ':' + status.port, 'target': '_blank' }, _('Open Mirror'))])])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mm-card' }, [
|
||||
E('div', { 'class': 'mm-card-title' }, ['\u26A1 ', _('Actions')]),
|
||||
E('div', { 'class': 'mm-actions' }, [
|
||||
E('button', { 'class': 'mm-btn mm-btn-success', 'click': ui.createHandlerFn(this, 'handleStart'), 'disabled': status.running }, _('Start')),
|
||||
E('button', { 'class': 'mm-btn mm-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop'), 'disabled': !status.running }, _('Stop'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null, handleSave: null, handleReset: null
|
||||
});
|
||||
@ -1,427 +1,54 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# RPCD backend for MagicMirror2 LuCI interface
|
||||
# Copyright (C) 2026 CyberMind.fr (SecuBox)
|
||||
#
|
||||
# RPCD backend for MagicMirror2 LuCI app
|
||||
|
||||
. /lib/functions.sh
|
||||
CONFIG="magicmirror2"
|
||||
CONTAINER="secbx-magicmirror"
|
||||
|
||||
DATA_DIR=$(uci -q get magicmirror2.main.data_path || echo "/srv/magicmirror2")
|
||||
LXC_NAME="magicmirror2"
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
|
||||
local docker_available=0
|
||||
command -v docker >/dev/null 2>&1 && docker_available=1
|
||||
|
||||
local running=0
|
||||
local pid=""
|
||||
local lxc_state=""
|
||||
local web_url=""
|
||||
|
||||
# Check LXC container status
|
||||
if command -v lxc-info >/dev/null 2>&1; then
|
||||
lxc_state=$(lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -oE 'RUNNING|STOPPED' || echo "UNKNOWN")
|
||||
if [ "$lxc_state" = "RUNNING" ]; then
|
||||
running=1
|
||||
pid=$(lxc-info -n "$LXC_NAME" -p 2>/dev/null | grep -oE '[0-9]+' || echo "0")
|
||||
fi
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER" && running=1
|
||||
fi
|
||||
|
||||
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
|
||||
local port=$(uci -q get magicmirror2.main.port || echo "8085")
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
local installed=0
|
||||
[ "$docker_available" = "1" ] && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "magicmirror" && installed=1
|
||||
|
||||
[ "$running" = "1" ] && web_url="http://${router_ip}:${port}"
|
||||
|
||||
# Count installed modules
|
||||
local module_count=0
|
||||
if [ -d "$DATA_DIR/modules" ]; then
|
||||
module_count=$(ls -d "$DATA_DIR/modules"/MMM-* "$DATA_DIR/modules"/mm-* 2>/dev/null | wc -l)
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
cat <<EOFJ
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"pid": ${pid:-0},
|
||||
"lxc_state": "$lxc_state",
|
||||
"port": $port,
|
||||
"web_url": "$web_url",
|
||||
"module_count": $module_count,
|
||||
"data_path": "$DATA_DIR"
|
||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||
"port": ${port:-8080},
|
||||
"data_path": "${data_path:-/srv/magicmirror}"
|
||||
}
|
||||
EOF
|
||||
EOFJ
|
||||
}
|
||||
|
||||
# Get main configuration
|
||||
get_config() {
|
||||
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
|
||||
local port=$(uci -q get magicmirror2.main.port || echo "8085")
|
||||
local address=$(uci -q get magicmirror2.main.address || echo "0.0.0.0")
|
||||
local data_path=$(uci -q get magicmirror2.main.data_path || echo "/srv/magicmirror2")
|
||||
local memory_limit=$(uci -q get magicmirror2.main.memory_limit || echo "512M")
|
||||
local language=$(uci -q get magicmirror2.main.language || echo "en")
|
||||
local timezone=$(uci -q get magicmirror2.main.timezone || echo "Europe/Paris")
|
||||
local units=$(uci -q get magicmirror2.main.units || echo "metric")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"port": $port,
|
||||
"address": "$address",
|
||||
"data_path": "$data_path",
|
||||
"memory_limit": "$memory_limit",
|
||||
"language": "$language",
|
||||
"timezone": "$timezone",
|
||||
"units": "$units"
|
||||
}
|
||||
EOF
|
||||
do_install() {
|
||||
command -v magicmirror2ctl >/dev/null 2>&1 && { magicmirror2ctl install >/tmp/mm2-install.log 2>&1 & echo '{"success":true,"message":"Installing"}'; } || echo '{"success":false,"error":"magicmirror2ctl not found"}'
|
||||
}
|
||||
|
||||
# Get display configuration
|
||||
get_display_config() {
|
||||
local width=$(uci -q get magicmirror2.display.width || echo "1920")
|
||||
local height=$(uci -q get magicmirror2.display.height || echo "1080")
|
||||
local zoom=$(uci -q get magicmirror2.display.zoom || echo "1.0")
|
||||
local brightness=$(uci -q get magicmirror2.display.brightness || echo "100")
|
||||
do_start() { [ -x /etc/init.d/magicmirror2 ] && /etc/init.d/magicmirror2 start >/dev/null 2>&1; echo '{"success":true}'; }
|
||||
do_stop() { [ -x /etc/init.d/magicmirror2 ] && /etc/init.d/magicmirror2 stop >/dev/null 2>&1; echo '{"success":true}'; }
|
||||
do_restart() { [ -x /etc/init.d/magicmirror2 ] && /etc/init.d/magicmirror2 restart >/dev/null 2>&1; echo '{"success":true}'; }
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"width": $width,
|
||||
"height": $height,
|
||||
"zoom": $zoom,
|
||||
"brightness": $brightness
|
||||
}
|
||||
EOF
|
||||
list_methods() { cat <<'EOFM'
|
||||
{"status":{},"install":{},"start":{},"stop":{},"restart":{}}
|
||||
EOFM
|
||||
}
|
||||
|
||||
# Get weather configuration
|
||||
get_weather_config() {
|
||||
local enabled=$(uci -q get magicmirror2.weather.enabled || echo "0")
|
||||
local provider=$(uci -q get magicmirror2.weather.provider || echo "openweathermap")
|
||||
local api_key=$(uci -q get magicmirror2.weather.api_key || echo "")
|
||||
local location=$(uci -q get magicmirror2.weather.location || echo "")
|
||||
local location_id=$(uci -q get magicmirror2.weather.location_id || echo "")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"provider": "$provider",
|
||||
"api_key": "$api_key",
|
||||
"location": "$location",
|
||||
"location_id": "$location_id"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get module configuration
|
||||
get_modules_config() {
|
||||
cat <<EOF
|
||||
{
|
||||
"clock": {
|
||||
"enabled": $([ "$(uci -q get magicmirror2.clock.enabled || echo 1)" = "1" ] && echo "true" || echo "false"),
|
||||
"display_seconds": $([ "$(uci -q get magicmirror2.clock.display_seconds || echo 1)" = "1" ] && echo "true" || echo "false"),
|
||||
"show_date": $([ "$(uci -q get magicmirror2.clock.show_date || echo 1)" = "1" ] && echo "true" || echo "false")
|
||||
},
|
||||
"calendar": {
|
||||
"enabled": $([ "$(uci -q get magicmirror2.calendar.enabled || echo 0)" = "1" ] && echo "true" || echo "false"),
|
||||
"max_entries": $(uci -q get magicmirror2.calendar.max_entries || echo 10)
|
||||
},
|
||||
"newsfeed": {
|
||||
"enabled": $([ "$(uci -q get magicmirror2.newsfeed.enabled || echo 0)" = "1" ] && echo "true" || echo "false"),
|
||||
"max_news_items": $(uci -q get magicmirror2.newsfeed.max_news_items || echo 5)
|
||||
},
|
||||
"compliments": {
|
||||
"enabled": $([ "$(uci -q get magicmirror2.compliments.enabled || echo 1)" = "1" ] && echo "true" || echo "false")
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# List installed modules
|
||||
get_installed_modules() {
|
||||
local modules_dir="$DATA_DIR/modules"
|
||||
|
||||
echo '{"modules":['
|
||||
|
||||
local first=1
|
||||
if [ -d "$modules_dir" ]; then
|
||||
# List MMM-* modules
|
||||
for module_dir in "$modules_dir"/MMM-*; do
|
||||
[ -d "$module_dir" ] || continue
|
||||
[ -f "$module_dir/package.json" ] || continue
|
||||
local name=$(basename "$module_dir")
|
||||
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
|
||||
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 100 | sed 's/"/\\"/g')
|
||||
local author=$(jsonfilter -i "$module_dir/package.json" -e '@.author' 2>/dev/null | sed 's/"/\\"/g')
|
||||
local has_config="false"
|
||||
[ -f "$DATA_DIR/config/${name}.json" ] && has_config="true"
|
||||
|
||||
[ "$first" = "1" ] || echo ","
|
||||
first=0
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"name": "$name",
|
||||
"version": "$version",
|
||||
"description": "$desc",
|
||||
"author": "$author",
|
||||
"has_config": $has_config,
|
||||
"path": "$module_dir"
|
||||
}
|
||||
EOF
|
||||
done
|
||||
# List mm-* modules
|
||||
for module_dir in "$modules_dir"/mm-*; do
|
||||
[ -d "$module_dir" ] || continue
|
||||
[ -f "$module_dir/package.json" ] || continue
|
||||
local name=$(basename "$module_dir")
|
||||
local version=$(jsonfilter -i "$module_dir/package.json" -e '@.version' 2>/dev/null || echo "unknown")
|
||||
local desc=$(jsonfilter -i "$module_dir/package.json" -e '@.description' 2>/dev/null | head -c 100 | sed 's/"/\\"/g')
|
||||
local author=$(jsonfilter -i "$module_dir/package.json" -e '@.author' 2>/dev/null | sed 's/"/\\"/g')
|
||||
local has_config="false"
|
||||
[ -f "$DATA_DIR/config/${name}.json" ] && has_config="true"
|
||||
|
||||
[ "$first" = "1" ] || echo ","
|
||||
first=0
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"name": "$name",
|
||||
"version": "$version",
|
||||
"description": "$desc",
|
||||
"author": "$author",
|
||||
"has_config": $has_config,
|
||||
"path": "$module_dir"
|
||||
}
|
||||
EOF
|
||||
done
|
||||
fi
|
||||
|
||||
echo ']}'
|
||||
}
|
||||
|
||||
# Service control
|
||||
service_start() {
|
||||
/etc/init.d/magicmirror2 start >/dev/null 2>&1
|
||||
sleep 3
|
||||
get_status
|
||||
}
|
||||
|
||||
service_stop() {
|
||||
/etc/init.d/magicmirror2 stop >/dev/null 2>&1
|
||||
sleep 1
|
||||
get_status
|
||||
}
|
||||
|
||||
service_restart() {
|
||||
/etc/init.d/magicmirror2 restart >/dev/null 2>&1
|
||||
sleep 3
|
||||
get_status
|
||||
}
|
||||
|
||||
# Install module
|
||||
install_module() {
|
||||
local module_name="$1"
|
||||
|
||||
if [ -z "$module_name" ]; then
|
||||
echo '{"success":false,"message":"Module name required"}'
|
||||
return
|
||||
fi
|
||||
|
||||
local result=$(/usr/sbin/mm2ctl module install "$module_name" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Module installed successfully"}'
|
||||
else
|
||||
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove module
|
||||
remove_module() {
|
||||
local module_name="$1"
|
||||
|
||||
if [ -z "$module_name" ]; then
|
||||
echo '{"success":false,"message":"Module name required"}'
|
||||
return
|
||||
fi
|
||||
|
||||
local result=$(/usr/sbin/mm2ctl module remove "$module_name" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Module removed successfully"}'
|
||||
else
|
||||
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Update module(s)
|
||||
update_modules() {
|
||||
local module_name="$1"
|
||||
|
||||
local result=$(/usr/sbin/mm2ctl module update $module_name 2>&1)
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Modules updated successfully"}'
|
||||
else
|
||||
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
|
||||
echo "{\"success\":false,\"message\":\"$escaped\"}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Regenerate config
|
||||
regenerate_config() {
|
||||
/usr/sbin/mm2ctl config >/dev/null 2>&1
|
||||
echo '{"success":true,"message":"Configuration regenerated"}'
|
||||
}
|
||||
|
||||
# Set configuration
|
||||
set_config() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
|
||||
if [ -z "$key" ] || [ -z "$value" ]; then
|
||||
echo '{"success":false,"message":"Key and value required"}'
|
||||
return
|
||||
fi
|
||||
|
||||
# Handle boolean conversion
|
||||
case "$value" in
|
||||
true) value="1" ;;
|
||||
false) value="0" ;;
|
||||
esac
|
||||
|
||||
# Determine section based on key
|
||||
local section="main"
|
||||
case "$key" in
|
||||
width|height|zoom|brightness)
|
||||
section="display"
|
||||
;;
|
||||
provider|api_key|location|location_id)
|
||||
section="weather"
|
||||
;;
|
||||
display_seconds|show_date|show_week)
|
||||
section="clock"
|
||||
;;
|
||||
max_entries|fetch_interval)
|
||||
section="calendar"
|
||||
;;
|
||||
max_news_items|show_description|show_source_title)
|
||||
section="newsfeed"
|
||||
;;
|
||||
update_interval)
|
||||
section="compliments"
|
||||
;;
|
||||
esac
|
||||
|
||||
uci set "magicmirror2.$section.$key=$value"
|
||||
uci commit magicmirror2
|
||||
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Get web URL for iframe
|
||||
get_web_url() {
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
local port=$(uci -q get magicmirror2.main.port || echo "8085")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"web_url": "http://$router_ip:$port",
|
||||
"port": $port
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# RPCD list method
|
||||
case "$1" in
|
||||
list)
|
||||
cat <<EOF
|
||||
{
|
||||
"get_status": {},
|
||||
"get_config": {},
|
||||
"get_display_config": {},
|
||||
"get_weather_config": {},
|
||||
"get_modules_config": {},
|
||||
"get_installed_modules": {},
|
||||
"get_web_url": {},
|
||||
"service_start": {},
|
||||
"service_stop": {},
|
||||
"service_restart": {},
|
||||
"install_module": {"name": "string"},
|
||||
"remove_module": {"name": "string"},
|
||||
"update_modules": {"name": "string"},
|
||||
"regenerate_config": {},
|
||||
"set_config": {"key": "string", "value": "string"}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
get_status)
|
||||
get_status
|
||||
;;
|
||||
get_config)
|
||||
get_config
|
||||
;;
|
||||
get_display_config)
|
||||
get_display_config
|
||||
;;
|
||||
get_weather_config)
|
||||
get_weather_config
|
||||
;;
|
||||
get_modules_config)
|
||||
get_modules_config
|
||||
;;
|
||||
get_installed_modules)
|
||||
get_installed_modules
|
||||
;;
|
||||
get_web_url)
|
||||
get_web_url
|
||||
;;
|
||||
service_start)
|
||||
service_start
|
||||
;;
|
||||
service_stop)
|
||||
service_stop
|
||||
;;
|
||||
service_restart)
|
||||
service_restart
|
||||
;;
|
||||
install_module)
|
||||
read -r input
|
||||
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
install_module "$name"
|
||||
;;
|
||||
remove_module)
|
||||
read -r input
|
||||
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
remove_module "$name"
|
||||
;;
|
||||
update_modules)
|
||||
read -r input
|
||||
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
update_modules "$name"
|
||||
;;
|
||||
regenerate_config)
|
||||
regenerate_config
|
||||
;;
|
||||
set_config)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
set_config "$key" "$value"
|
||||
;;
|
||||
*)
|
||||
echo '{"error":"Unknown method"}'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo '{"error":"Unknown command"}'
|
||||
;;
|
||||
list) list_methods ;;
|
||||
call) case "$2" in status) get_status ;; install) do_install ;; start) do_start ;; stop) do_stop ;; restart) do_restart ;; *) echo '{"error":"Unknown method"}' ;; esac ;;
|
||||
*) echo '{"error":"Unknown command"}' ;;
|
||||
esac
|
||||
|
||||
@ -1,45 +1,8 @@
|
||||
{
|
||||
"admin/secubox/services/magicmirror2": {
|
||||
"title": "MagicMirror2",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-magicmirror2"],
|
||||
"uci": {"magicmirror2": true}
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/magicmirror2/dashboard": {
|
||||
"title": "Dashboard",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "magicmirror2/dashboard"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/magicmirror2/webui": {
|
||||
"title": "Display",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "magicmirror2/webui"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/magicmirror2/modules": {
|
||||
"title": "Modules",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "magicmirror2/modules"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/magicmirror2/settings": {
|
||||
"title": "Settings",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "magicmirror2/settings"
|
||||
}
|
||||
"title": "MagicMirror",
|
||||
"action": { "type": "view", "path": "magicmirror2/overview" },
|
||||
"depends": { "acl": ["luci-app-magicmirror2"] },
|
||||
"order": 70
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,7 @@
|
||||
{
|
||||
"luci-app-magicmirror2": {
|
||||
"description": "Grant access to MagicMirror2 dashboard",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.magicmirror2": [
|
||||
"get_status",
|
||||
"get_config",
|
||||
"get_display_config",
|
||||
"get_weather_config",
|
||||
"get_modules_config",
|
||||
"get_installed_modules",
|
||||
"get_web_url"
|
||||
]
|
||||
},
|
||||
"uci": ["magicmirror2"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.magicmirror2": [
|
||||
"service_start",
|
||||
"service_stop",
|
||||
"service_restart",
|
||||
"install_module",
|
||||
"remove_module",
|
||||
"update_modules",
|
||||
"regenerate_config",
|
||||
"set_config"
|
||||
]
|
||||
},
|
||||
"uci": ["magicmirror2"]
|
||||
}
|
||||
"description": "Grant access to MagicMirror2",
|
||||
"read": { "ubus": { "luci.magicmirror2": ["status"] }, "uci": ["magicmirror2"] },
|
||||
"write": { "ubus": { "luci.magicmirror2": ["install", "start", "stop", "restart"] }, "uci": ["magicmirror2"] }
|
||||
}
|
||||
}
|
||||
|
||||
32
package/secubox/luci-app-mailinabox/Makefile
Normal file
32
package/secubox/luci-app-mailinabox/Makefile
Normal file
@ -0,0 +1,32 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Mail-in-a-Box
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-mailinabox
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/luci-app-mailinabox/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.mailinabox $(1)/usr/libexec/rpcd/luci.mailinabox
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-mailinabox.json $(1)/usr/share/luci/menu.d/luci-app-mailinabox.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-mailinabox.json $(1)/usr/share/rpcd/acl.d/luci-app-mailinabox.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/mailinabox
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/mailinabox/*.js $(1)/www/luci-static/resources/view/mailinabox/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.mailinabox', method: 'status', expect: {} });
|
||||
var callInstall = rpc.declare({ object: 'luci.mailinabox', method: 'install', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.mailinabox', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.mailinabox', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.mailinabox', method: 'restart', expect: {} });
|
||||
|
||||
var css = '.mb-container{max-width:900px;margin:0 auto}.mb-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#3b82f6 0%,#1d4ed8 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.mb-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.mb-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.mb-status.running{background:rgba(16,185,129,.2)}.mb-status.stopped{background:rgba(239,68,68,.2)}.mb-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.mb-status.running .mb-dot{background:#10b981}.mb-status.stopped .mb-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.mb-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.mb-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.mb-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem}.mb-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.mb-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.mb-info-value{font-size:1rem;font-weight:500}.mb-actions{display:flex;gap:.75rem;flex-wrap:wrap}.mb-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.mb-btn-primary{background:linear-gradient(135deg,#3b82f6,#1d4ed8);color:#fff}.mb-btn-success{background:#10b981;color:#fff}.mb-btn-danger{background:#ef4444;color:#fff}.mb-btn-secondary{background:#6b7280;color:#fff}.mb-btn:disabled{opacity:.5;cursor:not-allowed}.mb-ports{display:flex;gap:1rem;flex-wrap:wrap;margin-top:1rem}.mb-port{padding:.5rem 1rem;background:#e0e7ff;border-radius:8px;font-size:.85rem}.mb-port-name{font-weight:600;color:#3b82f6}.mb-not-installed{text-align:center;padding:3rem}.mb-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin:1.5rem 0;text-align:left}.mb-feature{padding:.75rem;background:#eff6ff;border-radius:8px;font-size:.9rem}';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return callStatus(); },
|
||||
|
||||
handleInstall: function() {
|
||||
ui.showModal(_('Installing Mail Server'), [E('p', { 'class': 'spinning' }, _('Installing...'))]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', r.message || _('Started')));
|
||||
else ui.addNotification(null, E('p', _('Failed: ') + r.error), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting...'))]);
|
||||
callStart().then(function(r) { ui.hideModal(); ui.addNotification(null, E('p', _('Started'))); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping...'))]);
|
||||
callStop().then(function(r) { ui.hideModal(); ui.addNotification(null, E('p', _('Stopped'))); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
if (!document.getElementById('mb-mail-styles')) {
|
||||
var s = document.createElement('style'); s.id = 'mb-mail-styles'; s.textContent = css; document.head.appendChild(s);
|
||||
}
|
||||
|
||||
if (!status.installed || !status.docker_available) {
|
||||
return E('div', { 'class': 'mb-container' }, [
|
||||
E('div', { 'class': 'mb-header' }, [
|
||||
E('h2', {}, ['\ud83d\udce7 ', _('Mail Server')]),
|
||||
E('div', { 'class': 'mb-status stopped' }, [E('span', { 'class': 'mb-dot' }), E('span', {}, _('Not Installed'))])
|
||||
]),
|
||||
E('div', { 'class': 'mb-card' }, [
|
||||
E('div', { 'class': 'mb-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\ud83d\udce7'),
|
||||
E('h3', {}, _('Mail-in-a-Box')),
|
||||
E('p', {}, _('Self-hosted email server with SMTP, IMAP, spam filtering, and webmail.')),
|
||||
E('div', { 'class': 'mb-features' }, [
|
||||
E('div', { 'class': 'mb-feature' }, '\ud83d\udce4 SMTP'),
|
||||
E('div', { 'class': 'mb-feature' }, '\ud83d\udce5 IMAP'),
|
||||
E('div', { 'class': 'mb-feature' }, '\ud83d\udee1 SpamAssassin'),
|
||||
E('div', { 'class': 'mb-feature' }, '\ud83d\udd12 SSL/TLS'),
|
||||
E('div', { 'class': 'mb-feature' }, '\ud83c\udf10 Webmail'),
|
||||
E('div', { 'class': 'mb-feature' }, '\ud83d\udc80 Fail2ban')
|
||||
]),
|
||||
!status.docker_available ? E('div', { 'style': 'color:#ef4444;margin-bottom:1rem' }, _('Docker required')) : '',
|
||||
E('button', { 'class': 'mb-btn mb-btn-primary', 'click': ui.createHandlerFn(this, 'handleInstall'), 'disabled': !status.docker_available }, _('Install Mail Server'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'mb-container' }, [
|
||||
E('div', { 'class': 'mb-header' }, [
|
||||
E('h2', {}, ['\ud83d\udce7 ', _('Mail Server')]),
|
||||
E('div', { 'class': 'mb-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'mb-dot' }),
|
||||
E('span', {}, status.running ? _('Running') : _('Stopped'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mb-card' }, [
|
||||
E('div', { 'class': 'mb-card-title' }, ['\u2139\ufe0f ', _('Configuration')]),
|
||||
E('div', { 'class': 'mb-info-grid' }, [
|
||||
E('div', { 'class': 'mb-info-item' }, [E('div', { 'class': 'mb-info-label' }, _('Hostname')), E('div', { 'class': 'mb-info-value' }, status.hostname)]),
|
||||
E('div', { 'class': 'mb-info-item' }, [E('div', { 'class': 'mb-info-label' }, _('Domain')), E('div', { 'class': 'mb-info-value' }, status.domain)]),
|
||||
E('div', { 'class': 'mb-info-item' }, [E('div', { 'class': 'mb-info-label' }, _('Data Path')), E('div', { 'class': 'mb-info-value' }, status.data_path)])
|
||||
]),
|
||||
E('div', { 'class': 'mb-ports' }, [
|
||||
E('div', { 'class': 'mb-port' }, [E('span', { 'class': 'mb-port-name' }, 'SMTP'), ' :' + status.smtp_port]),
|
||||
E('div', { 'class': 'mb-port' }, [E('span', { 'class': 'mb-port-name' }, 'IMAP'), ' :' + status.imap_port]),
|
||||
E('div', { 'class': 'mb-port' }, [E('span', { 'class': 'mb-port-name' }, 'IMAPS'), ' :' + status.imaps_port])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mb-card' }, [
|
||||
E('div', { 'class': 'mb-card-title' }, ['\u26a1 ', _('Actions')]),
|
||||
E('div', { 'class': 'mb-actions' }, [
|
||||
E('button', { 'class': 'mb-btn mb-btn-success', 'click': ui.createHandlerFn(this, 'handleStart'), 'disabled': status.running }, _('Start')),
|
||||
E('button', { 'class': 'mb-btn mb-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop'), 'disabled': !status.running }, _('Stop')),
|
||||
E('a', { 'href': L.url('admin', 'secubox', 'services', 'mailinabox', 'settings'), 'class': 'mb-btn mb-btn-secondary' }, _('Settings'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null, handleSave: null, handleReset: null
|
||||
});
|
||||
@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return uci.load('mailinabox'); },
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('mailinabox', _('Mail Server Settings'),
|
||||
_('Configure your mail server. IMPORTANT: Set hostname and domain before installing.'));
|
||||
|
||||
s = m.section(form.TypedSection, 'mailinabox', _('General Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Value, 'hostname', _('Mail Hostname'),
|
||||
_('Full hostname for mail server (e.g., mail.example.com)'));
|
||||
o.default = 'mail.example.com';
|
||||
|
||||
o = s.option(form.Value, 'domain', _('Domain'),
|
||||
_('Primary email domain (e.g., example.com)'));
|
||||
o.default = 'example.com';
|
||||
|
||||
o = s.option(form.Value, 'data_path', _('Data Path'));
|
||||
o.default = '/srv/mailserver';
|
||||
|
||||
o = s.option(form.Value, 'timezone', _('Timezone'));
|
||||
o.default = 'UTC';
|
||||
|
||||
s = m.section(form.TypedSection, 'mailinabox', _('Ports'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'smtp_port', _('SMTP Port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '25';
|
||||
|
||||
o = s.option(form.Value, 'submission_port', _('Submission Port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '587';
|
||||
|
||||
o = s.option(form.Value, 'imap_port', _('IMAP Port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '143';
|
||||
|
||||
o = s.option(form.Value, 'imaps_port', _('IMAPS Port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '993';
|
||||
|
||||
s = m.section(form.TypedSection, 'mailinabox', _('Features'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Flag, 'enable_spamassassin', _('SpamAssassin'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'enable_clamav', _('ClamAV Antivirus'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Flag, 'enable_fail2ban', _('Fail2ban'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.ListValue, 'ssl_type', _('SSL Type'));
|
||||
o.value('letsencrypt', _("Let's Encrypt"));
|
||||
o.value('manual', _('Manual'));
|
||||
o.value('self-signed', _('Self-signed'));
|
||||
o.default = 'letsencrypt';
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,139 @@
|
||||
#!/bin/sh
|
||||
# RPCD backend for Mail-in-a-Box LuCI app
|
||||
|
||||
CONFIG="mailinabox"
|
||||
CONTAINER="secbx-mailserver"
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
get_status() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local hostname=$(uci_get hostname)
|
||||
local domain=$(uci_get domain)
|
||||
local data_path=$(uci_get data_path)
|
||||
|
||||
local docker_available=0
|
||||
command -v docker >/dev/null 2>&1 && docker_available=1
|
||||
|
||||
local running=0
|
||||
local container_status="stopped"
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
if docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then
|
||||
running=1
|
||||
container_status="running"
|
||||
fi
|
||||
fi
|
||||
|
||||
local installed=0
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
docker images --format "{{.Repository}}" 2>/dev/null | grep -q "docker-mailserver" && installed=1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||
"container_status": "$container_status",
|
||||
"hostname": "${hostname:-mail.example.com}",
|
||||
"domain": "${domain:-example.com}",
|
||||
"data_path": "${data_path:-/srv/mailserver}",
|
||||
"smtp_port": $(uci_get smtp_port || echo 25),
|
||||
"imap_port": $(uci_get imap_port || echo 143),
|
||||
"imaps_port": $(uci_get imaps_port || echo 993)
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
get_config() {
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": "$(uci_get enabled || echo 0)",
|
||||
"hostname": "$(uci_get hostname || echo mail.example.com)",
|
||||
"domain": "$(uci_get domain || echo example.com)",
|
||||
"data_path": "$(uci_get data_path || echo /srv/mailserver)",
|
||||
"timezone": "$(uci_get timezone || echo UTC)",
|
||||
"smtp_port": "$(uci_get smtp_port || echo 25)",
|
||||
"submission_port": "$(uci_get submission_port || echo 587)",
|
||||
"imap_port": "$(uci_get imap_port || echo 143)",
|
||||
"imaps_port": "$(uci_get imaps_port || echo 993)",
|
||||
"enable_spamassassin": "$(uci_get enable_spamassassin || echo 1)",
|
||||
"enable_clamav": "$(uci_get enable_clamav || echo 0)",
|
||||
"ssl_type": "$(uci_get ssl_type || echo letsencrypt)"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
save_config() {
|
||||
local input; read -r input
|
||||
local hostname=$(echo "$input" | jsonfilter -e '@.hostname' 2>/dev/null)
|
||||
local domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
|
||||
[ -n "$hostname" ] && uci_set hostname "$hostname"
|
||||
[ -n "$domain" ] && uci_set domain "$domain"
|
||||
echo '{"success": true}'
|
||||
}
|
||||
|
||||
do_install() {
|
||||
if command -v mailinaboxctl >/dev/null 2>&1; then
|
||||
mailinaboxctl install >/tmp/mailinabox-install.log 2>&1 &
|
||||
echo '{"success": true, "message": "Installation started"}'
|
||||
else
|
||||
echo '{"success": false, "error": "mailinaboxctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
do_start() {
|
||||
[ -x /etc/init.d/mailinabox ] && /etc/init.d/mailinabox start >/dev/null 2>&1 && uci_set enabled '1'
|
||||
echo '{"success": true}'
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
[ -x /etc/init.d/mailinabox ] && /etc/init.d/mailinabox stop >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
}
|
||||
|
||||
do_restart() {
|
||||
[ -x /etc/init.d/mailinabox ] && /etc/init.d/mailinabox restart >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
}
|
||||
|
||||
get_logs() {
|
||||
local log_content=""
|
||||
[ -f /tmp/mailinabox-install.log ] && log_content=$(tail -n 50 /tmp/mailinabox-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
||||
echo "{\"logs\": \"$log_content\"}"
|
||||
}
|
||||
|
||||
list_methods() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
"status": {},
|
||||
"get_config": {},
|
||||
"save_config": {"hostname": "string", "domain": "string"},
|
||||
"install": {},
|
||||
"start": {},
|
||||
"stop": {},
|
||||
"restart": {},
|
||||
"logs": {}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list) list_methods ;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
get_config) get_config ;;
|
||||
save_config) save_config ;;
|
||||
install) do_install ;;
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
restart) do_restart ;;
|
||||
logs) get_logs ;;
|
||||
*) echo '{"error": "Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
*) echo '{"error": "Unknown command"}' ;;
|
||||
esac
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
"admin/secubox/services/mailinabox": {
|
||||
"title": "Mail Server",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-mailinabox"]
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/mailinabox/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mailinabox/overview"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/mailinabox/settings": {
|
||||
"title": "Settings",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mailinabox/settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-mailinabox": {
|
||||
"description": "Grant access to Mail-in-a-Box",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.mailinabox": ["status", "get_config", "logs"]
|
||||
},
|
||||
"uci": ["mailinabox"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.mailinabox": ["install", "start", "stop", "restart", "update", "save_config", "add_account", "list_accounts"]
|
||||
},
|
||||
"uci": ["mailinabox"]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -24,11 +24,12 @@ return view.extend({
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'nginx_container', _('Nginx Container'),
|
||||
_('Name of the LXC container running nginx'));
|
||||
o.placeholder = 'nginx';
|
||||
o.default = 'nginx';
|
||||
o.rmempty = false;
|
||||
o = s.option(form.ListValue, 'runtime', _('Runtime'),
|
||||
_('Web server runtime for serving static sites'));
|
||||
o.value('auto', _('Auto-detect (Recommended)'));
|
||||
o.value('uhttpd', _('uhttpd (Lightweight)'));
|
||||
o.value('nginx', _('nginx LXC (Full-featured)'));
|
||||
o.default = 'auto';
|
||||
|
||||
o = s.option(form.Value, 'sites_root', _('Sites Root Path'),
|
||||
_('Directory where site files are stored'));
|
||||
@ -36,6 +37,17 @@ return view.extend({
|
||||
o.default = '/srv/metablogizer/sites';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'gitea_url', _('Gitea URL'),
|
||||
_('URL of Gitea server for cloning repositories'));
|
||||
o.placeholder = 'http://localhost:3000';
|
||||
o.default = 'http://localhost:3000';
|
||||
|
||||
o = s.option(form.Value, 'nginx_container', _('Nginx Container'),
|
||||
_('Name of the LXC container running nginx (only for nginx runtime)'));
|
||||
o.placeholder = 'nginx';
|
||||
o.default = 'nginx';
|
||||
o.depends('runtime', 'nginx');
|
||||
|
||||
// Info section
|
||||
s = m.section(form.TypedSection, 'metablogizer', _('Information'));
|
||||
s.anonymous = true;
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
UCI_CONFIG="metablogizer"
|
||||
SITES_ROOT="/srv/metablogizer/sites"
|
||||
NGINX_CONTAINER="nginx"
|
||||
PORT_BASE=8900
|
||||
|
||||
# Helper: Get UCI value with default
|
||||
get_uci() {
|
||||
@ -20,19 +21,42 @@ get_uci() {
|
||||
echo "${value:-$default}"
|
||||
}
|
||||
|
||||
# Runtime detection (uhttpd preferred, nginx fallback)
|
||||
detect_runtime() {
|
||||
local configured=$(get_uci main runtime "auto")
|
||||
case "$configured" in
|
||||
uhttpd) [ -x /etc/init.d/uhttpd ] && echo "uhttpd" || echo "none" ;;
|
||||
nginx) lxc-info -n "$NGINX_CONTAINER" >/dev/null 2>&1 && echo "nginx" || echo "none" ;;
|
||||
auto|*) [ -x /etc/init.d/uhttpd ] && echo "uhttpd" || \
|
||||
(lxc-info -n "$NGINX_CONTAINER" >/dev/null 2>&1 && echo "nginx" || echo "none") ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Get next available port for uhttpd
|
||||
get_next_port() {
|
||||
local port=$PORT_BASE
|
||||
while uci show uhttpd 2>/dev/null | grep -q "listen_http='0.0.0.0:$port'"; do
|
||||
port=$((port + 1))
|
||||
done
|
||||
echo $port
|
||||
}
|
||||
|
||||
# Status method - get overall status and list all sites
|
||||
method_status() {
|
||||
local enabled nginx_running site_count
|
||||
local enabled runtime detected_runtime nginx_running site_count
|
||||
|
||||
enabled=$(get_uci main enabled 0)
|
||||
runtime=$(get_uci main runtime "auto")
|
||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||
NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER")
|
||||
|
||||
# Check nginx container
|
||||
# Detect runtime
|
||||
detected_runtime=$(detect_runtime)
|
||||
|
||||
# Check nginx container if using nginx
|
||||
nginx_running="0"
|
||||
if lxc-info -n "$NGINX_CONTAINER" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
nginx_running="1"
|
||||
else
|
||||
nginx_running="0"
|
||||
fi
|
||||
|
||||
# Count sites
|
||||
@ -43,6 +67,8 @@ method_status() {
|
||||
|
||||
json_init
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "runtime" "$runtime"
|
||||
json_add_string "detected_runtime" "$detected_runtime"
|
||||
json_add_boolean "nginx_running" "$nginx_running"
|
||||
json_add_int "site_count" "$site_count"
|
||||
json_add_string "sites_root" "$SITES_ROOT"
|
||||
@ -197,29 +223,72 @@ method_create_site() {
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 5. Create HAProxy backend
|
||||
# 5. Detect runtime and configure accordingly
|
||||
local current_runtime=$(detect_runtime)
|
||||
local port=""
|
||||
local server_address="192.168.255.1"
|
||||
local server_port="80"
|
||||
|
||||
if [ "$current_runtime" = "uhttpd" ]; then
|
||||
# Create uhttpd instance
|
||||
port=$(get_next_port)
|
||||
uci set "uhttpd.metablog_${section_id}=uhttpd"
|
||||
uci set "uhttpd.metablog_${section_id}.listen_http=0.0.0.0:$port"
|
||||
uci set "uhttpd.metablog_${section_id}.home=$SITES_ROOT/$name"
|
||||
uci set "uhttpd.metablog_${section_id}.index_page=index.html"
|
||||
uci set "uhttpd.metablog_${section_id}.error_page=/index.html"
|
||||
uci commit uhttpd
|
||||
/etc/init.d/uhttpd reload 2>/dev/null
|
||||
server_port="$port"
|
||||
else
|
||||
# Configure nginx location in container
|
||||
_configure_nginx "$name"
|
||||
local nginx_ip
|
||||
nginx_ip=$(lxc-info -n "$NGINX_CONTAINER" -iH 2>/dev/null | head -1)
|
||||
[ -n "$nginx_ip" ] && server_address="$nginx_ip"
|
||||
fi
|
||||
|
||||
# Save port to site config
|
||||
[ -n "$port" ] && uci set "$UCI_CONFIG.$section_id.port=$port"
|
||||
uci set "$UCI_CONFIG.$section_id.runtime=$current_runtime"
|
||||
|
||||
# 6. Create HAProxy backend
|
||||
local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
||||
|
||||
# Create backend via HAProxy RPCD
|
||||
echo "{\"name\":\"$backend_name\",\"mode\":\"http\",\"enabled\":\"1\"}" | \
|
||||
/usr/libexec/rpcd/luci.haproxy call create_backend >/dev/null 2>&1
|
||||
uci set "haproxy.$backend_name=backend"
|
||||
uci set "haproxy.$backend_name.name=$backend_name"
|
||||
uci set "haproxy.$backend_name.mode=http"
|
||||
uci set "haproxy.$backend_name.balance=roundrobin"
|
||||
uci set "haproxy.$backend_name.enabled=1"
|
||||
|
||||
# Create server pointing to nginx container
|
||||
local nginx_ip
|
||||
nginx_ip=$(lxc-info -n "$NGINX_CONTAINER" -iH 2>/dev/null | head -1)
|
||||
[ -z "$nginx_ip" ] && nginx_ip="nginx"
|
||||
# Create server
|
||||
local server_name="${backend_name}_srv"
|
||||
uci set "haproxy.$server_name=server"
|
||||
uci set "haproxy.$server_name.backend=$backend_name"
|
||||
uci set "haproxy.$server_name.name=srv"
|
||||
uci set "haproxy.$server_name.address=$server_address"
|
||||
uci set "haproxy.$server_name.port=$server_port"
|
||||
uci set "haproxy.$server_name.weight=100"
|
||||
uci set "haproxy.$server_name.check=1"
|
||||
uci set "haproxy.$server_name.enabled=1"
|
||||
|
||||
echo "{\"backend\":\"$backend_name\",\"name\":\"nginx\",\"address\":\"$nginx_ip\",\"port\":\"80\",\"check\":\"1\"}" | \
|
||||
/usr/libexec/rpcd/luci.haproxy call create_server >/dev/null 2>&1
|
||||
|
||||
# 6. Create HAProxy vhost
|
||||
# 7. Create HAProxy vhost
|
||||
local vhost_name=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||
local acme_val="0"
|
||||
[ "$ssl" = "1" ] && acme_val="1"
|
||||
echo "{\"domain\":\"$domain\",\"backend\":\"$backend_name\",\"ssl\":\"$ssl\",\"ssl_redirect\":\"$ssl\",\"acme\":\"$acme_val\",\"enabled\":\"1\"}" | \
|
||||
/usr/libexec/rpcd/luci.haproxy call create_vhost >/dev/null 2>&1
|
||||
|
||||
# 7. Configure nginx location in container
|
||||
_configure_nginx "$name"
|
||||
uci set "haproxy.$vhost_name=vhost"
|
||||
uci set "haproxy.$vhost_name.domain=$domain"
|
||||
uci set "haproxy.$vhost_name.backend=$backend_name"
|
||||
uci set "haproxy.$vhost_name.ssl=$ssl"
|
||||
uci set "haproxy.$vhost_name.ssl_redirect=$ssl"
|
||||
uci set "haproxy.$vhost_name.acme=$acme_val"
|
||||
uci set "haproxy.$vhost_name.enabled=1"
|
||||
uci commit haproxy
|
||||
|
||||
# Regenerate HAProxy config
|
||||
/usr/sbin/haproxyctl generate >/dev/null 2>&1
|
||||
/etc/init.d/haproxy reload >/dev/null 2>&1
|
||||
|
||||
uci commit "$UCI_CONFIG"
|
||||
|
||||
@ -283,19 +352,32 @@ method_delete_site() {
|
||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||
NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER")
|
||||
|
||||
# Get site runtime
|
||||
local site_runtime=$(get_uci "$id" runtime "")
|
||||
|
||||
# 1. Delete HAProxy vhost
|
||||
local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||
echo "{\"id\":\"$vhost_id\"}" | \
|
||||
/usr/libexec/rpcd/luci.haproxy call delete_vhost >/dev/null 2>&1
|
||||
uci delete "haproxy.$vhost_id" 2>/dev/null
|
||||
|
||||
# 2. Delete HAProxy backend
|
||||
# 2. Delete HAProxy backend and server
|
||||
local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
||||
echo "{\"id\":\"$backend_name\"}" | \
|
||||
/usr/libexec/rpcd/luci.haproxy call delete_backend >/dev/null 2>&1
|
||||
uci delete "haproxy.$backend_name" 2>/dev/null
|
||||
uci delete "haproxy.${backend_name}_srv" 2>/dev/null
|
||||
uci commit haproxy
|
||||
/usr/sbin/haproxyctl generate >/dev/null 2>&1
|
||||
/etc/init.d/haproxy reload >/dev/null 2>&1
|
||||
|
||||
# 3. Remove nginx config
|
||||
rm -f "/var/lib/lxc/$NGINX_CONTAINER/rootfs/etc/nginx/sites.d/metablog-$name.conf"
|
||||
lxc-attach -n "$NGINX_CONTAINER" -- nginx -s reload 2>/dev/null || true
|
||||
# 3. Remove runtime config
|
||||
if [ "$site_runtime" = "uhttpd" ]; then
|
||||
# Remove uhttpd instance
|
||||
uci delete "uhttpd.metablog_$id" 2>/dev/null
|
||||
uci commit uhttpd
|
||||
/etc/init.d/uhttpd reload 2>/dev/null
|
||||
else
|
||||
# Remove nginx config
|
||||
rm -f "/var/lib/lxc/$NGINX_CONTAINER/rootfs/etc/nginx/sites.d/metablog-$name.conf"
|
||||
lxc-attach -n "$NGINX_CONTAINER" -- nginx -s reload 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 4. Remove site directory
|
||||
rm -rf "$SITES_ROOT/$name"
|
||||
@ -537,31 +619,141 @@ method_update_site() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Upload file to site
|
||||
method_upload_file() {
|
||||
local id filename content
|
||||
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var id id
|
||||
json_get_var filename filename
|
||||
json_get_var content content
|
||||
|
||||
if [ -z "$id" ] || [ -z "$filename" ] || [ -z "$content" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Missing required fields (id, filename, content)"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
local name
|
||||
name=$(get_uci "$id" name "")
|
||||
if [ -z "$name" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Site not found"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||
local site_path="$SITES_ROOT/$name"
|
||||
local file_path="$site_path/$filename"
|
||||
|
||||
# Create directory structure if needed
|
||||
local dir_path=$(dirname "$file_path")
|
||||
mkdir -p "$dir_path"
|
||||
|
||||
# Decode base64 content and write file
|
||||
echo "$content" | base64 -d > "$file_path" 2>/dev/null
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
chmod 644 "$file_path"
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "filename" "$filename"
|
||||
json_add_string "path" "$file_path"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Failed to write file"
|
||||
json_dump
|
||||
fi
|
||||
}
|
||||
|
||||
# List files in a site
|
||||
method_list_files() {
|
||||
local id
|
||||
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var id id
|
||||
|
||||
if [ -z "$id" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Missing site id"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
local name
|
||||
name=$(get_uci "$id" name "")
|
||||
if [ -z "$name" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Site not found"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||
local site_path="$SITES_ROOT/$name"
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_array "files"
|
||||
|
||||
if [ -d "$site_path" ]; then
|
||||
find "$site_path" -type f 2>/dev/null | while read -r file; do
|
||||
local rel_path="${file#$site_path/}"
|
||||
local size=$(stat -c%s "$file" 2>/dev/null || echo "0")
|
||||
json_add_object ""
|
||||
json_add_string "name" "$rel_path"
|
||||
json_add_int "size" "$size"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get global settings
|
||||
method_get_settings() {
|
||||
json_init
|
||||
json_add_boolean "enabled" "$(get_uci main enabled 0)"
|
||||
json_add_string "runtime" "$(get_uci main runtime auto)"
|
||||
json_add_string "detected_runtime" "$(detect_runtime)"
|
||||
json_add_string "nginx_container" "$(get_uci main nginx_container nginx)"
|
||||
json_add_string "sites_root" "$(get_uci main sites_root /srv/metablogizer/sites)"
|
||||
json_add_string "gitea_url" "$(get_uci main gitea_url http://localhost:3000)"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Save global settings
|
||||
method_save_settings() {
|
||||
local enabled nginx_container sites_root
|
||||
local enabled runtime nginx_container sites_root gitea_url
|
||||
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var enabled enabled
|
||||
json_get_var runtime runtime
|
||||
json_get_var nginx_container nginx_container
|
||||
json_get_var sites_root sites_root
|
||||
json_get_var gitea_url gitea_url
|
||||
|
||||
# Ensure main section exists
|
||||
uci -q get "$UCI_CONFIG.main" >/dev/null 2>&1 || uci set "$UCI_CONFIG.main=metablogizer"
|
||||
|
||||
[ -n "$enabled" ] && uci set "$UCI_CONFIG.main.enabled=$enabled"
|
||||
[ -n "$runtime" ] && uci set "$UCI_CONFIG.main.runtime=$runtime"
|
||||
[ -n "$nginx_container" ] && uci set "$UCI_CONFIG.main.nginx_container=$nginx_container"
|
||||
[ -n "$sites_root" ] && uci set "$UCI_CONFIG.main.sites_root=$sites_root"
|
||||
[ -n "$gitea_url" ] && uci set "$UCI_CONFIG.main.gitea_url=$gitea_url"
|
||||
uci commit "$UCI_CONFIG"
|
||||
|
||||
json_init
|
||||
@ -582,6 +774,8 @@ case "$1" in
|
||||
"delete_site": { "id": "string" },
|
||||
"sync_site": { "id": "string" },
|
||||
"get_publish_info": { "id": "string" },
|
||||
"upload_file": { "id": "string", "filename": "string", "content": "string" },
|
||||
"list_files": { "id": "string" },
|
||||
"get_settings": {},
|
||||
"save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" }
|
||||
}
|
||||
@ -597,6 +791,8 @@ EOF
|
||||
delete_site) method_delete_site ;;
|
||||
sync_site) method_sync_site ;;
|
||||
get_publish_info) method_get_publish_info ;;
|
||||
upload_file) method_upload_file ;;
|
||||
list_files) method_list_files ;;
|
||||
get_settings) method_get_settings ;;
|
||||
save_settings) method_save_settings ;;
|
||||
*) echo '{"error": "unknown method"}' ;;
|
||||
|
||||
@ -9,9 +9,13 @@
|
||||
"get_site",
|
||||
"get_publish_info",
|
||||
"get_settings"
|
||||
]
|
||||
],
|
||||
"file": ["read", "list", "stat"]
|
||||
},
|
||||
"uci": ["metablogizer"]
|
||||
"uci": ["metablogizer"],
|
||||
"file": {
|
||||
"/srv/metablogizer/sites/*": ["read", "list"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
@ -20,7 +24,9 @@
|
||||
"update_site",
|
||||
"delete_site",
|
||||
"sync_site",
|
||||
"save_settings"
|
||||
"save_settings",
|
||||
"upload_file",
|
||||
"list_files"
|
||||
],
|
||||
"luci.haproxy": [
|
||||
"create_backend",
|
||||
@ -28,9 +34,13 @@
|
||||
"create_vhost",
|
||||
"delete_backend",
|
||||
"delete_vhost"
|
||||
]
|
||||
],
|
||||
"file": ["write", "remove"]
|
||||
},
|
||||
"uci": ["metablogizer"]
|
||||
"uci": ["metablogizer"],
|
||||
"file": {
|
||||
"/srv/metablogizer/sites/*": ["write"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.mitmproxy', method: 'status', expect: {} });
|
||||
var callInstall = rpc.declare({ object: 'luci.mitmproxy', method: 'install', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.mitmproxy', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.mitmproxy', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.mitmproxy', method: 'restart', expect: {} });
|
||||
|
||||
var css = '.mp-container{max-width:900px;margin:0 auto}.mp-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#f97316 0%,#ea580c 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.mp-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.mp-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.mp-status.running{background:rgba(16,185,129,.2)}.mp-status.stopped{background:rgba(239,68,68,.2)}.mp-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.mp-status.running .mp-dot{background:#10b981}.mp-status.stopped .mp-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.mp-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.mp-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.mp-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem}.mp-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.mp-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.mp-info-value{font-size:1rem;font-weight:500}.mp-actions{display:flex;gap:.75rem;flex-wrap:wrap}.mp-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.mp-btn-primary{background:linear-gradient(135deg,#f97316,#ea580c);color:#fff}.mp-btn-success{background:#10b981;color:#fff}.mp-btn-danger{background:#ef4444;color:#fff}.mp-btn:disabled{opacity:.5;cursor:not-allowed}.mp-not-installed{text-align:center;padding:3rem}.mp-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin:1.5rem 0}.mp-feature{padding:.75rem;background:#fff7ed;border-radius:8px;font-size:.9rem}.mp-warning{background:#fef3c7;border:1px solid #f59e0b;border-radius:8px;padding:1rem;margin-top:1rem;font-size:.9rem;color:#92400e}';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return callStatus(); },
|
||||
|
||||
handleInstall: function() {
|
||||
ui.showModal(_('Installing mitmproxy'), [E('p', { 'class': 'spinning' }, _('Installing...'))]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting...'))]);
|
||||
callStart().then(function() { ui.hideModal(); location.reload(); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping...'))]);
|
||||
callStop().then(function() { ui.hideModal(); location.reload(); })
|
||||
.catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
if (!document.getElementById('mp-styles')) {
|
||||
var s = document.createElement('style'); s.id = 'mp-styles'; s.textContent = css; document.head.appendChild(s);
|
||||
}
|
||||
|
||||
if (!status.installed || !status.docker_available) {
|
||||
return E('div', { 'class': 'mp-container' }, [
|
||||
E('div', { 'class': 'mp-header' }, [
|
||||
E('h2', {}, ['\uD83D\uDD0D ', _('mitmproxy')]),
|
||||
E('div', { 'class': 'mp-status stopped' }, [E('span', { 'class': 'mp-dot' }), _('Not Installed')])
|
||||
]),
|
||||
E('div', { 'class': 'mp-card' }, [
|
||||
E('div', { 'class': 'mp-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\uD83D\uDD0D'),
|
||||
E('h3', {}, _('mitmproxy')),
|
||||
E('p', {}, _('Interactive HTTPS proxy for debugging, testing, and security analysis.')),
|
||||
E('div', { 'class': 'mp-features' }, [
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDCCA Web UI'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDD12 HTTPS'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDCDD Logging'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDD04 Replay'),
|
||||
E('div', { 'class': 'mp-feature' }, '\u2699 Scripting'),
|
||||
E('div', { 'class': 'mp-feature' }, '\uD83D\uDCE6 Export')
|
||||
]),
|
||||
E('div', { 'class': 'mp-warning' }, _('Note: This is a security analysis tool. Only use for legitimate debugging and testing purposes.')),
|
||||
!status.docker_available ? E('div', { 'style': 'color:#ef4444;margin:1rem 0' }, _('Docker required')) : '',
|
||||
E('button', { 'class': 'mp-btn mp-btn-primary', 'style': 'margin-top:1rem', 'click': ui.createHandlerFn(this, 'handleInstall'), 'disabled': !status.docker_available }, _('Install mitmproxy'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'mp-container' }, [
|
||||
E('div', { 'class': 'mp-header' }, [
|
||||
E('h2', {}, ['\uD83D\uDD0D ', _('mitmproxy')]),
|
||||
E('div', { 'class': 'mp-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'mp-dot' }),
|
||||
status.running ? _('Running') : _('Stopped')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-card' }, [
|
||||
E('div', { 'class': 'mp-card-title' }, ['\u2139\uFE0F ', _('Configuration')]),
|
||||
E('div', { 'class': 'mp-info-grid' }, [
|
||||
E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Proxy Port')), E('div', { 'class': 'mp-info-value' }, String(status.proxy_port))]),
|
||||
E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Web UI Port')), E('div', { 'class': 'mp-info-value' }, String(status.web_port))]),
|
||||
E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Web UI')), E('div', { 'class': 'mp-info-value' }, [E('a', { 'href': 'http://' + window.location.hostname + ':' + status.web_port, 'target': '_blank' }, _('Open UI'))])])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mp-card' }, [
|
||||
E('div', { 'class': 'mp-card-title' }, ['\u26A1 ', _('Actions')]),
|
||||
E('div', { 'class': 'mp-actions' }, [
|
||||
E('button', { 'class': 'mp-btn mp-btn-success', 'click': ui.createHandlerFn(this, 'handleStart'), 'disabled': status.running }, _('Start')),
|
||||
E('button', { 'class': 'mp-btn mp-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop'), 'disabled': !status.running }, _('Stop'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null, handleSave: null, handleReset: null
|
||||
});
|
||||
@ -1,558 +1,56 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# RPCD backend for mitmproxy LuCI interface
|
||||
# Copyright (C) 2025 CyberMind.fr (SecuBox)
|
||||
#
|
||||
# RPCD backend for mitmproxy LuCI app
|
||||
|
||||
. /lib/functions.sh
|
||||
CONFIG="mitmproxy"
|
||||
CONTAINER="secbx-mitmproxy"
|
||||
|
||||
DATA_DIR=$(uci -q get mitmproxy.main.data_path || echo "/srv/mitmproxy")
|
||||
LXC_NAME="mitmproxy"
|
||||
CONF_DIR="$DATA_DIR"
|
||||
LOG_FILE="$DATA_DIR/requests.log"
|
||||
FLOW_FILE="$DATA_DIR/flows.bin"
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local web_port=$(uci_get web_port)
|
||||
local proxy_port=$(uci_get proxy_port)
|
||||
local data_path=$(uci_get data_path)
|
||||
|
||||
local docker_available=0
|
||||
command -v docker >/dev/null 2>&1 && docker_available=1
|
||||
|
||||
local running=0
|
||||
local pid=""
|
||||
local mode="unknown"
|
||||
local web_url=""
|
||||
local lxc_state=""
|
||||
local nft_active="false"
|
||||
|
||||
# Check LXC container status
|
||||
if command -v lxc-info >/dev/null 2>&1; then
|
||||
lxc_state=$(lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -oE 'RUNNING|STOPPED' || echo "UNKNOWN")
|
||||
if [ "$lxc_state" = "RUNNING" ]; then
|
||||
running=1
|
||||
mode="mitmweb"
|
||||
pid=$(lxc-info -n "$LXC_NAME" -p 2>/dev/null | grep -oE '[0-9]+' || echo "0")
|
||||
fi
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER" && running=1
|
||||
fi
|
||||
|
||||
# Fallback: check for direct process
|
||||
if [ "$running" = "0" ]; then
|
||||
if pgrep mitmweb >/dev/null 2>&1; then
|
||||
running=1
|
||||
pid=$(pgrep mitmweb | head -1)
|
||||
mode="mitmweb"
|
||||
elif pgrep mitmdump >/dev/null 2>&1; then
|
||||
running=1
|
||||
pid=$(pgrep mitmdump | head -1)
|
||||
mode="mitmdump"
|
||||
fi
|
||||
fi
|
||||
local installed=0
|
||||
[ "$docker_available" = "1" ] && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "mitmproxy" && installed=1
|
||||
|
||||
# Check nftables rules
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
nft list table inet mitmproxy >/dev/null 2>&1 && nft_active="true"
|
||||
fi
|
||||
|
||||
local enabled=$(uci -q get mitmproxy.main.enabled || echo "0")
|
||||
local proxy_port=$(uci -q get mitmproxy.main.proxy_port || echo "8080")
|
||||
local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081")
|
||||
local proxy_mode=$(uci -q get mitmproxy.main.mode || echo "regular")
|
||||
local filtering_enabled=$(uci -q get mitmproxy.filtering.enabled || echo "0")
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
|
||||
[ "$running" = "1" ] && [ "$mode" = "mitmweb" ] && web_url="http://${router_ip}:${web_port}"
|
||||
|
||||
cat <<EOF
|
||||
cat <<EOFJ
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"pid": ${pid:-0},
|
||||
"mode": "$mode",
|
||||
"proxy_mode": "$proxy_mode",
|
||||
"lxc_state": "$lxc_state",
|
||||
"proxy_port": $proxy_port,
|
||||
"web_port": $web_port,
|
||||
"web_url": "$web_url",
|
||||
"ca_installed": $([ -f "$CONF_DIR/mitmproxy-ca-cert.pem" ] && echo "true" || echo "false"),
|
||||
"nft_active": $nft_active,
|
||||
"filtering_enabled": $([ "$filtering_enabled" = "1" ] && echo "true" || echo "false")
|
||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||
"web_port": ${web_port:-8081},
|
||||
"proxy_port": ${proxy_port:-8080},
|
||||
"data_path": "${data_path:-/srv/mitmproxy}"
|
||||
}
|
||||
EOF
|
||||
EOFJ
|
||||
}
|
||||
|
||||
# Get main configuration
|
||||
get_config() {
|
||||
local enabled=$(uci -q get mitmproxy.main.enabled || echo "0")
|
||||
local mode=$(uci -q get mitmproxy.main.mode || echo "regular")
|
||||
local proxy_port=$(uci -q get mitmproxy.main.proxy_port || echo "8080")
|
||||
local web_host=$(uci -q get mitmproxy.main.web_host || echo "0.0.0.0")
|
||||
local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081")
|
||||
local data_path=$(uci -q get mitmproxy.main.data_path || echo "/srv/mitmproxy")
|
||||
local memory_limit=$(uci -q get mitmproxy.main.memory_limit || echo "256M")
|
||||
local ssl_insecure=$(uci -q get mitmproxy.main.ssl_insecure || echo "0")
|
||||
local anticache=$(uci -q get mitmproxy.main.anticache || echo "0")
|
||||
local anticomp=$(uci -q get mitmproxy.main.anticomp || echo "0")
|
||||
local flow_detail=$(uci -q get mitmproxy.main.flow_detail || echo "1")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"mode": "$mode",
|
||||
"proxy_port": $proxy_port,
|
||||
"web_host": "$web_host",
|
||||
"web_port": $web_port,
|
||||
"data_path": "$data_path",
|
||||
"memory_limit": "$memory_limit",
|
||||
"ssl_insecure": $([ "$ssl_insecure" = "1" ] && echo "true" || echo "false"),
|
||||
"anticache": $([ "$anticache" = "1" ] && echo "true" || echo "false"),
|
||||
"anticomp": $([ "$anticomp" = "1" ] && echo "true" || echo "false"),
|
||||
"flow_detail": $flow_detail
|
||||
}
|
||||
EOF
|
||||
do_install() {
|
||||
command -v mitmproxyctl >/dev/null 2>&1 && { mitmproxyctl install >/tmp/mitmproxy-install.log 2>&1 & echo '{"success":true,"message":"Installing"}'; } || echo '{"success":false,"error":"mitmproxyctl not found"}'
|
||||
}
|
||||
|
||||
# Get transparent mode configuration
|
||||
get_transparent_config() {
|
||||
local enabled=$(uci -q get mitmproxy.transparent.enabled || echo "0")
|
||||
local interface=$(uci -q get mitmproxy.transparent.interface || echo "br-lan")
|
||||
local redirect_http=$(uci -q get mitmproxy.transparent.redirect_http || echo "1")
|
||||
local redirect_https=$(uci -q get mitmproxy.transparent.redirect_https || echo "1")
|
||||
local http_port=$(uci -q get mitmproxy.transparent.http_port || echo "80")
|
||||
local https_port=$(uci -q get mitmproxy.transparent.https_port || echo "443")
|
||||
do_start() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy start >/dev/null 2>&1; echo '{"success":true}'; }
|
||||
do_stop() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy stop >/dev/null 2>&1; echo '{"success":true}'; }
|
||||
do_restart() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy restart >/dev/null 2>&1; echo '{"success":true}'; }
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"interface": "$interface",
|
||||
"redirect_http": $([ "$redirect_http" = "1" ] && echo "true" || echo "false"),
|
||||
"redirect_https": $([ "$redirect_https" = "1" ] && echo "true" || echo "false"),
|
||||
"http_port": $http_port,
|
||||
"https_port": $https_port
|
||||
}
|
||||
EOF
|
||||
list_methods() { cat <<'EOFM'
|
||||
{"status":{},"install":{},"start":{},"stop":{},"restart":{}}
|
||||
EOFM
|
||||
}
|
||||
|
||||
# Get whitelist configuration
|
||||
get_whitelist_config() {
|
||||
local enabled=$(uci -q get mitmproxy.whitelist.enabled || echo "1")
|
||||
|
||||
# Get bypass_ip list
|
||||
local bypass_ips=$(uci -q get mitmproxy.whitelist.bypass_ip 2>/dev/null | tr ' ' '\n' | while read ip; do
|
||||
[ -n "$ip" ] && printf '"%s",' "$ip"
|
||||
done | sed 's/,$//')
|
||||
|
||||
# Get bypass_domain list
|
||||
local bypass_domains=$(uci -q get mitmproxy.whitelist.bypass_domain 2>/dev/null | tr ' ' '\n' | while read domain; do
|
||||
[ -n "$domain" ] && printf '"%s",' "$domain"
|
||||
done | sed 's/,$//')
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"bypass_ip": [${bypass_ips}],
|
||||
"bypass_domain": [${bypass_domains}]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get filtering configuration
|
||||
get_filtering_config() {
|
||||
local enabled=$(uci -q get mitmproxy.filtering.enabled || echo "0")
|
||||
local log_requests=$(uci -q get mitmproxy.filtering.log_requests || echo "1")
|
||||
local filter_cdn=$(uci -q get mitmproxy.filtering.filter_cdn || echo "0")
|
||||
local filter_media=$(uci -q get mitmproxy.filtering.filter_media || echo "0")
|
||||
local block_ads=$(uci -q get mitmproxy.filtering.block_ads || echo "0")
|
||||
local addon_script=$(uci -q get mitmproxy.filtering.addon_script || echo "/etc/mitmproxy/addons/secubox_filter.py")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"log_requests": $([ "$log_requests" = "1" ] && echo "true" || echo "false"),
|
||||
"filter_cdn": $([ "$filter_cdn" = "1" ] && echo "true" || echo "false"),
|
||||
"filter_media": $([ "$filter_media" = "1" ] && echo "true" || echo "false"),
|
||||
"block_ads": $([ "$block_ads" = "1" ] && echo "true" || echo "false"),
|
||||
"addon_script": "$addon_script"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get all configuration in one call
|
||||
get_all_config() {
|
||||
cat <<EOF
|
||||
{
|
||||
"main": $(get_config),
|
||||
"transparent": $(get_transparent_config),
|
||||
"whitelist": $(get_whitelist_config),
|
||||
"filtering": $(get_filtering_config)
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get statistics
|
||||
get_stats() {
|
||||
local total_requests=0
|
||||
local unique_hosts=0
|
||||
local flow_size="0"
|
||||
local cdn_requests=0
|
||||
local media_requests=0
|
||||
local blocked_ads=0
|
||||
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
total_requests=$(wc -l < "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
# Use jsonfilter for parsing (OpenWrt native)
|
||||
if command -v jsonfilter >/dev/null 2>&1; then
|
||||
unique_hosts=$(cat "$LOG_FILE" 2>/dev/null | while read line; do
|
||||
echo "$line" | jsonfilter -e '@.request.host' 2>/dev/null
|
||||
done | sort -u | wc -l)
|
||||
cdn_requests=$(grep -c '"category":"cdn"' "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
media_requests=$(grep -c '"category":"media"' "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
blocked_ads=$(grep -c '"category":"blocked_ad"' "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$FLOW_FILE" ]; then
|
||||
flow_size=$(ls -l "$FLOW_FILE" 2>/dev/null | awk '{print $5}' || echo "0")
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"total_requests": $total_requests,
|
||||
"unique_hosts": $unique_hosts,
|
||||
"flow_file_size": $flow_size,
|
||||
"cdn_requests": $cdn_requests,
|
||||
"media_requests": $media_requests,
|
||||
"blocked_ads": $blocked_ads
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get recent requests
|
||||
get_requests() {
|
||||
local limit="${1:-50}"
|
||||
local category="${2:-}"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo '{"requests":[]}'
|
||||
return
|
||||
fi
|
||||
|
||||
# Filter by category if specified
|
||||
if [ -n "$category" ] && [ "$category" != "all" ]; then
|
||||
echo '{"requests":['
|
||||
grep "\"category\":\"$category\"" "$LOG_FILE" 2>/dev/null | tail -"$limit" | \
|
||||
awk 'BEGIN{first=1}{if(!first)printf ",";first=0;print}' 2>/dev/null || echo ""
|
||||
echo ']}'
|
||||
else
|
||||
echo '{"requests":['
|
||||
tail -"$limit" "$LOG_FILE" 2>/dev/null | \
|
||||
awk 'BEGIN{first=1}{if(!first)printf ",";first=0;print}' 2>/dev/null || echo ""
|
||||
echo ']}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Get top hosts
|
||||
get_top_hosts() {
|
||||
local limit="${1:-20}"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo '{"hosts":[]}'
|
||||
return
|
||||
fi
|
||||
|
||||
echo '{"hosts":['
|
||||
# Parse JSON using grep/sed for compatibility
|
||||
grep -o '"host":"[^"]*"' "$LOG_FILE" 2>/dev/null | \
|
||||
sed 's/"host":"//;s/"$//' | \
|
||||
sort | uniq -c | sort -rn | head -"$limit" | \
|
||||
awk 'BEGIN{first=1} {
|
||||
if(!first) printf ",";
|
||||
first=0;
|
||||
gsub(/"/, "\\\"", $2);
|
||||
printf "{\"host\":\"%s\",\"count\":%d}", $2, $1
|
||||
}'
|
||||
echo ']}'
|
||||
}
|
||||
|
||||
# Service control
|
||||
service_start() {
|
||||
/etc/init.d/mitmproxy start >/dev/null 2>&1
|
||||
sleep 2
|
||||
get_status
|
||||
}
|
||||
|
||||
service_stop() {
|
||||
/etc/init.d/mitmproxy stop >/dev/null 2>&1
|
||||
sleep 1
|
||||
get_status
|
||||
}
|
||||
|
||||
service_restart() {
|
||||
/etc/init.d/mitmproxy restart >/dev/null 2>&1
|
||||
sleep 2
|
||||
get_status
|
||||
}
|
||||
|
||||
# Setup firewall rules
|
||||
firewall_setup() {
|
||||
/usr/sbin/mitmproxyctl firewall-setup 2>&1
|
||||
local result=$?
|
||||
if [ $result -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Firewall rules applied"}'
|
||||
else
|
||||
echo '{"success":false,"message":"Failed to apply firewall rules"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Clear firewall rules
|
||||
firewall_clear() {
|
||||
/usr/sbin/mitmproxyctl firewall-clear 2>&1
|
||||
local result=$?
|
||||
if [ $result -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Firewall rules cleared"}'
|
||||
else
|
||||
echo '{"success":false,"message":"Failed to clear firewall rules"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Set configuration
|
||||
set_config() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local section="main"
|
||||
|
||||
case "$key" in
|
||||
save_flows|capture_*)
|
||||
section="capture"
|
||||
;;
|
||||
redirect_*|interface|http_port|https_port)
|
||||
section="transparent"
|
||||
;;
|
||||
bypass_ip|bypass_domain)
|
||||
section="whitelist"
|
||||
;;
|
||||
filter_*|log_requests|block_ads|addon_script)
|
||||
section="filtering"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle boolean conversion
|
||||
case "$value" in
|
||||
true) value="1" ;;
|
||||
false) value="0" ;;
|
||||
esac
|
||||
|
||||
uci set "mitmproxy.$section.$key=$value"
|
||||
uci commit mitmproxy
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Add to list (for bypass_ip, bypass_domain)
|
||||
add_to_list() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local section="whitelist"
|
||||
|
||||
uci add_list "mitmproxy.$section.$key=$value"
|
||||
uci commit mitmproxy
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Remove from list
|
||||
remove_from_list() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local section="whitelist"
|
||||
|
||||
uci del_list "mitmproxy.$section.$key=$value"
|
||||
uci commit mitmproxy
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Clear captured data
|
||||
clear_data() {
|
||||
rm -f "$DATA_DIR"/*.log "$DATA_DIR"/*.bin 2>/dev/null
|
||||
echo '{"success":true,"message":"Captured data cleared"}'
|
||||
}
|
||||
|
||||
# Get CA certificate info
|
||||
get_ca_info() {
|
||||
local cert="$CONF_DIR/mitmproxy-ca-cert.pem"
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081")
|
||||
|
||||
if [ -f "$cert" ]; then
|
||||
local subject=$(openssl x509 -in "$cert" -noout -subject 2>/dev/null | sed 's/subject=//')
|
||||
local expires=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | sed 's/notAfter=//')
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"installed": true,
|
||||
"path": "$cert",
|
||||
"subject": "$subject",
|
||||
"expires": "$expires",
|
||||
"download_url": "http://$router_ip:$web_port/cert"
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat <<EOF
|
||||
{
|
||||
"installed": false,
|
||||
"path": "$cert",
|
||||
"download_url": ""
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
get_web_token() {
|
||||
# Token is written to /data/.mitmproxy_token inside container
|
||||
# /data is bind-mounted to DATA_DIR on host
|
||||
local token_file="$DATA_DIR/.mitmproxy_token"
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081")
|
||||
local token=""
|
||||
|
||||
# Try reading token from host-mounted path
|
||||
if [ -f "$token_file" ]; then
|
||||
token=$(cat "$token_file" 2>/dev/null | tr -d '\n\r')
|
||||
fi
|
||||
|
||||
# Fallback: read token directly from container if host file is missing/empty
|
||||
if [ -z "$token" ] && command -v lxc-attach >/dev/null 2>&1; then
|
||||
if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
token=$(lxc-attach -n "$LXC_NAME" -- cat /data/.mitmproxy_token 2>/dev/null | tr -d '\n\r')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Second fallback: parse token from mitmweb log inside container
|
||||
if [ -z "$token" ] && command -v lxc-attach >/dev/null 2>&1; then
|
||||
if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
token=$(lxc-attach -n "$LXC_NAME" -- grep -o 'token=[a-zA-Z0-9_-]*' /tmp/mitmweb.log 2>/dev/null | head -1 | cut -d= -f2)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Construct URL - only add token parameter if token exists
|
||||
local web_url="http://$router_ip:$web_port"
|
||||
local web_url_with_token="$web_url"
|
||||
if [ -n "$token" ]; then
|
||||
web_url_with_token="$web_url/?token=$token"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"token": "$token",
|
||||
"web_url": "$web_url",
|
||||
"web_url_with_token": "$web_url_with_token"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# RPCD list method
|
||||
case "$1" in
|
||||
list)
|
||||
cat <<EOF
|
||||
{
|
||||
"get_status": {},
|
||||
"get_config": {},
|
||||
"get_transparent_config": {},
|
||||
"get_whitelist_config": {},
|
||||
"get_filtering_config": {},
|
||||
"get_all_config": {},
|
||||
"get_stats": {},
|
||||
"get_requests": {"limit": 50, "category": "all"},
|
||||
"get_top_hosts": {"limit": 20},
|
||||
"get_ca_info": {},
|
||||
"get_web_token": {},
|
||||
"service_start": {},
|
||||
"service_stop": {},
|
||||
"service_restart": {},
|
||||
"firewall_setup": {},
|
||||
"firewall_clear": {},
|
||||
"set_config": {"key": "string", "value": "string"},
|
||||
"add_to_list": {"key": "string", "value": "string"},
|
||||
"remove_from_list": {"key": "string", "value": "string"},
|
||||
"clear_data": {}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
get_status)
|
||||
get_status
|
||||
;;
|
||||
get_config)
|
||||
get_config
|
||||
;;
|
||||
get_transparent_config)
|
||||
get_transparent_config
|
||||
;;
|
||||
get_whitelist_config)
|
||||
get_whitelist_config
|
||||
;;
|
||||
get_filtering_config)
|
||||
get_filtering_config
|
||||
;;
|
||||
get_all_config)
|
||||
get_all_config
|
||||
;;
|
||||
get_stats)
|
||||
get_stats
|
||||
;;
|
||||
get_requests)
|
||||
read -r input
|
||||
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null || echo "50")
|
||||
category=$(echo "$input" | jsonfilter -e '@.category' 2>/dev/null || echo "all")
|
||||
get_requests "$limit" "$category"
|
||||
;;
|
||||
get_top_hosts)
|
||||
read -r input
|
||||
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null || echo "20")
|
||||
get_top_hosts "$limit"
|
||||
;;
|
||||
get_ca_info)
|
||||
get_ca_info
|
||||
;;
|
||||
get_web_token)
|
||||
get_web_token
|
||||
;;
|
||||
service_start)
|
||||
service_start
|
||||
;;
|
||||
service_stop)
|
||||
service_stop
|
||||
;;
|
||||
service_restart)
|
||||
service_restart
|
||||
;;
|
||||
firewall_setup)
|
||||
firewall_setup
|
||||
;;
|
||||
firewall_clear)
|
||||
firewall_clear
|
||||
;;
|
||||
set_config)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
set_config "$key" "$value"
|
||||
;;
|
||||
add_to_list)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
add_to_list "$key" "$value"
|
||||
;;
|
||||
remove_from_list)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
remove_from_list "$key" "$value"
|
||||
;;
|
||||
clear_data)
|
||||
clear_data
|
||||
;;
|
||||
*)
|
||||
echo '{"error":"Unknown method"}'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo '{"error":"Unknown command"}'
|
||||
;;
|
||||
list) list_methods ;;
|
||||
call) case "$2" in status) get_status ;; install) do_install ;; start) do_start ;; stop) do_stop ;; restart) do_restart ;; *) echo '{"error":"Unknown method"}' ;; esac ;;
|
||||
*) echo '{"error":"Unknown command"}' ;;
|
||||
esac
|
||||
|
||||
@ -1,45 +1,8 @@
|
||||
{
|
||||
"admin/secubox/security/mitmproxy": {
|
||||
"title": "mitmproxy",
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-mitmproxy"],
|
||||
"uci": {"mitmproxy": true}
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/mitmproxy/dashboard": {
|
||||
"title": "Dashboard",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mitmproxy/dashboard"
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/mitmproxy/webui": {
|
||||
"title": "Web UI",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mitmproxy/webui"
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/mitmproxy/requests": {
|
||||
"title": "Requests",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mitmproxy/requests"
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/mitmproxy/settings": {
|
||||
"title": "Settings",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mitmproxy/settings"
|
||||
}
|
||||
"action": { "type": "view", "path": "mitmproxy/overview" },
|
||||
"depends": { "acl": ["luci-app-mitmproxy"] },
|
||||
"order": 60
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,43 +1,7 @@
|
||||
{
|
||||
"luci-app-mitmproxy": {
|
||||
"description": "Grant access to mitmproxy LuCI app",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.mitmproxy": [
|
||||
"get_status",
|
||||
"get_config",
|
||||
"get_transparent_config",
|
||||
"get_whitelist_config",
|
||||
"get_filtering_config",
|
||||
"get_all_config",
|
||||
"get_stats",
|
||||
"get_requests",
|
||||
"get_top_hosts",
|
||||
"get_ca_info",
|
||||
"get_web_token"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"mitmproxy"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.mitmproxy": [
|
||||
"service_start",
|
||||
"service_stop",
|
||||
"service_restart",
|
||||
"firewall_setup",
|
||||
"firewall_clear",
|
||||
"set_config",
|
||||
"add_to_list",
|
||||
"remove_from_list",
|
||||
"clear_data"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"mitmproxy"
|
||||
]
|
||||
}
|
||||
"description": "Grant access to mitmproxy",
|
||||
"read": { "ubus": { "luci.mitmproxy": ["status"] }, "uci": ["mitmproxy"] },
|
||||
"write": { "ubus": { "luci.mitmproxy": ["install", "start", "stop", "restart"] }, "uci": ["mitmproxy"] }
|
||||
}
|
||||
}
|
||||
|
||||
32
package/secubox/luci-app-nextcloud/Makefile
Normal file
32
package/secubox/luci-app-nextcloud/Makefile
Normal file
@ -0,0 +1,32 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Nextcloud
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-nextcloud
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/luci-app-nextcloud/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.nextcloud $(1)/usr/libexec/rpcd/luci.nextcloud
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-nextcloud.json $(1)/usr/share/luci/menu.d/luci-app-nextcloud.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-nextcloud.json $(1)/usr/share/rpcd/acl.d/luci-app-nextcloud.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/nextcloud
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/nextcloud/*.js $(1)/www/luci-static/resources/view/nextcloud/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@ -0,0 +1,213 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.nextcloud', method: 'status', expect: {} });
|
||||
var callInstall = rpc.declare({ object: 'luci.nextcloud', method: 'install', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.nextcloud', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.nextcloud', method: 'stop', expect: {} });
|
||||
var callRestart = rpc.declare({ object: 'luci.nextcloud', method: 'restart', expect: {} });
|
||||
|
||||
var css = '.nc-container{max-width:900px;margin:0 auto}.nc-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#0082c9 0%,#00639b 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.nc-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.nc-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.nc-status.running{background:rgba(16,185,129,.2)}.nc-status.stopped{background:rgba(239,68,68,.2)}.nc-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.nc-status.running .nc-dot{background:#10b981}.nc-status.stopped .nc-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.nc-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.nc-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.nc-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}.nc-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.nc-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.nc-info-value{font-size:1.1rem;font-weight:500}.nc-actions{display:flex;gap:.75rem;flex-wrap:wrap}.nc-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.nc-btn-primary{background:linear-gradient(135deg,#0082c9,#00639b);color:#fff}.nc-btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,130,201,.3)}.nc-btn-success{background:#10b981;color:#fff}.nc-btn-danger{background:#ef4444;color:#fff}.nc-btn-secondary{background:#6b7280;color:#fff}.nc-btn:disabled{opacity:.5;cursor:not-allowed}.nc-webui{display:flex;align-items:center;gap:1rem;padding:1rem;background:linear-gradient(135deg,rgba(0,130,201,.1),rgba(0,99,155,.1));border-radius:12px;margin-top:1rem}.nc-webui-icon{font-size:2rem}.nc-webui-info{flex:1}.nc-webui-url{font-family:monospace;color:#0082c9}.nc-not-installed{text-align:center;padding:3rem}.nc-not-installed h3{margin-bottom:1rem;color:#333}.nc-not-installed p{color:#666;margin-bottom:1.5rem}.nc-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin:1.5rem 0;text-align:left}.nc-feature{padding:.75rem;background:#f0f9ff;border-radius:8px;font-size:.9rem}';
|
||||
|
||||
return view.extend({
|
||||
pollActive: true,
|
||||
|
||||
load: function() {
|
||||
return callStatus();
|
||||
},
|
||||
|
||||
startPolling: function() {
|
||||
var self = this;
|
||||
this.pollActive = true;
|
||||
poll.add(L.bind(function() {
|
||||
if (!this.pollActive) return Promise.resolve();
|
||||
return callStatus().then(L.bind(function(status) {
|
||||
this.updateStatus(status);
|
||||
}, this));
|
||||
}, this), 5);
|
||||
},
|
||||
|
||||
updateStatus: function(status) {
|
||||
var badge = document.querySelector('.nc-status');
|
||||
var statusText = document.querySelector('.nc-status-text');
|
||||
|
||||
if (badge && statusText) {
|
||||
badge.className = 'nc-status ' + (status.running ? 'running' : 'stopped');
|
||||
statusText.textContent = status.running ? _('Running') : _('Stopped');
|
||||
}
|
||||
},
|
||||
|
||||
handleInstall: function() {
|
||||
var self = this;
|
||||
ui.showModal(_('Installing Nextcloud'), [
|
||||
E('p', { 'class': 'spinning' }, _('Installing Nextcloud. This may take several minutes...'))
|
||||
]);
|
||||
callInstall().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) {
|
||||
ui.addNotification(null, E('p', r.message || _('Installation started')));
|
||||
self.startPolling();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Failed: ') + (r.error || 'Unknown error')), 'error');
|
||||
}
|
||||
}).catch(function(e) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Nextcloud...'))]);
|
||||
callStart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud started')));
|
||||
else ui.addNotification(null, E('p', _('Failed to start')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping Nextcloud...'))]);
|
||||
callStop().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud stopped')));
|
||||
else ui.addNotification(null, E('p', _('Failed to stop')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
handleRestart: function() {
|
||||
ui.showModal(_('Restarting...'), [E('p', { 'class': 'spinning' }, _('Restarting Nextcloud...'))]);
|
||||
callRestart().then(function(r) {
|
||||
ui.hideModal();
|
||||
if (r.success) ui.addNotification(null, E('p', _('Nextcloud restarted')));
|
||||
else ui.addNotification(null, E('p', _('Failed to restart')), 'error');
|
||||
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); });
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
var self = this;
|
||||
|
||||
if (!document.getElementById('nc-styles')) {
|
||||
var s = document.createElement('style');
|
||||
s.id = 'nc-styles';
|
||||
s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
// Not installed view
|
||||
if (!status.installed || !status.docker_available) {
|
||||
return E('div', { 'class': 'nc-container' }, [
|
||||
E('div', { 'class': 'nc-header' }, [
|
||||
E('h2', {}, ['\u2601\ufe0f ', _('Nextcloud')]),
|
||||
E('div', { 'class': 'nc-status stopped' }, [
|
||||
E('span', { 'class': 'nc-dot' }),
|
||||
E('span', { 'class': 'nc-status-text' }, _('Not Installed'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'nc-card' }, [
|
||||
E('div', { 'class': 'nc-not-installed' }, [
|
||||
E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\u2601\ufe0f'),
|
||||
E('h3', {}, _('Nextcloud')),
|
||||
E('p', {}, _('Self-hosted productivity platform with file sync, calendar, contacts, and more.')),
|
||||
E('div', { 'class': 'nc-features' }, [
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcc1 ' + _('File Sync')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcc5 ' + _('Calendar')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udc65 ' + _('Contacts')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcdd ' + _('Documents')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udcf7 ' + _('Photos')),
|
||||
E('div', { 'class': 'nc-feature' }, '\ud83d\udd12 ' + _('E2E Encryption'))
|
||||
]),
|
||||
!status.docker_available ? E('div', { 'style': 'color:#ef4444;margin-bottom:1rem' }, _('Docker is required but not available')) : '',
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'handleInstall'),
|
||||
'disabled': !status.docker_available
|
||||
}, _('Install Nextcloud'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// Installed view
|
||||
this.startPolling();
|
||||
|
||||
return E('div', { 'class': 'nc-container' }, [
|
||||
E('div', { 'class': 'nc-header' }, [
|
||||
E('h2', {}, ['\u2601\ufe0f ', _('Nextcloud')]),
|
||||
E('div', { 'class': 'nc-status ' + (status.running ? 'running' : 'stopped') }, [
|
||||
E('span', { 'class': 'nc-dot' }),
|
||||
E('span', { 'class': 'nc-status-text' }, status.running ? _('Running') : _('Stopped'))
|
||||
])
|
||||
]),
|
||||
|
||||
// Info Card
|
||||
E('div', { 'class': 'nc-card' }, [
|
||||
E('div', { 'class': 'nc-card-title' }, ['\u2139\ufe0f ', _('Service Information')]),
|
||||
E('div', { 'class': 'nc-info-grid' }, [
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Port')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.port || '80')
|
||||
]),
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Admin User')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.admin_user || 'admin')
|
||||
]),
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Trusted Domains')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.trusted_domains || 'cloud.local')
|
||||
]),
|
||||
E('div', { 'class': 'nc-info-item' }, [
|
||||
E('div', { 'class': 'nc-info-label' }, _('Data Path')),
|
||||
E('div', { 'class': 'nc-info-value' }, status.data_path || '/srv/nextcloud')
|
||||
])
|
||||
]),
|
||||
|
||||
// Web UI Link
|
||||
status.running && status.web_accessible ? E('div', { 'class': 'nc-webui' }, [
|
||||
E('div', { 'class': 'nc-webui-icon' }, '\ud83c\udf10'),
|
||||
E('div', { 'class': 'nc-webui-info' }, [
|
||||
E('div', { 'style': 'font-weight:600' }, _('Web Interface')),
|
||||
E('div', { 'class': 'nc-webui-url' }, status.web_url)
|
||||
]),
|
||||
E('a', {
|
||||
'href': status.web_url,
|
||||
'target': '_blank',
|
||||
'class': 'nc-btn nc-btn-primary'
|
||||
}, _('Open'))
|
||||
]) : ''
|
||||
]),
|
||||
|
||||
// Actions Card
|
||||
E('div', { 'class': 'nc-card' }, [
|
||||
E('div', { 'class': 'nc-card-title' }, ['\u26a1 ', _('Actions')]),
|
||||
E('div', { 'class': 'nc-actions' }, [
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-success',
|
||||
'click': ui.createHandlerFn(this, 'handleStart'),
|
||||
'disabled': status.running
|
||||
}, _('Start')),
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-danger',
|
||||
'click': ui.createHandlerFn(this, 'handleStop'),
|
||||
'disabled': !status.running
|
||||
}, _('Stop')),
|
||||
E('button', {
|
||||
'class': 'nc-btn nc-btn-secondary',
|
||||
'click': ui.createHandlerFn(this, 'handleRestart'),
|
||||
'disabled': !status.running
|
||||
}, _('Restart')),
|
||||
E('a', {
|
||||
'href': L.url('admin', 'secubox', 'services', 'nextcloud', 'settings'),
|
||||
'class': 'nc-btn nc-btn-secondary'
|
||||
}, _('Settings'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.load('nextcloud');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('nextcloud', _('Nextcloud Settings'),
|
||||
_('Configure Nextcloud settings. Changes require service restart to take effect.'));
|
||||
|
||||
s = m.section(form.TypedSection, 'nextcloud', _('General Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'),
|
||||
_('Enable Nextcloud'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Web UI Port'),
|
||||
_('Port for the Nextcloud web interface'));
|
||||
o.datatype = 'port';
|
||||
o.default = '80';
|
||||
o.placeholder = '80';
|
||||
|
||||
o = s.option(form.Value, 'data_path', _('Data Path'),
|
||||
_('Path to store Nextcloud data'));
|
||||
o.default = '/srv/nextcloud';
|
||||
o.placeholder = '/srv/nextcloud';
|
||||
|
||||
o = s.option(form.Value, 'admin_user', _('Admin Username'),
|
||||
_('Administrator username for initial setup'));
|
||||
o.default = 'admin';
|
||||
o.placeholder = 'admin';
|
||||
|
||||
o = s.option(form.Value, 'admin_password', _('Admin Password'),
|
||||
_('Administrator password for initial setup. Required for first install.'));
|
||||
o.password = true;
|
||||
o.placeholder = _('Enter password');
|
||||
|
||||
o = s.option(form.Value, 'trusted_domains', _('Trusted Domains'),
|
||||
_('Comma-separated list of trusted domains (e.g., cloud.example.com,192.168.1.1)'));
|
||||
o.default = 'cloud.local';
|
||||
o.placeholder = 'cloud.local';
|
||||
|
||||
o = s.option(form.Value, 'timezone', _('Timezone'),
|
||||
_('Timezone for the container'));
|
||||
o.default = 'UTC';
|
||||
o.placeholder = 'UTC';
|
||||
|
||||
o = s.option(form.Value, 'image', _('Docker Image'),
|
||||
_('Docker image to use'));
|
||||
o.default = 'nextcloud:latest';
|
||||
o.placeholder = 'nextcloud:latest';
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,235 @@
|
||||
#!/bin/sh
|
||||
# RPCD backend for Nextcloud LuCI app
|
||||
|
||||
CONFIG="nextcloud"
|
||||
CONTAINER="secbx-nextcloud"
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
local admin_user=$(uci_get admin_user)
|
||||
local trusted_domains=$(uci_get trusted_domains)
|
||||
local image=$(uci_get image)
|
||||
|
||||
# Check if Docker is available
|
||||
local docker_available=0
|
||||
command -v docker >/dev/null 2>&1 && docker_available=1
|
||||
|
||||
# Check if container is running
|
||||
local running=0
|
||||
local container_status="stopped"
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
if docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then
|
||||
running=1
|
||||
container_status="running"
|
||||
elif docker ps -a --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER"; then
|
||||
container_status="stopped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if installed (image exists)
|
||||
local installed=0
|
||||
if [ "$docker_available" = "1" ]; then
|
||||
docker images --format "{{.Repository}}" 2>/dev/null | grep -q "nextcloud" && installed=1
|
||||
fi
|
||||
|
||||
# Check web UI accessibility
|
||||
local web_accessible=0
|
||||
if [ "$running" = "1" ]; then
|
||||
wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${port:-80}/" 2>/dev/null && web_accessible=1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||
"container_status": "$container_status",
|
||||
"port": ${port:-80},
|
||||
"data_path": "${data_path:-/srv/nextcloud}",
|
||||
"admin_user": "${admin_user:-admin}",
|
||||
"trusted_domains": "${trusted_domains:-cloud.local}",
|
||||
"image": "${image:-nextcloud:latest}",
|
||||
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"),
|
||||
"web_url": "http://192.168.255.1:${port:-80}"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get configuration
|
||||
get_config() {
|
||||
local enabled=$(uci_get enabled)
|
||||
local port=$(uci_get port)
|
||||
local data_path=$(uci_get data_path)
|
||||
local admin_user=$(uci_get admin_user)
|
||||
local admin_password=$(uci_get admin_password)
|
||||
local trusted_domains=$(uci_get trusted_domains)
|
||||
local timezone=$(uci_get timezone)
|
||||
local image=$(uci_get image)
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": "${enabled:-0}",
|
||||
"port": "${port:-80}",
|
||||
"data_path": "${data_path:-/srv/nextcloud}",
|
||||
"admin_user": "${admin_user:-admin}",
|
||||
"admin_password": "${admin_password:-}",
|
||||
"trusted_domains": "${trusted_domains:-cloud.local}",
|
||||
"timezone": "${timezone:-UTC}",
|
||||
"image": "${image:-nextcloud:latest}"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Save configuration
|
||||
save_config() {
|
||||
local input
|
||||
read -r input
|
||||
|
||||
local port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)
|
||||
local data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null)
|
||||
local admin_user=$(echo "$input" | jsonfilter -e '@.admin_user' 2>/dev/null)
|
||||
local admin_password=$(echo "$input" | jsonfilter -e '@.admin_password' 2>/dev/null)
|
||||
local trusted_domains=$(echo "$input" | jsonfilter -e '@.trusted_domains' 2>/dev/null)
|
||||
local timezone=$(echo "$input" | jsonfilter -e '@.timezone' 2>/dev/null)
|
||||
|
||||
[ -n "$port" ] && uci_set port "$port"
|
||||
[ -n "$data_path" ] && uci_set data_path "$data_path"
|
||||
[ -n "$admin_user" ] && uci_set admin_user "$admin_user"
|
||||
[ -n "$admin_password" ] && uci_set admin_password "$admin_password"
|
||||
[ -n "$trusted_domains" ] && uci_set trusted_domains "$trusted_domains"
|
||||
[ -n "$timezone" ] && uci_set timezone "$timezone"
|
||||
|
||||
echo '{"success": true}'
|
||||
}
|
||||
|
||||
# Install Nextcloud
|
||||
do_install() {
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
nextcloudctl install >/tmp/nextcloud-install.log 2>&1 &
|
||||
echo '{"success": true, "message": "Installation started in background"}'
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Start service
|
||||
do_start() {
|
||||
if [ -x /etc/init.d/nextcloud ]; then
|
||||
/etc/init.d/nextcloud start >/dev/null 2>&1
|
||||
uci_set enabled '1'
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop service
|
||||
do_stop() {
|
||||
if [ -x /etc/init.d/nextcloud ]; then
|
||||
/etc/init.d/nextcloud stop >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Restart service
|
||||
do_restart() {
|
||||
if [ -x /etc/init.d/nextcloud ]; then
|
||||
/etc/init.d/nextcloud restart >/dev/null 2>&1
|
||||
echo '{"success": true}'
|
||||
else
|
||||
echo '{"success": false, "error": "Service not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Update container
|
||||
do_update() {
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
nextcloudctl update >/tmp/nextcloud-update.log 2>&1 &
|
||||
echo '{"success": true, "message": "Update started in background"}'
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Run OCC command
|
||||
do_occ() {
|
||||
local input
|
||||
read -r input
|
||||
local cmd=$(echo "$input" | jsonfilter -e '@.command' 2>/dev/null)
|
||||
|
||||
if [ -z "$cmd" ]; then
|
||||
echo '{"success": false, "error": "No command specified"}'
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v nextcloudctl >/dev/null 2>&1; then
|
||||
local output=$(nextcloudctl occ $cmd 2>&1)
|
||||
echo "{\"success\": true, \"output\": \"$(echo "$output" | sed 's/"/\\"/g' | tr '\n' ' ')\"}"
|
||||
else
|
||||
echo '{"success": false, "error": "nextcloudctl not found"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Get logs
|
||||
get_logs() {
|
||||
local lines=50
|
||||
local log_content=""
|
||||
|
||||
if [ -f /tmp/nextcloud-install.log ]; then
|
||||
log_content=$(tail -n $lines /tmp/nextcloud-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
|
||||
fi
|
||||
|
||||
echo "{\"logs\": \"$log_content\"}"
|
||||
}
|
||||
|
||||
# RPCD list method
|
||||
list_methods() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
"status": {},
|
||||
"get_config": {},
|
||||
"save_config": {"port": "string", "data_path": "string", "admin_user": "string", "admin_password": "string", "trusted_domains": "string", "timezone": "string"},
|
||||
"install": {},
|
||||
"start": {},
|
||||
"stop": {},
|
||||
"restart": {},
|
||||
"update": {},
|
||||
"occ": {"command": "string"},
|
||||
"logs": {}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main entry point
|
||||
case "$1" in
|
||||
list)
|
||||
list_methods
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
get_config) get_config ;;
|
||||
save_config) save_config ;;
|
||||
install) do_install ;;
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
restart) do_restart ;;
|
||||
update) do_update ;;
|
||||
occ) do_occ ;;
|
||||
logs) get_logs ;;
|
||||
*) echo '{"error": "Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Unknown command"}'
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
"admin/secubox/services/nextcloud": {
|
||||
"title": "Nextcloud",
|
||||
"order": 55,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-nextcloud"]
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/nextcloud/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "nextcloud/overview"
|
||||
}
|
||||
},
|
||||
"admin/secubox/services/nextcloud/settings": {
|
||||
"title": "Settings",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "nextcloud/settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-nextcloud": {
|
||||
"description": "Grant access to Nextcloud",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.nextcloud": ["status", "get_config", "logs"]
|
||||
},
|
||||
"uci": ["nextcloud"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.nextcloud": ["install", "start", "stop", "restart", "update", "save_config", "occ"]
|
||||
},
|
||||
"uci": ["nextcloud"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -328,9 +328,9 @@ return baseclass.extend({
|
||||
icon: '\ud83e\udde5',
|
||||
iconBg: 'rgba(124, 58, 237, 0.15)',
|
||||
iconColor: '#7c3aed',
|
||||
section: 'services',
|
||||
section: 'security',
|
||||
path: 'admin/services/tor-shield/overview',
|
||||
service: 'tor',
|
||||
service: 'tor-shield',
|
||||
version: '1.0.0'
|
||||
},
|
||||
'jellyfin': {
|
||||
@ -404,6 +404,78 @@ return baseclass.extend({
|
||||
path: 'admin/secubox/services/gitea/overview',
|
||||
service: 'gitea',
|
||||
version: '1.22.0'
|
||||
},
|
||||
'lyrion': {
|
||||
id: 'lyrion',
|
||||
name: 'Lyrion Music Server',
|
||||
desc: 'Self-hosted music streaming with Squeezebox/Logitech Media Server compatibility',
|
||||
icon: '\ud83c\udfb5',
|
||||
iconBg: 'rgba(236, 72, 153, 0.15)',
|
||||
iconColor: '#ec4899',
|
||||
section: 'services',
|
||||
path: 'admin/secubox/services/lyrion/overview',
|
||||
service: 'lyrion',
|
||||
version: '9.0.3'
|
||||
},
|
||||
'ollama': {
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
desc: 'Run large language models locally with easy-to-use CLI and API',
|
||||
icon: '\ud83e\uddac',
|
||||
iconBg: 'rgba(99, 102, 241, 0.15)',
|
||||
iconColor: '#6366f1',
|
||||
section: 'services',
|
||||
path: 'admin/secubox/services/ollama/overview',
|
||||
service: 'ollama',
|
||||
version: '0.5.0'
|
||||
},
|
||||
'streamlit': {
|
||||
id: 'streamlit',
|
||||
name: 'Streamlit',
|
||||
desc: 'Python data apps and dashboards with instant web deployment',
|
||||
icon: '\ud83d\udcca',
|
||||
iconBg: 'rgba(255, 75, 75, 0.15)',
|
||||
iconColor: '#ff4b4b',
|
||||
section: 'services',
|
||||
path: 'admin/secubox/services/streamlit/overview',
|
||||
service: 'streamlit',
|
||||
version: '1.40.0'
|
||||
},
|
||||
'zigbee2mqtt': {
|
||||
id: 'zigbee2mqtt',
|
||||
name: 'Zigbee2MQTT',
|
||||
desc: 'Bridge Zigbee devices to MQTT for smart home automation',
|
||||
icon: '\ud83d\udca1',
|
||||
iconBg: 'rgba(245, 158, 11, 0.15)',
|
||||
iconColor: '#f59e0b',
|
||||
section: 'services',
|
||||
path: 'admin/secubox/services/zigbee2mqtt/overview',
|
||||
service: 'zigbee2mqtt',
|
||||
version: '1.40.0'
|
||||
},
|
||||
'domoticz': {
|
||||
id: 'domoticz',
|
||||
name: 'Domoticz',
|
||||
desc: 'Home automation system with support for various sensors and devices',
|
||||
icon: '\ud83c\udfe0',
|
||||
iconBg: 'rgba(34, 197, 94, 0.15)',
|
||||
iconColor: '#22c55e',
|
||||
section: 'services',
|
||||
path: 'admin/secubox/services/domoticz/overview',
|
||||
service: 'domoticz',
|
||||
version: '2024.7'
|
||||
},
|
||||
'mailinabox': {
|
||||
id: 'mailinabox',
|
||||
name: 'Mail-in-a-Box',
|
||||
desc: 'Self-hosted email server with webmail, calendar, and contacts',
|
||||
icon: '\ud83d\udce7',
|
||||
iconBg: 'rgba(59, 130, 246, 0.15)',
|
||||
iconColor: '#3b82f6',
|
||||
section: 'services',
|
||||
path: 'admin/secubox/services/mailinabox/overview',
|
||||
service: 'mailinabox',
|
||||
version: '2.0.0'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -464,8 +464,7 @@ return view.extend({
|
||||
}, ['\uD83D\uDD0D ', _('Leak Test')]),
|
||||
E('button', {
|
||||
'class': 'tor-btn tor-btn-warning',
|
||||
'click': L.bind(this.handleRestart, this),
|
||||
'disabled': !status.enabled
|
||||
'click': L.bind(this.handleRestart, this)
|
||||
}, ['\u21BB ', _('Restart')]),
|
||||
E('a', {
|
||||
'class': 'tor-btn',
|
||||
|
||||
@ -598,6 +598,24 @@ _add_server_to_backend() {
|
||||
|
||||
[ -n "$address" ] || return
|
||||
|
||||
# Validate address - if it's a hostname (not IP), try to resolve it
|
||||
if ! echo "$address" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
# It's a hostname, try to resolve it
|
||||
local resolved_ip=""
|
||||
resolved_ip=$(nslookup "$address" 2>/dev/null | awk '/^Address: / { print $2; exit }')
|
||||
if [ -z "$resolved_ip" ]; then
|
||||
# Try getent as fallback
|
||||
resolved_ip=$(getent hosts "$address" 2>/dev/null | awk '{print $1; exit}')
|
||||
fi
|
||||
if [ -z "$resolved_ip" ]; then
|
||||
log_warn "Cannot resolve hostname '$address' for server $server_name in backend $target_backend - skipping"
|
||||
return
|
||||
fi
|
||||
# Use the resolved IP instead
|
||||
log_debug "Resolved $address to $resolved_ip"
|
||||
address="$resolved_ip"
|
||||
fi
|
||||
|
||||
local check_opt=""
|
||||
[ "$check" = "1" ] && check_opt="check"
|
||||
|
||||
|
||||
@ -396,18 +396,22 @@ STUB
|
||||
mkdir -p /config/prefs/plugin /config/cache /music /var/log/lyrion
|
||||
chown -R nobody:nobody /config /var/log/lyrion
|
||||
|
||||
# Create startup script
|
||||
# Create startup script that runs as nobody user
|
||||
cat > /opt/lyrion/start.sh << 'START'
|
||||
#!/bin/sh
|
||||
cd /opt/lyrion
|
||||
|
||||
# Ensure directories exist with proper permissions
|
||||
mkdir -p /config/prefs/plugin /config/cache /var/log/lyrion
|
||||
chown -R nobody:nobody /config /var/log/lyrion 2>/dev/null || true
|
||||
exec perl slimserver.pl \
|
||||
chown -R nobody:nobody /config /var/log/lyrion /opt/lyrion 2>/dev/null || true
|
||||
|
||||
# Run Lyrion as nobody user to avoid permission issues
|
||||
exec su -s /bin/sh nobody -c "cd /opt/lyrion && exec perl slimserver.pl \
|
||||
--prefsdir /config/prefs \
|
||||
--cachedir /config/cache \
|
||||
--logdir /var/log/lyrion \
|
||||
--httpport 9000 \
|
||||
--cliport 9090
|
||||
--cliport 9090"
|
||||
START
|
||||
chmod +x /opt/lyrion/start.sh
|
||||
|
||||
|
||||
29
package/secubox/secubox-app-metablogizer/Makefile
Normal file
29
package/secubox/secubox-app-metablogizer/Makefile
Normal file
@ -0,0 +1,29 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-metablogizer
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-metablogizer
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=MetaBlogizer Static Site Publisher
|
||||
DEPENDS:=+git +uhttpd
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-metablogizer/description
|
||||
Static site publisher with auto-vhost creation.
|
||||
Supports uhttpd (default) and nginx LXC runtimes.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-metablogizer/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/metablogizerctl $(1)/usr/sbin/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/etc/config/metablogizer $(1)/etc/config/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-metablogizer))
|
||||
@ -0,0 +1,6 @@
|
||||
config metablogizer 'main'
|
||||
option enabled '1'
|
||||
option runtime 'auto'
|
||||
option sites_root '/srv/metablogizer/sites'
|
||||
option gitea_url 'http://localhost:3000'
|
||||
option port_base '8900'
|
||||
@ -0,0 +1,512 @@
|
||||
#!/bin/sh
|
||||
# SecuBox MetaBlogizer - Static Site Publisher
|
||||
# Supports uhttpd (default) and nginx LXC runtime
|
||||
# Copyright (C) 2025 CyberMind.fr
|
||||
|
||||
CONFIG="metablogizer"
|
||||
SITES_ROOT="/srv/metablogizer/sites"
|
||||
NGINX_LXC="metablogizer-nginx"
|
||||
LXC_PATH="/srv/lxc"
|
||||
PORT_BASE=8900
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
log_info() { echo "[INFO] $*"; logger -t metablogizer "$*"; }
|
||||
log_warn() { echo "[WARN] $*" >&2; }
|
||||
log_error() { echo "[ERROR] $*" >&2; }
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
MetaBlogizer - Static Site Publisher
|
||||
|
||||
Usage: metablogizerctl <command> [options]
|
||||
|
||||
Site Commands:
|
||||
list List all sites
|
||||
create <name> <domain> [repo] Create new site
|
||||
delete <name> Delete site
|
||||
sync <name> Sync site from git repo
|
||||
publish <name> Publish site (create HAProxy vhost)
|
||||
|
||||
Runtime Commands:
|
||||
runtime Show current runtime
|
||||
runtime set <uhttpd|nginx> Set runtime preference
|
||||
|
||||
Management:
|
||||
status Show overall status
|
||||
install-nginx Install nginx LXC container (optional)
|
||||
|
||||
Runtime Selection:
|
||||
auto - Auto-detect (uhttpd preferred)
|
||||
uhttpd - Use uhttpd instances (lightweight)
|
||||
nginx - Use nginx LXC container (more features)
|
||||
EOF
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# Runtime Detection
|
||||
# ===========================================
|
||||
|
||||
has_uhttpd() { [ -x /etc/init.d/uhttpd ]; }
|
||||
|
||||
has_nginx_lxc() {
|
||||
command -v lxc-info >/dev/null 2>&1 && \
|
||||
[ -d "$LXC_PATH/$NGINX_LXC/rootfs" ]
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
local configured=$(uci_get main.runtime)
|
||||
|
||||
case "$configured" in
|
||||
uhttpd)
|
||||
if has_uhttpd; then
|
||||
echo "uhttpd"
|
||||
else
|
||||
log_error "uhttpd requested but not available"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
nginx)
|
||||
if has_nginx_lxc; then
|
||||
echo "nginx"
|
||||
else
|
||||
log_error "nginx LXC requested but not installed"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
auto|*)
|
||||
# Prefer uhttpd (lighter), fall back to nginx
|
||||
if has_uhttpd; then
|
||||
echo "uhttpd"
|
||||
elif has_nginx_lxc; then
|
||||
echo "nginx"
|
||||
else
|
||||
log_error "No runtime available"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# Site Management
|
||||
# ===========================================
|
||||
|
||||
get_next_port() {
|
||||
local port=$PORT_BASE
|
||||
while uci show uhttpd 2>/dev/null | grep -q "listen_http='0.0.0.0:$port'"; do
|
||||
port=$((port + 1))
|
||||
done
|
||||
echo $port
|
||||
}
|
||||
|
||||
site_exists() {
|
||||
local name="$1"
|
||||
uci -q get ${CONFIG}.site_${name} >/dev/null 2>&1
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
echo "MetaBlogizer Sites:"
|
||||
echo "==================="
|
||||
|
||||
local runtime=$(detect_runtime 2>/dev/null)
|
||||
echo "Runtime: ${runtime:-none}"
|
||||
echo ""
|
||||
|
||||
config_load "$CONFIG"
|
||||
|
||||
local found=0
|
||||
_print_site() {
|
||||
local section="$1"
|
||||
local name domain port enabled gitea_repo
|
||||
|
||||
config_get name "$section" name
|
||||
config_get domain "$section" domain
|
||||
config_get port "$section" port
|
||||
config_get enabled "$section" enabled "0"
|
||||
config_get gitea_repo "$section" gitea_repo ""
|
||||
|
||||
[ -z "$name" ] && return
|
||||
|
||||
local status="disabled"
|
||||
[ "$enabled" = "1" ] && status="enabled"
|
||||
|
||||
local dir_status="missing"
|
||||
[ -d "$SITES_ROOT/$name" ] && dir_status="exists"
|
||||
|
||||
printf " %-15s %-25s :%-5s [%s] %s\n" "$name" "$domain" "$port" "$status" "$dir_status"
|
||||
found=1
|
||||
}
|
||||
config_foreach _print_site site
|
||||
|
||||
[ "$found" = "0" ] && echo " No sites configured"
|
||||
}
|
||||
|
||||
cmd_create() {
|
||||
local name="$1"
|
||||
local domain="$2"
|
||||
local gitea_repo="$3"
|
||||
|
||||
[ -z "$name" ] && { log_error "Site name required"; return 1; }
|
||||
[ -z "$domain" ] && { log_error "Domain required"; return 1; }
|
||||
|
||||
# Sanitize name
|
||||
name=$(echo "$name" | tr -cd 'a-z0-9_-')
|
||||
|
||||
if site_exists "$name"; then
|
||||
log_error "Site '$name' already exists"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local runtime=$(detect_runtime) || return 1
|
||||
local port=$(get_next_port)
|
||||
|
||||
log_info "Creating site: $name ($domain) on port $port using $runtime"
|
||||
|
||||
# Create site directory with proper permissions
|
||||
mkdir -p "$SITES_ROOT/$name"
|
||||
chmod 755 "$SITES_ROOT/$name"
|
||||
|
||||
# Create placeholder index
|
||||
cat > "$SITES_ROOT/$name/index.html" <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>$name</title>
|
||||
<meta property="og:title" content="$name">
|
||||
<meta property="og:url" content="https://$domain">
|
||||
<meta property="og:type" content="website">
|
||||
<style>
|
||||
body { font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px; }
|
||||
h1 { color: #3b82f6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>$name</h1>
|
||||
<p>Site published with MetaBlogizer</p>
|
||||
<p><a href="https://$domain">https://$domain</a></p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
chmod 644 "$SITES_ROOT/$name/index.html"
|
||||
|
||||
# Clone from Gitea if repo specified
|
||||
if [ -n "$gitea_repo" ]; then
|
||||
local gitea_url=$(uci_get main.gitea_url)
|
||||
[ -z "$gitea_url" ] && gitea_url="http://localhost:3000"
|
||||
|
||||
log_info "Cloning from $gitea_url/$gitea_repo..."
|
||||
rm -rf "$SITES_ROOT/$name"
|
||||
git clone "$gitea_url/$gitea_repo.git" "$SITES_ROOT/$name" 2>/dev/null || {
|
||||
log_warn "Git clone failed, using placeholder"
|
||||
mkdir -p "$SITES_ROOT/$name"
|
||||
}
|
||||
# Set proper permissions for web serving
|
||||
chmod -R 755 "$SITES_ROOT/$name"
|
||||
find "$SITES_ROOT/$name" -type f -exec chmod 644 {} \;
|
||||
fi
|
||||
|
||||
# Configure runtime
|
||||
case "$runtime" in
|
||||
uhttpd)
|
||||
_create_uhttpd_site "$name" "$port"
|
||||
;;
|
||||
nginx)
|
||||
_create_nginx_site "$name"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Save site config
|
||||
uci set ${CONFIG}.site_${name}=site
|
||||
uci set ${CONFIG}.site_${name}.name="$name"
|
||||
uci set ${CONFIG}.site_${name}.domain="$domain"
|
||||
uci set ${CONFIG}.site_${name}.port="$port"
|
||||
uci set ${CONFIG}.site_${name}.runtime="$runtime"
|
||||
[ -n "$gitea_repo" ] && uci set ${CONFIG}.site_${name}.gitea_repo="$gitea_repo"
|
||||
uci set ${CONFIG}.site_${name}.enabled="1"
|
||||
uci commit ${CONFIG}
|
||||
|
||||
log_info "Site created: $name"
|
||||
log_info "Directory: $SITES_ROOT/$name"
|
||||
log_info "Local URL: http://localhost:$port"
|
||||
|
||||
echo ""
|
||||
echo "Next: Run 'metablogizerctl publish $name' to create HAProxy vhost"
|
||||
}
|
||||
|
||||
_create_uhttpd_site() {
|
||||
local name="$1"
|
||||
local port="$2"
|
||||
|
||||
log_info "Creating uhttpd instance for $name on port $port"
|
||||
|
||||
uci set uhttpd.metablog_${name}=uhttpd
|
||||
uci set uhttpd.metablog_${name}.listen_http="0.0.0.0:$port"
|
||||
uci set uhttpd.metablog_${name}.home="$SITES_ROOT/$name"
|
||||
uci set uhttpd.metablog_${name}.index_page="index.html"
|
||||
uci set uhttpd.metablog_${name}.error_page="/index.html"
|
||||
uci commit uhttpd
|
||||
|
||||
/etc/init.d/uhttpd reload 2>/dev/null || /etc/init.d/uhttpd restart
|
||||
}
|
||||
|
||||
_create_nginx_site() {
|
||||
local name="$1"
|
||||
|
||||
if ! has_nginx_lxc; then
|
||||
log_error "nginx LXC not installed. Run: metablogizerctl install-nginx"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Creating nginx config for $name"
|
||||
|
||||
local nginx_conf="$LXC_PATH/$NGINX_LXC/rootfs/etc/nginx/sites.d"
|
||||
mkdir -p "$nginx_conf"
|
||||
|
||||
cat > "$nginx_conf/metablog-$name.conf" <<EOF
|
||||
location /$name/ {
|
||||
alias /srv/sites/$name/;
|
||||
index index.html;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Reload nginx in container
|
||||
lxc-attach -n "$NGINX_LXC" -- nginx -s reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
cmd_publish() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { log_error "Site name required"; return 1; }
|
||||
|
||||
if ! site_exists "$name"; then
|
||||
log_error "Site '$name' not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local domain=$(uci_get site_${name}.domain)
|
||||
local port=$(uci_get site_${name}.port)
|
||||
|
||||
[ -z "$domain" ] && { log_error "Site domain not configured"; return 1; }
|
||||
|
||||
log_info "Publishing $name to $domain"
|
||||
|
||||
# Create HAProxy backend
|
||||
local backend_name="metablog_${name}"
|
||||
uci set haproxy.${backend_name}=backend
|
||||
uci set haproxy.${backend_name}.name="$backend_name"
|
||||
uci set haproxy.${backend_name}.mode="http"
|
||||
uci set haproxy.${backend_name}.balance="roundrobin"
|
||||
uci set haproxy.${backend_name}.enabled="1"
|
||||
|
||||
# Create HAProxy server
|
||||
local server_name="${backend_name}_srv"
|
||||
uci set haproxy.${server_name}=server
|
||||
uci set haproxy.${server_name}.backend="$backend_name"
|
||||
uci set haproxy.${server_name}.name="uhttpd"
|
||||
uci set haproxy.${server_name}.address="192.168.255.1"
|
||||
uci set haproxy.${server_name}.port="$port"
|
||||
uci set haproxy.${server_name}.weight="100"
|
||||
uci set haproxy.${server_name}.check="1"
|
||||
uci set haproxy.${server_name}.enabled="1"
|
||||
|
||||
# Create HAProxy vhost
|
||||
local vhost_name=$(echo "$domain" | tr '.-' '_')
|
||||
uci set haproxy.${vhost_name}=vhost
|
||||
uci set haproxy.${vhost_name}.domain="$domain"
|
||||
uci set haproxy.${vhost_name}.backend="$backend_name"
|
||||
uci set haproxy.${vhost_name}.ssl="1"
|
||||
uci set haproxy.${vhost_name}.ssl_redirect="1"
|
||||
uci set haproxy.${vhost_name}.acme="1"
|
||||
uci set haproxy.${vhost_name}.enabled="1"
|
||||
|
||||
uci commit haproxy
|
||||
|
||||
# Regenerate HAProxy config
|
||||
/usr/sbin/haproxyctl generate 2>/dev/null
|
||||
/etc/init.d/haproxy reload 2>/dev/null
|
||||
|
||||
log_info "Site published!"
|
||||
echo ""
|
||||
echo "URL: https://$domain"
|
||||
echo ""
|
||||
echo "To request SSL certificate:"
|
||||
echo " haproxyctl cert add $domain"
|
||||
}
|
||||
|
||||
cmd_delete() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { log_error "Site name required"; return 1; }
|
||||
|
||||
log_info "Deleting site: $name"
|
||||
|
||||
# Remove uhttpd instance
|
||||
uci delete uhttpd.metablog_${name} 2>/dev/null
|
||||
uci commit uhttpd
|
||||
/etc/init.d/uhttpd reload 2>/dev/null
|
||||
|
||||
# Remove HAProxy config
|
||||
local domain=$(uci_get site_${name}.domain)
|
||||
if [ -n "$domain" ]; then
|
||||
local vhost_name=$(echo "$domain" | tr '.-' '_')
|
||||
uci delete haproxy.${vhost_name} 2>/dev/null
|
||||
uci delete haproxy.metablog_${name} 2>/dev/null
|
||||
uci delete haproxy.metablog_${name}_srv 2>/dev/null
|
||||
uci commit haproxy
|
||||
/usr/sbin/haproxyctl generate 2>/dev/null
|
||||
/etc/init.d/haproxy reload 2>/dev/null
|
||||
fi
|
||||
|
||||
# Remove site config
|
||||
uci delete ${CONFIG}.site_${name} 2>/dev/null
|
||||
uci commit ${CONFIG}
|
||||
|
||||
# Optionally remove files
|
||||
if [ -d "$SITES_ROOT/$name" ]; then
|
||||
echo "Site directory: $SITES_ROOT/$name"
|
||||
echo "Remove manually if desired: rm -rf $SITES_ROOT/$name"
|
||||
fi
|
||||
|
||||
log_info "Site deleted"
|
||||
}
|
||||
|
||||
cmd_sync() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { log_error "Site name required"; return 1; }
|
||||
|
||||
local gitea_repo=$(uci_get site_${name}.gitea_repo)
|
||||
[ -z "$gitea_repo" ] && { log_error "No git repo configured for $name"; return 1; }
|
||||
|
||||
local site_dir="$SITES_ROOT/$name"
|
||||
[ ! -d "$site_dir" ] && { log_error "Site directory not found"; return 1; }
|
||||
|
||||
log_info "Syncing $name from git..."
|
||||
|
||||
cd "$site_dir"
|
||||
if [ -d ".git" ]; then
|
||||
git pull origin main 2>/dev/null || git pull origin master 2>/dev/null || git pull
|
||||
else
|
||||
local gitea_url=$(uci_get main.gitea_url)
|
||||
[ -z "$gitea_url" ] && gitea_url="http://localhost:3000"
|
||||
git clone "$gitea_url/$gitea_repo.git" /tmp/metablog-sync-$$
|
||||
cp -r /tmp/metablog-sync-$$/* "$site_dir/"
|
||||
rm -rf /tmp/metablog-sync-$$
|
||||
fi
|
||||
|
||||
log_info "Sync complete"
|
||||
}
|
||||
|
||||
cmd_runtime() {
|
||||
local action="$1"
|
||||
local value="$2"
|
||||
|
||||
if [ "$action" = "set" ]; then
|
||||
case "$value" in
|
||||
uhttpd|nginx|auto)
|
||||
uci_set main.runtime "$value"
|
||||
log_info "Runtime set to: $value"
|
||||
;;
|
||||
*)
|
||||
log_error "Invalid runtime: $value (use uhttpd, nginx, or auto)"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
local configured=$(uci_get main.runtime)
|
||||
local detected=$(detect_runtime 2>/dev/null)
|
||||
echo "Configured: ${configured:-auto}"
|
||||
echo "Detected: ${detected:-none}"
|
||||
echo ""
|
||||
echo "Available:"
|
||||
has_uhttpd && echo " - uhttpd (installed)" || echo " - uhttpd (not available)"
|
||||
has_nginx_lxc && echo " - nginx LXC (installed)" || echo " - nginx LXC (not installed)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo "MetaBlogizer Status"
|
||||
echo "==================="
|
||||
|
||||
local enabled=$(uci_get main.enabled)
|
||||
local runtime=$(detect_runtime 2>/dev/null)
|
||||
local sites_count=$(uci show $CONFIG 2>/dev/null | grep -c "=site")
|
||||
|
||||
echo "Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")"
|
||||
echo "Runtime: ${runtime:-none}"
|
||||
echo "Sites: $sites_count"
|
||||
echo "Sites Root: $SITES_ROOT"
|
||||
echo ""
|
||||
|
||||
cmd_list
|
||||
}
|
||||
|
||||
cmd_install_nginx() {
|
||||
log_info "Installing nginx LXC container..."
|
||||
|
||||
command -v lxc-start >/dev/null 2>&1 || {
|
||||
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
|
||||
return 1
|
||||
}
|
||||
|
||||
local rootfs="$LXC_PATH/$NGINX_LXC/rootfs"
|
||||
mkdir -p "$LXC_PATH/$NGINX_LXC"
|
||||
|
||||
# Download Alpine
|
||||
local arch="aarch64"
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="x86_64" ;;
|
||||
armv7l) arch="armv7" ;;
|
||||
esac
|
||||
|
||||
log_info "Downloading Alpine Linux..."
|
||||
wget -q -O /tmp/alpine-nginx.tar.gz \
|
||||
"https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/$arch/alpine-minirootfs-3.19.0-$arch.tar.gz" || {
|
||||
log_error "Failed to download Alpine"
|
||||
return 1
|
||||
}
|
||||
|
||||
mkdir -p "$rootfs"
|
||||
tar xzf /tmp/alpine-nginx.tar.gz -C "$rootfs"
|
||||
rm -f /tmp/alpine-nginx.tar.gz
|
||||
|
||||
# Configure
|
||||
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
|
||||
|
||||
# Install nginx
|
||||
chroot "$rootfs" /bin/sh -c "apk update && apk add --no-cache nginx"
|
||||
|
||||
# Create LXC config
|
||||
cat > "$LXC_PATH/$NGINX_LXC/config" <<EOF
|
||||
lxc.uts.name = $NGINX_LXC
|
||||
lxc.rootfs.path = dir:$rootfs
|
||||
lxc.net.0.type = none
|
||||
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||
lxc.mount.entry = $SITES_ROOT srv/sites none bind,create=dir 0 0
|
||||
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
|
||||
lxc.init.cmd = /usr/sbin/nginx -g 'daemon off;'
|
||||
EOF
|
||||
|
||||
log_info "nginx LXC installed"
|
||||
log_info "Start with: lxc-start -n $NGINX_LXC -d"
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# Main
|
||||
# ===========================================
|
||||
|
||||
case "${1:-}" in
|
||||
list) shift; cmd_list "$@" ;;
|
||||
create) shift; cmd_create "$@" ;;
|
||||
delete) shift; cmd_delete "$@" ;;
|
||||
sync) shift; cmd_sync "$@" ;;
|
||||
publish) shift; cmd_publish "$@" ;;
|
||||
runtime) shift; cmd_runtime "$@" ;;
|
||||
status) shift; cmd_status "$@" ;;
|
||||
install-nginx) shift; cmd_install_nginx "$@" ;;
|
||||
help|--help|-h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
@ -10,10 +10,15 @@ set -e
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
SECUBOX_VERSION="0.8.0"
|
||||
SECUBOX_VERSION="0.8.1"
|
||||
LOG_FILE="/var/log/secubox/core.log"
|
||||
PID_FILE="/var/run/secubox/core.pid"
|
||||
STATE_DIR="/var/run/secubox"
|
||||
WATCHDOG_STATE="/var/run/secubox/watchdog.json"
|
||||
|
||||
# Services to monitor (init.d name:check_method:restart_delay)
|
||||
# check_method: pid, docker, lxc, port:PORT
|
||||
MONITORED_SERVICES="haproxy:pid:5 crowdsec:pid:10 tor:pid:10"
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
@ -162,6 +167,102 @@ run_health_check() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Service watchdog function
|
||||
run_watchdog() {
|
||||
local watchdog_enabled=$(uci -q get secubox.main.watchdog_enabled || echo "1")
|
||||
[ "$watchdog_enabled" != "1" ] && return 0
|
||||
|
||||
# Get monitored services from UCI or use defaults
|
||||
local services=$(uci -q get secubox.main.watchdog_services || echo "$MONITORED_SERVICES")
|
||||
local restart_count=0
|
||||
local status_json=""
|
||||
|
||||
log debug "Watchdog: Checking services..."
|
||||
|
||||
for service_entry in $services; do
|
||||
local service_name=$(echo "$service_entry" | cut -d: -f1)
|
||||
local check_method=$(echo "$service_entry" | cut -d: -f2)
|
||||
local restart_delay=$(echo "$service_entry" | cut -d: -f3)
|
||||
[ -z "$restart_delay" ] && restart_delay=5
|
||||
|
||||
# Check if service init script exists
|
||||
[ ! -x "/etc/init.d/$service_name" ] && continue
|
||||
|
||||
# Check if service is supposed to be enabled
|
||||
/etc/init.d/$service_name enabled >/dev/null 2>&1 || continue
|
||||
|
||||
local is_running=false
|
||||
|
||||
case "$check_method" in
|
||||
pid)
|
||||
# Check via pidof or pgrep
|
||||
if pgrep "$service_name" >/dev/null 2>&1; then
|
||||
is_running=true
|
||||
fi
|
||||
;;
|
||||
docker)
|
||||
# Check Docker container
|
||||
if docker ps --filter "name=$service_name" --format "{{.Names}}" 2>/dev/null | grep -q "$service_name"; then
|
||||
is_running=true
|
||||
fi
|
||||
;;
|
||||
lxc)
|
||||
# Check LXC container
|
||||
if lxc-info -n "$service_name" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||
is_running=true
|
||||
fi
|
||||
;;
|
||||
port:*)
|
||||
# Check if port is listening
|
||||
local port=$(echo "$check_method" | cut -d: -f2)
|
||||
# Use /proc/net/tcp (ports in hex)
|
||||
local port_hex=$(printf '%04X' "$port")
|
||||
if grep -q ":$port_hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null; then
|
||||
is_running=true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$is_running" = "false" ]; then
|
||||
log warn "Watchdog: $service_name is down, restarting..."
|
||||
sleep "$restart_delay"
|
||||
|
||||
# Double-check before restart (service might have recovered)
|
||||
case "$check_method" in
|
||||
pid) pgrep "$service_name" >/dev/null 2>&1 && continue ;;
|
||||
esac
|
||||
|
||||
/etc/init.d/$service_name restart >/dev/null 2>&1
|
||||
restart_count=$((restart_count + 1))
|
||||
|
||||
# Log restart event
|
||||
log info "Watchdog: Restarted $service_name"
|
||||
fi
|
||||
done
|
||||
|
||||
# Save watchdog state
|
||||
json_init
|
||||
json_add_string "last_check" "$(date -Iseconds)"
|
||||
json_add_int "restarts" "$restart_count"
|
||||
json_dump > "$WATCHDOG_STATE" 2>/dev/null
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Get list of UCI-configured services to watch
|
||||
get_watchdog_services() {
|
||||
# Core services always monitored if enabled
|
||||
local core_services="haproxy crowdsec"
|
||||
|
||||
# Scan for secubox apps with watchdog=1
|
||||
for conf in $(uci show 2>/dev/null | grep "\.watchdog=" | grep "'1'" | cut -d. -f1-2); do
|
||||
local service=$(uci -q get "$conf.service")
|
||||
[ -n "$service" ] && core_services="$core_services $service"
|
||||
done
|
||||
|
||||
echo "$core_services"
|
||||
}
|
||||
|
||||
# Daemon mode
|
||||
daemon_mode() {
|
||||
log info "SecuBox Core daemon starting (version $SECUBOX_VERSION)"
|
||||
@ -172,13 +273,27 @@ daemon_mode() {
|
||||
# Get health check interval
|
||||
local health_interval=$(uci -q get secubox.main.health_check_interval || echo "300")
|
||||
|
||||
# Get watchdog interval (faster than health check)
|
||||
local watchdog_interval=$(uci -q get secubox.main.watchdog_interval || echo "60")
|
||||
|
||||
# Main daemon loop
|
||||
local health_counter=0
|
||||
local health_cycles=$((health_interval / watchdog_interval))
|
||||
[ "$health_cycles" -lt 1 ] && health_cycles=1
|
||||
|
||||
while true; do
|
||||
# Run periodic health check
|
||||
run_health_check > /tmp/secubox/health-status.json
|
||||
# Run watchdog every cycle
|
||||
run_watchdog
|
||||
|
||||
# Run health check every N cycles
|
||||
health_counter=$((health_counter + 1))
|
||||
if [ "$health_counter" -ge "$health_cycles" ]; then
|
||||
run_health_check > /tmp/secubox/health-status.json
|
||||
health_counter=0
|
||||
fi
|
||||
|
||||
# Sleep until next check
|
||||
sleep "$health_interval"
|
||||
sleep "$watchdog_interval"
|
||||
done
|
||||
}
|
||||
|
||||
@ -197,8 +312,11 @@ case "$1" in
|
||||
log info "Reloading configuration"
|
||||
killall -HUP secubox-core 2>/dev/null || true
|
||||
;;
|
||||
watchdog)
|
||||
run_watchdog
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {daemon|status|health|reload}"
|
||||
echo "Usage: $0 {daemon|status|health|reload|watchdog}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Loading…
Reference in New Issue
Block a user