From 2e2e4c7ee42ad1a016b47730b63fbb3c2172f9ad Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 20 Apr 2022 21:03:00 +0100 Subject: [PATCH 1/8] Implemented Webfinger endpoint. --- routers/web/web.go | 1 + routers/web/webfinger.go | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 routers/web/webfinger.go diff --git a/routers/web/web.go b/routers/web/web.go index 0de6f1372275c..59949579a5c75 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -288,6 +288,7 @@ func RegisterRoutes(m *web.Route) { m.Get("/openid-configuration", auth.OIDCWellKnown) if setting.Federation.Enabled { m.Get("/nodeinfo", NodeInfoLinks) + m.Get("/webfinger", WebfingerQuery) } m.Get("/change-password", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/user/settings/account", http.StatusTemporaryRedirect) diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go new file mode 100644 index 0000000000000..22008765fabee --- /dev/null +++ b/routers/web/webfinger.go @@ -0,0 +1,112 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package web + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +var webfingerRessourcePattern = regexp.MustCompile(`(?i)\A([a-z^:]+):(.*)\z`) + +// 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"` + Titles map[string]string `json:"titles,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +} + +// WebfingerQuery returns informations about a resource +// https://datatracker.ietf.org/doc/html/rfc7565 +func WebfingerQuery(ctx *context.Context) { + resource := ctx.FormTrim("resource") + + scheme := "acct" + uri := resource + + match := webfingerRessourcePattern.FindStringSubmatch(resource) + if match != nil { + scheme = match[1] + uri = match[2] + } + + appURL, _ := url.Parse(setting.AppURL) + + var u *user_model.User + var err error + + switch scheme { + case "acct": + // allow only the current host + parts := strings.SplitN(uri, "@", 2) + if len(parts) != 2 { + ctx.Error(http.StatusBadRequest) + return + } + if parts[1] != appURL.Host { + ctx.Error(http.StatusBadRequest) + return + } + + u, err = user_model.GetUserByNameCtx(ctx, parts[0]) + case "mailto": + u, err = user_model.GetUserByEmailContext(ctx, uri) + default: + ctx.Error(http.StatusBadRequest) + return + } + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Error(http.StatusNotFound) + } else { + log.Error("Error getting user: %v", err) + ctx.Error(http.StatusInternalServerError) + } + return + } + + // Should we check IsUserVisibleToViewer here? + + aliases := make([]string, 0, 1) + if !u.KeepEmailPrivate { + aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email)) + } + + links := []*webfingerLink{ + { + Rel: "http://webfinger.net/rel/profile-page", + Type: "text/html", + Href: u.HTMLURL(), + }, + { + Rel: "http://webfinger.net/rel/avatar", + Href: u.AvatarLink(), + }, + } + + ctx.JSON(http.StatusOK, &webfingerJRD{ + Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host), + Aliases: aliases, + Links: links, + }) +} From eca171190128a425b6162c50e132895be8d520b7 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 22 Apr 2022 13:55:58 +0000 Subject: [PATCH 2/8] Add visible check. --- routers/web/webfinger.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index 22008765fabee..c6f915a6e46a1 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -85,7 +85,10 @@ func WebfingerQuery(ctx *context.Context) { return } - // Should we check IsUserVisibleToViewer here? + if !user_model.IsUserVisibleToViewer(u, ctx.Doer) { + ctx.Error(http.StatusNotFound) + return + } aliases := make([]string, 0, 1) if !u.KeepEmailPrivate { From b28ee5df1605774ce371976a8cde0853546a11fe Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 22 Apr 2022 13:56:17 +0000 Subject: [PATCH 3/8] Add user profile as alias. --- routers/web/webfinger.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index c6f915a6e46a1..3e8f4078490ea 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -90,7 +90,9 @@ func WebfingerQuery(ctx *context.Context) { return } - aliases := make([]string, 0, 1) + aliases := []string{ + u.HTMLURL(), + } if !u.KeepEmailPrivate { aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email)) } From 8a3403dd600f0401a10af98b274b5ab4ec7bb48e Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 23 Apr 2022 10:04:24 +0000 Subject: [PATCH 4/8] Fail if KeepEmailPrivate. --- routers/web/webfinger.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index 3e8f4078490ea..fb73d8464f3a4 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -71,6 +71,9 @@ func WebfingerQuery(ctx *context.Context) { u, err = user_model.GetUserByNameCtx(ctx, parts[0]) case "mailto": u, err = user_model.GetUserByEmailContext(ctx, uri) + if u != nil && u.KeepEmailPrivate { + err = user_model.ErrUserNotExist{} + } default: ctx.Error(http.StatusBadRequest) return From 9a5d701d827747b4f423be2b95f731626278ca39 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 25 Apr 2022 14:47:10 +0000 Subject: [PATCH 5/8] Use url.Parse instead of regexp. --- routers/web/webfinger.go | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index fb73d8464f3a4..6bbddb4c0e4b6 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "regexp" "strings" user_model "code.gitea.io/gitea/models/user" @@ -17,8 +16,6 @@ import ( "code.gitea.io/gitea/modules/setting" ) -var webfingerRessourcePattern = regexp.MustCompile(`(?i)\A([a-z^:]+):(.*)\z`) - // https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4 type webfingerJRD struct { @@ -39,26 +36,20 @@ type webfingerLink struct { // WebfingerQuery returns informations about a resource // https://datatracker.ietf.org/doc/html/rfc7565 func WebfingerQuery(ctx *context.Context) { - resource := ctx.FormTrim("resource") - - scheme := "acct" - uri := resource + appURL, _ := url.Parse(setting.AppURL) - match := webfingerRessourcePattern.FindStringSubmatch(resource) - if match != nil { - scheme = match[1] - uri = match[2] + resource, err := url.Parse(ctx.FormTrim("resource")) + if err != nil { + ctx.Error(http.StatusBadRequest) + return } - appURL, _ := url.Parse(setting.AppURL) - var u *user_model.User - var err error - switch scheme { + switch resource.Scheme { case "acct": // allow only the current host - parts := strings.SplitN(uri, "@", 2) + parts := strings.SplitN(resource.Opaque, "@", 2) if len(parts) != 2 { ctx.Error(http.StatusBadRequest) return @@ -70,7 +61,7 @@ func WebfingerQuery(ctx *context.Context) { u, err = user_model.GetUserByNameCtx(ctx, parts[0]) case "mailto": - u, err = user_model.GetUserByEmailContext(ctx, uri) + u, err = user_model.GetUserByEmailContext(ctx, resource.Opaque) if u != nil && u.KeepEmailPrivate { err = user_model.ErrUserNotExist{} } From e8429b87d996ad128e84f9fee0ac5e793d120646 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 25 Apr 2022 14:47:16 +0000 Subject: [PATCH 6/8] Added tests. --- integrations/webfinger_test.go | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 integrations/webfinger_test.go diff --git a/integrations/webfinger_test.go b/integrations/webfinger_test.go new file mode 100644 index 0000000000000..5195e9a20ab93 --- /dev/null +++ b/integrations/webfinger_test.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestWebfinger(t *testing.T) { + defer prepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) + + appURL, _ := url.Parse(setting.AppURL) + + type webfingerLink struct { + Rel string `json:"rel,omitempty"` + Type string `json:"type,omitempty"` + Href string `json:"href,omitempty"` + Titles map[string]string `json:"titles,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` + } + + 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"` + } + + session := loginUser(t, "user1") + + req := NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, appURL.Host)) + resp := MakeRequest(t, req, http.StatusOK) + + var jrd webfingerJRD + DecodeJSON(t, resp, &jrd) + assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject) + assert.ElementsMatch(t, []string{user.HTMLURL()}, jrd.Aliases) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, "unknown.host")) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", "user31", appURL.Host)) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", "user31", appURL.Host)) + session.MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=mailto:%s", user.Email)) + MakeRequest(t, req, http.StatusNotFound) +} From bee83c9050f5dcef723aa9e889b812bfe5e29f33 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 25 Apr 2022 20:39:43 +0000 Subject: [PATCH 7/8] Set test settings. --- integrations/webfinger_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/integrations/webfinger_test.go b/integrations/webfinger_test.go index 5195e9a20ab93..b2efc334467b7 100644 --- a/integrations/webfinger_test.go +++ b/integrations/webfinger_test.go @@ -20,6 +20,9 @@ import ( func TestWebfinger(t *testing.T) { defer prepareTestEnv(t)() + old := setting.Federation.Enabled + setting.Federation.Enabled = true + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) appURL, _ := url.Parse(setting.AppURL) @@ -60,4 +63,6 @@ func TestWebfinger(t *testing.T) { req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=mailto:%s", user.Email)) MakeRequest(t, req, http.StatusNotFound) + + setting.Federation.Enabled = old } From c5253882e9a95de9f02230d0a6754dd24b25dfeb Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 2 May 2022 20:26:37 +0000 Subject: [PATCH 8/8] Fix tests. --- integrations/webfinger_test.go | 6 +++--- routers/web/web.go | 11 +++++++++-- routers/web/webfinger.go | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/integrations/webfinger_test.go b/integrations/webfinger_test.go index b2efc334467b7..8ba93c3f204b8 100644 --- a/integrations/webfinger_test.go +++ b/integrations/webfinger_test.go @@ -20,8 +20,10 @@ import ( func TestWebfinger(t *testing.T) { defer prepareTestEnv(t)() - old := setting.Federation.Enabled setting.Federation.Enabled = true + defer func() { + setting.Federation.Enabled = false + }() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) @@ -63,6 +65,4 @@ func TestWebfinger(t *testing.T) { req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=mailto:%s", user.Email)) MakeRequest(t, req, http.StatusNotFound) - - setting.Federation.Enabled = old } diff --git a/routers/web/web.go b/routers/web/web.go index 59949579a5c75..985adfa8b6b04 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -279,6 +279,13 @@ func RegisterRoutes(m *web.Route) { } } + federationEnabled := func(ctx *context.Context) { + if !setting.Federation.Enabled { + ctx.Error(http.StatusNotFound) + return + } + } + // FIXME: not all routes need go through same middleware. // Especially some AJAX requests, we can reduce middleware number to improve performance. // Routers. @@ -286,10 +293,10 @@ func RegisterRoutes(m *web.Route) { m.Get("/", Home) m.Group("/.well-known", func() { m.Get("/openid-configuration", auth.OIDCWellKnown) - if setting.Federation.Enabled { + m.Group("", func() { m.Get("/nodeinfo", NodeInfoLinks) m.Get("/webfinger", WebfingerQuery) - } + }, federationEnabled) m.Get("/change-password", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/user/settings/account", http.StatusTemporaryRedirect) }) diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go index 6bbddb4c0e4b6..27d0351b81db9 100644 --- a/routers/web/webfinger.go +++ b/routers/web/webfinger.go @@ -73,7 +73,7 @@ func WebfingerQuery(ctx *context.Context) { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusNotFound) } else { - log.Error("Error getting user: %v", err) + log.Error("Error getting user: %s Error: %v", resource.Opaque, err) ctx.Error(http.StatusInternalServerError) } return