Mateusz Urbanek 403cd5a563
fix: centralize schematic ownership enforcement
Move ownership/auth checks from scattered frontend handlers into
  schematic.Factory.Get, which now accepts an OwnershipChecker. This
  eliminates duplicated checkOwnership methods across http and spdx
  frontends and ensures anonymous callers cannot probe schematic
  existence when auth is enabled.

  Also guard PXE credential embedding behind AuthProvider != nil so
  credentials are never propagated when auth is disabled.

Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
2026-04-21 15:12:24 +02:00

145 lines
3.9 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package http
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/blang/semver/v4"
"github.com/julienschmidt/httprouter"
"github.com/siderolabs/gen/xerrors"
"go.uber.org/zap"
"github.com/siderolabs/image-factory/internal/asset/cache"
"github.com/siderolabs/image-factory/internal/mime"
"github.com/siderolabs/image-factory/internal/profile"
"github.com/siderolabs/image-factory/pkg/enterprise"
)
// checksumSuffixes maps supported checksum file extensions to themselves.
var checksumSuffixes = map[string]struct{}{
".sha512": {},
".sha256": {},
}
// handleImage handles downloading of boot assets.
//
//nolint:gocyclo,cyclop
func (f *Frontend) handleImage(ctx context.Context, w http.ResponseWriter, r *http.Request, p httprouter.Params) error {
schematicID := p.ByName("schematic")
// If the request is coming from the external PXE URL we disable redirects.
disableRedirect := r.Host == f.options.ExternalPXEURL.Host
path := p.ByName("path")
// Detect a checksum suffix early: strip it and record which algorithm was
// requested so we compute a checksum instead of streaming the asset bytes.
// This check must happen before schematic/version lookup so that
// non-enterprise builds return 402 regardless of schematic availability.
var checksumSuffix string
for suffix := range checksumSuffixes {
if strings.HasSuffix(path, suffix) {
checksumSuffix = suffix
path = strings.TrimSuffix(path, suffix)
break
}
}
wantChecksum := checksumSuffix != ""
if wantChecksum && f.checksummer == nil {
return xerrors.NewTaggedf[enterprise.ErrNotEnabledTag]("enterprise not enabled: checksum endpoint is not available")
}
schematic, err := f.schematicFactory.Get(ctx, schematicID, f.options.AuthProvider)
if err != nil {
return err
}
versionTag := p.ByName("version")
if !strings.HasPrefix(versionTag, "v") {
versionTag = "v" + versionTag
}
version, err := semver.Parse(versionTag[1:])
if err != nil {
return fmt.Errorf("error parsing version: %w", err)
}
prof, err := profile.ParseFromPath(path, version.String())
if err != nil {
return fmt.Errorf("error parsing profile from path: %w", err)
}
prof, err = profile.EnhanceFromSchematic(ctx, prof, schematic, f.artifactsManager, f.secureBootService, versionTag)
if err != nil {
return fmt.Errorf("error enhancing profile from schematic: %w", err)
}
filename := path
if r.URL.Query().Get("filename") != "" {
filename = r.URL.Query().Get("filename")
f.logger.Info("using filename override", zap.String("filename", filename))
}
asset, err := f.assetBuilder.Build(ctx, prof, version.String(), path, filename)
if err != nil {
return err
}
// Checksum path: delegate to the enterprise checksummer.
if wantChecksum {
reader, readerErr := asset.Reader()
if readerErr != nil {
return readerErr
}
return f.checksummer.WriteChecksum(ctx, w, r, reader, asset.Size(), filename, checksumSuffix)
}
if asset, ok := asset.(cache.RedirectableAsset); ok && !disableRedirect && r.Method != http.MethodHead {
var url string
url, err = asset.Redirect(ctx, filename)
if err == nil {
http.Redirect(w, r, url, http.StatusFound)
return nil
}
f.logger.Warn("asset does not support redirection, serving directly", zap.Error(err))
}
w.Header().Set("Content-Length", strconv.FormatInt(asset.Size(), 10))
w.Header().Set("Content-Type", mime.ContentType(path))
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return nil
}
reader, err := asset.Reader()
if err != nil {
return err
}
defer reader.Close() //nolint:errcheck
_, err = io.Copy(w, reader)
return err
}