diff --git a/go.mod b/go.mod index 0f40e88..95034a8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/aeolyus/gull go 1.14 require ( - github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/gorilla/mux v1.7.4 github.com/jinzhu/gorm v1.9.12 ) diff --git a/go.sum b/go.sum index 09b12f3..c3d2831 100644 --- a/go.sum +++ b/go.sum @@ -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/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= diff --git a/handlers/handlers.go b/handlers/handlers.go index 5f2aba1..a643922 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,7 +5,6 @@ import ( "github.com/aeolyus/gull/utils" "net/http" - valid "github.com/asaskevich/govalidator" "github.com/gorilla/mux" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" @@ -62,7 +61,7 @@ func (a *App) CreateShortURL(w http.ResponseWriter, r *http.Request) { return } // Verify URL is valid - if !valid.IsRequestURL(u.URL) { + if !utils.IsValidURL(u.URL) { http.Error(w, "Invalid URL", http.StatusBadRequest) return } diff --git a/utils/utils.go b/utils/utils.go index b8f0350..b1de458 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -13,8 +13,49 @@ const ( // RFC 3986 Section 2.3 URI Unreserved Characters 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 func IsValidAlias(str string) bool { valid, err := regexp.MatchString(URIUnreservedChars, str) diff --git a/utils/utils_test.go b/utils/utils_test.go index bc8b7f3..f0d5c44 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestValidAlias(t *testing.T) { +func TestIsValidAlias(t *testing.T) { tests := map[string]struct { name string str string @@ -27,8 +27,92 @@ func TestValidAlias(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - if IsValidAlias(tc.str) != tc.val { - t.Errorf("For '%s', expected %t. Got %t instead.", tc.str, tc.val, IsValidAlias(tc.str)) + res := 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) } }) }