Mercurial Hosting > d2o
view main.go @ 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 |
line wrap: on
line source
package main import ( "bufio" "crypto/tls" "fmt" "io" "log" "net" "net/http" "net/http/httputil" "net/textproto" "net/url" "os" "path" "path/filepath" "regexp" "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 { cfgPath = os.Args[1] } f, err := os.Open(cfgPath) if err != nil { log.Fatalf("d2o: cannot open config: %v", err) } cfg, err := icf.Parse(f) f.Close() if err != nil { log.Fatalf("d2o: config error: %v", err) } 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) verbosePrintf("d2o: GOMAXPROCS = %d", n) } } } ports := collectPorts(cfg) if len(ports) == 0 { log.Fatal("d2o: no port directives found in config") } h := &handler{cfg: cfg} errCh := make(chan error, len(ports)) for _, pc := range ports { go func(pc portConfig) { errCh <- pc.listen(h) }(pc) } log.Fatal(<-errCh) } // --- Port collection -------------------------------------------------------- type portConfig struct { addr string certFile string keyFile string isTLS bool } func (pc portConfig) listen(h http.Handler) error { if !pc.isTLS { verbosePrintf("d2o: listening on %s (http)", pc.addr) return http.ListenAndServe(pc.addr, h) } cert, err := tls.LoadX509KeyPair(pc.certFile, pc.keyFile) if err != nil { return fmt.Errorf("d2o: tls: %w", err) } ln, err := tls.Listen("tcp", pc.addr, &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, }) if err != nil { return fmt.Errorf("d2o: listen %s: %w", pc.addr, err) } verbosePrintf("d2o: listening on %s (https)", pc.addr) return http.Serve(ln, h) } func collectPorts(cfg *icf.Config) []portConfig { seen := make(map[string]bool) var out []portConfig for _, b := range cfg.Blocks { dirs := cfg.ResolveBlock(b, nil) var cert, key string for _, d := range dirs { if d.Key == "tls" { cert = safeArg(d.Args, 0) key = safeArg(d.Args, 1) } } for _, d := range dirs { switch d.Key { case "port": addr := ":" + safeArg(d.Args, 0) if !seen[addr] { seen[addr] = true out = append(out, portConfig{addr: addr}) } case "port+tls": addr := ":" + safeArg(d.Args, 0) c := safeArg(d.Args, 1) k := safeArg(d.Args, 2) if c == "" { c = cert } if k == "" { k = key } if !seen[addr] { seen[addr] = true out = append(out, portConfig{addr: addr, certFile: c, keyFile: k, isTLS: true}) } } } } return out } // --- HTTP Handler ----------------------------------------------------------- type handler struct { cfg *icf.Config } 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) dirs, caps := h.cfg.Match(host + reqPath) if dirs == nil { dirs, caps = h.cfg.Match(host) } if dirs == nil { 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(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, caps map[string]string) { var ( rootDir string rootShow bool ndex []string fcgiAddr string fcgiPat string rprxAddr string rdirCode int rdirURL string ) for _, d := range dirs { switch d.Key { case "root": rootDir = safeArg(d.Args, 0) rootShow = safeArg(d.Args, 1) == "show" case "ndex": ndex = d.Args case "fcgi": fcgiAddr = safeArg(d.Args, 0) fcgiPat = safeArg(d.Args, 1) if fcgiPat == "" { fcgiPat = "*" } case "rprx": rprxAddr = safeArg(d.Args, 0) case "rdir": rdirCode, _ = strconv.Atoi(safeArg(d.Args, 0)) rdirURL = safeArg(d.Args, 1) } } if rdirURL != "" { 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 { verbosePrintf("d2o: fcgi error: %v", err) http.Error(w, "gateway error", http.StatusBadGateway) } 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 } } verbosePrintf("d2o: static -> %s (%s)", rootDir, r.URL.Path) serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat, fsPath, displayPath) return } http.Error(w, "not found", http.StatusNotFound) } // --- Static ----------------------------------------------------------------- // 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, fsPath, displayPath string) { fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(fsPath))) info, err := os.Stat(fpath) if os.IsNotExist(err) { http.Error(w, "not found", http.StatusNotFound) return } if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if info.IsDir() { for _, idx := range ndex { 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 { log.Printf("d2o: fcgi error: %v", err) http.Error(w, "gateway error", http.StatusBadGateway) } return } http.ServeFile(w, r, idxPath) return } } if !show { http.Error(w, "forbidden", http.StatusForbidden) return } listDir(w, r, fpath, displayPath) return } http.ServeFile(w, r, fpath) } func listDir(w http.ResponseWriter, r *http.Request, dir, urlPath string) { entries, err := os.ReadDir(dir) if err != nil { http.Error(w, "cannot read directory", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, "<html><head><title>Index of %s</title><style>body{font-family:monospace}</style></head><body>\n", urlPath) fmt.Fprintf(w, "<h2>Index of %s</h2><pre>\n", urlPath) if urlPath != "/" { fmt.Fprintf(w, "<a href=\"..\">..</a>\n") } for _, e := range entries { name := e.Name() if e.IsDir() { name += "/" } fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", path.Join(urlPath, name), name) } fmt.Fprintf(w, "</pre><i>d2o webserver</i></body></html>") } // --- Reverse proxy ---------------------------------------------------------- func serveReverseProxy(w http.ResponseWriter, r *http.Request, target string) { if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") { target = "http://" + target } u, err := url.Parse(target) if err != nil { http.Error(w, "bad gateway config", http.StatusInternalServerError) 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) { verbosePrintf("d2o: rprx error: %v", err) http.Error(w, "bad gateway", http.StatusBadGateway) } proxy.ServeHTTP(w, r) } // --- FastCGI ---------------------------------------------------------------- func serveFCGI(w http.ResponseWriter, r *http.Request, addr, docRoot string) error { network, address := parseFCGIAddr(addr) client, err := fcgi.Dial(network, address) if err != nil { return fmt.Errorf("connect %s: %w", addr, err) } defer client.Close() scriptPath := r.URL.Path if docRoot != "" { scriptPath = filepath.Join(docRoot, filepath.FromSlash(r.URL.Path)) } params := map[string]string{ "REQUEST_METHOD": r.Method, "SCRIPT_FILENAME": scriptPath, "SCRIPT_NAME": r.URL.Path, "REQUEST_URI": r.URL.RequestURI(), "QUERY_STRING": r.URL.RawQuery, "SERVER_PROTOCOL": r.Proto, "SERVER_NAME": stripPort(r.Host), "DOCUMENT_ROOT": docRoot, "GATEWAY_INTERFACE": "CGI/1.1", "SERVER_SOFTWARE": "d2o/1.0", } if r.TLS != nil { params["HTTPS"] = "on" } for k, vs := range r.Header { key := "HTTP_" + strings.ToUpper(strings.ReplaceAll(k, "-", "_")) params[key] = strings.Join(vs, ", ") } if ct := r.Header.Get("Content-Type"); ct != "" { params["CONTENT_TYPE"] = ct } if r.ContentLength >= 0 { params["CONTENT_LENGTH"] = strconv.FormatInt(r.ContentLength, 10) } // Use Do() instead of Request() — php-fpm returns CGI response (no HTTP status line), // not a full HTTP response. Request() expects "HTTP/1.1 200 OK" and panics on code 0. cgiReader, err := client.Do(params, r.Body) if err != nil { return fmt.Errorf("fcgi request: %w", err) } // Parse CGI headers manually br := bufio.NewReader(cgiReader) tp := textproto.NewReader(br) mime, err := tp.ReadMIMEHeader() if err != nil && len(mime) == 0 { return fmt.Errorf("fcgi response headers: %w", err) } status := http.StatusOK if s := mime.Get("Status"); s != "" { code, _, _ := strings.Cut(s, " ") if n, err := strconv.Atoi(code); err == nil && n > 0 { status = n } mime.Del("Status") } for k, vs := range mime { for _, v := range vs { w.Header().Add(k, v) } } w.WriteHeader(status) io.Copy(w, br) return nil } func parseFCGIAddr(addr string) (network, address string) { if strings.HasPrefix(addr, "unix:") { return "unix", strings.TrimPrefix(addr, "unix:") } return "tcp", addr } // --- Helpers ---------------------------------------------------------------- func stripPort(host string) string { if h, _, err := net.SplitHostPort(host); err == nil { return h } return host } func safeArg(args []string, i int) string { if i < len(args) { return args[i] } return "" } func matchGlob(pattern, s string) bool { if pattern == "*" { return true } regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$" matched, err := regexp.MatchString(regPat, s) return err == nil && matched } 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 } }
