|
0
|
1 package main
|
|
|
2
|
|
|
3 import (
|
|
|
4 "crypto/tls"
|
|
3
|
5 "bufio"
|
|
0
|
6 "fmt"
|
|
3
|
7 "io"
|
|
0
|
8 "log"
|
|
3
|
9 "net/textproto"
|
|
0
|
10 "net"
|
|
|
11 "net/http"
|
|
|
12 "net/http/httputil"
|
|
|
13 "net/url"
|
|
|
14 "os"
|
|
|
15 "path"
|
|
|
16 "path/filepath"
|
|
|
17 "regexp"
|
|
|
18 "runtime"
|
|
|
19 "strconv"
|
|
|
20 "strings"
|
|
|
21
|
|
3
|
22
|
|
0
|
23 "d2o/fcgi"
|
|
|
24 "d2o/icf"
|
|
|
25 )
|
|
|
26
|
|
|
27 func main() {
|
|
|
28 cfgPath := "/etc/d2obase"
|
|
|
29 if len(os.Args) > 1 {
|
|
|
30 cfgPath = os.Args[1]
|
|
|
31 }
|
|
|
32
|
|
|
33 f, err := os.Open(cfgPath)
|
|
|
34 if err != nil {
|
|
|
35 log.Fatalf("d2o: cannot open config: %v", err)
|
|
|
36 }
|
|
|
37 cfg, err := icf.Parse(f)
|
|
|
38 f.Close()
|
|
|
39 if err != nil {
|
|
|
40 log.Fatalf("d2o: config error: %v", err)
|
|
|
41 }
|
|
|
42
|
|
|
43 for _, d := range cfg.Abstract("d2o") {
|
|
|
44 switch d.Key {
|
|
|
45 case "threads":
|
|
|
46 n, err := strconv.Atoi(safeArg(d.Args, 0))
|
|
|
47 if err == nil && n > 0 {
|
|
|
48 runtime.GOMAXPROCS(n)
|
|
|
49 log.Printf("d2o: GOMAXPROCS = %d", n)
|
|
|
50 }
|
|
|
51 }
|
|
|
52 }
|
|
|
53
|
|
|
54 ports := collectPorts(cfg)
|
|
|
55 if len(ports) == 0 {
|
|
|
56 log.Fatal("d2o: no port directives found in config")
|
|
|
57 }
|
|
|
58
|
|
|
59 h := &handler{cfg: cfg}
|
|
|
60 errCh := make(chan error, len(ports))
|
|
|
61
|
|
|
62 for _, pc := range ports {
|
|
|
63 go func(pc portConfig) {
|
|
|
64 errCh <- pc.listen(h)
|
|
|
65 }(pc)
|
|
|
66 }
|
|
|
67
|
|
|
68 log.Fatal(<-errCh)
|
|
|
69 }
|
|
|
70
|
|
|
71 // --- Port collection --------------------------------------------------------
|
|
|
72
|
|
|
73 type portConfig struct {
|
|
|
74 addr string
|
|
|
75 certFile string
|
|
|
76 keyFile string
|
|
|
77 isTLS bool
|
|
|
78 }
|
|
|
79
|
|
|
80 func (pc portConfig) listen(h http.Handler) error {
|
|
|
81 if !pc.isTLS {
|
|
|
82 log.Printf("d2o: listening on %s (http)", pc.addr)
|
|
|
83 return http.ListenAndServe(pc.addr, h)
|
|
|
84 }
|
|
|
85 cert, err := tls.LoadX509KeyPair(pc.certFile, pc.keyFile)
|
|
|
86 if err != nil {
|
|
|
87 return fmt.Errorf("d2o: tls: %w", err)
|
|
|
88 }
|
|
|
89 ln, err := tls.Listen("tcp", pc.addr, &tls.Config{
|
|
|
90 Certificates: []tls.Certificate{cert},
|
|
|
91 MinVersion: tls.VersionTLS12,
|
|
|
92 })
|
|
|
93 if err != nil {
|
|
|
94 return fmt.Errorf("d2o: listen %s: %w", pc.addr, err)
|
|
|
95 }
|
|
|
96 log.Printf("d2o: listening on %s (https)", pc.addr)
|
|
|
97 return http.Serve(ln, h)
|
|
|
98 }
|
|
|
99
|
|
|
100 func collectPorts(cfg *icf.Config) []portConfig {
|
|
|
101 seen := make(map[string]bool)
|
|
|
102 var out []portConfig
|
|
|
103
|
|
|
104 for _, b := range cfg.Blocks {
|
|
|
105 dirs := cfg.ResolveBlock(b, nil)
|
|
|
106
|
|
|
107 var cert, key string
|
|
|
108 for _, d := range dirs {
|
|
|
109 if d.Key == "tls" {
|
|
|
110 cert = safeArg(d.Args, 0)
|
|
|
111 key = safeArg(d.Args, 1)
|
|
|
112 }
|
|
|
113 }
|
|
|
114
|
|
|
115 for _, d := range dirs {
|
|
|
116 switch d.Key {
|
|
|
117 case "port":
|
|
|
118 addr := ":" + safeArg(d.Args, 0)
|
|
|
119 if !seen[addr] {
|
|
|
120 seen[addr] = true
|
|
|
121 out = append(out, portConfig{addr: addr})
|
|
|
122 }
|
|
|
123 case "port+tls":
|
|
|
124 addr := ":" + safeArg(d.Args, 0)
|
|
|
125 c := safeArg(d.Args, 1)
|
|
|
126 k := safeArg(d.Args, 2)
|
|
|
127 if c == "" {
|
|
|
128 c = cert
|
|
|
129 }
|
|
|
130 if k == "" {
|
|
|
131 k = key
|
|
|
132 }
|
|
|
133 if !seen[addr] {
|
|
|
134 seen[addr] = true
|
|
|
135 out = append(out, portConfig{addr: addr, certFile: c, keyFile: k, isTLS: true})
|
|
|
136 }
|
|
|
137 }
|
|
|
138 }
|
|
|
139 }
|
|
|
140 return out
|
|
|
141 }
|
|
|
142
|
|
|
143 // --- HTTP Handler -----------------------------------------------------------
|
|
|
144
|
|
|
145 type handler struct {
|
|
|
146 cfg *icf.Config
|
|
|
147 }
|
|
|
148
|
|
|
149 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
150 host := stripPort(r.Host)
|
|
|
151 reqPath := path.Clean(r.URL.Path)
|
|
|
152
|
|
|
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 (
|
|
3
|
167 rootDir string
|
|
|
168 rootIndex []string // nil = hide; non-nil = show, try these index files first
|
|
|
169 fcgiAddr string
|
|
|
170 fcgiPat string
|
|
|
171 rprxAddr string
|
|
0
|
172 )
|
|
|
173
|
|
|
174 for _, d := range dirs {
|
|
|
175 switch d.Key {
|
|
|
176 case "root":
|
|
|
177 rootDir = safeArg(d.Args, 0)
|
|
3
|
178 switch safeArg(d.Args, 1) {
|
|
|
179 case "show":
|
|
|
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 }
|
|
0
|
192 case "fcgi":
|
|
|
193 fcgiAddr = safeArg(d.Args, 0)
|
|
|
194 fcgiPat = safeArg(d.Args, 1)
|
|
|
195 if fcgiPat == "" {
|
|
|
196 fcgiPat = "*"
|
|
|
197 }
|
|
|
198 case "rprx":
|
|
|
199 rprxAddr = safeArg(d.Args, 0)
|
|
|
200 }
|
|
|
201 }
|
|
|
202
|
|
3
|
203 // Priority: rprx > fcgi > static root
|
|
0
|
204 if rprxAddr != "" {
|
|
|
205 serveReverseProxy(w, r, rprxAddr)
|
|
|
206 return
|
|
|
207 }
|
|
|
208 if fcgiAddr != "" && matchGlob(fcgiPat, r.URL.Path) {
|
|
|
209 if err := serveFCGI(w, r, fcgiAddr, rootDir); err != nil {
|
|
|
210 log.Printf("d2o: fcgi error: %v", err)
|
|
|
211 http.Error(w, "gateway error", http.StatusBadGateway)
|
|
|
212 }
|
|
|
213 return
|
|
|
214 }
|
|
|
215 if rootDir != "" {
|
|
3
|
216 serveStatic(w, r, rootDir, rootIndex)
|
|
0
|
217 return
|
|
|
218 }
|
|
|
219
|
|
|
220 http.Error(w, "not found", http.StatusNotFound)
|
|
|
221 }
|
|
|
222
|
|
|
223 // --- Static -----------------------------------------------------------------
|
|
3
|
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) {
|
|
0
|
229 fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path)))
|
|
|
230
|
|
|
231 info, err := os.Stat(fpath)
|
|
|
232 if os.IsNotExist(err) {
|
|
|
233 http.Error(w, "not found", http.StatusNotFound)
|
|
|
234 return
|
|
|
235 }
|
|
|
236 if err != nil {
|
|
|
237 http.Error(w, "internal error", http.StatusInternalServerError)
|
|
|
238 return
|
|
|
239 }
|
|
1
|
240
|
|
0
|
241 if info.IsDir() {
|
|
3
|
242 if rootIndex == nil {
|
|
|
243 http.Error(w, "forbidden", http.StatusForbidden)
|
|
|
244 return
|
|
|
245 }
|
|
|
246 for _, idx := range rootIndex {
|
|
1
|
247 idxPath := filepath.Join(fpath, idx)
|
|
|
248 if _, err := os.Stat(idxPath); err == nil {
|
|
|
249 http.ServeFile(w, r, idxPath)
|
|
|
250 return
|
|
|
251 }
|
|
|
252 }
|
|
0
|
253 listDir(w, r, fpath, r.URL.Path)
|
|
|
254 return
|
|
|
255 }
|
|
1
|
256
|
|
0
|
257 http.ServeFile(w, r, fpath)
|
|
|
258 }
|
|
|
259
|
|
|
260 func listDir(w http.ResponseWriter, r *http.Request, dir, urlPath string) {
|
|
|
261 entries, err := os.ReadDir(dir)
|
|
|
262 if err != nil {
|
|
|
263 http.Error(w, "cannot read directory", http.StatusInternalServerError)
|
|
|
264 return
|
|
|
265 }
|
|
|
266 w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
267 fmt.Fprintf(w, "<html><head><title>Index of %s</title></head><body>\n", urlPath)
|
|
|
268 fmt.Fprintf(w, "<h2>Index of %s</h2><hr><pre>\n", urlPath)
|
|
|
269 if urlPath != "/" {
|
|
|
270 fmt.Fprintf(w, "<a href=\"..\">..</a>\n")
|
|
|
271 }
|
|
|
272 for _, e := range entries {
|
|
|
273 name := e.Name()
|
|
|
274 if e.IsDir() {
|
|
|
275 name += "/"
|
|
|
276 }
|
|
|
277 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", path.Join(urlPath, name), name)
|
|
|
278 }
|
|
|
279 fmt.Fprintf(w, "</pre><hr><i>d2o webserver</i></body></html>")
|
|
|
280 }
|
|
|
281
|
|
|
282 // --- Reverse proxy ----------------------------------------------------------
|
|
|
283
|
|
|
284 func serveReverseProxy(w http.ResponseWriter, r *http.Request, target string) {
|
|
|
285 if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
|
|
|
286 target = "http://" + target
|
|
|
287 }
|
|
|
288 u, err := url.Parse(target)
|
|
|
289 if err != nil {
|
|
|
290 http.Error(w, "bad gateway config", http.StatusInternalServerError)
|
|
|
291 return
|
|
|
292 }
|
|
|
293 proxy := httputil.NewSingleHostReverseProxy(u)
|
|
|
294 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
|
|
295 log.Printf("d2o: rprx error: %v", err)
|
|
|
296 http.Error(w, "bad gateway", http.StatusBadGateway)
|
|
|
297 }
|
|
|
298 proxy.ServeHTTP(w, r)
|
|
|
299 }
|
|
|
300
|
|
|
301 // --- FastCGI ----------------------------------------------------------------
|
|
|
302
|
|
|
303 func serveFCGI(w http.ResponseWriter, r *http.Request, addr, docRoot string) error {
|
|
|
304 network, address := parseFCGIAddr(addr)
|
|
3
|
305 client, err := fcgi.Dial(network, address)
|
|
0
|
306 if err != nil {
|
|
|
307 return fmt.Errorf("connect %s: %w", addr, err)
|
|
|
308 }
|
|
3
|
309 defer client.Close()
|
|
0
|
310
|
|
|
311 scriptPath := r.URL.Path
|
|
|
312 if docRoot != "" {
|
|
|
313 scriptPath = filepath.Join(docRoot, filepath.FromSlash(r.URL.Path))
|
|
|
314 }
|
|
|
315
|
|
|
316 params := map[string]string{
|
|
|
317 "REQUEST_METHOD": r.Method,
|
|
|
318 "SCRIPT_FILENAME": scriptPath,
|
|
|
319 "SCRIPT_NAME": r.URL.Path,
|
|
|
320 "REQUEST_URI": r.URL.RequestURI(),
|
|
|
321 "QUERY_STRING": r.URL.RawQuery,
|
|
|
322 "SERVER_PROTOCOL": r.Proto,
|
|
|
323 "SERVER_NAME": stripPort(r.Host),
|
|
|
324 "DOCUMENT_ROOT": docRoot,
|
|
|
325 "GATEWAY_INTERFACE": "CGI/1.1",
|
|
|
326 "SERVER_SOFTWARE": "d2o/1.0",
|
|
|
327 }
|
|
|
328 if r.TLS != nil {
|
|
|
329 params["HTTPS"] = "on"
|
|
|
330 }
|
|
|
331 for k, vs := range r.Header {
|
|
|
332 key := "HTTP_" + strings.ToUpper(strings.ReplaceAll(k, "-", "_"))
|
|
|
333 params[key] = strings.Join(vs, ", ")
|
|
|
334 }
|
|
|
335 if ct := r.Header.Get("Content-Type"); ct != "" {
|
|
|
336 params["CONTENT_TYPE"] = ct
|
|
|
337 }
|
|
|
338 if r.ContentLength >= 0 {
|
|
|
339 params["CONTENT_LENGTH"] = strconv.FormatInt(r.ContentLength, 10)
|
|
|
340 }
|
|
|
341
|
|
3
|
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
|
|
0
|
374 }
|
|
|
375
|
|
|
376 func parseFCGIAddr(addr string) (network, address string) {
|
|
|
377 if strings.HasPrefix(addr, "unix:") {
|
|
|
378 return "unix", strings.TrimPrefix(addr, "unix:")
|
|
|
379 }
|
|
|
380 return "tcp", addr
|
|
|
381 }
|
|
|
382
|
|
|
383 // --- Helpers ----------------------------------------------------------------
|
|
|
384
|
|
|
385 func stripPort(host string) string {
|
|
|
386 if h, _, err := net.SplitHostPort(host); err == nil {
|
|
|
387 return h
|
|
|
388 }
|
|
|
389 return host
|
|
|
390 }
|
|
|
391
|
|
|
392 func safeArg(args []string, i int) string {
|
|
|
393 if i < len(args) {
|
|
|
394 return args[i]
|
|
|
395 }
|
|
|
396 return ""
|
|
|
397 }
|
|
|
398
|
|
|
399 func matchGlob(pattern, s string) bool {
|
|
|
400 if pattern == "*" {
|
|
|
401 return true
|
|
|
402 }
|
|
|
403 regPat := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), regexp.QuoteMeta("*"), ".*") + "$"
|
|
|
404 matched, err := regexp.MatchString(regPat, s)
|
|
|
405 return err == nil && matched
|
|
2
|
406 } |