changeset 0:48bdab3eec8a

Initial
author Atarwn Gard <a@qwa.su>
date Mon, 09 Mar 2026 00:37:49 +0500
parents
children 3e7247db5c6e
files fcgi/fcgi.go go.mod icf/icf.go main.go
diffstat 4 files changed, 876 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fcgi/fcgi.go	Mon Mar 09 00:37:49 2026 +0500
@@ -0,0 +1,195 @@
+// Package fcgi implements a minimal FastCGI client.
+//
+// Supports sending a single request over a pre-dialed net.Conn and streaming
+// the CGI response back to an http.ResponseWriter.
+package fcgi
+
+import (
+	"bufio"
+	"encoding/binary"
+	"io"
+	"net/http"
+	"net/textproto"
+	"strings"
+)
+
+// Reference: https://fastcgi-archives.github.io/FastCGI_Specification.html
+
+const (
+	fcgiVersion       = 1
+	fcgiBeginRequest  = 1
+	fcgiParams        = 4
+	fcgiStdin         = 5
+	fcgiStdout        = 6
+	fcgiEndRequest    = 3
+	fcgiRoleResponder = 1
+	fcgiRequestID     = 1
+)
+
+// Do sends params and the request body over conn as a FastCGI request, then
+// parses the response and writes it to w. conn is closed by the caller.
+func Do(w http.ResponseWriter, r *http.Request, conn io.ReadWriter, params map[string]string) error {
+	bw := bufio.NewWriter(conn)
+
+	if err := writeRecord(bw, fcgiBeginRequest, fcgiRequestID,
+		[]byte{0, fcgiRoleResponder, 0, 0, 0, 0, 0, 0}); err != nil {
+		return err
+	}
+
+	if err := writeRecord(bw, fcgiParams, fcgiRequestID, encodeParams(params)); err != nil {
+		return err
+	}
+	if err := writeRecord(bw, fcgiParams, fcgiRequestID, nil); err != nil {
+		return err
+	}
+
+	if r.Body != nil {
+		buf := make([]byte, 4096)
+		for {
+			n, err := r.Body.Read(buf)
+			if n > 0 {
+				if werr := writeRecord(bw, fcgiStdin, fcgiRequestID, buf[:n]); werr != nil {
+					return werr
+				}
+			}
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				return err
+			}
+		}
+	}
+	if err := writeRecord(bw, fcgiStdin, fcgiRequestID, nil); err != nil {
+		return err
+	}
+	if err := bw.Flush(); err != nil {
+		return err
+	}
+
+	// Read response
+	var stdout strings.Builder
+	br := bufio.NewReader(conn)
+
+	for {
+		hdr := make([]byte, 8)
+		if _, err := io.ReadFull(br, hdr); err != nil {
+			return err
+		}
+		recType := hdr[1]
+		contentLen := int(binary.BigEndian.Uint16(hdr[4:6]))
+		paddingLen := int(hdr[6])
+
+		content := make([]byte, contentLen)
+		if _, err := io.ReadFull(br, content); err != nil {
+			return err
+		}
+		if paddingLen > 0 {
+			if _, err := io.ReadFull(br, make([]byte, paddingLen)); err != nil {
+				return err
+			}
+		}
+
+		switch recType {
+		case fcgiStdout:
+			stdout.Write(content)
+		case fcgiEndRequest:
+			return forwardResponse(w, stdout.String())
+		}
+		// fcgiStderr is silently discarded
+	}
+}
+
+func forwardResponse(w http.ResponseWriter, raw string) error {
+	sep := "\r\n\r\n"
+	idx := strings.Index(raw, sep)
+	advance := 4
+	if idx == -1 {
+		sep = "\n\n"
+		idx = strings.Index(raw, sep)
+		advance = 2
+	}
+	if idx == -1 {
+		w.Write([]byte(raw))
+		return nil
+	}
+
+	headerPart := raw[:idx]
+	body := raw[idx+advance:]
+
+	tp := textproto.NewReader(bufio.NewReader(strings.NewReader(headerPart + "\r\n\r\n")))
+	mime, _ := tp.ReadMIMEHeader()
+
+	status := 200
+	if s := mime.Get("Status"); s != "" {
+		code, _, _ := strings.Cut(s, " ")
+		if n := parseIntFast(code); n > 0 {
+			status = n
+		}
+		mime.Del("Status")
+	}
+
+	for k, vs := range mime {
+		for _, v := range vs {
+			w.Header().Add(k, v)
+		}
+	}
+	w.WriteHeader(status)
+	w.Write([]byte(body))
+	return nil
+}
+
+func writeRecord(w io.Writer, recType uint8, reqID uint16, content []byte) error {
+	length := len(content)
+	padding := (8 - (length % 8)) % 8
+	hdr := []byte{
+		fcgiVersion, recType,
+		byte(reqID >> 8), byte(reqID),
+		byte(length >> 8), byte(length),
+		byte(padding), 0,
+	}
+	if _, err := w.Write(hdr); err != nil {
+		return err
+	}
+	if len(content) > 0 {
+		if _, err := w.Write(content); err != nil {
+			return err
+		}
+	}
+	if padding > 0 {
+		if _, err := w.Write(make([]byte, padding)); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func encodeParams(params map[string]string) []byte {
+	var buf []byte
+	for k, v := range params {
+		buf = appendLen(buf, len(k))
+		buf = appendLen(buf, len(v))
+		buf = append(buf, k...)
+		buf = append(buf, v...)
+	}
+	return buf
+}
+
+func appendLen(buf []byte, n int) []byte {
+	if n <= 127 {
+		return append(buf, byte(n))
+	}
+	return append(buf, byte((n>>24)|0x80), byte(n>>16), byte(n>>8), byte(n))
+}
+
+func parseIntFast(s string) int {
+	n := 0
+	for i := 0; i < len(s); i++ {
+		c := s[i]
+		if c < '0' || c > '9' {
+			return 0
+		}
+		n = n*10 + int(c-'0')
+	}
+	return n
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/go.mod	Mon Mar 09 00:37:49 2026 +0500
@@ -0,0 +1,3 @@
+module d2o
+
+go 1.21
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/icf/icf.go	Mon Mar 09 00:37:49 2026 +0500
@@ -0,0 +1,321 @@
+// Package icf implements the Inherited Configuration Format parser.
+//
+// ICF is a rule-based configuration format with variables, abstract blocks
+// (mixins), pattern matching with named capture groups, and brace expansion.
+//
+// Syntax:
+//
+//	; comment
+//	KEY=value                       variable
+//	@name                           abstract block (mixin)
+//	|> directive arg {a,b}          directive with optional brace expansion
+//	block.id @mixin                 concrete block inheriting a mixin
+//	<cap>.example.com               block with named capture group
+//	<_>.example.com                 anonymous wildcard
+package icf
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"strings"
+)
+
+// Directive is a single key + arguments line inside a block.
+type Directive struct {
+	Key  string
+	Args []string
+}
+
+// Config holds the parsed and fully resolved state of an ICF file.
+type Config struct {
+	vars     map[string]string
+	abstract map[string][]Directive
+	Blocks   []ParsedBlock
+}
+
+type ParsedBlock struct {
+	ID         string
+	Mixin      string
+	Directives []Directive
+}
+
+// Parse reads an ICF document and returns a ready Config.
+func Parse(r io.Reader) (*Config, error) {
+	c := &Config{
+		vars:     make(map[string]string),
+		abstract: make(map[string][]Directive),
+	}
+
+	var raw []ParsedBlock
+	var cur *ParsedBlock
+
+	flush := func() {
+		if cur != nil {
+			raw = append(raw, *cur)
+			cur = nil
+		}
+	}
+
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		line := stripComment(strings.TrimSpace(scanner.Text()))
+		if line == "" {
+			continue
+		}
+
+		// Variable assignment: KEY=value
+		if i := strings.Index(line, "="); i > 0 && !strings.HasPrefix(line, "|>") {
+			key := line[:i]
+			if isVarName(key) {
+				c.vars[key] = strings.TrimSpace(line[i+1:])
+				continue
+			}
+		}
+
+		// Directive: |> key args...
+		if strings.HasPrefix(line, "|>") {
+			if cur == nil {
+				return nil, fmt.Errorf("icf: directive outside block: %q", line)
+			}
+			parts := strings.Fields(strings.TrimSpace(line[2:]))
+			if len(parts) == 0 {
+				continue
+			}
+			cur.Directives = append(cur.Directives, Directive{
+				Key:  parts[0],
+				Args: braceExpand(parts[1:]),
+			})
+			continue
+		}
+
+		// Block header: id [@mixin]
+		flush()
+		parts := strings.Fields(line)
+		pb := ParsedBlock{ID: parts[0]}
+		if len(parts) >= 2 && strings.HasPrefix(parts[1], "@") {
+			pb.Mixin = parts[1][1:]
+		}
+		cur = &pb
+	}
+	flush()
+
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	// Separate abstract from concrete blocks
+	for _, b := range raw {
+		if strings.HasPrefix(b.ID, "@") {
+			c.abstract[b.ID[1:]] = b.Directives
+		} else {
+			c.Blocks = append(c.Blocks, b)
+		}
+	}
+
+	return c, nil
+}
+
+// Abstract returns the directives of a named abstract block with variables
+// substituted. Returns nil if not found.
+func (c *Config) Abstract(name string) []Directive {
+	dirs, ok := c.abstract[name]
+	if !ok {
+		return nil
+	}
+	return c.applyVars(dirs, nil)
+}
+
+// Match finds the most specific block matching input (e.g. "host/path") and
+// returns resolved directives plus named captures.
+// Domain part is matched exactly (with captures); path part uses prefix match.
+func (c *Config) Match(input string) ([]Directive, map[string]string) {
+	inHost, inPath, _ := strings.Cut(input, "/")
+
+	type hit struct {
+		block    ParsedBlock
+		captures map[string]string
+		score    int
+	}
+	var best *hit
+
+	for _, b := range c.Blocks {
+		patHost, patPath, hasPath := strings.Cut(b.ID, "/")
+
+		caps := make(map[string]string)
+
+		domScore, ok := matchExact(patHost, inHost, caps)
+		if !ok {
+			continue
+		}
+
+		pathScore := 0
+		if hasPath {
+			pathScore, ok = matchPrefix(patPath, inPath, caps)
+			if !ok {
+				continue
+			}
+		}
+
+		score := domScore*1000 + pathScore
+		if best == nil || score > best.score {
+			best = &hit{block: b, captures: caps, score: score}
+		}
+	}
+
+	if best == nil {
+		return nil, nil
+	}
+
+	return c.ResolveBlock(best.block, best.captures), best.captures
+}
+
+// ResolveBlock merges mixin then block directives, substituting vars+captures.
+func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive {
+	var merged []Directive
+	if b.Mixin != "" {
+		merged = append(merged, c.abstract[b.Mixin]...)
+	}
+	merged = append(merged, b.Directives...)
+	return c.applyVars(merged, caps)
+}
+
+func (c *Config) applyVars(dirs []Directive, caps map[string]string) []Directive {
+	out := make([]Directive, len(dirs))
+	for i, d := range dirs {
+		out[i].Key = c.subst(d.Key, caps)
+		out[i].Args = make([]string, len(d.Args))
+		for j, a := range d.Args {
+			out[i].Args[j] = c.subst(a, caps)
+		}
+	}
+	return out
+}
+
+func (c *Config) subst(s string, caps map[string]string) string {
+	if !strings.Contains(s, "$") {
+		return s
+	}
+	var b strings.Builder
+	i := 0
+	for i < len(s) {
+		if s[i] != '$' {
+			b.WriteByte(s[i])
+			i++
+			continue
+		}
+		j := i + 1
+		for j < len(s) && isVarChar(s[j]) {
+			j++
+		}
+		name := s[i+1 : j]
+		if v, ok := caps[name]; ok {
+			b.WriteString(v)
+		} else if v, ok := c.vars[name]; ok {
+			b.WriteString(v)
+		} else {
+			b.WriteString(s[i:j])
+		}
+		i = j
+	}
+	return b.String()
+}
+
+func matchExact(pat, s string, caps map[string]string) (int, bool) {
+	score, rem, ok := matchCaptures(pat, s, caps)
+	if !ok || rem != "" {
+		return 0, false
+	}
+	return score, true
+}
+
+func matchPrefix(pat, s string, caps map[string]string) (int, bool) {
+	score, _, ok := matchCaptures(pat, s, caps)
+	return score, ok
+}
+
+func matchCaptures(pat, inp string, caps map[string]string) (int, string, bool) {
+	for {
+		if pat == "" {
+			return 0, inp, true
+		}
+
+		if strings.HasPrefix(pat, "<") {
+			end := strings.Index(pat, ">")
+			if end == -1 {
+				return 0, "", false
+			}
+			capName := pat[1:end]
+			rest := pat[end+1:]
+
+			for split := 1; split <= len(inp); split++ {
+				candidate := inp[:split]
+				remaining := inp[split:]
+				score, finalRem, ok := matchCaptures(rest, remaining, caps)
+				if ok {
+					if capName != "_" {
+						caps[capName] = candidate
+					}
+					return score, finalRem, true
+				}
+			}
+			return 0, "", false
+		}
+
+		if inp == "" || pat[0] != inp[0] {
+			return 0, "", false
+		}
+		score, rem, ok := matchCaptures(pat[1:], inp[1:], caps)
+		return score + 1, rem, ok
+	}
+}
+
+func braceExpand(args []string) []string {
+	var out []string
+	for _, a := range args {
+		out = append(out, expandOne(a)...)
+	}
+	return out
+}
+
+func expandOne(s string) []string {
+	start := strings.Index(s, "{")
+	end := strings.Index(s, "}")
+	if start == -1 || end == -1 || end < start {
+		return []string{s}
+	}
+	prefix := s[:start]
+	suffix := s[end+1:]
+	var out []string
+	for _, v := range strings.Split(s[start+1:end], ",") {
+		out = append(out, prefix+strings.TrimSpace(v)+suffix)
+	}
+	return out
+}
+
+func stripComment(line string) string {
+	if strings.HasPrefix(line, ";") {
+		return ""
+	}
+	if i := strings.Index(line, " ;"); i != -1 {
+		return strings.TrimSpace(line[:i])
+	}
+	return line
+}
+
+func isVarName(s string) bool {
+	if s == "" {
+		return false
+	}
+	for i := 0; i < len(s); i++ {
+		if !isVarChar(s[i]) {
+			return false
+		}
+	}
+	return true
+}
+
+func isVarChar(c byte) bool {
+	return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
+		(c >= '0' && c <= '9') || c == '_'
+}
--- /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
+}