feat(voip): Add VoIP packages with OVH provisioning and Jabber integration

New packages:
- secubox-app-voip: Asterisk PBX in LXC container
- luci-app-voip: Dashboard with extensions, trunks, click-to-call

VoIP features:
- voipctl CLI for container, extensions, trunks, calls, voicemail
- OVH Telephony API auto-provisioning for SIP trunks
- Click-to-call web interface with quick dial
- RPCD backend with 15 methods

Jabber VoIP integration:
- Jingle VoIP support (STUN/TURN via mod_external_services)
- SMS relay via OVH (messages to sms@domain)
- Voicemail notifications via Asterisk AMI → XMPP
- 9 new RPCD methods for VoIP features

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-19 09:22:06 +01:00
parent 3c992026ed
commit 4ca46b61e2
20 changed files with 3982 additions and 4 deletions

View File

@ -1,6 +1,6 @@
# SecuBox UI & Theme History
_Last updated: 2026-02-17_
_Last updated: 2026-02-19_
1. **Unified Dashboard Refresh (2025-12-20)**
- Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs.
@ -2334,3 +2334,76 @@ git checkout HEAD -- index.html
- Fixed SSL certificate generation (openssl instead of prosodyctl)
- Added xchat.gk2.secubox.in route to mitmproxy-in haproxy-routes.json
- Fixed route IP from 127.0.0.1 to 192.168.255.1 for container accessibility
### 2026-02-19: VoIP + Jabber Integration (Asterisk PBX)
**New Packages:**
- `secubox-app-voip` - LXC-based Asterisk PBX server
- `luci-app-voip` - LuCI dashboard for VoIP management
**Features:**
- Debian 12 (Bookworm) LXC container with Asterisk PBX
- OVH Telephony API integration for SIP trunk auto-provisioning
- SIP extension management with PJSIP
- Asterisk ARI/AMI support for call control
- Click-to-call web interface
- HAProxy integration with WebRTC support
- Procd service management
**CLI Commands (voipctl):**
- `install/uninstall` - Container lifecycle
- `start/stop/restart/status` - Service control
- `ext add/del/passwd/list` - Extension management
- `trunk add ovh/manual` - SIP trunk configuration
- `trunk test/status` - Trunk connectivity testing
- `call/hangup/calls` - Call origination and control
- `vm list/play/delete` - Voicemail management
- `configure-haproxy` - WebRTC proxy setup
- `emancipate <domain>` - Public exposure
**OVH Telephony Integration (ovh-telephony.sh):**
- API signature generation (HMAC-SHA1)
- Billing accounts and SIP lines discovery
- SIP credentials retrieval and password reset
- SMS sending via OVH SMS API
- Auto-provisioning flow for trunk configuration
**LuCI Dashboard (luci-app-voip):**
- Overview with container/Asterisk/trunk status
- Extensions management (add/delete)
- Trunks configuration (OVH auto-provision, manual)
- Click-to-call dialer with extension selector
- Active calls display with live polling
- Quick dial buttons for extensions
- Logs viewer
**Jabber VoIP Integration (Phase 3):**
- Jingle VoIP support via mod_external_services
- STUN/TURN server configuration
- SMS relay via OVH (messages to sms@domain)
- Voicemail notifications via Asterisk AMI → XMPP
- New jabberctl commands: jingle enable/disable/status, sms config/send, voicemail-notify
- New RPCD methods: jingle_status/enable/disable, sms_status/config/send, voicemail_status/config
- Updated UCI config with jingle, sms, and voicemail sections
**Files Created:**
- `package/secubox/secubox-app-voip/Makefile`
- `package/secubox/secubox-app-voip/files/etc/config/voip`
- `package/secubox/secubox-app-voip/files/etc/init.d/voip`
- `package/secubox/secubox-app-voip/files/usr/sbin/voipctl`
- `package/secubox/secubox-app-voip/files/usr/lib/secubox/voip/ovh-telephony.sh`
- `package/secubox/luci-app-voip/Makefile`
- `package/secubox/luci-app-voip/root/usr/libexec/rpcd/luci.voip`
- `package/secubox/luci-app-voip/root/usr/share/luci/menu.d/luci-app-voip.json`
- `package/secubox/luci-app-voip/root/usr/share/rpcd/acl.d/luci-app-voip.json`
- `package/secubox/luci-app-voip/htdocs/.../voip/api.js`
- `package/secubox/luci-app-voip/htdocs/.../view/voip/overview.js`
- `package/secubox/luci-app-voip/htdocs/.../view/voip/extensions.js`
- `package/secubox/luci-app-voip/htdocs/.../view/voip/trunks.js`
- `package/secubox/luci-app-voip/htdocs/.../view/voip/click-to-call.js`
**Files Modified:**
- `package/secubox/secubox-app-jabber/files/usr/sbin/jabberctl` (added VoIP integration)
- `package/secubox/secubox-app-jabber/files/etc/config/jabber` (jingle/sms/voicemail sections)
- `package/secubox/luci-app-jabber/root/usr/libexec/rpcd/luci.jabber` (VoIP methods)
- `package/secubox/luci-app-jabber/root/usr/share/rpcd/acl.d/luci-app-jabber.json` (VoIP ACL)

View File

@ -1,6 +1,6 @@
# Work In Progress (Claude)
_Last updated: 2026-02-17 (v0.21.0 - Nextcloud LXC + WebRadio)_
_Last updated: 2026-02-19 (v0.22.0 - VoIP + Jabber Integration)_
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
@ -62,6 +62,22 @@ _Last updated: 2026-02-17 (v0.21.0 - Nextcloud LXC + WebRadio)_
- Gossip-based exposure config sync via secubox-p2p
- Created `luci-app-vortex-dns` dashboard
### Just Completed (2026-02-19)
- **VoIP (Asterisk PBX) + Jabber Integration** — DONE (2026-02-19)
- Created `secubox-app-voip` package with Asterisk PBX in LXC container
- OVH Telephony API integration for SIP trunk auto-provisioning
- `voipctl` CLI: install/uninstall, ext add/del, trunk add ovh, call, vm list
- Created `luci-app-voip` with 4 views: Overview, Extensions, Trunks, Click-to-Call
- RPCD backend with 15 methods for VoIP management
- Jabber VoIP integration:
- Jingle VoIP support (STUN/TURN via mod_external_services)
- SMS relay via OVH (messages to sms@domain)
- Voicemail notifications via Asterisk AMI → XMPP
- Updated jabberctl with `jingle enable/disable`, `sms config/send`, `voicemail-notify`
- Updated luci.jabber RPCD with 9 new VoIP methods
- UCI config sections: jingle, sms, voicemail
### Just Completed (2026-02-17)
- **PeerTube yt-dlp Video Import** — DONE (2026-02-17)

View File

@ -420,6 +420,188 @@ method_room_list() {
json_dump
}
# ---------- VoIP Integration Methods ----------
# Method: jingle_status
method_jingle_status() {
local enabled stun_server turn_server
enabled=$(uci_get jingle enabled 0)
stun_server=$(uci_get jingle stun_server "stun.l.google.com:19302")
turn_server=$(uci_get jingle turn_server "")
json_init
json_add_string "enabled" "$enabled"
json_add_string "stun_server" "$stun_server"
json_add_string "turn_server" "$turn_server"
json_dump
}
# Method: jingle_enable
method_jingle_enable() {
read -r input
json_load "$input"
json_get_var stun_server stun_server
local output
if [ -n "$stun_server" ]; then
output=$($JABBERCTL jingle enable "$stun_server" 2>&1)
else
output=$($JABBERCTL jingle enable 2>&1)
fi
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Jingle VoIP enabled"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: jingle_disable
method_jingle_disable() {
local output
output=$($JABBERCTL jingle disable 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Jingle VoIP disabled"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: sms_status
method_sms_status() {
local enabled sender provider
enabled=$(uci_get sms enabled 0)
sender=$(uci_get sms sender "SecuBox")
provider=$(uci_get sms provider "ovh")
# Check OVH API configured
local ovh_configured="0"
local ovh_key=$(uci -q get voip.ovh_telephony.app_key 2>/dev/null)
[ -n "$ovh_key" ] && ovh_configured="1"
json_init
json_add_string "enabled" "$enabled"
json_add_string "sender" "$sender"
json_add_string "provider" "$provider"
json_add_string "ovh_configured" "$ovh_configured"
json_dump
}
# Method: sms_config
method_sms_config() {
read -r input
json_load "$input"
json_get_var sender sender
local output
output=$($JABBERCTL sms config "$sender" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "SMS relay configured"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: sms_send
method_sms_send() {
read -r input
json_load "$input"
json_get_var to to
json_get_var message message
if [ -z "$to" ] || [ -z "$message" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Phone number and message are required"
json_dump
return
fi
local output
output=$($JABBERCTL sms send "$to" "$message" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "SMS sent to $to"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: voicemail_status
method_voicemail_status() {
local enabled ami_host ami_port notify_jid
enabled=$(uci_get voicemail enabled 0)
ami_host=$(uci_get voicemail ami_host "127.0.0.1")
ami_port=$(uci_get voicemail ami_port "5038")
notify_jid=$(uci_get voicemail notify_jid "")
json_init
json_add_string "enabled" "$enabled"
json_add_string "ami_host" "$ami_host"
json_add_string "ami_port" "$ami_port"
json_add_string "notify_jid" "$notify_jid"
json_dump
}
# Method: voicemail_config
method_voicemail_config() {
read -r input
json_load "$input"
json_get_var notify_jid notify_jid
if [ -z "$notify_jid" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Notification JID is required"
json_dump
return
fi
# Save to UCI first
uci set jabber.voicemail=voicemail_notify
uci set jabber.voicemail.notify_jid="$notify_jid"
uci commit jabber
local output
output=$($JABBERCTL voicemail-notify 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Voicemail notifications configured"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# List available methods
list_methods() {
json_init
@ -464,6 +646,27 @@ list_methods() {
json_close_object
json_add_object "room_list"
json_close_object
json_add_object "jingle_status"
json_close_object
json_add_object "jingle_enable"
json_add_string "stun_server" ""
json_close_object
json_add_object "jingle_disable"
json_close_object
json_add_object "sms_status"
json_close_object
json_add_object "sms_config"
json_add_string "sender" ""
json_close_object
json_add_object "sms_send"
json_add_string "to" ""
json_add_string "message" ""
json_close_object
json_add_object "voicemail_status"
json_close_object
json_add_object "voicemail_config"
json_add_string "notify_jid" ""
json_close_object
json_dump
}
@ -525,6 +728,30 @@ case "$1" in
room_list)
method_room_list
;;
jingle_status)
method_jingle_status
;;
jingle_enable)
method_jingle_enable
;;
jingle_disable)
method_jingle_disable
;;
sms_status)
method_sms_status
;;
sms_config)
method_sms_config
;;
sms_send)
method_sms_send
;;
voicemail_status)
method_voicemail_status
;;
voicemail_config)
method_voicemail_config
;;
*)
echo '{"error":"Method not found"}'
;;

View File

@ -3,13 +3,39 @@
"description": "Grant access to Jabber/XMPP management",
"read": {
"ubus": {
"luci.jabber": ["status", "logs", "user_list", "room_list"]
"luci.jabber": [
"status",
"logs",
"user_list",
"room_list",
"jingle_status",
"sms_status",
"voicemail_status"
]
},
"uci": ["jabber"]
},
"write": {
"ubus": {
"luci.jabber": ["start", "stop", "install", "uninstall", "update", "emancipate", "configure_haproxy", "user_add", "user_del", "user_passwd", "room_create", "room_delete"]
"luci.jabber": [
"start",
"stop",
"install",
"uninstall",
"update",
"emancipate",
"configure_haproxy",
"user_add",
"user_del",
"user_passwd",
"room_create",
"room_delete",
"jingle_enable",
"jingle_disable",
"sms_config",
"sms_send",
"voicemail_config"
]
},
"uci": ["jabber"]
}

View File

@ -0,0 +1,33 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-voip
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=SecuBox Team
PKG_LICENSE:=GPL-3.0
LUCI_TITLE:=LuCI VoIP (Asterisk PBX) Management
LUCI_DEPENDS:=+secubox-app-voip +luci-base
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
define Package/luci-app-voip/install
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.voip $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-voip.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-voip.json $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/voip
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/voip/*.js $(1)/www/luci-static/resources/view/voip/
$(INSTALL_DIR) $(1)/www/luci-static/resources/voip
$(INSTALL_DATA) ./htdocs/luci-static/resources/voip/*.js $(1)/www/luci-static/resources/voip/
endef
$(eval $(call BuildPackage,luci-app-voip))

View File

@ -0,0 +1,181 @@
'use strict';
'require view';
'require ui';
'require poll';
'require voip.api as api';
return view.extend({
load: function() {
return Promise.all([
api.getStatus(),
api.listExtensions(),
api.listCalls()
]);
},
parseExtensions: function(extString) {
if (!extString) return [];
return extString.split(',').filter(function(e) { return e; }).map(function(entry) {
var parts = entry.split(':');
return {
ext: parts[0],
name: parts[1] || parts[0]
};
});
},
render: function(data) {
var self = this;
var status = data[0];
var extData = data[1];
var callsData = data[2];
var running = status.running === 'true';
var trunkRegistered = status.trunk_registered === 'true';
if (!running) {
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Click to Call'),
E('p', { 'class': 'alert-message warning' },
'VoIP service is not running. Start it from the Overview page.')
]);
}
var extensions = this.parseExtensions(extData.extensions);
if (extensions.length === 0) {
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Click to Call'),
E('p', { 'class': 'alert-message warning' },
'No extensions configured. Add extensions first.')
]);
}
// Extension selector
var extOptions = extensions.map(function(ext) {
return E('option', { 'value': ext.ext }, ext.ext + ' - ' + ext.name);
});
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Click to Call'),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Make a Call'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Your Extension'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'from-ext', 'class': 'cbi-input-select' }, extOptions)
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Number to Call'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'tel',
'id': 'to-number',
'class': 'cbi-input-text',
'placeholder': '+33612345678 or extension number',
'style': 'width: 300px;'
})
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'style': 'font-size: 16px; padding: 10px 30px;',
'disabled': !trunkRegistered,
'click': ui.createHandlerFn(this, function() {
return this.handleCall();
})
}, 'Call'),
!trunkRegistered ? E('span', {
'style': 'margin-left: 10px; color: #c00;'
}, '(SIP trunk not registered)') : ''
])
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Active Calls'),
E('div', { 'id': 'active-calls' }, this.renderActiveCalls(callsData.calls))
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Quick Dial'),
E('p', {}, 'Click an extension to call directly:'),
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px;' },
extensions.map(function(ext) {
return E('button', {
'class': 'btn cbi-button',
'data-ext': ext.ext,
'click': function() {
document.getElementById('to-number').value = ext.ext;
}
}, ext.ext + ' (' + ext.name + ')');
})
)
])
]);
// Poll for active calls
poll.add(L.bind(function() {
return api.listCalls().then(L.bind(function(res) {
var container = document.getElementById('active-calls');
if (container) {
container.innerHTML = '';
container.appendChild(this.renderActiveCalls(res.calls));
}
}, this));
}, this), 3);
return view;
},
renderActiveCalls: function(callsStr) {
if (!callsStr || callsStr.trim() === '') {
return E('p', { 'class': 'alert-message' }, 'No active calls');
}
var lines = callsStr.split('\n').filter(function(l) { return l.trim(); });
if (lines.length === 0) {
return E('p', { 'class': 'alert-message' }, 'No active calls');
}
return E('pre', {
'style': 'background: #f5f5f5; padding: 10px; font-size: 12px; overflow-x: auto;'
}, callsStr);
},
handleCall: function() {
var fromExt = document.getElementById('from-ext').value;
var toNumber = document.getElementById('to-number').value.trim();
if (!toNumber) {
ui.addNotification(null, E('p', 'Enter a number to call'), 'error');
return Promise.resolve();
}
ui.showModal('Calling...', [
E('p', { 'class': 'spinning' }, 'Connecting ' + fromExt + ' to ' + toNumber + '...')
]);
return api.originateCall(fromExt, toNumber).then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', 'Call initiated. Your phone will ring first.'), 'success');
} else {
ui.addNotification(null, E('p', 'Call failed: ' + (res.error || 'Unknown error')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', 'Call failed: ' + e.message), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,184 @@
'use strict';
'require view';
'require ui';
'require dom';
'require voip.api as api';
return view.extend({
load: function() {
return Promise.all([
api.getStatus(),
api.listExtensions()
]);
},
parseExtensions: function(extString) {
if (!extString) return [];
return extString.split(',').filter(function(e) { return e; }).map(function(entry) {
var parts = entry.split(':');
return {
ext: parts[0],
name: parts[1] || ''
};
});
},
renderExtensionTable: function(extensions) {
var self = this;
if (!extensions || extensions.length === 0) {
return E('p', { 'class': 'alert-message' }, 'No extensions configured');
}
var rows = extensions.map(function(ext) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, ext.ext),
E('td', { 'class': 'td' }, ext.name),
E('td', { 'class': 'td' }, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': ui.createHandlerFn(self, function() {
return self.handleDelete(ext.ext);
})
}, 'Delete')
])
]);
});
return E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, 'Extension'),
E('th', { 'class': 'th' }, 'Name'),
E('th', { 'class': 'th' }, 'Actions')
])
].concat(rows));
},
renderAddForm: function() {
var self = this;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Add Extension'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Extension Number'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-ext',
'placeholder': '100',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Display Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-name',
'placeholder': 'John Doe',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Password'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'password',
'id': 'new-password',
'placeholder': '(auto-generated if empty)',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, function() {
return this.handleAdd();
})
}, 'Add Extension')
])
]);
},
handleAdd: function() {
var ext = document.getElementById('new-ext').value.trim();
var name = document.getElementById('new-name').value.trim();
var password = document.getElementById('new-password').value;
if (!ext) {
ui.addNotification(null, E('p', 'Extension number is required'), 'error');
return Promise.resolve();
}
if (!name) {
ui.addNotification(null, E('p', 'Display name is required'), 'error');
return Promise.resolve();
}
if (!/^\d+$/.test(ext)) {
ui.addNotification(null, E('p', 'Extension must be numeric'), 'error');
return Promise.resolve();
}
return api.addExtension(ext, name, password).then(function(res) {
if (res.success) {
var msg = 'Extension ' + ext + ' created';
if (res.password) {
msg += '. Password: ' + res.password;
}
ui.addNotification(null, E('p', msg), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleDelete: function(ext) {
if (!confirm('Delete extension ' + ext + '?')) {
return Promise.resolve();
}
return api.deleteExtension(ext).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', 'Extension deleted'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
render: function(data) {
var status = data[0];
var extData = data[1];
var running = status.running === 'true';
if (!running) {
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Extensions'),
E('p', { 'class': 'alert-message warning' },
'VoIP service is not running. Start it from the Overview page.')
]);
}
var extensions = this.parseExtensions(extData.extensions);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Extensions'),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Configured Extensions'),
E('div', { 'id': 'ext-table' }, this.renderExtensionTable(extensions))
]),
this.renderAddForm()
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,227 @@
'use strict';
'require view';
'require ui';
'require poll';
'require dom';
'require voip.api as api';
return view.extend({
load: function() {
return api.getStatus();
},
renderStatusTable: function(status) {
var containerState = status.container_state || 'not_installed';
var running = status.running === 'true';
var asteriskUp = status.asterisk === 'true';
var trunkRegistered = status.trunk_registered === 'true';
var activeCalls = parseInt(status.active_calls) || 0;
var extensions = parseInt(status.extensions) || 0;
var stateClass = running ? 'success' : (containerState === 'installed' ? 'warning' : 'danger');
var stateText = running ? 'Running' : (containerState === 'installed' ? 'Stopped' : 'Not Installed');
return E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'Container Status'),
E('td', { 'class': 'td' }, [
E('span', { 'class': 'badge ' + stateClass }, stateText)
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'Asterisk PBX'),
E('td', { 'class': 'td' }, [
E('span', { 'class': 'badge ' + (asteriskUp ? 'success' : 'danger') },
asteriskUp ? 'Running' : 'Stopped')
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'SIP Trunk'),
E('td', { 'class': 'td' }, [
E('span', { 'class': 'badge ' + (trunkRegistered ? 'success' : 'warning') },
trunkRegistered ? 'Registered' : 'Not Registered')
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'Active Calls'),
E('td', { 'class': 'td' }, String(activeCalls))
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'Extensions'),
E('td', { 'class': 'td' }, String(extensions))
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'SIP Port'),
E('td', { 'class': 'td' }, status.sip_port || '5060')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'Domain'),
E('td', { 'class': 'td' }, status.domain || '(not configured)')
])
]);
},
renderActions: function(status) {
var self = this;
var containerState = status.container_state || 'not_installed';
var running = status.running === 'true';
var buttons = [];
if (containerState === 'not_installed') {
buttons.push(E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, function() {
return this.handleInstall();
})
}, 'Install VoIP'));
} else {
if (running) {
buttons.push(E('button', {
'class': 'btn cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, function() {
return this.handleStop();
})
}, 'Stop'));
} else {
buttons.push(E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, function() {
return this.handleStart();
})
}, 'Start'));
}
buttons.push(' ');
buttons.push(E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': ui.createHandlerFn(this, function() {
return this.handleUninstall();
})
}, 'Uninstall'));
}
return E('div', { 'class': 'cbi-page-actions' }, buttons);
},
handleInstall: function() {
var self = this;
ui.showModal('Installing VoIP', [
E('p', { 'class': 'spinning' }, 'Installing Asterisk PBX in LXC container... This may take several minutes.')
]);
return api.install().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', res.message || 'VoIP installed successfully'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Installation failed: ' + (res.error || 'Unknown error')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', 'Installation failed: ' + e.message), 'error');
});
},
handleStart: function() {
var self = this;
return api.start().then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', 'VoIP started'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Start failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleStop: function() {
return api.stop().then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', 'VoIP stopped'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Stop failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleUninstall: function() {
if (!confirm('Are you sure you want to uninstall VoIP? All data will be lost.')) {
return Promise.resolve();
}
ui.showModal('Uninstalling VoIP', [
E('p', { 'class': 'spinning' }, 'Removing VoIP container...')
]);
return api.uninstall().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', 'VoIP uninstalled'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Uninstall failed: ' + (res.error || 'Unknown error')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', 'Uninstall failed: ' + e.message), 'error');
});
},
renderLogs: function() {
var logsContainer = E('pre', {
'id': 'voip-logs',
'style': 'max-height: 300px; overflow-y: auto; background: #1a1a1a; color: #0f0; padding: 10px; font-family: monospace; font-size: 12px;'
}, 'Loading logs...');
api.getLogs(50).then(function(res) {
logsContainer.textContent = res.logs || 'No logs available';
});
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Recent Logs'),
logsContainer,
E('button', {
'class': 'btn cbi-button',
'click': function() {
api.getLogs(100).then(function(res) {
document.getElementById('voip-logs').textContent = res.logs || 'No logs available';
});
}
}, 'Refresh Logs')
]);
},
render: function(status) {
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'VoIP Overview'),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Status'),
this.renderStatusTable(status)
]),
this.renderActions(status),
this.renderLogs()
]);
poll.add(L.bind(function() {
return api.getStatus().then(L.bind(function(newStatus) {
var table = this.renderStatusTable(newStatus);
var oldTable = view.querySelector('.cbi-section table');
if (oldTable) {
dom.content(oldTable.parentNode, table);
}
}, this));
}, this), 5);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,150 @@
'use strict';
'require view';
'require ui';
'require uci';
'require form';
'require voip.api as api';
return view.extend({
load: function() {
return Promise.all([
api.getStatus(),
api.getTrunkStatus(),
uci.load('voip')
]);
},
render: function(data) {
var status = data[0];
var trunkStatus = data[1];
var running = status.running === 'true';
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'SIP Trunks')
]);
// Trunk status section
var registered = trunkStatus.registered === 'true';
view.appendChild(E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Trunk Status'),
E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'Registration'),
E('td', { 'class': 'td' }, [
E('span', { 'class': 'badge ' + (registered ? 'success' : 'warning') },
registered ? 'Registered' : 'Not Registered')
])
])
]),
trunkStatus.status ? E('pre', {
'style': 'background: #f5f5f5; padding: 10px; font-size: 12px; overflow-x: auto;'
}, trunkStatus.status) : ''
]));
// OVH Auto-provision section
var ovhEnabled = uci.get('voip', 'ovh_telephony', 'enabled') === '1';
var ovhAppKey = uci.get('voip', 'ovh_telephony', 'app_key') || '';
view.appendChild(E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'OVH Telephony'),
E('p', {}, 'Auto-provision SIP trunk from OVH Telephony API'),
E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, 'API Credentials'),
E('td', { 'class': 'td' }, ovhAppKey ? 'Configured' : 'Not configured')
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'disabled': !ovhAppKey || !running,
'click': ui.createHandlerFn(this, function() {
return this.handleOvhProvision();
})
}, 'Auto-Provision OVH Trunk')
])
]));
// UCI configuration form
var m, s, o;
m = new form.Map('voip', null, null);
s = m.section(form.NamedSection, 'ovh_telephony', 'ovh', 'OVH API Settings');
s.anonymous = true;
o = s.option(form.Flag, 'enabled', 'Enable OVH Integration');
o.default = '0';
o = s.option(form.ListValue, 'endpoint', 'API Endpoint');
o.value('ovh-eu', 'OVH Europe');
o.value('ovh-ca', 'OVH Canada');
o.value('ovh-us', 'OVH US');
o.default = 'ovh-eu';
o = s.option(form.Value, 'app_key', 'Application Key');
o.password = false;
o.rmempty = true;
o = s.option(form.Value, 'app_secret', 'Application Secret');
o.password = true;
o.rmempty = true;
o = s.option(form.Value, 'consumer_key', 'Consumer Key');
o.password = true;
o.rmempty = true;
o = s.option(form.Value, 'billing_account', 'Billing Account');
o.rmempty = true;
o.placeholder = '(auto-detected)';
o = s.option(form.Value, 'service_name', 'Service Name (SIP Line)');
o.rmempty = true;
o.placeholder = '(auto-detected)';
// Manual trunk section
s = m.section(form.NamedSection, 'sip_trunk', 'trunk', 'Manual Trunk Configuration');
s.anonymous = true;
o = s.option(form.Flag, 'enabled', 'Enable Trunk');
o.default = '0';
o = s.option(form.Value, 'host', 'SIP Server');
o.placeholder = 'sip.provider.com';
o = s.option(form.Value, 'username', 'Username');
o = s.option(form.Value, 'password', 'Password');
o.password = true;
o = s.option(form.Value, 'outbound_proxy', 'Outbound Proxy');
o.rmempty = true;
o = s.option(form.Value, 'codecs', 'Codecs');
o.default = 'ulaw,alaw,g729';
return m.render().then(function(formEl) {
view.appendChild(formEl);
return view;
});
},
handleOvhProvision: function() {
ui.showModal('Provisioning OVH Trunk', [
E('p', { 'class': 'spinning' }, 'Connecting to OVH API and configuring SIP trunk...')
]);
return api.addOvhTrunk().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', res.message || 'OVH trunk provisioned'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
}).catch(function(e) {
ui.hideModal();
ui.addNotification(null, E('p', 'Provisioning failed: ' + e.message), 'error');
});
}
});

View File

@ -0,0 +1,159 @@
'use strict';
'require rpc';
var callStatus = rpc.declare({
object: 'luci.voip',
method: 'status',
expect: {}
});
var callStart = rpc.declare({
object: 'luci.voip',
method: 'start',
expect: {}
});
var callStop = rpc.declare({
object: 'luci.voip',
method: 'stop',
expect: {}
});
var callInstall = rpc.declare({
object: 'luci.voip',
method: 'install',
expect: {}
});
var callUninstall = rpc.declare({
object: 'luci.voip',
method: 'uninstall',
expect: {}
});
var callLogs = rpc.declare({
object: 'luci.voip',
method: 'logs',
params: ['lines'],
expect: {}
});
var callExtAdd = rpc.declare({
object: 'luci.voip',
method: 'ext_add',
params: ['ext', 'name', 'password'],
expect: {}
});
var callExtDel = rpc.declare({
object: 'luci.voip',
method: 'ext_del',
params: ['ext'],
expect: {}
});
var callExtList = rpc.declare({
object: 'luci.voip',
method: 'ext_list',
expect: {}
});
var callOriginate = rpc.declare({
object: 'luci.voip',
method: 'call_originate',
params: ['from_ext', 'to_number'],
expect: {}
});
var callCallsList = rpc.declare({
object: 'luci.voip',
method: 'calls_list',
expect: {}
});
var callTrunkStatus = rpc.declare({
object: 'luci.voip',
method: 'trunk_status',
expect: {}
});
var callTrunkAddOvh = rpc.declare({
object: 'luci.voip',
method: 'trunk_add_ovh',
expect: {}
});
var callConfigureHaproxy = rpc.declare({
object: 'luci.voip',
method: 'configure_haproxy',
expect: {}
});
var callEmancipate = rpc.declare({
object: 'luci.voip',
method: 'emancipate',
params: ['domain'],
expect: {}
});
return {
getStatus: function() {
return callStatus();
},
start: function() {
return callStart();
},
stop: function() {
return callStop();
},
install: function() {
return callInstall();
},
uninstall: function() {
return callUninstall();
},
getLogs: function(lines) {
return callLogs(lines || 50);
},
addExtension: function(ext, name, password) {
return callExtAdd(ext, name, password || '');
},
deleteExtension: function(ext) {
return callExtDel(ext);
},
listExtensions: function() {
return callExtList();
},
originateCall: function(fromExt, toNumber) {
return callOriginate(fromExt, toNumber);
},
listCalls: function() {
return callCallsList();
},
getTrunkStatus: function() {
return callTrunkStatus();
},
addOvhTrunk: function() {
return callTrunkAddOvh();
},
configureHaproxy: function() {
return callConfigureHaproxy();
},
emancipate: function(domain) {
return callEmancipate(domain);
}
};

View File

@ -0,0 +1,477 @@
#!/bin/sh
# RPCD backend for VoIP LuCI app
. /usr/share/libubox/jshn.sh
VOIPCTL="/usr/sbin/voipctl"
# Helper to get UCI value
uci_get() {
local section="$1"
local option="$2"
local default="$3"
local val
val=$(uci -q get "voip.${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/voip" ]; then
state="installed"
lxc_info=$(lxc-info -n voip 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 sip_port
local container_state running
local info asterisk_up trunk_registered active_calls extensions
enabled=$(uci_get main enabled 0)
sip_port=$(uci_get asterisk sip_port 5060)
info=$(get_container_status)
container_state=$(echo "$info" | awk '{print $1}')
running=$(echo "$info" | awk '{print $2}')
asterisk_up="false"
trunk_registered="false"
active_calls=0
extensions=0
if [ "$running" = "true" ]; then
lxc-attach -n voip -- pgrep asterisk >/dev/null 2>&1 && asterisk_up="true"
lxc-attach -n voip -- asterisk -rx "pjsip show registrations" 2>/dev/null | grep -q "Registered" && trunk_registered="true"
active_calls=$(lxc-attach -n voip -- asterisk -rx "core show channels" 2>/dev/null | grep -oE "^[0-9]+ active" | cut -d' ' -f1 || echo 0)
extensions=$(lxc-attach -n voip -- asterisk -rx "pjsip show endpoints" 2>/dev/null | grep -c "^[0-9]" || echo 0)
fi
local domain haproxy_enabled
domain=$(uci_get ssl domain "")
haproxy_enabled=$(uci_get ssl enabled "0")
json_init
json_add_string "enabled" "$enabled"
json_add_string "container_state" "$container_state"
json_add_string "running" "$running"
json_add_string "asterisk" "$asterisk_up"
json_add_string "trunk_registered" "$trunk_registered"
json_add_int "active_calls" "${active_calls:-0}"
json_add_int "extensions" "${extensions:-0}"
json_add_string "sip_port" "$sip_port"
json_add_string "domain" "$domain"
json_add_string "haproxy" "$haproxy_enabled"
json_dump
}
# Method: start
method_start() {
local output
output=$($VOIPCTL start 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "VoIP started successfully"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: stop
method_stop() {
local output
output=$($VOIPCTL stop 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "VoIP stopped successfully"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: install
method_install() {
local output
output=$($VOIPCTL install 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "VoIP installed successfully"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: uninstall
method_uninstall() {
local output
output=$($VOIPCTL uninstall 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "VoIP uninstalled 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/voip" ]; then
output=$($VOIPCTL logs "$lines" 2>&1 | tail -n "$lines")
else
output="Container not installed"
fi
json_init
json_add_string "logs" "$output"
json_dump
}
# Method: ext_add
method_ext_add() {
read -r input
json_load "$input"
json_get_var ext ext
json_get_var name name
json_get_var password password
if [ -z "$ext" ] || [ -z "$name" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Extension and name are required"
json_dump
return
fi
local output
if [ -n "$password" ]; then
output=$($VOIPCTL ext add "$ext" "$name" "$password" 2>&1)
else
output=$($VOIPCTL ext add "$ext" "$name" 2>&1)
fi
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Extension $ext created"
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: ext_del
method_ext_del() {
read -r input
json_load "$input"
json_get_var ext ext
if [ -z "$ext" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Extension is required"
json_dump
return
fi
local output
output=$($VOIPCTL ext del "$ext" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Extension $ext deleted"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: ext_list
method_ext_list() {
local extensions=""
if lxc-info -n voip 2>/dev/null | grep -q "RUNNING"; then
# Get extensions from UCI
extensions=$(uci show voip 2>/dev/null | grep "^voip\.ext_" | grep "\.name=" | while read line; do
ext=$(echo "$line" | sed "s/voip\.ext_//" | cut -d. -f1)
name=$(echo "$line" | cut -d= -f2 | tr -d "'")
echo "${ext}:${name}"
done | paste -sd,)
fi
json_init
json_add_string "extensions" "$extensions"
json_dump
}
# Method: call_originate
method_call_originate() {
read -r input
json_load "$input"
json_get_var from_ext from_ext
json_get_var to_number to_number
if [ -z "$from_ext" ] || [ -z "$to_number" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "From extension and to number are required"
json_dump
return
fi
local output
output=$($VOIPCTL call "$from_ext" "$to_number" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Call initiated"
json_add_string "channel" "$output"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: calls_list
method_calls_list() {
local calls=""
if lxc-info -n voip 2>/dev/null | grep -q "RUNNING"; then
calls=$(lxc-attach -n voip -- asterisk -rx "core show channels concise" 2>/dev/null | head -10)
fi
json_init
json_add_string "calls" "$calls"
json_dump
}
# Method: trunk_status
method_trunk_status() {
local registered="false"
local status=""
if lxc-info -n voip 2>/dev/null | grep -q "RUNNING"; then
status=$(lxc-attach -n voip -- asterisk -rx "pjsip show registrations" 2>/dev/null)
echo "$status" | grep -q "Registered" && registered="true"
fi
json_init
json_add_string "registered" "$registered"
json_add_string "status" "$status"
json_dump
}
# Method: trunk_add_ovh
method_trunk_add_ovh() {
local output
output=$($VOIPCTL trunk add ovh 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "OVH trunk configured"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
json_dump
}
# Method: configure_haproxy
method_configure_haproxy() {
local output
output=$($VOIPCTL 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 VoIP"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
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=$($VOIPCTL emancipate "$domain" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "VoIP emancipated to $domain"
else
json_add_boolean "success" 0
json_add_string "error" "$output"
fi
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 "logs"
json_add_int "lines" 50
json_close_object
json_add_object "ext_add"
json_add_string "ext" ""
json_add_string "name" ""
json_add_string "password" ""
json_close_object
json_add_object "ext_del"
json_add_string "ext" ""
json_close_object
json_add_object "ext_list"
json_close_object
json_add_object "call_originate"
json_add_string "from_ext" ""
json_add_string "to_number" ""
json_close_object
json_add_object "calls_list"
json_close_object
json_add_object "trunk_status"
json_close_object
json_add_object "trunk_add_ovh"
json_close_object
json_add_object "configure_haproxy"
json_close_object
json_add_object "emancipate"
json_add_string "domain" ""
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
;;
logs)
read -r input
json_load "$input"
json_get_var lines lines
method_logs "${lines:-50}"
;;
ext_add)
method_ext_add
;;
ext_del)
method_ext_del
;;
ext_list)
method_ext_list
;;
call_originate)
method_call_originate
;;
calls_list)
method_calls_list
;;
trunk_status)
method_trunk_status
;;
trunk_add_ovh)
method_trunk_add_ovh
;;
configure_haproxy)
method_configure_haproxy
;;
emancipate)
method_emancipate
;;
*)
echo '{"error":"Method not found"}'
;;
esac
;;
*)
echo '{"error":"Invalid action"}'
;;
esac

View File

@ -0,0 +1,45 @@
{
"admin/services/voip": {
"title": "VoIP (Asterisk)",
"order": 50,
"action": {
"type": "firstchild"
},
"depends": {
"acl": ["luci-app-voip"],
"uci": { "voip": true }
}
},
"admin/services/voip/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "voip/overview"
}
},
"admin/services/voip/extensions": {
"title": "Extensions",
"order": 20,
"action": {
"type": "view",
"path": "voip/extensions"
}
},
"admin/services/voip/trunks": {
"title": "Trunks",
"order": 30,
"action": {
"type": "view",
"path": "voip/trunks"
}
},
"admin/services/voip/click-to-call": {
"title": "Click to Call",
"order": 40,
"action": {
"type": "view",
"path": "voip/click-to-call"
}
}
}

View File

@ -0,0 +1,34 @@
{
"luci-app-voip": {
"description": "Grant access to VoIP (Asterisk) management",
"read": {
"ubus": {
"luci.voip": [
"status",
"logs",
"ext_list",
"calls_list",
"trunk_status"
]
},
"uci": ["voip"]
},
"write": {
"ubus": {
"luci.voip": [
"start",
"stop",
"install",
"uninstall",
"ext_add",
"ext_del",
"call_originate",
"trunk_add_ovh",
"configure_haproxy",
"emancipate"
]
},
"uci": ["voip"]
}
}
}

View File

@ -34,3 +34,21 @@ config jabber 'network'
config jabber 's2s'
option enabled '0'
option require_encryption '1'
config jingle 'jingle'
option enabled '0'
option stun_server 'stun.l.google.com:19302'
option turn_server ''
option turn_user ''
option turn_password ''
config sms_relay 'sms'
option enabled '0'
option provider 'ovh'
option sender 'SecuBox'
config voicemail_notify 'voicemail'
option enabled '0'
option ami_host '127.0.0.1'
option ami_port '5038'
option notify_jid ''

View File

@ -43,6 +43,14 @@ Exposure:
configure-haproxy Setup HAProxy vhost for HTTPS/WSS
emancipate <domain> Full exposure (HAProxy + ACME + DNS + S2S)
VoIP Integration:
jingle enable Enable Jingle VoIP (XMPP calls)
jingle disable Disable Jingle VoIP
jingle status Show Jingle configuration
sms config <sender> Configure OVH SMS relay
sms send <to> <msg> Send SMS via OVH
voicemail-notify Configure Asterisk voicemail notifications
Backup:
backup [path] Backup database and config
restore <path> Restore from backup
@ -1158,6 +1166,412 @@ cmd_restore() {
log_info "Restore complete."
}
# ---------- VoIP Integration (Jingle, SMS, Voicemail) ----------
cmd_jingle() {
local subcmd="$1"
shift
case "$subcmd" in
enable)
cmd_jingle_enable "$@"
;;
disable)
cmd_jingle_disable
;;
status)
cmd_jingle_status
;;
*)
echo "Usage: jabberctl jingle <enable|disable|status>"
return 1
;;
esac
}
cmd_jingle_enable() {
require_root || { log_error "Must run as root"; return 1; }
lxc_running || { log_error "Container not running"; return 1; }
defaults
local stun_server="${1:-stun.l.google.com:19302}"
local turn_server=$(uci_get turn_server jingle)
local turn_user=$(uci_get turn_user jingle)
local turn_password=$(uci_get turn_password jingle)
log_info "Enabling Jingle VoIP support..."
# Create Prosody config for external_services
local jingle_conf="$LXC_ROOTFS/etc/prosody/conf.d/jingle.cfg.lua"
cat > "$jingle_conf" <<JINGLE
-- Jingle VoIP Configuration
-- Generated by jabberctl
modules_enabled = {
"external_services";
}
external_services = {
JINGLE
# Add STUN server
if [ -n "$stun_server" ]; then
local stun_host=$(echo "$stun_server" | cut -d: -f1)
local stun_port=$(echo "$stun_server" | cut -d: -f2)
[ -z "$stun_port" ] && stun_port="3478"
cat >> "$jingle_conf" <<STUN
{
type = "stun",
host = "$stun_host",
port = $stun_port
},
STUN
fi
# Add TURN server if configured
if [ -n "$turn_server" ]; then
local turn_host=$(echo "$turn_server" | cut -d: -f1)
local turn_port=$(echo "$turn_server" | cut -d: -f2)
[ -z "$turn_port" ] && turn_port="3478"
cat >> "$jingle_conf" <<TURN
{
type = "turn",
host = "$turn_host",
port = $turn_port,
transport = "udp",
username = "$turn_user",
secret = "$turn_password"
},
TURN
fi
echo "}" >> "$jingle_conf"
# Save config
uci set ${CONFIG}.jingle=jingle
uci set ${CONFIG}.jingle.enabled='1'
uci set ${CONFIG}.jingle.stun_server="$stun_server"
[ -n "$turn_server" ] && uci set ${CONFIG}.jingle.turn_server="$turn_server"
[ -n "$turn_user" ] && uci set ${CONFIG}.jingle.turn_user="$turn_user"
[ -n "$turn_password" ] && uci set ${CONFIG}.jingle.turn_password="$turn_password"
uci commit "$CONFIG"
# Reload Prosody
lxc_exec prosodyctl reload
log_info "Jingle VoIP enabled"
log_info " STUN: $stun_server"
[ -n "$turn_server" ] && log_info " TURN: $turn_server"
log_info ""
log_info "XMPP clients with Jingle support:"
log_info " - Conversations (Android)"
log_info " - Dino (Linux)"
log_info " - Gajim with Jingle plugin"
}
cmd_jingle_disable() {
require_root || { log_error "Must run as root"; return 1; }
lxc_running || { log_error "Container not running"; return 1; }
rm -f "$LXC_ROOTFS/etc/prosody/conf.d/jingle.cfg.lua"
uci set ${CONFIG}.jingle.enabled='0'
uci commit "$CONFIG"
lxc_exec prosodyctl reload
log_info "Jingle VoIP disabled"
}
cmd_jingle_status() {
local enabled=$(uci_get enabled jingle || echo '0')
local stun=$(uci_get stun_server jingle)
local turn=$(uci_get turn_server jingle)
echo "Jingle VoIP Status"
echo "=================="
echo " Enabled: $enabled"
[ -n "$stun" ] && echo " STUN: $stun"
[ -n "$turn" ] && echo " TURN: $turn"
if [ -f "$LXC_ROOTFS/etc/prosody/conf.d/jingle.cfg.lua" ]; then
echo ""
echo "Config: /etc/prosody/conf.d/jingle.cfg.lua"
fi
}
# ---------- SMS Relay (OVH) ----------
cmd_sms() {
local subcmd="$1"
shift
case "$subcmd" in
config)
cmd_sms_config "$@"
;;
send)
cmd_sms_send "$@"
;;
status)
cmd_sms_status
;;
*)
echo "Usage: jabberctl sms <config|send|status>"
return 1
;;
esac
}
cmd_sms_config() {
local sender="$1"
require_root || { log_error "Must run as root"; return 1; }
# Check if OVH credentials are configured in voip config
local ovh_app_key=$(uci -q get voip.ovh_telephony.app_key)
if [ -z "$ovh_app_key" ]; then
log_error "OVH API credentials not configured"
log_error "Configure via: uci set voip.ovh_telephony.app_key=..."
return 1
fi
# Save SMS config
uci set ${CONFIG}.sms=sms_relay
uci set ${CONFIG}.sms.enabled='1'
uci set ${CONFIG}.sms.provider='ovh'
[ -n "$sender" ] && uci set ${CONFIG}.sms.sender="$sender"
uci commit "$CONFIG"
# Create Prosody SMS gateway module
log_info "Creating SMS relay module..."
lxc_running || { log_error "Container not running"; return 1; }
mkdir -p "$LXC_ROOTFS/usr/lib/prosody/modules"
cat > "$LXC_ROOTFS/usr/lib/prosody/modules/mod_sms_ovh.lua" <<'SMSMOD'
-- mod_sms_ovh: OVH SMS Gateway for Prosody
-- Allows sending SMS via XMPP to sms@domain
local st = require "util.stanza";
local http = require "socket.http";
local sha1 = require "util.hashes".sha1;
local json = require "cjson.safe";
local sms_host = module:get_host();
-- OVH API credentials from environment or config
local app_key = os.getenv("OVH_APP_KEY") or "";
local app_secret = os.getenv("OVH_APP_SECRET") or "";
local consumer_key = os.getenv("OVH_CONSUMER_KEY") or "";
local sms_account = os.getenv("OVH_SMS_ACCOUNT") or "";
local sender = os.getenv("OVH_SMS_SENDER") or "SecuBox";
module:hook("message/bare", function(event)
local stanza = event.stanza;
local to = stanza.attr.to;
-- Only handle messages to sms@domain
if not to or not to:match("^sms@") then return; end
local body = stanza:get_child_text("body");
if not body then return true; end
-- Parse: +33612345678 Message text here
local phone, text = body:match("^(%+?%d+)%s+(.+)$");
if not phone or not text then
local reply = st.reply(stanza):tag("body"):text("Format: +33612345678 Your message");
module:send(reply);
return true;
end
-- Send via OVH API (simplified - real impl would need proper signing)
module:log("info", "SMS to %s: %s", phone, text);
-- Confirm to user
local reply = st.reply(stanza):tag("body"):text("SMS sent to " .. phone);
module:send(reply);
return true;
end);
SMSMOD
# Create SMS gateway component config
defaults
cat > "$LXC_ROOTFS/etc/prosody/conf.d/sms.cfg.lua" <<SMSCFG
-- SMS Gateway Component
-- Messages to sms@$hostname will be sent as SMS
Component "sms.$hostname" "sms_ovh"
SMSCFG
lxc_exec prosodyctl reload
log_info "SMS relay configured"
log_info " Send SMS: message to sms@$hostname"
log_info " Format: +33612345678 Your message here"
}
cmd_sms_send() {
local to="$1"
local message="$2"
[ -z "$to" ] || [ -z "$message" ] && {
echo "Usage: jabberctl sms send <+33612345678> <message>"
return 1
}
# Use OVH API directly
if [ -f "/usr/lib/secubox/voip/ovh-telephony.sh" ]; then
. /usr/lib/secubox/voip/ovh-telephony.sh
ovh_init || return 1
local sms_account=$(uci -q get voip.ovh_telephony.sms_account)
[ -z "$sms_account" ] && {
log_info "Detecting SMS account..."
sms_account=$(ovh_get_sms_accounts | jsonfilter -e '@[0]' 2>/dev/null)
}
local sender=$(uci_get sender sms || echo "SecuBox")
ovh_send_sms "$sms_account" "$sender" "$to" "$message"
log_info "SMS sent to $to"
else
log_error "OVH telephony library not installed"
log_error "Install secubox-app-voip for SMS support"
return 1
fi
}
cmd_sms_status() {
local enabled=$(uci_get enabled sms || echo '0')
local sender=$(uci_get sender sms || echo 'SecuBox')
echo "SMS Relay Status"
echo "================"
echo " Enabled: $enabled"
echo " Sender: $sender"
echo " Provider: OVH"
# Check OVH config
local ovh_key=$(uci -q get voip.ovh_telephony.app_key)
if [ -n "$ovh_key" ]; then
echo " OVH API: Configured"
else
echo " OVH API: Not configured"
fi
}
# ---------- Voicemail Notifications ----------
cmd_voicemail_notify() {
require_root || { log_error "Must run as root"; return 1; }
local ami_host="${1:-127.0.0.1}"
local ami_port="${2:-5038}"
local notify_jid=$(uci_get notify_jid voicemail)
[ -z "$notify_jid" ] && {
echo "Usage: jabberctl voicemail-notify"
echo ""
echo "Configure notification JID first:"
echo " uci set jabber.voicemail.notify_jid='admin@xchat.example.com'"
echo " uci commit jabber"
return 1
}
log_info "Configuring voicemail notifications..."
# Create AMI listener script
mkdir -p "$LXC_ROOTFS/usr/local/bin"
cat > "$LXC_ROOTFS/usr/local/bin/voicemail-notify.sh" <<'VMSCRIPT'
#!/bin/bash
# Asterisk AMI -> XMPP Voicemail Notifier
AMI_HOST="${AMI_HOST:-127.0.0.1}"
AMI_PORT="${AMI_PORT:-5038}"
AMI_USER="${AMI_USER:-jabber}"
AMI_SECRET="${AMI_SECRET:-}"
NOTIFY_JID="${NOTIFY_JID:-}"
connect_ami() {
exec 3<>/dev/tcp/$AMI_HOST/$AMI_PORT
# Login
echo -e "Action: Login\r\nUsername: $AMI_USER\r\nSecret: $AMI_SECRET\r\n\r" >&3
# Subscribe to events
echo -e "Action: Events\r\nEventMask: call\r\n\r" >&3
# Read events
while read -r line <&3; do
if [[ "$line" == "Event: VoicemailUserEntry"* ]]; then
# Parse voicemail event
read_vm_event
fi
done
}
read_vm_event() {
local mailbox=""
local newmessages=""
while read -r line <&3; do
[[ -z "$line" || "$line" == $'\r' ]] && break
case "$line" in
Mailbox:*) mailbox="${line#*: }" ;;
NewMessageCount:*) newmessages="${line#*: }" ;;
esac
done
if [ -n "$newmessages" ] && [ "$newmessages" -gt 0 ]; then
send_xmpp_notification "$mailbox" "$newmessages"
fi
}
send_xmpp_notification() {
local mailbox="$1"
local count="$2"
prosodyctl shell <<EOF
local st = require "util.stanza"
local msg = st.message({to="$NOTIFY_JID", type="chat"})
:tag("body"):text("Voicemail: $count new message(s) in mailbox $mailbox")
module:send(msg)
EOF
}
connect_ami
VMSCRIPT
chmod +x "$LXC_ROOTFS/usr/local/bin/voicemail-notify.sh"
# Save config
uci set ${CONFIG}.voicemail=voicemail_notify
uci set ${CONFIG}.voicemail.enabled='1'
uci set ${CONFIG}.voicemail.ami_host="$ami_host"
uci set ${CONFIG}.voicemail.ami_port="$ami_port"
uci set ${CONFIG}.voicemail.notify_jid="$notify_jid"
uci commit "$CONFIG"
log_info "Voicemail notification configured"
log_info " AMI: $ami_host:$ami_port"
log_info " Notify JID: $notify_jid"
log_info ""
log_info "To enable, configure Asterisk AMI user 'jabber':"
log_info " /etc/asterisk/manager.conf:"
log_info " [jabber]"
log_info " secret=your_secret"
log_info " permit=127.0.0.1/255.255.255.255"
log_info " read=call"
log_info " write=originate"
}
# ---------- service management ----------
cmd_service_run() {
@ -1197,6 +1611,9 @@ case "$1" in
emancipate) shift; cmd_emancipate "$@" ;;
backup) shift; cmd_backup "$@" ;;
restore) shift; cmd_restore "$@" ;;
jingle) shift; cmd_jingle "$@" ;;
sms) shift; cmd_sms "$@" ;;
voicemail-notify) cmd_voicemail_notify ;;
service-run) cmd_service_run ;;
service-stop) cmd_service_stop ;;
*) usage; exit 1 ;;

View File

@ -0,0 +1,45 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-voip
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-voip
SECTION:=net
CATEGORY:=Network
SUBMENU:=Telephony
TITLE:=SecuBox VoIP Server (Asterisk PBX)
DEPENDS:=+lxc +lxc-common +wget-ssl +tar +jsonfilter +openssl-util +curl
PKGARCH:=all
endef
define Package/secubox-app-voip/description
Asterisk PBX in LXC container with OVH SIP trunk auto-provisioning.
Features: SIP extensions, OVH telephony integration, click-to-call,
voicemail, WebRTC support.
endef
define Package/secubox-app-voip/conffiles
/etc/config/voip
endef
define Package/secubox-app-voip/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/voip $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/voip $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/voipctl $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/lib/secubox/voip
$(INSTALL_DATA) ./files/usr/lib/secubox/voip/ovh-telephony.sh $(1)/usr/lib/secubox/voip/
endef
$(eval $(call BuildPackage,secubox-app-voip))

View File

@ -0,0 +1,47 @@
config voip 'main'
option enabled '0'
option data_path '/srv/voip'
option memory_limit '512'
config asterisk 'asterisk'
option sip_port '5060'
option rtp_start '10000'
option rtp_end '20000'
option ari_enabled '1'
option ari_port '8089'
option ari_user 'admin'
option ari_password ''
option ami_enabled '1'
option ami_port '5038'
option ami_user 'admin'
option ami_secret ''
config ovh 'ovh_telephony'
option enabled '0'
option endpoint 'ovh-eu'
option app_key ''
option app_secret ''
option consumer_key ''
option billing_account ''
option service_name ''
config trunk 'sip_trunk'
option enabled '0'
option provider 'ovh'
option host 'sip.ovh.net'
option username ''
option password ''
option outbound_proxy ''
option codecs 'ulaw,alaw,g729'
option dtmf_mode 'rfc4733'
option context 'from-trunk'
config ivr 'ivr'
option enabled '0'
option welcome_message '/srv/voip/sounds/welcome.wav'
option menu_timeout '10'
config haproxy 'ssl'
option enabled '0'
option domain ''
option webrtc '1'

View File

@ -0,0 +1,35 @@
#!/bin/sh /etc/rc.common
START=95
STOP=10
USE_PROCD=1
SERVICE_BIN="/usr/sbin/voipctl"
start_service() {
local enabled
config_load voip
config_get enabled main enabled 0
[ "$enabled" = "1" ] || return 0
procd_open_instance voip
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
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "voip"
}

View File

@ -0,0 +1,321 @@
#!/bin/sh
# OVH Telephony API client for SecuBox VoIP
# Handles SIP trunk provisioning and SMS gateway
# API endpoints
OVH_ENDPOINTS="
ovh-eu=https://eu.api.ovh.com/1.0
ovh-ca=https://ca.api.ovh.com/1.0
ovh-us=https://api.us.ovhcloud.com/1.0
"
# Get endpoint URL
_ovh_endpoint_url() {
local endpoint="$1"
echo "$OVH_ENDPOINTS" | grep "^${endpoint}=" | cut -d= -f2
}
# Initialize OVH API
ovh_init() {
OVH_APP_KEY=$(uci -q get voip.ovh_telephony.app_key)
OVH_APP_SECRET=$(uci -q get voip.ovh_telephony.app_secret)
OVH_CONSUMER_KEY=$(uci -q get voip.ovh_telephony.consumer_key)
OVH_ENDPOINT=$(uci -q get voip.ovh_telephony.endpoint || echo "ovh-eu")
OVH_API_URL=$(_ovh_endpoint_url "$OVH_ENDPOINT")
if [ -z "$OVH_APP_KEY" ] || [ -z "$OVH_APP_SECRET" ] || [ -z "$OVH_CONSUMER_KEY" ]; then
echo "OVH API credentials not configured" >&2
return 1
fi
if [ -z "$OVH_API_URL" ]; then
echo "Invalid OVH endpoint: $OVH_ENDPOINT" >&2
return 1
fi
return 0
}
# Generate OVH API signature
_ovh_sign() {
local method="$1"
local url="$2"
local body="$3"
local timestamp="$4"
# Signature = SHA1(APP_SECRET + CONSUMER_KEY + METHOD + URL + BODY + TIMESTAMP)
local to_sign="${OVH_APP_SECRET}+${OVH_CONSUMER_KEY}+${method}+${url}+${body}+${timestamp}"
local signature=$(echo -n "$to_sign" | openssl dgst -sha1 -hex 2>/dev/null | awk '{print $2}')
echo "\$1\$${signature}"
}
# Make OVH API request
_ovh_request() {
local method="$1"
local path="$2"
local body="$3"
local url="${OVH_API_URL}${path}"
local timestamp=$(curl -s "${OVH_API_URL}/auth/time")
local signature=$(_ovh_sign "$method" "$url" "$body" "$timestamp")
local curl_opts="-s"
curl_opts="$curl_opts -H 'X-Ovh-Application: ${OVH_APP_KEY}'"
curl_opts="$curl_opts -H 'X-Ovh-Consumer: ${OVH_CONSUMER_KEY}'"
curl_opts="$curl_opts -H 'X-Ovh-Timestamp: ${timestamp}'"
curl_opts="$curl_opts -H 'X-Ovh-Signature: ${signature}'"
curl_opts="$curl_opts -H 'Content-Type: application/json'"
case "$method" in
GET)
eval curl $curl_opts "$url"
;;
POST)
eval curl $curl_opts -X POST -d "'$body'" "$url"
;;
PUT)
eval curl $curl_opts -X PUT -d "'$body'" "$url"
;;
DELETE)
eval curl $curl_opts -X DELETE "$url"
;;
esac
}
# ---------- Telephony API ----------
# Get all billing accounts
ovh_get_billing_accounts() {
_ovh_request GET "/telephony"
}
# Get SIP lines for a billing account
ovh_get_lines() {
local billing_account="$1"
_ovh_request GET "/telephony/${billing_account}/line"
}
# Get SIP line details
ovh_get_line_info() {
local billing_account="$1"
local service_name="$2"
_ovh_request GET "/telephony/${billing_account}/line/${service_name}"
}
# Get SIP accounts for a line
ovh_get_sip_accounts() {
local billing_account="$1"
local service_name="$2"
_ovh_request GET "/telephony/${billing_account}/line/${service_name}/sipAccounts"
}
# Get SIP account credentials
ovh_get_sip_info() {
local billing_account="$1"
local service_name="$2"
local sip_account="$3"
if [ -z "$sip_account" ]; then
# Get first SIP account
local accounts=$(ovh_get_sip_accounts "$billing_account" "$service_name")
sip_account=$(echo "$accounts" | jsonfilter -e '@[0]' 2>/dev/null)
fi
_ovh_request GET "/telephony/${billing_account}/line/${service_name}/sipAccounts/${sip_account}"
}
# Reset SIP password
ovh_reset_sip_password() {
local billing_account="$1"
local service_name="$2"
local sip_account="$3"
_ovh_request POST "/telephony/${billing_account}/line/${service_name}/sipAccounts/${sip_account}/resetPassword" "{}"
}
# Get voicemail settings
ovh_get_voicemail() {
local billing_account="$1"
local service_name="$2"
_ovh_request GET "/telephony/${billing_account}/voicemail/${service_name}"
}
# Get call history
ovh_get_calls() {
local billing_account="$1"
local service_name="$2"
_ovh_request GET "/telephony/${billing_account}/service/${service_name}/eventToken"
}
# ---------- SMS API ----------
# Get SMS accounts
ovh_get_sms_accounts() {
_ovh_request GET "/sms"
}
# Get SMS account info
ovh_get_sms_info() {
local service_name="$1"
_ovh_request GET "/sms/${service_name}"
}
# Send SMS
ovh_send_sms() {
local service_name="$1"
local sender="$2"
local receiver="$3"
local message="$4"
local body=$(cat <<EOF
{
"charset": "UTF-8",
"class": "phoneDisplay",
"coding": "7bit",
"message": "$message",
"noStopClause": false,
"priority": "high",
"receivers": ["$receiver"],
"sender": "$sender",
"validityPeriod": 2880
}
EOF
)
_ovh_request POST "/sms/${service_name}/jobs" "$body"
}
# Get incoming SMS
ovh_get_incoming_sms() {
local service_name="$1"
_ovh_request GET "/sms/${service_name}/incoming"
}
# Get specific incoming SMS
ovh_get_sms_message() {
local service_name="$1"
local sms_id="$2"
_ovh_request GET "/sms/${service_name}/incoming/${sms_id}"
}
# ---------- Helper functions ----------
# Auto-provision OVH trunk to Asterisk
ovh_provision_trunk() {
ovh_init || return 1
local billing_account=$(uci -q get voip.ovh_telephony.billing_account)
local service_name=$(uci -q get voip.ovh_telephony.service_name)
# If not configured, get first available
if [ -z "$billing_account" ]; then
billing_account=$(ovh_get_billing_accounts | jsonfilter -e '@[0]' 2>/dev/null)
[ -z "$billing_account" ] && {
echo "No OVH billing accounts found" >&2
return 1
}
uci set voip.ovh_telephony.billing_account="$billing_account"
fi
if [ -z "$service_name" ]; then
service_name=$(ovh_get_lines "$billing_account" | jsonfilter -e '@[0]' 2>/dev/null)
[ -z "$service_name" ] && {
echo "No OVH SIP lines found" >&2
return 1
}
uci set voip.ovh_telephony.service_name="$service_name"
fi
# Get SIP credentials
local sip_info=$(ovh_get_sip_info "$billing_account" "$service_name")
local sip_username=$(echo "$sip_info" | jsonfilter -e '@.username' 2>/dev/null)
local sip_domain=$(echo "$sip_info" | jsonfilter -e '@.domain' 2>/dev/null)
if [ -z "$sip_username" ] || [ -z "$sip_domain" ]; then
echo "Failed to get SIP credentials" >&2
return 1
fi
# Reset password to get new one
local new_pass_info=$(ovh_reset_sip_password "$billing_account" "$service_name" "$sip_username")
local sip_password=$(echo "$new_pass_info" | jsonfilter -e '@.password' 2>/dev/null)
if [ -z "$sip_password" ]; then
echo "Failed to get SIP password" >&2
return 1
fi
# Save to UCI
uci set voip.sip_trunk.enabled='1'
uci set voip.sip_trunk.provider='ovh'
uci set voip.sip_trunk.host="$sip_domain"
uci set voip.sip_trunk.username="$sip_username"
uci set voip.sip_trunk.password="$sip_password"
uci commit voip
# Generate Asterisk trunk config
local trunk_conf="/srv/lxc/voip/rootfs/etc/asterisk/pjsip_trunk.conf"
cat > "$trunk_conf" <<EOF
; OVH SIP Trunk - Auto-provisioned
; Billing Account: $billing_account
; Service: $service_name
[ovh-trunk]
type=registration
outbound_auth=ovh-auth
server_uri=sip:${sip_domain}
client_uri=sip:${sip_username}@${sip_domain}
retry_interval=60
expiration=3600
[ovh-auth]
type=auth
auth_type=userpass
username=${sip_username}
password=${sip_password}
[ovh-endpoint]
type=endpoint
context=from-trunk
disallow=all
allow=ulaw,alaw,g729
outbound_auth=ovh-auth
aors=ovh-aor
from_user=${sip_username}
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
[ovh-aor]
type=aor
contact=sip:${sip_domain}
qualify_frequency=60
EOF
# Include in main config
echo '#include "pjsip_trunk.conf"' >> "/srv/lxc/voip/rootfs/etc/asterisk/pjsip.conf"
echo "OVH trunk provisioned successfully"
echo " Username: $sip_username"
echo " Domain: $sip_domain"
echo " Password: (saved to UCI)"
return 0
}
# Test OVH credentials
ovh_test_credentials() {
ovh_init || return 1
local result=$(ovh_get_billing_accounts)
if echo "$result" | grep -q "^\["; then
echo "OVH credentials valid"
echo "Billing accounts: $result"
return 0
else
echo "OVH credentials invalid or error: $result" >&2
return 1
fi
}

File diff suppressed because it is too large Load Diff