Mercurial Hosting > d2o
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.go Mon Mar 09 00:37:49 2026 +0500 @@ -0,0 +1,357 @@ +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 +}
