diff options
Diffstat (limited to 'vendor/code.superseriousbusiness.org/activity/pub')
19 files changed, 5683 insertions, 0 deletions
diff --git a/vendor/code.superseriousbusiness.org/activity/pub/README.md b/vendor/code.superseriousbusiness.org/activity/pub/README.md new file mode 100644 index 000000000..37261e712 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/README.md @@ -0,0 +1,270 @@ +# pub + +Implements the Social and Federating Protocols in the ActivityPub specification. + +## Reference & Tutorial + +The [go-fed website](https://go-fed.org/) contains tutorials and reference +materials, in addition to the rest of this README. + +## How To Use + +``` +go get github.com/go-fed/activity +``` + +The root of all ActivityPub behavior is the `Actor`, which requires you to +implement a few interfaces: + +```golang +import ( + "code.superseriousbusiness.org/activity/pub" +) + +type myActivityPubApp struct { /* ... */ } +type myAppsDatabase struct { /* ... */ } +type myAppsClock struct { /* ... */ } + +var ( + // Your app will implement pub.CommonBehavior, and either + // pub.SocialProtocol, pub.FederatingProtocol, or both. + myApp = &myActivityPubApp{} + myCommonBehavior pub.CommonBehavior = myApp + mySocialProtocol pub.SocialProtocol = myApp + myFederatingProtocol pub.FederatingProtocol = myApp + // Your app's database implementation. + myDatabase pub.Database = &myAppsDatabase{} + // Your app's clock. + myClock pub.Clock = &myAppsClock{} +) + +// Only support the C2S Social protocol +actor := pub.NewSocialActor( + myCommonBehavior, + mySocialProtocol, + myDatabase, + myClock) +// OR +// +// Only support S2S Federating protocol +actor = pub.NewFederatingActor( + myCommonBehavior, + myFederatingProtocol, + myDatabase, + myClock) +// OR +// +// Support both C2S Social and S2S Federating protocol. +actor = pub.NewActor( + myCommonBehavior, + mySocialProtocol, + myFederatingProtocol, + myDatabase, + myClock) +``` + +Next, hook the `Actor` into your web server: + +```golang +// The application's actor +var actor pub.Actor +var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + c := context.Background() + // Populate c with request-specific information + if handled, err := actor.PostOutbox(c, w, r); err != nil { + // Write to w + return + } else if handled { + return + } else if handled, err = actor.GetOutbox(c, w, r); err != nil { + // Write to w + return + } else if handled { + return + } + // else: + // + // Handle non-ActivityPub request, such as serving a webpage. +} +var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + c := context.Background() + // Populate c with request-specific information + if handled, err := actor.PostInbox(c, w, r); err != nil { + // Write to w + return + } else if handled { + return + } else if handled, err = actor.GetInbox(c, w, r); err != nil { + // Write to w + return + } else if handled { + return + } + // else: + // + // Handle non-ActivityPub request, such as serving a webpage. +} +// Add the handlers to a HTTP server +serveMux := http.NewServeMux() +serveMux.HandleFunc("/actor/outbox", outboxHandler) +serveMux.HandleFunc("/actor/inbox", inboxHandler) +var server http.Server +server.Handler = serveMux +``` + +To serve ActivityStreams data: + +```golang +myHander := pub.NewActivityStreamsHandler(myDatabase, myClock) +var activityStreamsHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + c := context.Background() + // Populate c with request-specific information + if handled, err := myHandler(c, w, r); err != nil { + // Write to w + return + } else if handled { + return + } + // else: + // + // Handle non-ActivityPub request, such as serving a webpage. +} +serveMux.HandleFunc("/some/data/like/a/note", activityStreamsHandler) +``` + +### Dependency Injection + +Package `pub` relies on dependency injection to provide out-of-the-box support +for ActivityPub. The interfaces to be satisfied are: + +* `CommonBehavior` - Behavior needed regardless of which Protocol is used. +* `SocialProtocol` - Behavior needed for the Social Protocol. +* `FederatingProtocol` - Behavior needed for the Federating Protocol. +* `Database` - The data store abstraction, not tied to the `database/sql` +package. +* `Clock` - The server's internal clock. +* `Transport` - Responsible for the network that serves requests and deliveries +of ActivityStreams data. A `HttpSigTransport` type is provided. + +These implementations form the core of an application's behavior without +worrying about the particulars and pitfalls of the ActivityPub protocol. +Implementing these interfaces gives you greater assurance about being +ActivityPub compliant. + +### Application Logic + +The `SocialProtocol` and `FederatingProtocol` are responsible for returning +callback functions compatible with `streams.TypeResolver`. They also return +`SocialWrappedCallbacks` and `FederatingWrappedCallbacks`, which are nothing +more than a bundle of default behaviors for types like `Create`, `Update`, and +so on. + +Applications will want to focus on implementing their specific behaviors in the +callbacks, and have fine-grained control over customization: + +```golang +// Implements the FederatingProtocol interface. +// +// This illustration can also be applied for the Social Protocol. +func (m *myAppsFederatingProtocol) Callbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}) { + // The context 'c' has request-specific logic and can be used to apply complex + // logic building the right behaviors, if desired. + // + // 'c' will later be passed through to the callbacks created below. + wrapped = pub.FederatingWrappedCallbacks{ + Create: func(ctx context.Context, create vocab.ActivityStreamsCreate) error { + // This function is wrapped by default behavior. + // + // More application specific logic can be written here. + // + // 'ctx' will have request-specific information from the HTTP handler. It + // is the same as the 'c' passed to the Callbacks method. + // 'create' has, at this point, already triggered the recommended + // ActivityPub side effect behavior. The application can process it + // further as needed. + return nil + }, + } + // The 'other' must contain functions that satisfy the signature pattern + // required by streams.JSONResolver. + // + // If they are not, at runtime errors will be returned to indicate this. + other = []interface{}{ + // The FederatingWrappedCallbacks has default behavior for an "Update" type, + // but since we are providing this behavior in "other" and not in the + // FederatingWrappedCallbacks.Update member, we will entirely replace the + // default behavior provided by go-fed. Be careful that this still + // implements ActivityPub properly. + func(ctx context.Context, update vocab.ActivityStreamsUpdate) error { + // This function is NOT wrapped by default behavior. + // + // Application specific logic can be written here. + // + // 'ctx' will have request-specific information from the HTTP handler. It + // is the same as the 'c' passed to the Callbacks method. + // 'update' will NOT trigger the recommended ActivityPub side effect + // behavior. The application should do so in addition to any other custom + // side effects required. + return nil + }, + // The "Listen" type has no default suggested behavior in ActivityPub, so + // this just makes this application able to handle "Listen" activities. + func(ctx context.Context, listen vocab.ActivityStreamsListen) error { + // This function is NOT wrapped by default behavior. There's not a + // FederatingWrappedCallbacks.Listen member to wrap. + // + // Application specific logic can be written here. + // + // 'ctx' will have request-specific information from the HTTP handler. It + // is the same as the 'c' passed to the Callbacks method. + // 'listen' can be processed with side effects as the application needs. + return nil + }, + } + return +} +``` + +The `pub` package supports applications that grow into more custom solutions by +overriding the default behaviors as needed. + +### ActivityStreams Extensions: Future-Proofing An Application + +Package `pub` relies on the `streams.TypeResolver` and `streams.JSONResolver` +code generated types. As new ActivityStreams extensions are developed and their +code is generated, `pub` will automatically pick up support for these +extensions. + +The steps to rapidly implement a new extension in a `pub` application are: + +1. Generate an OWL definition of the ActivityStreams extension. This definition +could be the same one defining the vocabulary at the `@context` IRI. +2. Run `astool` to autogenerate the golang types in the `streams` package. +3. Implement the application's callbacks in the `FederatingProtocol.Callbacks` +or `SocialProtocol.Callbacks` for the new behaviors needed. +4. Build the application, which builds `pub`, with the newly generated `streams` +code. No code changes in `pub` are required. + +Whether an author of an ActivityStreams extension or an application developer, +these quick steps should reduce the barrier to adopion in a statically-typed +environment. + +### DelegateActor + +For those that need a near-complete custom ActivityPub solution, or want to have +that possibility in the future after adopting go-fed, the `DelegateActor` +interface can be used to obtain an `Actor`: + +```golang +// Use custom ActivityPub implementation +actor = pub.NewCustomActor( + myDelegateActor, + isSocialProtocolEnabled, + isFederatedProtocolEnabled, + myAppsClock) +``` + +It does not guarantee that an implementation adheres to the ActivityPub +specification. It acts as a stepping stone for applications that want to build +up to a fully custom solution and not be locked into the `pub` package +implementation. diff --git a/vendor/code.superseriousbusiness.org/activity/pub/activity.go b/vendor/code.superseriousbusiness.org/activity/pub/activity.go new file mode 100644 index 000000000..35d667749 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/activity.go @@ -0,0 +1,49 @@ +package pub + +import ( + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// Activity represents any ActivityStreams Activity type. +// +// The Activity types provided in the streams package implement this. +type Activity interface { + // Activity is also a vocab.Type + vocab.Type + // GetActivityStreamsActor returns the "actor" property if it exists, and + // nil otherwise. + GetActivityStreamsActor() vocab.ActivityStreamsActorProperty + // GetActivityStreamsAudience returns the "audience" property if it + // exists, and nil otherwise. + GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty + // GetActivityStreamsBcc returns the "bcc" property if it exists, and nil + // otherwise. + GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty + // GetActivityStreamsBto returns the "bto" property if it exists, and nil + // otherwise. + GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty + // GetActivityStreamsCc returns the "cc" property if it exists, and nil + // otherwise. + GetActivityStreamsCc() vocab.ActivityStreamsCcProperty + // GetActivityStreamsTo returns the "to" property if it exists, and nil + // otherwise. + GetActivityStreamsTo() vocab.ActivityStreamsToProperty + // GetActivityStreamsAttributedTo returns the "attributedTo" property if + // it exists, and nil otherwise. + GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty + // GetActivityStreamsObject returns the "object" property if it exists, + // and nil otherwise. + GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty + // SetActivityStreamsActor sets the "actor" property. + SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty) + // SetActivityStreamsObject sets the "object" property. + SetActivityStreamsObject(i vocab.ActivityStreamsObjectProperty) + // SetActivityStreamsTo sets the "to" property. + SetActivityStreamsTo(i vocab.ActivityStreamsToProperty) + // SetActivityStreamsBto sets the "bto" property. + SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty) + // SetActivityStreamsBcc sets the "bcc" property. + SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty) + // SetActivityStreamsAttributedTo sets the "attributedTo" property. + SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/actor.go b/vendor/code.superseriousbusiness.org/activity/pub/actor.go new file mode 100644 index 000000000..860325f5d --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/actor.go @@ -0,0 +1,128 @@ +package pub + +import ( + "context" + "net/http" + "net/url" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// Actor represents ActivityPub's actor concept. It conceptually has an inbox +// and outbox that receives either a POST or GET request, which triggers side +// effects in the federating application. +// +// An Actor within an application may federate server-to-server (Federation +// Protocol), client-to-server (Social API), or both. The Actor represents the +// server in either use case. +// +// An actor can be created by calling NewSocialActor (only the Social Protocol +// is supported), NewFederatingActor (only the Federating Protocol is +// supported), NewActor (both are supported), or NewCustomActor (neither are). +// +// Not all Actors have the same behaviors depending on the constructor used to +// create them. Refer to the constructor's documentation to determine the exact +// behavior of the Actor on an application. +// +// The behaviors documented here are common to all Actors returned by any +// constructor. +type Actor interface { + // 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. + // + // The request and data of your application will be interpreted as + // having an HTTPS protocol scheme. + PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) + // 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). + PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) + // 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. + GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) + // 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. + // + // The request will be interpreted as having an HTTPS scheme. + PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) + // 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). + PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) + // 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. + GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) +} + +// FederatingActor is an Actor that allows programmatically delivering an +// Activity to a federating peer. +type FederatingActor interface { + 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. + Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/base_actor.go b/vendor/code.superseriousbusiness.org/activity/pub/base_actor.go new file mode 100644 index 000000000..248fbd361 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/base_actor.go @@ -0,0 +1,475 @@ +package pub + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "code.superseriousbusiness.org/activity/streams" + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// baseActor must satisfy the Actor interface. +var _ Actor = &baseActor{} + +// baseActor is an application-independent ActivityPub implementation. It does +// not implement the entire protocol, and relies on a delegate to do so. It +// only implements the part of the protocol that is side-effect-free, allowing +// an existing application to write a DelegateActor that glues their application +// into the ActivityPub world. +// +// It is preferred to use a DelegateActor provided by this library, so that the +// application does not need to worry about the ActivityPub implementation. +type baseActor struct { + // delegate contains application-specific delegation logic. + delegate DelegateActor + // enableSocialProtocol enables or disables the Social API, the client to + // server part of ActivityPub. Useful if permitting remote clients to + // act on behalf of the users of the client application. + enableSocialProtocol bool + // enableFederatedProtocol enables or disables the Federated Protocol, or the + // server to server part of ActivityPub. Useful to permit integrating + // with the rest of the federative web. + enableFederatedProtocol bool + // clock simply tracks the current time. + clock Clock +} + +// baseActorFederating must satisfy the FederatingActor interface. +var _ FederatingActor = &baseActorFederating{} + +// baseActorFederating is a baseActor that also satisfies the FederatingActor +// interface. +// +// The baseActor is preserved as an Actor which will not successfully cast to a +// FederatingActor. +type baseActorFederating struct { + baseActor +} + +// NewSocialActor builds a new Actor concept that handles only the Social +// Protocol part of ActivityPub. +// +// This Actor can be created once in an application and reused to handle +// multiple requests concurrently and for different endpoints. +// +// It leverages as much of go-fed as possible to ensure the implementation is +// compliant with the ActivityPub specification, while providing enough freedom +// to be productive without shooting one's self in the foot. +// +// Do not try to use NewSocialActor and NewFederatingActor together to cover +// both the Social and Federating parts of the protocol. Instead, use NewActor. +func NewSocialActor(c CommonBehavior, + c2s SocialProtocol, + db Database, + clock Clock) Actor { + return &baseActor{ + // Use SideEffectActor without s2s. + delegate: NewSideEffectActor(c, nil, c2s, db, clock), + enableSocialProtocol: true, + clock: clock, + } +} + +// NewFederatingActor builds a new Actor concept that handles only the Federating +// Protocol part of ActivityPub. +// +// This Actor can be created once in an application and reused to handle +// multiple requests concurrently and for different endpoints. +// +// It leverages as much of go-fed as possible to ensure the implementation is +// compliant with the ActivityPub specification, while providing enough freedom +// to be productive without shooting one's self in the foot. +// +// Do not try to use NewSocialActor and NewFederatingActor together to cover +// both the Social and Federating parts of the protocol. Instead, use NewActor. +func NewFederatingActor(c CommonBehavior, + s2s FederatingProtocol, + db Database, + clock Clock) FederatingActor { + return &baseActorFederating{ + baseActor{ + // Use SideEffectActor without c2s. + delegate: NewSideEffectActor(c, s2s, nil, db, clock), + enableFederatedProtocol: true, + clock: clock, + }, + } +} + +// NewActor builds a new Actor concept that handles both the Social and +// Federating Protocol parts of ActivityPub. +// +// This Actor can be created once in an application and reused to handle +// multiple requests concurrently and for different endpoints. +// +// It leverages as much of go-fed as possible to ensure the implementation is +// compliant with the ActivityPub specification, while providing enough freedom +// to be productive without shooting one's self in the foot. +func NewActor(c CommonBehavior, + c2s SocialProtocol, + s2s FederatingProtocol, + db Database, + clock Clock) FederatingActor { + return &baseActorFederating{ + baseActor{ + delegate: NewSideEffectActor(c, s2s, c2s, db, clock), + enableSocialProtocol: true, + enableFederatedProtocol: true, + clock: clock, + }, + } +} + +// NewCustomActor allows clients to create a custom ActivityPub implementation +// for the Social Protocol, Federating Protocol, or both. +// +// It still uses the library as a high-level scaffold, which has the benefit of +// allowing applications to grow into a custom ActivityPub solution without +// having to refactor the code that passes HTTP requests into the Actor. +// +// It is possible to create a DelegateActor that is not ActivityPub compliant. +// Use with due care. +// +// If you find yourself passing a SideEffectActor in as the DelegateActor, +// consider using NewActor, NewFederatingActor, or NewSocialActor instead. +func NewCustomActor(delegate DelegateActor, + enableSocialProtocol, enableFederatedProtocol bool, + clock Clock) FederatingActor { + return &baseActorFederating{ + baseActor{ + delegate: delegate, + enableSocialProtocol: enableSocialProtocol, + enableFederatedProtocol: enableFederatedProtocol, + clock: clock, + }, + } +} + +// PostInbox implements the generic algorithm for handling a POST request to an +// actor's inbox independent on an application. It relies on a delegate to +// implement application specific functionality. +// +// Only supports serving data with identifiers having the HTTPS scheme. +func (b *baseActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + return b.PostInboxScheme(c, w, r, "https") +} + +// PostInbox implements the generic algorithm for handling a POST request to an +// actor's inbox independent on an application. It relies on a delegate to +// implement application specific functionality. +// +// Specifying the "scheme" allows for retrieving ActivityStreams content with +// identifiers such as HTTP, HTTPS, or other protocol schemes. +func (b *baseActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + // Do nothing if it is not an ActivityPub POST request. + if !isActivityPubPost(r) { + return false, nil + } + // If the Federated Protocol is not enabled, then this endpoint is not + // enabled. + if !b.enableFederatedProtocol { + w.WriteHeader(http.StatusMethodNotAllowed) + return true, nil + } + // Check the peer request is authentic. + c, authenticated, err := b.delegate.AuthenticatePostInbox(c, w, r) + if err != nil { + return true, err + } else if !authenticated { + return true, nil + } + // Begin processing the request, but have not yet applied + // authorization (ex: blocks). Obtain the activity reject unknown + // activities. + m, err := readActivityPubReq(r) + if err != nil { + return true, err + } + asValue, err := streams.ToType(c, m) + if err != nil && !streams.IsUnmatchedErr(err) { + return true, err + } else if streams.IsUnmatchedErr(err) { + // Respond with bad request -- we do not understand the type. + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + activity, ok := asValue.(Activity) + if !ok { + return true, fmt.Errorf("activity streams value is not an Activity: %T", asValue) + } + if activity.GetJSONLDId() == nil { + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + // Allow server implementations to set context data with a hook. + c, err = b.delegate.PostInboxRequestBodyHook(c, r, activity) + if err != nil { + return true, err + } + // Check authorization of the activity. + authorized, err := b.delegate.AuthorizePostInbox(c, w, activity) + if err != nil { + return true, err + } else if !authorized { + return true, nil + } + // 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. + inboxId := requestId(r, scheme) + err = b.delegate.PostInbox(c, inboxId, activity) + if 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 == ErrObjectRequired || err == ErrTargetRequired { + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + 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 := b.delegate.InboxForwarding(c, inboxId, activity); err != nil { + return true, err + } + // Request has been processed. Begin responding to the request. + // + // Simply respond with an OK status to the peer. + w.WriteHeader(http.StatusOK) + return true, nil +} + +// GetInbox implements the generic algorithm for handling a GET request to an +// actor's inbox independent on an application. It relies on a delegate to +// implement application specific functionality. +func (b *baseActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + // Do nothing if it is not an ActivityPub GET request. + if !isActivityPubGet(r) { + return false, nil + } + // Delegate authenticating and authorizing the request. + c, authenticated, err := b.delegate.AuthenticateGetInbox(c, w, r) + if err != nil { + return true, err + } else if !authenticated { + return true, nil + } + // Everything is good to begin processing the request. + oc, err := b.delegate.GetInbox(c, r) + if err != nil { + return true, err + } + // Deduplicate the 'orderedItems' property by ID. + err = dedupeOrderedItems(oc) + if err != nil { + return true, err + } + // Request has been processed. Begin responding to the request. + // + // Serialize the OrderedCollection. + m, err := streams.Serialize(oc) + if err != nil { + return true, err + } + raw, err := json.Marshal(m) + if err != nil { + return true, err + } + // Write the response. + addResponseHeaders(w.Header(), b.clock, raw) + w.WriteHeader(http.StatusOK) + n, err := w.Write(raw) + if err != nil { + return true, err + } else if n != len(raw) { + return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw)) + } + return true, nil +} + +// PostOutbox implements the generic algorithm for handling a POST request to an +// actor's outbox independent on an application. It relies on a delegate to +// implement application specific functionality. +// +// Only supports serving data with identifiers having the HTTPS scheme. +func (b *baseActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + return b.PostOutboxScheme(c, w, r, "https") +} + +// PostOutbox implements the generic algorithm for handling a POST request to an +// actor's outbox independent on an application. It relies on a delegate to +// implement application specific functionality. +// +// Specifying the "scheme" allows for retrieving ActivityStreams content with +// identifiers such as HTTP, HTTPS, or other protocol schemes. +func (b *baseActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + // Do nothing if it is not an ActivityPub POST request. + if !isActivityPubPost(r) { + return false, nil + } + // If the Social API is not enabled, then this endpoint is not enabled. + if !b.enableSocialProtocol { + w.WriteHeader(http.StatusMethodNotAllowed) + return true, nil + } + // Delegate authenticating and authorizing the request. + c, authenticated, err := b.delegate.AuthenticatePostOutbox(c, w, r) + if err != nil { + return true, err + } else if !authenticated { + return true, nil + } + // Everything is good to begin processing the request. + m, err := readActivityPubReq(r) + if err != nil { + return true, err + } + // Note that converting to a Type will NOT successfully convert types + // not known to go-fed. This prevents accidentally wrapping an Activity + // type unknown to go-fed in a Create below. Instead, + // streams.ErrUnhandledType will be returned here. + asValue, err := streams.ToType(c, m) + if err != nil && !streams.IsUnmatchedErr(err) { + return true, err + } else if streams.IsUnmatchedErr(err) { + // Respond with bad request -- we do not understand the type. + w.WriteHeader(http.StatusBadRequest) + return true, nil + } + // Allow server implementations to set context data with a hook. + c, err = b.delegate.PostOutboxRequestBodyHook(c, r, asValue) + if err != nil { + return true, err + } + // The HTTP request steps are complete, complete the rest of the outbox + // and delivery process. + outboxId := requestId(r, scheme) + activity, err := b.deliver(c, outboxId, asValue, m) + // 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 client. + if err == ErrObjectRequired || err == ErrTargetRequired { + w.WriteHeader(http.StatusBadRequest) + return true, nil + } else if err != nil { + return true, err + } + // Respond to the request with the new Activity's IRI location. + w.Header().Set(locationHeader, activity.GetJSONLDId().Get().String()) + w.WriteHeader(http.StatusCreated) + return true, nil +} + +// GetOutbox implements the generic algorithm for handling a Get request to an +// actor's outbox independent on an application. It relies on a delegate to +// implement application specific functionality. +func (b *baseActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + // Do nothing if it is not an ActivityPub GET request. + if !isActivityPubGet(r) { + return false, nil + } + // Delegate authenticating and authorizing the request. + c, authenticated, err := b.delegate.AuthenticateGetOutbox(c, w, r) + if err != nil { + return true, err + } else if !authenticated { + return true, nil + } + // Everything is good to begin processing the request. + oc, err := b.delegate.GetOutbox(c, r) + if err != nil { + return true, err + } + // Request has been processed. Begin responding to the request. + // + // Serialize the OrderedCollection. + m, err := streams.Serialize(oc) + if err != nil { + return true, err + } + raw, err := json.Marshal(m) + if err != nil { + return true, err + } + // Write the response. + addResponseHeaders(w.Header(), b.clock, raw) + w.WriteHeader(http.StatusOK) + n, err := w.Write(raw) + if err != nil { + return true, err + } else if n != len(raw) { + return true, fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(raw)) + } + return true, nil +} + +// deliver delegates all outbox handling steps and optionally will federate the +// activity if the federated protocol is enabled. +// +// This function is not exported so an Actor that only supports C2S cannot be +// type casted to a FederatingActor. It doesn't exactly fit the Send method +// signature anyways. +// +// Note: 'm' is nilable. +func (b *baseActor) deliver(c context.Context, outbox *url.URL, asValue vocab.Type, m map[string]interface{}) (activity Activity, err error) { + // If the value is not an Activity or type extending from Activity, then + // we need to wrap it in a Create Activity. + if !streams.IsOrExtendsActivityStreamsActivity(asValue) { + asValue, err = b.delegate.WrapInCreate(c, asValue, outbox) + if err != nil { + return + } + } + // At this point, this should be a safe conversion. If this error is + // triggered, then there is either a bug in the delegation of + // WrapInCreate, behavior is not lining up in the generated ExtendedBy + // code, or something else is incorrect with the type system. + var ok bool + activity, ok = asValue.(Activity) + if !ok { + err = fmt.Errorf("activity streams value is not an Activity: %T", asValue) + return + } + // Delegate generating new IDs for the activity and all new objects. + if err = b.delegate.AddNewIDs(c, activity); err != nil { + return + } + // Post the activity to the actor's outbox and trigger side effects for + // that particular Activity type. + // + // Since 'm' is nil-able and side effects may need access to literal nil + // values, such as for Update activities, ensure 'm' is non-nil. + if m == nil { + m, err = asValue.Serialize() + if err != nil { + return + } + } + deliverable, err := b.delegate.PostOutbox(c, activity, outbox, m) + if err != nil { + return + } + // Request has been processed and all side effects internal to this + // application server have finished. Begin side effects affecting other + // servers and/or the client who sent this request. + // + // If we are federating and the type is a deliverable one, then deliver + // the activity to federating peers. + if b.enableFederatedProtocol && deliverable { + if err = b.delegate.Deliver(c, outbox, activity); err != nil { + return + } + } + return +} + +// Send is programmatically accessible if the federated protocol is enabled. +func (b *baseActorFederating) Send(c context.Context, outbox *url.URL, t vocab.Type) (Activity, error) { + return b.deliver(c, outbox, t, nil) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/clock.go b/vendor/code.superseriousbusiness.org/activity/pub/clock.go new file mode 100644 index 000000000..bf19e49f7 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/clock.go @@ -0,0 +1,11 @@ +package pub + +import ( + "time" +) + +// Clock determines the time. +type Clock interface { + // Now returns the current time. + Now() time.Time +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/common_behavior.go b/vendor/code.superseriousbusiness.org/activity/pub/common_behavior.go new file mode 100644 index 000000000..aaa6cf8b2 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/common_behavior.go @@ -0,0 +1,90 @@ +package pub + +import ( + "context" + "net/http" + "net/url" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// Common 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. +type CommonBehavior interface { + // 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. + AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // 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. + AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // 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. + GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) + // 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. + NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/database.go b/vendor/code.superseriousbusiness.org/activity/pub/database.go new file mode 100644 index 000000000..af7756c34 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/database.go @@ -0,0 +1,152 @@ +package pub + +import ( + "context" + "net/url" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +type Database interface { + // Lock takes a lock for the object at the specified id. If an error + // is returned, the lock must not have been taken. + // + // The lock must be able to succeed for an id that does not exist in + // the database. This means acquiring the lock does not guarantee the + // entry exists in the database. + // + // Locks are encouraged to be lightweight and in the Go layer, as some + // processes require tight loops acquiring and releasing locks. + // + // Used to ensure race conditions in multiple requests do not occur. + Lock(c context.Context, id *url.URL) (unlock func(), err error) + // InboxContains returns true if the OrderedCollection at 'inbox' + // contains the specified 'id'. + // + // The library makes this call only after acquiring a lock first. + InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) + // GetInbox returns the first ordered collection page of the outbox at + // the specified IRI, for prepending new items. + // + // The library makes this call only after acquiring a lock first. + GetInbox(c context.Context, inboxIRI *url.URL) (inbox vocab.ActivityStreamsOrderedCollectionPage, err error) + // SetInbox saves the inbox value given from GetInbox, with new items + // prepended. Note that the new items must not be added as independent + // database entries. Separate calls to Create will do that. + // + // The library makes this call only after acquiring a lock first. + SetInbox(c context.Context, inbox vocab.ActivityStreamsOrderedCollectionPage) error + // Owns returns true if the database has an entry for the IRI and it + // exists in the database. + // + // The library makes this call only after acquiring a lock first. + Owns(c context.Context, id *url.URL) (owns bool, err error) + // ActorForOutbox fetches the actor's IRI for the given outbox IRI. + // + // The library makes this call only after acquiring a lock first. + ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) + // ActorForInbox fetches the actor's IRI for the given outbox IRI. + // + // The library makes this call only after acquiring a lock first. + ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) + // OutboxForInbox fetches the corresponding actor's outbox IRI for the + // actor's inbox IRI. + // + // The library makes this call only after acquiring a lock first. + OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) + // InboxesForIRI fetches inboxes corresponding to the given iri. + // This allows your server to skip remote dereferencing of iris + // in order to speed up message delivery, if desired. + // + // It is acceptable to just return nil or an empty slice for the inboxIRIs, + // if you don't know the inbox iri, or you don't wish to use this feature. + // In this case, the library will attempt to resolve inboxes of the iri + // by remote dereferencing instead. + // + // If the input iri is the iri of an Actor, then the inbox for the actor + // should be returned as a single-entry slice. + // + // If the input iri is a Collection (such as a Collection of followers), + // then each follower inbox IRI should be returned in the inboxIRIs slice. + // + // The library makes this call only after acquiring a lock first. + InboxesForIRI(c context.Context, iri *url.URL) (inboxIRIs []*url.URL, err error) + // Exists returns true if the database has an entry for the specified + // id. It may not be owned by this application instance. + // + // The library makes this call only after acquiring a lock first. + Exists(c context.Context, id *url.URL) (exists bool, err error) + // Get returns the database entry for the specified id. + // + // The library makes this call only after acquiring a lock first. + Get(c context.Context, id *url.URL) (value vocab.Type, err error) + // Create adds a new entry to the database which must be able to be + // keyed by its id. + // + // Note that Activity values received from federated peers may also be + // created in the database this way if the Federating Protocol is + // enabled. The client may freely decide to store only the id instead of + // the entire value. + // + // The library makes this call only after acquiring a lock first. + // + // Under certain conditions and network activities, Create may be called + // multiple times for the same ActivityStreams object. + Create(c context.Context, asType vocab.Type) error + // Update sets an existing entry to the database based on the value's + // id. + // + // Note that Activity values received from federated peers may also be + // updated in the database this way if the Federating Protocol is + // enabled. The client may freely decide to store only the id instead of + // the entire value. + // + // The library makes this call only after acquiring a lock first. + Update(c context.Context, asType vocab.Type) error + // Delete removes the entry with the given id. + // + // Delete is only called for federated objects. Deletes from the Social + // Protocol instead call Update to create a Tombstone. + // + // The library makes this call only after acquiring a lock first. + Delete(c context.Context, id *url.URL) error + // GetOutbox returns the first ordered collection page of the outbox + // at the specified IRI, for prepending new items. + // + // The library makes this call only after acquiring a lock first. + GetOutbox(c context.Context, outboxIRI *url.URL) (outbox vocab.ActivityStreamsOrderedCollectionPage, err error) + // SetOutbox saves the outbox value given from GetOutbox, with new items + // prepended. Note that the new items must not be added as independent + // database entries. Separate calls to Create will do that. + // + // The library makes this call only after acquiring a lock first. + SetOutbox(c context.Context, outbox vocab.ActivityStreamsOrderedCollectionPage) error + // NewID creates a new IRI id for the provided activity or object. The + // implementation does not need to set the 'id' property and simply + // needs to determine the value. + // + // The go-fed library will handle setting the 'id' property on the + // activity or object provided with the value returned. + NewID(c context.Context, t vocab.Type) (id *url.URL, err error) + // Followers obtains the Followers Collection for an actor with the + // given id. + // + // If modified, the library will then call Update. + // + // The library makes this call only after acquiring a lock first. + Followers(c context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) + // Following obtains the Following Collection for an actor with the + // given id. + // + // If modified, the library will then call Update. + // + // The library makes this call only after acquiring a lock first. + Following(c context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) + // Liked obtains the Liked Collection for an actor with the + // given id. + // + // If modified, the library will then call Update. + // + // The library makes this call only after acquiring a lock first. + Liked(c context.Context, actorIRI *url.URL) (liked vocab.ActivityStreamsCollection, err error) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/delegate_actor.go b/vendor/code.superseriousbusiness.org/activity/pub/delegate_actor.go new file mode 100644 index 000000000..28a225b24 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/delegate_actor.go @@ -0,0 +1,249 @@ +package pub + +import ( + "context" + "net/http" + "net/url" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// DelegateActor contains the detailed interface an application must satisfy in +// order to implement the ActivityPub specification. +// +// Note that an implementation of this interface is implicitly provided in the +// calls to NewActor, NewSocialActor, and NewFederatingActor. +// +// Implementing the DelegateActor requires familiarity with the ActivityPub +// specification because it does not a strong enough abstraction for the client +// application to ignore the ActivityPub spec. It is very possible to implement +// this interface and build a foot-gun that trashes the fediverse without being +// ActivityPub compliant. Please use with due consideration. +// +// Alternatively, build an application that uses the parts of the pub library +// that do not require implementing a DelegateActor so that the ActivityPub +// implementation is completely provided out of the box. +type DelegateActor interface { + // Hook callback after parsing the request body for a federated request + // to the Actor's inbox. + // + // Can be used to set contextual information based on the Activity + // received. + // + // Only called if the Federated Protocol is enabled. + // + // Warning: Neither authentication nor authorization has taken place at + // this time. Doing anything beyond setting contextual information is + // strongly discouraged. + // + // If an error is returned, it is passed back to the caller of + // 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. + PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) + // Hook callback after parsing the request body for a client request + // to the Actor's outbox. + // + // Can be used to set contextual information based on the + // ActivityStreams object received. + // + // Only called if the Social API is enabled. + // + // Warning: Neither authentication nor authorization has taken place at + // this time. Doing anything beyond setting contextual information is + // strongly discouraged. + // + // If an error is returned, it is passed back to the caller of + // PostOutbox. In this case, the DelegateActor implementation must not + // write a response to the ResponseWriter as is expected that the caller + // to PostOutbox will do so when handling the error. + PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) + // AuthenticatePostInbox delegates the authentication of a POST to an + // inbox. + // + // Only called if the Federated Protocol is enabled. + // + // If an error is returned, it is passed back to the caller of + // PostInbox. 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. + AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // 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. + AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // AuthorizePostInbox delegates the authorization of an activity that + // has been sent by POST to an inbox. + // + // Only called if the Federated Protocol is enabled. + // + // If an error is returned, it is passed back to the caller of + // PostInbox. 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 'authorized' is ignored. + // + // If no error is returned, but authorization fails, then authorized + // 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 + // authorized must be true and error nil. The request will continue + // to be processed. + AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (authorized bool, err error) + // PostInbox delegates the side effects of adding to the inbox and + // determining if it is a request that should be blocked. + // + // Only called if the Federated Protocol is enabled. + // + // As a side effect, PostInbox sets the federated data in the inbox, but + // not on its own in the database, as InboxForwarding (which is called + // later) must decide whether it has seen this activity before in order + // to determine whether to do the forwarding algorithm. + // + // If the error is ErrObjectRequired or ErrTargetRequired, then a Bad + // Request status is sent in the response. + PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error + // InboxForwarding delegates inbox forwarding logic when a POST request + // is received in the Actor's inbox. + // + // Only called if the Federated Protocol is enabled. + // + // The delegate is responsible for determining whether to do the inbox + // forwarding, as well as actually conducting it if it determines it + // needs to. + // + // As a side effect, InboxForwarding must set the federated data in the + // database, independently of the inbox, however it sees fit in order to + // determine whether it has seen the activity before. + // + // The provided url is the inbox of the recipient of the Activity. The + // Activity is examined for the information about who to inbox forward + // to. + // + // If an error is returned, it is returned to the caller of PostInbox. + InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error + // PostOutbox delegates the logic for side effects and adding to the + // outbox. + // + // Always called, regardless whether the Federated Protocol or Social + // API is enabled. In the case of the Social API being enabled, side + // effects of the Activity must occur. + // + // The delegate is responsible for adding the activity to the database's + // general storage for independent retrieval, and not just within the + // actor's outbox. + // + // If the error is ErrObjectRequired or ErrTargetRequired, then a Bad + // Request status is sent in the response. + // + // Note that 'rawJSON' is an unfortunate consequence where an 'Update' + // Activity is the only one that explicitly cares about 'null' values in + // JSON. Since go-fed does not differentiate between 'null' values and + // values that are simply not present, the 'rawJSON' map is ONLY needed + // for this narrow and specific use case. + PostOutbox(c context.Context, a Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (deliverable bool, e error) + // AddNewIDs sets new URL ids on the activity. It also does so for all + // 'object' properties if the Activity is a Create type. + // + // Only called if the Social API is enabled. + // + // If an error is returned, it is returned to the caller of PostOutbox. + AddNewIDs(c context.Context, a Activity) error + // Deliver sends a federated message. Called only if federation is + // enabled. + // + // Called if the Federated Protocol is enabled. + // + // The provided url is the outbox of the sender. The Activity contains + // the information about the intended recipients. + // + // If an error is returned, it is returned to the caller of PostOutbox. + Deliver(c context.Context, outbox *url.URL, activity Activity) error + // AuthenticatePostOutbox delegates the authentication and authorization + // of a POST to an outbox. + // + // Only called if the Social API is enabled. + // + // If an error is returned, it is passed back to the caller of + // PostOutbox. 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. + AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // 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. + AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // WrapInCreate wraps the provided object in a Create ActivityStreams + // activity. The provided URL is the actor's outbox endpoint. + // + // Only called if the Social API is enabled. + WrapInCreate(c context.Context, value vocab.Type, outboxIRI *url.URL) (vocab.ActivityStreamsCreate, error) + // 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. + GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) + // GetInbox 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. + // + // AuthenticateGetInbox will be called prior to this. + // + // Always called, regardless whether the Federated Protocol or Social + // API is enabled. + GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/doc.go b/vendor/code.superseriousbusiness.org/activity/pub/doc.go new file mode 100644 index 000000000..93c778179 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/doc.go @@ -0,0 +1,9 @@ +// Package pub implements the ActivityPub protocol. +// +// Note that every time the ActivityStreams types are changed (added, removed) +// due to code generation, the internal function toASType needs to be modified +// to know about these types. +// +// Note that every version change should also include a change in the version.go +// file. +package pub diff --git a/vendor/code.superseriousbusiness.org/activity/pub/federating_protocol.go b/vendor/code.superseriousbusiness.org/activity/pub/federating_protocol.go new file mode 100644 index 000000000..302e4f5ca --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/federating_protocol.go @@ -0,0 +1,125 @@ +package pub + +import ( + "context" + "net/http" + "net/url" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// FederatingProtocol contains behaviors an application needs to satisfy for the +// full ActivityPub S2S implementation to be supported by this library. +// +// It is only required if the client application wants to support the server-to- +// server, or federating, protocol. +// +// It is passed to the library as a dependency injection from the client +// application. +type FederatingProtocol interface { + // Hook callback after parsing the request body for a federated request + // to the Actor's inbox. + // + // Can be used to set contextual information based on the Activity + // received. + // + // Only called if the Federated Protocol is enabled. + // + // Warning: Neither authentication nor authorization has taken place at + // this time. Doing anything beyond setting contextual information is + // strongly discouraged. + // + // If an error is returned, it is passed back to the caller of + // 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. + PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) + // AuthenticatePostInbox delegates the authentication of a POST to an + // inbox. + // + // If an error is returned, it is passed back to the caller of + // PostInbox. 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. + AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // Blocked should determine whether to permit a set of actors given by + // their ids are able to interact with this particular end user due to + // being blocked or other application-specific logic. + // + // If an error is returned, it is passed back to the caller of + // PostInbox. + // + // If no error is returned, but authentication or authorization fails, + // then blocked must be true and error nil. An http.StatusForbidden + // will be written in the wresponse. + // + // Finally, if the authentication and authorization succeeds, then + // blocked must be false and error nil. The request will continue + // to be processed. + Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error) + // FederatingCallbacks returns the application logic that handles + // ActivityStreams received from federating peers. + // + // Note that certain types of callbacks will be 'wrapped' with default + // behaviors supported natively by the library. Other callbacks + // compatible with streams.TypeResolver can be specified by 'other'. + // + // For example, setting the 'Create' field in the + // FederatingWrappedCallbacks lets an application dependency inject + // additional behaviors they want to take place, including the default + // behavior supplied by this library. This is guaranteed to be compliant + // with the ActivityPub Social protocol. + // + // To override the default behavior, instead supply the function in + // 'other', which does not guarantee the application will be compliant + // with the ActivityPub Social Protocol. + // + // Applications are not expected to handle every single ActivityStreams + // type and extension. The unhandled ones are passed to DefaultCallback. + FederatingCallbacks(c context.Context) (wrapped FederatingWrappedCallbacks, other []interface{}, err error) + // DefaultCallback is called for types that go-fed can deserialize but + // are not handled by the application's callbacks returned in the + // Callbacks method. + // + // Applications are not expected to handle every single ActivityStreams + // type and extension, so the unhandled ones are passed to + // DefaultCallback. + DefaultCallback(c context.Context, activity Activity) error + // MaxInboxForwardingRecursionDepth determines how deep to search within + // an activity to determine if inbox forwarding needs to occur. + // + // Zero or negative numbers indicate infinite recursion. + MaxInboxForwardingRecursionDepth(c context.Context) int + // MaxDeliveryRecursionDepth determines how deep to search within + // collections owned by peers when they are targeted to receive a + // delivery. + // + // Zero or negative numbers indicate infinite recursion. + MaxDeliveryRecursionDepth(c context.Context) int + // FilterForwarding allows the implementation to apply business logic + // such as blocks, spam filtering, and so on to a list of potential + // Collections and OrderedCollections of recipients when inbox + // forwarding has been triggered. + // + // The activity is provided as a reference for more intelligent + // logic to be used, but the implementation must not modify it. + FilterForwarding(c context.Context, potentialRecipients []*url.URL, a Activity) (filteredRecipients []*url.URL, err error) + // GetInbox 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. + // + // AuthenticateGetInbox will be called prior to this. + // + // Always called, regardless whether the Federated Protocol or Social + // API is enabled. + GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/federating_wrapped_callbacks.go b/vendor/code.superseriousbusiness.org/activity/pub/federating_wrapped_callbacks.go new file mode 100644 index 000000000..8a8f32aaa --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/federating_wrapped_callbacks.go @@ -0,0 +1,917 @@ +package pub + +import ( + "context" + "fmt" + "net/url" + + "code.superseriousbusiness.org/activity/streams" + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// OnFollowBehavior enumerates the different default actions that the go-fed +// library can provide when receiving a Follow Activity from a peer. +type OnFollowBehavior int + +const ( + // OnFollowDoNothing does not take any action when a Follow Activity + // is received. + OnFollowDoNothing OnFollowBehavior = iota + // OnFollowAutomaticallyAccept triggers the side effect of sending an + // Accept of this Follow request in response. + OnFollowAutomaticallyAccept + // OnFollowAutomaticallyAccept triggers the side effect of sending a + // Reject of this Follow request in response. + OnFollowAutomaticallyReject +) + +// FederatingWrappedCallbacks lists the callback functions that already have +// some side effect behavior provided by the pub library. +// +// These functions are wrapped for the Federating Protocol. +type FederatingWrappedCallbacks struct { + // Create handles additional side effects for the Create ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping callback for the Federating Protocol ensures the + // 'object' property is created in the database. + // + // Create calls Create for each object in the federated Activity. + Create func(context.Context, vocab.ActivityStreamsCreate) error + // Update handles additional side effects for the Update ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping callback for the Federating Protocol ensures the + // 'object' property is updated in the database. + // + // Update calls Update on the federated entry from the database, with a + // new value. + Update func(context.Context, vocab.ActivityStreamsUpdate) error + // Delete handles additional side effects for the Delete ActivityStreams + // type, specific to the application using go-fed. + // + // Delete removes the federated entry from the database. + Delete func(context.Context, vocab.ActivityStreamsDelete) error + // Follow handles additional side effects for the Follow ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function can have one of several default behaviors, + // depending on the value of the OnFollow setting. + Follow func(context.Context, vocab.ActivityStreamsFollow) error + // OnFollow determines what action to take for this particular callback + // if a Follow Activity is handled. + OnFollow OnFollowBehavior + // Accept handles additional side effects for the Accept ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function determines if this 'Accept' is in response to a + // 'Follow'. If so, then the 'actor' is added to the original 'actor's + // 'following' collection. + // + // Otherwise, no side effects are done by go-fed. + Accept func(context.Context, vocab.ActivityStreamsAccept) error + // Reject handles additional side effects for the Reject ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function has no default side effects. However, if this + // 'Reject' is in response to a 'Follow' then the client MUST NOT go + // forward with adding the 'actor' to the original 'actor's 'following' + // collection by the client application. + Reject func(context.Context, vocab.ActivityStreamsReject) error + // Add handles additional side effects for the Add ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function will add the 'object' IRIs to a specific + // 'target' collection if the 'target' collection(s) live on this + // server. + Add func(context.Context, vocab.ActivityStreamsAdd) error + // Remove handles additional side effects for the Remove ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function will remove all 'object' IRIs from a specific + // 'target' collection if the 'target' collection(s) live on this + // server. + Remove func(context.Context, vocab.ActivityStreamsRemove) error + // Like handles additional side effects for the Like ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function will add the activity to the "likes" collection + // on all 'object' targets owned by this server. + Like func(context.Context, vocab.ActivityStreamsLike) error + // Announce handles additional side effects for the Announce + // ActivityStreams type, specific to the application using go-fed. + // + // The wrapping function will add the activity to the "shares" + // collection on all 'object' targets owned by this server. + Announce func(context.Context, vocab.ActivityStreamsAnnounce) error + // Undo handles additional side effects for the Undo ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function ensures the 'actor' on the 'Undo' + // is be the same as the 'actor' on all Activities being undone. + // It enforces that the actors on the Undo must correspond to all of the + // 'object' actors in some manner. + // + // It is expected that the application will implement the proper + // reversal of activities that are being undone. + Undo func(context.Context, vocab.ActivityStreamsUndo) error + // Block handles additional side effects for the Block ActivityStreams + // type, specific to the application using go-fed. + // + // The wrapping function provides no default side effects. It simply + // calls the wrapped function. However, note that Blocks should not be + // received from a federated peer, as delivering Blocks explicitly + // deviates from the original ActivityPub specification. + Block func(context.Context, vocab.ActivityStreamsBlock) error + + // Sidechannel data -- this is set at request handling time. These must + // be set before the callbacks are used. + + // db is the Database the FederatingWrappedCallbacks should use. + db Database + // inboxIRI is the inboxIRI that is handling this callback. + inboxIRI *url.URL + // addNewIds creates new 'id' entries on an activity and its objects if + // it is a Create activity. + addNewIds func(c context.Context, activity Activity) error + // deliver delivers an outgoing message. + deliver func(c context.Context, outboxIRI *url.URL, activity Activity) error + // newTransport creates a new Transport. + newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error) +} + +// callbacks returns the WrappedCallbacks members into a single interface slice +// for use in streams.Resolver callbacks. +// +// If the given functions have a type that collides with the default behavior, +// then disable our default behavior +func (w FederatingWrappedCallbacks) callbacks(fns []interface{}) []interface{} { + enableCreate := true + enableUpdate := true + enableDelete := true + enableFollow := true + enableAccept := true + enableReject := true + enableAdd := true + enableRemove := true + enableLike := true + enableAnnounce := true + enableUndo := true + enableBlock := true + for _, fn := range fns { + switch fn.(type) { + default: + continue + case func(context.Context, vocab.ActivityStreamsCreate) error: + enableCreate = false + case func(context.Context, vocab.ActivityStreamsUpdate) error: + enableUpdate = false + case func(context.Context, vocab.ActivityStreamsDelete) error: + enableDelete = false + case func(context.Context, vocab.ActivityStreamsFollow) error: + enableFollow = false + case func(context.Context, vocab.ActivityStreamsAccept) error: + enableAccept = false + case func(context.Context, vocab.ActivityStreamsReject) error: + enableReject = false + case func(context.Context, vocab.ActivityStreamsAdd) error: + enableAdd = false + case func(context.Context, vocab.ActivityStreamsRemove) error: + enableRemove = false + case func(context.Context, vocab.ActivityStreamsLike) error: + enableLike = false + case func(context.Context, vocab.ActivityStreamsAnnounce) error: + enableAnnounce = false + case func(context.Context, vocab.ActivityStreamsUndo) error: + enableUndo = false + case func(context.Context, vocab.ActivityStreamsBlock) error: + enableBlock = false + } + } + if enableCreate { + fns = append(fns, w.create) + } + if enableUpdate { + fns = append(fns, w.update) + } + if enableDelete { + fns = append(fns, w.deleteFn) + } + if enableFollow { + fns = append(fns, w.follow) + } + if enableAccept { + fns = append(fns, w.accept) + } + if enableReject { + fns = append(fns, w.reject) + } + if enableAdd { + fns = append(fns, w.add) + } + if enableRemove { + fns = append(fns, w.remove) + } + if enableLike { + fns = append(fns, w.like) + } + if enableAnnounce { + fns = append(fns, w.announce) + } + if enableUndo { + fns = append(fns, w.undo) + } + if enableBlock { + fns = append(fns, w.block) + } + return fns +} + +// create implements the federating Create activity side effects. +func (w FederatingWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error { + t := iter.GetType() + if t == nil && iter.IsIRI() { + // Attempt to dereference the IRI instead + tport, err := w.newTransport(c, w.inboxIRI, goFedUserAgent()) + if err != nil { + return err + } + resp, err := tport.Dereference(c, iter.GetIRI()) + if err != nil { + return err + } + m, err := readActivityPubResp(resp) + if err != nil { + return err + } + t, err = streams.ToType(c, m) + if err != nil { + return err + } + } else if t == nil { + return fmt.Errorf("cannot handle federated create: object is neither a value nor IRI") + } + id, err := GetId(t) + if err != nil { + return err + } + var unlock func() + unlock, err = w.db.Lock(c, id) + if err != nil { + return err + } + defer unlock() + if err := w.db.Create(c, t); err != nil { + return err + } + return nil + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + if err := loopFn(iter); err != nil { + return err + } + } + if w.Create != nil { + return w.Create(c, a) + } + return nil +} + +// update implements the federating Update activity side effects. +func (w FederatingWrappedCallbacks) update(c context.Context, a vocab.ActivityStreamsUpdate) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + if err := mustHaveActivityOriginMatchObjects(a); err != nil { + return err + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error { + t := iter.GetType() + if t == nil { + return fmt.Errorf("update requires an object to be wholly provided") + } + id, err := GetId(t) + if err != nil { + return err + } + var unlock func() + unlock, err = w.db.Lock(c, id) + if err != nil { + return err + } + defer unlock() + if err := w.db.Update(c, t); err != nil { + return err + } + return nil + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + if err := loopFn(iter); err != nil { + return err + } + } + if w.Update != nil { + return w.Update(c, a) + } + return nil +} + +// deleteFn implements the federating Delete activity side effects. +func (w FederatingWrappedCallbacks) deleteFn(c context.Context, a vocab.ActivityStreamsDelete) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + if err := mustHaveActivityOriginMatchObjects(a); err != nil { + return err + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error { + id, err := ToId(iter) + if err != nil { + return err + } + var unlock func() + unlock, err = w.db.Lock(c, id) + if err != nil { + return err + } + defer unlock() + if err := w.db.Delete(c, id); err != nil { + return err + } + return nil + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + if err := loopFn(iter); err != nil { + return err + } + } + if w.Delete != nil { + return w.Delete(c, a) + } + return nil +} + +// follow implements the federating Follow activity side effects. +func (w FederatingWrappedCallbacks) follow(c context.Context, a vocab.ActivityStreamsFollow) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + // Check that we own at least one of the 'object' properties, and ensure + // it is to the actor that owns this inbox. + // + // If not then don't send a response. It was federated to us as an FYI, + // by mistake, or some other reason. + unlock, err := w.db.Lock(c, w.inboxIRI) + if err != nil { + return err + } + // WARNING: Unlock not deferred. + actorIRI, err := w.db.ActorForInbox(c, w.inboxIRI) + unlock() // unlock even on error + if err != nil { + return err + } + // Unlock must be called by now and every branch above. + isMe := false + if w.OnFollow != OnFollowDoNothing { + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + if id.String() == actorIRI.String() { + isMe = true + break + } + } + } + if isMe { + // Prepare the response. + var response Activity + if w.OnFollow == OnFollowAutomaticallyAccept { + response = streams.NewActivityStreamsAccept() + } else if w.OnFollow == OnFollowAutomaticallyReject { + response = streams.NewActivityStreamsReject() + } else { + return fmt.Errorf("unknown OnFollowBehavior: %d", w.OnFollow) + } + // Set us as the 'actor'. + me := streams.NewActivityStreamsActorProperty() + response.SetActivityStreamsActor(me) + me.AppendIRI(actorIRI) + // Set the Follow as the 'object' property. + op := streams.NewActivityStreamsObjectProperty() + response.SetActivityStreamsObject(op) + op.AppendActivityStreamsFollow(a) + // Add all actors on the original Follow to the 'to' property. + recipients := make([]*url.URL, 0) + to := streams.NewActivityStreamsToProperty() + response.SetActivityStreamsTo(to) + followActors := a.GetActivityStreamsActor() + for iter := followActors.Begin(); iter != followActors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + to.AppendIRI(id) + recipients = append(recipients, id) + } + if w.OnFollow == OnFollowAutomaticallyAccept { + // If automatically accepting, then also update our + // followers collection with the new actors. + // + // If automatically rejecting, do not update the + // followers collection. + unlock, err := w.db.Lock(c, actorIRI) + if err != nil { + return err + } + // WARNING: Unlock not deferred. + followers, err := w.db.Followers(c, actorIRI) + if err != nil { + unlock() + return err + } + items := followers.GetActivityStreamsItems() + if items == nil { + items = streams.NewActivityStreamsItemsProperty() + followers.SetActivityStreamsItems(items) + } + for _, elem := range recipients { + items.PrependIRI(elem) + } + err = w.db.Update(c, followers) + unlock() // unlock even on error + if err != nil { + return err + } + // Unlock must be called by now and every branch above. + } + // Lock without defer! + unlock, err := w.db.Lock(c, w.inboxIRI) + if err != nil { + return err + } + outboxIRI, err := w.db.OutboxForInbox(c, w.inboxIRI) + unlock() // unlock after, regardless + if err != nil { + return err + } + // Everything must be unlocked by now. + if err := w.addNewIds(c, response); err != nil { + return err + } else if err := w.deliver(c, outboxIRI, response); err != nil { + return err + } + } + if w.Follow != nil { + return w.Follow(c, a) + } + return nil +} + +// accept implements the federating Accept activity side effects. +func (w FederatingWrappedCallbacks) accept(c context.Context, a vocab.ActivityStreamsAccept) error { + op := a.GetActivityStreamsObject() + if op != nil && op.Len() > 0 { + // Get this actor's id. + unlock, err := w.db.Lock(c, w.inboxIRI) + if err != nil { + return err + } + // WARNING: Unlock not deferred. + actorIRI, err := w.db.ActorForInbox(c, w.inboxIRI) + unlock() // unlock after regardless + if err != nil { + return err + } + // Unlock must be called by now and every branch above. + // + // Determine if we are in a follow on the 'object' property. + // + // TODO: Handle Accept multiple Follow. + var maybeMyFollowIRI *url.URL + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + t := iter.GetType() + if t == nil && iter.IsIRI() { + // Attempt to dereference the IRI instead + tport, err := w.newTransport(c, w.inboxIRI, goFedUserAgent()) + if err != nil { + return err + } + resp, err := tport.Dereference(c, iter.GetIRI()) + if err != nil { + return err + } + m, err := readActivityPubResp(resp) + if err != nil { + return err + } + t, err = streams.ToType(c, m) + if err != nil { + return err + } + } else if t == nil { + return fmt.Errorf("cannot handle federated create: object is neither a value nor IRI") + } + // Ensure it is a Follow. + if !streams.IsOrExtendsActivityStreamsFollow(t) { + continue + } + follow, ok := t.(Activity) + if !ok { + return fmt.Errorf("a Follow in an Accept does not satisfy the Activity interface") + } + followId, err := GetId(follow) + if err != nil { + return err + } + // Ensure that we are one of the actors on the Follow. + actors := follow.GetActivityStreamsActor() + for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + if id.String() == actorIRI.String() { + maybeMyFollowIRI = followId + break + } + } + // Continue breaking if we found ourselves + if maybeMyFollowIRI != nil { + break + } + } + // If we received an Accept whose 'object' is a Follow with an + // Accept that we sent, add to the following collection. + if maybeMyFollowIRI != nil { + // Verify our Follow request exists and the peer didn't + // fabricate it. + activityActors := a.GetActivityStreamsActor() + if activityActors == nil || activityActors.Len() == 0 { + return fmt.Errorf("an Accept with a Follow has no actors") + } + // This may be a duplicate check if we dereferenced the + // Follow above. TODO: Separate this logic to avoid + // redundancy. + // + // Use an anonymous function to properly scope the + // database lock, immediately call it. + err = func() error { + unlock, err := w.db.Lock(c, maybeMyFollowIRI) + if err != nil { + return err + } + defer unlock() + t, err := w.db.Get(c, maybeMyFollowIRI) + if err != nil { + return err + } + if !streams.IsOrExtendsActivityStreamsFollow(t) { + return fmt.Errorf("peer gave an Accept wrapping a Follow but provided a non-Follow id") + } + follow, ok := t.(Activity) + if !ok { + return fmt.Errorf("a Follow in an Accept does not satisfy the Activity interface") + } + // Ensure that we are one of the actors on the Follow. + ok = false + actors := follow.GetActivityStreamsActor() + for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + if id.String() == actorIRI.String() { + ok = true + break + } + } + if !ok { + return fmt.Errorf("peer gave an Accept wrapping a Follow but we are not the actor on that Follow") + } + // Build map of original Accept actors + acceptActors := make(map[string]bool) + for iter := activityActors.Begin(); iter != activityActors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + acceptActors[id.String()] = false + } + // Verify all actor(s) were on the original Follow. + followObj := follow.GetActivityStreamsObject() + for iter := followObj.Begin(); iter != followObj.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + if _, ok := acceptActors[id.String()]; ok { + acceptActors[id.String()] = true + } + } + for _, found := range acceptActors { + if !found { + return fmt.Errorf("peer gave an Accept wrapping a Follow but was not an object in the original Follow") + } + } + return nil + }() + if err != nil { + return err + } + // Add the peer to our following collection. + unlock, err := w.db.Lock(c, actorIRI) + if err != nil { + return err + } + // WARNING: Unlock not deferred. + following, err := w.db.Following(c, actorIRI) + if err != nil { + unlock() + return err + } + items := following.GetActivityStreamsItems() + if items == nil { + items = streams.NewActivityStreamsItemsProperty() + following.SetActivityStreamsItems(items) + } + for iter := activityActors.Begin(); iter != activityActors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + unlock() + return err + } + items.PrependIRI(id) + } + err = w.db.Update(c, following) + unlock() // unlock after regardless + if err != nil { + return err + } + // Unlock must be called by now and every branch above. + } + } + if w.Accept != nil { + return w.Accept(c, a) + } + return nil +} + +// reject implements the federating Reject activity side effects. +func (w FederatingWrappedCallbacks) reject(c context.Context, a vocab.ActivityStreamsReject) error { + if w.Reject != nil { + return w.Reject(c, a) + } + return nil +} + +// add implements the federating Add activity side effects. +func (w FederatingWrappedCallbacks) add(c context.Context, a vocab.ActivityStreamsAdd) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + target := a.GetActivityStreamsTarget() + if target == nil || target.Len() == 0 { + return ErrTargetRequired + } + if err := add(c, op, target, w.db); err != nil { + return err + } + if w.Add != nil { + return w.Add(c, a) + } + return nil +} + +// remove implements the federating Remove activity side effects. +func (w FederatingWrappedCallbacks) remove(c context.Context, a vocab.ActivityStreamsRemove) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + target := a.GetActivityStreamsTarget() + if target == nil || target.Len() == 0 { + return ErrTargetRequired + } + if err := remove(c, op, target, w.db); err != nil { + return err + } + if w.Remove != nil { + return w.Remove(c, a) + } + return nil +} + +// like implements the federating Like activity side effects. +func (w FederatingWrappedCallbacks) like(c context.Context, a vocab.ActivityStreamsLike) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + id, err := GetId(a) + if err != nil { + return err + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error { + objId, err := ToId(iter) + if err != nil { + return err + } + unlock, err := w.db.Lock(c, objId) + if err != nil { + return err + } + defer unlock() + if owns, err := w.db.Owns(c, objId); err != nil { + return err + } else if !owns { + return nil + } + t, err := w.db.Get(c, objId) + if err != nil { + return err + } + l, ok := t.(likeser) + if !ok { + return fmt.Errorf("cannot add Like to likes collection for type %T", t) + } + // Get 'likes' property on the object, creating default if + // necessary. + likes := l.GetActivityStreamsLikes() + if likes == nil { + likes = streams.NewActivityStreamsLikesProperty() + l.SetActivityStreamsLikes(likes) + } + // Get 'likes' value, defaulting to a collection. + likesT := likes.GetType() + if likesT == nil { + col := streams.NewActivityStreamsCollection() + likesT = col + likes.SetActivityStreamsCollection(col) + } + // Prepend the activity's 'id' on the 'likes' Collection or + // OrderedCollection. + if col, ok := likesT.(itemser); ok { + items := col.GetActivityStreamsItems() + if items == nil { + items = streams.NewActivityStreamsItemsProperty() + col.SetActivityStreamsItems(items) + } + items.PrependIRI(id) + } else if oCol, ok := likesT.(orderedItemser); ok { + oItems := oCol.GetActivityStreamsOrderedItems() + if oItems == nil { + oItems = streams.NewActivityStreamsOrderedItemsProperty() + oCol.SetActivityStreamsOrderedItems(oItems) + } + oItems.PrependIRI(id) + } else { + return fmt.Errorf("likes type is neither a Collection nor an OrderedCollection: %T", likesT) + } + err = w.db.Update(c, t) + if err != nil { + return err + } + return nil + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + if err := loopFn(iter); err != nil { + return err + } + } + if w.Like != nil { + return w.Like(c, a) + } + return nil +} + +// announce implements the federating Announce activity side effects. +func (w FederatingWrappedCallbacks) announce(c context.Context, a vocab.ActivityStreamsAnnounce) error { + id, err := GetId(a) + if err != nil { + return err + } + op := a.GetActivityStreamsObject() + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(iter vocab.ActivityStreamsObjectPropertyIterator) error { + objId, err := ToId(iter) + if err != nil { + return err + } + unlock, err := w.db.Lock(c, objId) + if err != nil { + return err + } + defer unlock() + if owns, err := w.db.Owns(c, objId); err != nil { + return err + } else if !owns { + return nil + } + t, err := w.db.Get(c, objId) + if err != nil { + return err + } + s, ok := t.(shareser) + if !ok { + return fmt.Errorf("cannot add Announce to Shares collection for type %T", t) + } + // Get 'shares' property on the object, creating default if + // necessary. + shares := s.GetActivityStreamsShares() + if shares == nil { + shares = streams.NewActivityStreamsSharesProperty() + s.SetActivityStreamsShares(shares) + } + // Get 'shares' value, defaulting to a collection. + sharesT := shares.GetType() + if sharesT == nil { + col := streams.NewActivityStreamsCollection() + sharesT = col + shares.SetActivityStreamsCollection(col) + } + // Prepend the activity's 'id' on the 'shares' Collection or + // OrderedCollection. + if col, ok := sharesT.(itemser); ok { + items := col.GetActivityStreamsItems() + if items == nil { + items = streams.NewActivityStreamsItemsProperty() + col.SetActivityStreamsItems(items) + } + items.PrependIRI(id) + } else if oCol, ok := sharesT.(orderedItemser); ok { + oItems := oCol.GetActivityStreamsOrderedItems() + if oItems == nil { + oItems = streams.NewActivityStreamsOrderedItemsProperty() + oCol.SetActivityStreamsOrderedItems(oItems) + } + oItems.PrependIRI(id) + } else { + return fmt.Errorf("shares type is neither a Collection nor an OrderedCollection: %T", sharesT) + } + err = w.db.Update(c, t) + if err != nil { + return err + } + return nil + } + if op != nil { + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + if err := loopFn(iter); err != nil { + return err + } + } + } + if w.Announce != nil { + return w.Announce(c, a) + } + return nil +} + +// undo implements the federating Undo activity side effects. +func (w FederatingWrappedCallbacks) undo(c context.Context, a vocab.ActivityStreamsUndo) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + actors := a.GetActivityStreamsActor() + if err := mustHaveActivityActorsMatchObjectActors(c, actors, op, w.newTransport, w.inboxIRI); err != nil { + return err + } + if w.Undo != nil { + return w.Undo(c, a) + } + return nil +} + +// block implements the federating Block activity side effects. +func (w FederatingWrappedCallbacks) block(c context.Context, a vocab.ActivityStreamsBlock) error { + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + if w.Block != nil { + return w.Block(c, a) + } + return nil +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/handlers.go b/vendor/code.superseriousbusiness.org/activity/pub/handlers.go new file mode 100644 index 000000000..53c44c6fc --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/handlers.go @@ -0,0 +1,115 @@ +package pub + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "code.superseriousbusiness.org/activity/streams" +) + +var ErrNotFound = errors.New("go-fed/activity: ActivityStreams data not found") + +// HandlerFunc determines whether an incoming HTTP request is an ActivityStreams +// GET request, and if so attempts to serve ActivityStreams data. +// +// If an error is returned, then the calling function is responsible for writing +// to the ResponseWriter as part of error handling. +// +// If 'isASRequest' is false and there is no error, then the calling function +// may continue processing the request, and the HandlerFunc will not have +// written anything to the ResponseWriter. For example, a webpage may be served +// instead. +// +// If 'isASRequest' is true and there is no error, then the HandlerFunc +// successfully served the request and wrote to the ResponseWriter. +// +// Callers are responsible for authorized access to this resource. +type HandlerFunc func(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error) + +// NewActivityStreamsHandler creates a HandlerFunc to serve ActivityStreams +// requests which are coming from other clients or servers that wish to obtain +// an ActivityStreams representation of data. +// +// Strips retrieved ActivityStreams values of sensitive fields ('bto' and 'bcc') +// before responding with them. Sets the appropriate HTTP status code for +// Tombstone Activities as well. +// +// Defaults to supporting content to be retrieved by HTTPS only. +func NewActivityStreamsHandler(db Database, clock Clock) HandlerFunc { + return NewActivityStreamsHandlerScheme(db, clock, "https") +} + +// NewActivityStreamsHandlerScheme creates a HandlerFunc to serve +// ActivityStreams requests which are coming from other clients or servers that +// wish to obtain an ActivityStreams representation of data provided by the +// specified protocol scheme. +// +// Strips retrieved ActivityStreams values of sensitive fields ('bto' and 'bcc') +// before responding with them. Sets the appropriate HTTP status code for +// Tombstone Activities as well. +// +// Specifying the "scheme" allows for retrieving ActivityStreams content with +// identifiers such as HTTP, HTTPS, or other protocol schemes. +// +// Returns ErrNotFound when the database does not retrieve any data and no +// errors occurred during retrieval. +func NewActivityStreamsHandlerScheme(db Database, clock Clock, scheme string) HandlerFunc { + return func(c context.Context, w http.ResponseWriter, r *http.Request) (isASRequest bool, err error) { + // Do nothing if it is not an ActivityPub GET request + if !isActivityPubGet(r) { + return + } + isASRequest = true + id := requestId(r, scheme) + + var unlock func() + + // Lock and obtain a copy of the requested ActivityStreams value + unlock, err = db.Lock(c, id) + if err != nil { + return + } + // WARNING: Unlock not deferred + t, err := db.Get(c, id) + unlock() // unlock even on error + if err != nil { + return + } + // Unlock must have been called by this point and in every + // branch above + if t == nil { + err = ErrNotFound + return + } + // Remove sensitive fields. + clearSensitiveFields(t) + // Serialize the fetched value. + m, err := streams.Serialize(t) + if err != nil { + return + } + raw, err := json.Marshal(m) + if err != nil { + return + } + // Construct the response. + addResponseHeaders(w.Header(), clock, raw) + // Write the response. + if streams.IsOrExtendsActivityStreamsTombstone(t) { + w.WriteHeader(http.StatusGone) + } else { + w.WriteHeader(http.StatusOK) + } + n, err := w.Write(raw) + if err != nil { + return + } else if n != len(raw) { + err = fmt.Errorf("only wrote %d of %d bytes", n, len(raw)) + return + } + return + } +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/property_interfaces.go b/vendor/code.superseriousbusiness.org/activity/pub/property_interfaces.go new file mode 100644 index 000000000..3da014c70 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/property_interfaces.go @@ -0,0 +1,118 @@ +package pub + +import ( + "net/url" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// inReplyToer is an ActivityStreams type with an 'inReplyTo' property +type inReplyToer interface { + GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty +} + +// objecter is an ActivityStreams type with an 'object' property +type objecter interface { + GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty +} + +// targeter is an ActivityStreams type with a 'target' property +type targeter interface { + GetActivityStreamsTarget() vocab.ActivityStreamsTargetProperty +} + +// tagger is an ActivityStreams type with a 'tag' property +type tagger interface { + GetActivityStreamsTag() vocab.ActivityStreamsTagProperty +} + +// hrefer is an ActivityStreams type with a 'href' property +type hrefer interface { + GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty +} + +// itemser is an ActivityStreams type with an 'items' property +type itemser interface { + GetActivityStreamsItems() vocab.ActivityStreamsItemsProperty + SetActivityStreamsItems(vocab.ActivityStreamsItemsProperty) +} + +// orderedItemser is an ActivityStreams type with an 'orderedItems' property +type orderedItemser interface { + GetActivityStreamsOrderedItems() vocab.ActivityStreamsOrderedItemsProperty + SetActivityStreamsOrderedItems(vocab.ActivityStreamsOrderedItemsProperty) +} + +// publisheder is an ActivityStreams type with a 'published' property +type publisheder interface { + GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty +} + +// updateder is an ActivityStreams type with an 'updateder' property +type updateder interface { + GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty +} + +// toer is an ActivityStreams type with a 'to' property +type toer interface { + GetActivityStreamsTo() vocab.ActivityStreamsToProperty + SetActivityStreamsTo(i vocab.ActivityStreamsToProperty) +} + +// btoer is an ActivityStreams type with a 'bto' property +type btoer interface { + GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty + SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty) +} + +// ccer is an ActivityStreams type with a 'cc' property +type ccer interface { + GetActivityStreamsCc() vocab.ActivityStreamsCcProperty + SetActivityStreamsCc(i vocab.ActivityStreamsCcProperty) +} + +// bccer is an ActivityStreams type with a 'bcc' property +type bccer interface { + GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty + SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty) +} + +// audiencer is an ActivityStreams type with an 'audience' property +type audiencer interface { + GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty + SetActivityStreamsAudience(i vocab.ActivityStreamsAudienceProperty) +} + +// inboxer is an ActivityStreams type with an 'inbox' property +type inboxer interface { + GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty +} + +// attributedToer is an ActivityStreams type with an 'attributedTo' property +type attributedToer interface { + GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty + SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty) +} + +// likeser is an ActivityStreams type with a 'likes' property +type likeser interface { + GetActivityStreamsLikes() vocab.ActivityStreamsLikesProperty + SetActivityStreamsLikes(i vocab.ActivityStreamsLikesProperty) +} + +// shareser is an ActivityStreams type with a 'shares' property +type shareser interface { + GetActivityStreamsShares() vocab.ActivityStreamsSharesProperty + SetActivityStreamsShares(i vocab.ActivityStreamsSharesProperty) +} + +// actorer is an ActivityStreams type with an 'actor' property +type actorer interface { + GetActivityStreamsActor() vocab.ActivityStreamsActorProperty + SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty) +} + +// appendIRIer is an ActivityStreams type that can Append IRIs. +type appendIRIer interface { + AppendIRI(v *url.URL) +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/side_effect_actor.go b/vendor/code.superseriousbusiness.org/activity/pub/side_effect_actor.go new file mode 100644 index 000000000..820dca403 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/side_effect_actor.go @@ -0,0 +1,1047 @@ +package pub + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "code.superseriousbusiness.org/activity/streams" + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// sideEffectActor must satisfy the DelegateActor interface. +var _ DelegateActor = &SideEffectActor{} + +// SideEffectActor is a DelegateActor that handles the ActivityPub +// implementation side effects, but requires a more opinionated application to +// be written. +// +// Note that when using the SideEffectActor with an application that good-faith +// implements its required interfaces, the ActivityPub specification is +// guaranteed to be correctly followed. +type SideEffectActor struct { + // When doing deliveries to remote servers via the s2s protocol, the side effect + // actor will by default use the Serialize function from the streams package. + // However, this can be overridden after the side effect actor is intantiated, + // by setting the exposed Serialize function on the struct. For example: + // + // a := NewSideEffectActor(...) + // a.Serialize = func(a vocab.Type) (m map[string]interface{}, e error) { + // // Put your custom serializer logic here. + // } + // + // Note that you should only do this *immediately* after instantiating the side + // effect actor -- never while your application is already running, as this will + // likely cause race conditions or other problems! In most cases, you will never + // need to change this; it's provided solely to allow easier customization by + // applications. + Serialize func(a vocab.Type) (m map[string]interface{}, e error) + + // When doing deliveries to remote servers via the s2s protocol, it may be desirable + // for implementations to be able to pre-sort recipients so that higher-priority + // recipients are higher up in the delivery queue, and lower-priority recipients + // are further down. This can be achieved by setting the DeliveryRecipientPreSort + // function on the side effect actor after it's instantiated. For example: + // + // a := NewSideEffectActor(...) + // a.DeliveryRecipientPreSort = func(actorAndCollectionIRIs []*url.URL) []*url.URL { + // // Put your sorting logic here. + // } + // + // The actorAndCollectionIRIs parameter will be the initial list of IRIs derived by + // looking at the "to", "cc", "bto", "bcc", and "audience" properties of the activity + // being delivered, excluding the AP public IRI, and before dereferencing of inboxes. + // It may look something like this: + // + // [ + // "https://example.org/users/someone/followers", // <-- collection IRI + // "https://another.example.org/users/someone_else", // <-- actor IRI + // "[...]" // <-- etc + // ] + // + // In this case, implementers may wish to sort the slice so that the directly-addressed + // actor "https://another.example.org/users/someone_else" occurs at an earlier index in + // the slice than the followers collection "https://example.org/users/someone/followers", + // so that "@someone_else" receives the delivery first. + // + // Note that you should only do this *immediately* after instantiating the side + // effect actor -- never while your application is already running, as this will + // likely cause race conditions or other problems! It's also completely fine to not + // set this function at all -- in this case, no pre-sorting of recipients will be + // performed, and delivery will occur in a non-determinate order. + DeliveryRecipientPreSort func(actorAndCollectionIRIs []*url.URL) []*url.URL + + common CommonBehavior + s2s FederatingProtocol + c2s SocialProtocol + db Database + clock Clock +} + +// NewSideEffectActor returns a new SideEffectActor, which satisfies the +// DelegateActor interface. Most of the time you will not need to call this +// function, and should instead rely on the NewSocialActor, NewFederatingActor, +// and NewActor functions, all of which use a SideEffectActor under the hood. +// Nevertheless, this function is exposed in case application developers need +// a SideEffectActor for some other reason (tests, monkey patches, etc). +// +// If you are using the returned SideEffectActor for federation, ensure that s2s +// is not nil. Likewise, if you are using it for the social protocol, ensure +// that c2s is not nil. +func NewSideEffectActor(c CommonBehavior, + s2s FederatingProtocol, + c2s SocialProtocol, + db Database, + clock Clock) *SideEffectActor { + return &SideEffectActor{ + Serialize: streams.Serialize, + common: c, + s2s: s2s, + c2s: c2s, + db: db, + clock: clock, + } +} + +// PostInboxRequestBodyHook defers to the delegate. +func (a *SideEffectActor) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity Activity) (context.Context, error) { + return a.s2s.PostInboxRequestBodyHook(c, r, activity) +} + +// PostOutboxRequestBodyHook defers to the delegate. +func (a *SideEffectActor) PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) { + return a.c2s.PostOutboxRequestBodyHook(c, r, data) +} + +// AuthenticatePostInbox defers to the delegate to authenticate the request. +func (a *SideEffectActor) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { + return a.s2s.AuthenticatePostInbox(c, w, r) +} + +// AuthenticateGetInbox defers to the delegate to authenticate the request. +func (a *SideEffectActor) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { + return a.common.AuthenticateGetInbox(c, w, r) +} + +// AuthenticatePostOutbox defers to the delegate to authenticate the request. +func (a *SideEffectActor) AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { + return a.c2s.AuthenticatePostOutbox(c, w, r) +} + +// AuthenticateGetOutbox defers to the delegate to authenticate the request. +func (a *SideEffectActor) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) { + return a.common.AuthenticateGetOutbox(c, w, r) +} + +// GetOutbox delegates to the SocialProtocol. +func (a *SideEffectActor) GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { + return a.common.GetOutbox(c, r) +} + +// GetInbox delegates to the FederatingProtocol. +func (a *SideEffectActor) GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { + return a.s2s.GetInbox(c, r) +} + +// AuthorizePostInbox defers to the federating protocol whether the peer request +// is authorized based on the actors' ids. +func (a *SideEffectActor) AuthorizePostInbox(c context.Context, w http.ResponseWriter, activity Activity) (authorized bool, err error) { + authorized = false + actor := activity.GetActivityStreamsActor() + if actor == nil { + err = fmt.Errorf("no actors in post to inbox") + return + } + var iris []*url.URL + for i := 0; i < actor.Len(); i++ { + iter := actor.At(i) + if iter.IsIRI() { + iris = append(iris, iter.GetIRI()) + } else if t := iter.GetType(); t != nil { + iris = append(iris, activity.GetJSONLDId().Get()) + } else { + err = fmt.Errorf("actor at index %d is missing an id", i) + return + } + } + // Determine if the actor(s) sending this request are blocked. + var blocked bool + if blocked, err = a.s2s.Blocked(c, iris); err != nil { + return + } else if blocked { + w.WriteHeader(http.StatusForbidden) + return + } + authorized = true + return +} + +// PostInbox handles the side effects of determining whether to block the peer's +// request, adding the activity to the actor's inbox, and triggering side +// effects based on the activity's type. +func (a *SideEffectActor) PostInbox(c context.Context, inboxIRI *url.URL, activity Activity) error { + isNew, err := a.addToInboxIfNew(c, inboxIRI, activity) + if err != nil { + return err + } + if isNew { + wrapped, other, err := a.s2s.FederatingCallbacks(c) + if err != nil { + return err + } + // Populate side channels. + wrapped.db = a.db + wrapped.inboxIRI = inboxIRI + wrapped.newTransport = a.common.NewTransport + wrapped.deliver = a.Deliver + wrapped.addNewIds = a.AddNewIDs + res, err := streams.NewTypeResolver(wrapped.callbacks(other)...) + if err != nil { + return err + } + if err = res.Resolve(c, activity); err != nil && !streams.IsUnmatchedErr(err) { + return err + } else if streams.IsUnmatchedErr(err) { + err = a.s2s.DefaultCallback(c, activity) + if err != nil { + return err + } + } + } + return nil +} + +// InboxForwarding implements the 3-part inbox forwarding algorithm specified in +// the ActivityPub specification. Does not modify the Activity, but may send +// outbound requests as a side effect. +// +// InboxForwarding sets the federated data in the database. +func (a *SideEffectActor) InboxForwarding(c context.Context, inboxIRI *url.URL, activity Activity) error { + // 1. Must be first time we have seen this Activity. + // + // Obtain the id of the activity + id := activity.GetJSONLDId() + // Acquire a lock for the id. To be held for the rest of execution. + unlock, err := a.db.Lock(c, id.Get()) + if err != nil { + return err + } + // WARNING: Unlock is not deferred + // + // If the database already contains the activity, exit early. + exists, err := a.db.Exists(c, id.Get()) + if err != nil { + unlock() + return err + } else if exists { + unlock() + return nil + } + // Attempt to create the activity entry. + err = a.db.Create(c, activity) + unlock() // unlock even on error return + if err != nil { + return err + } + // Unlock by this point and in every branch above. + // + // 2. The values of 'to', 'cc', or 'audience' are Collections owned by + // this server. + var r []*url.URL + to := activity.GetActivityStreamsTo() + if to != nil { + for iter := to.Begin(); iter != to.End(); iter = iter.Next() { + val, err := ToId(iter) + if err != nil { + return err + } + r = append(r, val) + } + } + cc := activity.GetActivityStreamsCc() + if cc != nil { + for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() { + val, err := ToId(iter) + if err != nil { + return err + } + r = append(r, val) + } + } + audience := activity.GetActivityStreamsAudience() + if audience != nil { + for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() { + val, err := ToId(iter) + if err != nil { + return err + } + r = append(r, val) + } + } + // Find all IRIs owned by this server. We need to find all of them so + // that forwarding can properly occur. + var myIRIs []*url.URL + for _, iri := range r { + if err != nil { + return err + } + var unlock func() + unlock, err = a.db.Lock(c, iri) + if err != nil { + return err + } + // WARNING: Unlock is not deferred + owns, err := a.db.Owns(c, iri) + unlock() // unlock even on error + if err != nil { + return err + } else if !owns { + continue + } + // Unlock by this point and in every branch above. + myIRIs = append(myIRIs, iri) + } + // Finally, load our IRIs to determine if they are a Collection or + // OrderedCollection. + // + // Load the unfiltered IRIs. + var colIRIs []*url.URL + col := make(map[string]itemser) + oCol := make(map[string]orderedItemser) + for _, iri := range myIRIs { + var unlock func() + unlock, err = a.db.Lock(c, iri) + if err != nil { + return err + } + // WARNING: Not Unlocked + t, err := a.db.Get(c, iri) + if err != nil { + return err + } + if streams.IsOrExtendsActivityStreamsOrderedCollection(t) { + if im, ok := t.(orderedItemser); ok { + oCol[iri.String()] = im + colIRIs = append(colIRIs, iri) + defer unlock() + } else { + unlock() // unlock instantly + } + } else if streams.IsOrExtendsActivityStreamsCollection(t) { + if im, ok := t.(itemser); ok { + col[iri.String()] = im + colIRIs = append(colIRIs, iri) + defer unlock() + } else { + unlock() // unlock instantly + } + } else { + unlock() // unlock instantly + } + } + // If we own none of the Collection IRIs in 'to', 'cc', or 'audience' + // then no need to do inbox forwarding. We have nothing to forward to. + if len(colIRIs) == 0 { + return nil + } + // 3. The values of 'inReplyTo', 'object', 'target', or 'tag' are owned + // by this server. This is only a boolean trigger: As soon as we get + // a hit that we own something, then we should do inbox forwarding. + maxDepth := a.s2s.MaxInboxForwardingRecursionDepth(c) + ownsValue, err := a.hasInboxForwardingValues(c, inboxIRI, activity, maxDepth, 0) + if err != nil { + return err + } + // If we don't own any of the 'inReplyTo', 'object', 'target', or 'tag' + // values, then no need to do inbox forwarding. + if !ownsValue { + return nil + } + // Do the inbox forwarding since the above conditions hold true. Support + // the behavior of letting the application filter out the resulting + // collections to be targeted. + toSend, err := a.s2s.FilterForwarding(c, colIRIs, activity) + if err != nil { + return err + } + recipients := make([]*url.URL, 0, len(toSend)) + for _, iri := range toSend { + if c, ok := col[iri.String()]; ok { + if it := c.GetActivityStreamsItems(); it != nil { + for iter := it.Begin(); iter != it.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + recipients = append(recipients, id) + } + } + } else if oc, ok := oCol[iri.String()]; ok { + if oit := oc.GetActivityStreamsOrderedItems(); oit != nil { + for iter := oit.Begin(); iter != oit.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + recipients = append(recipients, id) + } + } + } + } + return a.deliverToRecipients(c, inboxIRI, activity, recipients) +} + +// PostOutbox handles the side effects of adding the activity to the actor's +// outbox, and triggering side effects based on the activity's type. +// +// This implementation assumes all types are meant to be delivered except for +// the ActivityStreams Block type. +func (a *SideEffectActor) PostOutbox(c context.Context, activity Activity, outboxIRI *url.URL, rawJSON map[string]interface{}) (deliverable bool, err error) { + // TODO: Determine this if c2s is nil + deliverable = true + if a.c2s != nil { + var wrapped SocialWrappedCallbacks + var other []interface{} + wrapped, other, err = a.c2s.SocialCallbacks(c) + if err != nil { + return + } + // Populate side channels. + wrapped.db = a.db + wrapped.outboxIRI = outboxIRI + wrapped.rawActivity = rawJSON + wrapped.clock = a.clock + wrapped.newTransport = a.common.NewTransport + undeliverable := false + wrapped.undeliverable = &undeliverable + var res *streams.TypeResolver + res, err = streams.NewTypeResolver(wrapped.callbacks(other)...) + if err != nil { + return + } + if err = res.Resolve(c, activity); err != nil && !streams.IsUnmatchedErr(err) { + return + } else if streams.IsUnmatchedErr(err) { + deliverable = true + err = a.c2s.DefaultCallback(c, activity) + if err != nil { + return + } + } else { + deliverable = !undeliverable + } + } + err = a.addToOutbox(c, outboxIRI, activity) + return +} + +// AddNewIDs creates new 'id' entries on an activity and its objects if it is a +// Create activity. +func (a *SideEffectActor) AddNewIDs(c context.Context, activity Activity) error { + id, err := a.db.NewID(c, activity) + if err != nil { + return err + } + activityId := streams.NewJSONLDIdProperty() + activityId.Set(id) + activity.SetJSONLDId(activityId) + if streams.IsOrExtendsActivityStreamsCreate(activity) { + o, ok := activity.(objecter) + if !ok { + return fmt.Errorf("cannot add new id for Create: %T has no object property", activity) + } + if oProp := o.GetActivityStreamsObject(); oProp != nil { + for iter := oProp.Begin(); iter != oProp.End(); iter = iter.Next() { + t := iter.GetType() + if t == nil { + return fmt.Errorf("cannot add new id for object in Create: object is not embedded as a value literal") + } + id, err = a.db.NewID(c, t) + if err != nil { + return err + } + objId := streams.NewJSONLDIdProperty() + objId.Set(id) + t.SetJSONLDId(objId) + } + } + } + return nil +} + +// deliver will complete the peer-to-peer sending of a federated message to +// another server. +// +// Must be called if at least the federated protocol is supported. +func (a *SideEffectActor) Deliver(c context.Context, outboxIRI *url.URL, activity Activity) error { + recipients, err := a.prepare(c, outboxIRI, activity) + if err != nil { + return err + } + return a.deliverToRecipients(c, outboxIRI, activity, recipients) +} + +// WrapInCreate wraps an object with a Create activity. +func (a *SideEffectActor) WrapInCreate(c context.Context, obj vocab.Type, outboxIRI *url.URL) (create vocab.ActivityStreamsCreate, err error) { + var unlock func() + unlock, err = a.db.Lock(c, outboxIRI) + if err != nil { + return + } + // WARNING: No deferring the Unlock + actorIRI, err := a.db.ActorForOutbox(c, outboxIRI) + unlock() // unlock after regardless + if err != nil { + return + } + // Unlock the lock at this point and every branch above + return wrapInCreate(c, obj, actorIRI) +} + +// deliverToRecipients will take a prepared Activity and send it to specific +// recipients on behalf of an actor. +func (a *SideEffectActor) deliverToRecipients(c context.Context, boxIRI *url.URL, activity Activity, recipients []*url.URL) error { + // Call whichever serializer is + // set on the side effect actor. + m, err := a.Serialize(activity) + if err != nil { + return err + } + + tp, err := a.common.NewTransport(c, boxIRI, goFedUserAgent()) + if err != nil { + return err + } + + return tp.BatchDeliver(c, m, recipients) +} + +// addToOutbox adds the activity to the outbox and creates the activity in the +// internal database as its own entry. +func (a *SideEffectActor) addToOutbox(c context.Context, outboxIRI *url.URL, activity Activity) error { + // Set the activity in the database first. + id := activity.GetJSONLDId() + unlock, err := a.db.Lock(c, id.Get()) + if err != nil { + return err + } + // WARNING: Unlock not deferred + err = a.db.Create(c, activity) + unlock() // unlock after regardless + if err != nil { + return err + } + // WARNING: Unlock(c, id) should be called by this point and in every + // return before here. + // + // Acquire a lock to read the outbox. Defer release. + unlock, err = a.db.Lock(c, outboxIRI) + if err != nil { + return err + } + defer unlock() + outbox, err := a.db.GetOutbox(c, outboxIRI) + if err != nil { + return err + } + // Prepend the activity to the list of 'orderedItems'. + oi := outbox.GetActivityStreamsOrderedItems() + if oi == nil { + oi = streams.NewActivityStreamsOrderedItemsProperty() + } + oi.PrependIRI(id.Get()) + outbox.SetActivityStreamsOrderedItems(oi) + // Save in the database. + err = a.db.SetOutbox(c, outbox) + return err +} + +// addToInboxIfNew will add the activity to the inbox at the specified IRI if +// the activity's ID has not yet been added to the inbox. +// +// It does not add the activity to this database's know federated data. +// +// Returns true when the activity is novel. +func (a *SideEffectActor) addToInboxIfNew(c context.Context, inboxIRI *url.URL, activity Activity) (isNew bool, err error) { + // Acquire a lock to read the inbox. Defer release. + var unlock func() + unlock, err = a.db.Lock(c, inboxIRI) + if err != nil { + return + } + defer unlock() + // Obtain the id of the activity + id := activity.GetJSONLDId() + // If the inbox already contains the URL, early exit. + contains, err := a.db.InboxContains(c, inboxIRI, id.Get()) + if err != nil { + return + } else if contains { + return + } + // It is a new id, acquire the inbox. + isNew = true + inbox, err := a.db.GetInbox(c, inboxIRI) + if err != nil { + return + } + // Prepend the activity to the list of 'orderedItems'. + oi := inbox.GetActivityStreamsOrderedItems() + if oi == nil { + oi = streams.NewActivityStreamsOrderedItemsProperty() + } + oi.PrependIRI(id.Get()) + inbox.SetActivityStreamsOrderedItems(oi) + // Save in the database. + err = a.db.SetInbox(c, inbox) + return +} + +// Given an ActivityStreams value, recursively examines ownership of the id or +// href and the ones on properties applicable to inbox forwarding. +// +// Recursion may be limited by providing a 'maxDepth' greater than zero. A +// value of zero or a negative number will result in infinite recursion. +func (a *SideEffectActor) hasInboxForwardingValues(c context.Context, inboxIRI *url.URL, val vocab.Type, maxDepth, currDepth int) (bool, error) { + // Stop recurring if we are exceeding the maximum depth and the maximum + // is a positive number. + if maxDepth > 0 && currDepth >= maxDepth { + return false, nil + } + // Determine if we own the 'id' of any values on the properties we care + // about. + types, iris := getInboxForwardingValues(val) + // For IRIs, simply check if we own them. + for _, iri := range iris { + unlock, err := a.db.Lock(c, iri) + if err != nil { + return false, err + } + // WARNING: Unlock is not deferred + owns, err := a.db.Owns(c, iri) + unlock() // unlock after regardless + if err != nil { + return false, err + } else if owns { + return true, nil + } + // Unlock by this point and in every branch above + } + // For embedded literals, check the id. + for _, val := range types { + id, err := GetId(val) + if err != nil { + return false, err + } + var unlock func() + unlock, err = a.db.Lock(c, id) + if err != nil { + return false, err + } + // WARNING: Unlock is not deferred + owns, err := a.db.Owns(c, id) + unlock() // unlock after regardless + if err != nil { + return false, err + } else if owns { + return true, nil + } + // Unlock by this point and in every branch above + } + // Recur Preparation: Try fetching the IRIs so we can recur into them. + for _, iri := range iris { + // Dereferencing the IRI. + tport, err := a.common.NewTransport(c, inboxIRI, goFedUserAgent()) + if err != nil { + return false, err + } + resp, err := tport.Dereference(c, iri) + if err != nil { + // Do not fail the entire process if the data is + // missing. + continue + } + m, err := readActivityPubResp(resp) + if err != nil { + return false, err + } + t, err := streams.ToType(c, m) + if err != nil { + // Do not fail the entire process if we cannot handle + // the type. + continue + } + types = append(types, t) + } + // Recur. + for _, nextVal := range types { + if has, err := a.hasInboxForwardingValues(c, inboxIRI, nextVal, maxDepth, currDepth+1); err != nil { + return false, err + } else if has { + return true, nil + } + } + return false, nil +} + +// prepare takes a deliverableObject and returns a list of the final +// recipient inbox IRIs. Additionally, the deliverableObject will have +// any hidden hidden recipients ("bto" and "bcc") stripped from it. +// +// Only call if both the social and federated protocol are supported. +func (a *SideEffectActor) prepare( + ctx context.Context, + outboxIRI *url.URL, + activity Activity, +) ([]*url.URL, error) { + // Iterate through to, bto, cc, bcc, and audience + // to extract a slice of addressee IRIs / IDs. + // + // The resulting slice might look something like: + // + // [ + // "https://example.org/users/someone/followers", // <-- collection IRI + // "https://another.example.org/users/someone_else", // <-- actor IRI + // "[...]" // <-- etc + // ] + var actorsAndCollections []*url.URL + if to := activity.GetActivityStreamsTo(); to != nil { + for iter := to.Begin(); iter != to.End(); iter = iter.Next() { + var err error + actorsAndCollections, err = appendToActorsAndCollectionsIRIs( + iter, actorsAndCollections, + ) + if err != nil { + return nil, err + } + } + } + + if bto := activity.GetActivityStreamsBto(); bto != nil { + for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() { + var err error + actorsAndCollections, err = appendToActorsAndCollectionsIRIs( + iter, actorsAndCollections, + ) + if err != nil { + return nil, err + } + } + } + + if cc := activity.GetActivityStreamsCc(); cc != nil { + for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() { + var err error + actorsAndCollections, err = appendToActorsAndCollectionsIRIs( + iter, actorsAndCollections, + ) + if err != nil { + return nil, err + } + } + } + + if bcc := activity.GetActivityStreamsBcc(); bcc != nil { + for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() { + var err error + actorsAndCollections, err = appendToActorsAndCollectionsIRIs( + iter, actorsAndCollections, + ) + if err != nil { + return nil, err + } + } + } + + if audience := activity.GetActivityStreamsAudience(); audience != nil { + for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() { + var err error + actorsAndCollections, err = appendToActorsAndCollectionsIRIs( + iter, actorsAndCollections, + ) + if err != nil { + return nil, err + } + } + } + + // PRE-SORTING + + // If the pre-delivery sort function is defined, + // call it now so that implementations can sort + // delivery order to their preferences. + if a.DeliveryRecipientPreSort != nil { + actorsAndCollections = a.DeliveryRecipientPreSort(actorsAndCollections) + } + + // We now need to dereference the actor or collection + // IRIs to derive inboxes that we can POST requests to. + var ( + inboxes = make([]*url.URL, 0, len(actorsAndCollections)) + derefdEntries = make(map[string]struct{}, len(actorsAndCollections)) + ) + + // First check if the implemented database logic + // can return any of these inboxes without having + // to make remote dereference calls (much cheaper). + for _, actorOrCollection := range actorsAndCollections { + actorOrCollectionStr := actorOrCollection.String() + if _, derefd := derefdEntries[actorOrCollectionStr]; derefd { + // Ignore potential duplicates + // we've already derefd to inbox(es). + continue + } + + // BEGIN LOCK + unlock, err := a.db.Lock(ctx, actorOrCollection) + if err != nil { + return nil, err + } + + // Try to get inbox(es) for this actor or collection. + gotInboxes, err := a.db.InboxesForIRI(ctx, actorOrCollection) + + // END LOCK + unlock() + + if err != nil { + return nil, err + } + + if len(gotInboxes) == 0 { + // No hit(s). + continue + } + + // We have one or more hits. + inboxes = append(inboxes, gotInboxes...) + + // Mark this actor or collection as deref'd. + derefdEntries[actorOrCollectionStr] = struct{}{} + } + + // Now look for any remaining actors/collections + // that weren't already dereferenced into inboxes + // with db calls; find these by making deref calls + // to remote instances. + // + // First get a transport to do the http calls. + t, err := a.common.NewTransport(ctx, outboxIRI, goFedUserAgent()) + if err != nil { + return nil, err + } + + // Make HTTP calls to unpack collection IRIs into + // Actor IRIs and then into Actor types, ignoring + // actors or collections we've already deref'd. + actorsFromRemote, err := a.resolveActors( + ctx, + t, + actorsAndCollections, + derefdEntries, + 0, a.s2s.MaxDeliveryRecursionDepth(ctx), + ) + if err != nil { + return nil, err + } + + // Release no-longer-needed collections. + clear(derefdEntries) + clear(actorsAndCollections) + + // Extract inbox IRI from each deref'd Actor (if any). + inboxesFromRemote, err := actorsToInboxIRIs(actorsFromRemote) + if err != nil { + return nil, err + } + + // Combine db-discovered inboxes and remote-discovered + // inboxes into a final list of destination inboxes. + inboxes = append(inboxes, inboxesFromRemote...) + + // POST FILTERING + + // Do a final pass of the inboxes to: + // + // 1. Deduplicate entries. + // 2. Ensure that the list of inboxes doesn't + // contain the inbox of whoever the outbox + // belongs to, no point delivering to oneself. + // + // To do this we first need to get the + // inbox IRI of this outbox's Actor. + + // BEGIN LOCK + unlock, err := a.db.Lock(ctx, outboxIRI) + if err != nil { + return nil, err + } + + // Get the IRI of the Actor who owns this outbox. + outboxActorIRI, err := a.db.ActorForOutbox(ctx, outboxIRI) + + // END LOCK + unlock() + + if err != nil { + return nil, err + } + + // BEGIN LOCK + unlock, err = a.db.Lock(ctx, outboxActorIRI) + if err != nil { + return nil, err + } + + // Now get the Actor who owns this outbox. + outboxActor, err := a.db.Get(ctx, outboxActorIRI) + + // END LOCK + unlock() + + if err != nil { + return nil, err + } + + // Extract the inbox IRI for the outbox Actor. + inboxOfOutboxActor, err := getInbox(outboxActor) + if err != nil { + return nil, err + } + + // Deduplicate the final inboxes slice, and filter + // out of the inbox of this outbox actor (if present). + inboxes = filterInboxIRIs(inboxes, inboxOfOutboxActor) + + // Now that we've derived inboxes to deliver + // the activity to, strip off any bto or bcc + // recipients, as per the AP spec requirements. + stripHiddenRecipients(activity) + + // All done! + return inboxes, nil +} + +// resolveActors takes a list of Actor id URIs and returns them as concrete +// instances of actorObject. It attempts to apply recursively when it encounters +// a target that is a Collection or OrderedCollection. +// +// Any IRI strings in the ignores map will be skipped (use this when +// you've already dereferenced some of the actorAndCollectionIRIs). +// +// If maxDepth is zero or negative, then recursion is infinitely applied. +// +// If a recipient is a Collection or OrderedCollection, then the server MUST +// dereference the collection, WITH the user's credentials. +// +// Note that this also applies to CollectionPage and OrderedCollectionPage. +func (a *SideEffectActor) resolveActors( + ctx context.Context, + t Transport, + actorAndCollectionIRIs []*url.URL, + ignores map[string]struct{}, + depth, maxDepth int, +) ([]vocab.Type, error) { + if maxDepth > 0 && depth >= maxDepth { + // Hit our max depth. + return nil, nil + } + + if len(actorAndCollectionIRIs) == 0 { + // Nothing to do. + return nil, nil + } + + // Optimistically assume 1:1 mapping of IRIs to actors. + actors := make([]vocab.Type, 0, len(actorAndCollectionIRIs)) + + // Deref each actorOrCollectionIRI if not ignored. + for _, actorOrCollectionIRI := range actorAndCollectionIRIs { + _, ignore := ignores[actorOrCollectionIRI.String()] + if ignore { + // Don't try to + // deref this one. + continue + } + + // TODO: Determine if more logic is needed here for + // inaccessible collections owned by peer servers. + actor, more, err := a.dereferenceForResolvingInboxes(ctx, t, actorOrCollectionIRI) + if err != nil { + // Missing recipient -- skip. + continue + } + + if actor != nil { + // Got a hit. + actors = append(actors, actor) + } + + // If this was a collection, get more. + recurActors, err := a.resolveActors( + ctx, + t, + more, + ignores, + depth+1, maxDepth, + ) + if err != nil { + return nil, err + } + + actors = append(actors, recurActors...) + } + + return actors, nil +} + +// dereferenceForResolvingInboxes dereferences an IRI solely for finding an +// actor's inbox IRI to deliver to. +// +// The returned actor could be nil, if it wasn't an actor (ex: a Collection or +// OrderedCollection). +func (a *SideEffectActor) dereferenceForResolvingInboxes(c context.Context, t Transport, actorIRI *url.URL) (actor vocab.Type, moreActorIRIs []*url.URL, err error) { + var resp *http.Response + resp, err = t.Dereference(c, actorIRI) + if err != nil { + return + } + var m map[string]interface{} + m, err = readActivityPubResp(resp) + if err != nil { + return + } + actor, err = streams.ToType(c, m) + if err != nil { + return + } + // Attempt to see if the 'actor' is really some sort of type that has + // an 'items' or 'orderedItems' property. + if v, ok := actor.(itemser); ok { + if i := v.GetActivityStreamsItems(); i != nil { + for iter := i.Begin(); iter != i.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + moreActorIRIs = append(moreActorIRIs, id) + } + } + actor = nil + } else if v, ok := actor.(orderedItemser); ok { + if i := v.GetActivityStreamsOrderedItems(); i != nil { + for iter := i.Begin(); iter != i.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + moreActorIRIs = append(moreActorIRIs, id) + } + } + actor = nil + } + return +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/social_protocol.go b/vendor/code.superseriousbusiness.org/activity/pub/social_protocol.go new file mode 100644 index 000000000..515947d15 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/social_protocol.go @@ -0,0 +1,83 @@ +package pub + +import ( + "context" + "net/http" + + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// SocialProtocol contains behaviors an application needs to satisfy for the +// full ActivityPub C2S implementation to be supported by this library. +// +// It is only required if the client application wants to support the client-to- +// server, or social, protocol. +// +// It is passed to the library as a dependency injection from the client +// application. +type SocialProtocol interface { + // Hook callback after parsing the request body for a client request + // to the Actor's outbox. + // + // Can be used to set contextual information based on the + // ActivityStreams object received. + // + // Only called if the Social API is enabled. + // + // Warning: Neither authentication nor authorization has taken place at + // this time. Doing anything beyond setting contextual information is + // strongly discouraged. + // + // If an error is returned, it is passed back to the caller of + // PostOutbox. In this case, the DelegateActor implementation must not + // write a response to the ResponseWriter as is expected that the caller + // to PostOutbox will do so when handling the error. + PostOutboxRequestBodyHook(c context.Context, r *http.Request, data vocab.Type) (context.Context, error) + // AuthenticatePostOutbox delegates the authentication of a POST to an + // outbox. + // + // Only called if the Social API is enabled. + // + // If an error is returned, it is passed back to the caller of + // PostOutbox. 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. + AuthenticatePostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) + // SocialCallbacks returns the application logic that handles + // ActivityStreams received from C2S clients. + // + // Note that certain types of callbacks will be 'wrapped' with default + // behaviors supported natively by the library. Other callbacks + // compatible with streams.TypeResolver can be specified by 'other'. + // + // For example, setting the 'Create' field in the SocialWrappedCallbacks + // lets an application dependency inject additional behaviors they want + // to take place, including the default behavior supplied by this + // library. This is guaranteed to be compliant with the ActivityPub + // Social protocol. + // + // To override the default behavior, instead supply the function in + // 'other', which does not guarantee the application will be compliant + // with the ActivityPub Social Protocol. + // + // Applications are not expected to handle every single ActivityStreams + // type and extension. The unhandled ones are passed to DefaultCallback. + SocialCallbacks(c context.Context) (wrapped SocialWrappedCallbacks, other []interface{}, err error) + // DefaultCallback is called for types that go-fed can deserialize but + // are not handled by the application's callbacks returned in the + // Callbacks method. + // + // Applications are not expected to handle every single ActivityStreams + // type and extension, so the unhandled ones are passed to + // DefaultCallback. + DefaultCallback(c context.Context, activity Activity) error +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/social_wrapped_callbacks.go b/vendor/code.superseriousbusiness.org/activity/pub/social_wrapped_callbacks.go new file mode 100644 index 000000000..d1c53fcaf --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/social_wrapped_callbacks.go @@ -0,0 +1,534 @@ +package pub + +import ( + "context" + "fmt" + "net/url" + + "code.superseriousbusiness.org/activity/streams" + "code.superseriousbusiness.org/activity/streams/vocab" +) + +// SocialWrappedCallbacks lists the callback functions that already have some +// side effect behavior provided by the pub library. +// +// These functions are wrapped for the Social Protocol. +type SocialWrappedCallbacks struct { + // Create handles additional side effects for the Create ActivityStreams + // type. + // + // The wrapping callback copies the actor(s) to the 'attributedTo' + // property and copies recipients between the Create activity and all + // objects. It then saves the entry in the database. + Create func(context.Context, vocab.ActivityStreamsCreate) error + // Update handles additional side effects for the Update ActivityStreams + // type. + // + // The wrapping callback applies new top-level values on an object to + // the stored objects. Any top-level null literals will be deleted on + // the stored objects as well. + Update func(context.Context, vocab.ActivityStreamsUpdate) error + // Delete handles additional side effects for the Delete ActivityStreams + // type. + // + // The wrapping callback replaces the object(s) with tombstones in the + // database. + Delete func(context.Context, vocab.ActivityStreamsDelete) error + // Follow handles additional side effects for the Follow ActivityStreams + // type. + // + // The wrapping callback only ensures the 'Follow' has at least one + // 'object' entry, but otherwise has no default side effect. + Follow func(context.Context, vocab.ActivityStreamsFollow) error + // Add handles additional side effects for the Add ActivityStreams + // type. + // + // + // The wrapping function will add the 'object' IRIs to a specific + // 'target' collection if the 'target' collection(s) live on this + // server. + Add func(context.Context, vocab.ActivityStreamsAdd) error + // Remove handles additional side effects for the Remove ActivityStreams + // type. + // + // The wrapping function will remove all 'object' IRIs from a specific + // 'target' collection if the 'target' collection(s) live on this + // server. + Remove func(context.Context, vocab.ActivityStreamsRemove) error + // Like handles additional side effects for the Like ActivityStreams + // type. + // + // The wrapping function will add the objects on the activity to the + // "liked" collection of this actor. + Like func(context.Context, vocab.ActivityStreamsLike) error + // Undo handles additional side effects for the Undo ActivityStreams + // type. + // + // + // The wrapping function ensures the 'actor' on the 'Undo' + // is be the same as the 'actor' on all Activities being undone. + // It enforces that the actors on the Undo must correspond to all of the + // 'object' actors in some manner. + // + // It is expected that the application will implement the proper + // reversal of activities that are being undone. + Undo func(context.Context, vocab.ActivityStreamsUndo) error + // Block handles additional side effects for the Block ActivityStreams + // type. + // + // The wrapping callback only ensures the 'Block' has at least one + // 'object' entry, but otherwise has no default side effect. It is up + // to the wrapped application function to properly enforce the new + // blocking behavior. + // + // Note that go-fed does not federate 'Block' activities received in the + // Social Protocol. + Block func(context.Context, vocab.ActivityStreamsBlock) error + + // Sidechannel data -- this is set at request handling time. These must + // be set before the callbacks are used. + + // db is the Database the SocialWrappedCallbacks should use. It must be + // set before calling the callbacks. + db Database + // outboxIRI is the outboxIRI that is handling this callback. + outboxIRI *url.URL + // rawActivity is the JSON map literal received when deserializing the + // request body. + rawActivity map[string]interface{} + // clock is the server's clock. + clock Clock + // newTransport creates a new Transport. + newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error) + // undeliverable is a sidechannel out, indicating if the handled activity + // should not be delivered to a peer. + // + // Its provided default value will always be used when a custom function + // is called. + undeliverable *bool +} + +// callbacks returns the WrappedCallbacks members into a single interface slice +// for use in streams.Resolver callbacks. +// +// If the given functions have a type that collides with the default behavior, +// then disable our default behavior +func (w SocialWrappedCallbacks) callbacks(fns []interface{}) []interface{} { + enableCreate := true + enableUpdate := true + enableDelete := true + enableFollow := true + enableAdd := true + enableRemove := true + enableLike := true + enableUndo := true + enableBlock := true + for _, fn := range fns { + switch fn.(type) { + default: + continue + case func(context.Context, vocab.ActivityStreamsCreate) error: + enableCreate = false + case func(context.Context, vocab.ActivityStreamsUpdate) error: + enableUpdate = false + case func(context.Context, vocab.ActivityStreamsDelete) error: + enableDelete = false + case func(context.Context, vocab.ActivityStreamsFollow) error: + enableFollow = false + case func(context.Context, vocab.ActivityStreamsAdd) error: + enableAdd = false + case func(context.Context, vocab.ActivityStreamsRemove) error: + enableRemove = false + case func(context.Context, vocab.ActivityStreamsLike) error: + enableLike = false + case func(context.Context, vocab.ActivityStreamsUndo) error: + enableUndo = false + case func(context.Context, vocab.ActivityStreamsBlock) error: + enableBlock = false + } + } + if enableCreate { + fns = append(fns, w.create) + } + if enableUpdate { + fns = append(fns, w.update) + } + if enableDelete { + fns = append(fns, w.deleteFn) + } + if enableFollow { + fns = append(fns, w.follow) + } + if enableAdd { + fns = append(fns, w.add) + } + if enableRemove { + fns = append(fns, w.remove) + } + if enableLike { + fns = append(fns, w.like) + } + if enableUndo { + fns = append(fns, w.undo) + } + if enableBlock { + fns = append(fns, w.block) + } + return fns +} + +// create implements the social Create activity side effects. +func (w SocialWrappedCallbacks) create(c context.Context, a vocab.ActivityStreamsCreate) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + // Obtain all actor IRIs. + actors := a.GetActivityStreamsActor() + createActorIds := make(map[string]*url.URL) + if actors != nil { + createActorIds = make(map[string]*url.URL, actors.Len()) + for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + createActorIds[id.String()] = id + } + } + // Obtain each object's 'attributedTo' IRIs. + objectAttributedToIds := make([]map[string]*url.URL, op.Len()) + for i := range objectAttributedToIds { + objectAttributedToIds[i] = make(map[string]*url.URL) + } + for i := 0; i < op.Len(); i++ { + t := op.At(i).GetType() + attrToer, ok := t.(attributedToer) + if !ok { + continue + } + attr := attrToer.GetActivityStreamsAttributedTo() + if attr == nil { + attr = streams.NewActivityStreamsAttributedToProperty() + attrToer.SetActivityStreamsAttributedTo(attr) + } + for iter := attr.Begin(); iter != attr.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objectAttributedToIds[i][id.String()] = id + } + } + // Put all missing actor IRIs onto all object attributedTo properties. + for k, v := range createActorIds { + for i, attributedToMap := range objectAttributedToIds { + if _, ok := attributedToMap[k]; !ok { + t := op.At(i).GetType() + attrToer, ok := t.(attributedToer) + if !ok { + continue + } + attr := attrToer.GetActivityStreamsAttributedTo() + attr.AppendIRI(v) + } + } + } + // Put all missing object attributedTo IRIs onto the actor property + // if there is one. + if actors != nil { + for _, attributedToMap := range objectAttributedToIds { + for k, v := range attributedToMap { + if _, ok := createActorIds[k]; !ok { + actors.AppendIRI(v) + } + } + } + } + // Copy over the 'to', 'bto', 'cc', 'bcc', and 'audience' recipients + // between the activity and all child objects and vice versa. + if err := normalizeRecipients(a); err != nil { + return err + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(i int) error { + obj := op.At(i).GetType() + id, err := GetId(obj) + if err != nil { + return err + } + var unlock func() + unlock, err = w.db.Lock(c, id) + if err != nil { + return err + } + defer unlock() + if err := w.db.Create(c, obj); err != nil { + return err + } + return nil + } + // Persist all objects we've created, which will include sensitive + // recipients such as 'bcc' and 'bto'. + for i := 0; i < op.Len(); i++ { + if err := loopFn(i); err != nil { + return err + } + } + if w.Create != nil { + return w.Create(c, a) + } + return nil +} + +// update implements the social Update activity side effects. +func (w SocialWrappedCallbacks) update(c context.Context, a vocab.ActivityStreamsUpdate) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + // Obtain all object ids, which should be owned by this server. + objIds := make([]*url.URL, 0, op.Len()) + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objIds = append(objIds, id) + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(idx int, loopId *url.URL) error { + unlock, err := w.db.Lock(c, loopId) + if err != nil { + return err + } + defer unlock() + t, err := w.db.Get(c, loopId) + if err != nil { + return err + } + m, err := t.Serialize() + if err != nil { + return err + } + // Copy over new top-level values. + objType := op.At(idx).GetType() + if objType == nil { + return fmt.Errorf("object at index %d is not a literal type value", idx) + } + newM, err := objType.Serialize() + if err != nil { + return err + } + for k, v := range newM { + m[k] = v + } + // Delete top-level values where the raw Activity had nils. + for k, v := range w.rawActivity { + if _, ok := m[k]; v == nil && ok { + delete(m, k) + } + } + newT, err := streams.ToType(c, m) + if err != nil { + return err + } + if err = w.db.Update(c, newT); err != nil { + return err + } + return nil + } + for i, id := range objIds { + if err := loopFn(i, id); err != nil { + return err + } + } + if w.Update != nil { + return w.Update(c, a) + } + return nil +} + +// deleteFn implements the social Delete activity side effects. +func (w SocialWrappedCallbacks) deleteFn(c context.Context, a vocab.ActivityStreamsDelete) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + // Obtain all object ids, which should be owned by this server. + objIds := make([]*url.URL, 0, op.Len()) + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objIds = append(objIds, id) + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(idx int, loopId *url.URL) error { + unlock, err := w.db.Lock(c, loopId) + if err != nil { + return err + } + defer unlock() + t, err := w.db.Get(c, loopId) + if err != nil { + return err + } + tomb := toTombstone(t, loopId, w.clock.Now()) + if err := w.db.Update(c, tomb); err != nil { + return err + } + return nil + } + for i, id := range objIds { + if err := loopFn(i, id); err != nil { + return err + } + } + if w.Delete != nil { + return w.Delete(c, a) + } + return nil +} + +// follow implements the social Follow activity side effects. +func (w SocialWrappedCallbacks) follow(c context.Context, a vocab.ActivityStreamsFollow) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + if w.Follow != nil { + return w.Follow(c, a) + } + return nil +} + +// add implements the social Add activity side effects. +func (w SocialWrappedCallbacks) add(c context.Context, a vocab.ActivityStreamsAdd) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + target := a.GetActivityStreamsTarget() + if target == nil || target.Len() == 0 { + return ErrTargetRequired + } + if err := add(c, op, target, w.db); err != nil { + return err + } + if w.Add != nil { + return w.Add(c, a) + } + return nil +} + +// remove implements the social Remove activity side effects. +func (w SocialWrappedCallbacks) remove(c context.Context, a vocab.ActivityStreamsRemove) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + target := a.GetActivityStreamsTarget() + if target == nil || target.Len() == 0 { + return ErrTargetRequired + } + if err := remove(c, op, target, w.db); err != nil { + return err + } + if w.Remove != nil { + return w.Remove(c, a) + } + return nil +} + +// like implements the social Like activity side effects. +func (w SocialWrappedCallbacks) like(c context.Context, a vocab.ActivityStreamsLike) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + // Get this actor's IRI. + unlock, err := w.db.Lock(c, w.outboxIRI) + if err != nil { + return err + } + // WARNING: Unlock not deferred. + actorIRI, err := w.db.ActorForOutbox(c, w.outboxIRI) + unlock() // unlock even on error + if err != nil { + return err + } + // Unlock must be called by now and every branch above. + // + // Now obtain this actor's 'liked' collection. + unlock, err = w.db.Lock(c, actorIRI) + if err != nil { + return err + } + defer unlock() + liked, err := w.db.Liked(c, actorIRI) + if err != nil { + return err + } + likedItems := liked.GetActivityStreamsItems() + if likedItems == nil { + likedItems = streams.NewActivityStreamsItemsProperty() + liked.SetActivityStreamsItems(likedItems) + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + objId, err := ToId(iter) + if err != nil { + return err + } + likedItems.PrependIRI(objId) + } + err = w.db.Update(c, liked) + if err != nil { + return err + } + if w.Like != nil { + return w.Like(c, a) + } + return nil +} + +// undo implements the social Undo activity side effects. +func (w SocialWrappedCallbacks) undo(c context.Context, a vocab.ActivityStreamsUndo) error { + *w.undeliverable = false + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + actors := a.GetActivityStreamsActor() + if err := mustHaveActivityActorsMatchObjectActors(c, actors, op, w.newTransport, w.outboxIRI); err != nil { + return err + } + if w.Undo != nil { + return w.Undo(c, a) + } + return nil +} + +// block implements the social Block activity side effects. +func (w SocialWrappedCallbacks) block(c context.Context, a vocab.ActivityStreamsBlock) error { + *w.undeliverable = true + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return ErrObjectRequired + } + if w.Block != nil { + return w.Block(c, a) + } + return nil +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/transport.go b/vendor/code.superseriousbusiness.org/activity/pub/transport.go new file mode 100644 index 000000000..b74bfedbf --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/transport.go @@ -0,0 +1,219 @@ +package pub + +import ( + "bytes" + "context" + "crypto" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "sync" + + "code.superseriousbusiness.org/httpsig" +) + +const ( + // acceptHeaderValue is the Accept header value indicating that the + // response should contain an ActivityStreams object. + acceptHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" +) + +// isSuccess returns true if the HTTP status code is either OK, Created, or +// Accepted. +func isSuccess(code int) bool { + return code == http.StatusOK || + code == http.StatusCreated || + code == http.StatusAccepted +} + +// Transport makes ActivityStreams calls to other servers in order to send or +// receive ActivityStreams data. +// +// It is responsible for setting the appropriate request headers, signing the +// requests if needed, and facilitating the traffic between this server and +// another. +// +// The transport is exclusively used to issue requests on behalf of an actor, +// and is never sending requests on behalf of the server in general. +// +// It may be reused multiple times, but never concurrently. +type Transport interface { + // Dereference fetches the ActivityStreams object located at this IRI with + // a GET request. Note that Response will only be returned on status = OK. + Dereference(c context.Context, iri *url.URL) (*http.Response, error) + + // Deliver sends an ActivityStreams object. + Deliver(c context.Context, obj map[string]interface{}, to *url.URL) error + + // BatchDeliver sends an ActivityStreams object to multiple recipients. + BatchDeliver(c context.Context, obj map[string]interface{}, recipients []*url.URL) error +} + +// Transport must be implemented by HttpSigTransport. +var _ Transport = &HttpSigTransport{} + +// HttpSigTransport makes a dereference call using HTTP signatures to +// authenticate the request on behalf of a particular actor. +// +// No rate limiting is applied. +// +// Only one request is tried per call. +type HttpSigTransport struct { + client HttpClient + appAgent string + gofedAgent string + clock Clock + getSigner httpsig.Signer + getSignerMu *sync.Mutex + postSigner httpsig.Signer + postSignerMu *sync.Mutex + pubKeyId string + privKey crypto.PrivateKey +} + +// NewHttpSigTransport returns a new Transport. +// +// It sends requests specifically on behalf of a specific actor on this server. +// The actor's credentials are used to add an HTTP Signature to requests, which +// requires an actor's private key, a unique identifier for their public key, +// and an HTTP Signature signing algorithm. +// +// The client lets users issue requests through any HTTP client, including the +// standard library's HTTP client. +// +// The appAgent uniquely identifies the calling application's requests, so peers +// may aid debugging the requests incoming from this server. Note that the +// agent string will also include one for go-fed, so at minimum peer servers can +// reach out to the go-fed library to aid in notifying implementors of malformed +// or unsupported requests. +func NewHttpSigTransport( + client HttpClient, + appAgent string, + clock Clock, + getSigner, postSigner httpsig.Signer, + pubKeyId string, + privKey crypto.PrivateKey) *HttpSigTransport { + return &HttpSigTransport{ + client: client, + appAgent: appAgent, + gofedAgent: goFedUserAgent(), + clock: clock, + getSigner: getSigner, + getSignerMu: &sync.Mutex{}, + postSigner: postSigner, + postSignerMu: &sync.Mutex{}, + pubKeyId: pubKeyId, + privKey: privKey, + } +} + +// Dereference sends a GET request signed with an HTTP Signature to obtain an ActivityStreams value. +func (h HttpSigTransport) Dereference(c context.Context, iri *url.URL) (*http.Response, error) { + req, err := http.NewRequest("GET", iri.String(), nil) + if err != nil { + return nil, err + } + req = req.WithContext(c) + req.Header.Add(acceptHeader, acceptHeaderValue) + req.Header.Add("Accept-Charset", "utf-8") + req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") + req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent)) + req.Header.Set("Host", iri.Host) + h.getSignerMu.Lock() + err = h.getSigner.SignRequest(h.privKey, h.pubKeyId, req, nil) + h.getSignerMu.Unlock() + if err != nil { + return nil, err + } + resp, err := h.client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + _ = resp.Body.Close() + return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status) + } + return resp, nil +} + +// Deliver sends a POST request with an HTTP Signature. +func (h HttpSigTransport) Deliver(c context.Context, data map[string]interface{}, to *url.URL) error { + b, err := json.Marshal(data) + if err != nil { + return err + } + return h.deliver(c, b, to) +} + +// BatchDeliver sends concurrent POST requests. Returns an error if any of the requests had an error. +func (h HttpSigTransport) BatchDeliver(c context.Context, data map[string]interface{}, recipients []*url.URL) error { + b, err := json.Marshal(data) + if err != nil { + return err + } + var wg sync.WaitGroup + errCh := make(chan error, len(recipients)) + for _, recipient := range recipients { + wg.Add(1) + go func(r *url.URL) { + defer wg.Done() + if err := h.deliver(c, b, r); err != nil { + errCh <- err + } + }(recipient) + } + wg.Wait() + errs := make([]string, 0, len(recipients)) +outer: + for { + select { + case e := <-errCh: + errs = append(errs, e.Error()) + default: + break outer + } + } + if len(errs) > 0 { + return fmt.Errorf("batch deliver had at least one failure: %s", strings.Join(errs, "; ")) + } + return nil +} + +func (h HttpSigTransport) deliver(c context.Context, b []byte, to *url.URL) error { + req, err := http.NewRequest("POST", to.String(), bytes.NewReader(b)) + if err != nil { + return err + } + req = req.WithContext(c) + req.Header.Add(contentTypeHeader, contentTypeHeaderValue) + req.Header.Add("Accept-Charset", "utf-8") + req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") + req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent)) + req.Header.Set("Host", to.Host) + h.postSignerMu.Lock() + err = h.postSigner.SignRequest(h.privKey, h.pubKeyId, req, b) + h.postSignerMu.Unlock() + if err != nil { + return err + } + resp, err := h.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if !isSuccess(resp.StatusCode) { + return fmt.Errorf("POST request to %s failed (%d): %s", to.String(), resp.StatusCode, resp.Status) + } + return nil +} + +// HttpClient sends http requests, and is an abstraction only needed by the +// HttpSigTransport. The standard library's Client satisfies this interface. +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// HttpClient must be implemented by http.Client. +var _ HttpClient = &http.Client{} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/util.go b/vendor/code.superseriousbusiness.org/activity/pub/util.go new file mode 100644 index 000000000..dc78bb964 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/util.go @@ -0,0 +1,1077 @@ +package pub + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "code.superseriousbusiness.org/activity/streams" + "code.superseriousbusiness.org/activity/streams/vocab" +) + +var ( + // ErrObjectRequired indicates the activity needs its object property + // set. Can be returned by DelegateActor's PostInbox or PostOutbox so a + // Bad Request response is set. + ErrObjectRequired = errors.New("object property required on the provided activity") + // ErrTargetRequired indicates the activity needs its target property + // set. Can be returned by DelegateActor's PostInbox or PostOutbox so a + // Bad Request response is set. + ErrTargetRequired = errors.New("target property required on the provided activity") +) + +// activityStreamsMediaTypes contains all of the accepted ActivityStreams media +// types. Generated at init time. +var activityStreamsMediaTypes []string + +func init() { + activityStreamsMediaTypes = []string{ + "application/activity+json", + } + jsonLdType := "application/ld+json" + for _, semi := range []string{";", " ;", " ; ", "; "} { + for _, profile := range []string{ + "profile=https://www.w3.org/ns/activitystreams", + "profile=\"https://www.w3.org/ns/activitystreams\"", + } { + activityStreamsMediaTypes = append( + activityStreamsMediaTypes, + fmt.Sprintf("%s%s%s", jsonLdType, semi, profile)) + } + } +} + +// headerIsActivityPubMediaType returns true if the header string contains one +// of the accepted ActivityStreams media types. +// +// Note we don't try to build a comprehensive parser and instead accept a +// tolerable amount of whitespace since the HTTP specification is ambiguous +// about the format and significance of whitespace. +func headerIsActivityPubMediaType(header string) bool { + for _, mediaType := range activityStreamsMediaTypes { + if strings.Contains(header, mediaType) { + return true + } + } + return false +} + +const ( + // The Content-Type header. + contentTypeHeader = "Content-Type" + // The Accept header. + acceptHeader = "Accept" +) + +// readActivityPubReq reads ActivityPub data from an incoming request, handling body close. +func readActivityPubReq(req *http.Request) (map[string]interface{}, error) { + // Ensure closed when done. + defer req.Body.Close() + + var m map[string]interface{} + + // Wrap body in a JSON decoder. + dec := json.NewDecoder(req.Body) + + // Decode JSON body as "raw" AP data map. + if err := dec.Decode(&m); err != nil { + return nil, err + } + + // Perform a final second decode to ensure no trailing + // garbage data or second JSON value (indicates malformed). + if err := dec.Decode(&struct{}{}); err != io.EOF { + return nil, errors.New("trailing data after json") + } + + return m, nil +} + +// readActivityPubResp reads ActivityPub data from a dereference response, handling media type check and body close. +func readActivityPubResp(resp *http.Response) (map[string]interface{}, error) { + // Ensure closed when done. + defer resp.Body.Close() + + // Check the incoming response media type is the expected ActivityPub content-type. + if mediaType := resp.Header.Get("Content-Type"); !headerIsActivityPubMediaType(mediaType) { + return nil, fmt.Errorf("%s %s resp was not ActivityPub media type: %s", resp.Request.Method, resp.Request.URL, mediaType) + } + + var m map[string]interface{} + + // Wrap body in a JSON decoder. + dec := json.NewDecoder(resp.Body) + + // Decode JSON body as "raw" AP data map. + if err := dec.Decode(&m); err != nil { + return nil, err + } + + // Perform a final second decode to ensure no trailing + // garbage data or second JSON value (indicates malformed). + if err := dec.Decode(&struct{}{}); err != io.EOF { + return nil, errors.New("trailing data after json") + } + + return m, nil +} + +// isActivityPubPost returns true if the request is a POST request that has the +// ActivityStreams content type header +func isActivityPubPost(r *http.Request) bool { + return r.Method == "POST" && headerIsActivityPubMediaType(r.Header.Get(contentTypeHeader)) +} + +// isActivityPubGet returns true if the request is a GET request that has the +// ActivityStreams content type header +func isActivityPubGet(r *http.Request) bool { + return r.Method == "GET" && headerIsActivityPubMediaType(r.Header.Get(acceptHeader)) +} + +// dedupeOrderedItems deduplicates the 'orderedItems' within an ordered +// collection type. Deduplication happens by the 'id' property. +func dedupeOrderedItems(oc orderedItemser) error { + oi := oc.GetActivityStreamsOrderedItems() + if oi == nil { + return nil + } + seen := make(map[string]bool, oi.Len()) + for i := 0; i < oi.Len(); { + var id *url.URL + + iter := oi.At(i) + asType := iter.GetType() + if asType != nil { + var err error + id, err = GetId(asType) + if err != nil { + return err + } + } else if iter.IsIRI() { + id = iter.GetIRI() + } else { + return fmt.Errorf("element %d in OrderedCollection does not have an ID nor is an IRI", i) + } + if seen[id.String()] { + oi.Remove(i) + } else { + seen[id.String()] = true + i++ + } + } + return nil +} + +const ( + // The Location header + locationHeader = "Location" + // Contains the ActivityStreams Content-Type value. + contentTypeHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + // The Date header. + dateHeader = "Date" + // The Digest header. + digestHeader = "Digest" + // The delimiter used in the Digest header. + digestDelimiter = "=" + // SHA-256 string for the Digest header. + sha256Digest = "SHA-256" +) + +// addResponseHeaders sets headers needed in the HTTP response, such but not +// limited to the Content-Type, Date, and Digest headers. +func addResponseHeaders(h http.Header, c Clock, responseContent []byte) { + h.Set(contentTypeHeader, contentTypeHeaderValue) + // RFC 7231 ยง7.1.1.2 + h.Set(dateHeader, c.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") + // RFC 3230 and RFC 5843 + var b bytes.Buffer + b.WriteString(sha256Digest) + b.WriteString(digestDelimiter) + hashed := sha256.Sum256(responseContent) + b.WriteString(base64.StdEncoding.EncodeToString(hashed[:])) + h.Set(digestHeader, b.String()) +} + +// IdProperty is a property that can readily have its id obtained +type IdProperty interface { + // GetIRI returns the IRI of this property. When IsIRI returns false, + // GetIRI will return an arbitrary value. + GetIRI() *url.URL + // GetType returns the value in this property as a Type. Returns nil if + // the value is not an ActivityStreams type, such as an IRI or another + // value. + GetType() vocab.Type + // IsIRI returns true if this property is an IRI. + IsIRI() bool +} + +// ToId returns an IdProperty's id. +func ToId(i IdProperty) (*url.URL, error) { + if i.GetType() != nil { + return GetId(i.GetType()) + } else if i.IsIRI() { + return i.GetIRI(), nil + } + return nil, fmt.Errorf("cannot determine id of activitystreams property") +} + +// GetId will attempt to find the 'id' property or, if it happens to be a +// Link or derived from Link type, the 'href' property instead. +// +// Returns an error if the id is not set and either the 'href' property is not +// valid on this type, or it is also not set. +func GetId(t vocab.Type) (*url.URL, error) { + if id := t.GetJSONLDId(); id != nil { + return id.Get(), nil + } else if h, ok := t.(hrefer); ok { + if href := h.GetActivityStreamsHref(); href != nil { + return href.Get(), nil + } + } + return nil, fmt.Errorf("cannot determine id of activitystreams value") +} + +// getInboxForwardingValues obtains the 'inReplyTo', 'object', 'target', and +// 'tag' values on an ActivityStreams value. +func getInboxForwardingValues(o vocab.Type) (t []vocab.Type, iri []*url.URL) { + // 'inReplyTo' + if i, ok := o.(inReplyToer); ok { + if irt := i.GetActivityStreamsInReplyTo(); irt != nil { + for iter := irt.Begin(); iter != irt.End(); iter = iter.Next() { + if tv := iter.GetType(); tv != nil { + t = append(t, tv) + } else { + iri = append(iri, iter.GetIRI()) + } + } + } + } + // 'tag' + if i, ok := o.(tagger); ok { + if tag := i.GetActivityStreamsTag(); tag != nil { + for iter := tag.Begin(); iter != tag.End(); iter = iter.Next() { + if tv := iter.GetType(); tv != nil { + t = append(t, tv) + } else { + iri = append(iri, iter.GetIRI()) + } + } + } + } + // 'object' + if i, ok := o.(objecter); ok { + if obj := i.GetActivityStreamsObject(); obj != nil { + for iter := obj.Begin(); iter != obj.End(); iter = iter.Next() { + if tv := iter.GetType(); tv != nil { + t = append(t, tv) + } else { + iri = append(iri, iter.GetIRI()) + } + } + } + } + // 'target' + if i, ok := o.(targeter); ok { + if tar := i.GetActivityStreamsTarget(); tar != nil { + for iter := tar.Begin(); iter != tar.End(); iter = iter.Next() { + if tv := iter.GetType(); tv != nil { + t = append(t, tv) + } else { + iri = append(iri, iter.GetIRI()) + } + } + } + } + return +} + +// wrapInCreate will automatically wrap the provided object in a Create +// activity. This will copy over the 'to', 'bto', 'cc', 'bcc', and 'audience' +// properties. It will also copy over the published time if present. +func wrapInCreate(ctx context.Context, o vocab.Type, actor *url.URL) (c vocab.ActivityStreamsCreate, err error) { + c = streams.NewActivityStreamsCreate() + // Object property + oProp := streams.NewActivityStreamsObjectProperty() + oProp.AppendType(o) + c.SetActivityStreamsObject(oProp) + // Actor Property + actorProp := streams.NewActivityStreamsActorProperty() + actorProp.AppendIRI(actor) + c.SetActivityStreamsActor(actorProp) + // Published Property + if v, ok := o.(publisheder); ok { + c.SetActivityStreamsPublished(v.GetActivityStreamsPublished()) + } + // Copying over properties. + if v, ok := o.(toer); ok { + if to := v.GetActivityStreamsTo(); to != nil { + activityTo := streams.NewActivityStreamsToProperty() + for iter := to.Begin(); iter != to.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + activityTo.AppendIRI(id) + } + c.SetActivityStreamsTo(activityTo) + } + } + if v, ok := o.(btoer); ok { + if bto := v.GetActivityStreamsBto(); bto != nil { + activityBto := streams.NewActivityStreamsBtoProperty() + for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + activityBto.AppendIRI(id) + } + c.SetActivityStreamsBto(activityBto) + } + } + if v, ok := o.(ccer); ok { + if cc := v.GetActivityStreamsCc(); cc != nil { + activityCc := streams.NewActivityStreamsCcProperty() + for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + activityCc.AppendIRI(id) + } + c.SetActivityStreamsCc(activityCc) + } + } + if v, ok := o.(bccer); ok { + if bcc := v.GetActivityStreamsBcc(); bcc != nil { + activityBcc := streams.NewActivityStreamsBccProperty() + for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + activityBcc.AppendIRI(id) + } + c.SetActivityStreamsBcc(activityBcc) + } + } + if v, ok := o.(audiencer); ok { + if aud := v.GetActivityStreamsAudience(); aud != nil { + activityAudience := streams.NewActivityStreamsAudienceProperty() + for iter := aud.Begin(); iter != aud.End(); iter = iter.Next() { + var id *url.URL + id, err = ToId(iter) + if err != nil { + return + } + activityAudience.AppendIRI(id) + } + c.SetActivityStreamsAudience(activityAudience) + } + } + return +} + +const ( + // PublicActivityPubIRI is the IRI that indicates an Activity is meant + // to be visible for general public consumption. + PublicActivityPubIRI = "https://www.w3.org/ns/activitystreams#Public" + publicJsonLD = "Public" + publicJsonLDAS = "as:Public" +) + +// IsPublic determines if an IRI string is the Public collection as defined in +// the spec, including JSON-LD compliant collections. +func IsPublic(s string) bool { + return s == PublicActivityPubIRI || s == publicJsonLD || s == publicJsonLDAS +} + +// Derives an ID URI from the given IdProperty and, if it's not the +// magic AP Public IRI, appends it to the actorsAndCollections slice. +func appendToActorsAndCollectionsIRIs( + iter IdProperty, + actorsAndCollections []*url.URL, +) ([]*url.URL, error) { + id, err := ToId(iter) + if err != nil { + return nil, err + } + + // Ignore Public IRI as we + // can't deliver to it directly. + if !IsPublic(id.String()) { + actorsAndCollections = append(actorsAndCollections, id) + } + + return actorsAndCollections, nil +} + +// actorsToInboxIRIs extracts the 'inbox' IRIs from actor types. +func actorsToInboxIRIs(t []vocab.Type) (u []*url.URL, err error) { + for _, elem := range t { + var iri *url.URL + iri, err = getInbox(elem) + if err != nil { + return + } + u = append(u, iri) + } + return +} + +// getInbox extracts the 'inbox' IRI from an actor type. +func getInbox(t vocab.Type) (u *url.URL, err error) { + ib, ok := t.(inboxer) + if !ok { + err = fmt.Errorf("actor type %T has no inbox", t) + return + } + inbox := ib.GetActivityStreamsInbox() + return ToId(inbox) +} + +// filterInboxIRIs will deduplicate the given inboxes +// slice, while also leaving out any filtered IRIs. +func filterInboxIRIs( + inboxes []*url.URL, + filtered ...*url.URL, +) []*url.URL { + // Prepopulate the ignored map with each filtered IRI. + ignored := make(map[string]struct{}, len(filtered)+len(inboxes)) + for _, filteredIRI := range filtered { + ignored[filteredIRI.String()] = struct{}{} + } + + deduped := make([]*url.URL, 0, len(inboxes)) + for _, inbox := range inboxes { + inboxStr := inbox.String() + _, ignore := ignored[inboxStr] + if ignore { + // We already included + // this URI in out, or + // we should ignore it. + continue + } + + // Include this IRI in output, and + // add entry to the ignored map to + // ensure we don't include it again. + deduped = append(deduped, inbox) + ignored[inboxStr] = struct{}{} + } + + return deduped +} + +// stripHiddenRecipients removes "bto" and "bcc" from the activity. +// +// Note that this requirement of the specification is under "Section 6: Client +// to Server Interactions", the Social API, and not the Federative API. +func stripHiddenRecipients(activity Activity) { + activity.SetActivityStreamsBto(nil) + activity.SetActivityStreamsBcc(nil) + op := activity.GetActivityStreamsObject() + if op != nil { + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + if v, ok := iter.GetType().(btoer); ok { + v.SetActivityStreamsBto(nil) + } + if v, ok := iter.GetType().(bccer); ok { + v.SetActivityStreamsBcc(nil) + } + } + } +} + +// mustHaveActivityOriginMatchObjects ensures that the Host in the activity id +// IRI matches all of the Hosts in the object id IRIs. +func mustHaveActivityOriginMatchObjects(a Activity) error { + originIRI, err := GetId(a) + if err != nil { + return err + } + originHost := originIRI.Host + op := a.GetActivityStreamsObject() + if op == nil || op.Len() == 0 { + return nil + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + iri, err := ToId(iter) + if err != nil { + return err + } + if originHost != iri.Host { + return fmt.Errorf("object %q: not in activity origin", iri) + } + } + return nil +} + +// normalizeRecipients ensures the activity and object have the same 'to', +// 'bto', 'cc', 'bcc', and 'audience' properties. Copy the Activity's recipients +// to objects, and the objects to the activity, but does NOT copy objects' +// recipients to each other. +func normalizeRecipients(a vocab.ActivityStreamsCreate) error { + // Phase 0: Acquire all recipients on the activity. + // + // Obtain the actorTo map + actorToMap := make(map[string]*url.URL) + actorTo := a.GetActivityStreamsTo() + if actorTo == nil { + actorTo = streams.NewActivityStreamsToProperty() + a.SetActivityStreamsTo(actorTo) + } + for iter := actorTo.Begin(); iter != actorTo.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorToMap[id.String()] = id + } + // Obtain the actorBto map + actorBtoMap := make(map[string]*url.URL) + actorBto := a.GetActivityStreamsBto() + if actorBto == nil { + actorBto = streams.NewActivityStreamsBtoProperty() + a.SetActivityStreamsBto(actorBto) + } + for iter := actorBto.Begin(); iter != actorBto.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorBtoMap[id.String()] = id + } + // Obtain the actorCc map + actorCcMap := make(map[string]*url.URL) + actorCc := a.GetActivityStreamsCc() + if actorCc == nil { + actorCc = streams.NewActivityStreamsCcProperty() + a.SetActivityStreamsCc(actorCc) + } + for iter := actorCc.Begin(); iter != actorCc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorCcMap[id.String()] = id + } + // Obtain the actorBcc map + actorBccMap := make(map[string]*url.URL) + actorBcc := a.GetActivityStreamsBcc() + if actorBcc == nil { + actorBcc = streams.NewActivityStreamsBccProperty() + a.SetActivityStreamsBcc(actorBcc) + } + for iter := actorBcc.Begin(); iter != actorBcc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorBccMap[id.String()] = id + } + // Obtain the actorAudience map + actorAudienceMap := make(map[string]*url.URL) + actorAudience := a.GetActivityStreamsAudience() + if actorAudience == nil { + actorAudience = streams.NewActivityStreamsAudienceProperty() + a.SetActivityStreamsAudience(actorAudience) + } + for iter := actorAudience.Begin(); iter != actorAudience.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + actorAudienceMap[id.String()] = id + } + // Obtain the objects maps for each recipient type. + o := a.GetActivityStreamsObject() + objsTo := make([]map[string]*url.URL, o.Len()) + objsBto := make([]map[string]*url.URL, o.Len()) + objsCc := make([]map[string]*url.URL, o.Len()) + objsBcc := make([]map[string]*url.URL, o.Len()) + objsAudience := make([]map[string]*url.URL, o.Len()) + for i := 0; i < o.Len(); i++ { + iter := o.At(i) + // Phase 1: Acquire all existing recipients on the object. + // + // Object to + objsTo[i] = make(map[string]*url.URL) + var oTo vocab.ActivityStreamsToProperty + if tr, ok := iter.GetType().(toer); !ok { + return fmt.Errorf("the Create object at %d has no 'to' property", i) + } else { + oTo = tr.GetActivityStreamsTo() + if oTo == nil { + oTo = streams.NewActivityStreamsToProperty() + tr.SetActivityStreamsTo(oTo) + } + } + for iter := oTo.Begin(); iter != oTo.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsTo[i][id.String()] = id + } + // Object bto + objsBto[i] = make(map[string]*url.URL) + var oBto vocab.ActivityStreamsBtoProperty + if tr, ok := iter.GetType().(btoer); !ok { + return fmt.Errorf("the Create object at %d has no 'bto' property", i) + } else { + oBto = tr.GetActivityStreamsBto() + if oBto == nil { + oBto = streams.NewActivityStreamsBtoProperty() + tr.SetActivityStreamsBto(oBto) + } + } + for iter := oBto.Begin(); iter != oBto.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsBto[i][id.String()] = id + } + // Object cc + objsCc[i] = make(map[string]*url.URL) + var oCc vocab.ActivityStreamsCcProperty + if tr, ok := iter.GetType().(ccer); !ok { + return fmt.Errorf("the Create object at %d has no 'cc' property", i) + } else { + oCc = tr.GetActivityStreamsCc() + if oCc == nil { + oCc = streams.NewActivityStreamsCcProperty() + tr.SetActivityStreamsCc(oCc) + } + } + for iter := oCc.Begin(); iter != oCc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsCc[i][id.String()] = id + } + // Object bcc + objsBcc[i] = make(map[string]*url.URL) + var oBcc vocab.ActivityStreamsBccProperty + if tr, ok := iter.GetType().(bccer); !ok { + return fmt.Errorf("the Create object at %d has no 'bcc' property", i) + } else { + oBcc = tr.GetActivityStreamsBcc() + if oBcc == nil { + oBcc = streams.NewActivityStreamsBccProperty() + tr.SetActivityStreamsBcc(oBcc) + } + } + for iter := oBcc.Begin(); iter != oBcc.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsBcc[i][id.String()] = id + } + // Object audience + objsAudience[i] = make(map[string]*url.URL) + var oAudience vocab.ActivityStreamsAudienceProperty + if tr, ok := iter.GetType().(audiencer); !ok { + return fmt.Errorf("the Create object at %d has no 'audience' property", i) + } else { + oAudience = tr.GetActivityStreamsAudience() + if oAudience == nil { + oAudience = streams.NewActivityStreamsAudienceProperty() + tr.SetActivityStreamsAudience(oAudience) + } + } + for iter := oAudience.Begin(); iter != oAudience.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + objsAudience[i][id.String()] = id + } + // Phase 2: Apply missing recipients to the object from the + // activity. + // + // Activity to -> Object to + for k, v := range actorToMap { + if _, ok := objsTo[i][k]; !ok { + oTo.AppendIRI(v) + } + } + // Activity bto -> Object bto + for k, v := range actorBtoMap { + if _, ok := objsBto[i][k]; !ok { + oBto.AppendIRI(v) + } + } + // Activity cc -> Object cc + for k, v := range actorCcMap { + if _, ok := objsCc[i][k]; !ok { + oCc.AppendIRI(v) + } + } + // Activity bcc -> Object bcc + for k, v := range actorBccMap { + if _, ok := objsBcc[i][k]; !ok { + oBcc.AppendIRI(v) + } + } + // Activity audience -> Object audience + for k, v := range actorAudienceMap { + if _, ok := objsAudience[i][k]; !ok { + oAudience.AppendIRI(v) + } + } + } + // Phase 3: Apply missing recipients to the activity from the objects. + // + // Object to -> Activity to + for i := 0; i < len(objsTo); i++ { + for k, v := range objsTo[i] { + if _, ok := actorToMap[k]; !ok { + actorTo.AppendIRI(v) + } + } + } + // Object bto -> Activity bto + for i := 0; i < len(objsBto); i++ { + for k, v := range objsBto[i] { + if _, ok := actorBtoMap[k]; !ok { + actorBto.AppendIRI(v) + } + } + } + // Object cc -> Activity cc + for i := 0; i < len(objsCc); i++ { + for k, v := range objsCc[i] { + if _, ok := actorCcMap[k]; !ok { + actorCc.AppendIRI(v) + } + } + } + // Object bcc -> Activity bcc + for i := 0; i < len(objsBcc); i++ { + for k, v := range objsBcc[i] { + if _, ok := actorBccMap[k]; !ok { + actorBcc.AppendIRI(v) + } + } + } + // Object audience -> Activity audience + for i := 0; i < len(objsAudience); i++ { + for k, v := range objsAudience[i] { + if _, ok := actorAudienceMap[k]; !ok { + actorAudience.AppendIRI(v) + } + } + } + return nil +} + +// toTombstone creates a Tombstone object for the given ActivityStreams value. +func toTombstone(obj vocab.Type, id *url.URL, now time.Time) vocab.ActivityStreamsTombstone { + tomb := streams.NewActivityStreamsTombstone() + // id property + idProp := streams.NewJSONLDIdProperty() + idProp.Set(id) + tomb.SetJSONLDId(idProp) + // formerType property + former := streams.NewActivityStreamsFormerTypeProperty() + tomb.SetActivityStreamsFormerType(former) + // Populate Former Type + former.AppendXMLSchemaString(obj.GetTypeName()) + // Copy over the published property if it existed + if pubber, ok := obj.(publisheder); ok { + if pub := pubber.GetActivityStreamsPublished(); pub != nil { + tomb.SetActivityStreamsPublished(pub) + } + } + // Copy over the updated property if it existed + if upder, ok := obj.(updateder); ok { + if upd := upder.GetActivityStreamsUpdated(); upd != nil { + tomb.SetActivityStreamsUpdated(upd) + } + } + // Set deleted time to now. + deleted := streams.NewActivityStreamsDeletedProperty() + deleted.Set(now) + tomb.SetActivityStreamsDeleted(deleted) + return tomb +} + +// mustHaveActivityActorsMatchObjectActors ensures that the actors on types in +// the 'object' property are all listed in the 'actor' property. +func mustHaveActivityActorsMatchObjectActors(c context.Context, + actors vocab.ActivityStreamsActorProperty, + op vocab.ActivityStreamsObjectProperty, + newTransport func(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t Transport, err error), + boxIRI *url.URL, +) error { + activityActorMap := make(map[string]bool, actors.Len()) + for iter := actors.Begin(); iter != actors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + activityActorMap[id.String()] = true + } + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + iri, err := ToId(iter) + if err != nil { + return err + } + // Attempt to dereference the IRI, regardless whether it is a + // type or IRI + tport, err := newTransport(c, boxIRI, goFedUserAgent()) + if err != nil { + return err + } + resp, err := tport.Dereference(c, iri) + if err != nil { + return err + } + m, err := readActivityPubResp(resp) + if err != nil { + return err + } + t, err := streams.ToType(c, m) + if err != nil { + return err + } + ac, ok := t.(actorer) + if !ok { + return fmt.Errorf("cannot verify actors: object value has no 'actor' property") + } + objActors := ac.GetActivityStreamsActor() + for iter := objActors.Begin(); iter != objActors.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + if !activityActorMap[id.String()] { + return fmt.Errorf("activity does not have all actors from its object's actors") + } + } + } + return nil +} + +// add implements the logic of adding object ids to a target Collection or +// OrderedCollection. This logic is shared by both the C2S and S2S protocols. +func add(c context.Context, + op vocab.ActivityStreamsObjectProperty, + target vocab.ActivityStreamsTargetProperty, + db Database, +) error { + opIds := make([]*url.URL, 0, op.Len()) + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + opIds = append(opIds, id) + } + targetIds := make([]*url.URL, 0, op.Len()) + for iter := target.Begin(); iter != target.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + targetIds = append(targetIds, id) + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(t *url.URL) error { + unlock, err := db.Lock(c, t) + if err != nil { + return err + } + defer unlock() + if owns, err := db.Owns(c, t); err != nil { + return err + } else if !owns { + return nil + } + tp, err := db.Get(c, t) + if err != nil { + return err + } + if streams.IsOrExtendsActivityStreamsOrderedCollection(tp) { + oi, ok := tp.(orderedItemser) + if !ok { + return fmt.Errorf("type extending from OrderedCollection cannot convert to orderedItemser interface") + } + oiProp := oi.GetActivityStreamsOrderedItems() + if oiProp == nil { + oiProp = streams.NewActivityStreamsOrderedItemsProperty() + oi.SetActivityStreamsOrderedItems(oiProp) + } + for _, objId := range opIds { + oiProp.AppendIRI(objId) + } + } else if streams.IsOrExtendsActivityStreamsCollection(tp) { + i, ok := tp.(itemser) + if !ok { + return fmt.Errorf("type extending from Collection cannot convert to itemser interface") + } + iProp := i.GetActivityStreamsItems() + if iProp == nil { + iProp = streams.NewActivityStreamsItemsProperty() + i.SetActivityStreamsItems(iProp) + } + for _, objId := range opIds { + iProp.AppendIRI(objId) + } + } else { + return fmt.Errorf("target in Add is neither a Collection nor an OrderedCollection") + } + err = db.Update(c, tp) + if err != nil { + return err + } + return nil + } + for _, t := range targetIds { + if err := loopFn(t); err != nil { + return err + } + } + return nil +} + +// remove implements the logic of removing object ids to a target Collection or +// OrderedCollection. This logic is shared by both the C2S and S2S protocols. +func remove(c context.Context, + op vocab.ActivityStreamsObjectProperty, + target vocab.ActivityStreamsTargetProperty, + db Database, +) error { + opIds := make(map[string]bool, op.Len()) + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + opIds[id.String()] = true + } + targetIds := make([]*url.URL, 0, op.Len()) + for iter := target.Begin(); iter != target.End(); iter = iter.Next() { + id, err := ToId(iter) + if err != nil { + return err + } + targetIds = append(targetIds, id) + } + // Create anonymous loop function to be able to properly scope the defer + // for the database lock at each iteration. + loopFn := func(t *url.URL) error { + unlock, err := db.Lock(c, t) + if err != nil { + return err + } + defer unlock() + if owns, err := db.Owns(c, t); err != nil { + return err + } else if !owns { + return nil + } + tp, err := db.Get(c, t) + if err != nil { + return err + } + if streams.IsOrExtendsActivityStreamsOrderedCollection(tp) { + oi, ok := tp.(orderedItemser) + if !ok { + return fmt.Errorf("type extending from OrderedCollection cannot convert to orderedItemser interface") + } + oiProp := oi.GetActivityStreamsOrderedItems() + if oiProp != nil { + for i := 0; i < oiProp.Len(); /*Conditional*/ { + id, err := ToId(oiProp.At(i)) + if err != nil { + return err + } + if opIds[id.String()] { + oiProp.Remove(i) + } else { + i++ + } + } + } + } else if streams.IsOrExtendsActivityStreamsCollection(tp) { + i, ok := tp.(itemser) + if !ok { + return fmt.Errorf("type extending from Collection cannot convert to itemser interface") + } + iProp := i.GetActivityStreamsItems() + if iProp != nil { + for i := 0; i < iProp.Len(); /*Conditional*/ { + id, err := ToId(iProp.At(i)) + if err != nil { + return err + } + if opIds[id.String()] { + iProp.Remove(i) + } else { + i++ + } + } + } + } else { + return fmt.Errorf("target in Remove is neither a Collection nor an OrderedCollection") + } + err = db.Update(c, tp) + if err != nil { + return err + } + return nil + } + for _, t := range targetIds { + if err := loopFn(t); err != nil { + return err + } + } + return nil +} + +// clearSensitiveFields removes the 'bto' and 'bcc' entries on the given value +// and recursively on every 'object' property value. +func clearSensitiveFields(obj vocab.Type) { + if t, ok := obj.(btoer); ok { + t.SetActivityStreamsBto(nil) + } + if t, ok := obj.(bccer); ok { + t.SetActivityStreamsBcc(nil) + } + if t, ok := obj.(objecter); ok { + op := t.GetActivityStreamsObject() + if op != nil { + for iter := op.Begin(); iter != op.End(); iter = iter.Next() { + clearSensitiveFields(iter.GetType()) + } + } + } +} + +// requestId forms an ActivityPub id based on the HTTP request. Always assumes +// that the id is HTTPS. +func requestId(r *http.Request, scheme string) *url.URL { + id := r.URL + id.Host = r.Host + id.Scheme = scheme + return id +} diff --git a/vendor/code.superseriousbusiness.org/activity/pub/version.go b/vendor/code.superseriousbusiness.org/activity/pub/version.go new file mode 100644 index 000000000..23b958ce5 --- /dev/null +++ b/vendor/code.superseriousbusiness.org/activity/pub/version.go @@ -0,0 +1,15 @@ +package pub + +import ( + "fmt" +) + +const ( + // Version string, used in the User-Agent + version = "v1.0.0" +) + +// goFedUserAgent returns the user agent string for the go-fed library. +func goFedUserAgent() string { + return fmt.Sprintf("(go-fed/activity %s)", version) +} |
