feat: Initial project structure with TUI, SSH, SIP parser, and capture modules
This commit is contained in:
180
internal/capture/capturer.go
Normal file
180
internal/capture/capturer.go
Normal file
@@ -0,0 +1,180 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user