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.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user