auth/internal/domain/client_id.go

214 lines
4.8 KiB
Go
Raw Permalink Normal View History

2021-12-25 18:55:35 +00:00
package domain
import (
"fmt"
"net"
2021-12-25 18:55:35 +00:00
"net/url"
2021-12-29 20:08:30 +00:00
"strconv"
2021-12-25 18:55:35 +00:00
"strings"
"testing"
"inet.af/netaddr"
"source.toby3d.me/toby3d/auth/internal/common"
2021-12-25 18:55:35 +00:00
)
// ClientID is a URL client identifier.
type ClientID struct {
clientID *url.URL
isLocalhost bool
2021-12-25 18:55:35 +00:00
}
2022-12-26 14:09:34 +00:00
//nolint:gochecknoglobals // slices cannot be constants
2021-12-25 18:55:35 +00:00
var (
localhostIPv4 = netaddr.MustParseIP("127.0.0.1")
localhostIPv6 = netaddr.MustParseIP("::1")
)
2022-01-29 17:50:45 +00:00
// ParseClientID parse string as client ID URL identifier.
2022-12-26 14:09:34 +00:00
//
//nolint:funlen,cyclop
2022-01-29 17:50:45 +00:00
func ParseClientID(src string) (*ClientID, error) {
cid, err := url.Parse(src)
if err != nil {
return nil, NewError(
ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
2024-05-06 13:05:17 +00:00
if cid.Scheme == "" {
cid.Scheme = "https"
}
if !strings.EqualFold(cid.Scheme, "http") && !strings.EqualFold(cid.Scheme, "https") {
return nil, NewError(
ErrorCodeInvalidRequest,
"client identifier URL MUST have either an https or http scheme, got '"+cid.Scheme+"'",
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
if cid.Path == "" {
cid.Path = "/"
}
if strings.Contains(cid.Path, "/.") || strings.Contains(cid.Path, "/..") {
return nil, NewError(
ErrorCodeInvalidRequest,
"client identifier URL MUST contain a path component and MUST NOT contain "+
"single-dot or double-dot path segments, got '"+cid.Path+"'",
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
if cid.Fragment != "" {
return nil, NewError(
ErrorCodeInvalidRequest,
"client identifier URL MUST NOT contain a fragment component, got '"+cid.Fragment+"'",
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
if cid.User != nil {
return nil, NewError(
ErrorCodeInvalidRequest,
"client identifier URL MUST NOT contain a username or password component, got '"+
cid.User.String()+"'",
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
domain := cid.Hostname()
2021-12-25 18:55:35 +00:00
if domain == "" {
return nil, NewError(
ErrorCodeInvalidRequest,
"client host name MUST be domain name or a loopback interface, got '"+domain+"'",
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
ip, err := netaddr.ParseIP(domain)
if err != nil {
ipPort, err := netaddr.ParseIPPort(domain)
if err != nil {
resolvedAddr, err := net.LookupIP(domain)
if err != nil {
return nil, fmt.Errorf("cannot resolve client_id domain: %w", err)
}
ip, _ = netaddr.FromStdIP(resolvedAddr[0])
isLocalhost := ip.Compare(localhostIPv4) == 0 || ip.Compare(localhostIPv6) == 0
return &ClientID{
clientID: cid,
isLocalhost: isLocalhost,
}, nil
2021-12-25 18:55:35 +00:00
}
ip = ipPort.IP()
}
isLocalhost := ip.Compare(localhostIPv4) == 0 || ip.Compare(localhostIPv6) == 0
if !ip.IsLoopback() && !isLocalhost {
return nil, NewError(
ErrorCodeInvalidRequest,
"client identifier URL MUST NOT be IPv4 or IPv6 addresses except for IPv4 "+
2021-12-25 18:55:35 +00:00
"127.0.0.1 or IPv6 [::1]",
"https://indieauth.net/source/#client-identifier",
)
2021-12-25 18:55:35 +00:00
}
2022-01-29 17:50:45 +00:00
return &ClientID{
clientID: cid,
isLocalhost: isLocalhost,
2022-01-29 17:50:45 +00:00
}, nil
2021-12-25 18:55:35 +00:00
}
2022-01-29 17:50:45 +00:00
// TestClientID returns valid random generated ClientID for tests.
func TestClientID(tb testing.TB, forceURL ...string) *ClientID {
2021-12-25 18:55:35 +00:00
tb.Helper()
in := "https://127.0.0.1/"
if len(forceURL) > 0 {
in = forceURL[0]
}
u, _ := url.Parse(in)
2021-12-25 18:55:35 +00:00
return &ClientID{
clientID: u,
isLocalhost: len(forceURL) < 1,
}
2021-12-25 18:55:35 +00:00
}
2022-01-29 17:50:45 +00:00
// UnmarshalForm implements custom unmarshler for form values.
2021-12-25 18:55:35 +00:00
func (cid *ClientID) UnmarshalForm(v []byte) error {
clientID, err := ParseClientID(string(v))
2021-12-25 18:55:35 +00:00
if err != nil {
return fmt.Errorf("ClientID: UnmarshalForm: %w", err)
2021-12-25 18:55:35 +00:00
}
2021-12-29 20:08:30 +00:00
*cid = *clientID
return nil
}
2022-01-29 17:50:45 +00:00
// UnmarshalJSON implements custom unmarshler for JSON.
2021-12-29 20:08:30 +00:00
func (cid *ClientID) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return fmt.Errorf("ClientID: UnmarshalJSON: %w", err)
2021-12-29 20:08:30 +00:00
}
clientID, err := ParseClientID(src)
2021-12-29 20:08:30 +00:00
if err != nil {
return fmt.Errorf("ClientID: UnmarshalJSON: %w", err)
2021-12-29 20:08:30 +00:00
}
*cid = *clientID
2021-12-25 18:55:35 +00:00
return nil
}
2022-01-29 17:50:45 +00:00
// MarshalForm implements custom marshler for JSON.
2022-01-12 18:04:40 +00:00
func (cid ClientID) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(cid.String())), nil
}
// IsEqual checks what cid is equal to provided v.
func (cid ClientID) IsEqual(v ClientID) bool {
return cid.clientID.String() == v.clientID.String()
}
2022-01-29 17:50:45 +00:00
// URL returns url.URL representation of client ID.
2022-01-12 18:04:40 +00:00
func (cid ClientID) URL() *url.URL {
out, _ := url.Parse(cid.clientID.String())
return out
2021-12-25 18:55:35 +00:00
}
func (cid ClientID) IsLocalhost() bool {
return cid.isLocalhost
}
2021-12-25 18:55:35 +00:00
// String returns string representation of client ID.
2022-01-12 18:04:40 +00:00
func (cid ClientID) String() string {
if cid.clientID == nil {
return ""
}
2021-12-29 20:08:30 +00:00
return cid.clientID.String()
2021-12-25 18:55:35 +00:00
}
func (cid ClientID) GoString() string {
if cid.clientID == nil {
return "domain.ClientID(" + common.Und + ")"
}
return "domain.ClientID(" + cid.clientID.String() + ")"
}