diff --git a/package/secubox/luci-app-repo/Makefile b/package/secubox/luci-app-repo/Makefile new file mode 100644 index 00000000..744df899 --- /dev/null +++ b/package/secubox/luci-app-repo/Makefile @@ -0,0 +1,26 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-repo +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=Apache-2.0 + +LUCI_TITLE:=LuCI Package Repository Dashboard +LUCI_DEPENDS:=+secubox-app-repo + +include $(INCLUDE_DIR)/package.mk + +define Package/luci-app-repo/install + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/repo + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/repo/*.js $(1)/www/luci-static/resources/view/repo/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-repo.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-repo.json $(1)/usr/share/rpcd/acl.d/ +endef + +$(eval $(call BuildPackage,luci-app-repo)) diff --git a/package/secubox/luci-app-repo/htdocs/luci-static/resources/view/repo/dashboard.js b/package/secubox/luci-app-repo/htdocs/luci-static/resources/view/repo/dashboard.js new file mode 100644 index 00000000..abc4a7d8 --- /dev/null +++ b/package/secubox/luci-app-repo/htdocs/luci-static/resources/view/repo/dashboard.js @@ -0,0 +1,203 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require uci'; +'require rpc'; +'require ui'; + +var callStatus = rpc.declare({ + object: 'luci.repo', + method: 'status', + expect: {} +}); + +var callPackages = rpc.declare({ + object: 'luci.repo', + method: 'packages', + params: ['arch'], + expect: {} +}); + +var callSync = rpc.declare({ + object: 'luci.repo', + method: 'sync', + params: ['version'], + expect: {} +}); + +var callLogs = rpc.declare({ + object: 'luci.repo', + method: 'logs', + params: ['lines'], + expect: {} +}); + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + uci.load('repo') + ]); + }, + + formatBytes: function(bytes) { + if (bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }, + + renderStatus: function(status) { + var statusClass = status.running ? 'success' : 'danger'; + var statusText = status.running ? _('Running') : _('Stopped'); + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Repository Status')), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left', 'style': 'width:200px' }, _('Server Status')), + E('div', { 'class': 'td' }, [ + E('span', { 'class': 'label ' + statusClass }, statusText), + status.running ? E('span', {}, ' (port ' + status.port + ')') : '' + ]) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, _('Version')), + E('div', { 'class': 'td' }, status.version || '-') + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, _('GitHub Repository')), + E('div', { 'class': 'td' }, status.github_repo || '-') + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, _('Last Sync')), + E('div', { 'class': 'td' }, status.last_sync || _('Never')) + ]), + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, _('Auto Sync')), + E('div', { 'class': 'td' }, status.auto_sync ? + _('Every %d hours').format(status.sync_interval) : _('Disabled')) + ]) + ]) + ]); + }, + + renderArchitectures: function(status) { + var archs = status.architectures || {}; + var rows = []; + + Object.keys(archs).sort().forEach(function(arch) { + rows.push(E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, arch), + E('div', { 'class': 'td' }, archs[arch] + ' ' + _('packages')) + ])); + }); + + if (rows.length === 0) { + rows.push(E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td', 'colspan': 2 }, _('No packages synced yet')) + ])); + } + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Available Architectures')), + E('div', { 'class': 'table' }, rows) + ]); + }, + + renderActions: function(status) { + var self = this; + + var syncBtn = E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + ui.showModal(_('Sync Packages'), [ + E('p', {}, _('Sync packages from GitHub release?')), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + ui.hideModal(); + ui.showModal(_('Syncing...'), [ + E('p', { 'class': 'spinning' }, _('Downloading packages from GitHub...')) + ]); + callSync(status.version).then(function() { + setTimeout(function() { + ui.hideModal(); + window.location.reload(); + }, 3000); + }); + } + }, _('Sync Now')) + ]) + ]); + } + }, _('Sync Packages')); + + var logsBtn = E('button', { + 'class': 'cbi-button', + 'click': function() { + callLogs(100).then(function(result) { + var logs = (result.logs || '').split('|').join('\n'); + ui.showModal(_('Sync Logs'), [ + E('pre', { 'style': 'max-height:400px;overflow:auto;font-size:12px;' }, logs || _('No logs')), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, _('Close')) + ]) + ]); + }); + } + }, _('View Logs')); + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Actions')), + E('div', { 'class': 'cbi-value' }, [ + syncBtn, ' ', logsBtn + ]) + ]); + }, + + renderUsage: function(status) { + var port = status.port || 8888; + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Usage')), + E('p', {}, _('Add to /etc/opkg/customfeeds.conf:')), + E('pre', { 'style': 'background:#f5f5f5;padding:10px;border-radius:4px;' }, [ + '# Local repository\n', + 'src/gz secubox_luci http://127.0.0.1:' + port + '/luci/{ARCH}\n\n', + '# Or via HTTPS (external)\n', + 'src/gz secubox_luci https://repo.secubox.in/luci/{ARCH}' + ].join('')), + E('p', {}, _('Replace {ARCH} with your architecture: x86_64, aarch64_cortex-a72, aarch64_generic, etc.')) + ]); + }, + + render: function(data) { + var status = data[0] || {}; + + return E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Package Repository')), + E('div', { 'class': 'cbi-map-descr' }, + _('SecuBox package repository - serves OpenWrt packages locally for opkg installation.')), + this.renderStatus(status), + this.renderArchitectures(status), + this.renderActions(status), + this.renderUsage(status) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-repo/root/usr/share/luci/menu.d/luci-app-repo.json b/package/secubox/luci-app-repo/root/usr/share/luci/menu.d/luci-app-repo.json new file mode 100644 index 00000000..c34b42b9 --- /dev/null +++ b/package/secubox/luci-app-repo/root/usr/share/luci/menu.d/luci-app-repo.json @@ -0,0 +1,14 @@ +{ + "admin/services/repo": { + "title": "Package Repository", + "order": 85, + "action": { + "type": "view", + "path": "repo/dashboard" + }, + "depends": { + "acl": ["luci-app-repo"], + "uci": {"repo": true} + } + } +} diff --git a/package/secubox/luci-app-repo/root/usr/share/rpcd/acl.d/luci-app-repo.json b/package/secubox/luci-app-repo/root/usr/share/rpcd/acl.d/luci-app-repo.json new file mode 100644 index 00000000..3e3a506c --- /dev/null +++ b/package/secubox/luci-app-repo/root/usr/share/rpcd/acl.d/luci-app-repo.json @@ -0,0 +1,17 @@ +{ + "luci-app-repo": { + "description": "SecuBox Package Repository Dashboard", + "read": { + "ubus": { + "luci.repo": ["status", "config", "packages", "logs"] + }, + "uci": ["repo"] + }, + "write": { + "ubus": { + "luci.repo": ["sync"] + }, + "uci": ["repo"] + } + } +} diff --git a/package/secubox/secubox-app-repo/Makefile b/package/secubox/secubox-app-repo/Makefile new file mode 100644 index 00000000..c0791009 --- /dev/null +++ b/package/secubox/secubox-app-repo/Makefile @@ -0,0 +1,60 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-repo +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=GPL-3.0 + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-repo + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=SecuBox Package Repository Manager + DEPENDS:=+uhttpd +wget +gzip +coreutils-stat + PKGARCH:=all +endef + +define Package/secubox-app-repo/description + SecuBox Package Repository Manager - hosts and syncs OpenWrt packages + from GitHub releases for local opkg installation. +endef + +define Package/secubox-app-repo/conffiles +/etc/config/repo +endef + +define Build/Compile +endef + +define Package/secubox-app-repo/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./root/usr/sbin/repoctl $(1)/usr/sbin/ + $(INSTALL_BIN) ./root/usr/sbin/repo-sync $(1)/usr/sbin/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.repo $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-repo.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./root/etc/init.d/repo-server $(1)/etc/init.d/ + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./root/etc/config/repo $(1)/etc/config/ +endef + +define Package/secubox-app-repo/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + /etc/init.d/repo-server enable + /etc/init.d/repo-server start + /etc/init.d/rpcd restart +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-repo)) diff --git a/package/secubox/secubox-app-repo/root/etc/config/repo b/package/secubox/secubox-app-repo/root/etc/config/repo new file mode 100644 index 00000000..a8e2f489 --- /dev/null +++ b/package/secubox/secubox-app-repo/root/etc/config/repo @@ -0,0 +1,8 @@ +config repo 'main' + option enabled '1' + option version 'v1.0.0-beta' + option github_repo 'gkerma/secubox-openwrt' + option port '8888' + option auto_sync '1' + option sync_interval '6' + option last_sync '' diff --git a/package/secubox/secubox-app-repo/root/etc/init.d/repo-server b/package/secubox/secubox-app-repo/root/etc/init.d/repo-server new file mode 100755 index 00000000..763b39df --- /dev/null +++ b/package/secubox/secubox-app-repo/root/etc/init.d/repo-server @@ -0,0 +1,29 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=10 +USE_PROCD=1 + +REPO_DIR="/srv/repo.secubox.in" + +start_service() { + . /lib/functions.sh + config_load repo + config_get enabled main enabled "1" + config_get port main port "8888" + + [ "$enabled" = "1" ] || return 0 + + mkdir -p "$REPO_DIR" + + procd_open_instance + procd_set_param command /usr/sbin/uhttpd -f -h "$REPO_DIR" -p "0.0.0.0:$port" + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "repo" +} diff --git a/package/secubox/secubox-app-repo/root/usr/libexec/rpcd/luci.repo b/package/secubox/secubox-app-repo/root/usr/libexec/rpcd/luci.repo new file mode 100755 index 00000000..49b4c426 --- /dev/null +++ b/package/secubox/secubox-app-repo/root/usr/libexec/rpcd/luci.repo @@ -0,0 +1,137 @@ +#!/bin/sh + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +REPO_DIR="/srv/repo.secubox.in" +LOG_FILE="/var/log/repo-sync.log" + +case "$1" in + list) + echo '{"status":{},"config":{},"packages":{"arch":"string"},"sync":{"version":"string"},"logs":{"lines":"number"}}' + ;; + call) + case "$2" in + status) + json_init + + # Load config + config_load repo + config_get enabled main enabled "1" + config_get version main version "unknown" + config_get github_repo main github_repo "" + config_get port main port "8888" + config_get last_sync main last_sync "" + config_get auto_sync main auto_sync "0" + config_get sync_interval main sync_interval "6" + + json_add_boolean "enabled" "$enabled" + json_add_string "version" "$version" + json_add_string "github_repo" "$github_repo" + json_add_int "port" "$port" + json_add_string "last_sync" "$last_sync" + json_add_boolean "auto_sync" "$auto_sync" + json_add_int "sync_interval" "$sync_interval" + + # Server status + if netstat -tln 2>/dev/null | grep -q ":$port "; then + json_add_boolean "running" 1 + else + json_add_boolean "running" 0 + fi + + # Package counts by architecture + json_add_object "architectures" + for dir in "$REPO_DIR/luci"/*; do + [ -d "$dir" ] || continue + arch=$(basename "$dir") + count=$(ls "$dir"/*.ipk 2>/dev/null | wc -l) + json_add_int "$arch" "$count" + done + json_close_object + + json_dump + ;; + + config) + json_init + config_load repo + + config_get enabled main enabled "1" + config_get version main version "" + config_get github_repo main github_repo "" + config_get port main port "8888" + config_get auto_sync main auto_sync "0" + config_get sync_interval main sync_interval "6" + + json_add_boolean "enabled" "$enabled" + json_add_string "version" "$version" + json_add_string "github_repo" "$github_repo" + json_add_int "port" "$port" + json_add_boolean "auto_sync" "$auto_sync" + json_add_int "sync_interval" "$sync_interval" + + json_dump + ;; + + packages) + read -r input + json_load "$input" + json_get_var arch arch "x86_64" + + json_init + json_add_string "arch" "$arch" + json_add_array "packages" + + dir="$REPO_DIR/luci/$arch" + if [ -d "$dir" ]; then + for ipk in "$dir"/*.ipk; do + [ -f "$ipk" ] || continue + name=$(basename "$ipk") + size=$(stat -c%s "$ipk" 2>/dev/null || echo 0) + json_add_object "" + json_add_string "name" "$name" + json_add_int "size" "$size" + json_close_object + done + fi + + json_close_array + json_dump + ;; + + sync) + read -r input + json_load "$input" + json_get_var version version "" + + if [ -n "$version" ]; then + uci set repo.main.version="$version" + uci commit repo + fi + + # Run sync in background + /usr/sbin/repo-sync & + + json_init + json_add_boolean "started" 1 + json_add_string "message" "Sync started in background" + json_dump + ;; + + logs) + read -r input + json_load "$input" + json_get_var lines lines 50 + + json_init + if [ -f "$LOG_FILE" ]; then + json_add_string "logs" "$(tail -n "$lines" "$LOG_FILE" | sed 's/"/\\"/g' | tr '\n' '|')" + else + json_add_string "logs" "" + fi + json_dump + ;; + esac + ;; +esac diff --git a/package/secubox/secubox-app-repo/root/usr/sbin/repo-sync b/package/secubox/secubox-app-repo/root/usr/sbin/repo-sync new file mode 100755 index 00000000..fbea1e5b --- /dev/null +++ b/package/secubox/secubox-app-repo/root/usr/sbin/repo-sync @@ -0,0 +1,131 @@ +#!/bin/sh +# SecuBox Package Repository Sync Script +# Syncs packages from GitHub releases to local repo + +. /lib/functions.sh + +REPO_DIR="/srv/repo.secubox.in" +CONFIG_FILE="/etc/config/repo" +LOG_FILE="/var/log/repo-sync.log" + +log() { + local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*" + echo "$msg" + echo "$msg" >> "$LOG_FILE" +} + +# Load config +config_load repo +config_get GITHUB_REPO main github_repo "gkerma/secubox-openwrt" +config_get VERSION main version "v1.0.0-beta" +config_get ENABLED main enabled "1" + +[ "$ENABLED" = "1" ] || { log "Repo sync disabled"; exit 0; } + +VERSION_NUM="${VERSION#v}" +TMP_DIR="/tmp/repo-sync-$$" + +log "Starting sync from $GITHUB_REPO $VERSION" + +mkdir -p "$TMP_DIR" +mkdir -p "$REPO_DIR/packages" "$REPO_DIR/luci" "$REPO_DIR/catalog" +cd "$TMP_DIR" + +# Architecture mappings: github-arch:opkg-arch +ARCHS="x86-64:x86_64 aarch64-generic:aarch64_generic aarch64-cortex-a72:aarch64_cortex-a72 rockchip-armv8:aarch64_generic mips-24kc:mips_24kc mipsel-24kc:mipsel_24kc" + +for arch_map in $ARCHS; do + ARCH="${arch_map%%:*}" + OPKG_ARCH="${arch_map##*:}" + TARBALL="secubox-${VERSION_NUM}-${ARCH}.tar.gz" + URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${TARBALL}" + + log "Downloading $TARBALL..." + if wget -q -O "$TARBALL" "$URL" 2>/dev/null; then + mkdir -p "$REPO_DIR/packages/$OPKG_ARCH" + mkdir -p "$REPO_DIR/luci/$OPKG_ARCH" + + # Extract + mkdir -p "extract-$ARCH" + tar -xzf "$TARBALL" -C "extract-$ARCH" 2>/dev/null + + # Sort packages + find "extract-$ARCH" -name '*.ipk' | while read pkg; do + PKG_NAME="$(basename "$pkg")" + if echo "$PKG_NAME" | grep -q '^luci-'; then + cp "$pkg" "$REPO_DIR/luci/$OPKG_ARCH/" + else + cp "$pkg" "$REPO_DIR/packages/$OPKG_ARCH/" + fi + done + + log " Extracted to $OPKG_ARCH" + else + log " Skipping $ARCH (not found)" + fi +done + +# Generate Packages index +log "Generating opkg indexes..." +for basedir in "$REPO_DIR/packages" "$REPO_DIR/luci"; do + for dir in "$basedir"/*; do + [ -d "$dir" ] || continue + cd "$dir" + + rm -f Packages Packages.gz + + for ipk in *.ipk 2>/dev/null; do + [ -f "$ipk" ] || continue + SIZE=$(stat -c%s "$ipk" 2>/dev/null || ls -l "$ipk" | awk '{print $5}') + MD5=$(md5sum "$ipk" | cut -d' ' -f1) + PKG=$(echo "$ipk" | sed 's/_.*//g') + + echo "Package: $PKG" + echo "Version: 0.0.0-r1" + echo "Architecture: all" + echo "Filename: $ipk" + echo "Size: $SIZE" + echo "MD5Sum: $MD5" + echo "" + done > Packages + + gzip -9c Packages > Packages.gz + log " $(basename "$dir"): $(grep -c '^Package:' Packages 2>/dev/null || echo 0) packages" + done +done + +# Create index.html +cat > "$REPO_DIR/index.html" << 'HTML' + +
Add to /etc/opkg/customfeeds.conf:
src/gz secubox_packages https://repo.secubox.in/packages/{ARCH}
+src/gz secubox_luci https://repo.secubox.in/luci/{ARCH}
+