Commit Diff


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. "<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"
@@ -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 <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)
+			}
+		})
+	}
+}
+
 func TestReadRequest(t *testing.T) {
 	f, err := os.Open("testdata/invite")
 	if err != nil {