Commit e334fd6

bryfry <bryon@fryer.io>
2026-01-06 20:46:46
Phase 1: Implement basic HTTP proxy
Implemented a working HTTP proxy server that can forward requests to remote servers. This is the foundation for the gowarcprox project. Components implemented: - Configuration package with default values and CLI flag support - Proxy server with graceful startup and shutdown - HTTP handler that forwards requests and responses - CLI using cobra for argument parsing - Structured logging with slog Features: - HTTP proxy functionality (non-HTTPS) - Request/response forwarding with proper header handling - Client IP tracking via X-Forwarded-For - Hop-by-hop header filtering - Configurable socket timeouts - Graceful shutdown on SIGINT/SIGTERM Testing: Successfully tested with curl --proxy localhost:8000 http://example.com/ HTTPS support (CONNECT method) will be added in Phase 2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ab8a64f
Changed files (7)
cmd/gowarcprox/main.go
@@ -0,0 +1,157 @@
+package main
+
+import (
+	"fmt"
+	"log/slog"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/internetarchive/gowarcprox/internal/proxy"
+	"github.com/internetarchive/gowarcprox/pkg/config"
+	"github.com/spf13/cobra"
+)
+
+var (
+	cfg     *config.Config
+	version = "0.1.0"
+)
+
+func main() {
+	if err := rootCmd.Execute(); err != nil {
+		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+var rootCmd = &cobra.Command{
+	Use:   "gowarcprox",
+	Short: "gowarcprox - HTTP/HTTPS MITM proxy for web archiving",
+	Long: `gowarcprox is a WARC writing MITM HTTP/S proxy.
+It records HTTP/HTTPS traffic to WARC files for web archiving purposes.
+
+This is a Go rewrite of the Python warcprox project, leveraging the
+internetarchive/gowarc library for WARC file handling.`,
+	Version: version,
+	RunE:    run,
+}
+
+func init() {
+	cfg = config.NewDefaultConfig()
+
+	// Network flags
+	rootCmd.Flags().StringVarP(&cfg.Address, "address", "b", cfg.Address,
+		"Address to listen on")
+	rootCmd.Flags().IntVarP(&cfg.Port, "port", "p", cfg.Port,
+		"Port to listen on")
+	rootCmd.Flags().DurationVar(&cfg.SocketTimeout, "socket-timeout", cfg.SocketTimeout,
+		"Socket timeout")
+
+	// WARC output flags
+	rootCmd.Flags().StringVarP(&cfg.WARCDirectory, "directory", "d", cfg.WARCDirectory,
+		"WARC output directory")
+	rootCmd.Flags().StringVar(&cfg.WARCPrefix, "prefix", cfg.WARCPrefix,
+		"WARC filename prefix")
+	rootCmd.Flags().Int64Var(&cfg.WARCSize, "size", cfg.WARCSize,
+		"WARC file size limit in bytes")
+	rootCmd.Flags().BoolVarP(&cfg.Verbose, "gzip", "z", cfg.WARCCompression == "gzip",
+		"Compress WARC files with gzip")
+	rootCmd.Flags().StringVarP(&cfg.DigestAlgorithm, "digest", "g", cfg.DigestAlgorithm,
+		"Digest algorithm: sha1, sha256, blake3")
+	rootCmd.Flags().IntVar(&cfg.WARCWriterThreads, "warc-writer-threads", cfg.WARCWriterThreads,
+		"WARC writer pool size")
+
+	// HTTPS/Certificate flags
+	rootCmd.Flags().StringVarP(&cfg.CACertFile, "cacert", "c", cfg.CACertFile,
+		"CA certificate file")
+	rootCmd.Flags().StringVar(&cfg.CertsDir, "certs-dir", cfg.CertsDir,
+		"Directory for generated certificates")
+
+	// Deduplication flags
+	rootCmd.Flags().StringVar(&cfg.DedupDBFile, "dedup-db", cfg.DedupDBFile,
+		"Dedup database file")
+
+	// Statistics flags
+	rootCmd.Flags().StringVar(&cfg.StatsDBFile, "stats-db", cfg.StatsDBFile,
+		"Stats database file")
+
+	// Performance flags
+	rootCmd.Flags().IntVar(&cfg.MaxThreads, "max-threads", cfg.MaxThreads,
+		"Max proxy handler threads")
+	rootCmd.Flags().IntVar(&cfg.QueueSize, "queue-size", cfg.QueueSize,
+		"RecordedURL queue size")
+	rootCmd.Flags().IntVar(&cfg.TmpFileMaxMemory, "tmp-file-max-memory", cfg.TmpFileMaxMemory,
+		"Max memory before spooling to disk")
+	rootCmd.Flags().Int64Var(&cfg.MaxResourceSize, "max-resource-size", cfg.MaxResourceSize,
+		"Max resource size before truncation (0=unlimited)")
+
+	// Logging flags
+	rootCmd.Flags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose,
+		"Verbose logging")
+	rootCmd.Flags().StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel,
+		"Log level: debug, info, warn, error")
+}
+
+func run(cmd *cobra.Command, args []string) error {
+	// Setup logger
+	logger := setupLogger(cfg)
+
+	logger.Info("starting gowarcprox",
+		"version", version,
+		"address", cfg.Address,
+		"port", cfg.Port)
+
+	// Create the proxy server
+	server, err := proxy.NewServer(cfg, logger)
+	if err != nil {
+		return fmt.Errorf("failed to create server: %w", err)
+	}
+
+	// Start the server
+	if err := server.Start(); err != nil {
+		return fmt.Errorf("failed to start server: %w", err)
+	}
+
+	logger.Info("proxy server listening",
+		"address", server.Addr().String())
+
+	// Setup signal handling for graceful shutdown
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
+
+	// Wait for shutdown signal
+	sig := <-sigChan
+	logger.Info("received shutdown signal", "signal", sig.String())
+
+	// Stop the server
+	if err := server.Stop(); err != nil {
+		return fmt.Errorf("failed to stop server: %w", err)
+	}
+
+	logger.Info("gowarcprox shutdown complete")
+	return nil
+}
+
+func setupLogger(cfg *config.Config) *slog.Logger {
+	var level slog.Level
+
+	switch cfg.LogLevel {
+	case "debug":
+		level = slog.LevelDebug
+	case "info":
+		level = slog.LevelInfo
+	case "warn":
+		level = slog.LevelWarn
+	case "error":
+		level = slog.LevelError
+	default:
+		level = slog.LevelInfo
+	}
+
+	opts := &slog.HandlerOptions{
+		Level: level,
+	}
+
+	handler := slog.NewTextHandler(os.Stdout, opts)
+	return slog.New(handler)
+}
internal/proxy/handler.go
@@ -0,0 +1,196 @@
+package proxy
+
+import (
+	"fmt"
+	"io"
+	"log/slog"
+	"net/http"
+	"strings"
+
+	"github.com/internetarchive/gowarcprox/pkg/config"
+)
+
+// Handler handles HTTP proxy requests
+type Handler struct {
+	config *config.Config
+	client *http.Client
+	logger *slog.Logger
+}
+
+// NewHandler creates a new HTTP handler
+func NewHandler(cfg *config.Config, logger *slog.Logger) *Handler {
+	return &Handler{
+		config: cfg,
+		client: &http.Client{
+			// Don't follow redirects - let the client handle them
+			CheckRedirect: func(req *http.Request, via []*http.Request) error {
+				return http.ErrUseLastResponse
+			},
+			Timeout: cfg.SocketTimeout,
+		},
+		logger: logger,
+	}
+}
+
+// ServeHTTP handles incoming HTTP requests
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	h.logger.Debug("received request",
+		"method", r.Method,
+		"url", r.URL.String(),
+		"host", r.Host)
+
+	// Handle CONNECT for HTTPS (will be implemented in Phase 2)
+	if r.Method == http.MethodConnect {
+		h.handleConnect(w, r)
+		return
+	}
+
+	// Handle regular HTTP proxy requests
+	h.handleHTTP(w, r)
+}
+
+// handleConnect handles CONNECT requests for HTTPS tunneling
+// This is a stub for Phase 1 - will be fully implemented in Phase 2
+func (h *Handler) handleConnect(w http.ResponseWriter, r *http.Request) {
+	h.logger.Warn("CONNECT not yet supported (HTTPS tunneling coming in Phase 2)",
+		"host", r.Host)
+	http.Error(w, "CONNECT method not yet implemented", http.StatusNotImplemented)
+}
+
+// handleHTTP handles regular HTTP proxy requests
+func (h *Handler) handleHTTP(w http.ResponseWriter, r *http.Request) {
+	// For proxy requests, the URL should be absolute
+	// Make sure we have a valid URL
+	if r.URL.Scheme == "" {
+		h.logger.Error("invalid proxy request: missing scheme", "url", r.URL.String())
+		http.Error(w, "Bad Request: URL must be absolute for proxy requests", http.StatusBadRequest)
+		return
+	}
+
+	// Create a new request to the remote server
+	outReq, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
+	if err != nil {
+		h.logger.Error("failed to create outbound request", "error", err)
+		http.Error(w, "Bad Gateway", http.StatusBadGateway)
+		return
+	}
+
+	// Copy headers from original request
+	// Remove hop-by-hop headers
+	for name, values := range r.Header {
+		if isHopByHopHeader(name) {
+			continue
+		}
+		for _, value := range values {
+			outReq.Header.Add(name, value)
+		}
+	}
+
+	// Set X-Forwarded-For header
+	if clientIP := getClientIP(r); clientIP != "" {
+		prior := outReq.Header.Get("X-Forwarded-For")
+		if prior != "" {
+			outReq.Header.Set("X-Forwarded-For", prior+", "+clientIP)
+		} else {
+			outReq.Header.Set("X-Forwarded-For", clientIP)
+		}
+	}
+
+	// Send the request to the remote server
+	resp, err := h.client.Do(outReq)
+	if err != nil {
+		h.logger.Error("failed to fetch from remote server",
+			"url", r.URL.String(),
+			"error", err)
+		http.Error(w, "Bad Gateway", http.StatusBadGateway)
+		return
+	}
+	defer resp.Body.Close()
+
+	// Copy response headers
+	for name, values := range resp.Header {
+		if isHopByHopHeader(name) {
+			continue
+		}
+		for _, value := range values {
+			w.Header().Add(name, value)
+		}
+	}
+
+	// Write status code
+	w.WriteHeader(resp.StatusCode)
+
+	// Copy response body
+	written, err := io.Copy(w, resp.Body)
+	if err != nil {
+		h.logger.Error("failed to copy response body",
+			"url", r.URL.String(),
+			"error", err)
+		return
+	}
+
+	h.logger.Info("proxied request",
+		"method", r.Method,
+		"url", r.URL.String(),
+		"status", resp.StatusCode,
+		"bytes", written)
+}
+
+// isHopByHopHeader checks if a header is hop-by-hop
+// These headers should not be forwarded
+func isHopByHopHeader(name string) bool {
+	hopByHopHeaders := []string{
+		"Connection",
+		"Keep-Alive",
+		"Proxy-Authenticate",
+		"Proxy-Authorization",
+		"Te",
+		"Trailer",
+		"Transfer-Encoding",
+		"Upgrade",
+	}
+
+	nameLower := strings.ToLower(name)
+	for _, h := range hopByHopHeaders {
+		if strings.ToLower(h) == nameLower {
+			return true
+		}
+	}
+	return false
+}
+
+// getClientIP extracts the client IP address from the request
+func getClientIP(r *http.Request) string {
+	if ip := r.Header.Get("X-Real-IP"); ip != "" {
+		return ip
+	}
+	if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
+		// X-Forwarded-For can contain multiple IPs, get the first one
+		if idx := strings.Index(ip, ","); idx != -1 {
+			return strings.TrimSpace(ip[:idx])
+		}
+		return ip
+	}
+	// Fall back to RemoteAddr
+	if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
+		return r.RemoteAddr[:idx]
+	}
+	return r.RemoteAddr
+}
+
+// logRequest logs detailed request information for debugging
+func (h *Handler) logRequest(r *http.Request) {
+	h.logger.Debug("request details",
+		"method", r.Method,
+		"url", r.URL.String(),
+		"proto", r.Proto,
+		"host", r.Host,
+		"remote_addr", r.RemoteAddr,
+		"content_length", r.ContentLength)
+
+	for name, values := range r.Header {
+		for _, value := range values {
+			h.logger.Debug(fmt.Sprintf("header: %s: %s", name, value))
+		}
+	}
+}
internal/proxy/proxy.go
@@ -0,0 +1,112 @@
+package proxy
+
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"net"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/internetarchive/gowarcprox/pkg/config"
+)
+
+// Server represents the proxy server
+type Server struct {
+	config   *config.Config
+	listener net.Listener
+	server   *http.Server
+	handler  *Handler
+	wg       sync.WaitGroup
+	ctx      context.Context
+	cancel   context.CancelFunc
+	logger   *slog.Logger
+}
+
+// NewServer creates a new proxy server
+func NewServer(cfg *config.Config, logger *slog.Logger) (*Server, error) {
+	if logger == nil {
+		logger = slog.Default()
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	s := &Server{
+		config: cfg,
+		ctx:    ctx,
+		cancel: cancel,
+		logger: logger,
+	}
+
+	// Create the HTTP handler
+	s.handler = NewHandler(cfg, logger)
+
+	// Create the HTTP server
+	s.server = &http.Server{
+		Handler:      s.handler,
+		ReadTimeout:  cfg.SocketTimeout,
+		WriteTimeout: cfg.SocketTimeout,
+		IdleTimeout:  cfg.SocketTimeout,
+	}
+
+	return s, nil
+}
+
+// Start starts the proxy server
+func (s *Server) Start() error {
+	addr := fmt.Sprintf("%s:%d", s.config.Address, s.config.Port)
+
+	listener, err := net.Listen("tcp", addr)
+	if err != nil {
+		return fmt.Errorf("failed to listen on %s: %w", addr, err)
+	}
+	s.listener = listener
+
+	s.logger.Info("proxy server starting",
+		"address", s.config.Address,
+		"port", s.config.Port)
+
+	// Start accepting connections
+	s.wg.Add(1)
+	go func() {
+		defer s.wg.Done()
+		if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed {
+			s.logger.Error("server error", "error", err)
+		}
+	}()
+
+	return nil
+}
+
+// Stop gracefully stops the proxy server
+func (s *Server) Stop() error {
+	s.logger.Info("proxy server stopping")
+
+	// Cancel context to signal all goroutines to stop
+	s.cancel()
+
+	// Give the server a grace period to finish in-flight requests
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// Shutdown the HTTP server
+	if err := s.server.Shutdown(ctx); err != nil {
+		s.logger.Error("server shutdown error", "error", err)
+		return fmt.Errorf("server shutdown failed: %w", err)
+	}
+
+	// Wait for all goroutines to finish
+	s.wg.Wait()
+
+	s.logger.Info("proxy server stopped")
+	return nil
+}
+
+// Addr returns the address the server is listening on
+func (s *Server) Addr() net.Addr {
+	if s.listener != nil {
+		return s.listener.Addr()
+	}
+	return nil
+}
pkg/config/config.go
@@ -0,0 +1,74 @@
+package config
+
+import (
+	"time"
+)
+
+// Config holds the configuration for gowarcprox
+type Config struct {
+	// Network configuration
+	Address       string
+	Port          int
+	SocketTimeout time.Duration
+
+	// WARC output configuration
+	WARCDirectory      string
+	WARCPrefix         string
+	WARCSize           int64
+	WARCCompression    string // "gzip", "zstd", or ""
+	DigestAlgorithm    string // "sha1", "sha256", "blake3"
+	WARCWriterThreads  int
+
+	// HTTPS/Certificate configuration
+	CACertFile string
+	CertsDir   string
+
+	// Deduplication configuration
+	DedupEnabled bool
+	DedupDBFile  string
+
+	// Statistics configuration
+	StatsEnabled bool
+	StatsDBFile  string
+
+	// Performance configuration
+	MaxThreads             int
+	QueueSize              int
+	TmpFileMaxMemory       int
+	MaxResourceSize        int64
+	BatchFlushTimeout      time.Duration
+	BatchFlushMaxURLs      int
+
+	// Logging configuration
+	Verbose  bool
+	LogLevel string
+}
+
+// NewDefaultConfig returns a Config with default values
+func NewDefaultConfig() *Config {
+	return &Config{
+		Address:              "localhost",
+		Port:                 8000,
+		SocketTimeout:        60 * time.Second,
+		WARCDirectory:        "./warcs",
+		WARCPrefix:           "warcprox",
+		WARCSize:             1000000000, // 1GB
+		WARCCompression:      "gzip",
+		DigestAlgorithm:      "sha1",
+		WARCWriterThreads:    1,
+		CACertFile:           "warcprox-ca.pem",
+		CertsDir:             "./warcprox-ca",
+		DedupEnabled:         true,
+		DedupDBFile:          "warcprox.sqlite",
+		StatsEnabled:         true,
+		StatsDBFile:          "warcprox.sqlite",
+		MaxThreads:           100,
+		QueueSize:            1000,
+		TmpFileMaxMemory:     524288, // 512KB
+		MaxResourceSize:      0,      // unlimited
+		BatchFlushTimeout:    10 * time.Second,
+		BatchFlushMaxURLs:    500,
+		Verbose:              false,
+		LogLevel:             "info",
+	}
+}
.gitignore
@@ -1,3 +1,4 @@
 /src/
 /venv/
 /scratch/
+/gowarcprox
go.mod
@@ -0,0 +1,29 @@
+module github.com/internetarchive/gowarcprox
+
+go 1.25.5
+
+require (
+	github.com/andybalholm/brotli v1.1.1 // indirect
+	github.com/dolthub/maphash v0.1.0 // indirect
+	github.com/gammazero/deque v1.0.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/internetarchive/gowarc v0.8.96 // indirect
+	github.com/klauspost/compress v1.18.1 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
+	github.com/mattn/go-sqlite3 v1.14.33 // indirect
+	github.com/maypok86/otter v1.2.4 // indirect
+	github.com/miekg/dns v1.1.68 // indirect
+	github.com/refraction-networking/utls v1.8.1 // indirect
+	github.com/spf13/cobra v1.10.2 // indirect
+	github.com/spf13/pflag v1.0.9 // indirect
+	github.com/ulikunitz/xz v0.5.15 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/zeebo/blake3 v0.2.4 // indirect
+	golang.org/x/crypto v0.44.0 // indirect
+	golang.org/x/mod v0.24.0 // indirect
+	golang.org/x/net v0.47.0 // indirect
+	golang.org/x/sync v0.18.0 // indirect
+	golang.org/x/sys v0.38.0 // indirect
+	golang.org/x/tools v0.33.0 // indirect
+)
go.sum
@@ -0,0 +1,51 @@
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
+github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
+github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
+github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/internetarchive/gowarc v0.8.96 h1:MMw92JjOMscMByIGogvkfHrQTgTEGVvcfoPEw1/b84k=
+github.com/internetarchive/gowarc v0.8.96/go.mod h1:dB1LkgWMHl014TrTiEoiC/hDtT2yr4pHkPHemfsLp+I=
+github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
+github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
+github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
+github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
+github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
+github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
+github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
+github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
+github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
+github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
+github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
+golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=