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
|
#!/bin/sh
|
||||||
#
|
# RPCD backend for MagicMirror2 LuCI app
|
||||||
# RPCD backend for MagicMirror2 LuCI interface
|
|
||||||
# Copyright (C) 2026 CyberMind.fr (SecuBox)
|
|
||||||
#
|
|
||||||
|
|
||||||
. /lib/functions.sh
|
CONFIG="magicmirror2"
|
||||||
|
CONTAINER="secbx-magicmirror"
|
||||||
|
|
||||||
DATA_DIR=$(uci -q get magicmirror2.main.data_path || echo "/srv/magicmirror2")
|
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||||
LXC_NAME="magicmirror2"
|
|
||||||
|
|
||||||
# Get service status
|
|
||||||
get_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 running=0
|
||||||
local pid=""
|
if [ "$docker_available" = "1" ]; then
|
||||||
local lxc_state=""
|
docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER" && running=1
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
|
local installed=0
|
||||||
local port=$(uci -q get magicmirror2.main.port || echo "8085")
|
[ "$docker_available" = "1" ] && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "magicmirror" && installed=1
|
||||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
|
||||||
|
|
||||||
[ "$running" = "1" ] && web_url="http://${router_ip}:${port}"
|
cat <<EOFJ
|
||||||
|
|
||||||
# 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
|
|
||||||
{
|
{
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||||
"pid": ${pid:-0},
|
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||||
"lxc_state": "$lxc_state",
|
"port": ${port:-8080},
|
||||||
"port": $port,
|
"data_path": "${data_path:-/srv/magicmirror}"
|
||||||
"web_url": "$web_url",
|
|
||||||
"module_count": $module_count,
|
|
||||||
"data_path": "$DATA_DIR"
|
|
||||||
}
|
}
|
||||||
EOF
|
EOFJ
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get main configuration
|
do_install() {
|
||||||
get_config() {
|
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"}'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get display configuration
|
do_start() { [ -x /etc/init.d/magicmirror2 ] && /etc/init.d/magicmirror2 start >/dev/null 2>&1; echo '{"success":true}'; }
|
||||||
get_display_config() {
|
do_stop() { [ -x /etc/init.d/magicmirror2 ] && /etc/init.d/magicmirror2 stop >/dev/null 2>&1; echo '{"success":true}'; }
|
||||||
local width=$(uci -q get magicmirror2.display.width || echo "1920")
|
do_restart() { [ -x /etc/init.d/magicmirror2 ] && /etc/init.d/magicmirror2 restart >/dev/null 2>&1; echo '{"success":true}'; }
|
||||||
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")
|
|
||||||
|
|
||||||
cat <<EOF
|
list_methods() { cat <<'EOFM'
|
||||||
{
|
{"status":{},"install":{},"start":{},"stop":{},"restart":{}}
|
||||||
"width": $width,
|
EOFM
|
||||||
"height": $height,
|
|
||||||
"zoom": $zoom,
|
|
||||||
"brightness": $brightness
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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
|
case "$1" in
|
||||||
list)
|
list) list_methods ;;
|
||||||
cat <<EOF
|
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"}' ;;
|
||||||
"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"}'
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -1,45 +1,8 @@
|
|||||||
{
|
{
|
||||||
"admin/secubox/services/magicmirror2": {
|
"admin/secubox/services/magicmirror2": {
|
||||||
"title": "MagicMirror2",
|
"title": "MagicMirror",
|
||||||
"order": 60,
|
"action": { "type": "view", "path": "magicmirror2/overview" },
|
||||||
"action": {
|
"depends": { "acl": ["luci-app-magicmirror2"] },
|
||||||
"type": "firstchild"
|
"order": 70
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,7 @@
|
|||||||
{
|
{
|
||||||
"luci-app-magicmirror2": {
|
"luci-app-magicmirror2": {
|
||||||
"description": "Grant access to MagicMirror2 dashboard",
|
"description": "Grant access to MagicMirror2",
|
||||||
"read": {
|
"read": { "ubus": { "luci.magicmirror2": ["status"] }, "uci": ["magicmirror2"] },
|
||||||
"ubus": {
|
"write": { "ubus": { "luci.magicmirror2": ["install", "start", "stop", "restart"] }, "uci": ["magicmirror2"] }
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.default = '1';
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|
||||||
o = s.option(form.Value, 'nginx_container', _('Nginx Container'),
|
o = s.option(form.ListValue, 'runtime', _('Runtime'),
|
||||||
_('Name of the LXC container running nginx'));
|
_('Web server runtime for serving static sites'));
|
||||||
o.placeholder = 'nginx';
|
o.value('auto', _('Auto-detect (Recommended)'));
|
||||||
o.default = 'nginx';
|
o.value('uhttpd', _('uhttpd (Lightweight)'));
|
||||||
o.rmempty = false;
|
o.value('nginx', _('nginx LXC (Full-featured)'));
|
||||||
|
o.default = 'auto';
|
||||||
|
|
||||||
o = s.option(form.Value, 'sites_root', _('Sites Root Path'),
|
o = s.option(form.Value, 'sites_root', _('Sites Root Path'),
|
||||||
_('Directory where site files are stored'));
|
_('Directory where site files are stored'));
|
||||||
@ -36,6 +37,17 @@ return view.extend({
|
|||||||
o.default = '/srv/metablogizer/sites';
|
o.default = '/srv/metablogizer/sites';
|
||||||
o.rmempty = false;
|
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
|
// Info section
|
||||||
s = m.section(form.TypedSection, 'metablogizer', _('Information'));
|
s = m.section(form.TypedSection, 'metablogizer', _('Information'));
|
||||||
s.anonymous = true;
|
s.anonymous = true;
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
UCI_CONFIG="metablogizer"
|
UCI_CONFIG="metablogizer"
|
||||||
SITES_ROOT="/srv/metablogizer/sites"
|
SITES_ROOT="/srv/metablogizer/sites"
|
||||||
NGINX_CONTAINER="nginx"
|
NGINX_CONTAINER="nginx"
|
||||||
|
PORT_BASE=8900
|
||||||
|
|
||||||
# Helper: Get UCI value with default
|
# Helper: Get UCI value with default
|
||||||
get_uci() {
|
get_uci() {
|
||||||
@ -20,19 +21,42 @@ get_uci() {
|
|||||||
echo "${value:-$default}"
|
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
|
# Status method - get overall status and list all sites
|
||||||
method_status() {
|
method_status() {
|
||||||
local enabled nginx_running site_count
|
local enabled runtime detected_runtime nginx_running site_count
|
||||||
|
|
||||||
enabled=$(get_uci main enabled 0)
|
enabled=$(get_uci main enabled 0)
|
||||||
|
runtime=$(get_uci main runtime "auto")
|
||||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||||
NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER")
|
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
|
if lxc-info -n "$NGINX_CONTAINER" -s 2>/dev/null | grep -q "RUNNING"; then
|
||||||
nginx_running="1"
|
nginx_running="1"
|
||||||
else
|
|
||||||
nginx_running="0"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Count sites
|
# Count sites
|
||||||
@ -43,6 +67,8 @@ method_status() {
|
|||||||
|
|
||||||
json_init
|
json_init
|
||||||
json_add_boolean "enabled" "$enabled"
|
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_boolean "nginx_running" "$nginx_running"
|
||||||
json_add_int "site_count" "$site_count"
|
json_add_int "site_count" "$site_count"
|
||||||
json_add_string "sites_root" "$SITES_ROOT"
|
json_add_string "sites_root" "$SITES_ROOT"
|
||||||
@ -197,29 +223,72 @@ method_create_site() {
|
|||||||
EOF
|
EOF
|
||||||
fi
|
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')"
|
local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
||||||
|
|
||||||
# Create backend via HAProxy RPCD
|
uci set "haproxy.$backend_name=backend"
|
||||||
echo "{\"name\":\"$backend_name\",\"mode\":\"http\",\"enabled\":\"1\"}" | \
|
uci set "haproxy.$backend_name.name=$backend_name"
|
||||||
/usr/libexec/rpcd/luci.haproxy call create_backend >/dev/null 2>&1
|
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
|
# Create server
|
||||||
local nginx_ip
|
local server_name="${backend_name}_srv"
|
||||||
nginx_ip=$(lxc-info -n "$NGINX_CONTAINER" -iH 2>/dev/null | head -1)
|
uci set "haproxy.$server_name=server"
|
||||||
[ -z "$nginx_ip" ] && nginx_ip="nginx"
|
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\"}" | \
|
# 7. Create HAProxy vhost
|
||||||
/usr/libexec/rpcd/luci.haproxy call create_server >/dev/null 2>&1
|
local vhost_name=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
|
|
||||||
# 6. Create HAProxy vhost
|
|
||||||
local acme_val="0"
|
local acme_val="0"
|
||||||
[ "$ssl" = "1" ] && acme_val="1"
|
[ "$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
|
uci set "haproxy.$vhost_name=vhost"
|
||||||
_configure_nginx "$name"
|
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"
|
uci commit "$UCI_CONFIG"
|
||||||
|
|
||||||
@ -283,19 +352,32 @@ method_delete_site() {
|
|||||||
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT")
|
||||||
NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER")
|
NGINX_CONTAINER=$(get_uci main nginx_container "$NGINX_CONTAINER")
|
||||||
|
|
||||||
|
# Get site runtime
|
||||||
|
local site_runtime=$(get_uci "$id" runtime "")
|
||||||
|
|
||||||
# 1. Delete HAProxy vhost
|
# 1. Delete HAProxy vhost
|
||||||
local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
|
local vhost_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
echo "{\"id\":\"$vhost_id\"}" | \
|
uci delete "haproxy.$vhost_id" 2>/dev/null
|
||||||
/usr/libexec/rpcd/luci.haproxy call delete_vhost >/dev/null 2>&1
|
|
||||||
|
|
||||||
# 2. Delete HAProxy backend
|
# 2. Delete HAProxy backend and server
|
||||||
local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
local backend_name="metablog_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')"
|
||||||
echo "{\"id\":\"$backend_name\"}" | \
|
uci delete "haproxy.$backend_name" 2>/dev/null
|
||||||
/usr/libexec/rpcd/luci.haproxy call delete_backend >/dev/null 2>&1
|
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
|
# 3. Remove runtime config
|
||||||
rm -f "/var/lib/lxc/$NGINX_CONTAINER/rootfs/etc/nginx/sites.d/metablog-$name.conf"
|
if [ "$site_runtime" = "uhttpd" ]; then
|
||||||
lxc-attach -n "$NGINX_CONTAINER" -- nginx -s reload 2>/dev/null || true
|
# 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
|
# 4. Remove site directory
|
||||||
rm -rf "$SITES_ROOT/$name"
|
rm -rf "$SITES_ROOT/$name"
|
||||||
@ -537,31 +619,141 @@ method_update_site() {
|
|||||||
json_dump
|
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
|
# Get global settings
|
||||||
method_get_settings() {
|
method_get_settings() {
|
||||||
json_init
|
json_init
|
||||||
json_add_boolean "enabled" "$(get_uci main enabled 0)"
|
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 "nginx_container" "$(get_uci main nginx_container nginx)"
|
||||||
json_add_string "sites_root" "$(get_uci main sites_root /srv/metablogizer/sites)"
|
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
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save global settings
|
# Save global settings
|
||||||
method_save_settings() {
|
method_save_settings() {
|
||||||
local enabled nginx_container sites_root
|
local enabled runtime nginx_container sites_root gitea_url
|
||||||
|
|
||||||
read -r input
|
read -r input
|
||||||
json_load "$input"
|
json_load "$input"
|
||||||
json_get_var enabled enabled
|
json_get_var enabled enabled
|
||||||
|
json_get_var runtime runtime
|
||||||
json_get_var nginx_container nginx_container
|
json_get_var nginx_container nginx_container
|
||||||
json_get_var sites_root sites_root
|
json_get_var sites_root sites_root
|
||||||
|
json_get_var gitea_url gitea_url
|
||||||
|
|
||||||
# Ensure main section exists
|
# Ensure main section exists
|
||||||
uci -q get "$UCI_CONFIG.main" >/dev/null 2>&1 || uci set "$UCI_CONFIG.main=metablogizer"
|
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 "$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 "$nginx_container" ] && uci set "$UCI_CONFIG.main.nginx_container=$nginx_container"
|
||||||
[ -n "$sites_root" ] && uci set "$UCI_CONFIG.main.sites_root=$sites_root"
|
[ -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"
|
uci commit "$UCI_CONFIG"
|
||||||
|
|
||||||
json_init
|
json_init
|
||||||
@ -582,6 +774,8 @@ case "$1" in
|
|||||||
"delete_site": { "id": "string" },
|
"delete_site": { "id": "string" },
|
||||||
"sync_site": { "id": "string" },
|
"sync_site": { "id": "string" },
|
||||||
"get_publish_info": { "id": "string" },
|
"get_publish_info": { "id": "string" },
|
||||||
|
"upload_file": { "id": "string", "filename": "string", "content": "string" },
|
||||||
|
"list_files": { "id": "string" },
|
||||||
"get_settings": {},
|
"get_settings": {},
|
||||||
"save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" }
|
"save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" }
|
||||||
}
|
}
|
||||||
@ -597,6 +791,8 @@ EOF
|
|||||||
delete_site) method_delete_site ;;
|
delete_site) method_delete_site ;;
|
||||||
sync_site) method_sync_site ;;
|
sync_site) method_sync_site ;;
|
||||||
get_publish_info) method_get_publish_info ;;
|
get_publish_info) method_get_publish_info ;;
|
||||||
|
upload_file) method_upload_file ;;
|
||||||
|
list_files) method_list_files ;;
|
||||||
get_settings) method_get_settings ;;
|
get_settings) method_get_settings ;;
|
||||||
save_settings) method_save_settings ;;
|
save_settings) method_save_settings ;;
|
||||||
*) echo '{"error": "unknown method"}' ;;
|
*) echo '{"error": "unknown method"}' ;;
|
||||||
|
|||||||
@ -9,9 +9,13 @@
|
|||||||
"get_site",
|
"get_site",
|
||||||
"get_publish_info",
|
"get_publish_info",
|
||||||
"get_settings"
|
"get_settings"
|
||||||
]
|
],
|
||||||
|
"file": ["read", "list", "stat"]
|
||||||
},
|
},
|
||||||
"uci": ["metablogizer"]
|
"uci": ["metablogizer"],
|
||||||
|
"file": {
|
||||||
|
"/srv/metablogizer/sites/*": ["read", "list"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
@ -20,7 +24,9 @@
|
|||||||
"update_site",
|
"update_site",
|
||||||
"delete_site",
|
"delete_site",
|
||||||
"sync_site",
|
"sync_site",
|
||||||
"save_settings"
|
"save_settings",
|
||||||
|
"upload_file",
|
||||||
|
"list_files"
|
||||||
],
|
],
|
||||||
"luci.haproxy": [
|
"luci.haproxy": [
|
||||||
"create_backend",
|
"create_backend",
|
||||||
@ -28,9 +34,13 @@
|
|||||||
"create_vhost",
|
"create_vhost",
|
||||||
"delete_backend",
|
"delete_backend",
|
||||||
"delete_vhost"
|
"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
|
#!/bin/sh
|
||||||
#
|
# RPCD backend for mitmproxy LuCI app
|
||||||
# RPCD backend for mitmproxy LuCI interface
|
|
||||||
# Copyright (C) 2025 CyberMind.fr (SecuBox)
|
|
||||||
#
|
|
||||||
|
|
||||||
. /lib/functions.sh
|
CONFIG="mitmproxy"
|
||||||
|
CONTAINER="secbx-mitmproxy"
|
||||||
|
|
||||||
DATA_DIR=$(uci -q get mitmproxy.main.data_path || echo "/srv/mitmproxy")
|
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||||
LXC_NAME="mitmproxy"
|
|
||||||
CONF_DIR="$DATA_DIR"
|
|
||||||
LOG_FILE="$DATA_DIR/requests.log"
|
|
||||||
FLOW_FILE="$DATA_DIR/flows.bin"
|
|
||||||
|
|
||||||
# Get service status
|
|
||||||
get_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 running=0
|
||||||
local pid=""
|
if [ "$docker_available" = "1" ]; then
|
||||||
local mode="unknown"
|
docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER" && running=1
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fallback: check for direct process
|
local installed=0
|
||||||
if [ "$running" = "0" ]; then
|
[ "$docker_available" = "1" ] && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "mitmproxy" && installed=1
|
||||||
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
|
|
||||||
|
|
||||||
# Check nftables rules
|
cat <<EOFJ
|
||||||
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
|
|
||||||
{
|
{
|
||||||
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
|
||||||
"pid": ${pid:-0},
|
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
|
||||||
"mode": "$mode",
|
"web_port": ${web_port:-8081},
|
||||||
"proxy_mode": "$proxy_mode",
|
"proxy_port": ${proxy_port:-8080},
|
||||||
"lxc_state": "$lxc_state",
|
"data_path": "${data_path:-/srv/mitmproxy}"
|
||||||
"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")
|
|
||||||
}
|
}
|
||||||
EOF
|
EOFJ
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get main configuration
|
do_install() {
|
||||||
get_config() {
|
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"}'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get transparent mode configuration
|
do_start() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy start >/dev/null 2>&1; echo '{"success":true}'; }
|
||||||
get_transparent_config() {
|
do_stop() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy stop >/dev/null 2>&1; echo '{"success":true}'; }
|
||||||
local enabled=$(uci -q get mitmproxy.transparent.enabled || echo "0")
|
do_restart() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy restart >/dev/null 2>&1; echo '{"success":true}'; }
|
||||||
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")
|
|
||||||
|
|
||||||
cat <<EOF
|
list_methods() { cat <<'EOFM'
|
||||||
{
|
{"status":{},"install":{},"start":{},"stop":{},"restart":{}}
|
||||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
EOFM
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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
|
case "$1" in
|
||||||
list)
|
list) list_methods ;;
|
||||||
cat <<EOF
|
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"}' ;;
|
||||||
"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"}'
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -1,45 +1,8 @@
|
|||||||
{
|
{
|
||||||
"admin/secubox/security/mitmproxy": {
|
"admin/secubox/security/mitmproxy": {
|
||||||
"title": "mitmproxy",
|
"title": "mitmproxy",
|
||||||
"order": 50,
|
"action": { "type": "view", "path": "mitmproxy/overview" },
|
||||||
"action": {
|
"depends": { "acl": ["luci-app-mitmproxy"] },
|
||||||
"type": "firstchild"
|
"order": 60
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,43 +1,7 @@
|
|||||||
{
|
{
|
||||||
"luci-app-mitmproxy": {
|
"luci-app-mitmproxy": {
|
||||||
"description": "Grant access to mitmproxy LuCI app",
|
"description": "Grant access to mitmproxy",
|
||||||
"read": {
|
"read": { "ubus": { "luci.mitmproxy": ["status"] }, "uci": ["mitmproxy"] },
|
||||||
"ubus": {
|
"write": { "ubus": { "luci.mitmproxy": ["install", "start", "stop", "restart"] }, "uci": ["mitmproxy"] }
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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',
|
icon: '\ud83e\udde5',
|
||||||
iconBg: 'rgba(124, 58, 237, 0.15)',
|
iconBg: 'rgba(124, 58, 237, 0.15)',
|
||||||
iconColor: '#7c3aed',
|
iconColor: '#7c3aed',
|
||||||
section: 'services',
|
section: 'security',
|
||||||
path: 'admin/services/tor-shield/overview',
|
path: 'admin/services/tor-shield/overview',
|
||||||
service: 'tor',
|
service: 'tor-shield',
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
},
|
},
|
||||||
'jellyfin': {
|
'jellyfin': {
|
||||||
@ -404,6 +404,78 @@ return baseclass.extend({
|
|||||||
path: 'admin/secubox/services/gitea/overview',
|
path: 'admin/secubox/services/gitea/overview',
|
||||||
service: 'gitea',
|
service: 'gitea',
|
||||||
version: '1.22.0'
|
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')]),
|
}, ['\uD83D\uDD0D ', _('Leak Test')]),
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'tor-btn tor-btn-warning',
|
'class': 'tor-btn tor-btn-warning',
|
||||||
'click': L.bind(this.handleRestart, this),
|
'click': L.bind(this.handleRestart, this)
|
||||||
'disabled': !status.enabled
|
|
||||||
}, ['\u21BB ', _('Restart')]),
|
}, ['\u21BB ', _('Restart')]),
|
||||||
E('a', {
|
E('a', {
|
||||||
'class': 'tor-btn',
|
'class': 'tor-btn',
|
||||||
|
|||||||
@ -598,6 +598,24 @@ _add_server_to_backend() {
|
|||||||
|
|
||||||
[ -n "$address" ] || return
|
[ -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=""
|
local check_opt=""
|
||||||
[ "$check" = "1" ] && check_opt="check"
|
[ "$check" = "1" ] && check_opt="check"
|
||||||
|
|
||||||
|
|||||||
@ -396,18 +396,22 @@ STUB
|
|||||||
mkdir -p /config/prefs/plugin /config/cache /music /var/log/lyrion
|
mkdir -p /config/prefs/plugin /config/cache /music /var/log/lyrion
|
||||||
chown -R nobody:nobody /config /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'
|
cat > /opt/lyrion/start.sh << 'START'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cd /opt/lyrion
|
cd /opt/lyrion
|
||||||
|
|
||||||
|
# Ensure directories exist with proper permissions
|
||||||
mkdir -p /config/prefs/plugin /config/cache /var/log/lyrion
|
mkdir -p /config/prefs/plugin /config/cache /var/log/lyrion
|
||||||
chown -R nobody:nobody /config /var/log/lyrion 2>/dev/null || true
|
chown -R nobody:nobody /config /var/log/lyrion /opt/lyrion 2>/dev/null || true
|
||||||
exec perl slimserver.pl \
|
|
||||||
|
# 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 \
|
--prefsdir /config/prefs \
|
||||||
--cachedir /config/cache \
|
--cachedir /config/cache \
|
||||||
--logdir /var/log/lyrion \
|
--logdir /var/log/lyrion \
|
||||||
--httpport 9000 \
|
--httpport 9000 \
|
||||||
--cliport 9090
|
--cliport 9090"
|
||||||
START
|
START
|
||||||
chmod +x /opt/lyrion/start.sh
|
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
|
. /lib/functions.sh
|
||||||
. /usr/share/libubox/jshn.sh
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
SECUBOX_VERSION="0.8.0"
|
SECUBOX_VERSION="0.8.1"
|
||||||
LOG_FILE="/var/log/secubox/core.log"
|
LOG_FILE="/var/log/secubox/core.log"
|
||||||
PID_FILE="/var/run/secubox/core.pid"
|
PID_FILE="/var/run/secubox/core.pid"
|
||||||
STATE_DIR="/var/run/secubox"
|
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
|
# Logging function
|
||||||
log() {
|
log() {
|
||||||
@ -162,6 +167,102 @@ run_health_check() {
|
|||||||
return 0
|
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
|
||||||
daemon_mode() {
|
daemon_mode() {
|
||||||
log info "SecuBox Core daemon starting (version $SECUBOX_VERSION)"
|
log info "SecuBox Core daemon starting (version $SECUBOX_VERSION)"
|
||||||
@ -172,13 +273,27 @@ daemon_mode() {
|
|||||||
# Get health check interval
|
# Get health check interval
|
||||||
local health_interval=$(uci -q get secubox.main.health_check_interval || echo "300")
|
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
|
# Main daemon loop
|
||||||
|
local health_counter=0
|
||||||
|
local health_cycles=$((health_interval / watchdog_interval))
|
||||||
|
[ "$health_cycles" -lt 1 ] && health_cycles=1
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# Run periodic health check
|
# Run watchdog every cycle
|
||||||
run_health_check > /tmp/secubox/health-status.json
|
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 until next check
|
||||||
sleep "$health_interval"
|
sleep "$watchdog_interval"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,8 +312,11 @@ case "$1" in
|
|||||||
log info "Reloading configuration"
|
log info "Reloading configuration"
|
||||||
killall -HUP secubox-core 2>/dev/null || true
|
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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user