caddy-sip-guardian/topology_test.go
Ryan Malloy f76946fc41 Add SIP topology hiding feature (B2BUA-lite)
Implements RFC 3261 compliant topology hiding to protect internal
infrastructure from external attackers:

New files:
- sipmsg.go: SIP message parsing/serialization with full header support
- sipheaders.go: Via, Contact, From/To header parsing with compact forms
- dialog_state.go: Dialog and transaction state management for response correlation
- topology.go: TopologyHider handler for caddy-l4 integration
- topology_test.go: Comprehensive unit tests (26 new tests, 60 total)

Features:
- Via header insertion (proxy adds own Via, pops on response)
- Contact header rewriting (hide internal IPs behind proxy address)
- Sensitive header stripping (P-Asserted-Identity, Server, etc.)
- Call-ID anonymization (optional)
- Private IP masking in all headers
- Dialog state tracking for stateful response routing
- Transaction state for stateless operation

Caddyfile configuration:
  sip_topology_hider {
    proxy_host 203.0.113.1
    proxy_port 5060
    upstream udp/192.168.1.100:5060
    rewrite_via
    rewrite_contact
    strip_headers P-Preferred-Identity Server
  }
2025-12-07 19:02:50 -07:00

834 lines
22 KiB
Go

package sipguardian
import (
"strings"
"testing"
"time"
"go.uber.org/zap"
)
// =============================================================================
// SIP Message Parsing Tests (sipmsg.go)
// =============================================================================
func TestParseSIPRequest(t *testing.T) {
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776asdhds\r\n" +
"From: \"Alice\" <sip:alice@example.com>;tag=1928301774\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: a84b4c76e66710@pc33.example.com\r\n" +
"CSeq: 314159 INVITE\r\n" +
"Contact: <sip:alice@192.168.1.100:5060>\r\n" +
"Content-Type: application/sdp\r\n" +
"Content-Length: 4\r\n" +
"\r\n" +
"test"
msg, err := ParseSIPMessage([]byte(raw))
if err != nil {
t.Fatalf("Failed to parse SIP request: %v", err)
}
if !msg.IsRequest {
t.Error("Expected IsRequest to be true")
}
if msg.Method != "INVITE" {
t.Errorf("Expected method INVITE, got %s", msg.Method)
}
if msg.RequestURI != "sip:bob@example.com" {
t.Errorf("Expected RequestURI sip:bob@example.com, got %s", msg.RequestURI)
}
if msg.SIPVersion != "SIP/2.0" {
t.Errorf("Expected SIP/2.0, got %s", msg.SIPVersion)
}
if string(msg.Body) != "test" {
t.Errorf("Expected body 'test', got '%s'", string(msg.Body))
}
}
func TestParseSIPResponse(t *testing.T) {
raw := "SIP/2.0 200 OK\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776asdhds;received=10.0.0.1\r\n" +
"From: \"Alice\" <sip:alice@example.com>;tag=1928301774\r\n" +
"To: <sip:bob@example.com>;tag=a6c85cf\r\n" +
"Call-ID: a84b4c76e66710@pc33.example.com\r\n" +
"CSeq: 314159 INVITE\r\n" +
"Contact: <sip:bob@192.168.1.200:5060>\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, err := ParseSIPMessage([]byte(raw))
if err != nil {
t.Fatalf("Failed to parse SIP response: %v", err)
}
if msg.IsRequest {
t.Error("Expected IsRequest to be false for response")
}
if msg.StatusCode != 200 {
t.Errorf("Expected status code 200, got %d", msg.StatusCode)
}
if msg.ReasonPhrase != "OK" {
t.Errorf("Expected reason phrase 'OK', got '%s'", msg.ReasonPhrase)
}
}
func TestSIPMessageGetHeader(t *testing.T) {
raw := "REGISTER sip:example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:alice@example.com>\r\n" +
"Call-ID: test123@host\r\n" +
"CSeq: 1 REGISTER\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
// Test case-insensitive lookup
via := msg.GetHeader("via")
if via == nil {
t.Error("Failed to get Via header (lowercase)")
}
Via := msg.GetHeader("Via")
if Via == nil {
t.Error("Failed to get Via header (mixed case)")
}
// Test GetCallID helper
callID := msg.GetCallID()
if callID != "test123@host" {
t.Errorf("Expected Call-ID 'test123@host', got '%s'", callID)
}
// Test GetFromTag helper
fromTag := msg.GetFromTag()
if fromTag != "123" {
t.Errorf("Expected From tag '123', got '%s'", fromTag)
}
}
func TestSIPMessageCompactHeaders(t *testing.T) {
// Test compact header forms (RFC 3261 Section 7.3.3)
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"v: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776\r\n" +
"f: <sip:alice@example.com>;tag=123\r\n" +
"t: <sip:bob@example.com>\r\n" +
"i: compact-call-id@host\r\n" +
"CSeq: 1 INVITE\r\n" +
"m: <sip:alice@192.168.1.100>\r\n" +
"l: 0\r\n" +
"\r\n"
msg, err := ParseSIPMessage([]byte(raw))
if err != nil {
t.Fatalf("Failed to parse message with compact headers: %v", err)
}
// Via (v)
if msg.GetHeader("Via") == nil {
t.Error("Failed to get Via via compact form 'v'")
}
// Call-ID (i)
if msg.GetCallID() != "compact-call-id@host" {
t.Error("Failed to get Call-ID via compact form 'i'")
}
// Contact (m)
if msg.GetHeader("Contact") == nil {
t.Error("Failed to get Contact via compact form 'm'")
}
}
func TestSIPMessageSerialize(t *testing.T) {
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
serialized := msg.Serialize()
// Parse again and verify round-trip
msg2, err := ParseSIPMessage(serialized)
if err != nil {
t.Fatalf("Failed to parse serialized message: %v", err)
}
if msg.Method != msg2.Method {
t.Errorf("Method mismatch after round-trip: %s vs %s", msg.Method, msg2.Method)
}
if msg.GetCallID() != msg2.GetCallID() {
t.Errorf("Call-ID mismatch after round-trip")
}
}
func TestSIPMessageHeaderManipulation(t *testing.T) {
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
// Test PrependHeader (for Via insertion)
msg.PrependHeader("Via", "SIP/2.0/UDP proxy.example.com:5060;branch=z9hG4bKproxy")
vias := msg.GetHeaders("Via")
if len(vias) != 2 {
t.Errorf("Expected 2 Via headers after prepend, got %d", len(vias))
}
if !strings.Contains(vias[0].Value, "proxy.example.com") {
t.Error("Prepended Via should be first")
}
// Test RemoveFirstHeader
msg.RemoveFirstHeader("Via")
vias = msg.GetHeaders("Via")
if len(vias) != 1 {
t.Errorf("Expected 1 Via header after removal, got %d", len(vias))
}
// Test SetHeader
msg.SetHeader("User-Agent", "Test/1.0")
ua := msg.GetHeader("User-Agent")
if ua == nil || ua.Value != "Test/1.0" {
t.Error("SetHeader failed")
}
// Test RemoveHeader
msg.RemoveHeader("User-Agent")
if msg.GetHeader("User-Agent") != nil {
t.Error("RemoveHeader failed")
}
}
func TestSIPMessageClone(t *testing.T) {
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 4\r\n" +
"\r\n" +
"test"
msg, _ := ParseSIPMessage([]byte(raw))
clone := msg.Clone()
// Modify original
msg.SetHeader("Via", "modified")
msg.Body = []byte("modified")
// Clone should be unchanged
if clone.GetHeader("Via").Value == "modified" {
t.Error("Clone was affected by original modification")
}
if string(clone.Body) != "test" {
t.Error("Clone body was affected by original modification")
}
}
// =============================================================================
// Header Parsing Tests (sipheaders.go)
// =============================================================================
func TestParseViaHeader(t *testing.T) {
tests := []struct {
name string
input string
expected ViaHeader
}{
{
name: "basic UDP",
input: "SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776asdhds",
expected: ViaHeader{
Protocol: "SIP/2.0",
Transport: "UDP",
Host: "192.168.1.100",
Port: 5060,
Branch: "z9hG4bK776asdhds",
},
},
{
name: "with received and rport",
input: "SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776;received=10.0.0.1;rport=12345",
expected: ViaHeader{
Protocol: "SIP/2.0",
Transport: "UDP",
Host: "192.168.1.100",
Port: 5060,
Branch: "z9hG4bK776",
Received: "10.0.0.1",
RPort: 12345,
},
},
{
name: "TCP transport",
input: "SIP/2.0/TCP proxy.example.com:5060;branch=z9hG4bKabc",
expected: ViaHeader{
Protocol: "SIP/2.0",
Transport: "TCP",
Host: "proxy.example.com",
Port: 5060,
Branch: "z9hG4bKabc",
},
},
{
name: "default port",
input: "SIP/2.0/UDP 192.168.1.100;branch=z9hG4bK123",
expected: ViaHeader{
Protocol: "SIP/2.0",
Transport: "UDP",
Host: "192.168.1.100",
Port: 5060, // Default
Branch: "z9hG4bK123",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
via, err := ParseViaHeader(tt.input)
if err != nil {
t.Fatalf("ParseViaHeader failed: %v", err)
}
if via.Protocol != tt.expected.Protocol {
t.Errorf("Protocol: got %s, want %s", via.Protocol, tt.expected.Protocol)
}
if via.Transport != tt.expected.Transport {
t.Errorf("Transport: got %s, want %s", via.Transport, tt.expected.Transport)
}
if via.Host != tt.expected.Host {
t.Errorf("Host: got %s, want %s", via.Host, tt.expected.Host)
}
if via.Port != tt.expected.Port {
t.Errorf("Port: got %d, want %d", via.Port, tt.expected.Port)
}
if via.Branch != tt.expected.Branch {
t.Errorf("Branch: got %s, want %s", via.Branch, tt.expected.Branch)
}
})
}
}
func TestViaHeaderSerialize(t *testing.T) {
via := &ViaHeader{
Protocol: "SIP/2.0",
Transport: "UDP",
Host: "192.168.1.100",
Port: 5060,
Branch: "z9hG4bK776",
Received: "10.0.0.1",
RPort: 12345,
}
serialized := via.Serialize()
// Parse it back
via2, err := ParseViaHeader(serialized)
if err != nil {
t.Fatalf("Failed to parse serialized Via: %v", err)
}
if via2.Host != via.Host || via2.Branch != via.Branch || via2.Received != via.Received {
t.Error("Via header round-trip failed")
}
}
func TestParseContactHeader(t *testing.T) {
tests := []struct {
name string
input string
uri string
dname string
}{
{
name: "simple URI",
input: "<sip:alice@192.168.1.100:5060>",
uri: "sip:alice@192.168.1.100:5060",
dname: "",
},
{
name: "with display name",
input: "\"Alice\" <sip:alice@example.com>;expires=3600",
uri: "sip:alice@example.com",
dname: "Alice",
},
{
name: "star contact (REGISTER)",
input: "*",
uri: "*",
dname: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
contact, err := ParseContactHeader(tt.input)
if err != nil {
t.Fatalf("ParseContactHeader failed: %v", err)
}
if contact.URI != tt.uri {
t.Errorf("URI: got %s, want %s", contact.URI, tt.uri)
}
if contact.DisplayName != tt.dname {
t.Errorf("DisplayName: got %s, want %s", contact.DisplayName, tt.dname)
}
})
}
}
func TestParseFromToHeader(t *testing.T) {
tests := []struct {
name string
input string
uri string
tag string
dname string
}{
{
name: "From with tag",
input: "\"Bob\" <sip:bob@example.com>;tag=a6c85cf",
uri: "sip:bob@example.com",
tag: "a6c85cf",
dname: "Bob",
},
{
name: "To without tag",
input: "<sip:alice@example.com>",
uri: "sip:alice@example.com",
tag: "",
dname: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
header, err := ParseFromToHeader(tt.input)
if err != nil {
t.Fatalf("ParseFromToHeader failed: %v", err)
}
if header.URI != tt.uri {
t.Errorf("URI: got %s, want %s", header.URI, tt.uri)
}
if header.Tag != tt.tag {
t.Errorf("Tag: got %s, want %s", header.Tag, tt.tag)
}
if header.DisplayName != tt.dname {
t.Errorf("DisplayName: got %s, want %s", header.DisplayName, tt.dname)
}
})
}
}
func TestExtractHostFromURI(t *testing.T) {
tests := []struct {
uri string
expected string
}{
{"sip:alice@example.com", "example.com"},
{"sip:bob@192.168.1.100:5060", "192.168.1.100"},
{"sips:secure@tls.example.com:5061;transport=tls", "tls.example.com"},
{"sip:user@host;param=value", "host"},
}
for _, tt := range tests {
t.Run(tt.uri, func(t *testing.T) {
host := ExtractHostFromURI(tt.uri)
if host != tt.expected {
t.Errorf("ExtractHostFromURI(%s) = %s, want %s", tt.uri, host, tt.expected)
}
})
}
}
func TestRewriteURIHost(t *testing.T) {
tests := []struct {
uri string
newHost string
newPort int
expected string
}{
{
uri: "sip:alice@192.168.1.100:5060",
newHost: "proxy.example.com",
newPort: 5060,
expected: "sip:alice@proxy.example.com",
},
{
uri: "sip:bob@internal.lan:5060;transport=udp",
newHost: "external.example.com",
newPort: 5080,
expected: "sip:bob@external.example.com:5080;transport=udp",
},
}
for _, tt := range tests {
t.Run(tt.uri, func(t *testing.T) {
result := RewriteURIHost(tt.uri, tt.newHost, tt.newPort)
if result != tt.expected {
t.Errorf("RewriteURIHost(%s, %s, %d) = %s, want %s",
tt.uri, tt.newHost, tt.newPort, result, tt.expected)
}
})
}
}
func TestGenerateBranch(t *testing.T) {
branch := GenerateBranch()
// RFC 3261: Branch must start with "z9hG4bK"
if !strings.HasPrefix(branch, "z9hG4bK") {
t.Errorf("Branch %s does not start with z9hG4bK", branch)
}
// Should be unique
branch2 := GenerateBranch()
if branch == branch2 {
t.Error("GenerateBranch should generate unique values")
}
}
// =============================================================================
// Dialog State Tests (dialog_state.go)
// =============================================================================
func TestDialogManagerCreateAndGet(t *testing.T) {
logger := zap.NewNop()
dm := NewDialogManager(logger, 5*time.Minute)
// Create a mock SIP message
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776test\r\n" +
"From: <sip:alice@example.com>;tag=fromtag123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: testcall@example.com\r\n" +
"CSeq: 1 INVITE\r\n" +
"Contact: <sip:alice@192.168.1.100>\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
// Create dialog
state := dm.CreateDialog(msg, "192.168.1.100", 5060)
if state == nil {
t.Fatal("CreateDialog returned nil")
}
if state.CallID != "testcall@example.com" {
t.Errorf("CallID mismatch: got %s", state.CallID)
}
if state.FromTag != "fromtag123" {
t.Errorf("FromTag mismatch: got %s", state.FromTag)
}
// Get by Call-ID
retrieved := dm.GetDialogByCallID("testcall@example.com", "fromtag123")
if retrieved == nil {
t.Fatal("GetDialogByCallID returned nil")
}
if retrieved != state {
t.Error("Retrieved dialog doesn't match created dialog")
}
// Get by branch
byBranch := dm.GetDialogByBranch("z9hG4bK776test")
if byBranch == nil {
t.Fatal("GetDialogByBranch returned nil")
}
if byBranch != state {
t.Error("Retrieved dialog by branch doesn't match")
}
}
func TestDialogManagerUpdateAndConfirm(t *testing.T) {
logger := zap.NewNop()
dm := NewDialogManager(logger, 5*time.Minute)
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bKupdate\r\n" +
"From: <sip:alice@example.com>;tag=from456\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: updatetest@example.com\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
dm.CreateDialog(msg, "192.168.1.100", 5060)
// Update with To-tag (from 200 OK)
dm.UpdateDialog("updatetest@example.com", "from456", "totag789")
state := dm.GetDialogByCallID("updatetest@example.com", "from456")
if state.ToTag != "totag789" {
t.Errorf("ToTag not updated: got %s", state.ToTag)
}
// Confirm dialog
dm.ConfirmDialog("updatetest@example.com", "from456")
if !state.IsConfirmed {
t.Error("Dialog not confirmed")
}
// Terminate dialog
dm.TerminateDialog("updatetest@example.com", "from456")
if !state.IsTerminated {
t.Error("Dialog not terminated")
}
}
func TestDialogManagerRemove(t *testing.T) {
logger := zap.NewNop()
dm := NewDialogManager(logger, 5*time.Minute)
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bKremove\r\n" +
"From: <sip:alice@example.com>;tag=removetag\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: removetest@example.com\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
dm.CreateDialog(msg, "192.168.1.100", 5060)
// Remove dialog
dm.RemoveDialog("removetest@example.com", "removetag")
// Should be gone
if dm.GetDialogByCallID("removetest@example.com", "removetag") != nil {
t.Error("Dialog not removed from call-id index")
}
if dm.GetDialogByBranch("z9hG4bKremove") != nil {
t.Error("Dialog not removed from branch index")
}
}
func TestDialogManagerStats(t *testing.T) {
logger := zap.NewNop()
dm := NewDialogManager(logger, 5*time.Minute)
// Create multiple dialogs in different states
for i := 0; i < 3; i++ {
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bKstats" + string(rune('a'+i)) + "\r\n" +
"From: <sip:alice@example.com>;tag=statstag" + string(rune('a'+i)) + "\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: statstest" + string(rune('a'+i)) + "@example.com\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
dm.CreateDialog(msg, "192.168.1.100", 5060)
}
// Confirm one
dm.ConfirmDialog("statstesta@example.com", "statstaga")
// Terminate one
dm.TerminateDialog("statstestb@example.com", "statstagb")
stats := dm.GetStats()
if stats["total_dialogs"].(int) != 3 {
t.Errorf("Expected 3 total dialogs, got %v", stats["total_dialogs"])
}
if stats["confirmed_dialogs"].(int) != 1 {
t.Errorf("Expected 1 confirmed dialog, got %v", stats["confirmed_dialogs"])
}
if stats["terminated_dialogs"].(int) != 1 {
t.Errorf("Expected 1 terminated dialog, got %v", stats["terminated_dialogs"])
}
}
func TestTransactionManager(t *testing.T) {
logger := zap.NewNop()
tm := NewTransactionManager(logger, 32*time.Second)
// Create transaction
state := tm.CreateTransaction("z9hG4bKtx123", "INVITE", "192.168.1.100", 5060, "SIP/2.0/UDP original:5060")
if state == nil {
t.Fatal("CreateTransaction returned nil")
}
// Retrieve
retrieved := tm.GetTransaction("z9hG4bKtx123")
if retrieved == nil {
t.Fatal("GetTransaction returned nil")
}
if retrieved.Method != "INVITE" {
t.Errorf("Method mismatch: got %s", retrieved.Method)
}
// Remove
tm.RemoveTransaction("z9hG4bKtx123")
if tm.GetTransaction("z9hG4bKtx123") != nil {
t.Error("Transaction not removed")
}
}
// =============================================================================
// Topology Hider Tests (topology.go)
// =============================================================================
func TestTopologyHiderConfig(t *testing.T) {
th := &TopologyHider{
Enabled: true,
ProxyHost: "proxy.example.com",
ProxyPort: 5060,
RewriteVia: true,
RewriteContact: true,
AnonymizeCallID: false,
HidePrivateIPs: true,
StripHeaders: []string{"Server", "User-Agent"},
}
if !th.Enabled {
t.Error("Expected Enabled to be true")
}
if len(th.StripHeaders) != 2 {
t.Errorf("Expected 2 strip headers, got %d", len(th.StripHeaders))
}
}
func TestIsDialogCreating(t *testing.T) {
tests := []struct {
method string
expected bool
}{
{"INVITE", true},
{"SUBSCRIBE", true},
{"REFER", true},
{"NOTIFY", true},
{"REGISTER", false},
{"OPTIONS", false},
{"BYE", false},
{"CANCEL", false},
}
for _, tt := range tests {
t.Run(tt.method, func(t *testing.T) {
raw := tt.method + " sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK123\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 1 " + tt.method + "\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
if msg.IsDialogCreating() != tt.expected {
t.Errorf("%s: IsDialogCreating() = %v, want %v",
tt.method, msg.IsDialogCreating(), tt.expected)
}
})
}
}
func TestIsDialogTerminating(t *testing.T) {
byeMsg := "BYE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK123\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>;tag=456\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 2 BYE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(byeMsg))
if !msg.IsDialogTerminating() {
t.Error("BYE should be dialog terminating")
}
inviteMsg := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK123\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg2, _ := ParseSIPMessage([]byte(inviteMsg))
if msg2.IsDialogTerminating() {
t.Error("INVITE should not be dialog terminating")
}
}
func TestSensitiveHeadersList(t *testing.T) {
expected := []string{
"P-Preferred-Identity",
"P-Asserted-Identity",
"Remote-Party-ID",
"P-Charging-Vector",
"P-Charging-Function-Addresses",
"Server",
"User-Agent",
"X-Asterisk-HangupCause",
"X-Asterisk-HangupCauseCode",
}
if len(SensitiveHeaders) != len(expected) {
t.Errorf("Expected %d sensitive headers, got %d", len(expected), len(SensitiveHeaders))
}
for _, h := range expected {
found := false
for _, sh := range SensitiveHeaders {
if sh == h {
found = true
break
}
}
if !found {
t.Errorf("Missing sensitive header: %s", h)
}
}
}
func TestUpdateContentLength(t *testing.T) {
raw := "INVITE sip:bob@example.com SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK123\r\n" +
"From: <sip:alice@example.com>;tag=123\r\n" +
"To: <sip:bob@example.com>\r\n" +
"Call-ID: test@host\r\n" +
"CSeq: 1 INVITE\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
msg, _ := ParseSIPMessage([]byte(raw))
// Set a body
msg.Body = []byte("v=0\r\no=- 12345 12345 IN IP4 192.168.1.100\r\ns=-\r\n")
msg.UpdateContentLength()
cl := msg.GetHeader("Content-Length")
if cl == nil {
t.Fatal("Content-Length header missing")
}
if cl.Value != "48" {
t.Errorf("Content-Length should be 48, got %s", cl.Value)
}
}