Files
AdGuardHome/internal/filtering/http.go
Stanislav Chzhen aac36a2d2f Pull request 1907: 951-blocked-services-schedule-api
Updates #951.

Squashed commit of the following:

commit 6b840fd516f5a87fde0420e3aceb9c239b22c974
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Aug 29 19:53:03 2023 +0300

    client: imp docs more

commit 7fc8f0363fbe4c4266cb0f67428fe4d18c351d2d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Aug 29 19:40:00 2023 +0300

    client: imp docs

commit 00bc14d5760614f2797714cdc2c4c19b1a94b86e
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 18:43:49 2023 +0300

    try to fix lock file

commit d749df74b576091e0b58928d86ea8b3b49f919da
Merge: c69f9230b e1f6229e5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Aug 28 18:14:02 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule-api

commit c69f9230b12f7c983db06b74324b3df77d74b32b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 17:16:20 2023 +0300

    revert eslintrc

commit b37916c2dff0ddea5293d87570bb58e3443d2d21
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 12:02:39 2023 +0300

    fix translations

commit f5bb67d81506c687d0abd580049a3eee0af808e0
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 11:43:57 2023 +0300

    fix helpers

commit 13ec6a8b3a0acfb62762ae7e46c6e98eb7c82212
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 11:24:57 2023 +0300

    remove todo

commit 23724ec2fd683ed17b9f1cee841ad9aaf4c9d04f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 09:56:56 2023 +0300

    add clients schedule form

commit 84d29e558a329068e64e7a95ee183946aa4515b5
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 25 17:44:40 2023 +0300

    fix schedule form

commit 83e4017688082e9eb670091d5a24d98157050502
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 18 12:58:16 2023 +0300

    remove unused

commit ef2b68e138da382e3cf42586ae604e12d9493504
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 18 12:57:37 2023 +0300

    client: fix translation string

commit 32ea80c968f52f18adbc811b2f06874644cdfe20
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 18 12:26:26 2023 +0300

    wip schedule

commit 9b770873859186c9424c8d108812e32ddff33bad
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 21 14:29:50 2023 +0300

    all: imp naming

commit ea4e9514ea3b264bcce7f2a301db817de4e87059
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jul 19 18:09:27 2023 +0300

    all: imp code

commit 98a705bdaa5c1e79394c73e5d75af2416fe9f297
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jul 18 18:23:26 2023 +0300

    all: imp naming

commit 4f84b55c7bfc9f7b680feac0ec45f5ea9189299a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 14 15:01:17 2023 +0300

    all: add global schedule api

commit 87cf1646869ee9138964b47a27b7493674c8854a
Merge: cabb80ac1 2adc8624c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 14 12:09:29 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule-api

commit cabb80ac16de437a8118bb0166479574379c97a3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jul 13 13:37:23 2023 +0300

    openapi: fix typo

commit 2279b03acbcfc3d76216f8aaf30ae1c7894127bc
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jul 13 12:26:19 2023 +0300

    all: imp docs

... and 3 more commits
2023-08-29 20:03:40 +03:00

585 lines
15 KiB
Go

