Commit e334fd6
Changed files (7)
cmd
gowarcprox
internal
proxy
pkg
config
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=