feat: Initial project structure with TUI, SSH, SIP parser, and capture modules

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-19 13:46:27 +01:00
commit 3e5742d353
9 changed files with 932 additions and 0 deletions

257
internal/sip/parser.go Normal file
View File

@@ -0,0 +1,257 @@
package sip
import (
"regexp"
"strings"
)
// Method represents a SIP method
type Method string
const (
MethodINVITE Method = "INVITE"
MethodACK Method = "ACK"
MethodBYE Method = "BYE"
MethodCANCEL Method = "CANCEL"
MethodREGISTER Method = "REGISTER"
MethodOPTIONS Method = "OPTIONS"
MethodPRACK Method = "PRACK"
MethodSUBSCRIBE Method = "SUBSCRIBE"
MethodNOTIFY Method = "NOTIFY"
MethodPUBLISH Method = "PUBLISH"
MethodINFO Method = "INFO"
MethodREFER Method = "REFER"
MethodMESSAGE Method = "MESSAGE"
MethodUPDATE Method = "UPDATE"
)
// Packet represents a parsed SIP packet
type Packet struct {
Raw string
IsRequest bool
Method Method
StatusCode int
StatusText string
RequestURI string
// Common headers
CallID string
From string
To string
Via []string
CSeq string
Contact string
ContentType string
// Network info
SourceIP string
SourcePort int
DestIP string
DestPort int
// SDP body if present
SDP *SDP
}
// SDP represents a parsed SDP body
type SDP struct {
Raw string
SessionName string
Origin string
Connection string // c= line (IP for media)
MediaSections []MediaSection
}
// MediaSection represents an m= line in SDP
type MediaSection struct {
MediaType string // audio, video, etc
Port int
Protocol string // RTP/AVP, etc
Formats []string
Attributes map[string]string
}
// Parse parses a raw SIP message
func Parse(raw string) (*Packet, error) {
p := &Packet{Raw: raw}
lines := strings.Split(raw, "\r\n")
if len(lines) == 0 {
lines = strings.Split(raw, "\n")
}
if len(lines) == 0 {
return nil, nil
}
// Parse first line (request or response)
firstLine := lines[0]
if strings.HasPrefix(firstLine, "SIP/2.0") {
p.IsRequest = false
parts := strings.SplitN(firstLine, " ", 3)
if len(parts) >= 2 {
p.StatusCode = parseStatusCode(parts[1])
}
if len(parts) >= 3 {
p.StatusText = parts[2]
}
} else {
p.IsRequest = true
parts := strings.SplitN(firstLine, " ", 3)
if len(parts) >= 1 {
p.Method = Method(parts[0])
}
if len(parts) >= 2 {
p.RequestURI = parts[1]
}
}
// Parse headers
headerEnd := 0
for i, line := range lines[1:] {
if line == "" {
headerEnd = i + 2
break
}
p.parseHeader(line)
}
// Parse SDP body if present
if headerEnd > 0 && headerEnd < len(lines) && strings.Contains(p.ContentType, "application/sdp") {
body := strings.Join(lines[headerEnd:], "\r\n")
p.SDP = ParseSDP(body)
}
return p, nil
}
func (p *Packet) parseHeader(line string) {
idx := strings.Index(line, ":")
if idx < 0 {
return
}
name := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
switch strings.ToLower(name) {
case "call-id", "i":
p.CallID = value
case "from", "f":
p.From = value
case "to", "t":
p.To = value
case "via", "v":
p.Via = append(p.Via, value)
case "cseq":
p.CSeq = value
case "contact", "m":
p.Contact = value
case "content-type", "c":
p.ContentType = value
}
}
func parseStatusCode(s string) int {
var code int
for _, c := range s {
if c >= '0' && c <= '9' {
code = code*10 + int(c-'0')
} else {
break
}
}
return code
}
// ParseSDP parses an SDP body
func ParseSDP(body string) *SDP {
sdp := &SDP{Raw: body}
lines := strings.Split(body, "\r\n")
if len(lines) <= 1 {
lines = strings.Split(body, "\n")
}
var currentMedia *MediaSection
for _, line := range lines {
if len(line) < 2 || line[1] != '=' {
continue
}
typ := line[0]
value := line[2:]
switch typ {
case 'o':
sdp.Origin = value
case 's':
sdp.SessionName = value
case 'c':
sdp.Connection = value
case 'm':
if currentMedia != nil {
sdp.MediaSections = append(sdp.MediaSections, *currentMedia)
}
currentMedia = parseMediaLine(value)
case 'a':
if currentMedia != nil {
parseAttribute(currentMedia, value)
}
}
}
if currentMedia != nil {
sdp.MediaSections = append(sdp.MediaSections, *currentMedia)
}
return sdp
}
func parseMediaLine(value string) *MediaSection {
parts := strings.Fields(value)
if len(parts) < 3 {
return &MediaSection{}
}
m := &MediaSection{
MediaType: parts[0],
Protocol: parts[2],
Attributes: make(map[string]string),
}
if port := parsePort(parts[1]); port > 0 {
m.Port = port
}
if len(parts) > 3 {
m.Formats = parts[3:]
}
return m
}
func parsePort(s string) int {
var port int
for _, c := range s {
if c >= '0' && c <= '9' {
port = port*10 + int(c-'0')
} else {
break
}
}
return port
}
func parseAttribute(m *MediaSection, value string) {
if idx := strings.Index(value, ":"); idx > 0 {
m.Attributes[value[:idx]] = value[idx+1:]
} else {
m.Attributes[value] = ""
}
}
// Regular expressions for extracting IP addresses
var ipRegex = regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
// GetSDPMediaIP extracts the media IP from SDP
func (s *SDP) GetSDPMediaIP() string {
if s.Connection != "" {
matches := ipRegex.FindString(s.Connection)
if matches != "" {
return matches
}
}
return ""
}