wahhh getting there

This commit is contained in:
tsmethurst 2021-05-06 14:01:43 +02:00
parent 41e6e8ed10
commit d4c919d273
12 changed files with 214 additions and 151 deletions

View File

@ -23,7 +23,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// UsersGETHandler should be served at https://example.org/users/:username.
@ -55,60 +54,14 @@ func (m *Module) UsersGETHandler(c *gin.Context) {
}
l.Tracef("negotiated format: %s", format)
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := m.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
l.Errorf("database error getting account with username %s: %s", requestedUsername, err)
// we'll just return not authorized here to avoid giving anything away
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
// and create a transport for it
transport, err := m.federator.TransportController().NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey)
// make a copy of the context to pass along so we don't break anything
cp := c.Copy()
user, err := m.processor.GetAPUser(requestedUsername, cp.Request) // GetAPUser handles auth as well
if err != nil {
l.Errorf("error creating transport for username %s: %s", requestedUsername, err)
// we'll just return not authorized here to avoid giving anything away
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
l.Info(err.Error())
c.JSON(err.Code(), gin.H{"error": err.Safe()})
return
}
// authenticate the request
authentication, err := federation.AuthenticateFederatedRequest(transport, c.Request)
if err != nil {
l.Errorf("error authenticating GET user request: %s", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
if !authentication.Authenticated {
l.Debug("request not authorized")
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
requestingAccount := &gtsmodel.Account{}
if authentication.RequestingPublicKeyID != nil {
if err := m.db.GetWhere("public_key_uri", authentication.RequestingPublicKeyID.String(), requestingAccount); err != nil {
}
}
authorization, err := federation.AuthorizeFederatedRequest
person, err := m.tc.AccountToAS(requestedAccount)
if err != nil {
l.Errorf("error converting account to ap person: %s", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
data, err := person.Serialize()
if err != nil {
l.Errorf("error serializing user: %s", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
c.JSON(http.StatusOK, data)
c.JSON(http.StatusOK, user)
}

View File

@ -104,7 +104,30 @@ func (f *federatingDB) Unlock(c context.Context, id *url.URL) error {
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) {
return false, nil
if !util.IsInboxPath(inbox) {
return false, fmt.Errorf("%s is not an inbox URI", inbox.String())
}
if !util.IsStatusesPath(id) {
return false, fmt.Errorf("%s is not a status URI", id.String())
}
_, statusID, err := util.ParseStatusesPath(inbox)
if err != nil {
return false, fmt.Errorf("status URI %s was not parseable: %s", id.String(), err)
}
if err := f.db.GetByID(statusID, &gtsmodel.Status{}); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// we don't have it
return false, nil
}
// actual error
return false, fmt.Errorf("error getting status from db: %s", err)
}
// we must have it
return true, nil
}
// GetInbox returns the first ordered collection page of the outbox at
@ -128,11 +151,6 @@ func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOr
// the database has an entry for the IRI.
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
l := f.log.WithFields(logrus.Fields{
"func": "Owns",
"activityID": id.String(),
})
// if the id host isn't this instance host, we don't own this IRI
if id.Host != f.config.Host {
return false, nil
@ -142,27 +160,18 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
if util.IsStatusesPath(id) {
username, uid, err := util.ParseStatusesPath(id)
_, uid, err := util.ParseStatusesPath(id)
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
acct := &gtsmodel.Account{}
if err := f.db.GetLocalAccountByUsername(username, acct); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
status := &gtsmodel.Status{}
if err := f.db.GetByID(uid, status); err != nil {
if err := f.db.GetWhere("uri", uid, &gtsmodel.Status{}); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// there are no entries for this status
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)
}
// the user exists, the status exists, we own both, we're good
return true, nil
}
@ -172,34 +181,52 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
acct := &gtsmodel.Account{}
if err := f.db.GetLocalAccountByUsername(username, acct); err != nil {
if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil {
if _, ok := err.(ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
// the user exists, we own it, we're good
return true, nil
}
l.Info("could not match activityID")
return false, nil
return false, fmt.Errorf("could not match activityID: %s", id.String())
}
// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) {
return nil, nil
if !util.IsOutboxPath(outboxIRI) {
return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String())
}
acct := &gtsmodel.Account{}
if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil {
if _, ok := err.(ErrNoEntries); ok {
return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String())
}
return nil, fmt.Errorf("db error searching for actor with outbox %s", outboxIRI.String())
}
return url.Parse(acct.URI)
}
// ActorForInbox fetches the actor's IRI for the given outbox IRI.
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) {
return nil, nil
if !util.IsInboxPath(inboxIRI) {
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
}
acct := &gtsmodel.Account{}
if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
if _, ok := err.(ErrNoEntries); ok {
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
}
return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String())
}
return url.Parse(acct.URI)
}
// OutboxForInbox fetches the corresponding actor's outbox IRI for the
@ -207,7 +234,17 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) {
return nil, nil
if !util.IsInboxPath(inboxIRI) {
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
}
acct := &gtsmodel.Account{}
if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
if _, ok := err.(ErrNoEntries); ok {
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
}
return nil, fmt.Errorf("db error searching for actor with inbox %s", inboxIRI.String())
}
return url.Parse(acct.OutboxURI)
}
// Exists returns true if the database has an entry for the specified

