mirror of
https://github.com/OliveTin/OliveTin
synced 2026-08-25 20:16:30 +00:00
188 lines
4.9 KiB
Go
188 lines
4.9 KiB
Go
package webhooks
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/OliveTin/OliveTin/internal/auth"
|
|
"github.com/OliveTin/OliveTin/internal/config"
|
|
"github.com/OliveTin/OliveTin/internal/executor"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type ActionWebhookConfig struct {
|
|
Action *config.Action
|
|
Config config.WebhookConfig
|
|
}
|
|
|
|
type WebhookHandler struct {
|
|
cfg *config.Config
|
|
executor *executor.Executor
|
|
}
|
|
|
|
func NewWebhookHandler(cfg *config.Config, ex *executor.Executor) *WebhookHandler {
|
|
return &WebhookHandler{
|
|
cfg: cfg,
|
|
executor: ex,
|
|
}
|
|
}
|
|
|
|
func (h *WebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
payload, err := h.readPayload(r)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
matchingActions := h.findMatchingActions(r, payload)
|
|
if len(matchingActions) == 0 {
|
|
h.writeOKResponse(w, "no matching webhook actions")
|
|
return
|
|
}
|
|
|
|
processed := h.processMatchingActions(matchingActions, r, payload)
|
|
log.WithFields(log.Fields{
|
|
"matched": len(matchingActions),
|
|
"processed": processed,
|
|
}).Infof("Webhook processed")
|
|
|
|
h.writeOKResponse(w, "webhook actions")
|
|
}
|
|
|
|
func (h *WebhookHandler) readPayload(r *http.Request) ([]byte, error) {
|
|
maxSize := int64(1024 * 1024)
|
|
payload, err := io.ReadAll(io.LimitReader(r.Body, maxSize))
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"error": err,
|
|
}).Warnf("Failed to read webhook payload")
|
|
return nil, err
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
func (h *WebhookHandler) writeOKResponse(w http.ResponseWriter, context string) {
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write([]byte("OK")); err != nil {
|
|
log.WithError(err).Warnf("Failed to write response for %s", context)
|
|
}
|
|
}
|
|
|
|
func (h *WebhookHandler) processMatchingActions(matchingActions []ActionWebhookConfig, r *http.Request, payload []byte) int {
|
|
processed := 0
|
|
for _, actionConfig := range matchingActions {
|
|
if h.processWebhook(actionConfig, r, payload) {
|
|
processed++
|
|
}
|
|
}
|
|
return processed
|
|
}
|
|
|
|
func (h *WebhookHandler) findMatchingActions(r *http.Request, payload []byte) []ActionWebhookConfig {
|
|
var matches []ActionWebhookConfig
|
|
|
|
for _, action := range h.cfg.Actions {
|
|
matches = append(matches, h.findMatchingWebhooksForAction(action, r, payload)...)
|
|
}
|
|
|
|
return matches
|
|
}
|
|
|
|
func (h *WebhookHandler) findMatchingWebhooksForAction(action *config.Action, r *http.Request, payload []byte) []ActionWebhookConfig {
|
|
var matches []ActionWebhookConfig
|
|
|
|
for _, webhookConfig := range action.ExecOnWebhook {
|
|
webhookConfigCopy := webhookConfig
|
|
|
|
if webhookConfigCopy.Template != "" {
|
|
ApplyGitHubTemplate(&webhookConfigCopy, webhookConfigCopy.Template)
|
|
}
|
|
|
|
matcher := NewWebhookMatcher(webhookConfigCopy, r, payload)
|
|
if matcher.Matches() {
|
|
matches = append(matches, ActionWebhookConfig{
|
|
Action: action,
|
|
Config: webhookConfigCopy,
|
|
})
|
|
}
|
|
}
|
|
|
|
return matches
|
|
}
|
|
|
|
func (h *WebhookHandler) processWebhook(actionConfig ActionWebhookConfig, r *http.Request, payload []byte) bool {
|
|
verifier := NewAuthVerifier(actionConfig.Config)
|
|
if !verifier.Verify(r, payload) {
|
|
log.WithFields(log.Fields{
|
|
"actionTitle": actionConfig.Action.Title,
|
|
"authType": actionConfig.Config.AuthType,
|
|
}).Warnf("Webhook authentication failed")
|
|
return false
|
|
}
|
|
|
|
matcher := NewWebhookMatcher(actionConfig.Config, r, payload)
|
|
|
|
args, err := matcher.ExtractArguments()
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"actionTitle": actionConfig.Action.Title,
|
|
"error": err,
|
|
}).Warnf("Failed to extract webhook arguments")
|
|
return false
|
|
}
|
|
|
|
justification, err := matcher.ExtractJustification()
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"actionTitle": actionConfig.Action.Title,
|
|
"error": err,
|
|
}).Warnf("Failed to extract webhook justification")
|
|
return false
|
|
}
|
|
|
|
h.executeAction(actionConfig.Action, args, justification)
|
|
return true
|
|
}
|
|
|
|
func (h *WebhookHandler) executeAction(action *config.Action, args map[string]string, justification string) {
|
|
binding := h.executor.FindBindingWithNoEntity(action)
|
|
if binding == nil {
|
|
log.WithFields(log.Fields{
|
|
"actionTitle": action.Title,
|
|
}).Warnf("Action binding not found, skipping execution")
|
|
return
|
|
}
|
|
|
|
definedArgs := filterToDefinedArguments(args, action)
|
|
req := &executor.ExecutionRequest{
|
|
Binding: binding,
|
|
Cfg: h.cfg,
|
|
Tags: []string{"webhook"},
|
|
Arguments: definedArgs,
|
|
Justification: justification,
|
|
AuthenticatedUser: auth.UserFromSystem(h.cfg, "webhook"),
|
|
}
|
|
|
|
h.executor.ExecRequest(req)
|
|
}
|
|
|
|
func filterToDefinedArguments(args map[string]string, action *config.Action) map[string]string {
|
|
definedNames := make(map[string]struct{})
|
|
for _, arg := range action.Arguments {
|
|
definedNames[arg.Name] = struct{}{}
|
|
}
|
|
filtered := make(map[string]string)
|
|
for k, v := range args {
|
|
if _, ok := definedNames[k]; ok {
|
|
filtered[k] = v
|
|
}
|
|
}
|
|
return filtered
|
|
}
|