diff --git a/internal/app/app.go b/internal/app/app.go index 936bba61..448d52e1 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -15,6 +15,7 @@ import ( "github.com/anfragment/zen/internal/certstore" "github.com/anfragment/zen/internal/cfg" "github.com/anfragment/zen/internal/cosmetic" + "github.com/anfragment/zen/internal/cssrule" "github.com/anfragment/zen/internal/filter" "github.com/anfragment/zen/internal/jsrule" "github.com/anfragment/zen/internal/logger" @@ -174,9 +175,10 @@ func (a *App) StartProxy() (err error) { } cosmeticRulesInjector := cosmetic.NewInjector() + cssRulesInjector := cssrule.NewInjector() jsRuleInjector := jsrule.NewInjector() - filter, err := filter.NewFilter(a.config, ruleMatcher, exceptionRuleMatcher, scriptletInjector, cosmeticRulesInjector, jsRuleInjector, a.eventsHandler) + filter, err := filter.NewFilter(a.config, ruleMatcher, exceptionRuleMatcher, scriptletInjector, cosmeticRulesInjector, cssRulesInjector, jsRuleInjector, a.eventsHandler) if err != nil { return fmt.Errorf("create filter: %v", err) } diff --git a/internal/cssrule/injector.go b/internal/cssrule/injector.go new file mode 100644 index 00000000..2fc4895d --- /dev/null +++ b/internal/cssrule/injector.go @@ -0,0 +1,78 @@ +package cssrule + +import ( + "bytes" + "errors" + "fmt" + "log" + "net/http" + "regexp" + "strings" + + "github.com/anfragment/zen/internal/hostmatch" + "github.com/anfragment/zen/internal/htmlrewrite" + "github.com/anfragment/zen/internal/logger" +) + +var ( + RuleRegex = regexp.MustCompile(`.*#@?\$#.+`) + primaryRuleRegex = regexp.MustCompile(`(.*?)#\$#(.*)`) + exceptionRuleRegex = regexp.MustCompile(`(.*?)#@\$#(.+)`) + + injectionStart = []byte("") +) + +type store interface { + AddPrimaryRule(hostnamePatterns string, css string) error + AddExceptionRule(hostnamePatterns string, css string) error + Get(hostname string) []string +} + +type Injector struct { + store store +} + +func NewInjector() *Injector { + return &Injector{ + store: hostmatch.NewHostMatcher[string](), + } +} + +func (inj *Injector) AddRule(rule string) error { + if match := primaryRuleRegex.FindStringSubmatch(rule); match != nil { + if err := inj.store.AddPrimaryRule(match[1], match[2]); err != nil { + return fmt.Errorf("add primary rule: %w", err) + } + return nil + } + + if match := exceptionRuleRegex.FindStringSubmatch(rule); match != nil { + if err := inj.store.AddExceptionRule(match[1], match[2]); err != nil { + return fmt.Errorf("add exception rule: %w", err) + } + return nil + } + + return errors.New("unsupported syntax") +} + +func (inj *Injector) Inject(req *http.Request, res *http.Response) error { + hostname := req.URL.Hostname() + cssRules := inj.store.Get(hostname) + log.Printf("got %d css rules for %q", len(cssRules), logger.Redacted(hostname)) + if len(cssRules) == 0 { + return nil + } + + var ruleInjection bytes.Buffer + ruleInjection.Write(injectionStart) + ruleInjection.WriteString(strings.Join(cssRules, "")) + ruleInjection.Write(injectionEnd) + + htmlrewrite.ReplaceHeadContents(res, func(match []byte) []byte { + return bytes.Join([][]byte{match, ruleInjection.Bytes()}, nil) + }) + + return nil +} diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 57390135..1dab6a98 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -16,6 +16,7 @@ import ( "github.com/anfragment/zen/internal/cfg" "github.com/anfragment/zen/internal/cosmetic" + "github.com/anfragment/zen/internal/cssrule" "github.com/anfragment/zen/internal/jsrule" "github.com/anfragment/zen/internal/logger" "github.com/anfragment/zen/internal/rule" @@ -52,6 +53,11 @@ type cosmeticRulesInjector interface { AddRule(string) error } +type cssRulesInjector interface { + Inject(*http.Request, *http.Response) error + AddRule(string) error +} + type jsRuleInjector interface { AddRule(rule string) error Inject(*http.Request, *http.Response) error @@ -66,6 +72,7 @@ type Filter struct { exceptionRuleMatcher ruleMatcher scriptletsInjector scriptletsInjector cosmeticRulesInjector cosmeticRulesInjector + cssRulesInjector cssRulesInjector jsRuleInjector jsRuleInjector eventsEmitter filterEventsEmitter } @@ -80,7 +87,7 @@ var ( ) // NewFilter creates and initializes a new filter. -func NewFilter(config config, ruleMatcher ruleMatcher, exceptionRuleMatcher ruleMatcher, scriptletsInjector scriptletsInjector, cosmeticRulesInjector cosmeticRulesInjector, jsRuleInjector jsRuleInjector, eventsEmitter filterEventsEmitter) (*Filter, error) { +func NewFilter(config config, ruleMatcher ruleMatcher, exceptionRuleMatcher ruleMatcher, scriptletsInjector scriptletsInjector, cosmeticRulesInjector cosmeticRulesInjector, cssRulesInjector cssRulesInjector, jsRuleInjector jsRuleInjector, eventsEmitter filterEventsEmitter) (*Filter, error) { if config == nil { return nil, errors.New("config is nil") } @@ -96,6 +103,9 @@ func NewFilter(config config, ruleMatcher ruleMatcher, exceptionRuleMatcher rule if cosmeticRulesInjector == nil { return nil, errors.New("cosmeticRulesInjector is nil") } + if cssRulesInjector == nil { + return nil, errors.New("cssRulesInjector is nil") + } if jsRuleInjector == nil { return nil, errors.New("jsRuleInjector is nil") } @@ -109,6 +119,7 @@ func NewFilter(config config, ruleMatcher ruleMatcher, exceptionRuleMatcher rule exceptionRuleMatcher: exceptionRuleMatcher, scriptletsInjector: scriptletsInjector, cosmeticRulesInjector: cosmeticRulesInjector, + cssRulesInjector: cssRulesInjector, jsRuleInjector: jsRuleInjector, eventsEmitter: eventsEmitter, } @@ -195,6 +206,13 @@ func (f *Filter) AddRule(rule string, filterListName *string, filterListTrusted } } + if filterListTrusted && cssrule.RuleRegex.MatchString(rule) { + if err := f.cssRulesInjector.AddRule(rule); err != nil { + return false, fmt.Errorf("add css rule: %w", err) + } + return false, nil + } + if filterListTrusted && jsrule.RuleRegex.MatchString(rule) { if err := f.jsRuleInjector.AddRule(rule); err != nil { return false, fmt.Errorf("add js rule: %w", err) @@ -268,6 +286,9 @@ func (f *Filter) HandleResponse(req *http.Request, res *http.Response) error { if err := f.cosmeticRulesInjector.Inject(req, res); err != nil { log.Printf("error injecting cosmetic rules for %q: %v", logger.Redacted(req.URL), err) } + if err := f.cssRulesInjector.Inject(req, res); err != nil { + log.Printf("error injecting css rules for %q: %v", logger.Redacted(req.URL), err) + } if err := f.jsRuleInjector.Inject(req, res); err != nil { // The error is recoverable, so we log it and continue processing the response. log.Printf("error injecting js rules for %q: %v", logger.Redacted(req.URL), err)