domain blocking more work

This commit is contained in:
tsmethurst 2021-06-30 12:02:28 +02:00
commit 99eb3bf564
11 changed files with 160 additions and 58 deletions

View file

@ -1,6 +1,7 @@
package admin
import (
"errors"
"fmt"
"net/http"
@ -59,6 +60,10 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
}
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest) error {
// TODO: add some validation here later if necessary
// add some more validation here later if necessary
if form.Domain == "" {
return errors.New("empty domain provided")
}
return nil
}

View file

@ -115,7 +115,7 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca
//
// Also note that this function *does not* dereference the remote account that the signature key is associated with.
// Other functions should use the returned URL to dereference the remote account, if required.
func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *http.Request) (*url.URL, error) {
func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *http.Request) (*url.URL, bool, error) {
var publicKey interface{}
var pkOwnerURI *url.URL
@ -126,23 +126,24 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
verifier, err := httpsig.NewVerifier(r)
if err != nil {
return nil, fmt.Errorf("could not create http sig verifier: %s", err)
return nil, false, fmt.Errorf("could not create http sig verifier: %s", err)
}
// The key ID should be given in the signature so that we know where to fetch it from the remote server.
// This will be something like https://example.org/users/whatever_requesting_user#main-key
requestingPublicKeyID, err := url.Parse(verifier.KeyId())
if err != nil {
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
return nil, false, fmt.Errorf("could not parse key id into a url: %s", err)
}
// if the domain is blocked we want to make as few calls towards it as possible, so already bail here if that's the case!
blockedDomain, err := f.blockedDomain(requestingPublicKeyID.Host)
if err != nil {
return nil, fmt.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)
return nil, false, fmt.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)
}
if blockedDomain {
return nil, fmt.Errorf("host %s was domain blocked, aborting auth", requestingPublicKeyID.Host)
f.log.Infof("domain %s is blocked", requestingPublicKeyID.Host)
return nil, false, nil
}
requestingRemoteAccount := &gtsmodel.Account{}
@ -152,12 +153,12 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
// LOCAL ACCOUNT REQUEST
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
return nil, false, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
}
publicKey = requestingLocalAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
if err != nil {
return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
return nil, false, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
}
} else if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingRemoteAccount); err == nil {
// REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY
@ -165,7 +166,7 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
publicKey = requestingRemoteAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI)
if err != nil {
return nil, fmt.Errorf("error parsing url %s: %s", requestingRemoteAccount.URI, err)
return nil, false, fmt.Errorf("error parsing url %s: %s", requestingRemoteAccount.URI, err)
}
} else {
// REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY
@ -173,72 +174,55 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
// so we need to authenticate the request properly by dereferencing the remote key
transport, err := f.GetTransportForUser(requestedUsername)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
return nil, false, fmt.Errorf("transport err: %s", err)
}
// The actual http call to the remote server is made right here in the Dereference function.
b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
if err != nil {
return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
return nil, false, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
}
// if the key isn't in the response, we can't authenticate the request
requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
if err != nil {
return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
return nil, false, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
}
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
return nil, false, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
}
// and decode the PEM so that we can parse it as a golang public key
pubKeyPem := pkPemProp.Get()
block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
return nil, false, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
}
publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
return nil, false, fmt.Errorf("could not parse public key from block bytes: %s", err)
}
// all good! we just need the URI of the key owner to return
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
return nil, false, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
}
pkOwnerURI = pkOwnerProp.GetIRI()
}
if publicKey == nil {
return nil, errors.New("returned public key was empty")
return nil, false, errors.New("returned public key was empty")
}
// do the actual authentication here!
algo := httpsig.RSA_SHA256 // TODO: make this more robust
if err := verifier.Verify(publicKey, algo); err != nil {
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
return nil, false, nil
}
return pkOwnerURI, nil
}
func (f *federator) blockedDomain(host string) (bool, error) {
b := &gtsmodel.DomainBlock{}
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
if err == nil {
// block exists
return true, nil
}
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries so there's no block
return false, nil
}
// there's an actual error
return false, err
return pkOwnerURI, true, nil
}

View file

