package web import ( "fmt" "net" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/owen/vibedns/internal/app" ) // listen binds the management address, explaining common failures. func listen(addr string) (net.Listener, error) { ln, err := net.Listen("tcp", addr) if err == nil { return ln, nil } msg := err.Error() switch { case strings.Contains(msg, "address already in use"): return nil, fmt.Errorf("management interface cannot bind %s: the address is already in use", addr) case strings.Contains(msg, "permission denied"): return nil, fmt.Errorf("management interface cannot bind %s: permission denied "+ "(ports below 1024 need root or CAP_NET_BIND_SERVICE)", addr) default: return nil, fmt.Errorf("management interface cannot bind %s: %w", addr, err) } } func parseURL(s string) (*url.URL, error) { return url.Parse(s) } // httpLimiter is a coarse fixed-window request counter per client address. type httpLimiter struct { mu sync.Mutex windows map[string]*window lastGC time.Time } type window struct { count int start time.Time } func newHTTPLimiter() *httpLimiter { return &httpLimiter{windows: map[string]*window{}} } func (l *httpLimiter) allow(key string, perMinute int) bool { if perMinute <= 0 { return true } now := time.Now() l.mu.Lock() defer l.mu.Unlock() if now.Sub(l.lastGC) > 5*time.Minute { for k, w := range l.windows { if now.Sub(w.start) > 2*time.Minute { delete(l.windows, k) } } l.lastGC = now } w, ok := l.windows[key] if !ok || now.Sub(w.start) >= time.Minute { l.windows[key] = &window{count: 1, start: now} return true } w.count++ return w.count <= perMinute } // --- form helpers ------------------------------------------------------- // formString reads a trimmed form value. func formString(r *http.Request, key string) string { return strings.TrimSpace(r.FormValue(key)) } // formBool reads a checkbox. Boolean fields pair a hidden false value with a // checkbox true value, so a checked field arrives as ["false", "true"]. Treat // the field as true when any submitted value is true instead of relying on // FormValue, which only returns the first value. func formBool(r *http.Request, key string) bool { if err := r.ParseForm(); err != nil { return false } for _, raw := range r.Form[key] { switch strings.ToLower(strings.TrimSpace(raw)) { case "1", "true", "on", "yes": return true } } return false } // formBoolPtr returns nil when the field was not submitted at all. func formBoolPtr(r *http.Request, key string) *bool { if err := r.ParseForm(); err != nil { return nil } if _, ok := r.Form[key]; !ok { return nil } v := formBool(r, key) return &v } // formInt reads an integer form field, falling back to def when empty. func formInt(r *http.Request, key string, def int) int { raw := formString(r, key) if raw == "" { return def } v, err := strconv.Atoi(raw) if err != nil { return def } return v } // formUint32 reads an unsigned form field. func formUint32(r *http.Request, key string, def uint32) uint32 { raw := formString(r, key) if raw == "" { return def } v, err := strconv.ParseUint(raw, 10, 32) if err != nil { return def } return uint32(v) } // formUint32Ptr returns nil when the field is empty, which distinguishes // "inherit the zone default" from an explicit value. func formUint32Ptr(r *http.Request, key string) *uint32 { raw := formString(r, key) if raw == "" { return nil } v, err := strconv.ParseUint(raw, 10, 32) if err != nil { return nil } out := uint32(v) return &out } // formInt64s reads a repeated integer field, such as a set of checkboxes. func formInt64s(r *http.Request, key string) []int64 { var out []int64 for _, raw := range r.Form[key] { v, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err == nil && v > 0 { out = append(out, v) } } return out } // parseForm parses the request body, reporting an oversized upload clearly. func parseForm(r *http.Request) error { if err := r.ParseForm(); err != nil { if strings.Contains(err.Error(), "http: request body too large") { return app.Invalid("The submitted data is larger than the configured upload limit.") } return app.Invalid("The form data could not be read: %v", err) } return nil } // parseMultipart parses a file upload up to the configured limit. func parseMultipart(r *http.Request, maxMemoryMB int) error { if maxMemoryMB <= 0 { maxMemoryMB = 8 } if err := r.ParseMultipartForm(int64(maxMemoryMB) * 1024 * 1024); err != nil { if strings.Contains(err.Error(), "http: request body too large") { return app.Invalid("The uploaded file is larger than the configured upload limit.") } return app.Invalid("The upload could not be read: %v", err) } return nil } // pagination computes offsets from query parameters. type pagination struct { Page int PerPage int Offset int Total int Pages int HasPrev bool HasNext bool From int To int } func newPagination(r *http.Request, defaultPerPage int) pagination { page := formInt(r, "page", 1) if page < 1 { page = 1 } per := formInt(r, "per_page", defaultPerPage) switch { case per < 10: per = 10 case per > 500: per = 500 } return pagination{Page: page, PerPage: per, Offset: (page - 1) * per} } // withTotal fills in the derived fields once the row count is known. func (p pagination) withTotal(total int) pagination { p.Total = total p.Pages = (total + p.PerPage - 1) / p.PerPage if p.Pages < 1 { p.Pages = 1 } p.HasPrev = p.Page > 1 p.HasNext = p.Page < p.Pages p.From = p.Offset + 1 p.To = p.Offset + p.PerPage if p.To > total { p.To = total } if total == 0 { p.From = 0 } return p } // parseInt64 parses a numeric query parameter. func parseInt64(s string) (int64, error) { return strconv.ParseInt(strings.TrimSpace(s), 10, 64) } // parseDate reads a date or datetime filter from a form field. endOfDay // extends a bare date to 23:59:59 so a "to" filter includes that whole day. func parseDate(s string, endOfDay bool) (time.Time, bool) { s = strings.TrimSpace(s) if s == "" { return time.Time{}, false } for _, layout := range []string{"2006-01-02T15:04", "2006-01-02 15:04:05", "2006-01-02"} { t, err := time.ParseInLocation(layout, s, time.Local) if err != nil { continue } if endOfDay && layout == "2006-01-02" { t = t.Add(24*time.Hour - time.Second) } return t, true } return time.Time{}, false }