diff --git a/cmd/gitea-dynamic-runner/controller.go b/cmd/gitea-dynamic-runner/controller.go index 6801004..e416a90 100644 --- a/cmd/gitea-dynamic-runner/controller.go +++ b/cmd/gitea-dynamic-runner/controller.go @@ -126,7 +126,7 @@ func runController(ctx context.Context) error { } facadeServer := runnerfacade.Server{ Facade: facade, ListenAddress: config.FacadeListen, TrustDomain: config.TrustDomain, - WorkloadAPIAddr: config.WorkloadAPIAddr, + WorkloadAPIAddr: config.WorkloadAPIAddr, UpstreamURL: config.GiteaURL, } components := controller.Registry{ controller.Scheduler: runComponent(func(ctx context.Context) error { diff --git a/internal/runnerfacade/facade_test.go b/internal/runnerfacade/facade_test.go index 73fe3c4..87e6d66 100644 --- a/internal/runnerfacade/facade_test.go +++ b/internal/runnerfacade/facade_test.go @@ -130,6 +130,31 @@ func TestAPIHandlerMatchesOfficialRunnerBasePath(t *testing.T) { } } +func TestHandlerProxiesRepositoryTrafficToGitea(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/owner/repo/info/refs" || request.Header.Get("Authorization") != "Basic checkout-token" { + t.Errorf("request path=%q authorization=%q", request.URL.Path, request.Header.Get("Authorization")) + response.WriteHeader(http.StatusBadRequest) + return + } + response.WriteHeader(http.StatusOK) + })) + defer upstream.Close() + + facade, _, _ := testFacade(t) + handler, err := Handler(facade, upstream.URL) + if err != nil { + t.Fatal(err) + } + request := httptest.NewRequest(http.MethodGet, "http://facade/owner/repo/info/refs", nil) + request.Header.Set("Authorization", "Basic checkout-token") + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("status = %d", response.Code) + } +} + func TestFacadeRejectsWrongIdentityOrCapability(t *testing.T) { facade, assignment, token := testFacade(t) wrongIdentity := WithSPIFFEID(context.Background(), "spiffe://ddupan.top/ci/owner/repo/other") diff --git a/internal/runnerfacade/server.go b/internal/runnerfacade/server.go index 717c9a6..95384c7 100644 --- a/internal/runnerfacade/server.go +++ b/internal/runnerfacade/server.go @@ -7,6 +7,8 @@ import ( "fmt" "net" "net/http" + "net/http/httputil" + "net/url" "time" "github.com/spiffe/go-spiffe/v2/spiffeid" @@ -25,19 +27,40 @@ func APIHandler(facade *Facade) http.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 == "" { - return errors.New("runner facade, listen address, and trust domain are required") + 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 { @@ -61,7 +84,7 @@ func (s Server) Run(ctx context.Context) error { tlsListener := tls.NewListener(listener, tlsconfig.MTLSServerConfig( source, source, tlsconfig.AuthorizeMemberOf(trustDomain), )) - httpServer := &http.Server{Handler: APIHandler(s.Facade), ReadHeaderTimeout: 10 * time.Second} + httpServer := &http.Server{Handler: handler, ReadHeaderTimeout: 10 * time.Second} serverErrors := make(chan error, 1) go func() { serverErrors <- httpServer.Serve(tlsListener) }()