Files
hakurei/internal/rosa/mirror.go
T
cat 76360979f9 internal/rosa: mirror response write methods
For the fs.FS implementation and static publishing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-07 20:18:18 +09:00

402 lines
9.4 KiB
Go

package rosa
import (
"compress/gzip"
"context"
"crypto/ed25519"
"crypto/sha512"
"errors"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"unique"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
// Remote is an authenticated cache mirror.
type Remote struct {
// Mirror URL.
url *url.URL
// Trusted public key.
pub ed25519.PublicKey
// For requests to the mirror.
c *http.Client
}
// NewRemote returns a populated [Remote].
func NewRemote(c *http.Client, base string, pub ed25519.PublicKey) (Remote, error) {
u, err := url.Parse(base)
return Remote{u, pub, c}, err
}
// get makes a [http.MethodGet] request and returns the response, or nil if
// the response StatusCode is [http.StatusNotFound].
func (r Remote) get(ctx context.Context, elem ...string) (*http.Response, error) {
if r.url == nil || len(r.pub) != ed25519.PublicKeySize || r.c == nil {
return nil, os.ErrInvalid
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
r.url.JoinPath(elem...).String(),
nil,
)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Rosa/1.1")
var resp *http.Response
if resp, err = r.c.Do(req); err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusOK:
return resp, nil
case http.StatusNotFound:
return nil, resp.Body.Close()
default:
_ = resp.Body.Close()
return nil, pkg.ResponseStatusError(resp.StatusCode)
}
}
const (
// dirArtifact holds signed artifact outcome checksums.
dirArtifact = "artifact"
// dirOutcome holds outcome archives by their checksum.
dirOutcome = "outcome"
// dirStatus holds signed status files.
dirStatus = "status"
)
// An OutcomeBadSizeError describes a mirror outcome with unexpected size.
type OutcomeBadSizeError struct {
Ident unique.Handle[pkg.ID]
Size int64
}
func (e OutcomeBadSizeError) Error() string {
if e.Size < 0 {
return "remote did not return outcome size for " +
pkg.Encode(e.Ident.Value())
}
return "outcome size " + strconv.FormatInt(e.Size, 10) +
" invalid for " + pkg.Encode(e.Ident.Value())
}
// An OutcomeAuthError describes a mirror outcome with invalid signature.
type OutcomeAuthError unique.Handle[pkg.ID]
func (e OutcomeAuthError) Error() string {
return "invalid outcome signature for " +
pkg.Encode(unique.Handle[pkg.ID](e).Value())
}
// Artifact fetches and authenticates an outcome.
func (r Remote) Artifact(
ctx context.Context,
id unique.Handle[pkg.ID],
) (*pkg.Checksum, error) {
if len(r.pub) != ed25519.PublicKeySize || r.c == nil {
return nil, os.ErrInvalid
}
resp, err := r.get(ctx, dirArtifact, pkg.Encode(id.Value()))
if err != nil || resp == nil {
return nil, err
}
var buf [ed25519.SignatureSize + 2*len(pkg.Checksum{})]byte
if resp.ContentLength != int64(len(buf)) {
_ = resp.Body.Close()
return nil, OutcomeBadSizeError{id, resp.ContentLength}
}
if _, err = io.ReadFull(resp.Body, buf[:]); err != nil {
return nil, errors.Join(err, resp.Body.Close())
} else if err = resp.Body.Close(); err != nil {
return nil, err
}
if !ed25519.Verify(
r.pub,
buf[ed25519.SignatureSize:],
buf[:ed25519.SignatureSize],
) {
return nil, OutcomeAuthError(id)
} else if unique.Make((pkg.ID)(buf[ed25519.SignatureSize:])) != id {
return nil, OutcomeAuthError(id)
}
return (*pkg.Checksum)(buf[ed25519.SignatureSize+len(pkg.Checksum{}):]), nil
}
// Checksum returns an artifact satisfying checksum.
func (r Remote) Checksum(checksum unique.Handle[pkg.Checksum]) pkg.Artifact {
return pkg.NewArchive(pkg.NewHTTPGet(
r.c,
r.url.JoinPath(dirOutcome, pkg.Encode(checksum.Value())).String(),
checksum.Value(),
))
}
// A StatusBadSizeError describes a mirror status with unexpected size.
type StatusBadSizeError unique.Handle[pkg.ID]
func (e StatusBadSizeError) Error() string {
return "status payload too short for " +
pkg.Encode(unique.Handle[pkg.ID](e).Value())
}
// A StatusAuthError describes a mirror status with invalid signature.
type StatusAuthError unique.Handle[pkg.ID]
func (e StatusAuthError) Error() string {
return "invalid status signature for " +
pkg.Encode(unique.Handle[pkg.ID](e).Value())
}
// Status authenticates the checksum of a status file and returns its
// corresponding measured reader.
func (r Remote) Status(
ctx *pkg.RContext,
id unique.Handle[pkg.ID],
) (io.ReadCloser, error) {
resp, err := r.get(ctx.Unwrap(), dirStatus, pkg.Encode(id.Value()))
if err != nil || resp == nil {
return nil, err
}
var buf [ed25519.SignatureSize + 2*len(pkg.Checksum{})]byte
if _, err = io.ReadFull(resp.Body, buf[:]); err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
err = StatusBadSizeError(id)
}
return nil, err
}
if !ed25519.Verify(
r.pub,
buf[ed25519.SignatureSize:],
buf[:ed25519.SignatureSize],
) {
_ = resp.Body.Close()
return nil, StatusAuthError(id)
} else if unique.Make((pkg.ID)(buf[ed25519.SignatureSize:])) != id {
return nil, StatusAuthError(id)
}
return ctx.NewMeasuredReader(
resp.Body,
unique.Make((pkg.Checksum)(buf[ed25519.SignatureSize+len(pkg.Checksum{}):])),
), nil
}
// Mirror services requests made by [Remote].
type Mirror struct {
// Cache on-filesystem format.
fsys fs.FS
// Signs payloads.
key ed25519.PrivateKey
// Verbose output.
msg message.Msg
}
// NewMirror returns a populated [Mirror] for servicing mirror requests.
func NewMirror(
msg message.Msg,
fsys fs.FS,
key ed25519.PrivateKey,
) Mirror {
return Mirror{fsys, key, msg}
}
// writeArtifact writes a signed dirArtifact response.
func (m Mirror) writeArtifact(
buf *[2 * len(pkg.Checksum{})]byte,
w io.Writer,
header http.Header,
) error {
ids := pkg.Encode((pkg.Checksum)(buf[:len(pkg.Checksum{})]))
if linkname, err := fs.ReadLink(m.fsys, filepath.Join(
"identifier",
ids,
)); err != nil {
return err
} else if err = pkg.Decode(
(*pkg.Checksum)(buf[len(pkg.Checksum{}):]),
filepath.Base(linkname),
); err != nil {
return err
}
m.msg.Verbosef("serving artifact %s", ids)
if header != nil {
header.Set(
"Content-Length",
strconv.Itoa(ed25519.SignatureSize+len(buf)),
)
}
_, err := w.Write(append(ed25519.Sign(m.key, buf[:]), buf[:]...))
return err
}
// writeOutcome writes a dirOutcome response.
func (m Mirror) writeOutcome(
buf *pkg.Checksum,
w io.Writer,
header http.Header,
) error {
checksums := pkg.Encode(*buf)
rel := filepath.Join("checksum", checksums)
if _, err := fs.Lstat(m.fsys, rel); err != nil {
return err
}
m.msg.Verbosef("serving outcome %s", checksums)
_fsys, err := fs.Sub(m.fsys, rel)
if err != nil {
return err
}
var gw *gzip.Writer
if gw, err = gzip.NewWriterLevel(w, gzip.BestCompression); err != nil {
return err
}
if header != nil {
header.Set("Content-Encoding", "gzip")
}
if err = pkg.Write(_fsys, ".", gw); err != nil {
_ = gw.Close()
return err
}
return gw.Close()
}
// writeStatus writes a signed dirStatus response.
func (m Mirror) writeStatus(
buf *[2 * len(pkg.Checksum{})]byte,
w io.Writer,
_ http.Header,
) error {
ids := pkg.Encode((pkg.Checksum)(buf[:len(pkg.Checksum{})]))
f, err := m.fsys.Open(filepath.Join(
"status",
ids,
))
if err != nil {
return err
}
s, ok := f.(io.Seeker)
if !ok {
return &fs.PathError{Op: "seek", Path: ids, Err: fs.ErrInvalid}
}
m.msg.Verbosef("serving status %s", ids)
h := sha512.New384()
if _, err = io.Copy(h, f); err != nil {
_ = f.Close()
return err
}
h.Sum(buf[len(pkg.Checksum{}):len(pkg.Checksum{})])
if _, err = w.Write(append(ed25519.Sign(m.key, buf[:]), buf[:]...)); err != nil {
_ = f.Close()
return err
} else if _, err = s.Seek(0, io.SeekStart); err != nil {
_ = f.Close()
return err
} else if _, err = io.Copy(w, f); err != nil {
_ = f.Close()
return err
}
return f.Close()
}
// writeF is a helper for writing a mirror response with error handling.
func writeF[B any](
m Mirror,
w http.ResponseWriter,
buf B,
f func(
buf B,
w io.Writer,
header http.Header,
) error,
) {
err := f(buf, w, w.Header())
if err == nil {
return
}
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
return
}
m.msg.GetLogger().Println(err)
w.WriteHeader(http.StatusInternalServerError)
}
// Register configures an [http.ServeMux] for servicing mirror requests.
func (m Mirror) Register(mux *http.ServeMux) {
const identName = "ident"
mux.HandleFunc("/"+dirArtifact+"/{"+identName+"}", func(
w http.ResponseWriter,
req *http.Request,
) {
var buf [2 * len(pkg.Checksum{})]byte
if err := pkg.Decode(
(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
req.PathValue(identName),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeF(m, w, &buf, m.writeArtifact)
})
mux.HandleFunc("/"+dirOutcome+"/{"+identName+"}", func(
w http.ResponseWriter,
req *http.Request,
) {
if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
w.WriteHeader(http.StatusNotAcceptable)
return
}
var buf pkg.Checksum
if err := pkg.Decode(
&buf,
req.PathValue(identName),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeF(m, w, &buf, m.writeOutcome)
})
mux.HandleFunc("/"+dirStatus+"/{"+identName+"}", func(
w http.ResponseWriter,
req *http.Request,
) {
var buf [2 * len(pkg.Checksum{})]byte
if err := pkg.Decode(
(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
req.PathValue(identName),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeF(m, w, &buf, m.writeStatus)
})
}