diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go index 3e7d3d62b..a51cd8add 100644 --- a/internal/api/s2s/user/inboxpost.go +++ b/internal/api/s2s/user/inboxpost.go @@ -47,12 +47,13 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) { c.JSON(withCode.Code(), withCode.Safe()) return } - l.Debug(err) + l.Debugf("InboxPOSTHandler: error processing request: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) return } if !posted { + l.Debugf("request could not be handled as an AP request; headers were: %+v", c.Request.Header) c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) } } diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go index 44d60670d..3a9737860 100644 --- a/internal/api/s2s/webfinger/webfingerget.go +++ b/internal/api/s2s/webfinger/webfingerget.go @@ -24,42 +24,53 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" ) // WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org func (m *Module) WebfingerGETRequest(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "WebfingerGETRequest", + "user-agent": c.Request.UserAgent(), + }) q, set := c.GetQuery("resource") if !set || q == "" { + l.Debug("aborting request because no resource was set in query") c.JSON(http.StatusBadRequest, gin.H{"error": "no 'resource' in request query"}) return } withAcct := strings.Split(q, "acct:") if len(withAcct) != 2 { + l.Debugf("aborting request because resource query %s could not be split by 'acct:'", q) c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } usernameDomain := strings.Split(withAcct[1], "@") if len(usernameDomain) != 2 { + l.Debugf("aborting request because username and domain could not be parsed from %s", withAcct[1]) c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } username := strings.ToLower(usernameDomain[0]) domain := strings.ToLower(usernameDomain[1]) if username == "" || domain == "" { + l.Debug("aborting request because username or domain was empty") c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } if domain != m.config.Host { + l.Debug("aborting request because domain %s does not belong to this instance", domain) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("domain %s does not belong to this instance", domain)}) return } resp, err := m.processor.GetWebfingerAccount(username, c.Request) if err != nil { + l.Debugf("aborting request with an error: %s", err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) return } diff --git a/internal/api/security/robots.go b/internal/api/security/robots.go new file mode 100644 index 000000000..65056072a --- /dev/null +++ b/internal/api/security/robots.go @@ -0,0 +1,17 @@ +package security + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +const robotsString = `User-agent: * +Disallow: / +` + +// RobotsGETHandler returns the most restrictive possible robots.txt file in response to a call to /robots.txt. +// The response instructs bots with *any* user agent not to index the instance at all. +func (m *Module) RobotsGETHandler(c *gin.Context) { + c.String(http.StatusOK, robotsString) +} diff --git a/internal/api/security/security.go b/internal/api/security/security.go index 523b5dd55..7298bc7cb 100644 --- a/internal/api/security/security.go +++ b/internal/api/security/security.go @@ -19,12 +19,16 @@ package security import ( + "net/http" + "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/router" ) +const robotsPath = "/robots.txt" + // Module implements the ClientAPIModule interface for security middleware type Module struct { config *config.Config @@ -44,5 +48,6 @@ func (m *Module) Route(s router.Router) error { s.AttachMiddleware(m.FlocBlock) s.AttachMiddleware(m.ExtraHeaders) s.AttachMiddleware(m.UserAgentBlock) + s.AttachHandler(http.MethodGet, robotsPath, m.RobotsGETHandler) return nil } diff --git a/internal/api/security/useragentblock.go b/internal/api/security/useragentblock.go index f7d3a4ffc..82d65742a 100644 --- a/internal/api/security/useragentblock.go +++ b/internal/api/security/useragentblock.go @@ -23,20 +23,24 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" ) -// UserAgentBlock is a middleware that prevents google chrome cohort tracking by -// writing the Permissions-Policy header after all other parts of the request have been completed. -// See: https://plausible.io/blog/google-floc +// UserAgentBlock blocks requests with undesired, empty, or invalid user-agent strings. func (m *Module) UserAgentBlock(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "UserAgentBlock", + }) ua := c.Request.UserAgent() if ua == "" { + l.Debug("aborting request because there's no user-agent set") c.AbortWithStatus(http.StatusTeapot) return } - if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) { + if strings.Contains(strings.ToLower(ua), strings.ToLower("friendica")) { + l.Debugf("aborting request with user-agent %s because it contains 'friendica'", ua) c.AbortWithStatus(http.StatusTeapot) return } diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index d08544c99..5d7aea376 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -9,6 +9,7 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -58,7 +59,43 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA } for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() { + // check if the object is an IRI + if iter.IsIRI() { + // we have just the URI of whatever is being accepted, so we need to find out what it is + acceptedObjectIRI := iter.GetIRI() + if util.IsFollowPath(acceptedObjectIRI) { + // ACCEPT FOLLOW + gtsFollowRequest := >smodel.FollowRequest{} + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: acceptedObjectIRI.String()}}, gtsFollowRequest); err != nil { + return fmt.Errorf("ACCEPT: couldn't get follow request with id %s from the database: %s", acceptedObjectIRI.String(), err) + } + + // make sure the addressee of the original follow is the same as whatever inbox this landed in + if gtsFollowRequest.AccountID != inboxAcct.ID { + return errors.New("ACCEPT: follow object account and inbox account were not the same") + } + follow, err := f.db.AcceptFollowRequest(gtsFollowRequest.AccountID, gtsFollowRequest.TargetAccountID) + if err != nil { + return err + } + + fromFederatorChan <- gtsmodel.FromFederator{ + APObjectType: gtsmodel.ActivityStreamsFollow, + APActivityType: gtsmodel.ActivityStreamsAccept, + GTSModel: follow, + ReceivingAccount: inboxAcct, + } + + return nil + } + } + + // check if iter is an AP object / type + if iter.GetType() == nil { + continue + } switch iter.GetType().GetTypeName() { + // we have the whole object so we can figure out what we're accepting case string(gtsmodel.ActivityStreamsFollow): // ACCEPT FOLLOW asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 5e679b25e..3ab6e2eca 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -164,7 +164,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { return fmt.Errorf("could not convert Like to fave: %s", err) } - newID, err := id.NewULIDFromTime(fave.CreatedAt) + newID, err := id.NewULID() if err != nil { return err } diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index 8ca681bb2..3feee6457 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -48,6 +48,9 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) } for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() { + if iter.GetType() == nil { + continue + } switch iter.GetType().GetTypeName() { case string(gtsmodel.ActivityStreamsFollow): // UNDO FOLLOW diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go index 7e78d43c9..c4c20d7e0 100644 --- a/internal/processing/timeline.go +++ b/internal/processing/timeline.go @@ -34,15 +34,6 @@ import ( ) func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { - l := p.log.WithFields(logrus.Fields{ - "func": "HomeTimelineGet", - "maxID": maxID, - "sinceID": sinceID, - "minID": minID, - "limit": limit, - "local": local, - }) - resp := &apimodel.StatusTimelineResponse{ Statuses: []*apimodel.Status{}, } @@ -53,9 +44,6 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st sinceIDMarker := sinceID minIDMarker := minID - l.Debugf("\n entering grabloop \n") - - l.Debugf("\n querying the db \n") gtsStatuses, err := p.db.GetStatusesWhereFollowing(authed.Account.ID, maxIDMarker, sinceIDMarker, minIDMarker, limit, local) if err != nil { if _, ok := err.(db.ErrNoEntries); !ok { @@ -64,18 +52,6 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st } for _, gtsStatus := range gtsStatuses { - // haveAlready := false - // for _, apiStatus := range apiStatuses { - // if apiStatus.ID == gtsStatus.ID { - // haveAlready = true - // break - // } - // } - // if haveAlready { - // l.Debugf("\n we have status with id %d already so continuing past this iteration of the loop \n", gtsStatus.ID) - // continue - // } - // pull relevant accounts from the status -- we need this both for checking visibility and for serializing relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(gtsStatus) if err != nil { @@ -103,7 +79,6 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st continue } - l.Debug("\n appending to the statuses slice \n") apiStatuses = append(apiStatuses, apiStatus) sort.Slice(apiStatuses, func(i int, j int) bool { is, err := time.Parse(time.RFC3339, apiStatuses[i].CreatedAt) @@ -120,7 +95,6 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st }) if len(apiStatuses) == limit { - l.Debugf("\n we have enough statuses, returning \n") // we have enough break } @@ -143,7 +117,7 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st Host: p.config.Host, Path: "/api/v1/timelines/home", RawPath: url.PathEscape("api/v1/timelines/home"), - RawQuery: url.QueryEscape(fmt.Sprintf("limit=%d&max_id=%s", limit, apiStatuses[len(apiStatuses)-1].ID)), + RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, apiStatuses[len(apiStatuses)-1].ID), } next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String()) diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 3a9cc05a7..c0442b544 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -117,10 +117,14 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo // url property url, err := extractURL(accountable) - if err != nil { - return nil, fmt.Errorf("could not extract url for person with id %s: %s", uri.String(), err) + if err == nil { + // take the URL if we can find it + acct.URL = url.String() + } else { + // otherwise just take the account URI as the URL + acct.URL = uri.String() } - acct.URL = url.String() + // InboxURI if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil { diff --git a/internal/util/regexes.go b/internal/util/regexes.go index 8c61f592f..586eb30df 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -85,6 +85,11 @@ var ( // followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following followingPathRegex = regexp.MustCompile(followingPathRegexString) + followPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, FollowPath, ulidRegexString) + // followPathRegex parses a path that validates and captures the username part and the ulid part + // from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH + followPathRegex = regexp.MustCompile(followPathRegexString) + ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}` likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath) diff --git a/internal/util/uri.go b/internal/util/uri.go index b046338e8..7d4892960 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -189,6 +189,11 @@ func IsFollowingPath(id *url.URL) bool { return followingPathRegex.MatchString(id.Path) } +// IsFollowPath returns true if the given URL path corresponds to eg /users/example_username/follow/SOME_ULID_OF_A_FOLLOW +func IsFollowPath(id *url.URL) bool { + return followPathRegex.MatchString(id.Path) +} + // IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked func IsLikedPath(id *url.URL) bool { return likedPathRegex.MatchString(id.Path)