diff --git a/packages/tsurlfilter/CHANGELOG.md b/packages/tsurlfilter/CHANGELOG.md index da8d0e6fd..89430f6ee 100644 --- a/packages/tsurlfilter/CHANGELOG.md +++ b/packages/tsurlfilter/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for `$to` modifier in the MV3 converter. +- Support for `$method` modifier in the MV3 converter. + ## [2.1.6] - 2023-08-04 diff --git a/packages/tsurlfilter/src/rules/declarative-converter/README.md b/packages/tsurlfilter/src/rules/declarative-converter/README.md index a9836063d..fb64eb5f2 100644 --- a/packages/tsurlfilter/src/rules/declarative-converter/README.md +++ b/packages/tsurlfilter/src/rules/declarative-converter/README.md @@ -884,7 +884,7 @@ the blocking rule will not be applied despite it has the `$important` modifier ``` ## $method -Status: not implemented yet +Status: supported
Examples:
@@ -897,7 +897,37 @@ example 1 ↓↓↓↓ converted to ↓↓↓↓ ```json -[] +[ + { + "id": 1, + "action": { + "type": "block" + }, + "condition": { + "urlFilter": "||evil.com^", + "requestMethods": [ + "get", + "head" + ], + "isUrlFilterCaseSensitive": false, + "resourceTypes": [ + "main_frame", + "sub_frame", + "stylesheet", + "script", + "image", + "font", + "object", + "xmlhttprequest", + "ping", + "media", + "websocket", + "other" + ] + }, + "priority": 76 + } +] ``` example 2 @@ -909,7 +939,37 @@ example 2 ↓↓↓↓ converted to ↓↓↓↓ ```json -[] +[ + { + "id": 1, + "action": { + "type": "block" + }, + "condition": { + "urlFilter": "||evil.com^", + "excludedRequestMethods": [ + "post", + "put" + ], + "isUrlFilterCaseSensitive": false, + "resourceTypes": [ + "main_frame", + "sub_frame", + "stylesheet", + "script", + "image", + "font", + "object", + "xmlhttprequest", + "ping", + "media", + "websocket", + "other" + ] + }, + "priority": 2 + } +] ``` example 3 @@ -921,7 +981,36 @@ example 3 ↓↓↓↓ converted to ↓↓↓↓ ```json -[] +[ + { + "id": 1, + "action": { + "type": "allow" + }, + "condition": { + "urlFilter": "||evil.com", + "requestMethods": [ + "get" + ], + "isUrlFilterCaseSensitive": false, + "resourceTypes": [ + "main_frame", + "sub_frame", + "stylesheet", + "script", + "image", + "font", + "object", + "xmlhttprequest", + "ping", + "media", + "websocket", + "other" + ] + }, + "priority": 100101 + } +] ``` example 4 @@ -933,7 +1022,36 @@ example 4 ↓↓↓↓ converted to ↓↓↓↓ ```json -[] +[ + { + "id": 1, + "action": { + "type": "allow" + }, + "condition": { + "urlFilter": "||evil.com", + "excludedRequestMethods": [ + "post" + ], + "isUrlFilterCaseSensitive": false, + "resourceTypes": [ + "main_frame", + "sub_frame", + "stylesheet", + "script", + "image", + "font", + "object", + "xmlhttprequest", + "ping", + "media", + "websocket", + "other" + ] + }, + "priority": 100002 + } +] ``` @@ -3663,4 +3781,4 @@ example 1. ## $app (not supported in extension) -## $extension (not supported in extension) +## $extension (not supported in extension) \ No newline at end of file diff --git a/packages/tsurlfilter/src/rules/declarative-converter/declarative-rule.ts b/packages/tsurlfilter/src/rules/declarative-converter/declarative-rule.ts index 8241a8c24..2c19207de 100644 --- a/packages/tsurlfilter/src/rules/declarative-converter/declarative-rule.ts +++ b/packages/tsurlfilter/src/rules/declarative-converter/declarative-rule.ts @@ -9,6 +9,7 @@ import { z as zod } from 'zod'; import { RequestType } from '../../request-type'; +import { HTTPMethod } from '../../modifiers/method-modifier'; /** * https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#type-DomainType @@ -150,6 +151,26 @@ export enum RequestMethod { Put = 'put', } +/** + * tsurlfilter {@link HTTPMethod} without {@link HTTPMethod.TRACE} + * because it is not supported by {@link RequestMethod}. + */ +export type SupportedHttpMethod = Exclude; + +/** + * Map {@link HTTPMethod} to declarative {@link RequestMethod}. + */ +export const DECLARATIVE_REQUEST_METHOD_MAP: Record = { + [HTTPMethod.GET]: RequestMethod.Get, + [HTTPMethod.POST]: RequestMethod.Post, + [HTTPMethod.PUT]: RequestMethod.Put, + [HTTPMethod.DELETE]: RequestMethod.Delete, + [HTTPMethod.PATCH]: RequestMethod.Patch, + [HTTPMethod.HEAD]: RequestMethod.Head, + [HTTPMethod.OPTIONS]: RequestMethod.Options, + [HTTPMethod.CONNECT]: RequestMethod.Connect, +}; + /** * https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#type-RuleCondition */ @@ -165,7 +186,7 @@ const RuleConditionValidator = zod.strictObject({ isUrlFilterCaseSensitive: zod.boolean().optional(), regexFilter: zod.string().optional(), requestDomains: zod.string().array().optional(), - requestMethods: zod.string().array().optional(), + requestMethods: zod.nativeEnum(RequestMethod).array().optional(), /** * If none of the `excludedResourceTypes` and `resourceTypes` are specified, * all resource types except "main_frame" will be matched. diff --git a/packages/tsurlfilter/src/rules/declarative-converter/grouped-rules-converters/abstract-rule-converter.ts b/packages/tsurlfilter/src/rules/declarative-converter/grouped-rules-converters/abstract-rule-converter.ts index 237e6f455..fc39bf029 100644 --- a/packages/tsurlfilter/src/rules/declarative-converter/grouped-rules-converters/abstract-rule-converter.ts +++ b/packages/tsurlfilter/src/rules/declarative-converter/grouped-rules-converters/abstract-rule-converter.ts @@ -114,6 +114,9 @@ import { RuleActionHeaders, ModifyHeaderInfo, DECLARATIVE_RESOURCE_TYPES_MAP, + DECLARATIVE_REQUEST_METHOD_MAP, + SupportedHttpMethod, + RequestMethod, } from '../declarative-rule'; import { TooComplexRegexpError, @@ -127,6 +130,7 @@ import { ResourcesPathError } from '../errors/converter-options-errors'; import { RedirectModifier } from '../../../modifiers/redirect-modifier'; import { RemoveHeaderModifier } from '../../../modifiers/remove-header-modifier'; import { CSP_HEADER_NAME } from '../../../modifiers/csp-modifier'; +import { HTTPMethod } from '../../../modifiers/method-modifier'; /** * Contains the generic logic for converting a {@link NetworkRule} @@ -170,6 +174,22 @@ export abstract class DeclarativeRuleConverter { .map(([resourceTypeKey]) => resourceTypeKey) as ResourceType[]; } + /** + * Converts list of tsurlfilter {@link HTTPMethod|methods} to declarative + * supported http {@link RequestMethod|methods} via excluding 'trace' method. + * + * @param methods List of {@link HTTPMethod|methods}. + * + * @returns List of {@link RequestMethod|methods}. + */ + private static mapHttpMethodToDeclarativeHttpMethod(methods: HTTPMethod[]): RequestMethod[] { + return methods + // Filters unsupported `trace` method + .filter((m): m is SupportedHttpMethod => m !== HTTPMethod.TRACE) + // Map tsurlfilter http method to supported declarative http method + .map((m) => DECLARATIVE_REQUEST_METHOD_MAP[m]); + } + /** * Checks if the string contains only ASCII characters. * @@ -428,13 +448,13 @@ export abstract class DeclarativeRuleConverter { // set initiatorDomains const permittedDomains = rule.getPermittedDomains(); - if (permittedDomains && permittedDomains.length > 0) { + if (permittedDomains && permittedDomains.length !== 0) { condition.initiatorDomains = this.toASCII(permittedDomains); } // set excludedInitiatorDomains const excludedDomains = rule.getRestrictedDomains(); - if (excludedDomains && excludedDomains.length > 0) { + if (excludedDomains && excludedDomains.length !== 0) { condition.excludedInitiatorDomains = this.toASCII(excludedDomains); } @@ -446,9 +466,9 @@ export abstract class DeclarativeRuleConverter { // Can be specified $to or $denyallow, but not together. const denyAllowDomains = rule.getDenyAllowDomains(); const restrictedToDomains = rule.getRestrictedToDomains(); - if (denyAllowDomains && denyAllowDomains.length > 0) { + if (denyAllowDomains && denyAllowDomains.length !== 0) { condition.excludedRequestDomains = this.toASCII(denyAllowDomains); - } else if (restrictedToDomains && restrictedToDomains.length > 0) { + } else if (restrictedToDomains && restrictedToDomains.length !== 0) { condition.excludedRequestDomains = this.toASCII(restrictedToDomains); } @@ -465,6 +485,16 @@ export abstract class DeclarativeRuleConverter { condition.resourceTypes = this.getResourceTypes(permittedRequestTypes); } + const permittedMethods = rule.getPermittedMethods(); + if (permittedMethods && permittedMethods.length !== 0) { + condition.requestMethods = this.mapHttpMethodToDeclarativeHttpMethod(permittedMethods); + } + + const restrictedMethods = rule.getRestrictedMethods(); + if (restrictedMethods && restrictedMethods.length !== 0) { + condition.excludedRequestMethods = this.mapHttpMethodToDeclarativeHttpMethod(restrictedMethods); + } + // set isUrlFilterCaseSensitive condition.isUrlFilterCaseSensitive = rule.isOptionEnabled(NetworkRuleOption.MatchCase); @@ -478,9 +508,10 @@ export abstract class DeclarativeRuleConverter { */ const shouldMatchAllResourcesTypes = rule.isOptionEnabled(NetworkRuleOption.RemoveHeader) || rule.isOptionEnabled(NetworkRuleOption.Csp) - || rule.isOptionEnabled(NetworkRuleOption.To); + || rule.isOptionEnabled(NetworkRuleOption.To) + || rule.isOptionEnabled(NetworkRuleOption.Method); const emptyResourceTypes = !condition.resourceTypes && !condition.excludedResourceTypes; - if (shouldMatchAllResourcesTypes && emptyResourceTypes && !rule.isAllowlist()) { + if (shouldMatchAllResourcesTypes && emptyResourceTypes) { condition.resourceTypes = [ ResourceType.MainFrame, ResourceType.SubFrame, @@ -588,6 +619,7 @@ export abstract class DeclarativeRuleConverter { * $removeheader - if it contains a title from a prohibited list * (see {@link RemoveHeaderModifier.FORBIDDEN_HEADERS}); * $jsonprune; + * $method - if the modifier contains 'trace' method, * $hls. * * @param rule - Network rule. @@ -680,6 +712,33 @@ export abstract class DeclarativeRuleConverter { return null; }; + /** + * Checks if the $method values in the provided network rule + * are supported for conversion to MV3. + * + * @param r Network rule. + * @param name Modifier's name. + * + * @returns Error {@link UnsupportedModifierError} or null if rule is supported. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const checkMethodModifierFn = (r: NetworkRule, name: string): UnsupportedModifierError | null => { + const permittedMethods = r.getPermittedMethods(); + const restrictedMethods = r.getRestrictedMethods(); + if ( + permittedMethods?.some((method) => method === HTTPMethod.TRACE) + || restrictedMethods?.some((method) => method === HTTPMethod.TRACE) + ) { + return new UnsupportedModifierError( + // eslint-disable-next-line max-len + `Network rule with $method modifier containing 'trace' method is not supported: "${r.getText()}"`, + r, + ); + } + + return null; + }; + const unsupportedOptions = [ /* Specific exceptions */ { option: NetworkRuleOption.Elemhide, name: '$elemhide', skipConversion: true }, @@ -692,7 +751,6 @@ export abstract class DeclarativeRuleConverter { { option: NetworkRuleOption.Extension, name: '$extension' }, { option: NetworkRuleOption.Stealth, name: '$stealth' }, /* Specific exceptions */ - { option: NetworkRuleOption.Method, name: '$method' }, { option: NetworkRuleOption.Popup, name: '$popup', @@ -720,6 +778,11 @@ export abstract class DeclarativeRuleConverter { name: '$removeheader', customChecks: [checkAllowRulesFn, checkRemoveHeaderModifierFn], }, + { + option: NetworkRuleOption.Method, + name: '$method', + customChecks: [checkMethodModifierFn], + }, { option: NetworkRuleOption.JsonPrune, name: '$jsonprune' }, { option: NetworkRuleOption.Hls, name: '$hls' }, ]; diff --git a/packages/tsurlfilter/src/rules/declarative-converter/readme.txt b/packages/tsurlfilter/src/rules/declarative-converter/readme.txt index 4978b74ae..a47003bcc 100644 --- a/packages/tsurlfilter/src/rules/declarative-converter/readme.txt +++ b/packages/tsurlfilter/src/rules/declarative-converter/readme.txt @@ -157,7 +157,7 @@ page$domain=targetdomain.com|~example.org */BannerAd.gif$match-case ! ## $method -! Status: not implemented yet +! Status: supported !
! Examples: !
diff --git a/packages/tsurlfilter/test/rules/declarative-converter/declarative-rule-converter.test.ts b/packages/tsurlfilter/test/rules/declarative-converter/declarative-rule-converter.test.ts index 52fb35bf7..02692b541 100644 --- a/packages/tsurlfilter/test/rules/declarative-converter/declarative-rule-converter.test.ts +++ b/packages/tsurlfilter/test/rules/declarative-converter/declarative-rule-converter.test.ts @@ -1429,4 +1429,142 @@ describe('DeclarativeRuleConverter', () => { }); }); }); + + describe('check $method', () => { + it('converts rule with two permitted methods', () => { + const filterId = 0; + const rules = createRulesFromText( + filterId, + ['||evil.com$method=get|head'], + ); + + const { + declarativeRules, + } = DeclarativeRulesConverter.convert( + [[filterId, rules]], + ); + expect(declarativeRules.length).toBe(1); + expect(declarativeRules[0]).toEqual({ + id: 1, + priority: 76, + action: { + type: 'block', + }, + condition: { + requestMethods: ['get', 'head'], + isUrlFilterCaseSensitive: false, + urlFilter: '||evil.com', + resourceTypes: allResourcesTypes, + }, + }); + }); + + it('converts rule with two restricted methods', () => { + const filterId = 0; + const rules = createRulesFromText( + filterId, + ['||evil.com$method=~post|~put'], + ); + + const { + declarativeRules, + } = DeclarativeRulesConverter.convert( + [[filterId, rules]], + ); + expect(declarativeRules.length).toBe(1); + expect(declarativeRules[0]).toEqual({ + id: 1, + priority: 2, + action: { + type: 'block', + }, + condition: { + excludedRequestMethods: ['post', 'put'], + isUrlFilterCaseSensitive: false, + urlFilter: '||evil.com', + resourceTypes: allResourcesTypes, + }, + }); + }); + + it('allowlist rule with one permitted method', () => { + const filterId = 0; + const rules = createRulesFromText( + filterId, + ['@@||evil.com$method=get'], + ); + + const { + declarativeRules, + } = DeclarativeRulesConverter.convert( + [[filterId, rules]], + ); + expect(declarativeRules.length).toBe(1); + expect(declarativeRules[0]).toEqual({ + id: 1, + priority: 100101, + action: { + type: 'allow', + }, + condition: { + requestMethods: ['get'], + isUrlFilterCaseSensitive: false, + urlFilter: '||evil.com', + resourceTypes: allResourcesTypes, + }, + }); + }); + + it('allowlist rule with two restricted methods', () => { + const filterId = 0; + const rules = createRulesFromText( + filterId, + ['@@||evil.com$method=~post'], + ); + + const { + declarativeRules, + } = DeclarativeRulesConverter.convert( + [[filterId, rules]], + ); + expect(declarativeRules.length).toBe(1); + expect(declarativeRules[0]).toEqual({ + id: 1, + priority: 100002, + action: { + type: 'allow', + }, + condition: { + excludedRequestMethods: ['post'], + isUrlFilterCaseSensitive: false, + urlFilter: '||evil.com', + resourceTypes: allResourcesTypes, + }, + }); + }); + + it('returns UnsupportedModifierError for `trace` method', () => { + const filterId = 0; + const ruleText = '||evil.com$method=trace'; + const rules = createRulesFromText(filterId, [ruleText]); + + const { + declarativeRules, + errors, + } = DeclarativeRulesConverter.convert( + [[filterId, rules]], + ); + expect(declarativeRules.length).toBe(0); + expect(errors.length).toBe(1); + + const networkRule = new NetworkRule(ruleText, filterId); + + const err = new UnsupportedModifierError( + // eslint-disable-next-line max-len + `Network rule with $method modifier containing 'trace' method is not supported: "${networkRule.getText()}"`, + networkRule, + ); + expect(errors[0]).toStrictEqual(err); + }); + }); });