remove unused & implement webfinger

This commit is contained in:
Michael Jerger 2023-12-08 20:37:26 +01:00
parent 73a38ea0d1
commit b5a467e94d
3 changed files with 119 additions and 261 deletions

View file

@ -1,23 +1,16 @@
// Copyright 2023 The forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub package activitypub
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/validation" "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 { type PersonId struct {
userId string userId string
source string source string
@ -28,142 +21,6 @@ type PersonId struct {
unvalidatedInput string 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) { func NewPersonId(uri string, source string) (PersonId, error) {
if !validation.IsValidExternalURL(uri) { if !validation.IsValidExternalURL(uri) {
return PersonId{}, fmt.Errorf("uri %s is not a valid external url", 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 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
}

View file

@ -5,119 +5,50 @@ package activitypub
import ( import (
"testing" "testing"
"code.gitea.io/gitea/modules/forgefed"
ap "github.com/go-ap/activitypub"
) )
var emptyMockStar *forgefed.Star = &forgefed.Star{ func TestNewPersonId(t *testing.T) {
Source: "", expected := PersonId{
Activity: ap.Activity{ userId: "1",
Actor: ap.IRI(""), source: "forgejo",
Type: "Star", schema: "https",
Object: ap.IRI(""), 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{ expected = PersonId{
Source: "", userId: "1",
Activity: ap.Activity{ source: "forgejo",
Actor: ap.IRI(""), schema: "https",
Type: "Star", path: "api/v1/activitypub/user-id",
Object: ap.IRI("https://example.com/"), host: "an.other.host",
}, port: "443",
} unvalidatedInput: "https://an.other.host:443/api/v1/activitypub/user-id/1",
}
var mockStar *forgefed.Star = &forgefed.Star{ sut, _ = NewPersonId("https://an.other.host:443/api/v1/activitypub/user-id/1", "forgejo")
Source: "forgejo", if sut != expected {
Activity: ap.Activity{ t.Errorf("expected: %v\n but was: %v\n", expected, sut)
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.")
} }
} }
func TestValidateAndParseIRINoPath(t *testing.T) { func TestWebfingerId(t *testing.T) {
item := emptyMockStar.Object.GetLink().String() sut, _ := NewPersonId("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo")
if sut.AsWebfinger() != "@12345@codeberg.org" {
_, err := ValidateAndParseIRI(item) t.Errorf("wrong webfinger: %v", sut.AsWebfinger())
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: "",
} }
got := ParseActorID(item, "forgejo") sut, _ = NewPersonId("https://Codeberg.org/api/v1/activitypub/user-id/12345", "forgejo")
if sut.AsWebfinger() != "@12345@codeberg.org" {
if got != want { t.Errorf("wrong webfinger: %v", sut.AsWebfinger())
t.Errorf("\nParseActorID did not return want: %v\n but %v", want, got)
} }
} }
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) { func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
_, err := NewPersonId("", "forgejo") _, err := NewPersonId("", "forgejo")
if err == nil { if err == nil {
@ -144,6 +75,10 @@ func TestShouldThrowErrorOnInvalidInput(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("uri may not contain relative path elements") 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") _, err = NewPersonId("https://an.other.host/api/v1/activitypub/user-id/1", "forgejo")
if err != nil { if err != nil {

View file

@ -238,26 +238,15 @@ func RepositoryInbox(ctx *context.APIContext) {
activity := web.GetForm(ctx).(*forgefed.Star) 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()) 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. // parse actorId
// "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
actorId, err := activitypub.NewPersonId(activity.Actor.GetID().String(), string(activity.Source)) actorId, err := activitypub.NewPersonId(activity.Actor.GetID().String(), string(activity.Source))
if err != nil { if err != nil {
ctx.ServerError("Validate actorId", err) ctx.ServerError("Validate actorId", err)
return return
} }
// Is the PersonId Struct valid?
actorId.PanicIfInvalid()
log.Info("RepositoryInbox: Actor parsed. %v", actorId) 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) log.Info("remotStargazer: %v", remoteStargazer)
// Check if user already exists // Check if user already exists