commit 5740e5eef190ea7e2a372f6c5da31c40910fe507
parent ef08e53ee37488a036de6aacf1895d843ab065a0
Author: Oliver Lowe <o@olowe.co>
Date: Sat, 22 Mar 2025 20:22:07 +1100
sip: handle To, From fields in the header with their own type
Now we handle the tag header parameter, and all the different forms
the values of To and From can have.
Diffstat:
2 files changed, 127 insertions(+), 10 deletions(-)
diff --git a/internal/sip/sip.go b/internal/sip/sip.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/textproto"
+ "net/url"
"strconv"
"strings"
"unicode"
@@ -31,11 +32,88 @@ type Request struct {
ContentLength int64
ContentType string
Sequence int
+ To Address
+ From Address
Via Via
Body io.Reader
}
+type URI url.URL
+
+func (u URI) String() string {
+ return "<" + (*url.URL)(&u).String() + ">"
+}
+
+type Address struct {
+ Name string
+ URI
+ Tag string
+}
+
+func (a Address) String() string {
+ var tag string
+ if a.Tag != "" {
+ tag = ";tag=" + a.Tag
+ }
+ if a.Name != "" {
+ return fmt.Sprintf("%s %s%s", a.Name, a.URI, tag)
+ }
+ return a.URI.String() + tag
+}
+
+func ParseAddress(s string) (Address, error) {
+ s = strings.TrimSpace(s)
+
+ // TODO(otl): we're parsing header parameters - should we generalise somewhere?
+ // See section 20.
+ before, tag, found := strings.Cut(s, ";")
+ if found {
+ if !strings.HasPrefix(tag, "tag=") {
+ return Address{}, fmt.Errorf("bad tag: missing %q prefix", "tag=")
+ }
+ tag = tag[4:]
+ }
+ addr := Address{Tag: tag}
+
+ // bare URI without angle brackets
+ // e.g. "sip:test@example.com"
+ u, err := url.Parse(before)
+ if err == nil {
+ addr.URI = URI(*u)
+ return addr, nil
+ }
+
+ // URI without name
+ // e.g. "<sip:test@example.com>"
+ if strings.HasPrefix(before, "<") && strings.HasSuffix(before, ">") {
+ trimmed := strings.Trim(before, "<>")
+ u, err := url.Parse(trimmed)
+ if err != nil {
+ return addr, err
+ }
+ addr.URI = URI(*u)
+ return addr, nil
+ }
+
+ i := strings.Index(before, "<")
+ if i < 0 {
+ return addr, fmt.Errorf("missing angle bracket after name")
+ }
+ j := strings.Index(before, ">")
+ if j < 0 {
+ return addr, fmt.Errorf("missing closing angle bracket")
+ }
+ addr.Name = strings.TrimSpace(before[:i])
+
+ u, err = url.Parse(before[i+1 : j])
+ if err != nil {
+ return addr, fmt.Errorf("parse uri: %w", err)
+ }
+ addr.URI = URI(*u)
+ return addr, nil
+}
+
const magicViaCookie = "z9hG4bK"
const (
@@ -96,12 +174,22 @@ func parseRequest(msg *message) (*Request, error) {
func WriteRequest(w io.Writer, req *Request) (n int64, err error) {
// section 8.1.1. We can set Max-Forwards automatically.
- required := []string{"To", "From", "CSeq", "Call-ID"}
+ required := []string{"CSeq", "Call-ID"}
for _, s := range required {
if req.Header.Get(s) == "" {
return 0, fmt.Errorf("missing field %s in header", s)
}
}
+
+ if req.To.URI.String() == "" {
+ return 0, fmt.Errorf("empty uri in to header field")
+ }
+ if req.From.URI.String() == "" {
+ return 0, fmt.Errorf("empty uri in from header field")
+ }
+ req.Header.Set("To", req.To.String())
+ req.Header.Set("From", req.From.String())
+
if req.Via.Address == "" {
return 0, fmt.Errorf("empty address in via header field")
} else if req.Via.Branch == "" {
diff --git a/internal/sip/sip_test.go b/internal/sip/sip_test.go
@@ -10,18 +10,47 @@ import (
func TestWriteRequest(t *testing.T) {
header := make(textproto.MIMEHeader)
- header.Set("Call-ID", "blabla")
- header.Set("To", "test <sip:test@example.com>")
- header.Set("From", "Oliver <sip:o@olowe.co>")
- header.Set("CSeq", "1 "+MethodRegister)
+ header.Set("Call-ID", "a84b4c76e66710@pc33.example.com")
+ header.Set("CSeq", "314159 "+MethodInvite)
+ header.Set("Contact", "<sip:alice@pc33.example.com>")
req := &Request{
- Method: MethodRegister,
- URI: "sip:test@example.com",
+ Method: MethodInvite,
+ URI: "sip:bob@example.com",
+ To: Address{Name: "Bob", URI: URI{Scheme: "sip", Opaque: "bob@example.com"}},
+ From: Address{Name: "Alice", URI: URI{Scheme: "sip", Opaque: "alice@example.com"}},
+ Via: Via{Address: "pc33.example.com", Branch: "776asdhds"},
Header: header,
}
- _, err := WriteRequest(io.Discard, req)
- if err == nil {
- t.Errorf("no error writing request with zero Via field")
+
+ if _, err := WriteRequest(io.Discard, req); err != nil {
+ t.Fatalf("write request: %v", err)
+ }
+}
+
+func TestAddress(t *testing.T) {
+ var tests = []struct {
+ name string
+ addr string
+ want string
+ }{
+ {"bare", "sip:test@example.com", "<sip:test@example.com>"},
+ {"basic", "<sip:test@example.com>", "<sip:test@example.com>"},
+ {"bare tag", "sip:+1234@example.com;tag=887s", "<sip:+1234@example.com>;tag=887s"},
+ {"tag", "<sip:test@example.com>;tag=1234", "<sip:test@example.com>;tag=1234"},
+ {"name", "Oliver <sip:test@example.com>", "Oliver <sip:test@example.com>"},
+ {"name tag", "Oliver <sip:test@example.com>;tag=1234", "Oliver <sip:test@example.com>;tag=1234"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseAddress(tt.addr)
+ if err != nil {
+ t.Fatalf("parse %q: %v", tt.addr, err)
+ }
+ if got.String() != tt.want {
+ t.Fatalf("ParseAddress(%q) = %s, want %s", tt.addr, got, tt.want)
+ }
+ })
}
}