#!/bin/sh
#
# Renders /etc/config/asterisk's `config trunk` sections into:
#   /etc/asterisk/pjsip_trunks.conf       (auth/aor/registration/endpoint/identify)
#   /etc/asterisk/extensions_trunks.conf  ([globals] + inbound context per trunk)
# Run by the asterisk init.d and as a uci config-trigger reload hook.

set -e

. /lib/functions.sh

OUTDIR=/etc/asterisk
PJSIP_OUT=$OUTDIR/pjsip_trunks.conf
EXT_OUT=$OUTDIR/extensions_trunks.conf

GEN_HEADER="; Generated by asterisk-uci-gen from /etc/config/asterisk — do not edit by hand."

# Serialize concurrent invocations — the init.d start and the uci
# config-change trigger can fire together. Without this, two renders race
# on truncating + appending PJSIP_OUT/EXT_OUT and produce a torn config.
# The lock fd is held for the life of the process and released on exit.
exec 9>/var/lock/asterisk-uci-gen.lock 2>/dev/null || true
flock 9 2>/dev/null || true

mkdir -p "$OUTDIR"
printf '%s\n' "$GEN_HEADER" > "$PJSIP_OUT"
printf '%s\n' "$GEN_HEADER" > "$EXT_OUT"

GLOBALS_TMP=$(mktemp)
CONTEXTS_TMP=$(mktemp)
trap 'rm -f "$GLOBALS_TMP" "$CONTEXTS_TMP"' EXIT

format_sip_host() {
	local host="$1"

	case "$host" in
		\[*\]) echo "$host" ;;
		*:*) echo "[$host]" ;;
		*) echo "$host" ;;
	esac
}

escape_ini() {
	# Strip CR/LF and INI section brackets first so a UCI value can't
	# inject extra config lines or a fake [section]; then escape ';' and
	# '"' (Asterisk's INI escapes). printf avoids echo's backslash quirks.
	printf '%s' "$1" | tr -d '\r\n[]' | sed -e 's/;/\\;/g' -e 's/"/\\"/g'
}

map_target() {
	# Inbound bridges into PROSLIC/0 use the proslic-predial Gosub to
	# install an adaptive jitterbuffer before the bridge engages —
	# without it the FXS ALSA write path can EBADFD on the first frame.
	case "$1" in
		fxs|proslic|'') echo 'PROSLIC/0,30,b(proslic-predial^s^1)'; return ;;
		quectel|mobile|asterisk-chan-quectel) echo 'Quectel/quectel0/${EXTEN},60' ;;
		*)              echo "$1,30" ;;
	esac
}

emit_trunk() {
	local section="$1"
	local enabled server server_port domain username password
	local expiration auth_type protocol dtmf_mode phone_number
	local target_channel proxy proxy_port
	local media_encryption=""

	config_get_bool enabled "$section" enabled 1
	[ "$enabled" -eq 1 ] || return 0

	config_get server         "$section" server
	config_get server_port    "$section" server_port 5060
	config_get domain         "$section" domain "$server"
	config_get username       "$section" username
	config_get password       "$section" password
	config_get expiration     "$section" expiration 3600
	config_get auth_type      "$section" auth_type userpass
	config_get protocol       "$section" protocol udp
	config_get dtmf_mode      "$section" dtmf_mode rfc4733
	config_get phone_number   "$section" phone_number "$username"
	config_get target_channel "$section" target_channel fxs
	config_get proxy          "$section" proxy "$server"
	config_get proxy_port     "$section" proxy_port "$server_port"

	[ -n "$server" ] && [ -n "$username" ] && [ -n "$password" ] || return 0

	# username and phone_number become the user-part of the rendered SIP
	# URIs (from_user, contact, client_uri, registration). Reject anything
	# outside a safe user-part charset — notably '@', which would redirect
	# the contact to an attacker-chosen host. UCI is root-writable, but
	# this contains the blast radius of a compromised WebUI session.
	case "$username$phone_number" in
		*[!A-Za-z0-9._+-]*)
			echo "asterisk-uci-gen: trunk '$section' username/phone_number has unsafe characters, skipping" >&2
			return 0
			;;
	esac

	local server_host="$(format_sip_host "$server")"
	local domain_host="$(format_sip_host "$domain")"
	local proxy_host="$(format_sip_host "$proxy")"

	local id="trunk${section}"
	# Endpoints reference the global [udp]/[tcp]/[tls] transports
	# declared in pjsip.conf — a per-trunk transport would re-bind
	# the same port and fail with "Address in use" on every reload.
	local transport_id="${protocol}"

	local dest
	dest="$(map_target "$target_channel")"

	local server_ep="$server_host"
	[ "$server_port" != "0" ] && [ -n "$server_port" ] && server_ep="${server_host}:${server_port}"

	local proxy_ep="$proxy_host"
	[ "$proxy_port" != "0" ] && [ -n "$proxy_port" ] && proxy_ep="${proxy_host}:${proxy_port}"

	local user_esc=$(escape_ini "$username")
	local pass_esc=$(escape_ini "$password")
	local dom_esc=$(escape_ini "$domain_host")
	local srv_esc=$(escape_ini "$server")
	local phone_esc=$(escape_ini "$phone_number")
	local srv_ep_esc=$(escape_ini "$server_ep")
	local prx_ep_esc=$(escape_ini "$proxy_ep")

	# Valid PJSIP values: 'no' (default), 'sdes' (RFC 4568 — SIP-over-TLS),
	# 'dtls' (DTLS-SRTP, needs extra dtls_* config). 'srtp' is NOT valid.
	if [ "$protocol" = "tls" ]; then
		media_encryption="media_encryption=sdes"
	fi

	cat >> "$PJSIP_OUT" <<EOF

