From 310d740cee06047cca2ccac50280819cef190640 Mon Sep 17 00:00:00 2001 From: bom Date: Fri, 29 Dec 2023 12:10:07 +0100 Subject: [PATCH] Start NodeInfo implementation --- models/forgefed/actor.go | 52 ++++++---------- models/forgefed/nodeinfo.go | 78 ++++++++++++++++++++++++ models/forgefed/nodeinfo_test.go | 67 ++++++++++++++++++++ models/forgefed/star.go | 14 ----- routers/api/v1/activitypub/repository.go | 4 ++ 5 files changed, 167 insertions(+), 48 deletions(-) create mode 100644 models/forgefed/nodeinfo.go create mode 100644 models/forgefed/nodeinfo_test.go diff --git a/models/forgefed/actor.go b/models/forgefed/actor.go index 1f54566d41..b633b61514 100644 --- a/models/forgefed/actor.go +++ b/models/forgefed/actor.go @@ -36,7 +36,7 @@ type RepositoryID struct { } // newActorID receives already validated inputs -func newActorID(validatedURI *url.URL, source string) (ActorID, error) { +func NewActorID(validatedURI *url.URL) (ActorID, error) { pathWithActorID := strings.Split(validatedURI.Path, "/") if containsEmptyString(pathWithActorID) { pathWithActorID = removeEmptyStrings(pathWithActorID) @@ -47,20 +47,30 @@ func newActorID(validatedURI *url.URL, source string) (ActorID, error) { result := ActorID{} result.ID = id - result.Source = source result.Schema = validatedURI.Scheme result.Host = validatedURI.Hostname() result.Path = pathWithoutActorID result.Port = validatedURI.Port() result.UnvalidatedInput = validatedURI.String() - if valid, err := IsValid(result); !valid { - return ActorId{}, err + if valid, outcome := validation.IsValid(result); !valid { + return ActorID{}, outcome } return result, nil } +func newActorID(validatedURI *url.URL, source string) (ActorID, error) { + result, err := NewActorID(validatedURI) + if err != nil { + return ActorID{}, err + } + + result.Source = source + + return result, nil +} + func NewPersonID(uri, source string) (PersonID, error) { // TODO: remove after test //if !validation.IsValidExternalURL(uri) { @@ -138,12 +148,10 @@ func (id PersonID) HostSuffix() string { func (id ActorID) Validate() []string { var result []string result = append(result, validation.ValidateNotEmpty(id.ID, "userId")...) - result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) result = append(result, validation.ValidateNotEmpty(id.Schema, "schema")...) result = append(result, validation.ValidateNotEmpty(id.Path, "path")...) result = append(result, validation.ValidateNotEmpty(id.Host, "host")...) result = append(result, validation.ValidateNotEmpty(id.UnvalidatedInput, "unvalidatedInput")...) - result = append(result, validation.ValidateOneOf(id.Source, []string{"forgejo", "gitea"})...) if id.UnvalidatedInput != id.AsURI() { result = append(result, fmt.Sprintf("not all input: %q was parsed: %q", id.UnvalidatedInput, id.AsURI())) @@ -154,6 +162,8 @@ func (id ActorID) Validate() []string { func (id PersonID) Validate() []string { result := id.ActorID.Validate() + result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) + result = append(result, validation.ValidateOneOf(id.Source, []string{"forgejo", "gitea"})...) switch id.Source { case "forgejo", "gitea": if strings.ToLower(id.Path) != "api/v1/activitypub/user-id" && strings.ToLower(id.Path) != "api/activitypub/user-id" { @@ -165,6 +175,8 @@ func (id PersonID) Validate() []string { func (id RepositoryID) Validate() []string { result := id.ActorID.Validate() + result = append(result, validation.ValidateNotEmpty(id.Source, "source")...) + result = append(result, validation.ValidateOneOf(id.Source, []string{"forgejo", "gitea"})...) switch id.Source { case "forgejo", "gitea": if strings.ToLower(id.Path) != "api/v1/activitypub/repository-id" && strings.ToLower(id.Path) != "api/activitypub/repository-id" { @@ -192,31 +204,3 @@ func removeEmptyStrings(ls []string) []string { } return rs } - -func IsValid[T Validateables](value T) (bool, error) { - if err := value.Validate(); len(err) > 0 { - errString := strings.Join(err, "\n") - return false, fmt.Errorf(errString) - } - return true, nil -} - -/* -func (a RepositoryId) IsValid() (bool, error) { - if err := a.Validate(); len(err) > 0 { - errString := strings.Join(err, "\n") - return false, fmt.Errorf(errString) - } - - return true, nil -} - -func (a PersonId) IsValid() (bool, error) { - if err := a.Validate(); len(err) > 0 { - errString := strings.Join(err, "\n") - return false, fmt.Errorf(errString) - } - - return true, nil -} -*/ diff --git a/models/forgefed/nodeinfo.go b/models/forgefed/nodeinfo.go new file mode 100644 index 0000000000..fb1e5d35da --- /dev/null +++ b/models/forgefed/nodeinfo.go @@ -0,0 +1,78 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "net/url" + + "code.gitea.io/gitea/modules/validation" + "github.com/valyala/fastjson" +) + +type ( + SourceType string +) + +type SourceTypes []SourceType + +const ( + ForgejoSourceType SourceType = "frogejo" +) + +var KnownSourceTypes = SourceTypes{ + ForgejoSourceType, +} + +// NodeInfo data type +// swagger:model +type NodeInfoWellKnown struct { + Href string +} + +func NodeInfoWellKnownUnmarshalJSON(data []byte) (NodeInfoWellKnown, error) { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return NodeInfoWellKnown{}, err + } + href := string(val.GetStringBytes("links", "0", "href")) + return NodeInfoWellKnown{Href: href}, nil +} + +func NewNodeInfoWellKnown(body []byte) (NodeInfoWellKnown, error) { + result, err := NodeInfoWellKnownUnmarshalJSON(body) + if err != nil { + return NodeInfoWellKnown{}, err + } + + if valid, outcome := validation.IsValid(result); !valid { + return NodeInfoWellKnown{}, outcome + } + + return NodeInfoWellKnown{}, nil +} + +// Validate collects error strings in a slice and returns this +func (node NodeInfoWellKnown) Validate() []string { + var result []string + result = append(result, validation.ValidateNotEmpty(node.Href, "Href")...) + + parsedUrl, err := url.Parse(node.Href) + if err != nil { + result = append(result, err.Error()) + return result + } + + if parsedUrl.Host == "" { + result = append(result, "Href has to be absolute") + } + + result = append(result, validation.ValidateOneOf(parsedUrl.Scheme, []string{"http", "https"})...) + + if parsedUrl.RawQuery != "" { + result = append(result, "Href may not contain query") + } + + return result +} diff --git a/models/forgefed/nodeinfo_test.go b/models/forgefed/nodeinfo_test.go new file mode 100644 index 0000000000..0df7b905b5 --- /dev/null +++ b/models/forgefed/nodeinfo_test.go @@ -0,0 +1,67 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "reflect" + "testing" + + "code.gitea.io/gitea/modules/validation" +) + +func Test_NodeInfoWellKnownUnmarshalJSON(t *testing.T) { + type testPair struct { + item []byte + want NodeInfoWellKnown + wantErr error + } + + tests := map[string]testPair{ + "with href": { + item: []byte(`{"links":[{"href":"https://federated-repo.prod.meissa.de/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`), + want: NodeInfoWellKnown{ + Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo", + }, + }, + "empty": { + item: []byte(``), + wantErr: fmt.Errorf("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""), + }, + // "with too long href": { + // item: []byte(`{"links":[{"href":"https://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfohttps://federated-repo.prod.meissa.de/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`), + // wantErr: fmt.Errorf("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""), + // }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := NodeInfoWellKnownUnmarshalJSON(tt.item) + if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() { + t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("UnmarshalJSON() got = %q, want %q", got, tt.want) + } + }) + } +} + +func Test_NodeInfoWellKnownValidate(t *testing.T) { + sut := NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo"} + if b, err := validation.IsValid(sut); !b { + t.Errorf("sut should be valid, %v, %v", sut, err) + } + + sut = NodeInfoWellKnown{Href: "./federated-repo.prod.meissa.de/api/v1/nodeinfo"} + if _, err := validation.IsValid(sut); err.Error() != "Href has to be absolute\nValue is not contained in allowed values [[http https]]" { + t.Errorf("validation error expected but was: %v\n", err) + } + + sut = NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo?alert=1"} + if _, err := validation.IsValid(sut); err.Error() != "Href may not contain query" { + t.Errorf("sut should be valid, %v, %v", sut, err) + } +} diff --git a/models/forgefed/star.go b/models/forgefed/star.go index 7e38b07c2c..21f44361ef 100644 --- a/models/forgefed/star.go +++ b/models/forgefed/star.go @@ -8,24 +8,10 @@ import ( "github.com/valyala/fastjson" ) -type ( - SourceType string -) - -type SourceTypes []SourceType - const ( StarType ap.ActivityVocabularyType = "Star" ) -const ( - ForgejoSourceType SourceType = "frogejo" -) - -var KnownSourceTypes = SourceTypes{ - ForgejoSourceType, -} - // Star activity data type // swagger:model type Star struct { diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go index 4828b60cc5..006f48d429 100644 --- a/routers/api/v1/activitypub/repository.go +++ b/routers/api/v1/activitypub/repository.go @@ -90,6 +90,10 @@ func RepositoryInbox(ctx *context.APIContext) { log.Info("RepositoryInbox: activity:%v", activity) // parse actorID (person) + // rawActorID, err := forgefed.NewActorID(activity.Actor.GetID().String()) + + // nodeInfo, err := createNodeInfo(rawActorID) + actorID, err := forgefed.NewPersonID(activity.Actor.GetID().String(), string(activity.Source)) if err != nil { ctx.ServerError("Validate actorId", err)