Add function to serve ActivityPub object.

This also incorporates some interface changes to permit both HTTP
Signatures and other forms of authentication and authorization when
accessing data. This means support for something like OAuth2 should be
doable in conjunction with HTTP Signatures.

Tests are broken; this commit should not be used as a build point.
このコミットが含まれているのは:
Cory Slep 2018-05-25 00:23:50 +02:00
コミット 2986f7601b
4個のファイルの変更219行の追加35行の削除

ファイルの表示

@ -21,18 +21,8 @@ var (
ErrTargetRequired = errors.New("target property required")
)
// TODO: Helper http Handler for serving ActivityStream objects (handle optional HTTP sigs as well)
// TODO: Helper http Handler for serving Tombstone objects
// TODO: Helper http Handler for serving deleted objects
// TODO: Helper http Handler for serving actor's likes
// TODO: Helper http Handler for serving actor's followers
// TODO: Helper http Handler for serving actor's following
// TODO: Helper for sending arbitrary ActivityPub objects.
// TODO: Authenticate server-to-server deliveries.
// Pubber provides methods for interacting with ActivityPub clients and
// ActivityPub federating servers.
type Pubber interface {
@ -229,7 +219,7 @@ func (f *federator) GetInbox(c context.Context, w http.ResponseWriter, r *http.R
if err != nil {
return true, err
}
w.Header().Set(contentTypeHeader, responseContentTypeHeader)
addResponseHeaders(w.Header(), f.Clock, b)
w.WriteHeader(http.StatusOK)
n, err := w.Write(b)
if err != nil {
@ -248,19 +238,35 @@ func (f *federator) PostOutbox(c context.Context, w http.ResponseWriter, r *http
w.WriteHeader(http.StatusMethodNotAllowed)
return true, nil
}
v, err := httpsig.NewVerifier(r)
if err != nil {
w.WriteHeader(http.StatusForbidden)
return true, nil
authenticated := false
authorized := false
if verifier := f.SocialApp.GetSocialAPIVerifier(); verifier != nil {
authenticated, authorized, err := verifier.VerifyForOutbox(r, *r.URL)
if err != nil {
return true, err
} else if authenticated && !authorized {
w.WriteHeader(http.StatusForbidden)
return true, nil
} else if !authenticated && !authorized {
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
}
pk, algo, err := f.SocialApp.GetPublicKey(c, v.KeyId(), *r.URL)
if err != nil {
return true, err
}
err = v.Verify(pk, algo)
if err != nil {
w.WriteHeader(http.StatusForbidden)
return true, nil
if !!authenticated && authorized {
v, err := httpsig.NewVerifier(r)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
pk, algo, err := f.SocialApp.GetPublicKeyForOutbox(c, v.KeyId(), *r.URL)
if err != nil {
return true, err
}
err = v.Verify(pk, algo)
if err != nil {
w.WriteHeader(http.StatusForbidden)
return true, nil
}
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -328,7 +334,7 @@ func (f *federator) GetOutbox(c context.Context, w http.ResponseWriter, r *http.
if err != nil {
return true, err
}
w.Header().Set(contentTypeHeader, responseContentTypeHeader)
addResponseHeaders(w.Header(), f.Clock, b)
w.WriteHeader(http.StatusOK)
n, err := w.Write(b)
if err != nil {
@ -478,11 +484,7 @@ func (f *federator) handleClientUpdate(c context.Context, rawJson map[string]int
if err != nil {
return err
}
obj, ok := pObj.(vocab.Serializer)
if !ok {
return fmt.Errorf("PubObject is not vocab.Serializer: %T", pObj)
}
m, err := obj.Serialize()
m, err := pObj.Serialize()
if err != nil {
return err
}

116
pub/handlers.go ノーマルファイル
ファイルの表示

@ -0,0 +1,116 @@
package pub
import (
"context"
"crypto"
"encoding/json"
"fmt"
"github.com/go-fed/httpsig"
"net/http"
"net/url"
)
// ServeActivityPubObject will serve the ActivityPub object with the given IRI
// in the request. Note that requests must be signed with HTTP signatures or
// else they will be denied access. To explicitly opt out of this protection,
// use ServeActivityPubObjectWithVerificationMethod instead.
func ServeActivityPubObject(c context.Context, a Application, clock Clock, w http.ResponseWriter, r *http.Request) (handled bool, err error) {
return serveActivityPubObject(c, a, clock, w, r, nil)
}
// 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.
func ServeActivityPubObjectWithVerificationMethod(c context.Context, a Application, clock Clock, w http.ResponseWriter, r *http.Request, verifier SocialAPIVerifier) (handled bool, err error) {
return serveActivityPubObject(c, a, clock, w, r, verifier)
}
func serveActivityPubObject(c context.Context, a Application, clock Clock, w http.ResponseWriter, r *http.Request, verifier SocialAPIVerifier) (handled bool, err error) {
handled = isActivityPubGet(r)
if !handled {
return
}
id := *r.URL
if !a.Owns(c, id) {
w.WriteHeader(http.StatusNotFound)
return
}
var verifiedUser *url.URL
authenticated := false
authorized := false
if verifier != nil {
verifiedUser, authenticated, authorized, err = verifier.Verify(r)
if err != nil {
return
} else if authenticated && !authorized {
w.WriteHeader(http.StatusForbidden)
return
} else if !authenticated && !authorized {
w.WriteHeader(http.StatusBadRequest)
return
} else if !authenticated && authorized {
// Protect against bad implementations.
if verifiedUser != nil {
verifiedUser = nil
}
}
}
if verifiedUser == nil {
var v httpsig.Verifier
v, err = httpsig.NewVerifier(r)
if err != nil { // Unsigned request
if !authenticated {
w.WriteHeader(http.StatusBadRequest)
err = nil
return
}
} else { // Signed request
var publicKey crypto.PublicKey
var algo httpsig.Algorithm
var user url.URL
publicKey, algo, user, err = a.GetPublicKey(c, v.KeyId())
if err != nil {
return
}
err = v.Verify(publicKey, algo)
if err != nil && !authenticated {
w.WriteHeader(http.StatusForbidden)
err = nil
return
} else if err == nil {
verifiedUser = &user
}
}
}
var pObj PubObject
if verifiedUser != nil {
pObj, err = a.GetAsVerifiedUser(c, *r.URL, *verifiedUser)
} else {
pObj, err = a.Get(c, *r.URL)
}
if err != nil {
return
}
var m map[string]interface{}
m, err = pObj.Serialize()
if err != nil {
return
}
addJSONLDContext(m)
var b []byte
b, err = json.Marshal(m)
if err != nil {
return
}
addResponseHeaders(w.Header(), clock, b)
w.WriteHeader(http.StatusOK)
n, err := w.Write(b)
if err != nil {
return
} else if n != len(b) {
err = fmt.Errorf("ResponseWriter.Write wrote %d of %d bytes", n, len(b))
return
}
return
}

ファイルの表示

@ -32,6 +32,35 @@ type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// SocialAPIVerifier will verify incoming requests from clients and is meant to
// encapsulate authentication functionality by standards such as OAuth (RFC
// 6749).
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.
VerifyForOutbox(r *http.Request, outbox url.URL) (authn, authz bool, err error)
}
// Application is provided by users of this library in order to implement a
// social-federative-web application.
//
@ -43,6 +72,10 @@ type Application interface {
Owns(c context.Context, id url.URL) bool
// Get fetches the ActivityStream representation of the given id.
Get(c context.Context, id url.URL) (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) (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)
@ -61,17 +94,15 @@ type Application interface {
// 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)
}
// SocialApp is provided by users of this library and designed to handle
// receiving messages from ActivityPub clients through the Social API.
type SocialApp interface {
// 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. The application must make sure that the actor whose boxIRI
// is passed in matches the public key id that is requested, or return
// an error.
GetPublicKey(c context.Context, publicKeyId string, boxIRI url.URL) (crypto.PublicKey, httpsig.Algorithm, error)
// CanAdd returns true if the provided object is allowed to be added to
// the given target collection.
CanAdd(c context.Context, o vocab.ObjectType, t vocab.ObjectType) bool
@ -81,6 +112,21 @@ type SocialApp interface {
// AddToOutboxResolver(c context.Context) (*streams.Resolver, error)
// 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() 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)
}
// FederateApp is provided by users of this library and designed to handle
@ -216,6 +262,7 @@ type Deliverer interface {
// PubObject is an ActivityPub Object.
type PubObject interface {
vocab.Serializer
Typer
GetId() url.URL
SetId(url.URL)

ファイルの表示

@ -4,6 +4,8 @@ import (
"bytes"
"context"
"crypto"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-fed/activity/streams"
@ -21,12 +23,16 @@ const (
responseContentTypeHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
getAcceptHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
contentTypeHeader = "Content-Type"
dateHeader = "Date"
digestHeader = "Digest"
acceptHeader = "Accept"
publicActivityPub = "https://www.w3.org/ns/activitystreams#Public"
publicJsonLD = "Public"
publicJsonLDAS = "as:Public"
jsonLDContext = "@context"
activityPubContext = "https://www.w3.org/ns/activitystreams"
sha256Digest = "SHA-256"
digestDelimiter = "="
)
var alternatives = []string{"application/activity+json"}
@ -69,6 +75,19 @@ func addJSONLDContext(m map[string]interface{}) {
m[jsonLDContext] = activityPubContext
}
func addResponseHeaders(h http.Header, c Clock, responseContent []byte) {
h.Set(contentTypeHeader, responseContentTypeHeader)
// 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())
}
// dereference makes an HTTP GET request to an IRI in order to obtain the
// ActivityStream representation.
//