@@ -12,6 +12,7 @@ import ( |
| 12 | "io/fs" | 12 | "io/fs" |
| 13 | "log/slog" | 13 | "log/slog" |
| 14 | "net/http" | 14 | "net/http" |
| | 15 | + "time" |
| 15 | | 16 | |
| 16 | "github.com/go-chi/chi/v5" | 17 | "github.com/go-chi/chi/v5" |
| 17 | | 18 | |
@@ -53,6 +54,12 @@ type Deps struct { |
| 53 | // CSRF-protected group. Two-segment match doesn't collide with the | 54 | // CSRF-protected group. Two-segment match doesn't collide with the |
| 54 | // /{username} catch-all. | 55 | // /{username} catch-all. |
| 55 | RepoHomeMounter func(chi.Router) | 56 | 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) |
| 56 | // ProfileMounter, when non-nil, registers the /{username} catch-all | 63 | // ProfileMounter, when non-nil, registers the /{username} catch-all |
| 57 | // route. MUST run last in its group — chi matches in registration | 64 | // route. MUST run last in its group — chi matches in registration |
| 58 | // order, and {username} swallows everything else. | 65 | // order, and {username} swallows everything else. |
@@ -101,6 +108,8 @@ func RegisterChi(r *chi.Mux, deps Deps) (*chi.Mux, middleware.PanicHandler, http |
| 101 | // Static and health endpoints are CSRF-exempt; everything else passes | 108 | // Static and health endpoints are CSRF-exempt; everything else passes |
| 102 | // through the CSRF wrapper for state-changing methods. | 109 | // through the CSRF wrapper for state-changing methods. |
| 103 | r.Group(func(r chi.Router) { | 110 | r.Group(func(r chi.Router) { |
| | 111 | + r.Use(middleware.Compress) |
| | 112 | + r.Use(middleware.Timeout(30 * time.Second)) |
| 104 | r.Handle("/static/*", http.StripPrefix("/static/", staticFileServer(deps.StaticFS))) | 113 | r.Handle("/static/*", http.StripPrefix("/static/", staticFileServer(deps.StaticFS))) |
| 105 | r.Get("/healthz", healthz) | 114 | r.Get("/healthz", healthz) |
| 106 | r.Handle("/readyz", readinessHandler(deps.ReadyCheck, deps.Logger)) | 115 | r.Handle("/readyz", readinessHandler(deps.ReadyCheck, deps.Logger)) |
@@ -115,8 +124,23 @@ func RegisterChi(r *chi.Mux, deps Deps) (*chi.Mux, middleware.PanicHandler, http |
| 115 | } | 124 | } |
| 116 | }) | 125 | }) |
| 117 | | 126 | |
| 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 { |
| | 133 | + 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. |
| 119 | r.Group(func(r chi.Router) { | 141 | r.Group(func(r chi.Router) { |
| | 142 | + r.Use(middleware.Compress) |
| | 143 | + r.Use(middleware.Timeout(30 * time.Second)) |
| 120 | r.Use(csrf) | 144 | r.Use(csrf) |
| 121 | r.Get("/", helloHandler{render: rr, logoSVG: deps.LogoSVG, logger: deps.Logger}.ServeHTTP) | 145 | r.Get("/", helloHandler{render: rr, logoSVG: deps.LogoSVG, logger: deps.Logger}.ServeHTTP) |
| 122 | // /internal/panic is a dev affordance: GET it to trigger the | 146 | // /internal/panic is a dev affordance: GET it to trigger the |