mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00:00 
			
		
		
		
	This PR is part of #4767. It 1. adds the ability to follow a local person from a distant federation server (see tests/integration/api_activitypub_person_inbox_follow_test.go) 2. streamlines the router code (refactor the person conversion & handling of inbox requests in service direction, unifies service call signature & error handling) 3. introduces queues for decoupling outgoing communication (delivery retry to cope network issues or distant service downtimes) and 4. adds minor fixes to integration tests (test timeout & invalid inbox activities) Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8720 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>
		
			
				
	
	
		
			243 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package federation
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"database/sql"
 | |
| 	"fmt"
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 
 | |
| 	"forgejo.org/models/forgefed"
 | |
| 	"forgejo.org/models/user"
 | |
| 	"forgejo.org/modules/activitypub"
 | |
| 	"forgejo.org/modules/auth/password"
 | |
| 	fm "forgejo.org/modules/forgefed"
 | |
| 	"forgejo.org/modules/log"
 | |
| 	"forgejo.org/modules/setting"
 | |
| 	"forgejo.org/modules/validation"
 | |
| 
 | |
| 	"github.com/google/uuid"
 | |
| )
 | |
| 
 | |
| func Init() error {
 | |
| 	if !setting.Federation.Enabled {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return initDeliveryQueue()
 | |
| }
 | |
| 
 | |
| func FindOrCreateFederationHost(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) {
 | |
| 	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 FindOrCreateFederatedUser(ctx context.Context, 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("Local ActivityPub user found (actorURI: %#v, user: %#v)", actorURI, user)
 | |
| 	} else {
 | |
| 		log.Trace("Attempting to create new user and federatedUser for actorURI: %#v", actorURI)
 | |
| 		user, federatedUser, err = createUserFromAP(ctx, personID, federationHost.ID)
 | |
| 		if err != nil {
 | |
| 			return nil, nil, nil, err
 | |
| 		}
 | |
| 		log.Trace("Created user %#v with federatedUser %#v from distant server", user, federatedUser)
 | |
| 	}
 | |
| 	log.Trace("Got user: %v", user.Name)
 | |
| 
 | |
| 	return user, federatedUser, federationHost, nil
 | |
| }
 | |
| 
 | |
| func findFederatedUser(ctx context.Context, 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 nil, nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	user, federatedUser, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	return user, federatedUser, federationHost, nil
 | |
| }
 | |
| 
 | |
| 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.KeyID())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	body, err = client.GetBody(nodeInfoWellKnown.Href)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	nodeInfo, err := forgefed.NewNodeInfo(body)
 | |
| 	if err != nil {
 | |
| 		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
 | |
| 	}
 | |
| 
 | |
| 	err = forgefed.CreateFederationHost(ctx, &result)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &result, nil
 | |
| }
 | |
| 
 | |
| 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.KeyID())
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	body, err := apClient.GetBody(personID.AsURI())
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	person := fm.ForgePerson{}
 | |
| 	err = person.UnmarshalJSON(body)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	if res, err := validation.IsValid(person); !res {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	log.Info("Fetched valid person from distant server: %q", person)
 | |
| 
 | |
| 	localFqdn, err := url.ParseRequestURI(setting.AppURL)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
 | |
| 	loginName := personID.AsLoginName()
 | |
| 	name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
 | |
| 	fullName := person.Name.String()
 | |
| 
 | |
| 	if len(person.Name) == 0 {
 | |
| 		fullName = name
 | |
| 	}
 | |
| 
 | |
| 	password, err := password.Generate(32)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 
 | |
| 	inbox, err := url.ParseRequestURI(person.Inbox.GetLink().String())
 | |
| 	if err != nil {
 | |
| 		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,
 | |
| 		FullName:                     fullName,
 | |
| 		Email:                        email,
 | |
| 		EmailNotificationsPreference: "disabled",
 | |
| 		Passwd:                       password,
 | |
| 		MustChangePassword:           false,
 | |
| 		LoginName:                    loginName,
 | |
| 		Type:                         user.UserTypeRemoteUser,
 | |
| 		IsAdmin:                      false,
 | |
| 	}
 | |
| 
 | |
| 	federatedUser := user.FederatedUser{
 | |
| 		ExternalID:            personID.ID,
 | |
| 		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,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	log.Info("Fetched person's %q federatedUser from distant server: %q", person, 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
 | |
| }
 |