From 6fee55dcff976f3eeae5879fe91d2f27780d0da4 Mon Sep 17 00:00:00 2001 From: tobi Date: Wed, 15 Oct 2025 18:57:57 +0200 Subject: [chore] Rationalize HTTP return codes for fedi endpoints, other tidying up (#4503) # Description > If this is a code change, please include a summary of what you've coded, and link to the issue(s) it closes/implements. > > If this is a documentation change, please briefly describe what you've changed and why. This pull request does some refactoring of the fedi API endpoints and processing functions, and the authenticate + pub key deref functions, to try to return fewer silly HTTP codes like 410 Gone (when a *remote* account is gone, not a local one), and 500 errors where something isn't really an error. Also does some general tidying up and renaming for consistency. ## Checklist Please put an x inside each checkbox to indicate that you've read and followed it: `[ ]` -> `[x]` If this is a documentation change, only the first checkbox must be filled (you can delete the others if you want). - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have not leveraged AI to create the proposed changes. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [x] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. - [ ] I/we have added tests that cover new code. - [x] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4503 Co-authored-by: tobi Co-committed-by: tobi --- internal/processing/fedi/accept.go | 57 -------------- internal/processing/fedi/authorization.go | 87 ++++++++++++++++++++- internal/processing/fedi/collections.go | 85 +++++++++++---------- internal/processing/fedi/common.go | 107 ++++++++++---------------- internal/processing/fedi/emoji.go | 57 ++++++++++---- internal/processing/fedi/status.go | 36 ++++++--- internal/processing/fedi/user.go | 123 +++++++++++++----------------- internal/processing/fedi/wellknown.go | 9 ++- 8 files changed, 296 insertions(+), 265 deletions(-) delete mode 100644 internal/processing/fedi/accept.go (limited to 'internal/processing') diff --git a/internal/processing/fedi/accept.go b/internal/processing/fedi/accept.go deleted file mode 100644 index 97e36fbb3..000000000 --- a/internal/processing/fedi/accept.go +++ /dev/null @@ -1,57 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package fedi - -import ( - "context" - - "code.superseriousbusiness.org/gotosocial/internal/ap" - "code.superseriousbusiness.org/gotosocial/internal/gtserror" -) - -// AcceptGet handles the getting of a fedi/activitypub -// representation of a local interaction acceptance. -// -// It performs appropriate authentication before -// returning a JSON serializable interface. -func (p *Processor) AcceptGet( - ctx context.Context, - requestedUser string, - intReqID string, -) (any, gtserror.WithCode) { - // Ensure valid request, intReq exists, etc. - intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID) - if errWithCode != nil { - return nil, errWithCode - } - - // Convert + serialize the Accept. - accept, err := p.converter.InteractionReqToASAccept(ctx, intReq) - if err != nil { - err := gtserror.Newf("error converting to accept: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } - - data, err := ap.Serialize(accept) - if err != nil { - err := gtserror.Newf("error serializing accept: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/fedi/authorization.go b/internal/processing/fedi/authorization.go index bbba6a2d8..276dd3a82 100644 --- a/internal/processing/fedi/authorization.go +++ b/internal/processing/fedi/authorization.go @@ -19,9 +19,13 @@ package fedi import ( "context" + "errors" + "fmt" "code.superseriousbusiness.org/gotosocial/internal/ap" + "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" ) // AuthorizationGet handles the getting of a fedi/activitypub @@ -35,7 +39,7 @@ func (p *Processor) AuthorizationGet( intReqID string, ) (any, gtserror.WithCode) { // Ensure valid request, intReq exists, etc. - intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID) + intReq, errWithCode := p.validateAuthGetRequest(ctx, requestedUser, intReqID) if errWithCode != nil { return nil, errWithCode } @@ -55,3 +59,84 @@ func (p *Processor) AuthorizationGet( return data, nil } + +// AcceptGet handles the getting of a fedi/activitypub +// representation of a local interaction acceptance. +// +// It performs appropriate authentication before +// returning a JSON serializable interface. +func (p *Processor) AcceptGet( + ctx context.Context, + requestedUser string, + intReqID string, +) (any, gtserror.WithCode) { + // Ensure valid request, intReq exists, etc. + intReq, errWithCode := p.validateAuthGetRequest(ctx, requestedUser, intReqID) + if errWithCode != nil { + return nil, errWithCode + } + + // Convert + serialize the Accept. + accept, err := p.converter.InteractionReqToASAccept(ctx, intReq) + if err != nil { + err := gtserror.Newf("error converting to accept: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + data, err := ap.Serialize(accept) + if err != nil { + err := gtserror.Newf("error serializing accept: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} + +// validateAuthGetRequest is a shortcut function +// for returning an accepted interaction request +// targeting `requestedUser`. +func (p *Processor) validateAuthGetRequest( + ctx context.Context, + requestedUser string, + intReqID string, +) (*gtsmodel.InteractionRequest, gtserror.WithCode) { + // Authenticate incoming request, getting related accounts. + auth, errWithCode := p.authenticate(ctx, requestedUser) + if errWithCode != nil { + return nil, errWithCode + } + + if auth.handshakingURI != nil { + // We're currently handshaking, which means we don't know + // this account yet. This should be a very rare race condition. + err := gtserror.Newf("network race handshaking %s", auth.handshakingURI) + return nil, gtserror.NewErrorInternalError(err) + } + + // Fetch interaction request with the given ID. + req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Ensure that this is an existing + // and *accepted* interaction request. + if req == nil || !req.IsAccepted() { + const text = "interaction request not found" + return nil, gtserror.NewErrorNotFound(errors.New(text)) + } + + // Ensure interaction request was accepted + // by the account in the request path. + if req.TargetAccountID != auth.receiver.ID { + text := fmt.Sprintf( + "account %s is not targeted by interaction request %s and therefore can't accept it", + requestedUser, intReqID, + ) + return nil, gtserror.NewErrorNotFound(errors.New(text)) + } + + // All fine. + return req, nil +} diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go index ae4860b15..b67651dff 100644 --- a/internal/processing/fedi/collections.go +++ b/internal/processing/fedi/collections.go @@ -54,24 +54,24 @@ func (p *Processor) OutboxGet( ctx context.Context, requestedUser string, page *paging.Page, -) (interface{}, gtserror.WithCode) { +) (any, gtserror.WithCode) { // Authenticate incoming request, getting related accounts. auth, errWithCode := p.authenticate(ctx, requestedUser) if errWithCode != nil { return nil, errWithCode } - receivingAcct := auth.receivingAcct + receiver := auth.receiver // Parse the collection ID object from account's followers URI. - collectionID, err := url.Parse(receivingAcct.OutboxURI) + collectionID, err := url.Parse(receiver.OutboxURI) if err != nil { - err := gtserror.Newf("error parsing account outbox uri %s: %w", receivingAcct.OutboxURI, err) + err := gtserror.Newf("error parsing account outbox uri %s: %w", receiver.OutboxURI, err) return nil, gtserror.NewErrorInternalError(err) } // Ensure we have stats for this account. - if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil { - err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err) + if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil { + err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err) return nil, gtserror.NewErrorInternalError(err) } @@ -83,8 +83,8 @@ func (p *Processor) OutboxGet( switch { - case receivingAcct.IsInstance() || - *receivingAcct.Settings.HideCollections: + case receiver.IsInstance() || + *receiver.Settings.HideCollections: // If account that hides collections, or instance // account (ie., can't post / have relationships), // just return barest stub of collection. @@ -94,7 +94,7 @@ func (p *Processor) OutboxGet( // If paging disabled, or we're currently handshaking // the requester, just return collection that links // to first page (i.e. path below), with no items. - params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount) + params.Total = util.Ptr(*receiver.Stats.StatusesCount) params.First = new(paging.Page) params.Query = make(url.Values, 1) params.Query.Set("limit", "40") // enables paging @@ -105,7 +105,7 @@ func (p *Processor) OutboxGet( // Get page of full public statuses. statuses, err := p.state.DB.GetAccountStatuses( ctx, - receivingAcct.ID, + receiver.ID, page.GetLimit(), // limit true, // excludeReplies true, // excludeReblogs @@ -133,7 +133,7 @@ func (p *Processor) OutboxGet( // (eg., local-only statuses, if the requester is remote). statuses, err = p.visFilter.StatusesVisible( ctx, - auth.requestingAcct, + auth.requester, statuses, ) if err != nil { @@ -142,7 +142,7 @@ func (p *Processor) OutboxGet( } // Start building AS collection page params. - params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount) + params.Total = util.Ptr(*receiver.Stats.StatusesCount) var pageParams ap.CollectionPageParams pageParams.CollectionParams = params @@ -194,24 +194,24 @@ func (p *Processor) FollowersGet( ctx context.Context, requestedUser string, page *paging.Page, -) (interface{}, gtserror.WithCode) { +) (any, gtserror.WithCode) { // Authenticate incoming request, getting related accounts. auth, errWithCode := p.authenticate(ctx, requestedUser) if errWithCode != nil { return nil, errWithCode } - receivingAcct := auth.receivingAcct + receiver := auth.receiver // Parse the collection ID object from account's followers URI. - collectionID, err := url.Parse(receivingAcct.FollowersURI) + collectionID, err := url.Parse(receiver.FollowersURI) if err != nil { - err := gtserror.Newf("error parsing account followers uri %s: %w", receivingAcct.FollowersURI, err) + err := gtserror.Newf("error parsing account followers uri %s: %w", receiver.FollowersURI, err) return nil, gtserror.NewErrorInternalError(err) } // Ensure we have stats for this account. - if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil { - err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err) + if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil { + err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err) return nil, gtserror.NewErrorInternalError(err) } @@ -223,8 +223,8 @@ func (p *Processor) FollowersGet( switch { - case receivingAcct.IsInstance() || - *receivingAcct.Settings.HideCollections: + case receiver.IsInstance() || + *receiver.Settings.HideCollections: // If account that hides collections, or instance // account (ie., can't post / have relationships), // just return barest stub of collection. @@ -234,7 +234,7 @@ func (p *Processor) FollowersGet( // If paging disabled, or we're currently handshaking // the requester, just return collection that links // to first page (i.e. path below), with no items. - params.Total = util.Ptr(*receivingAcct.Stats.FollowersCount) + params.Total = util.Ptr(*receiver.Stats.FollowersCount) params.First = new(paging.Page) params.Query = make(url.Values, 1) params.Query.Set("limit", "40") // enables paging @@ -243,7 +243,7 @@ func (p *Processor) FollowersGet( default: // Paging enabled. // Get page of full follower objects with attached accounts. - followers, err := p.state.DB.GetAccountFollowers(ctx, receivingAcct.ID, page) + followers, err := p.state.DB.GetAccountFollowers(ctx, receiver.ID, page) if err != nil { err := gtserror.Newf("error getting followers: %w", err) return nil, gtserror.NewErrorInternalError(err) @@ -260,7 +260,7 @@ func (p *Processor) FollowersGet( } // Start building AS collection page params. - params.Total = util.Ptr(*receivingAcct.Stats.FollowersCount) + params.Total = util.Ptr(*receiver.Stats.FollowersCount) var pageParams ap.CollectionPageParams pageParams.CollectionParams = params @@ -306,24 +306,24 @@ func (p *Processor) FollowersGet( // FollowingGet returns the serialized ActivityPub // collection of a local account's following collection, // which contains links to accounts followed by this account. -func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page *paging.Page) (interface{}, gtserror.WithCode) { +func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page *paging.Page) (any, gtserror.WithCode) { // Authenticate incoming request, getting related accounts. auth, errWithCode := p.authenticate(ctx, requestedUser) if errWithCode != nil { return nil, errWithCode } - receivingAcct := auth.receivingAcct + receiver := auth.receiver // Parse collection ID from account's following URI. - collectionID, err := url.Parse(receivingAcct.FollowingURI) + collectionID, err := url.Parse(receiver.FollowingURI) if err != nil { - err := gtserror.Newf("error parsing account following uri %s: %w", receivingAcct.FollowingURI, err) + err := gtserror.Newf("error parsing account following uri %s: %w", receiver.FollowingURI, err) return nil, gtserror.NewErrorInternalError(err) } // Ensure we have stats for this account. - if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil { - err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err) + if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil { + err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err) return nil, gtserror.NewErrorInternalError(err) } @@ -334,8 +334,8 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page params.ID = collectionID switch { - case receivingAcct.IsInstance() || - *receivingAcct.Settings.HideCollections: + case receiver.IsInstance() || + *receiver.Settings.HideCollections: // If account that hides collections, or instance // account (ie., can't post / have relationships), // just return barest stub of collection. @@ -345,7 +345,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page // If paging disabled, or we're currently handshaking // the requester, just return collection that links // to first page (i.e. path below), with no items. - params.Total = util.Ptr(*receivingAcct.Stats.FollowingCount) + params.Total = util.Ptr(*receiver.Stats.FollowingCount) params.First = new(paging.Page) params.Query = make(url.Values, 1) params.Query.Set("limit", "40") // enables paging @@ -354,7 +354,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page default: // Paging enabled. // Get page of full follower objects with attached accounts. - follows, err := p.state.DB.GetAccountFollows(ctx, receivingAcct.ID, page) + follows, err := p.state.DB.GetAccountFollows(ctx, receiver.ID, page) if err != nil { err := gtserror.Newf("error getting follows: %w", err) return nil, gtserror.NewErrorInternalError(err) @@ -371,7 +371,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page } // Start AS collection page params. - params.Total = util.Ptr(*receivingAcct.Stats.FollowingCount) + params.Total = util.Ptr(*receiver.Stats.FollowingCount) var pageParams ap.CollectionPageParams pageParams.CollectionParams = params @@ -416,28 +416,29 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page // FeaturedCollectionGet returns an ordered collection of the requested username's Pinned posts. // The returned collection have an `items` property which contains an ordered list of status URIs. -func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUser string) (interface{}, gtserror.WithCode) { +func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUser string) (any, gtserror.WithCode) { // Authenticate incoming request, getting related accounts. auth, errWithCode := p.authenticate(ctx, requestedUser) if errWithCode != nil { return nil, errWithCode } - receivingAcct := auth.receivingAcct + receiver := auth.receiver - statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, receivingAcct.ID) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorInternalError(err) - } + statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, receiver.ID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting pinned statuses: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - collection, err := p.converter.StatusesToASFeaturedCollection(ctx, receivingAcct.FeaturedCollectionURI, statuses) + collection, err := p.converter.StatusesToASFeaturedCollection(ctx, receiver.FeaturedCollectionURI, statuses) if err != nil { + err := gtserror.Newf("error converting pinned statuses: %w", err) return nil, gtserror.NewErrorInternalError(err) } data, err := ap.Serialize(collection) if err != nil { + err := gtserror.Newf("error serializing: %w", err) return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/fedi/common.go b/internal/processing/fedi/common.go index fc783f93e..ff6ed6fd4 100644 --- a/internal/processing/fedi/common.go +++ b/internal/processing/fedi/common.go @@ -20,7 +20,6 @@ package fedi import ( "context" "errors" - "fmt" "net/url" "code.superseriousbusiness.org/gotosocial/internal/db" @@ -30,21 +29,23 @@ import ( type commonAuth struct { handshakingURI *url.URL // Set to requestingAcct's URI if we're currently handshaking them. - requestingAcct *gtsmodel.Account // Remote account making request to this instance. - receivingAcct *gtsmodel.Account // Local account receiving the request. + requester *gtsmodel.Account // Remote account making request to this instance. + receiver *gtsmodel.Account // Local account receiving the request. } +// authenticate is a util function for authenticating a signed GET +// request to one of the AP/fedi resources handled in this package. func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*commonAuth, gtserror.WithCode) { - // First get the requested (receiving) LOCAL account with username from database. + // Get the requested local account + // with given username from database. receiver, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUser, "") - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - // Real db error. - err = gtserror.Newf("db error getting account %s: %w", requestedUser, err) - return nil, gtserror.NewErrorInternalError(err) - } + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf("db error getting account %s: %w", requestedUser, err) + return nil, gtserror.NewErrorInternalError(err) + } - // Account just not found in the db. + if receiver == nil { + err := gtserror.Newf("account %s not found in the db", requestedUser) return nil, gtserror.NewErrorNotFound(err) } @@ -60,74 +61,44 @@ func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*co // don't know the requester yet. return &commonAuth{ handshakingURI: pubKeyAuth.OwnerURI, - receivingAcct: receiver, + receiver: receiver, }, nil } // Get requester from auth. requester := pubKeyAuth.Owner - // Ensure block does not exist between receiver and requester. - blocked, err := p.state.DB.IsEitherBlocked(ctx, receiver.ID, requester.ID) - if err != nil { - err := gtserror.Newf("error checking block: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - const text = "block exists between accounts" - return nil, gtserror.NewErrorForbidden(errors.New(text)) + // Check if requester is suspended. + switch { + case !requester.IsSuspended(): + // No problem. + + case requester.DeletedSelf(): + // Requester deleted their own account. + // Why are they now requesting something? + err := gtserror.Newf("requester %s self-deleted", requester.UsernameDomain()) + return nil, gtserror.NewErrorUnauthorized(err) + + default: + // Admin from our instance likely suspended account. + err := gtserror.Newf("requester %s is suspended", requester.UsernameDomain()) + return nil, gtserror.NewErrorForbidden(err) } - return &commonAuth{ - requestingAcct: requester, - receivingAcct: receiver, - }, nil -} - -// validateIntReqRequest is a shortcut function -// for returning an accepted interaction request -// targeting `requestedUser`. -func (p *Processor) validateIntReqRequest( - ctx context.Context, - requestedUser string, - intReqID string, -) (*gtsmodel.InteractionRequest, gtserror.WithCode) { - // Authenticate incoming request, getting related accounts. - auth, errWithCode := p.authenticate(ctx, requestedUser) - if errWithCode != nil { - return nil, errWithCode - } - - if auth.handshakingURI != nil { - // We're currently handshaking, which means we don't know - // this account yet. This should be a very rare race condition. - err := gtserror.Newf("network race handshaking %s", auth.handshakingURI) - return nil, gtserror.NewErrorInternalError(err) - } - - // Fetch interaction request with the given ID. - req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err) + // Ensure receiver does not block requester. + blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID) + if err != nil { + err := gtserror.Newf("db error checking block: %w", err) return nil, gtserror.NewErrorInternalError(err) } - // Ensure that this is an existing - // and *accepted* interaction request. - if req == nil || !req.IsAccepted() { - const text = "interaction request not found" - return nil, gtserror.NewErrorNotFound(errors.New(text)) - } - - // Ensure interaction request was accepted - // by the account in the request path. - if req.TargetAccountID != auth.receivingAcct.ID { - text := fmt.Sprintf( - "account %s is not targeted by interaction request %s and therefore can't accept it", - requestedUser, intReqID, - ) - return nil, gtserror.NewErrorNotFound(errors.New(text)) + if blocked { + var text = requestedUser + " blocks " + requester.Username + return nil, gtserror.NewErrorForbidden(errors.New(text)) } - // All fine. - return req, nil + return &commonAuth{ + requester: requester, + receiver: receiver, + }, nil } diff --git a/internal/processing/fedi/emoji.go b/internal/processing/fedi/emoji.go index 8db8b48ea..e7e3ec406 100644 --- a/internal/processing/fedi/emoji.go +++ b/internal/processing/fedi/emoji.go @@ -19,38 +19,69 @@ package fedi import ( "context" - "fmt" + "errors" "code.superseriousbusiness.org/gotosocial/internal/ap" + "code.superseriousbusiness.org/gotosocial/internal/config" + "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" ) -// EmojiGet handles the GET for a federated emoji originating from this instance. -func (p *Processor) EmojiGet(ctx context.Context, requestedEmojiID string) (interface{}, gtserror.WithCode) { - if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil { +// EmojiGet handles the GET for an emoji originating from this instance. +func (p *Processor) EmojiGet(ctx context.Context, emojiID string) (any, gtserror.WithCode) { + // Authenticate incoming request. + // + // Pass hostname string to this function to indicate + // it's the instance account being requested, as + // emojis are always owned by the instance account. + auth, errWithCode := p.authenticate(ctx, config.GetHost()) + if errWithCode != nil { return nil, errWithCode } - requestedEmoji, err := p.state.DB.GetEmojiByID(ctx, requestedEmojiID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, err)) + if auth.handshakingURI != nil { + // We're currently handshaking, which means + // we don't know this account yet. This should + // be a very rare race condition. + err := gtserror.Newf("network race handshaking %s", auth.handshakingURI) + return nil, gtserror.NewErrorInternalError(err) + } + + // Get the requested emoji. + emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting emoji %s: %w", emojiID, err) + return nil, gtserror.NewErrorNotFound(err) } - if !requestedEmoji.IsLocal() { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s doesn't belong to this instance (domain %s)", requestedEmojiID, requestedEmoji.Domain)) + if emoji == nil { + err := gtserror.Newf("emoji %s not found in the db", emojiID) + return nil, gtserror.NewErrorNotFound(err) } - if *requestedEmoji.Disabled { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID)) + // Only serve *our* + // emojis on this path. + if !emoji.IsLocal() { + err := gtserror.Newf("emoji %s doesn't belong to this instance (domain is %s)", emojiID, emoji.Domain) + return nil, gtserror.NewErrorNotFound(err) } - apEmoji, err := p.converter.EmojiToAS(ctx, requestedEmoji) + // Don't serve emojis that have + // been disabled by an admin. + if *emoji.Disabled { + err := gtserror.Newf("emoji with id %s has been disabled by an admin", emojiID) + return nil, gtserror.NewErrorNotFound(err) + } + + apEmoji, err := p.converter.EmojiToAS(ctx, emoji) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err)) + err := gtserror.Newf("error converting emoji %s to ap: %s", emojiID, err) + return nil, gtserror.NewErrorInternalError(err) } data, err := ap.Serialize(apEmoji) if err != nil { + err := gtserror.Newf("error serializing emoji %s: %w", emojiID, err) return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go index 497bcc177..d1de6f4c1 100644 --- a/internal/processing/fedi/status.go +++ b/internal/processing/fedi/status.go @@ -26,6 +26,7 @@ import ( "code.superseriousbusiness.org/activity/streams/vocab" "code.superseriousbusiness.org/gotosocial/internal/ap" + "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -33,9 +34,13 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/util" ) -// StatusGet handles the getting of a fedi/activitypub representation of a local status. +// StatusGet handles getting an AP representation of a local status. // It performs appropriate authentication before returning a JSON serializable interface. -func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusID string) (interface{}, gtserror.WithCode) { +func (p *Processor) StatusGet( + ctx context.Context, + requestedUser string, + statusID string, +) (any, gtserror.WithCode) { // Authenticate incoming request, getting related accounts. auth, errWithCode := p.authenticate(ctx, requestedUser) if errWithCode != nil { @@ -49,16 +54,23 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI err := gtserror.Newf("network race handshaking %s", auth.handshakingURI) return nil, gtserror.NewErrorInternalError(err) } - - receivingAcct := auth.receivingAcct - requestingAcct := auth.requestingAcct + receiver := auth.receiver + requester := auth.requester status, err := p.state.DB.GetStatusByID(ctx, statusID) - if err != nil { + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting status: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + if status == nil { + // TODO: Update this to serve "gone" + // when a status has been deleted. + err := gtserror.Newf("status %s not found in the db", statusID) return nil, gtserror.NewErrorNotFound(err) } - if status.AccountID != receivingAcct.ID { + if status.AccountID != receiver.ID { const text = "status does not belong to receiving account" return nil, gtserror.NewErrorNotFound(errors.New(text)) } @@ -68,7 +80,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI return nil, gtserror.NewErrorNotFound(errors.New(text)) } - visible, err := p.visFilter.StatusVisible(ctx, requestingAcct, status) + visible, err := p.visFilter.StatusVisible(ctx, requester, status) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -93,7 +105,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI return data, nil } -// GetStatus handles the getting of a fedi/activitypub representation of replies to a status, +// GetStatus handles getting an AP representation of replies to a status, // performing appropriate authentication before returning a JSON serializable interface to the caller. func (p *Processor) StatusRepliesGet( ctx context.Context, @@ -101,7 +113,7 @@ func (p *Processor) StatusRepliesGet( statusID string, page *paging.Page, onlyOtherAccounts bool, -) (interface{}, gtserror.WithCode) { +) (any, gtserror.WithCode) { // Authenticate incoming request, getting related accounts. auth, errWithCode := p.authenticate(ctx, requestedUser) if errWithCode != nil { @@ -116,8 +128,8 @@ func (p *Processor) StatusRepliesGet( return nil, gtserror.NewErrorInternalError(err) } - receivingAcct := auth.receivingAcct - requestingAcct := auth.requestingAcct + receivingAcct := auth.receiver + requestingAcct := auth.requester // Get target status and ensure visible to requester. status, errWithCode := p.c.GetVisibleTargetStatus(ctx, diff --git a/internal/processing/fedi/user.go b/internal/processing/fedi/user.go index 53dfd6022..9fb338673 100644 --- a/internal/processing/fedi/user.go +++ b/internal/processing/fedi/user.go @@ -20,96 +20,83 @@ package fedi import ( "context" "errors" - "fmt" - "net/url" "code.superseriousbusiness.org/gotosocial/internal/ap" "code.superseriousbusiness.org/gotosocial/internal/db" + "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" - "code.superseriousbusiness.org/gotosocial/internal/uris" ) -// UserGet handles the getting of a fedi/activitypub representation of a user/account, -// performing authentication before returning a JSON serializable interface to the caller. -func (p *Processor) UserGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // (Try to) get the requested local account from the db. - receiver, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - // Account just not found w/ this username. - err := fmt.Errorf("account with username %s not found in the db", requestedUsername) - return nil, gtserror.NewErrorNotFound(err) - } +// UserGet handles getting an AP representation of an account. +// It does auth before returning a JSON serializable interface to the caller. +func (p *Processor) UserGet( + ctx context.Context, + requestedUser string, +) (any, gtserror.WithCode) { + // Authenticate incoming request, getting related accounts. + // + // We may currently be handshaking with the remote account + // making the request. Unlike with other fedi endpoints, + // don't bother checking this; if we're still handshaking + // just serve the AP representation of our account anyway. + // + // This ensures that we don't get stuck in a loop with another + // GtS instance, where each instance is trying repeatedly to + // dereference the other account that's making the request + // before it will reveal its own account. + // + // Instead, we end up in an 'I'll show you mine if you show me + // yours' situation, where we sort of agree to reveal each + // other's profiles at the same time. + auth, errWithCode := p.authenticate(ctx, requestedUser) + if errWithCode != nil { + return nil, errWithCode + } - // Real db error. - err := fmt.Errorf("db error getting account with username %s: %w", requestedUsername, err) + // Generate the proper AP representation. + accountable, err := p.converter.AccountToAS(ctx, auth.receiver) + if err != nil { + err := gtserror.Newf("error converting to accountable: %w", err) return nil, gtserror.NewErrorInternalError(err) } - if uris.IsPublicKeyPath(requestURL) { - // If request is on a public key path, we don't need to - // authenticate this request. However, we'll only serve - // the bare minimum user profile needed for the pubkey. - // - // TODO: https://codeberg.org/superseriousbusiness/gotosocial/issues/1186 - minimalPerson, err := p.converter.AccountToASMinimal(ctx, receiver) - if err != nil { - err := gtserror.Newf("error converting to minimal account: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } - - // Return early with bare minimum data. - return data(minimalPerson) + data, err := ap.Serialize(accountable) + if err != nil { + err := gtserror.Newf("error serializing accountable: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // If the request is not on a public key path, we want to - // try to authenticate it before we serve any data, so that - // we can serve a more complete profile. - pubKeyAuth, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode // likely 401 - } + return data, nil +} - // Auth passed, generate the proper AP representation. - accountable, err := p.converter.AccountToAS(ctx, receiver) - if err != nil { - err := gtserror.Newf("error converting account: %w", err) +// UserGetMinimal returns a minimal AP representation +// of the requested account, containing just the public +// key, without doing authentication. +func (p *Processor) UserGetMinimal( + ctx context.Context, + requestedUser string, +) (any, gtserror.WithCode) { + acct, err := p.state.DB.GetAccountByUsernameDomain( + gtscontext.SetBarebones(ctx), + requestedUser, "", + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting account %s: %w", requestedUser, err) return nil, gtserror.NewErrorInternalError(err) } - if pubKeyAuth.Handshaking { - // If we are currently handshaking with the remote account - // making the request, then don't be coy: just serve the AP - // representation of the target account. - // - // This handshake check ensures that we don't get stuck in - // a loop with another GtS instance, where each instance is - // trying repeatedly to dereference the other account that's - // making the request before it will reveal its own account. - // - // Instead, we end up in an 'I'll show you mine if you show me - // yours' situation, where we sort of agree to reveal each - // other's profiles at the same time. - return data(accountable) + if acct == nil { + err := gtserror.Newf("account %s not found in the db", requestedUser) + return nil, gtserror.NewErrorNotFound(err) } - // Get requester from auth. - requester := pubKeyAuth.Owner - - // Check that block does not exist between receiver and requester. - blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID) + // Generate minimal AP representation. + accountable, err := p.converter.AccountToASMinimal(ctx, acct) if err != nil { - err := gtserror.Newf("error checking block: %w", err) + err := gtserror.Newf("error converting to accountable: %w", err) return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - const text = "block exists between accounts" - return nil, gtserror.NewErrorForbidden(errors.New(text)) } - return data(accountable) -} - -func data(accountable ap.Accountable) (interface{}, gtserror.WithCode) { data, err := ap.Serialize(accountable) if err != nil { err := gtserror.Newf("error serializing accountable: %w", err) diff --git a/internal/processing/fedi/wellknown.go b/internal/processing/fedi/wellknown.go index 0e6989803..236f09257 100644 --- a/internal/processing/fedi/wellknown.go +++ b/internal/processing/fedi/wellknown.go @@ -47,7 +47,7 @@ var ( nodeInfoProtocols = []string{"activitypub"} nodeInfoInbound = []string{} nodeInfoOutbound = []string{} - nodeInfoMetadata = make(map[string]interface{}) + nodeInfoMetadata = make(map[string]any) ) // NodeInfoRelGet returns a well known response giving the path to node info. @@ -156,11 +156,12 @@ func (p *Processor) HostMetaGet() *apimodel.HostMeta { } // WebfingerGet handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. -func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) { +func (p *Processor) WebfingerGet(ctx context.Context, requestedUser string) (*apimodel.WellKnownResponse, gtserror.WithCode) { // Get the local account the request is referring to. - requestedAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUsername, "") + requestedAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUser, "") if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + err := gtserror.Newf("db error getting account %s: %s", requestedUser, err) + return nil, gtserror.NewErrorNotFound(err) } return &apimodel.WellKnownResponse{ -- cgit v1.2.3