mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:02:25 -05:00 
			
		
		
		
	[feature] Support Actor URIs for webfinger queries (#2187)
* [feature] Support Actor URIs for webfinger queries It's now possible to pass an Actor URI as the resource to query for when doing a webfinger query. The code now extracts the username and domain from the URI. The URI needs to be fully qualified, including having a scheme of http or https to be recognised as such. The acct scheme is handled as we used to, including dealing with an erroneous leading @ on the username. We retain the ability to handle resources without a scheme by parsing them again with the acct scheme if the original parse failed. This can happen due to parsing ambiguities when dealing with a string like user@domain.tld:port. * [bugfix] Remove debugging changes * [chore] Make TestExtractNamestring table-driven * [chore] Unnest Trim and Split for readability
This commit is contained in:
		
					parent
					
						
							
								7011f57b09
							
						
					
				
			
			
				commit
				
					
						2cac5a4613
					
				
			
		
					 3 changed files with 203 additions and 85 deletions
				
			
		|  | @ -143,6 +143,45 @@ func (suite *WebfingerGetTestSuite) TestFingerUser() { | ||||||
| }`, resp) | }`, resp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *WebfingerGetTestSuite) TestFingerUserActorURI() { | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 	host := config.GetHost() | ||||||
|  | 
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		resource string | ||||||
|  | 	}{ | ||||||
|  | 		{resource: fmt.Sprintf("https://%s/@%s", host, targetAccount.Username)}, | ||||||
|  | 		{resource: fmt.Sprintf("https://%s/users/%s", host, targetAccount.Username)}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		tt := tt | ||||||
|  | 		suite.Run(tt.resource, func() { | ||||||
|  | 			requestPath := fmt.Sprintf("/%s?resource=%s", webfinger.WebfingerBasePath, tt.resource) | ||||||
|  | 			resp := suite.finger(requestPath) | ||||||
|  | 			suite.Equal(`{ | ||||||
|  |   "subject": "acct:the_mighty_zork@localhost:8080", | ||||||
|  |   "aliases": [ | ||||||
|  |     "http://localhost:8080/users/the_mighty_zork", | ||||||
|  |     "http://localhost:8080/@the_mighty_zork" | ||||||
|  |   ], | ||||||
|  |   "links": [ | ||||||
|  |     { | ||||||
|  |       "rel": "http://webfinger.net/rel/profile-page", | ||||||
|  |       "type": "text/html", | ||||||
|  |       "href": "http://localhost:8080/@the_mighty_zork" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "rel": "self", | ||||||
|  |       "type": "application/activity+json", | ||||||
|  |       "href": "http://localhost:8080/users/the_mighty_zork" | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | }`, resp) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHost() { | func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHost() { | ||||||
| 	targetAccount := suite.funkifyAccountDomain("gts.example.org", "example.org") | 	targetAccount := suite.funkifyAccountDomain("gts.example.org", "example.org") | ||||||
| 	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, config.GetHost()) | 	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, config.GetHost()) | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ package util | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/regexes" | 	"github.com/superseriousbusiness/gotosocial/internal/regexes" | ||||||
|  | @ -40,19 +41,83 @@ func ExtractNamestringParts(mention string) (username, host string, err error) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ExtractWebfingerParts returns username test_user and | // ExtractWebfingerParts returns the username and domain from either an | ||||||
| // domain example.org from a string like acct:test_user@example.org, | // account query or an actor URI. | ||||||
| // or acct:@test_user@example.org. |  | ||||||
| // | // | ||||||
| // If nothing is extracted, it will return an error. | // All implementations in the wild generate webfinger account resource | ||||||
|  | // queries with the "acct" scheme and without a leading "@"" on the username. | ||||||
|  | // This is also the format the "subject" in a webfinger response adheres to. | ||||||
|  | // | ||||||
|  | // Despite this fact, we're being permissive about a single leading @. This | ||||||
|  | // makes a query for acct:user@domain.tld and acct:@user@domain.tld | ||||||
|  | // equivalent. But a query for acct:@@user@domain.tld will have its username | ||||||
|  | // returned with the @ prefix. | ||||||
|  | // | ||||||
|  | // We also permit a resource of user@domain.tld or @user@domain.tld, without | ||||||
|  | // a scheme. In that case it gets interpreted as if it was using the "acct" | ||||||
|  | // scheme. | ||||||
|  | // | ||||||
|  | // When parsing fails, an error is returned. | ||||||
| func ExtractWebfingerParts(webfinger string) (username, host string, err error) { | func ExtractWebfingerParts(webfinger string) (username, host string, err error) { | ||||||
| 	// remove the acct: prefix if it's present | 	orig := webfinger | ||||||
| 	webfinger = strings.TrimPrefix(webfinger, "acct:") |  | ||||||
| 
 | 
 | ||||||
| 	// prepend an @ if necessary | 	u, oerr := url.ParseRequestURI(webfinger) | ||||||
| 	if webfinger[0] != '@' { | 	if oerr != nil { | ||||||
| 		webfinger = "@" + webfinger | 		// Most likely reason for failing to parse is if the "acct" scheme was | ||||||
|  | 		// missing but a :port was included. So try an extra time with the scheme. | ||||||
|  | 		u, err = url.ParseRequestURI("acct:" + webfinger) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", "", fmt.Errorf("failed to parse %s with acct sheme: %w", orig, oerr) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return ExtractNamestringParts(webfinger) | 	if u.Scheme == "http" || u.Scheme == "https" { | ||||||
|  | 		return ExtractWebfingerPartsFromURI(u) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if u.Scheme != "acct" { | ||||||
|  | 		return "", "", fmt.Errorf("unsupported scheme: %s for resource: %s", u.Scheme, orig) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	stripped := strings.TrimPrefix(u.Opaque, "@") | ||||||
|  | 	userDomain := strings.Split(stripped, "@") | ||||||
|  | 	if len(userDomain) != 2 { | ||||||
|  | 		return "", "", fmt.Errorf("failed to extract user and domain from: %s", orig) | ||||||
|  | 	} | ||||||
|  | 	return userDomain[0], userDomain[1], nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ExtractWebfingerPartsFromURI returns the user and domain extracted from | ||||||
|  | // the passed in URI. The URI should be an actor URI. | ||||||
|  | // | ||||||
|  | // The domain returned is the hostname, and the user will be extracted | ||||||
|  | // from either /@test_user or /users/test_user. These two paths match the | ||||||
|  | // "aliasses" we include in our webfinger response and are also present in | ||||||
|  | // our "links". | ||||||
|  | // | ||||||
|  | // Like with ExtractWebfingerParts, we're being permissive about a single | ||||||
|  | // leading @. | ||||||
|  | // | ||||||
|  | // Errors are returned in case we end up with an empty domain or username. | ||||||
|  | func ExtractWebfingerPartsFromURI(uri *url.URL) (username, host string, err error) { | ||||||
|  | 	host = uri.Host | ||||||
|  | 	if host == "" { | ||||||
|  | 		return "", "", fmt.Errorf("failed to extract domain from: %s", uri) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// strip any leading slashes | ||||||
|  | 	path := strings.TrimLeft(uri.Path, "/") | ||||||
|  | 	segs := strings.Split(path, "/") | ||||||
|  | 	if segs[0] == "users" { | ||||||
|  | 		username = segs[1] | ||||||
|  | 	} else { | ||||||
|  | 		username = segs[0] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	username = strings.TrimPrefix(username, "@") | ||||||
|  | 	if username == "" { | ||||||
|  | 		return "", "", fmt.Errorf("failed to extract username from: %s", uri) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
| package util_test | package util_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"net/url" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
|  | @ -28,88 +29,101 @@ type NamestringSuite struct { | ||||||
| 	suite.Suite | 	suite.Suite | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *NamestringSuite) TestExtractWebfingerParts1() { | func (suite *NamestringSuite) TestExtractWebfingerParts() { | ||||||
| 	webfinger := "acct:stonerkitty.monster@stonerkitty.monster" | 	tests := []struct { | ||||||
| 	username, host, err := util.ExtractWebfingerParts(webfinger) | 		in, username, domain, err string | ||||||
|  | 	}{ | ||||||
|  | 		{in: "acct:stonerkitty.monster@stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "acct:stonerkitty.monster@stonerkitty.monster:8080", username: "stonerkitty.monster", domain: "stonerkitty.monster:8080"}, | ||||||
|  | 		{in: "acct:@stonerkitty.monster@stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "stonerkitty.monster@stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "stonerkitty.monster@stonerkitty.monster:8080", username: "stonerkitty.monster", domain: "stonerkitty.monster:8080"}, | ||||||
|  | 		{in: "@stonerkitty.monster@stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "acct:@@stonerkitty.monster@stonerkitty.monster", err: "failed to extract user and domain from: acct:@@stonerkitty.monster@stonerkitty.monster"}, | ||||||
|  | 		{in: "acct:@stonerkitty.monster@@stonerkitty.monster", err: "failed to extract user and domain from: acct:@stonerkitty.monster@@stonerkitty.monster"}, | ||||||
|  | 		{in: "@@stonerkitty.monster@stonerkitty.monster", err: "failed to extract user and domain from: @@stonerkitty.monster@stonerkitty.monster"}, | ||||||
|  | 		{in: "@stonerkitty.monster@@stonerkitty.monster", err: "failed to extract user and domain from: @stonerkitty.monster@@stonerkitty.monster"}, | ||||||
|  | 		{in: "s3:stonerkitty.monster@stonerkitty.monster", err: "unsupported scheme: s3 for resource: s3:stonerkitty.monster@stonerkitty.monster"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		tt := tt | ||||||
|  | 		suite.Run(tt.in, func() { | ||||||
|  | 			suite.T().Parallel() | ||||||
|  | 			username, domain, err := util.ExtractWebfingerParts(tt.in) | ||||||
|  | 			if tt.err == "" { | ||||||
| 				suite.NoError(err) | 				suite.NoError(err) | ||||||
| 
 | 				suite.Equal(tt.username, username) | ||||||
| 	suite.Equal("stonerkitty.monster", username) | 				suite.Equal(tt.domain, domain) | ||||||
| 	suite.Equal("stonerkitty.monster", host) | 			} else { | ||||||
|  | 				suite.EqualError(err, tt.err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *NamestringSuite) TestExtractWebfingerParts2() { | func (suite *NamestringSuite) TestExtractWebfingerPartsFromURI() { | ||||||
| 	webfinger := "@stonerkitty.monster@stonerkitty.monster" | 	tests := []struct { | ||||||
| 	username, host, err := util.ExtractWebfingerParts(webfinger) | 		in, username, domain, err string | ||||||
|  | 	}{ | ||||||
|  | 		{in: "https://stonerkitty.monster/users/stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/users/@stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/@stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/@@stonerkitty.monster", username: "@stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "https://stonerkitty.monster:8080/users/stonerkitty.monster", username: "stonerkitty.monster", domain: "stonerkitty.monster:8080"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/users/stonerkitty.monster/evil", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/@stonerkitty.monster/evil", username: "stonerkitty.monster", domain: "stonerkitty.monster"}, | ||||||
|  | 		{in: "/@stonerkitty.monster", err: "failed to extract domain from: /@stonerkitty.monster"}, | ||||||
|  | 		{in: "/users/stonerkitty.monster", err: "failed to extract domain from: /users/stonerkitty.monster"}, | ||||||
|  | 		{in: "@stonerkitty.monster", err: "failed to extract domain from: @stonerkitty.monster"}, | ||||||
|  | 		{in: "users/stonerkitty.monster", err: "failed to extract domain from: users/stonerkitty.monster"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/users/", err: "failed to extract username from: https://stonerkitty.monster/users/"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/users/@", err: "failed to extract username from: https://stonerkitty.monster/users/@"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/@", err: "failed to extract username from: https://stonerkitty.monster/@"}, | ||||||
|  | 		{in: "https://stonerkitty.monster/", err: "failed to extract username from: https://stonerkitty.monster/"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		tt := tt | ||||||
|  | 		suite.Run(tt.in, func() { | ||||||
|  | 			suite.T().Parallel() | ||||||
|  | 			uri, _ := url.Parse(tt.in) | ||||||
|  | 			username, domain, err := util.ExtractWebfingerPartsFromURI(uri) | ||||||
|  | 			if tt.err == "" { | ||||||
| 				suite.NoError(err) | 				suite.NoError(err) | ||||||
| 
 | 				suite.Equal(tt.username, username) | ||||||
| 	suite.Equal("stonerkitty.monster", username) | 				suite.Equal(tt.domain, domain) | ||||||
| 	suite.Equal("stonerkitty.monster", host) | 			} else { | ||||||
|  | 				suite.EqualError(err, tt.err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *NamestringSuite) TestExtractWebfingerParts3() { | func (suite *NamestringSuite) TestExtractNamestring() { | ||||||
| 	webfinger := "acct:someone@somewhere" | 	tests := []struct { | ||||||
| 	username, host, err := util.ExtractWebfingerParts(webfinger) | 		in, username, host, err string | ||||||
|  | 	}{ | ||||||
|  | 		{in: "@stonerkitty.monster@stonerkitty.monster", username: "stonerkitty.monster", host: "stonerkitty.monster"}, | ||||||
|  | 		{in: "@stonerkitty.monster", username: "stonerkitty.monster"}, | ||||||
|  | 		{in: "@someone@somewhere", username: "someone", host: "somewhere"}, | ||||||
|  | 		{in: "", err: "couldn't match mention "}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		tt := tt | ||||||
|  | 		suite.Run(tt.in, func() { | ||||||
|  | 			suite.T().Parallel() | ||||||
|  | 			username, host, err := util.ExtractNamestringParts(tt.in) | ||||||
|  | 			if tt.err != "" { | ||||||
|  | 				suite.EqualError(err, tt.err) | ||||||
|  | 			} else { | ||||||
| 				suite.NoError(err) | 				suite.NoError(err) | ||||||
| 
 | 				suite.Equal(tt.username, username) | ||||||
| 	suite.Equal("someone", username) | 				suite.Equal(tt.host, host) | ||||||
| 	suite.Equal("somewhere", host) | 			} | ||||||
| } | 		}) | ||||||
| 
 | 	} | ||||||
| func (suite *NamestringSuite) TestExtractWebfingerParts4() { |  | ||||||
| 	webfinger := "@stoner-kitty.monster@stonerkitty.monster" |  | ||||||
| 	username, host, err := util.ExtractWebfingerParts(webfinger) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	suite.Equal("stoner-kitty.monster", username) |  | ||||||
| 	suite.Equal("stonerkitty.monster", host) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (suite *NamestringSuite) TestExtractWebfingerParts5() { |  | ||||||
| 	webfinger := "@stonerkitty.monster" |  | ||||||
| 	username, host, err := util.ExtractWebfingerParts(webfinger) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	suite.Equal("stonerkitty.monster", username) |  | ||||||
| 	suite.Empty(host) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (suite *NamestringSuite) TestExtractWebfingerParts6() { |  | ||||||
| 	webfinger := "@@stonerkitty.monster" |  | ||||||
| 	_, _, err := util.ExtractWebfingerParts(webfinger) |  | ||||||
| 	suite.EqualError(err, "couldn't match mention @@stonerkitty.monster") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (suite *NamestringSuite) TestExtractNamestringParts1() { |  | ||||||
| 	namestring := "@stonerkitty.monster@stonerkitty.monster" |  | ||||||
| 	username, host, err := util.ExtractNamestringParts(namestring) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	suite.Equal("stonerkitty.monster", username) |  | ||||||
| 	suite.Equal("stonerkitty.monster", host) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (suite *NamestringSuite) TestExtractNamestringParts2() { |  | ||||||
| 	namestring := "@stonerkitty.monster" |  | ||||||
| 	username, host, err := util.ExtractNamestringParts(namestring) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	suite.Equal("stonerkitty.monster", username) |  | ||||||
| 	suite.Empty(host) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (suite *NamestringSuite) TestExtractNamestringParts3() { |  | ||||||
| 	namestring := "@someone@somewhere" |  | ||||||
| 	username, host, err := util.ExtractWebfingerParts(namestring) |  | ||||||
| 	suite.NoError(err) |  | ||||||
| 
 |  | ||||||
| 	suite.Equal("someone", username) |  | ||||||
| 	suite.Equal("somewhere", host) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (suite *NamestringSuite) TestExtractNamestringParts4() { |  | ||||||
| 	namestring := "" |  | ||||||
| 	_, _, err := util.ExtractNamestringParts(namestring) |  | ||||||
| 	suite.EqualError(err, "couldn't match mention ") |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestNamestringSuite(t *testing.T) { | func TestNamestringSuite(t *testing.T) { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue