mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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:
		
							
								
								
									
										68
									
								
								integrations/webfinger_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								integrations/webfinger_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
		})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								routers/web/webfinger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								routers/web/webfinger.go
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user