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 }