summaryrefslogtreecommitdiff
path: root/api/dns/dns.go
diff options
context:
space:
mode:
Diffstat (limited to 'api/dns/dns.go')
-rw-r--r--api/dns/dns.go147
1 files changed, 147 insertions, 0 deletions
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 <username>.<zone> 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 <username>.<zone> 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.<user>.<zone>.").
+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
+ }
+}