;
; ===== Trunk '$section' -> $server_ep (user $username) =====
;
[${id}_auth]
type=auth
auth_type=$auth_type
username=$user_esc
password=$pass_esc

[${id}]
type=aor
; Contact MUST include a user-part. chan_pjsip replaces the user-part
; with the dialed digits at Dial() time; if it's missing some PJSIP
; versions emit "INVITE sip:<server>" with no user and the upstream
; dispatches to extension "s" instead of the dialed number.
contact=sip:$user_esc@$prx_ep_esc
qualify_frequency=60
qualify_timeout=3.0

[${id}_reg]
type=registration
transport=${transport_id}
outbound_auth=${id}_auth
server_uri=sip:$srv_ep_esc
client_uri=sip:$phone_esc@$dom_esc
contact_user=$phone_esc
retry_interval=60
max_retries=10000
forbidden_retry_interval=60
fatal_retry_interval=60
expiration=$expiration
; ;lr is REQUIRED — without it PJSIP treats outbound_proxy as a
; strict-route (RFC 2543) and pre-swaps Request-URI with the Route
; URI, dropping the user-part from the Request-URI on the wire.
${proxy:+outbound_proxy=sip:$prx_ep_esc\\;lr}

[${id}]
type=endpoint
context=${id}_in
disallow=all
allow=alaw
allow=ulaw
auth=${id}_auth
outbound_auth=${id}_auth
aors=${id}
from_user=$phone_esc
from_domain=$dom_esc
dtmf_mode=$dtmf_mode
transport=${transport_id}
${proxy:+outbound_proxy=sip:$prx_ep_esc\\;lr}
${media_encryption}
direct_media=no
rewrite_contact=yes
rtp_symmetric=yes
force_rport=yes

[${id}_id]
type=identify
endpoint=${id}
match=$srv_esc
EOF

	cat >> "$GLOBALS_TMP" <<EOF
${id}_phone_number=$phone_number
${id}_username=$username
EOF

	cat >> "$CONTEXTS_TMP" <<EOF

; ===== Trunk '$section' contexts =====
[${id}_in]
; _X. (one digit then anything) — '_.' would match empty extension and
; Asterisk warns against it. Inbound DIDs always have at least one digit.
exten => _X.,1,NoOp(Inbound on trunk '$section' from \${CALLERID(num)} to \${EXTEN})
same => n,Dial($dest)
same => n,Hangup()

exten => s,1,NoOp(Inbound on trunk '$section' from \${CALLERID(num)} to s)
same => n,Dial($dest)
same => n,Hangup()
EOF

}

ast_en="$(jsonfilter -q -i /etc/board.json -e "@.hwinfo[\"asterisk\"]" 2>/dev/null)"
[ "$ast_en" = "true" ] || exit 0

config_load asterisk

config_get_bool enabled general enabled 0
[ "$enabled" -eq 1 ] || exit 0

config_foreach emit_trunk trunk

if [ -s "$GLOBALS_TMP" ]; then
	{
		printf '\n[globals]\n'
		cat "$GLOBALS_TMP"
	} >> "$EXT_OUT"
fi

cat "$CONTEXTS_TMP" >> "$EXT_OUT"

# Passwords land in pjsip_trunks.conf — restrict to the asterisk
# daemon. Guarded with `|| true` because uci-defaults can run before
# the asterisk user exists at first install; permtab.d/asterisk fixes
# ownership on the next boot.
chown asterisk:asterisk "$PJSIP_OUT" "$EXT_OUT" 2>/dev/null || true
chmod 0640 "$PJSIP_OUT" "$EXT_OUT" 2>/dev/null || true

# Asterisk 23 dropped the "pjsip reload" alias.
if pidof asterisk >/dev/null 2>&1; then
	asterisk -rx "module reload res_pjsip.so" >/dev/null 2>&1 || true
	asterisk -rx "dialplan reload" >/dev/null 2>&1 || true
fi

exit 0
