changeset 8:2ffb8028ccbb

add loggingmodes and fix path matching
author Atarwn Gard <a@qwa.su>
date Tue, 17 Mar 2026 19:55:07 +0500
parents 8e4813b4e509
children ec97184ea63d
files icf/icf.go main.go
diffstat 2 files changed, 164 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/icf/icf.go	Tue Mar 10 12:12:33 2026 +0500
+++ b/icf/icf.go	Tue Mar 17 19:55:07 2026 +0500
@@ -305,7 +305,7 @@
 			capName := pat[1:end]
 			rest := pat[end+1:]
 
-			for split := 1; split <= len(inp); split++ {
+			for split := len(inp); split >= 1; split-- {
 				score, finalRem, ok := matchCaptures(rest, inp[split:], caps)
 				if ok {
 					if capName != "_" {
--- a/main.go	Tue Mar 10 12:12:33 2026 +0500
+++ b/main.go	Tue Mar 17 19:55:07 2026 +0500
@@ -1,15 +1,15 @@
 package main
 
 import (
+	"bufio"
 	"crypto/tls"
-	"bufio"
 	"fmt"
 	"io"
 	"log"
-	"net/textproto"
 	"net"
 	"net/http"
 	"net/http/httputil"
+	"net/textproto"
 	"net/url"
 	"os"
 	"path"
@@ -18,12 +18,52 @@
 	"runtime"
 	"strconv"
 	"strings"
+	"time"
 
-	
 	"d2o/fcgi"
 	"d2o/icf"
 )
 
+type loggingMode uint8
+
+const (
+	logNone loggingMode = iota
+	logAccess
+	logVerbose
+)
+
+var (
+	curLoggingMode loggingMode = logVerbose
+	accessLogger               = log.New(os.Stderr, "", log.LstdFlags)
+)
+
+func setLoggingMode(m loggingMode) {
+	curLoggingMode = m
+	switch m {
+	case logNone:
+		log.SetOutput(io.Discard)
+	case logAccess:
+		log.SetOutput(io.Discard)
+	case logVerbose:
+		// keep standard logger output
+	}
+}
+
+func accessEnabled() bool  { return curLoggingMode == logAccess || curLoggingMode == logVerbose }
+func verboseEnabled() bool { return curLoggingMode == logVerbose }
+
+func accessPrintf(format string, args ...any) {
+	if accessEnabled() {
+		accessLogger.Printf(format, args...)
+	}
+}
+
+func verbosePrintf(format string, args ...any) {
+	if verboseEnabled() {
+		log.Printf(format, args...)
+	}
+}
+
 func main() {
 	cfgPath := "/etc/d2obase"
 	if len(os.Args) > 1 {
@@ -42,11 +82,22 @@
 
 	for _, d := range cfg.Abstract("d2o") {
 		switch d.Key {
+		case "logging":
+			switch strings.ToLower(safeArg(d.Args, 0)) {
+			case "", "verbose":
+				setLoggingMode(logVerbose)
+			case "none":
+				setLoggingMode(logNone)
+			case "access":
+				setLoggingMode(logAccess)
+			default:
+				log.Fatalf("d2o: unknown logging mode %q (expected none|access|verbose)", safeArg(d.Args, 0))
+			}
 		case "threads":
 			n, err := strconv.Atoi(safeArg(d.Args, 0))
 			if err == nil && n > 0 {
 				runtime.GOMAXPROCS(n)
-				log.Printf("d2o: GOMAXPROCS = %d", n)
+				verbosePrintf("d2o: GOMAXPROCS = %d", n)
 			}
 		}
 	}
@@ -79,7 +130,7 @@
 
 func (pc portConfig) listen(h http.Handler) error {
 	if !pc.isTLS {
-		log.Printf("d2o: listening on %s (http)", pc.addr)
+		verbosePrintf("d2o: listening on %s (http)", pc.addr)
 		return http.ListenAndServe(pc.addr, h)
 	}
 	cert, err := tls.LoadX509KeyPair(pc.certFile, pc.keyFile)
@@ -93,7 +144,7 @@
 	if err != nil {
 		return fmt.Errorf("d2o: listen %s: %w", pc.addr, err)
 	}
-	log.Printf("d2o: listening on %s (https)", pc.addr)
+	verbosePrintf("d2o: listening on %s (https)", pc.addr)
 	return http.Serve(ln, h)
 }
 
@@ -147,6 +198,9 @@
 }
 
 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	start := time.Now()
+	rr := &respRecorder{ResponseWriter: w, status: 0}
+
 	host := stripPort(r.Host)
 	reqPath := path.Clean(r.URL.Path)
 
@@ -155,14 +209,18 @@
 		dirs, caps = h.cfg.Match(host)
 	}
 	if dirs == nil {
-		http.Error(w, "not found", http.StatusNotFound)
+		http.Error(rr, "not found", http.StatusNotFound)
+		rr.ensureStatus(http.StatusNotFound)
+		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))
 		return
 	}
 
-	h.serve(w, r, dirs, caps)
+	h.serve(rr, r, dirs, caps)
+	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, _ map[string]string) {
+func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, caps map[string]string) {
 	var (
 		rootDir  string
 		rootShow bool
@@ -199,22 +257,39 @@
 		if rdirCode == 0 {
 			rdirCode = http.StatusFound
 		}
+		verbosePrintf("d2o: rdir %d -> %s", rdirCode, rdirURL)
 		http.Redirect(w, r, rdirURL, rdirCode)
 		return
 	}
 	if rprxAddr != "" {
+		verbosePrintf("d2o: rprx -> %s", rprxAddr)
 		serveReverseProxy(w, r, rprxAddr)
 		return
 	}
 	if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) {
+		verbosePrintf("d2o: fcgi -> %s (%s)", fcgiAddr, r.URL.Path)
 		if err := serveFCGI(w, r, fcgiAddr, rootDir); err != nil {
-			log.Printf("d2o: fcgi error: %v", err)
+			verbosePrintf("d2o: fcgi error: %v", err)
 			http.Error(w, "gateway error", http.StatusBadGateway)
 		}
 		return
 	}
 	if rootDir != "" {
-		serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat)
+		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
+			}
+		}
+
+		verbosePrintf("d2o: static -> %s (%s)", rootDir, r.URL.Path)
+		serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat, fsPath, displayPath)
 		return
 	}
 
