170 lines
4.3 KiB
Go
170 lines
4.3 KiB
Go
package webhooks
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/OliveTin/OliveTin/internal/config"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type AuthVerifier struct {
|
|
config config.WebhookConfig
|
|
}
|
|
|
|
func NewAuthVerifier(cfg config.WebhookConfig) *AuthVerifier {
|
|
return &AuthVerifier{config: cfg}
|
|
}
|
|
|
|
func (v *AuthVerifier) Verify(r *http.Request, payload []byte) bool {
|
|
verifier := v.getVerifier()
|
|
if verifier == nil {
|
|
return v.handleUnknownAuthType()
|
|
}
|
|
return verifier(r, payload)
|
|
}
|
|
|
|
type authVerifierFunc func(*http.Request, []byte) bool
|
|
|
|
func (v *AuthVerifier) getVerifier() authVerifierFunc {
|
|
if v.config.AuthType == "" || v.config.AuthType == "none" {
|
|
return func(_ *http.Request, _ []byte) bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
verifierMap := v.buildVerifierMap()
|
|
if verifier, ok := verifierMap[v.config.AuthType]; ok {
|
|
return verifier
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *AuthVerifier) buildVerifierMap() map[string]authVerifierFunc {
|
|
return map[string]authVerifierFunc{
|
|
"hmac-sha256": v.verifyHMAC256,
|
|
"hmac-sha1": v.verifyHMAC1,
|
|
"bearer": func(r *http.Request, _ []byte) bool {
|
|
return v.verifyBearer(r)
|
|
},
|
|
"basic": func(r *http.Request, _ []byte) bool {
|
|
return v.verifyBasic(r)
|
|
},
|
|
}
|
|
}
|
|
|
|
func (v *AuthVerifier) handleUnknownAuthType() bool {
|
|
log.WithFields(log.Fields{
|
|
"authType": v.config.AuthType,
|
|
}).Warnf("Unknown auth type, rejecting")
|
|
return false
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyHMAC256(r *http.Request, payload []byte) bool {
|
|
if v.config.Secret == "" {
|
|
log.Warnf("HMAC-SHA256 auth requires secret")
|
|
return false
|
|
}
|
|
|
|
headerName := v.config.AuthHeader
|
|
if headerName == "" {
|
|
headerName = "X-Webhook-Signature"
|
|
}
|
|
|
|
signature := r.Header.Get(headerName)
|
|
if signature == "" {
|
|
log.Debugf("Missing signature header: %s", headerName)
|
|
return false
|
|
}
|
|
|
|
expectedSig := strings.TrimPrefix(signature, "sha256=")
|
|
|
|
mac := hmac.New(sha256.New, []byte(v.config.Secret))
|
|
mac.Write(payload)
|
|
computedSig := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
return hmac.Equal([]byte(expectedSig), []byte(computedSig))
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyHMAC1(r *http.Request, payload []byte) bool {
|
|
if v.config.Secret == "" {
|
|
log.Warnf("HMAC-SHA1 auth requires secret")
|
|
return false
|
|
}
|
|
|
|
headerName := v.config.AuthHeader
|
|
if headerName == "" {
|
|
headerName = "X-Webhook-Signature"
|
|
}
|
|
|
|
signature := r.Header.Get(headerName)
|
|
if signature == "" {
|
|
log.Debugf("Missing signature header: %s", headerName)
|
|
return false
|
|
}
|
|
|
|
expectedSig := strings.TrimPrefix(signature, "sha1=")
|
|
|
|
mac := hmac.New(sha1.New, []byte(v.config.Secret))
|
|
mac.Write(payload)
|
|
computedSig := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
return hmac.Equal([]byte(expectedSig), []byte(computedSig))
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyBearer(r *http.Request) bool {
|
|
if v.config.Secret == "" {
|
|
log.Warnf("Bearer auth requires secret")
|
|
return false
|
|
}
|
|
|
|
authHeader := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
|
log.Debugf("Missing or invalid Bearer token")
|
|
return false
|
|
}
|
|
|
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
|
tokenBytes := []byte(token)
|
|
secretBytes := []byte(v.config.Secret)
|
|
return len(tokenBytes) == len(secretBytes) && subtle.ConstantTimeCompare(tokenBytes, secretBytes) == 1
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyBasic(r *http.Request) bool {
|
|
if v.config.Secret == "" {
|
|
log.Warnf("Basic auth requires secret")
|
|
return false
|
|
}
|
|
|
|
username, password, ok := r.BasicAuth()
|
|
if !ok {
|
|
log.Debugf("Missing Basic auth header")
|
|
return false
|
|
}
|
|
|
|
return v.verifyBasicCredentials(username, password)
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyBasicCredentials(username, password string) bool {
|
|
parts := strings.SplitN(v.config.Secret, ":", 2)
|
|
if len(parts) == 2 {
|
|
return v.verifyBasicWithUsername(username, password, parts[0], parts[1])
|
|
}
|
|
return v.verifyBasicPasswordOnly(password)
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyBasicWithUsername(username, password, expectedUsername, expectedPassword string) bool {
|
|
usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername))
|
|
passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword))
|
|
return usernameMatch == 1 && passwordMatch == 1
|
|
}
|
|
|
|
func (v *AuthVerifier) verifyBasicPasswordOnly(password string) bool {
|
|
return subtle.ConstantTimeCompare([]byte(password), []byte(v.config.Secret)) == 1
|
|
}
|