jarld downloads blocklists (Adblock, hosts or plain domains), checks them daily and reloads named only when a list changed; your own block and allow rules take precedence, and the most specific rule wins. Lists and rules apply per view (default: every view that offers recursion). Web UI Blocking page with name checks, jarl blocklist and jarl block, the /blocking API, blocked counts in the statistics. Database schema 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package model
|
|
|
|
import (
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Blocklist is a list of domains to block, downloaded from URL and kept up
|
|
// to date by jarld (docs/blocking.md).
|
|
type Blocklist struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"` // a DNS label: it names the list's policy zone
|
|
URL string `json:"url"`
|
|
// ViewIDs are the views the list applies to; empty means every view
|
|
// that offers recursion (see BlockingViews).
|
|
ViewIDs []int64 `json:"view_ids"`
|
|
Disabled bool `json:"disabled,omitempty"`
|
|
Comment string `json:"comment"`
|
|
Meta
|
|
}
|
|
|
|
// Block rule actions.
|
|
const (
|
|
BlockActionBlock = "block"
|
|
BlockActionAllow = "allow"
|
|
)
|
|
|
|
// BlockRule blocks or allows a domain, and unless Exact the names below
|
|
// it. Rules take precedence over blocklists.
|
|
type BlockRule struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"` // lowercase, without the final dot
|
|
Action string `json:"action"` // block or allow
|
|
Exact bool `json:"exact,omitempty"`
|
|
ViewIDs []int64 `json:"view_ids"` // as for Blocklist
|
|
Comment string `json:"comment"`
|
|
Meta
|
|
}
|
|
|
|
// BlocklistStatus is what jarld knows about a list's downloads. It is not
|
|
// configuration: it is kept outside changesets.
|
|
type BlocklistStatus struct {
|
|
ID int64 `json:"id"`
|
|
URL string `json:"url"` // the URL last tried
|
|
ETag string `json:"-"`
|
|
LastModified string `json:"-"`
|
|
CheckedAt *time.Time `json:"checked_at,omitempty"` // last attempt
|
|
UpdatedAt *time.Time `json:"updated_at,omitempty"` // last time the content changed
|
|
Entries int `json:"entries"` // domains
|
|
Records int `json:"records"` // policy zone records
|
|
Skipped int `json:"skipped"` // lines that weren't usable
|
|
SHA256 string `json:"-"`
|
|
Error string `json:"error,omitempty"` // the last attempt failed
|
|
}
|
|
|
|
// BlocklistByName finds a blocklist by name.
|
|
func (s *State) BlocklistByName(name string) *Blocklist {
|
|
for _, b := range s.Blocklists {
|
|
if strings.EqualFold(b.Name, name) {
|
|
return b
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SortedBlocklists returns the blocklists ordered by name.
|
|
func (s *State) SortedBlocklists() []*Blocklist {
|
|
out := make([]*Blocklist, 0, len(s.Blocklists))
|
|
for _, b := range s.Blocklists {
|
|
out = append(out, b)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// SortedBlockRules returns the rules ordered by domain, parents first.
|
|
func (s *State) SortedBlockRules() []*BlockRule {
|
|
out := make([]*BlockRule, 0, len(s.BlockRules))
|
|
for _, r := range s.BlockRules {
|
|
out = append(out, r)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
a, b := out[i], out[j]
|
|
if a.Name != b.Name {
|
|
return CanonicalLess(a.Name+".", b.Name+".")
|
|
}
|
|
if a.Exact != b.Exact {
|
|
return !a.Exact
|
|
}
|
|
return a.ID < b.ID
|
|
})
|
|
return out
|
|
}
|
|
|
|
// RecursionOn reports whether a view (0: the server without views) offers
|
|
// recursion: its recursion option, else the server's, else BIND's default
|
|
// (yes).
|
|
func (s *State) RecursionOn(viewID int64) bool {
|
|
o := s.FindOption(ScopeView, viewID, "recursion")
|
|
if viewID == 0 || o == nil {
|
|
if g := s.FindOption(ScopeOptions, 0, "recursion"); o == nil {
|
|
o = g
|
|
}
|
|
}
|
|
if o == nil {
|
|
return true
|
|
}
|
|
switch strings.ToLower(strings.Trim(strings.TrimSpace(o.Value), `";`)) {
|
|
case "no", "false", "0":
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// BlockingViews returns the views a blocklist or rule with viewIDs applies
|
|
// to, in view order: the given views, or when none are given every view
|
|
// that offers recursion (blocking only affects recursive queries). Without
|
|
// views it returns []int64{0}, the whole server, if it offers recursion.
|
|
func (s *State) BlockingViews(viewIDs []int64) []int64 {
|
|
views := s.OrderedViews()
|
|
if len(views) == 0 {
|
|
if !s.RecursionOn(0) {
|
|
return nil
|
|
}
|
|
return []int64{0}
|
|
}
|
|
var out []int64
|
|
for _, v := range views {
|
|
if len(viewIDs) == 0 && s.RecursionOn(v.ID) || slices.Contains(viewIDs, v.ID) {
|
|
out = append(out, v.ID)
|
|
}
|
|
}
|
|
return out
|
|
}
|