42
internal/federation/clock.go
Normal file
42
internal/federation/clock.go
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
)
|
||||
|
||||
/*
|
||||
GOFED CLOCK INTERFACE
|
||||
Determines the time.
|
||||
*/
|
||||
|
||||
// Clock implements the Clock interface of go-fed
|
||||
type Clock struct{}
|
||||
|
||||
// Now just returns the time now
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func NewClock() pub.Clock {
|
||||
return &Clock{}
|
||||
}
|
||||
152
internal/federation/commonbehavior.go
Normal file
152
internal/federation/commonbehavior.go
Normal file
@ -0,0 +1,152 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
/*
|
||||
GOFED COMMON BEHAVIOR INTERFACE
|
||||
Contains functions required for both the Social API and Federating Protocol.
|
||||
It is passed to the library as a dependency injection from the client
|
||||
application.
|
||||
*/
|
||||
|
||||
// AuthenticateGetInbox delegates the authentication of a GET to an
|
||||
// inbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
func (f *federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
// IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through
|
||||
// the CLIENT API, not through the federation API, so we just do nothing here.
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox delegates the authentication of a GET to an
|
||||
// outbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetOutbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
func (f *federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
// IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through
|
||||
// the CLIENT API, not through the federation API, so we just do nothing here.
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// GetOutbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
//
|
||||
// AuthenticateGetOutbox will be called prior to this.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
func (f *federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
// IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through
|
||||
// the CLIENT API, not through the federation API, so we just do nothing here.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// NewTransport returns a new Transport on behalf of a specific actor.
|
||||
//
|
||||
// The actorBoxIRI will be either the inbox or outbox of an actor who is
|
||||
// attempting to do the dereferencing or delivery. Any authentication
|
||||
// scheme applied on the request must be based on this actor. The
|
||||
// request must contain some sort of credential of the user, such as a
|
||||
// HTTP Signature.
|
||||
//
|
||||
// The gofedAgent passed in should be used by the Transport
|
||||
// implementation in the User-Agent, as well as the application-specific
|
||||
// user agent string. The gofedAgent will indicate this library's use as
|
||||
// well as the library's version number.
|
||||
//
|
||||
// Any server-wide rate-limiting that needs to occur should happen in a
|
||||
// Transport implementation. This factory function allows this to be
|
||||
// created, so peer servers are not DOS'd.
|
||||
//
|
||||
// Any retry logic should also be handled by the Transport
|
||||
// implementation.
|
||||
//
|
||||
// Note that the library will not maintain a long-lived pointer to the
|
||||
// returned Transport so that any private credentials are able to be
|
||||
// garbage collected.
|
||||
func (f *federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) {
|
||||
|
||||
var username string
|
||||
var err error
|
||||
|
||||
if util.IsInboxPath(actorBoxIRI) {
|
||||
username, err = util.ParseInboxPath(actorBoxIRI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse path %s as an inbox: %s", actorBoxIRI.String(), err)
|
||||
}
|
||||
} else if util.IsOutboxPath(actorBoxIRI) {
|
||||
username, err = util.ParseOutboxPath(actorBoxIRI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse path %s as an outbox: %s", actorBoxIRI.String(), err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("id %s was neither an inbox path nor an outbox path", actorBoxIRI.String())
|
||||
}
|
||||
|
||||
account := >smodel.Account{}
|
||||
if err := f.db.GetLocalAccountByUsername(username, account); err != nil {
|
||||
return nil, fmt.Errorf("error getting account with username %s from the db: %s", username, err)
|
||||
}
|
||||
|
||||
return f.transportController.NewTransport(account.PublicKeyURI, account.PrivateKey)
|
||||
}
|
||||
136
internal/federation/federatingactor.go
Normal file
136
internal/federation/federatingactor.go
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
)
|
||||
|
||||
// federatingActor implements the go-fed federating protocol interface
|
||||
type federatingActor struct {
|
||||
actor pub.FederatingActor
|
||||
}
|
||||
|
||||
// newFederatingProtocol returns the gotosocial implementation of the GTSFederatingProtocol interface
|
||||
func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
|
||||
actor := pub.NewFederatingActor(c, s2s, db, clock)
|
||||
|
||||
return &federatingActor{
|
||||
actor: actor,
|
||||
}
|
||||
}
|
||||
|
||||
// Send a federated activity.
|
||||
//
|
||||
// The provided url must be the outbox of the sender. All processing of
|
||||
// the activity occurs similarly to the C2S flow:
|
||||
// - If t is not an Activity, it is wrapped in a Create activity.
|
||||
// - A new ID is generated for the activity.
|
||||
// - The activity is added to the specified outbox.
|
||||
// - The activity is prepared and delivered to recipients.
|
||||
//
|
||||
// Note that this function will only behave as expected if the
|
||||
// implementation has been constructed to support federation. This
|
||||
// method will guaranteed work for non-custom Actors. For custom actors,
|
||||
// care should be used to not call this method if only C2S is supported.
|
||||
func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
|
||||
return f.actor.Send(c, outbox, t)
|
||||
}
|
||||
|
||||
// PostInbox returns true if the request was handled as an ActivityPub
|
||||
// POST to an actor's inbox. If false, the request was not an
|
||||
// ActivityPub request and may still be handled by the caller in
|
||||
// another way, such as serving a web page.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the Actor was constructed with the Federated Protocol enabled,
|
||||
// side effects will occur.
|
||||
//
|
||||
// If the Federated Protocol is not enabled, writes the
|
||||
// http.StatusMethodNotAllowed status code in the response. No side
|
||||
// effects occur.
|
||||
func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return f.actor.PostInbox(c, w, r)
|
||||
}
|
||||
|
||||
// GetInbox returns true if the request was handled as an ActivityPub
|
||||
// GET to an actor's inbox. If false, the request was not an ActivityPub
|
||||
// request and may still be handled by the caller in another way, such
|
||||
// as serving a web page.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the request is an ActivityPub request, the Actor will defer to the
|
||||
// application to determine the correct authorization of the request and
|
||||
// the resulting OrderedCollection to respond with. The Actor handles
|
||||
// serializing this OrderedCollection and responding with the correct
|
||||
// headers and http.StatusOK.
|
||||
func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return f.actor.GetInbox(c, w, r)
|
||||
}
|
||||
|
||||
// PostOutbox returns true if the request was handled as an ActivityPub
|
||||
// POST to an actor's outbox. If false, the request was not an
|
||||
// ActivityPub request and may still be handled by the caller in another
|
||||
// way, such as serving a web page.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the Actor was constructed with the Social Protocol enabled, side
|
||||
// effects will occur.
|
||||
//
|
||||
// If the Social Protocol is not enabled, writes the
|
||||
// http.StatusMethodNotAllowed status code in the response. No side
|
||||
// effects occur.
|
||||
//
|
||||
// If the Social and Federated Protocol are both enabled, it will handle
|
||||
// the side effects of receiving an ActivityStream Activity, and then
|
||||
// federate the Activity to peers.
|
||||
func (f *federatingActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return f.actor.PostOutbox(c, w, r)
|
||||
}
|
||||
|
||||
// GetOutbox returns true if the request was handled as an ActivityPub
|
||||
// GET to an actor's outbox. If false, the request was not an
|
||||
// ActivityPub request.
|
||||
//
|
||||
// If the error is nil, then the ResponseWriter's headers and response
|
||||
// has already been written. If a non-nil error is returned, then no
|
||||
// response has been written.
|
||||
//
|
||||
// If the request is an ActivityPub request, the Actor will defer to the
|
||||
// application to determine the correct authorization of the request and
|
||||
// the resulting OrderedCollection to respond with. The Actor handles
|
||||
// serializing this OrderedCollection and responding with the correct
|
||||
// headers and http.StatusOK.
|
||||
func (f *federatingActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
return f.actor.GetOutbox(c, w, r)
|
||||
}
|
||||
@ -16,34 +16,23 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Package federation provides ActivityPub/federation functionality for GoToSocial
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// New returns a go-fed compatible federating actor
|
||||
func New(db db.DB, log *logrus.Logger) pub.FederatingActor {
|
||||
f := &Federator{
|
||||
db: db,
|
||||
}
|
||||
return pub.NewFederatingActor(f, f, db.Federation(), f)
|
||||
}
|
||||
|
||||
// Federator implements several go-fed interfaces in one convenient location
|
||||
type Federator struct {
|
||||
db db.DB
|
||||
}
|
||||
|
||||
/*
|
||||
GO FED FEDERATING PROTOCOL INTERFACE
|
||||
FederatingProtocol contains behaviors an application needs to satisfy for the
|
||||
@ -70,9 +59,21 @@ type Federator struct {
|
||||
// PostInbox. In this case, the DelegateActor implementation must not
|
||||
// write a response to the ResponseWriter as is expected that the caller
|
||||
// to PostInbox will do so when handling the error.
|
||||
func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
|
||||
l := f.log.WithFields(logrus.Fields{
|
||||
"func": "PostInboxRequestBodyHook",
|
||||
"useragent": r.UserAgent(),
|
||||
"url": r.URL.String(),
|
||||
})
|
||||
|
||||
if activity == nil {
|
||||
err := errors.New("nil activity in PostInboxRequestBodyHook")
|
||||
l.Debug(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctxWithActivity := context.WithValue(ctx, util.APActivity, activity)
|
||||
return ctxWithActivity, nil
|
||||
}
|
||||
|
||||
// AuthenticatePostInbox delegates the authentication of a POST to an
|
||||
@ -91,9 +92,54 @@ func (f *Federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
// TODO
|
||||
return nil, false, nil
|
||||
func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
l := f.log.WithFields(logrus.Fields{
|
||||
"func": "AuthenticatePostInbox",
|
||||
"useragent": r.UserAgent(),
|
||||
"url": r.URL.String(),
|
||||
})
|
||||
l.Trace("received request to authenticate")
|
||||
|
||||
requestedAccountI := ctx.Value(util.APAccount)
|
||||
if requestedAccountI == nil {
|
||||
return ctx, false, errors.New("requested account not set in context")
|
||||
}
|
||||
|
||||
requestedAccount, ok := requestedAccountI.(*gtsmodel.Account)
|
||||
if !ok || requestedAccount == nil {
|
||||
return ctx, false, errors.New("requested account not parsebale from context")
|
||||
}
|
||||
|
||||
publicKeyOwnerURI, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r)
|
||||
if err != nil {
|
||||
l.Debugf("request not authenticated: %s", err)
|
||||
return ctx, false, fmt.Errorf("not authenticated: %s", err)
|
||||
}
|
||||
|
||||
requestingAccount := >smodel.Account{}
|
||||
if err := f.db.GetWhere("uri", publicKeyOwnerURI.String(), requestingAccount); err != nil {
|
||||
// there's been a proper error so return it
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err)
|
||||
}
|
||||
|
||||
// we don't know this account (yet) so let's dereference it right now
|
||||
// TODO: slow-fed
|
||||
person, err := f.DereferenceRemoteAccount(requestedAccount.Username, publicKeyOwnerURI)
|
||||
if err != nil {
|
||||
return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err)
|
||||
}
|
||||
|
||||
a, err := f.typeConverter.ASRepresentationToAccount(person)
|
||||
if err != nil {
|
||||
return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err)
|
||||
}
|
||||
requestingAccount = a
|
||||
}
|
||||
|
||||
contextWithRequestingAccount := context.WithValue(ctx, util.APRequestingAccount, requestingAccount)
|
||||
|
||||
return contextWithRequestingAccount, true, nil
|
||||
}
|
||||
|
||||
// Blocked should determine whether to permit a set of actors given by
|
||||
@ -110,7 +156,7 @@ func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// blocked must be false and error nil. The request will continue
|
||||
// to be processed.
|
||||
func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
|
||||
func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
|
||||
// TODO
|
||||
return false, nil
|
||||
}
|
||||
@ -134,7 +180,7 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
||||
//
|
||||
// Applications are not expected to handle every single ActivityStreams
|
||||
// type and extension. The unhandled ones are passed to DefaultCallback.
|
||||
func (f *Federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) {
|
||||
func (f *federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) {
|
||||
// TODO
|
||||
return pub.FederatingWrappedCallbacks{}, nil, nil
|
||||
}
|
||||
@ -146,8 +192,12 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrap
|
||||
// Applications are not expected to handle every single ActivityStreams
|
||||
// type and extension, so the unhandled ones are passed to
|
||||
// DefaultCallback.
|
||||
func (f *Federator) DefaultCallback(ctx context.Context, activity pub.Activity) error {
|
||||
// TODO
|
||||
func (f *federator) DefaultCallback(ctx context.Context, activity pub.Activity) error {
|
||||
l := f.log.WithFields(logrus.Fields{
|
||||
"func": "DefaultCallback",
|
||||
"aptype": activity.GetTypeName(),
|
||||
})
|
||||
l.Debugf("received unhandle-able activity type so ignoring it")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -155,7 +205,7 @@ func (f *Federator) DefaultCallback(ctx context.Context, activity pub.Activity)
|
||||
// an activity to determine if inbox forwarding needs to occur.
|
||||
//
|
||||
// Zero or negative numbers indicate infinite recursion.
|
||||
func (f *Federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
|
||||
func (f *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
|
||||
// TODO
|
||||
return 0
|
||||
}
|
||||
@ -165,7 +215,7 @@ func (f *Federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
|
||||
// delivery.
|
||||
//
|
||||
// Zero or negative numbers indicate infinite recursion.
|
||||
func (f *Federator) MaxDeliveryRecursionDepth(ctx context.Context) int {
|
||||
func (f *federator) MaxDeliveryRecursionDepth(ctx context.Context) int {
|
||||
// TODO
|
||||
return 0
|
||||
}
|
||||
@ -177,7 +227,7 @@ func (f *Federator) MaxDeliveryRecursionDepth(ctx context.Context) int {
|
||||
//
|
||||
// The activity is provided as a reference for more intelligent
|
||||
// logic to be used, but the implementation must not modify it.
|
||||
func (f *Federator) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
|
||||
func (f *federator) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
@ -190,114 +240,8 @@ func (f *Federator) FilterForwarding(ctx context.Context, potentialRecipients []
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
func (f *Federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
// TODO
|
||||
func (f *federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
// IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through
|
||||
// the CLIENT API, not through the federation API, so we just do nothing here.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/*
|
||||
GOFED COMMON BEHAVIOR INTERFACE
|
||||
Contains functions required for both the Social API and Federating Protocol.
|
||||
It is passed to the library as a dependency injection from the client
|
||||
application.
|
||||
*/
|
||||
|
||||
// AuthenticateGetInbox delegates the authentication of a GET to an
|
||||
// inbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetInbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
func (f *Federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
// TODO
|
||||
// use context.WithValue() and context.Value() to set and get values through here
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// AuthenticateGetOutbox delegates the authentication of a GET to an
|
||||
// outbox.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
//
|
||||
// If an error is returned, it is passed back to the caller of
|
||||
// GetOutbox. In this case, the implementation must not write a
|
||||
// response to the ResponseWriter as is expected that the client will
|
||||
// do so when handling the error. The 'authenticated' is ignored.
|
||||
//
|
||||
// If no error is returned, but authentication or authorization fails,
|
||||
// then authenticated must be false and error nil. It is expected that
|
||||
// the implementation handles writing to the ResponseWriter in this
|
||||
// case.
|
||||
//
|
||||
// Finally, if the authentication and authorization succeeds, then
|
||||
// authenticated must be true and error nil. The request will continue
|
||||
// to be processed.
|
||||
func (f *Federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
|
||||
// TODO
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// GetOutbox returns the OrderedCollection inbox of the actor for this
|
||||
// context. It is up to the implementation to provide the correct
|
||||
// collection for the kind of authorization given in the request.
|
||||
//
|
||||
// AuthenticateGetOutbox will be called prior to this.
|
||||
//
|
||||
// Always called, regardless whether the Federated Protocol or Social
|
||||
// API is enabled.
|
||||
func (f *Federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// NewTransport returns a new Transport on behalf of a specific actor.
|
||||
//
|
||||
// The actorBoxIRI will be either the inbox or outbox of an actor who is
|
||||
// attempting to do the dereferencing or delivery. Any authentication
|
||||
// scheme applied on the request must be based on this actor. The
|
||||
// request must contain some sort of credential of the user, such as a
|
||||
// HTTP Signature.
|
||||
//
|
||||
// The gofedAgent passed in should be used by the Transport
|
||||
// implementation in the User-Agent, as well as the application-specific
|
||||
// user agent string. The gofedAgent will indicate this library's use as
|
||||
// well as the library's version number.
|
||||
//
|
||||
// Any server-wide rate-limiting that needs to occur should happen in a
|
||||
// Transport implementation. This factory function allows this to be
|
||||
// created, so peer servers are not DOS'd.
|
||||
//
|
||||
// Any retry logic should also be handled by the Transport
|
||||
// implementation.
|
||||
//
|
||||
// Note that the library will not maintain a long-lived pointer to the
|
||||
// returned Transport so that any private credentials are able to be
|
||||
// garbage collected.
|
||||
func (f *Federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/*
|
||||
GOFED CLOCK INTERFACE
|
||||
Determines the time.
|
||||
*/
|
||||
|
||||
// Now returns the current time.
|
||||
func (f *Federator) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
79
internal/federation/federator.go
Normal file
79
internal/federation/federator.go
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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 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) (typeutils.Accountable, 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
|
||||
}
|
||||
|
||||
type federator struct {
|
||||
config *config.Config
|
||||
db db.DB
|
||||
clock pub.Clock
|
||||
typeConverter typeutils.TypeConverter
|
||||
transportController transport.Controller
|
||||
actor pub.FederatingActor
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// NewFederator returns a new federator
|
||||
func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator {
|
||||
|
||||
clock := &Clock{}
|
||||
f := &federator{
|
||||
config: config,
|
||||
db: db,
|
||||
clock: &Clock{},
|
||||
typeConverter: typeConverter,
|
||||
transportController: transportController,
|
||||
log: log,
|
||||
}
|
||||
actor := newFederatingActor(f, f, db.Federation(), clock)
|
||||
f.actor = actor
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *federator) FederatingActor() pub.FederatingActor {
|
||||
return f.actor
|
||||
}
|
||||
190
internal/federation/federator_test.go
Normal file
190
internal/federation/federator_test.go
Normal file
@ -0,0 +1,190 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package federation_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type ProtocolTestSuite struct {
|
||||
suite.Suite
|
||||
config *config.Config
|
||||
db db.DB
|
||||
log *logrus.Logger
|
||||
storage storage.Storage
|
||||
typeConverter typeutils.TypeConverter
|
||||
accounts map[string]*gtsmodel.Account
|
||||
activities map[string]testrig.ActivityWithSignature
|
||||
}
|
||||
|
||||
// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
|
||||
func (suite *ProtocolTestSuite) SetupSuite() {
|
||||
// setup standard items
|
||||
suite.config = testrig.NewTestConfig()
|
||||
suite.db = testrig.NewTestDB()
|
||||
suite.log = testrig.NewTestLog()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.typeConverter = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.accounts = testrig.NewTestAccounts()
|
||||
suite.activities = testrig.NewTestActivities(suite.accounts)
|
||||
}
|
||||
|
||||
func (suite *ProtocolTestSuite) SetupTest() {
|
||||
testrig.StandardDBSetup(suite.db)
|
||||
|
||||
}
|
||||
|
||||
// TearDownTest drops tables to make sure there's no data in the db
|
||||
func (suite *ProtocolTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
}
|
||||
|
||||
// make sure PostInboxRequestBodyHook properly sets the inbox username and activity on the context
|
||||
func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
|
||||
|
||||
// the activity we're gonna use
|
||||
activity := suite.activities["dm_for_zork"]
|
||||
|
||||
// setup transport controller with a no-op client so we don't make external calls
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return nil, nil
|
||||
}))
|
||||
// setup module being tested
|
||||
federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter)
|
||||
|
||||
// setup request
|
||||
ctx := context.Background()
|
||||
request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) // the endpoint we're hitting
|
||||
request.Header.Set("Signature", activity.SignatureHeader)
|
||||
|
||||
// trigger the function being tested, and return the new context it creates
|
||||
newContext, err := federator.PostInboxRequestBodyHook(ctx, request, activity.Activity)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), newContext)
|
||||
|
||||
// activity should be set on context now
|
||||
activityI := newContext.Value(util.APActivity)
|
||||
assert.NotNil(suite.T(), activityI)
|
||||
returnedActivity, ok := activityI.(pub.Activity)
|
||||
assert.True(suite.T(), ok)
|
||||
assert.NotNil(suite.T(), returnedActivity)
|
||||
assert.EqualValues(suite.T(), activity.Activity, returnedActivity)
|
||||
}
|
||||
|
||||
func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
|
||||
|
||||
// the activity we're gonna use
|
||||
activity := suite.activities["dm_for_zork"]
|
||||
sendingAccount := suite.accounts["remote_account_1"]
|
||||
inboxAccount := suite.accounts["local_account_1"]
|
||||
|
||||
encodedPublicKey, err := x509.MarshalPKIXPublicKey(sendingAccount.PublicKey)
|
||||
assert.NoError(suite.T(), err)
|
||||
publicKeyBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: encodedPublicKey,
|
||||
})
|
||||
publicKeyString := strings.ReplaceAll(string(publicKeyBytes), "\n", "\\n")
|
||||
|
||||
// for this test we need the client to return the public key of the activity creator on the 'remote' instance
|
||||
responseBodyString := fmt.Sprintf(`
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
|
||||
"id": "%s",
|
||||
"type": "Person",
|
||||
"preferredUsername": "%s",
|
||||
"inbox": "%s",
|
||||
|
||||
"publicKey": {
|
||||
"id": "%s",
|
||||
"owner": "%s",
|
||||
"publicKeyPem": "%s"
|
||||
}
|
||||
}`, sendingAccount.URI, sendingAccount.Username, sendingAccount.InboxURI, sendingAccount.PublicKeyURI, sendingAccount.URI, publicKeyString)
|
||||
|
||||
// create a transport controller whose client will just return the response body string we specified above
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
r := ioutil.NopCloser(bytes.NewReader([]byte(responseBodyString)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: r,
|
||||
}, nil
|
||||
}))
|
||||
|
||||
// now setup module being tested, with the mock transport controller
|
||||
federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter)
|
||||
|
||||
// setup request
|
||||
ctx := context.Background()
|
||||
// by the time AuthenticatePostInbox is called, PostInboxRequestBodyHook should have already been called,
|
||||
// which should have set the account and username onto the request. We can replicate that behavior here:
|
||||
ctxWithAccount := context.WithValue(ctx, util.APAccount, inboxAccount)
|
||||
ctxWithActivity := context.WithValue(ctxWithAccount, util.APActivity, activity)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) // the endpoint we're hitting
|
||||
// we need these headers for the request to be validated
|
||||
request.Header.Set("Signature", activity.SignatureHeader)
|
||||
request.Header.Set("Date", activity.DateHeader)
|
||||
request.Header.Set("Digest", activity.DigestHeader)
|
||||
// we can pass this recorder as a writer and read it back after
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// trigger the function being tested, and return the new context it creates
|
||||
newContext, authed, err := federator.AuthenticatePostInbox(ctxWithActivity, recorder, request)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.True(suite.T(), authed)
|
||||
|
||||
// since we know this account already it should be set on the context
|
||||
requestingAccountI := newContext.Value(util.APRequestingAccount)
|
||||
assert.NotNil(suite.T(), requestingAccountI)
|
||||
requestingAccount, ok := requestingAccountI.(*gtsmodel.Account)
|
||||
assert.True(suite.T(), ok)
|
||||
assert.Equal(suite.T(), sendingAccount.Username, requestingAccount.Username)
|
||||
}
|
||||
|
||||
func TestProtocolTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ProtocolTestSuite))
|
||||
}
|
||||
237
internal/federation/util.go
Normal file
237
internal/federation/util.go
Normal file
@ -0,0 +1,237 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
/*
|
||||
publicKeyer is BORROWED DIRECTLY FROM https://github.com/go-fed/apcore/blob/master/ap/util.go
|
||||
Thank you @cj@mastodon.technology ! <3
|
||||
*/
|
||||
type publicKeyer interface {
|
||||
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
||||
}
|
||||
|
||||
/*
|
||||
getPublicKeyFromResponse is adapted from https://github.com/go-fed/apcore/blob/master/ap/util.go
|
||||
Thank you @cj@mastodon.technology ! <3
|
||||
*/
|
||||
func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (vocab.W3IDSecurityV1PublicKey, error) {
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := streams.ToType(c, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pker, ok := t.(publicKeyer)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %T", t)
|
||||
}
|
||||
|
||||
pkp := pker.GetW3IDSecurityV1PublicKey()
|
||||
if pkp == nil {
|
||||
return nil, errors.New("publicKey property is not provided")
|
||||
}
|
||||
|
||||
var pkpFound vocab.W3IDSecurityV1PublicKey
|
||||
for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() {
|
||||
if !pkpIter.IsW3IDSecurityV1PublicKey() {
|
||||
continue
|
||||
}
|
||||
pkValue := pkpIter.Get()
|
||||
var pkID *url.URL
|
||||
pkID, err = pub.GetId(pkValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pkID.String() != keyID.String() {
|
||||
continue
|
||||
}
|
||||
pkpFound = pkValue
|
||||
break
|
||||
}
|
||||
|
||||
if pkpFound == nil {
|
||||
return nil, fmt.Errorf("cannot find publicKey with id: %s", keyID)
|
||||
}
|
||||
|
||||
return pkpFound, nil
|
||||
}
|
||||
|
||||
// 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. 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.
|
||||
// Ie., if the request on this server is for https://example.org/users/some_username then you should pass in the username 'some_username'.
|
||||
// The remote server will then know that this is the user making the dereferencing request, and they can decide to allow or deny the request depending on their settings.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("could not create http sig verifier: %s", err)
|
||||
}
|
||||
|
||||
// The key ID should be given in the signature so that we know where to fetch it from the remote server.
|
||||
// This will be something like https://example.org/users/whatever_requesting_user#main-key
|
||||
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
|
||||
}
|
||||
|
||||
transport, err := f.GetTransportForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
}
|
||||
|
||||
// The actual http call to the remote server is made right here in the Dereference function.
|
||||
b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
|
||||
}
|
||||
|
||||
// if the key isn't in the response, we can't authenticate the request
|
||||
requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
|
||||
}
|
||||
|
||||
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
|
||||
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
|
||||
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
|
||||
return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
|
||||
}
|
||||
|
||||
// and decode the PEM so that we can parse it as a golang public key
|
||||
pubKeyPem := pkPemProp.Get()
|
||||
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")
|
||||
}
|
||||
|
||||
p, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
||||
}
|
||||
if p == nil {
|
||||
return nil, errors.New("returned public key was empty")
|
||||
}
|
||||
|
||||
// do the actual authentication here!
|
||||
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
||||
if err := verifier.Verify(p, algo); err != nil {
|
||||
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
|
||||
}
|
||||
|
||||
// all good! we just need the URI of the key owner to return
|
||||
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
|
||||
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
|
||||
return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
|
||||
}
|
||||
pkOwnerURI := pkOwnerProp.GetIRI()
|
||||
|
||||
return pkOwnerURI, nil
|
||||
}
|
||||
|
||||
func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) {
|
||||
|
||||
transport, err := f.GetTransportForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
}
|
||||
|
||||
b, err := transport.Dereference(context.Background(), remoteAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deferencing %s: %s", remoteAccountID.String(), err)
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling bytes into json: %s", err)
|
||||
}
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error resolving json into ap vocab type: %s", err)
|
||||
}
|
||||
|
||||
switch t.GetTypeName() {
|
||||
case string(gtsmodel.ActivityStreamsPerson):
|
||||
p, ok := t.(vocab.ActivityStreamsPerson)
|
||||
if !ok {
|
||||
return nil, errors.New("error resolving type as activitystreams person")
|
||||
}
|
||||
return p, nil
|
||||
case string(gtsmodel.ActivityStreamsApplication):
|
||||
// TODO: convert application into person
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
|
||||
}
|
||||
|
||||
func (f *federator) GetTransportForUser(username string) (pub.Transport, error) {
|
||||
// We need an account to use to create a transport for dereferecing the signature.
|
||||
// If a username has been given, we can fetch the account with that username and use it.
|
||||
// Otherwise, we can take the instance account and use those credentials to make the request.
|
||||
ourAccount := >smodel.Account{}
|
||||
var u string
|
||||
if username == "" {
|
||||
u = f.config.Host
|
||||
} else {
|
||||
u = username
|
||||
}
|
||||
if err := f.db.GetLocalAccountByUsername(u, ourAccount); err != nil {
|
||||
return nil, fmt.Errorf("error getting account %s from db: %s", username, err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user