package runnerfacade import ( "context" "crypto/tls" "errors" "fmt" "net" "net/http" "net/http/httputil" "net/url" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" ) const APIBasePath = "/api/actions" // APIHandler exposes the facade at the base path used by the official Runner. // SPIFFE middleware runs after the TLS listener has authenticated the peer. func APIHandler(facade *Facade) http.Handler { path, handler := facade.Handler() mux := http.NewServeMux() mux.Handle(APIBasePath+path, http.StripPrefix(APIBasePath, SPIFFEMiddleware(handler))) return mux } // Handler keeps RunnerService calls inside the authenticated facade while // forwarding repository and artifact HTTP traffic to the real Gitea server. // Official Runner derives checkout URLs from its registration instance URL, // which intentionally points at the executor-local SPIFFE proxy. func Handler(facade *Facade, upstreamURL string) (http.Handler, error) { target, err := url.Parse(upstreamURL) if err != nil || (target.Scheme != "http" && target.Scheme != "https") || target.Host == "" { return nil, errors.New("Gitea upstream must be an absolute HTTP URL") } path, service := facade.Handler() mux := http.NewServeMux() mux.Handle(APIBasePath+path, http.StripPrefix(APIBasePath, SPIFFEMiddleware(service))) proxy := httputil.NewSingleHostReverseProxy(target) director := proxy.Director proxy.Director = func(request *http.Request) { director(request) request.Host = target.Host } mux.Handle("/", proxy) return mux, nil } type Server struct { Facade *Facade ListenAddress string TrustDomain string WorkloadAPIAddr string UpstreamURL string } // Run serves the RunnerService facade with workload-to-workload mTLS. Any // identity in the local trust domain may complete TLS; the facade then requires // the exact logical task identity stored in its assignment registry. func (s Server) Run(ctx context.Context) error { if s.Facade == nil || s.ListenAddress == "" || s.TrustDomain == "" || s.UpstreamURL == "" { return errors.New("runner facade, listen address, trust domain, and Gitea upstream are required") } handler, err := Handler(s.Facade, s.UpstreamURL) if err != nil { return err } trustDomain, err := spiffeid.TrustDomainFromString(s.TrustDomain) if err != nil { return fmt.Errorf("parse facade trust domain: %w", err) } options := []workloadapi.X509SourceOption{} if s.WorkloadAPIAddr != "" { options = append(options, workloadapi.WithClientOptions(workloadapi.WithAddr(s.WorkloadAPIAddr))) } source, err := workloadapi.NewX509Source(ctx, options...) if err != nil { return fmt.Errorf("open facade SPIFFE Workload API X509 source: %w", err) } defer source.Close() listener, err := net.Listen("tcp", s.ListenAddress) if err != nil { return fmt.Errorf("listen for runner facade: %w", err) } defer listener.Close() tlsListener := tls.NewListener(listener, tlsconfig.MTLSServerConfig( source, source, tlsconfig.AuthorizeMemberOf(trustDomain), )) httpServer := &http.Server{Handler: handler, ReadHeaderTimeout: 10 * time.Second} serverErrors := make(chan error, 1) go func() { serverErrors <- httpServer.Serve(tlsListener) }() select { case <-ctx.Done(): shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second) shutdownErr := httpServer.Shutdown(shutdownContext) cancel() serverErr := <-serverErrors if errors.Is(serverErr, http.ErrServerClosed) { serverErr = nil } return errors.Join(shutdownErr, serverErr) case err := <-serverErrors: if errors.Is(err, http.ErrServerClosed) { return nil } return fmt.Errorf("serve runner facade: %w", err) } }