From 6055b4fca0d90c952591f3d5a519b8f9d84a680d Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 11:42:12 +0100 Subject: [PATCH 01/11] Add todo --- routers/web/repo/setting/setting.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index bcb4de1b9e..6d8e0b7ead 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -205,7 +205,7 @@ func SettingsPost(ctx *context.Context) { ctx.Redirect(repo.Link() + "/settings") return } - + // ToDo: Validate for max length before committing to db if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return From f327c0da24eb8427532aafaa6a263166b2af9ab9 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 12:18:29 +0100 Subject: [PATCH 02/11] Cap max size of federated repo list at 2048 bytes --- modules/validation/helpers.go | 4 ++++ routers/web/repo/setting/setting.go | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 677c2f3b9d..6069714410 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -157,6 +157,10 @@ func IsValidFederatedRepoURLList(urls string) bool { return true } +func IsOfValidLength(str string) bool { + return len(str) <= 2048 +} + var ( validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 6d8e0b7ead..6df98fb47a 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -197,6 +197,11 @@ func SettingsPost(ctx *context.Context) { case form.FederationRepos == "": repo.FederationRepos = "" // Validate + case !validation.IsOfValidLength(form.FederationRepos): // ToDo: Use for public testing only. In production we might need longer strings. + ctx.Data["ERR_FederationRepos"] = true + ctx.Flash.Error("The given string was larger than 2048 bytes") + ctx.Redirect(repo.Link() + "/settings") + return case validation.IsValidFederatedRepoURL(form.FederationRepos): repo.FederationRepos = form.FederationRepos default: @@ -205,7 +210,7 @@ func SettingsPost(ctx *context.Context) { ctx.Redirect(repo.Link() + "/settings") return } - // ToDo: Validate for max length before committing to db + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return From 689837b63a25d003f8977de01ce0001ed60a26b8 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 12:59:59 +0100 Subject: [PATCH 03/11] Fix typos --- modules/forgefed/federation_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/forgefed/federation_service.go b/modules/forgefed/federation_service.go index 8b5e0714bf..dd3b94777a 100644 --- a/modules/forgefed/federation_service.go +++ b/modules/forgefed/federation_service.go @@ -37,7 +37,7 @@ func LikeActivity(ctx *context.APIContext, form any, repositoryID int64) (int, s } federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host) if err != nil { - return http.StatusInternalServerError, "Could not loading FederationHost", err + return http.StatusInternalServerError, "Could not load FederationHost", err } if federationHost == nil { result, err := CreateFederationHostFromAP(ctx, rawActorID) From e4242dafd931bc64cbb1359010f8641358972e11 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 14:41:11 +0100 Subject: [PATCH 04/11] Add Function description --- modules/forgefed/federation_service.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/forgefed/federation_service.go b/modules/forgefed/federation_service.go index dd3b94777a..68c8212882 100644 --- a/modules/forgefed/federation_service.go +++ b/modules/forgefed/federation_service.go @@ -22,6 +22,13 @@ import ( "github.com/google/uuid" ) +// LikeActivity receives a ForgeLike activity and does the following: +// Validation of the activity +// Creation of a (remote) federationHost if not existing +// Creation of a forgefed Person if not existing +// Validation of incoming RepositoryID against Local RepositoryID +// Star the repo if it wasn't already stared +// Do some mitigation against out of order attacks func LikeActivity(ctx *context.APIContext, form any, repositoryID int64) (int, string, error) { activity := form.(*forgefed.ForgeLike) if res, err := validation.IsValid(activity); !res { From a02ec0363bfc0b4f32e2eb20e32a15a41c7f5aeb Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 14:42:04 +0100 Subject: [PATCH 05/11] Add todo --- modules/forgefed/federation_service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/forgefed/federation_service.go b/modules/forgefed/federation_service.go index 68c8212882..4c89a43c64 100644 --- a/modules/forgefed/federation_service.go +++ b/modules/forgefed/federation_service.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" ) +// ToDo: May need to change the name to reflect workings of function better // LikeActivity receives a ForgeLike activity and does the following: // Validation of the activity // Creation of a (remote) federationHost if not existing From 42a41ce2bca1f3e82d1f288893458b3bf1195c5f Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 14:42:16 +0100 Subject: [PATCH 06/11] Remove todo --- routers/api/v1/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 31b6b7c9f4..6d56bb36ec 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -898,7 +898,7 @@ func Routes() *web.Route { }, context_service.UserIDAssignmentAPI()) m.Group("/repository-id/{repository-id}", func() { m.Get("", activitypub.Repository) - m.Post("/inbox", // ToDo: Post or Put? + m.Post("/inbox", // TODO: bind ativities here bind(forgefed.ForgeLike{}), // TODO: activitypub.ReqHTTPSignature(), From 84f2aab570e73f69f031757dc74030b9479c4d03 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 14:42:38 +0100 Subject: [PATCH 07/11] Add todo --- routers/web/repo/setting/setting.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 6df98fb47a..624ae89b9c 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -192,6 +192,7 @@ func SettingsPost(ctx *context.Context) { return } + // ToDo: Use Federated Repo Struct & Update Federated Repo Table switch { // Allow clearing the field case form.FederationRepos == "": From 2e0584bdf34cae80827d48b38ddb5b4e80de4104 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 15:37:37 +0100 Subject: [PATCH 08/11] Clearer error message "May" is also interchangeable with "could". "Should" fits better in this context. --- modules/validation/validatable.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/validation/validatable.go b/modules/validation/validatable.go index 24b1ce6682..405450aa1c 100644 --- a/modules/validation/validatable.go +++ b/modules/validation/validatable.go @@ -46,7 +46,7 @@ func ValidateNotEmpty(value any, fieldName string) []string { if isValid { return []string{} } - return []string{fmt.Sprintf("Field %v may not be empty", fieldName)} + return []string{fmt.Sprintf("Field %v should not be empty", fieldName)} } func ValidateMaxLen(value string, maxLen int, fieldName string) []string { From ed256ca540456a577ee40a76c8536e2acbae14f6 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 16:27:08 +0100 Subject: [PATCH 09/11] Implement NewForgeLike --- models/forgefed/activity.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/models/forgefed/activity.go b/models/forgefed/activity.go index 4016b71b38..b0d17b9a9c 100644 --- a/models/forgefed/activity.go +++ b/models/forgefed/activity.go @@ -6,6 +6,7 @@ package forgefed import ( "time" + "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/validation" ap "github.com/go-ap/activitypub" @@ -18,6 +19,21 @@ type ForgeLike struct { ap.Activity } +func NewForgeLike(ctx *context.APIContext) (ForgeLike, error) { + result := ForgeLike{} + actorIRI := ctx.Repo.Owner.APAPIURL() + objectIRI := ctx.Repo.Repository.APAPIURL() + result.Type = ap.LikeType + // ToDo: Would validating the source by Actor.Type field make sense? + result.Actor = ap.ActorNew(ap.IRI(actorIRI), "ForgejoUser") // Thats us, a User + result.Object = ap.ObjectNew(ap.ActivityVocabularyType(objectIRI)) // Thats them, a Repository + result.StartTime = time.Now() + if valid, err := validation.IsValid(result); !valid { + return ForgeLike{}, err + } + return result, nil +} + func (like ForgeLike) MarshalJSON() ([]byte, error) { return like.Activity.MarshalJSON() } From 0c6c43003cbf5a7156d34e501890b799aeccfec0 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 16:27:35 +0100 Subject: [PATCH 10/11] Implement getting APAPIURL for repo and user --- models/repo/repo.go | 5 +++++ models/user/user.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/models/repo/repo.go b/models/repo/repo.go index af3488b8f6..2cd3b51b6e 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -346,6 +346,11 @@ func (repo *Repository) APIURL() string { return setting.AppURL + "api/v1/repos/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) } +// APAPIURL returns the activitypub repository API URL +func (repo *Repository) APAPIURL() string { + return setting.AppURL + "api/v1/activitypub/repository-id/" + url.PathEscape(string(repo.ID)) +} + // GetCommitsCountCacheKey returns cache key used for commits count caching. func (repo *Repository) GetCommitsCountCacheKey(contextName string, isRef bool) string { var prefix string diff --git a/models/user/user.go b/models/user/user.go index e016ff5fbd..3e47bfccc8 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -301,6 +301,11 @@ func (u *User) HTMLURL() string { return setting.AppURL + url.PathEscape(u.Name) } +// APAPIURL returns the IRI to the api endpoint of the user +func (u *User) APAPIURL() string { + return setting.AppURL + url.PathEscape("api/v1/activitypub/user-id/") + url.PathEscape(string(u.ID)) +} + // OrganisationLink returns the organization sub page link. func (u *User) OrganisationLink() string { return setting.AppSubURL + "/org/" + url.PathEscape(u.Name) From 3e6eb255b357e25f040e640542e3f5e578c72c44 Mon Sep 17 00:00:00 2001 From: erik Date: Thu, 21 Mar 2024 16:29:13 +0100 Subject: [PATCH 11/11] WIP Initial, naive implementation of sending stars to fed repos Currently no rate limits are respected The mechanisms to use the Federated repo table need to be used --- routers/api/v1/user/star.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 2659789ddd..a07407ffc3 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -7,12 +7,16 @@ package user import ( std_context "context" "net/http" + "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/forgefed" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" @@ -160,6 +164,32 @@ func Star(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "StarRepo", err) return } + if setting.Federation.Enabled { + + likeActivity, err := forgefed.NewForgeLike(ctx) + if err != nil { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + return + } + + json, err := likeActivity.MarshalJSON() + if err != nil { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + return + } + + apclient, err := activitypub.NewClient(ctx, ctx.Doer, ctx.Doer.APAPIURL()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "StarRepo", err) + return + } + // ToDo: Change this to the standalone table of FederatedRepos + for _, target := range strings.Split(ctx.Repo.Repository.FederationRepos, ";") { + apclient.Post([]byte(json), target) + } + + // Send to list of federated repos + } ctx.Status(http.StatusNoContent) }