commit 5740e5eef190ea7e2a372f6c5da31c40910fe507 from: Oliver Lowe date: Sat Mar 22 09:22:07 2025 UTC 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. commit - ef08e53ee37488a036de6aacf1895d843ab065a0 commit + 5740e5eef190ea7e2a372f6c5da31c40910fe507 blob - bf9410a1356f23098df574452efc9126020f4ea7 blob + ef73037cc9d6f7a5afef4090b30f7e5fb2bc9e5b --- internal/sip/sip.go +++ internal/sip/sip.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/textproto" + "net/url" "strconv" "strings" "unicode" @@ -31,9 +32,86 @@ 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. "" + 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" @@ -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 == "" { blob - 6e2d88dd488c1be65fc6cdaeb96bfd4647f2611d blob + 3a2f63e678d99994c90bf9877c1f1cad2ff11568 --- internal/sip/sip_test.go +++ internal/sip/sip_test.go @@ -10,21 +10,50 @@ import ( func TestWriteRequest(t *testing.T) { header := make(textproto.MIMEHeader) - header.Set("Call-ID", "blabla") - header.Set("To", "test ") - header.Set("From", "Oliver ") - header.Set("CSeq", "1 "+MethodRegister) + header.Set("Call-ID", "a84b4c76e66710@pc33.example.com") + header.Set("CSeq", "314159 "+MethodInvite) + header.Set("Contact", "") 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", ""}, + {"basic", "", ""}, + {"bare tag", "sip:+1234@example.com;tag=887s", ";tag=887s"}, + {"tag", ";tag=1234", ";tag=1234"}, + {"name", "Oliver ", "Oliver "}, + {"name tag", "Oliver ;tag=1234", "Oliver ;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) + } + }) + } +} + func TestReadRequest(t *testing.T) { f, err := os.Open("testdata/invite") if err != nil {