Simplify sip-call CLI and isolate temporary SIP configuration
This commit is contained in:
@@ -9,18 +9,19 @@ make build-sip-call
|
||||
cp cmd/sip-call/sip.env.example sip-xx.env
|
||||
chmod 600 sip-xx.env
|
||||
# 填写服务商确认的线路参数及本机 Asterisk 路径后执行:
|
||||
./dist/sip-call call --sip sip-xx.env 18601010101
|
||||
./dist/sip-call sip-xx.env 18601010101
|
||||
# 需要本通抓包与 SIP/RTP 证据时:
|
||||
./dist/sip-call call --sip sip-xx.env -D 18601010101
|
||||
./dist/sip-call -D sip-xx.env 18601010101
|
||||
```
|
||||
|
||||
`--debug` 与 `-D` 等价。环境安装统一预装 tcpdump/tshark(生产与测试相同);预装不等于开启抓包。默认模式不调用或检查这些工具;调试模式要求两者可执行、指定接口可抓包且权限充分。调试抓包未就绪或在 Dial 前已退出就不拨号。参数后的号码是原始号码,仅添加该文件声明的前缀一次;不要自己先加线路前缀。
|
||||
命令格式为 `sip-call [-D|--debug] <线路.env> <原始号码>`,不再支持 `call` 子命令或 `--sip` 参数。`--debug` 与 `-D` 等价。环境安装统一预装 tcpdump/tshark(生产与测试相同);预装不等于开启抓包。默认模式不调用或检查这些工具;调试模式要求两者可执行、指定接口可抓包且权限充分。调试抓包未就绪或在 Dial 前已退出就不拨号。参数后的号码是原始号码,仅添加该文件声明的前缀一次;不要自己先加线路前缀。
|
||||
|
||||
**命令会真实拨号。每次执行都须另获线路和号码的明确授权;本文示例不是授权。** 独立工具已另经使用者批准部署到登记测试机;没有执行真实试拨。
|
||||
|
||||
## 运行条件
|
||||
|
||||
- 在已有原生 Asterisk 的主机运行;已有 `asterisk.conf`、0600 的 `pjsip.conf`、原生 PJSIP/ARI 模块及私有 `ari-secret`。
|
||||
- `pjsip.conf` 须预先配置且仅配置一次 `#tryinclude sip-call-managed.conf`。这是主机的一次性准备;工具不会自动补写,缺少或重复入口就拒绝执行。该临时文件不存在时,Asterisk 正常忽略此入口。
|
||||
- 以 Asterisk 配置的所属用户运行(登记测试机为 `rogee`),**不要 `sudo sip-call`**。root 或文件归属不匹配会在创建结果、抓包及改配置前明确拒绝;不自动改归属或放宽权限。
|
||||
- 使用现有本地 ARI 用户 `go-sip-agent` 和已配置的 loopback HTTP 地址;密码只从 `ASTERISK_CONFIG_DIR/ari-secret` 在内存读取,不复制到 ENV 或结果。
|
||||
- 已有静态 `go-sip-udp` UDP transport,绑定 `0.0.0.0:5060`;工具不更改静态 transport,不启动/重启 Asterisk。
|
||||
@@ -41,11 +42,13 @@ sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump
|
||||
|
||||
## 自动配置与退出
|
||||
|
||||
取得该 Asterisk 配置目录的独占测试锁,确认空闲后保存原 `pjsip.conf` 到本次私有目录的 `pjsip.conf.before`。临时增加独立 include/endpoint/AOR,使用原生 `module reload res_pjsip.so` 并核对明确的成功响应,不依赖可选的 `pjsip reload` 别名,不把 CLI 退出码 0 当作加载成功;随后核对实际服务地址、主叫、codec、transport 和 context。通过才执行一次原生 ARI Dial。
|
||||
取得该 Asterisk 配置目录的独占测试锁,确认空闲和固定入口已准备后,只创建本次专用 `sip-call-managed.conf`,其中包含独立 endpoint/AOR。工具不改写、不备份 `pjsip.conf`,也不修改原有业务线路;输入的 `SIP.env` 只读且保留。使用原生 `module reload res_pjsip.so` 并核对明确的成功响应,不依赖可选的 `pjsip reload` 别名,不把 CLI 退出码 0 当作加载成功;随后核对实际服务地址、主叫、codec、transport 和 context。通过才执行一次原生 ARI Dial。
|
||||
|
||||
收到真正的 `Up` 和 `StasisStart` 才记录接通;按 `HOLD_SECONDS` 保持后挂断。观察到通道结束或挂断后的 ARI 404 才记录结束确认。正常结束后恢复原 `pjsip.conf` 原字节及原所属用户/组、保持 `0600`,删除本次专用配置、reload 并确认 endpoint 已移除,不改 `go-sip-managed.conf` 或业务 SIP 加载代次。临时 include 同样使用原配置的所属用户/组;恢复前后及 reload 后均核对配置归属和权限,期间发生变化则明确报错并保留恢复资料,不覆盖变化后的文件。
|
||||
收到真正的 `Up` 和 `StasisStart` 才记录接通;按 `HOLD_SECONDS` 保持后挂断。观察到通道结束或挂断后的 ARI 404 才记录结束确认。正常结束后,仅删除本次专用配置、reload 并确认 endpoint 和 AOR 均已移除,不改 `go-sip-managed.conf` 或业务 SIP 加载代次。临时文件使用原配置的所属用户/组和 `0600`;清理前核对原配置及临时文件的内容、归属和权限,reload 后再次核对原配置,发生变化则明确报错,不覆盖或删除变化后的文件。
|
||||
|
||||
旧 `sip-call-managed.conf`/include、并发配置修改或未知通道状态均报错。无法确认结束时保留测试配置和原配置副本供人工排查,**不要直接重跑或自行清理**。恢复、抓包停止或证据解析失败都保留错误并以非零退出,不报告“全部正常”。
|
||||
已有 `sip-call-managed.conf`、并发配置修改或未知通道状态均报错。无法确认结束时保留临时配置及本次结果/证据供人工排查,**不要直接重跑或自行清理**。清理、抓包停止或证据解析失败都保留错误并以非零退出,不报告“全部正常”。
|
||||
|
||||
`~/sip-call/tool-backups/` 是安装时保存的旧版工具和来源清单,不是 SIP 配置备份,也不是拨号必需目录。本次修改不删除这些旧工具或任何历史证据。
|
||||
|
||||
## 输出与证据
|
||||
|
||||
|
||||
+5
-14
@@ -26,30 +26,21 @@ func main() {
|
||||
}
|
||||
func newRootCommand(run callRunner) *cobra.Command {
|
||||
var debug bool
|
||||
var file string
|
||||
root := &cobra.Command{Use: "sip-call", Short: "单通 Asterisk 外呼线路测试(不接 AI)", SilenceUsage: true, SilenceErrors: true, RunE: func(*cobra.Command, []string) error {
|
||||
return errors.New("请使用 sip-call call --sip <线路.env> <原始号码>")
|
||||
}}
|
||||
root.PersistentFlags().BoolVarP(&debug, "debug", "D", false, "开启本通抓包和 SIP/RTP 证据提取;缺少工具或权限就拒绝拨号")
|
||||
call := &cobra.Command{Use: "call --sip <线路.env> <原始号码>", Short: "自动临时配置线路,只拨一次,接通后短暂保持并挂断", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := sipcall.LoadConfig(file)
|
||||
root := &cobra.Command{Use: "sip-call <线路.env> <原始号码>", Short: "单通 Asterisk 外呼线路测试(不接 AI)", SilenceUsage: true, SilenceErrors: true, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := sipcall.LoadConfig(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = c.Route(args[0]); err != nil {
|
||||
if _, err = c.Route(args[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
result, runErr := run(cmd.Context(), c, args[0], debug)
|
||||
result, runErr := run(cmd.Context(), c, args[1], debug)
|
||||
if result != nil {
|
||||
runErr = errors.Join(runErr, printReport(cmd.OutOrStdout(), result))
|
||||
}
|
||||
return runErr
|
||||
}}
|
||||
call.Flags().StringVar(&file, "sip", "", "线路 ENV 文件(0600;不执行 shell)")
|
||||
if err := call.MarkFlagRequired("sip"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
root.AddCommand(call)
|
||||
root.Flags().BoolVarP(&debug, "debug", "D", false, "开启本通抓包和 SIP/RTP 证据提取;缺少工具或权限就拒绝拨号")
|
||||
return root
|
||||
}
|
||||
func printReport(out io.Writer, r *sipcall.Report) error {
|
||||
|
||||
+65
-18
@@ -11,11 +11,10 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCallCLIHasNoBusinessOrAIDependencies(t *testing.T) {
|
||||
for _, args := range [][]string{{"call", "--sip", "line.env", "18601010101"}, {"call", "--sip", "line.env", "-D", "18601010101"}, {"--debug", "call", "--sip", "line.env", "18601010101"}} {
|
||||
dir := t.TempDir()
|
||||
env := filepath.Join(dir, "line.env")
|
||||
data := `SIP_ID=line
|
||||
func testLineENV(t *testing.T) string {
|
||||
t.Helper()
|
||||
env := filepath.Join(t.TempDir(), "line.env")
|
||||
data := `SIP_ID=line
|
||||
SIP_SERVER=192.0.2.1
|
||||
SIP_PORT=5060
|
||||
SIP_CALLER_ID=BD123
|
||||
@@ -32,19 +31,28 @@ RING_SECONDS=20
|
||||
HOLD_SECONDS=3
|
||||
DEBUG_INTERFACE=any
|
||||
`
|
||||
if err := os.WriteFile(env, []byte(data), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range args {
|
||||
if args[i] == "line.env" {
|
||||
args[i] = env
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(env, []byte(data), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func TestCallCLIHasNoBusinessOrAIDependencies(t *testing.T) {
|
||||
env := testLineENV(t)
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
debug bool
|
||||
}{
|
||||
{[]string{env, "18601010101"}, false},
|
||||
{[]string{"-D", env, "18601010101"}, true},
|
||||
{[]string{"--debug", env, "18601010101"}, true},
|
||||
{[]string{env, "-D", "18601010101"}, true},
|
||||
{[]string{env, "--debug", "18601010101"}, true},
|
||||
} {
|
||||
calls := 0
|
||||
root := newRootCommand(func(ctx context.Context, c sipcall.Config, n string, debug bool) (*sipcall.Report, error) {
|
||||
calls++
|
||||
want := strings.Contains(strings.Join(args, " "), "-D") || args[0] == "--debug"
|
||||
if debug != want || n != "18601010101" || c.ID != "line" {
|
||||
if debug != tc.debug || n != "18601010101" || c.ID != "line" {
|
||||
t.Fatal("incorrect CLI inputs")
|
||||
}
|
||||
return &sipcall.Report{Status: "connected", Directory: "/tmp/result", Callee: n, DialedCallee: c.Prefix + n, Debug: debug}, nil
|
||||
@@ -52,9 +60,9 @@ DEBUG_INTERFACE=any
|
||||
out := &bytes.Buffer{}
|
||||
root.SetOut(out)
|
||||
root.SetErr(out)
|
||||
root.SetArgs(args)
|
||||
root.SetArgs(tc.args)
|
||||
if err := root.Execute(); err != nil || calls != 1 {
|
||||
t.Fatalf("%v %v", args, err)
|
||||
t.Fatalf("%v %v", tc.args, err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "result.json") {
|
||||
t.Fatal("result path missing")
|
||||
@@ -63,7 +71,13 @@ DEBUG_INTERFACE=any
|
||||
}
|
||||
|
||||
func TestCallCLIRejectsMissingOrMalformedInputsBeforeHostAccess(t *testing.T) {
|
||||
for _, args := range [][]string{{}, {"call"}, {"call", "123"}, {"call", "--sip", "missing.env", "+123"}, {"call", "--sip", "missing.env", "123", "456"}, {"agent"}} {
|
||||
env := testLineENV(t)
|
||||
for _, args := range [][]string{
|
||||
{}, {"-D"}, {env}, {env, "123", "456"}, {"missing.env", "123"},
|
||||
{env, "+123"}, {env, ""}, {env, strings.Repeat("1", 33)}, {env, "123"},
|
||||
{"--unknown", env, "123"}, {"agent"}, {"call", "123"},
|
||||
{"call", env, "123"}, {"call", "--sip", env, "123"}, {"--sip", env, "123"},
|
||||
} {
|
||||
root := newRootCommand(func(context.Context, sipcall.Config, string, bool) (*sipcall.Report, error) {
|
||||
t.Fatal("invalid input must never access host")
|
||||
return nil, nil
|
||||
@@ -77,6 +91,39 @@ func TestCallCLIRejectsMissingOrMalformedInputsBeforeHostAccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallCLIHelpShowsPositionalArguments(t *testing.T) {
|
||||
root := newRootCommand(func(context.Context, sipcall.Config, string, bool) (*sipcall.Report, error) {
|
||||
t.Fatal("help must never access host")
|
||||
return nil, nil
|
||||
})
|
||||
out := &bytes.Buffer{}
|
||||
root.SetOut(out)
|
||||
root.SetArgs([]string{"--help"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, text := range []string{"sip-call <线路.env> <原始号码>", "-D, --debug"} {
|
||||
if !strings.Contains(out.String(), text) {
|
||||
t.Fatalf("help is missing %q: %s", text, out.String())
|
||||
}
|
||||
}
|
||||
if strings.Contains(out.String(), "--sip") || strings.Contains(out.String(), "Available Commands:") {
|
||||
t.Fatalf("help still advertises subcommands or --sip: %s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallCLIPropagatesRunFailure(t *testing.T) {
|
||||
want := errors.New("unconfirmed call")
|
||||
root := newRootCommand(func(context.Context, sipcall.Config, string, bool) (*sipcall.Report, error) {
|
||||
return &sipcall.Report{Status: "unknown", Directory: "/tmp/test"}, want
|
||||
})
|
||||
root.SetOut(&bytes.Buffer{})
|
||||
root.SetArgs([]string{testLineENV(t), "18601010101"})
|
||||
if err := root.Execute(); !errors.Is(err, want) {
|
||||
t.Fatalf("run failure hidden: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugStartupFailureDoesNotClaimCaptureDisabled(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
err := printReport(out, &sipcall.Report{Status: "not_dialed", Directory: "/tmp/test", Debug: true, Failure: "tcpdump startup failed"})
|
||||
|
||||
Reference in New Issue
Block a user