internal recursive dns server #2
			
				
			
		
		
		
	|  | @ -11,4 +11,4 @@ RUN go build -o /app/hatecomputers | ||||||
| 
 | 
 | ||||||
| EXPOSE 8080 | 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"] | ||||||
|  |  | ||||||
							
								
								
									
										74
									
								
								api/dns.go
								
								
								
								
							
							
						
						
									
										74
									
								
								api/dns.go
								
								
								
								
							|  | @ -1,6 +1,7 @@ | ||||||
| package api | package api | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"database/sql" | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | @ -8,16 +9,31 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare" | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare" | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" | 	"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 { | type FormError struct { | ||||||
| 	Errors []string | 	Errors []string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func userCanFuckWithDNSRecord(user *database.User, record *database.DNSRecord) bool { | func userCanFuckWithDNSRecord(dbConn *sql.DB, user *database.User, record *database.DNSRecord) bool { | ||||||
| 	return user.ID == record.UserID && (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username)) | 	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 { | 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{}, | 			Errors: []string{}, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		internal := req.FormValue("internal") == "on" | ||||||
| 		name := req.FormValue("name") | 		name := req.FormValue("name") | ||||||
|  | 		if internal && !strings.HasSuffix(name, ".") { | ||||||
|  | 			name += "." | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		recordType := req.FormValue("type") | 		recordType := req.FormValue("type") | ||||||
|  | 		recordType = strings.ToUpper(recordType) | ||||||
|  | 
 | ||||||
| 		recordContent := req.FormValue("content") | 		recordContent := req.FormValue("content") | ||||||
| 		ttl := req.FormValue("ttl") | 		ttl := req.FormValue("ttl") | ||||||
| 		ttlNum, err := strconv.Atoi(ttl) | 		ttlNum, err := strconv.Atoi(ttl) | ||||||
|  | @ -50,11 +73,12 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		dnsRecord := &database.DNSRecord{ | 		dnsRecord := &database.DNSRecord{ | ||||||
| 			UserID:  context.User.ID, | 			UserID:   context.User.ID, | ||||||
| 			Name:    name, | 			Name:     name, | ||||||
| 			Type:    recordType, | 			Type:     recordType, | ||||||
| 			Content: recordContent, | 			Content:  recordContent, | ||||||
| 			TTL:     ttlNum, | 			TTL:      ttlNum, | ||||||
|  | 			Internal: internal, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID) | 		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") | 			formErrors.Errors = append(formErrors.Errors, "max records reached") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !userCanFuckWithDNSRecord(context.User, dnsRecord) { | 		if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord) { | ||||||
| 			formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username) | 			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 { | 		if len(formErrors.Errors) == 0 { | ||||||
| 			cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord) | 			if dnsRecord.Internal { | ||||||
| 			if err != nil { | 				dnsRecord.ID = utils.RandomId() | ||||||
| 				log.Println(err) | 			} else { | ||||||
| 				formErrors.Errors = append(formErrors.Errors, err.Error()) | 				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 { | 		if len(formErrors.Errors) == 0 { | ||||||
|  | @ -113,16 +141,18 @@ func DeleteDNSRecordContinuation(context *RequestContext, req *http.Request, res | ||||||
| 			return failure(context, req, resp) | 			return failure(context, req, resp) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !userCanFuckWithDNSRecord(context.User, record) { | 		if !userCanFuckWithDNSRecord(context.DBConn, context.User, record) { | ||||||
| 			resp.WriteHeader(http.StatusUnauthorized) | 			resp.WriteHeader(http.StatusUnauthorized) | ||||||
| 			return failure(context, req, resp) | 			return failure(context, req, resp) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId) | 		if !record.Internal { | ||||||
| 		if err != nil { | 			err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId) | ||||||
| 			log.Println(err) | 			if err != nil { | ||||||
| 			resp.WriteHeader(http.StatusInternalServerError) | 				log.Println(err) | ||||||
| 			return failure(context, req, resp) | 				resp.WriteHeader(http.StatusInternalServerError) | ||||||
|  | 				return failure(context, req, resp) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		err = database.DeleteDNSRecord(context.DBConn, recordId) | 		err = database.DeleteDNSRecord(context.DBConn, recordId) | ||||||
|  |  | ||||||
							
								
								
									
										25
									
								
								args/args.go
								
								
								
								
							
							
						
						
									
										25
									
								
								args/args.go
								
								
								
								
							|  | @ -15,25 +15,35 @@ type Arguments struct { | ||||||
| 	StaticPath      string | 	StaticPath      string | ||||||
| 	CloudflareToken string | 	CloudflareToken string | ||||||
| 	CloudflareZone  string | 	CloudflareZone  string | ||||||
| 	Port            int |  | ||||||
| 	Server          bool |  | ||||||
| 	Migrate         bool |  | ||||||
| 	Scheduler       bool |  | ||||||
| 
 | 
 | ||||||
|  | 	Migrate   bool | ||||||
|  | 	Scheduler bool | ||||||
|  | 
 | ||||||
|  | 	Port             int | ||||||
|  | 	Server           bool | ||||||
| 	OauthConfig      *oauth2.Config | 	OauthConfig      *oauth2.Config | ||||||
| 	OauthUserInfoURI string | 	OauthUserInfoURI string | ||||||
|  | 
 | ||||||
|  | 	Dns          bool | ||||||
|  | 	DnsRecursion []string | ||||||
|  | 	DnsPort      int | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GetArgs() (*Arguments, error) { | 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") | 	databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database") | ||||||
| 	templatePath := flag.String("template-path", "./templates", "Path to the template directory") | 	templatePath := flag.String("template-path", "./templates", "Path to the template directory") | ||||||
| 	staticPath := flag.String("static-path", "./static", "Path to the static directory") | 	staticPath := flag.String("static-path", "./static", "Path to the static directory") | ||||||
| 
 | 
 | ||||||
| 	scheduler := flag.Bool("scheduler", false, "Run scheduled jobs via cron") | 	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") | 	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() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN") | 	cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN") | ||||||
|  | @ -86,6 +96,9 @@ func GetArgs() (*Arguments, error) { | ||||||
| 		Server:          *server, | 		Server:          *server, | ||||||
| 		Migrate:         *migrate, | 		Migrate:         *migrate, | ||||||
| 		Scheduler:       *scheduler, | 		Scheduler:       *scheduler, | ||||||
|  | 		Dns:             *dns, | ||||||
|  | 		DnsRecursion:    strings.Split(*dnsRecursion, ","), | ||||||
|  | 		DnsPort:         *dnsPort, | ||||||
| 
 | 
 | ||||||
| 		OauthConfig:      oauthConfig, | 		OauthConfig:      oauthConfig, | ||||||
| 		OauthUserInfoURI: oauthUserInfoURI, | 		OauthUserInfoURI: oauthUserInfoURI, | ||||||
|  |  | ||||||
|  | @ -2,8 +2,10 @@ package database | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
|  | 	"fmt" | ||||||
| 	_ "github.com/mattn/go-sqlite3" | 	_ "github.com/mattn/go-sqlite3" | ||||||
| 	"log" | 	"log" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -14,6 +16,7 @@ type DNSRecord struct { | ||||||
| 	Type      string    `json:"type"` | 	Type      string    `json:"type"` | ||||||
| 	Content   string    `json:"content"` | 	Content   string    `json:"content"` | ||||||
| 	TTL       int       `json:"ttl"` | 	TTL       int       `json:"ttl"` | ||||||
|  | 	Internal  bool      `json:"internal"` | ||||||
| 	CreatedAt time.Time `json:"created_at"` | 	CreatedAt time.Time `json:"created_at"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -29,7 +32,7 @@ func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) { | ||||||
| 	var records []DNSRecord | 	var records []DNSRecord | ||||||
| 	for rows.Next() { | 	for rows.Next() { | ||||||
| 		var record DNSRecord | 		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 { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | @ -43,7 +46,7 @@ func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) { | ||||||
| 	log.Println("saving dns record", record) | 	log.Println("saving dns record", record) | ||||||
| 
 | 
 | ||||||
| 	record.CreatedAt = time.Now() | 	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 { | 	if err != nil { | ||||||
| 		return nil, err | 		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) | 	row := db.QueryRow("SELECT * FROM dns_records WHERE id = ?", recordID) | ||||||
| 	var record DNSRecord | 	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 { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -72,3 +75,53 @@ func DeleteDNSRecord(db *sql.DB, recordID string) error { | ||||||
| 	} | 	} | ||||||
| 	return nil | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -57,6 +57,7 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) { | ||||||
| 		type TEXT NOT NULL, | 		type TEXT NOT NULL, | ||||||
| 		content TEXT NOT NULL, | 		content TEXT NOT NULL, | ||||||
| 		ttl INTEGER NOT NULL, | 		ttl INTEGER NOT NULL, | ||||||
|  |                 internal BOOLEAN NOT NULL, | ||||||
| 		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | 		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
| 		FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);`) | 		FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);`) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -65,6 +66,26 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) { | ||||||
| 	return dbConn, nil | 	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) { | func MigrateUserSessions(dbConn *sql.DB) (*sql.DB, error) { | ||||||
| 	log.Println("migrating user_sessions table") | 	log.Println("migrating user_sessions table") | ||||||
| 
 | 
 | ||||||
|  | @ -88,6 +109,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) { | ||||||
| 		MigrateUsers, | 		MigrateUsers, | ||||||
| 		MigrateUserSessions, | 		MigrateUserSessions, | ||||||
| 		MigrateApiKeys, | 		MigrateApiKeys, | ||||||
|  | 		MigrateDomainOwners, | ||||||
| 		MigrateDNSRecords, | 		MigrateDNSRecords, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										4
									
								
								go.mod
								
								
								
								
							|  | @ -6,6 +6,7 @@ require ( | ||||||
| 	github.com/go-co-op/gocron v1.37.0 | 	github.com/go-co-op/gocron v1.37.0 | ||||||
| 	github.com/joho/godotenv v1.5.1 | 	github.com/joho/godotenv v1.5.1 | ||||||
| 	github.com/mattn/go-sqlite3 v1.14.22 | 	github.com/mattn/go-sqlite3 v1.14.22 | ||||||
|  | 	github.com/miekg/dns v1.1.58 | ||||||
| 	golang.org/x/oauth2 v0.18.0 | 	golang.org/x/oauth2 v0.18.0 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -14,7 +15,10 @@ require ( | ||||||
| 	github.com/google/uuid v1.4.0 // indirect | 	github.com/google/uuid v1.4.0 // indirect | ||||||
| 	github.com/robfig/cron/v3 v3.0.1 // indirect | 	github.com/robfig/cron/v3 v3.0.1 // indirect | ||||||
| 	go.uber.org/atomic v1.9.0 // 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/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/appengine v1.6.7 // indirect | ||||||
| 	google.golang.org/protobuf v1.31.0 // indirect | 	google.golang.org/protobuf v1.31.0 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										10
									
								
								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/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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= | ||||||
| github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | 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 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | ||||||
| go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | 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/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.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 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= | ||||||
| golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= | 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 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= | ||||||
| golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= | 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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | 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.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= | 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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= | ||||||
| google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								main.go
								
								
								
								
							
							
						
						
									
										28
									
								
								main.go
								
								
								
								
							|  | @ -6,6 +6,7 @@ import ( | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api" | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/api" | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/args" | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/args" | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/database" | ||||||
|  | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/dns" | ||||||
| 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/scheduler" | 	"git.hatecomputers.club/hatecomputers/hatecomputers.club/scheduler" | ||||||
| 	"github.com/joho/godotenv" | 	"github.com/joho/godotenv" | ||||||
| ) | ) | ||||||
|  | @ -40,10 +41,27 @@ func main() { | ||||||
| 
 | 
 | ||||||
| 	if argv.Server { | 	if argv.Server { | ||||||
| 		server := api.MakeServer(argv, dbConn) | 		server := api.MakeServer(argv, dbConn) | ||||||
| 		err = server.ListenAndServe() | 		log.Println("🚀🚀 API listening on port", argv.Port) | ||||||
| 		if err != nil { | 
 | ||||||
| 			log.Fatal(err) | 		go func() { | ||||||
| 		} | 			err = server.ListenAndServe() | ||||||
| 		log.Println("🚀🚀 server listening on port", argv.Port) | 			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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,13 +28,12 @@ | ||||||
|     <h2>Add An API Key</h2> |     <h2>Add An API Key</h2> | ||||||
|     <hr> |     <hr> | ||||||
|     <input type="submit" value="Generate" /> |     <input type="submit" value="Generate" /> | ||||||
|   </form> |     {{ if .FormError }} | ||||||
| 
 |  | ||||||
|   {{ if .FormError }} |  | ||||||
|     {{ if (len .FormError.Errors) }} |     {{ if (len .FormError.Errors) }} | ||||||
|       {{ range $error := .FormError.Errors }} |     {{ range $error := .FormError.Errors }} | ||||||
|         <div class="error">{{ $error }}</div> |     <div class="error">{{ $error }}</div> | ||||||
|       {{ end }} |  | ||||||
|     {{ end }} |     {{ end }} | ||||||
|   {{ end }} |     {{ end }} | ||||||
|  |     {{ end }} | ||||||
|  |   </form> | ||||||
| {{ end }} | {{ end }} | ||||||
|  |  | ||||||
|  | @ -5,11 +5,12 @@ | ||||||
|       <th>Name</th> |       <th>Name</th> | ||||||
|       <th>Content</th> |       <th>Content</th> | ||||||
|       <th>TTL</th> |       <th>TTL</th> | ||||||
|  |       <th>Internal</th> | ||||||
|       <th>Delete</th> |       <th>Delete</th> | ||||||
|     </tr> |     </tr> | ||||||
|     {{ if (eq (len .DNSRecords) 0) }} |     {{ if (eq (len .DNSRecords) 0) }} | ||||||
|     <tr> |     <tr> | ||||||
|       <td colspan="5"><span class="blinky">No DNS records found</span></td> |       <td colspan="6"><span class="blinky">No DNS records found</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {{ end }} |     {{ end }} | ||||||
|     {{ range $record := .DNSRecords }} |     {{ range $record := .DNSRecords }} | ||||||
|  | @ -17,6 +18,7 @@ | ||||||
| 	<td>{{ $record.Type }}</td> | 	<td>{{ $record.Type }}</td> | ||||||
| 	<td>{{ $record.Name }}</td> | 	<td>{{ $record.Name }}</td> | ||||||
| 	<td>{{ $record.Content }}</td> | 	<td>{{ $record.Content }}</td> | ||||||
|  | 	<td>{{ $record.Internal }}</td> | ||||||
| 	<td>{{ $record.TTL }}</td> | 	<td>{{ $record.TTL }}</td> | ||||||
| 	<td> | 	<td> | ||||||
| 	  <form method="POST" action="/dns/delete"> | 	  <form method="POST" action="/dns/delete"> | ||||||
|  | @ -64,14 +66,26 @@ | ||||||
| 	   value="{{ .RecordForm.TTL }}" | 	   value="{{ .RecordForm.TTL }}" | ||||||
| 	   {{ end }} | 	   {{ end }} | ||||||
| 	   required /> | 	   required /> | ||||||
|  |     <label for="internal"> | ||||||
|  |       Internal | ||||||
|  |       <input style='display:inline;width:auto;' type="checkbox" name="internal" | ||||||
|  | 	   {{ if .RecordForm.Internal }} | ||||||
|  | 	   checked | ||||||
|  | 	   {{ end }} | ||||||
|  | 	   /> | ||||||
|  |     </label> | ||||||
|  |      | ||||||
|  | 
 | ||||||
|     <input type="submit" value="Add" /> |     <input type="submit" value="Add" /> | ||||||
|  | 
 | ||||||
|  |     {{ if .FormError }} | ||||||
|  |     {{ if (len .FormError.Errors) }} | ||||||
|  |     {{ range $error := .FormError.Errors }} | ||||||
|  |     <div class="error">{{ $error }}</div> | ||||||
|  |     {{ end }} | ||||||
|  |     {{ end }} | ||||||
|  |     {{ end }} | ||||||
|   </form> |   </form> | ||||||
| 
 | 
 | ||||||
|   {{ if .FormError }} | 
 | ||||||
|     {{ if (len .FormError.Errors) }} |  | ||||||
|       {{ range $error := .FormError.Errors }} |  | ||||||
|         <div class="error">{{ $error }}</div> |  | ||||||
|       {{ end }} |  | ||||||
|     {{ end }} |  | ||||||
|   {{ end }} |  | ||||||
| {{ end }} | {{ end }} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue