pub

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 16, 2018 License: BSD-3-Clause Imports: 16 Imported by: 0

README

pub

Implements both the SocialAPI and FederateAPI in the ActivityPub specification.

Disclaimer

This library is designed with flexibility in mind. The cost of doing so is that writing an ActivityPub application requires a lot of careful considerations that are not trivial. ActivityPub is an Application transport layer that is also tied to a specific data model, making retrofits nontrivial as well.

How To Use

There are two ActivityPub APIs: the SocialAPI between a user and your ActivityPub server, and the FederateAPI between your ActivityPub server and another server peer. This library lets you choose one or both.

Lightning intro to ActivityPub: ActivityPub uses ActivityStreams as data. This lives in go-fed/activity/vocab. ActivityPub has a concept of actors who can send, receive, and read their messages. When sending and receiving messages from a client (such as on their phone) to an ActivityPub server, it is via the SocialAPI. When it is between two ActivityPub servers, it is via the FederateAPI.

Next, there are two kinds of ActivityPub requests to handle:

  1. Requests that GET or POST to stuff owned by an actor like their inbox or outbox.
  2. Requests that GET ActivityStream objects hosted on your server.

The first is the most complex, and requires the creation of a Pubber. It is created depending on which APIs are to be supported:

// Only support SocialAPI
s := pub.NewSocialPubber(...)
// Only support FederateAPI
f := pub.NewFederatingPubber(...)
// Support both APIs
sf := pub.NewPubber(...)

Note that only the creation of the Pubber is affected by the decision of which API to support. Once created, the Pubber should be used in the same manner regardless of the API it is supporting. This allows your application to easily adopt one API first and migrate to both later by simply changing how the Pubber is created.

To use the Pubber, call its methods in the HTTP handlers responsible for an actor's inbox and outbox:

// Given:
//     var myPubber pub.Pubber
var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
  c := context.Background()
  // Populate c with application specific information
  if handled, err := myPubber.PostOutbox(c, w, r); err != nil {
    // Write to w
  } else if handled {
    return
  }
  if handled, err := myPubber.GetOutbox(c, w, r); err != nil {
    // Write to w
  } else if handled {
    return
  }
  // Handle non-ActivityPub request, such as responding with an HTML
  // representation with correct view permissions.
}
var inboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
  c := context.Background()
  // Populate c with application specific information
  if handled, err := myPubber.PostInbox(c, w, r); err != nil {
    // Write to w
  } else if handled {
    return
  }
  if handled, err := myPubber.GetInbox(c, w, r); err != nil {
    // Write to w
  } else if handled {
    return
  }
  // Handle non-ActivityPub request, such as responding with an HTML
  // representation with correct view permissions.
}

Finally, to handle the second kind of request, use the HandlerFunc within HTTP handler functions in a similar way. There are two ways to create HandlerFunc, which depend on decisions we will address later:

asHandler := pub.ServeActivityPubObject(...)
var activityStreamHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
  c := context.Background()
  // Populate c with application specific information
  if handled, err := asHandler(c, w, r); err != nil {
    // Write to w
  } else if handled {
    return
  }
  // Handle non-ActivityPub request, such as responding with an HTML
  // representation with correct view permissions.
}

That's all that's required to support ActivityPub.

How To Create

You may have noticed that using the library is deceptively straightforward. This is because creating the Pubber and HandlerFunc types is not trivial and requires forethought.

There are a lot of interfaces that must be satisfied in order to have a complete working ActivityPub server.

Note that context.Context is passed everywhere possible, to allow your implementation to keep a request-specific context throughout the lifecycle of an ActivityPub request.

Application Interface

Regardless of which of the SocialAPI and FederateAPI chosen, the Application interface contains the set of core methods fundamental to the functionality of this library. It contains a lot of the storage fetching and writing, all of which is keyed by *url.URL. To protect against race conditions, this library will inform whether it is fetching data to read-only or fetching for read-or- write.

Note that under some conditions, ActivityPub verifies the peer's request. It does so using HTTP Signatures. However, this requires knowing the other party's public key, and fetching this remotely is do-able. However, this library assumes this server already has it locally; at this time it is up to implementations to remotely fetch it if needed.

