[utils/handlers] Add regex URL validation and remove govalidator

This commit is contained in:
Richard Huang 2020-05-15 16:35:01 -07:00
parent c25d50c35f
commit 5b45a6df96
No known key found for this signature in database
GPG key ID: FFDEF81D05C2EC94
5 changed files with 129 additions and 8 deletions

1
go.mod
View file

@ -3,7 +3,6 @@ module github.com/aeolyus/gull
go 1.14 go 1.14
require ( require (
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
github.com/jinzhu/gorm v1.9.12 github.com/jinzhu/gorm v1.9.12
) )

2
go.sum
View file

@ -1,5 +1,3 @@
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=

View file

@ -5,7 +5,6 @@ import (
"github.com/aeolyus/gull/utils" "github.com/aeolyus/gull/utils"
"net/http" "net/http"
valid "github.com/asaskevich/govalidator"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/jinzhu/gorm/dialects/sqlite"
@ -62,7 +61,7 @@ func (a *App) CreateShortURL(w http.ResponseWriter, r *http.Request) {
return return
} }
// Verify URL is valid // Verify URL is valid
if !valid.IsRequestURL(u.URL) { if !utils.IsValidURL(u.URL) {
http.Error(w, "Invalid URL", http.StatusBadRequest) http.Error(w, "Invalid URL", http.StatusBadRequest)
return return
} }

View file

