package sipguardian import ( "bytes" "errors" "fmt" "strconv" "strings" ) // SIPMessage represents a parsed SIP message (request or response) type SIPMessage struct { // Common fields IsRequest bool SIPVersion string Headers []SIPHeader Body []byte // Request fields Method string RequestURI string // Response fields StatusCode int ReasonPhrase string } // SIPHeader represents a single SIP header type SIPHeader struct { Name string Value string } // Common errors var ( ErrInvalidSIPMessage = errors.New("invalid SIP message format") ErrEmptyMessage = errors.New("empty message") ErrNoStartLine = errors.New("missing start line") ) // ParseSIPMessage parses raw bytes into a SIPMessage structure func ParseSIPMessage(data []byte) (*SIPMessage, error) { if len(data) == 0 { return nil, ErrEmptyMessage } msg := &SIPMessage{ Headers: make([]SIPHeader, 0), } // Split into headers and body at double CRLF parts := bytes.SplitN(data, []byte("\r\n\r\n"), 2) headerSection := parts[0] if len(parts) > 1 { msg.Body = parts[1] } // Split header section into lines lines := bytes.Split(headerSection, []byte("\r\n")) if len(lines) == 0 { return nil, ErrNoStartLine } // Parse start line (first line) if err := msg.parseStartLine(string(lines[0])); err != nil { return nil, err } // Parse headers (remaining lines) for i := 1; i < len(lines); i++ { line := string(lines[i]) if line == "" { continue } // Handle header continuation (line starting with whitespace) if len(line) > 0 && (line[0] == ' ' || line[0] == '\t') { // Append to previous header if len(msg.Headers) > 0 { msg.Headers[len(msg.Headers)-1].Value += " " + strings.TrimSpace(line) } continue } // Parse header name: value colonIdx := strings.Index(line, ":") if colonIdx > 0 { name := strings.TrimSpace(line[:colonIdx]) value := strings.TrimSpace(line[colonIdx+1:]) msg.Headers = append(msg.Headers, SIPHeader{ Name: name, Value: value, }) } } return msg, nil } // parseStartLine parses the first line of a SIP message func (m *SIPMessage) parseStartLine(line string) error { parts := strings.Fields(line) if len(parts) < 2 { return ErrInvalidSIPMessage } // Check if it's a response (starts with SIP/2.0) if strings.HasPrefix(parts[0], "SIP/") { m.IsRequest = false m.SIPVersion = parts[0] // Parse status code if len(parts) < 2 { return ErrInvalidSIPMessage } code, err := strconv.Atoi(parts[1]) if err != nil { return fmt.Errorf("invalid status code: %v", err) } m.StatusCode = code // Reason phrase is the rest if len(parts) >= 3 { m.ReasonPhrase = strings.Join(parts[2:], " ") } } else { // It's a request m.IsRequest = true m.Method = parts[0] if len(parts) < 3 { return ErrInvalidSIPMessage } m.RequestURI = parts[1] m.SIPVersion = parts[2] } return nil } // Serialize converts the SIPMessage back to wire format func (m *SIPMessage) Serialize() []byte { var buf bytes.Buffer // Write start line if m.IsRequest { buf.WriteString(m.Method) buf.WriteByte(' ') buf.WriteString(m.RequestURI) buf.WriteByte(' ') buf.WriteString(m.SIPVersion) } else { buf.WriteString(m.SIPVersion) buf.WriteByte(' ') buf.WriteString(strconv.Itoa(m.StatusCode)) buf.WriteByte(' ') buf.WriteString(m.ReasonPhrase) } buf.WriteString("\r\n") // Write headers for _, h := range m.Headers { buf.WriteString(h.Name) buf.WriteString(": ") buf.WriteString(h.Value) buf.WriteString("\r\n") } // End of headers buf.WriteString("\r\n") // Write body if present if len(m.Body) > 0 { buf.Write(m.Body) } return buf.Bytes() } // GetHeader returns the first header with the given name (case-insensitive) func (m *SIPMessage) GetHeader(name string) *SIPHeader { lowerName := strings.ToLower(name) // Also check compact form compactName := getCompactForm(lowerName) for i := range m.Headers { headerLower := strings.ToLower(m.Headers[i].Name) if headerLower == lowerName || headerLower == compactName { return &m.Headers[i] } } return nil } // GetHeaders returns all headers with the given name (case-insensitive) func (m *SIPMessage) GetHeaders(name string) []*SIPHeader { lowerName := strings.ToLower(name) compactName := getCompactForm(lowerName) var result []*SIPHeader for i := range m.Headers { headerLower := strings.ToLower(m.Headers[i].Name) if headerLower == lowerName || headerLower == compactName { result = append(result, &m.Headers[i]) } } return result } // SetHeader sets a header value, replacing existing or adding new func (m *SIPMessage) SetHeader(name, value string) { lowerName := strings.ToLower(name) compactName := getCompactForm(lowerName) for i := range m.Headers { headerLower := strings.ToLower(m.Headers[i].Name) if headerLower == lowerName || headerLower == compactName { m.Headers[i].Value = value return } } // Not found, add new m.Headers = append(m.Headers, SIPHeader{Name: name, Value: value}) } // PrependHeader adds a header at the beginning of the header list func (m *SIPMessage) PrependHeader(name, value string) { newHeader := SIPHeader{Name: name, Value: value} m.Headers = append([]SIPHeader{newHeader}, m.Headers...) } // AppendHeader adds a header at the end of the header list func (m *SIPMessage) AppendHeader(name, value string) { m.Headers = append(m.Headers, SIPHeader{Name: name, Value: value}) } // RemoveHeader removes all headers with the given name (case-insensitive) func (m *SIPMessage) RemoveHeader(name string) { lowerName := strings.ToLower(name) compactName := getCompactForm(lowerName) newHeaders := make([]SIPHeader, 0, len(m.Headers)) for _, h := range m.Headers { headerLower := strings.ToLower(h.Name) if headerLower != lowerName && headerLower != compactName { newHeaders = append(newHeaders, h) } } m.Headers = newHeaders } // RemoveFirstHeader removes only the first header with the given name func (m *SIPMessage) RemoveFirstHeader(name string) bool { lowerName := strings.ToLower(name) compactName := getCompactForm(lowerName) for i, h := range m.Headers { headerLower := strings.ToLower(h.Name) if headerLower == lowerName || headerLower == compactName { m.Headers = append(m.Headers[:i], m.Headers[i+1:]...) return true } } return false } // GetCallID returns the Call-ID header value func (m *SIPMessage) GetCallID() string { if h := m.GetHeader("Call-ID"); h != nil { return h.Value } return "" } // GetFromTag extracts the tag parameter from the From header func (m *SIPMessage) GetFromTag() string { if h := m.GetHeader("From"); h != nil { return extractTagParam(h.Value) } return "" } // GetToTag extracts the tag parameter from the To header func (m *SIPMessage) GetToTag() string { if h := m.GetHeader("To"); h != nil { return extractTagParam(h.Value) } return "" } // GetCSeq returns the CSeq number and method func (m *SIPMessage) GetCSeq() (int, string) { if h := m.GetHeader("CSeq"); h != nil { parts := strings.Fields(h.Value) if len(parts) >= 2 { seq, _ := strconv.Atoi(parts[0]) return seq, parts[1] } } return 0, "" } // GetViaBranch extracts the branch parameter from the top Via header func (m *SIPMessage) GetViaBranch() string { if h := m.GetHeader("Via"); h != nil { return extractViaParam(h.Value, "branch") } return "" } // Clone creates a deep copy of the message func (m *SIPMessage) Clone() *SIPMessage { clone := &SIPMessage{ IsRequest: m.IsRequest, SIPVersion: m.SIPVersion, Method: m.Method, RequestURI: m.RequestURI, StatusCode: m.StatusCode, ReasonPhrase: m.ReasonPhrase, Headers: make([]SIPHeader, len(m.Headers)), } copy(clone.Headers, m.Headers) if len(m.Body) > 0 { clone.Body = make([]byte, len(m.Body)) copy(clone.Body, m.Body) } return clone } // getCompactForm returns the compact form of a header name func getCompactForm(name string) string { compactForms := map[string]string{ "call-id": "i", "contact": "m", "content-encoding": "e", "content-length": "l", "content-type": "c", "from": "f", "subject": "s", "supported": "k", "to": "t", "via": "v", } if compact, ok := compactForms[name]; ok { return compact } return "" } // extractTagParam extracts the tag parameter from a From/To header value func extractTagParam(headerValue string) string { // Look for ;tag= parameter idx := strings.Index(strings.ToLower(headerValue), ";tag=") if idx < 0 { return "" } tagStart := idx + 5 // len(";tag=") rest := headerValue[tagStart:] // Find end of tag value (semicolon or end of string) endIdx := strings.IndexAny(rest, ";,>") if endIdx < 0 { return rest } return rest[:endIdx] } // extractViaParam extracts a parameter from a Via header value func extractViaParam(headerValue, param string) string { lower := strings.ToLower(headerValue) search := ";" + param + "=" idx := strings.Index(lower, search) if idx < 0 { return "" } valueStart := idx + len(search) rest := headerValue[valueStart:] // Find end of value endIdx := strings.IndexAny(rest, ";,") if endIdx < 0 { return rest } return rest[:endIdx] } // IsDialogCreating returns true if this is a dialog-creating request func (m *SIPMessage) IsDialogCreating() bool { if !m.IsRequest { return false } switch m.Method { case "INVITE", "SUBSCRIBE", "REFER", "NOTIFY": return true } return false } // IsDialogTerminating returns true if this ends a dialog func (m *SIPMessage) IsDialogTerminating() bool { if !m.IsRequest { return false } return m.Method == "BYE" } // UpdateContentLength updates the Content-Length header to match body size func (m *SIPMessage) UpdateContentLength() { m.SetHeader("Content-Length", strconv.Itoa(len(m.Body))) }