Mercurial Hosting > d2o
comparison main.go @ 3:eb705d4cdcd7
fix fcgi
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 09 Mar 2026 02:16:06 +0500 |
| parents | d19133be91ba |
| children | 07b6f06899e0 |
comparison
equal
deleted
inserted
replaced
| 2:d19133be91ba | 3:eb705d4cdcd7 |
|---|---|
| 1 package main | 1 package main |
| 2 | 2 |
| 3 import ( | 3 import ( |
| 4 "crypto/tls" | 4 "crypto/tls" |
| 5 "bufio" | |
| 5 "fmt" | 6 "fmt" |
| 7 "io" | |
| 6 "log" | 8 "log" |
| 9 "net/textproto" | |
| 7 "net" | 10 "net" |
| 8 "net/http" | 11 "net/http" |
| 9 "net/http/httputil" | 12 "net/http/httputil" |
| 10 "net/url" | 13 "net/url" |
| 11 "os" | 14 "os" |
| 14 "regexp" | 17 "regexp" |
| 15 "runtime" | 18 "runtime" |
| 16 "strconv" | 19 "strconv" |
| 17 "strings" | 20 "strings" |
| 18 | 21 |
| 22 | |
| 19 "d2o/fcgi" | 23 "d2o/fcgi" |
| 20 "d2o/icf" | 24 "d2o/icf" |
| 21 ) | 25 ) |
| 22 | 26 |
| 23 func main() { | 27 func main() { |
| 158 h.serve(w, r, dirs, caps) | 162 h.serve(w, r, dirs, caps) |
| 159 } | 163 } |
| 160 | 164 |
| 161 func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { | 165 func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { |
| 162 var ( | 166 var ( |
| 163 rootDir string | 167 rootDir string |
| 164 rootShow bool | 168 rootIndex []string // nil = hide; non-nil = show, try these index files first |
| 165 ndex []string | 169 fcgiAddr string |
| 166 fcgiAddr string | 170 fcgiPat string |
| 167 fcgiPat string | 171 rprxAddr string |
| 168 rprxAddr string | |
| 169 rdirCode int | |
| 170 rdirURL string | |
| 171 ) | 172 ) |
| 172 | 173 |
| 173 for _, d := range dirs { | 174 for _, d := range dirs { |
| 174 switch d.Key { | 175 switch d.Key { |
| 175 case "root": | 176 case "root": |
| 176 rootDir = safeArg(d.Args, 0) | 177 rootDir = safeArg(d.Args, 0) |
| 177 rootShow = safeArg(d.Args, 1) == "show" | 178 switch safeArg(d.Args, 1) { |
| 178 case "ndex": | 179 case "show": |
| 179 ndex = d.Args | 180 // root /path show [index.php index.html ...] |
| 181 // up to 12 index file candidates; default is index.html | |
| 182 if len(d.Args) >= 3 { | |
| 183 rootIndex = d.Args[2:] | |
| 184 } else { | |
| 185 rootIndex = []string{"index.html"} | |
| 186 } | |
| 187 case "hide", "": | |
| 188 rootIndex = nil | |
| 189 default: | |
| 190 log.Printf("d2o: root: unknown mode %q (want show|hide)", safeArg(d.Args, 1)) | |
| 191 } | |
| 180 case "fcgi": | 192 case "fcgi": |
| 181 fcgiAddr = safeArg(d.Args, 0) | 193 fcgiAddr = safeArg(d.Args, 0) |
| 182 fcgiPat = safeArg(d.Args, 1) | 194 fcgiPat = safeArg(d.Args, 1) |
| 183 if fcgiPat == "" { | 195 if fcgiPat == "" { |
| 184 fcgiPat = "*" | 196 fcgiPat = "*" |
| 185 } | 197 } |
| 186 case "rprx": | 198 case "rprx": |
| 187 rprxAddr = safeArg(d.Args, 0) | 199 rprxAddr = safeArg(d.Args, 0) |
| 188 case "rdir": | 200 } |
| 189 rdirCode, _ = strconv.Atoi(safeArg(d.Args, 0)) | 201 } |
| 190 rdirURL = safeArg(d.Args, 1) | 202 |
| 191 } | 203 // Priority: rprx > fcgi > static root |
| 192 } | |
| 193 | |
| 194 if rdirURL != "" { | |
| 195 if rdirCode == 0 { | |
| 196 rdirCode = http.StatusFound | |
| 197 } | |
| 198 http.Redirect(w, r, rdirURL, rdirCode) | |
| 199 return | |
| 200 } | |
| 201 if rprxAddr != "" { | 204 if rprxAddr != "" { |
| 202 serveReverseProxy(w, r, rprxAddr) | 205 serveReverseProxy(w, r, rprxAddr) |
| 203 return | 206 return |
| 204 } | 207 } |
| 205 if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) { | 208 if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) { |
| 208 http.Error(w, "gateway error", http.StatusBadGateway) | 211 http.Error(w, "gateway error", http.StatusBadGateway) |
| 209 } | 212 } |
| 210 return | 213 return |
| 211 } | 214 } |
| 212 if rootDir != "" { | 215 if rootDir != "" { |
| 213 serveStatic(w, r, rootDir, rootShow, ndex) | 216 serveStatic(w, r, rootDir, rootIndex) |
| 214 return | 217 return |
| 215 } | 218 } |
| 216 | 219 |
| 217 http.Error(w, "not found", http.StatusNotFound) | 220 http.Error(w, "not found", http.StatusNotFound) |
| 218 } | 221 } |
| 219 | 222 |
| 220 // --- Static ----------------------------------------------------------------- | 223 // --- Static ----------------------------------------------------------------- |
| 221 func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, show bool, ndex []string) { | 224 |
| 225 // serveStatic serves files from rootDir. | |
| 226 // rootIndex == nil: directory listing forbidden (hide). | |
| 227 // rootIndex != nil: try each as index candidate; if none found, show listing. | |
| 228 func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, rootIndex []string) { | |
| 222 fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) | 229 fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) |
| 223 | 230 |
| 224 info, err := os.Stat(fpath) | 231 info, err := os.Stat(fpath) |
| 225 if os.IsNotExist(err) { | 232 if os.IsNotExist(err) { |
| 226 http.Error(w, "not found", http.StatusNotFound) | 233 http.Error(w, "not found", http.StatusNotFound) |
| 230 http.Error(w, "internal error", http.StatusInternalServerError) | 237 http.Error(w, "internal error", http.StatusInternalServerError) |
| 231 return | 238 return |
| 232 } | 239 } |
| 233 | 240 |
| 234 if info.IsDir() { | 241 if info.IsDir() { |
| 235 for _, idx := range ndex { | 242 if rootIndex == nil { |
| 243 http.Error(w, "forbidden", http.StatusForbidden) | |
| 244 return | |
| 245 } | |
| 246 for _, idx := range rootIndex { | |
| 236 idxPath := filepath.Join(fpath, idx) | 247 idxPath := filepath.Join(fpath, idx) |
| 237 if _, err := os.Stat(idxPath); err == nil { | 248 if _, err := os.Stat(idxPath); err == nil { |
| 238 http.ServeFile(w, r, idxPath) | 249 http.ServeFile(w, r, idxPath) |
| 239 return | 250 return |
| 240 } | 251 } |
| 241 } | |
| 242 if !show { | |
| 243 http.Error(w, "forbidden", http.StatusForbidden) | |
| 244 return | |
| 245 } | 252 } |
| 246 listDir(w, r, fpath, r.URL.Path) | 253 listDir(w, r, fpath, r.URL.Path) |
| 247 return | 254 return |
| 248 } | 255 } |
| 249 | 256 |
| 293 | 300 |
| 294 // --- FastCGI ---------------------------------------------------------------- | 301 // --- FastCGI ---------------------------------------------------------------- |
| 295 | 302 |
| 296 func serveFCGI(w http.ResponseWriter, r *http.Request, addr, docRoot string) error { | 303 func serveFCGI(w http.ResponseWriter, r *http.Request, addr, docRoot string) error { |
| 297 network, address := parseFCGIAddr(addr) | 304 network, address := parseFCGIAddr(addr) |
| 298 conn, err := net.Dial(network, address) | 305 client, err := fcgi.Dial(network, address) |
| 299 if err != nil { | 306 if err != nil { |
| 300 return fmt.Errorf("connect %s: %w", addr, err) | 307 return fmt.Errorf("connect %s: %w", addr, err) |
| 301 } | 308 } |
| 302 defer conn.Close() | 309 defer client.Close() |
| 303 | 310 |
| 304 scriptPath := r.URL.Path | 311 scriptPath := r.URL.Path |
| 305 if docRoot != "" { | 312 if docRoot != "" { |
| 306 scriptPath = filepath.Join(docRoot, filepath.FromSlash(r.URL.Path)) | 313 scriptPath = filepath.Join(docRoot, filepath.FromSlash(r.URL.Path)) |
| 307 } | 314 } |
| 330 } | 337 } |
| 331 if r.ContentLength >= 0 { | 338 if r.ContentLength >= 0 { |
| 332 params["CONTENT_LENGTH"] = strconv.FormatInt(r.ContentLength, 10) | 339 params["CONTENT_LENGTH"] = strconv.FormatInt(r.ContentLength, 10) |
| 333 } | 340 } |
| 334 | 341 |
| 335 return fcgi.Do(w, r, conn, params) | 342 // Use Do() instead of Request() — php-fpm returns CGI response (no HTTP status line), |
| 343 // not a full HTTP response. Request() expects "HTTP/1.1 200 OK" and panics on code 0. | |
| 344 cgiReader, err := client.Do(params, r.Body) | |
| 345 if err != nil { | |
| 346 return fmt.Errorf("fcgi request: %w", err) | |
| 347 } | |
| 348 | |
| 349 // Parse CGI headers manually | |
| 350 br := bufio.NewReader(cgiReader) | |
| 351 tp := textproto.NewReader(br) | |
| 352 mime, err := tp.ReadMIMEHeader() | |
| 353 if err != nil && len(mime) == 0 { | |
| 354 return fmt.Errorf("fcgi response headers: %w", err) | |
| 355 } | |
| 356 | |
| 357 status := http.StatusOK | |
| 358 if s := mime.Get("Status"); s != "" { | |
| 359 code, _, _ := strings.Cut(s, " ") | |
| 360 if n, err := strconv.Atoi(code); err == nil && n > 0 { | |
| 361 status = n | |
| 362 } | |
| 363 mime.Del("Status") | |
| 364 } | |
| 365 | |
| 366 for k, vs := range mime { | |
| 367 for _, v := range vs { | |
| 368 w.Header().Add(k, v) | |
| 369 } | |
| 370 } | |
| 371 w.WriteHeader(status) | |
| 372 io.Copy(w, br) | |
| 373 return nil | |
| 336 } | 374 } |
| 337 | 375 |
| 338 func parseFCGIAddr(addr string) (network, address string) { | 376 func parseFCGIAddr(addr string) (network, address string) { |
| 339 if strings.HasPrefix(addr, "unix:") { | 377 if strings.HasPrefix(addr, "unix:") { |
| 340 return "unix", strings.TrimPrefix(addr, "unix:") | 378 return "unix", strings.TrimPrefix(addr, "unix:") |
