Files
ipcr-forge/forge/hook.go
T
Worph 6fe4c40cbe IPCR Forge v3: Gitea as the source of truth, public repos mirrored to Radicle
Users found Radicle-as-source unfriendly (v1, v2). Gitea is now where
people work; its Actions build images into the staging registry IPCR
follows, as in v2.

- bridge/: ipcr-forge-bridge (Go). Install step mints a public-only
  token, registers one system webhook and creates the ipcr-hello example;
  the service mirrors every public repo one way to Radicle (rad init,
  canonical tags rule, git push rad) and serves the forge's page.
- Compose: gitea + act_runner on a separate dind; tile opens Gitea;
  tips link the forge page.
- Removed the Radicle CI broker (ci/) and the nginx landing page.
2026-10-04 18:34:26 +00:00

64 lines
1.9 KiB
Go

package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strings"
)
// hookHandler receives Gitea's system webhook. It only learns *which* repository changed: the sync
// itself re-reads everything from Gitea, so a payload is never trusted for content, and a burst of
// deliveries for one repository collapses into one sync.
type hookHandler struct {
secretFile string
enqueue func(id int64)
}
func (h *hookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
secret, _ := os.ReadFile(h.secretFile)
if len(strings.TrimSpace(string(secret))) == 0 {
http.Error(w, "not configured yet", http.StatusServiceUnavailable)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, 32<<20))
if err != nil {
http.Error(w, "read error", http.StatusBadRequest)
return
}
if !validSignature([]byte(strings.TrimSpace(string(secret))), body, r.Header.Get("X-Gitea-Signature")) {
log.Printf("hook: bad signature from %s (event %q)", r.RemoteAddr, r.Header.Get("X-Gitea-Event"))
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
var p struct {
Repository *struct {
ID int64 `json:"id"`
} `json:"repository"`
}
if err := json.Unmarshal(body, &p); err != nil || p.Repository == nil || p.Repository.ID == 0 {
// Signed, but about no repository (a user or organisation event): nothing to do.
w.WriteHeader(http.StatusNoContent)
return
}
h.enqueue(p.Repository.ID)
// Answered at once: Gitea gives a delivery 5 seconds, a sync can take minutes.
w.WriteHeader(http.StatusAccepted)
}
// validSignature checks Gitea's X-Gitea-Signature: hex HMAC-SHA256 of the raw body, no prefix.
func validSignature(secret, body []byte, sig string) bool {
got, err := hex.DecodeString(strings.TrimSpace(sig))
if err != nil || len(got) == 0 {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write(body)
return hmac.Equal(got, mac.Sum(nil))
}