Files
telephony-inspector/internal/capture/capturer.go

181 lines
3.5 KiB
Go
Raw Normal View History

package capture
import (
"bufio"
"fmt"
"io"
"strings"
"sync"
"telephony-inspector/internal/sip"
internalSSH "telephony-inspector/internal/ssh"
)
// Capturer handles SIP packet capture via SSH
type Capturer struct {
sshClient *internalSSH.Client
cleanup func() error
running bool
mu sync.Mutex
// Callbacks
OnPacket func(*sip.Packet)
OnError func(error)
}
// NewCapturer creates a new capturer with the given SSH config
func NewCapturer(cfg internalSSH.Config) *Capturer {
return &Capturer{
sshClient: internalSSH.NewClient(cfg),
}
}
// Connect establishes the SSH connection
func (c *Capturer) Connect() error {
return c.sshClient.Connect()
}
// Close closes the connection and stops capture
func (c *Capturer) Close() error {
c.Stop()
return c.sshClient.Close()
}
// Start begins capturing SIP traffic
func (c *Capturer) Start(iface string, port int) error {
c.mu.Lock()
if c.running {
c.mu.Unlock()
return fmt.Errorf("capture already running")
}
c.running = true
c.mu.Unlock()
// Build tcpdump command
// -l: line buffered for real-time output
// -A: print packet payload in ASCII
// -s 0: capture full packets
cmd := fmt.Sprintf("sudo tcpdump -l -A -s 0 -i %s port %d 2>/dev/null", iface, port)
stdout, stderr, cleanup, err := c.sshClient.StartCommand(cmd)
if err != nil {
c.mu.Lock()
c.running = false
c.mu.Unlock()
return err
}
c.cleanup = cleanup
// Process stdout in goroutine
go c.processStream(stdout)
// Log stderr
go c.processErrors(stderr)
return nil
}
// Stop stops the capture
func (c *Capturer) Stop() {
c.mu.Lock()
defer c.mu.Unlock()
if !c.running {
return
}
c.running = false
if c.cleanup != nil {
c.cleanup()
c.cleanup = nil
}
}
// IsRunning returns whether capture is active
func (c *Capturer) IsRunning() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.running
}
func (c *Capturer) processStream(r io.Reader) {
scanner := bufio.NewScanner(r)
var buffer strings.Builder
inSIPMessage := false
for scanner.Scan() {
c.mu.Lock()
running := c.running
c.mu.Unlock()
if !running {
break
}
line := scanner.Text()
// Detect start of SIP message
if isSIPStart(line) {
// If we were building a message, parse it
if buffer.Len() > 0 {
c.parseAndEmit(buffer.String())
buffer.Reset()
}
inSIPMessage = true
}
if inSIPMessage {
buffer.WriteString(line)
buffer.WriteString("\r\n")
// Detect end of SIP message (double CRLF or content complete)
// This is simplified - real implementation would track Content-Length
}
}
// Parse remaining buffer
if buffer.Len() > 0 {
c.parseAndEmit(buffer.String())
}
}
func (c *Capturer) processErrors(r io.Reader) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if c.OnError != nil {
c.OnError(fmt.Errorf("tcpdump: %s", scanner.Text()))
}
}
}
func (c *Capturer) parseAndEmit(raw string) {
packet, err := sip.Parse(raw)
if err != nil {
if c.OnError != nil {
c.OnError(err)
}
return
}
if packet != nil && c.OnPacket != nil {
c.OnPacket(packet)
}
}
// isSIPStart checks if a line looks like the start of a SIP message
func isSIPStart(line string) bool {
sipMethods := []string{"INVITE", "ACK", "BYE", "CANCEL", "REGISTER", "OPTIONS", "PRACK", "SUBSCRIBE", "NOTIFY", "PUBLISH", "INFO", "REFER", "MESSAGE", "UPDATE"}
// Response
if strings.HasPrefix(line, "SIP/2.0") {
return true
}
// Request
for _, m := range sipMethods {
if strings.HasPrefix(line, m+" ") {
return true
}
}
return false
}