// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package main import ( "context" "errors" "fmt" "io" "log" "net" "net/http" "os" "os/signal" "strconv" "syscall" "time" "github.com/creachadair/command" "github.com/creachadair/gocache" "github.com/creachadair/taskgroup" ) var flags struct { CacheDir string `flag:"cache-dir,default=$GOCACHE_DIR,Local cache directory (required)"` S3Bucket string `flag:"bucket,default=$GOCACHE_S3_BUCKET,S3 bucket name (required)"` S3Region string `flag:"region,default=$GOCACHE_S3_REGION,S3 region"` S3Endpoint string `flag:"s3-endpoint-url,default=$GOCACHE_S3_ENDPOINT_URL,S3 custom endpoint URL (if unset, use AWS default)"` S3PathStyle bool `flag:"s3-path-style,default=$GOCACHE_S3_PATH_STYLE,S3 path-style URLs (optional)"` KeyPrefix string `flag:"prefix,default=$GOCACHE_KEY_PREFIX,S3 key prefix (optional)"` MinUploadSize int64 `flag:"min-upload-size,default=$GOCACHE_MIN_SIZE,Minimum object size to upload to S3 (in bytes)"` Concurrency int `flag:"c,default=$GOCACHE_CONCURRENCY,Maximum number of concurrent requests"` S3Concurrency int `flag:"u,default=$GOCACHE_S3_CONCURRENCY,Maximum concurrency for upload to S3"` PrintMetrics bool `flag:"metrics,default=$GOCACHE_METRICS,Print summary metrics to stderr at exit"` Expiration time.Duration `flag:"expiry,default=$GOCACHE_EXPIRY,Cache expiration period (optional)"` Verbose bool `flag:"v,default=$GOCACHE_VERBOSE,Enable verbose logging"` DebugLog int `flag:"debug,default=$GOCACHE_DEBUG,Enable detailed per-request debug logging (noisy)"` } const ( debugBuildCache = 1 << iota debugModProxy debugRevProxy ) // runDirect runs a cache communicating on stdin/stdout, for use as a direct // GOCACHEPROG plugin. func runDirect(env *command.Env) error { s, _, err := initCacheServer(env) if err != nil { return err } if err := s.Run(env.Context(), os.Stdin, os.Stdout); err != nil { return fmt.Errorf("cache server exited with error: %w", err) } if flags.Verbose || flags.PrintMetrics { fmt.Fprintln(os.Stderr, s.Metrics()) } return nil } var serveFlags struct { Plugin int `flag:"plugin,default=$GOCACHE_PLUGIN,Plugin service port (required)"` HTTP string `flag:"http,default=$GOCACHE_HTTP,HTTP service address ([host]:port)"` ModProxy bool `flag:"modproxy,default=$GOCACHE_MODPROXY,Enable a Go module proxy (requires --http)"` RevProxy string `flag:"revproxy,default=$GOCACHE_REVPROXY,Reverse proxy these hosts (comma-separated; requires --http)"` SumDB string `flag:"sumdb,default=$GOCACHE_SUMDB,SumDB servers to proxy for (comma-separated)"` } func noopClose(context.Context) error { return nil } // runServe runs a cache communicating over a local TCP socket. func runServe(env *command.Env) error { if serveFlags.Plugin <= 0 { return env.Usagef("you must provide a --plugin port") } // Initialize the cache server. Unlike a direct server, only close down and // wait for cache cleanup when the whole process exits. s, s3c, err := initCacheServer(env) if err != nil { return err } closeHook := s.Close s.Close = noopClose // Listen for connections from the Go toolchain on the specified socket. lst, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", serveFlags.Plugin)) if err != nil { return fmt.Errorf("listen: %w", err) } log.Printf("plugin listening at %q", lst.Addr()) ctx, cancel := signal.NotifyContext(env.Context(), syscall.SIGINT, syscall.SIGTERM) defer cancel() var g taskgroup.Group g.Run(func() { <-ctx.Done() log.Printf("closing plugin listener") lst.Close() }) // If a module proxy is enabled, start it. modProxy, modCleanup, err := initModProxy(env.SetContext(ctx), s3c) if err != nil { lst.Close() return fmt.Errorf("module proxy: %w", err) } defer modCleanup() // If a reverse proxy is enabled, start it. revProxy, err := initRevProxy(env.SetContext(ctx), s3c, &g) if err != nil { lst.Close() return fmt.Errorf("reverse proxy: %w", err) } // If an HTTP server is enabled, start it up with debug routes // and whatever other services were requested. if serveFlags.HTTP != "" { srv := &http.Server{ Addr: serveFlags.HTTP, Handler: makeHandler(modProxy, revProxy), } g.Go(srv.ListenAndServe) vprintf("HTTP server listening at %q", serveFlags.HTTP) g.Run(func() { <-ctx.Done() vprintf("stopping HTTP service") srv.Shutdown(context.Background()) }) } for { conn, err := lst.Accept() if err != nil { if !errors.Is(err, net.ErrClosed) { log.Printf("accept failed: %v, exiting server loop", err) } break } log.Printf("new client connection") g.Go(func() error { defer func() { log.Printf("client connection closed") conn.Close() }() return s.Run(ctx, conn, conn) }) } log.Printf("server loop exited, waiting for client exit") g.Wait() if closeHook != nil { ctx := gocache.WithLogf(context.Background(), log.Printf) if err := closeHook(ctx); err != nil { log.Printf("server close: %v (ignored)", err) } } return nil } // runConnect implements a direct cache proxy by connecting to a remote server. func runConnect(env *command.Env, plugin string) error { port, err := strconv.Atoi(plugin) if err != nil { return fmt.Errorf("invalid plugin port: %w", err) } conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port)) if err != nil { return fmt.Errorf("dial: %w", err) } start := time.Now() vprintf("connected to %q", conn.RemoteAddr()) out := taskgroup.Go(func() error { defer conn.(*net.TCPConn).CloseWrite() // let the server finish return copy(conn, os.Stdin) }) if rerr := copy(os.Stdout, conn); rerr != nil { vprintf("read responses: %v", err) } out.Wait() conn.Close() vprintf("connection closed (%v elapsed)", time.Since(start)) return nil } // copy emulates the base case of io.Copy, but does not attempt to use the // io.ReaderFrom or io.WriterTo implementations. // // TODO(creachadair): For some reason io.Copy does not work correctly when r is // a pipe (e.g., stdin) and w is a TCP socket. Figure out why. func copy(w io.Writer, r io.Reader) error { var buf [4096]byte for { nr, err := r.Read(buf[:]) if nr > 0 { if nw, err := w.Write(buf[:nr]); err != nil { return fmt.Errorf("copy to: %w", err) } else if nw < nr { return fmt.Errorf("wrote %d < %d bytes: %w", nw, nr, io.ErrShortWrite) } } if err == io.EOF { return nil } else if err != nil { return fmt.Errorf("copy from: %w", err) } } }