180 lines
6.0 KiB
Go
180 lines
6.0 KiB
Go
package sipcall
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type cliFunc func(context.Context, string) ([]byte, error)
|
|
|
|
const testInclude = "\n#tryinclude sip-call-managed.conf\n"
|
|
|
|
// Configuration is temporary and never overwrites go-sip-managed.conf or its
|
|
// revision. A previous unfinished test is a blocker, not an automatic cleanup.
|
|
func applyConfiguration(ctx context.Context, c Config, cli cliFunc) (restore func() error, err error) {
|
|
basePath := filepath.Join(c.ConfigDir, "pjsip.conf")
|
|
managed := filepath.Join(c.ConfigDir, "sip-call-managed.conf")
|
|
info, err := os.Lstat(basePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !info.Mode().IsRegular() || info.Mode().Perm() != 0600 {
|
|
return nil, errors.New("pjsip.conf must be a private regular file (0600)")
|
|
}
|
|
base, err := os.ReadFile(basePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bytes.Contains(base, []byte("sip-call-managed.conf")) {
|
|
return nil, errors.New("existing sip-call include requires manual diagnosis; refusing to alter it")
|
|
}
|
|
transport, err := cli(ctx, "pjsip show transports")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
found := false
|
|
for _, line := range strings.Split(string(transport), "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 6 && fields[0] == "Transport:" && fields[1] == "go-sip-udp" && fields[2] == "udp" && fields[len(fields)-1] == "0.0.0.0:5060" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, errors.New("approved static go-sip-udp transport is absent; no hot change or restart is permitted")
|
|
}
|
|
text := fmt.Sprintf("; Temporary isolated sip-call endpoint; no authentication or registration.\n[%s]\ntype=endpoint\ntransport=go-sip-udp\ncontext=go-sip-no-inbound\ndisallow=all\nallow=alaw\naors=%s-aor\nfrom_user=%s\ncallerid=\"%s\" <%s>\ndirect_media=no\n\n[%s-aor]\ntype=aor\ncontact=sip:%s:%d\n", c.Endpoint(), c.Endpoint(), c.CallerID, c.CallerID, c.CallerID, c.Endpoint(), c.Server, c.Port)
|
|
file, err := os.OpenFile(managed, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("test configuration already exists or cannot be created; do not overwrite: %w", err)
|
|
}
|
|
_, writeErr := file.WriteString(text)
|
|
err = errors.Join(writeErr, file.Sync(), file.Close())
|
|
backup, backupErr := os.OpenFile(filepath.Join(c.resultDir, "pjsip.conf.before"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
|
if backupErr == nil {
|
|
_, backupErr = backup.Write(base)
|
|
backupErr = errors.Join(backupErr, backup.Sync(), backup.Close())
|
|
}
|
|
err = errors.Join(err, backupErr)
|
|
appended := append(append([]byte(nil), base...), []byte(testInclude)...)
|
|
restore = func() error {
|
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
channels, e := cli(cleanupCtx, "core show channels count")
|
|
if e != nil {
|
|
return e
|
|
}
|
|
m := activeChannels.FindSubmatch(channels)
|
|
if len(m) != 2 || string(m[1]) != "0" {
|
|
return errors.New("cannot restore configuration while native channel state is active or uncertain")
|
|
}
|
|
now, e := os.ReadFile(basePath)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if !bytes.Equal(now, base) && !bytes.Equal(now, appended) {
|
|
return errors.New("pjsip.conf changed during test; private pjsip.conf.before preserved, refusing to overwrite concurrent changes")
|
|
}
|
|
if bytes.Equal(now, appended) {
|
|
if e = atomicConfig(basePath, base); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
if e := os.Remove(managed); e != nil {
|
|
return e
|
|
}
|
|
if e := reloadPJSIP(cleanupCtx, cli); e != nil {
|
|
return e
|
|
}
|
|
out, e := cli(cleanupCtx, "pjsip show endpoint "+c.Endpoint())
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if !strings.Contains(string(out), "Unable to find object") {
|
|
return errors.New("test endpoint removal could not be verified")
|
|
}
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return restore, err
|
|
}
|
|
if err = atomicConfig(basePath, appended); err != nil {
|
|
return restore, err
|
|
}
|
|
if err = reloadPJSIP(ctx, cli); err != nil {
|
|
return restore, err
|
|
}
|
|
endpoint, err := cli(ctx, "pjsip show endpoint "+c.Endpoint())
|
|
if err != nil {
|
|
return restore, err
|
|
}
|
|
for _, want := range []string{c.Endpoint() + "-aor", "alaw", "go-sip-udp", "go-sip-no-inbound"} {
|
|
if !bytes.Contains(endpoint, []byte(want)) {
|
|
return restore, errors.New("test endpoint runtime does not match requested AOR/codec/transport/context")
|
|
}
|
|
}
|
|
for _, pair := range [][2]string{{"from_user", c.CallerID}, {"callerid", fmt.Sprintf("\"%s\" <%s>", c.CallerID, c.CallerID)}} {
|
|
found := false
|
|
for _, line := range strings.Split(string(endpoint), "\n") {
|
|
k, v, ok := strings.Cut(line, ":")
|
|
if ok && strings.TrimSpace(k) == pair[0] && strings.TrimSpace(v) == pair[1] {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return restore, errors.New("test endpoint runtime caller identity differs from requested caller")
|
|
}
|
|
}
|
|
aor, err := cli(ctx, "pjsip show aor "+c.Endpoint()+"-aor")
|
|
if err != nil {
|
|
return restore, err
|
|
}
|
|
if !bytes.Contains(aor, []byte(fmt.Sprintf("sip:%s:%d", c.Server, c.Port))) {
|
|
return restore, errors.New("test AOR runtime does not match requested SIP server")
|
|
}
|
|
return restore, nil
|
|
}
|
|
|
|
// Asterisk CLI errors can exit zero. Confirm the native module response before
|
|
// checking loaded objects; pjsip reload is an optional alias, not a native command.
|
|
func reloadPJSIP(ctx context.Context, cli cliFunc) error {
|
|
out, err := cli(ctx, "module reload res_pjsip.so")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
if strings.TrimSpace(line) == "Module 'res_pjsip.so' reloaded successfully." {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("native PJSIP reload not confirmed (output_sha256=%x)", sha256.Sum256(out))
|
|
}
|
|
|
|
func atomicConfig(path string, data []byte) error {
|
|
f, err := os.CreateTemp(filepath.Dir(path), ".sip-call-write-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := f.Name()
|
|
defer os.Remove(name)
|
|
_, writeErr := f.Write(data)
|
|
err = errors.Join(writeErr, f.Sync(), f.Close())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = os.Rename(name, path); err != nil {
|
|
return err
|
|
}
|
|
dir, err := os.Open(filepath.Dir(path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors.Join(dir.Sync(), dir.Close())
|
|
}
|