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
+}