@ -13,8 +13,49 @@ const (
// RFC 3986 Section 2.3 URI Unreserved Characters // RFC 3986 Section 2.3 URI Unreserved Characters
URIUnreservedChars = `^([A-Za-z0-9_.~-])+$` URIUnreservedChars = `^([A-Za-z0-9_.~-])+$`
// https://gist.github.com/dperini/729294
URLRegex = `^` +
// protocol identifier (optional)
// short syntax // still required
`(?:(?:(?:https?|ftp):)?\/\/)` +
// user:pass BasicAuth (optional)
`(?:\S+(?::\S*)?@)?` +
`(?:` +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broadcast addresses
// (first & last IP address of each class)
`(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])` +
`(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}` +
`(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))` +
`|` +
// host & domain names, may end with dot
// can be replaced by a shortest alternative
// (?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.)+
`(?:` +
`(?:` +
`[a-z0-9\\u00a1-\\uffff]` +
`[a-z0-9\\u00a1-\\uffff_-]{0,62}` +
`)?` +
`[a-z0-9\\u00a1-\\uffff]\.` +
`)+` +
// TLD identifier name, may end with dot
`(?:[a-z\\u00a1-\\uffff]{2,}\.?)` +
`)` +
// port number (optional)
`(?::\d{2,5})?` +
// resource path (optional)
`(?:[/?#]\S*)?` +
`$`
) )
func IsValidURL(str string) bool {
valid, err := regexp.MatchString(URLRegex, str)
return valid && err == nil
}
// Tests whether a string is in the alphanumeric charset // Tests whether a string is in the alphanumeric charset
func IsValidAlias(str string) bool { func IsValidAlias(str string) bool {
valid, err := regexp.MatchString(URIUnreservedChars, str) valid, err := regexp.MatchString(URIUnreservedChars, str)

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestValidAlias(t *testing.T) { func TestIsValidAlias(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
name string name string
str string str string
@ -27,8 +27,92 @@ func TestValidAlias(t *testing.T) {
for name, tc := range tests { for name, tc := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if IsValidAlias(tc.str) != tc.val { res := IsValidAlias(tc.str)
t.Errorf("For '%s', expected %t. Got %t instead.", tc.str, tc.val, IsValidAlias(tc.str)) if res != tc.val {
t.Errorf("For '%s', expected %t. Got %t instead.", tc.str, tc.val, res)
}
})
}
}
func TestIsValidURL(t *testing.T) {
tests := map[string]struct {
name string
url string
val bool
}{
// Should match
"http://foo.com/blah_blah": {url: "http://foo.com/blah_blah", val: true},
"http://foo.com/blah_blah/": {url: "http://foo.com/blah_blah/", val: true},
"http://foo.com/blah_blah_(wikipedia)": {url: "http://foo.com/blah_blah_(wikipedia)", val: true},
"http://foo.com/blah_blah_(wikipedia)_(again)": {url: "http://foo.com/blah_blah_(wikipedia)_(again)", val: true},
"http://www.example.com/wpstyle/?p=364": {url: "http://www.example.com/wpstyle/?p=364", val: true},
"https://www.example.com/foo/?bar=baz&inga=42&quux": {url: "https://www.example.com/foo/?bar=baz&inga=42&quux", val: true},
"http://userid:password@example.com:8080": {url: "http://userid:password@example.com:8080", val: true},
"http://userid:password@example.com:8080/": {url: "http://userid:password@example.com:8080/", val: true},
"http://userid@example.com": {url: "http://userid@example.com", val: true},
"http://userid@example.com/": {url: "http://userid@example.com/", val: true},
"http://userid@example.com:8080": {url: "http://userid@example.com:8080", val: true},
"http://userid@example.com:8080/": {url: "http://userid@example.com:8080/", val: true},
"http://userid:password@example.com": {url: "http://userid:password@example.com", val: true},
"http://userid:password@example.com/": {url: "http://userid:password@example.com/", val: true},
"http://142.42.1.1/": {url: "http://142.42.1.1/", val: true},
"http://142.42.1.1:8080/": {url: "http://142.42.1.1:8080/", val: true},
"http://foo.com/blah_(wikipedia)#cite-1": {url: "http://foo.com/blah_(wikipedia)#cite-1", val: true},
"http://foo.com/blah_(wikipedia)_blah#cite-1": {url: "http://foo.com/blah_(wikipedia)_blah#cite-1", val: true},
"http://foo.com/unicode_(✪)_in_parens": {url: "http://foo.com/unicode_(✪)_in_parens", val: true},
"http://foo.com/(something)?after=parens": {url: "http://foo.com/(something)?after=parens", val: true},
"http://code.google.com/events/#&product=browser": {url: "http://code.google.com/events/#&product=browser", val: true},
"http://j.mp": {url: "http://j.mp", val: true},
"ftp://foo.bar/baz": {url: "ftp://foo.bar/baz", val: true},
"http://foo.bar/?q=Test%20URL-encoded%20stuff": {url: "http://foo.bar/?q=Test%20URL-encoded%20stuff", val: true},
"http://1337.net": {url: "http://1337.net", val: true},
"http://a.b-c.de": {url: "http://a.b-c.de", val: true},
"http://223.255.255.254": {url: "http://223.255.255.254", val: true},
"https://foo_bar.example.com/": {url: "https://foo_bar.example.com/", val: true},
"http://www.foo.bar./": {url: "http://www.foo.bar.", val: true},
"http://a.b--c.de/": {url: "http://a.b--c.de/", val: true},
// Should not match
"http://": {url: "http://", val: false},
"http://.": {url: "http://.", val: false},
"http://..": {url: "http://..", val: false},
"http://../": {url: "http://../", val: false},
"http://?": {url: "http://?", val: false},
"http://??": {url: "http://??", val: false},
"http://??/": {url: "http://??/", val: false},
"http://#": {url: "http://#", val: false},
"http://##": {url: "http://##", val: false},
"http://##/": {url: "http://##/", val: false},
"http://foo.bar?q=Spaces should be encoded": {url: "http://foo.bar?q=Spaces should be encoded", val: false},
"//": {url: "//", val: false},
"//a": {url: "//a", val: false},
"///a": {url: "///a", val: false},
"///": {url: "///", val: false},
"http:///a": {url: "http:///a", val: false},
"foo.com": {url: "foo.com", val: false},
"rdar://1234": {url: "rdar://1234", val: false},
"h://test": {url: "h://test", val: false},
"http:// shouldfail.com": {url: "http:// shouldfail.com", val: false},
":// should fail": {url: ":// should fail", val: false},
"http://foo.bar/foo(bar)baz quux": {url: "http://foo.bar/foo(bar)baz quux", val: false},
"ftps://foo.bar/": {url: "ftps://foo.bar/", val: false},
"http://-error-.invalid/": {url: "http://-error-.invalid/", val: false},
"http://-a.b.co": {url: "http://-a.b.co", val: false},
"http://a.b-.co": {url: "http://a.b-.co", val: false},
"http://0.0.0.0": {url: "http://0.0.0.0", val: false},
"http://10.1.1.0": {url: "http://10.1.1.0", val: false},
"http://224.1.1.1": {url: "http://224.1.1.1", val: false},
"http://1.1.1.1.1": {url: "http://1.1.1.1.1", val: false},
"http://3628126748": {url: "http://3628126748", val: false},
"http://.www.foo.bar/": {url: "http://.www.foo.bar/", val: false},
"http://.www.foo.bar./": {url: "http://.www.foo.bar./", val: false},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
res := IsValidURL(tc.url)
if res != tc.val {
t.Errorf("For '%s', expected %t. Got %t instead.", tc.url, tc.val, res)
} }
}) })
} }