changeset 12:84384cccda0e default tip

bubfix subpaths and remove hardcode
author atarwn@g5
date Mon, 23 Mar 2026 12:05:05 +0500
parents 350589d762a0
children
files README.md icf/icf.go main.go
diffstat 3 files changed, 70 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Thu Mar 19 20:00:49 2026 +0500
+++ b/README.md	Mon Mar 23 12:05:05 2026 +0500
@@ -2,6 +2,28 @@
 
 Минималистичный веб-сервер на Go с конфигурацией в формате ICF.
 
+## Сборка
+
+Обязательные компоненты:
+- go
+
+Рекомендуемые:
+- just
+- lowdown
+
+Сборка и установка:
+
+```
+$ just build # просто сборка
+$ just install # сборка и установка
+```
+
+Если вы скачали только обязательные компоненты
+
+```
+$ go build -o d2o .
+```
+
 ## Структура проекта
 
 ```
@@ -270,4 +292,4 @@
 - **Brace expansion без вложенности.** `{a,{b,c}}` не работает.
 - **FastCGI — один запрос на соединение.** Keep-alive с FPM не реализован.
 - **Нет HTTP→HTTPS редиректа** из коробки — нужно реализовывать отдельным блоком на порту 80.
-- **Нет hot reload** конфига — требуется перезапуск процесса.
\ No newline at end of file
+- **Нет hot reload** конфига — требуется перезапуск процесса.
--- a/icf/icf.go	Thu Mar 19 20:00:49 2026 +0500
+++ b/icf/icf.go	Mon Mar 23 12:05:05 2026 +0500
@@ -146,13 +146,14 @@
 // Match finds the most specific block matching input (e.g. "host/path") and
 // returns resolved directives plus named captures.
 // Domain is matched exactly; path uses prefix match.
-func (c *Config) Match(input string) ([]Directive, map[string]string) {
+func (c *Config) Match(input string) ([]Directive, map[string]string, string) {
 	inHost, inPath, _ := strings.Cut(input, "/")
 
 	type hit struct {
 		block    ParsedBlock
 		captures map[string]string
 		score    int
+		rem      string
 	}
 	var best *hit
 
@@ -166,24 +167,33 @@
 		}
 
 		pathScore := 0
+		rem := inPath
 		if hasPath {
-			pathScore, ok = matchPrefix(patPath, inPath, caps)
+			var pathRem string
+			pathScore, pathRem, ok = matchPrefix(patPath, inPath, caps)
 			if !ok {
 				continue
 			}
+			rem = pathRem
 		}
 
 		score := domScore*1000 + pathScore
 		if best == nil || score > best.score {
-			best = &hit{block: b, captures: caps, score: score}
+			best = &hit{block: b, captures: caps, score: score, rem: rem}
 		}
 	}
 
 	if best == nil {
-		return nil, nil
+		return nil, nil, ""
 	}
 
-	return c.ResolveBlock(best.block, best.captures), best.captures
+	// rem is the unmatched suffix after the path pattern.
+	// Ensure it starts with "/" to form a valid absolute subpath.
+	subPath := best.rem
+	if !strings.HasPrefix(subPath, "/") {
+		subPath = "/" + subPath
+	}
+	return c.ResolveBlock(best.block, best.captures), best.captures, subPath
 }
 
 // ResolveBlock merges mixin directives (lower priority) with block directives,
@@ -286,9 +296,9 @@
 	return score, true
 }
 
-func matchPrefix(pat, s string, caps map[string]string) (int, bool) {
-	score, _, ok := matchCaptures(pat, s, caps)
-	return score, ok
+func matchPrefix(pat, s string, caps map[string]string) (int, string, bool) {
+	score, rem, ok := matchCaptures(pat, s, caps)
+	return score, rem, ok
 }
 
 func matchCaptures(pat, inp string, caps map[string]string) (int, string, bool) {
@@ -306,6 +316,10 @@
 			rest := pat[end+1:]
 
 			for split := len(inp); split >= 1; split-- {
+				// Captures never span path separators: <n> matches one segment only.
+				if strings.Contains(inp[:split], "/") {
+					continue
+				}
 				score, finalRem, ok := matchCaptures(rest, inp[split:], caps)
 				if ok {
 					if capName != "_" {
--- a/main.go	Thu Mar 19 20:00:49 2026 +0500
+++ b/main.go	Mon Mar 23 12:05:05 2026 +0500
@@ -192,9 +192,10 @@
 	host := stripPort(r.Host)
 	reqPath := path.Clean(r.URL.Path)
 
-	dirs, caps := h.cfg.Match(host + reqPath)
+	dirs, caps, subPath := h.cfg.Match(host + reqPath)
 	if dirs == nil {
-		dirs, caps = h.cfg.Match(host)
+		dirs, caps, _ = h.cfg.Match(host)
+		subPath = reqPath // host-only block: full path is the subpath
 	}
 	if dirs == nil {
 		http.Error(rr, "not found", http.StatusNotFound)
@@ -203,12 +204,12 @@
 		return
 	}
 
-	h.serve(rr, r, dirs, caps)
+	h.serve(rr, r, dirs, caps, subPath)
 	rr.ensureStatus(http.StatusOK)
 	accessPrintf("d2o: %s %s%s -> %d %dB (%s)", r.Method, r.Host, r.URL.RequestURI(), rr.status, rr.bytes, time.Since(start).Truncate(time.Millisecond))
 }
 
-func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, caps map[string]string) {
+func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, caps map[string]string, subPath string) {
 	var (
 		rootDir  string
 		rootShow bool
@@ -277,21 +278,12 @@
 		return
 	}
 	if rootDir != "" {
-		fsPath := path.Clean(r.URL.Path)
-		displayPath := fsPath
-		if user, ok := caps["user"]; ok && user != "" {
-			mount := "/~" + user
-			if fsPath == mount || strings.HasPrefix(fsPath, mount+"/") {
-				trimmed := strings.TrimPrefix(fsPath, mount)
-				if trimmed == "" {
-					trimmed = "/"
-				}
-				fsPath = trimmed
-			}
-		}
-
+		// subPath is the remainder after the matched path pattern, e.g. for
+		// block "host/~<user>" and request "/~alice/about.html", subPath="/about.html".
+		// For host-only blocks it equals the full request path.
+		displayPath := path.Clean(r.URL.Path)
 		verbosePrintf("d2o: static -> %s (%s)", rootDir, r.URL.Path)
-		serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat, cgiExec, cgiPat, fsPath, displayPath)
+		serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat, cgiExec, cgiPat, subPath, displayPath)
 		return
 	}
 
@@ -306,6 +298,11 @@
 func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string, fcgiAddr, fcgiPat, cgiExec, cgiPat string, fsPath, displayPath string) {
 	fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(fsPath)))
 
+	// Use a cloned request with fsPath as URL.Path so that http.ServeFile and
+	// fcgi/cgi handlers see a path relative to rootDir, not the original URL.
+	r2 := r.Clone(r.Context())
+	r2.URL.Path = fsPath
+
 	info, err := os.Stat(fpath)
 	if os.IsNotExist(err) {
 		http.Error(w, "not found", http.StatusNotFound)
@@ -321,21 +318,21 @@
 			idxPath := filepath.Join(fpath, idx)
 			if _, err := os.Stat(idxPath); err == nil {
 				if fcgiAddr != "" && matchGlob(fcgiPat, idx) {
-					r2 := r.Clone(r.Context())
-					r2.URL.Path = path.Join(r.URL.Path, idx)
-					if err := serveFCGI(w, r2, fcgiAddr, rootDir); err != nil {
+					r3 := r2.Clone(r2.Context())
+					r3.URL.Path = path.Join(fsPath, idx)
+					if err := serveFCGI(w, r3, fcgiAddr, rootDir); err != nil {
 						log.Printf("d2o: fcgi error: %v", err)
 						http.Error(w, "gateway error", http.StatusBadGateway)
 					}
 					return
 				}
 				if cgiExec != "" && matchGlob(cgiPat, idx) {
-					r2 := r.Clone(r.Context())
-					r2.URL.Path = path.Join(r.URL.Path, idx)
-					serveCGI(w, r2, cgiExec, rootDir)
+					r3 := r2.Clone(r2.Context())
+					r3.URL.Path = path.Join(fsPath, idx)
+					serveCGI(w, r3, cgiExec, rootDir)
 					return
 				}
-				http.ServeFile(w, r, idxPath)
+				http.ServeFile(w, r2, idxPath)
 				return
 			}
 		}
@@ -343,11 +340,11 @@
 			http.Error(w, "forbidden", http.StatusForbidden)
 			return
 		}
-		listDir(w, r, fpath, displayPath)
+		listDir(w, r2, fpath, displayPath)
 		return
 	}
 
-	http.ServeFile(w, r, fpath)
+	http.ServeFile(w, r2, fpath)
 }
 
 func listDir(w http.ResponseWriter, r *http.Request, dir, urlPath string) {
@@ -585,4 +582,4 @@
 	if rr.status == 0 {
 		rr.status = defaultCode
 	}
-}
+}
\ No newline at end of file