feat: Add pcap import, file browser, logging, local capture, and stable call ordering
This commit is contained in:
199
internal/sip/callflow.go
Normal file
199
internal/sip/callflow.go
Normal file
@@ -0,0 +1,199 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user