diff --git a/models/user/user.go b/models/user/user.go index 620ff9f702..040f471c15 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -9,6 +9,7 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/hex" + "errors" "fmt" "net/url" "os" @@ -1028,6 +1029,29 @@ func GetUserByName(ctx context.Context, name string) (*User, error) { return u, nil } +// GetUserByIRI returns user by given IRI. +func GetUserByIRI(ctx context.Context, iri string) (*User, error) { + if len(iri) == 0 { + return nil, ErrUserNotExist{0, iri, 0} + } + iriSplit := strings.Split(iri, "/") + if len(iriSplit) < 4 { + return nil, errors.New("not a Person actor IRI") + } + if iriSplit[2] == setting.Domain { + // Local user + return GetUserByName(ctx, iriSplit[len(iriSplit)-1]) + } + u := &User{LoginName: iri} + has, err := db.GetEngine(ctx).Get(u) + if err != nil { + return nil, err + } else if !has { + return nil, ErrUserNotExist{0, iri, 0} + } + return u, nil +} + // GetUserEmailsByNames returns a list of e-mails corresponds to names of users // that have their email notifications set to enabled or onmention. func GetUserEmailsByNames(ctx context.Context, names []string) []string { diff --git a/routers/api/v1/activitypub/authorize_interaction.go b/routers/api/v1/activitypub/authorize_interaction.go index 78adb7db18..b8f2bd49ec 100644 --- a/routers/api/v1/activitypub/authorize_interaction.go +++ b/routers/api/v1/activitypub/authorize_interaction.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/setting" @@ -51,12 +52,12 @@ func AuthorizeInteraction(ctx *context.Context) { ctx.ServerError("FederatedUserNew", err) return } - name, err := activitypub.PersonIRIToName(object.GetLink()) + user, err := user_model.GetUserByIRI(ctx, object.GetLink().String()) if err != nil { - ctx.ServerError("PersonIRIToName", err) + ctx.ServerError("GetUserByIRI", err) return } - ctx.Redirect(setting.AppURL + name) + ctx.Redirect(setting.AppURL + user.Name) case forgefed.RepositoryType: // Federated repository err = forgefed.OnRepository(object, func(r *forgefed.Repository) error { diff --git a/routers/api/v1/activitypub/create.go b/routers/api/v1/activitypub/create.go index 8157c3571b..82470a1a33 100644 --- a/routers/api/v1/activitypub/create.go +++ b/routers/api/v1/activitypub/create.go @@ -5,6 +5,7 @@ package activitypub import ( "context" + "encoding/json" "errors" "net/url" "strconv" @@ -26,20 +27,46 @@ import ( ap "github.com/go-ap/activitypub" ) + // Create a new federated user from a Person object func createPerson(ctx context.Context, person *ap.Person) error { - name, err := activitypub.PersonIRIToName(person.GetLink()) - if err != nil { + _, err := user_model.GetUserByIRI(ctx, person.GetLink().String()) + if user_model.IsErrUserNotExist(err) { + // User already exists return err } - exists, err := user_model.IsUserExist(ctx, 0, name) + personIRISplit := strings.Split(person.GetLink().String(), "/") + if len(personIRISplit) < 4 { + return errors.New("not a Person actor IRI") + } + + // Get instance by taking the domain of the IRI + instance := personIRISplit[2] + if instance == setting.Domain { + // Local user + return nil + } + + // Send a WebFinger request to get the username + uri, err := url.Parse("https://" + instance + "/.well-known/webfinger?resource=" + person.GetLink().String()) if err != nil { return err } - if exists { - return nil + resp, err := activitypub.Fetch(uri) + if err != nil { + return err } + var data activitypub.WebfingerJRD + err = json.Unmarshal(resp, &data) + if err != nil { + return err + } + subjectSplit := strings.Split(data.Subject, ":") + if subjectSplit[0] != "acct" { + return errors.New("subject is not an acct URI") + } + name := subjectSplit[1] var email string if person.Location != nil { @@ -55,7 +82,6 @@ func createPerson(ctx context.Context, person *ap.Person) error { user := &user_model.User{ Name: name, - FullName: person.Name.String(), // May not exist!! Email: email, LoginType: auth.Federated, LoginName: person.GetLink().String(), @@ -66,7 +92,11 @@ func createPerson(ctx context.Context, person *ap.Person) error { return err } + if person.Name != nil { + user.FullName = person.Name.String() + } if person.Icon != nil { + // Fetch and save user icon icon, err := ap.ToObject(person.Icon) if err != nil { return err @@ -75,12 +105,10 @@ func createPerson(ctx context.Context, person *ap.Person) error { if err != nil { return err } - body, err := activitypub.Fetch(iconURL) if err != nil { return err } - err = user_service.UploadAvatar(user, body) if err != nil { return err @@ -91,44 +119,13 @@ func createPerson(ctx context.Context, person *ap.Person) error { if err != nil { return err } + // Set public key return user_model.SetUserSetting(user.ID, user_model.UserActivityPubPubPem, person.PublicKey.PublicKeyPem) } -func createPersonFromIRI(ctx context.Context, personIRI ap.IRI) error { - ownerURL, err := url.Parse(personIRI.String()) - if err != nil { - return err - } - // Fetch person object - resp, err := activitypub.Fetch(ownerURL) - if err != nil { - return err - } - - // Parse person object - ap.ItemTyperFunc = forgefed.GetItemByType - ap.JSONItemUnmarshal = forgefed.JSONUnmarshalerFn - ap.NotEmptyChecker = forgefed.NotEmpty - object, err := ap.UnmarshalJSON(resp) - if err != nil { - return err - } - - // Create federated user - person, err := ap.ToActor(object) - if err != nil { - return err - } - return createPerson(ctx, person) -} - // Create a new federated repo from a Repository object func createRepository(ctx context.Context, repository *forgefed.Repository) error { - err := createPersonFromIRI(ctx, repository.AttributedTo.GetLink()) - if err != nil { - return err - } - user, err := activitypub.PersonIRIToUser(ctx, repository.AttributedTo.GetLink()) + user, err := user_model.GetUserByIRI(ctx, repository.AttributedTo.GetLink().String()) if err != nil { return err } @@ -200,7 +197,7 @@ func createIssue(ctx context.Context, ticket *forgefed.Ticket) error { } // Construct issue - user, err := activitypub.PersonIRIToUser(ctx, ap.IRI(ticket.AttributedTo.GetLink().String())) + user, err := user_model.GetUserByIRI(ctx, ticket.AttributedTo.GetLink().String()) if err != nil { return err } @@ -233,7 +230,7 @@ func createPullRequest(ctx context.Context, ticket *forgefed.Ticket) error { return err } - user, err := activitypub.PersonIRIToUser(ctx, ticket.AttributedTo.GetLink()) + user, err := user_model.GetUserByIRI(ctx, ticket.AttributedTo.GetLink().String()) if err != nil { return err } @@ -285,12 +282,7 @@ func createPullRequest(ctx context.Context, ticket *forgefed.Ticket) error { // Create a comment func createComment(ctx context.Context, note *ap.Note) error { - err := createPersonFromIRI(ctx, note.AttributedTo.GetLink()) - if err != nil { - return err - } - - user, err := activitypub.PersonIRIToUser(ctx, note.AttributedTo.GetLink()) + user, err := user_model.GetUserByIRI(ctx, note.AttributedTo.GetLink().String()) if err != nil { return err } diff --git a/routers/api/v1/activitypub/delete.go b/routers/api/v1/activitypub/delete.go index 5aa5c83268..48c63d1b2c 100644 --- a/routers/api/v1/activitypub/delete.go +++ b/routers/api/v1/activitypub/delete.go @@ -6,8 +6,8 @@ package activitypub import ( "context" + user_model "code.gitea.io/gitea/models/user" user_service "code.gitea.io/gitea/services/user" - "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" ) @@ -20,9 +20,9 @@ func delete(ctx context.Context, delete ap.Delete) error { if actorIRI != objectIRI { return nil } - + // Object is the user getting deleted - objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI) + objectUser, err := user_model.GetUserByIRI(ctx, objectIRI.String()) if err != nil { return err } diff --git a/routers/api/v1/activitypub/follow.go b/routers/api/v1/activitypub/follow.go index c950588412..68afc49bce 100644 --- a/routers/api/v1/activitypub/follow.go +++ b/routers/api/v1/activitypub/follow.go @@ -18,14 +18,14 @@ import ( func follow(ctx context.Context, follow ap.Follow) error { // Actor is the user performing the follow actorIRI := follow.Actor.GetLink() - actorUser, err := activitypub.PersonIRIToUser(ctx, actorIRI) + actorUser, err := user_model.GetUserByIRI(ctx, actorIRI.String()) if err != nil { return err } // Object is the user being followed objectIRI := follow.Object.GetLink() - objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI) + objectUser, err := user_model.GetUserByIRI(ctx, objectIRI.String()) // Must be a local user if err != nil || strings.Contains(objectUser.Name, "@") { return err @@ -54,14 +54,14 @@ func unfollow(ctx context.Context, unfollow ap.Undo) error { // Actor is the user performing the undo follow actorIRI := follow.Actor.GetLink() - actorUser, err := activitypub.PersonIRIToUser(ctx, actorIRI) + actorUser, err := user_model.GetUserByIRI(ctx, actorIRI.String()) if err != nil { return err } // Object is the user being unfollowed objectIRI := follow.Object.GetLink() - objectUser, err := activitypub.PersonIRIToUser(ctx, objectIRI) + objectUser, err := user_model.GetUserByIRI(ctx, objectIRI.String()) // Must be a local user if err != nil || strings.Contains(objectUser.Name, "@") { return err diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go index 5e28af7922..138d8a88eb 100644 --- a/routers/api/v1/activitypub/reqsignature.go +++ b/routers/api/v1/activitypub/reqsignature.go @@ -78,8 +78,6 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er return } // 4. Create a federated user for the actor - // TODO: This is a very bad place for creating federated users - // We end up creating way more users than necessary! var person ap.Person err = person.UnmarshalJSON(b) if err != nil { diff --git a/routers/api/v1/activitypub/star.go b/routers/api/v1/activitypub/star.go index ce99ec5a2f..a6d58c41ea 100644 --- a/routers/api/v1/activitypub/star.go +++ b/routers/api/v1/activitypub/star.go @@ -9,6 +9,7 @@ import ( "strings" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/services/activitypub" ap "github.com/go-ap/activitypub" @@ -16,7 +17,7 @@ import ( // Process a Like activity to star a repository func star(ctx context.Context, like ap.Like) (err error) { - user, err := activitypub.PersonIRIToUser(ctx, like.Actor.GetLink()) + user, err := user_model.GetUserByIRI(ctx, like.Actor.GetLink().String()) if err != nil { return } @@ -33,7 +34,7 @@ func unstar(ctx context.Context, unlike ap.Undo) (err error) { if !ok { return errors.New("could not cast object to like") } - user, err := activitypub.PersonIRIToUser(ctx, like.Actor.GetLink()) + user, err := user_model.GetUserByIRI(ctx, like.Actor.GetLink().String()) if err != nil { return } diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index 656982c9ac..13b93edaed 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -13,26 +13,11 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/activitypub" ) // https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4 -type webfingerJRD struct { - Subject string `json:"subject,omitempty"` - Aliases []string `json:"aliases,omitempty"` - Properties map[string]interface{} `json:"properties,omitempty"` - Links []*webfingerLink `json:"links,omitempty"` -} - -type webfingerLink struct { - Rel string `json:"rel,omitempty"` - Type string `json:"type,omitempty"` - Href string `json:"href,omitempty"` - Template string `json:"template,omitempty"` - Titles map[string]string `json:"titles,omitempty"` - Properties map[string]interface{} `json:"properties,omitempty"` -} - // WebfingerQuery returns information about a resource // https://datatracker.ietf.org/doc/html/rfc7565 func WebfingerQuery(ctx *context.Context) { @@ -65,6 +50,8 @@ func WebfingerQuery(ctx *context.Context) { if u != nil && u.KeepEmailPrivate { err = user_model.ErrUserNotExist{} } + case "https": + u, err = user_model.GetUserByIRI(ctx, ctx.FormString("resource")) default: ctx.Error(http.StatusBadRequest) return @@ -92,7 +79,7 @@ func WebfingerQuery(ctx *context.Context) { aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email)) } - links := []*webfingerLink{ + links := []*activitypub.WebfingerLink{ { Rel: "http://webfinger.net/rel/profile-page", Type: "text/html", @@ -114,7 +101,7 @@ func WebfingerQuery(ctx *context.Context) { } ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*") - ctx.JSON(http.StatusOK, &webfingerJRD{ + ctx.JSON(http.StatusOK, &activitypub.WebfingerJRD{ Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host), Aliases: aliases, Links: links, diff --git a/services/activitypub/iri.go b/services/activitypub/iri.go index 5221bd4124..468b13fb39 100644 --- a/services/activitypub/iri.go +++ b/services/activitypub/iri.go @@ -9,40 +9,11 @@ import ( "strings" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" ) -// Returns the username corresponding to a Person actor IRI -func PersonIRIToName(personIRI ap.IRI) (string, error) { - personIRISplit := strings.Split(personIRI.String(), "/") - if len(personIRISplit) < 4 { - return "", errors.New("not a Person actor IRI") - } - - instance := personIRISplit[2] - name := personIRISplit[len(personIRISplit)-1] - if instance == setting.Domain { - // Local user - return name, nil - } - // Remote user - // Get name in username@instance.com format - return name + "@" + instance, nil -} - -// Returns the user corresponding to a Person actor IRI -func PersonIRIToUser(ctx context.Context, personIRI ap.IRI) (*user_model.User, error) { - name, err := PersonIRIToName(personIRI) - if err != nil { - return nil, err - } - - return user_model.GetUserByName(ctx, name) -} - // Returns the owner and name corresponding to a Repository actor IRI func RepositoryIRIToName(repoIRI ap.IRI) (string, string, error) { repoIRISplit := strings.Split(repoIRI.String(), "/") diff --git a/services/activitypub/webfinger.go b/services/activitypub/webfinger.go new file mode 100644 index 0000000000..9f690daddc --- /dev/null +++ b/services/activitypub/webfinger.go @@ -0,0 +1,17 @@ +package activitypub; + +type WebfingerJRD struct { + Subject string `json:"subject,omitempty"` + Aliases []string `json:"aliases,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` + Links []*WebfingerLink `json:"links,omitempty"` +} + +type WebfingerLink struct { + Rel string `json:"rel,omitempty"` + Type string `json:"type,omitempty"` + Href string `json:"href,omitempty"` + Template string `json:"template,omitempty"` + Titles map[string]string `json:"titles,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +}