Skip to content

Commit 31613a0

Browse files
committed
Add support for validating a sip uri in the From and To fields
Add ParsedNumber to provide details about different types of To and From values including SIP URI's
1 parent f06f9c7 commit 31613a0

File tree

4 files changed

+219
-10
lines changed

4 files changed

+219
-10
lines changed

request.go

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"net/http"
1010
"sort"
1111
"strconv"
12+
"strings"
1213
"time"
1314

1415
"github.com/pkg/errors"
16+
"github.com/ttacon/libphonenumber"
1517
"go.opencensus.io/trace"
1618
)
1719

@@ -54,6 +56,76 @@ func (r RequestValues) TimestampOrNow() time.Time {
5456
return t
5557
}
5658

59+
// From returns a Number parsed from the raw From value
60+
func (r RequestValues) From() *ParsedNumber {
61+
return ParseNumber(r["From"])
62+
}
63+
64+
// To returns a Number parsed from the raw To value
65+
func (r RequestValues) To() *ParsedNumber {
66+
return ParseNumber(r["To"])
67+
}
68+
69+
// ParseNumber parses ether a E164 number or a SIP URI returning a ParsedNumber
70+
func ParseNumber(v string) *ParsedNumber {
71+
number := &ParsedNumber{
72+
Number: v,
73+
Raw: v,
74+
}
75+
76+
if err := validPhoneNumber(v, ""); err == nil {
77+
number.Valid = true
78+
return number
79+
}
80+
81+
u, err := parseSipURI(v)
82+
if err != nil {
83+
return number
84+
}
85+
86+
parts := strings.Split(u.Hostname(), ".")
87+
l := len(parts)
88+
89+
if l < 5 || strings.Join(parts[l-2:], ".") != "twilio.com" || parts[l-4] != "sip" {
90+
return number
91+
}
92+
93+
num, err := FormatNumber(u.User.Username())
94+
if err != nil {
95+
return number
96+
}
97+
98+
number.Valid = true
99+
number.Sip = true
100+
number.Number = num
101+
number.SipDomain = strings.Join(parts[:l-4], ".")
102+
number.Region = parts[l-4]
103+
104+
return number
105+
}
106+
107+
type ParsedNumber struct {
108+
Valid bool
109+
Number string
110+
Sip bool
111+
SipDomain string
112+
Region string
113+
Raw string
114+
}
115+
116+
// FormatNumber formates a number to E164 format
117+
func FormatNumber(number string) (string, error) {
118+
num, err := libphonenumber.Parse(number, "US")
119+
if err != nil {
120+
return number, errors.WithMessagef(errors.New("Invalid phone number"), "twiml.FormatNumber(): %s", err)
121+
}
122+
if !libphonenumber.IsValidNumber(num) {
123+
return number, errors.WithMessage(errors.New("Invalid phone number"), "twiml.FormatNumber()")
124+
}
125+
126+
return libphonenumber.Format(num, libphonenumber.E164), nil
127+
}
128+
57129
// Request is a twillio request expecting a TwiML response
58130
type Request struct {
59131
host string
@@ -75,7 +147,7 @@ func (req *Request) ValidatePost(ctx context.Context, authToken string) error {
75147
span.AddAttributes(trace.StringAttribute("url", url))
76148

77149
if req.r.Method != "POST" {
78-
return fmt.Errorf("twiml.Request.ValidatePost(): Expected a POST request, received %s", req.r.Method)
150+
return errors.WithMessage(fmt.Errorf("Expected a POST request, received %s", req.r.Method), "twiml.Request.ValidatePost()")
79151
}
80152

81153
if err := req.r.ParseForm(); err != nil {
@@ -86,7 +158,7 @@ func (req *Request) ValidatePost(ctx context.Context, authToken string) error {
86158
for p := range req.r.PostForm {
87159
params = append(params, p)
88160
}
89-
sort.Sort(sort.StringSlice(params))
161+
sort.Strings(params)
90162

91163
message := url
92164
for _, p := range params {
@@ -110,7 +182,7 @@ func (req *Request) ValidatePost(ctx context.Context, authToken string) error {
110182
if len(xTwilioSigHdr) == 1 {
111183
xTwilioSig = xTwilioSigHdr[0]
112184
}
113-
return fmt.Errorf("twiml.Request.ValidatePost(): Calculated Signature: %s, failed to match X-Twilio-Signature: %s", sig, xTwilioSig)
185+
return errors.WithMessage(fmt.Errorf("Calculated Signature: %s, failed to match X-Twilio-Signature: %s", sig, xTwilioSig), "twiml.Request.ValidatePost()")
114186
}
115187

116188
// Validate data
@@ -121,7 +193,7 @@ func (req *Request) ValidatePost(ctx context.Context, authToken string) error {
121193
}
122194
if valParam, ok := fieldValidators[p]; ok {
123195
if err := valParam.valFunc(val, valParam.valParam); err != nil {
124-
return fmt.Errorf("twiml.Request.ValidatePost(): Invalid form value: %s=%s, err: %s", p, val, err)
196+
return errors.WithMessage(fmt.Errorf("Invalid form value: %s=%s, err: %s", p, val, err), "twiml.Request.ValidatePost()")
125197
}
126198
}
127199
req.Values[p] = val
@@ -138,8 +210,8 @@ type valCfg struct {
138210
var fieldValidators = map[string]valCfg{
139211
// "CallSid": "CallSid",
140212
// "AccountSid": "AccountSid",
141-
"From": valCfg{valFunc: validPhoneNumber},
142-
"To": valCfg{valFunc: validPhoneNumber},
213+
"From": valCfg{valFunc: validFromOrTo},
214+
"To": valCfg{valFunc: validFromOrTo},
143215
// "CallStatus": "CallStatus",
144216
// "ApiVersion": "ApiVersion",
145217
// "ForwardedFrom": "ForwardedFrom",

request_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package twiml
22

3-
import "testing"
3+
import (
4+
"reflect"
5+
"testing"
6+
)
47

58
func TestRequestValues_Duration(t *testing.T) {
69
tests := []struct {
@@ -25,3 +28,28 @@ func TestRequestValues_Duration(t *testing.T) {
2528
})
2629
}
2730
}
31+
32+
func TestParseNumber(t *testing.T) {
33+
type args struct {
34+
v string
35+
}
36+
tests := []struct {
37+
name string
38+
args args
39+
want *ParsedNumber
40+
}{
41+
{name: "Valid Number", args: args{v: "+18005642365"}, want: &ParsedNumber{Valid: true, Number: "+18005642365", Raw: "+18005642365"}},
42+
{name: "Valid SIP us1", args: args{v: "sips:[email protected]:5061"}, want: &ParsedNumber{Valid: true, Number: "+18005642365", Sip: true, SipDomain: "domain", Region: "sip", Raw: "sips:[email protected]:5061"}},
43+
{name: "Valid SIP us2", args: args{v: "sips:[email protected]:5061"}, want: &ParsedNumber{Valid: true, Number: "+18005642365", Sip: true, SipDomain: "domain", Region: "sip", Raw: "sips:[email protected]:5061"}},
44+
{name: "Invalid SIP sip.domain.com", args: args{v: "sips:[email protected]:5061"}, want: &ParsedNumber{Number: "sips:[email protected]:5061", Raw: "sips:[email protected]:5061"}},
45+
{name: "Invalid SIP sip2.twilio.com", args: args{v: "sips:[email protected]:5061"}, want: &ParsedNumber{Number: "sips:[email protected]:5061", Raw: "sips:[email protected]:5061"}},
46+
{name: "Invalid SIP twilio.com", args: args{v: "sips:[email protected]:5061"}, want: &ParsedNumber{Number: "sips:[email protected]:5061", Raw: "sips:[email protected]:5061"}},
47+
}
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
if got := ParseNumber(tt.args.v); !reflect.DeepEqual(got, tt.want) {
51+
t.Errorf("ParseNumber() = %v, want %v", got, tt.want)
52+
}
53+
})
54+
}
55+
}

validators.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,27 @@ package twiml
33
import (
44
"errors"
55
"fmt"
6+
"net/url"
67
"strings"
78

89
"github.com/ttacon/libphonenumber"
910
)
1011

12+
// validFromOrTo checks that a valid phone number or sip uri is provided
13+
// param of "allowempty" will allow a nil value
14+
func validFromOrTo(v interface{}, param string) error {
15+
err := validPhoneNumber(v, param)
16+
if err == nil {
17+
return nil
18+
}
19+
20+
if err := validSipURI(v, param); err == nil {
21+
return nil
22+
}
23+
24+
return err
25+
}
26+
1127
// validPhoneNumber checks that a valid phone number is provided
1228
// param of "allowempty" will allow a nil value
1329
func validPhoneNumber(v interface{}, param string) error {
@@ -29,7 +45,7 @@ func validPhoneNumber(v interface{}, param string) error {
2945
}
3046
return validatePhoneNumber(*num)
3147
default:
32-
return fmt.Errorf("validDatastoreKey: Unexpected type %T", num)
48+
return fmt.Errorf("validPhoneNumber: Unexpected type %T", num)
3349
}
3450
}
3551

@@ -63,7 +79,7 @@ func validateKeyPadEntry(v interface{}, param string) error {
6379
}
6480
return validateNumericPoundStar(*num)
6581
default:
66-
return fmt.Errorf("validDatastoreKey: Unexpected type %T", num)
82+
return fmt.Errorf("validateKeyPadEntry: Unexpected type %T", num)
6783
}
6884
}
6985

@@ -75,9 +91,73 @@ func validateNumericPoundStar(v string) error {
7591
// returns an erro if a character is found which is not in charList
7692
func characterList(s string, charList string) error {
7793
for _, c := range s {
78-
if strings.Index(charList, string(c)) == -1 {
94+
if !strings.Contains(charList, string(c)) {
7995
return fmt.Errorf("Invalid: character '%s' is not allowed", string(c))
8096
}
8197
}
8298
return nil
8399
}
100+
101+
// validSipURI checks that a valid sip uri is provided
102+
// param of "allowempty" will allow a nil value
103+
func validSipURI(v interface{}, param string) error {
104+
switch num := v.(type) {
105+
case string:
106+
if num == "" {
107+
if param == "allowempty" {
108+
return nil
109+
}
110+
return errors.New("Required")
111+
}
112+
_, err := parseSipURI(num)
113+
return err
114+
case *string:
115+
if num == nil {
116+
if param == "allowempty" {
117+
return nil
118+
}
119+
return errors.New("Required")
120+
}
121+
_, err := parseSipURI(*num)
122+
return err
123+
default:
124+
return fmt.Errorf("validPhoneNumber: Unexpected type %T", num)
125+
}
126+
}
127+
128+
func parseSipURI(uri string) (*url.URL, error) {
129+
uri = strings.ToLower(uri)
130+
131+
if !strings.HasPrefix(uri, "sip") {
132+
return nil, errors.New("Invalid SIP URI")
133+
}
134+
135+
// Insert the // after the Schema to enable full parsing and avoid Opaque
136+
if strings.HasPrefix(uri, "sips:") {
137+
uri = strings.Replace(uri, "sips:", "sips://", 1)
138+
} else if strings.HasPrefix(uri, "sip:") {
139+
uri = strings.Replace(uri, "sip:", "sip://", 1)
140+
}
141+
142+
u, err := url.Parse(uri)
143+
if err != nil {
144+
return nil, errors.New("Invalid SIP URI")
145+
}
146+
147+
// Schema should be valid
148+
if u.Scheme != "sip" && u.Scheme != "sips" {
149+
return u, errors.New("Invalid SIP URI")
150+
}
151+
152+
// Path and Opaque should be empty
153+
if u.Path != "" || u.Opaque != "" {
154+
return u, errors.New("Invalid SIP URI")
155+
}
156+
157+
// Host and User should be provided
158+
if u.Host == "" || u.User.String() == "" {
159+
return u, errors.New("Invalid SIP URI")
160+
}
161+
162+
return u, nil
163+
}

validators_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package twiml
2+
3+
import "testing"
4+
5+
func Test_validSipURI(t *testing.T) {
6+
type args struct {
7+
v interface{}
8+
param string
9+
}
10+
tests := []struct {
11+
name string
12+
args args
13+
wantErr bool
14+
}{
15+
{name: "Valid SIPS URI", args: args{v: "sips:[email protected]:5061", param: ""}, wantErr: false},
16+
{name: "Valid SIP URI", args: args{v: "sip:[email protected]:5061", param: ""}, wantErr: false},
17+
{name: "Valid", args: args{v: "", param: "allowempty"}, wantErr: false},
18+
{name: "Invaid", args: args{v: "", param: ""}, wantErr: true},
19+
{name: "Invalid SIP URI", args: args{v: "+18002368945", param: ""}, wantErr: true},
20+
{name: "Invalid URL", args: args{v: "https://[email protected]:5061", param: ""}, wantErr: true},
21+
}
22+
for _, tt := range tests {
23+
t.Run(tt.name, func(t *testing.T) {
24+
if err := validSipURI(tt.args.v, tt.args.param); (err != nil) != tt.wantErr {
25+
t.Errorf("validSipURI() error = %v, wantErr %v", err, tt.wantErr)
26+
}
27+
})
28+
}
29+
}

0 commit comments

Comments
 (0)