Cache issued tgz in memory for one-time download

Instead of streaming the tgz directly to the response (which was
fragile under server write timeouts), handleIssueCert now:
- Builds the tgz into a bytes.Buffer
- Stores it in a sync.Map (tgzCache) under a random 16-byte hex token
- Redirects the browser to /pki/download/{token}

handleTGZDownload serves the cached bytes via LoadAndDelete, so the
archive is removed from memory after the first (and only) download.
An unknown or already-used token returns 404.

Also adds TestHandleTGZDownload covering the one-time-use and
not-found cases, and wires issueCertFn into mockVault.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-03-15 13:44:32 -07:00
parent 4deb469a9d
commit 4469c650cc
3 changed files with 112 additions and 19 deletions

View File

@@ -2,7 +2,10 @@ package webserver
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
@@ -40,6 +43,7 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
r.Post("/import-root", ws.requireAuth(ws.handleImportRoot))
r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer))
r.Post("/issue", ws.requireAuth(ws.handleIssueCert))
r.Get("/download/{token}", ws.requireAuth(ws.handleTGZDownload))
r.Get("/issuer/{issuer}", ws.requireAuth(ws.handleIssuerDetail))
r.Get("/cert/{serial}", ws.requireAuth(ws.handleCertDetail))
r.Get("/cert/{serial}/download", ws.requireAuth(ws.handleCertDownload))
@@ -479,11 +483,6 @@ func (ws *WebServer) handleIssuerDetail(w http.ResponseWriter, r *http.Request)
}
func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
// Disable the server-wide write deadline for this handler: it streams a
// tgz response only after several serial gRPC calls, which can easily
// consume the 30 s WriteTimeout before we start writing. We set our own
// 60 s deadline just before the write phase below.
_ = http.NewResponseController(w).SetWriteDeadline(time.Time{})
info := tokenInfoFromContext(r.Context())
token := extractCookie(r)
@@ -538,17 +537,11 @@ func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
return
}
// Stream a tgz archive containing the private key (PKCS8) and certificate.
// Extend the write deadline before streaming so that slow gRPC backends
// don't consume the server WriteTimeout before we start writing.
rc := http.NewResponseController(w)
_ = rc.SetWriteDeadline(time.Now().Add(60 * time.Second))
filename := issuedCert.Serial + ".tgz"
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
gw := gzip.NewWriter(w)
// Build the tgz archive in memory, store it in the cache, then redirect
// the browser to the one-time download URL so the archive is only served
// once and then discarded.
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
writeTarFile := func(name string, data []byte) error {
@@ -566,16 +559,48 @@ func (ws *WebServer) handleIssueCert(w http.ResponseWriter, r *http.Request) {
}
if err := writeTarFile("key.pem", []byte(issuedCert.KeyPEM)); err != nil {
ws.logger.Error("write key to tgz", "error", err)
ws.logger.Error("build tgz key", "error", err)
http.Error(w, "failed to build archive", http.StatusInternalServerError)
return
}
if err := writeTarFile("cert.pem", []byte(issuedCert.CertPEM)); err != nil {
ws.logger.Error("write cert to tgz", "error", err)
ws.logger.Error("build tgz cert", "error", err)
http.Error(w, "failed to build archive", http.StatusInternalServerError)
return
}
_ = tw.Close()
_ = gw.Close()
// Generate a random one-time token for the download URL.
var raw [16]byte
if _, err := rand.Read(raw[:]); err != nil {
ws.logger.Error("generate download token", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
dlToken := hex.EncodeToString(raw[:])
ws.tgzCache.Store(dlToken, &tgzEntry{
filename: issuedCert.Serial + ".tgz",
data: buf.Bytes(),
})
http.Redirect(w, r, "/pki/download/"+dlToken, http.StatusSeeOther)
}
func (ws *WebServer) handleTGZDownload(w http.ResponseWriter, r *http.Request) {
dlToken := chi.URLParam(r, "token")
val, ok := ws.tgzCache.LoadAndDelete(dlToken)
if !ok {
http.Error(w, "download not found or already used", http.StatusNotFound)
return
}
entry := val.(*tgzEntry)
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", "attachment; filename=\""+entry.filename+"\"")
_, _ = w.Write(entry.data)
}
func (ws *WebServer) handleCertDetail(w http.ResponseWriter, r *http.Request) {