auth/internal/domain/me.go

193 lines
4.5 KiB
Go

package domain
import (
"fmt"
"net"
"net/url"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
http "github.com/valyala/fasthttp"
"golang.org/x/xerrors"
)
// Me is a URL user identifier.
type Me struct {
me *http.URI
}
// ParseMe parse string as me URL identifier.
//nolint: funlen
func ParseMe(raw string) (*Me, error) {
me := http.AcquireURI()
if err := me.Parse(nil, []byte(raw)); err != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: err.Error(),
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
scheme := string(me.Scheme())
if scheme != "http" && scheme != "https" {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST have either an https or http scheme",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
path := string(me.PathOriginal())
if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST contain a path component (/ is a valid path), MUST NOT " +
"contain single-dot or double-dot path segments",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
if me.Hash() != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST NOT contain a fragment component",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
if me.Username() != nil || me.Password() != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile URL MUST NOT contain a username or password component",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
domain := string(me.Host())
if domain == "" {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile host name MUST be a domain name",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
if _, port, _ := net.SplitHostPort(domain); port != "" {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile MUST NOT contain a port",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
if net.ParseIP(domain) != nil {
return nil, Error{
Code: ErrorCodeInvalidRequest,
Description: "profile MUST NOT be ipv4 or ipv6 addresses",
URI: "https://indieauth.net/source/#user-profile-url",
State: "",
frame: xerrors.Caller(1),
}
}
return &Me{me: me}, nil
}
// TestMe returns valid random generated me for tests.
func TestMe(tb testing.TB, src string) *Me {
tb.Helper()
me, err := ParseMe(src)
require.NoError(tb, err)
return me
}
// UnmarshalForm implements custom unmarshler for form values.
func (m *Me) UnmarshalForm(v []byte) error {
me, err := ParseMe(string(v))
if err != nil {
return fmt.Errorf("UnmarshalForm: %w", err)
}
*m = *me
return nil
}
// UnmarshalJSON implements custom unmarshler for JSON.
func (m *Me) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return fmt.Errorf("UnmarshalJSON: %w", err)
}
me, err := ParseMe(src)
if err != nil {
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*m = *me
return nil
}
// MarshalJSON implements custom marshler for JSON.
func (m Me) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(m.String())), nil
}
// URI returns copy of parsed me in *fasthttp.URI representation.
// This copy MUST be released via fasthttp.ReleaseURI.
func (m Me) URI() *http.URI {
if m.me == nil {
return nil
}
u := http.AcquireURI()
m.me.CopyTo(u)
return u
}
// URL returns copy of parsed me in *url.URL representation.
func (m Me) URL() *url.URL {
if m.me == nil {
return nil
}
return &url.URL{
Scheme: string(m.me.Scheme()),
Host: string(m.me.Host()),
Path: string(m.me.Path()),
RawPath: string(m.me.PathOriginal()),
RawQuery: string(m.me.QueryString()),
Fragment: string(m.me.Hash()),
}
}
// String returns string representation of me.
func (m Me) String() string {
if m.me == nil {
return ""
}
return m.me.String()
}