Files
gitea-dynamic-runner/internal/runnerfacade/server.go
T

108 lines
3.6 KiB
Go

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)))
mux.Handle("/", httputil.NewSingleHostReverseProxy(target))
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)
}
}