feat(cell): configure private loopback ARI for user Asterisk
This commit is contained in:
@@ -22,8 +22,14 @@ installs `go-sip-asterisk.service` under `systemd --user`. Production requires
|
||||
`loginctl enable-linger rogee` and a verified reboot-persistent `enabled+active`
|
||||
service; `--nonprod` allows a session-scoped Debian 12 native build but does
|
||||
not certify reboot persistence. The management-approved static `pjsip.conf`,
|
||||
ARI and RTP configuration must be supplied separately. The bundled stage has
|
||||
no live SIP trunk or dialing authorization; verify loaded endpoints and contacts
|
||||
ARI and RTP configuration must be supplied separately. For the isolated
|
||||
nonproduction user service only, `configure-asterisk-user-ari.sh` creates
|
||||
private `ari.conf`, loopback-only `http.conf` and an owner-only `ari-secret`
|
||||
without printing credentials, overwriting existing files or restarting the
|
||||
service. Restart the user service only after separately verifying zero active
|
||||
calls, then read back ARI HTTP and both `enabled`/`active` state. Do not treat
|
||||
this local test setup as a management-approved production configuration.
|
||||
The bundled stage has no live SIP trunk or dialing authorization; verify loaded endpoints and contacts
|
||||
before any call. Do not substitute another Asterisk version or a container image.
|
||||
The Go Agent package only consumes the resulting approved Cell artifact and
|
||||
reports its applied revision. Local Mock fixtures do not prove real services.
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare private ARI/HTTP config for the native nonproduction user service.
|
||||
# Does not change the service state; restart only after confirming zero calls.
|
||||
set -euo pipefail
|
||||
|
||||
config="$HOME/.config/go-sip-asterisk"
|
||||
[[ -d "$config" && -r "$config/asterisk.conf" ]] || { echo 'user Asterisk config missing; fail-closed' >&2; exit 1; }
|
||||
for name in ari.conf http.conf ari-secret; do
|
||||
[[ ! -e "$config/$name" && ! -L "$config/$name" ]] || { echo "$name already exists; refusing to overwrite" >&2; exit 1; }
|
||||
done
|
||||
umask 077
|
||||
stage="$(mktemp -d "$config/.ari-setup.XXXXXXXX")"
|
||||
cleanup() {
|
||||
rm -f -- "$stage/ari.conf" "$stage/http.conf" "$stage/ari-secret"
|
||||
rmdir -- "$stage"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
secret="$(openssl rand -hex 32)"
|
||||
[[ "$secret" =~ ^[0-9a-f]{64}$ ]] || { echo 'could not generate ARI credential' >&2; exit 1; }
|
||||
printf '%s' "$secret" >"$stage/ari-secret"
|
||||
printf '[general]\nenabled = yes\npretty = no\n\n[go-sip-agent]\ntype = user\nread_only = no\npassword = %s\npassword_format = plain\n' "$secret" >"$stage/ari.conf"
|
||||
printf '[general]\nenabled = yes\nbindaddr = 127.0.0.1\nbindport = 8088\n' >"$stage/http.conf"
|
||||
|
||||
created=()
|
||||
for name in ari-secret ari.conf http.conf; do
|
||||
if ! ln -- "$stage/$name" "$config/$name"; then
|
||||
for own_file in "${created[@]}"; do rm -f -- "$config/$own_file"; done
|
||||
echo "cannot create $name without overwriting another file; fail-closed" >&2
|
||||
exit 1
|
||||
fi
|
||||
created+=("$name")
|
||||
done
|
||||
echo 'private Asterisk ARI/HTTP configuration created; service not restarted'
|
||||
@@ -0,0 +1,55 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureUserAsteriskARIPrivateAndNoOverwrite(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
root := filepath.Join(home, ".config", "go-sip-asterisk")
|
||||
if err := os.MkdirAll(root, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "asterisk.conf"), []byte("[directories]\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := "../../deploys/cell/configure-asterisk-user-ari.sh"
|
||||
run := func() (string, error) {
|
||||
command := exec.Command("bash", path)
|
||||
command.Env = append(os.Environ(), "HOME="+home)
|
||||
output, err := command.CombinedOutput()
|
||||
return string(output), err
|
||||
}
|
||||
if output, err := run(); err != nil {
|
||||
t.Fatalf("private ARI configuration failed: %v %s", err, output)
|
||||
}
|
||||
for _, name := range []string{"ari.conf", "http.conf", "ari-secret"} {
|
||||
info, err := os.Stat(filepath.Join(root, name))
|
||||
if err != nil || info.Mode().Perm() != 0600 {
|
||||
t.Fatalf("ARI file %s must be private: info=%v err=%v", name, info, err)
|
||||
}
|
||||
}
|
||||
password, err := os.ReadFile(filepath.Join(root, "ari-secret"))
|
||||
if err != nil || len(password) != 64 {
|
||||
t.Fatalf("ARI credential must be a generated 32-byte hex secret: err=%v length=%d", err, len(password))
|
||||
}
|
||||
ari, err := os.ReadFile(filepath.Join(root, "ari.conf"))
|
||||
if err != nil || !strings.Contains(string(ari), "password = "+string(password)) || !strings.Contains(string(ari), "read_only = no") {
|
||||
t.Fatalf("ARI config did not bind generated credential: err=%v", err)
|
||||
}
|
||||
http, err := os.ReadFile(filepath.Join(root, "http.conf"))
|
||||
if err != nil || !strings.Contains(string(http), "bindaddr = 127.0.0.1") || !strings.Contains(string(http), "bindport = 8088") {
|
||||
t.Fatalf("HTTP must be explicitly restricted to the local Cell: err=%v", err)
|
||||
}
|
||||
if output, err := run(); err == nil || !strings.Contains(output, "already exists") {
|
||||
t.Fatalf("repeat install must not overwrite private credentials: err=%v output=%s", err, output)
|
||||
}
|
||||
again, err := os.ReadFile(filepath.Join(root, "ari-secret"))
|
||||
if err != nil || string(again) != string(password) {
|
||||
t.Fatal("existing ARI credential was changed by a repeat install")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user