Some checks failed
CI / test (push) Failing after 4s
Watches issue/PR comments and pushes for flagged phrases; warns commenters via reply and opens a redacted correction PR for flagged file content, never rewriting history or deleting content directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
106 lines
2.7 KiB
Go
106 lines
2.7 KiB
Go
// Package webhook is the HTTP transport layer: it verifies Forgejo's
|
|
// webhook signature, decodes the event payload, and dispatches to bot.Bot.
|
|
package webhook
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
|
|
"git.folja.dev/awayatan/yaju-is-always-watching-you/internal/forgejo"
|
|
)
|
|
|
|
// Dispatcher is satisfied by *bot.Bot.
|
|
type Dispatcher interface {
|
|
HandleIssueComment(forgejo.IssueCommentPayload) error
|
|
HandlePush(forgejo.PushPayload) error
|
|
}
|
|
|
|
type Handler struct {
|
|
secret []byte
|
|
dispatcher Dispatcher
|
|
}
|
|
|
|
func NewHandler(secret string, dispatcher Dispatcher) *Handler {
|
|
return &Handler{secret: []byte(secret), dispatcher: dispatcher}
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) // 10MiB cap
|
|
if err != nil {
|
|
http.Error(w, "read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if !h.validSignature(r, body) {
|
|
http.Error(w, "invalid signature", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
event := firstHeader(r, "X-Forgejo-Event", "X-Gitea-Event")
|
|
var dispatchErr error
|
|
switch event {
|
|
case "issue_comment":
|
|
var p forgejo.IssueCommentPayload
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
http.Error(w, "decode payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
dispatchErr = h.dispatcher.HandleIssueComment(p)
|
|
case "push":
|
|
var p forgejo.PushPayload
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
http.Error(w, "decode payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
dispatchErr = h.dispatcher.HandlePush(p)
|
|
default:
|
|
// Unhandled event types are not an error — the webhook may be
|
|
// subscribed to more events than this bot reacts to.
|
|
}
|
|
|
|
if dispatchErr != nil {
|
|
log.Printf("yaju-keisatsu: handling %s event: %v", event, dispatchErr)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// validSignature checks X-Forgejo-Signature (falling back to
|
|
// X-Gitea-Signature for webhooks configured in Gitea-compat mode): the hex
|
|
// HMAC-SHA256 of the raw body under the shared webhook secret.
|
|
func (h *Handler) validSignature(r *http.Request, body []byte) bool {
|
|
sig := firstHeader(r, "X-Forgejo-Signature", "X-Gitea-Signature")
|
|
if sig == "" {
|
|
return false
|
|
}
|
|
got, err := hex.DecodeString(sig)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
mac := hmac.New(sha256.New, h.secret)
|
|
mac.Write(body)
|
|
want := mac.Sum(nil)
|
|
|
|
return hmac.Equal(got, want)
|
|
}
|
|
|
|
func firstHeader(r *http.Request, names ...string) string {
|
|
for _, n := range names {
|
|
if v := r.Header.Get(n); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|