Support Inbox Forwarding.

Client libraries can also filter the collections being targeted for the
inbox forwarding.
このコミットが含まれているのは:
Cory Slep 2018-05-10 12:49:37 +02:00
コミット 7b63dca953
4個のファイルの変更354行の追加35行の削除

ファイルの表示

@ -49,7 +49,7 @@ type Pubber interface {
GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error)
}
// NewPubber provides a Pubber that implements only the Social API in
// NewSocialPubber provides a Pubber that implements only the Social API in
// ActivityPub.
func NewSocialPubber(clock Clock, app Application, socialApp SocialApp, cb Callbacker) Pubber {
return &federator{
@ -61,38 +61,40 @@ func NewSocialPubber(clock Clock, app Application, socialApp SocialApp, cb Callb
}
}
// NewPubber provides a Pubber that implements only the Federating API in
// ActivityPub.
func NewFederatingPubber(clock Clock, app Application, fApp FederateApp, cb Callbacker, d Deliverer, client HttpClient, userAgent string, maxDeliveryDepth int) Pubber {
// NewFederatingPubber provides a Pubber that implements only the Federating API
// in ActivityPub.
func NewFederatingPubber(clock Clock, app Application, fApp FederateApp, cb Callbacker, d Deliverer, client HttpClient, userAgent string, maxDeliveryDepth, maxForwardingDepth int) Pubber {
return &federator{
Clock: clock,
App: app,
FederateApp: fApp,
ServerCallbacker: cb,
Client: client,
Agent: userAgent,
MaxDeliveryDepth: maxDeliveryDepth,
EnableServer: true,
deliverer: d,
Clock: clock,
App: app,
FederateApp: fApp,
ServerCallbacker: cb,
Client: client,
Agent: userAgent,
MaxDeliveryDepth: maxDeliveryDepth,
MaxInboxForwardingDepth: maxForwardingDepth,
EnableServer: true,
deliverer: d,
}
}
// NewPubber provides a Pubber that implements both the Social API and the
// Federating API in ActivityPub.
func NewPubber(clock Clock, app Application, sApp SocialApp, fApp FederateApp, client, server Callbacker, d Deliverer, httpClient HttpClient, userAgent string, maxDeliveryDepth int) Pubber {
func NewPubber(clock Clock, app Application, sApp SocialApp, fApp FederateApp, client, server Callbacker, d Deliverer, httpClient HttpClient, userAgent string, maxDeliveryDepth, maxForwardingDepth int) Pubber {
return &federator{
Clock: clock,
App: app,
SocialApp: sApp,
FederateApp: fApp,
Client: httpClient,
Agent: userAgent,
MaxDeliveryDepth: maxDeliveryDepth,
ServerCallbacker: server,
ClientCallbacker: client,
EnableClient: true,
EnableServer: true,
deliverer: d,
Clock: clock,
App: app,
SocialApp: sApp,
FederateApp: fApp,
Client: httpClient,
Agent: userAgent,
MaxDeliveryDepth: maxDeliveryDepth,
MaxInboxForwardingDepth: maxForwardingDepth,
ServerCallbacker: server,
ClientCallbacker: client,
EnableClient: true,
EnableServer: true,
deliverer: d,
}
}
@ -141,6 +143,13 @@ type federator struct {
//
// It is only required if EnableServer is true.
MaxDeliveryDepth int
// MaxInboxForwardingDepth is how deep the values are examined for
// determining ownership of whether to forward an Activity to
// collections or followers. Once this maximum is exceeded, the ghost
// replies issue may become a problem, but users may not mind.
//
// It is only required if EnableServer is true.
MaxInboxForwardingDepth int
// deliverer handles deliveries to other federated servers.
//
// It is only required if EnableServer is true.
@ -192,7 +201,9 @@ func (f *federator) PostInbox(c context.Context, w http.ResponseWriter, r *http.
if err := f.addToInbox(c, r, m); err != nil {
return true, err
}
// TODO: 7.1.2 Inbox forwarding
if err := f.inboxForwarding(c, m); err != nil {
return true, err
}
w.WriteHeader(http.StatusOK)
return true, nil
}

ファイルの表示

@ -442,6 +442,7 @@ type MockApplication struct {
t *testing.T
owns func(c context.Context, id url.URL) bool
get func(c context.Context, id url.URL) (PubObject, error)
has func(c context.Context, id url.URL) (bool, error)
set func(c context.Context, o PubObject) error
getInbox func(c context.Context, r *http.Request) (vocab.OrderedCollectionType, error)
getOutbox func(c context.Context, r *http.Request) (vocab.OrderedCollectionType, error)
@ -462,6 +463,13 @@ func (m *MockApplication) Get(c context.Context, id url.URL) (PubObject, error)
return m.get(c, id)
}
func (m *MockApplication) Has(c context.Context, id url.URL) (bool, error) {
if m.has == nil {
m.t.Fatal("unexpected call to MockApplication Has")
}
return m.has(c, id)
}
func (m *MockApplication) Set(c context.Context, o PubObject) error {
if m.set == nil {
m.t.Fatal("unexpected call to MockApplication Set")
@ -647,12 +655,13 @@ func (m *MockCallbacker) Reject(c context.Context, s *streams.Reject) error {
var _ FederateApp = &MockFederateApp{}
type MockFederateApp struct {
t *testing.T
canAdd func(c context.Context, obj vocab.ObjectType, target vocab.ObjectType) bool
canRemove func(c context.Context, obj vocab.ObjectType, target vocab.ObjectType) bool
onFollow func(c context.Context, s *streams.Follow) FollowResponse
unblocked func(c context.Context, actorIRIs []url.URL) error
getFollowing func(c context.Context, actor url.URL) (vocab.CollectionType, error)
t *testing.T
canAdd func(c context.Context, obj vocab.ObjectType, target vocab.ObjectType) bool
canRemove func(c context.Context, obj vocab.ObjectType, target vocab.ObjectType) bool
onFollow func(c context.Context, s *streams.Follow) FollowResponse
unblocked func(c context.Context, actorIRIs []url.URL) error
getFollowing func(c context.Context, actor url.URL) (vocab.CollectionType, error)
filterForwarding func(c context.Context, activity vocab.ActivityType, iris []url.URL) ([]url.URL, error)
}
func (m *MockFederateApp) CanAdd(c context.Context, obj vocab.ObjectType, target vocab.ObjectType) bool {
@ -690,6 +699,13 @@ func (m *MockFederateApp) GetFollowing(c context.Context, actor url.URL) (vocab.
return m.getFollowing(c, actor)
}
func (m *MockFederateApp) FilterForwarding(c context.Context, activity vocab.ActivityType, iris []url.URL) ([]url.URL, error) {
if m.filterForwarding == nil {
m.t.Fatal("unexpected call to MockFederateApp FilterForwarding")
}
return m.filterForwarding(c, activity, iris)
}
var _ Deliverer = &MockDeliverer{}
type MockDeliverer struct {
@ -734,7 +750,7 @@ func NewFederatingPubberTest(t *testing.T) (app *MockApplication, fedApp *MockFe
cb = &MockCallbacker{t: t}
d = &MockDeliverer{t: t}
h = &MockHttpClient{t: t}
p = NewFederatingPubber(clock, app, fedApp, cb, d, h, testAgent, 1)
p = NewFederatingPubber(clock, app, fedApp, cb, d, h, testAgent, 1, 1)
return
}
@ -747,7 +763,7 @@ func NewPubberTest(t *testing.T) (app *MockApplication, socialApp *MockSocialApp
fedCb = &MockCallbacker{t: t}
d = &MockDeliverer{t: t}
h = &MockHttpClient{t: t}
p = NewPubber(clock, app, socialApp, fedApp, socialCb, fedCb, d, h, testAgent, 1)
p = NewPubber(clock, app, socialApp, fedApp, socialCb, fedCb, d, h, testAgent, 1, 1)
return
}
@ -763,6 +779,9 @@ func PreparePostInboxTest(t *testing.T, app *MockApplication, socialApp *MockSoc
app.set = func(c context.Context, o PubObject) error {
return nil
}
app.has = func(c context.Context, id url.URL) (bool, error) {
return false, nil
}
return
}
@ -981,6 +1000,26 @@ func TestFederatingPubber_PostInbox(t *testing.T) {
}
return nil
}
gotHas := 0
var hasIriActivity url.URL
var hasIriTo url.URL
app.has = func(c context.Context, id url.URL) (bool, error) {
gotHas++
if gotHas == 1 {
hasIriActivity = id
return false, nil
} else {
hasIriTo = id
return true, nil
}
}
gotGet := 0
var gotIri url.URL
app.get = func(c context.Context, iri url.URL) (PubObject, error) {
gotGet++
gotIri = iri
return samActor, nil
}
gotCreate := 0
var gotCreateCallback *streams.Create
cb.create = func(c context.Context, s *streams.Create) error {
@ -1005,6 +1044,16 @@ func TestFederatingPubber_PostInbox(t *testing.T) {
t.Fatalf("expected %s, got %s", "OrderedCollection", l)
} else if l := setObject.GetType(0).(string); l != "Note" {
t.Fatalf("expected %s, got %s", "Note", l)
} else if gotHas != 2 {
t.Fatalf("expected %d, got %d", 2, gotHas)
} else if hasIriActivityString := (&hasIriActivity).String(); hasIriActivityString != noteActivityURIString {
t.Fatalf("expected %s, got %s", noteActivityURIString, hasIriActivityString)
} else if hasIriToString := (&hasIriTo).String(); hasIriToString != samIRIString {
t.Fatalf("expected %s, got %s", samIRIString, hasIriToString)
} else if gotGet != 1 {
t.Fatalf("expected %d, got %d", 1, gotGet)
} else if gotIriString := (&gotIri).String(); gotIriString != samIRIString {
t.Fatalf("expected %s, got %s", samIRIString, gotIriString)
} else if gotCreate != 1 {
t.Fatalf("expected %d, got %d", 1, gotCreate)
} else if s := gotCreateCallback.Raw().GetActorObject(0).GetId(); s.String() != sallyIRIString {
@ -1112,6 +1161,26 @@ func TestPubber_PostInbox(t *testing.T) {
}
return nil
}
gotHas := 0
var hasIriActivity url.URL
var hasIriTo url.URL
app.has = func(c context.Context, id url.URL) (bool, error) {
gotHas++
if gotHas == 1 {
hasIriActivity = id
return false, nil
} else {
hasIriTo = id
return true, nil
}
}
gotGet := 0
var gotIri url.URL
app.get = func(c context.Context, iri url.URL) (PubObject, error) {
gotGet++
gotIri = iri
return samActor, nil
}
gotCreate := 0
var gotCreateCallback *streams.Create
fedCb.create = func(c context.Context, s *streams.Create) error {
@ -1136,6 +1205,16 @@ func TestPubber_PostInbox(t *testing.T) {
t.Fatalf("expected %s, got %s", "OrderedCollection", l)
} else if l := setObject.GetType(0).(string); l != "Note" {
t.Fatalf("expected %s, got %s", "Note", l)
} else if gotHas != 2 {
t.Fatalf("expected %d, got %d", 2, gotHas)
} else if hasIriActivityString := (&hasIriActivity).String(); hasIriActivityString != noteActivityURIString {
t.Fatalf("expected %s, got %s", noteActivityURIString, hasIriActivityString)
} else if hasIriToString := (&hasIriTo).String(); hasIriToString != samIRIString {
t.Fatalf("expected %s, got %s", samIRIString, hasIriToString)
} else if gotGet != 1 {
t.Fatalf("expected %d, got %d", 1, gotGet)
} else if gotIriString := (&gotIri).String(); gotIriString != samIRIString {
t.Fatalf("expected %s, got %s", samIRIString, gotIriString)
} else if gotCreate != 1 {
t.Fatalf("expected %d, got %d", 1, gotCreate)
} else if s := gotCreateCallback.Raw().GetActorObject(0).GetId(); s.String() != sallyIRIString {

ファイルの表示

@ -41,6 +41,9 @@ 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)
// 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
@ -96,6 +99,17 @@ type FederateApp interface {
// 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)
}
// FollowResponse instructs how to proceed upon immediately receiving a request
@ -113,6 +127,9 @@ const (
// 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.

ファイルの表示

@ -483,6 +483,12 @@ func (f *federator) deliver(obj vocab.ActivityType) error {
if err != nil {
return err
}
return f.deliverToRecipients(obj, recipients)
}
// 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) error {
m, err := obj.Serialize()
if err != nil {
return err
@ -1335,6 +1341,212 @@ func (f *federator) addToInbox(c context.Context, r *http.Request, m map[string]
return f.App.Set(c, inbox)
}
// 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)
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.FederateApp.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)
}
// 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 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