@@ -226,8 +301,8 @@
 // serveStatic serves files from rootDir.
 // rootIndex == nil: directory listing forbidden (hide).
 // rootIndex != nil: try each as index candidate; if none found, show listing.
-func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string, fcgiAddr, fcgiPat string) {
-	fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path)))
+func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string, fcgiAddr, fcgiPat string, fsPath, displayPath string) {
+	fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(fsPath)))
 
 	info, err := os.Stat(fpath)
 	if os.IsNotExist(err) {
@@ -260,7 +335,7 @@
 			http.Error(w, "forbidden", http.StatusForbidden)
 			return
 		}
-		listDir(w, r, fpath, r.URL.Path)
+		listDir(w, r, fpath, displayPath)
 		return
 	}
 
@@ -301,8 +376,19 @@
 		return
 	}
 	proxy := httputil.NewSingleHostReverseProxy(u)
+	if verboseEnabled() {
+		origDirector := proxy.Director
+		proxy.Director = func(req *http.Request) {
+			origDirector(req)
+			log.Printf("d2o: rprx upstream request: %s %s", req.Method, req.URL.String())
+		}
+		proxy.ModifyResponse = func(resp *http.Response) error {
+			log.Printf("d2o: rprx upstream response: %d %s", resp.StatusCode, resp.Status)
+			return nil
+		}
+	}
 	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
-		log.Printf("d2o: rprx error: %v", err)
+		verbosePrintf("d2o: rprx error: %v", err)
 		http.Error(w, "bad gateway", http.StatusBadGateway)
 	}
 	proxy.ServeHTTP(w, r)
@@ -413,4 +499,65 @@
 	regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$"
 	matched, err := regexp.MatchString(regPat, s)
 	return err == nil && matched
-}
\ No newline at end of file
+}
+
+type respRecorder struct {
+	http.ResponseWriter
+	status int
+	bytes  int64
+}
+
+func (rr *respRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	h, ok := rr.ResponseWriter.(http.Hijacker)
+	if !ok {
+		return nil, nil, fmt.Errorf("hijack not supported")
+	}
+	return h.Hijack()
+}
+
+func (rr *respRecorder) Flush() {
+	if f, ok := rr.ResponseWriter.(http.Flusher); ok {
+		f.Flush()
+	}
+}
+
+func (rr *respRecorder) Push(target string, opts *http.PushOptions) error {
+	if p, ok := rr.ResponseWriter.(http.Pusher); ok {
+		return p.Push(target, opts)
+	}
+	return http.ErrNotSupported
+}
+
+func (rr *respRecorder) ReadFrom(src io.Reader) (int64, error) {
+	// Preserve io.Copy optimizations and count bytes.
+	rf, ok := rr.ResponseWriter.(io.ReaderFrom)
+	if !ok {
+		return io.Copy(rr, src)
+	}
+	if rr.status == 0 {
+		rr.status = http.StatusOK
+	}
+	n, err := rf.ReadFrom(src)
+	rr.bytes += n
+	return n, err
+}
+
+func (rr *respRecorder) WriteHeader(code int) {
+	rr.status = code
+	rr.ResponseWriter.WriteHeader(code)
+}
+
+func (rr *respRecorder) Write(p []byte) (int, error) {
+	if rr.status == 0 {
+		rr.status = http.StatusOK
+	}
+	n, err := rr.ResponseWriter.Write(p)
+	rr.bytes += int64(n)
+	return n, err
+}
+
+func (rr *respRecorder) ensureStatus(defaultCode int) {
+	if rr.status == 0 {
+		rr.status = defaultCode
+	}
+}