From cfc0d34a584254a9ed76620951c810771aff6f3b Mon Sep 17 00:00:00 2001 From: Alexander Hunt Date: Sun, 21 Jun 2026 12:43:31 -0700 Subject: STUFF --- api/dns/dns.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 api/dns/dns.go (limited to 'api/dns') diff --git a/api/dns/dns.go b/api/dns/dns.go new file mode 100644 index 0000000..4cda557 --- /dev/null +++ b/api/dns/dns.go @@ -0,0 +1,147 @@ +package dns + +import ( + "log" + "net/http" + "strconv" + "strings" + + "penguins.lan/api/types" + "penguins.lan/database" + "penguins.lan/utils" +) + +const MaxUserRecords = 50 + +var allowedTypes = map[string]bool{"A": true, "AAAA": true, "CNAME": true, "TXT": true} + +// ListDNSRecordsContinuation loads the current user's records for the editor. +func ListDNSRecordsContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + if context.User == nil { + return failure(context, req, resp) + } + records, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) + if err != nil { + log.Println("dns: list records failed:", err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + (*context.TemplateData)["DNSRecords"] = records + return success(context, req, resp) + } +} + +// CreateDNSRecordContinuation validates and upserts a record. Names are confined +// to the user's own . subtree; bare/relative names are expanded +// to it for convenience. +func CreateDNSRecordContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + fail := func(msg string, code int) types.ContinuationChain { + resp.WriteHeader(code) + (*context.TemplateData)["Error"] = types.BannerMessages{Messages: []string{msg}} + return failure(context, req, resp) + } + + recordType := strings.ToUpper(strings.TrimSpace(req.FormValue("type"))) + if !allowedTypes[recordType] { + return fail("type must be one of A, AAAA, CNAME, TXT", http.StatusBadRequest) + } + + name, ok := qualifyName(req.FormValue("name"), context.User.Username, context.Args.DnsZone) + if !ok { + return fail("name must be under "+context.User.Username+"."+context.Args.DnsZone, http.StatusBadRequest) + } + + content := strings.TrimSpace(req.FormValue("content")) + if content == "" { + return fail("content can't be empty", http.StatusBadRequest) + } + + ttl := 300 + if raw := strings.TrimSpace(req.FormValue("ttl")); raw != "" { + n, err := strconv.Atoi(raw) + if err != nil || n < 0 { + return fail("ttl must be a non-negative number", http.StatusBadRequest) + } + ttl = n + } + + count, err := database.CountUserDNSRecords(context.DBConn, context.User.ID) + if err != nil { + return fail("something broke, try again", http.StatusInternalServerError) + } + if count >= MaxUserRecords { + return fail("you've hit the record limit", http.StatusTooManyRequests) + } + + // A name may hold at most one CNAME; replace any existing one so + // repointing the primary (or re-running the wizard) doesn't duplicate. + if recordType == "CNAME" { + if err := database.DeleteUserRecordsByNameType(context.DBConn, context.User.ID, name, "CNAME"); err != nil { + return fail("something broke, try again", http.StatusInternalServerError) + } + } + + record := &database.DNSRecord{ + ID: utils.RandomId(), + UserID: context.User.ID, + Name: name, + Type: recordType, + Content: content, + TTL: ttl, + } + if _, err := database.SaveDNSRecord(context.DBConn, record); err != nil { + log.Println("dns: save record failed:", err) + return fail("couldn't save that record", http.StatusInternalServerError) + } + + http.Redirect(resp, req, "/profile", http.StatusSeeOther) + return success(context, req, resp) + } +} + +// DeleteDNSRecordContinuation removes a record the caller owns. +func DeleteDNSRecordContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { + return func(success types.Continuation, failure types.Continuation) types.ContinuationChain { + record, err := database.GetDNSRecord(context.DBConn, req.FormValue("id")) + if err != nil { + resp.WriteHeader(http.StatusNotFound) + return failure(context, req, resp) + } + if record.UserID != context.User.ID { + resp.WriteHeader(http.StatusUnauthorized) + return failure(context, req, resp) + } + if err := database.DeleteDNSRecord(context.DBConn, record.ID); err != nil { + log.Println("dns: delete record failed:", err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } + http.Redirect(resp, req, "/profile", http.StatusSeeOther) + return success(context, req, resp) + } +} + +// qualifyName normalizes a record name to a fully-qualified name (trailing dot) +// confined to the user's . subtree. A name already under the +// subtree is kept; a name elsewhere in the zone is rejected; anything else is +// treated as relative to the subtree (so "www" -> "www..."). +func qualifyName(raw, username, zone string) (string, bool) { + raw = strings.ToLower(strings.TrimSpace(raw)) + raw = strings.TrimSuffix(raw, ".") + if raw == "" { + return "", false + } + zone = strings.ToLower(zone) + userDomain := username + "." + zone + + switch { + case raw == userDomain || strings.HasSuffix(raw, "."+userDomain): + return raw + ".", true + case raw == zone || strings.HasSuffix(raw, "."+zone): + return "", false // in the zone but not the user's subtree + default: + return raw + "." + userDomain + ".", true + } +} -- cgit v1.3