diff --git a/Dockerfile b/Dockerfile index d859d8d..a46f6c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ RUN go build -o /app/hatecomputers EXPOSE 8080 -CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler"] +CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler", "--dns", "--dns-port", "8053", "--dns-recursion", "1.1.1.1:53,1.0.0.1:53"] diff --git a/api/dns.go b/api/dns.go index 5123acc..0205f5d 100644 --- a/api/dns.go +++ b/api/dns.go @@ -1,6 +1,7 @@ package api import ( + "database/sql" "log" "net/http" "strconv" @@ -8,16 +9,31 @@ import ( "git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare" "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/utils" ) -const MAX_USER_RECORDS = 20 +const MAX_USER_RECORDS = 65 type FormError struct { Errors []string } -func userCanFuckWithDNSRecord(user *database.User, record *database.DNSRecord) bool { - return user.ID == record.UserID && (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username)) +func userCanFuckWithDNSRecord(dbConn *sql.DB, user *database.User, record *database.DNSRecord) bool { + ownedByUser := (user.ID == record.UserID) + + if !record.Internal { + publicallyOwnedByUser := (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username)) + return ownedByUser && publicallyOwnedByUser + } + + owner, err := database.FindFirstDomainOwnerId(dbConn, record.Name) + if err != nil { + log.Println(err) + return false + } + + userIsOwnerOfDomain := owner == user.ID + return ownedByUser && userIsOwnerOfDomain } func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain { @@ -40,8 +56,15 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res Errors: []string{}, } + internal := req.FormValue("internal") == "on" name := req.FormValue("name") + if internal && !strings.HasSuffix(name, ".") { + name += "." + } + recordType := req.FormValue("type") + recordType = strings.ToUpper(recordType) + recordContent := req.FormValue("content") ttl := req.FormValue("ttl") ttlNum, err := strconv.Atoi(ttl) @@ -50,11 +73,12 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res } dnsRecord := &database.DNSRecord{ - UserID: context.User.ID, - Name: name, - Type: recordType, - Content: recordContent, - TTL: ttlNum, + UserID: context.User.ID, + Name: name, + Type: recordType, + Content: recordContent, + TTL: ttlNum, + Internal: internal, } dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) @@ -67,18 +91,22 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res formErrors.Errors = append(formErrors.Errors, "max records reached") } - if !userCanFuckWithDNSRecord(context.User, dnsRecord) { - formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username) + if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord) { + formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains") } if len(formErrors.Errors) == 0 { - cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord) - if err != nil { - log.Println(err) - formErrors.Errors = append(formErrors.Errors, err.Error()) - } + if dnsRecord.Internal { + dnsRecord.ID = utils.RandomId() + } else { + cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord) + if err != nil { + log.Println(err) + formErrors.Errors = append(formErrors.Errors, err.Error()) + } - dnsRecord.ID = cloudflareRecordId + dnsRecord.ID = cloudflareRecordId + } } if len(formErrors.Errors) == 0 { @@ -113,16 +141,18 @@ func DeleteDNSRecordContinuation(context *RequestContext, req *http.Request, res return failure(context, req, resp) } - if !userCanFuckWithDNSRecord(context.User, record) { + if !userCanFuckWithDNSRecord(context.DBConn, context.User, record) { resp.WriteHeader(http.StatusUnauthorized) return failure(context, req, resp) } - err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId) - if err != nil { - log.Println(err) - resp.WriteHeader(http.StatusInternalServerError) - return failure(context, req, resp) + if !record.Internal { + err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId) + if err != nil { + log.Println(err) + resp.WriteHeader(http.StatusInternalServerError) + return failure(context, req, resp) + } } err = database.DeleteDNSRecord(context.DBConn, recordId) diff --git a/args/args.go b/args/args.go index fe06f28..3be0abd 100644 --- a/args/args.go +++ b/args/args.go @@ -15,25 +15,35 @@ type Arguments struct { StaticPath string CloudflareToken string CloudflareZone string - Port int - Server bool - Migrate bool - Scheduler bool + Migrate bool + Scheduler bool + + Port int + Server bool OauthConfig *oauth2.Config OauthUserInfoURI string + + Dns bool + DnsRecursion []string + DnsPort int } func GetArgs() (*Arguments, error) { - port := flag.Int("port", 8080, "Port to listen on") databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database") templatePath := flag.String("template-path", "./templates", "Path to the template directory") staticPath := flag.String("static-path", "./static", "Path to the static directory") scheduler := flag.Bool("scheduler", false, "Run scheduled jobs via cron") - server := flag.Bool("server", false, "Run the server") migrate := flag.Bool("migrate", false, "Run the migrations") + port := flag.Int("port", 8080, "Port to listen on") + server := flag.Bool("server", false, "Run the server") + + dns := flag.Bool("dns", false, "Run DNS resolver") + dnsRecursion := flag.String("dns-recursion", "1.1.1.1:53,1.0.0.1:53", "Comma separated list of DNS resolvers") + dnsPort := flag.Int("dns-port", 8053, "Port to listen on for DNS resolver") + flag.Parse() cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN") @@ -86,6 +96,9 @@ func GetArgs() (*Arguments, error) { Server: *server, Migrate: *migrate, Scheduler: *scheduler, + Dns: *dns, + DnsRecursion: strings.Split(*dnsRecursion, ","), + DnsPort: *dnsPort, OauthConfig: oauthConfig, OauthUserInfoURI: oauthUserInfoURI, diff --git a/database/dns.go b/database/dns.go index bb5c1ef..568653d 100644 --- a/database/dns.go +++ b/database/dns.go @@ -2,8 +2,10 @@ package database import ( "database/sql" + "fmt" _ "github.com/mattn/go-sqlite3" "log" + "strings" "time" ) @@ -14,6 +16,7 @@ type DNSRecord struct { Type string `json:"type"` Content string `json:"content"` TTL int `json:"ttl"` + Internal bool `json:"internal"` CreatedAt time.Time `json:"created_at"` } @@ -29,7 +32,7 @@ func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { var records []DNSRecord for rows.Next() { var record DNSRecord - err := rows.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.CreatedAt) + err := rows.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.Internal, &record.CreatedAt) if err != nil { return nil, err } @@ -43,7 +46,7 @@ func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) { log.Println("saving dns record", record) record.CreatedAt = time.Now() - _, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.CreatedAt) + _, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, internal, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.Internal, record.CreatedAt) if err != nil { return nil, err @@ -56,7 +59,7 @@ func GetDNSRecord(db *sql.DB, recordID string) (*DNSRecord, error) { row := db.QueryRow("SELECT * FROM dns_records WHERE id = ?", recordID) var record DNSRecord - err := row.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.CreatedAt) + err := row.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.Internal, &record.CreatedAt) if err != nil { return nil, err } @@ -72,3 +75,53 @@ func DeleteDNSRecord(db *sql.DB, recordID string) error { } return nil } + +func FindFirstDomainOwnerId(db *sql.DB, domain string) (string, error) { + log.Println("finding domain owner for", domain) + + ownerID := "" + parts := strings.Split(domain, ".") + if len(parts) < 2 { + return ownerID, fmt.Errorf("invalid domain; must have at least two parts") + } + + for ownerID == "" { + row := db.QueryRow("SELECT user_id FROM domain_owners WHERE domain = ?", strings.Join(parts, ".")) + err := row.Scan(&ownerID) + + if err != nil { + if len(parts) == 1 { + break + } + parts = parts[1:] + } + } + + if ownerID == "" { + return ownerID, fmt.Errorf("no owner found for domain") + } + return ownerID, nil +} + +func FindDNSRecords(dbConn *sql.DB, name string, qtype string) ([]DNSRecord, error) { + log.Println("finding dns record(s) for", name, qtype) + + rows, err := dbConn.Query("SELECT * FROM dns_records WHERE name = ? AND type = ?", name, qtype) + if err != nil { + return nil, err + } + + defer rows.Close() + + var records []DNSRecord + for rows.Next() { + var record DNSRecord + err := rows.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.Internal, &record.CreatedAt) + if err != nil { + return nil, err + } + records = append(records, record) + } + + return records, nil +} diff --git a/database/migrate.go b/database/migrate.go index b75c123..de1db4c 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -57,6 +57,7 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) { type TEXT NOT NULL, content TEXT NOT NULL, ttl INTEGER NOT NULL, + internal BOOLEAN NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);`) if err != nil { @@ -65,6 +66,26 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) { return dbConn, nil } +func MigrateDomainOwners(dbConn *sql.DB) (*sql.DB, error) { + log.Println("migrating domain_owners table") + + _, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS domain_owners ( + user_id INTEGER NOT NULL, + domain TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + );`) + if err != nil { + return dbConn, err + } + + _, err = dbConn.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_domain_owners_domain ON domain_owners (domain);`) + if err != nil { + return dbConn, err + } + return dbConn, nil +} + func MigrateUserSessions(dbConn *sql.DB) (*sql.DB, error) { log.Println("migrating user_sessions table") @@ -88,6 +109,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) { MigrateUsers, MigrateUserSessions, MigrateApiKeys, + MigrateDomainOwners, MigrateDNSRecords, } diff --git a/dns/server.go b/dns/server.go new file mode 100644 index 0000000..63bb067 --- /dev/null +++ b/dns/server.go @@ -0,0 +1,110 @@ +package dns + +import ( + "database/sql" + "fmt" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/args" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" + "github.com/miekg/dns" + "log" +) + +const MAX_RECURSION = 10 + +func resolveRecursive(dbConn *sql.DB, dnsResolvers []string, domain string, qtype uint16, maxDepth int) ([]dns.RR, error) { + if maxDepth == 0 { + return nil, fmt.Errorf("too much recursion") + } + + internalCnames, err := database.FindDNSRecords(dbConn, domain, "CNAME") + if err != nil { + return nil, err + } + + answers := []dns.RR{} + for _, record := range internalCnames { + cnameRecursive, _ := resolveRecursive(dbConn, dnsResolvers, record.Content, qtype, maxDepth-1) + answers = append(answers, cnameRecursive...) + } + + qtypeName := dns.TypeToString[qtype] + if qtypeName == "" { + return nil, fmt.Errorf("invalid query type %d", qtype) + } + + typeDnsRecords, err := database.FindDNSRecords(dbConn, domain, qtypeName) + if err != nil { + return nil, err + } + for _, record := range typeDnsRecords { + answer, err := dns.NewRR(fmt.Sprintf("%s %d IN %s %s", record.Name, record.TTL, record.Type, record.Content)) + if err != nil { + return nil, err + } + answers = append(answers, answer) + } + + if len(answers) > 0 { + // base case; we found the answer + return answers, nil + } + + message := new(dns.Msg) + message.SetQuestion(dns.Fqdn(domain), qtype) + message.RecursionDesired = true + + client := new(dns.Client) + + i := 0 + in, _, err := client.Exchange(message, dnsResolvers[i]) + for err != nil { + i += 1 + if i == len(dnsResolvers) { + log.Println(err) + return nil, err + } + in, _, err = client.Exchange(message, dnsResolvers[i]) + } + + answers = append(answers, in.Answer...) + return answers, nil +} + +type DnsHandler struct { + DnsResolvers []string + DbConn *sql.DB +} + +func (h *DnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + msg := new(dns.Msg) + msg.SetReply(r) + msg.Authoritative = true + + for _, question := range r.Question { + answers, err := resolveRecursive(h.DbConn, h.DnsResolvers, question.Name, question.Qtype, MAX_RECURSION) + if err != nil { + fmt.Println(err) + continue + } + msg.Answer = append(msg.Answer, answers...) + } + + log.Println(msg.Answer) + w.WriteMsg(msg) +} + +func MakeServer(argv *args.Arguments, dbConn *sql.DB) *dns.Server { + handler := &DnsHandler{ + DnsResolvers: argv.DnsRecursion, + DbConn: dbConn, + } + addr := fmt.Sprintf(":%d", argv.DnsPort) + + return &dns.Server{ + Addr: addr, + Net: "udp", + Handler: handler, + UDPSize: 65535, + ReusePort: true, + } +} diff --git a/go.mod b/go.mod index 4a8b362..69cff5b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-co-op/gocron v1.37.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.22 + github.com/miekg/dns v1.1.58 golang.org/x/oauth2 v0.18.0 ) @@ -14,7 +15,10 @@ require ( github.com/google/uuid v1.4.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect go.uber.org/atomic v1.9.0 // indirect + golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 3a7a0bb..9f33b5d 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -41,15 +43,23 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/main.go b/main.go index 4b92b1b..2991821 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "git.hatecomputers.club/hatecomputers/hatecomputers.club/api" "git.hatecomputers.club/hatecomputers/hatecomputers.club/args" "git.hatecomputers.club/hatecomputers/hatecomputers.club/database" + "git.hatecomputers.club/hatecomputers/hatecomputers.club/dns" "git.hatecomputers.club/hatecomputers/hatecomputers.club/scheduler" "github.com/joho/godotenv" ) @@ -40,10 +41,27 @@ func main() { if argv.Server { server := api.MakeServer(argv, dbConn) - err = server.ListenAndServe() - if err != nil { - log.Fatal(err) - } - log.Println("🚀🚀 server listening on port", argv.Port) + log.Println("🚀🚀 API listening on port", argv.Port) + + go func() { + err = server.ListenAndServe() + if err != nil { + log.Fatal(err) + } + }() } + + if argv.Dns { + server := dns.MakeServer(argv, dbConn) + log.Println("🚀🚀 DNS resolver listening on port", argv.DnsPort) + go func() { + err = server.ListenAndServe() + if err != nil { + log.Fatal(err) + } + }() + } + + runForever := make(chan struct{}) + <-runForever } diff --git a/templates/api_keys.html b/templates/api_keys.html index 93eebd5..0aa3094 100644 --- a/templates/api_keys.html +++ b/templates/api_keys.html @@ -28,13 +28,12 @@

