[feature] Discover webfinger through host-meta (#1588)
* [feature] Discover webfinger through host-meta This implements a fallback for discovering the webfinger endpoint in case the /.well-known/webfinger endpoint wasn't properly redirected. Some instances do this because the recommendation used to be to use host-meta for the webfinger redirect in the before times. Closes #1558. * [bug] Ensure we only ever update cache on success * [chore] Move finger tests to their own place This adds a test suite for transport and moves the finger cache tests into there instead of abusing the search test suite. * [chore] cleanup the test a bit more We don't really need a separate function for the oddly located webfinger response as we check the full URL string anyway * Address review comments * [chore] update config example * [chore] access DB only through state in controller
This commit is contained in:
parent
b344c2c8f4
commit
e397272fe8
|
@ -110,7 +110,7 @@ var Start action.GTSAction = func(ctx context.Context) error {
|
||||||
oauthServer := oauth.New(ctx, dbService)
|
oauthServer := oauth.New(ctx, dbService)
|
||||||
typeConverter := typeutils.NewConverter(dbService)
|
typeConverter := typeutils.NewConverter(dbService)
|
||||||
federatingDB := federatingdb.New(&state, typeConverter)
|
federatingDB := federatingdb.New(&state, typeConverter)
|
||||||
transportController := transport.NewController(dbService, federatingDB, &federation.Clock{}, client)
|
transportController := transport.NewController(&state, federatingDB, &federation.Clock{}, client)
|
||||||
federator := federation.NewFederator(dbService, federatingDB, transportController, typeConverter, mediaManager)
|
federator := federation.NewFederator(dbService, federatingDB, transportController, typeConverter, mediaManager)
|
||||||
|
|
||||||
// decide whether to create a noop email sender (won't send emails) or a real one
|
// decide whether to create a noop email sender (won't send emails) or a real one
|
||||||
|
|
|
@ -287,6 +287,10 @@ cache:
|
||||||
user-ttl: "5m"
|
user-ttl: "5m"
|
||||||
user-sweep-freq: "30s"
|
user-sweep-freq: "30s"
|
||||||
|
|
||||||
|
webfinger-max-size": 250
|
||||||
|
webfinger-ttl: "24h"
|
||||||
|
webfinger-sweep-freq": "15m"
|
||||||
|
|
||||||
######################
|
######################
|
||||||
##### WEB CONFIG #####
|
##### WEB CONFIG #####
|
||||||
######################
|
######################
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
// WellKnownResponse represents the response to either a webfinger request for an 'acct' resource, or a request to nodeinfo.
|
// WellKnownResponse represents the response to either a webfinger request for an 'acct' resource, or a request to nodeinfo.
|
||||||
// For example, it would be returned from https://example.org/.well-known/webfinger?resource=acct:some_username@example.org
|
// For example, it would be returned from https://example.org/.well-known/webfinger?resource=acct:some_username@example.org
|
||||||
//
|
//
|
||||||
|
@ -32,12 +34,12 @@ type WellKnownResponse struct {
|
||||||
|
|
||||||
// Link represents one 'link' in a slice of links returned from a lookup request.
|
// Link represents one 'link' in a slice of links returned from a lookup request.
|
||||||
//
|
//
|
||||||
// See https://webfinger.net/
|
// See https://webfinger.net/ and https://www.rfc-editor.org/rfc/rfc6415.html#section-3.1
|
||||||
type Link struct {
|
type Link struct {
|
||||||
Rel string `json:"rel"`
|
Rel string `json:"rel" xml:"rel,attr"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty" xml:"type,attr,omitempty"`
|
||||||
Href string `json:"href,omitempty"`
|
Href string `json:"href,omitempty" xml:"href,attr,omitempty"`
|
||||||
Template string `json:"template,omitempty"`
|
Template string `json:"template,omitempty" xml:"template,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nodeinfo represents a version 2.1 or version 2.0 nodeinfo schema.
|
// Nodeinfo represents a version 2.1 or version 2.0 nodeinfo schema.
|
||||||
|
@ -87,3 +89,13 @@ type NodeInfoUsage struct {
|
||||||
type NodeInfoUsers struct {
|
type NodeInfoUsers struct {
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostMeta represents a hostmeta document.
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc6415.html#section-3
|
||||||
|
//
|
||||||
|
// swagger:model hostmeta
|
||||||
|
type HostMeta struct {
|
||||||
|
XMLName xml.Name `xml:"XRD"`
|
||||||
|
XMLNS string `xml:"xmlns,attr"`
|
||||||
|
Link []Link `xml:"Link"`
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"codeberg.org/gruf/go-cache/v3/result"
|
"codeberg.org/gruf/go-cache/v3/result"
|
||||||
|
"codeberg.org/gruf/go-cache/v3/ttl"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
|
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -71,6 +72,9 @@ type GTSCaches interface {
|
||||||
|
|
||||||
// User provides access to the gtsmodel User database cache.
|
// User provides access to the gtsmodel User database cache.
|
||||||
User() *result.Cache[*gtsmodel.User]
|
User() *result.Cache[*gtsmodel.User]
|
||||||
|
|
||||||
|
// Webfinger
|
||||||
|
Webfinger() *ttl.Cache[string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGTS returns a new default implementation of GTSCaches.
|
// NewGTS returns a new default implementation of GTSCaches.
|
||||||
|
@ -91,6 +95,7 @@ type gtsCaches struct {
|
||||||
status *result.Cache[*gtsmodel.Status]
|
status *result.Cache[*gtsmodel.Status]
|
||||||
tombstone *result.Cache[*gtsmodel.Tombstone]
|
tombstone *result.Cache[*gtsmodel.Tombstone]
|
||||||
user *result.Cache[*gtsmodel.User]
|
user *result.Cache[*gtsmodel.User]
|
||||||
|
webfinger *ttl.Cache[string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gtsCaches) Init() {
|
func (c *gtsCaches) Init() {
|
||||||
|
@ -106,6 +111,7 @@ func (c *gtsCaches) Init() {
|
||||||
c.initStatus()
|
c.initStatus()
|
||||||
c.initTombstone()
|
c.initTombstone()
|
||||||
c.initUser()
|
c.initUser()
|
||||||
|
c.initWebfinger()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gtsCaches) Start() {
|
func (c *gtsCaches) Start() {
|
||||||
|
@ -145,6 +151,9 @@ func (c *gtsCaches) Start() {
|
||||||
tryUntil("starting gtsmodel.User cache", 5, func() bool {
|
tryUntil("starting gtsmodel.User cache", 5, func() bool {
|
||||||
return c.user.Start(config.GetCacheGTSUserSweepFreq())
|
return c.user.Start(config.GetCacheGTSUserSweepFreq())
|
||||||
})
|
})
|
||||||
|
tryUntil("starting gtsmodel.Webfinger cache", 5, func() bool {
|
||||||
|
return c.webfinger.Start(config.GetCacheGTSWebfingerSweepFreq())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gtsCaches) Stop() {
|
func (c *gtsCaches) Stop() {
|
||||||
|
@ -160,6 +169,7 @@ func (c *gtsCaches) Stop() {
|
||||||
tryUntil("stopping gtsmodel.Status cache", 5, c.status.Stop)
|
tryUntil("stopping gtsmodel.Status cache", 5, c.status.Stop)
|
||||||
tryUntil("stopping gtsmodel.Tombstone cache", 5, c.tombstone.Stop)
|
tryUntil("stopping gtsmodel.Tombstone cache", 5, c.tombstone.Stop)
|
||||||
tryUntil("stopping gtsmodel.User cache", 5, c.user.Stop)
|
tryUntil("stopping gtsmodel.User cache", 5, c.user.Stop)
|
||||||
|
tryUntil("stopping gtsmodel.Webfinger cache", 5, c.webfinger.Stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gtsCaches) Account() *result.Cache[*gtsmodel.Account] {
|
func (c *gtsCaches) Account() *result.Cache[*gtsmodel.Account] {
|
||||||
|
@ -210,6 +220,10 @@ func (c *gtsCaches) User() *result.Cache[*gtsmodel.User] {
|
||||||
return c.user
|
return c.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *gtsCaches) Webfinger() *ttl.Cache[string, string] {
|
||||||
|
return c.webfinger
|
||||||
|
}
|
||||||
|
|
||||||
func (c *gtsCaches) initAccount() {
|
func (c *gtsCaches) initAccount() {
|
||||||
c.account = result.New([]result.Lookup{
|
c.account = result.New([]result.Lookup{
|
||||||
{Name: "ID"},
|
{Name: "ID"},
|
||||||
|
@ -355,3 +369,10 @@ func (c *gtsCaches) initUser() {
|
||||||
}, config.GetCacheGTSUserMaxSize())
|
}, config.GetCacheGTSUserMaxSize())
|
||||||
c.user.SetTTL(config.GetCacheGTSUserTTL(), true)
|
c.user.SetTTL(config.GetCacheGTSUserTTL(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *gtsCaches) initWebfinger() {
|
||||||
|
c.webfinger = ttl.New[string, string](
|
||||||
|
0,
|
||||||
|
config.GetCacheGTSWebfingerMaxSize(),
|
||||||
|
config.GetCacheGTSWebfingerTTL())
|
||||||
|
}
|
||||||
|
|
|
@ -207,6 +207,10 @@ type GTSCacheConfiguration struct {
|
||||||
UserMaxSize int `name:"user-max-size"`
|
UserMaxSize int `name:"user-max-size"`
|
||||||
UserTTL time.Duration `name:"user-ttl"`
|
UserTTL time.Duration `name:"user-ttl"`
|
||||||
UserSweepFreq time.Duration `name:"user-sweep-freq"`
|
UserSweepFreq time.Duration `name:"user-sweep-freq"`
|
||||||
|
|
||||||
|
WebfingerMaxSize int `name:"webfinger-max-size"`
|
||||||
|
WebfingerTTL time.Duration `name:"webfinger-ttl"`
|
||||||
|
WebfingerSweepFreq time.Duration `name:"webfinger-sweep-freq"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).
|
// MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).
|
||||||
|
|
|
@ -166,6 +166,10 @@ var Defaults = Configuration{
|
||||||
UserMaxSize: 100,
|
UserMaxSize: 100,
|
||||||
UserTTL: time.Minute * 5,
|
UserTTL: time.Minute * 5,
|
||||||
UserSweepFreq: time.Second * 30,
|
UserSweepFreq: time.Second * 30,
|
||||||
|
|
||||||
|
WebfingerMaxSize: 250,
|
||||||
|
WebfingerTTL: time.Hour * 24,
|
||||||
|
WebfingerSweepFreq: time.Minute * 15,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -3003,6 +3003,81 @@ func GetCacheGTSUserSweepFreq() time.Duration { return global.GetCacheGTSUserSwe
|
||||||
// SetCacheGTSUserSweepFreq safely sets the value for global configuration 'Cache.GTS.UserSweepFreq' field
|
// SetCacheGTSUserSweepFreq safely sets the value for global configuration 'Cache.GTS.UserSweepFreq' field
|
||||||
func SetCacheGTSUserSweepFreq(v time.Duration) { global.SetCacheGTSUserSweepFreq(v) }
|
func SetCacheGTSUserSweepFreq(v time.Duration) { global.SetCacheGTSUserSweepFreq(v) }
|
||||||
|
|
||||||
|
// GetCacheGTSWebfingerMaxSize safely fetches the Configuration value for state's 'Cache.GTS.WebfingerMaxSize' field
|
||||||
|
func (st *ConfigState) GetCacheGTSWebfingerMaxSize() (v int) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
v = st.config.Cache.GTS.WebfingerMaxSize
|
||||||
|
st.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheGTSWebfingerMaxSize safely sets the Configuration value for state's 'Cache.GTS.WebfingerMaxSize' field
|
||||||
|
func (st *ConfigState) SetCacheGTSWebfingerMaxSize(v int) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.Cache.GTS.WebfingerMaxSize = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheGTSWebfingerMaxSizeFlag returns the flag name for the 'Cache.GTS.WebfingerMaxSize' field
|
||||||
|
func CacheGTSWebfingerMaxSizeFlag() string { return "cache-gts-webfinger-max-size" }
|
||||||
|
|
||||||
|
// GetCacheGTSWebfingerMaxSize safely fetches the value for global configuration 'Cache.GTS.WebfingerMaxSize' field
|
||||||
|
func GetCacheGTSWebfingerMaxSize() int { return global.GetCacheGTSWebfingerMaxSize() }
|
||||||
|
|
||||||
|
// SetCacheGTSWebfingerMaxSize safely sets the value for global configuration 'Cache.GTS.WebfingerMaxSize' field
|
||||||
|
func SetCacheGTSWebfingerMaxSize(v int) { global.SetCacheGTSWebfingerMaxSize(v) }
|
||||||
|
|
||||||
|
// GetCacheGTSWebfingerTTL safely fetches the Configuration value for state's 'Cache.GTS.WebfingerTTL' field
|
||||||
|
func (st *ConfigState) GetCacheGTSWebfingerTTL() (v time.Duration) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
v = st.config.Cache.GTS.WebfingerTTL
|
||||||
|
st.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheGTSWebfingerTTL safely sets the Configuration value for state's 'Cache.GTS.WebfingerTTL' field
|
||||||
|
func (st *ConfigState) SetCacheGTSWebfingerTTL(v time.Duration) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.Cache.GTS.WebfingerTTL = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheGTSWebfingerTTLFlag returns the flag name for the 'Cache.GTS.WebfingerTTL' field
|
||||||
|
func CacheGTSWebfingerTTLFlag() string { return "cache-gts-webfinger-ttl" }
|
||||||
|
|
||||||
|
// GetCacheGTSWebfingerTTL safely fetches the value for global configuration 'Cache.GTS.WebfingerTTL' field
|
||||||
|
func GetCacheGTSWebfingerTTL() time.Duration { return global.GetCacheGTSWebfingerTTL() }
|
||||||
|
|
||||||
|
// SetCacheGTSWebfingerTTL safely sets the value for global configuration 'Cache.GTS.WebfingerTTL' field
|
||||||
|
func SetCacheGTSWebfingerTTL(v time.Duration) { global.SetCacheGTSWebfingerTTL(v) }
|
||||||
|
|
||||||
|
// GetCacheGTSWebfingerSweepFreq safely fetches the Configuration value for state's 'Cache.GTS.WebfingerSweepFreq' field
|
||||||
|
func (st *ConfigState) GetCacheGTSWebfingerSweepFreq() (v time.Duration) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
v = st.config.Cache.GTS.WebfingerSweepFreq
|
||||||
|
st.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheGTSWebfingerSweepFreq safely sets the Configuration value for state's 'Cache.GTS.WebfingerSweepFreq' field
|
||||||
|
func (st *ConfigState) SetCacheGTSWebfingerSweepFreq(v time.Duration) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.Cache.GTS.WebfingerSweepFreq = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheGTSWebfingerSweepFreqFlag returns the flag name for the 'Cache.GTS.WebfingerSweepFreq' field
|
||||||
|
func CacheGTSWebfingerSweepFreqFlag() string { return "cache-gts-webfinger-sweep-freq" }
|
||||||
|
|
||||||
|
// GetCacheGTSWebfingerSweepFreq safely fetches the value for global configuration 'Cache.GTS.WebfingerSweepFreq' field
|
||||||
|
func GetCacheGTSWebfingerSweepFreq() time.Duration { return global.GetCacheGTSWebfingerSweepFreq() }
|
||||||
|
|
||||||
|
// SetCacheGTSWebfingerSweepFreq safely sets the value for global configuration 'Cache.GTS.WebfingerSweepFreq' field
|
||||||
|
func SetCacheGTSWebfingerSweepFreq(v time.Duration) { global.SetCacheGTSWebfingerSweepFreq(v) }
|
||||||
|
|
||||||
// GetAdminAccountUsername safely fetches the Configuration value for state's 'AdminAccountUsername' field
|
// GetAdminAccountUsername safely fetches the Configuration value for state's 'AdminAccountUsername' field
|
||||||
func (st *ConfigState) GetAdminAccountUsername() (v string) {
|
func (st *ConfigState) GetAdminAccountUsername() (v string) {
|
||||||
st.mutex.Lock()
|
st.mutex.Lock()
|
||||||
|
|
|
@ -32,9 +32,9 @@ import (
|
||||||
"github.com/superseriousbusiness/activity/pub"
|
"github.com/superseriousbusiness/activity/pub"
|
||||||
"github.com/superseriousbusiness/activity/streams"
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
|
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Controller generates transports for use in making federation requests to other servers.
|
// Controller generates transports for use in making federation requests to other servers.
|
||||||
|
@ -47,7 +47,7 @@ type Controller interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type controller struct {
|
type controller struct {
|
||||||
db db.DB
|
state *state.State
|
||||||
fedDB federatingdb.DB
|
fedDB federatingdb.DB
|
||||||
clock pub.Clock
|
clock pub.Clock
|
||||||
client pub.HttpClient
|
client pub.HttpClient
|
||||||
|
@ -57,14 +57,14 @@ type controller struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController returns an implementation of the Controller interface for creating new transports
|
// NewController returns an implementation of the Controller interface for creating new transports
|
||||||
func NewController(db db.DB, federatingDB federatingdb.DB, clock pub.Clock, client pub.HttpClient) Controller {
|
func NewController(state *state.State, federatingDB federatingdb.DB, clock pub.Clock, client pub.HttpClient) Controller {
|
||||||
applicationName := config.GetApplicationName()
|
applicationName := config.GetApplicationName()
|
||||||
host := config.GetHost()
|
host := config.GetHost()
|
||||||
proto := config.GetProtocol()
|
proto := config.GetProtocol()
|
||||||
version := config.GetSoftwareVersion()
|
version := config.GetSoftwareVersion()
|
||||||
|
|
||||||
c := &controller{
|
c := &controller{
|
||||||
db: db,
|
state: state,
|
||||||
fedDB: federatingDB,
|
fedDB: federatingDB,
|
||||||
clock: clock,
|
clock: clock,
|
||||||
client: client,
|
client: client,
|
||||||
|
@ -138,7 +138,7 @@ func (c *controller) NewTransportForUsername(ctx context.Context, username strin
|
||||||
u = username
|
u = username
|
||||||
}
|
}
|
||||||
|
|
||||||
ourAccount, err := c.db.GetAccountByUsernameDomain(ctx, u, "")
|
ourAccount, err := c.state.DB.GetAccountByUsernameDomain(ctx, u, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting account %s from db: %s", username, err)
|
return nil, fmt.Errorf("error getting account %s from db: %s", username, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,29 +20,61 @@ package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *transport) Finger(ctx context.Context, targetUsername string, targetDomain string) ([]byte, error) {
|
// webfingerURLFor returns the URL to try a webfinger request against, as
|
||||||
// Prepare URL string
|
// well as if the URL was retrieved from cache. When the URL is retrieved
|
||||||
urlStr := "https://" +
|
// from cache we don't have to try and do host-meta discovery
|
||||||
targetDomain +
|
func (t *transport) webfingerURLFor(targetDomain string) (string, bool) {
|
||||||
"/.well-known/webfinger?resource=acct:" +
|
url := "https://" + targetDomain + "/.well-known/webfinger"
|
||||||
targetUsername + "@" + targetDomain
|
|
||||||
|
|
||||||
// Generate new GET request from URL string
|
wc := t.controller.state.Caches.GTS.Webfinger()
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
// We're doing the manual locking/unlocking here to be able to
|
||||||
|
// safely call Cache.Get instead of Get, as the latter updates the
|
||||||
|
// item expiry which we don't want to do here
|
||||||
|
wc.Lock()
|
||||||
|
item, ok := wc.Cache.Get(targetDomain)
|
||||||
|
wc.Unlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
url = item.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepWebfingerReq(ctx context.Context, loc, domain, username string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, loc, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value := url.QueryEscape("acct:" + username + "@" + domain)
|
||||||
|
req.URL.RawQuery = "resource=" + value
|
||||||
|
|
||||||
req.Header.Add("Accept", string(apiutil.AppJSON))
|
req.Header.Add("Accept", string(apiutil.AppJSON))
|
||||||
req.Header.Add("Accept", "application/jrd+json")
|
req.Header.Add("Accept", "application/jrd+json")
|
||||||
req.Header.Set("Host", req.URL.Host)
|
req.Header.Set("Host", req.URL.Host)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transport) Finger(ctx context.Context, targetUsername string, targetDomain string) ([]byte, error) {
|
||||||
|
// Generate new GET request
|
||||||
|
url, cached := t.webfingerURLFor(targetDomain)
|
||||||
|
req, err := prepWebfingerReq(ctx, url, targetDomain, targetUsername)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the HTTP request
|
// Perform the HTTP request
|
||||||
rsp, err := t.GET(req)
|
rsp, err := t.GET(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -50,10 +82,117 @@ func (t *transport) Finger(ctx context.Context, targetUsername string, targetDom
|
||||||
}
|
}
|
||||||
defer rsp.Body.Close()
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
// Check for an expected status code
|
// Check if the request succeeded so we can bail out early
|
||||||
if rsp.StatusCode != http.StatusOK {
|
if rsp.StatusCode == http.StatusOK {
|
||||||
return nil, fmt.Errorf("GET request to %s failed: %s", urlStr, rsp.Status)
|
if cached {
|
||||||
|
// If we got a success on a cached URL, i.e one set by us later on when
|
||||||
|
// a host-meta based webfinger request succeeded, set it again here to
|
||||||
|
// renew the TTL
|
||||||
|
t.controller.state.Caches.GTS.Webfinger().Set(targetDomain, url)
|
||||||
|
}
|
||||||
|
return io.ReadAll(rsp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From here on out, we're handling different failure scenarios and
|
||||||
|
// deciding whether we should do a host-meta based fallback or not
|
||||||
|
|
||||||
|
if (rsp.StatusCode >= 500 && rsp.StatusCode < 600) || cached {
|
||||||
|
// In case we got a 5xx, bail out irrespective of if the value
|
||||||
|
// was cached or not. The target may be broken or be signalling
|
||||||
|
// us to back-off.
|
||||||
|
//
|
||||||
|
// If it's any error but the URL was cached, bail out too
|
||||||
|
return nil, fmt.Errorf("GET request to %s failed: %s", req.URL.String(), rsp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// So far we've failed to get a successful response from the expected
|
||||||
|
// webfinger endpoint. Lets try and discover the webfinger endpoint
|
||||||
|
// through /.well-known/host-meta
|
||||||
|
host, err := t.webfingerFromHostMeta(ctx, targetDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to discover webfinger URL fallback for: %s through host-meta: %w", targetDomain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the original and host-meta URL are the same. If they
|
||||||
|
// are there's no sense in us trying the request again as it just
|
||||||
|
// failed
|
||||||
|
if host == url {
|
||||||
|
return nil, fmt.Errorf("webfinger discovery on %s returned endpoint we already tried: %s", targetDomain, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have a different URL for the webfinger
|
||||||
|
// endpoint, try the request against that endpoint instead
|
||||||
|
req, err = prepWebfingerReq(ctx, host, targetDomain, targetUsername)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the HTTP request
|
||||||
|
rsp, err = t.GET(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
if rsp.StatusCode != http.StatusOK {
|
||||||
|
// We've reached the end of the line here, both the original request
|
||||||
|
// and our attempt to resolve it through the fallback have failed
|
||||||
|
return nil, fmt.Errorf("GET request to %s failed: %s", req.URL.String(), rsp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the URL in cache here, since host-meta told us this should be the
|
||||||
|
// valid one, it's different from the default and our request to it did
|
||||||
|
// not fail in any manner
|
||||||
|
t.controller.state.Caches.GTS.Webfinger().Set(targetDomain, host)
|
||||||
|
|
||||||
return io.ReadAll(rsp.Body)
|
return io.ReadAll(rsp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *transport) webfingerFromHostMeta(ctx context.Context, targetDomain string) (string, error) {
|
||||||
|
// Build the request for the host-meta endpoint
|
||||||
|
hmurl := "https://" + targetDomain + "/.well-known/host-meta"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, hmurl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're doing XML
|
||||||
|
req.Header.Add("Accept", string(apiutil.AppXML))
|
||||||
|
req.Header.Add("Accept", "application/xrd+xml")
|
||||||
|
req.Header.Set("Host", req.URL.Host)
|
||||||
|
|
||||||
|
// Perform the HTTP request
|
||||||
|
rsp, err := t.GET(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
// Doesn't look like host-meta is working for this instance
|
||||||
|
if rsp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("GET request for %s failed: %s", req.URL.String(), rsp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
e := xml.NewDecoder(rsp.Body)
|
||||||
|
var hm apimodel.HostMeta
|
||||||
|
if err := e.Decode(&hm); err != nil {
|
||||||
|
// We got something, but it's not a host-meta document we understand
|
||||||
|
return "", fmt.Errorf("failed to decode host-meta response for %s at %s: %w", targetDomain, req.URL.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, link := range hm.Link {
|
||||||
|
// Based on what we currently understand, there should not be more than one
|
||||||
|
// of these with Rel="lrdd" in a host-meta document
|
||||||
|
if link.Rel == "lrdd" {
|
||||||
|
u, err := url.Parse(link.Template)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("lrdd link is not a valid url: %w", err)
|
||||||
|
}
|
||||||
|
// Get rid of the query template, we only want the scheme://host/path part
|
||||||
|
u.RawQuery = ""
|
||||||
|
urlStr := u.String()
|
||||||
|
return urlStr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no webfinger URL found")
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package transport_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FingerTestSuite struct {
|
||||||
|
TransportTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FingerTestSuite) TestFinger() {
|
||||||
|
wc := suite.state.Caches.GTS.Webfinger()
|
||||||
|
suite.Equal(0, wc.Len(), "expect webfinger cache to be empty")
|
||||||
|
|
||||||
|
_, err := suite.transport.Finger(context.TODO(), "brand_new_person", "unknown-instance.com")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(0, wc.Len(), "expect webfinger cache to be empty for normal webfinger request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FingerTestSuite) TestFingerWithHostMeta() {
|
||||||
|
wc := suite.state.Caches.GTS.Webfinger()
|
||||||
|
suite.Equal(0, wc.Len(), "expect webfinger cache to be empty")
|
||||||
|
|
||||||
|
_, err := suite.transport.Finger(context.TODO(), "someone", "misconfigured-instance.com")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(1, wc.Len(), "expect webfinger cache to hold one entry")
|
||||||
|
suite.True(wc.Has("misconfigured-instance.com"), "expect webfinger cache to have entry for misconfigured-instance.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FingerTestSuite) TestFingerWithHostMetaCacheStrategy() {
|
||||||
|
wc := suite.state.Caches.GTS.Webfinger()
|
||||||
|
suite.Equal(0, wc.Len(), "expect webfinger cache to be empty")
|
||||||
|
|
||||||
|
_, err := suite.transport.Finger(context.TODO(), "someone", "misconfigured-instance.com")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(1, wc.Len(), "expect webfinger cache to hold one entry")
|
||||||
|
wc.Lock()
|
||||||
|
suite.True(wc.Cache.Has("misconfigured-instance.com"), "expect webfinger cache to have entry for misconfigured-instance.com")
|
||||||
|
ent, _ := wc.Cache.Get("misconfigured-instance.com")
|
||||||
|
wc.Unlock()
|
||||||
|
|
||||||
|
initialTime := ent.Expiry
|
||||||
|
|
||||||
|
// finger them again
|
||||||
|
_, err = suite.transport.Finger(context.TODO(), "someone", "misconfigured-instance.com")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// there should still only be 1 cache entry
|
||||||
|
suite.Equal(1, wc.Len(), "expect webfinger cache to hold one entry")
|
||||||
|
wc.Lock()
|
||||||
|
suite.True(wc.Cache.Has("misconfigured-instance.com"), "expect webfinger cache to have entry for misconfigured-instance.com")
|
||||||
|
rep, _ := wc.Cache.Get("misconfigured-instance.com")
|
||||||
|
wc.Unlock()
|
||||||
|
|
||||||
|
repeatTime := rep.Expiry
|
||||||
|
|
||||||
|
// the TTL of the entry should have extended because we did a second
|
||||||
|
// successful finger
|
||||||
|
suite.NotEqual(initialTime, repeatTime, "expected webfinger cache entry to have different expiry times")
|
||||||
|
if repeatTime.Before(initialTime) {
|
||||||
|
suite.FailNow("expected webfinger cache entry to not be a time traveller")
|
||||||
|
}
|
||||||
|
|
||||||
|
// finger a non-existing user on that same instance which will return an error
|
||||||
|
_, err = suite.transport.Finger(context.TODO(), "invalid", "misconfigured-instance.com")
|
||||||
|
if err == nil {
|
||||||
|
suite.FailNow("expected request for invalid user to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// there should still only be 1 cache entry, because we don't evict from cache on failure
|
||||||
|
suite.Equal(1, wc.Len(), "expect webfinger cache to hold one entry")
|
||||||
|
wc.Lock()
|
||||||
|
suite.True(wc.Cache.Has("misconfigured-instance.com"), "expect webfinger cache to have entry for misconfigured-instance.com")
|
||||||
|
last, _ := wc.Cache.Get("misconfigured-instance.com")
|
||||||
|
wc.Unlock()
|
||||||
|
|
||||||
|
lastTime := last.Expiry
|
||||||
|
|
||||||
|
// The TTL of the previous and new entry should be the same since
|
||||||
|
// a failed request must not extend the entry TTL
|
||||||
|
suite.Equal(repeatTime, lastTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFingerTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &FingerTestSuite{})
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package transport_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransportTestSuite struct {
|
||||||
|
// standard suite interfaces
|
||||||
|
suite.Suite
|
||||||
|
db db.DB
|
||||||
|
storage *storage.Driver
|
||||||
|
mediaManager media.Manager
|
||||||
|
federator federation.Federator
|
||||||
|
processor *processing.Processor
|
||||||
|
emailSender email.Sender
|
||||||
|
sentEmails map[string]string
|
||||||
|
state state.State
|
||||||
|
|
||||||
|
// standard suite models
|
||||||
|
testTokens map[string]*gtsmodel.Token
|
||||||
|
testClients map[string]*gtsmodel.Client
|
||||||
|
testApplications map[string]*gtsmodel.Application
|
||||||
|
testUsers map[string]*gtsmodel.User
|
||||||
|
testAccounts map[string]*gtsmodel.Account
|
||||||
|
|
||||||
|
transport transport.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TransportTestSuite) SetupSuite() {
|
||||||
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
|
suite.testClients = testrig.NewTestClients()
|
||||||
|
suite.testApplications = testrig.NewTestApplications()
|
||||||
|
suite.testUsers = testrig.NewTestUsers()
|
||||||
|
suite.testAccounts = testrig.NewTestAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TransportTestSuite) SetupTest() {
|
||||||
|
suite.state.Caches.Init()
|
||||||
|
testrig.StartWorkers(&suite.state)
|
||||||
|
|
||||||
|
testrig.InitTestConfig()
|
||||||
|
testrig.InitTestLog()
|
||||||
|
|
||||||
|
suite.db = testrig.NewTestDB(&suite.state)
|
||||||
|
suite.state.DB = suite.db
|
||||||
|
suite.storage = testrig.NewInMemoryStorage()
|
||||||
|
suite.state.Storage = suite.storage
|
||||||
|
|
||||||
|
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||||
|
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../testrig/media")), suite.mediaManager)
|
||||||
|
suite.sentEmails = make(map[string]string)
|
||||||
|
suite.emailSender = testrig.NewEmailSender("../../web/template/", suite.sentEmails)
|
||||||
|
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
|
||||||
|
|
||||||
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
|
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
|
||||||
|
|
||||||
|
ts, err := suite.federator.TransportController().NewTransportForUsername(context.TODO(), "")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
suite.transport = ts
|
||||||
|
|
||||||
|
suite.NoError(suite.processor.Start())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TransportTestSuite) TearDownTest() {
|
||||||
|
testrig.StandardDBTeardown(suite.db)
|
||||||
|
testrig.StandardStorageTeardown(suite.storage)
|
||||||
|
testrig.StopWorkers(&suite.state)
|
||||||
|
}
|
|
@ -52,7 +52,10 @@ EXPECT=$(cat <<"EOF"
|
||||||
"tombstone-ttl": 300000000000,
|
"tombstone-ttl": 300000000000,
|
||||||
"user-max-size": 100,
|
"user-max-size": 100,
|
||||||
"user-sweep-freq": 30000000000,
|
"user-sweep-freq": 30000000000,
|
||||||
"user-ttl": 300000000000
|
"user-ttl": 300000000000,
|
||||||
|
"webfinger-max-size": 250,
|
||||||
|
"webfinger-sweep-freq": 900000000000,
|
||||||
|
"webfinger-ttl": 86400000000000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config-path": "internal/config/testdata/test.yaml",
|
"config-path": "internal/config/testdata/test.yaml",
|
||||||
|
|
|
@ -21,6 +21,7 @@ package testrig
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -52,7 +53,7 @@ const (
|
||||||
// PER TEST rather than per suite, so that the do function can be set on a test by test (or even more granular)
|
// PER TEST rather than per suite, so that the do function can be set on a test by test (or even more granular)
|
||||||
// basis.
|
// basis.
|
||||||
func NewTestTransportController(state *state.State, client pub.HttpClient) transport.Controller {
|
func NewTestTransportController(state *state.State, client pub.HttpClient) transport.Controller {
|
||||||
return transport.NewController(state.DB, NewTestFederatingDB(state), &federation.Clock{}, client)
|
return transport.NewController(state, NewTestFederatingDB(state), &federation.Clock{}, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockHTTPClient struct {
|
type MockHTTPClient struct {
|
||||||
|
@ -121,6 +122,10 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
responseContentLength = len(responseBytes)
|
responseContentLength = len(responseBytes)
|
||||||
} else if strings.Contains(req.URL.String(), ".well-known/webfinger") {
|
} else if strings.Contains(req.URL.String(), ".well-known/webfinger") {
|
||||||
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
||||||
|
} else if strings.Contains(req.URL.String(), ".weird-webfinger-location/webfinger") {
|
||||||
|
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
||||||
|
} else if strings.Contains(req.URL.String(), ".well-known/host-meta") {
|
||||||
|
responseCode, responseBytes, responseContentType, responseContentLength = HostMetaResponse(req)
|
||||||
} else if note, ok := mockHTTPClient.TestRemoteStatuses[req.URL.String()]; ok {
|
} else if note, ok := mockHTTPClient.TestRemoteStatuses[req.URL.String()]; ok {
|
||||||
// the request is for a note that we have stored
|
// the request is for a note that we have stored
|
||||||
noteI, err := streams.Serialize(note)
|
noteI, err := streams.Serialize(note)
|
||||||
|
@ -221,11 +226,47 @@ func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
return m.do(req)
|
return m.do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HostMetaResponse(req *http.Request) (responseCode int, responseBytes []byte, responseContentType string, responseContentLength int) {
|
||||||
|
var hm *apimodel.HostMeta
|
||||||
|
|
||||||
|
if req.URL.String() == "https://misconfigured-instance.com/.well-known/host-meta" {
|
||||||
|
hm = &apimodel.HostMeta{
|
||||||
|
XMLNS: "http://docs.oasis-open.org/ns/xri/xrd-1.0",
|
||||||
|
Link: []apimodel.Link{
|
||||||
|
{
|
||||||
|
Rel: "lrdd",
|
||||||
|
Type: "application/xrd+xml",
|
||||||
|
Template: "https://misconfigured-instance.com/.weird-webfinger-location/webfinger?resource={uri}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hm == nil {
|
||||||
|
log.Debugf(nil, "hostmeta response not available for %s", req.URL)
|
||||||
|
responseCode = http.StatusNotFound
|
||||||
|
responseBytes = []byte(``)
|
||||||
|
responseContentType = "application/xml"
|
||||||
|
responseContentLength = len(responseBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hmXML, err := xml.Marshal(hm)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
responseCode = http.StatusOK
|
||||||
|
responseBytes = hmXML
|
||||||
|
responseContentType = "application/xml"
|
||||||
|
responseContentLength = len(hmXML)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byte, responseContentType string, responseContentLength int) {
|
func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byte, responseContentType string, responseContentLength int) {
|
||||||
var wfr *apimodel.WellKnownResponse
|
var wfr *apimodel.WellKnownResponse
|
||||||
|
|
||||||
switch req.URL.String() {
|
switch req.URL.String() {
|
||||||
case "https://unknown-instance.com/.well-known/webfinger?resource=acct:some_group@unknown-instance.com":
|
case "https://unknown-instance.com/.well-known/webfinger?resource=acct%3Asome_group%40unknown-instance.com":
|
||||||
wfr = &apimodel.WellKnownResponse{
|
wfr = &apimodel.WellKnownResponse{
|
||||||
Subject: "acct:some_group@unknown-instance.com",
|
Subject: "acct:some_group@unknown-instance.com",
|
||||||
Links: []apimodel.Link{
|
Links: []apimodel.Link{
|
||||||
|
@ -236,7 +277,7 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "https://owncast.example.org/.well-known/webfinger?resource=acct:rgh@owncast.example.org":
|
case "https://owncast.example.org/.well-known/webfinger?resource=acct%3Argh%40owncast.example.org":
|
||||||
wfr = &apimodel.WellKnownResponse{
|
wfr = &apimodel.WellKnownResponse{
|
||||||
Subject: "acct:rgh@example.org",
|
Subject: "acct:rgh@example.org",
|
||||||
Links: []apimodel.Link{
|
Links: []apimodel.Link{
|
||||||
|
@ -247,7 +288,7 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "https://unknown-instance.com/.well-known/webfinger?resource=acct:brand_new_person@unknown-instance.com":
|
case "https://unknown-instance.com/.well-known/webfinger?resource=acct%3Abrand_new_person%40unknown-instance.com":
|
||||||
wfr = &apimodel.WellKnownResponse{
|
wfr = &apimodel.WellKnownResponse{
|
||||||
Subject: "acct:brand_new_person@unknown-instance.com",
|
Subject: "acct:brand_new_person@unknown-instance.com",
|
||||||
Links: []apimodel.Link{
|
Links: []apimodel.Link{
|
||||||
|
@ -258,7 +299,7 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "https://turnip.farm/.well-known/webfinger?resource=acct:turniplover6969@turnip.farm":
|
case "https://turnip.farm/.well-known/webfinger?resource=acct%3Aturniplover6969%40turnip.farm":
|
||||||
wfr = &apimodel.WellKnownResponse{
|
wfr = &apimodel.WellKnownResponse{
|
||||||
Subject: "acct:turniplover6969@turnip.farm",
|
Subject: "acct:turniplover6969@turnip.farm",
|
||||||
Links: []apimodel.Link{
|
Links: []apimodel.Link{
|
||||||
|
@ -269,7 +310,7 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "https://fossbros-anonymous.io/.well-known/webfinger?resource=acct:foss_satan@fossbros-anonymous.io":
|
case "https://fossbros-anonymous.io/.well-known/webfinger?resource=acct%3Afoss_satan%40fossbros-anonymous.io":
|
||||||
wfr = &apimodel.WellKnownResponse{
|
wfr = &apimodel.WellKnownResponse{
|
||||||
Subject: "acct:foss_satan@fossbros-anonymous.io",
|
Subject: "acct:foss_satan@fossbros-anonymous.io",
|
||||||
Links: []apimodel.Link{
|
Links: []apimodel.Link{
|
||||||
|
@ -280,7 +321,7 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "https://example.org/.well-known/webfinger?resource=acct:Some_User@example.org":
|
case "https://example.org/.well-known/webfinger?resource=acct%3ASome_User%40example.org":
|
||||||
wfr = &apimodel.WellKnownResponse{
|
wfr = &apimodel.WellKnownResponse{
|
||||||
Subject: "acct:Some_User@example.org",
|
Subject: "acct:Some_User@example.org",
|
||||||
Links: []apimodel.Link{
|
Links: []apimodel.Link{
|
||||||
|
@ -291,6 +332,17 @@ func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byt
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
case "https://misconfigured-instance.com/.weird-webfinger-location/webfinger?resource=acct%3Asomeone%40misconfigured-instance.com":
|
||||||
|
wfr = &apimodel.WellKnownResponse{
|
||||||
|
Subject: "acct:someone@misconfigured-instance.com",
|
||||||
|
Links: []apimodel.Link{
|
||||||
|
{
|
||||||
|
Rel: "self",
|
||||||
|
Type: applicationActivityJSON,
|
||||||
|
Href: "https://misconfigured-instance.com/users/someone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if wfr == nil {
|
if wfr == nil {
|
||||||
|
|
Loading…
Reference in New Issue