package pub import ( "context" "encoding/json" "fmt" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "io/ioutil" "net/http" "net/url" ) // 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{ delegate: &sideEffectActor{ common: c, c2s: c2s, db: db, clock: 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{ delegate: &sideEffectActor{ common: c, s2s: s2s, db: db, clock: 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: &sideEffectActor{ common: c, c2s: c2s, s2s: s2s, db: db, clock: 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. 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. raw, err := ioutil.ReadAll(r.Body) if err != nil { return true, err } var m map[string]interface{} if err = json.Unmarshal(raw, &m); 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. raw, err := ioutil.ReadAll(r.Body) if err != nil { return true, err } var m map[string]interface{} if err = json.Unmarshal(raw, &m); 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) }