From af1a26a68f9c912616eeb56bc8a4f52ba2210ae2 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Mon, 19 Feb 2024 05:18:17 -0500 Subject: [PATCH] [feature] Add Mastodon-compatible HTTP signature fallback (#2659) On outgoing `GET` requests that are signed (e.g. authorized fetch), if the initial request fails with `401`, try again, but _without_ the query parameters included in the HTTP signature. This is primarily useful for compatibility with Mastodon; though hopefully this can be removed in the not-too-distant future, as they've started changing their behavior here. Signed-off-by: Milas Bowman --- internal/gtscontext/context.go | 2 +- internal/transport/signing.go | 4 ++-- internal/transport/transport.go | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/gtscontext/context.go b/internal/gtscontext/context.go index 0d5ed5340..73b82744a 100644 --- a/internal/gtscontext/context.go +++ b/internal/gtscontext/context.go @@ -136,7 +136,7 @@ func HTTPSignatureVerifier(ctx context.Context) httpsig.VerifierWithOptions { // SetHTTPSignatureVerifier stores the given http signature verifier and returns the // wrapped context. See HTTPSignatureVerifier() for further information on the verifier value. -func SetHTTPSignatureVerifier(ctx context.Context, verifier httpsig.Verifier) context.Context { +func SetHTTPSignatureVerifier(ctx context.Context, verifier httpsig.VerifierWithOptions) context.Context { return context.WithValue(ctx, httpSigVerifierKey, verifier) } diff --git a/internal/transport/signing.go b/internal/transport/signing.go index dcd8e206f..e33e4a05f 100644 --- a/internal/transport/signing.go +++ b/internal/transport/signing.go @@ -30,13 +30,13 @@ var ( ) // NewGETSigner returns a new httpsig.Signer instance initialized with GTS GET preferences. -func NewGETSigner(expiresIn int64) (httpsig.Signer, error) { +func NewGETSigner(expiresIn int64) (httpsig.SignerWithOptions, error) { sig, _, err := httpsig.NewSigner(prefs, digestAlgo, getHeaders, httpsig.Signature, expiresIn) return sig, err } // NewPOSTSigner returns a new httpsig.Signer instance initialized with GTS POST preferences. -func NewPOSTSigner(expiresIn int64) (httpsig.Signer, error) { +func NewPOSTSigner(expiresIn int64) (httpsig.SignerWithOptions, error) { sig, _, err := httpsig.NewSigner(prefs, digestAlgo, postHeaders, httpsig.Signature, expiresIn) return sig, err } diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 558e187f0..a2a9dc23c 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -84,8 +84,8 @@ type transport struct { privkey crypto.PrivateKey signerExp time.Time - getSigner httpsig.Signer - postSigner httpsig.Signer + getSigner httpsig.SignerWithOptions + postSigner httpsig.SignerWithOptions signerMu sync.Mutex } @@ -97,7 +97,15 @@ func (t *transport) GET(r *http.Request) (*http.Response, error) { ctx = gtscontext.SetOutgoingPublicKeyID(ctx, t.pubKeyID) r = r.WithContext(ctx) // replace request ctx. r.Header.Set("User-Agent", t.controller.userAgent) - return t.controller.client.DoSigned(r, t.signGET()) + + resp, err := t.controller.client.DoSigned(r, t.signGET(httpsig.SignatureOption{ExcludeQueryStringFromPathPseudoHeader: false})) + if err != nil || resp.StatusCode != http.StatusUnauthorized { + return resp, err + } + + // try again without the path included in the HTTP signature for better compatibility + _ = resp.Body.Close() + return t.controller.client.DoSigned(r, t.signGET(httpsig.SignatureOption{ExcludeQueryStringFromPathPseudoHeader: true})) } func (t *transport) POST(r *http.Request, body []byte) (*http.Response, error) { @@ -112,10 +120,10 @@ func (t *transport) POST(r *http.Request, body []byte) (*http.Response, error) { } // signGET will safely sign an HTTP GET request. -func (t *transport) signGET() httpclient.SignFunc { +func (t *transport) signGET(opts httpsig.SignatureOption) httpclient.SignFunc { return func(r *http.Request) (err error) { t.safesign(func() { - err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, r, nil) + err = t.getSigner.SignRequestWithOptions(t.privkey, t.pubKeyID, r, nil, opts) }) return }