Files
forge/routers/web/misc/sync.go
zeekayandHanzo Dev 9a07b4d089 legal: Hanzo AI, Inc. copyright above the upstream notices
2978 files. The fork is 2026-07-15 (rebrand: gitea.dev -> github.com/hanzoai/git),
so 2026 is the year, and "Copyright 2026 Hanzo AI, Inc." was already the form
used by the handful of files that had ours.

Shape is now:

	// Copyright 2026 Hanzo AI, Inc. All rights reserved.
	// Copyright 2015 The Gogs Authors. All rights reserved.
	// Copyright 2019 The Gitea Authors. All rights reserved.
	// SPDX-License-Identifier: MIT

Ours on top, theirs retained verbatim. Retained because MIT requires it, and
separately because they are true — those authors did write that code.

Not renamed to "The Git Authors": Git is a different project under a different
license, and putting its name here would attribute this code to people who have
nothing to do with it. Adding our line is the fix; overwriting theirs would have
been both a licence violation and a false statement.

gofmt still reports import-order drift in some of these files. It predates this
change (verified against a stash) and is left alone rather than burying a
one-line-per-file legal change under 3000 files of unrelated reordering.

Build clean.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 13:49:52 -07:00

115 lines
3.8 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"strings"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/setting"
mirror_service "github.com/hanzoai/git/services/mirror"
)
// maxSyncBody bounds the read: a push payload is a few KiB, and an unbounded
// read ahead of authentication is a memory DoS.
const maxSyncBody = 1 << 20
// Sync is the one entry point for "this repo changed upstream, take it now".
// It exists because an org-level webhook is a single static URL that cannot
// carry a token header; authenticated callers already have the software's
// per-repo mirror-sync APIs, so this adds no second door for them.
//
// Direction is a property of the repo, not of this route — a pull mirror
// fetches, and that fetch propagates to its push mirrors (services/mirror), so
// one upstream push reaches every platform we mirror to in one hop.
//
// Mounted ahead of the session middleware because the credential is the payload
// itself: HMAC-SHA256 over the exact bytes in X-Hub-Signature-256, keyed by
// [mirror] SYNC_SECRET. Fail-closed — an unset secret refuses everything.
//
// A verified event naming a repo we do not mirror is 200, not 404: a delivery
// log is not a repository-existence oracle, and red deliveries for repos we
// deliberately skip would train the reader to ignore red.
func Sync(w http.ResponseWriter, req *http.Request) {
secret := setting.Mirror.SyncSecret
if secret == "" {
http.Error(w, "sync is not configured", http.StatusServiceUnavailable)
return
}
body, err := io.ReadAll(io.LimitReader(req.Body, maxSyncBody))
if err != nil {
http.Error(w, "unreadable body", http.StatusBadRequest)
return
}
if !validSignature(secret, req.Header.Get("X-Hub-Signature-256"), body) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
ownerName, repoName, ok := repoFromPayload(body)
if !ok {
// An event naming no repository: a ping, or one we do not act on.
_, _ = w.Write([]byte("ok"))
return
}
repo, err := repo_model.GetRepositoryByOwnerAndName(req.Context(), ownerName, repoName)
if err != nil {
_, _ = w.Write([]byte("ok"))
return
}
// The mirror row is the single test for "has an upstream": a plain repo or
// a push-only repo has none and must never be queued for a fetch.
if _, err := repo_model.GetMirrorByRepoID(req.Context(), repo.ID); err != nil {
_, _ = w.Write([]byte("ok"))
return
}
mirror_service.AddPullMirrorToQueue(repo.ID)
log.Trace("sync: queued pull for %s", repo.FullName())
_, _ = w.Write([]byte("queued"))
}
// repoFromPayload reads the repository a push event names, a field every
// platform whose events we accept spells the same way.
func repoFromPayload(body []byte) (ownerName, repoName string, ok bool) {
var payload struct {
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return "", "", false
}
ownerName, repoName, ok = strings.Cut(payload.Repository.FullName, "/")
return ownerName, repoName, ok && ownerName != "" && repoName != ""
}
// validSignature reports whether sigHeader is the "sha256=<hex>" HMAC of body
// under secret. Constant-time; a malformed header is simply invalid.
func validSignature(secret, sigHeader string, body []byte) bool {
hexDigest, ok := strings.CutPrefix(sigHeader, "sha256=")
if !ok {
return false
}
sent, err := hex.DecodeString(hexDigest)
if err != nil {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return hmac.Equal(sent, mac.Sum(nil))
}