View File

@ -578,6 +578,7 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachmen
}
func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) {
// TODO: check domain blocks as well
var blocked bool
if err := ps.conn.Model(&gtsmodel.Block{}).
Where("account_id = ?", account1).Where("target_account_id = ?", account2).

View File

@ -137,7 +137,9 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
requestingAccount = a
}
return nil, true, nil
contextWithRequestingAccount := context.WithValue(ctx, util.APRequestingAccount, requestingAccount)
return contextWithRequestingAccount, true, nil
}
// Blocked should determine whether to permit a set of actors given by

View File

@ -23,6 +23,7 @@ import (
"net/url"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams/vocab"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -32,9 +33,17 @@ import (
// Federator wraps various interfaces and functions to manage activitypub federation from gotosocial
type Federator interface {
// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes.
FederatingActor() pub.FederatingActor
TransportController() transport.Controller
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
// DereferenceRemoteAccount can be used to get the ActivityStreamsPerson representation of a remote account, based on the account ID (which is a URI).
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (vocab.ActivityStreamsPerson, error)
// GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username.
// This can be used for making signed http requests.
GetTransportForUser(username string) (pub.Transport, error)
pub.CommonBehavior
pub.FederatingProtocol
}
@ -69,7 +78,3 @@ func NewFederator(db db.DB, transportController transport.Controller, config *co
func (f *federator) FederatingActor() pub.FederatingActor {
return f.actor
}
func (f *federator) TransportController() transport.Controller {
return f.transportController
}

View File

@ -97,7 +97,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
request.Header.Set("Signature", activity.SignatureHeader)
// trigger the function being tested, and return the new context it creates
newContext, err := federator.FederatingProtocol().PostInboxRequestBodyHook(ctx, request, activity.Activity)
newContext, err := federator.PostInboxRequestBodyHook(ctx, request, activity.Activity)
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), newContext)
@ -173,7 +173,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
recorder := httptest.NewRecorder()
// trigger the function being tested, and return the new context it creates
newContext, authed, err := federator.FederatingProtocol().AuthenticatePostInbox(ctxWithActivity, recorder, request)
newContext, authed, err := federator.AuthenticatePostInbox(ctxWithActivity, recorder, request)
assert.NoError(suite.T(), err)
assert.True(suite.T(), authed)

View File

@ -93,13 +93,13 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca
return pkpFound, nil
}
// AuthenticateFederatedRequest authenticates any kind of federated request from a remote server. This includes things like
// GET requests for dereferencing users or statuses etc and POST requests for delivering new Activities.
//
// Error means the request did not pass authentication. No error means it's authentic.
// AuthenticateFederatedRequest authenticates any kind of incoming federated request from a remote server. This includes things like
// GET requests for dereferencing our users or statuses etc, and POST requests for delivering new Activities. The function returns
// the URL of the owner of the public key used in the http signature.
//
// Authenticate in this case is defined as just making sure that the http request is actually signed by whoever claims
// to have signed it, by fetching the public key from the signature and checking it against the remote public key.
// to have signed it, by fetching the public key from the signature and checking it against the remote public key. This function
// *does not* check whether the request is authorized, only whether it's authentic.
//
// The provided username will be used to generate a transport for making remote requests/derefencing the public key ID of the request signature.
// Ideally you should pass in the username of the user *being requested*, so that the remote server can decide how to handle the request based on who's making it.
@ -108,8 +108,8 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca
//
// Note that it is also valid to pass in an empty string here, in which case the keys of the instance account will be used.
//
// Note that this function *does not* dereference the remote account that the signature key is associated with, but it will
// return the owner of the public key, so that other functions can dereference it with that, as required.
// Also note that this function *does not* dereference the remote account that the signature key is associated with.
// Other functions should use the returned URL to dereference the remote account, if required.
func (f *federator) AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error) {
verifier, err := httpsig.NewVerifier(r)
if err != nil {
@ -225,9 +225,56 @@ func (f *federator) GetTransportForUser(username string) (pub.Transport, error)
return nil, fmt.Errorf("error getting account %s from db: %s", username, err)
}
transport, err := f.TransportController().NewTransport(ourAccount.PublicKeyURI, ourAccount.PrivateKey)
transport, err := f.transportController.NewTransport(ourAccount.PublicKeyURI, ourAccount.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error creating transport for user %s: %s", username, err)
}
return transport, nil
}
const (
activityStreamsContext = "https://www.w3.org/ns/activitystreams"
w3idContext = "https://w3id.org/security/v1"
tootContext = "http://joinmastodon.org/ns#"
schemaContext = "http://schema.org#"
)
// ActivityStreamsContext returns the url representation of https://www.w3.org/ns/activitystreams
func ActivityStreamsContext() *url.URL {
u, err := url.Parse(activityStreamsContext)
if err != nil {
panic(err)
}
return u
}
// W3IDContext returns the url representation of https://w3id.org/security/v1
func W3IDContext() *url.URL {
u, err := url.Parse(w3idContext)
if err != nil {
panic(err)
}
return u
}
// TootContext returns the url representation of http://joinmastodon.org/ns#
func TootContext() *url.URL {
u, err := url.Parse(tootContext)
if err != nil {
panic(err)
}
return u
}
// SchemaContext returns the url representation of http://schema.org#
func SchemaContext() *url.URL {
u, err := url.Parse(schemaContext)
if err != nil {
panic(err)
}
return u
}
func StandardContexts() vocab.ActivityStreamsContextProperty {
return nil
}

