#!/bin/sh
# The first start of a machine that just got the package. Everything a person
# would otherwise type after installing, done once, and written down in
# /root/nuxpbx-welcome.
#
# On Alpine it runs from the nuxpbx-firstboot service on the first boot of an
# installed disk; on Debian it runs from the package's postinst. Runs as
# root. Every step is safe to run again after a failure: what exists is left
# alone, what is missing is made.
set -eu

marker=/etc/nuxpbx/installed
env_file=/etc/nuxpbx/nuxpbx.env
welcome=/root/nuxpbx-welcome

if [ -e "$marker" ]; then
	echo "already set up"
	exit 0
fi

if command -v rc-service >/dev/null 2>&1; then
	os=alpine
elif command -v systemctl >/dev/null 2>&1; then
	os=debian
else
	echo "neither OpenRC nor systemd: set this machine up by hand, see the README" >&2
	exit 1
fi

# Enable now and after every boot, whichever init this is.
service_up() {
	case "$os" in
		alpine)
			rc-update add "$1" default >/dev/null 2>&1 || true
			rc-service "$1" restart >/dev/null 2>&1 || rc-service "$1" start
			;;
		debian)
			if [ -d /run/systemd/system ]; then
				systemctl enable "$1" >/dev/null 2>&1 || true
				systemctl restart "$1"
			elif [ -x "/etc/init.d/$1" ]; then
				# No systemd running (a container, a chroot): the init
				# scripts still start things, and nuxpbx has none, so it
				# is started by hand below.
				"/etc/init.d/$1" restart >/dev/null 2>&1 || "/etc/init.d/$1" start
			else
				echo "no init to start $1 with; start it yourself: $1" >&2
			fi
			;;
	esac
}

random() {
	head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
}

# The name this phone system answers to. Phones register against it, so it is
# the machine's own name: what the person typed during the installation,
# read from the file rather than resolved, because Alpine's hosts file
# would answer with a made up domain.
sip_domain=$(cat /etc/hostname 2>/dev/null || hostname)
case "$sip_domain" in
	""|localhost|localhost.localdomain) sip_domain="pbx.local" ;;
esac

# 0. Where updates come from. The disk was installed from the ISO alone, so
#    the package list points at a medium that is no longer there; Alpine's
#    own mirror and the phone system's repository replace it.
if [ "$os" = alpine ]; then
	branch="v$(cut -d. -f1,2 /etc/alpine-release)"
	if grep -q '^#\?/media/' /etc/apk/repositories 2>/dev/null; then
		cat >/etc/apk/repositories <<REPOS
https://dl-cdn.alpinelinux.org/alpine/$branch/main
https://dl-cdn.alpinelinux.org/alpine/$branch/community
REPOS
	fi
	grep -q 'apt.team-nifty.com/alpine' /etc/apk/repositories 2>/dev/null \
		|| echo "https://apt.team-nifty.com/alpine/$branch/nuxpbx" >>/etc/apk/repositories
fi

if [ "$os" = debian ] && [ ! -s /etc/apt/sources.list.d/team-nifty.list ]; then
	# Armored as served: apt reads that itself, and a minimal Debian has no gpg.
	mkdir -p /etc/apt/keyrings
	if curl -fsSL https://apt.team-nifty.com/repository-key.gpg -o /etc/apt/keyrings/team-nifty.asc 2>/dev/null; then
		echo "deb [signed-by=/etc/apt/keyrings/team-nifty.asc] https://apt.team-nifty.com/ stable main" >/etc/apt/sources.list.d/team-nifty.list
	else
		echo "could not fetch the repository key from apt.team-nifty.com; updates will need it, see nuxpbx-update" >&2
	fi
fi

# 1. PostgreSQL, on loopback, with a database of its own. Alpine hands over
#    an empty package and has to be asked for a cluster; Debian made one.
if [ "$os" = alpine ] && [ ! -s "$(ls -d /var/lib/postgresql/*/data 2>/dev/null | head -1)/PG_VERSION" ]; then
	rc-service postgresql setup
fi
service_up postgresql
for _ in $(seq 30); do
	su postgres -c "psql -q -c 'SELECT 1'" >/dev/null 2>&1 && break
	sleep 1
done

db_password=$(random)
if su postgres -c "psql -tAq -c \"SELECT 1 FROM pg_roles WHERE rolname = 'nuxpbx'\"" | grep -q 1; then
	su postgres -c "psql -q -c \"ALTER ROLE nuxpbx WITH LOGIN PASSWORD '$db_password'\""
else
	su postgres -c "psql -q -c \"CREATE ROLE nuxpbx WITH LOGIN PASSWORD '$db_password'\""
fi
if ! su postgres -c "psql -tAq -c \"SELECT 1 FROM pg_database WHERE datname = 'nuxpbx'\"" | grep -q 1; then
	su postgres -c "psql -q -c 'CREATE DATABASE nuxpbx OWNER nuxpbx'"
