[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 <devnull@milas.dev>
This commit is contained in:
Milas Bowman 2024-02-19 05:18:17 -05:00 committed by GitHub
parent 2db115fa36
commit af1a26a68f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 16 additions and 8 deletions

View file

@ -136,7 +136,7 @@ func HTTPSignatureVerifier(ctx context.Context) httpsig.VerifierWithOptions {
// SetHTTPSignatureVerifier stores the given http signature verifier and returns the // SetHTTPSignatureVerifier stores the given http signature verifier and returns the
// wrapped context. See HTTPSignatureVerifier() for further information on the verifier value. // 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) return context.WithValue(ctx, httpSigVerifierKey, verifier)
} }

View file

@ -30,13 +30,13 @@
) )
// NewGETSigner returns a new httpsig.Signer instance initialized with GTS GET preferences. // 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) sig, _, err := httpsig.NewSigner(prefs, digestAlgo, getHeaders, httpsig.Signature, expiresIn)
return sig, err return sig, err
} }
// NewPOSTSigner returns a new httpsig.Signer instance initialized with GTS POST preferences. // 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) sig, _, err := httpsig.NewSigner(prefs, digestAlgo, postHeaders, httpsig.Signature, expiresIn)
return sig, err return sig, err
} }

View file

@ -84,8 +84,8 @@ type transport struct {
privkey crypto.PrivateKey privkey crypto.PrivateKey
signerExp time.Time signerExp time.Time
getSigner httpsig.Signer getSigner httpsig.SignerWithOptions
postSigner httpsig.Signer postSigner httpsig.SignerWithOptions
signerMu sync.Mutex signerMu sync.Mutex
} }
@ -97,7 +97,15 @@ func (t *transport) GET(r *http.Request) (*http.Response, error) {
ctx = gtscontext.SetOutgoingPublicKeyID(ctx, t.pubKeyID) ctx = gtscontext.SetOutgoingPublicKeyID(ctx, t.pubKeyID)
r = r.WithContext(ctx) // replace request ctx. r = r.WithContext(ctx) // replace request ctx.
r.Header.Set("User-Agent", t.controller.userAgent) 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) { 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. // 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) { return func(r *http.Request) (err error) {
t.safesign(func() { 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 return
} }