diff options
| author | 2023-05-04 12:28:50 +0200 | |
|---|---|---|
| committer | 2023-05-04 12:28:50 +0200 | |
| commit | 5027d0ced25f06c12208cd618cfbb83518610d79 (patch) | |
| tree | 5e215da7d4e6eaa5f8be7e1eead39b63b3037847 /internal | |
| parent | [bugfix] Rework notifs to use min_id for paging up (#1734) (diff) | |
| download | gotosocial-5027d0ced25f06c12208cd618cfbb83518610d79.tar.xz | |
[bugfix] Serve correct 'application/jrd+json' content type for webfinger requests (#1738)
* [bugfix] Return `application/jrd+json` from webfinger queries
* update finger req content-type
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/util/mime.go | 2 | ||||
| -rw-r--r-- | internal/api/util/negotiate.go | 9 | ||||
| -rw-r--r-- | internal/api/wellknown/webfinger/webfinger_test.go | 36 | ||||
| -rw-r--r-- | internal/api/wellknown/webfinger/webfingerget.go | 15 | ||||
| -rw-r--r-- | internal/api/wellknown/webfinger/webfingerget_test.go | 221 | ||||
| -rw-r--r-- | internal/transport/finger.go | 4 | 
6 files changed, 127 insertions, 160 deletions
diff --git a/internal/api/util/mime.go b/internal/api/util/mime.go index 89aa23741..edd0dcecf 100644 --- a/internal/api/util/mime.go +++ b/internal/api/util/mime.go @@ -20,7 +20,6 @@ package util  // MIME represents a mime-type.  type MIME string -// MIME type  const (  	AppJSON           MIME = `application/json`  	AppXML            MIME = `application/xml` @@ -28,6 +27,7 @@ const (  	AppRSSXML         MIME = `application/rss+xml`  	AppActivityJSON   MIME = `application/activity+json`  	AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` +	AppJRDJSON        MIME = `application/jrd+json` // https://www.rfc-editor.org/rfc/rfc7033#section-10.2  	AppForm           MIME = `application/x-www-form-urlencoded`  	MultipartForm     MIME = `multipart/form-data`  	TextXML           MIME = `text/xml` diff --git a/internal/api/util/negotiate.go b/internal/api/util/negotiate.go index 0889e0346..1a4df7c40 100644 --- a/internal/api/util/negotiate.go +++ b/internal/api/util/negotiate.go @@ -35,6 +35,15 @@ var JSONAcceptHeaders = []MIME{  	AppJSON,  } +// WebfingerJSONAcceptHeaders is a slice of offers that prefers the +// jrd+json content type, but will be chill and fall back to app/json. +// This is to be used specifically for webfinger responses. +// See https://www.rfc-editor.org/rfc/rfc7033#section-10.2 +var WebfingerJSONAcceptHeaders = []MIME{ +	AppJRDJSON, +	AppJSON, +} +  // HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will  // fall back to JSON if necessary. This is useful for error handling, since it can  // be used to serve a nice HTML page if the caller accepts that, or just JSON if not. diff --git a/internal/api/wellknown/webfinger/webfinger_test.go b/internal/api/wellknown/webfinger/webfinger_test.go index 1d7902708..26143942c 100644 --- a/internal/api/wellknown/webfinger/webfinger_test.go +++ b/internal/api/wellknown/webfinger/webfinger_test.go @@ -18,12 +18,7 @@  package webfinger_test  import ( -	"crypto/rand" -	"crypto/rsa" -	"time" -  	"github.com/stretchr/testify/suite" -	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/email" @@ -103,34 +98,3 @@ func (suite *WebfingerStandardTestSuite) TearDownTest() {  	testrig.StandardStorageTeardown(suite.storage)  	testrig.StopWorkers(&suite.state)  } - -func accountDomainAccount() *gtsmodel.Account { -	privateKey, err := rsa.GenerateKey(rand.Reader, 2048) -	if err != nil { -		panic(err) -	} -	publicKey := &privateKey.PublicKey - -	acct := >smodel.Account{ -		ID:                    "01FG1K8EA7SYHEC7V6XKVNC4ZA", -		CreatedAt:             time.Now(), -		UpdatedAt:             time.Now(), -		Username:              "aaaaa", -		Domain:                "", -		Privacy:               gtsmodel.VisibilityDefault, -		Language:              "en", -		URI:                   "http://gts.example.org/users/aaaaa", -		URL:                   "http://gts.example.org/@aaaaa", -		InboxURI:              "http://gts.example.org/users/aaaaa/inbox", -		OutboxURI:             "http://gts.example.org/users/aaaaa/outbox", -		FollowingURI:          "http://gts.example.org/users/aaaaa/following", -		FollowersURI:          "http://gts.example.org/users/aaaaa/followers", -		FeaturedCollectionURI: "http://gts.example.org/users/aaaaa/collections/featured", -		ActorType:             ap.ActorPerson, -		PrivateKey:            privateKey, -		PublicKey:             publicKey, -		PublicKeyURI:          "http://gts.example.org/users/aaaaa/main-key", -	} - -	return acct -} diff --git a/internal/api/wellknown/webfinger/webfingerget.go b/internal/api/wellknown/webfinger/webfingerget.go index 5cd0a7a35..02f366ea6 100644 --- a/internal/api/wellknown/webfinger/webfingerget.go +++ b/internal/api/wellknown/webfinger/webfingerget.go @@ -18,6 +18,7 @@  package webfinger  import ( +	"encoding/json"  	"errors"  	"fmt"  	"net/http" @@ -48,14 +49,14 @@ import (  //	- .well-known  //  //	produces: -//	- application/json +//	- application/jrd+json  //  //	responses:  //		'200':  //			schema:  //				"$ref": "#/definitions/wellKnownResponse"  func (m *Module) WebfingerGETRequest(c *gin.Context) { -	if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { +	if _, err := apiutil.NegotiateAccept(c, apiutil.WebfingerJSONAcceptHeaders...); err != nil {  		apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)  		return  	} @@ -86,5 +87,13 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) {  		return  	} -	c.JSON(http.StatusOK, resp) +	b, err := json.Marshal(resp) +	if err != nil { +		apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1) +		return +	} + +	// Always return "application/jrd+json" regardless of negotiated +	// format. See https://www.rfc-editor.org/rfc/rfc7033#section-10.2 +	c.Data(http.StatusOK, string(apiutil.AppJRDJSON), b)  } diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go index 36244dee0..5b1cc47dd 100644 --- a/internal/api/wellknown/webfinger/webfingerget_test.go +++ b/internal/api/wellknown/webfinger/webfingerget_test.go @@ -20,16 +20,21 @@ package webfinger_test  import (  	"bytes"  	"context" +	"crypto/rand" +	"crypto/rsa"  	"encoding/json"  	"fmt" -	"io/ioutil" +	"io"  	"net/http"  	"net/http/httptest"  	"testing"  	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"  	"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"  	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/processing"  	"github.com/superseriousbusiness/gotosocial/testrig"  ) @@ -38,31 +43,85 @@ type WebfingerGetTestSuite struct {  	WebfingerStandardTestSuite  } -func (suite *WebfingerGetTestSuite) TestFingerUser() { -	targetAccount := suite.testAccounts["local_account_1"] - -	// setup request -	host := config.GetHost() -	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, host) - +func (suite *WebfingerGetTestSuite) finger(requestPath string) string { +	// Set up the request.  	recorder := httptest.NewRecorder()  	ctx, _ := testrig.CreateGinTestContext(recorder, nil) -	ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting -	ctx.Request.Header.Set("accept", "application/json") +	ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) +	ctx.Request.Header.Set("accept", "application/jrd+json") -	// trigger the function being tested +	// Trigger the handler.  	suite.webfingerModule.WebfingerGETRequest(ctx) -	// check response -	suite.EqualValues(http.StatusOK, recorder.Code) - +	// Read the result + return it +	// as nicely indented JSON.  	result := recorder.Result()  	defer result.Body.Close() -	b, err := ioutil.ReadAll(result.Body) -	suite.NoError(err) -	dst := new(bytes.Buffer) -	err = json.Indent(dst, b, "", "  ") -	suite.NoError(err) + +	// Result should always use the +	// webfinger content-type. +	if ct := result.Header.Get("content-type"); ct != string(apiutil.AppJRDJSON) { +		suite.FailNow("", "expected content type %s, got %s", apiutil.AppJRDJSON, ct) +	} + +	b, err := io.ReadAll(result.Body) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	var dst bytes.Buffer +	if err := json.Indent(&dst, b, "", "  "); err != nil { +		suite.FailNow(err.Error()) +	} + +	return dst.String() +} + +func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDomain string) *gtsmodel.Account { +	// Reset suite structs + config +	// to new host + account domain. +	config.SetHost(host) +	config.SetAccountDomain(accountDomain) +	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(&suite.state), &suite.state, suite.emailSender) +	suite.webfingerModule = webfinger.New(suite.processor) + +	// Generate a new account for the +	// tester, which uses the new host. +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048) +	if err != nil { +		panic(err) +	} +	publicKey := &privateKey.PublicKey + +	targetAccount := >smodel.Account{ +		ID:                    "01FG1K8EA7SYHEC7V6XKVNC4ZA", +		Username:              "new_account_domain_user", +		Privacy:               gtsmodel.VisibilityDefault, +		URI:                   "http://" + host + "/users/new_account_domain_user", +		URL:                   "http://" + host + "/@new_account_domain_user", +		InboxURI:              "http://" + host + "/users/new_account_domain_user/inbox", +		OutboxURI:             "http://" + host + "/users/new_account_domain_user/outbox", +		FollowingURI:          "http://" + host + "/users/new_account_domain_user/following", +		FollowersURI:          "http://" + host + "/users/new_account_domain_user/followers", +		FeaturedCollectionURI: "http://" + host + "/users/new_account_domain_user/collections/featured", +		ActorType:             ap.ActorPerson, +		PrivateKey:            privateKey, +		PublicKey:             publicKey, +		PublicKeyURI:          "http://" + host + "/users/new_account_domain_user/main-key", +	} + +	if err := suite.db.PutAccount(context.Background(), targetAccount); err != nil { +		suite.FailNow(err.Error()) +	} + +	return targetAccount +} + +func (suite *WebfingerGetTestSuite) TestFingerUser() { +	targetAccount := suite.testAccounts["local_account_1"] +	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, config.GetHost()) + +	resp := suite.finger(requestPath)  	suite.Equal(`{    "subject": "acct:the_mighty_zork@localhost:8080",    "aliases": [ @@ -81,144 +140,68 @@ func (suite *WebfingerGetTestSuite) TestFingerUser() {        "href": "http://localhost:8080/users/the_mighty_zork"      }    ] -}`, dst.String()) +}`, resp)  }  func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHost() { -	config.SetHost("gts.example.org") -	config.SetAccountDomain("example.org") - -	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(&suite.state), &suite.state, suite.emailSender) -	suite.webfingerModule = webfinger.New(suite.processor) - -	targetAccount := accountDomainAccount() -	if err := suite.db.Put(context.Background(), targetAccount); err != nil { -		panic(err) -	} +	targetAccount := suite.funkifyAccountDomain("gts.example.org", "example.org") +	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, config.GetHost()) -	// setup request -	host := config.GetHost() -	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, host) - -	recorder := httptest.NewRecorder() -	ctx, _ := testrig.CreateGinTestContext(recorder, nil) -	ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting -	ctx.Request.Header.Set("accept", "application/json") - -	// trigger the function being tested -	suite.webfingerModule.WebfingerGETRequest(ctx) - -	// check response -	suite.EqualValues(http.StatusOK, recorder.Code) - -	result := recorder.Result() -	defer result.Body.Close() -	b, err := ioutil.ReadAll(result.Body) -	suite.NoError(err) -	dst := new(bytes.Buffer) -	err = json.Indent(dst, b, "", "  ") -	suite.NoError(err) +	resp := suite.finger(requestPath)  	suite.Equal(`{ -  "subject": "acct:aaaaa@example.org", +  "subject": "acct:new_account_domain_user@example.org",    "aliases": [ -    "http://gts.example.org/users/aaaaa", -    "http://gts.example.org/@aaaaa" +    "http://gts.example.org/users/new_account_domain_user", +    "http://gts.example.org/@new_account_domain_user"    ],    "links": [      {        "rel": "http://webfinger.net/rel/profile-page",        "type": "text/html", -      "href": "http://gts.example.org/@aaaaa" +      "href": "http://gts.example.org/@new_account_domain_user"      },      {        "rel": "self",        "type": "application/activity+json", -      "href": "http://gts.example.org/users/aaaaa" +      "href": "http://gts.example.org/users/new_account_domain_user"      }    ] -}`, dst.String()) +}`, resp)  }  func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByAccountDomain() { -	config.SetHost("gts.example.org") -	config.SetAccountDomain("example.org") +	targetAccount := suite.funkifyAccountDomain("gts.example.org", "example.org") +	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, config.GetAccountDomain()) -	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(&suite.state), &suite.state, suite.emailSender) -	suite.webfingerModule = webfinger.New(suite.processor) - -	targetAccount := accountDomainAccount() -	if err := suite.db.Put(context.Background(), targetAccount); err != nil { -		panic(err) -	} - -	// setup request -	accountDomain := config.GetAccountDomain() -	requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, accountDomain) - -	recorder := httptest.NewRecorder() -	ctx, _ := testrig.CreateGinTestContext(recorder, nil) -	ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting -	ctx.Request.Header.Set("accept", "application/json") - -	// trigger the function being tested -	suite.webfingerModule.WebfingerGETRequest(ctx) - -	// check response -	suite.EqualValues(http.StatusOK, recorder.Code) - -	result := recorder.Result() -	defer result.Body.Close() -	b, err := ioutil.ReadAll(result.Body) -	suite.NoError(err) -	dst := new(bytes.Buffer) -	err = json.Indent(dst, b, "", "  ") -	suite.NoError(err) +	resp := suite.finger(requestPath)  	suite.Equal(`{ -  "subject": "acct:aaaaa@example.org", +  "subject": "acct:new_account_domain_user@example.org",    "aliases": [ -    "http://gts.example.org/users/aaaaa", -    "http://gts.example.org/@aaaaa" +    "http://gts.example.org/users/new_account_domain_user", +    "http://gts.example.org/@new_account_domain_user"    ],    "links": [      {        "rel": "http://webfinger.net/rel/profile-page",        "type": "text/html", -      "href": "http://gts.example.org/@aaaaa" +      "href": "http://gts.example.org/@new_account_domain_user"      },      {        "rel": "self",        "type": "application/activity+json", -      "href": "http://gts.example.org/users/aaaaa" +      "href": "http://gts.example.org/users/new_account_domain_user"      }    ] -}`, dst.String()) +}`, resp)  }  func (suite *WebfingerGetTestSuite) TestFingerUserWithoutAcct() { +	// Leave out the 'acct:' part in the request path; +	// the handler should be generous + still work OK.  	targetAccount := suite.testAccounts["local_account_1"] +	requestPath := fmt.Sprintf("/%s?resource=%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, config.GetHost()) -	// setup request -- leave out the 'acct:' prefix, which is prettymuch what pixelfed currently does -	host := config.GetHost() -	requestPath := fmt.Sprintf("/%s?resource=%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, host) - -	recorder := httptest.NewRecorder() -	ctx, _ := testrig.CreateGinTestContext(recorder, nil) -	ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting -	ctx.Request.Header.Set("accept", "application/json") - -	// trigger the function being tested -	suite.webfingerModule.WebfingerGETRequest(ctx) - -	// check response -	suite.EqualValues(http.StatusOK, recorder.Code) - -	result := recorder.Result() -	defer result.Body.Close() -	b, err := ioutil.ReadAll(result.Body) -	suite.NoError(err) -	dst := new(bytes.Buffer) -	err = json.Indent(dst, b, "", "  ") -	suite.NoError(err) +	resp := suite.finger(requestPath)  	suite.Equal(`{    "subject": "acct:the_mighty_zork@localhost:8080",    "aliases": [ @@ -237,7 +220,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithoutAcct() {        "href": "http://localhost:8080/users/the_mighty_zork"      }    ] -}`, dst.String()) +}`, resp)  }  func TestWebfingerGetTestSuite(t *testing.T) { diff --git a/internal/transport/finger.go b/internal/transport/finger.go index 4c3cacd7d..f106019b5 100644 --- a/internal/transport/finger.go +++ b/internal/transport/finger.go @@ -59,8 +59,10 @@ func prepWebfingerReq(ctx context.Context, loc, domain, username string) (*http.  	value := url.QueryEscape("acct:" + username + "@" + domain)  	req.URL.RawQuery = "resource=" + value +	// Prefer application/jrd+json, fall back to application/json. +	// See https://www.rfc-editor.org/rfc/rfc7033#section-10.2. +	req.Header.Add("Accept", string(apiutil.AppJRDJSON))  	req.Header.Add("Accept", string(apiutil.AppJSON)) -	req.Header.Add("Accept", "application/jrd+json")  	req.Header.Set("Host", req.URL.Host)  	return req, nil  | 