SocialAPI and FederateAPI Interfaces

These interfaces capture additional behaviors required by the SocialAPI and the FederateAPI.

The SocialAPI can additionally provide a mechanism for client authentication and authorization using frameworks like Oauth 2.0. Such frameworks are not natively supported in this library and must be supplied.

Callbacker Interface

One of these is needed per ActivityPub API supported. For example, if both the SocialAPI and FederateAPI are supported, then two of these are needed.

Upon receiving one of these activities from a POST to the inbox or outbox, the correct callbacker will be called to handle either a SocialAPI activity or a FederateAPI activity.

This is where the bulk of implementation-specific logic is expected to reside.

Do note that for some of these activities, default actions will already occur. For example, if receiving an Accept in response to a sent Follow, this library automatically handles adding the correct actor into the correct following collection. This means a lot of the social and federate functionality is provided out of the box.

Deliverer Interface

This is an optional interface. Since this library needs to send HTTP requests, it would be unwise for it to provide no way of allowing implementations to rate limit, persist across downtime, back off, etc. This interface is satisfied by the go-fed/activity/deliverer package which has an implementation that can remember to send requests across downtime.

If an implementation does not care to have this level of control, a synchronous implementation is very straightforward to make.

Other Interfaces

Other interfaces such as Typer and PubObject are meant to limit modification scope or require minimal ActivityStream compatibility to be used by this library. As long as the go-fed/activity/vocab or go-fed/activity/streams packages are being used, these interfaces will be natively supported.

Other Considerations

This library does not have an implementation report generated... yet! Once it is available, it will be linked here. Furthermore, the test server will also be an excellent tutorial resource. Unfortunately such a resource does not exist... yet!

Documentation

Overview

Package pub provides generic support for the ActivityPub Social API and Federation Protocol for client-to-server and server-to-server interactions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Application

type Application interface {
	// Owns returns true if the provided id is owned by this server.
	Owns(c context.Context, id *url.URL) bool
	// Get fetches the ActivityStream representation of the given id.
	Get(c context.Context, id *url.URL, rw RWType) (PubObject, error)
	// GetAsVerifiedUser fetches the ActivityStream representation of the
	// given id with the provided IRI representing the authenticated user
	// making the request.
	GetAsVerifiedUser(c context.Context, id, authdUser *url.URL, rw RWType) (PubObject, error)
	// Has determines if the server already knows about the object or
	// Activity specified by the given id.
	Has(c context.Context, id *url.URL) (bool, error)
	// Set should write or overwrite the value of the provided object for
	// its 'id'.
	Set(c context.Context, o PubObject) 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.
	GetInbox(c context.Context, r *http.Request, rw RWType) (vocab.OrderedCollectionType, 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.
	GetOutbox(c context.Context, r *http.Request, rw RWType) (vocab.OrderedCollectionType, error)
	// NewId takes in a client id token and returns an ActivityStreams IRI
	// id for a new Activity posted to the outbox. The object is provided
	// as a Typer so clients can use it to decide how to generate the IRI.
	NewId(c context.Context, t Typer) *url.URL
	// GetPublicKey fetches the public key for a user based on the public
	// key id. It also determines which algorithm to use to verify the
	// signature.
	GetPublicKey(c context.Context, publicKeyId string) (pubKey crypto.PublicKey, algo httpsig.Algorithm, user *url.URL, err error)
	// CanAdd returns true if the provided object is allowed to be added to
	// the given target collection. Applicable to either or both of the
	// SocialAPI and FederateAPI.
	CanAdd(c context.Context, o vocab.ObjectType, t vocab.ObjectType) bool
	// CanRemove returns true if the provided object is allowed to be
	// removed from the given target collection. Applicable to either or
	// both of the SocialAPI and FederateAPI.
	CanRemove(c context.Context, o vocab.ObjectType, t vocab.ObjectType) bool
}

Application is provided by users of this library in order to implement a social-federative-web application.

The contexts provided in these calls are passed through this library without modification, allowing implementations to pass-through request-scoped data in order to properly handle the request.

type Callbacker

type Callbacker interface {
	// Create Activity callback.
	Create(c context.Context, s *streams.Create) error
	// Update Activity callback.
	Update(c context.Context, s *streams.Update) error
	// Delete Activity callback.
	Delete(c context.Context, s *streams.Delete) error
	// Add Activity callback.
	Add(c context.Context, s *streams.Add) error
	// Remove Activity callback.
	Remove(c context.Context, s *streams.Remove) error
	// Like Activity callback.
	Like(c context.Context, s *streams.Like) error
	// Block Activity callback. By default, this implmentation does not
	// dictate how blocking should be implemented, so it is up to the
	// application to enforce this by implementing the FederateApp
	// interface.
	Block(c context.Context, s *streams.Block) error
	// Follow Activity callback. In the special case of server-to-server
	// delivery of a Follow activity, this implementation supports the
	// option of automatically replying with an 'Accept', 'Reject', or
	// waiting for human interaction as provided in the FederateApp
	// interface.
	//
	// In the special case that the FederateApp returned AutomaticAccept,
	// this library automatically handles adding the 'actor' to the
	// 'followers' collection of the 'object'.
	Follow(c context.Context, s *streams.Follow) error
	// Undo Activity callback. It is up to the client to provide support
	// for all 'Undo' operations; this implementation does not attempt to
	// provide a generic implementation.
	Undo(c context.Context, s *streams.Undo) error
	// Accept Activity callback. In the special case that this 'Accept'
	// activity has an 'object' of 'Follow' type, then the library will
	// handle adding the 'actor' to the 'following' collection of the
	// original 'actor' who requested the 'Follow'.
	Accept(c context.Context, s *streams.Accept) error
	// Reject Activity callback. Note that in the special case that this
	// 'Reject' activity has an 'object' of 'Follow' type, then the client
	// MUST NOT add the 'actor' to the 'following' collection of the
	// original 'actor' who requested the 'Follow'.
	Reject(c context.Context, s *streams.Reject) error
}

Callbacker provides an Application hooks into the lifecycle of the ActivityPub processes for both client-to-server and server-to-server interactions. These callbacks are called after their spec-compliant actions are completed, but before inbox forwarding and before delivery.

Note that at minimum, for inbox forwarding to work correctly, these Activities must be stored in the client application as a system of record.

Note that modifying the ActivityStream objects in a callback may cause unintentionally non-standard behavior if modifying core attributes, but otherwise affords clients powerful flexibility. Use responsibly.

type Clock

type Clock interface {
	Now() time.Time
}

Clock determines the time.

type Deliverer

type Deliverer interface {
	// Do schedules a message to be sent to a specific URL endpoint by
	// using toDo.
	Do(b []byte, to *url.URL, toDo func(b []byte, u *url.URL) error)
}

Deliverer schedules federated ActivityPub messages for delivery, possibly asynchronously.

type FederateAPI

type FederateAPI interface {
	// OnFollow determines whether to take any automatic reactions in
	// response to this follow. Note that if this application does not own
	// an object on the activity, then the 'AutomaticAccept' and
	// 'AutomaticReject' results will behave as if they were 'DoNothing'.
	OnFollow(c context.Context, s *streams.Follow) FollowResponse
	// Unblocked should return an error if the provided actor ids are not
	// able to interact with this particular end user due to being blocked
	// or other application-specific logic. This error is passed
	// transparently back to the request thread via PostInbox.
	//
	// If nil error is returned, then the received activity is processed as
	// a normal unblocked interaction.
	Unblocked(c context.Context, actorIRIs []*url.URL) error
	// FilterForwarding is invoked when a received activity needs to be
	// forwarded to specific inboxes owned by this server in order to avoid
	// the ghost reply problem. The IRIs provided are collections owned by
	// this server that the federate peer requested inbox forwarding to.
	//
	// Implementors must apply some sort of filtering to the provided IRI
	// collections. Without any filtering, the resulting application is
	// vulnerable to becoming a spam bot for a malicious federate peer.
	// Typical implementations will filter the iris down to be only the
	// follower collections owned by the actors targeted in the activity.
	FilterForwarding(c context.Context, activity vocab.ActivityType, iris []*url.URL) ([]*url.URL, error)
	// NewSigner returns a new httpsig.Signer for which deliveries can be
	// signed by the actor delivering the Activity. Let me take this moment
	// to really level with you, dear anonymous reader-of-documentation. You
	// want to use httpsig.RSA_SHA256 as the algorithm. Otherwise, your app
	// will not federate correctly and peers will reject the signatures. All
	// other known implementations using HTTP Signatures use RSA_SHA256,
	// hardcoded just like your implementation will be.
	//
	// Some people might think it funny to split the federation and use
	// their own algorithm. And while I give you the power to build the
	// largest foot-gun possible to blow away your limbs because I respect
	// your freedom, you as a developer have the responsibility to also be
	// cognizant of the wider community you are building for. Don't be a
	// dick.
	//
	// The headers available for inclusion in the signature are:
	//     Date
	//     User-Agent
	NewSigner() (httpsig.Signer, error)
	// PrivateKey fetches the private key and its associated public key ID.
	// The given URL is the inbox or outbox for the actor whose key is
	// needed.
	PrivateKey(boxIRI *url.URL) (privKey crypto.PrivateKey, pubKeyId string, err error)
}

FederateAPI is provided by users of this library and designed to handle receiving messages from ActivityPub servers through the Federative API.

type FederateApplication

type FederateApplication interface {
	Application
	FederateAPI
}

FederateApp is an implementation only for the Federating API part of the ActivityPub specification.

type FollowResponse

type FollowResponse int

FollowResponse instructs how to proceed upon immediately receiving a request to follow.

const (
	AutomaticAccept FollowResponse = iota
	AutomaticReject
	DoNothing
)

type HandlerFunc

type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request) (bool, error)

HandlerFunc returns true if it was able to handle the request as an ActivityPub request. If it handled the request then the error should be checked. The response will have already been written to when handled and there was no error. Client applications can freely choose how to handle the request if this function does not handle it.

Note that if the handler attempted to handle the request but returned an error, it is up to the client application to determine what headers and response to send to the requester.

func ServeActivityPubObject

func ServeActivityPubObject(a Application, clock Clock) HandlerFunc

ServeActivityPubObject will serve the ActivityPub object with the given IRI in the request. Note that requests may be signed with HTTP signatures or be permitted without any authentication scheme. To change this default behavior, use ServeActivityPubObjectWithVerificationMethod instead.

func ServeActivityPubObjectWithVerificationMethod

func ServeActivityPubObjectWithVerificationMethod(a Application, clock Clock, verifierFn func(context.Context) SocialAPIVerifier) HandlerFunc

ServeActivityPubObjectWithVerificationMethod will serve the ActivityPub object with the given IRI in the request. The rules for accessing the data are governed by the SocialAPIVerifier's behavior and may permit accessing data without having any credentials in the request.

type HttpClient

type HttpClient interface {
	Do(req *http.Request) (*http.Response, error)
}

HttpClient sends http requests.

type PubObject

type PubObject interface {
	vocab.Serializer
	Typer
	GetId() *url.URL
	SetId(*url.URL)
	HasId() bool
	AppendType(interface{})
	RemoveType(int)
}

PubObject is an ActivityPub Object.

func ToPubObject

func ToPubObject(m map[string]interface{}) (t []PubObject, e error)

ToPubObject transforms a json-deserialized ActivityStream object into a PubObject for use with the pub library. Note that for an object to be an ActivityPub object, it must have an 'id' and at least one 'type'.

type Pubber

