Mercurial Hosting > d2o
comparison main.go @ 0:48bdab3eec8a
Initial
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 09 Mar 2026 00:37:49 +0500 |
| parents | |
| children | 3e7247db5c6e |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:48bdab3eec8a |
|---|---|
| 1 package main | |
| 2 | |
| 3 import ( | |
| 4 "crypto/tls" | |
| 5 "fmt" | |
| 6 "log" | |
| 7 "net" | |
| 8 "net/http" | |
| 9 "net/http/httputil" | |
| 10 "net/url" | |
| 11 "os" | |
| 12 "path" | |
| 13 "path/filepath" | |
| 14 "regexp" | |
| 15 "runtime" | |
| 16 "strconv" | |
| 17 "strings" | |
| 18 | |
| 19 "d2o/fcgi" | |
| 20 "d2o/icf" | |
| 21 ) | |
| 22 | |
| 23 func main() { | |
| 24 cfgPath := "/etc/d2obase" | |
| 25 if len(os.Args) > 1 { | |
| 26 cfgPath = os.Args[1] | |
| 27 } | |
| 28 | |
| 29 f, err := os.Open(cfgPath) | |
| 30 if err != nil { | |
| 31 log.Fatalf("d2o: cannot open config: %v", err) | |
| 32 } | |
| 33 cfg, err := icf.Parse(f) | |
| 34 f.Close() | |
| 35 if err != nil { | |
| 36 log.Fatalf("d2o: config error: %v", err) | |
| 37 } | |
| 38 | |
| 39 // Apply @d2o global settings | |
| 40 for _, d := range cfg.Abstract("d2o") { | |
| 41 switch d.Key { | |
| 42 case "threads": | |
| 43 n, err := strconv.Atoi(safeArg(d.Args, 0)) | |
| 44 if err == nil && n > 0 { | |
| 45 runtime.GOMAXPROCS(n) | |
| 46 log.Printf("d2o: GOMAXPROCS = %d", n) | |
| 47 } | |
| 48 } | |
| 49 } | |
| 50 | |
| 51 ports := collectPorts(cfg) | |
| 52 if len(ports) == 0 { | |
| 53 log.Fatal("d2o: no port directives found in config") | |
| 54 } | |
| 55 | |
| 56 h := &handler{cfg: cfg} | |
| 57 errCh := make(chan error, len(ports)) | |
| 58 | |
| 59 for _, pc := range ports { | |
| 60 go func(pc portConfig) { | |
| 61 errCh <- pc.listen(h) | |
| 62 }(pc) | |
| 63 } | |
| 64 | |
| 65 log.Fatal(<-errCh) | |
| 66 } | |
| 67 | |
| 68 // --- Port collection -------------------------------------------------------- | |
| 69 | |
| 70 type portConfig struct { | |
| 71 addr string | |
| 72 certFile string | |
| 73 keyFile string | |
| 74 isTLS bool | |
| 75 } | |
| 76 | |
| 77 func (pc portConfig) listen(h http.Handler) error { | |
| 78 if !pc.isTLS { | |
| 79 log.Printf("d2o: listening on %s (http)", pc.addr) | |
| 80 return http.ListenAndServe(pc.addr, h) | |
| 81 } | |
| 82 cert, err := tls.LoadX509KeyPair(pc.certFile, pc.keyFile) | |
| 83 if err != nil { | |
| 84 return fmt.Errorf("d2o: tls: %w", err) | |
| 85 } | |
| 86 ln, err := tls.Listen("tcp", pc.addr, &tls.Config{ | |
| 87 Certificates: []tls.Certificate{cert}, | |
| 88 MinVersion: tls.VersionTLS12, | |
| 89 }) | |
| 90 if err != nil { | |
| 91 return fmt.Errorf("d2o: listen %s: %w", pc.addr, err) | |
| 92 } | |
| 93 log.Printf("d2o: listening on %s (https)", pc.addr) | |
| 94 return http.Serve(ln, h) | |
| 95 } | |
| 96 | |
| 97 // collectPorts scans all blocks for port / port+tls directives. | |
| 98 func collectPorts(cfg *icf.Config) []portConfig { | |
| 99 seen := make(map[string]bool) | |
| 100 var out []portConfig | |
| 101 | |
| 102 for _, b := range cfg.Blocks { | |
| 103 dirs := cfg.ResolveBlock(b, nil) | |
| 104 | |
| 105 // Collect tls paths defined in this block | |
| 106 var cert, key string | |
| 107 for _, d := range dirs { | |
| 108 if d.Key == "tls" { | |
| 109 cert = safeArg(d.Args, 0) | |
| 110 key = safeArg(d.Args, 1) | |
| 111 } | |
| 112 } | |
| 113 | |
| 114 for _, d := range dirs { | |
| 115 switch d.Key { | |
| 116 case "port": | |
| 117 addr := ":" + safeArg(d.Args, 0) | |
| 118 if !seen[addr] { | |
| 119 seen[addr] = true | |
| 120 out = append(out, portConfig{addr: addr}) | |
| 121 } | |
| 122 case "port+tls": | |
| 123 addr := ":" + safeArg(d.Args, 0) | |
| 124 c := safeArg(d.Args, 1) | |
| 125 k := safeArg(d.Args, 2) | |
| 126 if c == "" { | |
| 127 c = cert | |
| 128 } | |
| 129 if k == "" { | |
| 130 k = key | |
| 131 } | |
| 132 if !seen[addr] { | |
| 133 seen[addr] = true | |
| 134 out = append(out, portConfig{addr: addr, certFile: c, keyFile: k, isTLS: true}) | |
| 135 } | |
| 136 } | |
| 137 } | |
| 138 } | |
| 139 return out | |
| 140 } | |
| 141 | |
| 142 // --- HTTP Handler ----------------------------------------------------------- | |
| 143 | |
| 144 type handler struct { | |
| 145 cfg *icf.Config | |
| 146 } | |
| 147 | |
| 148 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
| 149 host := stripPort(r.Host) | |
| 150 reqPath := path.Clean(r.URL.Path) | |
| 151 | |
| 152 // Try host+path first, then host alone | |
| 153 dirs, caps := h.cfg.Match(host + reqPath) | |
| 154 if dirs == nil { | |
| 155 dirs, caps = h.cfg.Match(host) | |
| 156 } | |
| 157 if dirs == nil { | |
| 158 http.Error(w, "not found", http.StatusNotFound) | |
| 159 return | |
| 160 } | |
| 161 | |
| 162 h.serve(w, r, dirs, caps) | |
| 163 } | |
| 164 | |
| 165 func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { | |
| 166 var ( | |
| 167 rootDir string | |
| 168 rootShow bool | |
| 169 fcgiAddr string | |
| 170 fcgiPat string | |
| 171 rprxAddr string | |
| 172 ) | |
| 173 | |
| 174 for _, d := range dirs { | |
| 175 switch d.Key { | |
| 176 case "root": | |
| 177 rootDir = safeArg(d.Args, 0) | |
| 178 switch safeArg(d.Args, 1) { | |
| 179 case "show": | |
| 180 rootShow = true | |
| 181 case "hide", "": | |
| 182 rootShow = false | |
| 183 default: | |
| 184 log.Printf("d2o: root: unknown mode %q (want show|hide)", safeArg(d.Args, 1)) | |
| 185 } | |
| 186 case "fcgi": | |
| 187 fcgiAddr = safeArg(d.Args, 0) | |
| 188 fcgiPat = safeArg(d.Args, 1) | |
| 189 if fcgiPat == "" { | |
| 190 fcgiPat = "*" | |
| 191 } | |
| 192 case "rprx": | |
| 193 rprxAddr = safeArg(d.Args, 0) | |
| 194 } | |
| 195 } | |
| 196 | |
| 197 // Priority: rprx > fcgi > static root | |
| 198 if rprxAddr != "" { | |
| 199 serveReverseProxy(w, r, rprxAddr) | |
| 200 return | |
| 201 } | |
| 202 if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) { | |
| 203 if err := serveFCGI(w, r, fcgiAddr, rootDir); err != nil { | |
| 204 log.Printf("d2o: fcgi error: %v", err) | |
| 205 http.Error(w, "gateway error", http.StatusBadGateway) | |
| 206 } | |
| 207 return | |
| 208 } | |
| 209 if rootDir != "" { | |
| 210 serveStatic(w, r, rootDir, rootShow) | |
| 211 return | |
| 212 } | |
| 213 | |
| 214 http.Error(w, "not found", http.StatusNotFound) | |
| 215 } | |
| 216 | |
| 217 // --- Static ----------------------------------------------------------------- | |
| 218 | |
| 219 func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, showDir bool) { | |
| 220 fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) | |
| 221 | |
| 222 info, err := os.Stat(fpath) | |
| 223 if os.IsNotExist(err) { | |
| 224 http.Error(w, "not found", http.StatusNotFound) | |
| 225 return | |
| 226 } | |
| 227 if err != nil { | |
| 228 http.Error(w, "internal error", http.StatusInternalServerError) | |
| 229 return | |
| 230 } | |
| 231 if info.IsDir() { | |
| 232 if !showDir { | |
| 233 http.Error(w, "forbidden", http.StatusForbidden) | |
| 234 return | |
| 235 } | |
| 236 listDir(w, r, fpath, r.URL.Path) | |
| 237 return | |
| 238 } | |
| 239 http.ServeFile(w, r, fpath) | |
| 240 } | |
| 241 | |
| 242 func listDir(w http.ResponseWriter, r *http.Request, dir, urlPath string) { | |
| 243 entries, err := os.ReadDir(dir) | |
| 244 if err != nil { | |
| 245 http.Error(w, "cannot read directory", http.StatusInternalServerError) | |
| 246 return | |
| 247 } | |
| 248 w.Header().Set("Content-Type", "text/html; charset=utf-8") | |
| 249 fmt.Fprintf(w, "<html><head><title>Index of %s</title></head><body>\n", urlPath) | |
| 250 fmt.Fprintf(w, "<h2>Index of %s</h2><hr><pre>\n", urlPath) | |
| 251 if urlPath != "/" { | |
| 252 fmt.Fprintf(w, "<a href=\"..\">..</a>\n") | |
| 253 } | |
| 254 for _, e := range entries { | |
| 255 name := e.Name() | |
| 256 if e.IsDir() { | |
| 257 name += "/" | |
| 258 } | |
| 259 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", path.Join(urlPath, name), name) | |
| 260 } | |
| 261 fmt.Fprintf(w, "</pre><hr><i>d2o webserver</i></body></html>") | |
| 262 } | |
| 263 | |
| 264 // --- Reverse proxy ---------------------------------------------------------- | |
| 265 | |
| 266 func serveReverseProxy(w http.ResponseWriter, r *http.Request, target string) { | |
| 267 if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") { | |
| 268 target = "http://" + target | |
| 269 } | |
| 270 u, err := url.Parse(target) | |
| 271 if err != nil { | |
| 272 http.Error(w, "bad gateway config", http.StatusInternalServerError) | |
| 273 return | |
| 274 } | |
| 275 proxy := httputil.NewSingleHostReverseProxy(u) | |
| 276 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | |
| 277 log.Printf("d2o: rprx error: %v", err) | |
| 278 http.Error(w, "bad gateway", http.StatusBadGateway) | |
| 279 } | |
| 280 proxy.ServeHTTP(w, r) | |
| 281 } | |
| 282 | |
| 283 // --- FastCGI ---------------------------------------------------------------- | |
| 284 | |
| 285 func serveFCGI(w http.ResponseWriter, r *http.Request, addr, docRoot string) error { | |
| 286 network, address := parseFCGIAddr(addr) | |
| 287 conn, err := net.Dial(network, address) | |
| 288 if err != nil { | |
| 289 return fmt.Errorf("connect %s: %w", addr, err) | |
| 290 } | |
| 291 defer conn.Close() | |
| 292 | |
| 293 scriptPath := r.URL.Path | |
| 294 if docRoot != "" { | |
| 295 scriptPath = filepath.Join(docRoot, filepath.FromSlash(r.URL.Path)) | |
| 296 } | |
| 297 | |
| 298 params := map[string]string{ | |
| 299 "REQUEST_METHOD": r.Method, | |
| 300 "SCRIPT_FILENAME": scriptPath, | |
| 301 "SCRIPT_NAME": r.URL.Path, | |
| 302 "REQUEST_URI": r.URL.RequestURI(), | |
| 303 "QUERY_STRING": r.URL.RawQuery, | |
| 304 "SERVER_PROTOCOL": r.Proto, | |
| 305 "SERVER_NAME": stripPort(r.Host), | |
| 306 "DOCUMENT_ROOT": docRoot, | |
| 307 "GATEWAY_INTERFACE": "CGI/1.1", | |
| 308 "SERVER_SOFTWARE": "d2o/1.0", | |
| 309 } | |
| 310 if r.TLS != nil { | |
| 311 params["HTTPS"] = "on" | |
| 312 } | |
| 313 for k, vs := range r.Header { | |
| 314 key := "HTTP_" + strings.ToUpper(strings.ReplaceAll(k, "-", "_")) | |
| 315 params[key] = strings.Join(vs, ", ") | |
| 316 } | |
| 317 if ct := r.Header.Get("Content-Type"); ct != "" { | |
| 318 params["CONTENT_TYPE"] = ct | |
| 319 } | |
| 320 if r.ContentLength >= 0 { | |
| 321 params["CONTENT_LENGTH"] = strconv.FormatInt(r.ContentLength, 10) | |
| 322 } | |
| 323 | |
| 324 return fcgi.Do(w, r, conn, params) | |
| 325 } | |
| 326 | |
| 327 func parseFCGIAddr(addr string) (network, address string) { | |
| 328 if strings.HasPrefix(addr, "unix:") { | |
| 329 return "unix", strings.TrimPrefix(addr, "unix:") | |
| 330 } | |
| 331 return "tcp", addr | |
| 332 } | |
| 333 | |
| 334 // --- Helpers ---------------------------------------------------------------- | |
| 335 | |
| 336 func stripPort(host string) string { | |
| 337 if h, _, err := net.SplitHostPort(host); err == nil { | |
| 338 return h | |
| 339 } | |
| 340 return host | |
| 341 } | |
| 342 | |
| 343 func safeArg(args []string, i int) string { | |
| 344 if i < len(args) { | |
| 345 return args[i] | |
| 346 } | |
| 347 return "" | |
| 348 } | |
| 349 | |
| 350 func matchGlob(pattern, s string) bool { | |
| 351 if pattern == "*" { | |
| 352 return true | |
| 353 } | |
| 354 regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$" | |
| 355 matched, err := regexp.MatchString(regPat, s) | |
| 356 return err == nil && matched | |
| 357 } |
