Files
telephony-inspector/internal/sip/callflow.go

200 lines
4.3 KiB
Go

package sip
import (
"sync"
"time"
)
// CallFlow represents a SIP call with all its packets
type CallFlow struct {
CallID string
Packets []*Packet
StartTime time.Time
EndTime time.Time
// Summary info
From string
To string
State CallState
}
// CallState represents the current state of a call
type CallState string
const (
CallStateInitial CallState = "Initial"
CallStateRinging CallState = "Ringing"
CallStateConnected CallState = "Connected"
CallStateTerminated CallState = "Terminated"
CallStateFailed CallState = "Failed"
)
// CallFlowStore stores and manages call flows
type CallFlowStore struct {
mu sync.RWMutex
flows map[string]*CallFlow
}
// NewCallFlowStore creates a new call flow store
func NewCallFlowStore() *CallFlowStore {
return &CallFlowStore{
flows: make(map[string]*CallFlow),
}
}
// AddPacket adds a packet to the appropriate call flow
func (s *CallFlowStore) AddPacket(p *Packet) *CallFlow {
s.mu.Lock()
defer s.mu.Unlock()
if p.CallID == "" {
return nil
}
flow, exists := s.flows[p.CallID]
if !exists {
flow = &CallFlow{
CallID: p.CallID,
Packets: make([]*Packet, 0),
StartTime: time.Now(),
From: p.From,
To: p.To,
State: CallStateInitial,
}
s.flows[p.CallID] = flow
}
flow.Packets = append(flow.Packets, p)
flow.EndTime = time.Now()
// Update call state based on packet
s.updateState(flow, p)
return flow
}
// updateState updates the call state based on the packet
func (s *CallFlowStore) updateState(flow *CallFlow, p *Packet) {
if p.IsRequest {
switch p.Method {
case MethodINVITE:
if flow.State == CallStateInitial {
flow.State = CallStateInitial
}
case MethodBYE, MethodCANCEL:
flow.State = CallStateTerminated
}
} else {
// Response
switch {
case p.StatusCode >= 100 && p.StatusCode < 200:
if p.StatusCode == 180 || p.StatusCode == 183 {
flow.State = CallStateRinging
}
case p.StatusCode >= 200 && p.StatusCode < 300:
flow.State = CallStateConnected
case p.StatusCode >= 400:
flow.State = CallStateFailed
}
}
}
// GetFlow returns a call flow by Call-ID
func (s *CallFlowStore) GetFlow(callID string) *CallFlow {
s.mu.RLock()
defer s.mu.RUnlock()
return s.flows[callID]
}
// GetAllFlows returns all call flows
func (s *CallFlowStore) GetAllFlows() []*CallFlow {
s.mu.RLock()
defer s.mu.RUnlock()
flows := make([]*CallFlow, 0, len(s.flows))
for _, f := range s.flows {
flows = append(flows, f)
}
return flows
}
// GetRecentFlows returns the N most recent call flows sorted by StartTime (oldest first)
func (s *CallFlowStore) GetRecentFlows(n int) []*CallFlow {
flows := s.GetAllFlows()
// Sort by start time ascending (oldest first), then by CallID for stable order
for i := 0; i < len(flows)-1; i++ {
for j := i + 1; j < len(flows); j++ {
// Compare by StartTime first
if flows[i].StartTime.After(flows[j].StartTime) {
flows[i], flows[j] = flows[j], flows[i]
} else if flows[i].StartTime.Equal(flows[j].StartTime) {
// If same time, sort by CallID for stable order
if flows[i].CallID > flows[j].CallID {
flows[i], flows[j] = flows[j], flows[i]
}
}
}
}
if len(flows) > n {
flows = flows[:n]
}
return flows
}
// Count returns the number of call flows
func (s *CallFlowStore) Count() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.flows)
}
// Summary returns a string summary of a packet for display
func (p *Packet) Summary() string {
if p.IsRequest {
return string(p.Method)
}
return formatStatusCode(p.StatusCode, p.StatusText)
}
func formatStatusCode(code int, text string) string {
if text != "" {
return text
}
switch code {
case 100:
return "100 Trying"
case 180:
return "180 Ringing"
case 183:
return "183 Session Progress"
case 200:
return "200 OK"
case 400:
return "400 Bad Request"
case 401:
return "401 Unauthorized"
case 403:
return "403 Forbidden"
case 404:
return "404 Not Found"
case 408:
return "408 Request Timeout"
case 480:
return "480 Temporarily Unavailable"
case 486:
return "486 Busy Here"
case 487:
return "487 Request Terminated"
case 488:
return "488 Not Acceptable Here"
case 500:
return "500 Server Internal Error"
case 503:
return "503 Service Unavailable"
default:
return string(rune('0'+code/100)) + "xx"
}
}