From 01f5a13dd2499b9699e4837bb0ee0baa0c52f1a8 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 16 Jul 2019 17:42:11 +0300 Subject: [PATCH 01/12] + doc: add "Services Filter" section --- AGHTechDoc.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/AGHTechDoc.md b/AGHTechDoc.md index bd526fc8..54becca5 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -31,6 +31,9 @@ Contents: * API: List rewrite entries * API: Add a rewrite entry * API: Remove a rewrite entry +* Services Filter + * API: Get blocked services list + * API: Set blocked services list ## First startup @@ -536,6 +539,8 @@ Notes: * If `use_global_settings` is false, then the client-specific settings are used to override (enable or disable) global settings. +* If `use_global_blocked_services` is false, then the client-specific settings are used to override (enable or disable) global Blocked Services settings. + ### Get list of clients @@ -558,6 +563,8 @@ Response: parental_enabled: false safebrowsing_enabled: false safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] } ] auto_clients: [ @@ -585,6 +592,8 @@ Request: parental_enabled: false safebrowsing_enabled: false safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] } Response: @@ -613,6 +622,8 @@ Request: parental_enabled: false safebrowsing_enabled: false safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] } } @@ -743,3 +754,49 @@ Request: Response: 200 OK + + +## Services Filter + +Allows to quickly block popular sites globally or for specific client only. +UI manages these settings via global or per-client API. +UI and server have the same list of the services supported and this list must always be in synchronization. +UI code also contains icons for each service: `client/src/components/ui/Icons.js`. + +How it works: +* UI presents the list of services which user may want to block +* Admin clicks on the checkboxes in front of the services to block and presses Save +* UI sends `Set blocked services list` or `Update client` message +* Server updates the internal configuration +* When a user sends a DNS request for a host which is blocked by these settings, he won't receive its IP address +* Query log will show that this request was blocked by "Blocked services" + +Internally, all supported services are stored as a map: + + service name -> list of rules + + +### API: Get blocked services list + +Request: + + GET /control/blocked_services/list + +Response: + + 200 OK + + [ "name1", ... ] + + +### API: Set blocked services list + +Request: + + POST /control/blocked_services/set + + [ "name1", ... ] + +Response: + + 200 OK From 8307a5a4945b9313149f37b0b51c5a7a8d135f43 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Wed, 17 Jul 2019 19:17:35 +0300 Subject: [PATCH 02/12] + config: store/load "blocked_services" per-client setting --- home/config.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/home/config.go b/home/config.go index 9fa4cbad..f606ae03 100644 --- a/home/config.go +++ b/home/config.go @@ -37,6 +37,9 @@ type clientObject struct { ParentalEnabled bool `yaml:"parental_enabled"` SafeSearchEnabled bool `yaml:"safebrowsing_enabled"` SafeBrowsingEnabled bool `yaml:"safesearch_enabled"` + + UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` + BlockedServices []string `yaml:"blocked_services"` } type HTTPSServer struct { @@ -266,6 +269,9 @@ func parseConfig() error { ParentalEnabled: cy.ParentalEnabled, SafeSearchEnabled: cy.SafeSearchEnabled, SafeBrowsingEnabled: cy.SafeBrowsingEnabled, + + UseOwnBlockedServices: !cy.UseGlobalBlockedServices, + BlockedServices: cy.BlockedServices, } _, err = config.clients.Add(cli) if err != nil { @@ -317,6 +323,9 @@ func (c *configuration) write() error { ParentalEnabled: cli.ParentalEnabled, SafeSearchEnabled: cli.SafeSearchEnabled, SafeBrowsingEnabled: cli.SafeBrowsingEnabled, + + UseGlobalBlockedServices: !cli.UseOwnBlockedServices, + BlockedServices: cli.BlockedServices, } config.Clients = append(config.Clients, cy) } From 04a477c14a1eeea6426c07d5ae0434afe0b6b398 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 18 Jul 2019 12:27:10 +0300 Subject: [PATCH 03/12] + clients: add BlockedServices field --- home/clients.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/home/clients.go b/home/clients.go index 0c003a56..66253844 100644 --- a/home/clients.go +++ b/home/clients.go @@ -31,6 +31,9 @@ type Client struct { SafeSearchEnabled bool SafeBrowsingEnabled bool ParentalEnabled bool + + UseOwnBlockedServices bool // false: use global settings + BlockedServices []string } type clientJSON struct { @@ -42,6 +45,9 @@ type clientJSON struct { ParentalEnabled bool `json:"parental_enabled"` SafeSearchEnabled bool `json:"safebrowsing_enabled"` SafeBrowsingEnabled bool `json:"safesearch_enabled"` + + UseGlobalBlockedServices bool `json:"use_global_blocked_services"` + BlockedServices []string `json:"blocked_services"` } type clientSource uint @@ -391,6 +397,9 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) { ParentalEnabled: c.ParentalEnabled, SafeSearchEnabled: c.SafeSearchEnabled, SafeBrowsingEnabled: c.SafeBrowsingEnabled, + + UseGlobalBlockedServices: !c.UseOwnBlockedServices, + BlockedServices: c.BlockedServices, } if len(c.MAC) != 0 { @@ -438,6 +447,9 @@ func jsonToClient(cj clientJSON) (*Client, error) { ParentalEnabled: cj.ParentalEnabled, SafeSearchEnabled: cj.SafeSearchEnabled, SafeBrowsingEnabled: cj.SafeBrowsingEnabled, + + UseOwnBlockedServices: !cj.UseGlobalBlockedServices, + BlockedServices: cj.BlockedServices, } return &c, nil } From e81a9c7d5686dfab00c7d3d23d58ae13db1964a4 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 23 Jul 2019 12:21:37 +0300 Subject: [PATCH 04/12] + dnsfilter: use global and per-client BlockedServices array --- dnsfilter/dnsfilter.go | 42 +++++++++++++++++++++- dnsfilter/dnsfilter_test.go | 16 +++++++++ dnsforward/dnsforward.go | 4 +++ dnsforward/querylog.go | 4 +++ home/blocked_services.go | 70 +++++++++++++++++++++++++++++++++++++ home/dns.go | 23 ++++++++++-- home/home.go | 1 + 7 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 home/blocked_services.go diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go index 20d17493..cc724ff7 100644 --- a/dnsfilter/dnsfilter.go +++ b/dnsfilter/dnsfilter.go @@ -39,12 +39,19 @@ const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sens const defaultParentalSensitivity = 13 // use "TEEN" by default const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control +// ServiceEntry - blocked service array element +type ServiceEntry struct { + Name string + Rules []*urlfilter.NetworkRule +} + // RequestFilteringSettings is custom filtering settings type RequestFilteringSettings struct { FilteringEnabled bool SafeSearchEnabled bool SafeBrowsingEnabled bool ParentalEnabled bool + ServicesRules []ServiceEntry } // RewriteEntry is a rewrite array element @@ -139,6 +146,8 @@ const ( FilteredInvalid // FilteredSafeSearch - the host was replaced with safesearch variant FilteredSafeSearch + // FilteredBlockedService - the host is blocked by "blocked services" settings + FilteredBlockedService // ReasonRewrite - rewrite rule was applied ReasonRewrite @@ -155,6 +164,7 @@ func (i Reason) String() string { "FilteredParental", "FilteredInvalid", "FilteredSafeSearch", + "FilteredBlockedService", "Rewrite", } @@ -185,6 +195,9 @@ type Result struct { // for ReasonRewrite: CanonName string `json:",omitempty"` // CNAME value IPList []net.IP `json:",omitempty"` // list of IP addresses + + // for FilteredBlockedService: + ServiceName string `json:",omitempty"` // Name of the blocked service } // Matched can be used to see if any match at all was found, no matter filtered or not @@ -209,7 +222,7 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res setts.SafeSearchEnabled = d.SafeSearchEnabled setts.SafeBrowsingEnabled = d.SafeBrowsingEnabled setts.ParentalEnabled = d.ParentalEnabled - if len(clientAddr) != 0 && d.FilterHandler != nil { + if d.FilterHandler != nil { d.FilterHandler(clientAddr, &setts) } @@ -232,6 +245,13 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res } } + if len(setts.ServicesRules) != 0 { + result = matchBlockedServicesRules(host, setts.ServicesRules) + if result.Reason.Matched() { + return result, nil + } + } + // check safeSearch if no match if setts.SafeSearchEnabled { result, err = d.checkSafeSearch(host) @@ -326,6 +346,26 @@ func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result { return res } +func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result { + req := urlfilter.NewRequestForHostname(host) + res := Result{} + + for _, s := range svcs { + for _, rule := range s.Rules { + if rule.Match(req) { + res.Reason = FilteredBlockedService + res.IsFiltered = true + res.ServiceName = s.Name + res.Rule = rule.Text() + log.Debug("Blocked Services: matched rule: %s host: %s service: %s", + res.Rule, host, s.Name) + return res + } + } + } + return res +} + func setCacheResult(cache *fastcache.Cache, host string, res Result) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) diff --git a/dnsfilter/dnsfilter_test.go b/dnsfilter/dnsfilter_test.go index 7b78b7a5..7df5fe09 100644 --- a/dnsfilter/dnsfilter_test.go +++ b/dnsfilter/dnsfilter_test.go @@ -10,8 +10,10 @@ import ( "testing" "time" + "github.com/AdguardTeam/urlfilter" "github.com/bluele/gcache" "github.com/miekg/dns" + "github.com/stretchr/testify/assert" ) // HELPERS @@ -453,6 +455,12 @@ func applyClientSettings(clientAddr string, setts *RequestFilteringSettings) { setts.FilteringEnabled = false setts.ParentalEnabled = false setts.SafeBrowsingEnabled = true + + rule, _ := urlfilter.NewNetworkRule("||facebook.com^", 0) + s := ServiceEntry{} + s.Name = "facebook" + s.Rules = []*urlfilter.NetworkRule{rule} + setts.ServicesRules = append(setts.ServicesRules, s) } // Check behaviour without any per-client settings, @@ -485,6 +493,10 @@ func TestClientSettings(t *testing.T) { t.Fatalf("CheckHost safesearch") } + // not blocked + r, _ = d.CheckHost("facebook.com", dns.TypeA, "1.1.1.1") + assert.True(t, !r.IsFiltered) + // override client settings: d.FilterHandler = applyClientSettings @@ -505,6 +517,10 @@ func TestClientSettings(t *testing.T) { if !r.IsFiltered || r.Reason != FilteredSafeBrowsing { t.Fatalf("CheckHost FilteredSafeBrowsing") } + + // blocked by additional rules + r, _ = d.CheckHost("facebook.com", dns.TypeA, "1.1.1.1") + assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService) } // BENCHMARKS diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index 1661f68b..d09bf7be 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -96,6 +96,10 @@ type FilteringConfig struct { ParentalBlockHost string `yaml:"parental_block_host"` SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"` + // Names of services to block (globally). + // Per-client settings can override this configuration. + BlockedServices []string `json:"blocked_services"` + dnsfilter.Config `yaml:",inline"` } diff --git a/dnsforward/querylog.go b/dnsforward/querylog.go index aac4b25a..230cffd2 100644 --- a/dnsforward/querylog.go +++ b/dnsforward/querylog.go @@ -186,6 +186,10 @@ func (l *queryLog) getQueryLog() []map[string]interface{} { jsonEntry["filterId"] = entry.Result.FilterID } + if len(entry.Result.ServiceName) != 0 { + jsonEntry["service_name"] = entry.Result.ServiceName + } + answers := answerToMap(a) if answers != nil { jsonEntry["answer"] = answers diff --git a/home/blocked_services.go b/home/blocked_services.go new file mode 100644 index 00000000..7707e122 --- /dev/null +++ b/home/blocked_services.go @@ -0,0 +1,70 @@ +package home + +import ( + "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter" +) + +var serviceRules map[string][]*urlfilter.NetworkRule // service name -> filtering rules + +type svc struct { + name string + rules []string +} + +// Keep in sync with: +// client/src/helpers/constants.js +// client/src/components/ui/Icons.js +var serviceRulesArray = []svc{ + {"whatsapp", []string{"||whatsapp.net^"}}, + {"facebook", []string{"||facebook.com^"}}, + {"twitter", []string{"||twitter.com^", "||t.co^", "||twimg.com^"}}, + {"youtube", []string{"||youtube.com^", "||ytimg.com^"}}, + {"messenger", []string{"||fb.com^", "||facebook.com^"}}, + {"twitch", []string{"||twitch.tv^", "||ttvnw.net^"}}, + {"netflix", []string{"||nflxext.com^", "||netflix.com^"}}, + {"instagram", []string{"||instagram.com^"}}, + {"snapchat", []string{"||snapchat.com^"}}, + {"discord", []string{"||discord.gg^", "||discordapp.net^", "||discordapp.com^"}}, + {"ok", []string{"||ok.ru^"}}, + {"skype", []string{"||skype.com^"}}, + {"vk", []string{"||vk.com^"}}, + {"steam", []string{"||steam.com^"}}, + {"mail_ru", []string{"||mail.ru^"}}, +} + +// convert array to map +func initServices() { + serviceRules = make(map[string][]*urlfilter.NetworkRule) + for _, s := range serviceRulesArray { + rules := []*urlfilter.NetworkRule{} + for _, text := range s.rules { + rule, err := urlfilter.NewNetworkRule(text, 0) + if err != nil { + log.Error("urlfilter.NewNetworkRule: %s rule: %s", err, text) + continue + } + rules = append(rules, rule) + } + serviceRules[s.name] = rules + } +} + +// ApplyBlockedServices - set blocked services settings for this DNS request +func ApplyBlockedServices(setts *dnsfilter.RequestFilteringSettings, list []string) { + setts.ServicesRules = []dnsfilter.ServiceEntry{} + for _, name := range list { + rules, ok := serviceRules[name] + + if !ok { + log.Error("unknown service name: %s", name) + continue + } + + s := dnsfilter.ServiceEntry{} + s.Name = name + s.Rules = rules + setts.ServicesRules = append(setts.ServicesRules, s) + } +} diff --git a/home/dns.go b/home/dns.go index b195bd40..b474bc5d 100644 --- a/home/dns.go +++ b/home/dns.go @@ -54,6 +54,7 @@ func initDNSServer(baseDir string) { log.Error("upstream.AddressToUpstream: %s", err) return } + config.dnsctx.rdnsIP = make(map[string]bool) config.dnsctx.rdnsChannel = make(chan string, 256) go asyncRDNSLoop() @@ -210,19 +211,35 @@ func generateServerConfig() (dnsforward.ServerConfig, error) { newconfig.Upstreams = upstreamConfig.Upstreams newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams newconfig.AllServers = config.DNS.AllServers - newconfig.FilterHandler = applyClientSettings + newconfig.FilterHandler = applyAdditionalFiltering newconfig.OnDNSRequest = onDNSRequest return newconfig, nil } // If a client has his own settings, apply them -func applyClientSettings(clientAddr string, setts *dnsfilter.RequestFilteringSettings) { +func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) { + + ApplyBlockedServices(setts, config.DNS.BlockedServices) + + if len(clientAddr) == 0 { + return + } + c, ok := config.clients.Find(clientAddr) - if !ok || !c.UseOwnSettings { + if !ok { return } log.Debug("Using settings for client with IP %s", clientAddr) + + if c.UseOwnBlockedServices { + ApplyBlockedServices(setts, c.BlockedServices) + } + + if !c.UseOwnSettings { + return + } + setts.FilteringEnabled = c.FilteringEnabled setts.SafeSearchEnabled = c.SafeSearchEnabled setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled diff --git a/home/home.go b/home/home.go index 4d711559..3064ad80 100644 --- a/home/home.go +++ b/home/home.go @@ -101,6 +101,7 @@ func run(args options) { initConfig() config.clients.Init() + initServices() if !config.firstRun { // Do the upgrade if necessary From dc2d8cf0752dd63c435f65f1fe6c72d968cbc0ed Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 23 Jul 2019 12:16:36 +0300 Subject: [PATCH 05/12] + /control/blocked_services/* API --- home/blocked_services.go | 49 ++++++++++++++++++++++++++++++++++++++++ home/control.go | 1 + 2 files changed, 50 insertions(+) diff --git a/home/blocked_services.go b/home/blocked_services.go index 7707e122..23b74c51 100644 --- a/home/blocked_services.go +++ b/home/blocked_services.go @@ -1,6 +1,9 @@ package home import ( + "encoding/json" + "net/http" + "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/urlfilter" @@ -68,3 +71,49 @@ func ApplyBlockedServices(setts *dnsfilter.RequestFilteringSettings, list []stri setts.ServicesRules = append(setts.ServicesRules, s) } } + +func handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + + config.RLock() + list := config.DNS.BlockedServices + config.RUnlock() + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(list) + if err != nil { + httpError(w, http.StatusInternalServerError, "json.Encode: %s", err) + return + } +} + +func handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + + list := []string{} + err := json.NewDecoder(r.Body).Decode(&list) + if err != nil { + httpError(w, http.StatusBadRequest, "json.Decode: %s", err) + return + } + + config.Lock() + config.DNS.BlockedServices = list + config.Unlock() + + log.Debug("Updated blocked services list: %d", len(list)) + + err = writeAllConfigsAndReloadDNS() + if err != nil { + httpError(w, http.StatusBadRequest, "%s", err) + return + } + + returnOK(w) +} + +// RegisterBlockedServicesHandlers - register HTTP handlers +func RegisterBlockedServicesHandlers() { + http.HandleFunc("/control/blocked_services/list", postInstall(optionalAuth(ensureGET(handleBlockedServicesList)))) + http.HandleFunc("/control/blocked_services/set", postInstall(optionalAuth(ensurePOST(handleBlockedServicesSet)))) +} diff --git a/home/control.go b/home/control.go index daaf5dbd..1b535e3b 100644 --- a/home/control.go +++ b/home/control.go @@ -1022,6 +1022,7 @@ func registerControlHandlers() { RegisterTLSHandlers() RegisterClientsHandlers() registerRewritesHandlers() + RegisterBlockedServicesHandlers() http.HandleFunc("/dns-query", postInstall(handleDOH)) } From 15d07a40eb83c021032bfca5bbd532f3b032f2c5 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 25 Jul 2019 16:37:06 +0300 Subject: [PATCH 06/12] * refactor --- dnsfilter/dnsfilter.go | 11 +------- dnsfilter/dnsfilter_test.go | 52 +++++++++++++++++++++---------------- dnsforward/dnsforward.go | 12 ++++++++- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go index cc724ff7..47ce1f0e 100644 --- a/dnsfilter/dnsfilter.go +++ b/dnsfilter/dnsfilter.go @@ -206,7 +206,7 @@ func (r Reason) Matched() bool { } // CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled -func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Result, error) { +func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { // sometimes DNS clients will try to resolve ".", which is a request to get root servers if host == "" { return Result{Reason: NotFilteredNotFound}, nil @@ -217,15 +217,6 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res return Result{}, nil } - var setts RequestFilteringSettings - setts.FilteringEnabled = true - setts.SafeSearchEnabled = d.SafeSearchEnabled - setts.SafeBrowsingEnabled = d.SafeBrowsingEnabled - setts.ParentalEnabled = d.ParentalEnabled - if d.FilterHandler != nil { - d.FilterHandler(clientAddr, &setts) - } - var result Result var err error diff --git a/dnsfilter/dnsfilter_test.go b/dnsfilter/dnsfilter_test.go index 7df5fe09..d5eabafe 100644 --- a/dnsfilter/dnsfilter_test.go +++ b/dnsfilter/dnsfilter_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/assert" ) +var setts RequestFilteringSettings + // HELPERS // SAFE BROWSING // SAFE SEARCH @@ -46,10 +48,16 @@ func _Func() string { } func NewForTest(c *Config, filters map[int]string) *Dnsfilter { + setts = RequestFilteringSettings{} + setts.FilteringEnabled = true if c != nil { c.SafeBrowsingCacheSize = 1024 c.SafeSearchCacheSize = 1024 c.ParentalCacheSize = 1024 + + setts.SafeSearchEnabled = c.SafeSearchEnabled + setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled + setts.ParentalEnabled = c.ParentalEnabled } d := New(c, filters) purgeCaches() @@ -58,7 +66,7 @@ func NewForTest(c *Config, filters map[int]string) *Dnsfilter { func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } @@ -69,7 +77,7 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) { func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype uint16) { t.Helper() - ret, err := d.CheckHost(hostname, qtype, "") + ret, err := d.CheckHost(hostname, qtype, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } @@ -83,7 +91,7 @@ func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } @@ -214,7 +222,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) { // Check host for each domain for _, host := range yandex { - result, err := d.CheckHost(host, dns.TypeA, "") + result, err := d.CheckHost(host, dns.TypeA, &setts) if err != nil { t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err) } @@ -234,7 +242,7 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) { // Check host for each domain for _, host := range googleDomains { - result, err := d.CheckHost(host, dns.TypeA, "") + result, err := d.CheckHost(host, dns.TypeA, &setts) if err != nil { t.Errorf("SafeSearch doesn't work for %s cause %s", host, err) } @@ -254,7 +262,7 @@ func TestSafeSearchCacheYandex(t *testing.T) { var err error // Check host with disabled safesearch - result, err = d.CheckHost(domain, dns.TypeA, "") + result, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("Cannot check host due to %s", err) } @@ -265,7 +273,7 @@ func TestSafeSearchCacheYandex(t *testing.T) { d = NewForTest(&Config{SafeSearchEnabled: true}, nil) defer d.Destroy() - result, err = d.CheckHost(domain, dns.TypeA, "") + result, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) } @@ -291,7 +299,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) { d := NewForTest(nil, nil) defer d.Destroy() domain := "www.google.ru" - result, err := d.CheckHost(domain, dns.TypeA, "") + result, err := d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("Cannot check host due to %s", err) } @@ -322,7 +330,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) { } } - result, err = d.CheckHost(domain, dns.TypeA, "") + result, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) } @@ -435,7 +443,7 @@ func TestMatching(t *testing.T) { d := NewForTest(nil, filters) defer d.Destroy() - ret, err := d.CheckHost(test.hostname, dns.TypeA, "") + ret, err := d.CheckHost(test.hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", test.hostname, err) } @@ -451,7 +459,7 @@ func TestMatching(t *testing.T) { // CLIENT SETTINGS -func applyClientSettings(clientAddr string, setts *RequestFilteringSettings) { +func applyClientSettings(setts *RequestFilteringSettings) { setts.FilteringEnabled = false setts.ParentalEnabled = false setts.SafeBrowsingEnabled = true @@ -476,50 +484,50 @@ func TestClientSettings(t *testing.T) { // no client settings: // blocked by filters - r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("example.org", dns.TypeA, &setts) if !r.IsFiltered || r.Reason != FilteredBlackList { t.Fatalf("CheckHost FilteredBlackList") } // blocked by parental - r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts) if !r.IsFiltered || r.Reason != FilteredParental { t.Fatalf("CheckHost FilteredParental") } // safesearch is disabled - r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts) if r.IsFiltered { t.Fatalf("CheckHost safesearch") } // not blocked - r, _ = d.CheckHost("facebook.com", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts) assert.True(t, !r.IsFiltered) // override client settings: - d.FilterHandler = applyClientSettings + applyClientSettings(&setts) // override filtering settings - r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("example.org", dns.TypeA, &setts) if r.IsFiltered { t.Fatalf("CheckHost") } // override parental settings (force disable parental) - r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts) if r.IsFiltered { t.Fatalf("CheckHost") } // override safesearch settings (force enable safesearch) - r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts) if !r.IsFiltered || r.Reason != FilteredSafeBrowsing { t.Fatalf("CheckHost FilteredSafeBrowsing") } // blocked by additional rules - r, _ = d.CheckHost("facebook.com", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts) assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService) } @@ -530,7 +538,7 @@ func BenchmarkSafeBrowsing(b *testing.B) { defer d.Destroy() for n := 0; n < b.N; n++ { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } @@ -546,7 +554,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index d09bf7be..a5aa81fd 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -533,7 +533,17 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error if d.Addr != nil { clientAddr, _, _ = net.SplitHostPort(d.Addr.String()) } - res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, clientAddr) + + var setts dnsfilter.RequestFilteringSettings + setts.FilteringEnabled = true + setts.SafeSearchEnabled = s.conf.SafeSearchEnabled + setts.SafeBrowsingEnabled = s.conf.SafeBrowsingEnabled + setts.ParentalEnabled = s.conf.ParentalEnabled + if s.conf.FilterHandler != nil { + s.conf.FilterHandler(clientAddr, &setts) + } + + res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, &setts) if err != nil { // Return immediately if there's an error return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host) From 87b379b140f0482d17882101f08934b2d83b9e69 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 29 Jul 2019 19:04:11 +0300 Subject: [PATCH 07/12] + openapi: clients: add "blocked_services" field --- openapi/openapi.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d5ea137d..2679ebc6 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1572,6 +1572,12 @@ definitions: type: "boolean" safesearch_enabled: type: "boolean" + use_global_blocked_services: + type: "boolean" + blocked_services: + type: "array" + items: + type: "string" ClientAuto: type: "object" description: "Auto-Client information" From 3c684d1f85c853e039c28b909a55977431ae6ddc Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 29 Jul 2019 19:14:17 +0300 Subject: [PATCH 08/12] + openapi: add /blocked_services/* --- openapi/openapi.yaml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2679ebc6..d3a50a08 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -786,6 +786,35 @@ paths: 200: description: OK + + /blocked_services/list: + get: + tags: + - blocked_services + operationId: blockedServicesList + summary: 'Get blocked services list' + responses: + 200: + description: OK + schema: + $ref: "#/definitions/BlockedServicesArray" + + /blocked_services/set: + post: + tags: + - blocked_services + operationId: blockedServicesSet + summary: 'Set blocked services list' + parameters: + - in: body + name: "body" + schema: + $ref: "#/definitions/BlockedServicesArray" + responses: + 200: + description: OK + + # -------------------------------------------------- # Rewrite methods # -------------------------------------------------- @@ -1644,6 +1673,11 @@ definitions: description: "value of A, AAAA or CNAME DNS record" example: "127.0.0.1" + BlockedServicesArray: + type: "array" + items: + type: "string" + CheckConfigRequest: type: "object" description: "Configuration to be checked" From 92cebd5b3133a39c62500678ec7313f6a9f09807 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Thu, 18 Jul 2019 14:52:47 +0300 Subject: [PATCH 09/12] + client: handle blocked services --- client/package-lock.json | 6 +- client/package.json | 2 +- client/src/__locales/en.json | 11 +- client/src/actions/services.js | 37 +++ client/src/api/Api.js | 18 ++ client/src/components/Logs/Logs.css | 6 + client/src/components/Logs/index.js | 24 +- .../Settings/Clients/ClientsTable.js | 36 ++- .../src/components/Settings/Clients/Form.js | 238 ++++++++++-------- .../src/components/Settings/Clients/Modal.js | 23 +- .../components/Settings/Clients/Service.css | 79 ++++++ .../src/components/Settings/Services/Form.js | 90 +++++++ .../src/components/Settings/Services/index.js | 69 +++++ client/src/components/Settings/Settings.css | 9 + client/src/components/Settings/index.js | 12 +- client/src/components/ui/Icons.js | Bin 12633 -> 27149 bytes client/src/components/ui/Popover.css | 2 + client/src/components/ui/PopoverFilter.js | 34 ++- client/src/components/ui/Tabs.css | 14 ++ client/src/components/ui/Tabs.js | 10 +- client/src/containers/Settings.js | 6 +- client/src/helpers/constants.js | 63 +++++ client/src/helpers/form.js | 39 +++ client/src/helpers/helpers.js | 6 + client/src/reducers/index.js | 2 + client/src/reducers/services.js | 29 +++ 26 files changed, 739 insertions(+), 126 deletions(-) create mode 100644 client/src/actions/services.js create mode 100644 client/src/components/Settings/Clients/Service.css create mode 100644 client/src/components/Settings/Services/Form.js create mode 100644 client/src/components/Settings/Services/index.js create mode 100644 client/src/reducers/services.js diff --git a/client/package-lock.json b/client/package-lock.json index cedee41d..0c596c27 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -945,9 +945,9 @@ } }, "axios": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", - "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", "requires": { "follow-redirects": "1.5.10", "is-buffer": "^2.0.2" diff --git a/client/package.json b/client/package.json index dc64d590..10080eb5 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@nivo/line": "^0.49.1", - "axios": "^0.18.1", + "axios": "^0.19.0", "classnames": "^2.2.6", "date-fns": "^1.29.0", "file-saver": "^1.3.8", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 4aaac0e0..6a013cf6 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -344,5 +344,14 @@ "form_answer": "Enter IP address or domain name", "form_error_domain_format": "Invalid domain format", "form_error_answer_format": "Invalid answer format", - "configure": "Configure" + "configure": "Configure", + "main_settings": "Main settings", + "block_services": "Block specific services", + "blocked_services": "Blocked services", + "blocked_services_desc": "Allows to quickly block popular sites and services.", + "blocked_services_saved": "Blocked services successfully saved", + "blocked_services_global": "Use global blocked services", + "blocked_service": "Blocked service", + "block_all": "Block all", + "unblock_all": "Unblock all" } diff --git a/client/src/actions/services.js b/client/src/actions/services.js new file mode 100644 index 00000000..7aae500f --- /dev/null +++ b/client/src/actions/services.js @@ -0,0 +1,37 @@ +import { createAction } from 'redux-actions'; +import Api from '../api/Api'; +import { addErrorToast, addSuccessToast } from './index'; + +const apiClient = new Api(); + +export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST'); +export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE'); +export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS'); + +export const getBlockedServices = () => async (dispatch) => { + dispatch(getBlockedServicesRequest()); + try { + const data = await apiClient.getBlockedServices(); + dispatch(getBlockedServicesSuccess(data)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getBlockedServicesFailure()); + } +}; + +export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST'); +export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE'); +export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS'); + +export const setBlockedServices = values => async (dispatch) => { + dispatch(setBlockedServicesRequest()); + try { + await apiClient.setBlockedServices(values); + dispatch(setBlockedServicesSuccess()); + dispatch(getBlockedServices()); + dispatch(addSuccessToast('blocked_services_saved')); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setBlockedServicesFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 766cd499..a857766c 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -509,4 +509,22 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // Blocked services + BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' }; + BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; + + getBlockedServices() { + const { path, method } = this.BLOCKED_SERVICES_LIST; + return this.makeRequest(path, method); + } + + setBlockedServices(config) { + const { path, method } = this.BLOCKED_SERVICES_SET; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } } diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 3205e424..cd105626 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -19,6 +19,11 @@ overflow: hidden; } +.logs__row--icons { + max-width: 180px; + flex-flow: row wrap; +} + .logs__row .list-unstyled { margin-bottom: 0; overflow: hidden; @@ -26,6 +31,7 @@ .logs__text, .logs__row .list-unstyled li { + padding: 0 1px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index f8891206..93a1e420 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -8,6 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next'; import { HashLink as Link } from 'react-router-hash-link'; import { formatTime, getClientName } from '../../helpers/helpers'; +import { SERVICES } from '../../helpers/constants'; import { getTrackerData } from '../../helpers/trackers/trackers'; import PageTitle from '../ui/PageTitle'; import Card from '../ui/Card'; @@ -39,12 +40,8 @@ class Logs extends Component { } } - renderTooltip(isFiltered, rule, filter) { - if (rule) { - return (isFiltered && ); - } - return ''; - } + renderTooltip = (isFiltered, rule, filter, service) => + isFiltered && ; toggleBlocking = (type, domain) => { const { userRules } = this.props.filtering; @@ -146,6 +143,21 @@ class Logs extends Component { } } + if (reason === 'FilteredBlockedService') { + const getService = SERVICES + .find(service => service.id === row.original.serviceName); + const serviceName = getService && getService.name; + + return ( +
+ + {parsedFilteredReason} + + {this.renderTooltip(isFiltered, '', '', serviceName)} +
+ ); + } + if (isFiltered) { return (
diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index c12fa1e3..1dae9146 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -17,10 +17,19 @@ class ClientsTable extends Component { }; handleSubmit = (values) => { + let config = values; + + if (values && values.blocked_services) { + const blocked_services = Object + .keys(values.blocked_services) + .filter(service => values.blocked_services[service]); + config = { ...values, blocked_services }; + } + if (this.props.modalType === MODAL_TYPE.EDIT) { - this.handleFormUpdate(values, this.props.modalClientName); + this.handleFormUpdate(config, this.props.modalClientName); } else { - this.handleFormAdd(values); + this.handleFormAdd(config); } }; @@ -41,6 +50,7 @@ class ClientsTable extends Component { return { identifier, use_global_settings: true, + use_global_blocked_services: true, ...client, }; } @@ -48,6 +58,7 @@ class ClientsTable extends Component { return { identifier: CLIENT_ID.IP, use_global_settings: true, + use_global_blocked_services: true, }; }; @@ -116,6 +127,27 @@ class ClientsTable extends Component { ); }, }, + { + Header: this.props.t('blocked_services'), + accessor: 'blocked_services', + Cell: (row) => { + const { value, original } = row; + + if (original.use_global_blocked_services) { + return settings_global; + } + + return ( +
+ {value && value.length > 0 ? value.map(service => ( + + + + )) : '–'} +
+ ); + }, + }, { Header: this.props.t('table_statistics'), accessor: 'statistics', diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index b10487c4..ee267d94 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -5,18 +5,46 @@ import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form'; -import { CLIENT_ID } from '../../../helpers/constants'; +import Tabs from '../../ui/Tabs'; +import { toggleAllServices } from '../../../helpers/helpers'; +import { renderField, renderRadioField, renderSelectField, renderServiceField, ipv4, mac, required } from '../../../helpers/form'; +import { CLIENT_ID, SERVICES } from '../../../helpers/constants'; +import './Service.css'; + +const settingsCheckboxes = [ + { + name: 'use_global_settings', + placeholder: 'client_global_settings', + }, + { + name: 'filtering_enabled', + placeholder: 'block_domain_use_filters_and_hosts', + }, + { + name: 'safebrowsing_enabled', + placeholder: 'use_adguard_browsing_sec', + }, + { + name: 'parental_enabled', + placeholder: 'use_adguard_parental', + }, + { + name: 'safesearch_enabled', + placeholder: 'enforce_safe_search', + }, +]; let Form = (props) => { const { t, handleSubmit, reset, + change, pristine, submitting, clientIdentifier, useGlobalSettings, + useGlobalServices, toggleClientModal, processingAdding, processingUpdating, @@ -26,57 +54,70 @@ let Form = (props) => {
-
+
client_identifier -
- {clientIdentifier === CLIENT_ID.IP && ( -
+
+
+ {clientIdentifier === CLIENT_ID.IP && ( +
+ +
+ )} + {clientIdentifier === CLIENT_ID.MAC && ( +
+ +
+ )} +
+
- )} - {clientIdentifier === CLIENT_ID.MAC && ( -
- -
- )} +
{
-
- -
- -
- - settings - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
+ +
+ {settingsCheckboxes.map(setting => ( +
+ +
+ ))} +
+
+
+ +
+
+ +
+
+ +
+
+
+ {SERVICES.map(service => ( + + ))} +
+
+
+
@@ -188,10 +224,12 @@ Form.propTypes = { pristine: PropTypes.bool.isRequired, handleSubmit: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, + change: PropTypes.func.isRequired, submitting: PropTypes.bool.isRequired, toggleClientModal: PropTypes.func.isRequired, clientIdentifier: PropTypes.string, useGlobalSettings: PropTypes.bool, + useGlobalServices: PropTypes.bool, t: PropTypes.func.isRequired, processingAdding: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired, @@ -202,9 +240,11 @@ const selector = formValueSelector('clientForm'); Form = connect((state) => { const clientIdentifier = selector(state, 'identifier'); const useGlobalSettings = selector(state, 'use_global_settings'); + const useGlobalServices = selector(state, 'use_global_blocked_services'); return { clientIdentifier, useGlobalSettings, + useGlobalServices, }; })(Form); diff --git a/client/src/components/Settings/Clients/Modal.js b/client/src/components/Settings/Clients/Modal.js index 49e3483e..d5687344 100644 --- a/client/src/components/Settings/Clients/Modal.js +++ b/client/src/components/Settings/Clients/Modal.js @@ -6,6 +6,24 @@ import ReactModal from 'react-modal'; import { MODAL_TYPE } from '../../../helpers/constants'; import Form from './Form'; +const getInitialData = (initial) => { + if (initial && initial.blocked_services) { + const { blocked_services } = initial; + const blocked = {}; + + blocked_services.forEach((service) => { + blocked[service] = true; + }); + + return { + ...initial, + blocked_services: blocked, + }; + } + + return initial; +}; + const Modal = (props) => { const { isModalOpen, @@ -16,6 +34,7 @@ const Modal = (props) => { processingAdding, processingUpdating, } = props; + const initialData = getInitialData(currentClientData); return ( {
{ + const { + handleSubmit, + change, + pristine, + submitting, + processing, + processingSet, + } = props; + + return ( + +
+
+
+ +
+
+ +
+
+
+ {SERVICES.map(service => ( + + ))} +
+
+ +
+ +
+ + ); +}; + +Form.propTypes = { + pristine: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + change: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + processing: PropTypes.bool.isRequired, + processingSet: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ + form: 'servicesForm', + enableReinitialize: true, + }), +])(Form); diff --git a/client/src/components/Settings/Services/index.js b/client/src/components/Settings/Services/index.js new file mode 100644 index 00000000..362f1c96 --- /dev/null +++ b/client/src/components/Settings/Services/index.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withNamespaces } from 'react-i18next'; + +import Form from './Form'; +import Card from '../../ui/Card'; + +class Services extends Component { + handleSubmit = (values) => { + let config = values; + + if (values && values.blocked_services) { + const blocked_services = Object + .keys(values.blocked_services) + .filter(service => values.blocked_services[service]); + config = blocked_services; + } + + this.props.setBlockedServices(config); + }; + + + getInitialDataForServices = (initial) => { + if (initial) { + const blocked = {}; + + initial.forEach((service) => { + blocked[service] = true; + }); + + return { + blocked_services: blocked, + }; + } + + return initial; + }; + + + render() { + const { services, t } = this.props; + const initialData = this.getInitialDataForServices(services.list); + + return ( + +
+
+
+
+ ); + } +} + +Services.propTypes = { + t: PropTypes.func.isRequired, + services: PropTypes.object.isRequired, + setBlockedServices: PropTypes.func.isRequired, +}; + +export default withNamespaces()(Services); diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 48acf4eb..7f12dbbe 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -11,11 +11,20 @@ margin-bottom: 20px; } +.form__inline { + display: flex; + justify-content: flex-start; +} + .btn-standard { padding-left: 20px; padding-right: 20px; } +.btn-large { + min-width: 150px; +} + .form-control--textarea { min-height: 110px; } diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 1e072311..7391cbaf 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { withNamespaces, Trans } from 'react-i18next'; +import Services from './Services'; import Checkbox from '../ui/Checkbox'; import Loading from '../ui/Loading'; import PageTitle from '../ui/PageTitle'; @@ -35,6 +36,7 @@ class Settings extends Component { componentDidMount() { this.props.initSettings(this.settings); + this.props.getBlockedServices(); } renderSettings = (settings) => { @@ -59,7 +61,9 @@ class Settings extends Component { }; render() { - const { settings, t } = this.props; + const { + settings, services, setBlockedServices, t, + } = this.props; return ( @@ -74,6 +78,12 @@ class Settings extends Component {
+
+ +
)} diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js index ac9a2a5b6bc102d1ede1ca0ace64332f075d96e3..01f410c6c26739ed3cb29bb8702ec397d17d25ba 100644 GIT binary patch literal 27149 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 b|8P3}`J?~0PajTySp9S{|DQSi`K$i{^5hpD delta 12 TcmeCZ!gw=jgNfnf7n!O6CwB$B diff --git a/client/src/components/ui/Popover.css b/client/src/components/ui/Popover.css index f7e23836..707affbc 100644 --- a/client/src/components/ui/Popover.css +++ b/client/src/components/ui/Popover.css @@ -109,9 +109,11 @@ width: 20px; height: 20px; stroke: #9aa0ac; + color: #9aa0ac; } .popover__icon--green { + color: #66b574; stroke: #66b574; } diff --git a/client/src/components/ui/PopoverFilter.js b/client/src/components/ui/PopoverFilter.js index e54cf204..f10cc8a1 100644 --- a/client/src/components/ui/PopoverFilter.js +++ b/client/src/components/ui/PopoverFilter.js @@ -6,19 +6,36 @@ import './Popover.css'; class PopoverFilter extends Component { render() { + const { rule, filter, service } = this.props; + + if (!rule && !service) { + return ''; + } + return (
- + + +
-
- rule_label: {this.props.rule} -
- {this.props.filter &&
- filter_label: {this.props.filter} -
} + {rule && ( +
+ rule_label: {rule} +
+ )} + {filter && ( +
+ filter_label: {filter} +
+ )} + {service && ( +
+ blocked_service: {service} +
+ )}
@@ -27,8 +44,9 @@ class PopoverFilter extends Component { } PopoverFilter.propTypes = { - rule: PropTypes.string.isRequired, + rule: PropTypes.string, filter: PropTypes.string, + service: PropTypes.string, }; export default withNamespaces()(PopoverFilter); diff --git a/client/src/components/ui/Tabs.css b/client/src/components/ui/Tabs.css index cd1671e9..1e6f75c6 100644 --- a/client/src/components/ui/Tabs.css +++ b/client/src/components/ui/Tabs.css @@ -6,6 +6,20 @@ border-bottom: 1px solid #e8e8e8; } +.tabs__controls--form { + justify-content: flex-start; +} + +.tabs__controls--form .tab__control { + min-width: initial; + margin-right: 25px; + font-size: 14px; +} + +.tabs__controls--form .tab__icon { + display: none; +} + .tab__control { display: flex; flex-direction: column; diff --git a/client/src/components/ui/Tabs.js b/client/src/components/ui/Tabs.js index a15b0ee6..7da68fba 100644 --- a/client/src/components/ui/Tabs.js +++ b/client/src/components/ui/Tabs.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; import Tab from './Tab'; import './Tabs.css'; @@ -16,6 +17,7 @@ class Tabs extends Component { render() { const { props: { + controlClass, children, }, state: { @@ -23,9 +25,14 @@ class Tabs extends Component { }, } = this; + const getControlClass = classnames({ + tabs__controls: true, + [`tabs__controls--${controlClass}`]: controlClass, + }); + return (
-
+
{children.map((child) => { const { label, title } = child.props; @@ -54,6 +61,7 @@ class Tabs extends Component { } Tabs.propTypes = { + controlClass: PropTypes.string, children: PropTypes.array.isRequired, }; diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js index e50290f1..054d1d1b 100644 --- a/client/src/containers/Settings.js +++ b/client/src/containers/Settings.js @@ -1,11 +1,13 @@ import { connect } from 'react-redux'; import { initSettings, toggleSetting } from '../actions'; +import { getBlockedServices, setBlockedServices } from '../actions/services'; import Settings from '../components/Settings'; const mapStateToProps = (state) => { - const { settings } = state; + const { settings, services } = state; const props = { settings, + services, }; return props; }; @@ -13,6 +15,8 @@ const mapStateToProps = (state) => { const mapDispatchToProps = { initSettings, toggleSetting, + getBlockedServices, + setBlockedServices, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 896d873e..a5c8e922 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -181,3 +181,66 @@ export const CLIENT_ID = { }; export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients']; + +export const SERVICES = [ + { + id: 'facebook', + name: 'Facebook', + }, + { + id: 'whatsapp', + name: 'WhatsApp', + }, + { + id: 'instagram', + name: 'Instagram', + }, + { + id: 'twitter', + name: 'Twitter', + }, + { + id: 'youtube', + name: 'YouTube', + }, + { + id: 'netflix', + name: 'Netflix', + }, + { + id: 'snapchat', + name: 'Snapchat', + }, + { + id: 'messenger', + name: 'Messenger', + }, + { + id: 'twitch', + name: 'Twitch', + }, + { + id: 'discord', + name: 'Discord', + }, + { + id: 'skype', + name: 'Skype', + }, + { + id: 'steam', + name: 'Steam', + }, + { + id: 'ok', + name: 'OK', + }, + { + id: 'vk', + name: 'VK', + }, + { + id: 'mail_ru', + name: 'mail.ru', + }, +]; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index c4ffcb73..e3b6e46c 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -27,6 +27,23 @@ export const renderField = ({ ); +export const renderRadioField = ({ + input, placeholder, disabled, meta: { touched, error }, +}) => ( + + + {!disabled && touched && (error && {error})} + +); + export const renderSelectField = ({ input, placeholder, disabled, meta: { touched, error }, }) => ( @@ -46,6 +63,28 @@ export const renderSelectField = ({ ); +export const renderServiceField = ({ + input, placeholder, disabled, modifier, icon, meta: { touched, error }, +}) => ( + + + {!disabled && touched && (error && {error})} + +); + export const required = (value) => { if (value || value === 0) { return false; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 031747f3..0fd5baea 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -27,6 +27,7 @@ export const normalizeLogs = logs => logs.map((log) => { client, filterId, rule, + service_name, } = log; const { host: domain, type } = question; const responsesArray = response ? response.map((response) => { @@ -42,6 +43,7 @@ export const normalizeLogs = logs => logs.map((log) => { client, filterId, rule, + serviceName: service_name, }; }); @@ -225,3 +227,7 @@ export const sortClients = (clients) => { return clients.sort(compare); }; + +export const toggleAllServices = (services, change, isSelected) => { + services.forEach(service => change(`blocked_services.${service.id}`, isSelected)); +}; diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 32026a08..2913f5cc 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -10,6 +10,7 @@ import encryption from './encryption'; import clients from './clients'; import access from './access'; import rewrites from './rewrites'; +import services from './services'; const settings = handleActions({ [actions.initSettingsRequest]: state => ({ ...state, processing: true }), @@ -424,6 +425,7 @@ export default combineReducers({ clients, access, rewrites, + services, loadingBar: loadingBarReducer, form: formReducer, }); diff --git a/client/src/reducers/services.js b/client/src/reducers/services.js new file mode 100644 index 00000000..d91cadf5 --- /dev/null +++ b/client/src/reducers/services.js @@ -0,0 +1,29 @@ +import { handleActions } from 'redux-actions'; + +import * as actions from '../actions/services'; + +const services = handleActions( + { + [actions.getBlockedServicesRequest]: state => ({ ...state, processing: true }), + [actions.getBlockedServicesFailure]: state => ({ ...state, processing: false }), + [actions.getBlockedServicesSuccess]: (state, { payload }) => ({ + ...state, + list: payload, + processing: false, + }), + + [actions.setBlockedServicesRequest]: state => ({ ...state, processingSet: true }), + [actions.setBlockedServicesFailure]: state => ({ ...state, processingSet: false }), + [actions.setBlockedServicesSuccess]: state => ({ + ...state, + processingSet: false, + }), + }, + { + processing: true, + processingSet: false, + list: [], + }, +); + +export default services; From 0c46a70d9a2ce41f8a5123a2f7ea1bad09031a04 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Mon, 5 Aug 2019 11:42:39 +0300 Subject: [PATCH 10/12] + client: add prefix to the service icons --- .../Settings/Clients/ClientsTable.js | 2 +- .../src/components/Settings/Clients/Form.js | 2 +- .../src/components/Settings/Services/Form.js | 2 +- client/src/components/ui/Icons.js | Bin 27149 -> 27269 bytes 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index 1dae9146..1e98da08 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -141,7 +141,7 @@ class ClientsTable extends Component {
{value && value.length > 0 ? value.map(service => ( - + )) : '–'}
diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index ee267d94..58112401 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -180,7 +180,7 @@ let Form = (props) => { {SERVICES.map(service => ( { {SERVICES.map(service => ( SomNPCFPkV z$r&K=$=Q}~5W2&ywlT89jN-9b%!!a%@A`)c!CUQNio|>1wTc-b*X4JX5y1-&c*4j9 nv$rf8XlMQAJwaEI)K3hvVuo9KDpD1}c^7k(4PnyejE-f5 Date: Mon, 5 Aug 2019 12:54:17 +0300 Subject: [PATCH 11/12] * config: upgrade schema version: 3 -> 4 --- home/upgrade.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/home/upgrade.go b/home/upgrade.go index 135fd05c..2b781293 100644 --- a/home/upgrade.go +++ b/home/upgrade.go @@ -10,7 +10,7 @@ import ( yaml "gopkg.in/yaml.v2" ) -const currentSchemaVersion = 3 // used for upgrading from old configs to new config +const currentSchemaVersion = 4 // used for upgrading from old configs to new config // Performs necessary upgrade operations if needed func upgradeConfig() error { @@ -67,6 +67,11 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err if err != nil { return err } + case 3: + err := upgradeSchema3to4(diskConfig) + if err != nil { + return err + } default: err := fmt.Errorf("configuration file contains unknown schema_version, abort") log.Println(err) @@ -173,6 +178,39 @@ func upgradeSchema2to3(diskConfig *map[string]interface{}) error { return nil } +// Add use_global_blocked_services=true setting for existing "clients" array +func upgradeSchema3to4(diskConfig *map[string]interface{}) error { + log.Printf("%s(): called", _Func()) + + (*diskConfig)["schema_version"] = 4 + + clients, ok := (*diskConfig)["clients"] + if !ok { + return nil + } + + switch arr := clients.(type) { + case []interface{}: + + for i := range arr { + + switch c := arr[i].(type) { + + case map[interface{}]interface{}: + c["use_global_blocked_services"] = true + + default: + continue + } + } + + default: + return nil + } + + return nil +} + // jump three schemas at once -- this time we just do it sequentially func upgradeSchema0to3(diskConfig *map[string]interface{}) error { err := upgradeSchema0to1(diskConfig) From e7001c3bc4a7e1ec53d810cafbc90d7f2a624af5 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 5 Aug 2019 13:07:28 +0300 Subject: [PATCH 12/12] * config upgrade: sequential processing --- home/upgrade.go | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/home/upgrade.go b/home/upgrade.go index 2b781293..491e5a68 100644 --- a/home/upgrade.go +++ b/home/upgrade.go @@ -53,20 +53,23 @@ func upgradeConfig() error { func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error { switch oldVersion { case 0: - err := upgradeSchema0to3(diskConfig) + err := upgradeSchema0to1(diskConfig) if err != nil { return err } + fallthrough case 1: - err := upgradeSchema1to3(diskConfig) + err := upgradeSchema1to2(diskConfig) if err != nil { return err } + fallthrough case 2: err := upgradeSchema2to3(diskConfig) if err != nil { return err } + fallthrough case 3: err := upgradeSchema3to4(diskConfig) if err != nil { @@ -210,23 +213,3 @@ func upgradeSchema3to4(diskConfig *map[string]interface{}) error { return nil } - -// jump three schemas at once -- this time we just do it sequentially -func upgradeSchema0to3(diskConfig *map[string]interface{}) error { - err := upgradeSchema0to1(diskConfig) - if err != nil { - return err - } - - return upgradeSchema1to3(diskConfig) -} - -// jump two schemas at once -- this time we just do it sequentially -func upgradeSchema1to3(diskConfig *map[string]interface{}) error { - err := upgradeSchema1to2(diskConfig) - if err != nil { - return err - } - - return upgradeSchema2to3(diskConfig) -}