activity/pub/internal.go
Cory Slep 1c057922ca Fix intransitive activity generated code in vocab.
The sub-types of Intransitive Activity (Arrive, etc) would not satisfy
the vocab.IntransitiveActivityType interface, due to not accounting for
the parent WithoutProperties definition.

This fixes that code generation, so that all Activity subtypes will be
able to be properly converted to vocab.ActivityType or
vocab.IntransitiveActivityType.

Updated the PostOutbox code path to properly handle this distinction
when receiving a C2S IntransitiveActivity.
2018-08-20 23:08:02 +02:00

2201 行
59 KiB
Go

package pub
import (
"bytes"
"context"
"crypto"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/vocab"
"github.com/go-fed/httpsig"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
const (
postContentTypeHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
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 mediaTypes []string
func init() {
mediaTypes = []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\""} {
mediaTypes = append(mediaTypes, fmt.Sprintf("%s%s%s", jsonLdType, semi, profile))
}
}
}
func trimAll(s []string) []string {
var r []string
for _, e := range s {
r = append(r, strings.Trim(e, " "))
}
return r
}
func headerIsActivityPubMediaType(header string) bool {
for _, mediaType := range mediaTypes {
if strings.Contains(header, mediaType) {
return true
}
}
return false
}
func isActivityPubPost(r *http.Request) bool {
return r.Method == "POST" && headerIsActivityPubMediaType(r.Header.Get(contentTypeHeader))
}
func isActivityPubGet(r *http.Request) bool {
return r.Method == "GET" && headerIsActivityPubMediaType(r.Header.Get(acceptHeader))
}
// isPublic determines if a target is the Public collection as defined in the
// spec, including JSON-LD compliant collections.
func isPublic(s string) bool {
return s == publicActivityPub || s == publicJsonLD || s == publicJsonLDAS
}
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.
//
// creds is allowed to be nil.
func dereference(c HttpClient, u *url.URL, agent string, creds *creds, clock Clock) ([]byte, error) {
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Add(acceptHeader, getAcceptHeader)
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s (go-fed ActivityPub)", agent))
if creds != nil {
err := creds.signer.SignRequest(creds.privKey, creds.pubKeyId, req)
creds.privKey = nil
if err != nil {
return nil, err
}
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Request to %s failed (%d): %s", u.String(), resp.StatusCode, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}
type creds struct {
signer httpsig.Signer
privKey crypto.PrivateKey
pubKeyId string
}
// dereferenceAsUser is meant to be used by the activity handlers that need to
// handle IRI use cases where objects are expected. Returns an error if not
// federating.
func (f *federator) dereferenceAsUser(boxIRI, fetchIRI *url.URL) (obj vocab.ObjectType, err error) {
if !f.EnableServer {
err = fmt.Errorf("cannot dereference iri as user if not federating: %q", fetchIRI)
return
}
creds := &creds{}
creds.signer, err = f.FederateAPI.NewSigner()
if err != nil {
return
}
creds.privKey, creds.pubKeyId, err = f.FederateAPI.PrivateKey(boxIRI)
if err != nil {
return
}
resp, err := dereference(f.Client, fetchIRI, f.Agent, creds, f.Clock)
if err != nil {
return
}
var m map[string]interface{}
if err = json.Unmarshal(resp, &m); err != nil {
return
}
return toAnyObject(m)
}
// postToOutbox will attempt to send a POST request to the given URL with the
// body set to the provided bytes.
//
// creds is able to be nil.
func postToOutbox(c HttpClient, b []byte, to *url.URL, agent string, creds *creds, clock Clock) error {
byteCopy := make([]byte, len(b))
copy(byteCopy, b)
buf := bytes.NewBuffer(byteCopy)
req, err := http.NewRequest("POST", to.String(), buf)
if err != nil {
return err
}
req.Header.Add(contentTypeHeader, postContentTypeHeader)
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s (go-fed ActivityPub)", agent))
if creds != nil {
err := creds.signer.SignRequest(creds.privKey, creds.pubKeyId, req)
creds.privKey = nil
if err != nil {
return err
}
}
resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Request to %s failed (%d): %s", to.String(), resp.StatusCode, resp.Status)
}
return nil
}
// addNewIds will add new IDs not just for an activity, but all objects
// contained within the activity if it is a Create activity.
func (f *federator) addNewIds(c context.Context, a vocab.ActivityType) {
newId := f.App.NewId(c, a)
a.SetId(newId)
if vocab.HasTypeCreate(a) {
for i := 0; i < a.ObjectLen(); i++ {
if a.IsObject(i) {
obj := a.GetObject(i)
obj.SetId(f.App.NewId(c, obj))
}
}
}
}
// addNewIdsIntransitive will add new IDs for an intransitive activity.
func (f *federator) addNewIdsIntransitive(c context.Context, a vocab.IntransitiveActivityType) {
newId := f.App.NewId(c, a)
a.SetId(newId)
}
// 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 (f *federator) wrapInCreate(o vocab.ObjectType, actor *url.URL) (c *vocab.Create, err error) {
c = &vocab.Create{}
c.AppendType("Create")
c.AppendObject(o)
c.AppendActorIRI(actor)
if o.IsPublished() {
c.SetPublished(o.GetPublished())
}
for i := 0; i < o.ToLen(); i++ {
var to *url.URL
if o.IsToObject(i) {
obj := o.GetToObject(i)
if !obj.HasId() {
err = fmt.Errorf("to object missing id")
return
}
to = obj.GetId()
} else if o.IsToLink(i) {
href := o.GetToLink(i)
if !href.HasHref() {
err = fmt.Errorf("to link missing href")
return
}
to = href.GetHref()
} else if o.IsToIRI(i) {
to = o.GetToIRI(i)
}
c.AppendToIRI(to)
}
for i := 0; i < o.BtoLen(); i++ {
var bto *url.URL
if o.IsBtoObject(i) {
obj := o.GetBtoObject(i)
if !obj.HasId() {
err = fmt.Errorf("bto object missing id")
return
}
bto = obj.GetId()
} else if o.IsBtoLink(i) {
href := o.GetBtoLink(i)
if !href.HasHref() {
err = fmt.Errorf("bto link missing href")
return
}
bto = href.GetHref()
} else if o.IsBtoIRI(i) {
bto = o.GetBtoIRI(i)
}
c.AppendBtoIRI(bto)
}
for i := 0; i < o.CcLen(); i++ {
var cc *url.URL
if o.IsCcObject(i) {
obj := o.GetCcObject(i)
if !obj.HasId() {
err = fmt.Errorf("cc object missing id")
return
}
cc = obj.GetId()
} else if o.IsCcLink(i) {
href := o.GetCcLink(i)
if !href.HasHref() {
err = fmt.Errorf("cc link missing href")
return
}
cc = href.GetHref()
} else if o.IsCcIRI(i) {
cc = o.GetCcIRI(i)
}
c.AppendCcIRI(cc)
}
for i := 0; i < o.BccLen(); i++ {
var bcc *url.URL
if o.IsBccObject(i) {
obj := o.GetBccObject(i)
if !obj.HasId() {
err = fmt.Errorf("bcc object missing id")
return
}
bcc = obj.GetId()
} else if o.IsBccLink(i) {
href := o.GetBccLink(i)
if !href.HasHref() {
err = fmt.Errorf("bcc link missing href")
return
}
bcc = href.GetHref()
} else if o.IsBccIRI(i) {
bcc = o.GetBccIRI(i)
}
c.AppendBccIRI(bcc)
}
for i := 0; i < o.AudienceLen(); i++ {
var audience *url.URL
if o.IsAudienceObject(i) {
obj := o.GetAudienceObject(i)
if !obj.HasId() {
err = fmt.Errorf("audience object missing id")
return
}
audience = obj.GetId()
} else if o.IsAudienceLink(i) {
href := o.GetAudienceLink(i)
if !href.HasHref() {
err = fmt.Errorf("audience link missing href")
return
}
audience = href.GetHref()
} else if o.IsAudienceIRI(i) {
audience = o.GetAudienceIRI(i)
}
c.AppendAudienceIRI(audience)
}
return
}
// 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.
//
// If there is any disagreement between the activity and an object, we default
// to a no-op.
func (f *federator) sameRecipients(a vocab.ActivityType) error {
// First, map recipients for each object and the activity.
to := make([]map[string]interface{}, a.ObjectLen())
for i := 0; i < a.ObjectLen(); i++ {
to[i] = make(map[string]interface{})
if !a.IsObject(i) {
return fmt.Errorf("sameRecipients does not support 'to' object IRIs on Activities")
}
o := a.GetObject(i)
for j := 0; j < o.ToLen(); j++ {
if o.IsToObject(j) {
id := o.GetToObject(j).GetId()
to[i][id.String()] = o.GetToObject(j)
} else if o.IsToLink(j) {
id := o.GetToLink(j).GetHref()
to[i][id.String()] = o.GetToLink(j)
} else if o.IsToIRI(j) {
id := o.GetToIRI(j)
to[i][id.String()] = id
}
}
}
toActivity := make(map[string]interface{})
for i := 0; i < a.ToLen(); i++ {
if a.IsToObject(i) {
id := a.GetToObject(i).GetId()
toActivity[id.String()] = a.GetToObject(i)
} else if a.IsToLink(i) {
id := a.GetToLink(i).GetHref()
toActivity[id.String()] = a.GetToLink(i)
} else if a.IsToIRI(i) {
id := a.GetToIRI(i)
toActivity[id.String()] = id
}
}
bto := make([]map[string]interface{}, a.ObjectLen())
for i := 0; i < a.ObjectLen(); i++ {
bto[i] = make(map[string]interface{})
if !a.IsObject(i) {
return fmt.Errorf("sameRecipients does not support 'bto' object IRIs on Activities")
}
o := a.GetObject(i)
for j := 0; j < o.BtoLen(); j++ {
if o.IsBtoObject(j) {
id := o.GetBtoObject(j).GetId()
bto[i][id.String()] = o.GetBtoObject(j)
} else if o.IsBtoLink(j) {
id := o.GetBtoLink(j).GetHref()
bto[i][id.String()] = o.GetBtoLink(j)
} else if o.IsBtoIRI(j) {
id := o.GetBtoIRI(j)
bto[i][id.String()] = id
}
}
}
btoActivity := make(map[string]interface{})
for i := 0; i < a.BtoLen(); i++ {
if a.IsBtoObject(i) {
id := a.GetBtoObject(i).GetId()
btoActivity[id.String()] = a.GetBtoObject(i)
} else if a.IsBtoLink(i) {
id := a.GetBtoLink(i).GetHref()
btoActivity[id.String()] = a.GetBtoLink(i)
} else if a.IsBtoIRI(i) {
id := a.GetBtoIRI(i)
btoActivity[id.String()] = id
}
}
cc := make([]map[string]interface{}, a.ObjectLen())
for i := 0; i < a.ObjectLen(); i++ {
cc[i] = make(map[string]interface{})
if !a.IsObject(i) {
return fmt.Errorf("sameRecipients does not support 'cc' object IRIs on Activities")
}
o := a.GetObject(i)
for j := 0; j < o.CcLen(); j++ {
if o.IsCcObject(j) {
id := o.GetCcObject(j).GetId()
cc[i][id.String()] = o.GetCcObject(j)
} else if o.IsCcLink(j) {
id := o.GetCcLink(j).GetHref()
cc[i][id.String()] = o.GetCcLink(j)
} else if o.IsCcIRI(j) {
id := o.GetCcIRI(j)
cc[i][id.String()] = id
}
}
}
ccActivity := make(map[string]interface{})
for i := 0; i < a.CcLen(); i++ {
if a.IsCcObject(i) {
id := a.GetCcObject(i).GetId()
ccActivity[id.String()] = a.GetCcObject(i)
} else if a.IsCcLink(i) {
id := a.GetCcLink(i).GetHref()
ccActivity[id.String()] = a.GetCcLink(i)
} else if a.IsCcIRI(i) {
id := a.GetCcIRI(i)
ccActivity[id.String()] = id
}
}
bcc := make([]map[string]interface{}, a.ObjectLen())
for i := 0; i < a.ObjectLen(); i++ {
bcc[i] = make(map[string]interface{})
if !a.IsObject(i) {
return fmt.Errorf("sameRecipients does not support 'bcc' object IRIs on Activities")
}
o := a.GetObject(i)
for j := 0; j < o.BccLen(); j++ {
if o.IsBccObject(j) {
id := o.GetBccObject(j).GetId()
bcc[i][id.String()] = o.GetBccObject(j)
} else if o.IsBccLink(j) {
id := o.GetBccLink(j).GetHref()
bcc[i][id.String()] = o.GetBccLink(j)
} else if o.IsBccIRI(j) {
id := o.GetBccIRI(j)
bcc[i][id.String()] = id
}
}
}
bccActivity := make(map[string]interface{})
for i := 0; i < a.BccLen(); i++ {
if a.IsBccObject(i) {
id := a.GetBccObject(i).GetId()
bccActivity[id.String()] = a.GetBccObject(i)
} else if a.IsBccLink(i) {
id := a.GetBccLink(i).GetHref()
bccActivity[id.String()] = a.GetBccLink(i)
} else if a.IsBccIRI(i) {
id := a.GetBccIRI(i)
bccActivity[id.String()] = id
}
}
audience := make([]map[string]interface{}, a.ObjectLen())
for i := 0; i < a.ObjectLen(); i++ {
audience[i] = make(map[string]interface{})
if !a.IsObject(i) {
return fmt.Errorf("sameRecipients does not support 'audience' object IRIs on Activities")
}
o := a.GetObject(i)
for j := 0; j < o.AudienceLen(); j++ {
if o.IsAudienceObject(j) {
id := o.GetAudienceObject(j).GetId()
audience[i][id.String()] = o.GetAudienceObject(j)
} else if o.IsAudienceLink(j) {
id := o.GetAudienceLink(j).GetHref()
audience[i][id.String()] = o.GetAudienceLink(j)
} else if o.IsAudienceIRI(j) {
id := o.GetAudienceIRI(j)
audience[i][id.String()] = id
}
}
}
audienceActivity := make(map[string]interface{})
for i := 0; i < a.AudienceLen(); i++ {
if a.IsAudienceObject(i) {
id := a.GetAudienceObject(i).GetId()
audienceActivity[id.String()] = a.GetAudienceObject(i)
} else if a.IsAudienceLink(i) {
id := a.GetAudienceLink(i).GetHref()
audienceActivity[id.String()] = a.GetAudienceLink(i)
} else if a.IsAudienceIRI(i) {
id := a.GetAudienceIRI(i)
audienceActivity[id.String()] = id
}
}
// Next, add activity recipients to all objects if not already present
for k, v := range toActivity {
for i := 0; i < a.ObjectLen(); i++ {
if _, ok := to[i][k]; !ok {
var to *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("to object missing id")
}
to = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("to link missing href")
}
to = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
to = vIRI
}
a.GetObject(i).AppendToIRI(to)
}
}
}
for k, v := range btoActivity {
for i := 0; i < a.ObjectLen(); i++ {
if _, ok := bto[i][k]; !ok {
var bto *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("bto object missing id")
}
bto = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("bto link missing href")
}
bto = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
bto = vIRI
}
a.GetObject(i).AppendBtoIRI(bto)
}
}
}
for k, v := range ccActivity {
for i := 0; i < a.ObjectLen(); i++ {
if _, ok := cc[i][k]; !ok {
var cc *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("cc object missing id")
}
cc = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("cc link missing href")
}
cc = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
cc = vIRI
}
a.GetObject(i).AppendCcIRI(cc)
}
}
}
for k, v := range bccActivity {
for i := 0; i < a.ObjectLen(); i++ {
if _, ok := bcc[i][k]; !ok {
var bcc *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("bcc object missing id")
}
bcc = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("bcc link missing href")
}
bcc = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
bcc = vIRI
}
a.GetObject(i).AppendBccIRI(bcc)
}
}
}
for k, v := range audienceActivity {
for i := 0; i < a.ObjectLen(); i++ {
if _, ok := audience[i][k]; !ok {
var activity *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("activity object missing id")
}
activity = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("activity link missing href")
}
activity = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
activity = vIRI
}
a.GetObject(i).AppendAudienceIRI(activity)
}
}
}
// Finally, add all the objects' recipients to the activity if not
// already present.
for i := 0; i < a.ObjectLen(); i++ {
for k, v := range to[i] {
if _, ok := toActivity[k]; !ok {
var to *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("to object missing id")
}
to = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("to link missing href")
}
to = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
to = vIRI
}
a.AppendToIRI(to)
}
}
for k, v := range bto[i] {
if _, ok := btoActivity[k]; !ok {
var bto *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("bto object missing id")
}
bto = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("bto link missing href")
}
bto = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
bto = vIRI
}
a.AppendBtoIRI(bto)
}
}
for k, v := range cc[i] {
if _, ok := ccActivity[k]; !ok {
var cc *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("cc object missing id")
}
cc = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("cc link missing href")
}
cc = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
cc = vIRI
}
a.AppendCcIRI(cc)
}
}
for k, v := range bcc[i] {
if _, ok := bccActivity[k]; !ok {
var bcc *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("bcc object missing id")
}
bcc = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("bcc link missing href")
}
bcc = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
bcc = vIRI
}
a.AppendBccIRI(bcc)
}
}
for k, v := range audience[i] {
if _, ok := audienceActivity[k]; !ok {
var audience *url.URL
if vObj, ok := v.(vocab.ObjectType); ok {
if !vObj.HasId() {
return fmt.Errorf("audience object missing id")
}
audience = vObj.GetId()
} else if vLink, ok := v.(vocab.LinkType); ok {
if !vLink.HasHref() {
return fmt.Errorf("audience link missing href")
}
audience = vLink.GetHref()
} else if vIRI, ok := v.(*url.URL); ok {
audience = vIRI
}
a.AppendAudienceIRI(audience)
}
}
}
return nil
}
// TODO: (Section 7) HTTP caching mechanisms [RFC7234] SHOULD be respected when appropriate, both when receiving responses from other servers as well as sending responses to other servers.
// deliver will complete the peer-to-peer sending of a federated message to
// another server.
func (f *federator) deliver(obj vocab.ActivityType, boxIRI *url.URL) error {
recipients, err := f.prepare(boxIRI, obj)
if err != nil {
return err
}
creds := &creds{}
creds.signer, err = f.FederateAPI.NewSigner()
if err != nil {
return err
}
creds.privKey, creds.pubKeyId, err = f.FederateAPI.PrivateKey(boxIRI)
if err != nil {
return err
}
return f.deliverToRecipients(obj, recipients, creds)
}
// deliverToRecipients will take a prepared Activity and send it to specific
// recipients without examining the activity.
func (f *federator) deliverToRecipients(obj vocab.ActivityType, recipients []*url.URL, creds *creds) error {
m, err := obj.Serialize()
if err != nil {
return err
}
addJSONLDContext(m)
b, err := json.Marshal(m)
if err != nil {
return err
}
for _, to := range recipients {
f.deliverer.Do(b, to, func(b []byte, u *url.URL) error {
return postToOutbox(f.Client, b, u, f.Agent, creds, f.Clock)
})
}
return nil
}
// prepare takes a deliverableObject and returns a list of the proper recipient
// target URIs. Additionally, the deliverableObject will have any hidden
// hidden recipients ("bto" and "bcc") stripped from it.
func (c *federator) prepare(boxIRI *url.URL, o deliverableObject) ([]*url.URL, error) {
// Get inboxes of recipients
var r []*url.URL
r = append(r, getToIRIs(o)...)
r = append(r, getBToIRIs(o)...)
r = append(r, getCcIRIs(o)...)
r = append(r, getBccIRIs(o)...)
r = append(r, getAudienceIRIs(o)...)
// TODO: Support delivery to shared inbox
// 1. When an object is being delivered to the originating actor's
// followers, a server MAY reduce the number of receiving actors
// delivered to by identifying all followers which share the same
// sharedInbox who would otherwise be individual recipients and instead
// deliver objects to said sharedInbox.
// 2. If an object is addressed to the Public special collection, a
// server MAY deliver that object to all known sharedInbox endpoints on
// the network.
r = filterURLs(r, isPublic)
receiverActors, err := c.resolveInboxes(boxIRI, r, 0, c.MaxDeliveryDepth)
if err != nil {
return nil, err
}
targets, err := getInboxes(receiverActors)
if err != nil {
return nil, err
}
// Get inboxes of sender(s)
senderActors, err := c.resolveInboxes(boxIRI, getActorsAttributedToURI(o), 0, c.MaxDeliveryDepth)
if err != nil {
return nil, err
}
ignore, err := getInboxes(senderActors)
if err != nil {
return nil, err
}
// Post-processing
r = dedupeIRIs(targets, ignore)
stripHiddenRecipients(o)
return r, nil
}
// resolveInboxes takes a list of Actor id URIs and returns them as concrete
// instances of actorObject. It applies recursively when it encounters a target
// that is a Collection or OrderedCollection.
func (c *federator) resolveInboxes(boxIRI *url.URL, r []*url.URL, depth int, max int) ([]actor, error) {
if depth >= max {
return nil, nil
}
a := make([]actor, 0, len(r))
for _, u := range r {
// Do not retry here -- if a dereference fails, then fail the
// entire delivery.
actor, co, oc, cp, ocp, cr, err := c.dereferenceForResolvingInboxes(u, boxIRI, nil)
if err != nil {
return nil, err
}
var uris []*url.URL
if co != nil {
uris := getURIsInItemer(co.Raw())
actors, err := c.resolveInboxes(boxIRI, uris, depth+1, max)
if err != nil {
return nil, err
}
a = append(a, actors...)
} else if oc != nil {
uris := getURIsInOrderedItemer(oc.Raw())
actors, err := c.resolveInboxes(boxIRI, uris, depth+1, max)
if err != nil {
return nil, err
}
a = append(a, actors...)
} else if cp != nil {
cb := func(c vocab.CollectionPageType) error {
uris = getURIsInItemer(c)
return nil
}
err := doForCollectionPage(c.Client, c.Agent, cb, cp.Raw(), cr, c.Clock)
if err != nil {
return nil, err
}
actors, err := c.resolveInboxes(boxIRI, uris, depth+1, max)
if err != nil {
return nil, err
}
a = append(a, actors...)
} else if ocp != nil {
cb := func(c vocab.OrderedCollectionPageType) error {
uris = getURIsInOrderedItemer(c)
return nil
}
err := doForOrderedCollectionPage(c.Client, c.Agent, cb, ocp.Raw(), cr, c.Clock)
if err != nil {
return nil, err
}
actors, err := c.resolveInboxes(boxIRI, uris, depth+1, max)
if err != nil {
return nil, err
}
a = append(a, actors...)
} else if actor != nil {
a = append(a, actor)
}
}
return a, nil
}
func (c *federator) dereferenceForResolvingInboxes(u, boxIRI *url.URL, cr *creds) (actor actor, co *streams.Collection, oc *streams.OrderedCollection, cp *streams.CollectionPage, ocp *streams.OrderedCollectionPage, cred *creds, err error) {
// To pass back to calling function, since may be set recursively:
cred = cr
var resp []byte
resp, err = dereference(c.Client, u, c.Agent, cr, c.Clock)
if err != nil {
return
}
var m map[string]interface{}
if err = json.Unmarshal(resp, &m); err != nil {
return
}
// Set AT MOST one of: co, oc, cp, ocp
// If none of these are set, attempt to use actor
if err = toActorCollectionResolver(&actor, &co, &oc, &cp, &ocp).Deserialize(m); err != nil {
return
}
// 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.
//
// This jumps to the label above ONLY if we have not yet set the
// creds -- which happens at most once.
if (co != nil || oc != nil || cp != nil || ocp != nil) && cr == nil {
cr = &creds{}
cr.signer, err = c.FederateAPI.NewSigner()
if err != nil {
return
}
cr.privKey, cr.pubKeyId, err = c.FederateAPI.PrivateKey(boxIRI)
if err != nil {
return
}
return c.dereferenceForResolvingInboxes(u, boxIRI, cr)
}
return
}
// getInboxes extracts the 'inbox' IRIs from actors.
func getInboxes(a []actor) ([]*url.URL, error) {
var u []*url.URL
for _, actor := range a {
if actor.IsInboxAnyURI() {
u = append(u, actor.GetInboxAnyURI())
} else if actor.IsInboxOrderedCollection() {
oc := actor.GetInboxOrderedCollection()
if !oc.HasId() {
return nil, fmt.Errorf("actor inbox OrderedCollection has no IRI")
}
u = append(u, oc.GetId())
}
}
return u, nil
}
// getActorAttributedToURI attempts to find the URIs for the "actor" and
// "attributedTo" originators on the object.
func getActorsAttributedToURI(a actorObject) []*url.URL {
var u []*url.URL
for i := 0; i < a.AttributedToLen(); i++ {
if a.IsAttributedToObject(i) {
obj := a.GetAttributedToObject(i)
if obj.HasId() {
u = append(u, obj.GetId())
}
} else if a.IsAttributedToLink(i) {
l := a.GetAttributedToLink(i)
if l.HasHref() {
u = append(u, l.GetHref())
}
} else if a.IsAttributedToIRI(i) {
u = append(u, a.GetAttributedToIRI(i))
}
}
for i := 0; i < a.ActorLen(); i++ {
if a.IsActorObject(i) {
obj := a.GetActorObject(i)
if obj.HasId() {
u = append(u, obj.GetId())
}
} else if a.IsActorLink(i) {
l := a.GetActorLink(i)
if l.HasHref() {
u = append(u, l.GetHref())
}
} else if a.IsActorIRI(i) {
u = append(u, a.GetActorIRI(i))
}
}
return u
}
// stripHiddenRecipients removes "bto" and "bcc" from the deliverableObject.
// 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(o deliverableObject) {
for o.BtoLen() > 0 {
if o.IsBtoObject(0) {
o.RemoveBtoObject(0)
} else if o.IsBtoLink(0) {
o.RemoveBtoLink(0)
} else if o.IsBtoIRI(0) {
o.RemoveBtoIRI(0)
}
}
for o.BccLen() > 0 {
if o.IsBccObject(0) {
o.RemoveBccObject(0)
} else if o.IsBccLink(0) {
o.RemoveBccLink(0)
} else if o.IsBccIRI(0) {
o.RemoveBccIRI(0)
}
}
}
// dedupeIRIs will deduplicate final inbox IRIs. The ignore list is applied to
// the final list
func dedupeIRIs(recipients, ignored []*url.URL) (out []*url.URL) {
ignoredMap := make(map[string]bool, len(ignored))
for _, elem := range ignored {
ignoredMap[elem.String()] = true
}
outMap := make(map[string]bool, len(recipients))
for _, k := range recipients {
kStr := k.String()
if !ignoredMap[kStr] && !outMap[kStr] {
out = append(out, k)
outMap[kStr] = true
}
}
return
}
// dedupeOrderedItems will deduplicate the 'orderedItems' within an ordered
// collection type. Deduplication happens by simply examining the 'id'.
func (f *federator) dedupeOrderedItems(oc vocab.OrderedCollectionType) (vocab.OrderedCollectionType, error) {
i := 0
seen := make(map[string]bool, oc.OrderedItemsLen())
for i < oc.OrderedItemsLen() {
var id string
var removeFn func(int)
if oc.IsOrderedItemsObject(i) {
removeFn = oc.RemoveOrderedItemsObject
iri := oc.GetOrderedItemsObject(i).GetId()
id = iri.String()
} else if oc.IsOrderedItemsLink(i) {
removeFn = oc.RemoveOrderedItemsLink
iri := oc.GetOrderedItemsLink(i).GetId()
id = iri.String()
} else if oc.IsOrderedItemsIRI(i) {
removeFn = oc.RemoveOrderedItemsIRI
id = oc.GetOrderedItemsIRI(i).String()
}
if seen[id] {
removeFn(i)
} else {
seen[id] = true
i++
}
}
return oc, nil
}
// filterURLs removes urls whose strings match the provided filter
func filterURLs(u []*url.URL, fn func(s string) bool) []*url.URL {
i := 0
for i < len(u) {
if fn(u[i].String()) {
u = append(u[:i], u[i+1:]...)
} else {
i++
}
}
return u
}
func getToIRIs(o deliverableObject) []*url.URL {
var r []*url.URL
for i := 0; i < o.ToLen(); i++ {
if o.IsToObject(i) {
obj := o.GetToObject(i)
if obj.HasId() {
r = append(r, obj.GetId())
}
} else if o.IsToLink(i) {
l := o.GetToLink(i)
if l.HasHref() {
r = append(r, l.GetHref())
}
} else if o.IsToIRI(i) {
r = append(r, o.GetToIRI(i))
}
}
return r
}
func getBToIRIs(o deliverableObject) []*url.URL {
var r []*url.URL
for i := 0; i < o.BtoLen(); i++ {
if o.IsBtoObject(i) {
obj := o.GetBtoObject(i)
if obj.HasId() {
r = append(r, obj.GetId())
}
} else if o.IsBtoLink(i) {
l := o.GetBtoLink(i)
if l.HasHref() {
r = append(r, l.GetHref())
}
} else if o.IsBtoIRI(i) {
r = append(r, o.GetBtoIRI(i))
}
}
return r
}
func getCcIRIs(o deliverableObject) []*url.URL {
var r []*url.URL
for i := 0; i < o.CcLen(); i++ {
if o.IsCcObject(i) {
obj := o.GetCcObject(i)
if obj.HasId() {
r = append(r, obj.GetId())
}
} else if o.IsCcLink(i) {
l := o.GetCcLink(i)
if l.HasHref() {
r = append(r, l.GetHref())
}
} else if o.IsCcIRI(i) {
r = append(r, o.GetCcIRI(i))
}
}
return r
}
func getBccIRIs(o deliverableObject) []*url.URL {
var r []*url.URL
for i := 0; i < o.BccLen(); i++ {
if o.IsBccObject(i) {
obj := o.GetBccObject(i)
if obj.HasId() {
r = append(r, obj.GetId())
}
} else if o.IsBccLink(i) {
l := o.GetBccLink(i)
if l.HasHref() {
r = append(r, l.GetHref())
}
} else if o.IsBccIRI(i) {
r = append(r, o.GetBccIRI(i))
}
}
return r
}
func getAudienceIRIs(o deliverableObject) []*url.URL {
var r []*url.URL
for i := 0; i < o.AudienceLen(); i++ {
if o.IsAudienceObject(i) {
obj := o.GetAudienceObject(i)
if obj.HasId() {
r = append(r, obj.GetId())
}
} else if o.IsAudienceLink(i) {
l := o.GetAudienceLink(i)
if l.HasHref() {
r = append(r, l.GetHref())
}
} else if o.IsAudienceIRI(i) {
r = append(r, o.GetAudienceIRI(i))
}
}
return r
}
// doForCollectionPage applies a function over a collection and its subsequent
// pages recursively. It returns the first non-nil error it encounters.
func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionPageType) error, c vocab.CollectionPageType, creds *creds, clock Clock) error {
err := cb(c)
if err != nil {
return err
}
if c.IsNextCollectionPage() {
// Handle this one weird trick that other peers HATE federating
// with.
return doForCollectionPage(h, agent, cb, c.GetNextCollectionPage(), creds, clock)
} else if c.IsNextLink() {
l := c.GetNextLink()
if l.HasHref() {
u := l.GetHref()
resp, err := dereference(h, u, agent, creds, clock)
if err != nil {
return err
}
var m map[string]interface{}
err = json.Unmarshal(resp, &m)
if err != nil {
return err
}
next, err := toCollectionPage(m)
if err != nil {
return err
}
if next != nil {
return doForCollectionPage(h, agent, cb, next.Raw(), creds, clock)
}
}
} else if c.IsNextIRI() {
u := c.GetNextIRI()
resp, err := dereference(h, u, agent, creds, clock)
if err != nil {
return err
}
var m map[string]interface{}
err = json.Unmarshal(resp, &m)
if err != nil {
return err
}
next, err := toCollectionPage(m)
if err != nil {
return err
}
if next != nil {
return doForCollectionPage(h, agent, cb, next.Raw(), creds, clock)
}
}
return nil
}
// doForOrderedCollectionPage applies a function over a collection and its
// subsequent pages recursively. It returns the first non-nil error it
// encounters.
func doForOrderedCollectionPage(h HttpClient, agent string, cb func(c vocab.OrderedCollectionPageType) error, c vocab.OrderedCollectionPageType, creds *creds, clock Clock) error {
err := cb(c)
if err != nil {
return err
}
if c.IsNextOrderedCollectionPage() {
// Handle this one weird trick that other peers HATE federating
// with.
return doForOrderedCollectionPage(h, agent, cb, c.GetNextOrderedCollectionPage(), creds, clock)
} else if c.IsNextLink() {
l := c.GetNextLink()
if l.HasHref() {
u := l.GetHref()
resp, err := dereference(h, u, agent, creds, clock)
if err != nil {
return err
}
var m map[string]interface{}
err = json.Unmarshal(resp, &m)
if err != nil {
return err
}
next, err := toOrderedCollectionPage(m)
if err != nil {
return err
}
if next != nil {
return doForOrderedCollectionPage(h, agent, cb, next.Raw(), creds, clock)
}
}
} else if c.IsNextIRI() {
u := c.GetNextIRI()
resp, err := dereference(h, u, agent, creds, clock)
if err != nil {
return err
}
var m map[string]interface{}
err = json.Unmarshal(resp, &m)
if err != nil {
return err
}
next, err := toOrderedCollectionPage(m)
if err != nil {
return err
}
if next != nil {
return doForOrderedCollectionPage(h, agent, cb, next.Raw(), creds, clock)
}
}
return nil
}
type itemer interface {
ItemsLen() (l int)
IsItemsObject(index int) (ok bool)
GetItemsObject(index int) (v vocab.ObjectType)
IsItemsLink(index int) (ok bool)
GetItemsLink(index int) (v vocab.LinkType)
IsItemsIRI(index int) (ok bool)
GetItemsIRI(index int) (v *url.URL)
}
var _ itemer = &vocab.Collection{}
var _ itemer = &vocab.CollectionPage{}
// getURIsInItemer will extract 'items' from the provided Collection or
// CollectionPage.
func getURIsInItemer(i itemer) []*url.URL {
var u []*url.URL
for j := 0; j < i.ItemsLen(); j++ {
if i.IsItemsObject(j) {
obj := i.GetItemsObject(j)
if obj.HasId() {
u = append(u, obj.GetId())
}
} else if i.IsItemsLink(j) {
l := i.GetItemsLink(j)
if l.HasHref() {
u = append(u, l.GetHref())
}
} else if i.IsItemsIRI(j) {
u = append(u, i.GetItemsIRI(j))
}
}
return u
}
type orderedItemer interface {
OrderedItemsLen() (l int)
IsOrderedItemsObject(index int) (ok bool)
GetOrderedItemsObject(index int) (v vocab.ObjectType)
IsOrderedItemsLink(index int) (ok bool)
GetOrderedItemsLink(index int) (v vocab.LinkType)
IsOrderedItemsIRI(index int) (ok bool)
GetOrderedItemsIRI(index int) (v *url.URL)
}
var _ orderedItemer = &vocab.OrderedCollection{}
var _ orderedItemer = &vocab.OrderedCollectionPage{}
// getURIsInOrderedItemer will extract 'items' from the provided
// OrderedCollection or OrderedCollectionPage.
func getURIsInOrderedItemer(i orderedItemer) []*url.URL {
var u []*url.URL
for j := 0; j < i.OrderedItemsLen(); j++ {
if i.IsOrderedItemsObject(j) {
obj := i.GetOrderedItemsObject(j)
if obj.HasId() {
u = append(u, obj.GetId())
}
} else if i.IsOrderedItemsLink(j) {
l := i.GetOrderedItemsLink(j)
if l.HasHref() {
u = append(u, l.GetHref())
}
} else if i.IsOrderedItemsIRI(j) {
u = append(u, i.GetOrderedItemsIRI(j))
}
}
return u
}
type activityWithObject interface {
ObjectLen() (l int)
IsObject(index int) (ok bool)
GetObject(index int) (v vocab.ObjectType)
IsObjectIRI(index int) (ok bool)
GetObjectIRI(index int) (v *url.URL)
}
func getObjectIds(a activityWithObject) (u []*url.URL, e error) {
for i := 0; i < a.ObjectLen(); i++ {
if a.IsObject(i) {
obj := a.GetObject(i)
if !obj.HasId() {
e = fmt.Errorf("object has no id: %v", obj)
return
}
u = append(u, obj.GetId())
} else if a.IsObjectIRI(i) {
u = append(u, a.GetObjectIRI(i))
}
}
return
}
type activityWithTarget interface {
TargetLen() (l int)
IsTargetObject(index int) (ok bool)
GetTargetObject(index int) (v vocab.ObjectType)
IsTargetIRI(index int) (ok bool)
GetTargetIRI(index int) (v *url.URL)
}
func getTargetIds(a activityWithTarget) (u []*url.URL, e error) {
for i := 0; i < a.TargetLen(); i++ {
if a.IsTargetObject(i) {
obj := a.GetTargetObject(i)
if !obj.HasId() {
e = fmt.Errorf("object has no id: %v", obj)
return
}
u = append(u, obj.GetId())
} else if a.IsTargetIRI(i) {
u = append(u, a.GetTargetIRI(i))
}
}
return
}
func removeCollectionItemWithId(c vocab.CollectionType, iri *url.URL) {
for i := 0; i < c.ItemsLen(); i++ {
if c.IsItemsObject(i) {
o := c.GetItemsObject(i)
if !o.HasId() {
continue
}
if *o.GetId() == *iri {
c.RemoveItemsObject(i)
return
}
} else if c.IsItemsLink(i) {
l := c.GetItemsLink(i)
if !l.HasHref() {
continue
}
if *l.GetHref() == *iri {
c.RemoveItemsLink(i)
return
}
} else if c.IsItemsIRI(i) {
if *c.GetItemsIRI(i) == *iri {
c.RemoveItemsIRI(i)
return
}
}
}
}
func removeOrderedCollectionItemWithId(c vocab.OrderedCollectionType, iri *url.URL) {
for i := 0; i < c.OrderedItemsLen(); i++ {
if c.IsOrderedItemsObject(i) {
o := c.GetOrderedItemsObject(i)
if !o.HasId() {
continue
}
if *o.GetId() == *iri {
c.RemoveOrderedItemsObject(i)
return
}
} else if c.IsOrderedItemsLink(i) {
l := c.GetOrderedItemsLink(i)
if !l.HasHref() {
continue
}
if *l.GetHref() == *iri {
c.RemoveOrderedItemsLink(i)
return
}
} else if c.IsOrderedItemsIRI(i) {
if *c.GetOrderedItemsIRI(i) == *iri {
c.RemoveOrderedItemsIRI(i)
return
}
}
}
}
// toTombstone creates a Tombstone for the given object.
func toTombstone(obj vocab.ObjectType, id *url.URL, now time.Time) vocab.TombstoneType {
tomb := &vocab.Tombstone{}
tomb.SetId(id)
for i := 0; i < obj.TypeLen(); i++ {
if s, ok := obj.GetType(i).(string); ok {
tomb.AppendFormerTypeString(s)
} else if fObj, ok := obj.GetType(i).(vocab.ObjectType); ok {
tomb.AppendFormerTypeObject(fObj)
}
}
if obj.IsPublished() {
tomb.SetPublished(obj.GetPublished())
} else if obj.IsPublishedIRI() {
tomb.SetPublishedIRI(obj.GetPublishedIRI())
}
if obj.IsUpdated() {
tomb.SetUpdated(obj.GetUpdated())
} else if obj.IsUpdatedIRI() {
tomb.SetUpdatedIRI(obj.GetUpdatedIRI())
}
tomb.SetDeleted(now)
return tomb
}
type getActorCollectionFn func(actor vocab.ObjectType, lc *vocab.CollectionType, loc *vocab.OrderedCollectionType) (isIRI bool, e error)
func (f *federator) addAllObjectsToActorCollection(ctx context.Context, getter getActorCollectionFn, c vocab.ActivityType, prepend bool) error {
for i := 0; i < c.ActorLen(); i++ {
var iri *url.URL
if c.IsActorObject(i) {
obj := c.GetActorObject(i)
if !obj.HasId() {
return fmt.Errorf("actor does not have id")
}
iri = obj.GetId()
} else if c.IsActorLink(i) {
l := c.GetActorLink(i)
if !l.HasHref() {
return fmt.Errorf("actor Link href required")
}
iri = l.GetHref()
} else if c.IsActorIRI(i) {
iri = c.GetActorIRI(i)
}
if !f.App.Owns(ctx, iri) {
// TODO: Fetch or just store
continue
}
var actor vocab.ObjectType
pObj, err := f.App.Get(ctx, iri, ReadWrite)
if err != nil {
return err
}
ok := false
actor, ok = pObj.(vocab.ObjectType)
if !ok {
return fmt.Errorf("actor is not vocab.ObjectType")
}
// Obtain ordered/unordered collection
var lc vocab.CollectionType
var loc vocab.OrderedCollectionType
isIRI := false
if isIRI, err = getter(actor, &lc, &loc); err != nil {
return err
}
// Duplication detection
var iriSet map[string]bool
if lc != nil {
iriSet, err = getIRISetFromItems(lc)
} else if loc != nil {
iriSet, err = getIRISetFromOrderedItems(loc)
}
if err != nil {
return err
}
// Add object to collection if not a duplicate
for i := 0; i < c.ObjectLen(); i++ {
var iri *url.URL
if c.IsObjectIRI(i) {
iri = c.GetObjectIRI(i)
} else if c.IsObject(i) {
obj := c.GetObject(i)
if !obj.HasId() {
return fmt.Errorf("object at index %d has no id", i)
}
iri = obj.GetId()
}
if iriSet[iri.String()] {
continue
}
if lc != nil {
if prepend {
lc.PrependItemsIRI(iri)
} else {
lc.AppendItemsIRI(iri)
}
} else if loc != nil {
if prepend {
loc.PrependOrderedItemsIRI(iri)
} else {
loc.AppendOrderedItemsIRI(iri)
}
}
}
if isIRI {
if lc != nil {
err = f.App.Set(ctx, lc)
} else if loc != nil {
err = f.App.Set(ctx, loc)
}
if err != nil {
return err
}
} else if err := f.App.Set(ctx, actor); err != nil {
return err
}
}
return nil
}
type getObjectCollectionFn func(object vocab.ObjectType, lc *vocab.CollectionType, loc *vocab.OrderedCollectionType) (isIRI bool, e error)
func (f *federator) addAllActorsToObjectCollection(ctx context.Context, getter getObjectCollectionFn, c vocab.ActivityType, prepend bool) (bool, error) {
ownsAny := false
for i := 0; i < c.ObjectLen(); i++ {
var iri *url.URL
if c.IsObject(i) {
obj := c.GetObject(i)
if !obj.HasId() {
return ownsAny, fmt.Errorf("object does not have id")
}
iri = obj.GetId()
} else if c.IsObjectIRI(i) {
iri = c.GetObjectIRI(i)
}
if !f.App.Owns(ctx, iri) {
// TODO: Fetch or just store
continue
}
ownsAny = true
var object vocab.ObjectType
pObj, err := f.App.Get(ctx, iri, ReadWrite)
if err != nil {
return ownsAny, err
}
ok := false
object, ok = pObj.(vocab.ObjectType)
if !ok {
return ownsAny, fmt.Errorf("object is not vocab.ObjectType")
}
// Obtain ordered/unordered collection
var lc vocab.CollectionType
var loc vocab.OrderedCollectionType
isIRI := false
if isIRI, err = getter(object, &lc, &loc); err != nil {
return ownsAny, err
}
// Duplication detection
var iriSet map[string]bool
if lc != nil {
iriSet, err = getIRISetFromItems(lc)
} else if loc != nil {
iriSet, err = getIRISetFromOrderedItems(loc)
}
if err != nil {
return ownsAny, err
}
// Add actor to collection if not a duplicate
for i := 0; i < c.ActorLen(); i++ {
var iri *url.URL
if c.IsActorIRI(i) {
iri = c.GetActorIRI(i)
} else if c.IsActorObject(i) {
obj := c.GetActorObject(i)
if !obj.HasId() {
return ownsAny, fmt.Errorf("actor object at index %d has no id", i)
}
iri = obj.GetId()
} else if c.IsActorLink(i) {
l := c.GetActorLink(i)
if !l.HasHref() {
return ownsAny, fmt.Errorf("actor link at index %d has no id", i)
}
iri = l.GetHref()
}
if iriSet[iri.String()] {
continue
}
if lc != nil {
if prepend {
lc.AppendItemsIRI(iri)
} else {
lc.PrependItemsIRI(iri)
}
} else if loc != nil {
if prepend {
loc.PrependOrderedItemsIRI(iri)
} else {
loc.AppendOrderedItemsIRI(iri)
}
}
}
if isIRI {
if lc != nil {
err = f.App.Set(ctx, lc)
} else if loc != nil {
err = f.App.Set(ctx, loc)
}
if err != nil {
return ownsAny, err
}
} else if err := f.App.Set(ctx, object); err != nil {
return ownsAny, err
}
}
return ownsAny, nil
}
func (f *federator) addActivityToObjectCollection(ctx context.Context, getter getObjectCollectionFn, c vocab.ActivityType, prepend bool) (bool, error) {
ownsAny := false
for i := 0; i < c.ObjectLen(); i++ {
var objIri *url.URL
if c.IsObject(i) {
obj := c.GetObject(i)
if !obj.HasId() {
return ownsAny, fmt.Errorf("object does not have id")
}
objIri = obj.GetId()
} else if c.IsObjectIRI(i) {
objIri = c.GetObjectIRI(i)
}
if !f.App.Owns(ctx, objIri) {
// TODO: Fetch or just store
continue
}
ownsAny = true
var object vocab.ObjectType
pObj, err := f.App.Get(ctx, objIri, ReadWrite)
if err != nil {
return ownsAny, err
}
ok := false
object, ok = pObj.(vocab.ObjectType)
if !ok {
return ownsAny, fmt.Errorf("object is not vocab.ObjectType")
}
// Obtain ordered/unordered collection
var lc vocab.CollectionType
var loc vocab.OrderedCollectionType
isIRI := false
if isIRI, err = getter(object, &lc, &loc); err != nil {
return ownsAny, err
}
// Duplication detection
var iriSet map[string]bool
if lc != nil {
iriSet, err = getIRISetFromItems(lc)
} else if loc != nil {
iriSet, err = getIRISetFromOrderedItems(loc)
}
if err != nil {
return ownsAny, err
}
// Add activity to collection if not a duplicate
if !c.HasId() {
return ownsAny, fmt.Errorf("activity has no id")
}
iri := c.GetId()
if iriSet[iri.String()] {
continue
}
if lc != nil {
if prepend {
lc.AppendItemsIRI(iri)
} else {
lc.PrependItemsIRI(iri)
}
} else if loc != nil {
if prepend {
loc.PrependOrderedItemsIRI(iri)
} else {
loc.AppendOrderedItemsIRI(iri)
}
}
if isIRI {
if lc != nil {
err = f.App.Set(ctx, lc)
} else if loc != nil {
err = f.App.Set(ctx, loc)
}
if err != nil {
return ownsAny, err
}
} else if err := f.App.Set(ctx, object); err != nil {
return ownsAny, err
}
}
return ownsAny, nil
}
func (f *federator) ownsAnyObjects(c context.Context, a vocab.ActivityType) (bool, error) {
var iris []*url.URL
for i := 0; i < a.ObjectLen(); i++ {
if a.IsObject(i) {
obj := a.GetObject(i)
if !obj.HasId() {
return false, fmt.Errorf("object missing id")
}
iris = append(iris, obj.GetId())
} else if a.IsObjectIRI(i) {
iris = append(iris, a.GetObjectIRI(i))
}
}
return f.ownsAnyIRIs(c, iris), nil
}
func (f *federator) addToOutbox(c context.Context, r *http.Request, m map[string]interface{}) error {
outbox, err := f.App.GetOutbox(c, r, ReadWrite)
if err != nil {
return err
}
activity, err := toAnyActivity(m)
if err != nil {
return err
}
if err := f.App.Set(c, activity); err != nil {
return err
}
if !activity.HasId() {
return fmt.Errorf("activity missing id")
}
outbox.PrependOrderedItemsIRI(activity.GetId())
return f.App.Set(c, outbox)
}
func (f *federator) addToInboxIfNew(c context.Context, r *http.Request, m map[string]interface{}, callback func() error) error {
inbox, err := f.App.GetInbox(c, r, ReadWrite)
if err != nil {
return err
}
activity, err := toAnyActivity(m)
if err != nil {
return err
}
iriSet, err := getIRISetFromOrderedItems(inbox)
if err != nil {
return err
}
if !activity.HasId() {
return fmt.Errorf("activity missing id")
}
if !iriSet[activity.GetId().String()] {
if err := callback(); err != nil {
return err
}
inbox.PrependOrderedItemsIRI(activity.GetId())
return f.App.Set(c, inbox)
}
return nil
}
// Note: This is a mechanism for causing other victim servers to DDOS
// or forward spam on a malicious user's behalf. The trick is a simple
// one: Reply to a user, and CC a ton of 'follower' collections owned
// by the victim server. Bonus points for listing more 'follower'
// collections from other popular instances as well. Leveraging the
// Inbox Forwarding mechanism, a storm of messages will ensue.
//
// I don't want users of this library to be vulnerable to this kind of
// spam/DDOS storm. So here we allow the client application to filter
// out recipient collections.
func (f *federator) inboxForwarding(c context.Context, m map[string]interface{}) error {
a, err := toAnyActivity(m)
if err != nil {
return err
}
// 1. Must be first time we have seen this Activity.
if ok, err := f.App.Has(c, a.GetId()); err != nil {
return err
} else if ok {
return nil
}
// 2. The values of 'to', 'cc', or 'audience' are Collections owned by
// this server.
var r []*url.URL
r = append(r, getToIRIs(a)...)
r = append(r, getCcIRIs(a)...)
r = append(r, getAudienceIRIs(a)...)
var myIRIs []*url.URL
col := make(map[string]vocab.CollectionType, 0)
oCol := make(map[string]vocab.OrderedCollectionType, 0)
for _, iri := range r {
if ok, err := f.App.Has(c, iri); err != nil {
return err
} else if !ok {
continue
}
obj, err := f.App.Get(c, iri, Read)
if err != nil {
return err
}
if c, ok := obj.(vocab.CollectionType); ok {
col[iri.String()] = c
myIRIs = append(myIRIs, iri)
} else if oc, ok := obj.(vocab.OrderedCollectionType); ok {
oCol[iri.String()] = oc
myIRIs = append(myIRIs, iri)
}
}
if len(myIRIs) == 0 {
return nil
}
// 3. The values of 'inReplyTo', 'object', 'target', or 'tag' are owned
// by this server.
ownsValue := false
objs, l, iris := getInboxForwardingValues(a)
for _, obj := range objs {
if f.hasInboxForwardingValues(c, 0, f.MaxInboxForwardingDepth, obj) {
ownsValue = true
break
}
}
if !ownsValue && f.ownsAnyLinks(c, l) {
ownsValue = true
}
if !ownsValue && f.ownsAnyIRIs(c, iris) {
ownsValue = true
}
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 := f.FederateAPI.FilterForwarding(c, a, myIRIs)
if err != nil {
return err
}
recipients := make([]*url.URL, 0, len(toSend))
for _, iri := range toSend {
if c, ok := col[iri.String()]; ok {
for i := 0; i < c.ItemsLen(); i++ {
if c.IsItemsObject(i) {
obj := c.GetItemsObject(i)
if obj.HasId() {
recipients = append(recipients, obj.GetId())
}
} else if c.IsItemsLink(i) {
l := c.GetItemsLink(i)
if l.HasHref() {
recipients = append(recipients, l.GetHref())
}
} else if c.IsItemsIRI(i) {
recipients = append(recipients, c.GetItemsIRI(i))
}
}
} else if oc, ok := oCol[iri.String()]; ok {
for i := 0; i < oc.OrderedItemsLen(); i++ {
if oc.IsOrderedItemsObject(i) {
obj := oc.GetOrderedItemsObject(i)
if obj.HasId() {
recipients = append(recipients, obj.GetId())
}
} else if oc.IsOrderedItemsLink(i) {
l := oc.GetItemsLink(i)
if l.HasHref() {
recipients = append(recipients, l.GetHref())
}
} else if oc.IsOrderedItemsIRI(i) {
recipients = append(recipients, oc.GetOrderedItemsIRI(i))
}
}
}
}
return f.deliverToRecipients(a, recipients, nil)
}
// Given an 'inReplyTo', 'object', 'target', or 'tag' object, recursively
// examines those same values to determine if the app owns any, up to a maximum
// depth.
func (f *federator) hasInboxForwardingValues(c context.Context, depth, maxDepth int, o vocab.ObjectType) bool {
if depth == maxDepth {
return false
}
if f.App.Owns(c, o.GetId()) {
return true
}
objs, l, iris := getInboxForwardingValues(o)
for _, obj := range objs {
if f.hasInboxForwardingValues(c, depth+1, maxDepth, obj) {
return true
}
}
if f.ownsAnyLinks(c, l) {
return true
}
return f.ownsAnyIRIs(c, iris)
}
func (f *federator) ownsAnyIRIs(c context.Context, iris []*url.URL) bool {
for _, iri := range iris {
if f.App.Owns(c, iri) {
return true
}
// TODO: Dereference the IRI
}
return false
}
func (f *federator) ownsAnyLinks(c context.Context, links []vocab.LinkType) bool {
for _, link := range links {
if !link.HasHref() {
continue
}
href := link.GetHref()
if f.App.Owns(c, href) {
return true
}
// TODO: Dereference the IRI
}
return false
}
func (f *federator) ensureActivityOriginMatchesObjects(a vocab.ActivityType) error {
if !a.HasId() {
return fmt.Errorf("activity has no iri")
}
originIRI := a.GetId()
originHost := originIRI.Host
for i := 0; i < a.ObjectLen(); i++ {
if a.IsObject(i) {
obj := a.GetObject(i)
if !obj.HasId() {
return fmt.Errorf("object at index %d has no id", i)
}
iri := obj.GetId()
if originHost != iri.Host {
return fmt.Errorf("object %q: not in activity origin", iri)
}
} else if a.IsObjectIRI(i) {
iri := a.GetObjectIRI(i)
if originHost != iri.Host {
return fmt.Errorf("object %q: not in activity origin", iri)
}
}
}
return nil
}
func (f *federator) ensureActivityActorsMatchObjectActors(a vocab.ActivityType) error {
actorSet := make(map[string]bool, a.ActorLen())
for i := 0; i < a.ActorLen(); i++ {
if a.IsActorObject(i) {
obj := a.GetActorObject(i)
if !obj.HasId() {
return fmt.Errorf("actor object at index %d has no id", i)
}
actorSet[obj.GetId().String()] = true
} else if a.IsActorLink(i) {
l := a.GetActorLink(i)
if !l.HasHref() {
return fmt.Errorf("actor link at index %d has no href", i)
}
actorSet[l.GetHref().String()] = true
} else if a.IsActorIRI(i) {
actorSet[a.GetActorIRI(i).String()] = true
}
}
objectActors := make(map[string]bool, a.ObjectLen())
for i := 0; i < a.ObjectLen(); i++ {
if a.IsObject(i) {
obj := a.GetObject(i)
if !obj.HasId() {
return fmt.Errorf("object at index %d has no id", i)
}
objectActivity, ok := obj.(vocab.ActivityType)
if !ok {
return fmt.Errorf("object at index %d is not an activity", i)
}
for j := 0; j < objectActivity.ActorLen(); j++ {
if objectActivity.IsActorObject(j) {
obj := objectActivity.GetActorObject(j)
if !obj.HasId() {
return fmt.Errorf("actor object at index (%d,%d) has no id", i, j)
}
objectActors[obj.GetId().String()] = true
} else if objectActivity.IsActorLink(j) {
l := objectActivity.GetActorLink(j)
if !l.HasHref() {
return fmt.Errorf("actor link at index (%d,%d) has no href", i, j)
}
objectActors[l.GetHref().String()] = true
} else if objectActivity.IsActorIRI(j) {
objectActors[objectActivity.GetActorIRI(j).String()] = true
}
}
} else if a.IsObjectIRI(i) {
// TODO: Dereference IRI
iri := a.GetObjectIRI(i)
return fmt.Errorf("unimplemented: fetching IRI for UNDO verification of ownership of %q", iri)
}
}
for k := range objectActors {
if !actorSet[k] {
return fmt.Errorf("at least 1 activity actors missing: %q", k)
}
}
return nil
}
func getInboxForwardingValues(o vocab.ObjectType) (objs []vocab.ObjectType, l []vocab.LinkType, iri []*url.URL) {
// 'inReplyTo'
for i := 0; i < o.InReplyToLen(); i++ {
if o.IsInReplyToObject(i) {
objs = append(objs, o.GetInReplyToObject(i))
} else if o.IsInReplyToLink(i) {
l = append(l, o.GetInReplyToLink(i))
} else if o.IsInReplyToIRI(i) {
iri = append(iri, o.GetInReplyToIRI(i))
}
}
// 'tag'
for i := 0; i < o.TagLen(); i++ {
if o.IsTagObject(i) {
objs = append(objs, o.GetTagObject(i))
} else if o.IsTagLink(i) {
l = append(l, o.GetTagLink(i))
} else if o.IsTagIRI(i) {
iri = append(iri, o.GetTagIRI(i))
}
}
if a, ok := o.(vocab.ActivityType); ok {
// 'object'
for i := 0; i < a.ObjectLen(); i++ {
if a.IsObject(i) {
objs = append(objs, a.GetObject(i))
} else if a.IsObjectIRI(i) {
iri = append(iri, a.GetObjectIRI(i))
}
}
// 'target'
for i := 0; i < a.TargetLen(); i++ {
if a.IsTargetObject(i) {
objs = append(objs, a.GetTargetObject(i))
} else if a.IsTargetLink(i) {
l = append(l, a.GetTargetLink(i))
} else if a.IsTargetIRI(i) {
iri = append(iri, a.GetTargetIRI(i))
}
}
}
return
}
// Fetches an "object" on a raw JSON map of an Activity with the matching 'id'
// field. If there is no object matching the IRI, or the object just is an IRI,
// or the object wth the matching id is not in the array of objects, then a nil
// map is returned.
func getRawObject(m map[string]interface{}, id string) map[string]interface{} {
for k, v := range m {
if k == "object" {
switch val := v.(type) {
case map[string]interface{}:
if r, ok := val["id"]; ok {
if rId, ok := r.(string); ok && rId == id {
return val
}
}
case []interface{}:
for _, elem := range val {
if elemVal, ok := elem.(map[string]interface{}); ok {
if r, ok := elemVal["id"]; ok {
if rId, ok := r.(string); ok && rId == id {
return elemVal
}
}
}
}
}
}
}
return nil
}
// recursivelyApplyDeletes takes input map 'm' and deletes entries in its maps
// where the 'hasNils' map has nils. If the interface{} of the maps are
// themselves map[string]interface{} types, then this function recurs.
func recursivelyApplyDeletes(m, hasNils map[string]interface{}) {
for k, v := range hasNils {
if _, ok := m[k]; v == nil && ok {
delete(m, k)
} else if nilsSubMap, ok := v.(map[string]interface{}); ok {
if mSub, ok := m[k]; ok {
if mSubMap, ok := mSub.(map[string]interface{}); ok {
recursivelyApplyDeletes(mSubMap, nilsSubMap)
}
}
}
}
}
func getIRISetFromItems(c vocab.CollectionType) (map[string]bool, error) {
r := make(map[string]bool, c.ItemsLen())
for i := 0; i < c.ItemsLen(); i++ {
if c.IsItemsObject(i) {
obj := c.GetItemsObject(i)
if !obj.HasId() {
return r, fmt.Errorf("items object at index %d has no id", i)
}
r[obj.GetId().String()] = true
} else if c.IsItemsLink(i) {
l := c.GetItemsLink(i)
if !l.HasHref() {
return r, fmt.Errorf("items link at index %d has no href", i)
}
r[l.GetHref().String()] = true
} else if c.IsItemsIRI(i) {
r[c.GetItemsIRI(i).String()] = true
}
}
return r, nil
}
func getIRISetFromOrderedItems(c vocab.OrderedCollectionType) (map[string]bool, error) {
r := make(map[string]bool, c.OrderedItemsLen())
for i := 0; i < c.OrderedItemsLen(); i++ {
if c.IsOrderedItemsObject(i) {
obj := c.GetOrderedItemsObject(i)
if !obj.HasId() {
return r, fmt.Errorf("items object at index %d has no id", i)
}
r[obj.GetId().String()] = true
} else if c.IsOrderedItemsLink(i) {
l := c.GetOrderedItemsLink(i)
if !l.HasHref() {
return r, fmt.Errorf("items link at index %d has no href", i)
}
r[l.GetHref().String()] = true
} else if c.IsOrderedItemsIRI(i) {
r[c.GetOrderedItemsIRI(i).String()] = true
}
}
return r, nil
}
func clearSensitiveFields(obj vocab.ObjectType) {
for i := 0; i < obj.BtoLen(); i++ {
if obj.IsBtoObject(0) {
obj.RemoveBtoObject(0)
} else if obj.IsBtoLink(0) {
obj.RemoveBtoLink(0)
} else if obj.IsBtoIRI(0) {
obj.RemoveBtoIRI(0)
}
}
for i := 0; i < obj.BccLen(); i++ {
if obj.IsBccObject(0) {
obj.RemoveBccObject(0)
} else if obj.IsBccLink(0) {
obj.RemoveBccLink(0)
} else if obj.IsBccIRI(0) {
obj.RemoveBccIRI(0)
}
}
}