Maksim Totmin cc9da3ad7d refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00

232 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SIP тестер для e2e проверки pulse-lets-go + FreeSWITCH.
// Два режима:
//
// uas — отвечает 200 OK на SIP INVITE, симулирует PBX-ноду
// uac — шлёт SIP INVITE в FreeSWITCH, симулирует оператора
//
// Примеры:
//
// ./siptest -mode uas (PBX-нода на порту 5090)
// ./siptest -mode uac -r 10 -l 100 (оператор, 10 CPS, до 100 одновременных)
package main
import (
"flag"
"fmt"
"log"
"net"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/pulse-lets-go/internal/util"
)
const (
uasPort = 5090 // порт PBX-ноды (принимает bridged вызовы)
uacPort = 5061 // порт оператора (шлёт INVITE в FS)
fsPort = 5080 // порт FreeSWITCH (external profile — для транков)
)
// Статистика
var (
callsSent atomic.Int64
callsOK atomic.Int64
callsFailed atomic.Int64
)
func main() {
mode := flag.String("mode", "uas", "uas | uac")
rate := flag.Int("r", 10, "CPS (для uac)")
limit := flag.Int("l", 100, "максимум одновременных (для uac)")
maxCalls := flag.Int("m", 1000, "всего вызовов (для uac)")
host := flag.String("host", "127.0.0.1", "FS host/IP")
dest := flag.String("dest", "88005553535", "dest номер")
caller := flag.String("caller", "74951234567", "caller_id")
flag.Parse()
log.Printf("[siptest] режим=%s", *mode)
switch *mode {
case "uas":
runUAS()
case "uac":
runUAC(*rate, *limit, *maxCalls, *dest, *caller, *host)
default:
log.Fatalf("неизвестный режим: %s", *mode)
}
}
// ── UAS: PBX-нода (отвечает 200 OK на INVITE) ────────────────
func runUAS() {
addr := fmt.Sprintf("127.0.0.1:%d", uasPort)
conn, err := net.ListenPacket("udp", addr)
if err != nil {
log.Fatalf("UAS listen %s: %v", addr, err)
}
defer conn.Close()
log.Printf("[uas] PBX-нода слушает %s", addr)
buf := make([]byte, 4096)
for {
n, remoteAddr, err := conn.ReadFrom(buf)
if err != nil {
log.Printf("[uas] ошибка чтения: %v", err)
continue
}
msg := string(buf[:n])
if strings.HasPrefix(msg, "INVITE") || strings.Contains(msg, "INVITE") {
response := buildSIPResponse(msg, addr)
time.Sleep(50 * time.Millisecond) // имитация обработки вызова
if _, err := conn.WriteTo([]byte(response), remoteAddr); err != nil {
log.Printf("[uas] ошибка ответа: %v", err)
}
log.Printf("[uas] INVITE отвечен: %v", remoteAddr)
}
// ACK и BYE игнорируем
if strings.Contains(msg, "ACK") || strings.Contains(msg, "BYE") {
continue
}
}
}
func buildSIPResponse(invite, localAddr string) string {
via := extractHeader(invite, "Via:")
from := extractHeader(invite, "From:")
to := extractHeader(invite, "To:")
callID := extractHeader(invite, "Call-ID:")
cseq := extractHeader(invite, "CSeq:")
// Добавляем tag к To если его нет
to = strings.TrimSpace(to)
if !strings.Contains(to, ";tag=") {
to += ";tag=uas-" + util.RandomHex(4)
}
return fmt.Sprintf("SIP/2.0 200 OK\r\n"+
"%s\r\n"+
"%s\r\n"+
"%s\r\n"+
"%s\r\n"+
"%s\r\n"+
"Contact: <sip:pbx@%s>\r\n"+
"Content-Length: 0\r\n"+
"\r\n",
via, from, to, callID, cseq, localAddr)
}
func extractHeader(msg, prefix string) string {
lines := strings.Split(msg, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
line = strings.TrimRight(line, "\r")
if strings.HasPrefix(line, prefix) {
return line
}
}
return prefix
}
// ── UAC: Оператор (шлёт INVITE в FS) ─────────────────────────
func runUAC(rate, limit, maxCalls int, dest, caller, host string) {
fsAddr := fmt.Sprintf("%s:%d", host, fsPort)
srcAddr := fmt.Sprintf("127.0.0.1:%d", uacPort)
localAddr, err := net.ResolveUDPAddr("udp", srcAddr)
if err != nil {
log.Fatalf("resolve %s: %v", srcAddr, err)
}
remoteAddr, err := net.ResolveUDPAddr("udp", fsAddr)
if err != nil {
log.Fatalf("resolve %s: %v", fsAddr, err)
}
conn, err := net.ListenUDP("udp", localAddr)
if err != nil {
log.Fatalf("listen %s: %v", srcAddr, err)
}
defer conn.Close()
log.Printf("[uac] оператор %s → FS %s (rate=%d/s, limit=%d, total=%d)", srcAddr, fsAddr, rate, limit, maxCalls)
sem := make(chan struct{}, limit) // ограничение одновременных
var wg sync.WaitGroup
ticker := time.NewTicker(time.Second / time.Duration(rate))
defer ticker.Stop()
// Читаем ответы от FS
go func() {
buf := make([]byte, 4096)
for {
n, _, err := conn.ReadFrom(buf)
if err != nil {
return
}
msg := string(buf[:n])
if strings.Contains(msg, "200 OK") {
callsOK.Add(1)
}
}
}()
startTime := time.Now()
callNum := 0
for callNum < maxCalls {
select {
case <-ticker.C:
sem <- struct{}{} // занимаем слот
wg.Add(1)
callNum++
go func(num int) {
defer wg.Done()
defer func() { <-sem }() // освобождаем слот
invite := buildSIPInvite(dest, caller, num, srcAddr, fsAddr)
_, err := conn.WriteTo([]byte(invite), remoteAddr)
callsSent.Add(1)
if err != nil {
callsFailed.Add(1)
return
}
}(callNum)
}
}
wg.Wait()
elapsed := time.Since(startTime)
totalCalls := callsSent.Load()
okCalls := callsOK.Load()
log.Printf("[uac] ================ ИТОГИ ================")
log.Printf(" Всего вызовов: %d", totalCalls)
log.Printf(" Успешно (200 OK): %d", okCalls)
log.Printf(" CPS (факт): %.1f", float64(totalCalls)/elapsed.Seconds())
log.Printf(" Длительность: %v", elapsed.Round(time.Millisecond))
}
func buildSIPInvite(dest, caller string, callNum int, srcAddr, fsAddr string) string {
callID := fmt.Sprintf("call-%d-%s@%s", callNum, util.RandomHex(4), "127.0.0.1")
branch := "z9hG4bK-" + util.RandomHex(8)
return fmt.Sprintf("INVITE sip:%s@%s SIP/2.0\r\n"+
"Via: SIP/2.0/UDP %s;branch=%s\r\n"+
"From: <sip:%s@127.0.0.1>;tag=caller-%d\r\n"+
"To: <sip:%s@127.0.0.1>\r\n"+
"Call-ID: %s\r\n"+
"CSeq: 1 INVITE\r\n"+
"Contact: <sip:%s@%s>\r\n"+
"Content-Length: 0\r\n"+
"\r\n",
dest, fsAddr, srcAddr, branch, caller, callNum, dest, callID, caller, srcAddr)
}