internal/webhook/webhook.go
raw ยท 2767 bytes
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"github.com/tgckpg/flatgit/internal/config"
)
type Enqueuer interface {
Enqueue(string) bool
}
type Handler struct {
Config *config.Config
Queue Enqueuer
Logger *slog.Logger
MaxBody int64
}
type payload struct {
Ref string `json:"ref"`
Repository struct {
Name string `json:"name"`
FullName string `json:"full_name"`
CloneURL string `json:"clone_url"`
HTMLURL string `json:"html_url"`
} `json:"repository"`
}
func (h *Handler) Register(mux *http.ServeMux) {
mux.HandleFunc("/webhook/gitea", h.ServeHTTP)
mux.HandleFunc("/webhook/github", h.ServeHTTP)
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.MaxBody <= 0 {
h.MaxBody = 1 << 20
}
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, h.MaxBody))
if err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}
if !verifySignature(h.Config.Webhook.Secret, body, r) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
var p payload
if err := json.Unmarshal(body, &p); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
fullName := firstNonEmpty(p.Repository.FullName, p.Repository.Name)
if fullName == "" {
http.Error(w, "missing repository name", http.StatusBadRequest)
return
}
repo, ok := h.Config.RepoByWebhookName(fullName)
if !ok {
http.Error(w, fmt.Sprintf("repo not configured: %s", fullName), http.StatusNotFound)
return
}
queued := h.Queue.Enqueue(repo.Name)
if h.Logger != nil {
h.Logger.Info("webhook received", "repo", repo.FullName(), "ref", p.Ref, "queued", queued)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
_, _ = fmt.Fprintf(w, `{"ok":true,"queued":%t,"repo":%q}`+"\n", queued, repo.FullName())
}
func verifySignature(secret string, body []byte, r *http.Request) bool {
if secret == "" {
return true
}
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write(body)
expected := mac.Sum(nil)
github := r.Header.Get("X-Hub-Signature-256")
if strings.HasPrefix(github, "sha256=") {
got, err := hex.DecodeString(strings.TrimPrefix(github, "sha256="))
return err == nil && hmac.Equal(got, expected)
}
gitea := r.Header.Get("X-Gitea-Signature")
if gitea != "" {
got, err := hex.DecodeString(gitea)
return err == nil && hmac.Equal(got, expected)
}
return false
}
func firstNonEmpty(v ...string) string {
for _, s := range v {
if strings.TrimSpace(s) != "" {
return strings.TrimSpace(s)
}
}
return ""
}