auth/internal/domain/client_id.go

178 lines
4.4 KiB
Go
Raw Normal View History

2021-12-25 18:55:35 +00:00
package domain
import (
"fmt"
"net/url"
2021-12-29 20:08:30 +00:00
"strconv"
2021-12-25 18:55:35 +00:00
"strings"
"testing"
"github.com/stretchr/testify/require"
http "github.com/valyala/fasthttp"
"golang.org/x/xerrors"
"inet.af/netaddr"
)
// ClientID is a URL client identifier.
type ClientID struct {
2021-12-29 20:08:30 +00:00
clientID *http.URI
valid bool
2021-12-25 18:55:35 +00:00
}
//nolint: gochecknoglobals
var (
localhostIPv4 = netaddr.MustParseIP("127.0.0.1")
localhostIPv6 = netaddr.MustParseIP("::1")
)
2022-01-12 18:04:40 +00:00
//nolint: funlen
2021-12-25 18:55:35 +00:00
func NewClientID(raw string) (*ClientID, error) {
2021-12-29 20:08:30 +00:00
clientID := http.AcquireURI()
if err := clientID.Parse(nil, []byte(raw)); err != nil {
2021-12-25 18:55:35 +00:00
return nil, Error{
Code: "invalid_request",
Description: err.Error(),
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
2021-12-29 20:08:30 +00:00
scheme := string(clientID.Scheme())
2021-12-25 18:55:35 +00:00
if scheme != "http" && scheme != "https" {
return nil, Error{
Code: "invalid_request",
Description: "client identifier URL MUST have either an https or http scheme",
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
2021-12-29 20:08:30 +00:00
path := string(clientID.PathOriginal())
2021-12-25 18:55:35 +00:00
if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") {
return nil, Error{
Code: "invalid_request",
Description: "client identifier URL MUST contain a path component and MUST NOT contain " +
"single-dot or double-dot path segments",
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
2021-12-29 20:08:30 +00:00
if clientID.Hash() != nil {
2021-12-25 18:55:35 +00:00
return nil, Error{
Code: "invalid_request",
Description: "client identifier URL MUST NOT contain a fragment component",
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
2021-12-29 20:08:30 +00:00
if clientID.Username() != nil || clientID.Password() != nil {
2021-12-25 18:55:35 +00:00
return nil, Error{
Code: "invalid_request",
Description: "client identifier URL MUST NOT contain a username or password component",
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
2021-12-29 20:08:30 +00:00
domain := string(clientID.Host())
2021-12-25 18:55:35 +00:00
if domain == "" {
return nil, Error{
Code: "invalid_request",
Description: "client host name MUST be domain name or a loopback interface",
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
ip, err := netaddr.ParseIP(domain)
if err != nil {
ipPort, err := netaddr.ParseIPPort(domain)
if err != nil {
2021-12-29 20:08:30 +00:00
return &ClientID{clientID: clientID}, nil
2021-12-25 18:55:35 +00:00
}
ip = ipPort.IP()
}
if !ip.IsLoopback() && ip.Compare(localhostIPv4) != 0 && ip.Compare(localhostIPv6) != 0 {
return nil, Error{
Code: "invalid_request",
Description: "client identifier URL MUST NOT be IPv4 or IPv6 addresses except for IPv4 " +
"127.0.0.1 or IPv6 [::1]",
URI: "https://indieauth.net/source/#client-identifier",
Frame: xerrors.Caller(1),
}
}
2021-12-29 20:08:30 +00:00
return &ClientID{clientID: clientID}, nil
2021-12-25 18:55:35 +00:00
}
// TestClientID returns a valid random generated ClientID for tests.
func TestClientID(tb testing.TB) *ClientID {
tb.Helper()
2021-12-29 20:08:30 +00:00
clientID, err := NewClientID("https://app.example.com/")
2021-12-25 18:55:35 +00:00
require.NoError(tb, err)
2021-12-29 20:08:30 +00:00
return clientID
2021-12-25 18:55:35 +00:00
}
// UnmarshalForm implements a custom form.Unmarshaler.
func (cid *ClientID) UnmarshalForm(v []byte) error {
2021-12-29 20:08:30 +00:00
clientID, err := NewClientID(string(v))
2021-12-25 18:55:35 +00:00
if err != nil {
return fmt.Errorf("UnmarshalForm: %w", err)
}
2021-12-29 20:08:30 +00:00
*cid = *clientID
return nil
}
func (cid *ClientID) UnmarshalJSON(v []byte) error {
src, err := strconv.Unquote(string(v))
if err != nil {
return err
}
clientID, err := NewClientID(src)
if err != nil {
return fmt.Errorf("UnmarshalJSON: %w", err)
}
*cid = *clientID
2021-12-25 18:55:35 +00:00
return nil
}
2022-01-12 18:04:40 +00:00
func (cid ClientID) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(cid.String())), nil
}
2021-12-25 18:55:35 +00:00
// URI returns copy of parsed *fasthttp.URI.
// This copy MUST be released via fasthttp.ReleaseURI.
2022-01-12 18:04:40 +00:00
func (cid ClientID) URI() *http.URI {
2021-12-25 18:55:35 +00:00
u := http.AcquireURI()
2021-12-29 20:08:30 +00:00
cid.clientID.CopyTo(u)
2021-12-25 18:55:35 +00:00
return u
}
2022-01-12 18:04:40 +00:00
func (cid ClientID) URL() *url.URL {
2021-12-25 18:55:35 +00:00
return &url.URL{
2021-12-29 20:08:30 +00:00
Scheme: string(cid.clientID.Scheme()),
Host: string(cid.clientID.Host()),
Path: string(cid.clientID.Path()),
RawPath: string(cid.clientID.PathOriginal()),
RawQuery: string(cid.clientID.QueryString()),
Fragment: string(cid.clientID.Hash()),
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 {
2021-12-29 20:08:30 +00:00
return cid.clientID.String()
2021-12-25 18:55:35 +00:00
}