forgejo/routers/web/webfinger.go
Michael Jerger 6f501b1fdf
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Improved signature handling & instance actor (#8275)
This PR is part of https://codeberg.org/forgejo/forgejo/pulls/4767

It improves the signature handling:
1. move logic to a service (might be used from other services as well)
2. make a clear difference between ` ReqHTTPUserSignature` and `ReqHTTPUserOrInstanceSignature`
3. improve test ability (activitypub/client & distant_federation_server_mock

Adjust instance actor
1. name &
2. webfinger

## Strategy for next PRs is

Integration tests are in the driving seat.

I will step by step add integration tests form original PR and add code required by the integration test changes.

## Meta

Proposal howto process large PRs can be discussed here: https://codeberg.org/forgejo-contrib/federation/pulls/37

Current state with rendered diagrams can be found here: https://codeberg.org/meissa/federation/src/branch/merge-large-pr/doc/merge-large-pr.md

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8275
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
Co-committed-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
2025-07-01 19:49:00 +02:00

193 lines
4.3 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"fmt"
"net/http"
"net/url"
"strings"
user_model "forgejo.org/models/user"
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/services/context"
)
// 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) {
appURL, _ := url.Parse(setting.AppURL)
resource, err := url.Parse(ctx.FormTrim("resource"))
if err != nil {
ctx.Error(http.StatusBadRequest)
return
}
var u *user_model.User
switch resource.Scheme {
case "acct":
// allow only the current host
parts := strings.SplitN(resource.Opaque, "@", 2)
if len(parts) != 2 {
ctx.Error(http.StatusBadRequest)
return
}
if parts[1] != appURL.Host {
ctx.Error(http.StatusBadRequest)
return
}
// Instance actor
if parts[0] == "ghost" {
aliases := []string{
appURL.String() + "api/v1/activitypub/actor",
}
links := []*webfingerLink{
{
Rel: "self",
Type: "application/activity+json",
Href: appURL.String() + "api/v1/activitypub/actor",
},
}
ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
ctx.JSON(http.StatusOK, &webfingerJRD{
Subject: fmt.Sprintf("acct:%s@%s", "ghost", appURL.Host),
Aliases: aliases,
Links: links,
})
ctx.Resp.Header().Set("Content-Type", "application/jrd+json")
return
}
u, err = user_model.GetUserByName(ctx, parts[0])
case "mailto":
u, err = user_model.GetUserByEmail(ctx, resource.Opaque)
if u != nil && u.KeepEmailPrivate {
err = user_model.ErrUserNotExist{}
}
case "https", "http":
if resource.Host != appURL.Host {
ctx.Error(http.StatusBadRequest)
return
}
p := strings.Trim(resource.Path, "/")
if len(p) == 0 {
ctx.Error(http.StatusNotFound)
return
}
parts := strings.Split(p, "/")
switch len(parts) {
case 1: // user
u, err = user_model.GetUserByName(ctx, parts[0])
case 2: // repository
ctx.Error(http.StatusNotFound)
return
case 3:
switch parts[2] {
case "issues":
ctx.Error(http.StatusNotFound)
return
case "pulls":
ctx.Error(http.StatusNotFound)
return
case "projects":
ctx.Error(http.StatusNotFound)
return
default:
ctx.Error(http.StatusNotFound)
return
}
default:
ctx.Error(http.StatusNotFound)
return
}
default:
ctx.Error(http.StatusBadRequest)
return
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusNotFound)
} else {
log.Error("Error getting user: %s Error: %v", resource.Opaque, err)
ctx.Error(http.StatusInternalServerError)
}
return
}
if !user_model.IsUserVisibleToViewer(ctx, u, ctx.Doer) {
ctx.Error(http.StatusNotFound)
return
}
aliases := []string{
u.HTMLURL(),
appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(u.ID),
}
if !u.KeepEmailPrivate {
aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
}
links := []*webfingerLink{
{
Rel: "http://webfinger.net/rel/profile-page",
Type: "text/html",
Href: u.HTMLURL(),
},
{
Rel: "http://webfinger.net/rel/avatar",
Href: u.AvatarLink(ctx),
},
{
Rel: "self",
Type: "application/activity+json",
Href: appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(u.ID),
},
{
Rel: "http://openid.net/specs/connect/1.0/issuer",
Href: appURL.String(),
},
}
ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
ctx.JSON(http.StatusOK, &webfingerJRD{
Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host),
Aliases: aliases,
Links: links,
})
ctx.Resp.Header().Set("Content-Type", "application/jrd+json")
}