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:")