258 lines
5.0 KiB
Go
258 lines
5.0 KiB
Go
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 ""
|
|
}
|