From dd70b34dcbbb263baa39752a60feb163abebdd7f Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Fri, 3 Dec 2021 06:58:37 +0500 Subject: [PATCH] :label: Created me domain --- internal/domain/me.go | 114 +++++++++++++++++++++++++++++++++++++ internal/domain/me_test.go | 70 +++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 internal/domain/me.go create mode 100644 internal/domain/me_test.go diff --git a/internal/domain/me.go b/internal/domain/me.go new file mode 100644 index 0000000..9dc0c5d --- /dev/null +++ b/internal/domain/me.go @@ -0,0 +1,114 @@ +package domain + +import ( + "fmt" + "net" + "strings" + "testing" + + "github.com/stretchr/testify/require" + http "github.com/valyala/fasthttp" +) + +// Me is a user URL identifier. +type Me struct { + uri *http.URI + isValid bool +} + +// UnmarshalForm implements a custom form.Unmarshaler. +func (me *Me) UnmarshalForm(v []byte) error { + if err := me.Parse(v); err != nil { + return fmt.Errorf("cannot unmarshal form: %w", err) + } + + return nil +} + +// Parse parse and validate me identifier. +func (me *Me) Parse(v []byte) error { + if me.uri != nil { + http.ReleaseURI(me.uri) + } + + me.uri = http.AcquireURI() + if err := me.uri.Parse(nil, v); err != nil { + return fmt.Errorf("cannot parse me: %w", err) + } + + // NOTE(toby3d): MUST have either an https or http scheme + scheme := string(me.uri.Scheme()) + if scheme != "http" && scheme != "https" { + return nil + } + + // NOTE(toby3d): MUST contain a path component (/ is a valid path) + // NOTE(toby3d): MUST NOT contain single-dot or double-dot path segments + path := string(me.uri.PathOriginal()) + if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") { + return nil + } + + // NOTE(toby3d): MUST NOT contain a fragment component + if me.uri.Hash() != nil { + return nil + } + + // NOTE(toby3d): MUST NOT contain a username or password component + if me.uri.Username() != nil || me.uri.Password() != nil { + return nil + } + + // NOTE(toby3d): host names MUST be domain names + host := string(me.uri.Host()) + if host == "" { + return nil + } + + // NOTE(toby3d): MUST NOT contain a port + if _, _, err := net.SplitHostPort(host); err == nil { + return nil + } + + // NOTE(toby3d): MUST NOT be ipv4 or ipv6 addresses + if net.ParseIP(host) != nil { + return nil + } + + me.isValid = true + + return nil +} + +// String returns string representation of Me. +func (me *Me) String() string { + if me.uri == nil { + return "" + } + + return me.uri.String() +} + +// URI returns copy of parsed *fasthttp.URI. +// This copy MUST be released via fasthttp.ReleaseURI. +func (me *Me) URI() *http.URI { + u := http.AcquireURI() + me.uri.CopyTo(u) + + return u +} + +// IsValid returns true if Me is a valid identifier. +func (me *Me) IsValid() bool { + return me.isValid +} + +// TestMe returns a valid testing Me. +func TestMe(tb testing.TB) *Me { + tb.Helper() + + me := new(Me) + require.NoError(tb, me.Parse([]byte("https://user.example.net/"))) + + return me +} diff --git a/internal/domain/me_test.go b/internal/domain/me_test.go new file mode 100644 index 0000000..18b3cf8 --- /dev/null +++ b/internal/domain/me_test.go @@ -0,0 +1,70 @@ +package domain_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "source.toby3d.me/website/oauth/internal/domain" +) + +func TestMeIsValid(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + input string + isValid bool + }{{ + name: "valid", + input: "https://example.com/", + isValid: true, + }, { + name: "valid with path", + input: "https://example.com/username", + isValid: true, + }, { + name: "valid with query", + input: "https://example.com/users?id=100", + isValid: true, + }, { + name: "missing scheme", + input: "example.com", + isValid: false, + }, { + name: "invalid scheme", + input: "mailto:user@example.com", + isValid: false, + }, { + name: "contains a double-dot path segment", + input: "https://example.com/foo/../bar", + isValid: false, + }, { + name: "contains a fragment", + input: "https://example.com/#me", + isValid: false, + }, { + name: "contains a username and password", + input: "https://user:pass@example.com/", + isValid: false, + }, { + name: "contains a port", + input: "https://example.com:8443/", + isValid: false, + }, { + name: "host is an IP address", + input: "https://172.28.92.51/", + isValid: false, + }} { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + me := new(domain.Me) + require.NoError(t, me.Parse([]byte(testCase.input))) + assert.Equal(t, testCase.isValid, me.IsValid()) + }) + } +}