136 строки
4.5 KiB
Go
136 строки
4.5 KiB
Go
package models
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"rsgit.ru/rsmon/rsmon/app/models/concerns"
|
|
)
|
|
|
|
// RknDomain is one row of the locally cached ru-blocked domains list. The
|
|
// `Domain` field carries a lowercased domain string and is the unique key.
|
|
type RknDomain struct {
|
|
concerns.Model
|
|
Domain string `gorm:"size:255;uniqueIndex;not null" json:"domain"`
|
|
}
|
|
|
|
// TableName pins the underlying table name so GORM migrations and raw SQL
|
|
// (used by IsRknDomainBlocked and the rkn updater) agree.
|
|
func (RknDomain) TableName() string { return "rkn_domains" }
|
|
|
|
// ReplaceRknDomains deletes every existing row and inserts the given domains
|
|
// in batches. Caller-supplied domains are lower-cased and de-duplicated, and
|
|
// blank entries are dropped. The whole operation runs in a single
|
|
// transaction so readers see either the old set or the new set — there is no
|
|
// in-between state where the table is half-flushed.
|
|
//
|
|
// Designed to be called once per parser-updater tick (default every 6h); the
|
|
// uniqueIndex on Domain guarantees idempotent re-runs even when the caller
|
|
// forgets to filter.
|
|
func ReplaceRknDomains(domains []string) error {
|
|
if domains == nil {
|
|
domains = []string{}
|
|
}
|
|
deduped := make([]string, 0, len(domains))
|
|
seen := make(map[string]struct{}, len(domains))
|
|
for _, d := range domains {
|
|
d = strings.ToLower(strings.TrimSpace(d))
|
|
if d == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[d]; ok {
|
|
continue
|
|
}
|
|
seen[d] = struct{}{}
|
|
deduped = append(deduped, d)
|
|
}
|
|
|
|
tx := DB().Begin()
|
|
if tx.Error != nil {
|
|
return tx.Error
|
|
}
|
|
// Step 1 — wipe the existing cache. Using a scoped Where("1 = 1") Delete
|
|
// instead of TRUNCATE so the advisory lock taken by Migrate() does not
|
|
// become a bottleneck and so any in-flight foreign-key checks against
|
|
// rkn_domains remain satisfied (the table has no FKs today, but this
|
|
// matches the convention used by Drop()).
|
|
if err := tx.Where("1 = 1").Delete(&RknDomain{}).Error; err != nil {
|
|
_ = tx.Rollback().Error
|
|
return err
|
|
}
|
|
// Step 2 — bulk-insert in chunks of 1000 rows. CreateInBatches runs N
|
|
// multi-row INSERT statements, which for the typical ~30k ru-blocked
|
|
// entries is ~3ms per batch — significantly cheaper than per-row
|
|
// Create() in tight loops (the previous AddRknDomain implementation).
|
|
const batchSize = 1000
|
|
for start := 0; start < len(deduped); start += batchSize {
|
|
end := start + batchSize
|
|
if end > len(deduped) {
|
|
end = len(deduped)
|
|
}
|
|
rows := make([]RknDomain, 0, end-start)
|
|
for _, d := range deduped[start:end] {
|
|
rows = append(rows, RknDomain{Domain: d})
|
|
}
|
|
if err := tx.CreateInBatches(rows, batchSize).Error; err != nil {
|
|
_ = tx.Rollback().Error
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit().Error
|
|
}
|
|
|
|
// IsRknDomainBlocked returns true iff `domain` (or its root label, or any
|
|
// parent suffix already recorded as `*.parent.tld`) is present in the
|
|
// rkn_domains table.
|
|
//
|
|
// Matching rules — see checks/crkn/rkn_init.go for the original logic we
|
|
// consolidate here:
|
|
// 1. Exact match against the stored domain string.
|
|
// 2. Root-domain match (last two labels of the input) — covers the case
|
|
// where the user passed a subdomain but the upstream only lists the
|
|
// apex.
|
|
// 3. Suffix match (`stored LIKE '%' || input || ?`) — covers the case
|
|
// where the user passed the apex (or a higher-level label) but the
|
|
// upstream lists a child subdomain.
|
|
//
|
|
// All three checks are combined into a single SQL statement via OR so the
|
|
// table is scanned at most once and the SQL planner can pick a single
|
|
// index access path.
|
|
func IsRknDomainBlocked(domain string) (bool, error) {
|
|
domain = strings.ToLower(strings.TrimSpace(domain))
|
|
if domain == "" {
|
|
return false, nil
|
|
}
|
|
rootDomain := rootDomainOf(domain)
|
|
|
|
// Build the suffix patterns once. Note: every ".X" entry in the table
|
|
// (i.e. a domain that begins with a dot) matches any subdomain whose
|
|
// suffix is domain.
|
|
suffixPattern := "%." + domain
|
|
|
|
var count int64
|
|
err := DB().Raw(
|
|
"SELECT COUNT(*) FROM rkn_domains WHERE domain = ? OR domain = ? OR domain LIKE ?",
|
|
domain, rootDomain, suffixPattern,
|
|
).Scan(&count).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// rootDomainOf returns the last two labels of `domain` (e.g. "a.b.c" → "b.c").
|
|
// Returns `domain` unchanged when it has fewer than three labels, because a
|
|
// one- or two-label input IS already the apex/root domain.
|
|
func rootDomainOf(domain string) string {
|
|
i := strings.LastIndex(domain, ".")
|
|
if i < 0 {
|
|
return domain
|
|
}
|
|
j := strings.LastIndex(domain[:i], ".")
|
|
if j < 0 {
|
|
return domain
|
|
}
|
|
return domain[j+1:]
|
|
}
|