Skip to content

Commit 29d4cf6

Browse files
Consolidating validate and extract sip URI into one util function (#1864)
1 parent a9a1bff commit 29d4cf6

4 files changed

Lines changed: 104 additions & 66 deletions

File tree

‎.changeset/sip-name-addr-parser.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"github.com/livekit/protocol": patch
3+
"@livekit/protocol": patch
4+
---
5+
6+
Add `ParseSIPNameAddr`, which validates a name-addr value and returns its user part, replacing `sip.URIUser`

‎livekit/sip_validation.go‎

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,10 @@ func findAngleBrackets(value string) (int, int, error) {
283283
start := -1
284284
end := -1
285285

286-
for i, r := range value {
286+
// A leading quoted display name may contain angle brackets of its own.
287+
offset := len(value) - len(skipQuotedDisplayName(value))
288+
for i, r := range value[offset:] {
289+
i += offset
287290
switch r {
288291
case '<':
289292
if start != -1 {
@@ -313,27 +316,52 @@ func findAngleBrackets(value string) (int, int, error) {
313316

314317
// validateNameAddrHeader validates headers that use name-addr format per RFC 3261 Section 20.10
315318
func validateNameAddrHeader(value string) error {
319+
_, err := ParseSIPNameAddr(value)
320+
return err
321+
}
322+
323+
// ParseSIPNameAddr validates a value in name-addr or addr-spec format, as found
324+
// in From, To and Refer-To headers, and returns the user part of its URI. The
325+
// user is empty when the URI has none.
326+
func ParseSIPNameAddr(value string) (string, error) {
316327
// RFC 3261 Section 20.10 - name-addr format
317328
// name-addr = [display-name] <addr-spec>
318329
// addr-spec = SIP-URI / SIPS-URI / absoluteURI
319330

320331
uri := value
321332
start, end, err := findAngleBrackets(value)
322333
if err != nil {
323-
return err
334+
return "", err
324335
}
325336
if start >= 0 || end >= 0 {
326337
uri = value[start+1 : end]
327338
if err := validateDisplayName(strings.TrimSpace(value[:start])); err != nil {
328-
return err
339+
return "", err
329340
}
330341
} else {
331342
// This is a bare URI, and should comply with addr-spec, no special characters
332343
if strings.ContainsAny(value, ",? ") {
333-
return errors.New("bare URI with special characters")
344+
return "", errors.New("bare URI with special characters")
334345
}
335346
}
336-
return validateURI(uri)
347+
return uriUser(uri)
348+
}
349+
350+
// skipQuotedDisplayName returns the part of a name-addr after a leading quoted
351+
// display name. An unterminated quote returns the value unchanged.
352+
func skipQuotedDisplayName(s string) string {
353+
if !strings.HasPrefix(s, "\"") {
354+
return s
355+
}
356+
for i := 1; i < len(s); i++ {
357+
switch s[i] {
358+
case '\\':
359+
i++
360+
case '"':
361+
return s[i+1:]
362+
}
363+
}
364+
return s
337365
}
338366

339367
// validateDisplayName validates a display name in name-addr format
@@ -360,20 +388,31 @@ func validateDisplayName(displayName string) error {
360388
return nil
361389
}
362390

363-
// validateURI validates URIs that can appear in name-addr format
364-
func validateURI(uri string) error {
391+
// uriUser validates a URI that can appear in name-addr format and returns its
392+
// user part: up to the "@" for sip and sips, the whole number for tel, without
393+
// parameters or headers.
394+
func uriUser(uri string) (string, error) {
365395
// Just do the basics, full validation should be done by sip service
366-
scheme := strings.SplitN(uri, ":", 2)[0]
367-
if scheme != "sip" && scheme != "sips" && scheme != "tel" {
396+
scheme, rest, _ := strings.Cut(uri, ":")
397+
var user string
398+
switch strings.ToLower(scheme) {
399+
case "sip", "sips":
400+
if u, _, found := strings.Cut(rest, "@"); found {
401+
user = u
402+
}
403+
case "tel":
368404
// Technically, it either needs to be sip/s: or scheme://...
369405
// Thus, tel: uri should not be supported here... but we allow it because of de-facto usage.
370-
return errors.New("uri: scheme not one of sip, sips, or tel")
406+
user = rest
407+
default:
408+
return "", errors.New("uri: scheme not one of sip, sips, or tel")
371409
}
372410

373411
// Just no spaces, proper validation should be done in sip service
374412
if strings.Contains(uri, " ") {
375-
return errors.New("uri: contains spaces")
413+
return "", errors.New("uri: contains spaces")
376414
}
377415

378-
return nil
416+
user, _, _ = strings.Cut(user, ";")
417+
return user, nil
379418
}

‎livekit/sip_validation_test.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,13 @@ var ValidNameAddrHeaders = []string{
165165
`Alice Johnson <sip:u3@example.com>`,
166166
`"Alice \"Ace\" Johnson's device\\" <sip:user@host>`, // quoted + escapes
167167
`"Alice & Bob" <sip:user@host>`, // special char in quoted display
168+
`"Smith <Jr>" <sip:user@host>`, // angle brackets in quoted display
168169
`Alice <sip:user@host;transport=tcp?X-Custom=val>;tag=abc`,
170+
171+
// scheme is case-insensitive (RFC 3261 §19.1.4)
172+
`SIP:user@host`,
173+
`<SIPS:user@host>`,
174+
`Alice <Tel:+15551234567>`,
169175
}
170176

171177
// InvalidNameAddrHeaders contains invalid Name-addr format headers
@@ -304,3 +310,44 @@ func TestForbiddenSipHeaderNames(t *testing.T) {
304310
})
305311
}
306312
}
313+
314+
func TestParseSIPNameAddr(t *testing.T) {
315+
cases := []struct {
316+
value string
317+
user string
318+
ok bool
319+
}{
320+
{`sip:911@example.com`, "911", true},
321+
{`sips:911@example.com`, "911", true},
322+
{`tel:911`, "911", true},
323+
{`tel:+1-555-123-4567`, "+1-555-123-4567", true},
324+
{`<sip:911@example.com>`, "911", true},
325+
{`<sip:911@example.com;transport=tcp>`, "911", true},
326+
{`<sip:911@example.com?X-Custom=val>`, "911", true},
327+
{`<sip:911;phone-context=example.com@example.com>`, "911", true},
328+
{`<tel:911;phone-context=example.com>`, "911", true},
329+
{`<sip:911@example.com>;tag=abc`, "911", true},
330+
{`Alice <sip:911@example.com>`, "911", true},
331+
{`"Alice Johnson" <sip:911@example.com>`, "911", true},
332+
{`"Smith <Jr>" <sip:911@example.com>`, "911", true},
333+
{`SIP:911@example.com`, "911", true},
334+
{`sip:example.com`, "", true},
335+
{`<sip:u7@example.com`, "", false},
336+
{`Alice sip:u13@example.com`, "", false},
337+
{`"Alice <sip:911@example.com>`, "", false},
338+
{`http://example.com`, "", false},
339+
{`911`, "", false},
340+
{``, "", false},
341+
}
342+
for _, c := range cases {
343+
t.Run(c.value, func(t *testing.T) {
344+
user, err := ParseSIPNameAddr(c.value)
345+
if c.ok != (err == nil) {
346+
t.Fatalf("ParseSIPNameAddr(%q) error = %v, want ok=%v", c.value, err, c.ok)
347+
}
348+
if user != c.user {
349+
t.Errorf("ParseSIPNameAddr(%q) user = %q, want %q", c.value, user, c.user)
350+
}
351+
})
352+
}
353+
}

‎sip/phonenumber.go‎

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package sip
22

33
import (
4-
"strings"
5-
64
"github.com/livekit/protocol/livekit"
75
"github.com/nyaruka/phonenumbers"
86
)
@@ -72,55 +70,3 @@ func IsEmergencyNumber(number string) bool {
7270
}
7371
return false
7472
}
75-
76-
// URIUser returns the user part of a sip, sips or tel URI, with the display
77-
// name, angle brackets, host and parameters removed. A URI with no user part
78-
// and anything that is not a URI returns an empty string.
79-
func URIUser(uri string) string {
80-
uri = skipQuotedDisplayName(uri)
81-
if i := strings.Index(uri, "<"); i >= 0 {
82-
uri = uri[i+1:]
83-
if j := strings.Index(uri, ">"); j >= 0 {
84-
uri = uri[:j]
85-
}
86-
}
87-
colon := strings.Index(uri, ":")
88-
if colon < 0 {
89-
return ""
90-
}
91-
scheme, rest := strings.ToLower(uri[:colon]), uri[colon+1:]
92-
var user string
93-
switch scheme {
94-
case "sip", "sips":
95-
at := strings.Index(rest, "@")
96-
if at < 0 {
97-
return ""
98-
}
99-
user = rest[:at]
100-
case "tel":
101-
user = rest
102-
default:
103-
return ""
104-
}
105-
if i := strings.IndexAny(user, ";?"); i >= 0 {
106-
user = user[:i]
107-
}
108-
return user
109-
}
110-
111-
// skipQuotedDisplayName returns the part of a name-addr after a leading quoted
112-
// display name, so that a "<" inside the quotes is not taken as the URI start.
113-
func skipQuotedDisplayName(s string) string {
114-
if !strings.HasPrefix(s, "\"") {
115-
return s
116-
}
117-
for i := 1; i < len(s); i++ {
118-
switch s[i] {
119-
case '\\':
120-
i++
121-
case '"':
122-
return s[i+1:]
123-
}
124-
}
125-
return s
126-
}

0 commit comments

Comments
 (0)