comparison main.go @ 0:48bdab3eec8a

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