diff options
author | 2023-04-06 13:19:55 +0200 | |
---|---|---|
committer | 2023-04-06 12:19:55 +0100 | |
commit | c54510bc7407f22b58bd56ad65b2e3f60e8e4dc5 (patch) | |
tree | 26b1650686a8be480cd3115e3c2d814aa50e91c0 /internal/federation/federatingactor.go | |
parent | [bugfix] Always serialize orderedItems as array (#1673) (diff) | |
download | gotosocial-c54510bc7407f22b58bd56ad65b2e3f60e8e4dc5.tar.xz |
[bugfix] Normalize status content (don't parse status content as IRI) (#1665)
* start fannying about
* finish up Normalize
* tidy up
* pin to tag
* move errors about just a little bit
Diffstat (limited to 'internal/federation/federatingactor.go')
-rw-r--r-- | internal/federation/federatingactor.go | 271 |
1 files changed, 178 insertions, 93 deletions
diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go index eec45d0ba..18cdf2106 100644 --- a/internal/federation/federatingactor.go +++ b/internal/federation/federatingactor.go @@ -19,133 +19,218 @@ package federation import ( "context" + "encoding/json" + "fmt" + "io" "net/http" "net/url" + "strings" + "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/pub" + "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/log" ) -// federatingActor implements the go-fed federating protocol interface +// Potential incoming Content-Type header values; be +// lenient with whitespace and quotation mark placement. +var activityStreamsMediaTypes = []string{ + "application/activity+json", + "application/ld+json;profile=https://www.w3.org/ns/activitystreams", + "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"", + "application/ld+json ;profile=https://www.w3.org/ns/activitystreams", + "application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"", + "application/ld+json ; profile=https://www.w3.org/ns/activitystreams", + "application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"", + "application/ld+json; profile=https://www.w3.org/ns/activitystreams", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", +} + +// federatingActor wraps the pub.FederatingActor interface +// with some custom GoToSocial-specific logic. type federatingActor struct { - actor pub.FederatingActor + sideEffectActor pub.DelegateActor + wrapped pub.FederatingActor } -// newFederatingProtocol returns the gotosocial implementation of the GTSFederatingProtocol interface +// newFederatingProtocol returns a new federatingActor, which +// implements the pub.FederatingActor interface. func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor { - actor := pub.NewFederatingActor(c, s2s, db, clock) + sideEffectActor := pub.NewSideEffectActor(c, s2s, nil, db, clock) + customActor := pub.NewCustomActor(sideEffectActor, false, true, clock) return &federatingActor{ - actor: actor, + sideEffectActor: sideEffectActor, + wrapped: customActor, } } -// 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) { log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox) - return f.actor.Send(c, outbox, t) + return f.wrapped.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) + return f.PostInboxScheme(c, w, r, "https") } -// PostInboxScheme is similar to PostInbox, except clients are able to -// specify which protocol scheme to handle the incoming request and the -// data stored within the application (HTTP, HTTPS, etc). -func (f *federatingActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { - return f.actor.PostInboxScheme(c, w, r, scheme) +// PostInboxScheme is a reimplementation of the default baseActor +// implementation of PostInboxScheme in pub/base_actor.go. +// +// Key differences from that implementation: +// - More explicit debug logging when a request is not processed. +// - Normalize content of activity object. +// - Return code 202 instead of 200 on successful POST, to reflect +// that we process most side effects asynchronously. +func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + l := log. + WithContext(ctx). + WithFields([]kv.Field{ + {"userAgent", r.UserAgent()}, + {"path", r.URL.Path}, + }...) + + // Do nothing if this is not an ActivityPub POST request. + if !func() bool { + if r.Method != http.MethodPost { + l.Debugf("inbox request was %s rather than required POST", r.Method) + return false + } + + contentType := r.Header.Get("Content-Type") + for _, mediaType := range activityStreamsMediaTypes { + if strings.Contains(contentType, mediaType) { + return true + } + } + + l.Debugf("inbox POST request content-type %s was not recognized", contentType) + return false + }() { + return false, nil + } + + // Check the peer request is authentic. + ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r) + if err != nil { + return true, err + } else if !authenticated { + return true, nil + } + + // Begin processing the request, but note that we have + // not yet applied authorization (ex: blocks). + // + // Obtain the activity and reject unknown activities. + b, err := io.ReadAll(r.Body) + if err != nil { + err = fmt.Errorf("PostInboxScheme: error reading request body: %w", err) + return true, err + } + + var rawActivity map[string]interface{} + if err := json.Unmarshal(b, &rawActivity); err != nil { + err = fmt.Errorf("PostInboxScheme: error unmarshalling request body: %w", err) + return true, err + } + + t, err := streams.ToType(ctx, rawActivity) + if err != nil { + if !streams.IsUnmatchedErr(err) { + // Real error. + err = fmt.Errorf("PostInboxScheme: error matching json to type: %w", err) + return true, err + } + // Respond with bad request; we just couldn't + // match the type to one that we know about. + l.Debug("json could not be resolved to ActivityStreams value") + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + + activity, ok := t.(pub.Activity) + if !ok { + err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t) + return true, err + } + + if activity.GetJSONLDId() == nil { + l.Debugf("incoming Activity %s did not have required id property set", activity.GetTypeName()) + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + + // If activity Object is a Statusable, we'll want to replace the + // parsed `content` value with the value from the raw JSON instead. + // See https://github.com/superseriousbusiness/gotosocial/issues/1661 + ap.NormalizeActivityObject(activity, rawActivity) + + // Allow server implementations to set context data with a hook. + ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity) + if err != nil { + return true, err + } + + // Check authorization of the activity. + authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity) + if err != nil { + return true, err + } else if !authorized { + return true, nil + } + + // Copy existing URL + add request host and scheme. + inboxID := func() *url.URL { + id := &url.URL{} + *id = *r.URL + id.Host = r.Host + id.Scheme = scheme + return id + }() + + // Post the activity to the actor's inbox and trigger side effects for + // that particular Activity type. It is up to the delegate to resolve + // the given map. + if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil { + // Special case: We know it is a bad request if the object or + // target properties needed to be populated, but weren't. + // + // Send the rejection to the peer. + if err == pub.ErrObjectRequired || err == pub.ErrTargetRequired { + l.Debugf("malformed incoming Activity: %s", err) + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err) + return true, err + } + + // Our side effects are complete, now delegate determining whether to do inbox forwarding, as well as the action to do it. + if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil { + err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err) + return true, err + } + + // Request is now undergoing processing. + // Respond with an Accepted status. + w.WriteHeader(http.StatusAccepted) + return true, nil } -// 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) + return f.wrapped.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) + return f.wrapped.PostOutbox(c, w, r) } -// PostOutboxScheme is similar to PostOutbox, except clients are able to -// specify which protocol scheme to handle the incoming request and the -// data stored within the application (HTTP, HTTPS, etc). func (f *federatingActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { - return f.actor.PostOutboxScheme(c, w, r, scheme) + return f.wrapped.PostOutboxScheme(c, w, r, scheme) } -// 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) + return f.wrapped.GetOutbox(c, w, r) } |