fi
database="host=127.0.0.1 port=5432 user=nuxpbx password=$db_password dbname=nuxpbx"

# 2. Asterisk's own files, before nuxpbx writes the ones it owns into the same
#    directory. Alpine ships Asterisk with no configuration at all and its
#    data under /usr/share/asterisk, which the sample asterisk.conf does not
#    say, so two files of ours go in; Debian's own are right as they are.
#    unixODBC reads the driver list and the data sources from /etc; the data
#    source is the one nuxpbx renders, so it is a link.
if [ "$os" = alpine ]; then
	install -Dm644 /usr/share/nuxpbx/asterisk/asterisk.conf /etc/asterisk/asterisk.conf
	install -Dm644 /usr/share/nuxpbx/asterisk/modules.conf /etc/asterisk/modules.conf
fi
driver=$(find /usr/lib -name 'psqlodbcw.so' 2>/dev/null | head -1)
[ -n "$driver" ] || { echo "the PostgreSQL ODBC driver (psqlodbcw.so) is not installed" >&2; exit 1; }
if ! grep -q '^\[PostgreSQL\]' /etc/odbcinst.ini 2>/dev/null; then
	printf '\n[PostgreSQL]\nDescription = PostgreSQL ODBC, for Asterisk\nDriver = %s\n' "$driver" >>/etc/odbcinst.ini
fi
ln -sf /etc/asterisk/odbc.ini /etc/odbc.ini
mkdir -p /var/lib/asterisk /var/log/asterisk /var/spool/asterisk/monitor /var/spool/asterisk/voicemail \
	/usr/share/asterisk/sounds/nuxpbx /var/lib/nuxpbx /etc/nuxpbx

# 3. The phone system itself: schema, first administrator, Asterisk's files,
#    and the environment file with the generated secrets.
admin_password=$(head -c 24 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 20)
printf '%s' "$admin_password" | NUXPBX_DB="$database" nuxpbx install \
	--sip-domain "$sip_domain" \
	--admin admin \
	--db-host 127.0.0.1 --db-port 5432 --db-name nuxpbx --db-user nuxpbx --db-password "$db_password" \
	--asterisk-config /etc/asterisk \
	--env-file "$env_file" \
	--service-file none

# What the installer could not know about this machine: the interface is the
# whole point of the box and listens on every address, plain http because
# there is no certificate yet, and the browser interface and the sounds live
# where the package put them. Https is a first step in the browser; the line
# here, commented out, is for a certificate of your own or another port.
sed -i '/^NUXPBX_HTTP_ADDR=/d' "$env_file"
cat >>"$env_file" <<ENV
NUXPBX_HTTP_ADDR=0.0.0.0:8080
# The session cookie marks itself Secure: over https, and on any machine where https is switched on at all. Only for a proxy that terminates TLS in front of this machine, where every request arrives here as plain http: uncomment.
#NUXPBX_COOKIE_SECURE=true
# Https with a Let's Encrypt certificate is the fifth of the first steps in the browser. Only for a certificate of your own (NUXPBX_TLS_CERT and NUXPBX_TLS_KEY, PEM files) or another port: uncomment this, it wins over the browser.
#NUXPBX_HTTPS_ADDR=0.0.0.0:443
NUXPBX_UI_DIR=/usr/share/nuxpbx/ui
NUXPBX_SOUNDS=/usr/share/asterisk/sounds/nuxpbx
NUXPBX_RECORDINGS=/var/spool/asterisk/monitor
NUXPBX_VOICEMAIL=/var/spool/asterisk/voicemail
NUXPBX_LANGUAGE=${NUXPBX_LANGUAGE:-de}
# Where the nightly backup also goes, so it is not only on the disk it is a backup of. A path starting with / is a directory on another disk; backup@host:/srv/nuxpbx is a host over ssh, reached with the key below and with no password anywhere. See docs/install.md for the receiving side.
#NUXPBX_BACKUP_TO=backup@nas.example:/srv/nuxpbx
#NUXPBX_BACKUP_KEY=/etc/nuxpbx/backup.key
# How often a backup is written and sent besides the nightly one, in minutes. Only worth setting where a second machine stands by for this one: what it can lose in a takeover is whatever happened since the last copy. See docs/standby.md.
#NUXPBX_BACKUP_EVERY=15
# This machine stands by for another one: it pulls that installation's backups from the destination above and restores them into itself, registers no trunk and answers no phone until somebody runs nuxpbx takeover here. The whole walk through is docs/standby.md. NUXPBX_STANDBY_LIVE is where the live machine is asked whether it is still there, by address rather than by name, because the name is what gets pointed here during a takeover.
#NUXPBX_STANDBY_FOR=pbx.example.de
#NUXPBX_STANDBY_LIVE=http://198.51.100.10:8080/api/v1/health
#NUXPBX_STANDBY_EVERY=3600
ENV
chown nuxpbx:nuxpbx "$env_file"
chmod 600 "$env_file"