View File

@ -1,63 +1,70 @@
package message
import (
"fmt"
"net/http"
"github.com/go-fed/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *processor) GetAPUser(requestHeaders http.Header, username string) (interface{}, error) {
func (p *processor) GetAPUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
// get the account the request is referring to
requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
// // get the account the request is referring to
// requestedAccount := &gtsmodel.Account{}
// if err := m.db.GetLocalAccountByUsername(username, requestedAccount); err != nil {
// return nil, NewErrorNotAuthorized(fmt.Errorf("database error getting account with username %s: %s", username, err))
// }
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
return nil, NewErrorNotAuthorized(err)
}
// // and create a transport for it
// transport, err := p.federator.TransportController().NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey)
// if err != nil {
// l.Errorf("error creating transport for username %s: %s", requestedUsername, err)
// // we'll just return not authorized here to avoid giving anything away
// c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
// return
// }
requestingAccount := &gtsmodel.Account{}
err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount)
if err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// we don't have an entry for this account yet
// what we do now should depend on our chosen federation method
// for now though, we'll just dereference it
// TODO: slow-fed
requestingPerson, err := p.federator.DereferenceRemoteAccount(requestedUsername, requestingAccountURI)
if err != nil {
return nil, NewErrorInternalError(err)
}
requestedAccount, err = p.tc.ASPersonToAccount(requestingPerson)
if err != nil {
return nil, NewErrorInternalError(err)
}
if err := p.db.Put(requestingAccount); err != nil {
return nil, NewErrorInternalError(err)
}
} else {
// something has actually gone wrong
return nil, NewErrorInternalError(err)
}
}
// // authenticate the request
// authentication, err := federation.AuthenticateFederatedRequest(transport, c.Request)
// if err != nil {
// l.Errorf("error authenticating GET user request: %s", err)
// c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
// return
// }
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
if err != nil {
return nil, NewErrorInternalError(err)
}
// if !authentication.Authenticated {
// l.Debug("request not authorized")
// c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
// return
// }
if blocked {
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
// requestingAccount := &gtsmodel.Account{}
// if authentication.RequestingPublicKeyID != nil {
// if err := m.db.GetWhere("public_key_uri", authentication.RequestingPublicKeyID.String(), requestingAccount); err != nil {
requestedPerson, err := p.tc.AccountToAS(requestedAccount)
if err != nil {
return nil, NewErrorInternalError(err)
}
// }
// }
data, err := streams.Serialize(requestedPerson)
if err != nil {
return nil, NewErrorInternalError(err)
}
// authorization, err := federation.AuthorizeFederatedRequest
// person, err := m.tc.AccountToAS(requestedAccount)
// if err != nil {
// l.Errorf("error converting account to ap person: %s", err)
// c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
// return
// }
// data, err := person.Serialize()
// if err != nil {
// l.Errorf("error serializing user: %s", err)
// c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
// return
// }
// c.JSON(http.StatusOK, data)
return nil, nil
return data, nil
}

View File

@ -19,6 +19,8 @@
package message
import (
"net/http"
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
@ -48,7 +50,7 @@ type Processor interface {
FromFederator() chan FromFederator
/*
API-FACING PROCESSING FUNCTIONS
CLIENT API-FACING PROCESSING FUNCTIONS
These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply
to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
@ -82,6 +84,16 @@ type Processor interface {
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
/*
FEDERATION API-FACING PROCESSING FUNCTIONS
These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
response, pass work to the processor using a channel instead.
*/
GetAPUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
// Start starts the Processor, reading from its channels and passing messages back and forth.
Start() error
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.

View File

@ -82,11 +82,7 @@ func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatus
}
// return the frontend representation of the new status to the submitter
mastoStatus, err := p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil)
if err != nil {
return nil, err
}
return mastoStatus, nil
return p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil)
}
func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {

View File

@ -28,6 +28,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// Converts a gts model account into an Activity Streams person type, following
// the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/
func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {

View File

@ -23,6 +23,7 @@ import (
"fmt"
"testing"
"github.com/go-fed/activity/streams"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
@ -69,7 +70,7 @@ func (suite *InternalToASTestSuite) TestPostAccountToAS() {
asPerson, err := suite.typeconverter.AccountToAS(testAccount)
assert.NoError(suite.T(), err)
ser, err := asPerson.Serialize()
ser, err := streams.Serialize(asPerson)
assert.NoError(suite.T(), err)
bytes, err := json.Marshal(ser)