mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 06:12:25 -05:00 
			
		
		
		
	[bugfix] add stricter checks during all stages of dereferencing remote AS objects (#2639)
* add stricter checks during all stages of dereferencing remote AS objects * a comment
This commit is contained in:
		
					parent
					
						
							
								a3aa6042d7
							
						
					
				
			
			
				commit
				
					
						b9013a8ab3
					
				
			
		
					 15 changed files with 351 additions and 167 deletions
				
			
		|  | @ -104,6 +104,9 @@ var Start action.GTSAction = func(ctx context.Context) error { | ||||||
| 		return &http.Response{ | 		return &http.Response{ | ||||||
| 			StatusCode: 200, | 			StatusCode: 200, | ||||||
| 			Body:       r, | 			Body:       r, | ||||||
|  | 			Header: http.Header{ | ||||||
|  | 				"Content-Type": req.Header.Values("Accept"), | ||||||
|  | 			}, | ||||||
| 		}, nil | 		}, nil | ||||||
| 	}, "")) | 	}, "")) | ||||||
| 	mediaManager := testrig.NewTestMediaManager(&state) | 	mediaManager := testrig.NewTestMediaManager(&state) | ||||||
|  |  | ||||||
|  | @ -17,6 +17,8 @@ | ||||||
| 
 | 
 | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
|  | import "strings" | ||||||
|  | 
 | ||||||
