diff --git a/package/secubox/luci-app-jabber/Makefile b/package/secubox/luci-app-jabber/Makefile new file mode 100644 index 00000000..d33fa609 --- /dev/null +++ b/package/secubox/luci-app-jabber/Makefile @@ -0,0 +1,27 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Jabber/XMPP Server (Prosody) +LUCI_DEPENDS:=+luci-base +secubox-app-jabber +LUCI_PKGARCH:=all +PKG_LICENSE:=MIT + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-jabber/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-jabber.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-jabber.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.jabber $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/jabber + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/jabber/overview.js $(1)/www/luci-static/resources/view/jabber/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/jabber + $(INSTALL_DATA) ./htdocs/luci-static/resources/jabber/api.js $(1)/www/luci-static/resources/jabber/ +endef + +$(eval $(call BuildPackage,luci-app-jabber)) diff --git a/package/secubox/luci-app-jabber/htdocs/luci-static/resources/jabber/api.js b/package/secubox/luci-app-jabber/htdocs/luci-static/resources/jabber/api.js new file mode 100644 index 00000000..01504d8b --- /dev/null +++ b/package/secubox/luci-app-jabber/htdocs/luci-static/resources/jabber/api.js @@ -0,0 +1,107 @@ +'use strict'; +'require rpc'; + +return L.Class.extend({ + status: rpc.declare({ + object: 'luci.jabber', + method: 'status', + expect: { } + }), + + start: rpc.declare({ + object: 'luci.jabber', + method: 'start', + expect: { } + }), + + stop: rpc.declare({ + object: 'luci.jabber', + method: 'stop', + expect: { } + }), + + install: rpc.declare({ + object: 'luci.jabber', + method: 'install', + expect: { } + }), + + uninstall: rpc.declare({ + object: 'luci.jabber', + method: 'uninstall', + expect: { } + }), + + update: rpc.declare({ + object: 'luci.jabber', + method: 'update', + expect: { } + }), + + logs: rpc.declare({ + object: 'luci.jabber', + method: 'logs', + params: ['lines'], + expect: { } + }), + + emancipate: rpc.declare({ + object: 'luci.jabber', + method: 'emancipate', + params: ['domain'], + expect: { } + }), + + configureHaproxy: rpc.declare({ + object: 'luci.jabber', + method: 'configure_haproxy', + expect: { } + }), + + userAdd: rpc.declare({ + object: 'luci.jabber', + method: 'user_add', + params: ['jid', 'password'], + expect: { } + }), + + userDel: rpc.declare({ + object: 'luci.jabber', + method: 'user_del', + params: ['jid'], + expect: { } + }), + + userPasswd: rpc.declare({ + object: 'luci.jabber', + method: 'user_passwd', + params: ['jid', 'password'], + expect: { } + }), + + userList: rpc.declare({ + object: 'luci.jabber', + method: 'user_list', + expect: { } + }), + + roomCreate: rpc.declare({ + object: 'luci.jabber', + method: 'room_create', + params: ['name'], + expect: { } + }), + + roomDelete: rpc.declare({ + object: 'luci.jabber', + method: 'room_delete', + params: ['name'], + expect: { } + }), + + roomList: rpc.declare({ + object: 'luci.jabber', + method: 'room_list', + expect: { } + }) +}); diff --git a/package/secubox/luci-app-jabber/htdocs/luci-static/resources/view/jabber/overview.js b/package/secubox/luci-app-jabber/htdocs/luci-static/resources/view/jabber/overview.js new file mode 100644 index 00000000..f32b25d3 --- /dev/null +++ b/package/secubox/luci-app-jabber/htdocs/luci-static/resources/view/jabber/overview.js @@ -0,0 +1,420 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require ui'; +'require uci'; +'require form'; +'require jabber.api as api'; + +return view.extend({ + handleAction: function(action, args) { + var self = this; + var btn = document.activeElement; + + ui.showModal(_('Please wait...'), [ + E('p', { 'class': 'spinning' }, _('Processing request...')) + ]); + + var promise; + switch(action) { + case 'start': + promise = api.start(); + break; + case 'stop': + promise = api.stop(); + break; + case 'install': + promise = api.install(); + break; + case 'uninstall': + if (!confirm(_('This will remove the Jabber container. User data will be preserved. Continue?'))) + return ui.hideModal(); + promise = api.uninstall(); + break; + case 'update': + promise = api.update(); + break; + case 'configure_haproxy': + promise = api.configureHaproxy(); + break; + case 'emancipate': + var domain = args; + if (!domain) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Domain is required')), 'error'); + return; + } + promise = api.emancipate(domain); + break; + case 'user_add': + var jid = args.jid; + var password = args.password; + if (!jid) { + ui.hideModal(); + ui.addNotification(null, E('p', _('JID is required')), 'error'); + return; + } + promise = api.userAdd(jid, password); + break; + case 'user_del': + if (!confirm(_('Delete user ') + args + '?')) + return ui.hideModal(); + promise = api.userDel(args); + break; + default: + ui.hideModal(); + return; + } + + promise.then(function(res) { + ui.hideModal(); + if (res && res.success) { + var msg = res.message || _('Action completed'); + if (res.password) { + msg += '\n' + _('Password: ') + res.password; + } + ui.addNotification(null, E('p', { 'style': 'white-space: pre-wrap;' }, msg), 'success'); + self.load().then(function(data) { + dom.content(document.querySelector('#jabber-content'), self.renderContent(data)); + }); + } else { + ui.addNotification(null, E('p', res.error || _('Action failed')), 'error'); + } + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: ') + e.message), 'error'); + }); + }, + + load: function() { + return Promise.all([ + api.status(), + api.userList(), + uci.load('jabber') + ]); + }, + + renderInstallWizard: function() { + var self = this; + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Jabber/XMPP Server')), + E('p', {}, _('Prosody is a modern XMPP server written in Lua. It aims to be easy to set up and configure, and efficient with system resources.')), + E('div', { 'class': 'cbi-value' }, [ + E('h4', {}, _('Features')), + E('ul', {}, [ + E('li', {}, _('Secure messaging with end-to-end encryption (OMEMO)')), + E('li', {}, _('Multi-user chat rooms (MUC)')), + E('li', {}, _('File sharing with HTTP upload')), + E('li', {}, _('Server-to-server federation (S2S)')), + E('li', {}, _('BOSH and WebSocket for web clients')), + E('li', {}, _('Message archiving (MAM)')) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('h4', {}, _('Compatible Clients')), + E('ul', {}, [ + E('li', {}, _('Conversations (Android)')), + E('li', {}, _('Monal (iOS/macOS)')), + E('li', {}, _('Gajim (Windows/Linux)')), + E('li', {}, _('Dino (Linux)')), + E('li', {}, _('Converse.js (Web)')) + ]) + ]), + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-positive', + 'click': function() { self.handleAction('install'); } + }, _('Install Jabber/XMPP')) + ]) + ]); + }, + + renderStatusBadge: function(running) { + var color = running === 'true' ? '#4CAF50' : '#f44336'; + var text = running === 'true' ? _('Running') : _('Stopped'); + return E('span', { + 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + color + }, text); + }, + + renderContent: function(data) { + var self = this; + var status = data[0] || {}; + var userListData = data[1] || {}; + + if (status.container_state === 'not_installed') { + return this.renderInstallWizard(); + } + + var running = status.running === 'true'; + var haproxyConfigured = status.haproxy === '1'; + var domain = status.domain || ''; + var hostname = status.hostname || 'jabber.local'; + + // Parse user list + var users = []; + if (userListData.users) { + users = userListData.users.split(',').filter(function(u) { return u.length > 0; }); + } + + var accessUrl = ''; + if (running) { + if (domain && haproxyConfigured) { + accessUrl = 'https://' + domain; + } else { + accessUrl = 'http://192.168.255.1:' + (status.http_port || '5280'); + } + } + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Jabber/XMPP Server (Prosody)')), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Status')), + E('div', { 'class': 'cbi-value-field' }, this.renderStatusBadge(status.running)) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Hostname')), + E('div', { 'class': 'cbi-value-field' }, hostname) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('C2S Port')), + E('div', { 'class': 'cbi-value-field' }, status.c2s_port || '5222') + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('S2S Port')), + E('div', { 'class': 'cbi-value-field' }, [ + status.s2s_port || '5269', + ' ', + E('span', { + 'style': 'display:inline-block;padding:2px 8px;border-radius:3px;color:#fff;background:' + (status.s2s_enabled === '1' ? '#4CAF50' : '#9e9e9e') + }, status.s2s_enabled === '1' ? _('Federation ON') : _('Federation OFF')) + ]) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('HTTP/BOSH Port')), + E('div', { 'class': 'cbi-value-field' }, status.http_port || '5280') + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Users')), + E('div', { 'class': 'cbi-value-field' }, status.user_count || '0') + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('MUC (Chat Rooms)')), + E('div', { 'class': 'cbi-value-field' }, [ + E('span', { + 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (status.muc_enabled === '1' ? '#4CAF50' : '#9e9e9e') + }, status.muc_enabled === '1' ? _('Enabled') : _('Disabled')) + ]) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('HAProxy')), + E('div', { 'class': 'cbi-value-field' }, [ + E('span', { + 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (haproxyConfigured ? '#4CAF50' : '#9e9e9e') + }, haproxyConfigured ? _('Configured') : _('Not configured')), + ' ', + !haproxyConfigured ? E('button', { + 'class': 'btn cbi-button', + 'click': function() { self.handleAction('configure_haproxy'); } + }, _('Configure')) : '' + ]) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Domain')), + E('div', { 'class': 'cbi-value-field' }, domain || _('Not configured')) + ]), + + E('hr'), + + E('h4', {}, _('Service Controls')), + E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom: 20px;' }, [ + running ? + E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'click': function() { self.handleAction('stop'); } + }, _('Stop')) : + E('button', { + 'class': 'btn cbi-button cbi-button-positive', + 'click': function() { self.handleAction('start'); } + }, _('Start')), + ' ', + E('button', { + 'class': 'btn cbi-button', + 'click': function() { self.handleAction('update'); } + }, _('Update')), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'click': function() { self.handleAction('uninstall'); } + }, _('Uninstall')) + ]), + + E('hr'), + + E('h4', {}, _('User Management')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('New User')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'new-user-jid', + 'class': 'cbi-input-text', + 'placeholder': 'user@' + hostname, + 'style': 'width: 200px;' + }), + ' ', + E('input', { + 'type': 'password', + 'id': 'new-user-password', + 'class': 'cbi-input-text', + 'placeholder': _('Password (auto-generate if empty)'), + 'style': 'width: 200px;' + }), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-positive', + 'click': function() { + var jid = document.getElementById('new-user-jid').value; + var password = document.getElementById('new-user-password').value; + self.handleAction('user_add', { jid: jid, password: password }); + } + }, _('Add User')) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Registered Users')), + E('div', { 'class': 'cbi-value-field' }, [ + users.length > 0 ? + E('table', { 'class': 'table', 'style': 'width: auto;' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('JID')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ].concat(users.map(function(user) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, user), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-remove', + 'click': function() { self.handleAction('user_del', user); } + }, _('Delete')) + ]) + ]); + }))) : + E('em', {}, _('No users registered')) + ]) + ]), + + E('hr'), + + E('h4', {}, _('Emancipate (Public Exposure)')), + E('p', {}, _('Make Jabber publicly accessible with SSL certificate, DNS records, and S2S federation.')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Domain')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { + 'type': 'text', + 'id': 'emancipate-domain', + 'class': 'cbi-input-text', + 'placeholder': 'xmpp.example.com', + 'value': domain + }) + ]) + ]), + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': function() { + var domainInput = document.getElementById('emancipate-domain'); + self.handleAction('emancipate', domainInput.value); + } + }, _('Emancipate')) + ]), + E('p', { 'style': 'font-size: 12px; color: #666;' }, [ + _('DNS records needed: A record for domain, SRV records for _xmpp-client._tcp and _xmpp-server._tcp') + ]), + + E('hr'), + + E('h4', {}, _('Connection Info')), + E('div', { 'style': 'background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace;' }, [ + E('p', {}, [ + E('strong', {}, _('XMPP Server: ')), + hostname + ':' + (status.c2s_port || '5222') + ]), + E('p', {}, [ + E('strong', {}, _('BOSH URL: ')), + accessUrl + '/http-bind' + ]), + E('p', {}, [ + E('strong', {}, _('WebSocket: ')), + (domain && haproxyConfigured ? 'wss://' + domain : 'ws://192.168.255.1:' + (status.http_port || '5280')) + '/xmpp-websocket' + ]), + E('p', {}, [ + E('strong', {}, _('Admin JID: ')), + (status.admin_user || 'admin') + '@' + hostname + ]) + ]), + + E('hr'), + + E('h4', {}, _('Logs')), + E('div', { 'id': 'jabber-logs' }, [ + E('pre', { + 'style': 'background:#1e1e1e;color:#d4d4d4;padding:10px;max-height:300px;overflow:auto;font-size:12px;border-radius:4px;' + }, _('Loading logs...')) + ]), + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'btn cbi-button', + 'click': function() { + api.logs(100).then(function(res) { + var logsEl = document.querySelector('#jabber-logs pre'); + if (logsEl) { + logsEl.textContent = res.logs || _('No logs available'); + } + }); + } + }, _('Refresh Logs')) + ]) + ]); + }, + + render: function(data) { + var self = this; + + var content = E('div', { 'id': 'jabber-content' }, this.renderContent(data)); + + // Load logs initially + api.logs(50).then(function(res) { + var logsEl = document.querySelector('#jabber-logs pre'); + if (logsEl) { + logsEl.textContent = res.logs || _('No logs available'); + } + }); + + // Poll for status updates + poll.add(function() { + return api.status().then(function(status) { + // Update status badge if needed + }); + }, 10); + + return content; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-jabber/root/usr/libexec/rpcd/luci.jabber b/package/secubox/luci-app-jabber/root/usr/libexec/rpcd/luci.jabber new file mode 100755 index 00000000..4dbca9a5 --- /dev/null +++ b/package/secubox/luci-app-jabber/root/usr/libexec/rpcd/luci.jabber @@ -0,0 +1,536 @@ +#!/bin/sh + +# RPCD backend for Jabber/XMPP LuCI app + +. /usr/share/libubox/jshn.sh + +JABBERCTL="/usr/sbin/jabberctl" + +# Helper to get UCI value +uci_get() { + local section="$1" + local option="$2" + local default="$3" + local val + val=$(uci -q get "jabber.${section}.${option}") + echo "${val:-$default}" +} + +# Get container status +get_container_status() { + local state="not_installed" + local running="false" + local lxc_info="" + + if [ -d "/srv/lxc/jabber" ]; then + state="installed" + lxc_info=$(lxc-info -n jabber 2>/dev/null) + if echo "$lxc_info" | grep -q "State:.*RUNNING"; then + running="true" + fi + fi + + echo "$state $running" +} + +# Method: status +method_status() { + local enabled hostname c2s_port s2s_port http_port + local container_state running + local info user_count + + enabled=$(uci_get main enabled 0) + hostname=$(uci_get server hostname "jabber.local") + c2s_port=$(uci_get server c2s_port "5222") + s2s_port=$(uci_get server s2s_port "5269") + http_port=$(uci_get server http_port "5280") + + info=$(get_container_status) + container_state=$(echo "$info" | awk '{print $1}') + running=$(echo "$info" | awk '{print $2}') + + # Get user count + user_count=0 + if [ "$running" = "true" ]; then + user_count=$(lxc-attach -n jabber -- find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) + fi + + # Get configured domain if emancipated + local domain haproxy muc_enabled s2s_enabled + domain=$(uci_get network domain "") + haproxy=$(uci_get network haproxy "0") + muc_enabled=$(uci_get muc enabled "1") + s2s_enabled=$(uci_get s2s enabled "0") + + # Get admin info + local admin_email admin_user + admin_email=$(uci_get admin email "admin@localhost") + admin_user=$(uci_get admin initial_user "admin") + + json_init + json_add_string "enabled" "$enabled" + json_add_string "container_state" "$container_state" + json_add_string "running" "$running" + json_add_string "hostname" "$hostname" + json_add_string "c2s_port" "$c2s_port" + json_add_string "s2s_port" "$s2s_port" + json_add_string "http_port" "$http_port" + json_add_int "user_count" "$user_count" + json_add_string "domain" "$domain" + json_add_string "haproxy" "$haproxy" + json_add_string "muc_enabled" "$muc_enabled" + json_add_string "s2s_enabled" "$s2s_enabled" + json_add_string "admin_email" "$admin_email" + json_add_string "admin_user" "$admin_user" + json_dump +} + +# Method: start +method_start() { + local output + output=$($JABBERCTL start 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Jabber/XMPP started successfully" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: stop +method_stop() { + local output + output=$($JABBERCTL stop 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Jabber/XMPP stopped successfully" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: install +method_install() { + local output + output=$($JABBERCTL install 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Jabber/XMPP installed successfully" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: uninstall +method_uninstall() { + local output + output=$($JABBERCTL uninstall 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Jabber/XMPP uninstalled successfully" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: update +method_update() { + local output + output=$($JABBERCTL update 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Jabber/XMPP updated successfully" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: logs +method_logs() { + local lines="${1:-50}" + local output + + if [ -d "/srv/lxc/jabber" ]; then + output=$($JABBERCTL logs "$lines" 2>&1 | tail -n "$lines") + else + output="Container not installed" + fi + + json_init + json_add_string "logs" "$output" + json_dump +} + +# Method: emancipate +method_emancipate() { + read -r input + json_load "$input" + json_get_var domain domain + + if [ -z "$domain" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Domain is required" + json_dump + return + fi + + local output + output=$($JABBERCTL emancipate "$domain" 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Jabber/XMPP emancipated to $domain" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: configure_haproxy +method_configure_haproxy() { + local output + output=$($JABBERCTL configure-haproxy 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "HAProxy configured for Jabber" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: user_add +method_user_add() { + read -r input + json_load "$input" + json_get_var jid jid + json_get_var password password + + if [ -z "$jid" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "JID is required" + json_dump + return + fi + + local output + if [ -n "$password" ]; then + output=$($JABBERCTL user add "$jid" "$password" 2>&1) + else + output=$($JABBERCTL user add "$jid" 2>&1) + fi + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "User $jid created" + # Extract password from output + local new_pass=$(echo "$output" | grep -oE 'Password: [^ ]+' | cut -d: -f2 | tr -d ' ') + json_add_string "password" "$new_pass" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: user_del +method_user_del() { + read -r input + json_load "$input" + json_get_var jid jid + + if [ -z "$jid" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "JID is required" + json_dump + return + fi + + local output + output=$($JABBERCTL user del "$jid" 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "User $jid deleted" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: user_passwd +method_user_passwd() { + read -r input + json_load "$input" + json_get_var jid jid + json_get_var password password + + if [ -z "$jid" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "JID is required" + json_dump + return + fi + + local output + if [ -n "$password" ]; then + output=$($JABBERCTL user passwd "$jid" "$password" 2>&1) + else + output=$($JABBERCTL user passwd "$jid" 2>&1) + fi + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Password changed for $jid" + local new_pass=$(echo "$output" | grep -oE 'New password: [^ ]+' | cut -d: -f2 | tr -d ' ') + json_add_string "password" "$new_pass" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: user_list +method_user_list() { + local users="" + + if lxc-info -n jabber 2>/dev/null | grep -q "RUNNING"; then + users=$(lxc-attach -n jabber -- find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | while read f; do + user=$(basename "$f" .dat) + domain=$(echo "$f" | grep -oE '[^/]+/accounts' | cut -d/ -f1 | tr '%' '.') + echo "${user}@${domain}" + done | paste -sd,) + fi + + json_init + json_add_string "users" "$users" + json_dump +} + +# Method: room_create +method_room_create() { + read -r input + json_load "$input" + json_get_var name name + + if [ -z "$name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Room name is required" + json_dump + return + fi + + local output + output=$($JABBERCTL room create "$name" 2>&1) + local rc=$? + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Room $name available (created on first join)" + json_dump +} + +# Method: room_delete +method_room_delete() { + read -r input + json_load "$input" + json_get_var name name + + if [ -z "$name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Room name is required" + json_dump + return + fi + + local output + output=$($JABBERCTL room delete "$name" 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Room $name deleted" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + json_dump +} + +# Method: room_list +method_room_list() { + local rooms="" + + if lxc-info -n jabber 2>/dev/null | grep -q "RUNNING"; then + rooms=$(lxc-attach -n jabber -- find /var/lib/prosody -name "*.dat" -path "*rooms*" 2>/dev/null | while read f; do + room=$(basename "$f" .dat) + echo "$room" + done | paste -sd,) + fi + + json_init + json_add_string "rooms" "$rooms" + json_dump +} + +# List available methods +list_methods() { + json_init + json_add_object "status" + json_close_object + json_add_object "start" + json_close_object + json_add_object "stop" + json_close_object + json_add_object "install" + json_close_object + json_add_object "uninstall" + json_close_object + json_add_object "update" + json_close_object + json_add_object "logs" + json_add_int "lines" 50 + json_close_object + json_add_object "emancipate" + json_add_string "domain" "" + json_close_object + json_add_object "configure_haproxy" + json_close_object + json_add_object "user_add" + json_add_string "jid" "" + json_add_string "password" "" + json_close_object + json_add_object "user_del" + json_add_string "jid" "" + json_close_object + json_add_object "user_passwd" + json_add_string "jid" "" + json_add_string "password" "" + json_close_object + json_add_object "user_list" + json_close_object + json_add_object "room_create" + json_add_string "name" "" + json_close_object + json_add_object "room_delete" + json_add_string "name" "" + json_close_object + json_add_object "room_list" + json_close_object + json_dump +} + +# Main dispatcher +case "$1" in + list) + list_methods + ;; + call) + case "$2" in + status) + method_status + ;; + start) + method_start + ;; + stop) + method_stop + ;; + install) + method_install + ;; + uninstall) + method_uninstall + ;; + update) + method_update + ;; + logs) + read -r input + json_load "$input" + json_get_var lines lines + method_logs "${lines:-50}" + ;; + emancipate) + method_emancipate + ;; + configure_haproxy) + method_configure_haproxy + ;; + user_add) + method_user_add + ;; + user_del) + method_user_del + ;; + user_passwd) + method_user_passwd + ;; + user_list) + method_user_list + ;; + room_create) + method_room_create + ;; + room_delete) + method_room_delete + ;; + room_list) + method_room_list + ;; + *) + echo '{"error":"Method not found"}' + ;; + esac + ;; + *) + echo '{"error":"Invalid action"}' + ;; +esac diff --git a/package/secubox/luci-app-jabber/root/usr/share/luci/menu.d/luci-app-jabber.json b/package/secubox/luci-app-jabber/root/usr/share/luci/menu.d/luci-app-jabber.json new file mode 100644 index 00000000..cf8ac97d --- /dev/null +++ b/package/secubox/luci-app-jabber/root/usr/share/luci/menu.d/luci-app-jabber.json @@ -0,0 +1,14 @@ +{ + "admin/services/jabber": { + "title": "Jabber/XMPP", + "order": 68, + "action": { + "type": "view", + "path": "jabber/overview" + }, + "depends": { + "acl": ["luci-app-jabber"], + "uci": {"jabber": true} + } + } +} diff --git a/package/secubox/luci-app-jabber/root/usr/share/rpcd/acl.d/luci-app-jabber.json b/package/secubox/luci-app-jabber/root/usr/share/rpcd/acl.d/luci-app-jabber.json new file mode 100644 index 00000000..ed42fa45 --- /dev/null +++ b/package/secubox/luci-app-jabber/root/usr/share/rpcd/acl.d/luci-app-jabber.json @@ -0,0 +1,17 @@ +{ + "luci-app-jabber": { + "description": "Grant access to Jabber/XMPP management", + "read": { + "ubus": { + "luci.jabber": ["status", "logs", "user_list", "room_list"] + }, + "uci": ["jabber"] + }, + "write": { + "ubus": { + "luci.jabber": ["start", "stop", "install", "uninstall", "update", "emancipate", "configure_haproxy", "user_add", "user_del", "user_passwd", "room_create", "room_delete"] + }, + "uci": ["jabber"] + } + } +} diff --git a/package/secubox/secubox-app-jabber/Makefile b/package/secubox/secubox-app-jabber/Makefile new file mode 100644 index 00000000..0c895324 --- /dev/null +++ b/package/secubox/secubox-app-jabber/Makefile @@ -0,0 +1,45 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-jabber +PKG_RELEASE:=1 +PKG_VERSION:=1.0.0 +PKG_ARCH:=all +PKG_MAINTAINER:=CyberMind Studio +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-jabber + SECTION:=net + CATEGORY:=Network + PKGARCH:=all + SUBMENU:=SecuBox Apps + TITLE:=SecuBox Jabber/XMPP Server (Prosody) + DEPENDS:=+lxc +lxc-common +wget-ssl +tar +jsonfilter +endef + +define Package/secubox-app-jabber/description +Jabber/XMPP instant messaging server based on Prosody. +Runs in an LXC Debian container with full XMPP support. +Features multi-user chat (MUC), file uploads, and S2S federation. +endef + +define Package/secubox-app-jabber/conffiles +/etc/config/jabber +endef + +define Build/Compile +endef + +define Package/secubox-app-jabber/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/jabber $(1)/etc/config/jabber + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/jabber $(1)/etc/init.d/jabber + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/jabberctl $(1)/usr/sbin/jabberctl +endef + +$(eval $(call BuildPackage,secubox-app-jabber)) diff --git a/package/secubox/secubox-app-jabber/files/etc/config/jabber b/package/secubox/secubox-app-jabber/files/etc/config/jabber new file mode 100644 index 00000000..f1b63137 --- /dev/null +++ b/package/secubox/secubox-app-jabber/files/etc/config/jabber @@ -0,0 +1,36 @@ +config jabber 'main' + option enabled '0' + option data_path '/srv/jabber' + option memory_limit '512' + +config jabber 'server' + option hostname 'jabber.local' + option c2s_port '5222' + option s2s_port '5269' + option http_port '5280' + option https_port '5281' + +config jabber 'admin' + option email 'admin@localhost' + option initial_user 'admin' + option initial_password '' + +config jabber 'muc' + option enabled '1' + option host 'conference' + option default_room_logging '0' + +config jabber 'http_upload' + option enabled '1' + option max_size '10485760' + option host 'upload' + +config jabber 'network' + option domain '' + option haproxy '0' + option haproxy_ssl '1' + option firewall_wan '0' + +config jabber 's2s' + option enabled '0' + option require_encryption '1' diff --git a/package/secubox/secubox-app-jabber/files/etc/init.d/jabber b/package/secubox/secubox-app-jabber/files/etc/init.d/jabber new file mode 100755 index 00000000..e000aad0 --- /dev/null +++ b/package/secubox/secubox-app-jabber/files/etc/init.d/jabber @@ -0,0 +1,35 @@ +#!/bin/sh /etc/rc.common + +START=95 +STOP=10 +USE_PROCD=1 + +SERVICE_BIN="/usr/sbin/jabberctl" + +start_service() { + local enabled + config_load jabber + config_get enabled main enabled 0 + + [ "$enabled" = "1" ] || return 0 + + procd_open_instance + procd_set_param command "$SERVICE_BIN" service-run + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +stop_service() { + "$SERVICE_BIN" service-stop >/dev/null 2>&1 +} + +reload_service() { + stop + start +} + +service_triggers() { + procd_add_reload_trigger "jabber" +} diff --git a/package/secubox/secubox-app-jabber/files/usr/sbin/jabberctl b/package/secubox/secubox-app-jabber/files/usr/sbin/jabberctl new file mode 100755 index 00000000..6ee7e704 --- /dev/null +++ b/package/secubox/secubox-app-jabber/files/usr/sbin/jabberctl @@ -0,0 +1,1127 @@ +#!/bin/sh +# SecuBox Jabber Manager - LXC Debian container with Prosody XMPP Server + +CONFIG="jabber" +LXC_NAME="jabber" +LXC_PATH="/srv/lxc" +LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" +LXC_CONF="$LXC_PATH/$LXC_NAME/config" +DATA_PATH_DEFAULT="/srv/jabber" +PROSODY_VERSION="0.12" +OPKG_UPDATED=0 + +usage() { + cat <<'USAGE' +Usage: jabberctl + +Installation: + install Create LXC container with Prosody XMPP server + uninstall Remove container (preserves data) + update Update Prosody to latest version + check Run prerequisite checks + +Service: + start Start Jabber/XMPP server (via init) + stop Stop Jabber/XMPP server + restart Restart Jabber/XMPP server + status Show container and service status + logs [N] Show last N lines of logs (default: 50) + shell Open interactive shell in container + +Users: + user add [password] Create user (e.g. user@domain) + user del Delete user + user passwd [password] Change password + user list List all users + +Rooms (MUC): + room create Create conference room + room delete Delete conference room + room list List all rooms + +Exposure: + configure-haproxy Setup HAProxy vhost for HTTPS/WSS + emancipate Full exposure (HAProxy + ACME + DNS + S2S) + +Backup: + backup [path] Backup database and config + restore Restore from backup + +Internal: + service-run Run container via procd + service-stop Stop container +USAGE +} + +# ---------- helpers ---------- + +require_root() { [ "$(id -u)" -eq 0 ]; } + +uci_get() { + local key="$1" + local section="${2:-main}" + uci -q get ${CONFIG}.${section}.$key +} + +uci_set() { + local key="$1" + local value="$2" + local section="${3:-main}" + uci set ${CONFIG}.${section}.$key="$value" +} + +log_info() { echo "[INFO] $*"; logger -t jabberctl "$*"; } +log_warn() { echo "[WARN] $*"; logger -t jabberctl -p warning "$*"; } +log_error() { echo "[ERROR] $*" >&2; logger -t jabberctl -p err "$*"; } + +ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } + +ensure_packages() { + for pkg in "$@"; do + if ! opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then + if [ "$OPKG_UPDATED" -eq 0 ]; then + opkg update || return 1 + OPKG_UPDATED=1 + fi + opkg install "$pkg" || return 1 + fi + done +} + +defaults() { + data_path="$(uci_get data_path || echo $DATA_PATH_DEFAULT)" + memory_limit="$(uci_get memory_limit || echo 512)" + hostname="$(uci_get hostname server || echo jabber.local)" + c2s_port="$(uci_get c2s_port server || echo 5222)" + s2s_port="$(uci_get s2s_port server || echo 5269)" + http_port="$(uci_get http_port server || echo 5280)" + https_port="$(uci_get https_port server || echo 5281)" +} + +detect_arch() { + case "$(uname -m)" in + aarch64) echo "aarch64" ;; + armv7l) echo "armv7" ;; + x86_64) echo "x86_64" ;; + *) echo "x86_64" ;; + esac +} + +generate_password() { + head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16 +} + +# ---------- LXC helpers ---------- + +lxc_running() { + lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" +} + +lxc_exists() { + [ -f "$LXC_CONF" ] && [ -d "$LXC_ROOTFS" ] +} + +lxc_exec() { + lxc-attach -n "$LXC_NAME" -- "$@" +} + +lxc_stop() { + if lxc_running; then + lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true + sleep 2 + fi +} + +# ---------- prosodyctl wrapper ---------- + +prosodyctl() { + lxc_exec prosodyctl "$@" +} + +# ---------- rootfs creation ---------- + +lxc_create_rootfs() { + local arch=$(detect_arch) + + # Map to Debian architecture names + local debian_arch + case "$arch" in + aarch64) debian_arch="arm64" ;; + armv7) debian_arch="armhf" ;; + x86_64) debian_arch="amd64" ;; + *) debian_arch="amd64" ;; + esac + + ensure_dir "$LXC_ROOTFS" + + # Minimal Debian rootfs via tarball from LXC image server + local rootfs_url="https://images.linuxcontainers.org/images/debian/bookworm/${debian_arch}/default/" + log_info "Downloading Debian bookworm rootfs for ${debian_arch}..." + + # Get latest build directory + local latest_path + latest_path=$(wget -q -O - "$rootfs_url" 2>/dev/null | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | tail -1) + if [ -z "$latest_path" ]; then + log_error "Failed to find latest Debian rootfs build" + return 1 + fi + + local tarball="/tmp/debian-jabber.tar.xz" + local tarball_url="${rootfs_url}${latest_path}/rootfs.tar.xz" + wget -q -O "$tarball" "$tarball_url" || { + log_error "Failed to download Debian rootfs from $tarball_url" + return 1 + } + + tar -xJf "$tarball" -C "$LXC_ROOTFS" || { + log_error "Failed to extract Debian rootfs" + return 1 + } + rm -f "$tarball" + + # DNS + cp /etc/resolv.conf "$LXC_ROOTFS/etc/resolv.conf" 2>/dev/null || \ + echo "nameserver 8.8.8.8" > "$LXC_ROOTFS/etc/resolv.conf" + + # Create minimal /dev for chroot operations + mkdir -p "$LXC_ROOTFS/dev" + [ -c "$LXC_ROOTFS/dev/null" ] || mknod -m 666 "$LXC_ROOTFS/dev/null" c 1 3 2>/dev/null + [ -c "$LXC_ROOTFS/dev/zero" ] || mknod -m 666 "$LXC_ROOTFS/dev/zero" c 1 5 2>/dev/null + [ -c "$LXC_ROOTFS/dev/random" ] || mknod -m 666 "$LXC_ROOTFS/dev/random" c 1 8 2>/dev/null + [ -c "$LXC_ROOTFS/dev/urandom" ] || mknod -m 666 "$LXC_ROOTFS/dev/urandom" c 1 9 2>/dev/null + + # Configure apt sources + cat > "$LXC_ROOTFS/etc/apt/sources.list" <<'SOURCES' +deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware +deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware +deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware +SOURCES + + # Install Prosody XMPP server + log_info "Installing Prosody XMPP server..." + chroot "$LXC_ROOTFS" /bin/sh -c " + export DEBIAN_FRONTEND=noninteractive + apt-get update && \ + apt-get install -y --no-install-recommends \ + prosody \ + prosody-modules \ + lua-sec \ + lua-event \ + lua-dbi-sqlite3 \ + lua-zlib \ + ca-certificates \ + openssl \ + procps + " || { + log_error "Failed to install Prosody" + return 1 + } + + # Create directories + mkdir -p "$LXC_ROOTFS/var/lib/prosody" + mkdir -p "$LXC_ROOTFS/var/log/prosody" + mkdir -p "$LXC_ROOTFS/etc/prosody/conf.d" + mkdir -p "$LXC_ROOTFS/var/lib/prosody/http_upload" + + # Create startup script + create_startup_script + + # Clean up apt cache + chroot "$LXC_ROOTFS" /bin/sh -c " + apt-get clean + rm -rf /var/lib/apt/lists/* + " + + log_info "Rootfs created successfully" +} + +create_startup_script() { + cat > "$LXC_ROOTFS/opt/start-jabber.sh" <<'STARTUP' +#!/bin/bash +set -e + +export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +# Generate Prosody config if not exists +if [ ! -f /etc/prosody/prosody.cfg.lua.configured ]; then + echo "[JABBER] Generating Prosody configuration..." + + # Get hostname from environment or default + XMPP_DOMAIN="${XMPP_HOSTNAME:-jabber.local}" + ADMIN_USER="${XMPP_ADMIN:-admin}" + + # Generate self-signed certificates + if [ ! -f /var/lib/prosody/${XMPP_DOMAIN}.crt ]; then + echo "[JABBER] Generating SSL certificates..." + prosodyctl cert generate "$XMPP_DOMAIN" + fi + + # Create main config + cat > /etc/prosody/prosody.cfg.lua < "$LXC_CONF" </dev/null || true + fi + + local lan_ip=$(uci -q get network.lan.ipaddr || echo '192.168.255.1') + + log_info "" + log_info "==============================================" + log_info " Jabber/XMPP Server installed!" + log_info "==============================================" + log_info "" + log_info " Domain: $hostname" + log_info " C2S Port: $c2s_port (client connections)" + log_info " S2S Port: $s2s_port (server federation)" + log_info " HTTP/BOSH: http://${lan_ip}:$http_port/http-bind" + log_info " WebSocket: ws://${lan_ip}:$http_port/xmpp-websocket" + log_info "" + log_info " Admin JID: ${admin_user}@${hostname}" + log_info " Password: $admin_pass" + log_info "" + log_info " Clients: Conversations (Android), Monal (iOS)," + log_info " Gajim (Desktop), Dino (Linux)" + log_info "" + log_info " Expose externally:" + log_info " jabberctl emancipate xmpp.example.com" + log_info "" +} + +cmd_uninstall() { + require_root || { log_error "Must run as root"; return 1; } + + log_info "Uninstalling Jabber/XMPP server..." + + # Stop and disable + /etc/init.d/jabber stop 2>/dev/null + /etc/init.d/jabber disable 2>/dev/null + lxc_stop + + # Remove container but keep data + rm -rf "$LXC_ROOTFS" "$LXC_CONF" + + uci_set enabled '0' + uci commit "$CONFIG" + + defaults + log_info "Container removed. Data preserved in $data_path" +} + +cmd_update() { + require_root || { log_error "Must run as root"; return 1; } + + log_info "Updating Prosody..." + + if lxc_running; then + lxc_exec apt-get update + lxc_exec apt-get upgrade -y prosody prosody-modules + lxc_exec prosodyctl restart + log_info "Prosody updated successfully" + else + log_error "Container not running" + return 1 + fi +} + +cmd_check() { + echo "Jabber/XMPP Prerequisites Check" + echo "================================" + + # LXC + if command -v lxc-start >/dev/null 2>&1; then + echo "[OK] LXC installed" + else + echo "[FAIL] LXC not installed" + fi + + # Container exists + if lxc_exists; then + echo "[OK] Container exists" + else + echo "[--] Container not created" + fi + + # Container running + if lxc_running; then + echo "[OK] Container running" + else + echo "[--] Container not running" + fi + + # Prosody ports + defaults + for port in $c2s_port $s2s_port $http_port; do + if netstat -tln 2>/dev/null | grep -q ":${port} " || \ + grep -q ":$(printf '%04X' $port) " /proc/net/tcp 2>/dev/null; then + echo "[OK] Port $port listening" + else + echo "[--] Port $port not listening" + fi + done + + # Prosody process + if lxc_running; then + if lxc_exec pgrep prosody >/dev/null 2>&1; then + echo "[OK] Prosody process running" + else + echo "[FAIL] Prosody process not running" + fi + fi +} + +cmd_status() { + defaults + + # JSON output for RPCD + if [ "$1" = "--json" ]; then + local running=0 + local prosody_proc=0 + local user_count=0 + + lxc_running && running=1 + if [ "$running" = "1" ]; then + lxc_exec pgrep prosody >/dev/null 2>&1 && prosody_proc=1 + user_count=$(lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) + fi + + cat </dev/null 2>&1 && echo " Prosody: UP" || echo " Prosody: DOWN" + + # User count + local users=$(lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | wc -l) + echo "" + echo "Users: $users registered" + else + echo "State: STOPPED" + fi + + echo "" + local lan_ip=$(uci -q get network.lan.ipaddr || echo '192.168.255.1') + echo "Connection:" + echo " XMPP: ${hostname}:${c2s_port}" + echo " BOSH: http://${lan_ip}:${http_port}/http-bind" + echo " WebSocket: ws://${lan_ip}:${http_port}/xmpp-websocket" +} + +cmd_logs() { + local lines="${1:-50}" + + if lxc_running; then + echo "=== Prosody logs ===" + lxc_exec tail -n "$lines" /var/log/prosody/prosody.log 2>/dev/null || \ + echo "No Prosody logs found" + else + echo "Container not running" + fi +} + +cmd_shell() { + if lxc_running; then + lxc_exec /bin/bash || lxc_exec /bin/sh + else + log_error "Container not running" + return 1 + fi +} + +cmd_start() { + require_root || { log_error "Must run as root"; return 1; } + /etc/init.d/jabber start +} + +cmd_stop() { + require_root || { log_error "Must run as root"; return 1; } + /etc/init.d/jabber stop +} + +cmd_restart() { + require_root || { log_error "Must run as root"; return 1; } + /etc/init.d/jabber restart +} + +# ---------- user management ---------- + +cmd_user() { + local subcmd="$1" + shift + + case "$subcmd" in + add) + cmd_user_add "$@" + ;; + del|delete) + cmd_user_del "$@" + ;; + passwd|password) + cmd_user_passwd "$@" + ;; + list) + cmd_user_list + ;; + *) + echo "Usage: jabberctl user " + return 1 + ;; + esac +} + +cmd_user_add() { + local jid="$1" + local password="$2" + + [ -z "$jid" ] && { + echo "Usage: jabberctl user add [password]" + return 1 + } + + lxc_running || { log_error "Container not running"; return 1; } + + # Parse JID + local user=$(echo "$jid" | cut -d@ -f1) + local domain=$(echo "$jid" | cut -d@ -f2) + + [ -z "$domain" ] && { + defaults + domain="$hostname" + } + + [ -z "$password" ] && password=$(generate_password) + + prosodyctl register "$user" "$domain" "$password" + + log_info "User created: ${user}@${domain}" + log_info "Password: $password" +} + +cmd_user_del() { + local jid="$1" + + [ -z "$jid" ] && { + echo "Usage: jabberctl user del " + return 1 + } + + lxc_running || { log_error "Container not running"; return 1; } + + # Parse JID + local user=$(echo "$jid" | cut -d@ -f1) + local domain=$(echo "$jid" | cut -d@ -f2) + + [ -z "$domain" ] && { + defaults + domain="$hostname" + } + + prosodyctl deluser "${user}@${domain}" + log_info "User deleted: ${user}@${domain}" +} + +cmd_user_passwd() { + local jid="$1" + local password="$2" + + [ -z "$jid" ] && { + echo "Usage: jabberctl user passwd [password]" + return 1 + } + + lxc_running || { log_error "Container not running"; return 1; } + + # Parse JID + local user=$(echo "$jid" | cut -d@ -f1) + local domain=$(echo "$jid" | cut -d@ -f2) + + [ -z "$domain" ] && { + defaults + domain="$hostname" + } + + [ -z "$password" ] && password=$(generate_password) + + # Delete and recreate user with new password + prosodyctl deluser "${user}@${domain}" 2>/dev/null + prosodyctl register "$user" "$domain" "$password" + + log_info "Password changed for: ${user}@${domain}" + log_info "New password: $password" +} + +cmd_user_list() { + lxc_running || { log_error "Container not running"; return 1; } + + defaults + echo "Users for $hostname:" + echo "====================" + + # List all account files + lxc_exec find /var/lib/prosody -name "*.dat" -path "*accounts*" 2>/dev/null | while read f; do + user=$(basename "$f" .dat) + domain=$(echo "$f" | grep -oE '[^/]+/accounts' | cut -d/ -f1 | tr '%' '.') + echo " ${user}@${domain}" + done +} + +# ---------- room management ---------- + +cmd_room() { + local subcmd="$1" + shift + + case "$subcmd" in + create) + cmd_room_create "$@" + ;; + delete) + cmd_room_delete "$@" + ;; + list) + cmd_room_list + ;; + *) + echo "Usage: jabberctl room " + return 1 + ;; + esac +} + +cmd_room_create() { + local name="$1" + + [ -z "$name" ] && { + echo "Usage: jabberctl room create " + return 1 + } + + lxc_running || { log_error "Container not running"; return 1; } + + defaults + local muc_host=$(uci_get host muc || echo conference) + + # Create room via telnet/adhoc (Prosody doesn't have CLI for this) + log_info "Room: ${name}@${muc_host}.${hostname}" + log_info "Rooms are created automatically when first user joins." + log_info "Or create via XMPP client's room creation dialog." +} + +cmd_room_delete() { + local name="$1" + + [ -z "$name" ] && { + echo "Usage: jabberctl room delete " + return 1 + } + + lxc_running || { log_error "Container not running"; return 1; } + + defaults + local muc_host=$(uci_get host muc || echo conference) + + # Remove room data directory + local room_path="/var/lib/prosody/${muc_host}%2e${hostname}/rooms/${name}.dat" + lxc_exec rm -f "$room_path" 2>/dev/null + + log_info "Room deleted: ${name}@${muc_host}.${hostname}" +} + +cmd_room_list() { + lxc_running || { log_error "Container not running"; return 1; } + + defaults + local muc_host=$(uci_get host muc || echo conference) + + echo "Rooms on ${muc_host}.${hostname}:" + echo "=================================" + + lxc_exec find /var/lib/prosody -name "*.dat" -path "*rooms*" 2>/dev/null | while read f; do + room=$(basename "$f" .dat) + echo " ${room}@${muc_host}.${hostname}" + done +} + +# ---------- HAProxy integration ---------- + +cmd_configure_haproxy() { + require_root || { log_error "Must run as root"; return 1; } + defaults + + local domain=$(uci_get domain network) + [ -z "$domain" ] && domain="$hostname" + + # Create backend for BOSH/WebSocket + local backend_name="jabber_http" + + uci set haproxy.${backend_name}=backend + uci set haproxy.${backend_name}.name="$backend_name" + uci set haproxy.${backend_name}.mode='http' + uci set haproxy.${backend_name}.balance='roundrobin' + uci set haproxy.${backend_name}.enabled='1' + uci set haproxy.${backend_name}.timeout_server='3600s' + uci set haproxy.${backend_name}.timeout_tunnel='3600s' + uci set haproxy.${backend_name}.server="jabber 127.0.0.1:${http_port} check" + + # Create vhost + local vhost_name=$(echo "$domain" | tr '.-' '_') + uci set haproxy.${vhost_name}=vhost + uci set haproxy.${vhost_name}.domain="$domain" + uci set haproxy.${vhost_name}.backend="$backend_name" + uci set haproxy.${vhost_name}.ssl='1' + uci set haproxy.${vhost_name}.ssl_redirect='1' + uci set haproxy.${vhost_name}.acme='1' + uci set haproxy.${vhost_name}.enabled='1' + + uci commit haproxy + + # Update network config + uci_set haproxy '1' network + uci_set domain "$domain" network + uci commit "$CONFIG" + + # Regenerate and reload + if command -v haproxyctl >/dev/null 2>&1; then + haproxyctl generate + /etc/init.d/haproxy reload + fi + + log_info "HAProxy configured for $domain" + log_info "BOSH: https://$domain/http-bind" + log_info "WebSocket: wss://$domain/xmpp-websocket" +} + +cmd_emancipate() { + local domain="$1" + + [ -z "$domain" ] && { + echo "Usage: jabberctl emancipate " + return 1 + } + + require_root || { log_error "Must run as root"; return 1; } + + log_info "Emancipating Jabber at $domain..." + + # Update hostname + uci_set hostname "$domain" server + uci_set domain "$domain" network + uci commit "$CONFIG" + + # Update Prosody config + if lxc_running; then + # Regenerate certs for new domain + lxc_exec prosodyctl cert generate "$domain" + + # Update config file with new domain + lxc_exec sed -i "s/XMPP_HOSTNAME=.*/XMPP_HOSTNAME=$domain/" /opt/start-jabber.sh + + # Remove old config marker to trigger regeneration + lxc_exec rm -f /etc/prosody/prosody.cfg.lua.configured + + # Restart to apply + cmd_restart + fi + + # Configure HAProxy + cmd_configure_haproxy + + # Enable S2S federation + uci_set enabled '1' s2s + uci commit "$CONFIG" + + # Open firewall ports + local wan_open=$(uci_get firewall_wan network) + if [ "$wan_open" = "1" ]; then + # C2S port + uci add firewall rule + uci set firewall.@rule[-1].name='Jabber-C2S' + uci set firewall.@rule[-1].src='wan' + uci set firewall.@rule[-1].dest_port="${c2s_port}" + uci set firewall.@rule[-1].proto='tcp' + uci set firewall.@rule[-1].target='ACCEPT' + + # S2S port + uci add firewall rule + uci set firewall.@rule[-1].name='Jabber-S2S' + uci set firewall.@rule[-1].src='wan' + uci set firewall.@rule[-1].dest_port="${s2s_port}" + uci set firewall.@rule[-1].proto='tcp' + uci set firewall.@rule[-1].target='ACCEPT' + + uci commit firewall + /etc/init.d/firewall reload + fi + + log_info "" + log_info "==============================================" + log_info " Jabber/XMPP Emancipated!" + log_info "==============================================" + log_info "" + log_info " Domain: $domain" + log_info " XMPP C2S: ${domain}:${c2s_port}" + log_info " XMPP S2S: ${domain}:${s2s_port}" + log_info " BOSH: https://$domain/http-bind" + log_info " WebSocket: wss://$domain/xmpp-websocket" + log_info "" + log_info " DNS Records needed:" + log_info " A $domain -> your-ip" + log_info " SRV _xmpp-client._tcp.$domain 5222" + log_info " SRV _xmpp-server._tcp.$domain 5269" + log_info "" +} + +# ---------- backup/restore ---------- + +cmd_backup() { + local backup_path="${1:-/srv/jabber/backup}" + + require_root || { log_error "Must run as root"; return 1; } + lxc_running || { log_error "Container must be running"; return 1; } + + ensure_dir "$backup_path" + + local timestamp=$(date +%Y%m%d_%H%M%S) + local backup_file="$backup_path/jabber_${timestamp}.tar.gz" + + log_info "Creating backup..." + + # Create tarball with data + defaults + tar -czf "$backup_file" \ + -C "$data_path" data certs + + log_info "Backup created: $backup_file" +} + +cmd_restore() { + local backup_file="$1" + + [ -z "$backup_file" ] || [ ! -f "$backup_file" ] && { + echo "Usage: jabberctl restore " + return 1 + } + + require_root || { log_error "Must run as root"; return 1; } + + log_info "Restoring from $backup_file..." + + # Stop container + lxc_stop + + # Restore data + defaults + tar -xzf "$backup_file" -C "$data_path" + + # Start container + cmd_start + + log_info "Restore complete." +} + +# ---------- service management ---------- + +cmd_service_run() { + require_root || exit 1 + defaults + + # Verify container exists + lxc_exists || { log_error "Container not found. Run: jabberctl install"; exit 1; } + + log_info "Starting Jabber/XMPP container..." + + # Start container in foreground + exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONF" +} + +cmd_service_stop() { + log_info "Stopping Jabber/XMPP container..." + lxc_stop +} + +# ---------- main ---------- + +case "$1" in + install) cmd_install ;; + uninstall) cmd_uninstall ;; + update) cmd_update ;; + check) cmd_check ;; + start) cmd_start ;; + stop) cmd_stop ;; + restart) cmd_restart ;; + status) shift; cmd_status "$@" ;; + logs) shift; cmd_logs "$@" ;; + shell) cmd_shell ;; + user) shift; cmd_user "$@" ;; + room) shift; cmd_room "$@" ;; + configure-haproxy) cmd_configure_haproxy ;; + emancipate) shift; cmd_emancipate "$@" ;; + backup) shift; cmd_backup "$@" ;; + restore) shift; cmd_restore "$@" ;; + service-run) cmd_service_run ;; + service-stop) cmd_service_stop ;; + *) usage; exit 1 ;; +esac