WIP: fix: webfinger before loading search results for /explore/users/ #10

Draft
realaravinth wants to merge 11 commits from task-600 into forgejo
3 changed files with 196 additions and 17 deletions
Showing only changes of commit 6da9e15102 - Show all commits

View file

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/sitemap"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forgefed"
)
const (
@ -99,6 +100,31 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
return
}
}
if len(opts.Keyword) > 0 && forgefed.IsFingerable(opts.Keyword) {
webfingerRes, err := forgefed.WebFingerLookup(opts.Keyword)
if err != nil {
ctx.ServerError("SearchUsers", err)
return
}
person, err := forgefed.GetActor(webfingerRes.GetActorLink().Href)
if err != nil {
ctx.ServerError("SearchUsers", err)
return
}
_, err = forgefed.SavePerson(ctx, person)
if err != nil {
ctx.ServerError("SearchUsers", err)
return
}
// users, count, err = user_model.SearchUsers(ctx, opts)
// if err != nil {
// ctx.ServerError("SearchUsers", err)
// return
// }
}
if isSitemap {
m := sitemap.NewSitemap()
for _, item := range users {

View file

@ -13,25 +13,11 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forgefed"
)
// 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"`
}
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"`
}
// WebfingerQuery returns information about a resource
// https://datatracker.ietf.org/doc/html/rfc7565
func WebfingerQuery(ctx *context.Context) {
@ -104,7 +90,7 @@ func WebfingerQuery(ctx *context.Context) {
aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
}
links := []*webfingerLink{
links := []*forgefed.WebfingerLink{
{
Rel: "http://webfinger.net/rel/profile-page",
Type: "text/html",
@ -127,7 +113,7 @@ func WebfingerQuery(ctx *context.Context) {
ctx.Resp.Header().Add("Content-Type", "application/jrd+json")
ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
ctx.JSON(http.StatusOK, &webfingerJRD{
ctx.JSON(http.StatusOK, &forgefed.WebfingerJRD{
Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host),
Aliases: aliases,
Links: links,

View file

@ -0,0 +1,167 @@
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
}
}