Compare commits

..

2 Commits

Author SHA1 Message Date
db43e381dc remove regex hostname parsing (#67)
All checks were successful
continuous-integration/drone/push Build is passing
Drop regex validation for postgres hostname, because it was breaking when running in a docker-compose stack where hostnames can be just one word.

If necessary this can be added in again later, but it probably won't be necessary because it doesn't actually add anything useful!
2021-06-26 21:13:37 +02:00
f16ba8821d Add Drone config
All checks were successful
continuous-integration/drone/push Build is passing
See https://github.com/superseriousbusiness/gotosocial/issues/65.
2021-06-26 17:25:42 +02:00
16 changed files with 322 additions and 615 deletions

19
.drone.yml Normal file
View File

@ -0,0 +1,19 @@
---
kind: pipeline
name: automated container publishing
steps:
- name: publish image
image: plugins/docker
settings:
username:
from_secret: docker_reg_username
password:
from_secret: docker_reg_passwd
repo: decentral1se/gotosocial
tags: latest
trigger:
branch:
- main
event:
exclude:
- pull_request

View File

@ -2,6 +2,8 @@
![patrons](https://img.shields.io/liberapay/patrons/dumpsterqueer.svg?logo=liberapay) ![receives](https://img.shields.io/liberapay/receives/dumpsterqueer.svg?logo=liberapay) ![patrons](https://img.shields.io/liberapay/patrons/dumpsterqueer.svg?logo=liberapay) ![receives](https://img.shields.io/liberapay/receives/dumpsterqueer.svg?logo=liberapay)
[![Build Status](https://drone.autonomic.zone/api/badges/autonomic-cooperative/gotosocial/status.svg?ref=refs/heads/main)](https://drone.autonomic.zone/autonomic-cooperative/gotosocial)
Federated social media software. Federated social media software.
![Sloth logo made by Freepik from www.flaticon.com](./web/assets/sloth.png) ![Sloth logo made by Freepik from www.flaticon.com](./web/assets/sloth.png)

View File

@ -26,7 +26,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/mail" "net/mail"
"regexp"
"strings" "strings"
"time" "time"
@ -48,7 +47,6 @@ type postgresService struct {
conn *pg.DB conn *pg.DB
log *logrus.Logger log *logrus.Logger
cancel context.CancelFunc cancel context.CancelFunc
// federationDB pub.Database
} }
// NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.
@ -120,12 +118,6 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {
return nil, errors.New("no address set") return nil, errors.New("no address set")
} }
ipv4Regex := regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`)
hostnameRegex := regexp.MustCompile(`^(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,}$`)
if !hostnameRegex.MatchString(c.DBConfig.Address) && !ipv4Regex.MatchString(c.DBConfig.Address) && c.DBConfig.Address != "localhost" {
return nil, fmt.Errorf("address %s was neither an ipv4 address nor a valid hostname", c.DBConfig.Address)
}
// validate username // validate username
if c.DBConfig.User == "" { if c.DBConfig.User == "" {
return nil, errors.New("no user set") return nil, errors.New("no user set")

View File

@ -20,10 +20,15 @@ package federation
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"net/url"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
) )
/* /*
@ -96,3 +101,53 @@ func (f *federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.Activ
// the CLIENT API, not through the federation API, so we just do nothing here. // the CLIENT API, not through the federation API, so we just do nothing here.
return streams.NewActivityStreamsOrderedCollectionPage(), nil return streams.NewActivityStreamsOrderedCollectionPage(), nil
} }
// NewTransport returns a new Transport on behalf of a specific actor.
//
// The actorBoxIRI will be either the inbox or outbox of an actor who is
// attempting to do the dereferencing or delivery. Any authentication
// scheme applied on the request must be based on this actor. The
// request must contain some sort of credential of the user, such as a
// HTTP Signature.
//
// The gofedAgent passed in should be used by the Transport
// implementation in the User-Agent, as well as the application-specific
// user agent string. The gofedAgent will indicate this library's use as
// well as the library's version number.
//
// Any server-wide rate-limiting that needs to occur should happen in a
// Transport implementation. This factory function allows this to be
// created, so peer servers are not DOS'd.
//
// Any retry logic should also be handled by the Transport
// implementation.
//
// Note that the library will not maintain a long-lived pointer to the
// returned Transport so that any private credentials are able to be
// garbage collected.
func (f *federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) {
var username string
var err error
if util.IsInboxPath(actorBoxIRI) {
username, err = util.ParseInboxPath(actorBoxIRI)
if err != nil {
return nil, fmt.Errorf("couldn't parse path %s as an inbox: %s", actorBoxIRI.String(), err)
}
} else if util.IsOutboxPath(actorBoxIRI) {
username, err = util.ParseOutboxPath(actorBoxIRI)
if err != nil {
return nil, fmt.Errorf("couldn't parse path %s as an outbox: %s", actorBoxIRI.String(), err)
}
} else {
return nil, fmt.Errorf("id %s was neither an inbox path nor an outbox path", actorBoxIRI.String())
}
account := &gtsmodel.Account{}
if err := f.db.GetLocalAccountByUsername(username, account); err != nil {
return nil, fmt.Errorf("error getting account with username %s from the db: %s", username, err)
}
return f.transportController.NewTransport(account.PublicKeyURI, account.PrivateKey)
}

View File

@ -1,151 +0,0 @@
package federation
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) {
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
}
b, err := transport.Dereference(context.Background(), remoteAccountID)
if err != nil {
return nil, fmt.Errorf("error deferencing %s: %s", remoteAccountID.String(), err)
}
m := make(map[string]interface{})
if err := json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("error unmarshalling bytes into json: %s", err)
}
t, err := streams.ToType(context.Background(), m)
if err != nil {
return nil, fmt.Errorf("error resolving json into ap vocab type: %s", err)
}
switch t.GetTypeName() {
case string(gtsmodel.ActivityStreamsPerson):
p, ok := t.(vocab.ActivityStreamsPerson)
if !ok {
return nil, errors.New("error resolving type as activitystreams person")
}
return p, nil
case string(gtsmodel.ActivityStreamsApplication):
p, ok := t.(vocab.ActivityStreamsApplication)
if !ok {
return nil, errors.New("error resolving type as activitystreams application")
}
return p, nil
case string(gtsmodel.ActivityStreamsService):
p, ok := t.(vocab.ActivityStreamsService)
if !ok {
return nil, errors.New("error resolving type as activitystreams service")
}
return p, nil
}
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
}
func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error) {
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
}
b, err := transport.Dereference(context.Background(), remoteStatusID)
if err != nil {
return nil, fmt.Errorf("error deferencing %s: %s", remoteStatusID.String(), err)
}
m := make(map[string]interface{})
if err := json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("error unmarshalling bytes into json: %s", err)
}
t, err := streams.ToType(context.Background(), m)
if err != nil {
return nil, fmt.Errorf("error resolving json into ap vocab type: %s", err)
}
// Article, Document, Image, Video, Note, Page, Event, Place, Mention, Profile
switch t.GetTypeName() {
case gtsmodel.ActivityStreamsArticle:
p, ok := t.(vocab.ActivityStreamsArticle)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsArticle")
}
return p, nil
case gtsmodel.ActivityStreamsDocument:
p, ok := t.(vocab.ActivityStreamsDocument)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsDocument")
}
return p, nil
case gtsmodel.ActivityStreamsImage:
p, ok := t.(vocab.ActivityStreamsImage)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsImage")
}
return p, nil
case gtsmodel.ActivityStreamsVideo:
p, ok := t.(vocab.ActivityStreamsVideo)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsVideo")
}
return p, nil
case gtsmodel.ActivityStreamsNote:
p, ok := t.(vocab.ActivityStreamsNote)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsNote")
}
return p, nil
case gtsmodel.ActivityStreamsPage:
p, ok := t.(vocab.ActivityStreamsPage)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsPage")
}
return p, nil
case gtsmodel.ActivityStreamsEvent:
p, ok := t.(vocab.ActivityStreamsEvent)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsEvent")
}
return p, nil
case gtsmodel.ActivityStreamsPlace:
p, ok := t.(vocab.ActivityStreamsPlace)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsPlace")
}
return p, nil
case gtsmodel.ActivityStreamsProfile:
p, ok := t.(vocab.ActivityStreamsProfile)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsProfile")
}
return p, nil
}
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
}
func (f *federator) DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error) {
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
}
return transport.DereferenceInstance(context.Background(), remoteInstanceURI)
}

View File

@ -125,29 +125,6 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
return ctx, false, fmt.Errorf("not authenticated: %s", err) return ctx, false, fmt.Errorf("not authenticated: %s", err)
} }
// authentication has passed, so add an instance entry for this instance if it hasn't been done already
i := &gtsmodel.Instance{}
if err := f.db.GetWhere([]db.Where{{Key: "domain", Value: publicKeyOwnerURI.Host, CaseInsensitive: true}}, i); err != nil {
if _, ok := err.(db.ErrNoEntries); !ok {
// there's been an actual error
return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err)
}
// we don't have an entry for this instance yet so dereference it
i, err = f.DereferenceRemoteInstance(username, &url.URL{
Scheme: publicKeyOwnerURI.Scheme,
Host: publicKeyOwnerURI.Host,
})
if err != nil {
return nil, false, fmt.Errorf("could not dereference new remote instance %s during AuthenticatePostInbox: %s", publicKeyOwnerURI.Host, err)
}
// and put it in the db
if err := f.db.Put(i); err != nil {
return nil, false, fmt.Errorf("error inserting newly dereferenced instance %s: %s", publicKeyOwnerURI.Host, err)
}
}
requestingAccount := &gtsmodel.Account{} requestingAccount := &gtsmodel.Account{}
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil { if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil {
// there's been a proper error so return it // there's been a proper error so return it

View File

@ -27,7 +27,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/typeutils"
) )
@ -50,9 +49,6 @@ type Federator interface {
// DereferenceRemoteStatus can be used to get the representation of a remote status, based on its ID (which is a URI). // DereferenceRemoteStatus can be used to get the representation of a remote status, based on its ID (which is a URI).
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments. // The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error) DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error)
// DereferenceRemoteInstance takes the URL of a remote instance, and a username (optional) to spin up a transport with. It then
// does its damnedest to get some kind of information back about the instance, trying /api/v1/instance, then /.well-known/nodeinfo
DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error)
// GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username. // GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username.
// This can be used for making signed http requests. // This can be used for making signed http requests.
// //

View File

@ -1,84 +0,0 @@
package federation
import (
"context"
"fmt"
"net/url"
"github.com/go-fed/activity/pub"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// NewTransport returns a new Transport on behalf of a specific actor.
//
// The actorBoxIRI will be either the inbox or outbox of an actor who is
// attempting to do the dereferencing or delivery. Any authentication
// scheme applied on the request must be based on this actor. The
// request must contain some sort of credential of the user, such as a
// HTTP Signature.
//
// The gofedAgent passed in should be used by the Transport
// implementation in the User-Agent, as well as the application-specific
// user agent string. The gofedAgent will indicate this library's use as
// well as the library's version number.
//
// Any server-wide rate-limiting that needs to occur should happen in a
// Transport implementation. This factory function allows this to be
// created, so peer servers are not DOS'd.
//
// Any retry logic should also be handled by the Transport
// implementation.
//
// Note that the library will not maintain a long-lived pointer to the
// returned Transport so that any private credentials are able to be
// garbage collected.
func (f *federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) {
var username string
var err error
if util.IsInboxPath(actorBoxIRI) {
username, err = util.ParseInboxPath(actorBoxIRI)
if err != nil {
return nil, fmt.Errorf("couldn't parse path %s as an inbox: %s", actorBoxIRI.String(), err)
}
} else if util.IsOutboxPath(actorBoxIRI) {
username, err = util.ParseOutboxPath(actorBoxIRI)
if err != nil {
return nil, fmt.Errorf("couldn't parse path %s as an outbox: %s", actorBoxIRI.String(), err)
}
} else {
return nil, fmt.Errorf("id %s was neither an inbox path nor an outbox path", actorBoxIRI.String())
}
account := &gtsmodel.Account{}
if err := f.db.GetLocalAccountByUsername(username, account); err != nil {
return nil, fmt.Errorf("error getting account with username %s from the db: %s", username, err)
}
return f.transportController.NewTransport(account.PublicKeyURI, account.PrivateKey)
}
func (f *federator) GetTransportForUser(username string) (transport.Transport, error) {
// We need an account to use to create a transport for dereferecing something.
// If a username has been given, we can fetch the account with that username and use it.
// Otherwise, we can take the instance account and use those credentials to make the request.
ourAccount := &gtsmodel.Account{}
var u string
if username == "" {
u = f.config.Host
} else {
u = username
}
if err := f.db.GetLocalAccountByUsername(u, ourAccount); err != nil {
return nil, fmt.Errorf("error getting account %s from db: %s", username, err)
}
transport, err := f.transportController.NewTransport(ourAccount.PublicKeyURI, ourAccount.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error creating transport for user %s: %s", username, err)
}
return transport, nil
}

View File

@ -35,6 +35,8 @@ import (
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
) )
/* /*
@ -97,14 +99,11 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca
// AuthenticateFederatedRequest authenticates any kind of incoming federated request from a remote server. This includes things like // AuthenticateFederatedRequest authenticates any kind of incoming federated request from a remote server. This includes things like
// GET requests for dereferencing our users or statuses etc, and POST requests for delivering new Activities. The function returns // GET requests for dereferencing our users or statuses etc, and POST requests for delivering new Activities. The function returns
// the URL of the owner of the public key used in the requesting http signature. // the URL of the owner of the public key used in the http signature.
// //
// Authenticate in this case is defined as making sure that the http request is actually signed by whoever claims // Authenticate in this case is defined as just making sure that the http request is actually signed by whoever claims
// to have signed it, by fetching the public key from the signature and checking it against the remote public key. // to have signed it, by fetching the public key from the signature and checking it against the remote public key. This function
// // *does not* check whether the request is authorized, only whether it's authentic.
// To avoid making unnecessary http calls towards blocked domains, this function *does* bail early if an instance-level domain block exists
// for the request from the incoming domain. However, it does not check whether individual blocks exist between the requesting user or domain
// and the requested user: this should be done elsewhere.
// //
// The provided username will be used to generate a transport for making remote requests/derefencing the public key ID of the request signature. // The provided username will be used to generate a transport for making remote requests/derefencing the public key ID of the request signature.
// Ideally you should pass in the username of the user *being requested*, so that the remote server can decide how to handle the request based on who's making it. // Ideally you should pass in the username of the user *being requested*, so that the remote server can decide how to handle the request based on who's making it.
@ -115,12 +114,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. // 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. // 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(username string, r *http.Request) (*url.URL, error) {
var publicKey interface{}
var pkOwnerURI *url.URL
var err error
// set this extra field for signature validation // set this extra field for signature validation
r.Header.Set("host", f.config.Host) r.Header.Set("host", f.config.Host)
@ -136,19 +130,11 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
return nil, fmt.Errorf("could not parse key id into a url: %s", err) return nil, 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! var publicKey interface{}
blockedDomain, err := f.blockedDomain(requestingPublicKeyID.Host) var pkOwnerURI *url.URL
if err != nil {
return nil, 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)
}
requestingRemoteAccount := &gtsmodel.Account{} requestingRemoteAccount := &gtsmodel.Account{}
requestingLocalAccount := &gtsmodel.Account{} requestingLocalAccount := &gtsmodel.Account{}
requestingHost := requestingPublicKeyID.Host if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) {
if strings.EqualFold(requestingHost, f.config.Host) {
// LOCAL ACCOUNT REQUEST // LOCAL ACCOUNT REQUEST
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing // 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 { if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
@ -171,7 +157,7 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
// REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY // REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY
// the request is remote and we don't have the public key yet, // the request is remote and we don't have the public key yet,
// so we need to authenticate the request properly by dereferencing the remote key // so we need to authenticate the request properly by dereferencing the remote key
transport, err := f.GetTransportForUser(requestedUsername) transport, err := f.GetTransportForUser(username)
if err != nil { if err != nil {
return nil, fmt.Errorf("transport err: %s", err) return nil, fmt.Errorf("transport err: %s", err)
} }
@ -226,19 +212,152 @@ func (f *federator) AuthenticateFederatedRequest(requestedUsername string, r *ht
return pkOwnerURI, nil return pkOwnerURI, nil
} }
func (f *federator) blockedDomain(host string) (bool, error) { func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) {
b := &gtsmodel.DomainBlock{}
err := f.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) transport, err := f.GetTransportForUser(username)
if err == nil { if err != nil {
// block exists return nil, fmt.Errorf("transport err: %s", err)
return true, nil
} }
if _, ok := err.(db.ErrNoEntries); ok { b, err := transport.Dereference(context.Background(), remoteAccountID)
// there are no entries so there's no block if err != nil {
return false, nil return nil, fmt.Errorf("error deferencing %s: %s", remoteAccountID.String(), err)
} }
// there's an actual error m := make(map[string]interface{})
return false, err if err := json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("error unmarshalling bytes into json: %s", err)
}
t, err := streams.ToType(context.Background(), m)
if err != nil {
return nil, fmt.Errorf("error resolving json into ap vocab type: %s", err)
}
switch t.GetTypeName() {
case string(gtsmodel.ActivityStreamsPerson):
p, ok := t.(vocab.ActivityStreamsPerson)
if !ok {
return nil, errors.New("error resolving type as activitystreams person")
}
return p, nil
case string(gtsmodel.ActivityStreamsApplication):
p, ok := t.(vocab.ActivityStreamsApplication)
if !ok {
return nil, errors.New("error resolving type as activitystreams application")
}
return p, nil
case string(gtsmodel.ActivityStreamsService):
p, ok := t.(vocab.ActivityStreamsService)
if !ok {
return nil, errors.New("error resolving type as activitystreams service")
}
return p, nil
}
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
}
func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error) {
transport, err := f.GetTransportForUser(username)
if err != nil {
return nil, fmt.Errorf("transport err: %s", err)
}
b, err := transport.Dereference(context.Background(), remoteStatusID)
if err != nil {
return nil, fmt.Errorf("error deferencing %s: %s", remoteStatusID.String(), err)
}
m := make(map[string]interface{})
if err := json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("error unmarshalling bytes into json: %s", err)
}
t, err := streams.ToType(context.Background(), m)
if err != nil {
return nil, fmt.Errorf("error resolving json into ap vocab type: %s", err)
}
// Article, Document, Image, Video, Note, Page, Event, Place, Mention, Profile
switch t.GetTypeName() {
case gtsmodel.ActivityStreamsArticle:
p, ok := t.(vocab.ActivityStreamsArticle)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsArticle")
}
return p, nil
case gtsmodel.ActivityStreamsDocument:
p, ok := t.(vocab.ActivityStreamsDocument)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsDocument")
}
return p, nil
case gtsmodel.ActivityStreamsImage:
p, ok := t.(vocab.ActivityStreamsImage)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsImage")
}
return p, nil
case gtsmodel.ActivityStreamsVideo:
p, ok := t.(vocab.ActivityStreamsVideo)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsVideo")
}
return p, nil
case gtsmodel.ActivityStreamsNote:
p, ok := t.(vocab.ActivityStreamsNote)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsNote")
}
return p, nil
case gtsmodel.ActivityStreamsPage:
p, ok := t.(vocab.ActivityStreamsPage)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsPage")
}
return p, nil
case gtsmodel.ActivityStreamsEvent:
p, ok := t.(vocab.ActivityStreamsEvent)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsEvent")
}
return p, nil
case gtsmodel.ActivityStreamsPlace:
p, ok := t.(vocab.ActivityStreamsPlace)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsPlace")
}
return p, nil
case gtsmodel.ActivityStreamsProfile:
p, ok := t.(vocab.ActivityStreamsProfile)
if !ok {
return nil, errors.New("error resolving type as ActivityStreamsProfile")
}
return p, nil
}
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
}
func (f *federator) GetTransportForUser(username string) (transport.Transport, error) {
// We need an account to use to create a transport for dereferecing the signature.
// If a username has been given, we can fetch the account with that username and use it.
// Otherwise, we can take the instance account and use those credentials to make the request.
ourAccount := &gtsmodel.Account{}
var u string
if username == "" {
u = f.config.Host
} else {
u = username
}
if err := f.db.GetLocalAccountByUsername(u, ourAccount); err != nil {
return nil, fmt.Errorf("error getting account %s from db: %s", username, err)
}
transport, err := f.transportController.NewTransport(ourAccount.PublicKeyURI, ourAccount.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error creating transport for user %s: %s", username, err)
}
return transport, nil
} }

View File

@ -28,8 +28,6 @@ type Instance struct {
Terms string Terms string
// Contact email address for this instance // Contact email address for this instance
ContactEmail string ContactEmail string
// Username of the contact account for this instance
ContactAccountUsername string
// Contact account ID in the database for this instance // Contact account ID in the database for this instance
ContactAccountID string `pg:"type:CHAR(26)"` ContactAccountID string `pg:"type:CHAR(26)"`
// Reputation score of this instance // Reputation score of this instance

View File

@ -1,16 +0,0 @@
package transport
import (
"context"
"net/url"
)
func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
return t.sigTransport.BatchDeliver(c, b, recipients)
}
func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error {
l := t.log.WithField("func", "Deliver")
l.Debugf("performing POST to %s", to.String())
return t.sigTransport.Deliver(c, b, to)
}

View File

@ -1,12 +0,0 @@
package transport
import (
"context"
"net/url"
)
func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
l := t.log.WithField("func", "Dereference")
l.Debugf("performing GET to %s", iri.String())
return t.sigTransport.Dereference(c, iri)
}

View File

@ -1,185 +0,0 @@
package transport
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
)
func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
l := t.log.WithField("func", "DereferenceInstance")
var i *gtsmodel.Instance
var err error
// First try to dereference using /api/v1/instance.
// This will provide the most complete picture of an instance, and avoid unnecessary api calls.
//
// This will only work with Mastodon-api compatible instances: Mastodon, some Pleroma instances, GoToSocial.
l.Debugf("trying to dereference instance %s by /api/v1/instance", iri.Host)
i, err = dereferenceByAPIV1Instance(t, c, iri)
if err == nil {
l.Debugf("successfully dereferenced instance using /api/v1/instance")
return i, nil
}
l.Debugf("couldn't dereference instance using /api/v1/instance: %s", err)
// If that doesn't work, try to dereference using /.well-known/nodeinfo.
// This will involve two API calls and return less info overall, but should be more widely compatible.
l.Debugf("trying to dereference instance %s by /.well-known/nodeinfo", iri.Host)
i, err = dereferenceByNodeInfo(t, c, iri)
if err == nil {
l.Debugf("successfully dereferenced instance using /.well-known/nodeinfo")
return i, nil
}
l.Debugf("couldn't dereference instance using /.well-known/nodeinfo: %s", err)
return nil, fmt.Errorf("couldn't dereference instance %s using either /api/v1/instance or /.well-known/nodeinfo", iri.Host)
}
func dereferenceByAPIV1Instance(t *transport, c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
l := t.log.WithField("func", "dereferenceByAPIV1Instance")
cleanIRI := &url.URL{
Scheme: iri.Scheme,
Host: iri.Host,
Path: "api/v1/instance",
}
l.Debugf("performing GET to %s", cleanIRI.String())
req, err := http.NewRequest("GET", cleanIRI.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
req.Header.Add("Accept", "application/json")
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", cleanIRI.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", cleanIRI.String(), resp.StatusCode, resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if len(b) == 0 {
return nil, errors.New("response bytes was len 0")
}
// try to parse the returned bytes directly into an Instance model
apiResp := &apimodel.Instance{}
if err := json.Unmarshal(b, apiResp); err != nil {
return nil, err
}
var contactUsername string
if apiResp.ContactAccount != nil {
contactUsername = apiResp.ContactAccount.Username
}
ulid, err := id.NewRandomULID()
if err != nil {
return nil, err
}
i := &gtsmodel.Instance{
ID: ulid,
Domain: iri.Host,
Title: apiResp.Title,
URI: fmt.Sprintf("%s://%s", iri.Scheme, iri.Host),
ShortDescription: apiResp.ShortDescription,
Description: apiResp.Description,
ContactEmail: apiResp.Email,
ContactAccountUsername: contactUsername,
Version: apiResp.Version,
}
return i, nil
}
func dereferenceByNodeInfo(t *transport, c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
l := t.log.WithField("func", "dereferenceByNodeInfo")
cleanIRI := &url.URL{
Scheme: iri.Scheme,
Host: iri.Host,
Path: ".well-known/nodeinfo",
}
l.Debugf("performing GET to %s", cleanIRI.String())
req, err := http.NewRequest("GET", cleanIRI.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
req.Header.Add("Accept", "application/json")
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", cleanIRI.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", cleanIRI.String(), resp.StatusCode, resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if len(b) == 0 {
return nil, errors.New("dereferenceByNodeInfo: response bytes was len 0")
}
wellKnownResp := &apimodel.WellKnownResponse{}
if err := json.Unmarshal(b, wellKnownResp); err != nil {
return nil, fmt.Errorf("dereferenceByNodeInfo: could not unmarshal server response as WellKnownResponse: %s", err)
}
// look through the links for the first one that matches the nodeinfo schema, this is what we need
var nodeinfoHref *url.URL
for _, l := range wellKnownResp.Links {
if l.Href == "" || !strings.HasPrefix(l.Rel, "http://nodeinfo.diaspora.software/ns/schema") {
continue
}
nodeinfoHref, err = url.Parse(l.Href)
if err != nil {
return nil, fmt.Errorf("dereferenceByNodeInfo: couldn't parse url %s: %s", l.Href, err)
}
}
if nodeinfoHref == nil {
return nil, errors.New("could not find nodeinfo rel in well known response")
}
aaaaaaaaaaaaaaaaa // do the second query
return nil, errors.New("not yet implemented")
}

View File

@ -1,42 +0,0 @@
package transport
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
l := t.log.WithField("func", "DereferenceMedia")
l.Debugf("performing GET to %s", iri.String())
req, err := http.NewRequest("GET", iri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
if expectedContentType == "" {
req.Header.Add("Accept", "*/*")
} else {
req.Header.Add("Accept", expectedContentType)
}
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", iri.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}

View File

@ -1,48 +0,0 @@
package transport
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
func (t *transport) Finger(c context.Context, targetUsername string, targetDomain string) ([]byte, error) {
l := t.log.WithField("func", "Finger")
urlString := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", targetDomain, targetUsername, targetDomain)
l.Debugf("performing GET to %s", urlString)
iri, err := url.Parse(urlString)
if err != nil {
return nil, fmt.Errorf("Finger: error parsing url %s: %s", urlString, err)
}
l.Debugf("performing GET to %s", iri.String())
req, err := http.NewRequest("GET", iri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
req.Header.Add("Accept", "application/json")
req.Header.Add("Accept", "application/jrd+json")
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", iri.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}

View File

@ -3,23 +3,22 @@ package transport
import ( import (
"context" "context"
"crypto" "crypto"
"fmt"
"io/ioutil"
"net/http"
"net/url" "net/url"
"sync" "sync"
"github.com/go-fed/activity/pub" "github.com/go-fed/activity/pub"
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
// Transport wraps the pub.Transport interface with some additional // Transport wraps the pub.Transport interface with some additional
// functionality for fetching remote media. // functionality for fetching remote media.
type Transport interface { type Transport interface {
pub.Transport pub.Transport
// DereferenceMedia fetches the bytes of the given media attachment IRI, with the expectedContentType.
DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error)
// DereferenceInstance dereferences remote instance information, first by checking /api/v1/instance, and then by checking /.well-known/nodeinfo.
DereferenceInstance(c context.Context, iri *url.URL) (*gtsmodel.Instance, error)
// Finger performs a webfinger request with the given username and domain, and returns the bytes from the response body. // Finger performs a webfinger request with the given username and domain, and returns the bytes from the response body.
Finger(c context.Context, targetUsername string, targetDomains string) ([]byte, error) Finger(c context.Context, targetUsername string, targetDomains string) ([]byte, error)
} }
@ -37,3 +36,91 @@ type transport struct {
getSignerMu *sync.Mutex getSignerMu *sync.Mutex
log *logrus.Logger log *logrus.Logger
} }
func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
return t.sigTransport.BatchDeliver(c, b, recipients)
}
func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error {
l := t.log.WithField("func", "Deliver")
l.Debugf("performing POST to %s", to.String())
return t.sigTransport.Deliver(c, b, to)
}
func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
l := t.log.WithField("func", "Dereference")
l.Debugf("performing GET to %s", iri.String())
return t.sigTransport.Dereference(c, iri)
}
func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
l := t.log.WithField("func", "DereferenceMedia")
l.Debugf("performing GET to %s", iri.String())
req, err := http.NewRequest("GET", iri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
if expectedContentType == "" {
req.Header.Add("Accept", "*/*")
} else {
req.Header.Add("Accept", expectedContentType)
}
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", iri.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}
func (t *transport) Finger(c context.Context, targetUsername string, targetDomain string) ([]byte, error) {
l := t.log.WithField("func", "Finger")
urlString := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", targetDomain, targetUsername, targetDomain)
l.Debugf("performing GET to %s", urlString)
iri, err := url.Parse(urlString)
if err != nil {
return nil, fmt.Errorf("Finger: error parsing url %s: %s", urlString, err)
}
l.Debugf("performing GET to %s", iri.String())
req, err := http.NewRequest("GET", iri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
req.Header.Add("Accept", "application/json")
req.Header.Add("Accept", "application/jrd+json")
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", iri.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}