Add Webfinger endpoint (#19462)
This adds the [Webfinger](https://webfinger.net/) endpoint for federation. Supported schemes are `acct` and `mailto`. The profile and avatar url are returned as metadata.
This commit is contained in:
		
							parent
							
								
									a61a47f9a0
								
							
						
					
					
						commit
						3da9dafc60
					
				|  | @ -0,0 +1,68 @@ | |||
| // 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)() | ||||
| 
 | ||||
| 	setting.Federation.Enabled = true | ||||
| 	defer func() { | ||||
| 		setting.Federation.Enabled = false | ||||
| 	}() | ||||
| 
 | ||||
| 	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) | ||||
| } | ||||
|  | @ -282,6 +282,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.
 | ||||
|  | @ -289,9 +296,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) | ||||
| 		}) | ||||
|  |  | |||
|  | @ -0,0 +1,111 @@ | |||
| // 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" | ||||
| 	"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" | ||||
| ) | ||||
| 
 | ||||
| // 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) { | ||||
| 	appURL, _ := url.Parse(setting.AppURL) | ||||
| 
 | ||||
| 	resource, err := url.Parse(ctx.FormTrim("resource")) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var u *user_model.User | ||||
| 
 | ||||
| 	switch resource.Scheme { | ||||
| 	case "acct": | ||||
| 		// allow only the current host
 | ||||
| 		parts := strings.SplitN(resource.Opaque, "@", 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, resource.Opaque) | ||||
| 		if u != nil && u.KeepEmailPrivate { | ||||
| 			err = user_model.ErrUserNotExist{} | ||||
| 		} | ||||
| 	default: | ||||
| 		ctx.Error(http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		if user_model.IsErrUserNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound) | ||||
| 		} else { | ||||
| 			log.Error("Error getting user: %s Error: %v", resource.Opaque, err) | ||||
| 			ctx.Error(http.StatusInternalServerError) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !user_model.IsUserVisibleToViewer(u, ctx.Doer) { | ||||
| 		ctx.Error(http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	aliases := []string{ | ||||
| 		u.HTMLURL(), | ||||
| 	} | ||||
| 	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, | ||||
| 	}) | ||||
| } | ||||
		Loading…
	
		Reference in New Issue