package filtering
import (
"encoding/json"
"fmt"
"net/http"
"net/netip"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
)
// validateFilterURL validates the filter list URL or file name.
func validateFilterURL(urlStr string) (err error) {
defer func() { err = errors.Annotate(err, "checking filter: %w") }()
if filepath.IsAbs(urlStr) {
_, err = os.Stat(urlStr)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
return nil
}
u, err := url.ParseRequestURI(urlStr)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
} else if s := u.Scheme; s != aghhttp.SchemeHTTP && s != aghhttp.SchemeHTTPS {
return &url.Error{
Op: "Check scheme",
URL: urlStr,
Err: fmt.Errorf("only %v allowed", []string{aghhttp.SchemeHTTP, aghhttp.SchemeHTTPS}),
}
}
return nil
}
type filterAddJSON struct {
Name string `json:"name"`
URL string `json:"url"`
Whitelist bool `json:"whitelist"`
}
func (d *DNSFilter) 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 {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
}
// Check for duplicates
if d.filterExists(fj.URL) {
err = errFilterExists
aghhttp.Error(r, w, http.StatusBadRequest, "Filter with URL %q: %s", fj.URL, err)
return
}
// Set necessary properties
filt := FilterYAML{
Enabled: true,
URL: fj.URL,
Name: fj.Name,
white: fj.Whitelist,
Filter: Filter{
ID: assignUniqueFilterID(),
},
}
// Download the filter contents
ok, err := d.update(&filt)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Couldn't fetch filter from URL %q: %s",
filt.URL,
err,
)
return
}
if !ok {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Filter with URL %q 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.
err = d.filterAdd(filt)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Filter with URL %q: %s", filt.URL, err)
return
}
d.ConfigModified()
d.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 (d *DNSFilter) 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
}
var deleted FilterYAML
func() {
d.filtersMu.Lock()
defer d.filtersMu.Unlock()
filters := &d.Filters
if req.Whitelist {
filters = &d.WhitelistFilters
}
delIdx := slices.IndexFunc(*filters, func(flt FilterYAML) bool {
return flt.URL == req.URL
})
if delIdx == -1 {
log.Error("deleting filter with url %q: %s", req.URL, errFilterNotExist)
return
}
deleted = (*filters)[delIdx]
p := deleted.Path(d.DataDir)
err = os.Rename(p, p+".old")
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Error("deleting filter %d: renaming file %q: %s", deleted.ID, p, err)
return
}
*filters = slices.Delete(*filters, delIdx, delIdx+1)
log.Info("deleted filter %d", deleted.ID)
}()
d.ConfigModified()
d.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 filterURLReqData struct {
Name string `json:"name"`
URL string `json:"url"`
Enabled bool `json:"enabled"`
}
type filterURLReq struct {
Data *filterURLReqData `json:"data"`
URL string `json:"url"`
Whitelist bool `json:"whitelist"`
}
func (d *DNSFilter) 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, "decoding request: %s", err)
return
}
if fj.Data == nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", errors.Error("data is absent"))
return
}
err = validateFilterURL(fj.Data.URL)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "invalid url: %s", err)
return
}
filt := FilterYAML{
Enabled: fj.Data.Enabled,
Name: fj.Data.Name,
URL: fj.Data.URL,
}
restart, err := d.filterSetProperties(fj.URL, filt, fj.Whitelist)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, err.Error())
return
}
d.ConfigModified()
if restart {
d.EnableFilters(true)
}
}
// filteringRulesReq is the JSON structure for settings custom filtering rules.
type filteringRulesReq struct {
Rules []string `json:"rules"`
}
func (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
if aghhttp.WriteTextPlainDeprecated(w, r) {
return
}
req := &filteringRulesReq{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
return
}
d.UserRules = req.Rules
d.ConfigModified()
d.EnableFilters(true)
}
func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
type Req struct {
White bool `json:"whitelist"`
}
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
}
var ok bool
resp := struct {
Updated int `json:"updated"`
}{}
resp.Updated, _, ok = d.tryRefreshFilters(!req.White, req.White, true)
if !ok {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"filters update procedure is already running",
)
return
}
aghhttp.WriteJSONResponseOK(w, r, resp)
}
type filterJSON struct {
URL string `json:"url"`
Name string `json:"name"`
LastUpdated string `json:"last_updated,omitempty"`
ID int64 `json:"id"`
RulesCount uint32 `json:"rules_count"`
Enabled bool `json:"enabled"`
}
type filteringConfig struct {
Filters []filterJSON `json:"filters"`
WhitelistFilters []filterJSON `json:"whitelist_filters"`
UserRules []string `json:"user_rules"`
Interval uint32 `json:"interval"` // in hours
Enabled bool `json:"enabled"`
}
func filterToJSON(f FilterYAML) 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 (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
resp := filteringConfig{}
d.filtersMu.RLock()
resp.Enabled = d.FilteringEnabled
resp.Interval = d.FiltersUpdateIntervalHours
for _, f := range d.Filters {
fj := filterToJSON(f)
resp.Filters = append(resp.Filters, fj)
}
for _, f := range d.WhitelistFilters {
fj := filterToJSON(f)
resp.WhitelistFilters = append(resp.WhitelistFilters, fj)
}
resp.UserRules = d.UserRules
d.filtersMu.RUnlock()
aghhttp.WriteJSONResponseOK(w, r, resp)
}
// Set filtering configuration
func (d *DNSFilter) 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 !ValidateUpdateIvl(req.Interval) {
aghhttp.Error(r, w, http.StatusBadRequest, "Unsupported interval")
return
}
func() {
d.filtersMu.Lock()
defer d.filtersMu.Unlock()
d.FilteringEnabled = req.Enabled
d.FiltersUpdateIntervalHours = req.Interval
}()
d.ConfigModified()
d.EnableFilters(true)
}
type checkHostRespRule struct {
Text string `json:"text"`
FilterListID int64 `json:"filter_list_id"`
}
type checkHostResp struct {
Reason string `json:"reason"`
// 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 []netip.Addr `json:"ip_addrs"` // list of IP addresses
// FilterID is the ID of the rule's filter list.
//
// Deprecated: Use Rules[*].FilterListID.
FilterID int64 `json:"filter_id"`
}
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("name")
setts := d.Settings()
setts.FilteringEnabled = true
setts.ProtectionEnabled = true
d.ApplyBlockedServices(setts)
result, err := d.CheckHost(host, dns.TypeA, setts)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"couldn't apply filtering: %s: %s",
host,
err,
)
return
}
rulesLen := len(result.Rules)
resp := checkHostResp{
Reason: result.Reason.String(),
SvcName: result.ServiceName,
CanonName: result.CanonName,
IPList: result.IPList,
Rules: make([]*checkHostRespRule, len(result.Rules)),
}
if rulesLen > 0 {
resp.FilterID = result.Rules[0].FilterListID
resp.Rule = result.Rules[0].Text
}
for i, r := range result.Rules {
resp.Rules[i] = &checkHostRespRule{
FilterListID: r.FilterListID,
Text: r.Text,
}
}
aghhttp.WriteJSONResponseOK(w, r, resp)
}
// setProtectedBool sets the value of a boolean pointer under a lock. l must
// protect the value under ptr.
//
// TODO(e.burkov): Make it generic?
func setProtectedBool(mu *sync.RWMutex, ptr *bool, val bool) {
mu.Lock()
defer mu.Unlock()
*ptr = val
}
// protectedBool gets the value of a boolean pointer under a read lock. l must
// protect the value under ptr.
//
// TODO(e.burkov): Make it generic?
func protectedBool(mu *sync.RWMutex, ptr *bool) (val bool) {
mu.RLock()
defer mu.RUnlock()
return *ptr
}
// handleSafeBrowsingEnable is the handler for the POST
// /control/safebrowsing/enable HTTP API.
func (d *DNSFilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled, true)
d.Config.ConfigModified()
}
// handleSafeBrowsingDisable is the handler for the POST
// /control/safebrowsing/disable HTTP API.
func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled, false)
d.Config.ConfigModified()
}
// handleSafeBrowsingStatus is the handler for the GET
// /control/safebrowsing/status HTTP API.
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled),
}
aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleParentalEnable is the handler for the POST /control/parental/enable
// HTTP API.
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.ParentalEnabled, true)
d.Config.ConfigModified()
}
// handleParentalDisable is the handler for the POST /control/parental/disable
// HTTP API.
func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.ParentalEnabled, false)
d.Config.ConfigModified()
}
// handleParentalStatus is the handler for the GET /control/parental/status
// HTTP API.
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.ParentalEnabled),
}
aghhttp.WriteJSONResponseOK(w, r, resp)
}
// RegisterFilteringHandlers - register handlers
func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP := d.HTTPRegister
if registerHTTP == nil {
return
}
registerHTTP(http.MethodPost, "/control/safebrowsing/enable", d.handleSafeBrowsingEnable)
registerHTTP(http.MethodPost, "/control/safebrowsing/disable", d.handleSafeBrowsingDisable)
registerHTTP(http.MethodGet, "/control/safebrowsing/status", d.handleSafeBrowsingStatus)
registerHTTP(http.MethodPost, "/control/parental/enable", d.handleParentalEnable)
registerHTTP(http.MethodPost, "/control/parental/disable", d.handleParentalDisable)
registerHTTP(http.MethodGet, "/control/parental/status", d.handleParentalStatus)
registerHTTP(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable)
registerHTTP(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable)
registerHTTP(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus)
registerHTTP(http.MethodPut, "/control/safesearch/settings", d.handleSafeSearchSettings)
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
registerHTTP(http.MethodPut, "/control/rewrite/update", d.handleRewriteUpdate)
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
registerHTTP(http.MethodGet, "/control/blocked_services/all", d.handleBlockedServicesAll)
// Deprecated handlers.
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
registerHTTP(http.MethodGet, "/control/blocked_services/get", d.handleBlockedServicesGet)
registerHTTP(http.MethodPut, "/control/blocked_services/update", d.handleBlockedServicesUpdate)
registerHTTP(http.MethodGet, "/control/filtering/status", d.handleFilteringStatus)
registerHTTP(http.MethodPost, "/control/filtering/config", d.handleFilteringConfig)
registerHTTP(http.MethodPost, "/control/filtering/add_url", d.handleFilteringAddURL)
registerHTTP(http.MethodPost, "/control/filtering/remove_url", d.handleFilteringRemoveURL)
registerHTTP(http.MethodPost, "/control/filtering/set_url", d.handleFilteringSetURL)
registerHTTP(http.MethodPost, "/control/filtering/refresh", d.handleFilteringRefresh)
registerHTTP(http.MethodPost, "/control/filtering/set_rules", d.handleFilteringSetRules)
registerHTTP(http.MethodGet, "/control/filtering/check_host", d.handleCheckHost)
}
// ValidateUpdateIvl returns false if i is not a valid filters update interval.
func ValidateUpdateIvl(i uint32) bool {
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
}