feat: add .well-known/webfinger
This commit is contained in:
parent
507b7349ba
commit
9bde1a1aa7
4 changed files with 133 additions and 0 deletions
|
@ -1,5 +1,10 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Core CoreConfig `toml:"core"`
|
Core CoreConfig `toml:"core"`
|
||||||
Web WebConfig `toml:"web"`
|
Web WebConfig `toml:"web"`
|
||||||
|
@ -9,11 +14,34 @@ type Config struct {
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
// Domain should be the instance's full domain, including https:// but without the trailing slash.
|
// Domain should be the instance's full domain, including https:// but without the trailing slash.
|
||||||
Domain string `toml:"domain"`
|
Domain string `toml:"domain"`
|
||||||
|
// WebFingerDomain should be the instance's WebFinger domain, used for usernames.
|
||||||
|
// .well-known/webfinger *must* listen on this domain.
|
||||||
|
// It should only be the bare domain, i.e. `mercury.localhost`
|
||||||
|
WebFingerDomain string `toml:"webfinger_domain"`
|
||||||
|
|
||||||
// Port is the port the server should listen on.
|
// Port is the port the server should listen on.
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebFingerDomains returns the domains valid for WebFinger requests.
|
||||||
|
// The first one is always the canonical domain.
|
||||||
|
// This function is guaranteed to return at least one domain.
|
||||||
|
func (c WebConfig) WebFingerDomains() []string {
|
||||||
|
domains := make([]string, 0, 2)
|
||||||
|
|
||||||
|
if c.WebFingerDomain != "" {
|
||||||
|
domains = append(domains, c.WebFingerDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(c.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return append(domains, strings.TrimPrefix(
|
||||||
|
strings.TrimPrefix(c.Domain, "http://"), "https://"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(domains, u.Host)
|
||||||
|
}
|
||||||
|
|
||||||
type CoreConfig struct {
|
type CoreConfig struct {
|
||||||
Postgres string `toml:"postgres"`
|
Postgres string `toml:"postgres"`
|
||||||
Dev bool `toml:"dev"`
|
Dev bool `toml:"dev"`
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.sleepycat.moe/sam/mercury/web/app"
|
"git.sleepycat.moe/sam/mercury/web/app"
|
||||||
"git.sleepycat.moe/sam/mercury/web/auth"
|
"git.sleepycat.moe/sam/mercury/web/auth"
|
||||||
"git.sleepycat.moe/sam/mercury/web/frontend"
|
"git.sleepycat.moe/sam/mercury/web/frontend"
|
||||||
|
"git.sleepycat.moe/sam/mercury/web/wellknown"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,6 +26,13 @@ func Routes(app *app.App) {
|
||||||
r.Post("/sign_up", auth.PostSignup)
|
r.Post("/sign_up", auth.PostSignup)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// .well-known handlers
|
||||||
|
app.Router.Route("/.well-known", func(r chi.Router) {
|
||||||
|
wellknown := wellknown.New(app)
|
||||||
|
|
||||||
|
r.Get("/webfinger", api.WrapHandlerT(wellknown.WebFinger))
|
||||||
|
})
|
||||||
|
|
||||||
// APIv1 handlers
|
// APIv1 handlers
|
||||||
app.Router.Route("/api/v1", func(r chi.Router) {
|
app.Router.Route("/api/v1", func(r chi.Router) {
|
||||||
unauthedAccess := !app.AppConfig.Security.RestrictAPI
|
unauthedAccess := !app.AppConfig.Security.RestrictAPI
|
||||||
|
|
13
web/wellknown/module.go
Normal file
13
web/wellknown/module.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package wellknown
|
||||||
|
|
||||||
|
import "git.sleepycat.moe/sam/mercury/web/app"
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
*app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(app *app.App) *App {
|
||||||
|
return &App{
|
||||||
|
App: app,
|
||||||
|
}
|
||||||
|
}
|
84
web/wellknown/webfinger.go
Normal file
84
web/wellknown/webfinger.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package wellknown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sleepycat.moe/sam/mercury/internal/database/sql"
|
||||||
|
"git.sleepycat.moe/sam/mercury/web/api"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) WebFinger(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||||
|
ctx := r.Context()
|
||||||
|
validDomains := app.AppConfig.Web.WebFingerDomains()
|
||||||
|
|
||||||
|
resource, err := url.QueryUnescape(r.FormValue("resource"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, api.Error{Code: api.ErrBadRequest}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(resource, "acct:") {
|
||||||
|
return nil, api.Error{Code: api.ErrBadRequest, Details: "WebFinger only supports `acct:` queries"}
|
||||||
|
}
|
||||||
|
resource = strings.TrimPrefix(resource, "acct:")
|
||||||
|
username, domain, ok := strings.Cut(resource, "@")
|
||||||
|
if !ok {
|
||||||
|
return nil, api.Error{Code: api.ErrBadRequest}
|
||||||
|
}
|
||||||
|
if !anyMatches(validDomains, domain) {
|
||||||
|
return nil, api.Error{Code: api.ErrBadRequest, Details: "Not a local user on this instance"}
|
||||||
|
}
|
||||||
|
|
||||||
|
blog, err := app.Blog().ByName(ctx, username, "")
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNotFound {
|
||||||
|
return nil, api.Error{Code: api.ErrNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Err(err).Str("username", username).Msg("looking up user for webfinger request")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
webFingerHref := fmt.Sprintf("%s/@%s", app.AppConfig.Web.Domain, blog.Name)
|
||||||
|
|
||||||
|
return WebFinger{
|
||||||
|
Subject: fmt.Sprintf("acct:%s@%s", blog.Name, validDomains[0]),
|
||||||
|
Aliases: []string{webFingerHref},
|
||||||
|
Links: []WebFingerLink{
|
||||||
|
{
|
||||||
|
Rel: "http://webfinger.net/rel/profile-page",
|
||||||
|
Type: "text/html",
|
||||||
|
Href: webFingerHref,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rel: "self",
|
||||||
|
Type: "application/activity+json",
|
||||||
|
Href: webFingerHref,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebFinger struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
|
Links []WebFingerLink `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebFingerLink struct {
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyMatches[T comparable](slice []T, t T) bool {
|
||||||
|
for _, entry := range slice {
|
||||||
|
if entry == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Loading…
Reference in a new issue