Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

339 lines
12 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"net/http"
"net/url"
"reflect"
"strings"
"github.com/hanzoai/git/modules/htmlutil"
"github.com/hanzoai/git/modules/reqctx"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/web/middleware"
"github.com/hanzoai/git/modules/web/types"
"github.com/hanzoai/git/modules/binding"
)
// Bind binding an obj to a handler's context data
func Bind[T any](_ T) http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
theObj := new(T) // create a new form obj for every request but not use obj directly
data := middleware.GetContextData(req.Context())
binding.Bind(req, theObj)
SetForm(data, theObj)
middleware.AssignForm(theObj, data)
}
}
// SetForm set the form object
func SetForm(dataStore reqctx.ContextDataProvider, obj any) {
dataStore.GetData()["__form"] = obj
}
// GetForm returns the validate form information
func GetForm(dataStore reqctx.RequestDataStore) any {
return dataStore.GetData()["__form"]
}
// Router defines a route based on chi's router
type Router struct {
mux *Mux
afterRouting []any
curGroupPrefix string
curMiddlewares []any
}
// NewRouter creates a new route
func NewRouter() *Router {
router := &Router{mux: NewMux()}
// Everything above this package reads path parameters through
// RouteContext, not through the router. This is the one place the two are
// joined, so swapping the mux out is a change to this function rather than
// to every handler that wants a parameter.
router.mux.Use(ensureRouteContext)
return router
}
// ensureRouteContext attaches a RouteContext before routing, so the mux has
// somewhere to record what it captures.
func ensureRouteContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if GetRouteContext(req.Context()) == nil {
rc := NewRouteContext()
rc.RouteMethod = req.Method
req = WithRouteContext(req, rc)
}
next.ServeHTTP(resp, req)
})
}
// BeforeRouting adds middlewares which will be executed before the request path gets routed
// It should only be used for framework-level global middlewares when it needs to change request method & path.
func (r *Router) BeforeRouting(middlewares ...any) {
for _, m := range middlewares {
if !isNilOrFuncNil(m) {
r.mux.Use(toHandlerProvider(m))
}
}
}
// AfterRouting adds middlewares which will be executed after the request path gets routed
// It can see the routed path and resolved path parameters
func (r *Router) AfterRouting(middlewares ...any) {
r.afterRouting = append(r.afterRouting, middlewares...)
}
// Group mounts a sub-router along a "pattern" string.
func (r *Router) Group(pattern string, fn func(), middlewares ...any) {
previousGroupPrefix := r.curGroupPrefix
previousMiddlewares := r.curMiddlewares
r.curGroupPrefix += pattern
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
fn()
r.curGroupPrefix = previousGroupPrefix
r.curMiddlewares = previousMiddlewares
}
func (r *Router) getPattern(pattern string) string {
newPattern := r.curGroupPrefix + pattern
if !strings.HasPrefix(newPattern, "/") {
newPattern = "/" + newPattern
}
if newPattern == "/" {
return newPattern
}
return strings.TrimSuffix(newPattern, "/")
}
func isNilOrFuncNil(v any) bool {
if v == nil {
return true
}
r := reflect.ValueOf(v)
return r.Kind() == reflect.Func && r.IsNil()
}
func wrapMiddlewareAppendPre(all []middlewareProvider, middlewares []any) []middlewareProvider {
for _, m := range middlewares {
if h, ok := m.(types.PreMiddlewareProvider); ok && h != nil {
all = append(all, toHandlerProvider(middlewareProvider(h)))
}
}
return all
}
func wrapMiddlewareAppendNormal(all []middlewareProvider, middlewares []any) []middlewareProvider {
for _, m := range middlewares {
if _, ok := m.(types.PreMiddlewareProvider); !ok && !isNilOrFuncNil(m) {
all = append(all, toHandlerProvider(m))
}
}
return all
}
func wrapMiddlewareAndHandler(useMiddlewares, curMiddlewares, h []any) (_ []middlewareProvider, _ http.HandlerFunc, hasPreMiddlewares bool) {
if len(h) == 0 {
panic("no endpoint handler provided")
}
if isNilOrFuncNil(h[len(h)-1]) {
panic("endpoint handler can't be nil")
}
handlerProviders := make([]middlewareProvider, 0, len(useMiddlewares)+len(curMiddlewares)+len(h)+1)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, useMiddlewares)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, curMiddlewares)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, h)
hasPreMiddlewares = len(handlerProviders) > 0
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, useMiddlewares)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, curMiddlewares)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, h)
middlewares := handlerProviders[:len(handlerProviders)-1]
handlerFunc := handlerProviders[len(handlerProviders)-1](nil).ServeHTTP
mockPoint := RouterMockPoint(MockAfterMiddlewares)
if mockPoint != nil {
middlewares = append(middlewares, mockPoint)
}
return middlewares, handlerFunc, hasPreMiddlewares
}
// Methods adds the same handlers for multiple http "methods" (separated by ",").
// If any method is invalid, the lower level router will panic.
func (r *Router) Methods(methods, pattern string, h ...any) {
middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, h)
fullPattern := r.getPattern(pattern)
if strings.Contains(methods, ",") {
methods := strings.SplitSeq(methods, ",")
for method := range methods {
r.mux.Method(strings.TrimSpace(method), fullPattern, chain(middlewares, handlerFunc))
}
} else {
r.mux.Method(methods, fullPattern, chain(middlewares, handlerFunc))
}
}
// Mount attaches another Router along "/pattern/*"
func (r *Router) Mount(pattern string, subRouter *Router) {
handlerProviders := make([]middlewareProvider, 0, len(r.afterRouting)+len(r.curMiddlewares))
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, r.afterRouting)
handlerProviders = wrapMiddlewareAppendPre(handlerProviders, r.curMiddlewares)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, r.afterRouting)
handlerProviders = wrapMiddlewareAppendNormal(handlerProviders, r.curMiddlewares)
r.mux.Mount(r.getPattern(pattern), chain(handlerProviders, mountedHandler(subRouter)))
}
// Any delegate requests for all methods
func (r *Router) Any(pattern string, h ...any) {
middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, h)
r.mux.Method("", r.getPattern(pattern), chain(middlewares, handlerFunc))
}
// Delete delegate delete method
func (r *Router) Delete(pattern string, h ...any) {
r.Methods("DELETE", pattern, h...)
}
// Get delegate get method
func (r *Router) Get(pattern string, h ...any) {
r.Methods("GET", pattern, h...)
}
// Head delegate head method
func (r *Router) Head(pattern string, h ...any) {
r.Methods("HEAD", pattern, h...)
}
// Post delegate post method
func (r *Router) Post(pattern string, h ...any) {
r.Methods("POST", pattern, h...)
}
// Put delegate put method
func (r *Router) Put(pattern string, h ...any) {
r.Methods("PUT", pattern, h...)
}
// Patch delegate patch method
func (r *Router) Patch(pattern string, h ...any) {
r.Methods("PATCH", pattern, h...)
}
// ServeHTTP implements http.Handler
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: need to move it to the top-level common middleware, otherwise each "Mount" will cause it to be executed multiple times, which is inefficient.
r.normalizeRequestPath(w, req, r.mux)
}
// NotFound defines a handler to respond whenever a route could not be found.
func (r *Router) NotFound(h http.HandlerFunc) {
middlewares, handlerFunc, _ := wrapMiddlewareAndHandler(r.afterRouting, r.curMiddlewares, []any{h})
r.mux.NotFound(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
executeMiddlewaresHandler(w, req, middlewares, handlerFunc)
}))
}
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
normalized := false
normalizedPath := req.URL.EscapedPath()
if normalizedPath == "" {
normalizedPath, normalized = "/", true
} else if normalizedPath != "/" {
normalized = strings.HasSuffix(normalizedPath, "/")
normalizedPath = strings.TrimRight(normalizedPath, "/")
}
removeRepeatedSlashes := strings.Contains(normalizedPath, "//")
normalized = normalized || removeRepeatedSlashes
// the following code block is a slow-path for replacing all repeated slashes "//" to one single "/"
// if the path doesn't have repeated slashes, then no need to execute it
if removeRepeatedSlashes {
buf := &strings.Builder{}
for i := 0; i < len(normalizedPath); i++ {
if i == 0 || normalizedPath[i-1] != '/' || normalizedPath[i] != '/' {
buf.WriteByte(normalizedPath[i])
}
}
normalizedPath = buf.String()
}
// If the config tells the forge to use a sub-url path directly without reverse proxy,
// then we need to remove the sub-url path from the request URL path.
// But "/v2" is special for OCI container registry, it should always be in the root of the site.
if setting.UseSubURLPath {
remainingPath, ok := strings.CutPrefix(normalizedPath, setting.AppSubURL+"/")
if ok {
normalizedPath = "/" + remainingPath
} else if normalizedPath == setting.AppSubURL {
normalizedPath = "/"
} else if !strings.HasPrefix(normalizedPath+"/", "/v2/") {
// do not respond to other requests, to simulate a real sub-path environment
resp.Header().Add("Content-Type", "text/html; charset=utf-8")
resp.WriteHeader(http.StatusNotFound)
_, _ = htmlutil.HTMLPrintf(resp, `404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)
return
}
normalized = true
}
// if the path is normalized, then fill it back to the request
if normalized {
decodedPath, err := url.PathUnescape(normalizedPath)
if err != nil {
http.Error(resp, "400 Bad Request: unable to unescape path "+normalizedPath, http.StatusBadRequest)
return
}
req.URL.RawPath = normalizedPath
req.URL.Path = decodedPath
}
next.ServeHTTP(resp, req)
}
// Combo delegates requests to Combo
func (r *Router) Combo(pattern string, h ...any) *Combo {
return &Combo{r, pattern, h}
}
// PathGroup creates a group of paths which could be matched by regexp.
// It is only designed to resolve some special cases which chi router can't handle.
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
g := &RouterPathGroup{r: r, pathParam: "*"}
fn(g)
r.Any(pattern, append(h, g.ServeHTTP)...)
}
// chain wraps handlerFunc in middlewares, outermost first.
func chain(middlewares []middlewareProvider, handlerFunc http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
executeMiddlewaresHandler(w, req, middlewares, handlerFunc)
})
}
// mountedHandler routes the REMAINDER of the path with the sub-router.
//
// A mount at "/v1" means the sub-router's own patterns are written relative to
// it — "/repos/{username}" not "/v1/repos/{username}" — so handing it the full
// path matches nothing. The mux records the unconsumed remainder as "*" when it
// reaches a mount; this turns that into the path the sub-router routes on.
//
// The "*" is then cleared: it belongs to how the route was assembled, not to the
// route, and a handler asking for a parameter should not find it.
func mountedHandler(subRouter *Router) http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
if rc := GetRouteContext(req.Context()); rc != nil {
rc.RoutePath = "/" + strings.TrimPrefix(rc.Param("*"), "/")
rc.deleteParam("*")
}
subRouter.mux.ServeHTTP(resp, req)
}
}