401 lines
16 KiB
Markdown
401 lines
16 KiB
Markdown
# SvxPortal — installation HOWTO
|
|
|
|
SvxPortal is the web front-end for a SvxReflector: node map, monitor,
|
|
last-heard, talkgroup list, user accounts and reflector admin pages. It has
|
|
two parts, both in this repository:
|
|
|
|
- **the web**: plain HTML/JavaScript plus a small PHP backend that keeps
|
|
accounts in Redis. No build step, no framework, no composer.
|
|
- **svxdr**: a Go daemon that reads the reflector's `/status` and serves the
|
|
live data the dashboard shows. One `go build`, no other build tools.
|
|
|
|
How the pieces connect:
|
|
|
|
```
|
|
browser ──HTTPS──> nginx ──> static files + PHP-FPM ──> Redis (accounts, sessions, bans)
|
|
│
|
|
└── /svxportal/api/ ──> svxdr nodelist :8098
|
|
└──> svxdr proxy :8090 ──> svxreflector HTTP_SRV_PORT :8099
|
|
```
|
|
|
|
Steps in order: 0 get the code, 1 packages, 2 web files, 3 Redis, 4 PHP-FPM,
|
|
5 web config, 6 talkgroup file, 7 nginx + TLS, 8 first admin, 9 svxdr.
|
|
Step 10 is optional. Step 11 is troubleshooting.
|
|
|
|
What you need on the server:
|
|
|
|
| Component | Why |
|
|
|-----------|-----|
|
|
| nginx | serves the static files and passes `*.php` to PHP-FPM |
|
|
| PHP 8.4 with FPM | the `auth/` backend (login, accounts, admin API). Developed and tested on 8.4 |
|
|
| PHP extensions: `redis`, `mbstring`, `openssl`, `json`, `session` | used by `auth/lib.php` |
|
|
| Redis 6+ | stores accounts, sessions, bans, talkgroup requests |
|
|
| A way to send e-mail | signup verification and password reset (local MTA **or** an SMTP relay, see step 5) |
|
|
| TLS certificate | the session cookie is `Secure`; login does not work over plain HTTP |
|
|
| A running SvxReflector | we use [GeuReflector](https://github.com/audric/GeuReflector) (SvxReflector with trunking); build it per its own README. `install/svxreflector.conf.example` is for that build |
|
|
| Go 1.24+ | to compile svxdr (step 9). Debian 13 ships it; on Debian 12 / Ubuntu 24.04 the packaged Go is older but fetches the right toolchain itself, see step 9 |
|
|
|
|
Everything below assumes Debian 12/13 or Ubuntu 24.04, PHP 8.4 and that the
|
|
site will live at `https://svxportal.example.com`. Replace the host name
|
|
everywhere you see it.
|
|
|
|
---
|
|
|
|
## 0. Get the code
|
|
|
|
```bash
|
|
sudo apt install git
|
|
cd ~ && git clone https://git.sa7aux.se/bjorne/svx-web-pub.git svxportal
|
|
cd svxportal
|
|
```
|
|
|
|
All paths below are relative to this checkout unless they start with `/`.
|
|
|
|
## 1. Packages
|
|
|
|
Everything the web, Redis, TLS and svxdr need, in one go:
|
|
|
|
```bash
|
|
sudo apt install nginx php8.4-fpm php8.4-redis php8.4-mbstring php8.4-cli \
|
|
redis-server certbot openssl rsync curl python3 sudo golang-go
|
|
```
|
|
|
|
| Package | Used by |
|
|
|---------|---------|
|
|
| `nginx` | web server |
|
|
| `php8.4-fpm`, `php8.4-redis`, `php8.4-mbstring`, `php8.4-cli` | PHP backend; `-cli` for `svx-mkadmin.php` (step 8). `openssl`/`json`/`session` are built into Debian's PHP |
|
|
| `redis-server` | account store; also provides the `redis-server@` instance template (step 3) |
|
|
| `certbot` | Let's Encrypt certificate (step 7) |
|
|
| `openssl` | CSR parsing in the certificate admin pages, password generation |
|
|
| `rsync` | copying the web files into the webroot (step 2) |
|
|
| `curl`, `python3` | the optional talkgroup sync timer (step 6b) and the checks in this guide |
|
|
| `golang-go` | compiles svxdr (step 9) |
|
|
|
|
Check the PHP version you actually got (`php -v`) and use that number instead
|
|
of `8.4` in the file names below. On Debian 12 the PHP is 8.2; the portal is
|
|
developed on 8.4, so prefer Debian 13 or the `packages.sury.org` PHP repo.
|
|
|
|
## 2. Copy the web files
|
|
|
|
```bash
|
|
sudo mkdir -p /var/www/html/svxportal
|
|
sudo rsync -a --exclude .git --exclude install --exclude svxdr --exclude INSTALL.md --exclude README.md ./ /var/www/html/svxportal/
|
|
sudo chown -R www-data:www-data /var/www/html/svxportal
|
|
sudo find /var/www/html/svxportal -type d -exec chmod 2775 {} +
|
|
sudo find /var/www/html/svxportal -type f -exec chmod 0644 {} +
|
|
```
|
|
|
|
## 3. Redis (dedicated instance, unix socket)
|
|
|
|
```bash
|
|
sudo cp install/redis-svxportal.conf /etc/redis/redis-svxportal.conf
|
|
sudo mkdir -p /var/lib/redis-svxportal
|
|
sudo chown redis:redis /var/lib/redis-svxportal /etc/redis/redis-svxportal.conf
|
|
sudo chmod 640 /etc/redis/redis-svxportal.conf
|
|
```
|
|
|
|
Generate a password and put it in the config:
|
|
|
|
```bash
|
|
openssl rand -hex 32
|
|
sudo sed -i "s/^requirepass .*/requirepass PASTE-THE-VALUE-HERE/" /etc/redis/redis-svxportal.conf
|
|
sudo systemctl enable --now redis-server@svxportal
|
|
```
|
|
|
|
The socket is created with mode 770 and group `redis`, so the web user must be
|
|
in that group:
|
|
|
|
```bash
|
|
sudo usermod -aG redis www-data
|
|
```
|
|
|
|
Test:
|
|
|
|
```bash
|
|
redis-cli -s /run/redis-svxportal/redis.sock -a 'PASTE-THE-VALUE-HERE' ping # -> PONG
|
|
```
|
|
|
|
## 4. PHP-FPM
|
|
|
|
Two pools are used. The default pool (`www`) handles all normal pages. A
|
|
second small pool handles only `auth/admin_reflector_api.php`, so its longer
|
|
timeout and separate process limits do not affect the rest of the site.
|
|
|
|
```bash
|
|
sudo cp install/php-fpm-svxadmin.conf /etc/php/8.4/fpm/pool.d/svxadmin.conf
|
|
```
|
|
|
|
Hardening of the default pool is recommended. Edit
|
|
`/etc/php/8.4/fpm/pool.d/www.conf` and add at the end:
|
|
|
|
```ini
|
|
php_admin_value[disable_functions] = passthru,shell_exec,system,popen,proc_open,proc_close,pcntl_exec,dl,phpinfo,show_source
|
|
php_admin_value[expose_php] = Off
|
|
```
|
|
|
|
Do **not** add `exec` to that list. The portal uses `exec()` in the default
|
|
pool too: `openssl req` to read the e-mail out of a pending CSR, and the sudo
|
|
helpers from the node pages (`auth/node_auth_api.php`, `auth/node_cmd_api.php`,
|
|
`auth/admin_nodes.php`). With `exec` disabled those pages return errors.
|
|
|
|
Then:
|
|
|
|
```bash
|
|
sudo systemctl restart php8.4-fpm
|
|
ls -l /run/php/ # both php8.4-fpm.sock and php8.4-fpm-svxadmin.sock must exist
|
|
```
|
|
|
|
## 5. Web app config (`auth/config.php`)
|
|
|
|
```bash
|
|
cd /var/www/html/svxportal
|
|
sudo -u www-data cp auth/config.php.example auth/config.php
|
|
sudo chmod 640 auth/config.php
|
|
sudoedit auth/config.php
|
|
```
|
|
|
|
Set:
|
|
|
|
- `redis.unix_socket` -> `/run/redis-svxportal/redis.sock`
|
|
- `redis.password` -> the value from step 3
|
|
- `base_url` -> `https://svxportal.example.com` (used in the e-mail links)
|
|
- `mail.from` -> the sender address for verification / reset mails
|
|
- `mqtt.node_broker_host` / `mqtt.node_broker_port` -> the MQTT broker address
|
|
shown to node owners on the node pages (your public TLS port, usually 8883).
|
|
Without these the node MQTT page returns `mqtt_broker_not_configured`.
|
|
|
|
E-mail: with only `mail.from` set, PHP's `mail()` is used, which needs a
|
|
working local MTA (`/usr/sbin/sendmail`, e.g. `msmtp-mta` or `postfix`).
|
|
If you do not have one, add an SMTP relay instead and no MTA is needed:
|
|
|
|
```php
|
|
'mail' => [
|
|
'from' => 'no-reply@example.com',
|
|
'from_name' => 'SvxPortal',
|
|
'host' => 'smtp.example.com', // SSL SMTP (implicit TLS)
|
|
'port' => 465,
|
|
'user' => 'smtp-user',
|
|
'pass' => 'smtp-password',
|
|
],
|
|
```
|
|
|
|
`config.php` is listed in `.gitignore` and is never served by nginx (all
|
|
`*.php` go to PHP-FPM, which just executes `return [...]`). Keep it mode 640.
|
|
|
|
## 6. Talkgroup list file
|
|
|
|
The talkgroup list is a JSON file the portal reads and writes:
|
|
|
|
```bash
|
|
sudo cp install/tmpfiles-svxportal-tglist.conf /etc/tmpfiles.d/svxportal-tglist.conf
|
|
sudo systemd-tmpfiles --create /etc/tmpfiles.d/svxportal-tglist.conf
|
|
```
|
|
|
|
This creates `/etc/svxlink/tglist.json` owned by `www-data`. The path is
|
|
`tglist_file` in `config.php`; change both if you want it elsewhere.
|
|
|
|
### 6b. Optional: follower box that mirrors another portal's talkgroup list
|
|
|
|
If you run several reflectors and want ONE talkgroup list, one portal is the
|
|
master (its list is edited in the web) and the others pull it four times a day
|
|
with a systemd timer. Nothing but `curl` and `python3` is involved.
|
|
|
|
```bash
|
|
sudo cp install/systemd/svxportal-tglist-sync.service install/systemd/svxportal-tglist-sync.timer /etc/systemd/system/
|
|
sudo cp install/systemd/tglist-sync.sh /etc/svxlink/tglist-sync.sh && sudo chmod 755 /etc/svxlink/tglist-sync.sh
|
|
sudo cp install/systemd/default-svxportal-tglist-sync /etc/default/svxportal-tglist-sync
|
|
sudoedit /etc/default/svxportal-tglist-sync # UPSTREAM = the master's https://.../tglist.php
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now svxportal-tglist-sync.timer
|
|
sudo systemctl start svxportal-tglist-sync.service && journalctl -u svxportal-tglist-sync -n 3
|
|
```
|
|
|
|
On the master, or on a standalone box, do **not** install this (or leave
|
|
`UPSTREAM` empty; the script then exits without touching the file).
|
|
|
|
Other systemd notes:
|
|
|
|
- `svxreflector.service` is used **unmodified** from the SvxLink package. No
|
|
drop-in is needed for the portal.
|
|
- Redis runs from Debian's `redis-server@.service` template (step 3); no unit
|
|
file to add.
|
|
- There are no cron jobs. PHP session cleanup is Debian's stock
|
|
`phpsessionclean.timer`.
|
|
|
|
## 7. nginx + TLS
|
|
|
|
```bash
|
|
sudo mkdir -p /var/www/letsencrypt
|
|
sudo cp install/nginx-svxportal.conf /etc/nginx/sites-available/svxportal
|
|
sudo sed -i 's/svxportal\.example\.com/YOUR.HOST.NAME/g' /etc/nginx/sites-available/svxportal
|
|
```
|
|
|
|
Get a certificate first (the config references it):
|
|
|
|
```bash
|
|
sudo certbot certonly --webroot -w /var/www/letsencrypt -d YOUR.HOST.NAME
|
|
```
|
|
|
|
If certbot has not created `/etc/letsencrypt/options-ssl-nginx.conf` and
|
|
`ssl-dhparams.pem`, run `sudo certbot --nginx` once or remove those two lines.
|
|
|
|
```bash
|
|
sudo ln -s /etc/nginx/sites-available/svxportal /etc/nginx/sites-enabled/
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
```
|
|
|
|
Open `https://YOUR.HOST.NAME/` — the page should render. Click **Sign in** and
|
|
you should get a proper "wrong password" style error, not a 500. A 500 here
|
|
almost always means Redis (wrong socket/password, or `www-data` not in group
|
|
`redis`).
|
|
|
|
Also edit these files in the web root for your own site name: `robots.txt`,
|
|
`sitemap.xml` and the `<title>`/meta tags at the top of `index.html`.
|
|
|
|
## 8. First admin account
|
|
|
|
Accounts normally self-register and verify by e-mail. The **first** admin is
|
|
created from the command line with the bundled script; it reuses the
|
|
portal's own library, so it writes exactly what `signup.php` would:
|
|
|
|
```bash
|
|
cd /var/www/html/svxportal
|
|
sudo -u www-data php svx-mkadmin.php
|
|
```
|
|
|
|
Answer the prompts (callsign, e-mail, password, admin level). Levels:
|
|
|
|
| Level | Meaning |
|
|
|-------|---------|
|
|
| 1 | normal user |
|
|
| 2 | admin: accounts, nodes, certificates, talkgroups |
|
|
| 3 | super admin: may also see reflector/MQTT secrets and manage other admins |
|
|
|
|
Log in on the web page with that callsign. The **Admin** menu appears.
|
|
|
|
## 9. Live data backend: svxdr
|
|
|
|
The dashboard (node list, map, monitor, last-heard, trunks) fetches JSON and
|
|
Server-Sent Events from `/svxportal/api/...`, which nginx proxies to
|
|
`127.0.0.1:8098`. That backend is **svxdr**, included in `svxdr/`: one Go
|
|
binary with four modules switched on or off per box in `/etc/svxlink/svxdr.json`.
|
|
|
|
| Module | What it does | A plain reflector needs it? |
|
|
|--------|--------------|-----------------------------|
|
|
| `proxy` | reads the reflector's own `/status`, caches it, serves it enriched on 8090 | yes |
|
|
| `nodelist` | builds the node/lastheard/monitor/trunk data from the proxy + Redis, serves `/api/*` on 8098 | yes |
|
|
| `astmeta` | AllStar/Asterisk AMI talker metadata for USRP bridges | no, leave `false` |
|
|
| `xlink` | relays talker events to an MQTT broker | no, leave `false` |
|
|
|
|
Without svxdr the account/admin/talkgroup pages work, but the front page's
|
|
live views stay **empty** with no error banner (404 on `/svxportal/api/status`
|
|
in the browser console).
|
|
|
|
### Build
|
|
|
|
`svxdr/go.mod` asks for Go 1.24. Debian 13 ships 1.24. Debian 12 and Ubuntu
|
|
24.04 ship an older `golang-go`, but since Go 1.21 the `go` command downloads
|
|
the required toolchain by itself on first build, so the packaged Go works as
|
|
long as the box has internet access. The build also downloads the two Go
|
|
dependencies (go-redis, paho-mqtt) from proxy.golang.org.
|
|
|
|
```bash
|
|
go version # any 1.21+ is fine, see above
|
|
cd svxdr && CGO_ENABLED=0 go build -trimpath -o svxdr . && cd ..
|
|
ls -l svxdr/svxdr # ~12 MB static binary
|
|
sudo install -m0755 svxdr/svxdr /usr/local/bin/svxdr
|
|
```
|
|
|
|
No internet on the server? Build on any machine with Go (same command, add
|
|
`GOOS=linux GOARCH=amd64` or `arm64`) and copy the single binary over.
|
|
|
|
If `go build` complains about the toolchain, install Go from
|
|
<https://go.dev/dl/> instead of the distro package and rerun.
|
|
|
|
### Configure
|
|
|
|
```bash
|
|
sudo cp install/svxdr.json.example /etc/svxlink/svxdr.json
|
|
sudo chmod 640 /etc/svxlink/svxdr.json
|
|
sudoedit /etc/svxlink/svxdr.json
|
|
```
|
|
|
|
Set at least:
|
|
|
|
- `redis.socket` and `redis.password` -> the same Redis as `auth/config.php` (step 3).
|
|
- `proxy.reflector_status_url` -> your reflector's `HTTP_SRV_PORT` status URL.
|
|
With `HTTP_SRV_PORT=8099` in `svxreflector.conf` (as in
|
|
`install/svxreflector.conf.example`) the default `http://127.0.0.1:8099/status` is right.
|
|
- `nodelist.reflector_status_url` stays on the proxy (`http://127.0.0.1:8090/status`).
|
|
- All `CHANGEME` tokens: any random string; the ingest tokens only matter if a
|
|
remote box posts talker metadata to you.
|
|
- `nodelist.federation.seeds`: `/status` URLs of other reflectors you want in the
|
|
map that your own trunks do not reach. Empty = only what the crawl finds.
|
|
`aggregate_url` empty disables the gap detector.
|
|
|
|
The port layout must match the nginx config: reflector 8099 -> proxy 8090 ->
|
|
nodelist 8098 -> nginx `/svxportal/api/`.
|
|
|
|
### Run
|
|
|
|
```bash
|
|
sudo cp install/systemd/svxdr.service /etc/systemd/system/svxdr.service
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now svxdr
|
|
systemctl status svxdr --no-pager
|
|
curl -s http://127.0.0.1:8098/healthz # -> ok
|
|
curl -s http://127.0.0.1:8098/api/status | head -c 300
|
|
```
|
|
|
|
Config changes are picked up with `sudo systemctl reload svxdr` (SIGHUP), no
|
|
restart needed. `journalctl -u svxdr -f` shows what it polls and serves.
|
|
|
|
Endpoints the UI uses:
|
|
|
|
| Path | Used by |
|
|
|------|---------|
|
|
| `/svxportal/api/status` | node table, topology, CTCSS |
|
|
| `/svxportal/api/stream` | live updates (SSE) |
|
|
| `/svxportal/api/lastheard` | last-heard list |
|
|
| `/svxportal/api/monitor-nodes` | monitor view |
|
|
| `/svxportal/api/local-trunks` | trunks view |
|
|
|
|
## 10. Reflector admin features (optional, need sudo helpers)
|
|
|
|
The pages **Admin → Reflector**, **Certificates**, **Node control** and **MQTT**
|
|
talk to the running svxreflector on the same machine. They do it through
|
|
small root-owned helper scripts called with `sudo` from
|
|
`auth/admin_reflector_api.php`, `auth/node_cmd_api.php` and
|
|
`auth/node_auth_api.php`:
|
|
|
|
| Helper (expected in `/usr/local/sbin/`) | Purpose |
|
|
|------------------------------------------|---------|
|
|
| `svxreflector-pty` | writes `NODE BLOCK/UNBLOCK` and CA commands to the reflector's `COMMAND_PTY` |
|
|
| `svxreflector-systemctl` | restart/reload the `svxreflector` service |
|
|
| `svxreflector-users` | add/remove `[USERS]`/`[PASSWORDS]` entries in `svxreflector.conf` |
|
|
| `svx-mqtt-provision` | create/remove per-node MQTT accounts |
|
|
|
|
Those helpers and their `/etc/sudoers.d/` rules are **not part of this
|
|
repository**; they are specific to how each reflector box is set up. Write
|
|
your own for your environment (validate the callsign argument strictly, allow
|
|
`www-data` to run only that one script with `NOPASSWD`), or skip this: all
|
|
other pages work without them and the admin API returns a clear error when a
|
|
helper is missing.
|
|
|
|
The certificate pages additionally read `/etc/svxlink/svxreflector.conf` to
|
|
find `CERT_PKI_DIR` and watch `pending_csrs/` in it, so `www-data` needs read
|
|
access to the conf and the PKI directories (never to the private keys).
|
|
|
|
`install/svxreflector.conf.example` shows a complete reflector config as used
|
|
with this portal: PKI/CA paths, `COMMAND_PTY`, `HTTP_SRV_PORT`, the
|
|
`[USERS]`/`[PASSWORDS]` layout the helper edits, MQTT, and trunk/satellite
|
|
sections. All secrets and hostnames in it are placeholders.
|
|
|
|
## 11. Checklist when something does not work
|
|
|
|
- **500 on any `auth/*.php`**: `sudo tail -f /var/log/nginx/svxportal-error.log` and `journalctl -u php8.4-fpm`. Usually Redis auth or missing `php-redis`.
|
|
- **"Not connected" banner, empty node list**: no backend on `127.0.0.1:8098` (step 9).
|
|
- **No verification e-mail**: `mail()` without an MTA silently fails. Configure the SMTP relay (step 5) and check `journalctl -u php8.4-fpm` for `smtp:` lines.
|
|
- **Login works but is lost on next page**: site not on HTTPS (cookie is `Secure`) or `base_url` wrong.
|
|
- **Admin API says "exec disabled"**: `admin_reflector_api.php` was served by the default pool. Check the nginx `location =` block and that `php8.4-fpm-svxadmin.sock` exists.
|