diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go index d82dd0502a..f0ec0be926 100644 --- a/routers/api/v1/activitypub/person.go +++ b/routers/api/v1/activitypub/person.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/forgefed" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/activitypub" @@ -117,21 +116,19 @@ func PersonInbox(ctx *context.APIContext) { // Make sure keyID matches the user doing the activity _, keyID, _ := getKeyID(ctx.Req) - if activity.Actor != nil && !strings.HasPrefix(keyID, activity.Actor.GetLink().String()) { - ctx.ServerError("Actor does not match HTTP signature keyID", nil) + err = checkActivityAndKeyID(activity, keyID) + if err != nil { + ctx.ServerError("keyID does not match activity", err) return } - if activity.AttributedTo != nil && !strings.HasPrefix(keyID, activity.AttributedTo.GetLink().String()) { - ctx.ServerError("AttributedTo does not match HTTP signature keyID", nil) - return - } - // TODO: Check activity.Object actor and attributedTo // Process activity switch activity.Type { case ap.FollowType: + // Following a user err = follow(ctx, activity) case ap.UndoType: + // Unfollowing a user err = unfollow(ctx, activity) case ap.CreateType: // TODO: this is kinda a hack @@ -142,9 +139,7 @@ func PersonInbox(ctx *context.APIContext) { return createComment(ctx, n) }) default: - log.Info("Incoming unsupported ActivityStreams type: %s", activity.GetType()) - ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") - return + err = fmt.Errorf("unsupported ActivityStreams activity type: %s", activity.GetType()) } if err != nil { ctx.ServerError("Could not process activity", err) diff --git a/routers/api/v1/activitypub/repo.go b/routers/api/v1/activitypub/repo.go index 9a5c0f6a30..a5c2af399e 100644 --- a/routers/api/v1/activitypub/repo.go +++ b/routers/api/v1/activitypub/repo.go @@ -5,13 +5,13 @@ package activitypub import ( + "fmt" "io" "net/http" - "strings" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/forgefed" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" ) @@ -87,7 +87,7 @@ func RepoInbox(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - body, err := io.ReadAll(ctx.Req.Body) + body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, setting.Federation.MaxSize)) if err != nil { ctx.ServerError("Error reading request body", err) return @@ -105,17 +105,9 @@ func RepoInbox(ctx *context.APIContext) { // Make sure keyID matches the user doing the activity _, keyID, _ := getKeyID(ctx.Req) - if activity.Actor != nil && !strings.HasPrefix(keyID, activity.Actor.GetLink().String()) { - ctx.ServerError("Actor does not match HTTP signature keyID", nil) - return - } - if activity.AttributedTo != nil && !strings.HasPrefix(keyID, activity.AttributedTo.GetLink().String()) { - ctx.ServerError("AttributedTo does not match HTTP signature keyID", nil) - return - } - - if activity.Object == nil { - ctx.ServerError("Activity does not contain object", err) + err = checkActivityAndKeyID(activity, keyID) + if err != nil { + ctx.ServerError("keyID does not match activity", err) return } @@ -139,18 +131,20 @@ func RepoInbox(ctx *context.APIContext) { return createComment(ctx, n) }) default: - log.Info("Incoming unsupported ActivityStreams object type: %s", activity.Object.GetType()) - ctx.PlainText(http.StatusNotImplemented, "ActivityStreams object type not supported") - return + err = fmt.Errorf("unsupported ActivityStreams object type: %s", activity.Object.GetType()) } case ap.LikeType: + // Starring a repo err = star(ctx, activity) + case ap.UndoType: + // Unstarring a repo + err = unstar(ctx, activity) default: - ctx.PlainText(http.StatusNotImplemented, "ActivityStreams type not supported") - return + err = fmt.Errorf("unsupported ActivityStreams activity type: %s", activity.GetType()) } if err != nil { - ctx.ServerError("Error when processing", err) + ctx.ServerError("Could not process activity", err) + return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go index 8129b4539c..345a4877e0 100644 --- a/routers/api/v1/activitypub/reqsignature.go +++ b/routers/api/v1/activitypub/reqsignature.go @@ -7,6 +7,7 @@ import ( "crypto" "crypto/x509" "encoding/pem" + "errors" "fmt" "net/http" "net/url" @@ -99,3 +100,25 @@ func ReqHTTPSignature() func(ctx *gitea_context.APIContext) { } } } + +// Check if the keyID matches the activity to prevent impersonation +func checkActivityAndKeyID(activity ap.Activity, keyID string) error { + if activity.Actor != nil && keyID != activity.Actor.GetLink().String() + "#main-key" { + return errors.New("actor does not match HTTP signature keyID") + } + if activity.AttributedTo != nil && keyID != activity.AttributedTo.GetLink().String() + "#main-key" { + return errors.New("attributedTo does not match HTTP signature keyID") + } + if activity.Object == nil { + return errors.New("activity does not contain object") + } + return ap.OnActivity(activity.Object, func(a *ap.Activity) error { + if a.Actor != nil && keyID != a.Actor.GetLink().String() + "#main-key" { + return errors.New("actor does not match HTTP signature keyID") + } + if a.AttributedTo != nil && keyID != a.AttributedTo.GetLink().String() + "#main-key" { + return errors.New("attributedTo does not match HTTP signature keyID") + } + return nil + }) +} diff --git a/routers/api/v1/activitypub/star.go b/routers/api/v1/activitypub/star.go index df58f77f1a..30959b25c1 100644 --- a/routers/api/v1/activitypub/star.go +++ b/routers/api/v1/activitypub/star.go @@ -26,3 +26,20 @@ func star(ctx context.Context, like ap.Like) (err error) { } return repo_model.StarRepo(user.ID, repo.ID, true) } + +// Process an Undo Like activity to unstar a repository +func unstar(ctx context.Context, unlike ap.Undo) (err error) { + like, err := ap.To[ap.Like](unlike.Object) + if err != nil { + return err + } + user, err := activitypub.PersonIRIToUser(ctx, like.Actor.GetLink()) + if err != nil { + return + } + repo, err := activitypub.RepositoryIRIToRepository(ctx, like.Object.GetLink()) + if err != nil || strings.Contains(repo.Name, "@") || repo.IsPrivate { + return + } + return repo_model.StarRepo(user.ID, repo.ID, false) +} diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 0475489640..d7b567c429 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" + repo_service "code.gitea.io/gitea/services/repository" ) // getStarredRepos returns the repos that the user with the specified userID has @@ -151,7 +152,7 @@ func Star(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - err := repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err := repo_service.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) if err != nil { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return @@ -179,7 +180,7 @@ func Unstar(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - err := repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err := repo_service.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) if err != nil { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 8856ee6626..b26e88e201 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -287,9 +287,9 @@ func Action(ctx *context.Context) { case "unwatch": err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) case "star": - err = repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true) + err = repo_service.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true) case "unstar": - err = repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false) + err = repo_service.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false) case "accept_transfer": err = acceptOrRejectRepoTransfer(ctx, true) case "reject_transfer": diff --git a/services/activitypub/activities.go b/services/activitypub/activities.go index d0db0ea6fe..177ae30e5c 100644 --- a/services/activitypub/activities.go +++ b/services/activitypub/activities.go @@ -5,28 +5,38 @@ package activitypub import ( + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/forgefed" ap "github.com/go-ap/activitypub" ) // Create Follow activity -func Follow(actorUser, followUser *user_model.User) *ap.Follow { +func Follow(actorUser, followUser *user_model.User) (follow *ap.Follow) { object := ap.PersonNew(ap.IRI(followUser.LoginName)) - follow := ap.FollowNew("", object) + follow = ap.FollowNew("", object) follow.Type = ap.FollowType follow.Actor = ap.PersonNew(ap.IRI(actorUser.GetIRI())) follow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))} - return follow + return } // Create Undo Follow activity -func Unfollow(actorUser, followUser *user_model.User) *ap.Undo { +func Unfollow(actorUser, followUser *user_model.User) (unfollow *ap.Undo) { object := ap.PersonNew(ap.IRI(followUser.LoginName)) follow := ap.FollowNew("", object) follow.Actor = ap.PersonNew(ap.IRI(actorUser.GetIRI())) - unfollow := ap.UndoNew("", follow) + unfollow = ap.UndoNew("", follow) unfollow.Type = ap.UndoType unfollow.To = ap.ItemCollection{ap.Item(ap.IRI(followUser.LoginName + "/inbox"))} - return unfollow + return +} + +// Create Like activity +func Star(user *user_model.User, repo *repo_model.Repository) (like *ap.Like) { + like = ap.LikeNew("", forgefed.RepositoryNew(ap.IRI(repo.GetIRI()))) + like.Actor = ap.PersonNew(ap.IRI(user.GetIRI())) + like.To = ap.ItemCollection{ap.IRI(repo.GetIRI() + "/inbox")} + return } diff --git a/services/repository/star.go b/services/repository/star.go new file mode 100644 index 0000000000..748a910384 --- /dev/null +++ b/services/repository/star.go @@ -0,0 +1,33 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "strings" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/activitypub" +) + +// StarRepo or unstar repository. +func StarRepo(ctx context.Context, userID, repoID int64, star bool) error { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return err + } + if strings.Contains(repo.Name, "@") { + // Federated repo + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + return err + } + err = activitypub.Send(user, activitypub.Star(user, repo)) + if err != nil { + return err + } + } + return repo_model.StarRepo(userID, repoID, star) +}