type Pubber 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.
	//
	// 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.
	PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (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.
	//
	// 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.
	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.
	//
	// 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.
	PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (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.
	GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
}

Pubber provides methods for interacting with ActivityPub clients and ActivityPub federating servers.

func NewFederatingPubber

func NewFederatingPubber(clock Clock, app FederateApplication, cb Callbacker, d Deliverer, client HttpClient, userAgent string, maxDeliveryDepth, maxForwardingDepth int) Pubber

NewFederatingPubber provides a Pubber that implements only the Federating API in ActivityPub.

func NewPubber

func NewPubber(clock Clock, app SocialFederateApplication, client, server Callbacker, d Deliverer, httpClient HttpClient, userAgent string, maxDeliveryDepth, maxForwardingDepth int) Pubber

NewPubber provides a Pubber that implements both the Social API and the Federating API in ActivityPub.

func NewSocialPubber

func NewSocialPubber(clock Clock, app SocialApplication, cb Callbacker) Pubber

NewSocialPubber provides a Pubber that implements only the Social API in ActivityPub.

type RWType

type RWType int

RWType indicates the kind of reading being done.

const (
	// Read indicates the object is only being read.
	Read RWType = iota
	// ReadWrite indicates the object is being mutated as well.
	ReadWrite
)

type SocialAPI

type SocialAPI interface {
	// ActorIRI returns the actor's IRI associated with the given request.
	ActorIRI(c context.Context, r *http.Request) (*url.URL, error)
	// GetSocialAPIVerifier returns the authentication mechanism used for
	// incoming ActivityPub client requests. It is optional and allowed to
	// return null.
	//
	// Note that regardless of what this implementation returns, HTTP
	// Signatures is supported natively as a fallback.
	GetSocialAPIVerifier(c context.Context) SocialAPIVerifier
	// GetPublicKeyForOutbox fetches the public key for a user based on the
	// public key id. It also determines which algorithm to use to verify
	// the signature.
	//
	// Note that a key difference from Application's GetPublicKey is that
	// this function must make sure that the actor whose boxIRI is passed in
	// matches the public key id that is requested, or return an error.
	GetPublicKeyForOutbox(c context.Context, publicKeyId string, boxIRI *url.URL) (crypto.PublicKey, httpsig.Algorithm, error)
}

SocialAPI is provided by users of this library and designed to handle receiving messages from ActivityPub clients through the Social API.

type SocialAPIVerifier

type SocialAPIVerifier interface {
	// Verify will determine the authenticated user for the given request,
	// returning false if verification fails. If the request is entirely
	// missing the required fields in order to authenticate, this function
	// must return nil and false for all values to permit attempting
	// validation by HTTP Signatures. If there was an internal error
	// determining the authentication of the request, it is returned.
	//
	// Return values are interpreted as follows:
	//     (userFoo, true,  true,  <nil>) => userFoo passed authentication and is authorized
	//     (<any>,   true,  false, <nil>) => a user passed authentication but failed authorization (Permission denied)
	//     (<any>,   false, false, <nil>) => authentication failed: deny access (Bad request)
	//     (<nil>,   false, true,  <nil>) => authentication failed: must pass HTTP Signature verification or will be Permission Denied
	//     (<nil>,   true,  true,  <nil>) => "I don't care, try to validate using HTTP Signatures. If that still doesn't work, permit raw requests access anyway."
	//     (<any>,   <any>, <any>, error) => an internal error occurred during validation
	//
	// Be very careful that the 'authenticatedUser' value is non-nil when
	// returning 'authn' and 'authz' values of true, or else the library
	// will use the most permissive logic instead of the most restrictive as
	// outlined above.
	Verify(r *http.Request) (authenticatedUser *url.URL, authn, authz bool, err error)
	// VerifyForOutbox is the same as Verify, except that the request must
	// authenticate the owner of the provided outbox IRI.
	//
	// Return values are interpreted as follows:
	//     (true,  true,   <nil>) => user for the outbox passed authentication and is authorized
	//     (true,  false,  <nil>) => a user passed authentication but failed authorization for this outbox (Permission denied)
	//     (false, true,   <nil>) => authentication failed: must pass HTTP Signature verification or will be Permission Denied
	//     (false, false,  <nil>) => authentication failed: deny access (Bad request)
	//     (<any>, <any>,  error) => an internal error occurred during validation
	VerifyForOutbox(r *http.Request, outbox *url.URL) (authn, authz bool, err error)
}

SocialAPIVerifier will verify incoming requests from clients and is meant to encapsulate authentication functionality by standards such as OAuth (RFC 6749).

type SocialApplication

type SocialApplication interface {
	Application
	SocialAPI
}

SocialApp is an implementation only for the Social API part of the ActivityPub specification.

type SocialFederateApplication

type SocialFederateApplication interface {
	Application
	SocialAPI
	FederateAPI
}

SocialFederateApplication is an implementation for both the Social API and the Federating API parts of the ActivityPub specification.

type Typer

type Typer interface {
	vocab.Typer
}

Typer is an object that has a type.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL