tenseleyflow/shithub / 240e2f4

Browse files

S12: wire git-HTTP routes; move Compress+Timeout out of global into per-group middleware so git can stream uncompressed without the 30s cap

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
240e2f473b758e1dd3380cf330c0680e86a9b763
Parents
2e3ba81
Tree
3a7a7e5

3 changed files

StatusFile+-
A internal/web/githttp_wiring.go 42 0
M internal/web/handlers/handlers.go 25 1
M internal/web/server.go 10 2
internal/web/githttp_wiring.goadded
@@ -0,0 +1,42 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package web
4
+
5
+import (
6
+	"errors"
7
+	"fmt"
8
+	"log/slog"
9
+	"path/filepath"
10
+
11
+	"github.com/jackc/pgx/v5/pgxpool"
12
+
13
+	"github.com/tenseleyFlow/shithub/internal/infra/config"
14
+	"github.com/tenseleyFlow/shithub/internal/infra/storage"
15
+	githttph "github.com/tenseleyFlow/shithub/internal/web/handlers/githttp"
16
+)
17
+
18
+// buildGitHTTPHandlers wires the smart-HTTP route handlers. The bare
19
+// repos live at cfg.Storage.ReposRoot; we refuse to boot the git-HTTP
20
+// surface without it.
21
+func buildGitHTTPHandlers(
22
+	cfg config.Config,
23
+	pool *pgxpool.Pool,
24
+	logger *slog.Logger,
25
+) (*githttph.Handlers, error) {
26
+	if cfg.Storage.ReposRoot == "" {
27
+		return nil, errors.New("git-http: cfg.Storage.ReposRoot is empty")
28
+	}
29
+	root, err := filepath.Abs(cfg.Storage.ReposRoot)
30
+	if err != nil {
31
+		return nil, fmt.Errorf("git-http: resolve repos_root: %w", err)
32
+	}
33
+	rfs, err := storage.NewRepoFS(root)
34
+	if err != nil {
35
+		return nil, fmt.Errorf("git-http: NewRepoFS: %w", err)
36
+	}
37
+	return githttph.New(githttph.Deps{
38
+		Logger: logger,
39
+		Pool:   pool,
40
+		RepoFS: rfs,
41
+	})
42
+}
internal/web/handlers/handlers.gomodified
@@ -12,6 +12,7 @@ import (
1212
 	"io/fs"
1313
 	"log/slog"
1414
 	"net/http"
15
+	"time"
1516
 
1617
 	"github.com/go-chi/chi/v5"
1718
 
@@ -53,6 +54,12 @@ type Deps struct {
5354
 	// CSRF-protected group. Two-segment match doesn't collide with the
5455
 	// /{username} catch-all.
5556
 	RepoHomeMounter func(chi.Router)
57
+	// GitHTTPMounter, when non-nil, registers the smart-HTTP git routes
58
+	// (`*.git/info/refs`, `git-upload-pack`, `git-receive-pack`). MUST
59
+	// land in a route group that bypasses CSRF, response compression,
60
+	// and the global request timeout — git generates its own pack
61
+	// format, uses HTTP Basic, and clones can run for many minutes.
62
+	GitHTTPMounter func(chi.Router)
5663
 	// ProfileMounter, when non-nil, registers the /{username} catch-all
5764
 	// route. MUST run last in its group — chi matches in registration
5865
 	// order, and {username} swallows everything else.
@@ -101,6 +108,8 @@ func RegisterChi(r *chi.Mux, deps Deps) (*chi.Mux, middleware.PanicHandler, http
101108
 	// Static and health endpoints are CSRF-exempt; everything else passes
102109
 	// through the CSRF wrapper for state-changing methods.
103110
 	r.Group(func(r chi.Router) {
111
+		r.Use(middleware.Compress)
112
+		r.Use(middleware.Timeout(30 * time.Second))
104113
 		r.Handle("/static/*", http.StripPrefix("/static/", staticFileServer(deps.StaticFS)))
105114
 		r.Get("/healthz", healthz)
106115
 		r.Handle("/readyz", readinessHandler(deps.ReadyCheck, deps.Logger))
@@ -115,8 +124,23 @@ func RegisterChi(r *chi.Mux, deps Deps) (*chi.Mux, middleware.PanicHandler, http
115124
 		}
116125
 	})
117126
 
118
-	// Application routes — CSRF protected.
127
+	// Smart-HTTP git routes get their own group: NO CSRF (HTTP Basic
128
+	// flow, no browser form posts), NO response compression (git emits
129
+	// its own pack format), and NO global request timeout (long clones
130
+	// run for minutes). The global SecureHeaders / RealIP / RequestID
131
+	// stack still applies; everything else is per-group.
132
+	if deps.GitHTTPMounter != nil {
119133
 		r.Group(func(r chi.Router) {
134
+			deps.GitHTTPMounter(r)
135
+		})
136
+	}
137
+
138
+	// Application routes — CSRF protected. Compress + Timeout live in
139
+	// this group (and the static one above) rather than globally so the
140
+	// git-HTTP group can opt out.
141
+	r.Group(func(r chi.Router) {
142
+		r.Use(middleware.Compress)
143
+		r.Use(middleware.Timeout(30 * time.Second))
120144
 		r.Use(csrf)
121145
 		r.Get("/", helloHandler{render: rr, logoSVG: deps.LogoSVG, logger: deps.Logger}.ServeHTTP)
122146
 		// /internal/panic is a dev affordance: GET it to trigger the
internal/web/server.gomodified
@@ -125,8 +125,10 @@ func Run(ctx context.Context, opts Options) error {
125125
 		r.Use(tracing.Middleware)
126126
 	}
127127
 	r.Use(middleware.SecureHeaders(middleware.DefaultSecureHeaders()))
128
-	r.Use(middleware.Compress)
129
-	r.Use(middleware.Timeout(30 * time.Second))
128
+	// Compress + Timeout are NOT global: the smart-HTTP git routes need
129
+	// to stream uncompressed pack data for many minutes. RegisterChi
130
+	// applies them inside the CSRF-exempt and CSRF-protected groups but
131
+	// skips the git group.
130132
 	r.Use(middleware.SessionLoader(sessionStore, logger))
131133
 	if pool != nil {
132134
 		r.Use(middleware.OptionalUser(usernameLookup(pool)))
@@ -183,6 +185,12 @@ func Run(ctx context.Context, opts Options) error {
183185
 			})
184186
 		}
185187
 		deps.RepoHomeMounter = repoH.MountRepoHome
188
+
189
+		gitHTTPH, err := buildGitHTTPHandlers(cfg, pool, logger)
190
+		if err != nil {
191
+			return fmt.Errorf("git-http handlers: %w", err)
192
+		}
193
+		deps.GitHTTPMounter = gitHTTPH.MountSmartHTTP
186194
 	} else {
187195
 		logger.Warn("auth: no DB pool — signup/login routes not mounted")
188196
 	}