mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-07 09:55:41 +02:00
Improved signature handling & instance actor (#8275)
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
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
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>
This commit is contained in:
parent
7a8ff20bf3
commit
6f501b1fdf
20 changed files with 726 additions and 443 deletions
|
@ -34,9 +34,6 @@ forgejo.org/models/dbfs
|
|||
Create
|
||||
Rename
|
||||
|
||||
forgejo.org/models/forgefed
|
||||
GetFederationHost
|
||||
|
||||
forgejo.org/models/forgejo/semver
|
||||
GetVersion
|
||||
SetVersionString
|
||||
|
@ -68,7 +65,6 @@ forgejo.org/models/user
|
|||
DeleteUserSetting
|
||||
GetFederatedUser
|
||||
GetFederatedUserByUserID
|
||||
UpdateFederatedUser
|
||||
GetFollowersForUser
|
||||
AddFollower
|
||||
RemoveFollower
|
||||
|
@ -249,6 +245,9 @@ forgejo.org/routers/web/org
|
|||
forgejo.org/services/context
|
||||
GetPrivateContext
|
||||
|
||||
forgejo.org/services/federation
|
||||
Init
|
||||
|
||||
forgejo.org/services/repository
|
||||
IsErrForkAlreadyExist
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ func (u *User) APActorID() string {
|
|||
return fmt.Sprintf("%sapi/v1/activitypub/user-id/%s", setting.AppURL, url.PathEscape(fmt.Sprintf("%d", u.ID)))
|
||||
}
|
||||
|
||||
// APActorKeyID returns the ID of the user's public key
|
||||
func (u *User) APActorKeyID() string {
|
||||
// KeyID returns the ID of the user's public key
|
||||
func (u *User) KeyID() string {
|
||||
return u.APActorID() + "#main-key"
|
||||
}
|
||||
|
|
|
@ -57,14 +57,6 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
|
|||
return committer.Commit()
|
||||
}
|
||||
|
||||
func (federatedUser *FederatedUser) UpdateFederatedUser(ctx context.Context) error {
|
||||
if _, err := validation.IsValid(federatedUser); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.GetEngine(ctx).ID(federatedUser.ID).Cols("inbox_path").Update(federatedUser)
|
||||
return err
|
||||
}
|
||||
|
||||
func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
|
||||
federatedUser := new(FederatedUser)
|
||||
user := new(User)
|
||||
|
@ -219,7 +211,6 @@ func RemoveFollower(ctx context.Context, followedUser *User, followingUser *Fede
|
|||
return err
|
||||
}
|
||||
|
||||
// TODO: We should unify Activity-pub-following and classical following (see models/user/follow.go)
|
||||
func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) {
|
||||
if res, err := validation.IsValid(followedUser); !res {
|
||||
return false, err
|
||||
|
|
|
@ -150,7 +150,7 @@ func TestAPActorID_APActorID(t *testing.T) {
|
|||
|
||||
func TestKeyID(t *testing.T) {
|
||||
user := user_model.User{ID: 1}
|
||||
url := user.APActorKeyID()
|
||||
url := user.KeyID()
|
||||
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key"
|
||||
assert.Equal(t, expected, url)
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ func NewClientFactory() (c *ClientFactory, err error) {
|
|||
|
||||
type APClientFactory interface {
|
||||
WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error)
|
||||
WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error)
|
||||
}
|
||||
|
||||
// Client struct
|
||||
|
@ -103,12 +104,8 @@ type Client struct {
|
|||
}
|
||||
|
||||
// NewRequest function
|
||||
func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) {
|
||||
priv, err := GetPrivateKey(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privPem, _ := pem.Decode([]byte(priv))
|
||||
func (cf *ClientFactory) WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error) {
|
||||
privPem, _ := pem.Decode([]byte(privateKey))
|
||||
privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -126,6 +123,14 @@ func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pu
|
|||
return &c, nil
|
||||
}
|
||||
|
||||
func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) {
|
||||
priv, err := GetPrivateKey(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cf.WithKeysDirect(ctx, priv, pubID)
|
||||
}
|
||||
|
||||
// NewRequest function
|
||||
func (c *Client) newRequest(method string, b []byte, to string) (req *http.Request, err error) {
|
||||
buf := bytes.NewBuffer(b)
|
||||
|
@ -149,12 +154,14 @@ func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := signer.SignRequest(c.priv, c.pubID, req, b); err != nil {
|
||||
return nil, err
|
||||
if c.pubID != "" {
|
||||
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := signer.SignRequest(c.priv, c.pubID, req, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = c.client.Do(req)
|
||||
|
@ -167,12 +174,15 @@ func (c *Client) Get(to string) (resp *http.Response, err error) {
|
|||
if req, err = c.newRequest(http.MethodGet, nil, to); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.getHeaders, httpsig.Signature, httpsigExpirationTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := signer.SignRequest(c.priv, c.pubID, req, nil); err != nil {
|
||||
return nil, err
|
||||
|
||||
if c.pubID != "" {
|
||||
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.getHeaders, httpsig.Signature, httpsigExpirationTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := signer.SignRequest(c.priv, c.pubID, req, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = c.client.Do(req)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
@ -7,3 +8,15 @@ package structs
|
|||
type ActivityPub struct {
|
||||
Context string `json:"@context"`
|
||||
}
|
||||
|
||||
type APRemoteFollowOption struct {
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type APPersonFollowItem struct {
|
||||
ActorID string `json:"actor_id"`
|
||||
Note string `json:"note"`
|
||||
|
||||
OriginalURL string `json:"original_url"`
|
||||
OriginalItem string `json:"original_item"`
|
||||
}
|
||||
|
|
|
@ -10,56 +10,79 @@ import (
|
|||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
type FederationServerMockPerson struct {
|
||||
ID int64
|
||||
Name string
|
||||
PubKey string
|
||||
ID int64
|
||||
Name string
|
||||
PubKey string
|
||||
PrivKey string
|
||||
}
|
||||
type FederationServerMockRepository struct {
|
||||
ID int64
|
||||
}
|
||||
type ApActorMock struct {
|
||||
PrivKey string
|
||||
PubKey string
|
||||
}
|
||||
type FederationServerMock struct {
|
||||
ApActor ApActorMock
|
||||
Persons []FederationServerMockPerson
|
||||
Repositories []FederationServerMockRepository
|
||||
LastPost string
|
||||
}
|
||||
|
||||
func NewFederationServerMockPerson(id int64, name string) FederationServerMockPerson {
|
||||
priv, pub, _ := util.GenerateKeyPair(3072)
|
||||
return FederationServerMockPerson{
|
||||
ID: id,
|
||||
Name: name,
|
||||
PubKey: `"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` +
|
||||
`CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` +
|
||||
`T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` +
|
||||
`nx+nou+3dD7NluULLtdd7K+2x02trObKXCAzmi5/Dc+yKTzpFqEz+hLNCz7TImP/\ncK//NV9Q+X67J9O27baH9R9ZF4zMw8rv2Pg0WLSw1z7lLXwlgIsDapeMCsrxkVO4\n` +
|
||||
`LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"`,
|
||||
ID: id,
|
||||
Name: name,
|
||||
PubKey: pub,
|
||||
PrivKey: priv,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *FederationServerMockPerson) KeyID(host string) string {
|
||||
return fmt.Sprintf("%[1]v/api/v1/activitypub/user-id/%[2]v#main-key", host, p.ID)
|
||||
}
|
||||
|
||||
func NewFederationServerMockRepository(id int64) FederationServerMockRepository {
|
||||
return FederationServerMockRepository{
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApActorMock() ApActorMock {
|
||||
priv, pub, _ := util.GenerateKeyPair(1024)
|
||||
return ApActorMock{
|
||||
PrivKey: priv,
|
||||
PubKey: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ApActorMock) KeyID(host string) string {
|
||||
return fmt.Sprintf("%[1]v/api/v1/activitypub/actor#main-key", host)
|
||||
}
|
||||
|
||||
func (p FederationServerMockPerson) marshal(host string) string {
|
||||
return fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+
|
||||
`"id":"http://%[1]v/api/activitypub/user-id/%[2]v",`+
|
||||
`"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+
|
||||
`"type":"Person",`+
|
||||
`"icon":{"type":"Image","mediaType":"image/png","url":"http://%[1]v/avatars/1bb05d9a5f6675ed0272af9ea193063c"},`+
|
||||
`"url":"http://%[1]v/%[2]v",`+
|
||||
`"inbox":"http://%[1]v/api/activitypub/user-id/%[2]v/inbox",`+
|
||||
`"outbox":"http://%[1]v/api/activitypub/user-id/%[2]v/outbox",`+
|
||||
`"inbox":"http://%[1]v/api/v1/activitypub/user-id/%[2]v/inbox",`+
|
||||
`"outbox":"http://%[1]v/api/v1/activitypub/user-id/%[2]v/outbox",`+
|
||||
`"preferredUsername":"%[3]v",`+
|
||||
`"publicKey":{"id":"http://%[1]v/api/activitypub/user-id/%[2]v#main-key",`+
|
||||
`"owner":"http://%[1]v/api/activitypub/user-id/%[2]v",`+
|
||||
`"publicKeyPem":%[4]v}}`, host, p.ID, p.Name, p.PubKey)
|
||||
`"publicKey":{"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v#main-key",`+
|
||||
`"owner":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+
|
||||
`"publicKeyPem":%[4]q}}`, host, p.ID, p.Name, p.PubKey)
|
||||
}
|
||||
|
||||
func NewFederationServerMock() *FederationServerMock {
|
||||
return &FederationServerMock{
|
||||
ApActor: NewApActorMock(),
|
||||
Persons: []FederationServerMockPerson{
|
||||
NewFederationServerMockPerson(15, "stargoose1"),
|
||||
NewFederationServerMockPerson(30, "stargoose2"),
|
||||
|
@ -71,8 +94,18 @@ func NewFederationServerMock() *FederationServerMock {
|
|||
}
|
||||
}
|
||||
|
||||
func (mock *FederationServerMock) recordLastPost(t *testing.T, req *http.Request) {
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading body: %q", err)
|
||||
}
|
||||
mock.LastPost = strings.ReplaceAll(buf.String(), req.Host, "DISTANT_FEDERATION_HOST")
|
||||
}
|
||||
|
||||
func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
|
||||
federatedRoutes := http.NewServeMux()
|
||||
|
||||
federatedRoutes.HandleFunc("/.well-known/nodeinfo",
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo
|
||||
|
@ -87,30 +120,28 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
|
|||
`"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},`+
|
||||
`"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`)
|
||||
})
|
||||
|
||||
for _, person := range mock.Persons {
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/%v", person.ID),
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2
|
||||
fmt.Fprint(res, person.marshal(req.Host))
|
||||
})
|
||||
}
|
||||
for _, repository := range mock.Repositories {
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox", repository.ID),
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("POST /api/v1/activitypub/user-id/%v/inbox", person.ID),
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("POST expected at: %q", req.URL.EscapedPath())
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading body: %q", err)
|
||||
}
|
||||
mock.LastPost = buf.String()
|
||||
mock.recordLastPost(t, req)
|
||||
})
|
||||
}
|
||||
|
||||
for _, repository := range mock.Repositories {
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("POST /api/v1/activitypub/repository-id/%v/inbox", repository.ID),
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
mock.recordLastPost(t, req)
|
||||
})
|
||||
}
|
||||
federatedRoutes.HandleFunc("/",
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
t.Errorf("Unhandled request: %q", req.URL.EscapedPath())
|
||||
t.Errorf("Unhandled %v request: %q", req.Method, req.URL.EscapedPath())
|
||||
})
|
||||
federatedSrv := httptest.NewServer(federatedRoutes)
|
||||
return federatedSrv
|
||||
|
|
|
@ -32,7 +32,7 @@ func Actor(ctx *context.APIContext) {
|
|||
actor := ap.ActorNew(ap.IRI(link), ap.ApplicationType)
|
||||
|
||||
actor.PreferredUsername = ap.NaturalLanguageValuesNew()
|
||||
err := actor.PreferredUsername.Set("en", ap.Content(setting.Domain))
|
||||
err := actor.PreferredUsername.Set("en", ap.Content("ghost"))
|
||||
if err != nil {
|
||||
ctx.ServerError("PreferredUsername.Set", err)
|
||||
return
|
||||
|
@ -41,8 +41,6 @@ func Actor(ctx *context.APIContext) {
|
|||
actor.URL = ap.IRI(setting.AppURL)
|
||||
|
||||
actor.Inbox = ap.IRI(link + "/inbox")
|
||||
actor.Outbox = ap.IRI(link + "/outbox")
|
||||
|
||||
actor.PublicKey.ID = ap.IRI(link + "#main-key")
|
||||
actor.PublicKey.Owner = ap.IRI(link)
|
||||
|
||||
|
|
|
@ -4,132 +4,17 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
fm "forgejo.org/modules/forgefed"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
gitea_context "forgejo.org/services/context"
|
||||
"forgejo.org/services/federation"
|
||||
|
||||
"github.com/42wim/httpsig"
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
|
||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
}
|
||||
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
func getFederatedUser(ctx *gitea_context.APIContext, person *ap.Person, federationHost *forgefed.FederationHost) (*user.FederatedUser, error) {
|
||||
personID, err := fm.NewPersonID(person.ID.String(), string(federationHost.NodeInfo.SoftwareName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, federatedUser, err := user.FindFederatedUser(ctx, personID.ID, federationHost.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if federatedUser != nil {
|
||||
return federatedUser, nil
|
||||
}
|
||||
|
||||
_, newFederatedUser, err := federation.CreateUserFromAP(ctx, personID, federationHost.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newFederatedUser, nil
|
||||
}
|
||||
|
||||
func storePublicKey(ctx *gitea_context.APIContext, person *ap.Person, pubKeyBytes []byte) error {
|
||||
federationHost, err := federation.GetFederationHostForURI(ctx, person.ID.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if person.Type == ap.ActivityVocabularyType("Application") {
|
||||
federationHost.KeyID = sql.NullString{
|
||||
String: person.PublicKey.ID.String(),
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
federationHost.PublicKey = sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(federationHost.ID).Update(federationHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if person.Type == ap.ActivityVocabularyType("Person") {
|
||||
federatedUser, err := getFederatedUser(ctx, person, federationHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
federatedUser.KeyID = sql.NullString{
|
||||
String: person.PublicKey.ID.String(),
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
federatedUser.PublicKey = sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (person *ap.Person, pubKeyBytes []byte, p crypto.PublicKey, err error) {
|
||||
person = ap.PersonNew(ap.IRI(keyID.String()))
|
||||
err = person.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
|
||||
}
|
||||
|
||||
pubKey := person.PublicKey
|
||||
if pubKey.ID.String() != keyID.String() {
|
||||
return nil, nil, nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
|
||||
}
|
||||
|
||||
pubKeyBytes, err = decodePublicKeyPem(pubKey.PublicKeyPem)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
p, err = x509.ParsePKIXPublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return person, pubKeyBytes, p, err
|
||||
}
|
||||
|
||||
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
||||
func verifyHTTPUserOrInstanceSignature(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
||||
if !setting.Federation.SignatureEnforced {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -142,84 +27,64 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
|
|||
return false, err
|
||||
}
|
||||
|
||||
ID := v.KeyId()
|
||||
idIRI, err := url.Parse(ID)
|
||||
signatureAlgorithm := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
|
||||
pubKey, err := federation.FindOrCreateFederatedUserKey(ctx.Base, v.KeyId())
|
||||
if err != nil || pubKey == nil {
|
||||
pubKey, err = federation.FindOrCreateFederationHostKey(ctx.Base, v.KeyId())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
err = v.Verify(pubKey, signatureAlgorithm)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func verifyHTTPUserSignature(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
||||
if !setting.Federation.SignatureEnforced {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
r := ctx.Req
|
||||
|
||||
// 1. Figure out what key we need to verify
|
||||
v, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
signatureAlgorithm := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
|
||||
|
||||
// 2. Fetch the public key of the other actor
|
||||
// Try if the signing actor is an already known federated user
|
||||
_, federationUser, err := user.FindFederatedUserByKeyID(ctx, idIRI.String())
|
||||
pubKey, err := federation.FindOrCreateFederatedUserKey(ctx.Base, v.KeyId())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if federationUser != nil && federationUser.PublicKey.Valid {
|
||||
pubKey, err := x509.ParsePKIXPublicKey(federationUser.PublicKey.V)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||
return authenticated, err
|
||||
}
|
||||
|
||||
// Try if the signing actor is an already known federation host
|
||||
federationHost, err := forgefed.FindFederationHostByKeyID(ctx, idIRI.String())
|
||||
err = v.Verify(pubKey, signatureAlgorithm)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if federationHost != nil && federationHost.PublicKey.Valid {
|
||||
pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||
return authenticated, err
|
||||
}
|
||||
|
||||
// Fetch missing public key
|
||||
actionsUser := user.NewAPServerActor()
|
||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.APActorKeyID())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
b, err := apClient.GetBody(idIRI.String())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
person, pubKeyBytes, pubKey, err := getPublicKeyFromResponse(b, idIRI)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||
if authenticated {
|
||||
err = storePublicKey(ctx, person, pubKeyBytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return authenticated, err
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ReqHTTPSignature function
|
||||
func ReqHTTPSignature() func(ctx *gitea_context.APIContext) {
|
||||
func ReqHTTPUserOrInstanceSignature() func(ctx *gitea_context.APIContext) {
|
||||
return func(ctx *gitea_context.APIContext) {
|
||||
if authenticated, err := verifyHTTPSignatures(ctx); err != nil {
|
||||
if authenticated, err := verifyHTTPUserOrInstanceSignature(ctx); err != nil {
|
||||
log.Warn("verifyHttpSignatures failed: %v", err)
|
||||
ctx.Error(http.StatusBadRequest, "reqSignature", "request signature verification failed")
|
||||
} else if !authenticated {
|
||||
ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReqHTTPSignature function
|
||||
func ReqHTTPUserSignature() func(ctx *gitea_context.APIContext) {
|
||||
return func(ctx *gitea_context.APIContext) {
|
||||
if authenticated, err := verifyHTTPUserSignature(ctx); err != nil {
|
||||
log.Warn("verifyHttpSignatures failed: %v", err)
|
||||
ctx.Error(http.StatusBadRequest, "reqSignature", "request signature verification failed")
|
||||
} else if !authenticated {
|
||||
|
|
|
@ -92,6 +92,7 @@ import (
|
|||
_ "forgejo.org/routers/api/v1/swagger" // for swagger generation
|
||||
|
||||
"code.forgejo.org/go-chi/binding"
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func sudo() func(ctx *context.APIContext) {
|
||||
|
@ -826,24 +827,22 @@ func Routes() *web.Route {
|
|||
if setting.Federation.Enabled {
|
||||
m.Get("/nodeinfo", misc.NodeInfo)
|
||||
m.Group("/activitypub", func() {
|
||||
// deprecated, remove in 1.20, use /user-id/{user-id} instead
|
||||
m.Group("/user/{username}", func() {
|
||||
m.Get("", activitypub.ReqHTTPSignature(), activitypub.Person)
|
||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||
m.Group("/user-id/{user-id}", func() {
|
||||
m.Get("", activitypub.ReqHTTPSignature(), activitypub.Person)
|
||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||
m.Get("", activitypub.ReqHTTPUserOrInstanceSignature(), activitypub.Person)
|
||||
m.Post("/inbox",
|
||||
activitypub.ReqHTTPUserSignature(),
|
||||
bind(ap.Activity{}),
|
||||
activitypub.PersonInbox)
|
||||
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
|
||||
m.Group("/actor", func() {
|
||||
m.Get("", activitypub.Actor)
|
||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.ActorInbox)
|
||||
m.Post("/inbox", activitypub.ReqHTTPUserOrInstanceSignature(), activitypub.ActorInbox)
|
||||
})
|
||||
m.Group("/repository-id/{repository-id}", func() {
|
||||
m.Get("", activitypub.ReqHTTPSignature(), activitypub.Repository)
|
||||
m.Get("", activitypub.ReqHTTPUserSignature(), activitypub.Repository)
|
||||
m.Post("/inbox",
|
||||
bind(forgefed.ForgeLike{}),
|
||||
activitypub.ReqHTTPSignature(),
|
||||
activitypub.ReqHTTPUserSignature(),
|
||||
activitypub.RepositoryInbox)
|
||||
}, context.RepositoryIDAssignmentAPI())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
|
||||
|
|
|
@ -58,7 +58,33 @@ func WebfingerQuery(ctx *context.Context) {
|
|||
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 {
|
||||
|
|
|
@ -5,15 +5,12 @@ package federation
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/repo"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
"forgejo.org/modules/auth/password"
|
||||
|
@ -21,91 +18,84 @@ import (
|
|||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/validation"
|
||||
context_service "forgejo.org/services/context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ProcessLikeActivity receives a ForgeLike activity and does the following:
|
||||
// Validation of the activity
|
||||
// Creation of a (remote) federationHost if not existing
|
||||
// Creation of a forgefed Person if not existing
|
||||
// Validation of incoming RepositoryID against Local RepositoryID
|
||||
// Star the repo if it wasn't already stared
|
||||
// Do some mitigation against out of order attacks
|
||||
func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int, string, error) {
|
||||
activity := form.(*fm.ForgeLike)
|
||||
if res, err := validation.IsValid(activity); !res {
|
||||
return http.StatusNotAcceptable, "Invalid activity", err
|
||||
}
|
||||
log.Info("Activity validated:%v", activity)
|
||||
func Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse actorID (person)
|
||||
actorURI := activity.Actor.GetID().String()
|
||||
log.Info("actorURI was: %v", actorURI)
|
||||
federationHost, err := GetFederationHostForURI(ctx, actorURI)
|
||||
func FindOrCreateFederationHost(ctx *context_service.Base, actorURI string) (*forgefed.FederationHost, error) {
|
||||
rawActorID, err := fm.NewActorID(actorURI)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "Wrong FederationHost", err
|
||||
return nil, err
|
||||
}
|
||||
if !activity.IsNewer(federationHost.LatestActivity) {
|
||||
return http.StatusNotAcceptable, "Activity out of order.", errors.New("Activity already processed")
|
||||
federationHost, err := forgefed.FindFederationHostByFqdnAndPort(ctx, rawActorID.Host, rawActorID.HostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if federationHost == nil {
|
||||
result, err := createFederationHostFromAP(ctx, rawActorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
federationHost = result
|
||||
}
|
||||
return federationHost, nil
|
||||
}
|
||||
|
||||
func FindOrCreateFederatedUser(ctx *context_service.Base, actorURI string) (*user.User, *user.FederatedUser, *forgefed.FederationHost, error) {
|
||||
user, federatedUser, federationHost, err := findFederatedUser(ctx, actorURI)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
personID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
log.Trace("Found local federatedUser: %#v", user)
|
||||
} else {
|
||||
user, federatedUser, err = createUserFromAP(ctx, personID, federationHost.ID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
log.Trace("Created federatedUser from ap: %#v", user)
|
||||
}
|
||||
log.Trace("Got user: %v", user.Name)
|
||||
|
||||
return user, federatedUser, federationHost, nil
|
||||
}
|
||||
|
||||
func findFederatedUser(ctx *context_service.Base, actorURI string) (*user.User, *user.FederatedUser, *forgefed.FederationHost, error) {
|
||||
federationHost, err := FindOrCreateFederationHost(ctx, actorURI)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid PersonID", err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
log.Info("Actor accepted:%v", actorID)
|
||||
|
||||
// parse objectID (repository)
|
||||
objectID, err := fm.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
|
||||
user, federatedUser, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid objectId", err
|
||||
}
|
||||
if objectID.ID != fmt.Sprint(repositoryID) {
|
||||
return http.StatusNotAcceptable, "Invalid objectId", err
|
||||
}
|
||||
log.Info("Object accepted:%v", objectID)
|
||||
|
||||
// Check if user already exists
|
||||
user, _, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "Searching for user failed", err
|
||||
}
|
||||
if user != nil {
|
||||
log.Info("Found local federatedUser: %v", user)
|
||||
} else {
|
||||
user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "Error creating federatedUser", err
|
||||
}
|
||||
log.Info("Created federatedUser from ap: %v", user)
|
||||
}
|
||||
log.Info("Got user:%v", user.Name)
|
||||
|
||||
// execute the activity if the repo was not stared already
|
||||
alreadyStared := repo.IsStaring(ctx, user.ID, repositoryID)
|
||||
if !alreadyStared {
|
||||
err = repo.StarRepo(ctx, user.ID, repositoryID, true)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Error staring", err
|
||||
}
|
||||
}
|
||||
federationHost.LatestActivity = activity.StartTime
|
||||
err = forgefed.UpdateFederationHost(ctx, federationHost)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Error updating federatedHost", err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return 0, "", nil
|
||||
return user, federatedUser, federationHost, nil
|
||||
}
|
||||
|
||||
func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) {
|
||||
func createFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) {
|
||||
actionsUser := user.NewAPServerActor()
|
||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.APActorKeyID())
|
||||
client, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -130,6 +120,7 @@ func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forge
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: we should get key material here also to have it immediately
|
||||
result, err := forgefed.NewFederationHost(actorID.Host, nodeInfo, actorID.HostPort, actorID.HostSchema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -143,34 +134,14 @@ func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forge
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) {
|
||||
log.Info("Input was: %v", actorURI)
|
||||
rawActorID, err := fm.NewActorID(actorURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
federationHost, err := forgefed.FindFederationHostByFqdnAndPort(ctx, rawActorID.Host, rawActorID.HostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if federationHost == nil {
|
||||
result, err := CreateFederationHostFromAP(ctx, rawActorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
federationHost = result
|
||||
}
|
||||
return federationHost, nil
|
||||
}
|
||||
|
||||
func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
|
||||
func fetchUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
|
||||
actionsUser := user.NewAPServerActor()
|
||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.APActorKeyID())
|
||||
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -216,6 +187,11 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
pubKeyBytes, err := decodePublicKeyPem(person.PublicKey.PublicKeyPem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newUser := user.User{
|
||||
LowerName: strings.ToLower(name),
|
||||
Name: name,
|
||||
|
@ -234,86 +210,30 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
|||
FederationHostID: federationHostID,
|
||||
InboxPath: inbox.Path,
|
||||
NormalizedOriginalURL: personID.AsURI(),
|
||||
KeyID: sql.NullString{
|
||||
String: person.PublicKey.ID.String(),
|
||||
Valid: true,
|
||||
},
|
||||
PublicKey: sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
|
||||
log.Info("Fetch federatedUser:%q", federatedUser)
|
||||
return &newUser, &federatedUser, nil
|
||||
}
|
||||
|
||||
func createUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
|
||||
newUser, federatedUser, err := fetchUserFromAP(ctx, personID, federationHostID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = user.CreateFederatedUser(ctx, newUser, federatedUser)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
log.Info("Created federatedUser:%q", federatedUser)
|
||||
return &newUser, &federatedUser, nil
|
||||
}
|
||||
|
||||
// Create or update a list of FollowingRepo structs
|
||||
func StoreFollowingRepoList(ctx context.Context, localRepoID int64, followingRepoList []string) (int, string, error) {
|
||||
followingRepos := make([]*repo.FollowingRepo, 0, len(followingRepoList))
|
||||
for _, uri := range followingRepoList {
|
||||
federationHost, err := GetFederationHostForURI(ctx, uri)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "Wrong FederationHost", err
|
||||
}
|
||||
followingRepoID, err := fm.NewRepositoryID(uri, string(federationHost.NodeInfo.SoftwareName))
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid federated repo", err
|
||||
}
|
||||
followingRepo, err := repo.NewFollowingRepo(localRepoID, followingRepoID.ID, federationHost.ID, uri)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid federated repo", err
|
||||
}
|
||||
followingRepos = append(followingRepos, &followingRepo)
|
||||
}
|
||||
|
||||
if err := repo.StoreFollowingRepos(ctx, localRepoID, followingRepos); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
func DeleteFollowingRepos(ctx context.Context, localRepoID int64) error {
|
||||
return repo.StoreFollowingRepos(ctx, localRepoID, []*repo.FollowingRepo{})
|
||||
}
|
||||
|
||||
func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error {
|
||||
followingRepos, err := repo.FindFollowingReposByRepoID(ctx, repoID)
|
||||
log.Info("Federated Repos is: %v", followingRepos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
likeActivityList := make([]fm.ForgeLike, 0)
|
||||
for _, followingRepo := range followingRepos {
|
||||
log.Info("Found following repo: %v", followingRepo)
|
||||
target := followingRepo.URI
|
||||
likeActivity, err := fm.NewForgeLike(doer.APActorID(), target, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likeActivityList = append(likeActivityList, likeActivity)
|
||||
}
|
||||
|
||||
apclientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorKeyID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, activity := range likeActivityList {
|
||||
activity.StartTime = activity.StartTime.Add(time.Duration(i) * time.Second)
|
||||
json, err := activity.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = apclient.Post(json, fmt.Sprintf("%s/inbox", activity.Object))
|
||||
if err != nil {
|
||||
log.Error("error %v while sending activity: %q", err, activity)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return newUser, federatedUser, nil
|
||||
}
|
||||
|
|
146
services/federation/repo_like.go
Normal file
146
services/federation/repo_like.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/repo"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
fm "forgejo.org/modules/forgefed"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/validation"
|
||||
context_service "forgejo.org/services/context"
|
||||
)
|
||||
|
||||
// ProcessLikeActivity receives a ForgeLike activity and does the following:
|
||||
// Validation of the activity
|
||||
// Creation of a (remote) federationHost if not existing
|
||||
// Creation of a forgefed Person if not existing
|
||||
// Validation of incoming RepositoryID against Local RepositoryID
|
||||
// Star the repo if it wasn't already stared
|
||||
// Do some mitigation against out of order attacks
|
||||
func ProcessLikeActivity(ctx *context_service.APIContext, form any, repositoryID int64) (int, string, error) {
|
||||
activity := form.(*fm.ForgeLike)
|
||||
if res, err := validation.IsValid(activity); !res {
|
||||
return http.StatusNotAcceptable, "Invalid activity", err
|
||||
}
|
||||
log.Trace("Activity validated: %#v", activity)
|
||||
|
||||
// parse actorID (person)
|
||||
actorURI := activity.Actor.GetID().String()
|
||||
user, _, federationHost, err := FindOrCreateFederatedUser(ctx.Base, actorURI)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusNotAcceptable, "Federated user not found", err)
|
||||
return http.StatusInternalServerError, "FindOrCreateFederatedUser", err
|
||||
}
|
||||
|
||||
if !activity.IsNewer(federationHost.LatestActivity) {
|
||||
return http.StatusNotAcceptable, "Activity out of order.", errors.New("Activity already processed")
|
||||
}
|
||||
|
||||
// parse objectID (repository)
|
||||
objectID, err := fm.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid objectId", err
|
||||
}
|
||||
if objectID.ID != fmt.Sprint(repositoryID) {
|
||||
return http.StatusNotAcceptable, "Invalid objectId", err
|
||||
}
|
||||
log.Trace("Object accepted: %#v", objectID)
|
||||
|
||||
// execute the activity if the repo was not stared already
|
||||
alreadyStared := repo.IsStaring(ctx, user.ID, repositoryID)
|
||||
if !alreadyStared {
|
||||
err = repo.StarRepo(ctx, user.ID, repositoryID, true)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Error staring", err
|
||||
}
|
||||
}
|
||||
federationHost.LatestActivity = activity.StartTime
|
||||
err = forgefed.UpdateFederationHost(ctx, federationHost)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Error updating federatedHost", err
|
||||
}
|
||||
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
// Create or update a list of FollowingRepo structs
|
||||
func StoreFollowingRepoList(ctx *context_service.Context, localRepoID int64, followingRepoList []string) (int, string, error) {
|
||||
followingRepos := make([]*repo.FollowingRepo, 0, len(followingRepoList))
|
||||
for _, uri := range followingRepoList {
|
||||
federationHost, err := FindOrCreateFederationHost(ctx.Base, uri)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "Wrong FederationHost", err
|
||||
}
|
||||
followingRepoID, err := fm.NewRepositoryID(uri, string(federationHost.NodeInfo.SoftwareName))
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid federated repo", err
|
||||
}
|
||||
followingRepo, err := repo.NewFollowingRepo(localRepoID, followingRepoID.ID, federationHost.ID, uri)
|
||||
if err != nil {
|
||||
return http.StatusNotAcceptable, "Invalid federated repo", err
|
||||
}
|
||||
followingRepos = append(followingRepos, &followingRepo)
|
||||
}
|
||||
|
||||
if err := repo.StoreFollowingRepos(ctx, localRepoID, followingRepos); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
func DeleteFollowingRepos(ctx context.Context, localRepoID int64) error {
|
||||
return repo.StoreFollowingRepos(ctx, localRepoID, []*repo.FollowingRepo{})
|
||||
}
|
||||
|
||||
func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error {
|
||||
followingRepos, err := repo.FindFollowingReposByRepoID(ctx, repoID)
|
||||
log.Trace("Federated Repos is: %#v", followingRepos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
likeActivityList := make([]fm.ForgeLike, 0)
|
||||
for _, followingRepo := range followingRepos {
|
||||
log.Trace("Found following repo: %#v", followingRepo)
|
||||
target := followingRepo.URI
|
||||
likeActivity, err := fm.NewForgeLike(doer.APActorID(), target, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likeActivityList = append(likeActivityList, likeActivity)
|
||||
}
|
||||
|
||||
apclientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorID()+"#main-key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, activity := range likeActivityList {
|
||||
activity.StartTime = activity.StartTime.Add(time.Duration(i) * time.Second)
|
||||
json, err := activity.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = apclient.Post(json, fmt.Sprintf("%v/inbox", activity.Object))
|
||||
if err != nil {
|
||||
log.Error("error %v while sending activity: %#v", err, activity)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
234
services/federation/signature_service.go
Normal file
234
services/federation/signature_service.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
fm "forgejo.org/modules/forgefed"
|
||||
context_service "forgejo.org/services/context"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// Factory function for ActorID. Created struct is asserted to be valid
|
||||
func NewActorIDFromKeyID(ctx *context_service.Base, uri string) (fm.ActorID, error) {
|
||||
parsedURI, err := url.Parse(uri)
|
||||
parsedURI.Fragment = ""
|
||||
if err != nil {
|
||||
return fm.ActorID{}, err
|
||||
}
|
||||
|
||||
actionsUser := user.NewAPServerActor()
|
||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return fm.ActorID{}, err
|
||||
}
|
||||
|
||||
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
|
||||
if err != nil {
|
||||
return fm.ActorID{}, err
|
||||
}
|
||||
|
||||
userResponse, err := apClient.GetBody(parsedURI.String())
|
||||
if err != nil {
|
||||
return fm.ActorID{}, err
|
||||
}
|
||||
|
||||
var actor ap.Actor
|
||||
err = actor.UnmarshalJSON(userResponse)
|
||||
if err != nil {
|
||||
return fm.ActorID{}, err
|
||||
}
|
||||
|
||||
result, err := fm.NewActorID(actor.PublicKey.Owner.String())
|
||||
return result, err
|
||||
}
|
||||
|
||||
func FindOrCreateFederatedUserKey(ctx *context_service.Base, keyID string) (pubKey any, err error) {
|
||||
var federatedUser *user.FederatedUser
|
||||
var keyURL *url.URL
|
||||
|
||||
keyURL, err = url.Parse(keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try if the signing actor is an already known federated user
|
||||
_, federatedUser, err = user.FindFederatedUserByKeyID(ctx, keyURL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if federatedUser == nil {
|
||||
rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, federatedUser, _, err = FindOrCreateFederatedUser(ctx, rawActorID.AsURI())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
_, err = forgefed.GetFederationHost(ctx, federatedUser.FederationHostID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if federatedUser.PublicKey.Valid {
|
||||
pubKey, err := x509.ParsePKIXPublicKey(federatedUser.PublicKey.V)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// Fetch missing public key
|
||||
pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apPerson.Type == ap.ActivityVocabularyType("Person") {
|
||||
// Check federatedUser.id = person.id
|
||||
if federatedUser.ExternalID != apPerson.ID.String() {
|
||||
return nil, fmt.Errorf("federated user fetched (%v) does not match the stored one %v", apPerson, federatedUser)
|
||||
}
|
||||
// update federated user
|
||||
federatedUser.KeyID = sql.NullString{
|
||||
String: apPerson.PublicKey.ID.String(),
|
||||
Valid: true,
|
||||
}
|
||||
federatedUser.PublicKey = sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
}
|
||||
err = user.UpdateFederatedUser(ctx, federatedUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func FindOrCreateFederationHostKey(ctx *context_service.Base, keyID string) (pubKey any, err error) {
|
||||
keyURL, err := url.Parse(keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Is there an already known federation host?
|
||||
federationHost, err := forgefed.FindFederationHostByKeyID(ctx, keyURL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if federationHost == nil {
|
||||
federationHost, err = FindOrCreateFederationHost(ctx, rawActorID.AsURI())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Is there an already an key?
|
||||
if federationHost.PublicKey.Valid {
|
||||
pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// If not, fetch missing public key
|
||||
pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apPerson.Type == ap.ActivityVocabularyType("Application") {
|
||||
// Check federationhost.id = person.id
|
||||
if federationHost.HostPort != rawActorID.HostPort || federationHost.HostFqdn != rawActorID.Host ||
|
||||
federationHost.HostSchema != rawActorID.HostSchema {
|
||||
return nil, fmt.Errorf("federation host fetched (%v) does not match the stored one %v", apPerson, federationHost)
|
||||
}
|
||||
// update federation host
|
||||
federationHost.KeyID = sql.NullString{
|
||||
String: apPerson.PublicKey.ID.String(),
|
||||
Valid: true,
|
||||
}
|
||||
federationHost.PublicKey = sql.Null[sql.RawBytes]{
|
||||
V: pubKeyBytes,
|
||||
Valid: true,
|
||||
}
|
||||
err = forgefed.UpdateFederationHost(ctx, federationHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func fetchKeyFromAp(ctx *context_service.Base, keyURL url.URL) (pubKey any, pubKeyBytes []byte, apPerson *ap.Person, err error) {
|
||||
actionsUser := user.NewAPServerActor()
|
||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
b, err := apClient.GetBody(keyURL.String())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
person := ap.PersonNew(ap.IRI(keyURL.String()))
|
||||
err = person.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
|
||||
}
|
||||
|
||||
pubKeyFromAp := person.PublicKey
|
||||
if pubKeyFromAp.ID.String() != keyURL.String() {
|
||||
return nil, nil, nil, fmt.Errorf("cannot find publicKey with id: %v in %v", keyURL, string(b))
|
||||
}
|
||||
|
||||
pubKeyBytes, err = decodePublicKeyPem(pubKeyFromAp.PublicKeyPem)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
pubKey, err = x509.ParsePKIXPublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return pubKey, pubKeyBytes, person, err
|
||||
}
|
||||
|
||||
func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
|
||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
}
|
||||
|
||||
return block.Bytes, nil
|
||||
}
|
|
@ -29,7 +29,7 @@ func TestActivityPubClientBodySize(t *testing.T) {
|
|||
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
||||
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
|
||||
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.KeyID())
|
||||
require.NoError(t, err)
|
||||
|
||||
url := u.JoinPath("/api/v1/nodeinfo").String()
|
||||
|
|
|
@ -4,12 +4,18 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/forgefed"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/routers"
|
||||
"forgejo.org/services/contexttest"
|
||||
"forgejo.org/services/federation"
|
||||
"forgejo.org/tests"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
|
@ -31,10 +37,9 @@ func TestActivityPubActor(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ap.ApplicationType, actor.Type)
|
||||
assert.Equal(t, setting.Domain, actor.PreferredUsername.String())
|
||||
assert.Equal(t, "ghost", actor.PreferredUsername.String())
|
||||
keyID := actor.GetID().String()
|
||||
assert.Regexp(t, "activitypub/actor$", keyID)
|
||||
assert.Regexp(t, "activitypub/actor/outbox$", actor.Outbox.GetID().String())
|
||||
assert.Regexp(t, "activitypub/actor/inbox$", actor.Inbox.GetID().String())
|
||||
|
||||
pubKey := actor.PublicKey
|
||||
|
@ -46,3 +51,27 @@ func TestActivityPubActor(t *testing.T) {
|
|||
assert.NotNil(t, pubKeyPem)
|
||||
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
|
||||
}
|
||||
|
||||
func TestActorNewFromKeyId(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
ctx, _ := contexttest.MockAPIContext(t, "/api/v1/activitypub/actor")
|
||||
sut, err := federation.NewActorIDFromKeyID(ctx.Base, fmt.Sprintf("%sapi/v1/activitypub/actor#main-key", u))
|
||||
require.NoError(t, err)
|
||||
|
||||
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, forgefed.ActorID{
|
||||
ID: "actor",
|
||||
HostSchema: "http",
|
||||
Path: "api/v1/activitypub",
|
||||
Host: setting.Domain,
|
||||
HostPort: uint16(port),
|
||||
UnvalidatedInput: fmt.Sprintf("http://%s:%d/api/v1/activitypub/actor", setting.Domain, port),
|
||||
IsPortSupplemented: false,
|
||||
}, sut)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -26,28 +26,37 @@ import (
|
|||
func TestActivityPubPerson(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
userID := 2
|
||||
username := "user2"
|
||||
userURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", u, userID)
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
mock := test.NewFederationServerMock()
|
||||
federatedSrv := mock.DistantServer(t)
|
||||
defer federatedSrv.Close()
|
||||
|
||||
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
onGiteaRun(t, func(t *testing.T, localUrl *url.URL) {
|
||||
defer test.MockVariableValue(&setting.AppURL, localUrl.String())()
|
||||
|
||||
localUserID := 2
|
||||
localUserName := "user2"
|
||||
localUserURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", localUrl, localUserID)
|
||||
|
||||
// distantURL := federatedSrv.URL
|
||||
// distantUser15URL := fmt.Sprintf("%s/api/v1/activitypub/user-id/15", distantURL)
|
||||
|
||||
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
||||
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
|
||||
c, err := cf.WithKeysDirect(db.DefaultContext, mock.Persons[0].PrivKey,
|
||||
mock.Persons[0].KeyID(federatedSrv.URL))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unsigned request
|
||||
t.Run("UnsignedRequest", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", userURL)
|
||||
req := NewRequest(t, "GET", localUserURL)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("SignedRequestValidation", func(t *testing.T) {
|
||||
// Signed request
|
||||
resp, err := apClient.GetBody(userURL)
|
||||
resp, err := c.GetBody(localUserURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
var person ap.Person
|
||||
|
@ -55,13 +64,12 @@ func TestActivityPubPerson(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ap.PersonType, person.Type)
|
||||
assert.Equal(t, username, person.PreferredUsername.String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d$", userID), person.GetID())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d/outbox$", userID), person.Outbox.GetID().String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d/inbox$", userID), person.Inbox.GetID().String())
|
||||
assert.Equal(t, localUserName, person.PreferredUsername.String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d$", localUserID), person.GetID())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d/inbox$", localUserID), person.Inbox.GetID().String())
|
||||
|
||||
assert.NotNil(t, person.PublicKey)
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d#main-key$", userID), person.PublicKey.ID)
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d#main-key$", localUserID), person.PublicKey.ID)
|
||||
|
||||
assert.NotNil(t, person.PublicKey.PublicKeyPem)
|
||||
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", person.PublicKey.PublicKeyPem)
|
||||
|
|
|
@ -29,15 +29,18 @@ func TestActivityPubRepository(t *testing.T) {
|
|||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
mock := test.NewFederationServerMock()
|
||||
federatedSrv := mock.DistantServer(t)
|
||||
defer federatedSrv.Close()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
repositoryID := 2
|
||||
|
||||
apServerActor := user.NewAPServerActor()
|
||||
|
||||
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.APActorKeyID())
|
||||
c, err := cf.WithKeysDirect(db.DefaultContext, mock.Persons[0].PrivKey,
|
||||
mock.Persons[0].KeyID(federatedSrv.URL))
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := c.GetBody(fmt.Sprintf("%sapi/v1/activitypub/repository-id/%d", u, repositoryID))
|
||||
|
@ -53,9 +56,10 @@ func TestActivityPubRepository(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestActivityPubMissingRepository(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
repositoryID := 9999999
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%d", repositoryID))
|
||||
|
@ -72,14 +76,14 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
|
|||
defer federatedSrv.Close()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
apServerActor := user.NewAPServerActor()
|
||||
repositoryID := 2
|
||||
timeNow := time.Now().UTC()
|
||||
|
||||
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.APActorKeyID())
|
||||
c, err := cf.WithKeysDirect(db.DefaultContext, mock.Persons[0].PrivKey,
|
||||
mock.Persons[0].KeyID(federatedSrv.URL))
|
||||
require.NoError(t, err)
|
||||
|
||||
repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String()
|
||||
|
@ -148,6 +152,7 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
|
|||
|
||||
func TestActivityPubRepositoryInboxInvalid(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
|
@ -157,7 +162,7 @@ func TestActivityPubRepositoryInboxInvalid(t *testing.T) {
|
|||
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.APActorKeyID())
|
||||
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.KeyID())
|
||||
require.NoError(t, err)
|
||||
|
||||
repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String()
|
||||
|
|
|
@ -35,7 +35,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
|
|||
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||
require.NoError(t, err)
|
||||
|
||||
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
|
||||
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.KeyID())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unsigned request
|
||||
|
|
|
@ -52,6 +52,15 @@ func TestWebfinger(t *testing.T) {
|
|||
assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject)
|
||||
assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(user.ID)}, jrd.Aliases)
|
||||
|
||||
instanceReq := NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:ghost@%s", appURL.Host))
|
||||
instanceResp := MakeRequest(t, instanceReq, http.StatusOK)
|
||||
assert.Equal(t, "application/jrd+json", instanceResp.Header().Get("Content-Type"))
|
||||
|
||||
var instanceActor webfingerJRD
|
||||
DecodeJSON(t, instanceResp, &instanceActor)
|
||||
assert.Equal(t, "acct:ghost@"+appURL.Host, instanceActor.Subject)
|
||||
assert.ElementsMatch(t, []string{appURL.String() + "api/v1/activitypub/actor"}, instanceActor.Aliases)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, "unknown.host"))
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue