forgejo-federation/services/forgefed/webfinger.go

168 lines
3.6 KiB
Go

package forgefed
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"code.gitea.io/gitea/modules/log"
)
// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
type WebfingerJRD struct {
Subject string `json:"subject,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
Links []*WebfingerLink `json:"links,omitempty"`
}
func (w WebfingerJRD) GetAvatar() *WebfingerLink {
for _, link := range w.Links {
if link.Rel == "http://webfinger.net/rel/avatar" {
return link
}
}
return nil
}
func (w WebfingerJRD) GetProfilePage() *WebfingerLink {
for _, link := range w.Links {
if link.Rel == "http://webfinger.net/rel/profile-page" && link.Type == "text/html" {
return link
}
}
return nil
}
func (w WebfingerJRD) GetActorLink() *WebfingerLink {
for _, link := range w.Links {
if link.Rel == "self" && link.Type == "application/activity+json" {
return link
}
}
return nil
}
type WebfingerLink struct {
Rel string `json:"rel,omitempty"`
Type string `json:"type,omitempty"`
Href string `json:"href,omitempty"`
Titles map[string]string `json:"titles,omitempty"`
Properties map[string]any `json:"properties,omitempty"`
}
func GetHostnameFromResource(resource string) (string, error) {
r := resource
if strings.HasPrefix(resource, "@") {
resource, _ = strings.CutPrefix(resource, "@")
}
actor, err := url.Parse(resource)
if err != nil {
return "", err
}
var hostname string
switch actor.Scheme {
case "":
i := strings.Split(resource, "@")
if len(i) != 2 {
log.Error("Invalid webfinger query " + r)
return "", errors.New("Invalid webfinger query " + r)
}
hostname = i[1]
case "mailto":
i := strings.Split(resource, "@")
if len(i) != 2 {
log.Error("Invalid webfinger query " + r)
return "", errors.New("Invalid webfinger query " + r)
}
hostname = i[1]
case "https":
hostname = actor.Host
default:
log.Error("Invalid webfinger query " + r)
return "", errors.New("Invalid webfinger query" + r)
}
return hostname, nil
}
// Get Actor object by performing webfinger lookup
func WebFingerLookup(q string) (*WebfingerJRD, error) {
if strings.HasPrefix(q, "@") {
q, _ = strings.CutPrefix(q, "@")
}
actor, err := url.Parse(q)
if err != nil {
return nil, err
}
var res string
switch actor.Scheme {
case "":
res = fmt.Sprintf("acct:%s", q)
case "mailto":
res = q
case "https":
res = q
default:
return nil, errors.New("Invalid webfinger query")
}
hostname, err := GetHostnameFromResource(q)
if err != nil {
return nil, err
}
link := fmt.Sprintf("https://%s/.well-known/webfinger?resource=%s", hostname, res)
r, err := http.Get(link)
if err != nil {
return nil, err
}
defer r.Body.Close()
webfingerResponse := new(WebfingerJRD)
err = json.NewDecoder(r.Body).Decode(webfingerResponse)
if err != nil {
return nil, err
}
return webfingerResponse, nil
}
func IsFingerable(resource string) bool {
if strings.HasPrefix(resource, "@") {
resource, _ = strings.CutPrefix(resource, "@")
}
actor, err := url.Parse(resource)
if err != nil {
return false
}
switch actor.Scheme {
case "":
i := strings.Split(resource, "@")
if len(i) == 2 {
_ = i[1] // TODO: do len check before referencing element #2
return true
}
return false
case "mailto":
i := strings.Split(resource, "@")
if len(i) == 2 {
_ = i[1]
return true
}
return false
case "https":
return true
default:
return false
}
}