diff --git a/models/activitypub/actor.go b/models/activitypub/actor.go index 52c6a6494a..3af101b440 100644 --- a/models/activitypub/actor.go +++ b/models/activitypub/actor.go @@ -1,23 +1,16 @@ +// Copyright 2023 The forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package activitypub import ( "fmt" "net/url" - "strconv" "strings" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/validation" ) -type Validatable interface { // ToDo: What is the right package for this interface? - validate_is_not_nil() error - validate_is_not_empty() error - Validate() error - IsValid() (bool, error) - PanicIfInvalid() -} - type PersonId struct { userId string source string @@ -28,142 +21,6 @@ type PersonId struct { unvalidatedInput string } -func validate_is_not_empty(str string) error { - if str == "" { - return fmt.Errorf("the given string was empty") - } - return nil -} - -/* -Validate collects error strings in a slice and returns this -*/ -func (value PersonId) Validate() []string { - var result = []string{} - result = append(result, validation.ValidateNotEmpty(value.userId, "userId")...) - result = append(result, validation.ValidateNotEmpty(value.source, "source")...) - result = append(result, validation.ValidateNotEmpty(value.schema, "schema")...) - result = append(result, validation.ValidateNotEmpty(value.path, "path")...) - result = append(result, validation.ValidateNotEmpty(value.host, "host")...) - result = append(result, validation.ValidateNotEmpty(value.unvalidatedInput, "unvalidatedInput")...) - - result = append(result, validation.ValidateOneOf(value.source, []string{"forgejo", "gitea"})...) - switch value.source { - case "forgejo", "gitea": - if !strings.Contains(value.path, "api/v1/activitypub/user-id") { - result = append(result, fmt.Sprintf("path has to be a api path")) - } - } - - return result -} - -/* -IsValid concatenates the error messages with newlines and returns them if there are any -*/ -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 -} - -func (a PersonId) PanicIfInvalid() { - if valid, err := a.IsValid(); !valid { - panic(err) - } -} - -func (a PersonId) GetUserId() int { - result, err := strconv.Atoi(a.userId) - - if err != nil { - panic(err) - } - - return result -} - -func (a PersonId) GetNormalizedUri() string { - result := fmt.Sprintf("%s://%s:%s/%s/%s", a.schema, a.host, a.port, a.path, a.userId) - return result -} - -// Returns the combination of host:port if port exists, host otherwise -func (a PersonId) GetHostAndPort() string { - - if a.port != "" { - return strings.Join([]string{a.host, a.port}, ":") - } - - return a.host -} - -func containsEmptyString(ar []string) bool { - for _, elem := range ar { - if elem == "" { - return true - } - } - return false -} - -func removeEmptyStrings(ls []string) []string { - var rs []string - for _, str := range ls { - if str != "" { - rs = append(rs, str) - } - } - return rs -} - -func ValidateAndParseIRI(unvalidatedIRI string) (url.URL, error) { // ToDo: Validate that it is not the same host as ours. - err := validate_is_not_empty(unvalidatedIRI) // url.Parse seems to accept empty strings? - if err != nil { - return url.URL{}, err - } - - validatedURL, err := url.Parse(unvalidatedIRI) - if err != nil { - return url.URL{}, err - } - - if len(validatedURL.Path) <= 1 { - return url.URL{}, fmt.Errorf("path was empty") - } - - return *validatedURL, nil -} - -// TODO: This parsing is very Person-Specific. We should adjust the name & move to a better location (maybe forgefed package?) -func ParseActorID(validatedURL url.URL, source string) PersonId { // ToDo: Turn this into a factory function and do not split parsing and validation rigurously - - pathWithUserID := strings.Split(validatedURL.Path, "/") - - if containsEmptyString(pathWithUserID) { - pathWithUserID = removeEmptyStrings(pathWithUserID) - } - - length := len(pathWithUserID) - pathWithoutUserID := strings.Join(pathWithUserID[0:length-1], "/") - userId := pathWithUserID[length-1] - - log.Info("Actor: pathWithUserID: %s", pathWithUserID) - log.Info("Actor: pathWithoutUserID: %s", pathWithoutUserID) - log.Info("Actor: UserID: %s", userId) - - return PersonId{ // ToDo: maybe keep original input to validate against (maybe extra method) - userId: userId, - source: source, - schema: validatedURL.Scheme, - host: validatedURL.Hostname(), // u.Host returns hostname:port - path: pathWithoutUserID, - port: validatedURL.Port(), - } -} - func NewPersonId(uri string, source string) (PersonId, error) { if !validation.IsValidExternalURL(uri) { return PersonId{}, fmt.Errorf("uri %s is not a valid external url", uri) @@ -195,3 +52,80 @@ func NewPersonId(uri string, source string) (PersonId, error) { return actorId, nil } + +func (id PersonId) AsUri() string { + result := "" + if id.port == "" { + result = fmt.Sprintf("%s://%s/%s/%s", id.schema, id.host, id.path, id.userId) + } else { + result = fmt.Sprintf("%s://%s:%s/%s/%s", id.schema, id.host, id.port, id.path, id.userId) + } + return result +} + +func (id PersonId) AsWebfinger() string { + result := fmt.Sprintf("@%s@%s", strings.ToLower(id.userId), strings.ToLower(id.host)) + return result +} + +/* +Validate collects error strings in a slice and returns this +*/ +func (value PersonId) Validate() []string { + var result = []string{} + result = append(result, validation.ValidateNotEmpty(value.userId, "userId")...) + result = append(result, validation.ValidateNotEmpty(value.source, "source")...) + result = append(result, validation.ValidateNotEmpty(value.schema, "schema")...) + result = append(result, validation.ValidateNotEmpty(value.path, "path")...) + result = append(result, validation.ValidateNotEmpty(value.host, "host")...) + result = append(result, validation.ValidateNotEmpty(value.unvalidatedInput, "unvalidatedInput")...) + + result = append(result, validation.ValidateOneOf(value.source, []string{"forgejo", "gitea"})...) + switch value.source { + case "forgejo", "gitea": + if !strings.Contains(value.path, "api/v1/activitypub/user-id") { + result = append(result, fmt.Sprintf("path has to be a api path")) + } + } + if value.unvalidatedInput != value.AsUri() { + result = append(result, fmt.Sprintf("not all input: %q was parsed: %q", value.unvalidatedInput, value.AsUri())) + } + + return result +} + +/* +IsValid concatenates the error messages with newlines and returns them if there are any +*/ +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 +} + +func (a PersonId) PanicIfInvalid() { + if valid, err := a.IsValid(); !valid { + panic(err) + } +} + +func containsEmptyString(ar []string) bool { + for _, elem := range ar { + if elem == "" { + return true + } + } + return false +} + +func removeEmptyStrings(ls []string) []string { + var rs []string + for _, str := range ls { + if str != "" { + rs = append(rs, str) + } + } + return rs +} diff --git a/models/activitypub/actor_test.go b/models/activitypub/actor_test.go index 6959d8772a..2e8663a7ca 100644 --- a/models/activitypub/actor_test.go +++ b/models/activitypub/actor_test.go @@ -5,119 +5,50 @@ package activitypub import ( "testing" - - "code.gitea.io/gitea/modules/forgefed" - ap "github.com/go-ap/activitypub" ) -var emptyMockStar *forgefed.Star = &forgefed.Star{ - Source: "", - Activity: ap.Activity{ - Actor: ap.IRI(""), - Type: "Star", - Object: ap.IRI(""), - }, -} +func TestNewPersonId(t *testing.T) { + expected := PersonId{ + userId: "1", + source: "forgejo", + schema: "https", + path: "api/v1/activitypub/user-id", + host: "an.other.host", + port: "", + unvalidatedInput: "https://an.other.host/api/v1/activitypub/user-id/1", + } + sut, _ := NewPersonId("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") + if sut != expected { + t.Errorf("expected: %v\n but was: %v\n", expected, sut) + } -var invalidMockStar *forgefed.Star = &forgefed.Star{ - Source: "", - Activity: ap.Activity{ - Actor: ap.IRI(""), - Type: "Star", - Object: ap.IRI("https://example.com/"), - }, -} - -var mockStar *forgefed.Star = &forgefed.Star{ - Source: "forgejo", - Activity: ap.Activity{ - Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"), - Type: "Star", - Object: ap.IRI("https://codeberg.org/api/v1/activitypub/repository-id/1"), - }, -} - -func TestValidateAndParseIRIEmpty(t *testing.T) { - item := emptyMockStar.Object.GetLink().String() - - _, err := ValidateAndParseIRI(item) - - if err == nil { - t.Errorf("ValidateAndParseIRI returned no error for empty input.") + expected = PersonId{ + userId: "1", + source: "forgejo", + schema: "https", + path: "api/v1/activitypub/user-id", + host: "an.other.host", + port: "443", + unvalidatedInput: "https://an.other.host:443/api/v1/activitypub/user-id/1", + } + sut, _ = NewPersonId("https://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo") + if sut != expected { + t.Errorf("expected: %v\n but was: %v\n", expected, sut) } } -func TestValidateAndParseIRINoPath(t *testing.T) { - item := emptyMockStar.Object.GetLink().String() - - _, err := ValidateAndParseIRI(item) - - if err == nil { - t.Errorf("ValidateAndParseIRI returned no error for empty path.") - } -} - -func TestActorParserValid(t *testing.T) { - item, _ := ValidateAndParseIRI(mockStar.Actor.GetID().String()) - want := PersonId{ - userId: "1", - source: "forgejo", - schema: "https", - path: "api/v1/activitypub/user-id", - host: "repo.prod.meissa.de", - port: "", +func TestWebfingerId(t *testing.T) { + sut, _ := NewPersonId("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") + if sut.AsWebfinger() != "@12345@codeberg.org" { + t.Errorf("wrong webfinger: %v", sut.AsWebfinger()) } - got := ParseActorID(item, "forgejo") - - if got != want { - t.Errorf("\nParseActorID did not return want: %v\n but %v", want, got) + sut, _ = NewPersonId("https://Codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") + if sut.AsWebfinger() != "@12345@codeberg.org" { + t.Errorf("wrong webfinger: %v", sut.AsWebfinger()) } } -func TestValidateValid(t *testing.T) { - item := PersonId{ - userId: "1", - source: "forgejo", - schema: "https", - path: "/api/v1/activitypub/user-id/1", - host: "repo.prod.meissa.de", - port: "", - } - - if valid, _ := item.IsValid(); !valid { - t.Errorf("Actor was invalid with valid input.") - } -} - -func TestValidateInvalid(t *testing.T) { - item, _ := ValidateAndParseIRI("https://example.org/some-path/to/nowhere/") - - actor := ParseActorID(item, "forgejo") - - if valid, _ := actor.IsValid(); valid { - t.Errorf("Actor was valid with invalid input.") - } -} - -func TestGetHostAndPort(t *testing.T) { - item := PersonId{ - schema: "https", - userId: "1", - path: "/api/v1/activitypub/user-id/1", - host: "repo.prod.meissa.de", - port: "80", - } - want := "repo.prod.meissa.de:80" - - hostAndPort := item.GetHostAndPort() - - if hostAndPort != want { - t.Errorf("GetHostAndPort did not return correct host and port combination: %v", hostAndPort) - } - -} - func TestShouldThrowErrorOnInvalidInput(t *testing.T) { _, err := NewPersonId("", "forgejo") if err == nil { @@ -144,6 +75,10 @@ func TestShouldThrowErrorOnInvalidInput(t *testing.T) { if err == nil { t.Errorf("uri may not contain relative path elements") } + _, err = NewPersonId("https://myuser@an.other.host/api/v1/activitypub/user-id/1", "forgejo") + if err == nil { + t.Errorf("uri may not contain unparsed elements") + } _, err = NewPersonId("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo") if err != nil { diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go index ca1b5386e4..e6ddb62812 100644 --- a/routers/api/v1/activitypub/repository.go +++ b/routers/api/v1/activitypub/repository.go @@ -238,26 +238,15 @@ func RepositoryInbox(ctx *context.APIContext) { activity := web.GetForm(ctx).(*forgefed.Star) log.Info("RepositoryInbox: Activity.Source: %v, Activity.Actor %v, Activity.Actor.Id %v", activity.Source, activity.Actor, activity.Actor.GetID().String()) - // assume actor is: "actor": "https://codeberg.org/api/v1/activitypub/user-id/12345" - NB: This might be actually the ID? Maybe check vocabulary. - // "https://Codeberg.org/api/v1/activitypub/user-id/12345" - // "https://codeberg.org:443/api/v1/activitypub/user-id/12345" - // "https://codeberg.org/api/v1/activitypub/../activitypub/user-id/12345" - // "https://user:password@codeberg.org/api/v1/activitypub/user-id/12345" - // "https://codeberg.org/api/v1/activitypub//user-id/12345" - - // parse senderActorId - // senderActorId holds the data to construct the sender of the star + // parse actorId actorId, err := activitypub.NewPersonId(activity.Actor.GetID().String(), string(activity.Source)) if err != nil { ctx.ServerError("Validate actorId", err) return } - - // Is the PersonId Struct valid? - actorId.PanicIfInvalid() log.Info("RepositoryInbox: Actor parsed. %v", actorId) - remoteStargazer := actorId.GetNormalizedUri() // used as LoginName in newly created user + remoteStargazer := actorId.AsWebfinger() // used as LoginName in newly created user log.Info("remotStargazer: %v", remoteStargazer) // Check if user already exists