Add An API Key


- - - {{ if .FormError }} + {{ if .FormError }} {{ if (len .FormError.Errors) }} - {{ range $error := .FormError.Errors }} -
{{ $error }}
- {{ end }} + {{ range $error := .FormError.Errors }} +
{{ $error }}
{{ end }} - {{ end }} + {{ end }} + {{ end }} + {{ end }} diff --git a/templates/dns.html b/templates/dns.html index e317d05..8ea7bee 100644 --- a/templates/dns.html +++ b/templates/dns.html @@ -5,11 +5,12 @@ Name Content TTL + Internal Delete {{ if (eq (len .DNSRecords) 0) }} - No DNS records found + No DNS records found {{ end }} {{ range $record := .DNSRecords }} @@ -17,6 +18,7 @@ {{ $record.Type }} {{ $record.Name }} {{ $record.Content }} + {{ $record.Internal }} {{ $record.TTL }}
@@ -64,14 +66,26 @@ value="{{ .RecordForm.TTL }}" {{ end }} required /> + + + + + {{ if .FormError }} + {{ if (len .FormError.Errors) }} + {{ range $error := .FormError.Errors }} +
{{ $error }}
+ {{ end }} + {{ end }} + {{ end }}
- {{ if .FormError }} - {{ if (len .FormError.Errors) }} - {{ range $error := .FormError.Errors }} -
{{ $error }}
- {{ end }} - {{ end }} - {{ end }} + {{ end }}