Mercurial Hosting > d2o
changeset 12:84384cccda0e
bubfix subpaths and remove hardcode
| author | atarwn@g5 |
|---|---|
| date | Mon, 23 Mar 2026 12:05:05 +0500 |
| parents | 350589d762a0 |
| children | 9460f83a5664 |
| 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
