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 }