Mercurial Hosting > d2o
comparison main.go @ 8:2ffb8028ccbb
add loggingmodes and fix path matching
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Tue, 17 Mar 2026 19:55:07 +0500 |
| parents | 8e4813b4e509 |
| children | ec97184ea63d |
comparison
equal
deleted
inserted
replaced
| 7:8e4813b4e509 | 8:2ffb8028ccbb |
|---|---|
| 1 package main | 1 package main |
| 2 | 2 |
| 3 import ( | 3 import ( |
| 4 "bufio" | |
| 4 "crypto/tls" | 5 "crypto/tls" |
| 5 "bufio" | |
| 6 "fmt" | 6 "fmt" |
| 7 "io" | 7 "io" |
| 8 "log" | 8 "log" |
| 9 "net/textproto" | |
| 10 "net" | 9 "net" |
| 11 "net/http" | 10 "net/http" |
| 12 "net/http/httputil" | 11 "net/http/httputil" |
| 12 "net/textproto" | |
| 13 "net/url" | 13 "net/url" |
| 14 "os" | 14 "os" |
| 15 "path" | 15 "path" |
| 16 "path/filepath" | 16 "path/filepath" |
| 17 "regexp" | 17 "regexp" |
| 18 "runtime" | 18 "runtime" |
| 19 "strconv" | 19 "strconv" |
| 20 "strings" | 20 "strings" |
| 21 | 21 "time" |
| 22 | 22 |
| 23 "d2o/fcgi" | 23 "d2o/fcgi" |
| 24 "d2o/icf" | 24 "d2o/icf" |
| 25 ) | 25 ) |
| 26 | |
| 27 type loggingMode uint8 | |
| 28 | |
| 29 const ( | |
| 30 logNone loggingMode = iota | |
| 31 logAccess | |
| 32 logVerbose | |
| 33 ) | |
| 34 | |
| 35 var ( | |
| 36 curLoggingMode loggingMode = logVerbose | |
| 37 accessLogger = log.New(os.Stderr, "", log.LstdFlags) | |
| 38 ) | |
| 39 | |
| 40 func setLoggingMode(m loggingMode) { | |
| 41 curLoggingMode = m | |
| 42 switch m { | |
| 43 case logNone: | |
| 44 log.SetOutput(io.Discard) | |
| 45 case logAccess: | |
| 46 log.SetOutput(io.Discard) | |
| 47 case logVerbose: | |
| 48 // keep standard logger output | |
| 49 } | |
| 50 } | |
| 51 | |
| 52 func accessEnabled() bool { return curLoggingMode == logAccess || curLoggingMode == logVerbose } | |
| 53 func verboseEnabled() bool { return curLoggingMode == logVerbose } | |
| 54 | |
| 55 func accessPrintf(format string, args ...any) { | |
| 56 if accessEnabled() { | |
| 57 accessLogger.Printf(format, args...) | |
| 58 } | |
| 59 } | |
| 60 | |
| 61 func verbosePrintf(format string, args ...any) { | |
| 62 if verboseEnabled() { | |
| 63 log.Printf(format, args...) | |
| 64 } | |
| 65 } | |
| 26 | 66 |
| 27 func main() { | 67 func main() { |
| 28 cfgPath := "/etc/d2obase" | 68 cfgPath := "/etc/d2obase" |
| 29 if len(os.Args) > 1 { | 69 if len(os.Args) > 1 { |
| 30 cfgPath = os.Args[1] | 70 cfgPath = os.Args[1] |
| 40 log.Fatalf("d2o: config error: %v", err) | 80 log.Fatalf("d2o: config error: %v", err) |
| 41 } | 81 } |
| 42 | 82 |
| 43 for _, d := range cfg.Abstract("d2o") { | 83 for _, d := range cfg.Abstract("d2o") { |
| 44 switch d.Key { | 84 switch d.Key { |
| 85 case "logging": | |
| 86 switch strings.ToLower(safeArg(d.Args, 0)) { | |
| 87 case "", "verbose": | |
| 88 setLoggingMode(logVerbose) | |
| 89 case "none": | |
| 90 setLoggingMode(logNone) | |
| 91 case "access": | |
| 92 setLoggingMode(logAccess) | |
| 93 default: | |
| 94 log.Fatalf("d2o: unknown logging mode %q (expected none|access|verbose)", safeArg(d.Args, 0)) | |
| 95 } | |
| 45 case "threads": | 96 case "threads": |
| 46 n, err := strconv.Atoi(safeArg(d.Args, 0)) | 97 n, err := strconv.Atoi(safeArg(d.Args, 0)) |
| 47 if err == nil && n > 0 { | 98 if err == nil && n > 0 { |
| 48 runtime.GOMAXPROCS(n) | 99 runtime.GOMAXPROCS(n) |
| 49 log.Printf("d2o: GOMAXPROCS = %d", n) | 100 verbosePrintf("d2o: GOMAXPROCS = %d", n) |
| 50 } | 101 } |
| 51 } | 102 } |
| 52 } | 103 } |
| 53 | 104 |
| 54 ports := collectPorts(cfg) | 105 ports := collectPorts(cfg) |
| 77 isTLS bool | 128 isTLS bool |
| 78 } | 129 } |
| 79 | 130 |
| 80 func (pc portConfig) listen(h http.Handler) error { | 131 func (pc portConfig) listen(h http.Handler) error { |
| 81 if !pc.isTLS { | 132 if !pc.isTLS { |
| 82 log.Printf("d2o: listening on %s (http)", pc.addr) | 133 verbosePrintf("d2o: listening on %s (http)", pc.addr) |
| 83 return http.ListenAndServe(pc.addr, h) | 134 return http.ListenAndServe(pc.addr, h) |
| 84 } | 135 } |
| 85 cert, err := tls.LoadX509KeyPair(pc.certFile, pc.keyFile) | 136 cert, err := tls.LoadX509KeyPair(pc.certFile, pc.keyFile) |
| 86 if err != nil { | 137 if err != nil { |
| 87 return fmt.Errorf("d2o: tls: %w", err) | 138 return fmt.Errorf("d2o: tls: %w", err) |
| 91 MinVersion: tls.VersionTLS12, | 142 MinVersion: tls.VersionTLS12, |
| 92 }) | 143 }) |
| 93 if err != nil { | 144 if err != nil { |
| 94 return fmt.Errorf("d2o: listen %s: %w", pc.addr, err) | 145 return fmt.Errorf("d2o: listen %s: %w", pc.addr, err) |
| 95 } | 146 } |
| 96 log.Printf("d2o: listening on %s (https)", pc.addr) | 147 verbosePrintf("d2o: listening on %s (https)", pc.addr) |
| 97 return http.Serve(ln, h) | 148 return http.Serve(ln, h) |
| 98 } | 149 } |
| 99 | 150 |
| 100 func collectPorts(cfg *icf.Config) []portConfig { | 151 func collectPorts(cfg *icf.Config) []portConfig { |
| 101 seen := make(map[string]bool) | 152 seen := make(map[string]bool) |
| 145 type handler struct { | 196 type handler struct { |
| 146 cfg *icf.Config | 197 cfg *icf.Config |
| 147 } | 198 } |
| 148 | 199 |
| 149 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 200 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 201 start := time.Now() | |
| 202 rr := &respRecorder{ResponseWriter: w, status: 0} | |
| 203 | |
| 150 host := stripPort(r.Host) | 204 host := stripPort(r.Host) |
| 151 reqPath := path.Clean(r.URL.Path) | 205 reqPath := path.Clean(r.URL.Path) |
| 152 | 206 |
| 153 dirs, caps := h.cfg.Match(host + reqPath) | 207 dirs, caps := h.cfg.Match(host + reqPath) |
| 154 if dirs == nil { | 208 if dirs == nil { |
| 155 dirs, caps = h.cfg.Match(host) | 209 dirs, caps = h.cfg.Match(host) |
| 156 } | 210 } |
| 157 if dirs == nil { | 211 if dirs == nil { |
| 158 http.Error(w, "not found", http.StatusNotFound) | 212 http.Error(rr, "not found", http.StatusNotFound) |
| 159 return | 213 rr.ensureStatus(http.StatusNotFound) |
| 160 } | 214 accessPrintf("d2o: %s %s%s -> %d %dB (%s)", r.Method, r.Host, r.URL.RequestURI(), rr.status, rr.bytes, time.Since(start).Truncate(time.Millisecond)) |
| 161 | 215 return |
| 162 h.serve(w, r, dirs, caps) | 216 } |
| 163 } | 217 |
| 164 | 218 h.serve(rr, r, dirs, caps) |
| 165 func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { | 219 rr.ensureStatus(http.StatusOK) |
| 220 accessPrintf("d2o: %s %s%s -> %d %dB (%s)", r.Method, r.Host, r.URL.RequestURI(), rr.status, rr.bytes, time.Since(start).Truncate(time.Millisecond)) | |
| 221 } | |
| 222 | |
| 223 func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, caps map[string]string) { | |
| 166 var ( | 224 var ( |
| 167 rootDir string | 225 rootDir string |
| 168 rootShow bool | 226 rootShow bool |
| 169 ndex []string | 227 ndex []string |
| 170 fcgiAddr string | 228 fcgiAddr string |
| 197 | 255 |
| 198 if rdirURL != "" { | 256 if rdirURL != "" { |
| 199 if rdirCode == 0 { | 257 if rdirCode == 0 { |
| 200 rdirCode = http.StatusFound | 258 rdirCode = http.StatusFound |
| 201 } | 259 } |
| 260 verbosePrintf("d2o: rdir %d -> %s", rdirCode, rdirURL) | |
| 202 http.Redirect(w, r, rdirURL, rdirCode) | 261 http.Redirect(w, r, rdirURL, rdirCode) |
| 203 return | 262 return |
| 204 } | 263 } |
| 205 if rprxAddr != "" { | 264 if rprxAddr != "" { |
| 265 verbosePrintf("d2o: rprx -> %s", rprxAddr) | |
| 206 serveReverseProxy(w, r, rprxAddr) | 266 serveReverseProxy(w, r, rprxAddr) |
| 207 return | 267 return |
| 208 } | 268 } |
| 209 if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) { | 269 if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) { |
| 270 verbosePrintf("d2o: fcgi -> %s (%s)", fcgiAddr, r.URL.Path) | |
| 210 if err := serveFCGI(w, r, fcgiAddr, rootDir); err != nil { | 271 if err := serveFCGI(w, r, fcgiAddr, rootDir); err != nil { |
| 211 log.Printf("d2o: fcgi error: %v", err) | 272 verbosePrintf("d2o: fcgi error: %v", err) |
| 212 http.Error(w, "gateway error", http.StatusBadGateway) | 273 http.Error(w, "gateway error", http.StatusBadGateway) |
| 213 } | 274 } |
| 214 return | 275 return |
| 215 } | 276 } |
| 216 if rootDir != "" { | 277 if rootDir != "" { |
| 217 serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat) | 278 fsPath := path.Clean(r.URL.Path) |
| 279 displayPath := fsPath | |
| 280 if user, ok := caps["user"]; ok && user != "" { | |
| 281 mount := "/~" + user | |
| 282 if fsPath == mount || strings.HasPrefix(fsPath, mount+"/") { | |
| 283 trimmed := strings.TrimPrefix(fsPath, mount) | |
| 284 if trimmed == "" { | |
| 285 trimmed = "/" | |
| 286 } | |
| 287 fsPath = trimmed | |
| 288 } | |
| 289 } | |
| 290 | |
| 291 verbosePrintf("d2o: static -> %s (%s)", rootDir, r.URL.Path) | |
| 292 serveStatic(w, r, rootDir, rootShow, ndex, fcgiAddr, fcgiPat, fsPath, displayPath) | |
| 218 return | 293 return |
| 219 } | 294 } |
| 220 | 295 |
| 221 http.Error(w, "not found", http.StatusNotFound) | 296 http.Error(w, "not found", http.StatusNotFound) |
| 222 } | 297 } |
| 224 // --- Static ----------------------------------------------------------------- | 299 // --- Static ----------------------------------------------------------------- |
| 225 | 300 |
| 226 // serveStatic serves files from rootDir. | 301 // serveStatic serves files from rootDir. |
| 227 // rootIndex == nil: directory listing forbidden (hide). | 302 // rootIndex == nil: directory listing forbidden (hide). |
| 228 // rootIndex != nil: try each as index candidate; if none found, show listing. | 303 // rootIndex != nil: try each as index candidate; if none found, show listing. |
| 229 func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string, fcgiAddr, fcgiPat string) { | 304 func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string, fcgiAddr, fcgiPat string, fsPath, displayPath string) { |
| 230 fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) | 305 fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(fsPath))) |
| 231 | 306 |
| 232 info, err := os.Stat(fpath) | 307 info, err := os.Stat(fpath) |
| 233 if os.IsNotExist(err) { | 308 if os.IsNotExist(err) { |
| 234 http.Error(w, "not found", http.StatusNotFound) | 309 http.Error(w, "not found", http.StatusNotFound) |
| 235 return | 310 return |
| 258 } | 333 } |
| 259 if !show { | 334 if !show { |
| 260 http.Error(w, "forbidden", http.StatusForbidden) | 335 http.Error(w, "forbidden", http.StatusForbidden) |
| 261 return | 336 return |
| 262 } | 337 } |
| 263 listDir(w, r, fpath, r.URL.Path) | 338 listDir(w, r, fpath, displayPath) |
| 264 return | 339 return |
| 265 } | 340 } |
| 266 | 341 |
| 267 http.ServeFile(w, r, fpath) | 342 http.ServeFile(w, r, fpath) |
| 268 } | 343 } |
| 299 if err != nil { | 374 if err != nil { |
| 300 http.Error(w, "bad gateway config", http.StatusInternalServerError) | 375 http.Error(w, "bad gateway config", http.StatusInternalServerError) |
| 301 return | 376 return |
| 302 } | 377 } |
| 303 proxy := httputil.NewSingleHostReverseProxy(u) | 378 proxy := httputil.NewSingleHostReverseProxy(u) |
| 379 if verboseEnabled() { | |
| 380 origDirector := proxy.Director | |
| 381 proxy.Director = func(req *http.Request) { | |
| 382 origDirector(req) | |
| 383 log.Printf("d2o: rprx upstream request: %s %s", req.Method, req.URL.String()) | |
| 384 } | |
| 385 proxy.ModifyResponse = func(resp *http.Response) error { | |
| 386 log.Printf("d2o: rprx upstream response: %d %s", resp.StatusCode, resp.Status) | |
| 387 return nil | |
| 388 } | |
| 389 } | |
| 304 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | 390 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { |
| 305 log.Printf("d2o: rprx error: %v", err) | 391 verbosePrintf("d2o: rprx error: %v", err) |
| 306 http.Error(w, "bad gateway", http.StatusBadGateway) | 392 http.Error(w, "bad gateway", http.StatusBadGateway) |
| 307 } | 393 } |
| 308 proxy.ServeHTTP(w, r) | 394 proxy.ServeHTTP(w, r) |
| 309 } | 395 } |
| 310 | 396 |
| 412 } | 498 } |
| 413 regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$" | 499 regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$" |
| 414 matched, err := regexp.MatchString(regPat, s) | 500 matched, err := regexp.MatchString(regPat, s) |
| 415 return err == nil && matched | 501 return err == nil && matched |
| 416 } | 502 } |
| 503 | |
| 504 type respRecorder struct { | |
| 505 http.ResponseWriter | |
| 506 status int | |
| 507 bytes int64 | |
| 508 } | |
| 509 | |
| 510 func (rr *respRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { | |
| 511 h, ok := rr.ResponseWriter.(http.Hijacker) | |
| 512 if !ok { | |
| 513 return nil, nil, fmt.Errorf("hijack not supported") | |
| 514 } | |
| 515 return h.Hijack() | |
| 516 } | |
| 517 | |
| 518 func (rr *respRecorder) Flush() { | |
| 519 if f, ok := rr.ResponseWriter.(http.Flusher); ok { | |
| 520 f.Flush() | |
| 521 } | |
| 522 } | |
| 523 | |
| 524 func (rr *respRecorder) Push(target string, opts *http.PushOptions) error { | |
| 525 if p, ok := rr.ResponseWriter.(http.Pusher); ok { | |
| 526 return p.Push(target, opts) | |
| 527 } | |
| 528 return http.ErrNotSupported | |
| 529 } | |
| 530 | |
| 531 func (rr *respRecorder) ReadFrom(src io.Reader) (int64, error) { | |
| 532 // Preserve io.Copy optimizations and count bytes. | |
| 533 rf, ok := rr.ResponseWriter.(io.ReaderFrom) | |
| 534 if !ok { | |
| 535 return io.Copy(rr, src) | |
| 536 } | |
| 537 if rr.status == 0 { | |
| 538 rr.status = http.StatusOK | |
| 539 } | |
| 540 n, err := rf.ReadFrom(src) | |
| 541 rr.bytes += n | |
| 542 return n, err | |
| 543 } | |
| 544 | |
| 545 func (rr *respRecorder) WriteHeader(code int) { | |
| 546 rr.status = code | |
| 547 rr.ResponseWriter.WriteHeader(code) | |
| 548 } | |
| 549 | |
| 550 func (rr *respRecorder) Write(p []byte) (int, error) { | |
| 551 if rr.status == 0 { | |
| 552 rr.status = http.StatusOK | |
| 553 } | |
| 554 n, err := rr.ResponseWriter.Write(p) | |
| 555 rr.bytes += int64(n) | |
| 556 return n, err | |
| 557 } | |
| 558 | |
| 559 func (rr *respRecorder) ensureStatus(defaultCode int) { | |
| 560 if rr.status == 0 { | |
| 561 rr.status = defaultCode | |
| 562 } | |
| 563 } |