# 4. Who may write where. nuxpbx writes Asterisk's configuration and the
#    uploaded sounds, and sweeps old recordings; Asterisk writes recordings
#    and voicemail. Group ownership does that without either running as root.
chgrp -R asterisk /etc/asterisk
chmod 2775 /etc/asterisk
chmod g+w /etc/asterisk/*
chown -R nuxpbx:asterisk /usr/share/asterisk/sounds/nuxpbx
chmod 2775 /usr/share/asterisk/sounds/nuxpbx
chown -R asterisk:asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk
chmod 2775 /var/spool/asterisk/monitor /var/spool/asterisk/voicemail
chown nuxpbx:nuxpbx /var/lib/nuxpbx

# 5. Up, now and after every boot. On Alpine that includes busybox crond,
#    which is in the base system and off by default, and which is what runs
#    the daily backup in /etc/periodic/daily.
service_up asterisk
service_up nuxpbx
if [ "$os" = alpine ]; then
	service_up crond
fi

# 6. What this machine answers on, and what happens to somebody guessing.
#    Only here, on a machine that is new: an upgrade must not overrule what
#    somebody decided about their own firewall, and this whole file is
#    skipped once the marker exists.
if [ ! -e /etc/nuxpbx/firewall.conf ]; then
	cat >/etc/nuxpbx/firewall.conf <<'CONF'
# What this machine answers on. Read by nuxpbx-firewall, which runs at every
# boot and writes one nftables table called nuxpbx; nothing of this machine's
# own rules is touched, and `nuxpbx-firewall remove` is the whole undo.
#
# Open: ssh (22 and wherever sshd actually listens), the interface, SIP on
# 5060, the audio range out of /etc/asterisk/rtp.conf, and icmp. Everything
# else is dropped.
#
# off leaves this machine's rules alone, for a box that already sits behind a
# firewall of somebody else's.
FIREWALL=on

# Where ssh listens, for a machine where it cannot be read from ss, sshd or
# /etc/ssh/sshd_config. Without an answer from one of those, nuxpbx-firewall
# refuses rather than write rules that could lock this session out.
#SSH_PORTS="22"

# Anything else that answers here.
#EXTRA_TCP="80"
#EXTRA_UDP=""
CONF
fi

# Asterisk writes this once logger.conf tells it to, but fail2ban wants the
# file to exist before it will watch it, and it starts first on a fresh boot.
if [ ! -e /var/log/asterisk/security ]; then
	touch /var/log/asterisk/security
	chown asterisk:asterisk /var/log/asterisk/security 2>/dev/null || true
fi

if command -v nft >/dev/null 2>&1; then
	case "$os" in
		alpine) rc-update add nuxpbx-firewall default >/dev/null 2>&1 || true ;;
		debian) systemctl enable nuxpbx-firewall >/dev/null 2>&1 || true ;;
	esac
	/usr/sbin/nuxpbx-firewall apply \
		|| echo "the ports were left open: nuxpbx-firewall refused, see above" >&2
else
	echo "nftables is not installed, so nothing closed this machine's ports" >&2
fi

# The bans are Debian's alone: on Alpine fail2ban would pull Python onto an
# ISO that is otherwise a phone system, so that machine gets closed ports and
# no bans.
if [ "$os" = debian ] && command -v fail2ban-server >/dev/null 2>&1; then
	# Not fatal: a phone system that is up and unbanned is still a phone
	# system, and this file has already started it.
	service_up fail2ban || echo "fail2ban did not start; nothing is banning guesses" >&2
fi

touch "$marker"

address=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
cat >"$welcome" <<TEXT
This machine is a phone system.

  Open http://${address:-<this machine's address>}:8080 in a browser.
  Sign in as: admin
  Password:   $admin_password

Change that password first. It was generated here and written nowhere else
but this file, which only root can read. The overview begins with the first
steps: that password, the trunk, an extension, a phone, and https. Phones
register against $sip_domain, which the same section can rename.

The interface speaks plain http until it has a certificate. Point a name at
this machine, open port 443 to it, and type the name into the fifth first
step: the certificate comes from Let's Encrypt, from here. The environment
file, /etc/nuxpbx/nuxpbx.env, is only for a certificate of your own.

This machine answers on ssh, the interface, SIP and the audio range, and
drops everything else:  nuxpbx-firewall show
Switch it off in /etc/nuxpbx/firewall.conf if this box already sits behind a
firewall of somebody else's.

To update the phone system and the operating system:  nuxpbx-update
TEXT
chmod 600 "$welcome"
cat >/etc/motd <<TEXT

  This machine is a phone system. The first administrator and the
  password are in /root/nuxpbx-welcome.

TEXT
# The ISO's login screen said to run the installer; this machine is past that.
if [ "$os" = alpine ]; then
	cat >/etc/issue <<TEXT

  nuxpbx phone system, $sip_domain. Administration on http://${address:-<this machine>}:8080

TEXT
fi
echo "set up. $sip_domain answers on http://${address:-?}:8080, welcome in $welcome"
