package pub import ( "encoding/json" "fmt" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/vocab" "io/ioutil" "net/http" "net/url" "strings" "time" ) const ( postContentTypeHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" getAcceptHeader = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" contentTypeHeader = "Content-Type" acceptHeader = "Accept" publicActivityPub = "https://www.w3.org/ns/activitystreams#Public" publicJsonLD = "Public" publicJsonLDAS = "as:Public" ) var alternatives = []string{"application/activity+json"} func trimAll(s []string) []string { var r []string for _, e := range s { r = append(r, strings.Trim(e, " ")) } return r } func headerEqualsOneOf(header string, acceptable []string) bool { sanitizedHeader := strings.Join(trimAll(strings.Split(header, ";")), ";") for _, v := range acceptable { // Remove any number of whitespace after ;'s sanitizedV := strings.Join(trimAll(strings.Split(v, ";")), ";") if sanitizedHeader == sanitizedV { return true } } return false } func isActivityPubPost(r *http.Request) bool { return r.Method == "POST" && headerEqualsOneOf(r.Header.Get(contentTypeHeader), []string{postContentTypeHeader, contentTypeHeader}) } func isActivityPubGet(r *http.Request) bool { return r.Method == "GET" && headerEqualsOneOf(r.Header.Get(acceptHeader), []string{getAcceptHeader, contentTypeHeader}) } // 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 } // dereference makes an HTTP GET request to an IRI in order to obtain the // ActivityStream representation. func dereference(c *http.Client, u url.URL, agent string) ([]byte, error) { // TODO: (section 7.1) The server MUST dereference the collection (with the user's credentials) 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", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") req.Header.Add("User-Agent", fmt.Sprintf("%s (go-fed ActivityPub)", agent)) 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) } // 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. // 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 *Client) prepare(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: Handle public collection receiverActors, err := c.resolveInboxes(r, 0, c.MaxDepth) if err != nil { return nil, err } targets := getInboxes(receiverActors) // Get inboxes of sender(s) senderActors, err := c.resolveInboxes(getActorsAttributedToURI(o), 0, c.MaxDepth) if err != nil { return nil, err } ignore := getInboxes(senderActors) // 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 *Client) resolveInboxes(r []url.URL, depth int, max int) ([]ActorObject, error) { if depth >= max { return nil, nil } a := make([]ActorObject, 0, len(r)) for _, u := range r { // Do not retry here -- if a dereference fails, then fail the // entire delivery. resp, err := dereference(c.Client, u, c.Agent) if err != nil { return nil, err } var m map[string]interface{} if err = json.Unmarshal(resp, &m); err != nil { return nil, err } var actor ActorObject var co *streams.Collection var oc *streams.OrderedCollection var cp *streams.CollectionPage var ocp *streams.OrderedCollectionPage // 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 nil, err } // If a recipient is a Collection or OrderedCollection, then the // server MUST dereference the collection. Note that this also // applies to CollectionPage and OrderedCollectionPage. var uris []url.URL if co != nil { uris := getURIsInItemer(co.Raw()) actors, err := c.resolveInboxes(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(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()) if err != nil { return nil, err } actors, err := c.resolveInboxes(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()) if err != nil { return nil, err } actors, err := c.resolveInboxes(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 } // getInboxes extracts the 'inbox' IRIs from actors. func getInboxes(a []ActorObject) []url.URL { var u []url.URL for _, actor := range a { if actor.HasInbox() { u = append(u, actor.GetInbox()) } } return u } // 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.IsBtoIRI(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[url.URL]bool, len(ignored)) for _, elem := range ignored { ignoredMap[elem] = true } outMap := make(map[url.URL]bool, len(recipients)) for k, _ := range outMap { if !ignoredMap[k] { out = append(out, k) } } return } 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 *http.Client, agent string, cb func(c vocab.CollectionPageType) error, c vocab.CollectionPageType) 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()) } else if c.IsNextLink() { l := c.GetNextLink() if l.HasHref() { u := l.GetHref() resp, err := dereference(h, u, agent) 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()) } } } else if c.IsNextIRI() { u := c.GetNextIRI() resp, err := dereference(h, u, agent) 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()) } } 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 *http.Client, agent string, cb func(c vocab.OrderedCollectionPageType) error, c vocab.OrderedCollectionPageType) 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()) } else if c.IsNextLink() { l := c.GetNextLink() if l.HasHref() { u := l.GetHref() resp, err := dereference(h, u, agent) 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()) } } } else if c.IsNextIRI() { u := c.GetNextIRI() resp, err := dereference(h, u, agent) 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()) } } 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) } // 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) } // 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 }