@ -21,6 +21,10 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u
f.startHandshake(username, remoteAccountID)
defer f.stopHandshake(username, remoteAccountID)
if blocked, err := f.blockedDomain(remoteAccountID.Host); blocked || err != nil {
return nil, fmt.Errorf("DereferenceRemoteAccount: domain %s is blocked", remoteAccountID.Host)
}
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
@ -66,6 +70,10 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u
}
func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error) {
if blocked, err := f.blockedDomain(remoteStatusID.Host); blocked || err != nil {
return nil, fmt.Errorf("DereferenceRemoteStatus: domain %s is blocked", remoteStatusID.Host)
}
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
@ -148,6 +156,10 @@ func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url
}
func (f *federator) DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error) {
if blocked, err := f.blockedDomain(remoteInstanceURI.Host); blocked || err != nil {
return nil, fmt.Errorf("DereferenceRemoteInstance: domain %s is blocked", remoteInstanceURI.Host)
}
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
@ -186,6 +198,14 @@ func (f *federator) DereferenceStatusFields(status *gtsmodel.Status, requestingU
})
l.Debug("entering function")
statusURI, err := url.Parse(status.URI)
if err != nil {
return fmt.Errorf("DereferenceStatusFields: couldn't parse status URI %s: %s", status.URI, err)
}
if blocked, err := f.blockedDomain(statusURI.Host); blocked || err != nil {
return fmt.Errorf("DereferenceStatusFields: domain %s is blocked", statusURI.Host)
}
t, err := f.GetTransportForUser(requestingUsername)
if err != nil {
return fmt.Errorf("error creating transport: %s", err)
@ -321,6 +341,14 @@ func (f *federator) DereferenceAccountFields(account *gtsmodel.Account, requesti
"requestingUsername": requestingUsername,
})
accountURI, err := url.Parse(account.URI)
if err != nil {
return fmt.Errorf("DereferenceAccountFields: couldn't parse account URI %s: %s", account.URI, err)
}
if blocked, err := f.blockedDomain(accountURI.Host); blocked || err != nil {
return fmt.Errorf("DereferenceAccountFields: domain %s is blocked", accountURI.Host)
}
t, err := f.GetTransportForUser(requestingUsername)
if err != nil {
return fmt.Errorf("error getting transport for user: %s", err)
@ -342,12 +370,20 @@ func (f *federator) DereferenceAccountFields(account *gtsmodel.Account, requesti
func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
// we can't do anything unfortunately
return errors.New("dereferenceAnnounce: no URI to dereference")
return errors.New("DereferenceAnnounce: no URI to dereference")
}
boostedStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
if err != nil {
return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.GTSBoostedStatus.URI, err)
}
if blocked, err := f.blockedDomain(boostedStatusURI.Host); blocked || err != nil {
return fmt.Errorf("DereferenceAnnounce: domain %s is blocked", boostedStatusURI.Host)
}
// check if we already have the boosted status in the database
boostedStatus := &gtsmodel.Status{}
err := f.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
err = f.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
if err == nil {
// nice, we already have it so we don't actually need to dereference it from remote
announce.Content = boostedStatus.Content
@ -364,12 +400,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
}
// we don't have it so we need to dereference it
remoteStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
}
statusable, err := f.DereferenceRemoteStatus(requestingUsername, remoteStatusURI)
statusable, err := f.DereferenceRemoteStatus(requestingUsername, boostedStatusURI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
}
@ -460,6 +491,14 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
// to reflect the creation of these new attachments.
func (f *federator) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
accountURI, err := url.Parse(targetAccount.URI)
if err != nil {
return fmt.Errorf("fetchHeaderAndAviForAccount: couldn't parse account URI %s: %s", targetAccount.URI, err)
}
if blocked, err := f.blockedDomain(accountURI.Host); blocked || err != nil {
return fmt.Errorf("fetchHeaderAndAviForAccount: domain %s is blocked", accountURI.Host)
}
if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
a, err := f.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{
RemoteURL: targetAccount.AvatarRemoteURL,

View file

@ -119,10 +119,15 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err)
}
publicKeyOwnerURI, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r)
publicKeyOwnerURI, authenticated, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r)
if err != nil {
l.Debugf("request not authenticated: %s", err)
return ctx, false, fmt.Errorf("not authenticated: %s", err)
return ctx, false, err
}
if !authenticated {
w.WriteHeader(http.StatusForbidden)
return ctx, false, nil
}
// authentication has passed, so add an instance entry for this instance if it hasn't been done already
@ -230,6 +235,14 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
}
for _, uri := range actorIRIs {
blockedDomain, err := f.blockedDomain(uri.Host);
if err != nil {
return false, fmt.Errorf("error checking domain block: %s", err)
}
if blockedDomain {
return true, nil
}
a := &gtsmodel.Account{}
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil {
_, ok := err.(db.ErrNoEntries)

View file

@ -42,7 +42,13 @@ type Federator interface {
FederatingDB() federatingdb.DB
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
//
// If the request is valid and passes authentication, the URL of the key owner ID will be returned, as well as true, and nil.
//
// If the request does not pass authentication, or there's a domain block, nil, false, nil will be returned.
//
// If something goes wrong during authentication, nil, false, and an error will be returned.
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, bool, error)
// FingerRemoteAccount performs a webfinger lookup for a remote account, using the .well-known path. It will return the ActivityPub URI for that
// account, or an error if it doesn't exist or can't be retrieved.
FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error)
@ -97,6 +103,7 @@ func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController tr
clock: &Clock{},
typeConverter: typeConverter,
transportController: transportController,
mediaHandler: mediaHandler,
log: log,
handshakeSync: &sync.Mutex{},
}

