repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / cmd / pgs / cdn
Eric Bower  ·  2026-04-28

main.go

  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"io"
  7	"log/slog"
  8	"net"
  9	"net/http"
 10	"net/url"
 11	"strings"
 12
 13	"github.com/picosh/pico/pkg/apps/pgs"
 14	"github.com/picosh/pico/pkg/httpcache"
 15	"github.com/picosh/pico/pkg/shared"
 16	"github.com/prometheus/client_golang/prometheus/promhttp"
 17)
 18
 19func main() {
 20	pipeEnabled := shared.GetEnv("PICO_PIPE_ENABLED", "true")
 21	withPipe := strings.ToLower(pipeEnabled) == "true"
 22	logger := shared.CreateLogger("pgs-cdn", withPipe)
 23	ctx := context.Background()
 24	drain := pgs.CreateSubCacheDrain(ctx, logger)
 25	pubsub := pgs.NewPubsubPipe(drain)
 26	defer func() {
 27		_ = pubsub.Close()
 28	}()
 29	cfg := pgs.NewPgsConfig(logger, nil, nil, drain)
 30	proxy := newProxyServe(cfg.Logger)
 31	httpCache := pgs.NewPgsHttpCache(cfg, proxy)
 32	cacher := &cachedHttp{
 33		Logger: cfg.Logger,
 34		Cache:  httpCache,
 35	}
 36
 37	go pgs.CacheMgmt(ctx, cfg.CacheClearingQueue, cfg, httpCache.Cache)
 38
 39	portStr := fmt.Sprintf(":%s", cfg.WebPort)
 40	cfg.Logger.Info(
 41		"starting server on port",
 42		"port", cfg.WebPort,
 43		"domain", cfg.Domain,
 44	)
 45	err := http.ListenAndServe(portStr, cacher)
 46	cfg.Logger.Error("listen and serve", "err", err)
 47}
 48
 49type proxyServe struct {
 50	Logger    *slog.Logger
 51	transport *http.Transport
 52}
 53
 54func newProxyServe(logger *slog.Logger) *proxyServe {
 55	defaultTransport := http.DefaultTransport.(*http.Transport)
 56	oldDial := defaultTransport.DialContext
 57	transport := &http.Transport{
 58		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 59			return oldDial(ctx, "tcp", "ash.pgs.sh:443")
 60		},
 61	}
 62	return &proxyServe{Logger: logger, transport: transport}
 63}
 64
 65// Headers that should be stripped from upstream responses because the CDN's
 66// cache layer will add its own versions.
 67var stripHeaders = map[string]bool{
 68	"age":          true,
 69	"cache-status": true,
 70	"date":         true,
 71}
 72
 73func (p *proxyServe) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 74	// Dial ash.pgs.sh but keep the original Host header so ash.pgs.sh
 75	// can route the request to the correct subdomain (zmx.sh, etc.).
 76	// Without the Host header, ash.pgs.sh serves its default vhost (HTML).
 77	target, _ := url.Parse("https://ash.pgs.sh" + req.URL.Path)
 78	if req.URL.RawQuery != "" {
 79		target.RawQuery = req.URL.RawQuery
 80	}
 81	p.Logger.Info("proxying request to ash.pgs.sh", "url", target.String())
 82
 83	proxyReq := req.Clone(req.Context())
 84	proxyReq.URL.Scheme = target.Scheme
 85	proxyReq.URL.Host = target.Host
 86	proxyReq.URL.Path = target.Path
 87	proxyReq.URL.RawQuery = target.RawQuery
 88	proxyReq.RequestURI = ""
 89	// Prevent the upstream from returning a compressed body. The CDN cache
 90	// stores a single representation per URL; Caddy handles per-client
 91	// encoding on the way out. If we forward Accept-Encoding, origin may
 92	// return zstd-compressed bytes that get cached and then served to
 93	// clients that never requested zstd.
 94	proxyReq.Header.Del("Accept-Encoding")
 95	// Preserve the original Host header so ash.pgs.sh routes correctly.
 96	proxyReq.Host = req.Host
 97
 98	resp, err := p.transport.RoundTrip(proxyReq)
 99	if err != nil {
100		http.Error(w, err.Error(), http.StatusBadGateway)
101		return
102	}
103	defer func() {
104		_ = resp.Body.Close()
105	}()
106
107	// Copy headers from upstream, but strip cache-related headers that the
108	// CDN's cache layer will regenerate
109	for k, vals := range resp.Header {
110		if stripHeaders[strings.ToLower(k)] {
111			continue
112		}
113		w.Header()[k] = vals
114	}
115	w.WriteHeader(resp.StatusCode)
116	_, _ = io.Copy(w, resp.Body)
117}
118
119type cachedHttp struct {
120	Logger *slog.Logger
121	Cache  *httpcache.HttpCache
122}
123
124func (c *cachedHttp) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
125	if req.URL.Path == "/_metrics" {
126		promhttp.Handler().ServeHTTP(writer, req)
127		return
128	}
129
130	if req.URL.Path == "/check" {
131		c.Logger.Info("proxying `/check` request to ash.pgs.sh", "query", req.URL.RawQuery)
132		req, _ := http.NewRequest("GET", "https://ash.pgs.sh/check?"+req.URL.RawQuery, nil)
133		req.Host = "pgs.sh"
134		// reqDump, _ := httputil.DumpRequestOut(req, true)
135		// fmt.Printf("REQUEST:\n%s", string(reqDump))
136
137		resp, err := http.DefaultClient.Do(req)
138		if err != nil {
139			c.Logger.Error("check request", "err", err)
140		}
141		defer func() {
142			_ = resp.Body.Close()
143		}()
144		writer.WriteHeader(resp.StatusCode)
145		_, _ = io.Copy(writer, resp.Body)
146		return
147	}
148
149	c.Cache.ServeHTTP(writer, req)
150}