feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package aptrepo
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"server-deploy/internal/installplan"
|
||||
)
|
||||
|
||||
var metadataError = errors.New("invalid Docker repository metadata")
|
||||
|
||||
var pinnedPackageNames = [...]string{"docker-ce", "docker-ce-cli", "containerd.io", "docker-buildx-plugin", "docker-compose-plugin"}
|
||||
|
||||
// Resolve parses Release bytes already authenticated by the caller and binds
|
||||
// explicitly pinned packages to that Release. It does not verify signatures.
|
||||
func Resolve(release, index []byte, suite, arch string, versions map[string]string, now time.Time) (installplan.Lock, error) {
|
||||
if len(release) > 1<<20 || len(index) > 16<<20 || len(versions) != len(pinnedPackageNames) {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
for _, name := range pinnedPackageNames {
|
||||
if versions[name] == "" {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
}
|
||||
|
||||
var fields map[string]string
|
||||
if err := parseControl(release, func(stanza map[string]string) error {
|
||||
if fields != nil {
|
||||
return metadataError
|
||||
}
|
||||
fields = stanza
|
||||
return nil
|
||||
}); err != nil {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
if fields["suite"] != suite || fields["origin"] != "Docker" || fields["label"] != "Docker CE" || !hasWord(fields["architectures"], arch) || !hasWord(fields["components"], "stable") {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
date, err := time.Parse(time.RFC1123Z, fields["date"])
|
||||
if err != nil || date.After(now.Add(10*time.Minute)) || date.Before(now.Add(-30*24*time.Hour)) {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
if value, ok := fields["valid-until"]; ok {
|
||||
expiry, err := time.Parse(time.RFC1123Z, value)
|
||||
if err != nil || !expiry.After(now) || expiry.Before(date) {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
}
|
||||
// Bind the exact uncompressed index bytes before interpreting any records.
|
||||
indexSum := sha256.Sum256(index)
|
||||
expectedPath := "stable/binary-" + arch + "/Packages"
|
||||
seenPaths := make(map[string]bool)
|
||||
matched := false
|
||||
for _, line := range strings.Split(fields["sha256"], "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
entry := strings.Fields(line)
|
||||
if len(entry) != 3 || !validSHA256(entry[0]) || seenPaths[entry[2]] {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
seenPaths[entry[2]] = true
|
||||
size, err := decimalSize(entry[1])
|
||||
if err != nil {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
if entry[2] == expectedPath {
|
||||
if size != uint64(len(index)) || entry[0] != hex.EncodeToString(indexSum[:]) {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
|
||||
selected := make(map[string]installplan.Package)
|
||||
seenRecords := make(map[[3]string]bool)
|
||||
if err := parseControl(index, func(record map[string]string) error {
|
||||
identity := [3]string{record["package"], record["version"], record["architecture"]}
|
||||
if identity[0] != "" && identity[1] != "" && identity[2] != "" {
|
||||
if seenRecords[identity] {
|
||||
return metadataError
|
||||
}
|
||||
seenRecords[identity] = true
|
||||
}
|
||||
version, target := versions[identity[0]]
|
||||
if !target {
|
||||
return nil
|
||||
}
|
||||
for _, field := range []string{"package", "version", "architecture", "filename", "size", "sha256"} {
|
||||
if record[field] == "" || strings.Contains(record[field], "\n") {
|
||||
return metadataError
|
||||
}
|
||||
}
|
||||
if identity[1] != version || identity[2] != arch {
|
||||
return nil
|
||||
}
|
||||
size, err := decimalSize(record["size"])
|
||||
if err != nil {
|
||||
return metadataError
|
||||
}
|
||||
selected[identity[0]] = installplan.Package{
|
||||
Name: identity[0], Version: version, Filename: record["filename"],
|
||||
Size: size, Digest: "sha256:" + record["sha256"],
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
|
||||
releaseSum := sha256.Sum256(release)
|
||||
lock := installplan.Lock{
|
||||
ProtocolVersion: 1, Repository: "https://download.docker.com/linux/ubuntu",
|
||||
Suite: suite, Architecture: arch, ReleaseDigest: "sha256:" + hex.EncodeToString(releaseSum[:]),
|
||||
}
|
||||
for _, name := range pinnedPackageNames {
|
||||
p, ok := selected[name]
|
||||
if !ok {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
lock.Packages = append(lock.Packages, p)
|
||||
}
|
||||
if _, err := installplan.Validate(lock, suite, arch); err != nil {
|
||||
return installplan.Lock{}, metadataError
|
||||
}
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
// parseControl preserves continuation boundaries and rejects duplicate fields
|
||||
// before invoking visit. Processing one stanza at a time bounds retained values.
|
||||
func parseControl(raw []byte, visit func(map[string]string) error) error {
|
||||
if !utf8.Valid(raw) {
|
||||
return metadataError
|
||||
}
|
||||
text := strings.ReplaceAll(string(raw), "\r\n", "\n")
|
||||
for _, r := range text {
|
||||
if unicode.IsControl(r) && r != '\n' && r != '\t' {
|
||||
return metadataError
|
||||
}
|
||||
}
|
||||
fields := make(map[string]*strings.Builder)
|
||||
current := ""
|
||||
flush := func() error {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
stanza := make(map[string]string, len(fields))
|
||||
for name, value := range fields {
|
||||
stanza[name] = value.String()
|
||||
}
|
||||
if err := visit(stanza); err != nil {
|
||||
return err
|
||||
}
|
||||
fields = make(map[string]*strings.Builder)
|
||||
current = ""
|
||||
return nil
|
||||
}
|
||||
for line := range strings.SplitSeq(text, "\n") {
|
||||
if line == "" {
|
||||
if err := flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if line[0] == ' ' || line[0] == '\t' {
|
||||
if current == "" {
|
||||
return metadataError
|
||||
}
|
||||
fields[current].WriteByte('\n')
|
||||
fields[current].WriteString(strings.Trim(line, " \t"))
|
||||
continue
|
||||
}
|
||||
name, value, ok := strings.Cut(line, ":")
|
||||
if !ok || name == "" {
|
||||
return metadataError
|
||||
}
|
||||
for _, r := range name {
|
||||
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-') {
|
||||
return metadataError
|
||||
}
|
||||
}
|
||||
current = strings.ToLower(name)
|
||||
if _, exists := fields[current]; exists {
|
||||
return metadataError
|
||||
}
|
||||
fields[current] = &strings.Builder{}
|
||||
fields[current].WriteString(strings.Trim(value, " \t"))
|
||||
}
|
||||
return flush()
|
||||
}
|
||||
|
||||
func hasWord(value, word string) bool {
|
||||
for _, item := range strings.Fields(value) {
|
||||
if item == word {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func decimalSize(value string) (uint64, error) {
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return 0, metadataError
|
||||
}
|
||||
}
|
||||
size, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, metadataError
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func validSHA256(value string) bool {
|
||||
if len(value) != 64 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user