From adb422fedf64a2fc87bd296c9dc3d12c5e3a4d72 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Wed, 4 Sep 2019 14:12:00 +0300 Subject: [PATCH 1/3] filtering: refactor; change API; add "filters_update_interval" setting + config: "filters_update_interval" * add /control/filtering_info * remove /control/filtering/enable * remove /control/filtering/disable * add /control/filtering_config * remove /control/filtering/status * add /control/filtering/set_url * remove /control/filtering/enable_url * remove /control/filtering/disable_url --- AGHTechDoc.md | 78 ++++++++++++++ dnsfilter/dnsfilter.go | 6 +- dnsforward/dnsforward.go | 6 +- go.mod | 1 - home/config.go | 38 +++---- home/control.go | 13 +-- home/control_filtering.go | 208 +++++++++++++++++++++++--------------- home/control_install.go | 6 +- home/dns.go | 6 ++ home/filter.go | 52 +++++++--- home/filter_test.go | 37 +++++++ home/home.go | 18 +--- querylog/qlog.go | 4 +- querylog/querylog.go | 8 +- 14 files changed, 324 insertions(+), 157 deletions(-) create mode 100644 home/filter_test.go diff --git a/AGHTechDoc.md b/AGHTechDoc.md index cfeddd40..c2206449 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -45,6 +45,11 @@ Contents: * Query logs * API: Set querylog parameters * API: Get querylog parameters +* Filtering + * Filters update mechanism + * API: Get filtering parameters + * API: Set filtering parameters + * API: Set URL parameters ## Relations between subsystems @@ -1019,3 +1024,76 @@ Response: "enabled": true | false "interval": 1 | 7 | 30 | 90 } + + +## Filtering + +### Filters update mechanism + +Filters can be updated either manually by request from UI or automatically. +Auto-update interval can be configured in UI. If it is 0, auto-update is disabled. +When the last modification date of filter files is older than auto-update interval, auto-update procedure is started. +If an enabled filter file doesn't exist, it's downloaded on application startup. This includes the case when installation wizard is completed and there are no filter files yet. +When auto-update time comes, server starts the update procedure by downloading filter files. After new filter files are in place, it restarts DNS filtering module with new rules. +Only filters that are enabled by configuration can be updated. +As a result of the update procedure, all enabled filter files are written to disk, refreshed (their last modification date is equal to the current time) and loaded. + + +### API: Get filtering parameters + +Request: + + GET /control/filtering_info + +Response: + + 200 OK + + { + "enabled": true | false + "interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24 + "filters":[ + { + "id":1 + "enabled":true, + "url":"https://...", + "name":"...", + "rules_count":1234, + "last_updated":"2019-09-04T18:29:30+00:00", + } + ... + ], + "user_rules":["...", ...] + } + + +### API: Set filtering parameters + +Request: + + POST /control/filtering_config + + { + "enabled": true | false + "interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24 + } + +Response: + + 200 OK + + +### API: Set URL parameters + +Request: + + POST /control/filtering/set_url + + { + "url": "..." + "enabled": true | false + } + +Response: + + 200 OK diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go index 6b168a9c..14c12844 100644 --- a/dnsfilter/dnsfilter.go +++ b/dnsfilter/dnsfilter.go @@ -114,9 +114,9 @@ type Dnsfilter struct { // Filter represents a filter list type Filter struct { - ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase - Data []byte `json:"-" yaml:"-"` // List of rules divided by '\n' - FilePath string `json:"-" yaml:"-"` // Path to a filtering rules file + ID int64 // auto-assigned when filter is added (see nextFilterID) + Data []byte `yaml:"-"` // List of rules divided by '\n' + FilePath string `yaml:"-"` // Path to a filtering rules file } //go:generate stringer -type=Reason diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index ee1c1e0d..deb5892c 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -66,8 +66,10 @@ func NewServer(stats stats.Stats, queryLog querylog.QueryLog) *Server { // FilteringConfig represents the DNS filtering configuration of AdGuard Home // The zero FilteringConfig is empty and ready for use. type FilteringConfig struct { - ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features - FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists + ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features + FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists + FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours) + BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600) QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled diff --git a/go.mod b/go.mod index 8949d9b8..66751c7f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/AdguardTeam/golibs v0.2.1 github.com/AdguardTeam/urlfilter v0.5.0 github.com/NYTimes/gziphandler v1.1.1 - github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 github.com/etcd-io/bbolt v1.3.3 github.com/go-test/deep v1.0.1 diff --git a/home/config.go b/home/config.go index 9323b1ce..9062d148 100644 --- a/home/config.go +++ b/home/config.go @@ -10,7 +10,6 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/dhcpd" - "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/querylog" "github.com/AdguardTeam/AdGuardHome/stats" @@ -72,6 +71,7 @@ type configuration struct { client *http.Client stats stats.Stats queryLog querylog.QueryLog + filteringStarted bool // cached version.json to avoid hammering github.io for each page reload versionCheckJSON []byte @@ -172,16 +172,17 @@ var config = configuration{ Port: 53, StatsInterval: 1, FilteringConfig: dnsforward.FilteringConfig{ - ProtectionEnabled: true, // whether or not use any of dnsfilter features - FilteringEnabled: true, // whether or not use filter lists - BlockingMode: "nxdomain", // mode how to answer filtered requests - BlockedResponseTTL: 10, // in seconds - QueryLogEnabled: true, - QueryLogInterval: 1, - Ratelimit: 20, - RefuseAny: true, - BootstrapDNS: defaultBootstrap, - AllServers: false, + ProtectionEnabled: true, // whether or not use any of dnsfilter features + FilteringEnabled: true, // whether or not use filter lists + FiltersUpdateIntervalHours: 24, + BlockingMode: "nxdomain", // mode how to answer filtered requests + BlockedResponseTTL: 10, // in seconds + QueryLogEnabled: true, + QueryLogInterval: 1, + Ratelimit: 20, + RefuseAny: true, + BootstrapDNS: defaultBootstrap, + AllServers: false, }, UpstreamDNS: defaultDNS, }, @@ -191,12 +192,6 @@ var config = configuration{ PortDNSOverTLS: 853, // needs to be passed through to dnsproxy }, }, - Filters: []filter{ - {Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"}, - {Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"}, - {Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"}, - {Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"}, - }, DHCP: dhcpd.ServerConfig{ LeaseDuration: 86400, ICMPTimeout: 1000, @@ -226,6 +221,7 @@ func initConfig() { config.DNS.SafeSearchCacheSize = 1 * 1024 * 1024 config.DNS.ParentalCacheSize = 1 * 1024 * 1024 config.DNS.CacheTime = 30 + config.Filters = defaultFilters() } // getConfigFilename returns path to the current config file @@ -276,6 +272,9 @@ func parseConfig() error { if !checkStatsInterval(config.DNS.StatsInterval) { config.DNS.StatsInterval = 1 } + if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) { + config.DNS.FiltersUpdateIntervalHours = 24 + } if !checkQueryLogInterval(config.DNS.QueryLogInterval) { config.DNS.QueryLogInterval = 1 @@ -308,11 +307,6 @@ func parseConfig() error { return err } - // Deduplicate filters - deduplicateFilters() - - updateUniqueFilterID(config.Filters) - return nil } diff --git a/home/control.go b/home/control.go index 27187626..8fb2d15f 100644 --- a/home/control.go +++ b/home/control.go @@ -7,7 +7,6 @@ import ( "net/http" "strconv" "strings" - "time" "github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/dnsproxy/upstream" @@ -17,8 +16,6 @@ import ( "github.com/miekg/dns" ) -const updatePeriod = time.Hour * 24 - var protocols = []string{"tls://", "https://", "tcp://", "sdns://"} // ---------------- @@ -547,15 +544,6 @@ func registerControlHandlers() { httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage) http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) httpRegister(http.MethodPost, "/control/update", handleUpdate) - httpRegister(http.MethodPost, "/control/filtering/enable", handleFilteringEnable) - httpRegister(http.MethodPost, "/control/filtering/disable", handleFilteringDisable) - httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL) - httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL) - httpRegister(http.MethodPost, "/control/filtering/enable_url", handleFilteringEnableURL) - httpRegister(http.MethodPost, "/control/filtering/disable_url", handleFilteringDisableURL) - httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh) - httpRegister(http.MethodGet, "/control/filtering/status", handleFilteringStatus) - httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules) httpRegister(http.MethodPost, "/control/safebrowsing/enable", handleSafeBrowsingEnable) httpRegister(http.MethodPost, "/control/safebrowsing/disable", handleSafeBrowsingDisable) httpRegister(http.MethodGet, "/control/safebrowsing/status", handleSafeBrowsingStatus) @@ -575,6 +563,7 @@ func registerControlHandlers() { httpRegister(http.MethodGet, "/control/access/list", handleAccessList) httpRegister(http.MethodPost, "/control/access/set", handleAccessSet) + RegisterFilteringHandlers() RegisterTLSHandlers() RegisterClientsHandlers() registerRewritesHandlers() diff --git a/home/control_filtering.go b/home/control_filtering.go index 8eded166..03699953 100644 --- a/home/control_filtering.go +++ b/home/control_filtering.go @@ -5,74 +5,57 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "os" "strings" + "time" "github.com/AdguardTeam/golibs/log" - "github.com/asaskevich/govalidator" ) -func handleFilteringEnable(w http.ResponseWriter, r *http.Request) { - config.DNS.FilteringEnabled = true - httpUpdateConfigReloadDNSReturnOK(w, r) +// IsValidURL - return TRUE if URL is valid +func IsValidURL(rawurl string) bool { + url, err := url.ParseRequestURI(rawurl) + if err != nil { + return false //Couldn't even parse the rawurl + } + if len(url.Scheme) == 0 { + return false //No Scheme found + } + return true } -func handleFilteringDisable(w http.ResponseWriter, r *http.Request) { - config.DNS.FilteringEnabled = false - httpUpdateConfigReloadDNSReturnOK(w, r) -} - -func handleFilteringStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": config.DNS.FilteringEnabled, - } - - config.RLock() - data["filters"] = config.Filters - data["user_rules"] = config.UserRules - jsonVal, err := json.Marshal(data) - config.RUnlock() - - if err != nil { - httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } - - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) - if err != nil { - httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err) - return - } +type filterAddJSON struct { + Name string `json:"name"` + URL string `json:"url"` } func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) { - f := filter{} - err := json.NewDecoder(r.Body).Decode(&f) + fj := filterAddJSON{} + err := json.NewDecoder(r.Body).Decode(&fj) if err != nil { httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err) return } - if len(f.URL) == 0 { - http.Error(w, "URL parameter was not specified", http.StatusBadRequest) - return - } - - if valid := govalidator.IsRequestURL(f.URL); !valid { - http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest) + if !IsValidURL(fj.URL) { + http.Error(w, "Invalid URL", http.StatusBadRequest) return } // Check for duplicates - if filterExists(f.URL) { - httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", f.URL) + if filterExists(fj.URL) { + httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL) return } // Set necessary properties + f := filter{ + Enabled: true, + URL: fj.URL, + Name: fj.Name, + } f.ID = assignUniqueFilterID() - f.Enabled = true // Download the filter contents ok, err := f.update() @@ -133,7 +116,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) { return } - if valid := govalidator.IsRequestURL(req.URL); !valid { + if !IsValidURL(req.URL) { http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest) return } @@ -166,54 +149,27 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) { httpUpdateConfigReloadDNSReturnOK(w, r) } -func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) { - parameters, err := parseParametersFromBody(r.Body) - if err != nil { - httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err) - return - } - - url, ok := parameters["url"] - if !ok { - http.Error(w, "URL parameter was not specified", http.StatusBadRequest) - return - } - - if valid := govalidator.IsRequestURL(url); !valid { - http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest) - return - } - - found := filterEnable(url, true) - if !found { - http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) - return - } - - httpUpdateConfigReloadDNSReturnOK(w, r) +type filterURLJSON struct { + URL string `json:"url"` + Enabled bool `json:"enabled"` } -func handleFilteringDisableURL(w http.ResponseWriter, r *http.Request) { - parameters, err := parseParametersFromBody(r.Body) +func handleFilteringSetURL(w http.ResponseWriter, r *http.Request) { + fj := filterURLJSON{} + err := json.NewDecoder(r.Body).Decode(&fj) if err != nil { - httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err) + httpError(w, http.StatusBadRequest, "json decode: %s", err) return } - url, ok := parameters["url"] - if !ok { - http.Error(w, "URL parameter was not specified", http.StatusBadRequest) + if !IsValidURL(fj.URL) { + http.Error(w, "invalid URL", http.StatusBadRequest) return } - if valid := govalidator.IsRequestURL(url); !valid { - http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest) - return - } - - found := filterEnable(url, false) + found := filterEnable(fj.URL, fj.Enabled) if !found { - http.Error(w, "URL parameter was not previously added", http.StatusBadRequest) + http.Error(w, "URL doesn't exist", http.StatusBadRequest) return } @@ -235,3 +191,91 @@ func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) { updated := refreshFiltersIfNecessary(true) fmt.Fprintf(w, "OK %d filters updated\n", updated) } + +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"` + UserRules []string `json:"user_rules"` +} + +// Get filtering configuration +func handleFilteringInfo(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 := filterJSON{ + ID: f.ID, + Enabled: f.Enabled, + URL: f.URL, + Name: f.Name, + RulesCount: uint32(f.RulesCount), + } + + if f.LastUpdated.Second() != 0 { + fj.LastUpdated = f.LastUpdated.Format(time.RFC3339) + } + + resp.Filters = append(resp.Filters, fj) + } + resp.UserRules = config.UserRules + config.RUnlock() + + jsonVal, err := json.Marshal(resp) + if err != nil { + httpError(w, http.StatusInternalServerError, "json encode: %s", err) + return + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(jsonVal) + if err != nil { + httpError(w, http.StatusInternalServerError, "http write: %s", err) + } +} + +// Set filtering configuration +func handleFilteringConfig(w http.ResponseWriter, r *http.Request) { + req := filteringConfig{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + httpError(w, http.StatusBadRequest, "json decode: %s", err) + return + } + + if !checkFiltersUpdateIntervalHours(req.Interval) { + httpError(w, http.StatusBadRequest, "Unsupported interval") + return + } + + config.DNS.FilteringEnabled = req.Enabled + config.DNS.FiltersUpdateIntervalHours = req.Interval + httpUpdateConfigReloadDNSReturnOK(w, r) + + returnOK(w) +} + +// RegisterFilteringHandlers - register handlers +func RegisterFilteringHandlers() { + httpRegister(http.MethodGet, "/control/filtering_info", handleFilteringInfo) + httpRegister(http.MethodPost, "/control/filtering_config", handleFilteringConfig) + httpRegister(http.MethodPost, "/control/filtering/add_url", handleFilteringAddURL) + httpRegister(http.MethodPost, "/control/filtering/remove_url", handleFilteringRemoveURL) + httpRegister(http.MethodPost, "/control/filtering/set_url", handleFilteringSetURL) + httpRegister(http.MethodPost, "/control/filtering/refresh", handleFilteringRefresh) + httpRegister(http.MethodPost, "/control/filtering/set_rules", handleFilteringSetRules) +} + +func checkFiltersUpdateIntervalHours(i uint32) bool { + return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24 +} diff --git a/home/control_install.go b/home/control_install.go index e1193f6b..9eb47bfb 100644 --- a/home/control_install.go +++ b/home/control_install.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "os/exec" + "path/filepath" "strconv" "github.com/AdguardTeam/golibs/log" @@ -239,6 +240,9 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) { config.AuthName = newSettings.Username config.AuthPass = newSettings.Password + dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir) + initDNSServer(dnsBaseDir) + err = startDNSServer() if err != nil { config.firstRun = true @@ -255,8 +259,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) { return } - go refreshFiltersIfNecessary(false) - // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely if restartHTTP { diff --git a/home/dns.go b/home/dns.go index 53958bd5..a016d302 100644 --- a/home/dns.go +++ b/home/dns.go @@ -49,6 +49,7 @@ func initDNSServer(baseDir string) { config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog) initRDNS() + initFiltering() } func isRunning() bool { @@ -165,6 +166,11 @@ func startDNSServer() error { return errorx.Decorate(err, "Couldn't start forwarding DNS server") } + if !config.filteringStarted { + config.filteringStarted = true + startRefreshFilters() + } + return nil } diff --git a/home/filter.go b/home/filter.go index a3b6003b..73c4d9a0 100644 --- a/home/filter.go +++ b/home/filter.go @@ -21,13 +21,35 @@ var ( filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`) ) +func initFiltering() { + loadFilters() + deduplicateFilters() + updateUniqueFilterID(config.Filters) +} + +func startRefreshFilters() { + go func() { + _ = refreshFiltersIfNecessary(false) + }() + go periodicallyRefreshFilters() +} + +func defaultFilters() []filter { + return []filter{ + {Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"}, + {Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"}, + {Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"}, + {Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"}, + } +} + // field ordering is important -- yaml fields will mirror ordering from here type filter struct { - Enabled bool `json:"enabled"` - URL string `json:"url"` - Name string `json:"name" yaml:"name"` - RulesCount int `json:"rulesCount" yaml:"-"` - LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"-"` + Enabled bool + URL string + Name string `yaml:"name"` + RulesCount int `yaml:"-"` + LastUpdated time.Time `yaml:"-"` checksum uint32 // checksum of the file data dnsfilter.Filter `yaml:",inline"` @@ -119,8 +141,7 @@ func loadFilters() { err := filter.load() if err != nil { - // This is okay for the first start, the filter will be loaded later - log.Debug("Couldn't load filter %d contents due to %s", filter.ID, err) + log.Error("Couldn't load filter %d contents due to %s", filter.ID, err) } } } @@ -159,7 +180,12 @@ func assignUniqueFilterID() int64 { // Sets up a timer that will be checking for filters updates periodically func periodicallyRefreshFilters() { - for range time.Tick(time.Minute) { + for { + time.Sleep(1 * time.Hour) + if config.DNS.FiltersUpdateIntervalHours == 0 { + continue + } + refreshFiltersIfNecessary(false) } } @@ -180,10 +206,7 @@ func refreshFiltersIfNecessary(force bool) int { var updateFilters []filter var updateFlags []bool // 'true' if filter data has changed - if config.firstRun { - return 0 - } - + now := time.Now() config.RLock() for i := range config.Filters { f := &config.Filters[i] // otherwise we will be operating on a copy @@ -192,7 +215,8 @@ func refreshFiltersIfNecessary(force bool) int { continue } - if !force && time.Since(f.LastUpdated) <= updatePeriod { + expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60 + if !force && expireTime > now.Unix() { continue } @@ -214,7 +238,7 @@ func refreshFiltersIfNecessary(force bool) int { log.Printf("Failed to update filter %s: %s\n", uf.URL, err) continue } - uf.LastUpdated = time.Now() + uf.LastUpdated = now if updated { updateCount++ } diff --git a/home/filter_test.go b/home/filter_test.go new file mode 100644 index 00000000..63736c38 --- /dev/null +++ b/home/filter_test.go @@ -0,0 +1,37 @@ +package home + +import ( + "net/http" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestFilters(t *testing.T) { + config.client = &http.Client{ + Timeout: time.Minute * 5, + } + + f := filter{ + URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", + } + + // download + ok, err := f.update() + assert.True(t, ok && err == nil) + + // refresh + ok, err = f.update() + assert.True(t, !ok && err == nil) + + err = f.save() + assert.True(t, err == nil) + + err = f.load() + assert.True(t, err == nil) + + f.unload() + os.Remove(f.Path()) +} diff --git a/home/home.go b/home/home.go index 0ae64459..5f2d4fc0 100644 --- a/home/home.go +++ b/home/home.go @@ -135,22 +135,17 @@ func run(args options) { config.BindPort = args.bindPort } - loadFilters() - if !config.firstRun { // Save the updated config err := config.write() if err != nil { log.Fatal(err) } - } - // Init the DNS server instance before registering HTTP handlers - dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir) - initDNSServer(dnsBaseDir) + dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir) + initDNSServer(dnsBaseDir) - if !config.firstRun { - err := startDNSServer() + err = startDNSServer() if err != nil { log.Fatal(err) } @@ -165,13 +160,6 @@ func run(args options) { config.pidFileName = args.pidFile } - // Update filters we've just loaded right away, don't wait for periodic update timer - go func() { - refreshFiltersIfNecessary(false) - }() - // Schedule automatic filters updates - go periodicallyRefreshFilters() - // Initialize and run the admin Web interface box := packr.NewBox("../build/static") diff --git a/querylog/qlog.go b/querylog/qlog.go index cfc0604c..690f4e21 100644 --- a/querylog/qlog.go +++ b/querylog/qlog.go @@ -35,7 +35,7 @@ type queryLog struct { lock sync.RWMutex } -// newQueryLog creates a new instance of the query log +// create a new instance of the query log func newQueryLog(conf Config) *queryLog { l := queryLog{} l.logFile = filepath.Join(conf.BaseDir, queryLogFileName) @@ -53,7 +53,6 @@ func (l *queryLog) Configure(conf Config) { l.conf = conf } -// Clear memory buffer and remove the file func (l *queryLog) Clear() { l.fileFlushLock.Lock() defer l.fileFlushLock.Unlock() @@ -164,7 +163,6 @@ func (l *queryLog) Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Res } } -// getQueryLogJson returns a map with the current query log ready to be converted to a JSON func (l *queryLog) GetData() []map[string]interface{} { l.lock.RLock() values := make([]*logEntry, len(l.cache)) diff --git a/querylog/querylog.go b/querylog/querylog.go index c995183b..3a18eb53 100644 --- a/querylog/querylog.go +++ b/querylog/querylog.go @@ -10,14 +10,20 @@ import ( // QueryLog - main interface type QueryLog interface { + // Close query log object Close() // Set new configuration at runtime // Currently only 'Interval' field is supported. Configure(conf Config) + // Add a log entry Add(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Result, elapsed time.Duration, addr net.Addr, upstream string) + + // Get log entries GetData() []map[string]interface{} + + // Clear memory buffer and remove log files Clear() } @@ -27,7 +33,7 @@ type Config struct { Interval uint32 // interval to rotate logs (in hours) } -// New - create instance +// New - create a new instance of the query log func New(conf Config) QueryLog { return newQueryLog(conf) } From 57bb04685fec9ad22bd6a7231bfd3b4b65046e2f Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 6 Sep 2019 17:19:04 +0300 Subject: [PATCH 2/3] * openapi: update /filtering --- openapi/openapi.yaml | 100 +++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6542ee02..fa36fac9 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -438,34 +438,30 @@ paths: # Filtering status methods # -------------------------------------------------- - /filtering/status: + /filtering_info: get: tags: - filtering - operationId: filteringStatus - summary: 'Get status of rules-based filter' + operationId: filteringInfo + summary: 'Get filtering parameters' responses: 200: description: OK schema: - $ref: "#/definitions/FilteringStatus" + $ref: "#/definitions/FilterInfo" - /filtering/enable: + /filtering_config: post: tags: - filtering - operationId: filteringEnable - summary: 'Enable filtering' - responses: - 200: - description: OK - - /filtering/disable: - post: - tags: - - filtering - operationId: filteringDisable - summary: 'Disable filtering' + operationId: filteringConfig + summary: 'Set filtering parameters' + parameters: + - in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/FilterConfig" responses: 200: description: OK @@ -506,42 +502,19 @@ paths: 200: description: OK - /filtering/enable_url: + /filtering/set_url: post: tags: - filtering - operationId: filteringEnableURL - summary: 'Enable filter URL' + operationId: filteringSetURL + summary: 'Set URL parameters' consumes: - - text/plain + - application/json parameters: - - in: body - name: url - description: 'Previously added URL containing filtering rules' - required: true + - in: "body" + name: "body" schema: - type: string - example: 'url=https://filters.adtidy.org/windows/filters/15.txt' - responses: - 200: - description: OK - - /filtering/disable_url: - post: - tags: - - filtering - operationId: filteringDisableURL - summary: 'Disable filter URL' - consumes: - - text/plain - parameters: - - in: body - name: url - description: 'Previously added URL containing filtering rules' - required: true - schema: - type: string - example: 'url=https://filters.adtidy.org/windows/filters/15.txt' + $ref: "#/definitions/FilterSetUrl" responses: 200: description: OK @@ -1051,7 +1024,7 @@ definitions: lastUpdated: type: "string" format: "date-time" - example: "2018-10-30T12:18:57.223101822+03:00" + example: "2018-10-30T12:18:57+03:00" name: type: "string" example: "AdGuard Simplified Domain Names filter" @@ -1061,16 +1034,15 @@ definitions: url: type: "string" example: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt" - FilteringStatus: + + FilterInfo: type: "object" description: "Filtering settings" - required: - - "enabled" - - "filters" - - "user_rules" properties: enabled: type: "boolean" + interval: + type: "integer" filters: type: "array" items: @@ -1079,9 +1051,25 @@ definitions: type: "array" items: type: "string" - example: - - '||example.org^' - - '||example.com^' + + FilterConfig: + type: "object" + description: "Filtering settings" + properties: + enabled: + type: "boolean" + interval: + type: "integer" + + FilterSetUrl: + type: "object" + description: "Filtering URL settings" + properties: + url: + type: "string" + enabled: + type: "boolean" + GetVersionRequest: type: "object" description: "/version.json request data" From d0fc1dc54dfbc017f28c6c0afa4623c6259af557 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Thu, 12 Sep 2019 16:19:35 +0300 Subject: [PATCH 3/3] + client: handle filters configuration --- client/src/__locales/en.json | 8 +- client/src/__locales/ru.json | 20 +- client/src/actions/filtering.js | 145 ++++++++++++ client/src/actions/index.js | 158 +------------ client/src/api/Api.js | 49 ++-- client/src/components/Filters/Modal.js | 113 ++++------ client/src/components/Filters/UserRules.js | 12 +- client/src/components/Filters/index.js | 212 +++++++++++------- client/src/components/Logs/Logs.css | 4 + client/src/components/Logs/index.js | 6 +- .../components/Settings/FiltersConfig/Form.js | 88 ++++++++ .../Settings/FiltersConfig/index.js | 36 +++ client/src/components/Settings/Settings.css | 19 ++ client/src/components/Settings/index.js | 25 ++- client/src/components/ui/CellWrap.js | 19 ++ client/src/components/ui/Checkbox.css | 9 +- client/src/components/ui/Checkbox.js | 2 +- client/src/containers/Filters.js | 24 +- client/src/containers/Logs.js | 3 +- client/src/containers/Settings.js | 6 +- client/src/helpers/constants.js | 2 + client/src/helpers/form.js | 37 ++- client/src/helpers/helpers.js | 58 ++++- client/src/reducers/filtering.js | 86 +++++++ client/src/reducers/index.js | 64 +----- 25 files changed, 745 insertions(+), 460 deletions(-) create mode 100644 client/src/actions/filtering.js create mode 100644 client/src/components/Settings/FiltersConfig/Form.js create mode 100644 client/src/components/Settings/FiltersConfig/index.js create mode 100644 client/src/components/ui/CellWrap.js create mode 100644 client/src/reducers/filtering.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 570d28e1..9c59c654 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -378,5 +378,11 @@ "statistics_retention_desc": "If you decrease the interval value, some data will be lost", "statistics_clear": " Clear statistics", "statistics_clear_confirm": "Are you sure you want to clear statistics?", - "statistics_cleared": "Statistics successfully cleared" + "statistics_cleared": "Statistics successfully cleared", + "interval_hours": "{{count}} hour", + "interval_hours_plural": "{{count}} hours", + "filters_configuration": "Filters configuration", + "filters_enable": "Enable filters", + "filters_interval": "Filters update interval", + "disabled": "Disabled" } diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index 4bca94d4..88799f70 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -1,10 +1,9 @@ { "client_settings": "Настройки клиентов", "example_upstream_reserved": "вы можете указать DNS-сервер <0>для конкретного домена(ов)", - "upstream_parallel": "Использовать одновременные запроссы ко всем серверам для ускорения обработки запроса", + "upstream_parallel": "Использовать одновременные запросы ко всем серверам для ускорения обработки запроса", "bootstrap_dns": "Bootstrap DNS-серверы", "bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.", - "url_added_successfully": "URL успешно добавлен", "check_dhcp_servers": "Проверить DHCP-серверы", "save_config": "Сохранить конфигурацию", "enabled_dhcp": "DHCP-сервер включен", @@ -67,7 +66,6 @@ "disabled_protection": "Защита выкл.", "refresh_statics": "Обновить статистику", "dns_query": "DNS-запросы", - "blocked_by": "Заблокировано фильтрами", "stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты", "stats_adult": "Заблокированные \"взрослые\" сайты", "stats_query_domain": "Часто запрашиваемые домены", @@ -78,7 +76,6 @@ "top_clients": "Частые клиенты", "no_clients_found": "Клиентов не найдено", "general_statistics": "Общая статистика", - "number_of_dns_query_24_hours": "Количество DNS-запросов за 24 часа", "number_of_dns_query_blocked_24_hours": "Количество DNS-запросов, заблокированных фильтрами и блок-списками", "number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard", "number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных \"сайтов для взрослых\"", @@ -211,7 +208,7 @@ "install_devices_router_list_2": "Найдите настройки DHCP или DNS. Найдите буквы \"DNS\" рядом с текстовым полем, в которое можно ввести два или три ряда цифр, разделенных на 4 группы от одной до трёх цифр.", "install_devices_router_list_3": "Введите туда адрес вашего AdGuard Home.", "install_devices_windows_list_1": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.", - "install_devices_windows_list_2": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.", + "install_devices_windows_list_2": "Перейдите в \"Сеть и интернет\", а затем в \"Центр управления сетями и общим доступом\"", "install_devices_windows_list_3": "В левой стороне экрана найдите \"Изменение параметров адаптера\" и кликните по нему.", "install_devices_windows_list_4": "Выделите ваше активное подключение, затем кликните по нему правой клавишей мыши и выберите \"Свойства\".", "install_devices_windows_list_5": "Найдите в списке пункт \"IP версии 4 (TCP/IP)\", выделите его и затем снова нажмите \"Свойства\".", @@ -298,7 +295,6 @@ "client_deleted": "Клиент \"{{key}}\" успешно удален", "client_added": "Клиент \"{{key}}\" успешно добавлен", "client_updated": "Клиент \"{{key}}\" успешно обновлен", - "table_statistics": "Количество запросов (последние 24 часа)", "clients_not_found": "Клиентов не найдено", "client_confirm_delete": "Вы уверены, что хотите удалить клиента \"{{key}}\"?", "filter_confirm_delete": "Вы уверены, что хотите удалить фильтр?", @@ -309,7 +305,7 @@ "access_allowed_title": "Разрешенные клиенты", "access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.", "access_disallowed_title": "Запрещенные клиенты", - "access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроек, AdGuard Home будет игнорировать запросы с этих IP-адресов.", + "access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет игнорировать запросы с этих IP-адресов.", "access_blocked_title": "Заблокированные домены", "access_blocked_desc": "Не путайте это с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами.", "access_settings_saved": "Настройки доступа успешно сохранены", @@ -353,5 +349,13 @@ "blocked_services_global": "Использовать глобальные заблокированные сервисы", "blocked_service": "Заблокированный сервис", "block_all": "Заблокировать все", - "unblock_all": "Разблокировать все" + "unblock_all": "Разблокировать все", + "domain": "Домен", + "answer": "Ответ", + "interval_hours_0": "{{count}} час", + "interval_hours_1": "{{count}} часа", + "interval_hours_2": "{{count}} часов", + "interval_days_0": "{{count}} день", + "interval_days_1": "{{count}} дня", + "interval_days_2": "{{count}} дней" } \ No newline at end of file diff --git a/client/src/actions/filtering.js b/client/src/actions/filtering.js new file mode 100644 index 00000000..3f0c59d9 --- /dev/null +++ b/client/src/actions/filtering.js @@ -0,0 +1,145 @@ +import { createAction } from 'redux-actions'; +import { showLoading, hideLoading } from 'react-redux-loading-bar'; + +import { normalizeFilteringStatus, normalizeRulesTextarea } from '../helpers/helpers'; +import { addErrorToast, addSuccessToast } from './index'; +import apiClient from '../api/Api'; + +export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE'); +export const handleRulesChange = createAction('HANDLE_RULES_CHANGE'); + +export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST'); +export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE'); +export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS'); + +export const getFilteringStatus = () => async (dispatch) => { + dispatch(getFilteringStatusRequest()); + try { + const status = await apiClient.getFilteringStatus(); + dispatch(getFilteringStatusSuccess({ ...normalizeFilteringStatus(status) })); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getFilteringStatusFailure()); + } +}; + +export const setRulesRequest = createAction('SET_RULES_REQUEST'); +export const setRulesFailure = createAction('SET_RULES_FAILURE'); +export const setRulesSuccess = createAction('SET_RULES_SUCCESS'); + +export const setRules = rules => async (dispatch) => { + dispatch(setRulesRequest()); + try { + const normalizedRules = normalizeRulesTextarea(rules); + await apiClient.setRules(normalizedRules); + dispatch(addSuccessToast('updated_custom_filtering_toast')); + dispatch(setRulesSuccess()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setRulesFailure()); + } +}; + +export const addFilterRequest = createAction('ADD_FILTER_REQUEST'); +export const addFilterFailure = createAction('ADD_FILTER_FAILURE'); +export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS'); + +export const addFilter = (url, name) => async (dispatch) => { + dispatch(addFilterRequest()); + try { + await apiClient.addFilter(url, name); + dispatch(addFilterSuccess(url)); + dispatch(toggleFilteringModal()); + dispatch(addSuccessToast('filter_added_successfully')); + dispatch(getFilteringStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(addFilterFailure()); + } +}; + +export const removeFilterRequest = createAction('REMOVE_FILTER_REQUEST'); +export const removeFilterFailure = createAction('REMOVE_FILTER_FAILURE'); +export const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS'); + +export const removeFilter = url => async (dispatch) => { + dispatch(removeFilterRequest()); + try { + await apiClient.removeFilter(url); + dispatch(removeFilterSuccess(url)); + dispatch(getFilteringStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(removeFilterFailure()); + } +}; + +export const toggleFilterRequest = createAction('FILTER_TOGGLE_REQUEST'); +export const toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE'); +export const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS'); + +export const toggleFilterStatus = (url, enabled) => async (dispatch) => { + dispatch(toggleFilterRequest()); + try { + await apiClient.setFilterUrl({ url, enabled: !enabled }); + dispatch(toggleFilterSuccess(url)); + dispatch(getFilteringStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleFilterFailure()); + } +}; + +export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST'); +export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE'); +export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS'); + +export const refreshFilters = () => async (dispatch) => { + dispatch(refreshFiltersRequest()); + dispatch(showLoading()); + try { + const refreshText = await apiClient.refreshFilters(); + dispatch(refreshFiltersSuccess()); + + if (refreshText.includes('OK')) { + if (refreshText.includes('OK 0')) { + dispatch(addSuccessToast('all_filters_up_to_date_toast')); + } else { + dispatch(addSuccessToast(refreshText.replace(/OK /g, ''))); + } + } else { + dispatch(addErrorToast({ error: refreshText })); + } + + dispatch(getFilteringStatus()); + dispatch(hideLoading()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(refreshFiltersFailure()); + dispatch(hideLoading()); + } +}; + +export const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST'); +export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE'); +export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS'); + +export const setFiltersConfig = config => async (dispatch, getState) => { + dispatch(setFiltersConfigRequest()); + try { + const { enabled } = config; + const prevEnabled = getState().filtering.enabled; + let successToastMessage = 'config_successfully_saved'; + + if (prevEnabled !== enabled) { + successToastMessage = enabled ? 'enabled_filtering_toast' : 'disabled_filtering_toast'; + } + + await apiClient.setFiltersConfig(config); + dispatch(addSuccessToast(successToastMessage)); + dispatch(setFiltersConfigSuccess(config)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setFiltersConfigFailure()); + } +}; diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 8a132060..88e14ef9 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -1,10 +1,9 @@ import { createAction } from 'redux-actions'; import { t } from 'i18next'; -import { showLoading, hideLoading } from 'react-redux-loading-bar'; import axios from 'axios'; import versionCompare from '../helpers/versionCompare'; -import { normalizeFilteringStatus, normalizeTextarea, sortClients } from '../helpers/helpers'; +import { normalizeTextarea, sortClients } from '../helpers/helpers'; import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants'; import { getTlsStatus } from './encryption'; import apiClient from '../api/Api'; @@ -21,16 +20,6 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => { let successMessage = ''; try { switch (settingKey) { - case SETTINGS_NAMES.filtering: - if (status) { - successMessage = 'disabled_filtering_toast'; - await apiClient.disableFiltering(); - } else { - successMessage = 'enabled_filtering_toast'; - await apiClient.enableFiltering(); - } - dispatch(toggleSettingStatus({ settingKey })); - break; case SETTINGS_NAMES.safebrowsing: if (status) { successMessage = 'disabled_safe_browsing_toast'; @@ -77,18 +66,15 @@ export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS'); export const initSettings = settingsList => async (dispatch) => { dispatch(initSettingsRequest()); try { - const filteringStatus = await apiClient.getFilteringStatus(); const safebrowsingStatus = await apiClient.getSafebrowsingStatus(); const parentalStatus = await apiClient.getParentalStatus(); const safesearchStatus = await apiClient.getSafesearchStatus(); const { - filtering, safebrowsing, parental, safesearch, } = settingsList; const newSettingsList = { - filtering: { ...filtering, enabled: filteringStatus.enabled }, safebrowsing: { ...safebrowsing, enabled: safebrowsingStatus.enabled }, parental: { ...parental, enabled: parentalStatus.enabled }, safesearch: { ...safesearch, enabled: safesearchStatus.enabled }, @@ -100,21 +86,6 @@ export const initSettings = settingsList => async (dispatch) => { } }; -export const getFilteringRequest = createAction('GET_FILTERING_REQUEST'); -export const getFilteringFailure = createAction('GET_FILTERING_FAILURE'); -export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS'); - -export const getFiltering = () => async (dispatch) => { - dispatch(getFilteringRequest()); - try { - const filteringStatus = await apiClient.getFilteringStatus(); - dispatch(getFilteringSuccess(filteringStatus.enabled)); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getFilteringFailure()); - } -}; - export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST'); export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE'); export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS'); @@ -290,133 +261,6 @@ export const disableDns = () => async (dispatch) => { } }; -export const setRulesRequest = createAction('SET_RULES_REQUEST'); -export const setRulesFailure = createAction('SET_RULES_FAILURE'); -export const setRulesSuccess = createAction('SET_RULES_SUCCESS'); - -export const setRules = rules => async (dispatch) => { - dispatch(setRulesRequest()); - try { - const replacedLineEndings = rules - .replace(/^\n/g, '') - .replace(/\n\s*\n/g, '\n'); - await apiClient.setRules(replacedLineEndings); - dispatch(addSuccessToast('updated_custom_filtering_toast')); - dispatch(setRulesSuccess()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(setRulesFailure()); - } -}; - -export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST'); -export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE'); -export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS'); - -export const getFilteringStatus = () => async (dispatch) => { - dispatch(getFilteringStatusRequest()); - try { - const status = await apiClient.getFilteringStatus(); - dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) })); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getFilteringStatusFailure()); - } -}; - -export const toggleFilterRequest = createAction('FILTER_ENABLE_REQUEST'); -export const toggleFilterFailure = createAction('FILTER_ENABLE_FAILURE'); -export const toggleFilterSuccess = createAction('FILTER_ENABLE_SUCCESS'); - -export const toggleFilterStatus = url => async (dispatch, getState) => { - dispatch(toggleFilterRequest()); - const state = getState(); - const { filters } = state.filtering; - const filter = filters.filter(filter => filter.url === url)[0]; - const { enabled } = filter; - let toggleStatusMethod; - if (enabled) { - toggleStatusMethod = apiClient.disableFilter.bind(apiClient); - } else { - toggleStatusMethod = apiClient.enableFilter.bind(apiClient); - } - try { - await toggleStatusMethod(url); - dispatch(toggleFilterSuccess(url)); - dispatch(getFilteringStatus()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(toggleFilterFailure()); - } -}; - -export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST'); -export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE'); -export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS'); - -export const refreshFilters = () => async (dispatch) => { - dispatch(refreshFiltersRequest()); - dispatch(showLoading()); - try { - const refreshText = await apiClient.refreshFilters(); - dispatch(refreshFiltersSuccess()); - - if (refreshText.includes('OK')) { - if (refreshText.includes('OK 0')) { - dispatch(addSuccessToast('all_filters_up_to_date_toast')); - } else { - dispatch(addSuccessToast(refreshText.replace(/OK /g, ''))); - } - } else { - dispatch(addErrorToast({ error: refreshText })); - } - - dispatch(getFilteringStatus()); - dispatch(hideLoading()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(refreshFiltersFailure()); - dispatch(hideLoading()); - } -}; - -export const handleRulesChange = createAction('HANDLE_RULES_CHANGE'); - -export const addFilterRequest = createAction('ADD_FILTER_REQUEST'); -export const addFilterFailure = createAction('ADD_FILTER_FAILURE'); -export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS'); - -export const addFilter = (url, name) => async (dispatch) => { - dispatch(addFilterRequest()); - try { - await apiClient.addFilter(url, name); - dispatch(addFilterSuccess(url)); - dispatch(getFilteringStatus()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(addFilterFailure()); - } -}; - - -export const removeFilterRequest = createAction('ADD_FILTER_REQUEST'); -export const removeFilterFailure = createAction('ADD_FILTER_FAILURE'); -export const removeFilterSuccess = createAction('ADD_FILTER_SUCCESS'); - -export const removeFilter = url => async (dispatch) => { - dispatch(removeFilterRequest()); - try { - await apiClient.removeFilter(url); - dispatch(removeFilterSuccess(url)); - dispatch(getFilteringStatus()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(removeFilterFailure()); - } -}; - -export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE'); - export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE'); export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST'); export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE'); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index f39b28dc..187b7312 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -90,32 +90,19 @@ class Api { } // Filtering - FILTERING_STATUS = { path: 'filtering/status', method: 'GET' }; - FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' }; - FILTERING_DISABLE = { path: 'filtering/disable', method: 'POST' }; + FILTERING_INFO = { path: 'filtering_info', method: 'GET' }; FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' }; FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' }; FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' }; - FILTERING_ENABLE_FILTER = { path: 'filtering/enable_url', method: 'POST' }; - FILTERING_DISABLE_FILTER = { path: 'filtering/disable_url', method: 'POST' }; FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' }; + FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' }; + FILTERING_CONFIG = { path: 'filtering_config', method: 'POST' }; getFilteringStatus() { - const { path, method } = this.FILTERING_STATUS; + const { path, method } = this.FILTERING_INFO; return this.makeRequest(path, method); } - enableFiltering() { - const { path, method } = this.FILTERING_ENABLE; - return this.makeRequest(path, method); - } - - disableFiltering() { - const { path, method } = this.FILTERING_DISABLE; - return this.makeRequest(path, method); - } - - // TODO find out when to use force parameter refreshFilters() { const { path, method } = this.FILTERING_REFRESH; return this.makeRequest(path, method); @@ -151,26 +138,22 @@ class Api { return this.makeRequest(path, method, parameters); } - enableFilter(url) { - const { path, method } = this.FILTERING_ENABLE_FILTER; - const parameter = 'url'; - const requestBody = `${parameter}=${url}`; - const config = { - data: requestBody, - header: { 'Content-Type': 'text/plain' }, + setFiltersConfig(config) { + const { path, method } = this.FILTERING_CONFIG; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, }; - return this.makeRequest(path, method, config); + return this.makeRequest(path, method, parameters); } - disableFilter(url) { - const { path, method } = this.FILTERING_DISABLE_FILTER; - const parameter = 'url'; - const requestBody = `${parameter}=${url}`; - const config = { - data: requestBody, - header: { 'Content-Type': 'text/plain' }, + setFilterUrl(config) { + const { path, method } = this.FILTERING_SET_URL; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, }; - return this.makeRequest(path, method, config); + return this.makeRequest(path, method, parameters); } // Parental diff --git a/client/src/components/Filters/Modal.js b/client/src/components/Filters/Modal.js index 884117b7..c4b89437 100644 --- a/client/src/components/Filters/Modal.js +++ b/client/src/components/Filters/Modal.js @@ -33,27 +33,13 @@ class Modal extends Component { this.setState({ ...this.state, name }); }; - handleNext = () => { - this.props.addFilter(this.state.url, this.state.name); - setTimeout(() => { - if (this.props.isFilterAdded) { - this.closeModal(); - } - }, 2000); - }; - closeModal = () => { this.props.toggleModal(); this.setState({ ...this.state, ...initialState }); - } + }; render() { - const { - isOpen, - title, - inputDescription, - processingAddFilter, - } = this.props; + const { isOpen, processingAddFilter } = this.props; const { isUrlValid, url, name } = this.state; const inputUrlClass = classnames({ 'form-control mb-2': true, @@ -64,28 +50,7 @@ class Modal extends Component { 'form-control mb-2': true, 'is-valid': name.length > 0, }); - - const renderBody = () => { - if (!this.props.isFilterAdded) { - return ( - - - - {inputDescription && -
- {inputDescription} -
} -
- ); - } - return ( -
- filter_added_successfully -
- ); - }; - - const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0); + const isValidForSubmit = url.length > 0 && isUrlValid && name.length > 0; return (
-

- {title} -

- +

+ new_filter_btn +

+
- {renderBody()} -
- {!this.props.isFilterAdded && -
- - + + +
+ enter_valid_filter_url
- } +
+
+ + +
); @@ -134,12 +111,10 @@ class Modal extends Component { Modal.propTypes = { toggleModal: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, - title: PropTypes.string.isRequired, - inputDescription: PropTypes.string, addFilter: PropTypes.func.isRequired, - isFilterAdded: PropTypes.bool, - processingAddFilter: PropTypes.bool, - t: PropTypes.func, + isFilterAdded: PropTypes.bool.isRequired, + processingAddFilter: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, }; export default withNamespaces()(Modal); diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js index 39d39351..d2d78e91 100644 --- a/client/src/components/Filters/UserRules.js +++ b/client/src/components/Filters/UserRules.js @@ -15,13 +15,13 @@ class UserRules extends Component { }; render() { - const { t } = this.props; + const { t, userRules } = this.props; return (