Mercurial Hosting > d2o
view main.go @ 0:48bdab3eec8a
Initial
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 09 Mar 2026 00:37:49 +0500 |
| parents | |
| children | 3e7247db5c6e |
line wrap: on
line source
package main import ( "crypto/tls" "fmt" "log" "net" "net/http" "net/http/httputil" "net/url" "os" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "d2o/fcgi" "d2o/icf" ) 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) } // Apply @d2o global settings for _, d := range cfg.Abstract("d2o") { switch d.Key { case "threads": n, err := strconv.Atoi(safeArg(d.Args, 0)) if err == nil && n > 0 { runtime.GOMAXPROCS(n) log.Printf("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 { log.Printf("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) } log.Printf("d2o: listening on %s (https)", pc.addr) return http.Serve(ln, h) } // collectPorts scans all blocks for port / port+tls directives. func collectPorts(cfg *icf.Config) []portConfig { seen := make(map[string]bool) var out []portConfig for _, b := range cfg.Blocks { dirs := cfg.ResolveBlock(b, nil) // Collect tls paths defined in this block 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) { host := stripPort(r.Host) reqPath := path.Clean(r.URL.Path) // Try host+path first, then host alone dirs, caps := h.cfg.Match(host + reqPath) if dirs == nil { dirs, caps = h.cfg.Match(host) } if dirs == nil { http.Error(w, "not found", http.StatusNotFound) return } h.serve(w, r, dirs, caps) } func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { var ( rootDir string rootShow bool fcgiAddr string fcgiPat string rprxAddr string ) for _, d := range dirs { switch d.Key { case "root": rootDir = safeArg(d.Args, 0) switch safeArg(d.Args, 1) { case "show": rootShow = true case "hide", "": rootShow = false default: log.Printf("d2o: root: unknown mode %q (want show|hide)", safeArg(d.Args, 1)) } case "fcgi": fcgiAddr = safeArg(d.Args, 0) fcgiPat = safeArg(d.Args, 1) if fcgiPat == "" { fcgiPat = "*" } case "rprx": rprxAddr = safeArg(d.Args, 0) } } // Priority: rprx > fcgi > static root if rprxAddr != "" { serveReverseProxy(w, r, rprxAddr) return } if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) { if err := serveFCGI(w, r, fcgiAddr, rootDir); err != nil { log.Printf("d2o: fcgi error: %v", err) http.Error(w, "gateway error", http.StatusBadGateway) } return } if rootDir != "" { serveStatic(w, r, rootDir, rootShow) return } http.Error(w, "not found", http.StatusNotFound) } // --- Static ----------------------------------------------------------------- func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, showDir bool) { fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) 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() { if !showDir { http.Error(w, "forbidden", http.StatusForbidden) return } listDir(w, r, fpath, r.URL.Path) 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></head><body>\n", urlPath) fmt.Fprintf(w, "<h2>Index of %s</h2><hr><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><hr><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) proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { log.Printf("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) conn, err := net.Dial(network, address) if err != nil { return fmt.Errorf("connect %s: %w", addr, err) } defer conn.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) } return fcgi.Do(w, r, conn, params) } 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 }
