Initial commit: SecuBox v1.0.0-try2
This commit is contained in:
parent
b05776c24a
commit
ef240b650b
106
luci-app-auth-guardian/.github/workflows/build.yml
vendored
Normal file
106
luci-app-auth-guardian/.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
name: Build OpenWrt LuCI Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PKG_NAME: luci-app-auth-guardian
|
||||
OPENWRT_VERSION: '23.05.5'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check structure
|
||||
run: |
|
||||
test -f Makefile
|
||||
grep -q "PKG_NAME:=luci-app-auth-guardian" Makefile
|
||||
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.arch }}
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
target: x86
|
||||
subtarget: 64
|
||||
- arch: aarch64_cortex-a53
|
||||
target: mvebu
|
||||
subtarget: cortexa53
|
||||
- arch: aarch64_cortex-a72
|
||||
target: mvebu
|
||||
subtarget: cortexa72
|
||||
- arch: arm_cortex-a9_vfpv3-d16
|
||||
target: mvebu
|
||||
subtarget: cortexa9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang flex bison g++ gawk \
|
||||
gcc-multilib gettext git libncurses5-dev libssl-dev \
|
||||
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
|
||||
|
||||
- name: Download and extract SDK
|
||||
run: |
|
||||
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
|
||||
wget -q "${SDK_BASE}/sha256sums"
|
||||
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
|
||||
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
|
||||
wget -q "${SDK_BASE}/${SDK_FILE}"
|
||||
case "$SDK_FILE" in
|
||||
*.tar.xz) tar -xJf "$SDK_FILE" ;;
|
||||
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
|
||||
esac
|
||||
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
|
||||
mv "$SDK_DIR" sdk
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd sdk
|
||||
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
mkdir -p "package/${{ env.PKG_NAME }}"
|
||||
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
|
||||
make defconfig
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
|
||||
path: sdk/bin/**/*.ipk
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifacts/**/*.ipk
|
||||
generate_release_notes: true
|
||||
49
luci-app-auth-guardian/Makefile
Normal file
49
luci-app-auth-guardian/Makefile
Normal file
@ -0,0 +1,49 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-auth-guardian
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-auth-guardian
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=Auth Guardian - Authentication & Session Manager
|
||||
DEPENDS:=+luci-base +rpcd +nodogsplash
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/luci-app-auth-guardian/description
|
||||
Comprehensive authentication and session management:
|
||||
- Captive portal with customizable splash pages
|
||||
- OAuth2/OIDC integration (Google, GitHub, etc.)
|
||||
- Cookie-based session management
|
||||
- MAC authentication bypass
|
||||
- Voucher/ticket system
|
||||
- Time-based access control
|
||||
- User/device tracking
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-auth-guardian/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.auth-guardian $(1)/usr/libexec/rpcd/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/authguard $(1)/etc/config/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/auth-guardian
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/auth-guardian/*.js $(1)/www/luci-static/resources/view/auth-guardian/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/auth-guardian
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/auth-guardian/*.js $(1)/www/luci-static/resources/auth-guardian/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-auth-guardian))
|
||||
42
luci-app-auth-guardian/README.md
Normal file
42
luci-app-auth-guardian/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Auth Guardian for OpenWrt
|
||||
|
||||
Comprehensive authentication and session management system.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎨 Captive Portal
|
||||
- Customizable splash pages
|
||||
- Logo and branding support
|
||||
- Terms of service acceptance
|
||||
|
||||
### 🔑 OAuth Integration
|
||||
- Google Sign-In
|
||||
- GitHub Authentication
|
||||
- Facebook Login
|
||||
- Twitter/X Login
|
||||
|
||||
### 🎟️ Voucher System
|
||||
- Generate access codes
|
||||
- Time-limited validity
|
||||
- Bandwidth restrictions
|
||||
|
||||
### 🍪 Session Management
|
||||
- Secure cookies (HttpOnly, SameSite)
|
||||
- Session timeout control
|
||||
- Concurrent session limits
|
||||
|
||||
### ⏭️ Bypass Rules
|
||||
- MAC whitelist
|
||||
- IP whitelist
|
||||
- Domain exceptions
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install luci-app-auth-guardian
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - CyberMind Security
|
||||
51
luci-app-auth-guardian/demo/index.html
Normal file
51
luci-app-auth-guardian/demo/index.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Auth Guardian - Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||||
.splash { background: #1e293b; padding: 48px; border-radius: 24px; text-align: center; max-width: 400px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
|
||||
.logo { font-size: 64px; margin-bottom: 16px; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
.message { color: #94a3b8; margin-bottom: 32px; }
|
||||
.oauth-btn { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; padding: 14px; border-radius: 12px; border: none; font-size: 16px; font-weight: 600; cursor: pointer; margin-bottom: 12px; transition: transform 0.2s; }
|
||||
.oauth-btn:hover { transform: translateY(-2px); }
|
||||
.google { background: #fff; color: #333; }
|
||||
.github { background: #333; color: #fff; }
|
||||
.divider { display: flex; align-items: center; gap: 16px; margin: 24px 0; color: #64748b; }
|
||||
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #334155; }
|
||||
.voucher-input { width: 100%; padding: 14px; border-radius: 12px; border: 2px solid #334155; background: #0f172a; color: #f1f5f9; font-size: 16px; text-align: center; letter-spacing: 4px; text-transform: uppercase; }
|
||||
.voucher-input:focus { outline: none; border-color: #06b6d4; }
|
||||
.connect-btn { width: 100%; padding: 14px; border-radius: 12px; border: none; background: linear-gradient(135deg, #0891b2, #06b6d4); color: #fff; font-size: 16px; font-weight: 600; cursor: pointer; margin-top: 16px; }
|
||||
.terms { color: #64748b; font-size: 12px; margin-top: 24px; }
|
||||
.terms a { color: #06b6d4; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="splash">
|
||||
<div class="logo">🔐</div>
|
||||
<h1>Welcome</h1>
|
||||
<p class="message">Please authenticate to access the network</p>
|
||||
|
||||
<button class="oauth-btn google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="#4285f4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34a853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#fbbc05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#ea4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
|
||||
Sign in with Google
|
||||
</button>
|
||||
|
||||
<button class="oauth-btn github">
|
||||
<svg width="20" height="20" fill="#fff" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
Sign in with GitHub
|
||||
</button>
|
||||
|
||||
<div class="divider">or use voucher</div>
|
||||
|
||||
<input type="text" class="voucher-input" placeholder="XXXX-XXXX" maxlength="9">
|
||||
<button class="connect-btn">Connect</button>
|
||||
|
||||
<p class="terms">By connecting, you agree to our <a href="#">Terms of Service</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({object:'luci.auth-guardian',method:'status',expect:{}});
|
||||
var callSessions = rpc.declare({object:'luci.auth-guardian',method:'sessions',expect:{sessions:[]}});
|
||||
var callVouchers = rpc.declare({object:'luci.auth-guardian',method:'vouchers',expect:{vouchers:[]}});
|
||||
var callOAuthProviders = rpc.declare({object:'luci.auth-guardian',method:'oauth_providers',expect:{providers:[]}});
|
||||
var callBypassList = rpc.declare({object:'luci.auth-guardian',method:'bypass_list',expect:{}});
|
||||
var callGenerateVoucher = rpc.declare({object:'luci.auth-guardian',method:'generate_voucher'});
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getSessions: callSessions,
|
||||
getVouchers: callVouchers,
|
||||
getOAuthProviders: callOAuthProviders,
|
||||
getBypassList: callBypassList,
|
||||
generateVoucher: callGenerateVoucher
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require auth-guardian.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getBypassList(); },
|
||||
render: function(data) {
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '⏭️ Bypass Rules'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Devices and domains that bypass authentication.'),
|
||||
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:16px'}, [
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, '🖥️ MAC Addresses'),
|
||||
E('div', {}, (data.mac || []).map(function(m) {
|
||||
return E('div', {style:'padding:8px;background:#0f172a;border-radius:6px;margin-bottom:8px;font-family:monospace;color:#94a3b8'}, m);
|
||||
}))
|
||||
]),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, '🌐 IP Addresses'),
|
||||
E('div', {}, (data.ip || []).map(function(ip) {
|
||||
return E('div', {style:'padding:8px;background:#0f172a;border-radius:6px;margin-bottom:8px;font-family:monospace;color:#94a3b8'}, ip);
|
||||
}))
|
||||
]),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, '🔗 Domains'),
|
||||
E('div', {}, (data.domain || []).map(function(d) {
|
||||
return E('div', {style:'padding:8px;background:#0f172a;border-radius:6px;margin-bottom:8px;color:#94a3b8'}, d);
|
||||
}))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require auth-guardian.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getOAuthProviders(); },
|
||||
render: function(data) {
|
||||
var providers = data.providers || [];
|
||||
var icons = {google:'🔵',github:'⚫',facebook:'🔷',twitter:'🐦'};
|
||||
var colors = {google:'#4285f4',github:'#333',facebook:'#1877f2',twitter:'#1da1f2'};
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🔑 OAuth Providers'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Configure third-party authentication providers.'),
|
||||
E('div', {style:'display:grid;gap:16px'}, [
|
||||
['google', 'Google', 'Sign in with Google account'],
|
||||
['github', 'GitHub', 'Sign in with GitHub account'],
|
||||
['facebook', 'Facebook', 'Sign in with Facebook account'],
|
||||
['twitter', 'Twitter/X', 'Sign in with Twitter account']
|
||||
].map(function(p) {
|
||||
var provider = providers.find(function(x) { return x.id === p[0]; });
|
||||
var enabled = provider ? provider.enabled : false;
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;display:flex;align-items:center;gap:16px'}, [
|
||||
E('span', {style:'font-size:32px'}, icons[p[0]] || '🔐'),
|
||||
E('div', {style:'flex:1'}, [
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, p[1]),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px'}, p[2])
|
||||
]),
|
||||
E('span', {style:'padding:6px 12px;border-radius:6px;font-weight:600;background:'+(enabled?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, enabled ? 'Enabled' : 'Disabled')
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require auth-guardian.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([api.getStatus(), api.getSessions()]);
|
||||
},
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var sessions = data[1].sessions || [];
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('style', {}, [
|
||||
'.ag{font-family:system-ui,sans-serif}',
|
||||
'.ag-hdr{background:linear-gradient(135deg,#0891b2,#06b6d4);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
|
||||
'.ag-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
|
||||
'.ag-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
|
||||
'.ag-stat-val{font-size:28px;font-weight:700;color:#06b6d4}',
|
||||
'.ag-stat-lbl{font-size:12px;color:#94a3b8;margin-top:4px}'
|
||||
].join('')),
|
||||
E('div', {class:'ag'}, [
|
||||
E('div', {class:'ag-hdr'}, [
|
||||
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🔐 Auth Guardian'),
|
||||
E('p', {style:'margin:0;opacity:.9'}, 'Authentication & Session Management')
|
||||
]),
|
||||
E('div', {class:'ag-stats'}, [
|
||||
E('div', {class:'ag-stat'}, [
|
||||
E('div', {class:'ag-stat-val'}, status.enabled ? '✓' : '✗'),
|
||||
E('div', {class:'ag-stat-lbl'}, 'Status')
|
||||
]),
|
||||
E('div', {class:'ag-stat'}, [
|
||||
E('div', {class:'ag-stat-val'}, sessions.length),
|
||||
E('div', {class:'ag-stat-lbl'}, 'Active Sessions')
|
||||
]),
|
||||
E('div', {class:'ag-stat'}, [
|
||||
E('div', {class:'ag-stat-val'}, status.captive_portal_active ? '✓' : '✗'),
|
||||
E('div', {class:'ag-stat-lbl'}, 'Captive Portal')
|
||||
]),
|
||||
E('div', {class:'ag-stat'}, [
|
||||
E('div', {class:'ag-stat-val'}, status.auth_method || 'splash'),
|
||||
E('div', {class:'ag-stat-lbl'}, 'Auth Method')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require auth-guardian.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getSessions(); },
|
||||
render: function(data) {
|
||||
var sessions = data.sessions || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '👥 Active Sessions'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
sessions.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
|
||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
||||
E('th', {style:'padding:12px;text-align:left'}, 'Hostname'),
|
||||
E('th', {style:'padding:12px'}, 'IP'),
|
||||
E('th', {style:'padding:12px'}, 'MAC'),
|
||||
E('th', {style:'padding:12px'}, 'Status')
|
||||
])
|
||||
].concat(sessions.map(function(s) {
|
||||
return E('tr', {}, [
|
||||
E('td', {style:'padding:12px'}, s.hostname || 'Unknown'),
|
||||
E('td', {style:'padding:12px;font-family:monospace'}, s.ip),
|
||||
E('td', {style:'padding:12px;font-family:monospace;font-size:12px'}, s.mac),
|
||||
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e;font-size:12px'}, s.status))
|
||||
]);
|
||||
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No active sessions')
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🎨 Splash Page Editor'),
|
||||
E('p', {style:'color:#94a3b8'}, 'Customize the captive portal splash page appearance.'),
|
||||
E('div', {style:'display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px'}, [
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('h3', {style:'color:#f1f5f9;margin-bottom:16px'}, 'Settings'),
|
||||
E('div', {style:'margin-bottom:12px'}, [
|
||||
E('label', {style:'display:block;color:#94a3b8;font-size:13px;margin-bottom:4px'}, 'Title'),
|
||||
E('input', {type:'text',value:'Welcome',style:'width:100%;padding:8px;border-radius:6px;border:1px solid #334155;background:#0f172a;color:#f1f5f9'})
|
||||
]),
|
||||
E('div', {style:'margin-bottom:12px'}, [
|
||||
E('label', {style:'display:block;color:#94a3b8;font-size:13px;margin-bottom:4px'}, 'Message'),
|
||||
E('textarea', {style:'width:100%;padding:8px;border-radius:6px;border:1px solid #334155;background:#0f172a;color:#f1f5f9;height:80px'}, 'Please authenticate to access the network')
|
||||
]),
|
||||
E('div', {style:'margin-bottom:12px'}, [
|
||||
E('label', {style:'display:block;color:#94a3b8;font-size:13px;margin-bottom:4px'}, 'Button Color'),
|
||||
E('input', {type:'color',value:'#3b82f6',style:'width:60px;height:32px;border:none;border-radius:6px;cursor:pointer'})
|
||||
])
|
||||
]),
|
||||
E('div', {style:'background:#0f172a;padding:40px;border-radius:12px;text-align:center'}, [
|
||||
E('h2', {style:'color:#f1f5f9;margin-bottom:8px'}, 'Welcome'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:24px'}, 'Please authenticate to access the network'),
|
||||
E('button', {style:'background:#3b82f6;color:#fff;border:none;padding:12px 32px;border-radius:8px;font-weight:600;cursor:pointer'}, 'Connect'),
|
||||
E('p', {style:'color:#64748b;font-size:12px;margin-top:16px'}, 'Preview')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require auth-guardian.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getVouchers(); },
|
||||
render: function(data) {
|
||||
var vouchers = data.vouchers || [];
|
||||
var self = this;
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🎟️ Access Vouchers'),
|
||||
E('div', {style:'margin-bottom:16px'}, [
|
||||
E('button', {class:'cbi-button cbi-button-positive',click:function(){
|
||||
api.generateVoucher().then(function(r) {
|
||||
ui.addNotification(null, E('p', {}, 'Generated voucher: ' + r.code), 'success');
|
||||
location.reload();
|
||||
});
|
||||
}}, '+ Generate Voucher')
|
||||
]),
|
||||
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:16px'}, vouchers.map(function(v) {
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;text-align:center'}, [
|
||||
E('div', {style:'font-family:monospace;font-size:20px;font-weight:700;color:#06b6d4;letter-spacing:2px'}, v.code),
|
||||
E('div', {style:'margin-top:12px;color:#94a3b8;font-size:13px'}, 'Valid for 24 hours'),
|
||||
E('span', {style:'display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:'+(v.status==='unused'?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, v.status)
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
45
luci-app-auth-guardian/root/etc/config/authguard
Normal file
45
luci-app-auth-guardian/root/etc/config/authguard
Normal file
@ -0,0 +1,45 @@
|
||||
config authguard 'global'
|
||||
option enabled '1'
|
||||
option interface 'br-lan'
|
||||
option auth_method 'splash'
|
||||
option session_timeout '3600'
|
||||
option idle_timeout '600'
|
||||
|
||||
config splash 'default'
|
||||
option enabled '1'
|
||||
option title 'Welcome'
|
||||
option message 'Please authenticate to access the network'
|
||||
option logo '/luci-static/resources/auth-guardian/logo.png'
|
||||
option background_color '#0f172a'
|
||||
option button_color '#3b82f6'
|
||||
option require_terms '1'
|
||||
|
||||
config oauth 'google'
|
||||
option enabled '0'
|
||||
option client_id ''
|
||||
option client_secret ''
|
||||
option redirect_uri '/auth/callback/google'
|
||||
|
||||
config oauth 'github'
|
||||
option enabled '0'
|
||||
option client_id ''
|
||||
option client_secret ''
|
||||
option redirect_uri '/auth/callback/github'
|
||||
|
||||
config bypass 'whitelist'
|
||||
list mac 'AA:BB:CC:DD:EE:FF'
|
||||
list ip '192.168.1.100'
|
||||
list domain 'allowed.example.com'
|
||||
|
||||
config voucher 'system'
|
||||
option enabled '1'
|
||||
option validity '86400'
|
||||
option bandwidth_limit '10000'
|
||||
option prefix 'WIFI'
|
||||
|
||||
config session 'policy'
|
||||
option max_sessions '100'
|
||||
option session_cookie 'authguard_session'
|
||||
option secure_cookie '1'
|
||||
option httponly '1'
|
||||
option samesite 'Strict'
|
||||
147
luci-app-auth-guardian/root/usr/libexec/rpcd/luci.auth-guardian
Executable file
147
luci-app-auth-guardian/root/usr/libexec/rpcd/luci.auth-guardian
Executable file
@ -0,0 +1,147 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
SESSIONS_FILE="/tmp/authguard_sessions.json"
|
||||
VOUCHERS_FILE="/tmp/authguard_vouchers.json"
|
||||
|
||||
get_status() {
|
||||
json_init
|
||||
|
||||
local enabled auth_method
|
||||
config_load authguard
|
||||
config_get enabled global enabled "0"
|
||||
config_get auth_method global auth_method "splash"
|
||||
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "auth_method" "$auth_method"
|
||||
|
||||
# Count active sessions
|
||||
local sessions=0
|
||||
[ -f "$SESSIONS_FILE" ] && sessions=$(cat "$SESSIONS_FILE" | grep -c "active" || echo 0)
|
||||
json_add_int "active_sessions" "$sessions"
|
||||
|
||||
# Check nodogsplash
|
||||
local nds_running=0
|
||||
pgrep -f nodogsplash >/dev/null && nds_running=1
|
||||
json_add_boolean "captive_portal_active" "$nds_running"
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_sessions() {
|
||||
json_init
|
||||
json_add_array "sessions"
|
||||
|
||||
# Parse nodogsplash clients if available
|
||||
if command -v ndsctl >/dev/null 2>&1; then
|
||||
ndsctl json 2>/dev/null | jsonfilter -e '@.clients[*]' 2>/dev/null | while read client; do
|
||||
json_add_object ""
|
||||
json_add_string "data" "$client"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
# Also check DHCP leases for MAC addresses
|
||||
if [ -f /tmp/dhcp.leases ]; then
|
||||
while read expires mac ip hostname clientid; do
|
||||
json_add_object ""
|
||||
json_add_string "mac" "$mac"
|
||||
json_add_string "ip" "$ip"
|
||||
json_add_string "hostname" "${hostname:-unknown}"
|
||||
json_add_string "status" "connected"
|
||||
json_close_object
|
||||
done < /tmp/dhcp.leases
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_vouchers() {
|
||||
json_init
|
||||
json_add_array "vouchers"
|
||||
|
||||
if [ -f "$VOUCHERS_FILE" ]; then
|
||||
cat "$VOUCHERS_FILE"
|
||||
else
|
||||
# Generate sample vouchers
|
||||
for i in 1 2 3 4 5; do
|
||||
json_add_object ""
|
||||
json_add_string "code" "WIFI-$(head -c 4 /dev/urandom | hexdump -e '"%08x"' | tr '[:lower:]' '[:upper:]')"
|
||||
json_add_string "status" "unused"
|
||||
json_add_int "validity" "86400"
|
||||
json_add_int "created" "$(date +%s)"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_oauth_providers() {
|
||||
config_load authguard
|
||||
json_init
|
||||
json_add_array "providers"
|
||||
|
||||
_add_provider() {
|
||||
local enabled
|
||||
config_get enabled "$1" enabled "0"
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_provider oauth
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_bypass_list() {
|
||||
config_load authguard
|
||||
json_init
|
||||
json_add_array "mac"
|
||||
config_list_foreach whitelist mac _add_item
|
||||
json_close_array
|
||||
json_add_array "ip"
|
||||
config_list_foreach whitelist ip _add_item
|
||||
json_close_array
|
||||
json_add_array "domain"
|
||||
config_list_foreach whitelist domain _add_item
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
_add_item() {
|
||||
json_add_string "" "$1"
|
||||
}
|
||||
|
||||
generate_voucher() {
|
||||
local code="WIFI-$(head -c 4 /dev/urandom | hexdump -e '"%08x"' | tr '[:lower:]' '[:upper:]')"
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "code" "$code"
|
||||
json_add_int "validity" "86400"
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"sessions":{},"vouchers":{},"oauth_providers":{},"bypass_list":{},"generate_voucher":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
sessions) get_sessions ;;
|
||||
vouchers) get_vouchers ;;
|
||||
oauth_providers) get_oauth_providers ;;
|
||||
bypass_list) get_bypass_list ;;
|
||||
generate_voucher) generate_voucher ;;
|
||||
*) echo '{"error":"Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,37 @@
|
||||
{
|
||||
"admin/services/auth-guardian": {
|
||||
"title": "Auth Guardian",
|
||||
"order": 70,
|
||||
"action": {"type": "firstchild"}
|
||||
},
|
||||
"admin/services/auth-guardian/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {"type": "view", "path": "auth-guardian/overview"}
|
||||
},
|
||||
"admin/services/auth-guardian/sessions": {
|
||||
"title": "Sessions",
|
||||
"order": 20,
|
||||
"action": {"type": "view", "path": "auth-guardian/sessions"}
|
||||
},
|
||||
"admin/services/auth-guardian/vouchers": {
|
||||
"title": "Vouchers",
|
||||
"order": 30,
|
||||
"action": {"type": "view", "path": "auth-guardian/vouchers"}
|
||||
},
|
||||
"admin/services/auth-guardian/oauth": {
|
||||
"title": "OAuth Providers",
|
||||
"order": 40,
|
||||
"action": {"type": "view", "path": "auth-guardian/oauth"}
|
||||
},
|
||||
"admin/services/auth-guardian/splash": {
|
||||
"title": "Splash Page",
|
||||
"order": 50,
|
||||
"action": {"type": "view", "path": "auth-guardian/splash"}
|
||||
},
|
||||
"admin/services/auth-guardian/bypass": {
|
||||
"title": "Bypass Rules",
|
||||
"order": 60,
|
||||
"action": {"type": "view", "path": "auth-guardian/bypass"}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-auth-guardian": {
|
||||
"description": "Auth Guardian",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.auth-guardian": ["status", "sessions", "vouchers", "oauth_providers", "bypass_list"]
|
||||
},
|
||||
"uci": ["authguard"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.auth-guardian": ["generate_voucher"]
|
||||
},
|
||||
"uci": ["authguard"]
|
||||
}
|
||||
}
|
||||
}
|
||||
106
luci-app-bandwidth-manager/.github/workflows/build.yml
vendored
Normal file
106
luci-app-bandwidth-manager/.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
name: Build OpenWrt LuCI Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PKG_NAME: luci-app-bandwidth-manager
|
||||
OPENWRT_VERSION: '23.05.5'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check structure
|
||||
run: |
|
||||
test -f Makefile
|
||||
grep -q "PKG_NAME:=luci-app-bandwidth-manager" Makefile
|
||||
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.arch }}
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
target: x86
|
||||
subtarget: 64
|
||||
- arch: aarch64_cortex-a53
|
||||
target: mvebu
|
||||
subtarget: cortexa53
|
||||
- arch: aarch64_cortex-a72
|
||||
target: mvebu
|
||||
subtarget: cortexa72
|
||||
- arch: arm_cortex-a9_vfpv3-d16
|
||||
target: mvebu
|
||||
subtarget: cortexa9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang flex bison g++ gawk \
|
||||
gcc-multilib gettext git libncurses5-dev libssl-dev \
|
||||
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
|
||||
|
||||
- name: Download and extract SDK
|
||||
run: |
|
||||
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
|
||||
wget -q "${SDK_BASE}/sha256sums"
|
||||
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
|
||||
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
|
||||
wget -q "${SDK_BASE}/${SDK_FILE}"
|
||||
case "$SDK_FILE" in
|
||||
*.tar.xz) tar -xJf "$SDK_FILE" ;;
|
||||
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
|
||||
esac
|
||||
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
|
||||
mv "$SDK_DIR" sdk
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd sdk
|
||||
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
mkdir -p "package/${{ env.PKG_NAME }}"
|
||||
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
|
||||
make defconfig
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
|
||||
path: sdk/bin/**/*.ipk
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifacts/**/*.ipk
|
||||
generate_release_notes: true
|
||||
53
luci-app-bandwidth-manager/Makefile
Normal file
53
luci-app-bandwidth-manager/Makefile
Normal file
@ -0,0 +1,53 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-bandwidth-manager
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-bandwidth-manager
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=Bandwidth Manager - QoS, Quotas & Media Detection
|
||||
DEPENDS:=+luci-base +rpcd +tc +kmod-sched-cake +kmod-sched-fq-codel
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/luci-app-bandwidth-manager/description
|
||||
Advanced bandwidth management for OpenWrt with:
|
||||
- Per-client and per-group quotas (daily/monthly)
|
||||
- Bandwidth throttling and shaping
|
||||
- 8-level QoS priority classes
|
||||
- Automatic media detection (VoIP, Gaming, Streaming)
|
||||
- Time-based scheduling
|
||||
- Real-time statistics and graphs
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-bandwidth-manager/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.bandwidth-manager $(1)/usr/libexec/rpcd/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/bandwidth $(1)/etc/config/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/bandwidth-manager
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/bandwidth-manager/*.js $(1)/www/luci-static/resources/view/bandwidth-manager/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/bandwidth-manager
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/bandwidth-manager/*.js $(1)/www/luci-static/resources/bandwidth-manager/
|
||||
endef
|
||||
|
||||
define Package/luci-app-bandwidth-manager/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || /etc/init.d/rpcd reload
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-bandwidth-manager))
|
||||
50
luci-app-bandwidth-manager/README.md
Normal file
50
luci-app-bandwidth-manager/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Bandwidth Manager for OpenWrt
|
||||
|
||||
Advanced bandwidth management with QoS, quotas, and automatic media detection.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 QoS Priority Classes
|
||||
- 8 configurable priority levels
|
||||
- Per-class rate guarantees and ceilings
|
||||
- DSCP marking support
|
||||
|
||||
### 📊 Bandwidth Quotas
|
||||
- Daily and monthly limits
|
||||
- Per-client or per-group quotas
|
||||
- Configurable actions (throttle/block)
|
||||
|
||||
### 🎬 Media Detection
|
||||
- Automatic VoIP detection (SIP, RTP)
|
||||
- Gaming traffic prioritization
|
||||
- Streaming service identification
|
||||
- Domain-based classification
|
||||
|
||||
### ⏰ Time-Based Scheduling
|
||||
- Peak/off-peak configurations
|
||||
- Day-of-week rules
|
||||
- Automatic limit adjustments
|
||||
|
||||
### 👥 Client Management
|
||||
- Per-device statistics
|
||||
- MAC-based identification
|
||||
- Real-time monitoring
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install luci-app-bandwidth-manager
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `/etc/config/bandwidth` or use the LuCI interface.
|
||||
|
||||
## Demo
|
||||
|
||||
Open `demo/index.html` in a browser to see a live preview.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - CyberMind Security
|
||||
132
luci-app-bandwidth-manager/demo/index.html
Normal file
132
luci-app-bandwidth-manager/demo/index.html
Normal file
@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bandwidth Manager - Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { background: linear-gradient(135deg, #7c3aed, #a855f7); padding: 32px; border-radius: 16px; margin-bottom: 24px; }
|
||||
.header h1 { font-size: 32px; margin-bottom: 8px; }
|
||||
.header p { opacity: 0.9; }
|
||||
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
|
||||
.stat { background: #1e293b; padding: 24px; border-radius: 12px; text-align: center; }
|
||||
.stat-value { font-size: 36px; font-weight: 700; color: #a855f7; }
|
||||
.stat-label { color: #94a3b8; margin-top: 8px; }
|
||||
.section { background: #1e293b; padding: 24px; border-radius: 12px; margin-bottom: 16px; }
|
||||
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||||
.class-item { display: flex; align-items: center; gap: 16px; padding: 12px; background: #0f172a; border-radius: 8px; margin-bottom: 8px; }
|
||||
.class-name { width: 120px; font-weight: 600; }
|
||||
.class-bar { flex: 1; height: 8px; background: #334155; border-radius: 4px; overflow: hidden; }
|
||||
.class-fill { height: 100%; background: linear-gradient(90deg, #7c3aed, #a855f7); transition: width 0.3s; }
|
||||
.badge { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; background: #7c3aed20; color: #a855f7; }
|
||||
.gauge { width: 200px; height: 200px; margin: 0 auto; position: relative; }
|
||||
.gauge-bg { fill: none; stroke: #334155; stroke-width: 20; }
|
||||
.gauge-fill { fill: none; stroke: url(#gradient); stroke-width: 20; stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; transition: stroke-dasharray 0.5s; }
|
||||
.gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; }
|
||||
.gauge-value { font-size: 48px; font-weight: 700; color: #a855f7; }
|
||||
.gauge-label { color: #94a3b8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⚡ Bandwidth Manager</h1>
|
||||
<p>QoS, Quotas & Real-time Media Detection for OpenWrt</p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="download">0</div>
|
||||
<div class="stat-label">Download (Mbps)</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="upload">0</div>
|
||||
<div class="stat-label">Upload (Mbps)</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="clients">12</div>
|
||||
<div class="stat-label">Active Clients</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="quota">67%</div>
|
||||
<div class="stat-label">Quota Used</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||||
<div class="section">
|
||||
<div class="section-title">📊 QoS Classes</div>
|
||||
<div class="class-item">
|
||||
<span class="class-name">Real-time</span>
|
||||
<span class="badge">P1</span>
|
||||
<div class="class-bar"><div class="class-fill" style="width: 30%"></div></div>
|
||||
<span style="color: #94a3b8; font-size: 13px">30%</span>
|
||||
</div>
|
||||
<div class="class-item">
|
||||
<span class="class-name">Interactive</span>
|
||||
<span class="badge">P2</span>
|
||||
<div class="class-bar"><div class="class-fill" style="width: 20%"></div></div>
|
||||
<span style="color: #94a3b8; font-size: 13px">20%</span>
|
||||
</div>
|
||||
<div class="class-item">
|
||||
<span class="class-name">Streaming</span>
|
||||
<span class="badge">P3</span>
|
||||
<div class="class-bar"><div class="class-fill" style="width: 20%"></div></div>
|
||||
<span style="color: #94a3b8; font-size: 13px">20%</span>
|
||||
</div>
|
||||
<div class="class-item">
|
||||
<span class="class-name">Browsing</span>
|
||||
<span class="badge">P4</span>
|
||||
<div class="class-bar"><div class="class-fill" style="width: 15%"></div></div>
|
||||
<span style="color: #94a3b8; font-size: 13px">15%</span>
|
||||
</div>
|
||||
<div class="class-item">
|
||||
<span class="class-name">Bulk</span>
|
||||
<span class="badge">P6</span>
|
||||
<div class="class-bar"><div class="class-fill" style="width: 5%"></div></div>
|
||||
<span style="color: #94a3b8; font-size: 13px">5%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">🎯 Media Detection</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
|
||||
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #22c55e;">
|
||||
<div style="font-size: 24px; margin-bottom: 4px;">📞</div>
|
||||
<div style="font-weight: 600;">VoIP</div>
|
||||
<div style="color: #94a3b8; font-size: 12px;">Real-time class</div>
|
||||
</div>
|
||||
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #f59e0b;">
|
||||
<div style="font-size: 24px; margin-bottom: 4px;">🎮</div>
|
||||
<div style="font-weight: 600;">Gaming</div>
|
||||
<div style="color: #94a3b8; font-size: 12px;">Interactive class</div>
|
||||
</div>
|
||||
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #ef4444;">
|
||||
<div style="font-size: 24px; margin-bottom: 4px;">📺</div>
|
||||
<div style="font-weight: 600;">Streaming</div>
|
||||
<div style="color: #94a3b8; font-size: 12px;">Streaming class</div>
|
||||
</div>
|
||||
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #3b82f6;">
|
||||
<div style="font-size: 24px; margin-bottom: 4px;">📥</div>
|
||||
<div style="font-weight: 600;">Downloads</div>
|
||||
<div style="color: #94a3b8; font-size: 12px;">Bulk class</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simulate live data
|
||||
function updateStats() {
|
||||
document.getElementById('download').textContent = (Math.random() * 50 + 50).toFixed(1);
|
||||
document.getElementById('upload').textContent = (Math.random() * 20 + 10).toFixed(1);
|
||||
}
|
||||
setInterval(updateStats, 1000);
|
||||
updateStats();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({object:'luci.bandwidth-manager',method:'status',expect:{}});
|
||||
var callClasses = rpc.declare({object:'luci.bandwidth-manager',method:'classes',expect:{classes:[]}});
|
||||
var callQuotas = rpc.declare({object:'luci.bandwidth-manager',method:'quotas',expect:{quotas:[]}});
|
||||
var callMedia = rpc.declare({object:'luci.bandwidth-manager',method:'media',expect:{media:[]}});
|
||||
var callClients = rpc.declare({object:'luci.bandwidth-manager',method:'clients',expect:{clients:[]}});
|
||||
var callStats = rpc.declare({object:'luci.bandwidth-manager',method:'stats',expect:{}});
|
||||
var callApplyQos = rpc.declare({object:'luci.bandwidth-manager',method:'apply_qos'});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatSpeed(kbps) {
|
||||
if (kbps >= 1000) return (kbps / 1000).toFixed(1) + ' Mbps';
|
||||
return kbps + ' Kbps';
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getClasses: callClasses,
|
||||
getQuotas: callQuotas,
|
||||
getMedia: callMedia,
|
||||
getClients: callClients,
|
||||
getStats: callStats,
|
||||
applyQos: callApplyQos,
|
||||
formatBytes: formatBytes,
|
||||
formatSpeed: formatSpeed
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require bandwidth-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getClasses(); },
|
||||
render: function(data) {
|
||||
var classes = data.classes || [];
|
||||
var colors = ['#ef4444','#f59e0b','#eab308','#22c55e','#06b6d4','#3b82f6','#8b5cf6','#ec4899'];
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('style', {}, '.qos-grid{display:grid;gap:16px}.qos-card{background:#1e293b;padding:20px;border-radius:12px;border-left:4px solid}.qos-name{font-size:18px;font-weight:600;color:#f1f5f9}.qos-desc{color:#94a3b8;font-size:13px;margin-top:4px}.qos-stats{display:flex;gap:20px;margin-top:16px}.qos-stat{text-align:center}.qos-stat-val{font-size:24px;font-weight:700}.qos-stat-lbl{font-size:11px;color:#64748b}'),
|
||||
E('h2', {}, '📊 QoS Priority Classes'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, '8 priority levels for traffic classification. Lower number = higher priority.'),
|
||||
E('div', {class:'qos-grid'}, classes.map(function(c, i) {
|
||||
return E('div', {class:'qos-card',style:'border-color:'+colors[i%8]}, [
|
||||
E('div', {class:'qos-name'}, c.name),
|
||||
E('div', {class:'qos-desc'}, c.description),
|
||||
E('div', {class:'qos-stats'}, [
|
||||
E('div', {class:'qos-stat'}, [E('div', {class:'qos-stat-val',style:'color:'+colors[i%8]}, c.priority), E('div', {class:'qos-stat-lbl'}, 'Priority')]),
|
||||
E('div', {class:'qos-stat'}, [E('div', {class:'qos-stat-val',style:'color:'+colors[i%8]}, c.rate+'%'), E('div', {class:'qos-stat-lbl'}, 'Guaranteed')]),
|
||||
E('div', {class:'qos-stat'}, [E('div', {class:'qos-stat-val',style:'color:'+colors[i%8]}, c.ceil+'%'), E('div', {class:'qos-stat-lbl'}, 'Maximum')])
|
||||
])
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require bandwidth-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getClients(); },
|
||||
render: function(data) {
|
||||
var clients = data.clients || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '👥 Connected Clients'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
|
||||
clients.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
|
||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
||||
E('th', {style:'padding:12px;text-align:left'}, 'Hostname'),
|
||||
E('th', {style:'padding:12px'}, 'IP Address'),
|
||||
E('th', {style:'padding:12px'}, 'MAC'),
|
||||
E('th', {style:'padding:12px'}, 'Download'),
|
||||
E('th', {style:'padding:12px'}, 'Upload')
|
||||
])
|
||||
].concat(clients.map(function(c) {
|
||||
return E('tr', {}, [
|
||||
E('td', {style:'padding:12px;font-weight:600'}, c.hostname),
|
||||
E('td', {style:'padding:12px;text-align:center;font-family:monospace'}, c.ip),
|
||||
E('td', {style:'padding:12px;text-align:center;font-family:monospace;font-size:12px'}, c.mac),
|
||||
E('td', {style:'padding:12px;text-align:center;color:#22c55e'}, api.formatBytes(c.rx_bytes)),
|
||||
E('td', {style:'padding:12px;text-align:center;color:#3b82f6'}, api.formatBytes(c.tx_bytes))
|
||||
]);
|
||||
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No clients connected')
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require bandwidth-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getMedia(); },
|
||||
render: function(data) {
|
||||
var media = data.media || [];
|
||||
var icons = {voip:'📞',gaming:'🎮',streaming:'📺',download:'📥',social:'💬',work:'💼'};
|
||||
var colors = {voip:'#22c55e',gaming:'#f59e0b',streaming:'#ef4444',download:'#3b82f6',social:'#ec4899',work:'#8b5cf6'};
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🎯 Media Detection'),
|
||||
E('p', {style:'color:#94a3b8'}, 'Automatic traffic classification based on ports, protocols, and domains.'),
|
||||
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:20px'}, media.map(function(m) {
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;border-top:4px solid '+(colors[m.id]||'#64748b')}, [
|
||||
E('div', {style:'font-size:32px;margin-bottom:8px'}, icons[m.id] || '📦'),
|
||||
E('div', {style:'font-size:18px;font-weight:600;color:#f1f5f9'}, m.name),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px;margin-top:4px'}, 'Class: '+m.class)
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require bandwidth-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([api.getStatus(), api.getClasses(), api.getClients()]);
|
||||
},
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var classes = data[1].classes || [];
|
||||
var clients = data[2].clients || [];
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('style', {}, [
|
||||
'.bw{font-family:system-ui,sans-serif}',
|
||||
'.bw-hdr{background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
|
||||
'.bw-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
|
||||
'.bw-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
|
||||
'.bw-stat-val{font-size:28px;font-weight:700;color:#a855f7}',
|
||||
'.bw-stat-lbl{font-size:12px;color:#94a3b8;margin-top:4px}',
|
||||
'.bw-section{background:#1e293b;padding:20px;border-radius:10px;margin-bottom:16px}',
|
||||
'.bw-section-title{font-size:16px;font-weight:600;color:#f1f5f9;margin-bottom:16px}',
|
||||
'.bw-class{display:flex;align-items:center;gap:12px;padding:12px;background:#0f172a;border-radius:8px;margin-bottom:8px}',
|
||||
'.bw-class-bar{height:8px;border-radius:4px;background:#334155;flex:1}',
|
||||
'.bw-class-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#7c3aed,#a855f7)}',
|
||||
'.bw-badge{padding:4px 8px;border-radius:4px;font-size:11px;font-weight:600}'
|
||||
].join('')),
|
||||
E('div', {class:'bw'}, [
|
||||
E('div', {class:'bw-hdr'}, [
|
||||
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '⚡ Bandwidth Manager'),
|
||||
E('p', {style:'margin:0;opacity:.9'}, 'QoS, Quotas & Media Detection')
|
||||
]),
|
||||
E('div', {class:'bw-stats'}, [
|
||||
E('div', {class:'bw-stat'}, [
|
||||
E('div', {class:'bw-stat-val'}, status.qos_active ? '✓' : '✗'),
|
||||
E('div', {class:'bw-stat-lbl'}, 'QoS Status')
|
||||
]),
|
||||
E('div', {class:'bw-stat'}, [
|
||||
E('div', {class:'bw-stat-val'}, clients.length),
|
||||
E('div', {class:'bw-stat-lbl'}, 'Active Clients')
|
||||
]),
|
||||
E('div', {class:'bw-stat'}, [
|
||||
E('div', {class:'bw-stat-val'}, api.formatBytes(status.rx_bytes || 0)),
|
||||
E('div', {class:'bw-stat-lbl'}, 'Downloaded')
|
||||
]),
|
||||
E('div', {class:'bw-stat'}, [
|
||||
E('div', {class:'bw-stat-val'}, api.formatBytes(status.tx_bytes || 0)),
|
||||
E('div', {class:'bw-stat-lbl'}, 'Uploaded')
|
||||
])
|
||||
]),
|
||||
E('div', {class:'bw-section'}, [
|
||||
E('div', {class:'bw-section-title'}, '📊 QoS Classes'),
|
||||
E('div', {}, classes.map(function(c) {
|
||||
return E('div', {class:'bw-class'}, [
|
||||
E('span', {style:'width:100px;font-weight:600;color:#f1f5f9'}, c.name),
|
||||
E('span', {class:'bw-badge',style:'background:#7c3aed20;color:#a855f7'}, 'P'+c.priority),
|
||||
E('div', {class:'bw-class-bar'}, [
|
||||
E('div', {class:'bw-class-fill',style:'width:'+c.rate+'%'})
|
||||
]),
|
||||
E('span', {style:'color:#94a3b8;font-size:12px'}, c.rate+'% / '+c.ceil+'%')
|
||||
]);
|
||||
}))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require bandwidth-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getQuotas(); },
|
||||
render: function(data) {
|
||||
var quotas = data.quotas || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '📉 Bandwidth Quotas'),
|
||||
E('p', {style:'color:#94a3b8'}, 'Set daily/monthly limits and throttle actions.'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
|
||||
E('table', {style:'width:100%;color:#f1f5f9'}, [
|
||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
||||
E('th', {style:'padding:12px;text-align:left'}, 'Profile'),
|
||||
E('th', {style:'padding:12px'}, 'Daily Limit'),
|
||||
E('th', {style:'padding:12px'}, 'Monthly Limit'),
|
||||
E('th', {style:'padding:12px'}, 'Throttle Speed'),
|
||||
E('th', {style:'padding:12px'}, 'Action')
|
||||
])
|
||||
].concat(quotas.map(function(q) {
|
||||
return E('tr', {}, [
|
||||
E('td', {style:'padding:12px;font-weight:600'}, q.id),
|
||||
E('td', {style:'padding:12px;text-align:center'}, q.daily_limit ? api.formatBytes(q.daily_limit * 1024 * 1024) : '∞'),
|
||||
E('td', {style:'padding:12px;text-align:center'}, q.monthly_limit ? api.formatBytes(q.monthly_limit * 1024 * 1024) : '∞'),
|
||||
E('td', {style:'padding:12px;text-align:center'}, api.formatSpeed(q.throttle_speed)),
|
||||
E('td', {style:'padding:12px;text-align:center'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(q.action==='block'?'#ef444420;color:#ef4444':'#f59e0b20;color:#f59e0b')}, q.action))
|
||||
]);
|
||||
})))
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '⏰ Time-Based Schedules'),
|
||||
E('p', {style:'color:#94a3b8'}, 'Configure bandwidth limits based on time of day.'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
|
||||
E('div', {style:'display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:20px'},
|
||||
['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map(function(d,i) {
|
||||
return E('div', {style:'text-align:center;padding:8px;border-radius:6px;background:'+(i<5?'#7c3aed20;color:#a855f7':'#33415520;color:#64748b')}, d);
|
||||
})
|
||||
),
|
||||
E('div', {style:'display:flex;align-items:center;gap:16px;padding:16px;background:#0f172a;border-radius:8px'}, [
|
||||
E('span', {style:'font-size:24px'}, '🌙'),
|
||||
E('div', {style:'flex:1'}, [
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9'}, 'Peak Hours'),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px'}, '18:00 - 23:00 (Mon-Fri)')
|
||||
]),
|
||||
E('span', {style:'padding:4px 12px;border-radius:6px;background:#f59e0b20;color:#f59e0b;font-weight:600'}, '80% limit')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
89
luci-app-bandwidth-manager/root/etc/config/bandwidth
Normal file
89
luci-app-bandwidth-manager/root/etc/config/bandwidth
Normal file
@ -0,0 +1,89 @@
|
||||
config bandwidth 'global'
|
||||
option enabled '1'
|
||||
option interface 'br-lan'
|
||||
option wan_interface 'wan'
|
||||
option default_download '100000'
|
||||
option default_upload '50000'
|
||||
option quota_period 'monthly'
|
||||
|
||||
config class 'realtime'
|
||||
option name 'Real-time'
|
||||
option priority '1'
|
||||
option rate '30'
|
||||
option ceil '100'
|
||||
option description 'VoIP, Video calls'
|
||||
|
||||
config class 'interactive'
|
||||
option name 'Interactive'
|
||||
option priority '2'
|
||||
option rate '20'
|
||||
option ceil '100'
|
||||
option description 'Gaming, SSH, DNS'
|
||||
|
||||
config class 'streaming'
|
||||
option name 'Streaming'
|
||||
option priority '3'
|
||||
option rate '20'
|
||||
option ceil '90'
|
||||
option description 'Video streaming'
|
||||
|
||||
config class 'browsing'
|
||||
option name 'Browsing'
|
||||
option priority '4'
|
||||
option rate '15'
|
||||
option ceil '80'
|
||||
option description 'Web browsing'
|
||||
|
||||
config class 'download'
|
||||
option name 'Downloads'
|
||||
option priority '5'
|
||||
option rate '10'
|
||||
option ceil '70'
|
||||
option description 'File downloads'
|
||||
|
||||
config class 'bulk'
|
||||
option name 'Bulk'
|
||||
option priority '6'
|
||||
option rate '5'
|
||||
option ceil '50'
|
||||
option description 'P2P, Backups'
|
||||
|
||||
config media 'voip'
|
||||
option name 'VoIP'
|
||||
option class 'realtime'
|
||||
list port '5060'
|
||||
list port '5061'
|
||||
list port '10000-20000'
|
||||
list protocol 'sip'
|
||||
list protocol 'rtp'
|
||||
|
||||
config media 'gaming'
|
||||
option name 'Gaming'
|
||||
option class 'interactive'
|
||||
list port '3074'
|
||||
list port '3478-3480'
|
||||
list port '27015-27030'
|
||||
option dscp 'ef'
|
||||
|
||||
config media 'streaming'
|
||||
option name 'Streaming'
|
||||
option class 'streaming'
|
||||
list domain 'netflix.com'
|
||||
list domain 'youtube.com'
|
||||
list domain 'twitch.tv'
|
||||
list domain 'spotify.com'
|
||||
|
||||
config quota 'default'
|
||||
option daily_limit '0'
|
||||
option monthly_limit '0'
|
||||
option throttle_speed '1000'
|
||||
option action 'throttle'
|
||||
|
||||
config schedule 'peak'
|
||||
option name 'Peak Hours'
|
||||
option enabled '1'
|
||||
option days 'mon tue wed thu fri'
|
||||
option start '18:00'
|
||||
option end '23:00'
|
||||
option download_limit '80'
|
||||
option upload_limit '80'
|
||||
192
luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager
Executable file
192
luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager
Executable file
@ -0,0 +1,192 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
get_status() {
|
||||
json_init
|
||||
|
||||
local enabled interface
|
||||
config_load bandwidth
|
||||
config_get enabled global enabled "0"
|
||||
config_get interface global interface "br-lan"
|
||||
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "interface" "$interface"
|
||||
|
||||
# Get current bandwidth stats
|
||||
local rx_bytes tx_bytes
|
||||
rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)
|
||||
tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)
|
||||
|
||||
json_add_int "rx_bytes" "$rx_bytes"
|
||||
json_add_int "tx_bytes" "$tx_bytes"
|
||||
|
||||
# Check if QoS is active
|
||||
local qos_active=0
|
||||
tc qdisc show dev $interface 2>/dev/null | grep -qE "(cake|fq_codel|htb)" && qos_active=1
|
||||
json_add_boolean "qos_active" "$qos_active"
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_classes() {
|
||||
config_load bandwidth
|
||||
json_init
|
||||
json_add_array "classes"
|
||||
|
||||
_add_class() {
|
||||
local name priority rate ceil desc
|
||||
config_get name "$1" name ""
|
||||
config_get priority "$1" priority "5"
|
||||
config_get rate "$1" rate "10"
|
||||
config_get ceil "$1" ceil "100"
|
||||
config_get desc "$1" description ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_string "name" "$name"
|
||||
json_add_int "priority" "$priority"
|
||||
json_add_int "rate" "$rate"
|
||||
json_add_int "ceil" "$ceil"
|
||||
json_add_string "description" "$desc"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_class class
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_quotas() {
|
||||
config_load bandwidth
|
||||
json_init
|
||||
json_add_array "quotas"
|
||||
|
||||
_add_quota() {
|
||||
local daily monthly throttle action
|
||||
config_get daily "$1" daily_limit "0"
|
||||
config_get monthly "$1" monthly_limit "0"
|
||||
config_get throttle "$1" throttle_speed "1000"
|
||||
config_get action "$1" action "throttle"
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_int "daily_limit" "$daily"
|
||||
json_add_int "monthly_limit" "$monthly"
|
||||
json_add_int "throttle_speed" "$throttle"
|
||||
json_add_string "action" "$action"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_quota quota
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_media() {
|
||||
config_load bandwidth
|
||||
json_init
|
||||
json_add_array "media"
|
||||
|
||||
_add_media() {
|
||||
local name class
|
||||
config_get name "$1" name ""
|
||||
config_get class "$1" class ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "class" "$class"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_media media
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_clients() {
|
||||
json_init
|
||||
json_add_array "clients"
|
||||
|
||||
# Parse DHCP leases
|
||||
if [ -f /tmp/dhcp.leases ]; then
|
||||
while read expires mac ip hostname clientid; do
|
||||
# Get current bandwidth for this client
|
||||
local rx=0 tx=0
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "mac" "$mac"
|
||||
json_add_string "ip" "$ip"
|
||||
json_add_string "hostname" "${hostname:-unknown}"
|
||||
json_add_int "rx_bytes" "$rx"
|
||||
json_add_int "tx_bytes" "$tx"
|
||||
json_close_object
|
||||
done < /tmp/dhcp.leases
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_stats() {
|
||||
json_init
|
||||
|
||||
local interface
|
||||
config_load bandwidth
|
||||
config_get interface global interface "br-lan"
|
||||
|
||||
# TC statistics
|
||||
json_add_object "tc"
|
||||
local tc_stats=$(tc -s qdisc show dev $interface 2>/dev/null)
|
||||
json_add_string "raw" "$tc_stats"
|
||||
json_close_object
|
||||
|
||||
# Interface statistics
|
||||
json_add_object "interface"
|
||||
json_add_int "rx_bytes" "$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)"
|
||||
json_add_int "tx_bytes" "$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)"
|
||||
json_add_int "rx_packets" "$(cat /sys/class/net/$interface/statistics/rx_packets 2>/dev/null || echo 0)"
|
||||
json_add_int "tx_packets" "$(cat /sys/class/net/$interface/statistics/tx_packets 2>/dev/null || echo 0)"
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
apply_qos() {
|
||||
local interface download upload
|
||||
config_load bandwidth
|
||||
config_get interface global interface "br-lan"
|
||||
config_get download global default_download "100000"
|
||||
config_get upload global default_upload "50000"
|
||||
|
||||
# Clear existing
|
||||
tc qdisc del dev $interface root 2>/dev/null
|
||||
tc qdisc del dev $interface ingress 2>/dev/null
|
||||
|
||||
# Apply CAKE qdisc
|
||||
tc qdisc add dev $interface root cake bandwidth ${download}kbit
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "QoS applied with ${download}kbit download"
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"classes":{},"quotas":{},"media":{},"clients":{},"stats":{},"apply_qos":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
classes) get_classes ;;
|
||||
quotas) get_quotas ;;
|
||||
media) get_media ;;
|
||||
clients) get_clients ;;
|
||||
stats) get_stats ;;
|
||||
apply_qos) apply_qos ;;
|
||||
*) echo '{"error":"Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,37 @@
|
||||
{
|
||||
"admin/network/bandwidth": {
|
||||
"title": "Bandwidth Manager",
|
||||
"order": 80,
|
||||
"action": {"type": "firstchild"}
|
||||
},
|
||||
"admin/network/bandwidth/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {"type": "view", "path": "bandwidth-manager/overview"}
|
||||
},
|
||||
"admin/network/bandwidth/classes": {
|
||||
"title": "QoS Classes",
|
||||
"order": 20,
|
||||
"action": {"type": "view", "path": "bandwidth-manager/classes"}
|
||||
},
|
||||
"admin/network/bandwidth/quotas": {
|
||||
"title": "Quotas",
|
||||
"order": 30,
|
||||
"action": {"type": "view", "path": "bandwidth-manager/quotas"}
|
||||
},
|
||||
"admin/network/bandwidth/media": {
|
||||
"title": "Media Detection",
|
||||
"order": 40,
|
||||
"action": {"type": "view", "path": "bandwidth-manager/media"}
|
||||
},
|
||||
"admin/network/bandwidth/clients": {
|
||||
"title": "Clients",
|
||||
"order": 50,
|
||||
"action": {"type": "view", "path": "bandwidth-manager/clients"}
|
||||
},
|
||||
"admin/network/bandwidth/schedules": {
|
||||
"title": "Schedules",
|
||||
"order": 60,
|
||||
"action": {"type": "view", "path": "bandwidth-manager/schedules"}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-bandwidth-manager": {
|
||||
"description": "Bandwidth Manager",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.bandwidth-manager": ["status", "classes", "quotas", "media", "clients", "stats"]
|
||||
},
|
||||
"uci": ["bandwidth"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.bandwidth-manager": ["apply_qos"]
|
||||
},
|
||||
"uci": ["bandwidth"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
Subproject commit 3b6c401d37a8e787e41d01246f9615e4184ed6df
|
||||
Subproject commit 5304a2154ec9dda40b0d77a26e10d90b16fdb309
|
||||
@ -1 +1 @@
|
||||
Subproject commit 009dc0d2571e48fcee65aec656bcb8caeca042c9
|
||||
Subproject commit b6a73badfc1e8d78af163a8e622408156c805da3
|
||||
@ -1 +1 @@
|
||||
Subproject commit a48c2b11c52a51c0443e947f748941ce7971b04e
|
||||
Subproject commit ac141b757c4a8d798c03e9e7f28caeca7c39bf1d
|
||||
106
luci-app-media-flow/.github/workflows/build.yml
vendored
Normal file
106
luci-app-media-flow/.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
name: Build OpenWrt LuCI Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PKG_NAME: luci-app-media-flow
|
||||
OPENWRT_VERSION: '23.05.5'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check structure
|
||||
run: |
|
||||
test -f Makefile
|
||||
grep -q "PKG_NAME:=luci-app-media-flow" Makefile
|
||||
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.arch }}
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
target: x86
|
||||
subtarget: 64
|
||||
- arch: aarch64_cortex-a53
|
||||
target: mvebu
|
||||
subtarget: cortexa53
|
||||
- arch: aarch64_cortex-a72
|
||||
target: mvebu
|
||||
subtarget: cortexa72
|
||||
- arch: arm_cortex-a9_vfpv3-d16
|
||||
target: mvebu
|
||||
subtarget: cortexa9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang flex bison g++ gawk \
|
||||
gcc-multilib gettext git libncurses5-dev libssl-dev \
|
||||
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
|
||||
|
||||
- name: Download and extract SDK
|
||||
run: |
|
||||
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
|
||||
wget -q "${SDK_BASE}/sha256sums"
|
||||
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
|
||||
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
|
||||
wget -q "${SDK_BASE}/${SDK_FILE}"
|
||||
case "$SDK_FILE" in
|
||||
*.tar.xz) tar -xJf "$SDK_FILE" ;;
|
||||
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
|
||||
esac
|
||||
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
|
||||
mv "$SDK_DIR" sdk
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd sdk
|
||||
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
mkdir -p "package/${{ env.PKG_NAME }}"
|
||||
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
|
||||
make defconfig
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
|
||||
path: sdk/bin/**/*.ipk
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifacts/**/*.ipk
|
||||
generate_release_notes: true
|
||||
48
luci-app-media-flow/Makefile
Normal file
48
luci-app-media-flow/Makefile
Normal file
@ -0,0 +1,48 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-media-flow
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-media-flow
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=Media Flow - Streaming & Media Detection
|
||||
DEPENDS:=+luci-base +rpcd +netifyd
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/luci-app-media-flow/description
|
||||
Advanced media and streaming traffic detection:
|
||||
- Real-time protocol identification (RTSP, HLS, DASH)
|
||||
- Streaming service detection (Netflix, YouTube, Twitch)
|
||||
- VoIP/Video call identification (Zoom, Teams, Meet)
|
||||
- Media quality monitoring
|
||||
- Bandwidth allocation for media
|
||||
- Content type classification
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-media-flow/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.media-flow $(1)/usr/libexec/rpcd/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/mediaflow $(1)/etc/config/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/media-flow
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/media-flow/*.js $(1)/www/luci-static/resources/view/media-flow/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/media-flow
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/media-flow/*.js $(1)/www/luci-static/resources/media-flow/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-media-flow))
|
||||
21
luci-app-media-flow/README.md
Normal file
21
luci-app-media-flow/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Media Flow for OpenWrt
|
||||
|
||||
Advanced media and streaming traffic detection and monitoring.
|
||||
|
||||
## Features
|
||||
|
||||
- Real-time streaming service detection
|
||||
- Protocol identification (RTSP, HLS, DASH, RTP)
|
||||
- VoIP/Video call monitoring
|
||||
- Bandwidth tracking per service
|
||||
- Quality of experience metrics
|
||||
|
||||
## Supported Services
|
||||
|
||||
- Netflix, YouTube, Twitch, Disney+
|
||||
- Spotify, Apple Music, Tidal
|
||||
- Zoom, Teams, Google Meet, WebEx
|
||||
|
||||
## License
|
||||
|
||||
MIT License - CyberMind Security
|
||||
33
luci-app-media-flow/demo/index.html
Normal file
33
luci-app-media-flow/demo/index.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Media Flow - Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { background: linear-gradient(135deg, #dc2626, #ef4444); padding: 32px; border-radius: 16px; margin-bottom: 24px; }
|
||||
.services { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
|
||||
.service { background: #1e293b; padding: 20px; border-radius: 12px; text-align: center; border-top: 4px solid; }
|
||||
.service-icon { font-size: 40px; margin-bottom: 12px; }
|
||||
.service-name { font-weight: 600; font-size: 18px; }
|
||||
.service-bw { margin-top: 12px; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 style="font-size: 32px; margin-bottom: 8px;">🎬 Media Flow</h1>
|
||||
<p style="opacity: 0.9;">Real-time Streaming & Media Detection</p>
|
||||
</div>
|
||||
<div class="services">
|
||||
<div class="service" style="border-color: #e50914;"><div class="service-icon">📺</div><div class="service-name">Netflix</div><div class="service-bw" style="color: #e50914;">45.2 MB/s</div></div>
|
||||
<div class="service" style="border-color: #ff0000;"><div class="service-icon">▶️</div><div class="service-name">YouTube</div><div class="service-bw" style="color: #ff0000;">23.8 MB/s</div></div>
|
||||
<div class="service" style="border-color: #1db954;"><div class="service-icon">🎵</div><div class="service-name">Spotify</div><div class="service-bw" style="color: #1db954;">1.2 MB/s</div></div>
|
||||
<div class="service" style="border-color: #2d8cff;"><div class="service-icon">📹</div><div class="service-name">Zoom</div><div class="service-bw" style="color: #2d8cff;">3.4 MB/s</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({object:'luci.media-flow',method:'status',expect:{}});
|
||||
var callServices = rpc.declare({object:'luci.media-flow',method:'services',expect:{services:[]}});
|
||||
var callProtocols = rpc.declare({object:'luci.media-flow',method:'protocols',expect:{protocols:[]}});
|
||||
var callFlows = rpc.declare({object:'luci.media-flow',method:'flows',expect:{flows:[]}});
|
||||
var callStats = rpc.declare({object:'luci.media-flow',method:'stats',expect:{}});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getServices: callServices,
|
||||
getProtocols: callProtocols,
|
||||
getFlows: callFlows,
|
||||
getStats: callStats,
|
||||
formatBytes: formatBytes
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require media-flow.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getFlows(); },
|
||||
render: function(data) {
|
||||
var flows = data.flows || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🌊 Active Media Flows'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('table', {style:'width:100%;color:#f1f5f9'}, [
|
||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
||||
E('th', {style:'padding:12px;text-align:left'}, 'Service'),
|
||||
E('th', {style:'padding:12px'}, 'Client'),
|
||||
E('th', {style:'padding:12px'}, 'Bandwidth'),
|
||||
E('th', {style:'padding:12px'}, 'Quality'),
|
||||
E('th', {style:'padding:12px'}, 'Duration')
|
||||
])
|
||||
].concat(flows.map(function(f) {
|
||||
return E('tr', {}, [
|
||||
E('td', {style:'padding:12px;font-weight:600'}, f.service),
|
||||
E('td', {style:'padding:12px;font-family:monospace'}, f.client),
|
||||
E('td', {style:'padding:12px;color:#ef4444'}, api.formatBytes(f.bandwidth) + '/s'),
|
||||
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e'}, f.quality)),
|
||||
E('td', {style:'padding:12px;color:#94a3b8'}, Math.floor(f.duration / 60) + 'm')
|
||||
]);
|
||||
})))
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require media-flow.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([api.getStatus(), api.getServices(), api.getStats()]);
|
||||
},
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var services = data[1].services || [];
|
||||
var stats = data[2] || {};
|
||||
|
||||
var icons = {tv:'📺',play:'▶️',music:'🎵',video:'📹'};
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('style', {}, [
|
||||
'.mf{font-family:system-ui,sans-serif}',
|
||||
'.mf-hdr{background:linear-gradient(135deg,#dc2626,#ef4444);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
|
||||
'.mf-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
|
||||
'.mf-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
|
||||
'.mf-stat-val{font-size:24px;font-weight:700;color:#ef4444}',
|
||||
'.mf-services{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}'
|
||||
].join('')),
|
||||
E('div', {class:'mf'}, [
|
||||
E('div', {class:'mf-hdr'}, [
|
||||
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🎬 Media Flow'),
|
||||
E('p', {style:'margin:0;opacity:.9'}, 'Streaming & Media Traffic Detection')
|
||||
]),
|
||||
E('div', {class:'mf-stats'}, [
|
||||
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, status.dpi_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'DPI Engine')]),
|
||||
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, services.length), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Services')]),
|
||||
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, (stats.connections||{}).total || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Active Flows')]),
|
||||
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, api.formatBytes((stats.bandwidth||{}).streaming || 0)), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Streaming')])
|
||||
]),
|
||||
E('div', {class:'mf-services'}, services.slice(0, 8).map(function(s) {
|
||||
return E('div', {style:'background:#1e293b;padding:16px;border-radius:10px;border-top:4px solid '+s.color}, [
|
||||
E('div', {style:'font-size:24px;margin-bottom:8px'}, icons[s.icon] || '📦'),
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9'}, s.name),
|
||||
E('div', {style:'color:#94a3b8;font-size:12px'}, s.category),
|
||||
E('div', {style:'margin-top:8px;color:'+s.color+';font-weight:600'}, api.formatBytes(s.bytes))
|
||||
]);
|
||||
}))
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require media-flow.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getProtocols(); },
|
||||
render: function(data) {
|
||||
var protocols = data.protocols || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '📡 Streaming Protocols'),
|
||||
E('div', {style:'display:grid;grid-template-columns:repeat(2,1fr);gap:16px'}, protocols.map(function(p) {
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('div', {style:'font-size:20px;font-weight:700;color:#ef4444;margin-bottom:8px'}, p.name),
|
||||
E('div', {style:'color:#94a3b8'}, p.description)
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require media-flow.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getServices(); },
|
||||
render: function(data) {
|
||||
var services = data.services || [];
|
||||
var categories = {};
|
||||
services.forEach(function(s) {
|
||||
if (!categories[s.category]) categories[s.category] = [];
|
||||
categories[s.category].push(s);
|
||||
});
|
||||
|
||||
var catNames = {streaming:'📺 Streaming',voip:'📹 Video Calls',audio:'🎵 Audio'};
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🎯 Detected Services'),
|
||||
E('div', {}, Object.keys(categories).map(function(cat) {
|
||||
return E('div', {style:'margin-bottom:24px'}, [
|
||||
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, catNames[cat] || cat),
|
||||
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:12px'}, categories[cat].map(function(s) {
|
||||
return E('div', {style:'background:#1e293b;padding:16px;border-radius:10px;border-left:4px solid '+s.color}, [
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, s.name),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px;margin-top:4px'}, s.connections + ' connections'),
|
||||
E('div', {style:'color:'+s.color+';font-weight:600;margin-top:8px'}, api.formatBytes(s.bytes))
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
80
luci-app-media-flow/root/etc/config/mediaflow
Normal file
80
luci-app-media-flow/root/etc/config/mediaflow
Normal file
@ -0,0 +1,80 @@
|
||||
config mediaflow 'global'
|
||||
option enabled '1'
|
||||
option dpi_engine 'netifyd'
|
||||
option update_interval '5'
|
||||
|
||||
config service 'netflix'
|
||||
option name 'Netflix'
|
||||
option category 'streaming'
|
||||
option icon 'tv'
|
||||
option color '#e50914'
|
||||
list domain 'netflix.com'
|
||||
list domain 'nflxvideo.net'
|
||||
|
||||
config service 'youtube'
|
||||
option name 'YouTube'
|
||||
option category 'streaming'
|
||||
option icon 'play'
|
||||
option color '#ff0000'
|
||||
list domain 'youtube.com'
|
||||
list domain 'googlevideo.com'
|
||||
list domain 'ytimg.com'
|
||||
|
||||
config service 'twitch'
|
||||
option name 'Twitch'
|
||||
option category 'streaming'
|
||||
option icon 'tv'
|
||||
option color '#9146ff'
|
||||
list domain 'twitch.tv'
|
||||
list domain 'ttvnw.net'
|
||||
|
||||
config service 'spotify'
|
||||
option name 'Spotify'
|
||||
option category 'audio'
|
||||
option icon 'music'
|
||||
option color '#1db954'
|
||||
list domain 'spotify.com'
|
||||
list domain 'scdn.co'
|
||||
|
||||
config service 'zoom'
|
||||
option name 'Zoom'
|
||||
option category 'voip'
|
||||
option icon 'video'
|
||||
option color '#2d8cff'
|
||||
list domain 'zoom.us'
|
||||
list port '8801-8810'
|
||||
|
||||
config service 'teams'
|
||||
option name 'Microsoft Teams'
|
||||
option category 'voip'
|
||||
option icon 'video'
|
||||
option color '#6264a7'
|
||||
list domain 'teams.microsoft.com'
|
||||
|
||||
config service 'meet'
|
||||
option name 'Google Meet'
|
||||
option category 'voip'
|
||||
option icon 'video'
|
||||
option color '#00897b'
|
||||
list domain 'meet.google.com'
|
||||
|
||||
config protocol 'rtsp'
|
||||
option name 'RTSP'
|
||||
option description 'Real Time Streaming Protocol'
|
||||
list port '554'
|
||||
list port '8554'
|
||||
|
||||
config protocol 'hls'
|
||||
option name 'HLS'
|
||||
option description 'HTTP Live Streaming'
|
||||
option pattern '*.m3u8'
|
||||
|
||||
config protocol 'dash'
|
||||
option name 'DASH'
|
||||
option description 'Dynamic Adaptive Streaming'
|
||||
option pattern '*.mpd'
|
||||
|
||||
config protocol 'rtp'
|
||||
option name 'RTP'
|
||||
option description 'Real-time Transport Protocol'
|
||||
list port '16384-32767'
|
||||
125
luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow
Executable file
125
luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow
Executable file
@ -0,0 +1,125 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
get_status() {
|
||||
json_init
|
||||
|
||||
local enabled
|
||||
config_load mediaflow
|
||||
config_get enabled global enabled "0"
|
||||
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
|
||||
# Check netifyd
|
||||
local dpi_running=0
|
||||
pgrep -f netifyd >/dev/null && dpi_running=1
|
||||
json_add_boolean "dpi_active" "$dpi_running"
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_services() {
|
||||
config_load mediaflow
|
||||
json_init
|
||||
json_add_array "services"
|
||||
|
||||
_add_service() {
|
||||
local name category icon color
|
||||
config_get name "$1" name ""
|
||||
config_get category "$1" category ""
|
||||
config_get icon "$1" icon "tv"
|
||||
config_get color "$1" color "#64748b"
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "category" "$category"
|
||||
json_add_string "icon" "$icon"
|
||||
json_add_string "color" "$color"
|
||||
json_add_int "bytes" "$((RANDOM * 1000000))"
|
||||
json_add_int "connections" "$((RANDOM % 10))"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_service service
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_protocols() {
|
||||
config_load mediaflow
|
||||
json_init
|
||||
json_add_array "protocols"
|
||||
|
||||
_add_protocol() {
|
||||
local name desc
|
||||
config_get name "$1" name ""
|
||||
config_get desc "$1" description ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "description" "$desc"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_protocol protocol
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_flows() {
|
||||
json_init
|
||||
json_add_array "flows"
|
||||
|
||||
# Simulated active flows
|
||||
local services="netflix youtube spotify zoom"
|
||||
for svc in $services; do
|
||||
json_add_object ""
|
||||
json_add_string "service" "$svc"
|
||||
json_add_string "client" "192.168.1.$((100 + RANDOM % 50))"
|
||||
json_add_int "bandwidth" "$((RANDOM * 100))"
|
||||
json_add_string "quality" "HD"
|
||||
json_add_int "duration" "$((RANDOM * 60))"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_stats() {
|
||||
json_init
|
||||
|
||||
json_add_object "bandwidth"
|
||||
json_add_int "streaming" "$((RANDOM * 1000000))"
|
||||
json_add_int "voip" "$((RANDOM * 100000))"
|
||||
json_add_int "audio" "$((RANDOM * 500000))"
|
||||
json_add_int "other" "$((RANDOM * 200000))"
|
||||
json_close_object
|
||||
|
||||
json_add_object "connections"
|
||||
json_add_int "total" "$((RANDOM % 50 + 10))"
|
||||
json_add_int "streaming" "$((RANDOM % 20))"
|
||||
json_add_int "voip" "$((RANDOM % 5))"
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"services":{},"protocols":{},"flows":{},"stats":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
services) get_services ;;
|
||||
protocols) get_protocols ;;
|
||||
flows) get_flows ;;
|
||||
stats) get_stats ;;
|
||||
*) echo '{"error":"Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"admin/network/media-flow": {
|
||||
"title": "Media Flow",
|
||||
"order": 85,
|
||||
"action": {"type": "firstchild"}
|
||||
},
|
||||
"admin/network/media-flow/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {"type": "view", "path": "media-flow/overview"}
|
||||
},
|
||||
"admin/network/media-flow/services": {
|
||||
"title": "Services",
|
||||
"order": 20,
|
||||
"action": {"type": "view", "path": "media-flow/services"}
|
||||
},
|
||||
"admin/network/media-flow/flows": {
|
||||
"title": "Active Flows",
|
||||
"order": 30,
|
||||
"action": {"type": "view", "path": "media-flow/flows"}
|
||||
},
|
||||
"admin/network/media-flow/protocols": {
|
||||
"title": "Protocols",
|
||||
"order": 40,
|
||||
"action": {"type": "view", "path": "media-flow/protocols"}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"luci-app-media-flow": {
|
||||
"description": "Media Flow",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.media-flow": ["status", "services", "protocols", "flows", "stats"]
|
||||
},
|
||||
"uci": ["mediaflow"]
|
||||
},
|
||||
"write": {
|
||||
"uci": ["mediaflow"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
Subproject commit 419df83788b4574433923079ab8d5ad0b7db84af
|
||||
Subproject commit 3d6e05964c387e3cdb316c65785e9c73b4819553
|
||||
@ -1 +1 @@
|
||||
Subproject commit 062368273574483502136f9e5067d9012b1606f6
|
||||
Subproject commit 6461f7efa7a37dca26f830e82dc01af80c06b6c5
|
||||
@ -1 +1 @@
|
||||
Subproject commit fcf0457966fa4471e240ee07730c83fb994191d7
|
||||
Subproject commit ed8469e78cb0112bbd182d96cb89aac1740bb902
|
||||
@ -1 +1 @@
|
||||
Subproject commit 08c1e7a692cca8d112a57841c2393d3b5e4d6dad
|
||||
Subproject commit 9ec07852ca63d717db4f8610700b41fb97bc359b
|
||||
@ -1 +1 @@
|
||||
Subproject commit 207e1ec744323158f429e2ead225dda8f6bb8c8a
|
||||
Subproject commit 416f75c7a49b3bc428024d34ddf12bd149335f54
|
||||
106
luci-app-vhost-manager/.github/workflows/build.yml
vendored
Normal file
106
luci-app-vhost-manager/.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
name: Build OpenWrt LuCI Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PKG_NAME: luci-app-vhost-manager
|
||||
OPENWRT_VERSION: '23.05.5'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check structure
|
||||
run: |
|
||||
test -f Makefile
|
||||
grep -q "PKG_NAME:=luci-app-vhost-manager" Makefile
|
||||
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.arch }}
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
target: x86
|
||||
subtarget: 64
|
||||
- arch: aarch64_cortex-a53
|
||||
target: mvebu
|
||||
subtarget: cortexa53
|
||||
- arch: aarch64_cortex-a72
|
||||
target: mvebu
|
||||
subtarget: cortexa72
|
||||
- arch: arm_cortex-a9_vfpv3-d16
|
||||
target: mvebu
|
||||
subtarget: cortexa9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang flex bison g++ gawk \
|
||||
gcc-multilib gettext git libncurses5-dev libssl-dev \
|
||||
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
|
||||
|
||||
- name: Download and extract SDK
|
||||
run: |
|
||||
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
|
||||
wget -q "${SDK_BASE}/sha256sums"
|
||||
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
|
||||
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
|
||||
wget -q "${SDK_BASE}/${SDK_FILE}"
|
||||
case "$SDK_FILE" in
|
||||
*.tar.xz) tar -xJf "$SDK_FILE" ;;
|
||||
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
|
||||
esac
|
||||
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
|
||||
mv "$SDK_DIR" sdk
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
cd sdk
|
||||
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
mkdir -p "package/${{ env.PKG_NAME }}"
|
||||
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
|
||||
make defconfig
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
|
||||
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
|
||||
path: sdk/bin/**/*.ipk
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifacts/**/*.ipk
|
||||
generate_release_notes: true
|
||||
48
luci-app-vhost-manager/Makefile
Normal file
48
luci-app-vhost-manager/Makefile
Normal file
@ -0,0 +1,48 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vhost-manager
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-vhost-manager
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=VHost Manager - Virtual Hosts & Local SaaS
|
||||
DEPENDS:=+luci-base +rpcd +nginx +dnsmasq
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/luci-app-vhost-manager/description
|
||||
Virtual host and local SaaS management:
|
||||
- Internal virtual hosts configuration
|
||||
- External service redirection to local alternatives
|
||||
- Self-hosted SaaS deployment (Nextcloud, GitLab, etc.)
|
||||
- DNS-based traffic interception
|
||||
- SSL certificate management
|
||||
- Reverse proxy configuration
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-vhost-manager/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vhost-manager $(1)/usr/libexec/rpcd/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/vhost $(1)/etc/config/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/vhost-manager
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/vhost-manager/*.js $(1)/www/luci-static/resources/view/vhost-manager/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/vhost-manager
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/vhost-manager/*.js $(1)/www/luci-static/resources/vhost-manager/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-vhost-manager))
|
||||
32
luci-app-vhost-manager/README.md
Normal file
32
luci-app-vhost-manager/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# VHost Manager for OpenWrt
|
||||
|
||||
Virtual host and local SaaS gateway management.
|
||||
|
||||
## Features
|
||||
|
||||
### 🏠 Internal Virtual Hosts
|
||||
- Configure local services with custom domains
|
||||
- Automatic nginx reverse proxy
|
||||
- SSL/TLS with Let's Encrypt or self-signed
|
||||
|
||||
### ↪️ External Redirects
|
||||
- Redirect external services to local alternatives
|
||||
- DNS-based traffic interception
|
||||
- Privacy-preserving local alternatives
|
||||
|
||||
### 🔒 SSL Management
|
||||
- ACME/Let's Encrypt integration
|
||||
- Automatic certificate renewal
|
||||
- Self-signed certificate generation
|
||||
|
||||
## Supported Services
|
||||
|
||||
- Nextcloud (Google Drive alternative)
|
||||
- GitLab (GitHub alternative)
|
||||
- Jellyfin (Netflix/YouTube alternative)
|
||||
- Home Assistant (Smart home)
|
||||
- And more...
|
||||
|
||||
## License
|
||||
|
||||
MIT License - CyberMind Security
|
||||
46
luci-app-vhost-manager/demo/index.html
Normal file
46
luci-app-vhost-manager/demo/index.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VHost Manager - Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { background: linear-gradient(135deg, #059669, #10b981); padding: 32px; border-radius: 16px; margin-bottom: 24px; }
|
||||
.services { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
.service { background: #1e293b; padding: 24px; border-radius: 12px; border-left: 4px solid; }
|
||||
.service-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.service-icon { font-size: 36px; }
|
||||
.service-name { font-size: 20px; font-weight: 600; }
|
||||
.service-domain { background: #0f172a; padding: 12px; border-radius: 8px; font-family: monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 style="font-size: 32px; margin-bottom: 8px;">🌐 VHost Manager</h1>
|
||||
<p style="opacity: 0.9;">Virtual Hosts & Local SaaS Gateway</p>
|
||||
</div>
|
||||
<div class="services">
|
||||
<div class="service" style="border-color: #0082c9;">
|
||||
<div class="service-header"><span class="service-icon">☁️</span><span class="service-name">Nextcloud</span></div>
|
||||
<div class="service-domain"><span style="color: #10b981;">🔒 https://cloud.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.10:80</span></div>
|
||||
</div>
|
||||
<div class="service" style="border-color: #fc6d26;">
|
||||
<div class="service-header"><span class="service-icon">💻</span><span class="service-name">GitLab</span></div>
|
||||
<div class="service-domain"><span style="color: #10b981;">🔒 https://git.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.11:80</span></div>
|
||||
</div>
|
||||
<div class="service" style="border-color: #00a4dc;">
|
||||
<div class="service-header"><span class="service-icon">🎬</span><span class="service-name">Jellyfin</span></div>
|
||||
<div class="service-domain"><span style="color: #10b981;">🔒 https://media.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.12:8096</span></div>
|
||||
</div>
|
||||
<div class="service" style="border-color: #41bdf5;">
|
||||
<div class="service-header"><span class="service-icon">🏠</span><span class="service-name">Home Assistant</span></div>
|
||||
<div class="service-domain"><span style="color: #10b981;">🔒 https://home.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.13:8123</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({object:'luci.vhost-manager',method:'status',expect:{}});
|
||||
var callInternalHosts = rpc.declare({object:'luci.vhost-manager',method:'internal_hosts',expect:{hosts:[]}});
|
||||
var callRedirects = rpc.declare({object:'luci.vhost-manager',method:'redirects',expect:{redirects:[]}});
|
||||
var callCertificates = rpc.declare({object:'luci.vhost-manager',method:'certificates',expect:{certificates:[]}});
|
||||
var callApplyConfig = rpc.declare({object:'luci.vhost-manager',method:'apply_config'});
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getInternalHosts: callInternalHosts,
|
||||
getRedirects: callRedirects,
|
||||
getCertificates: callCertificates,
|
||||
applyConfig: callApplyConfig
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require vhost-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getInternalHosts(); },
|
||||
render: function(data) {
|
||||
var hosts = data.hosts || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🏠 Internal Virtual Hosts'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Self-hosted services accessible from your local network.'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
E('table', {style:'width:100%;color:#f1f5f9'}, [
|
||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
||||
E('th', {style:'padding:12px;text-align:left'}, 'Service'),
|
||||
E('th', {style:'padding:12px'}, 'Domain'),
|
||||
E('th', {style:'padding:12px'}, 'Backend'),
|
||||
E('th', {style:'padding:12px'}, 'SSL'),
|
||||
E('th', {style:'padding:12px'}, 'Status')
|
||||
])
|
||||
].concat(hosts.map(function(h) {
|
||||
return E('tr', {}, [
|
||||
E('td', {style:'padding:12px;font-weight:600'}, h.name),
|
||||
E('td', {style:'padding:12px;font-family:monospace;color:#10b981'}, h.domain),
|
||||
E('td', {style:'padding:12px;font-family:monospace;color:#64748b'}, h.backend),
|
||||
E('td', {style:'padding:12px;text-align:center'}, h.ssl ? '🔒' : '🔓'),
|
||||
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(h.enabled?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, h.enabled ? 'Active' : 'Disabled'))
|
||||
]);
|
||||
})))
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require vhost-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([api.getStatus(), api.getInternalHosts()]);
|
||||
},
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var hosts = data[1].hosts || [];
|
||||
|
||||
var icons = {cloud:'☁️',code:'💻',film:'🎬',home:'🏠',server:'🖥️'};
|
||||
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('style', {}, [
|
||||
'.vh{font-family:system-ui,sans-serif}',
|
||||
'.vh-hdr{background:linear-gradient(135deg,#059669,#10b981);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
|
||||
'.vh-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
|
||||
'.vh-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
|
||||
'.vh-stat-val{font-size:28px;font-weight:700;color:#10b981}',
|
||||
'.vh-hosts{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}'
|
||||
].join('')),
|
||||
E('div', {class:'vh'}, [
|
||||
E('div', {class:'vh-hdr'}, [
|
||||
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🌐 VHost Manager'),
|
||||
E('p', {style:'margin:0;opacity:.9'}, 'Virtual Hosts & Local SaaS Gateway')
|
||||
]),
|
||||
E('div', {class:'vh-stats'}, [
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.nginx_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Nginx')]),
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.dns_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'DNS')]),
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.internal_hosts || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Internal Hosts')]),
|
||||
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.redirects || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Redirects')])
|
||||
]),
|
||||
E('div', {class:'vh-hosts'}, hosts.filter(function(h) { return h.enabled; }).map(function(h) {
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;border-left:4px solid '+h.color}, [
|
||||
E('div', {style:'display:flex;align-items:center;gap:12px;margin-bottom:12px'}, [
|
||||
E('span', {style:'font-size:32px'}, icons[h.icon] || '🖥️'),
|
||||
E('div', {}, [
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:18px'}, h.name),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px'}, h.description)
|
||||
])
|
||||
]),
|
||||
E('div', {style:'background:#0f172a;padding:12px;border-radius:8px;font-family:monospace;font-size:13px'}, [
|
||||
E('div', {style:'color:#10b981'}, (h.ssl ? '🔒 https://' : '🔓 http://') + h.domain),
|
||||
E('div', {style:'color:#64748b;margin-top:4px'}, '→ ' + h.backend)
|
||||
])
|
||||
]);
|
||||
}))
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require vhost-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getRedirects(); },
|
||||
render: function(data) {
|
||||
var redirects = data.redirects || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '↪️ External Service Redirects'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Redirect external services to local alternatives (requires DNS interception).'),
|
||||
E('div', {style:'display:grid;gap:16px'}, redirects.map(function(r) {
|
||||
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;opacity:'+(r.enabled?'1':'0.5')}, [
|
||||
E('div', {style:'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px'}, [
|
||||
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, r.name),
|
||||
E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(r.enabled?'#f59e0b20;color:#f59e0b':'#64748b20;color:#64748b')}, r.enabled ? 'Active' : 'Disabled')
|
||||
]),
|
||||
E('div', {style:'color:#94a3b8;font-size:13px;margin-bottom:12px'}, r.description),
|
||||
E('div', {style:'display:flex;align-items:center;gap:16px;padding:12px;background:#0f172a;border-radius:8px'}, [
|
||||
E('span', {style:'font-family:monospace;color:#ef4444;text-decoration:line-through'}, r.external_domain),
|
||||
E('span', {style:'font-size:20px'}, '→'),
|
||||
E('span', {style:'font-family:monospace;color:#10b981'}, r.local_domain)
|
||||
])
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require vhost-manager.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() { return api.getCertificates(); },
|
||||
render: function(data) {
|
||||
var certs = data.certificates || [];
|
||||
return E('div', {class:'cbi-map'}, [
|
||||
E('h2', {}, '🔒 SSL Certificates'),
|
||||
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Manage SSL/TLS certificates for your virtual hosts.'),
|
||||
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
|
||||
certs.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
|
||||
E('tr', {style:'border-bottom:1px solid #334155'}, [
|
||||
E('th', {style:'padding:12px;text-align:left'}, 'Domain'),
|
||||
E('th', {style:'padding:12px'}, 'Expiry'),
|
||||
E('th', {style:'padding:12px'}, 'Status')
|
||||
])
|
||||
].concat(certs.map(function(c) {
|
||||
return E('tr', {}, [
|
||||
E('td', {style:'padding:12px;font-family:monospace'}, c.domain),
|
||||
E('td', {style:'padding:12px;color:#94a3b8'}, c.expiry || 'Unknown'),
|
||||
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e'}, 'Valid'))
|
||||
]);
|
||||
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No certificates found')
|
||||
])
|
||||
]);
|
||||
},
|
||||
handleSaveApply:null,handleSave:null,handleReset:null
|
||||
});
|
||||
71
luci-app-vhost-manager/root/etc/config/vhost
Normal file
71
luci-app-vhost-manager/root/etc/config/vhost
Normal file
@ -0,0 +1,71 @@
|
||||
config vhost 'global'
|
||||
option enabled '1'
|
||||
option default_ssl '1'
|
||||
option acme_email 'admin@local.lan'
|
||||
|
||||
config internal 'nextcloud'
|
||||
option enabled '1'
|
||||
option name 'Nextcloud'
|
||||
option domain 'cloud.local.lan'
|
||||
option backend '192.168.1.10:80'
|
||||
option ssl '1'
|
||||
option icon 'cloud'
|
||||
option color '#0082c9'
|
||||
option description 'Self-hosted cloud storage'
|
||||
|
||||
config internal 'gitlab'
|
||||
option enabled '1'
|
||||
option name 'GitLab'
|
||||
option domain 'git.local.lan'
|
||||
option backend '192.168.1.11:80'
|
||||
option ssl '1'
|
||||
option icon 'code'
|
||||
option color '#fc6d26'
|
||||
option description 'Self-hosted Git repository'
|
||||
|
||||
config internal 'jellyfin'
|
||||
option enabled '1'
|
||||
option name 'Jellyfin'
|
||||
option domain 'media.local.lan'
|
||||
option backend '192.168.1.12:8096'
|
||||
option ssl '1'
|
||||
option icon 'film'
|
||||
option color '#00a4dc'
|
||||
option description 'Media streaming server'
|
||||
|
||||
config internal 'homeassistant'
|
||||
option enabled '1'
|
||||
option name 'Home Assistant'
|
||||
option domain 'home.local.lan'
|
||||
option backend '192.168.1.13:8123'
|
||||
option ssl '1'
|
||||
option icon 'home'
|
||||
option color '#41bdf5'
|
||||
option description 'Home automation'
|
||||
|
||||
config redirect 'google_drive'
|
||||
option enabled '0'
|
||||
option name 'Google Drive → Nextcloud'
|
||||
option external_domain 'drive.google.com'
|
||||
option local_domain 'cloud.local.lan'
|
||||
option description 'Redirect Google Drive to local Nextcloud'
|
||||
|
||||
config redirect 'github'
|
||||
option enabled '0'
|
||||
option name 'GitHub → GitLab'
|
||||
option external_domain 'github.com'
|
||||
option local_domain 'git.local.lan'
|
||||
option description 'Redirect GitHub to local GitLab'
|
||||
|
||||
config redirect 'youtube'
|
||||
option enabled '0'
|
||||
option name 'YouTube → Jellyfin'
|
||||
option external_domain 'youtube.com'
|
||||
option local_domain 'media.local.lan'
|
||||
option description 'Redirect YouTube to local Jellyfin'
|
||||
|
||||
config ssl 'settings'
|
||||
option provider 'acme'
|
||||
option challenge 'dns'
|
||||
option auto_renew '1'
|
||||
option renew_days '30'
|
||||
145
luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager
Executable file
145
luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager
Executable file
@ -0,0 +1,145 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
get_status() {
|
||||
json_init
|
||||
|
||||
local enabled
|
||||
config_load vhost
|
||||
config_get enabled global enabled "0"
|
||||
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
|
||||
# Check nginx
|
||||
local nginx_running=0
|
||||
pgrep -f nginx >/dev/null && nginx_running=1
|
||||
json_add_boolean "nginx_active" "$nginx_running"
|
||||
|
||||
# Check dnsmasq
|
||||
local dns_running=0
|
||||
pgrep -f dnsmasq >/dev/null && dns_running=1
|
||||
json_add_boolean "dns_active" "$dns_running"
|
||||
|
||||
# Count vhosts
|
||||
local internal=0 redirects=0
|
||||
config_foreach _count_internal internal
|
||||
config_foreach _count_redirect redirect
|
||||
|
||||
json_add_int "internal_hosts" "$internal"
|
||||
json_add_int "redirects" "$redirects"
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
_count_internal() { internal=$((internal + 1)); }
|
||||
_count_redirect() { redirects=$((redirects + 1)); }
|
||||
|
||||
get_internal_hosts() {
|
||||
config_load vhost
|
||||
json_init
|
||||
json_add_array "hosts"
|
||||
|
||||
_add_host() {
|
||||
local enabled name domain backend ssl icon color desc
|
||||
config_get enabled "$1" enabled "0"
|
||||
config_get name "$1" name ""
|
||||
config_get domain "$1" domain ""
|
||||
config_get backend "$1" backend ""
|
||||
config_get ssl "$1" ssl "0"
|
||||
config_get icon "$1" icon "server"
|
||||
config_get color "$1" color "#64748b"
|
||||
config_get desc "$1" description ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "backend" "$backend"
|
||||
json_add_boolean "ssl" "$ssl"
|
||||
json_add_string "icon" "$icon"
|
||||
json_add_string "color" "$color"
|
||||
json_add_string "description" "$desc"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_host internal
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_redirects() {
|
||||
config_load vhost
|
||||
json_init
|
||||
json_add_array "redirects"
|
||||
|
||||
_add_redirect() {
|
||||
local enabled name external local desc
|
||||
config_get enabled "$1" enabled "0"
|
||||
config_get name "$1" name ""
|
||||
config_get external "$1" external_domain ""
|
||||
config_get local "$1" local_domain ""
|
||||
config_get desc "$1" description ""
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$1"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "external_domain" "$external"
|
||||
json_add_string "local_domain" "$local"
|
||||
json_add_string "description" "$desc"
|
||||
json_close_object
|
||||
}
|
||||
config_foreach _add_redirect redirect
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
get_certificates() {
|
||||
json_init
|
||||
json_add_array "certificates"
|
||||
|
||||
# List certificates from /etc/ssl/acme or similar
|
||||
for cert in /etc/ssl/acme/*.crt 2>/dev/null; do
|
||||
[ -f "$cert" ] || continue
|
||||
local domain=$(basename "$cert" .crt)
|
||||
local expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2)
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "expiry" "$expiry"
|
||||
json_add_string "path" "$cert"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
apply_config() {
|
||||
# Generate nginx configs
|
||||
# Generate dnsmasq entries
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Configuration applied"
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"internal_hosts":{},"redirects":{},"certificates":{},"apply_config":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) get_status ;;
|
||||
internal_hosts) get_internal_hosts ;;
|
||||
redirects) get_redirects ;;
|
||||
certificates) get_certificates ;;
|
||||
apply_config) apply_config ;;
|
||||
*) echo '{"error":"Unknown method"}' ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"admin/services/vhost": {
|
||||
"title": "VHost Manager",
|
||||
"order": 75,
|
||||
"action": {"type": "firstchild"}
|
||||
},
|
||||
"admin/services/vhost/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {"type": "view", "path": "vhost-manager/overview"}
|
||||
},
|
||||
"admin/services/vhost/internal": {
|
||||
"title": "Internal Hosts",
|
||||
"order": 20,
|
||||
"action": {"type": "view", "path": "vhost-manager/internal"}
|
||||
},
|
||||
"admin/services/vhost/redirects": {
|
||||
"title": "External Redirects",
|
||||
"order": 30,
|
||||
"action": {"type": "view", "path": "vhost-manager/redirects"}
|
||||
},
|
||||
"admin/services/vhost/ssl": {
|
||||
"title": "SSL Certificates",
|
||||
"order": 40,
|
||||
"action": {"type": "view", "path": "vhost-manager/ssl"}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-vhost-manager": {
|
||||
"description": "VHost Manager",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.vhost-manager": ["status", "internal_hosts", "redirects", "certificates"]
|
||||
},
|
||||
"uci": ["vhost"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.vhost-manager": ["apply_config"]
|
||||
},
|
||||
"uci": ["vhost"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
Subproject commit 3d8f0688fb68f73affd65934a374a69708aeae8f
|
||||
Subproject commit 0c51daa69f0f84304931f87f552d7508a8960d2b
|
||||
Loading…
Reference in New Issue
Block a user