Skip to content
quarry Artifact [ebba84ee61]
Login

Artifact [ebba84ee61]

Artifact ebba84ee614a7bae5c2c765b1be530b6a5ef191675ed742af0e40d5d703ab715:


package xfer

import "log"

// Claim is the forge's answer to a request to hold a project code.
type Claim int

const (
	// ClaimOK: the repository now holds the code.
	ClaimOK Claim = iota
	// ClaimTaken: another repository holds it.
	ClaimTaken
	// ClaimUnsure: it could not be shown that none does. Refused.
	ClaimUnsure
)

// Adoption is what the front needs from the forge to let an empty
// repository take the project code of the first project pushed into it.
//
// A store `zit init` made has a project code of its own, and a sync
// login is signed with SHA1(projectCode/user/token), so a push from a
// repository with any other code never verified: the forge derived the
// expected secret from the store's code and the client signed with its
// own. With `zit serve --adopt-project` the child accepts the first push
// into an EMPTY store whatever its code; this lets the login through to
// it, and only that far.
//
// Nothing here is asked until a login has verified, with the token, under
// the code the card names, and the account holds the check-in letter: a
// caller who proved nothing starts no work, so a repository that does not
// exist, one that is private and one that is empty are asked about
// identically, which is not at all.
type Adoption struct {
	// Code is the project code the forge holds for the repository, the
	// one its logins are derived from. A lookup in memory.
	Code func(repo string) (string, bool)
	// Empty reports whether the repository holds no check-in, from the
	// forge's own index and without running anything. False when unsure.
	// A lagging index can only say "empty" too long, and zit refuses the
	// push itself when the store is not.
	Empty func(repo string) bool
	// Recorded is the project code kept in the repository's row, "" if
	// none: what a claim leaves there until the store has answered.
	Recorded func(repo string) string
	// Claim gives repo the code if no other repository has it, in one
	// step. It fails closed.
	Claim func(repo, code string) Claim
	// Adopted reads the store's own project code and makes every cache
	// of it, and the repository's row, say so; a claim that did not come
	// true is given back by it. It reports whether it succeeded, and
	// retries by itself when it did not.
	Adopted func(repo string) bool
}

// SetAdoption turns first-push adoption on, which also makes every
// child start with --adopt-project: pass it only when the zit on PATH
// takes that option (zitcli.AdoptProjectVersion), as an older one
// rejects it and would not start.
func (f *Fronter) SetAdoption(a *Adoption) { f.adopt = a }

// AdoptAuth is an Auth that can derive a repository's credentials as if
// its project code were another one. Without it adoption is off.
type AdoptAuth interface {
	SecretsAt(repo, user, code string) []Credential
}

// What a caller who has proved who they are is told when their push
// cannot be adopted. Only ever said after a login verified with the
// token under the card's code and the account holds the check-in letter.
const (
	adoptTakenSentence  = "not accepted: this project is already hosted on this forge, so it cannot also be pushed into a new repository; clone or mirror the existing one"
	adoptUnsureSentence = "not accepted: this forge cannot yet tell whether this project is already hosted here; try again shortly"
)

// ValidProjectCode is validProjectCode, for tests outside the package.
func ValidProjectCode(code string) bool { return validProjectCode(code) }

// validProjectCode is the shape zit adopts: a full artifact hash, 40
// (SHA-1) or 64 (SHA-3) lowercase hex digits. Anything else is never
// verified against, so an empty, short or odd code derives nothing.
func validProjectCode(code string) bool {
	if len(code) != 40 && len(code) != 64 {
		return false
	}
	for _, c := range code {
		if !('0' <= c && c <= '9' || 'a' <= c && c <= 'f') {
			return false
		}
	}
	return true
}

// cardCode is the project code the request's login may ALSO be verified
// against, or "": the request pushes, its push cards agree on one
// well-formed code, and it is not the code the forge holds. Strings
// only: no lookup beyond memory, no process, nothing that depends on
// whether the caller is anybody.
func (f *Fronter) cardCode(name string, pr pushRequest) string {
	a := f.adopt
	if a == nil || f.auth == nil || !pr.hasPush || pr.codeMixed || !validProjectCode(pr.projectCode) {
		return ""
	}
	if _, ok := f.auth.(AdoptAuth); !ok {
		return ""
	}
	if held, ok := a.Code(name); ok && held == pr.projectCode {
		return ""
	}
	return pr.projectCode
}

// cardSecrets is the credentials the card's code derives, or none.
func (f *Fronter) cardSecrets(repo, user, code string) []Credential {
	if code == "" {
		return nil
	}
	if aa, ok := f.auth.(AdoptAuth); ok {
		return aa.SecretsAt(repo, user, code)
	}
	return nil
}

// adoption is the verdict on a push whose login verified under the
// card's code and whose account may check in.
type adoption struct {
	// admit: the push goes to the child, which is told to adopt.
	admit bool
	// claimed: the repository's row holds the code until Adopted runs.
	claimed bool
	// refusal is what to say, "" for the one unverified sentence.
	refusal string
}

// judgeAdoption decides whether a push of project code to repo, from a
// caller already proved, is let through. In order:
//
//  1. A row that records this very code, with no claim in flight on it,
//     means an earlier adoption landed and its refresh did not: the store is read, the caches are put
//     right, and the push is then an ordinary one.
//  2. The forge's index must show the repository empty. The caller who
//     fails this is refused as for any unverified card.
//  3. The code is claimed, in one step, against every other repository.
//
// The child is the last line: it refuses a different project from a
// store that holds anything.
func (f *Fronter) judgeAdoption(name, code string) adoption {
	a := f.adopt
	if !f.inFlight(name) && a.Recorded(name) == code {
		if a.Adopted(name) {
			if held, ok := a.Code(name); ok && held == code {
				return adoption{admit: true}
			}
		}
	}
	if !a.Empty(name) {
		return adoption{}
	}
	switch a.Claim(name, code) {
	case ClaimOK:
		return adoption{admit: true, claimed: true}
	case ClaimTaken:
		log.Printf("xfer: %s: first push not adopted: another repository has its project code", name)
		return adoption{refusal: adoptTakenSentence}
	default:
		log.Printf("xfer: %s: first push not adopted: could not show its project code is unused", name)
		return adoption{refusal: adoptUnsureSentence}
	}
}

// adopted reads back what the child did with a push that was let in to
// adopt a project code, before its reply reaches the pusher, whose next
// request is judged by the caches this puts right. It reports whether
// there was anything to do and it was done.
func (f *Fronter) adopted(name, adopting string) bool {
	if adopting == "" || f.adopt == nil {
		return false
	}
	return f.adopt.Adopted(name)
}

// flight counts, per repository, the pushes let in on a claim that the
// child has not yet answered. A row that holds a live claim is not
// "healed" by another pusher's request, or the claim would be undone
// under the push that holds it and another repository could take the code.
func (f *Fronter) flight(name string, delta int) {
	f.adoptMu.Lock()
	defer f.adoptMu.Unlock()
	if f.adoptFlight == nil {
		f.adoptFlight = map[string]int{}
	}
	f.adoptFlight[name] += delta
	if f.adoptFlight[name] <= 0 {
		delete(f.adoptFlight, name)
	}
}

func (f *Fronter) inFlight(name string) bool {
	f.adoptMu.Lock()
	defer f.adoptMu.Unlock()
	return f.adoptFlight[name] > 0
}