Re-vendor the shared files from sneak/prompts at 0653579 (closes #52)
check / check (push) Canceled after 0s

Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the
workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this
repository's own entries after the shared content. Lint (golangci-lint
v2.14.0) and test are Dockerfile phases that script/lint and script/test
build alone with a new CACHEBUST value, keeping Go's and golangci-lint's
caches; the image build depends on both. make fmt and make fmt-check also
format the markdown with prettier, pinned in yarn.lock. The agent
guidance moves to AGENTS.md. The Go code is fixed for what the new
configuration finds, and the AS and prefix detail reads now return the
rows' error after their loops.

Model: opus-5-5
This commit was merged in pull request #85.
This commit is contained in:
2026-10-19 12:10:11 +02:00
parent 4f515d718b
commit b89987e356
65 changed files with 5624 additions and 3509 deletions
+106 -78
View File
@@ -98,7 +98,8 @@ func (c *Client) LookupASN(ctx context.Context, asn int) (*ASNInfo, error) {
if err != nil {
// Return partial data from IANA if RIR query fails
info := c.parseResponse(asn, rir, ianaResp)
info.RawResponse = ianaResp + "\n--- RIR query failed: " + err.Error() + " ---\n"
info.RawResponse = fmt.Sprintf("%s\n--- RIR query failed: %v ---\n",
ianaResp, err)
return info, nil
}
@@ -120,28 +121,33 @@ func (c *Client) query(ctx context.Context, server, query string) (string, error
defer func() { _ = conn.Close() }()
// Set deadlines
if err := conn.SetWriteDeadline(time.Now().Add(writeTimeout)); err != nil {
err = conn.SetWriteDeadline(time.Now().Add(writeTimeout))
if err != nil {
return "", fmt.Errorf("set write deadline: %w", err)
}
// Send query
if _, err := fmt.Fprintf(conn, "%s\r\n", query); err != nil {
_, err = fmt.Fprintf(conn, "%s\r\n", query)
if err != nil {
return "", fmt.Errorf("write query: %w", err)
}
// Read response
if err := conn.SetReadDeadline(time.Now().Add(readTimeout)); err != nil {
err = conn.SetReadDeadline(time.Now().Add(readTimeout))
if err != nil {
return "", fmt.Errorf("set read deadline: %w", err)
}
var sb strings.Builder
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
sb.WriteString(scanner.Text())
sb.WriteString("\n")
}
if err := scanner.Err(); err != nil {
err = scanner.Err()
if err != nil {
return sb.String(), fmt.Errorf("read response: %w", err)
}
@@ -149,10 +155,10 @@ func (c *Client) query(ctx context.Context, server, query string) (string, error
}
// parseIANAReferral extracts the RIR and WHOIS server from an IANA response.
func (c *Client) parseIANAReferral(response string) (rir, whoisServer string) {
lines := strings.Split(response, "\n")
func (c *Client) parseIANAReferral(response string) (string, string) {
var rir string
for _, line := range lines {
for line := range strings.SplitSeq(response, "\n") {
line = strings.TrimSpace(line)
// Look for "refer:" line
@@ -183,18 +189,9 @@ func (c *Client) parseIANAReferral(response string) (rir, whoisServer string) {
// Also check organisation line for RIR hints
if strings.HasPrefix(strings.ToLower(line), "organisation:") {
org := strings.ToLower(line)
switch {
case strings.Contains(org, "arin"):
rir = RIRARIN
case strings.Contains(org, "ripe"):
rir = RIRRIPE
case strings.Contains(org, "apnic"):
rir = RIRAPNIC
case strings.Contains(org, "lacnic"):
rir = RIRLACNIC
case strings.Contains(org, "afrinic"):
rir = RIRAFRNIC
orgRIR := rirInOrganisationLine(strings.ToLower(line))
if orgRIR != "" {
rir = orgRIR
}
}
}
@@ -202,6 +199,25 @@ func (c *Client) parseIANAReferral(response string) (rir, whoisServer string) {
return rir, ""
}
// rirInOrganisationLine returns the RIR a lowercased IANA "organisation:"
// line names, or "" when it names none.
func rirInOrganisationLine(org string) string {
switch {
case strings.Contains(org, "arin"):
return RIRARIN
case strings.Contains(org, "ripe"):
return RIRRIPE
case strings.Contains(org, "apnic"):
return RIRAPNIC
case strings.Contains(org, "lacnic"):
return RIRLACNIC
case strings.Contains(org, "afrinic"):
return RIRAFRNIC
default:
return ""
}
}
// parseResponse extracts ASN information from a WHOIS response.
func (c *Client) parseResponse(asn int, rir, response string) *ASNInfo {
info := &ASNInfo{
@@ -211,11 +227,13 @@ func (c *Client) parseResponse(asn int, rir, response string) *ASNInfo {
}
lines := strings.Split(response, "\n")
var addressLines []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "%") || strings.HasPrefix(line, "#") {
if line == "" || strings.HasPrefix(line, "%") ||
strings.HasPrefix(line, "#") {
continue
}
@@ -232,63 +250,14 @@ func (c *Client) parseResponse(asn int, rir, response string) *ASNInfo {
continue
}
switch key {
// AS Name (varies by RIR)
case "asname", "as-name":
if info.ASName == "" {
info.ASName = value
}
// Organization
case "orgname", "org-name", "owner":
if info.OrgName == "" {
info.OrgName = value
}
case "orgid", "org-id", "org":
if info.OrgID == "" {
info.OrgID = value
}
// Address (collect multiple lines)
case "address":
if key == "address" {
addressLines = append(addressLines, value)
// Country
case "country":
if info.CountryCode == "" && len(value) == 2 {
info.CountryCode = strings.ToUpper(value)
}
// Abuse contact
case "orgabuseemail", "abuse-mailbox":
if info.AbuseEmail == "" {
info.AbuseEmail = value
}
case "orgabusephone":
if info.AbusePhone == "" {
info.AbusePhone = value
}
// Tech contact
case "orgtechemail":
if info.TechEmail == "" {
info.TechEmail = value
}
case "orgtechphone":
if info.TechPhone == "" {
info.TechPhone = value
}
// Registration dates
case "regdate", "created":
if info.RegDate == nil {
info.RegDate = c.parseDate(value)
}
case "updated", "last-modified", "changed":
if info.LastMod == nil {
info.LastMod = c.parseDate(value)
}
continue
}
c.setResponseField(info, key, value)
}
// Combine address lines
@@ -304,6 +273,65 @@ func (c *Client) parseResponse(asn int, rir, response string) *ASNInfo {
return info
}
// setResponseField stores the value of one "key: value" line of a WHOIS
// response in info, keeping the first value seen for each field.
func (c *Client) setResponseField(info *ASNInfo, key, value string) {
switch key {
// Country
case "country":
if info.CountryCode == "" && len(value) == 2 {
info.CountryCode = strings.ToUpper(value)
}
// Registration dates
case "regdate", "created":
if info.RegDate == nil {
info.RegDate = c.parseDate(value)
}
case "updated", "last-modified", "changed":
if info.LastMod == nil {
info.LastMod = c.parseDate(value)
}
default:
field := textFieldForKey(info, key)
if field != nil && *field == "" {
*field = value
}
}
}
// textFieldForKey returns the text field of info that a WHOIS response key
// fills, or nil when the key fills none.
func textFieldForKey(info *ASNInfo, key string) *string {
switch key {
// AS Name (varies by RIR)
case "asname", "as-name":
return &info.ASName
// Organization
case "orgname", "org-name", "owner":
return &info.OrgName
case "orgid", "org-id", "org":
return &info.OrgID
// Abuse contact
case "orgabuseemail", "abuse-mailbox":
return &info.AbuseEmail
case "orgabusephone":
return &info.AbusePhone
// Tech contact
case "orgtechemail":
return &info.TechEmail
case "orgtechphone":
return &info.TechPhone
default:
return nil
}
}
// parseDate attempts to parse various date formats used in WHOIS responses.
func (c *Client) parseDate(value string) *time.Time {
// Common formats
@@ -319,15 +347,15 @@ func (c *Client) parseDate(value string) *time.Time {
value = strings.TrimSpace(value)
// Handle "YYYYMMDD" format from LACNIC
if len(value) == lacnicDateFormatLen {
if _, err := time.Parse("20060102", value); err == nil {
t, _ := time.Parse("20060102", value)
t, err := time.Parse("20060102", value)
if err == nil {
return &t
}
}
for _, format := range formats {
if t, err := time.Parse(format, value); err == nil {
t, err := time.Parse(format, value)
if err == nil {
return &t
}
}