| const ( | const ( | ||||||
| 	// Possible GoToSocial mimetypes. | 	// Possible GoToSocial mimetypes. | ||||||
| 	AppJSON           = `application/json` | 	AppJSON           = `application/json` | ||||||
|  | @ -24,7 +26,8 @@ const ( | ||||||
| 	AppXMLXRD         = `application/xrd+xml` | 	AppXMLXRD         = `application/xrd+xml` | ||||||
| 	AppRSSXML         = `application/rss+xml` | 	AppRSSXML         = `application/rss+xml` | ||||||
| 	AppActivityJSON   = `application/activity+json` | 	AppActivityJSON   = `application/activity+json` | ||||||
| 	AppActivityLDJSON = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` | 	appActivityLDJSON = `application/ld+json` // without profile | ||||||
|  | 	AppActivityLDJSON = appActivityLDJSON + `; profile="https://www.w3.org/ns/activitystreams"` | ||||||
| 	AppJRDJSON        = `application/jrd+json` // https://www.rfc-editor.org/rfc/rfc7033#section-10.2 | 	AppJRDJSON        = `application/jrd+json` // https://www.rfc-editor.org/rfc/rfc7033#section-10.2 | ||||||
| 	AppForm           = `application/x-www-form-urlencoded` | 	AppForm           = `application/x-www-form-urlencoded` | ||||||
| 	MultipartForm     = `multipart/form-data` | 	MultipartForm     = `multipart/form-data` | ||||||
|  | @ -32,3 +35,112 @@ const ( | ||||||
| 	TextHTML          = `text/html` | 	TextHTML          = `text/html` | ||||||
| 	TextCSS           = `text/css` | 	TextCSS           = `text/css` | ||||||
| ) | ) | ||||||
|  | 
 | ||||||
|  | // JSONContentType returns whether is application/json(;charset=utf-8)? content-type. | ||||||
|  | func JSONContentType(ct string) bool { | ||||||
|  | 	p := splitContentType(ct) | ||||||
|  | 	p, ok := isUTF8ContentType(p) | ||||||
|  | 	return ok && len(p) == 1 && | ||||||
|  | 		p[0] == AppJSON | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JSONJRDContentType returns whether is application/(jrd+)?json(;charset=utf-8)? content-type. | ||||||
|  | func JSONJRDContentType(ct string) bool { | ||||||
|  | 	p := splitContentType(ct) | ||||||
|  | 	p, ok := isUTF8ContentType(p) | ||||||
|  | 	return ok && len(p) == 1 && | ||||||
|  | 		p[0] == AppJSON || | ||||||
|  | 		p[0] == AppJRDJSON | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // XMLContentType returns whether is application/xml(;charset=utf-8)? content-type. | ||||||
|  | func XMLContentType(ct string) bool { | ||||||
|  | 	p := splitContentType(ct) | ||||||
|  | 	p, ok := isUTF8ContentType(p) | ||||||
|  | 	return ok && len(p) == 1 && | ||||||
|  | 		p[0] == AppXML | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // XMLXRDContentType returns whether is application/(xrd+)?xml(;charset=utf-8)? content-type. | ||||||
|  | func XMLXRDContentType(ct string) bool { | ||||||
|  | 	p := splitContentType(ct) | ||||||
|  | 	p, ok := isUTF8ContentType(p) | ||||||
|  | 	return ok && len(p) == 1 && | ||||||
|  | 		p[0] == AppXML || | ||||||
|  | 		p[0] == AppXMLXRD | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ASContentType returns whether is valid ActivityStreams content-types: | ||||||
|  | // - application/activity+json | ||||||
|  | // - application/ld+json;profile=https://w3.org/ns/activitystreams | ||||||
|  | func ASContentType(ct string) bool { | ||||||
|  | 	p := splitContentType(ct) | ||||||
|  | 	p, ok := isUTF8ContentType(p) | ||||||
|  | 	if !ok { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	switch len(p) { | ||||||
|  | 	case 1: | ||||||
|  | 		return p[0] == AppActivityJSON | ||||||
|  | 	case 2: | ||||||
|  | 		return p[0] == appActivityLDJSON && | ||||||
|  | 			p[1] == "profile=https://www.w3.org/ns/activitystreams" || | ||||||
|  | 			p[1] == "profile=\"https://www.w3.org/ns/activitystreams\"" | ||||||
|  | 	default: | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NodeInfo2ContentType returns whether is nodeinfo schema 2.0 content-type. | ||||||
|  | func NodeInfo2ContentType(ct string) bool { | ||||||
|  | 	p := splitContentType(ct) | ||||||
|  | 	p, ok := isUTF8ContentType(p) | ||||||
|  | 	if !ok { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	switch len(p) { | ||||||
|  | 	case 1: | ||||||
|  | 		return p[0] == AppJSON | ||||||
|  | 	case 2: | ||||||
|  | 		return p[0] == AppJSON && | ||||||
|  | 			p[1] == "profile=\"http://nodeinfo.diaspora.software/ns/schema/2.0#\"" || | ||||||
|  | 			p[1] == "profile=http://nodeinfo.diaspora.software/ns/schema/2.0#" | ||||||
|  | 	default: | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // isUTF8ContentType checks for a provided charset in given | ||||||
|  | // type parts list, removes it and returns whether is utf-8. | ||||||
|  | func isUTF8ContentType(p []string) ([]string, bool) { | ||||||
|  | 	const charset = "charset=" | ||||||
|  | 	const charsetUTF8 = charset + "utf-8" | ||||||
|  | 	for i, part := range p { | ||||||
|  | 
 | ||||||
|  | 		// Only handle charset slice parts. | ||||||
|  | 		if part[:len(charset)] == charset { | ||||||
|  | 
 | ||||||
|  | 			// Check if is UTF-8 charset. | ||||||
|  | 			ok := (part == charsetUTF8) | ||||||
|  | 
 | ||||||
|  | 			// Drop this slice part. | ||||||
|  | 			_ = copy(p[i:], p[i+1:]) | ||||||
|  | 			p = p[:len(p)-1] | ||||||
|  | 
 | ||||||
|  | 			return p, ok | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return p, true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // splitContentType splits content-type into semi-colon | ||||||
|  | // separated parts. useful when a charset is provided. | ||||||
|  | // note this also maps all chars to their lowercase form. | ||||||
|  | func splitContentType(ct string) []string { | ||||||
|  | 	s := strings.Split(ct, ";") | ||||||
|  | 	for i := range s { | ||||||
|  | 		s[i] = strings.TrimSpace(s[i]) | ||||||
|  | 		s[i] = strings.ToLower(s[i]) | ||||||
|  | 	} | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										75
									
								
								internal/api/util/mime_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/api/util/mime_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | ||||||
|  | package util_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestIsASContentType(t *testing.T) { | ||||||
|  | 	for _, test := range []struct { | ||||||
|  | 		Input  string | ||||||
|  | 		Expect bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/activity+json", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/activity+json; charset=utf-8", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/activity+json;charset=utf-8", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/activity+json ;charset=utf-8", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/activity+json ; charset=utf-8", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json;profile=https://www.w3.org/ns/activitystreams", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json ;profile=https://www.w3.org/ns/activitystreams", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json ; profile=https://www.w3.org/ns/activitystreams", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json; profile=https://www.w3.org/ns/activitystreams", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", | ||||||
|  | 			Expect: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Input:  "application/ld+json", | ||||||
|  | 			Expect: false, | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		if util.ASContentType(test.Input) != test.Expect { | ||||||
|  | 			t.Errorf("did not get expected result %v for input: %s", test.Expect, test.Input) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -586,6 +586,16 @@ func (d *Dereferencer) enrichAccount( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if account.Username == "" { | 	if account.Username == "" { | ||||||
|  | 		// Assume the host from the | ||||||
|  | 		// ActivityPub representation. | ||||||
|  | 		id := ap.GetJSONLDId(apubAcc) | ||||||
|  | 		if id == nil { | ||||||
|  | 			return nil, nil, gtserror.New("no id property found on person, or id was not an iri") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Get IRI host value. | ||||||
|  | 		accHost := id.Host | ||||||
|  | 
 | ||||||
| 		// No username was provided, so no webfinger was attempted earlier. | 		// No username was provided, so no webfinger was attempted earlier. | ||||||
| 		// | 		// | ||||||
| 		// Now we have a username we can attempt again, to ensure up-to-date | 		// Now we have a username we can attempt again, to ensure up-to-date | ||||||
|  | @ -596,42 +606,37 @@ func (d *Dereferencer) enrichAccount( | ||||||
| 		// https://example.org/@someone@somewhere.else and we've been redirected | 		// https://example.org/@someone@somewhere.else and we've been redirected | ||||||
| 		// from example.org to somewhere.else: we want to take somewhere.else | 		// from example.org to somewhere.else: we want to take somewhere.else | ||||||
| 		// as the accountDomain then, not the example.org we were redirected from. | 		// as the accountDomain then, not the example.org we were redirected from. | ||||||
| 
 |  | ||||||
| 		// Assume the host from the returned |  | ||||||
| 		// ActivityPub representation. |  | ||||||
| 		id := ap.GetJSONLDId(apubAcc) |  | ||||||
| 		if id == nil { |  | ||||||
| 			return nil, nil, gtserror.New("no id property found on person, or id was not an iri") |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Get IRI host value. |  | ||||||
| 		accHost := id.Host |  | ||||||
| 
 |  | ||||||
| 		latestAcc.Domain, _, err = d.fingerRemoteAccount(ctx, | 		latestAcc.Domain, _, err = d.fingerRemoteAccount(ctx, | ||||||
| 			tsport, | 			tsport, | ||||||
| 			latestAcc.Username, | 			latestAcc.Username, | ||||||
| 			accHost, | 			accHost, | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			// We still couldn't webfinger the account, so we're not certain | 			// Webfingering account still failed, so we're not certain | ||||||
| 			// what the accountDomain actually is. Still, we can make a solid | 			// what the accountDomain actually is. Exit here for safety. | ||||||
| 			// guess that it's the Host of the ActivityPub URI of the account. | 			return nil, nil, gtserror.Newf( | ||||||
| 			// If we're wrong, we can just try again in a couple days. | 				"error webfingering remote account %s@%s: %w", | ||||||
| 			log.Errorf(ctx, "error webfingering[2] remote account %s@%s: %v", latestAcc.Username, accHost, err) | 				latestAcc.Username, accHost, err, | ||||||
| 			latestAcc.Domain = accHost | 			) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if latestAcc.Domain == "" { | 	if latestAcc.Domain == "" { | ||||||
| 		// Ensure we have a domain set by this point, | 		// Ensure we have a domain set by this point, | ||||||
| 		// otherwise it gets stored as a local user! | 		// otherwise it gets stored as a local user! | ||||||
| 		// |  | ||||||
| 		// TODO: there is probably a more granular way |  | ||||||
| 		// way of checking this in each of the above parts, |  | ||||||
| 		// and honestly it could do with a smol refactor. |  | ||||||
| 		return nil, nil, gtserror.Newf("empty domain for %s", uri) | 		return nil, nil, gtserror.Newf("empty domain for %s", uri) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure the final parsed account URI / URL matches | ||||||
|  | 	// the input URI we fetched (or received) it as. | ||||||
|  | 	if expect := uri.String(); latestAcc.URI != expect && | ||||||
|  | 		latestAcc.URL != expect { | ||||||
|  | 		return nil, nil, gtserror.Newf( | ||||||
|  | 			"dereferenced account uri %s does not match %s", | ||||||
|  | 			latestAcc.URI, expect, | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	/* | 	/* | ||||||
| 		BY THIS POINT we have more or less a fullly-formed | 		BY THIS POINT we have more or less a fullly-formed | ||||||
| 		representation of the target account, derived from | 		representation of the target account, derived from | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ package dereferencing_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | @ -207,6 +208,28 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() { | ||||||
| 	suite.Nil(fetchedAccount) | 	suite.Nil(fetchedAccount) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithNonMatchingURI() { | ||||||
|  | 	fetchingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	const ( | ||||||
|  | 		remoteURI    = "https://turnip.farm/users/turniplover6969" | ||||||
|  | 		remoteAltURI = "https://turnip.farm/users/turniphater420" | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	// Create a copy of this remote account at alternative URI. | ||||||
|  | 	remotePerson := suite.client.TestRemotePeople[remoteURI] | ||||||
|  | 	suite.client.TestRemotePeople[remoteAltURI] = remotePerson | ||||||
|  | 
 | ||||||
|  | 	// Attempt to fetch account at alternative URI, it should fail! | ||||||
|  | 	fetchedAccount, _, err := suite.dereferencer.GetAccountByURI( | ||||||
|  | 		context.Background(), | ||||||
|  | 		fetchingAccount.Username, | ||||||
|  | 		testrig.URLMustParse(remoteAltURI), | ||||||
|  | 	) | ||||||
|  | 	suite.Equal(err.Error(), fmt.Sprintf("enrichAccount: dereferenced account uri %s does not match %s", remoteURI, remoteAltURI)) | ||||||
|  | 	suite.Nil(fetchedAccount) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestAccountTestSuite(t *testing.T) { | func TestAccountTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(AccountTestSuite)) | 	suite.Run(t, new(AccountTestSuite)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -35,6 +35,7 @@ type DereferencerStandardTestSuite struct { | ||||||
| 	db      db.DB | 	db      db.DB | ||||||
| 	storage *storage.Driver | 	storage *storage.Driver | ||||||
| 	state   state.State | 	state   state.State | ||||||
|  | 	client  *testrig.MockHTTPClient | ||||||
| 
 | 
 | ||||||
| 	testRemoteStatuses    map[string]vocab.ActivityStreamsNote | 	testRemoteStatuses    map[string]vocab.ActivityStreamsNote | ||||||
| 	testRemotePeople      map[string]vocab.ActivityStreamsPerson | 	testRemotePeople      map[string]vocab.ActivityStreamsPerson | ||||||
|  | @ -72,11 +73,12 @@ func (suite *DereferencerStandardTestSuite) SetupTest() { | ||||||
| 		converter, | 		converter, | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | 	suite.client = testrig.NewMockHTTPClient(nil, "../../../testrig/media") | ||||||
| 	suite.storage = testrig.NewInMemoryStorage() | 	suite.storage = testrig.NewInMemoryStorage() | ||||||
| 	suite.state.DB = suite.db | 	suite.state.DB = suite.db | ||||||
| 	suite.state.Storage = suite.storage | 	suite.state.Storage = suite.storage | ||||||
| 	media := testrig.NewTestMediaManager(&suite.state) | 	media := testrig.NewTestMediaManager(&suite.state) | ||||||
| 	suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media")), media) | 	suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, suite.client), media) | ||||||
| 	testrig.StandardDBSetup(suite.db, nil) | 	testrig.StandardDBSetup(suite.db, nil) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,9 +21,9 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" |  | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/transport" | 	"github.com/superseriousbusiness/gotosocial/internal/transport" | ||||||
|  | @ -74,10 +74,12 @@ func (d *Dereferencer) fingerRemoteAccount( | ||||||
| 		return "", nil, err | 		return "", nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	_, accountDomain, err := util.ExtractWebfingerParts(resp.Subject) | 	accUsername, accDomain, err := util.ExtractWebfingerParts(resp.Subject) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = gtserror.Newf("error extracting subject parts for %s: %w", target, err) | 		err = gtserror.Newf("error extracting subject parts for %s: %w", target, err) | ||||||
| 		return "", nil, err | 		return "", nil, err | ||||||
|  | 	} else if accUsername != username { | ||||||
|  | 		return "", nil, gtserror.Newf("response username does not match input for %s: %w", target, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Look through links for the first | 	// Look through links for the first | ||||||
|  | @ -92,8 +94,7 @@ func (d *Dereferencer) fingerRemoteAccount( | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !strings.EqualFold(link.Type, "application/activity+json") && | 		if !apiutil.ASContentType(link.Type) { | ||||||
| 			!strings.EqualFold(link.Type, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") { |  | ||||||
| 			// Not an AP type, ignore. | 			// Not an AP type, ignore. | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | @ -121,7 +122,7 @@ func (d *Dereferencer) fingerRemoteAccount( | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// All looks good, return happily! | 		// All looks good, return happily! | ||||||
| 		return accountDomain, uri, nil | 		return accDomain, uri, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return "", nil, gtserror.Newf("no suitable self, AP-type link found in webfinger response for %s", target) | 	return "", nil, gtserror.Newf("no suitable self, AP-type link found in webfinger response for %s", target) | ||||||
|  |  | ||||||
|  | @ -396,7 +396,7 @@ func (d *Dereferencer) enrichStatus( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Ensure we have the author account of the status dereferenced (+ up-to-date). If this is a new status | 	// Ensure we have the author account of the status dereferenced (+ up-to-date). If this is a new status | ||||||
| 	// (i.e. status.AccountID == "") then any error here is irrecoverable. AccountID must ALWAYS be set. | 	// (i.e. status.AccountID == "") then any error here is irrecoverable. status.AccountID must ALWAYS be set. | ||||||
| 	if _, _, err := d.getAccountByURI(ctx, requestUser, attributedTo); err != nil && status.AccountID == "" { | 	if _, _, err := d.getAccountByURI(ctx, requestUser, attributedTo); err != nil && status.AccountID == "" { | ||||||
| 		return nil, nil, gtserror.Newf("failed to dereference status author %s: %w", uri, err) | 		return nil, nil, gtserror.Newf("failed to dereference status author %s: %w", uri, err) | ||||||
| 	} | 	} | ||||||
|  | @ -408,13 +408,33 @@ func (d *Dereferencer) enrichStatus( | ||||||
| 		return nil, nil, gtserror.Newf("error converting statusable to gts model for status %s: %w", uri, err) | 		return nil, nil, gtserror.Newf("error converting statusable to gts model for status %s: %w", uri, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check if we've previously | 	// Ensure final status isn't attempting | ||||||
| 	// stored this status in the DB. | 	// to claim being authored by local user. | ||||||
| 	// If we have, it'll be ID'd. | 	if latestStatus.Account.IsLocal() { | ||||||
| 	var isNew = (status.ID == "") | 		return nil, nil, gtserror.Newf( | ||||||
| 	if isNew { | 			"dereferenced status %s claiming to be local", | ||||||
| 		// No ID, we haven't stored this status before. | 			latestStatus.URI, | ||||||
| 		// Generate new status ID from the status publication time. | 		) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Ensure the final parsed status URI / URL matches | ||||||
|  | 	// the input URI we fetched (or received) it as. | ||||||
|  | 	if expect := uri.String(); latestStatus.URI != expect && | ||||||
|  | 		latestStatus.URL != expect { | ||||||
|  | 		return nil, nil, gtserror.Newf( | ||||||
|  | 			"dereferenced status uri %s does not match %s", | ||||||
|  | 			latestStatus.URI, expect, | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var isNew bool | ||||||
|  | 
 | ||||||
|  | 	// Based on the original provided | ||||||
|  | 	// status model, determine whether | ||||||
|  | 	// this is a new insert / update. | ||||||
|  | 	if isNew = (status.ID == ""); isNew { | ||||||
|  | 
 | ||||||
|  | 		// Generate new status ID from the provided creation date. | ||||||
| 		latestStatus.ID, err = id.NewULIDFromTime(latestStatus.CreatedAt) | 		latestStatus.ID, err = id.NewULIDFromTime(latestStatus.CreatedAt) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err) | 			log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err) | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ package dereferencing_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
|  | @ -218,6 +219,28 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithImageAndNoContent() { | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *StatusTestSuite) TestDereferenceStatusWithNonMatchingURI() { | ||||||
|  | 	fetchingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	const ( | ||||||
|  | 		remoteURI    = "https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042" | ||||||
|  | 		remoteAltURI = "https://turnip.farm/users/turniphater420/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042" | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	// Create a copy of this remote account at alternative URI. | ||||||
|  | 	remoteStatus := suite.client.TestRemoteStatuses[remoteURI] | ||||||
|  | 	suite.client.TestRemoteStatuses[remoteAltURI] = remoteStatus | ||||||
|  | 
 | ||||||
|  | 	// Attempt to fetch account at alternative URI, it should fail! | ||||||
|  | 	fetchedStatus, _, err := suite.dereferencer.GetStatusByURI( | ||||||
|  | 		context.Background(), | ||||||
|  | 		fetchingAccount.Username, | ||||||
|  | 		testrig.URLMustParse(remoteAltURI), | ||||||
|  | 	) | ||||||
|  | 	suite.Equal(err.Error(), fmt.Sprintf("enrichStatus: dereferenced status uri %s does not match %s", remoteURI, remoteAltURI)) | ||||||
|  | 	suite.Nil(fetchedStatus) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestStatusTestSuite(t *testing.T) { | func TestStatusTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(StatusTestSuite)) | 	suite.Run(t, new(StatusTestSuite)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,70 +23,18 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" |  | ||||||
| 
 | 
 | ||||||
| 	errorsv2 "codeberg.org/gruf/go-errors/v2" | 	errorsv2 "codeberg.org/gruf/go-errors/v2" | ||||||
| 	"codeberg.org/gruf/go-kv" | 	"codeberg.org/gruf/go-kv" | ||||||
| 	"github.com/superseriousbusiness/activity/pub" | 	"github.com/superseriousbusiness/activity/pub" | ||||||
| 	"github.com/superseriousbusiness/activity/streams/vocab" | 	"github.com/superseriousbusiness/activity/streams/vocab" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
|  | 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // IsASMediaType will return whether the given content-type string |  | ||||||
| // matches one of the 2 possible ActivityStreams incoming content types: |  | ||||||
| // - application/activity+json |  | ||||||
| // - application/ld+json;profile=https://w3.org/ns/activitystreams |  | ||||||
| // |  | ||||||
| // Where for the above we are leniant with whitespace, quotes, and charset. |  | ||||||
| func IsASMediaType(ct string) bool { |  | ||||||
| 	var ( |  | ||||||
| 		// First content-type part, |  | ||||||
| 		// contains the application/... |  | ||||||
| 		p1 string = ct //nolint:revive |  | ||||||
| 
 |  | ||||||
| 		// Second content-type part, |  | ||||||
| 		// contains AS IRI or charset |  | ||||||
| 		// if provided. |  | ||||||
| 		p2 string |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	// Split content-type by semi-colon. |  | ||||||
| 	sep := strings.IndexByte(ct, ';') |  | ||||||
| 	if sep >= 0 { |  | ||||||
| 		p1 = ct[:sep] |  | ||||||
| 
 |  | ||||||
| 		// Trim all start/end |  | ||||||
| 		// space of second part. |  | ||||||
| 		p2 = ct[sep+1:] |  | ||||||
| 		p2 = strings.Trim(p2, " ") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Trim any ending space from the |  | ||||||
| 	// main content-type part of string. |  | ||||||
| 	p1 = strings.TrimRight(p1, " ") |  | ||||||
| 
 |  | ||||||
| 	switch p1 { |  | ||||||
| 	case "application/activity+json": |  | ||||||
| 		// Accept with or without charset. |  | ||||||
| 		// This should be case insensitive. |  | ||||||
| 		// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#charset |  | ||||||
| 		return p2 == "" || strings.EqualFold(p2, "charset=utf-8") |  | ||||||
| 
 |  | ||||||
| 	case "application/ld+json": |  | ||||||
| 		// Drop any quotes around the URI str. |  | ||||||
| 		p2 = strings.ReplaceAll(p2, "\"", "") |  | ||||||
| 
 |  | ||||||
| 		// End part must be a ref to the main AS namespace IRI. |  | ||||||
| 		return p2 == "profile=https://www.w3.org/ns/activitystreams" |  | ||||||
| 
 |  | ||||||
| 	default: |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // federatingActor wraps the pub.FederatingActor | // federatingActor wraps the pub.FederatingActor | ||||||
| // with some custom GoToSocial-specific logic. | // with some custom GoToSocial-specific logic. | ||||||
| type federatingActor struct { | type federatingActor struct { | ||||||
|  | @ -124,7 +72,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr | ||||||
| 
 | 
 | ||||||
| 	// Ensure valid ActivityPub Content-Type. | 	// Ensure valid ActivityPub Content-Type. | ||||||
| 	// https://www.w3.org/TR/activitypub/#server-to-server-interactions | 	// https://www.w3.org/TR/activitypub/#server-to-server-interactions | ||||||
| 	if ct := r.Header.Get("Content-Type"); !IsASMediaType(ct) { | 	if ct := r.Header.Get("Content-Type"); !apiutil.ASContentType(ct) { | ||||||
| 		const ct1 = "application/activity+json" | 		const ct1 = "application/activity+json" | ||||||
| 		const ct2 = "application/ld+json;profile=https://w3.org/ns/activitystreams" | 		const ct2 = "application/ld+json;profile=https://w3.org/ns/activitystreams" | ||||||
| 		err := fmt.Errorf("Content-Type %s not acceptable, this endpoint accepts: [%q %q]", ct, ct1, ct2) | 		err := fmt.Errorf("Content-Type %s not acceptable, this endpoint accepts: [%q %q]", ct, ct1, ct2) | ||||||
|  |  | ||||||
|  | @ -154,71 +154,3 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() { | ||||||
| func TestFederatingActorTestSuite(t *testing.T) { | func TestFederatingActorTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(FederatingActorTestSuite)) | 	suite.Run(t, new(FederatingActorTestSuite)) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func TestIsASMediaType(t *testing.T) { |  | ||||||
| 	for _, test := range []struct { |  | ||||||
| 		Input  string |  | ||||||
| 		Expect bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/activity+json", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/activity+json; charset=utf-8", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/activity+json;charset=utf-8", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/activity+json ;charset=utf-8", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/activity+json ; charset=utf-8", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json;profile=https://www.w3.org/ns/activitystreams", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json ;profile=https://www.w3.org/ns/activitystreams", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json ; profile=https://www.w3.org/ns/activitystreams", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json; profile=https://www.w3.org/ns/activitystreams", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", |  | ||||||
| 			Expect: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			Input:  "application/ld+json", |  | ||||||
| 			Expect: false, |  | ||||||
| 		}, |  | ||||||
| 	} { |  | ||||||
| 		if federation.IsASMediaType(test.Input) != test.Expect { |  | ||||||
| 			t.Errorf("did not get expected result %v for input: %s", test.Expect, test.Input) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -64,9 +64,16 @@ func (t *transport) Dereference(ctx context.Context, iri *url.URL) ([]byte, erro | ||||||
| 	} | 	} | ||||||
| 	defer rsp.Body.Close() | 	defer rsp.Body.Close() | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure a non-error status response. | ||||||
| 	if rsp.StatusCode != http.StatusOK { | 	if rsp.StatusCode != http.StatusOK { | ||||||
| 		return nil, gtserror.NewFromResponse(rsp) | 		return nil, gtserror.NewFromResponse(rsp) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure that the incoming request content-type is expected. | ||||||
|  | 	if ct := rsp.Header.Get("Content-Type"); !apiutil.ASContentType(ct) { | ||||||
|  | 		err := gtserror.Newf("non activity streams response: %s", ct) | ||||||
|  | 		return nil, gtserror.SetMalformed(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return io.ReadAll(rsp.Body) | 	return io.ReadAll(rsp.Body) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -101,10 +101,17 @@ func dereferenceByAPIV1Instance(ctx context.Context, t *transport, iri *url.URL) | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure a non-error status response. | ||||||
| 	if resp.StatusCode != http.StatusOK { | 	if resp.StatusCode != http.StatusOK { | ||||||
| 		return nil, gtserror.NewFromResponse(resp) | 		return nil, gtserror.NewFromResponse(resp) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure that the incoming request content-type is expected. | ||||||
|  | 	if ct := resp.Header.Get("Content-Type"); !apiutil.JSONContentType(ct) { | ||||||
|  | 		err := gtserror.Newf("non json response type: %s", ct) | ||||||
|  | 		return nil, gtserror.SetMalformed(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	b, err := io.ReadAll(resp.Body) | 	b, err := io.ReadAll(resp.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -251,20 +258,27 @@ func callNodeInfoWellKnown(ctx context.Context, t *transport, iri *url.URL) (*ur | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure a non-error status response. | ||||||
| 	if resp.StatusCode != http.StatusOK { | 	if resp.StatusCode != http.StatusOK { | ||||||
| 		return nil, gtserror.NewFromResponse(resp) | 		return nil, gtserror.NewFromResponse(resp) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure that the incoming request content-type is expected. | ||||||
|  | 	if ct := resp.Header.Get("Content-Type"); !apiutil.JSONContentType(ct) { | ||||||
|  | 		err := gtserror.Newf("non json response type: %s", ct) | ||||||
|  | 		return nil, gtserror.SetMalformed(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	b, err := io.ReadAll(resp.Body) | 	b, err := io.ReadAll(resp.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} else if len(b) == 0 { | 	} else if len(b) == 0 { | ||||||
| 		return nil, errors.New("callNodeInfoWellKnown: response bytes was len 0") | 		return nil, gtserror.New("response bytes was len 0") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	wellKnownResp := &apimodel.WellKnownResponse{} | 	wellKnownResp := &apimodel.WellKnownResponse{} | ||||||
| 	if err := json.Unmarshal(b, wellKnownResp); err != nil { | 	if err := json.Unmarshal(b, wellKnownResp); err != nil { | ||||||
| 		return nil, fmt.Errorf("callNodeInfoWellKnown: could not unmarshal server response as WellKnownResponse: %s", err) | 		return nil, gtserror.Newf("could not unmarshal server response as WellKnownResponse: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// look through the links for the first one that matches the nodeinfo schema, this is what we need | 	// look through the links for the first one that matches the nodeinfo schema, this is what we need | ||||||
|  | @ -275,11 +289,11 @@ func callNodeInfoWellKnown(ctx context.Context, t *transport, iri *url.URL) (*ur | ||||||
| 		} | 		} | ||||||
| 		nodeinfoHref, err = url.Parse(l.Href) | 		nodeinfoHref, err = url.Parse(l.Href) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("callNodeInfoWellKnown: couldn't parse url %s: %s", l.Href, err) | 			return nil, gtserror.Newf("couldn't parse url %s: %w", l.Href, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if nodeinfoHref == nil { | 	if nodeinfoHref == nil { | ||||||
| 		return nil, errors.New("callNodeInfoWellKnown: could not find nodeinfo rel in well known response") | 		return nil, gtserror.New("could not find nodeinfo rel in well known response") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nodeinfoHref, nil | 	return nodeinfoHref, nil | ||||||
|  | @ -302,20 +316,27 @@ func callNodeInfo(ctx context.Context, t *transport, iri *url.URL) (*apimodel.No | ||||||
| 	} | 	} | ||||||
| 	defer resp.Body.Close() | 	defer resp.Body.Close() | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure a non-error status response. | ||||||
| 	if resp.StatusCode != http.StatusOK { | 	if resp.StatusCode != http.StatusOK { | ||||||
| 		return nil, gtserror.NewFromResponse(resp) | 		return nil, gtserror.NewFromResponse(resp) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure that the incoming request content-type is expected. | ||||||
|  | 	if ct := resp.Header.Get("Content-Type"); !apiutil.NodeInfo2ContentType(ct) { | ||||||
|  | 		err := gtserror.Newf("non nodeinfo schema 2.0 response: %s", ct) | ||||||
|  | 		return nil, gtserror.SetMalformed(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	b, err := io.ReadAll(resp.Body) | 	b, err := io.ReadAll(resp.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} else if len(b) == 0 { | 	} else if len(b) == 0 { | ||||||
| 		return nil, errors.New("callNodeInfo: response bytes was len 0") | 		return nil, gtserror.New("response bytes was len 0") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	niResp := &apimodel.Nodeinfo{} | 	niResp := &apimodel.Nodeinfo{} | ||||||
| 	if err := json.Unmarshal(b, niResp); err != nil { | 	if err := json.Unmarshal(b, niResp); err != nil { | ||||||
| 		return nil, fmt.Errorf("callNodeInfo: could not unmarshal server response as Nodeinfo: %s", err) | 		return nil, gtserror.Newf("could not unmarshal server response as Nodeinfo: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return niResp, nil | 	return niResp, nil | ||||||
|  |  | ||||||
|  | @ -97,9 +97,17 @@ func (t *transport) Finger(ctx context.Context, targetUsername string, targetDom | ||||||
| 			// again here to renew the TTL | 			// again here to renew the TTL | ||||||
| 			t.controller.state.Caches.GTS.Webfinger().Set(targetDomain, url) | 			t.controller.state.Caches.GTS.Webfinger().Set(targetDomain, url) | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
| 		if rsp.StatusCode == http.StatusGone { | 		if rsp.StatusCode == http.StatusGone { | ||||||
| 			return nil, fmt.Errorf("account has been deleted/is gone") | 			return nil, fmt.Errorf("account has been deleted/is gone") | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		// Ensure that the incoming request content-type is expected. | ||||||
|  | 		if ct := rsp.Header.Get("Content-Type"); !apiutil.JSONJRDContentType(ct) { | ||||||
|  | 			err := gtserror.Newf("non webfinger type response: %s", ct) | ||||||
|  | 			return nil, gtserror.SetMalformed(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		return io.ReadAll(rsp.Body) | 		return io.ReadAll(rsp.Body) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -192,6 +200,12 @@ func (t *transport) webfingerFromHostMeta(ctx context.Context, targetDomain stri | ||||||
| 		return "", fmt.Errorf("GET request for %s failed: %s", req.URL.String(), rsp.Status) | 		return "", fmt.Errorf("GET request for %s failed: %s", req.URL.String(), rsp.Status) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Ensure that the incoming request content-type is expected. | ||||||
|  | 	if ct := rsp.Header.Get("Content-Type"); !apiutil.XMLXRDContentType(ct) { | ||||||
|  | 		err := gtserror.Newf("non host-meta type response: %s", ct) | ||||||
|  | 		return "", gtserror.SetMalformed(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	e := xml.NewDecoder(rsp.Body) | 	e := xml.NewDecoder(rsp.Body) | ||||||
| 	var hm apimodel.HostMeta | 	var hm apimodel.HostMeta | ||||||
| 	if err := e.Decode(&hm); err != nil { | 	if err := e.Decode(&hm); err != nil { | ||||||
|  |  | ||||||
|  | @ -245,9 +245,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat | ||||||
| 			StatusCode:    responseCode, | 			StatusCode:    responseCode, | ||||||
| 			Body:          readCloser, | 			Body:          readCloser, | ||||||
| 			ContentLength: int64(responseContentLength), | 			ContentLength: int64(responseContentLength), | ||||||
| 			Header: http.Header{ | 			Header:        http.Header{"Content-Type": {responseContentType}}, | ||||||
| 				"content-type": {responseContentType}, |  | ||||||
| 			}, |  | ||||||
| 		}, nil | 		}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue