[bugfix/chore] Inbox post updates (#1821)
Co-authored-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
94792ff7e8
commit
045c279d47
|
@ -19,32 +19,34 @@ package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
||||||
// Eg., POST to https://example.org/users/whatever/inbox.
|
// Eg., POST to https://example.org/users/whatever/inbox.
|
||||||
func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
||||||
// usernames on our instance are always lowercase
|
_, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request)
|
||||||
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
if err != nil {
|
||||||
if requestedUsername == "" {
|
errWithCode := new(gtserror.WithCode)
|
||||||
err := errors.New("no username specified in request")
|
|
||||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
if !errors.As(err, errWithCode) {
|
||||||
|
// Something else went wrong, and someone forgot to return
|
||||||
|
// an errWithCode! It's chill though. Log the error but don't
|
||||||
|
// return it as-is to the caller, to avoid leaking internals.
|
||||||
|
log.Errorf(c.Request.Context(), "returning Bad Request to caller, err was: %q", err)
|
||||||
|
*errWithCode = gtserror.NewErrorBadRequest(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass along confirmed error with code to the main error handler
|
||||||
|
apiutil.ErrorHandler(c, *errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if posted, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil {
|
// Inbox POST body was Accepted for processing.
|
||||||
if withCode, ok := err.(gtserror.WithCode); ok {
|
c.JSON(http.StatusAccepted, gin.H{"status": http.StatusText(http.StatusAccepted)})
|
||||||
apiutil.ErrorHandler(c, withCode, m.processor.InstanceGetV1)
|
|
||||||
} else {
|
|
||||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
|
||||||
}
|
|
||||||
} else if !posted {
|
|
||||||
err := errors.New("unable to process request")
|
|
||||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -35,6 +37,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -44,11 +47,82 @@ type InboxPostTestSuite struct {
|
||||||
UserStandardTestSuite
|
UserStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InboxPostTestSuite) TestPostBlock() {
|
func (suite *InboxPostTestSuite) inboxPost(
|
||||||
blockingAccount := suite.testAccounts["remote_account_1"]
|
activity pub.Activity,
|
||||||
blockedAccount := suite.testAccounts["local_account_1"]
|
requestingAccount *gtsmodel.Account,
|
||||||
blockURI := testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3")
|
targetAccount *gtsmodel.Account,
|
||||||
|
expectedHTTPStatus int,
|
||||||
|
expectedBody string,
|
||||||
|
middlewares ...func(*gin.Context),
|
||||||
|
) {
|
||||||
|
var (
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
ctx, _ = testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prepare the requst body bytes.
|
||||||
|
bodyI, err := ap.Serialize(activity)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.MarshalIndent(bodyI, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
suite.T().Logf("prepared POST body:\n%s", string(b))
|
||||||
|
|
||||||
|
// Prepare signature headers for this Activity.
|
||||||
|
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(
|
||||||
|
activity,
|
||||||
|
requestingAccount.PublicKeyURI,
|
||||||
|
requestingAccount.PrivateKey,
|
||||||
|
testrig.URLMustParse(targetAccount.InboxURI),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Put the request together.
|
||||||
|
ctx.AddParam(users.UsernameKey, targetAccount.Username)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, targetAccount.InboxURI, bytes.NewReader(b))
|
||||||
|
ctx.Request.Header.Set("Signature", signature)
|
||||||
|
ctx.Request.Header.Set("Date", dateHeader)
|
||||||
|
ctx.Request.Header.Set("Digest", digestHeader)
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/activity+json")
|
||||||
|
|
||||||
|
// Pass the context through provided middlewares.
|
||||||
|
for _, middleware := range middlewares {
|
||||||
|
middleware(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the function being tested.
|
||||||
|
suite.userModule.InboxPOSTHandler(ctx)
|
||||||
|
|
||||||
|
// Read the result.
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err = io.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := gtserror.MultiError{}
|
||||||
|
|
||||||
|
// Check expected code + body.
|
||||||
|
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||||
|
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got an expected body, return early.
|
||||||
|
if expectedBody != "" && string(b) != expectedBody {
|
||||||
|
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := errs.Combine(); err != nil {
|
||||||
|
suite.FailNow("", "%v (body %s)", err, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InboxPostTestSuite) newBlock(blockID string, blockingAccount *gtsmodel.Account, blockedAccount *gtsmodel.Account) vocab.ActivityStreamsBlock {
|
||||||
block := streams.NewActivityStreamsBlock()
|
block := streams.NewActivityStreamsBlock()
|
||||||
|
|
||||||
// set the actor property to the block-ing account's URI
|
// set the actor property to the block-ing account's URI
|
||||||
|
@ -59,7 +133,7 @@ func (suite *InboxPostTestSuite) TestPostBlock() {
|
||||||
|
|
||||||
// set the ID property to the blocks's URI
|
// set the ID property to the blocks's URI
|
||||||
idProp := streams.NewJSONLDIdProperty()
|
idProp := streams.NewJSONLDIdProperty()
|
||||||
idProp.Set(blockURI)
|
idProp.Set(testrig.URLMustParse(blockID))
|
||||||
block.SetJSONLDId(idProp)
|
block.SetJSONLDId(idProp)
|
||||||
|
|
||||||
// set the object property to the target account's URI
|
// set the object property to the target account's URI
|
||||||
|
@ -74,188 +148,48 @@ func (suite *InboxPostTestSuite) TestPostBlock() {
|
||||||
toProp.AppendIRI(toIRI)
|
toProp.AppendIRI(toIRI)
|
||||||
block.SetActivityStreamsTo(toProp)
|
block.SetActivityStreamsTo(toProp)
|
||||||
|
|
||||||
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
|
return block
|
||||||
|
|
||||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(block, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
|
|
||||||
bodyI, err := ap.Serialize(block)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
bodyJson, err := json.Marshal(bodyI)
|
|
||||||
suite.NoError(err)
|
|
||||||
body := bytes.NewReader(bodyJson)
|
|
||||||
|
|
||||||
tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media"))
|
|
||||||
federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager)
|
|
||||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
|
||||||
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
|
|
||||||
userModule := users.New(processor)
|
|
||||||
suite.NoError(processor.Start())
|
|
||||||
|
|
||||||
// setup request
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("Signature", signature)
|
|
||||||
ctx.Request.Header.Set("Date", dateHeader)
|
|
||||||
ctx.Request.Header.Set("Digest", digestHeader)
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/activity+json")
|
|
||||||
|
|
||||||
// we need to pass the context through signature check first to set appropriate values on it
|
|
||||||
suite.signatureCheck(ctx)
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
|
||||||
gin.Param{
|
|
||||||
Key: users.UsernameKey,
|
|
||||||
Value: blockedAccount.Username,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger the function being tested
|
|
||||||
userModule.InboxPOSTHandler(ctx)
|
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.Empty(b)
|
|
||||||
|
|
||||||
// there should be a block in the database now between the accounts
|
|
||||||
dbBlock, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.NotNil(dbBlock)
|
|
||||||
suite.WithinDuration(time.Now(), dbBlock.CreatedAt, 30*time.Second)
|
|
||||||
suite.WithinDuration(time.Now(), dbBlock.UpdatedAt, 30*time.Second)
|
|
||||||
suite.Equal("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3", dbBlock.URI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPostUnblock verifies that a remote account with a block targeting one of our instance users should be able to undo that block.
|
func (suite *InboxPostTestSuite) newUndo(
|
||||||
func (suite *InboxPostTestSuite) TestPostUnblock() {
|
originalActivity pub.Activity,
|
||||||
blockingAccount := suite.testAccounts["remote_account_1"]
|
objectF func() vocab.ActivityStreamsObjectProperty,
|
||||||
blockedAccount := suite.testAccounts["local_account_1"]
|
to string,
|
||||||
|
undoIRI string,
|
||||||
// first put a block in the database so we have something to undo
|
) vocab.ActivityStreamsUndo {
|
||||||
blockURI := "http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3"
|
|
||||||
dbBlockID, err := id.NewRandomULID()
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
dbBlock := >smodel.Block{
|
|
||||||
ID: dbBlockID,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
URI: blockURI,
|
|
||||||
AccountID: blockingAccount.ID,
|
|
||||||
TargetAccountID: blockedAccount.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = suite.db.PutBlock(context.Background(), dbBlock)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
asBlock, err := suite.tc.BlockToAS(context.Background(), dbBlock)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
targetAccountURI := testrig.URLMustParse(blockedAccount.URI)
|
|
||||||
|
|
||||||
// create an Undo and set the appropriate actor on it
|
|
||||||
undo := streams.NewActivityStreamsUndo()
|
undo := streams.NewActivityStreamsUndo()
|
||||||
undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor())
|
|
||||||
|
|
||||||
// Set the block as the 'object' property.
|
// Set the appropriate actor.
|
||||||
undoObject := streams.NewActivityStreamsObjectProperty()
|
undo.SetActivityStreamsActor(originalActivity.GetActivityStreamsActor())
|
||||||
undoObject.AppendActivityStreamsBlock(asBlock)
|
|
||||||
undo.SetActivityStreamsObject(undoObject)
|
|
||||||
|
|
||||||
// Set the To of the undo as the target of the block
|
// Set the original activity uri as the 'object' property.
|
||||||
|
undo.SetActivityStreamsObject(objectF())
|
||||||
|
|
||||||
|
// Set the To of the undo as the target of the activity.
|
||||||
undoTo := streams.NewActivityStreamsToProperty()
|
undoTo := streams.NewActivityStreamsToProperty()
|
||||||
undoTo.AppendIRI(targetAccountURI)
|
undoTo.AppendIRI(testrig.URLMustParse(to))
|
||||||
undo.SetActivityStreamsTo(undoTo)
|
undo.SetActivityStreamsTo(undoTo)
|
||||||
|
|
||||||
|
// Set the ID property to the undo's URI.
|
||||||
undoID := streams.NewJSONLDIdProperty()
|
undoID := streams.NewJSONLDIdProperty()
|
||||||
undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5"))
|
undoID.SetIRI(testrig.URLMustParse(undoIRI))
|
||||||
undo.SetJSONLDId(undoID)
|
undo.SetJSONLDId(undoID)
|
||||||
|
|
||||||
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
|
return undo
|
||||||
|
|
||||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(undo, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
|
|
||||||
bodyI, err := ap.Serialize(undo)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
bodyJson, err := json.Marshal(bodyI)
|
|
||||||
suite.NoError(err)
|
|
||||||
body := bytes.NewReader(bodyJson)
|
|
||||||
|
|
||||||
tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media"))
|
|
||||||
federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager)
|
|
||||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
|
||||||
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
|
|
||||||
userModule := users.New(processor)
|
|
||||||
suite.NoError(processor.Start())
|
|
||||||
|
|
||||||
// setup request
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("Signature", signature)
|
|
||||||
ctx.Request.Header.Set("Date", dateHeader)
|
|
||||||
ctx.Request.Header.Set("Digest", digestHeader)
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/activity+json")
|
|
||||||
|
|
||||||
// we need to pass the context through signature check first to set appropriate values on it
|
|
||||||
suite.signatureCheck(ctx)
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
|
||||||
gin.Param{
|
|
||||||
Key: users.UsernameKey,
|
|
||||||
Value: blockedAccount.Username,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger the function being tested
|
|
||||||
userModule.InboxPOSTHandler(ctx)
|
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.Empty(b)
|
|
||||||
suite.Equal(http.StatusOK, result.StatusCode)
|
|
||||||
|
|
||||||
// the block should be undone
|
|
||||||
block, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID)
|
|
||||||
suite.ErrorIs(err, db.ErrNoEntries)
|
|
||||||
suite.Nil(block)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InboxPostTestSuite) TestPostUpdate() {
|
func (suite *InboxPostTestSuite) newUpdatePerson(person vocab.ActivityStreamsPerson, cc string, updateIRI string) vocab.ActivityStreamsUpdate {
|
||||||
receivingAccount := suite.testAccounts["local_account_1"]
|
|
||||||
updatedAccount := *suite.testAccounts["remote_account_1"]
|
|
||||||
updatedAccount.DisplayName = "updated display name!"
|
|
||||||
|
|
||||||
// add an emoji to the account; because we're serializing this remote
|
|
||||||
// account from our own instance, we need to cheat a bit to get the emoji
|
|
||||||
// to work properly, just for this test
|
|
||||||
testEmoji := >smodel.Emoji{}
|
|
||||||
*testEmoji = *testrig.NewTestEmojis()["yell"]
|
|
||||||
testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat
|
|
||||||
updatedAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
|
|
||||||
|
|
||||||
asAccount, err := suite.tc.AccountToAS(context.Background(), &updatedAccount)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// create an update
|
// create an update
|
||||||
update := streams.NewActivityStreamsUpdate()
|
update := streams.NewActivityStreamsUpdate()
|
||||||
|
|
||||||
// set the appropriate actor on it
|
// set the appropriate actor on it
|
||||||
updateActor := streams.NewActivityStreamsActorProperty()
|
updateActor := streams.NewActivityStreamsActorProperty()
|
||||||
updateActor.AppendIRI(testrig.URLMustParse(updatedAccount.URI))
|
updateActor.AppendIRI(person.GetJSONLDId().Get())
|
||||||
update.SetActivityStreamsActor(updateActor)
|
update.SetActivityStreamsActor(updateActor)
|
||||||
|
|
||||||
// Set the account as the 'object' property.
|
// Set the person as the 'object' property.
|
||||||
updateObject := streams.NewActivityStreamsObjectProperty()
|
updateObject := streams.NewActivityStreamsObjectProperty()
|
||||||
updateObject.AppendActivityStreamsPerson(asAccount)
|
updateObject.AppendActivityStreamsPerson(person)
|
||||||
update.SetActivityStreamsObject(updateObject)
|
update.SetActivityStreamsObject(updateObject)
|
||||||
|
|
||||||
// Set the To of the update as public
|
// Set the To of the update as public
|
||||||
|
@ -265,77 +199,179 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
|
||||||
|
|
||||||
// set the cc of the update to the receivingAccount
|
// set the cc of the update to the receivingAccount
|
||||||
updateCC := streams.NewActivityStreamsCcProperty()
|
updateCC := streams.NewActivityStreamsCcProperty()
|
||||||
updateCC.AppendIRI(testrig.URLMustParse(receivingAccount.URI))
|
updateCC.AppendIRI(testrig.URLMustParse(cc))
|
||||||
update.SetActivityStreamsCc(updateCC)
|
update.SetActivityStreamsCc(updateCC)
|
||||||
|
|
||||||
// set some random-ass ID for the activity
|
// set some random-ass ID for the activity
|
||||||
undoID := streams.NewJSONLDIdProperty()
|
updateID := streams.NewJSONLDIdProperty()
|
||||||
undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b"))
|
updateID.SetIRI(testrig.URLMustParse(updateIRI))
|
||||||
update.SetJSONLDId(undoID)
|
update.SetJSONLDId(updateID)
|
||||||
|
|
||||||
targetURI := testrig.URLMustParse(receivingAccount.InboxURI)
|
return update
|
||||||
|
}
|
||||||
|
|
||||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(update, updatedAccount.PublicKeyURI, updatedAccount.PrivateKey, targetURI)
|
func (suite *InboxPostTestSuite) newDelete(actorIRI string, objectIRI string, deleteIRI string) vocab.ActivityStreamsDelete {
|
||||||
bodyI, err := ap.Serialize(update)
|
// create a delete
|
||||||
suite.NoError(err)
|
delete := streams.NewActivityStreamsDelete()
|
||||||
|
|
||||||
bodyJson, err := json.Marshal(bodyI)
|
// set the appropriate actor on it
|
||||||
suite.NoError(err)
|
deleteActor := streams.NewActivityStreamsActorProperty()
|
||||||
body := bytes.NewReader(bodyJson)
|
deleteActor.AppendIRI(testrig.URLMustParse(actorIRI))
|
||||||
|
delete.SetActivityStreamsActor(deleteActor)
|
||||||
|
|
||||||
// use a different version of the mock http client which serves the updated
|
// Set 'object' property.
|
||||||
// version of the remote account, as though it had been updated there too;
|
deleteObject := streams.NewActivityStreamsObjectProperty()
|
||||||
// this is needed so it can be dereferenced + updated properly
|
deleteObject.AppendIRI(testrig.URLMustParse(objectIRI))
|
||||||
mockHTTPClient := testrig.NewMockHTTPClient(nil, "../../../../testrig/media")
|
delete.SetActivityStreamsObject(deleteObject)
|
||||||
mockHTTPClient.TestRemotePeople = map[string]vocab.ActivityStreamsPerson{
|
|
||||||
updatedAccount.URI: asAccount,
|
// Set the To of the delete as public
|
||||||
|
deleteTo := streams.NewActivityStreamsToProperty()
|
||||||
|
deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI))
|
||||||
|
delete.SetActivityStreamsTo(deleteTo)
|
||||||
|
|
||||||
|
// set some random-ass ID for the activity
|
||||||
|
deleteID := streams.NewJSONLDIdProperty()
|
||||||
|
deleteID.SetIRI(testrig.URLMustParse(deleteIRI))
|
||||||
|
delete.SetJSONLDId(deleteID)
|
||||||
|
|
||||||
|
return delete
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPostBlock verifies that a remote account can block one of
|
||||||
|
// our instance users.
|
||||||
|
func (suite *InboxPostTestSuite) TestPostBlock() {
|
||||||
|
var (
|
||||||
|
requestingAccount = suite.testAccounts["remote_account_1"]
|
||||||
|
targetAccount = suite.testAccounts["local_account_1"]
|
||||||
|
activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3"
|
||||||
|
)
|
||||||
|
|
||||||
|
block := suite.newBlock(activityID, requestingAccount, targetAccount)
|
||||||
|
|
||||||
|
// Block.
|
||||||
|
suite.inboxPost(
|
||||||
|
block,
|
||||||
|
requestingAccount,
|
||||||
|
targetAccount,
|
||||||
|
http.StatusAccepted,
|
||||||
|
`{"status":"Accepted"}`,
|
||||||
|
suite.signatureCheck,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure block created in the database.
|
||||||
|
var (
|
||||||
|
dbBlock *gtsmodel.Block
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if !testrig.WaitFor(func() bool {
|
||||||
|
dbBlock, err = suite.db.GetBlock(context.Background(), requestingAccount.ID, targetAccount.ID)
|
||||||
|
return err == nil && dbBlock != nil
|
||||||
|
}) {
|
||||||
|
suite.FailNow("timed out waiting for block to be created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPostUnblock verifies that a remote account who blocks
|
||||||
|
// one of our instance users should be able to undo that block.
|
||||||
|
func (suite *InboxPostTestSuite) TestPostUnblock() {
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
requestingAccount = suite.testAccounts["remote_account_1"]
|
||||||
|
targetAccount = suite.testAccounts["local_account_1"]
|
||||||
|
blockID = "http://fossbros-anonymous.io/blocks/01H1462TPRTVG2RTQCTSQ7N6Q0"
|
||||||
|
undoID = "http://fossbros-anonymous.io/some-activity/01H1463RDQNG5H98F29BXYHW6B"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Put a block in the database so we have something to undo.
|
||||||
|
block := >smodel.Block{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
URI: blockID,
|
||||||
|
AccountID: requestingAccount.ID,
|
||||||
|
TargetAccountID: targetAccount.ID,
|
||||||
|
}
|
||||||
|
if err := suite.db.PutBlock(ctx, block); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
tc := testrig.NewTestTransportController(&suite.state, mockHTTPClient)
|
// Create the undo from the AS model block.
|
||||||
federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager)
|
asBlock, err := suite.tc.BlockToAS(ctx, block)
|
||||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
if err != nil {
|
||||||
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
|
suite.FailNow(err.Error())
|
||||||
userModule := users.New(processor)
|
|
||||||
suite.NoError(processor.Start())
|
|
||||||
|
|
||||||
// setup request
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("Signature", signature)
|
|
||||||
ctx.Request.Header.Set("Date", dateHeader)
|
|
||||||
ctx.Request.Header.Set("Digest", digestHeader)
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/activity+json")
|
|
||||||
|
|
||||||
// we need to pass the context through signature check first to set appropriate values on it
|
|
||||||
suite.signatureCheck(ctx)
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
|
||||||
gin.Param{
|
|
||||||
Key: users.UsernameKey,
|
|
||||||
Value: receivingAccount.Username,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger the function being tested
|
undo := suite.newUndo(asBlock, func() vocab.ActivityStreamsObjectProperty {
|
||||||
userModule.InboxPOSTHandler(ctx)
|
// Append the whole block as Object.
|
||||||
|
op := streams.NewActivityStreamsObjectProperty()
|
||||||
|
op.AppendActivityStreamsBlock(asBlock)
|
||||||
|
return op
|
||||||
|
}, targetAccount.URI, undoID)
|
||||||
|
|
||||||
result := recorder.Result()
|
// Undo.
|
||||||
defer result.Body.Close()
|
suite.inboxPost(
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
undo,
|
||||||
suite.NoError(err)
|
requestingAccount,
|
||||||
suite.Empty(b)
|
targetAccount,
|
||||||
suite.Equal(http.StatusOK, result.StatusCode)
|
http.StatusAccepted,
|
||||||
|
`{"status":"Accepted"}`,
|
||||||
|
suite.signatureCheck,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure block removed from the database.
|
||||||
|
if !testrig.WaitFor(func() bool {
|
||||||
|
_, err := suite.db.GetBlockByID(ctx, block.ID)
|
||||||
|
return errors.Is(err, db.ErrNoEntries)
|
||||||
|
}) {
|
||||||
|
suite.FailNow("timed out waiting for block to be removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InboxPostTestSuite) TestPostUpdate() {
|
||||||
|
var (
|
||||||
|
requestingAccount = new(gtsmodel.Account)
|
||||||
|
targetAccount = suite.testAccounts["local_account_1"]
|
||||||
|
activityID = "http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5"
|
||||||
|
updatedDisplayName = "updated display name!"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy the requesting account, since we'll be changing it.
|
||||||
|
*requestingAccount = *suite.testAccounts["remote_account_1"]
|
||||||
|
|
||||||
|
// Update the account's display name.
|
||||||
|
requestingAccount.DisplayName = updatedDisplayName
|
||||||
|
|
||||||
|
// Add an emoji to the account; because we're serializing this
|
||||||
|
// remote account from our own instance, we need to cheat a bit
|
||||||
|
// to get the emoji to work properly, just for this test.
|
||||||
|
testEmoji := >smodel.Emoji{}
|
||||||
|
*testEmoji = *testrig.NewTestEmojis()["yell"]
|
||||||
|
testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat
|
||||||
|
requestingAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
|
||||||
|
|
||||||
|
// Create an update from the account.
|
||||||
|
asAccount, err := suite.tc.AccountToAS(context.Background(), requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
update := suite.newUpdatePerson(asAccount, targetAccount.URI, activityID)
|
||||||
|
|
||||||
|
// Update.
|
||||||
|
suite.inboxPost(
|
||||||
|
update,
|
||||||
|
requestingAccount,
|
||||||
|
targetAccount,
|
||||||
|
http.StatusAccepted,
|
||||||
|
`{"status":"Accepted"}`,
|
||||||
|
suite.signatureCheck,
|
||||||
|
)
|
||||||
|
|
||||||
// account should be changed in the database now
|
// account should be changed in the database now
|
||||||
var dbUpdatedAccount *gtsmodel.Account
|
var dbUpdatedAccount *gtsmodel.Account
|
||||||
|
|
||||||
if !testrig.WaitFor(func() bool {
|
if !testrig.WaitFor(func() bool {
|
||||||
// displayName should be updated
|
// displayName should be updated
|
||||||
dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), updatedAccount.ID)
|
dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), requestingAccount.ID)
|
||||||
return dbUpdatedAccount.DisplayName == "updated display name!"
|
return dbUpdatedAccount.DisplayName == updatedDisplayName
|
||||||
}) {
|
}) {
|
||||||
suite.FailNow("timed out waiting for account update")
|
suite.FailNow("timed out waiting for account update")
|
||||||
}
|
}
|
||||||
|
@ -347,134 +383,82 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
|
||||||
suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
|
suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
|
||||||
|
|
||||||
// everything else should be the same as it was before
|
// everything else should be the same as it was before
|
||||||
suite.EqualValues(updatedAccount.Username, dbUpdatedAccount.Username)
|
suite.EqualValues(requestingAccount.Username, dbUpdatedAccount.Username)
|
||||||
suite.EqualValues(updatedAccount.Domain, dbUpdatedAccount.Domain)
|
suite.EqualValues(requestingAccount.Domain, dbUpdatedAccount.Domain)
|
||||||
suite.EqualValues(updatedAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID)
|
suite.EqualValues(requestingAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID)
|
||||||
suite.EqualValues(updatedAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment)
|
suite.EqualValues(requestingAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment)
|
||||||
suite.EqualValues(updatedAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL)
|
suite.EqualValues(requestingAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL)
|
||||||
suite.EqualValues(updatedAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID)
|
suite.EqualValues(requestingAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID)
|
||||||
suite.EqualValues(updatedAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment)
|
suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment)
|
||||||
suite.EqualValues(updatedAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL)
|
suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL)
|
||||||
suite.EqualValues(updatedAccount.Note, dbUpdatedAccount.Note)
|
suite.EqualValues(requestingAccount.Note, dbUpdatedAccount.Note)
|
||||||
suite.EqualValues(updatedAccount.Memorial, dbUpdatedAccount.Memorial)
|
suite.EqualValues(requestingAccount.Memorial, dbUpdatedAccount.Memorial)
|
||||||
suite.EqualValues(updatedAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs)
|
suite.EqualValues(requestingAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs)
|
||||||
suite.EqualValues(updatedAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID)
|
suite.EqualValues(requestingAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID)
|
||||||
suite.EqualValues(updatedAccount.Bot, dbUpdatedAccount.Bot)
|
suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot)
|
||||||
suite.EqualValues(updatedAccount.Reason, dbUpdatedAccount.Reason)
|
suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason)
|
||||||
suite.EqualValues(updatedAccount.Locked, dbUpdatedAccount.Locked)
|
suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked)
|
||||||
suite.EqualValues(updatedAccount.Discoverable, dbUpdatedAccount.Discoverable)
|
suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable)
|
||||||
suite.EqualValues(updatedAccount.Privacy, dbUpdatedAccount.Privacy)
|
suite.EqualValues(requestingAccount.Privacy, dbUpdatedAccount.Privacy)
|
||||||
suite.EqualValues(updatedAccount.Sensitive, dbUpdatedAccount.Sensitive)
|
suite.EqualValues(requestingAccount.Sensitive, dbUpdatedAccount.Sensitive)
|
||||||
suite.EqualValues(updatedAccount.Language, dbUpdatedAccount.Language)
|
suite.EqualValues(requestingAccount.Language, dbUpdatedAccount.Language)
|
||||||
suite.EqualValues(updatedAccount.URI, dbUpdatedAccount.URI)
|
suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI)
|
||||||
suite.EqualValues(updatedAccount.URL, dbUpdatedAccount.URL)
|
suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL)
|
||||||
suite.EqualValues(updatedAccount.InboxURI, dbUpdatedAccount.InboxURI)
|
suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI)
|
||||||
suite.EqualValues(updatedAccount.OutboxURI, dbUpdatedAccount.OutboxURI)
|
suite.EqualValues(requestingAccount.OutboxURI, dbUpdatedAccount.OutboxURI)
|
||||||
suite.EqualValues(updatedAccount.FollowingURI, dbUpdatedAccount.FollowingURI)
|
suite.EqualValues(requestingAccount.FollowingURI, dbUpdatedAccount.FollowingURI)
|
||||||
suite.EqualValues(updatedAccount.FollowersURI, dbUpdatedAccount.FollowersURI)
|
suite.EqualValues(requestingAccount.FollowersURI, dbUpdatedAccount.FollowersURI)
|
||||||
suite.EqualValues(updatedAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI)
|
suite.EqualValues(requestingAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI)
|
||||||
suite.EqualValues(updatedAccount.ActorType, dbUpdatedAccount.ActorType)
|
suite.EqualValues(requestingAccount.ActorType, dbUpdatedAccount.ActorType)
|
||||||
suite.EqualValues(updatedAccount.PublicKey, dbUpdatedAccount.PublicKey)
|
suite.EqualValues(requestingAccount.PublicKey, dbUpdatedAccount.PublicKey)
|
||||||
suite.EqualValues(updatedAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI)
|
suite.EqualValues(requestingAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI)
|
||||||
suite.EqualValues(updatedAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt)
|
suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt)
|
||||||
suite.EqualValues(updatedAccount.SilencedAt, dbUpdatedAccount.SilencedAt)
|
suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt)
|
||||||
suite.EqualValues(updatedAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt)
|
suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt)
|
||||||
suite.EqualValues(updatedAccount.HideCollections, dbUpdatedAccount.HideCollections)
|
suite.EqualValues(requestingAccount.HideCollections, dbUpdatedAccount.HideCollections)
|
||||||
suite.EqualValues(updatedAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin)
|
suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InboxPostTestSuite) TestPostDelete() {
|
func (suite *InboxPostTestSuite) TestPostDelete() {
|
||||||
deletedAccount := *suite.testAccounts["remote_account_1"]
|
var (
|
||||||
receivingAccount := suite.testAccounts["local_account_1"]
|
ctx = context.Background()
|
||||||
|
requestingAccount = suite.testAccounts["remote_account_1"]
|
||||||
|
targetAccount = suite.testAccounts["local_account_1"]
|
||||||
|
activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3"
|
||||||
|
)
|
||||||
|
|
||||||
// create a delete
|
delete := suite.newDelete(requestingAccount.URI, requestingAccount.URI, activityID)
|
||||||
delete := streams.NewActivityStreamsDelete()
|
|
||||||
|
|
||||||
// set the appropriate actor on it
|
// Delete.
|
||||||
deleteActor := streams.NewActivityStreamsActorProperty()
|
suite.inboxPost(
|
||||||
deleteActor.AppendIRI(testrig.URLMustParse(deletedAccount.URI))
|
delete,
|
||||||
delete.SetActivityStreamsActor(deleteActor)
|
requestingAccount,
|
||||||
|
targetAccount,
|
||||||
// Set the account iri as the 'object' property.
|
http.StatusAccepted,
|
||||||
deleteObject := streams.NewActivityStreamsObjectProperty()
|
`{"status":"Accepted"}`,
|
||||||
deleteObject.AppendIRI(testrig.URLMustParse(deletedAccount.URI))
|
suite.signatureCheck,
|
||||||
delete.SetActivityStreamsObject(deleteObject)
|
)
|
||||||
|
|
||||||
// Set the To of the delete as public
|
|
||||||
deleteTo := streams.NewActivityStreamsToProperty()
|
|
||||||
deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI))
|
|
||||||
delete.SetActivityStreamsTo(deleteTo)
|
|
||||||
|
|
||||||
// set some random-ass ID for the activity
|
|
||||||
deleteID := streams.NewJSONLDIdProperty()
|
|
||||||
deleteID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b"))
|
|
||||||
delete.SetJSONLDId(deleteID)
|
|
||||||
|
|
||||||
targetURI := testrig.URLMustParse(receivingAccount.InboxURI)
|
|
||||||
|
|
||||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(delete, deletedAccount.PublicKeyURI, deletedAccount.PrivateKey, targetURI)
|
|
||||||
bodyI, err := ap.Serialize(delete)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
bodyJson, err := json.Marshal(bodyI)
|
|
||||||
suite.NoError(err)
|
|
||||||
body := bytes.NewReader(bodyJson)
|
|
||||||
|
|
||||||
tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media"))
|
|
||||||
federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager)
|
|
||||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
|
||||||
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
|
|
||||||
userModule := users.New(processor)
|
|
||||||
suite.NoError(processor.Start())
|
|
||||||
|
|
||||||
// setup request
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting
|
|
||||||
ctx.Request.Header.Set("Signature", signature)
|
|
||||||
ctx.Request.Header.Set("Date", dateHeader)
|
|
||||||
ctx.Request.Header.Set("Digest", digestHeader)
|
|
||||||
ctx.Request.Header.Set("Content-Type", "application/activity+json")
|
|
||||||
|
|
||||||
// we need to pass the context through signature check first to set appropriate values on it
|
|
||||||
suite.signatureCheck(ctx)
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
|
||||||
gin.Param{
|
|
||||||
Key: users.UsernameKey,
|
|
||||||
Value: receivingAccount.Username,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger the function being tested
|
|
||||||
userModule.InboxPOSTHandler(ctx)
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.Empty(b)
|
|
||||||
suite.Equal(http.StatusOK, result.StatusCode)
|
|
||||||
|
|
||||||
if !testrig.WaitFor(func() bool {
|
if !testrig.WaitFor(func() bool {
|
||||||
// local account 2 blocked foss_satan, that block should be gone now
|
// local account 2 blocked foss_satan, that block should be gone now
|
||||||
testBlock := suite.testBlocks["local_account_2_block_remote_account_1"]
|
testBlock := suite.testBlocks["local_account_2_block_remote_account_1"]
|
||||||
dbBlock := >smodel.Block{}
|
_, err := suite.db.GetBlockByID(ctx, testBlock.ID)
|
||||||
err = suite.db.GetByID(ctx, testBlock.ID, dbBlock)
|
|
||||||
return suite.ErrorIs(err, db.ErrNoEntries)
|
return suite.ErrorIs(err, db.ErrNoEntries)
|
||||||
}) {
|
}) {
|
||||||
suite.FailNow("timed out waiting for block to be removed")
|
suite.FailNow("timed out waiting for block to be removed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// no statuses from foss satan should be left in the database
|
if !testrig.WaitFor(func() bool {
|
||||||
dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false)
|
// no statuses from foss satan should be left in the database
|
||||||
suite.ErrorIs(err, db.ErrNoEntries)
|
dbStatuses, err := suite.db.GetAccountStatuses(ctx, requestingAccount.ID, 0, false, false, "", "", false, false)
|
||||||
suite.Empty(dbStatuses)
|
return len(dbStatuses) == 0 && errors.Is(err, db.ErrNoEntries)
|
||||||
|
}) {
|
||||||
|
suite.FailNow("timed out waiting for statuses to be removed")
|
||||||
|
}
|
||||||
|
|
||||||
dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID)
|
// Account should be stubbified.
|
||||||
|
dbAccount, err := suite.db.GetAccountByID(ctx, requestingAccount.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.Empty(dbAccount.Note)
|
suite.Empty(dbAccount.Note)
|
||||||
suite.Empty(dbAccount.DisplayName)
|
suite.Empty(dbAccount.DisplayName)
|
||||||
suite.Empty(dbAccount.AvatarMediaAttachmentID)
|
suite.Empty(dbAccount.AvatarMediaAttachmentID)
|
||||||
|
@ -489,6 +473,69 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
|
||||||
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
|
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *InboxPostTestSuite) TestPostEmptyCreate() {
|
||||||
|
var (
|
||||||
|
requestingAccount = suite.testAccounts["remote_account_1"]
|
||||||
|
targetAccount = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Post a create with no object.
|
||||||
|
create := streams.NewActivityStreamsCreate()
|
||||||
|
|
||||||
|
suite.inboxPost(
|
||||||
|
create,
|
||||||
|
requestingAccount,
|
||||||
|
targetAccount,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
`{"error":"Bad Request: incoming Activity Create did not have required id property set"}`,
|
||||||
|
suite.signatureCheck,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() {
|
||||||
|
var (
|
||||||
|
requestingAccount = suite.testAccounts["remote_account_1"]
|
||||||
|
targetAccount = suite.testAccounts["local_account_2"]
|
||||||
|
activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3"
|
||||||
|
)
|
||||||
|
|
||||||
|
person, err := suite.tc.AccountToAS(context.Background(), requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post an update from foss satan to turtle, who blocks him.
|
||||||
|
update := suite.newUpdatePerson(person, targetAccount.URI, activityID)
|
||||||
|
|
||||||
|
suite.inboxPost(
|
||||||
|
update,
|
||||||
|
requestingAccount,
|
||||||
|
targetAccount,
|
||||||
|
http.StatusForbidden,
|
||||||
|
`{"error":"Forbidden"}`,
|
||||||
|
suite.signatureCheck,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InboxPostTestSuite) TestPostUnauthorized() {
|
||||||
|
var (
|
||||||
|
requestingAccount = suite.testAccounts["remote_account_1"]
|
||||||
|
targetAccount = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Post an empty create.
|
||||||
|
create := streams.NewActivityStreamsCreate()
|
||||||
|
|
||||||
|
suite.inboxPost(
|
||||||
|
create,
|
||||||
|
requestingAccount,
|
||||||
|
targetAccount,
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
`{"error":"Unauthorized"}`,
|
||||||
|
// Omit signature check middleware.
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestInboxPostTestSuite(t *testing.T) {
|
func TestInboxPostTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &InboxPostTestSuite{})
|
suite.Run(t, &InboxPostTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package federation
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -31,21 +32,56 @@ import (
|
||||||
"github.com/superseriousbusiness/activity/streams"
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Potential incoming Content-Type header values; be
|
// IsASMediaType will return whether the given content-type string
|
||||||
// lenient with whitespace and quotation mark placement.
|
// matches one of the 2 possible ActivityStreams incoming content types:
|
||||||
var activityStreamsMediaTypes = []string{
|
// - application/activity+json
|
||||||
"application/activity+json",
|
// - application/ld+json;profile=https://w3.org/ns/activitystreams
|
||||||
"application/ld+json;profile=https://www.w3.org/ns/activitystreams",
|
//
|
||||||
"application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"",
|
// Where for the above we are leniant with whitespace and quotes.
|
||||||
"application/ld+json ;profile=https://www.w3.org/ns/activitystreams",
|
func IsASMediaType(ct string) bool {
|
||||||
"application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"",
|
var (
|
||||||
"application/ld+json ; profile=https://www.w3.org/ns/activitystreams",
|
// First content-type part,
|
||||||
"application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"",
|
// contains the application/...
|
||||||
"application/ld+json; profile=https://www.w3.org/ns/activitystreams",
|
p1 string = ct //nolint:revive
|
||||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
|
||||||
|
// Second content-type part,
|
||||||
|
// contains AS IRI if provided
|
||||||
|
p2 string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split content-type by semi-colon.
|
||||||
|
sep := strings.IndexByte(ct, ';')
|
||||||
|
if sep >= 0 {
|
||||||
|
p1 = ct[:sep]
|
||||||
|
p2 = ct[sep+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim any ending space from the
|
||||||
|
// main content-type part of string.
|
||||||
|
p1 = strings.TrimRight(p1, " ")
|
||||||
|
|
||||||
|
switch p1 {
|
||||||
|
case "application/activity+json":
|
||||||
|
return p2 == ""
|
||||||
|
|
||||||
|
case "application/ld+json":
|
||||||
|
// Trim all start/end space.
|
||||||
|
p2 = strings.Trim(p2, " ")
|
||||||
|
|
||||||
|
// Drop any quotes around the URI str.
|
||||||
|
p2 = strings.ReplaceAll(p2, "\"", "")
|
||||||
|
|
||||||
|
// End part must be a ref to the main AS namespace IRI.
|
||||||
|
return p2 == "profile=https://www.w3.org/ns/activitystreams"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// federatingActor wraps the pub.FederatingActor interface
|
// federatingActor wraps the pub.FederatingActor interface
|
||||||
|
@ -67,99 +103,165 @@ func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
|
|
||||||
log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox)
|
|
||||||
return f.wrapped.Send(c, outbox, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
|
||||||
return f.PostInboxScheme(c, w, r, "https")
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostInboxScheme is a reimplementation of the default baseActor
|
// PostInboxScheme is a reimplementation of the default baseActor
|
||||||
// implementation of PostInboxScheme in pub/base_actor.go.
|
// implementation of PostInboxScheme in pub/base_actor.go.
|
||||||
//
|
//
|
||||||
// Key differences from that implementation:
|
// Key differences from that implementation:
|
||||||
// - More explicit debug logging when a request is not processed.
|
// - More explicit debug logging when a request is not processed.
|
||||||
// - Normalize content of activity object.
|
// - Normalize content of activity object.
|
||||||
|
// - *ALWAYS* return gtserror.WithCode if there's an issue, to
|
||||||
|
// provide more helpful messages to remote callers.
|
||||||
// - Return code 202 instead of 200 on successful POST, to reflect
|
// - Return code 202 instead of 200 on successful POST, to reflect
|
||||||
// that we process most side effects asynchronously.
|
// that we process most side effects asynchronously.
|
||||||
func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
|
func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
|
||||||
l := log.
|
l := log.WithContext(ctx).
|
||||||
WithContext(ctx).
|
|
||||||
WithFields([]kv.Field{
|
WithFields([]kv.Field{
|
||||||
{"userAgent", r.UserAgent()},
|
{"userAgent", r.UserAgent()},
|
||||||
{"path", r.URL.Path},
|
{"path", r.URL.Path},
|
||||||
}...)
|
}...)
|
||||||
|
|
||||||
// Do nothing if this is not an ActivityPub POST request.
|
// Ensure valid ActivityPub Content-Type.
|
||||||
if !func() bool {
|
// https://www.w3.org/TR/activitypub/#server-to-server-interactions
|
||||||
if r.Method != http.MethodPost {
|
if ct := r.Header.Get("Content-Type"); !IsASMediaType(ct) {
|
||||||
l.Debugf("inbox request was %s rather than required POST", r.Method)
|
const ct1 = "application/activity+json"
|
||||||
return false
|
const ct2 = "application/ld+json;profile=https://w3.org/ns/activitystreams"
|
||||||
}
|
err := fmt.Errorf("Content-Type %s not acceptable, this endpoint accepts: [%q %q]", ct, ct1, ct2)
|
||||||
|
return false, gtserror.NewErrorNotAcceptable(err)
|
||||||
contentType := r.Header.Get("Content-Type")
|
|
||||||
for _, mediaType := range activityStreamsMediaTypes {
|
|
||||||
if strings.Contains(contentType, mediaType) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Debugf("inbox POST request content-type %s was not recognized", contentType)
|
|
||||||
return false
|
|
||||||
}() {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the peer request is authentic.
|
// Authenticate request by checking http signature.
|
||||||
ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r)
|
ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return false, gtserror.NewErrorInternalError(err)
|
||||||
} else if !authenticated {
|
} else if !authenticated {
|
||||||
return true, nil
|
return false, gtserror.NewErrorUnauthorized(errors.New("unauthorized"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin processing the request, but note that we have
|
/*
|
||||||
// not yet applied authorization (ex: blocks).
|
Begin processing the request, but note that we
|
||||||
|
have not yet applied authorization (ie., blocks).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Obtain the activity; reject unknown activities.
|
||||||
|
activity, errWithCode := resolveActivity(ctx, r)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return false, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set additional context data.
|
||||||
|
ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
|
||||||
|
if err != nil {
|
||||||
|
return false, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authorization of the activity.
|
||||||
|
authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity)
|
||||||
|
if err != nil {
|
||||||
|
return false, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authorized {
|
||||||
|
return false, gtserror.NewErrorForbidden(errors.New("blocked"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy existing URL + add request host and scheme.
|
||||||
|
inboxID := func() *url.URL {
|
||||||
|
u := new(url.URL)
|
||||||
|
*u = *r.URL
|
||||||
|
u.Host = r.Host
|
||||||
|
u.Scheme = scheme
|
||||||
|
return u
|
||||||
|
}()
|
||||||
|
|
||||||
|
// At this point we have everything we need, and have verified that
|
||||||
|
// the POST request is authentic (properly signed) and authorized
|
||||||
|
// (permitted to interact with the target inbox).
|
||||||
//
|
//
|
||||||
// Obtain the activity and reject unknown activities.
|
// Post the activity to the Actor's inbox and trigger side effects .
|
||||||
|
if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil {
|
||||||
|
// Special case: We know it is a bad request if the object or
|
||||||
|
// target properties needed to be populated, but weren't.
|
||||||
|
// Send the rejection to the peer.
|
||||||
|
if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) {
|
||||||
|
// Log the original error but return something a bit more generic.
|
||||||
|
l.Debugf("malformed incoming Activity: %q", err)
|
||||||
|
err = errors.New("malformed incoming Activity: an Object and/or Target was required but not set")
|
||||||
|
return false, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's been some real error.
|
||||||
|
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err)
|
||||||
|
return false, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Side effects are complete. Now delegate determining whether
|
||||||
|
// to do inbox forwarding, as well as the action to do it.
|
||||||
|
if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil {
|
||||||
|
// As a not-ideal side-effect, InboxForwarding will try
|
||||||
|
// to create entries if the federatingDB returns `false`
|
||||||
|
// when calling `Exists()` to determine whether the Activity
|
||||||
|
// is in the database.
|
||||||
|
//
|
||||||
|
// Since our `Exists()` function currently *always*
|
||||||
|
// returns false, it will *always* attempt to insert
|
||||||
|
// the Activity. Therefore, we ignore AlreadyExists
|
||||||
|
// errors.
|
||||||
|
//
|
||||||
|
// This check may be removed when the `Exists()` func
|
||||||
|
// is updated, and/or federating callbacks are handled
|
||||||
|
// properly.
|
||||||
|
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||||
|
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err)
|
||||||
|
return false, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request is now undergoing processing. Caller
|
||||||
|
// of this function will handle writing Accepted.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveActivity is a util function for pulling a
|
||||||
|
// pub.Activity type out of an incoming POST request.
|
||||||
|
func resolveActivity(ctx context.Context, r *http.Request) (pub.Activity, gtserror.WithCode) {
|
||||||
|
// Tidy up when done.
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
b, err := io.ReadAll(r.Body)
|
b, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("PostInboxScheme: error reading request body: %w", err)
|
err = fmt.Errorf("error reading request body: %w", err)
|
||||||
return true, err
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawActivity map[string]interface{}
|
var rawActivity map[string]interface{}
|
||||||
if err := json.Unmarshal(b, &rawActivity); err != nil {
|
if err := json.Unmarshal(b, &rawActivity); err != nil {
|
||||||
err = fmt.Errorf("PostInboxScheme: error unmarshalling request body: %w", err)
|
err = fmt.Errorf("error unmarshalling request body: %w", err)
|
||||||
return true, err
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := streams.ToType(ctx, rawActivity)
|
t, err := streams.ToType(ctx, rawActivity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !streams.IsUnmatchedErr(err) {
|
if !streams.IsUnmatchedErr(err) {
|
||||||
// Real error.
|
// Real error.
|
||||||
err = fmt.Errorf("PostInboxScheme: error matching json to type: %w", err)
|
err = fmt.Errorf("error matching json to type: %w", err)
|
||||||
return true, err
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respond with bad request; we just couldn't
|
// Respond with bad request; we just couldn't
|
||||||
// match the type to one that we know about.
|
// match the type to one that we know about.
|
||||||
l.Debug("json could not be resolved to ActivityStreams value")
|
err = errors.New("body json could not be resolved to ActivityStreams value")
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activity, ok := t.(pub.Activity)
|
activity, ok := t.(pub.Activity)
|
||||||
if !ok {
|
if !ok {
|
||||||
err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t)
|
err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t)
|
||||||
return true, err
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if activity.GetJSONLDId() == nil {
|
if activity.GetJSONLDId() == nil {
|
||||||
l.Debugf("incoming Activity %s did not have required id property set", activity.GetTypeName())
|
err = fmt.Errorf("incoming Activity %s did not have required id property set", activity.GetTypeName())
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If activity Object is a Statusable, we'll want to replace the
|
// If activity Object is a Statusable, we'll want to replace the
|
||||||
|
@ -168,56 +270,21 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
|
||||||
// Likewise, if it's an Accountable, we'll normalize some fields on it.
|
// Likewise, if it's an Accountable, we'll normalize some fields on it.
|
||||||
ap.NormalizeIncomingActivityObject(activity, rawActivity)
|
ap.NormalizeIncomingActivityObject(activity, rawActivity)
|
||||||
|
|
||||||
// Allow server implementations to set context data with a hook.
|
return activity, nil
|
||||||
ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
|
}
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check authorization of the activity.
|
/*
|
||||||
authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity)
|
Functions below are just lightly wrapped versions
|
||||||
if err != nil {
|
of the original go-fed federatingActor functions.
|
||||||
return true, err
|
*/
|
||||||
} else if !authorized {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy existing URL + add request host and scheme.
|
func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||||
inboxID := func() *url.URL {
|
return f.PostInboxScheme(c, w, r, "https")
|
||||||
id := &url.URL{}
|
}
|
||||||
*id = *r.URL
|
|
||||||
id.Host = r.Host
|
|
||||||
id.Scheme = scheme
|
|
||||||
return id
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Post the activity to the actor's inbox and trigger side effects for
|
func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
|
||||||
// that particular Activity type. It is up to the delegate to resolve
|
log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox)
|
||||||
// the given map.
|
return f.wrapped.Send(c, outbox, t)
|
||||||
if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil {
|
|
||||||
// Special case: We know it is a bad request if the object or
|
|
||||||
// target properties needed to be populated, but weren't.
|
|
||||||
//
|
|
||||||
// Send the rejection to the peer.
|
|
||||||
if err == pub.ErrObjectRequired || err == pub.ErrTargetRequired {
|
|
||||||
l.Debugf("malformed incoming Activity: %s", err)
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err)
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our side effects are complete, now delegate determining whether to do inbox forwarding, as well as the action to do it.
|
|
||||||
if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil {
|
|
||||||
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err)
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request is now undergoing processing.
|
|
||||||
// Respond with an Accepted status.
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||||
|
|
|
@ -151,3 +151,51 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
|
||||||
func TestFederatingActorTestSuite(t *testing.T) {
|
func TestFederatingActorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(FederatingActorTestSuite))
|
suite.Run(t, new(FederatingActorTestSuite))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsASMediaType(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
Input string
|
||||||
|
Expect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Input: "application/activity+json",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json;profile=https://www.w3.org/ns/activitystreams",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json ;profile=https://www.w3.org/ns/activitystreams",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json ; profile=https://www.w3.org/ns/activitystreams",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json; profile=https://www.w3.org/ns/activitystreams",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if federation.IsASMediaType(test.Input) != test.Expect {
|
||||||
|
t.Errorf("did not get expected result %v for input: %s", test.Expect, test.Input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue