gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

signaturecheck.go (4439B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package middleware
     19 
     20 import (
     21 	"context"
     22 	"net/http"
     23 	"net/url"
     24 
     25 	"github.com/superseriousbusiness/gotosocial/internal/db"
     26 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
     27 	"github.com/superseriousbusiness/gotosocial/internal/log"
     28 
     29 	"github.com/gin-gonic/gin"
     30 	"github.com/go-fed/httpsig"
     31 )
     32 
     33 const (
     34 	sigHeader  = string(httpsig.Signature)
     35 	authHeader = string(httpsig.Authorization)
     36 	// untyped error returned by httpsig when no signature is present
     37 	noSigError = "neither \"" + sigHeader + "\" nor \"" + authHeader + "\" have signature parameters"
     38 )
     39 
     40 // SignatureCheck returns a gin middleware for checking http signatures.
     41 //
     42 // The middleware first checks whether an incoming http request has been
     43 // http-signed with a well-formed signature. If so, it will check if the
     44 // domain that signed the request is permitted to access the server, using
     45 // the provided uriBlocked function. If the domain is blocked, the middleware
     46 // will abort the request chain with http code 403 forbidden. If it is not
     47 // blocked, the handler will set the key verifier and the signature in the
     48 // context for use down the line.
     49 //
     50 // In case of an error, the request will be aborted with http code 500.
     51 func SignatureCheck(uriBlocked func(context.Context, *url.URL) (bool, db.Error)) func(*gin.Context) {
     52 	return func(c *gin.Context) {
     53 		ctx := c.Request.Context()
     54 
     55 		// Create the signature verifier from the request;
     56 		// this will error if the request wasn't signed.
     57 		verifier, err := httpsig.NewVerifier(c.Request)
     58 		if err != nil {
     59 			// Only actually *abort* the request with 401
     60 			// if a signature was present but malformed.
     61 			// Otherwise proceed with an unsigned request;
     62 			// it's up to other functions to reject this.
     63 			if err.Error() != noSigError {
     64 				log.Debugf(ctx, "http signature was present but invalid: %s", err)
     65 				c.AbortWithStatus(http.StatusUnauthorized)
     66 			}
     67 
     68 			return
     69 		}
     70 
     71 		// The request was signed! The key ID should be given
     72 		// in the signature so that we know where to fetch it
     73 		// from the remote server. This will be something like:
     74 		// https://example.org/users/some_remote_user#main-key
     75 		pubKeyIDStr := verifier.KeyId()
     76 
     77 		// Key can sometimes be nil, according to url parse
     78 		// func: 'Trying to parse a hostname and path without
     79 		// a scheme is invalid but may not necessarily return
     80 		// an error, due to parsing ambiguities'. Catch this.
     81 		pubKeyID, err := url.Parse(pubKeyIDStr)
     82 		if err != nil || pubKeyID == nil {
     83 			log.Warnf(ctx, "pubkey id %s could not be parsed as a url", pubKeyIDStr)
     84 			c.AbortWithStatus(http.StatusUnauthorized)
     85 			return
     86 		}
     87 
     88 		// If the domain is blocked we want to bail as fast as
     89 		// possible without the request proceeding further.
     90 		blocked, err := uriBlocked(ctx, pubKeyID)
     91 		if err != nil {
     92 			log.Errorf(ctx, "error checking block for domain %s: %s", pubKeyID.Host, err)
     93 			c.AbortWithStatus(http.StatusInternalServerError)
     94 			return
     95 		}
     96 
     97 		if blocked {
     98 			log.Infof(ctx, "domain %s is blocked", pubKeyID.Host)
     99 			c.AbortWithStatus(http.StatusForbidden)
    100 			return
    101 		}
    102 
    103 		// Assume signature was set on Signature header,
    104 		// but fall back to Authorization header if necessary.
    105 		signature := c.GetHeader(sigHeader)
    106 		if signature == "" {
    107 			signature = c.GetHeader(authHeader)
    108 		}
    109 
    110 		// Set relevant values on the request context
    111 		// to save some work further down the line.
    112 		ctx = gtscontext.SetHTTPSignatureVerifier(ctx, verifier)
    113 		ctx = gtscontext.SetHTTPSignature(ctx, signature)
    114 		ctx = gtscontext.SetHTTPSignaturePubKeyID(ctx, pubKeyID)
    115 
    116 		// Replace request with a shallow
    117 		// copy with the new context.
    118 		c.Request = c.Request.WithContext(ctx)
    119 	}
    120 }