gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

federatingactor.go (11504B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package federation
     19 
     20 import (
     21 	"context"
     22 	"encoding/json"
     23 	"errors"
     24 	"fmt"
     25 	"io"
     26 	"net/http"
     27 	"net/url"
     28 	"strings"
     29 
     30 	"codeberg.org/gruf/go-kv"
     31 	"github.com/superseriousbusiness/activity/pub"
     32 	"github.com/superseriousbusiness/activity/streams"
     33 	"github.com/superseriousbusiness/activity/streams/vocab"
     34 	"github.com/superseriousbusiness/gotosocial/internal/ap"
     35 	"github.com/superseriousbusiness/gotosocial/internal/db"
     36 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     37 	"github.com/superseriousbusiness/gotosocial/internal/log"
     38 )
     39 
     40 // IsASMediaType will return whether the given content-type string
     41 // matches one of the 2 possible ActivityStreams incoming content types:
     42 // - application/activity+json
     43 // - application/ld+json;profile=https://w3.org/ns/activitystreams
     44 //
     45 // Where for the above we are leniant with whitespace and quotes.
     46 func IsASMediaType(ct string) bool {
     47 	var (
     48 		// First content-type part,
     49 		// contains the application/...
     50 		p1 string = ct //nolint:revive
     51 
     52 		// Second content-type part,
     53 		// contains AS IRI if provided
     54 		p2 string
     55 	)
     56 
     57 	// Split content-type by semi-colon.
     58 	sep := strings.IndexByte(ct, ';')
     59 	if sep >= 0 {
     60 		p1 = ct[:sep]
     61 		p2 = ct[sep+1:]
     62 	}
     63 
     64 	// Trim any ending space from the
     65 	// main content-type part of string.
     66 	p1 = strings.TrimRight(p1, " ")
     67 
     68 	switch p1 {
     69 	case "application/activity+json":
     70 		return p2 == ""
     71 
     72 	case "application/ld+json":
     73 		// Trim all start/end space.
     74 		p2 = strings.Trim(p2, " ")
     75 
     76 		// Drop any quotes around the URI str.
     77 		p2 = strings.ReplaceAll(p2, "\"", "")
     78 
     79 		// End part must be a ref to the main AS namespace IRI.
     80 		return p2 == "profile=https://www.w3.org/ns/activitystreams"
     81 
     82 	default:
     83 		return false
     84 	}
     85 }
     86 
     87 // federatingActor wraps the pub.FederatingActor
     88 // with some custom GoToSocial-specific logic.
     89 type federatingActor struct {
     90 	sideEffectActor pub.DelegateActor
     91 	wrapped         pub.FederatingActor
     92 }
     93 
     94 // newFederatingActor returns a federatingActor.
     95 func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
     96 	sideEffectActor := pub.NewSideEffectActor(c, s2s, nil, db, clock)
     97 	sideEffectActor.Serialize = ap.Serialize // hook in our own custom Serialize function
     98 
     99 	return &federatingActor{
    100 		sideEffectActor: sideEffectActor,
    101 		wrapped:         pub.NewCustomActor(sideEffectActor, false, true, clock),
    102 	}
    103 }
    104 
    105 // PostInboxScheme is a reimplementation of the default baseActor
    106 // implementation of PostInboxScheme in pub/base_actor.go.
    107 //
    108 // Key differences from that implementation:
    109 //   - More explicit debug logging when a request is not processed.
    110 //   - Normalize content of activity object.
    111 //   - *ALWAYS* return gtserror.WithCode if there's an issue, to
    112 //     provide more helpful messages to remote callers.
    113 //   - Return code 202 instead of 200 on successful POST, to reflect
    114 //     that we process most side effects asynchronously.
    115 func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
    116 	l := log.WithContext(ctx).
    117 		WithFields([]kv.Field{
    118 			{"userAgent", r.UserAgent()},
    119 			{"path", r.URL.Path},
    120 		}...)
    121 
    122 	// Ensure valid ActivityPub Content-Type.
    123 	// https://www.w3.org/TR/activitypub/#server-to-server-interactions
    124 	if ct := r.Header.Get("Content-Type"); !IsASMediaType(ct) {
    125 		const ct1 = "application/activity+json"
    126 		const ct2 = "application/ld+json;profile=https://w3.org/ns/activitystreams"
    127 		err := fmt.Errorf("Content-Type %s not acceptable, this endpoint accepts: [%q %q]", ct, ct1, ct2)
    128 		return false, gtserror.NewErrorNotAcceptable(err)
    129 	}
    130 
    131 	// Authenticate request by checking http signature.
    132 	ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r)
    133 	if err != nil {
    134 		return false, gtserror.NewErrorInternalError(err)
    135 	}
    136 
    137 	if !authenticated {
    138 		err = errors.New("not authenticated")
    139 		return false, gtserror.NewErrorUnauthorized(err)
    140 	}
    141 
    142 	/*
    143 		Begin processing the request, but note that we
    144 		have not yet applied authorization (ie., blocks).
    145 	*/
    146 
    147 	// Obtain the activity; reject unknown activities.
    148 	activity, errWithCode := resolveActivity(ctx, r)
    149 	if errWithCode != nil {
    150 		return false, errWithCode
    151 	}
    152 
    153 	// Set additional context data. Primarily this means
    154 	// looking at the Activity and seeing which IRIs are
    155 	// involved in it tangentially.
    156 	ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
    157 	if err != nil {
    158 		return false, gtserror.NewErrorInternalError(err)
    159 	}
    160 
    161 	// Check authorization of the activity; this will include blocks.
    162 	authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity)
    163 	if err != nil {
    164 		if errors.As(err, new(errOtherIRIBlocked)) {
    165 			// There's no direct block between requester(s) and
    166 			// receiver. However, one or more of the other IRIs
    167 			// involved in the request (account replied to, note
    168 			// boosted, etc) is blocked either at domain level or
    169 			// by the receiver. We don't need to return 403 here,
    170 			// instead, just return 202 accepted but don't do any
    171 			// further processing of the activity.
    172 			return true, nil
    173 		}
    174 
    175 		// Real error has occurred.
    176 		return false, gtserror.NewErrorInternalError(err)
    177 	}
    178 
    179 	if !authorized {
    180 		// Block exists either from this instance against
    181 		// one or more directly involved actors, or between
    182 		// receiving account and one of those actors.
    183 		err = errors.New("blocked")
    184 		return false, gtserror.NewErrorForbidden(err)
    185 	}
    186 
    187 	// Copy existing URL + add request host and scheme.
    188 	inboxID := func() *url.URL {
    189 		u := new(url.URL)
    190 		*u = *r.URL
    191 		u.Host = r.Host
    192 		u.Scheme = scheme
    193 		return u
    194 	}()
    195 
    196 	// At this point we have everything we need, and have verified that
    197 	// the POST request is authentic (properly signed) and authorized
    198 	// (permitted to interact with the target inbox).
    199 	//
    200 	// Post the activity to the Actor's inbox and trigger side effects .
    201 	if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil {
    202 		// Special case: We know it is a bad request if the object or
    203 		// target properties needed to be populated, but weren't.
    204 		// Send the rejection to the peer.
    205 		if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) {
    206 			// Log the original error but return something a bit more generic.
    207 			l.Debugf("malformed incoming Activity: %q", err)
    208 			err = errors.New("malformed incoming Activity: an Object and/or Target was required but not set")
    209 			return false, gtserror.NewErrorBadRequest(err, err.Error())
    210 		}
    211 
    212 		// There's been some real error.
    213 		err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err)
    214 		return false, gtserror.NewErrorInternalError(err)
    215 	}
    216 
    217 	// Side effects are complete. Now delegate determining whether
    218 	// to do inbox forwarding, as well as the action to do it.
    219 	if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil {
    220 		// As a not-ideal side-effect, InboxForwarding will try
    221 		// to create entries if the federatingDB returns `false`
    222 		// when calling `Exists()` to determine whether the Activity
    223 		// is in the database.
    224 		//
    225 		// Since our `Exists()` function currently *always*
    226 		// returns false, it will *always* attempt to insert
    227 		// the Activity. Therefore, we ignore AlreadyExists
    228 		// errors.
    229 		//
    230 		// This check may be removed when the `Exists()` func
    231 		// is updated, and/or federating callbacks are handled
    232 		// properly.
    233 		if !errors.Is(err, db.ErrAlreadyExists) {
    234 			// Failed inbox forwarding is not a show-stopper,
    235 			// and doesn't even necessarily denote a real error.
    236 			l.Warnf("error calling sideEffectActor.InboxForwarding: %q", err)
    237 		}
    238 	}
    239 
    240 	// Request is now undergoing processing. Caller
    241 	// of this function will handle writing Accepted.
    242 	return true, nil
    243 }
    244 
    245 // resolveActivity is a util function for pulling a
    246 // pub.Activity type out of an incoming POST request.
    247 func resolveActivity(ctx context.Context, r *http.Request) (pub.Activity, gtserror.WithCode) {
    248 	// Tidy up when done.
    249 	defer r.Body.Close()
    250 
    251 	b, err := io.ReadAll(r.Body)
    252 	if err != nil {
    253 		err = fmt.Errorf("error reading request body: %w", err)
    254 		return nil, gtserror.NewErrorInternalError(err)
    255 	}
    256 
    257 	var rawActivity map[string]interface{}
    258 	if err := json.Unmarshal(b, &rawActivity); err != nil {
    259 		err = fmt.Errorf("error unmarshalling request body: %w", err)
    260 		return nil, gtserror.NewErrorInternalError(err)
    261 	}
    262 
    263 	t, err := streams.ToType(ctx, rawActivity)
    264 	if err != nil {
    265 		if !streams.IsUnmatchedErr(err) {
    266 			// Real error.
    267 			err = fmt.Errorf("error matching json to type: %w", err)
    268 			return nil, gtserror.NewErrorInternalError(err)
    269 		}
    270 
    271 		// Respond with bad request; we just couldn't
    272 		// match the type to one that we know about.
    273 		err = errors.New("body json could not be resolved to ActivityStreams value")
    274 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    275 	}
    276 
    277 	activity, ok := t.(pub.Activity)
    278 	if !ok {
    279 		err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t)
    280 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    281 	}
    282 
    283 	if activity.GetJSONLDId() == nil {
    284 		err = fmt.Errorf("incoming Activity %s did not have required id property set", activity.GetTypeName())
    285 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    286 	}
    287 
    288 	// If activity Object is a Statusable, we'll want to replace the
    289 	// parsed `content` value with the value from the raw JSON instead.
    290 	// See https://github.com/superseriousbusiness/gotosocial/issues/1661
    291 	// Likewise, if it's an Accountable, we'll normalize some fields on it.
    292 	ap.NormalizeIncomingActivityObject(activity, rawActivity)
    293 
    294 	return activity, nil
    295 }
    296 
    297 /*
    298 	Functions below are just lightly wrapped versions
    299 	of the original go-fed federatingActor functions.
    300 */
    301 
    302 func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
    303 	return f.PostInboxScheme(c, w, r, "https")
    304 }
    305 
    306 func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
    307 	log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox)
    308 	return f.wrapped.Send(c, outbox, t)
    309 }
    310 
    311 func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
    312 	return f.wrapped.GetInbox(c, w, r)
    313 }
    314 
    315 func (f *federatingActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
    316 	return f.wrapped.PostOutbox(c, w, r)
    317 }
    318 
    319 func (f *federatingActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
    320 	return f.wrapped.PostOutboxScheme(c, w, r, scheme)
    321 }
    322 
    323 func (f *federatingActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
    324 	return f.wrapped.GetOutbox(c, w, r)
    325 }