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);
+ });
+ });
});