AdGuardHome/internal/home/controlfiltering.go

497 lines
12 KiB
Go
Raw Normal View History

package home
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// validateFilterURL validates the filter list URL or file name.
func validateFilterURL(urlStr string) (err error) {
if filepath.IsAbs(urlStr) {
_, err = os.Stat(urlStr)
if err != nil {
return fmt.Errorf("checking filter file: %w", err)
}
return nil
}
url, err := url.ParseRequestURI(urlStr)
if err != nil {
return fmt.Errorf("checking filter url: %w", err)
}
if s := url.Scheme; s != schemeHTTP && s != schemeHTTPS {
return fmt.Errorf("checking filter url: invalid scheme %q", s)
}
return nil
}
type filterAddJSON struct {
Name string `json:"name"`
URL string `json:"url"`
Whitelist bool `json:"whitelist"`
}
func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
fj := filterAddJSON{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
return
}
err = validateFilterURL(fj.URL)
if err != nil {
msg := fmt.Sprintf("invalid url: %s", err)
http.Error(w, msg, http.StatusBadRequest)
return
}
// Check for duplicates
if filterExists(fj.URL) {
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
return
}
// Set necessary properties
filt := filter{
Enabled: true,
URL: fj.URL,
Name: fj.Name,
white: fj.Whitelist,
}
filt.ID = assignUniqueFilterID()
// Download the filter contents
ok, err := f.update(&filt)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Couldn't fetch filter from url %s: %s",
filt.URL,
err,
)
return
}
if !ok {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Filter at the url %s is invalid (maybe it points to blank page?)",
filt.URL,
)
return
}
// URL is assumed valid so append it to filters, update config, write new
// file and reload it to engines.
if !filterAdd(filt) {
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", filt.URL)
return
}
onConfigModified()
enableFilters(true)
_, err = fmt.Fprintf(w, "OK %d rules\n", filt.RulesCount)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
}
}
func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
type request struct {
URL string `json:"url"`
Whitelist bool `json:"whitelist"`
}
req := request{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "failed to parse request body json: %s", err)
return
}
config.Lock()
filters := &config.Filters
if req.Whitelist {
filters = &config.WhitelistFilters
}
var deleted filter
var newFilters []filter
for _, f := range *filters {
if f.URL != req.URL {
newFilters = append(newFilters, f)
continue
}
deleted = f
path := f.Path()
err = os.Rename(path, path+".old")
if err != nil {
log.Error("deleting filter %q: %s", path, err)
}
}
*filters = newFilters
config.Unlock()
onConfigModified()
enableFilters(true)
// NOTE: The old files "filter.txt.old" aren't deleted. It's not really
// necessary, but will require the additional complicated code to run
// after enableFilters is done.
//
// TODO(a.garipov): Make sure the above comment is true.
_, err = fmt.Fprintf(w, "OK %d rules\n", deleted.RulesCount)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "couldn't write body: %s", err)
}
}
type filterURLJSON struct {
Name string `json:"name"`
URL string `json:"url"`
Enabled bool `json:"enabled"`
}
type filterURLReq struct {
URL string `json:"url"`
Whitelist bool `json:"whitelist"`
Data filterURLJSON `json:"data"`
}
func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
fj := filterURLReq{}
err := json.NewDecoder(r.Body).Decode(&fj)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
err = validateFilterURL(fj.Data.URL)
if err != nil {
msg := fmt.Sprintf("invalid url: %s", err)
http.Error(w, msg, http.StatusBadRequest)
return
}
filt := filter{
Enabled: fj.Data.Enabled,
Name: fj.Data.Name,
URL: fj.Data.URL,
}
status := f.filterSetProperties(fj.URL, filt, fj.Whitelist)
if (status & statusFound) == 0 {
http.Error(w, "URL doesn't exist", http.StatusBadRequest)
return
}
if (status & statusURLExists) != 0 {
http.Error(w, "URL already exists", http.StatusBadRequest)
return
}
onConfigModified()
restart := false
if (status & statusEnabledChanged) != 0 {
// we must add or remove filter rules
restart = true
}
if (status&statusUpdateRequired) != 0 && fj.Data.Enabled {
// download new filter and apply its rules
flags := filterRefreshBlocklists
if fj.Whitelist {
flags = filterRefreshAllowlists
}
nUpdated, _ := f.refreshFilters(flags, true)
// if at least 1 filter has been updated, refreshFilters() restarts the filtering automatically
// if not - we restart the filtering ourselves
restart = false
if nUpdated == 0 {
restart = true
}
}
if restart {
enableFilters(true)
}
}
func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
Pull request: 2305 limit message size Merge in DNS/adguard-home from 2305-limit-message-size to master Closes #2305. Squashed commit of the following: commit 6edd1e0521277a680f0053308efcf3d9cacc8e62 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 23 14:03:36 2020 +0300 aghio: fix final inaccuracies commit 4dd382aaf25132b31eb269749a2cd36daf0cb792 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 23 13:59:10 2020 +0300 all: improve code quality commit 060f923f6023d0e6f26441559b7023d5e5f96843 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 23 13:10:57 2020 +0300 aghio: add validation to constructor commit f57a2f596f5dc578548241c315c68dce7fc93905 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 19:19:26 2020 +0300 all: fix minor inaccuracies commit 93462c71725d3d00655a4bd565b77e64451fff60 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 19:13:23 2020 +0300 home: make test name follow convention commit 4922986ad84481b054479c43b4133a1b97bee86b Merge: 1f5472abc 046ec13fd Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 19:09:01 2020 +0300 Merge branch 'master' into 2305-limit-message-size commit 1f5472abcfa7427f389825fc59eb4253e1e2bfb7 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 19:08:21 2020 +0300 aghio: improve readability commit 60dc706b093fa22bbf62f13b2341934364ddc4df Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 18:44:08 2020 +0300 home: cover middleware with test commit bedf436b947ca1fa4493af2fc94f1f40beec7c35 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 17:10:23 2020 +0300 aghio: improved error informativeness commit 682c5da9f21fa330fb3536bb1c112129c91b9990 Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 13:37:51 2020 +0300 all: limit readers for ReadAll dealing with miscellanious data. commit 78c6dd8d90a0a43fe6ee3f9ed4d5fc637b15ba74 Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Nov 19 20:07:43 2020 +0300 all: handle ReadAll calls dealing with request's bodies. commit bfe1a6faf6468eb44515e2b0ecffa8c51f90b7e8 Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Nov 19 17:25:34 2020 +0300 home: add middlewares commit bbd1d491b318e6ba07f8af23ad546183383783a8 Merge: 7b77c2cad 62a8fe0b7 Author: Eugene Burkov <e.burkov@adguard.com> Date: Thu Nov 19 16:44:04 2020 +0300 Merge branch 'master' into 2305-limit-message-size commit 7b77c2cad03154177392460982e1d73ee2a30177 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 17 15:33:33 2020 +0300 aghio: create package
2020-11-23 04:14:08 -07:00
// This use of ReadAll is safe, because request's body is now limited.
body, err := io.ReadAll(r.Body)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
return
}
config.UserRules = strings.Split(string(body), "\n")
onConfigModified()
enableFilters(true)
}
func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
type Req struct {
White bool `json:"whitelist"`
}
type Resp struct {
Updated int `json:"updated"`
}
resp := Resp{}
var err error
req := Req{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
flags := filterRefreshBlocklists
if req.White {
flags = filterRefreshAllowlists
}
func() {
// Temporarily unlock the Context.controlLock because the
// f.refreshFilters waits for it to be unlocked but it's
// actually locked in ensure wrapper.
//
// TODO(e.burkov): Reconsider this messy syncing process.
Context.controlLock.Unlock()
defer Context.controlLock.Lock()
resp.Updated, err = f.refreshFilters(flags|filterRefreshForce, false)
}()
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
return
}
js, err := json.Marshal(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
type filterJSON struct {
ID int64 `json:"id"`
Enabled bool `json:"enabled"`
URL string `json:"url"`
Name string `json:"name"`
RulesCount uint32 `json:"rules_count"`
LastUpdated string `json:"last_updated"`
}
type filteringConfig struct {
Enabled bool `json:"enabled"`
Interval uint32 `json:"interval"` // in hours
Filters []filterJSON `json:"filters"`
WhitelistFilters []filterJSON `json:"whitelist_filters"`
UserRules []string `json:"user_rules"`
}
func filterToJSON(f filter) filterJSON {
fj := filterJSON{
ID: f.ID,
Enabled: f.Enabled,
URL: f.URL,
Name: f.Name,
RulesCount: uint32(f.RulesCount),
}
if !f.LastUpdated.IsZero() {
fj.LastUpdated = f.LastUpdated.Format(time.RFC3339)
}
return fj
}
// Get filtering configuration
func (f *Filtering) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
resp := filteringConfig{}
config.RLock()
resp.Enabled = config.DNS.FilteringEnabled
resp.Interval = config.DNS.FiltersUpdateIntervalHours
for _, f := range config.Filters {
fj := filterToJSON(f)
resp.Filters = append(resp.Filters, fj)
}
for _, f := range config.WhitelistFilters {
fj := filterToJSON(f)
resp.WhitelistFilters = append(resp.WhitelistFilters, fj)
}
resp.UserRules = config.UserRules
config.RUnlock()
jsonVal, err := json.Marshal(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
}
}
// Set filtering configuration
func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
req := filteringConfig{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
if !checkFiltersUpdateIntervalHours(req.Interval) {
aghhttp.Error(r, w, http.StatusBadRequest, "Unsupported interval")
return
}
func() {
config.Lock()
defer config.Unlock()
config.DNS.FilteringEnabled = req.Enabled
config.DNS.FiltersUpdateIntervalHours = req.Interval
}()
onConfigModified()
enableFilters(true)
}
type checkHostRespRule struct {
FilterListID int64 `json:"filter_list_id"`
Text string `json:"text"`
}
type checkHostResp struct {
Reason string `json:"reason"`
// FilterID is the ID of the rule's filter list.
//
// Deprecated: Use Rules[*].FilterListID.
FilterID int64 `json:"filter_id"`
// Rule is the text of the matched rule.
//
// Deprecated: Use Rules[*].Text.
Rule string `json:"rule"`
Rules []*checkHostRespRule `json:"rules"`
// for FilteredBlockedService:
SvcName string `json:"service_name"`
// for Rewrite:
CanonName string `json:"cname"` // CNAME value
IPList []net.IP `json:"ip_addrs"` // list of IP addresses
}
func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
host := q.Get("name")
setts := Context.dnsFilter.GetConfig()
setts.FilteringEnabled = true
setts.ProtectionEnabled = true
Context.dnsFilter.ApplyBlockedServices(&setts, nil, true)
result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"couldn't apply filtering: %s: %s",
host,
err,
)
return
}
resp := checkHostResp{}
resp.Reason = result.Reason.String()
resp.SvcName = result.ServiceName
resp.CanonName = result.CanonName
resp.IPList = result.IPList
if len(result.Rules) > 0 {
resp.FilterID = result.Rules[0].FilterListID
resp.Rule = result.Rules[0].Text
}
resp.Rules = make([]*checkHostRespRule, len(result.Rules))
for i, r := range result.Rules {
resp.Rules[i] = &checkHostRespRule{
FilterListID: r.FilterListID,
Text: r.Text,
}
}
js, err := json.Marshal(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
// RegisterFilteringHandlers - register handlers
func (f *Filtering) RegisterFilteringHandlers() {
httpRegister(http.MethodGet, "/control/filtering/status", f.handleFilteringStatus)
httpRegister(http.MethodPost, "/control/filtering/config", f.handleFilteringConfig)
httpRegister(http.MethodPost, "/control/filtering/add_url", f.handleFilteringAddURL)
httpRegister(http.MethodPost, "/control/filtering/remove_url", f.handleFilteringRemoveURL)
httpRegister(http.MethodPost, "/control/filtering/set_url", f.handleFilteringSetURL)
httpRegister(http.MethodPost, "/control/filtering/refresh", f.handleFilteringRefresh)
httpRegister(http.MethodPost, "/control/filtering/set_rules", f.handleFilteringSetRules)
httpRegister(http.MethodGet, "/control/filtering/check_host", f.handleCheckHost)
}
func checkFiltersUpdateIntervalHours(i uint32) bool {
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
}