View file

@ -30,6 +30,9 @@ import (
)
func (f *federator) FingerRemoteAccount(requestingUsername string, targetUsername string, targetDomain string) (*url.URL, error) {
if blocked, err := f.blockedDomain(targetDomain); blocked || err != nil {
return nil, fmt.Errorf("FingerRemoteAccount: domain %s is blocked", targetDomain)
}
t, err := f.GetTransportForUser(requestingUsername)
if err != nil {

View file

@ -0,0 +1,23 @@
package federation
import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (f *federator) blockedDomain(host string) (bool, error) {
b := &gtsmodel.DomainBlock{}
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
if err == nil {
// block exists
return true, nil
}
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries so there's no block
return false, nil
}
// there's an actual error
return false, err
}

View file

@ -35,7 +35,7 @@ func (p *processor) DomainBlockCreate(account *gtsmodel.Account, form *apimodel.
domainBlock := &gtsmodel.DomainBlock{}
err := p.db.GetWhere([]db.Where{{Key: "domain", Value: form.Domain, CaseInsensitive: true}}, domainBlock)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
if _, ok := err.(db.ErrNoEntries); !ok {
// something went wrong in the DB
return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error checking for existence of domain block %s: %s", form.Domain, err))
}
@ -130,6 +130,7 @@ selectAccountsLoop:
}
// an actual error has occurred
l.Errorf("domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s", block.Domain, err)
break selectAccountsLoop
}
for i, a := range accounts {

View file

@ -106,8 +106,8 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
}
} else if util.IsUserPath(request.URL) {
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(err)
}
@ -152,8 +152,8 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
}
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(err)
}
@ -197,8 +197,8 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
}
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(err)
}
@ -242,8 +242,8 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
}
// authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil {
requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
if err != nil || !authenticated {
return nil, gtserror.NewErrorNotAuthorized(err)
}
@ -356,6 +356,5 @@ func (p *processor) GetNodeInfo(request *http.Request) (*apimodel.Nodeinfo, gtse
func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
return posted, err
return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
}

View file

@ -2,6 +2,7 @@ package visibility
import (
"errors"
"net/url"
"fmt"
@ -16,6 +17,15 @@ func (f *filter) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount
"statusID": targetStatus.ID,
})
uri, err := url.Parse(targetStatus.URI)
if err != nil {
return false, fmt.Errorf("StatusVisible: error parsing uri: %s", targetStatus.URI)
}
if blocked, err := f.blockedDomain(uri.Host); blocked || err != nil {
l.Debugf("domain %s is blocked", uri.Host)
return blocked, err
}
aaaaaaaaaa
relevantAccounts, err := f.pullRelevantAccountsFromStatus(targetStatus)
if err != nil {
l.Debugf("error pulling relevant accounts for status %s: %s", targetStatus.ID, err)

View file

@ -3,6 +3,7 @@ package visibility
import (
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
@ -79,3 +80,20 @@ type relevantAccounts struct {
BoostedReplyToAccount *gtsmodel.Account
MentionedAccounts []*gtsmodel.Account
}
func (f *filter) blockedDomain(host string) (bool, error) {
b := &gtsmodel.DomainBlock{}
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b)
if err == nil {
// block exists
return true, nil
}
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries so there's no block
return false, nil
}
// there's an actual error
return false, err
}