signup: add DNS checker and run against suspicious hosts #3

Open
nbsp wants to merge 2 commits from nbsp/town:suspicious-hosts into trunk
3 changed files with 103 additions and 1 deletions

View File

@ -3,11 +3,15 @@ package main
import (
"bytes"
"database/sql"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"path"
"regexp"
"slices"
"strings"
"time"
@ -110,6 +114,39 @@ func (c *character) Say(msg string) string {
strings.TrimSpace(msg))
}
var ErrNoSuchDomain = errors.New("no host found for email address")
var ErrNoSuchMailserver = errors.New("no mail server found for email address")
// DigMX does some grubbing around to attempt to find valid email hosts, and
// then runs then through [net.LookupMX] and returns their mailserver domains.
// may return [ErrNoSuchDomain] or [ErrNoSuchMailserver].
func DigMX(raw string) (domains []string, err error) {
re := regexp.MustCompile(`@[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\b`) // good enough
candidates := re.FindAllString(raw, -1)
// the error checking tries to be very generous: if anything comes up
// positive we will throw no errors and just assume the rest was a fluke.
ok := false
for _, host := range candidates {
records, e := net.LookupMX(host[1:])
if e != nil {
err = ErrNoSuchDomain
} else if len(records) == 0 {
err = ErrNoSuchMailserver
} else {
ok = true
for _, record := range records {
domains = append(domains, record.Host)
}
}
}
if ok {
return domains, nil
}
return
}
func main() {
logFile := path.Join(logDir, fmt.Sprintf("%d", time.Now().Unix()))
logF, err := os.Create(logFile)
@ -201,9 +238,32 @@ func _main(l *log.Logger, db *sql.DB) error {
`),
"i'm sorry, before going further could you share an email with me?",
newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
func(s *scene) { su.Email = string(s.Input.Bytes()) },
func(s *scene) {
su.Email = string(s.Input.Bytes())
suspiciousHosts, err := models.SuspiciousHosts(db)
if err != nil {
// XXX: maybe log somewhere that the database failed
Review

logging good, see lockingwriter package and its use

logging good, see lockingwriter package and its use
return
}
var shDomains []string
for _, host := range suspiciousHosts {
shDomains = append(shDomains, host.Domain)
}
if records, err := DigMX(su.Email); err == nil {
for _, record := range records {
if slices.Contains(shDomains, record) {
su.Notes = append(su.Notes, models.SignupNote{
Review

I love using notes for this

I love using notes for this
Author: "dns",
Content: fmt.Sprintf("email address has suspicious host %s", record),
SignupID: su.ID,
})
}
}
}
},
func(s *scene, tv *tview.TextView, msg string) {
// TODO could check and see if it's email shaped and admonish if not
// NOTE(nbsp): DigMX call can see if email is invalid but this isn't used yet
Review

I predict splitting out the regex from DigMX but it's fine the way it is now.

The signing up user should not have any notion of suspicious email hosts but we should gently prod them until we see an email shaped thing

I predict splitting out the regex from DigMX but it's fine the way it is now. The signing up user should not have any notion of suspicious email hosts but we should gently prod them until we see an email shaped thing
trimmed := strings.TrimSpace(msg)
fmt.Fprintln(tv, s.Host.Say(fmt.Sprintf("I heard '%s'. Is that right? if so, /nod", trimmed)))
}),

View File

@ -193,3 +193,35 @@ func (s *TownSignup) All(db *sql.DB) ([]*TownSignup, error) {
return out, nil
}
type SuspiciousHost struct {
ID int64
Domain string
CommonName string
Tier int64
}
func SuspiciousHosts(db *sql.DB) ([]SuspiciousHost, error) {
rows, err := db.Query(`SELECT id, domain, common_name, tier FROM suspicious_hosts`)
if err != nil {
return nil, err
}
defer rows.Close()
out := []SuspiciousHost{}
for rows.Next() {
sh := SuspiciousHost{}
if err = rows.Scan(
&sh.ID,
&sh.Domain,
&sh.CommonName,
&sh.Tier,
); err != nil {
return nil, err
}
out = append(out, sh)
}
return out, nil
}

View File

@ -24,3 +24,13 @@ CREATE TABLE IF NOT EXISTS notes (
FOREIGN KEY (signupid) REFERENCES signups(signupid)
);
-- 2025-11-22: bad hosts
CREATE TABLE IF NOT EXISTS suspicious_hosts (
id INTEGER PRIMARY KEY,
domain TEXT,
-- unused but worth adding instead of another migration later
Review

I find unused stuff more confusing than later need to migrate so I'd leave out unused columns for now

I find unused stuff more confusing than later need to migrate so I'd leave out unused columns for now
common_name TEXT,
tier INTEGER,
)