Files
server-deploy/internal/aptrepo/metadata.go
T

233 lines
6.4 KiB
Go

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
}