Skip to content

Commit 43d118b

Browse files
authored
Merge pull request #212 from mozilla-services/feat/add-webkit-generator-script
[WebkitLists] P2. Add `lists2webkit` script to generate webkit compatible lists
2 parents 81898fc + e01c782 commit 43d118b

8 files changed

Lines changed: 245 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ __pycache__/
1515

1616
# coverage files
1717
.coverage
18-
.venv/
18+
.venv/
19+
webkit-lists/

constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,7 @@
105105

106106
VERS_LARGE_ENTITIES_SEPARATION_STARTED = 74
107107
VERSION_EMAIL_CATEGORY_INTRODUCED = 97
108+
109+
WEBKIT_LISTS_DIR = 'webkit-lists'
110+
WEBKIT_BLOCK_ALL = "block"
111+
WEBKIT_BLOCK_COOKIES = "block-cookies"

lists2webkit.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import json
2+
import os
3+
from settings import config
4+
from constants import (
5+
DNT_SECTIONS,
6+
PRE_DNT_SECTIONS,
7+
ENTITYLIST_SECTIONS,
8+
WEBKIT_LISTS_DIR,
9+
WEBKIT_BLOCK_ALL,
10+
WEBKIT_BLOCK_COOKIES
11+
)
12+
from utils import (
13+
get_blocked_domains,
14+
add_domain_to_list,
15+
load_json_from_url
16+
)
17+
18+
def process_and_sort_domains(domains):
19+
"""Sorts and adds domains to the list, returning successfully added domains."""
20+
added_domains = []
21+
previous_domain = None
22+
for domain in sorted(domains):
23+
if add_domain_to_list(domain, domain, previous_domain, None, []):
24+
added_domains.append(domain)
25+
previous_domain = domain
26+
return added_domains
27+
28+
def get_tracker_lists(section):
29+
"""Retrieves and processes tracker lists for a given section."""
30+
blocked_domains = get_blocked_domains(config, section)
31+
processed_domains = process_and_sort_domains(blocked_domains)
32+
print(f"Processing tracker list: {section}")
33+
return {section: processed_domains}
34+
35+
def get_entity_list(section):
36+
"""Retrieves the entity whitelist for a given section."""
37+
entity_data = load_json_from_url(config, section, 'entity_url')
38+
list_name = config.get(section, 'output')
39+
print(f"Processing entity list: {list_name}")
40+
return {
41+
res: {"properties": details.get("properties", [])}
42+
for details in entity_data.get("entities", {}).values()
43+
for res in details.get("resources", [])
44+
}
45+
46+
def build_url_filter(resource):
47+
escaped_resource = resource.replace('.', '\\.')
48+
return f"^https?://([^/]+\\.)?{escaped_resource}"
49+
50+
def find_entity_for_resource(resource, entities):
51+
for key_resource, entity in entities.items():
52+
if key_resource in resource:
53+
return entity
54+
return None
55+
56+
def build_rule(resource, action_type, entities):
57+
"""
58+
Builds a content blocking rule for WebKit based on the given resource, action type, and associated entities.
59+
60+
Content blocking rules in WebKit follow a declarative format.
61+
Each rule consists of a `trigger` defining when the rule activates and an `action` specifying what
62+
happens when it is activated.
63+
64+
- `resource`: The URL to block (used to create a `url-filter`).
65+
- `action_type`: The action type, e.g., "block" or "block-cookies".
66+
- `entities`: A mapping of resources to their associated entities and properties.
67+
68+
The `url-filter` is derived from the `resource`, specifying the URL pattern to match.
69+
If an entity is found for the resource, its properties are used to populate `unless-domain`,
70+
which specifies domains exempted from this rule.
71+
72+
The `load-type` is set to `["third-party"]` to limit the rule to third-party resources.
73+
NOTE: We can support first-party later by including `["first-party"]`.
74+
75+
Example of a WebKit rule:
76+
{
77+
"trigger": {
78+
"url-filter": "evil-tracker\\.js",
79+
"unless-domain": ["trusted.com"]
80+
},
81+
"action": {
82+
"type": "block"
83+
}
84+
}
85+
For more information, see: https://webkit.org/blog/3476/content-blockers-first-look/
86+
"""
87+
url_filter = build_url_filter(resource)
88+
entity = entities.get(resource) or find_entity_for_resource(resource, entities)
89+
unless_domains = [f"*{domain}" for domain in entity["properties"]] if entity and isinstance(entity.get("properties"), list) else []
90+
return {
91+
"action": {"type": action_type},
92+
"trigger": {
93+
"url-filter": url_filter,
94+
"load-type": ["third-party"],
95+
**({"unless-domain": unless_domains} if unless_domains else {})
96+
},
97+
}
98+
99+
def generate_content_blocker_list(resources, action_type, entities):
100+
"""Generates a list of content blocker rules for a category."""
101+
return [build_rule(resource, action_type, entities) for resource in resources]
102+
103+
def write_to_file(content, output_file):
104+
# NOTE: This function mimics the behavior of the Swift implementation.
105+
# We intentionally generate a compact JSON format using separators instead of pretty-printing
106+
# with json.dumps(indent=2/4). The goal is to create files that are small yet still readable.
107+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
108+
with open(output_file, "w") as f:
109+
f.write("[\n")
110+
for i, rule in enumerate(content):
111+
f.write(json.dumps(rule, separators=(',', ':')))
112+
if i < len(content) - 1:
113+
f.write(",\n")
114+
f.write("\n]")
115+
116+
def generate_webkit_lists(domains, action_type, name, entities):
117+
"""Generates and writes WebKit-compatible JSON lists."""
118+
rules = generate_content_blocker_list(domains, action_type, entities)
119+
output_file = f"{WEBKIT_LISTS_DIR}/disconnect-{action_type}-{name}.json"
120+
write_to_file(rules, output_file)
121+
122+
def main():
123+
tracker_lists = {}
124+
entities = {}
125+
# Process each section in the configuration
126+
for name in config.sections():
127+
section = config[name]
128+
ios_include_as = section.get('ios_include_as')
129+
if not ios_include_as:
130+
continue
131+
print(f"Processing section: {name}")
132+
133+
if name in PRE_DNT_SECTIONS or name in DNT_SECTIONS:
134+
tracker_lists.update(get_tracker_lists(name))
135+
136+
if name in ENTITYLIST_SECTIONS:
137+
entities = get_entity_list(name)
138+
139+
for name, domains in tracker_lists.items():
140+
# Generate WebKit block-all lists for all
141+
# sections with `ios_include_as``
142+
ios_include_as = config[name].get('ios_include_as')
143+
generate_webkit_lists(domains, WEBKIT_BLOCK_ALL, ios_include_as, entities)
144+
145+
# Optionally generate block-cookies WebKit lists for all
146+
# sections with `ios_block_cookies`
147+
if config[name].getboolean('ios_block_cookies', False):
148+
generate_webkit_lists(domains, WEBKIT_BLOCK_COOKIES, ios_include_as, entities)
149+
150+
print("All content blocker rules have been generated successfully.")
151+
152+
if __name__ == "__main__":
153+
main()

rs_dev.ini

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ output=basew3c-track-digest256
3434
categories=Content
3535
output=content-track-digest256
3636
versioning_needed=true
37+
ios_include_as=content
38+
ios_block_cookies=true
3739

3840
# DNT="EFF", content category only
3941
[tracking-protection-contenteff]
@@ -50,18 +52,24 @@ output=contentw3c-track-digest256
5052
categories=Advertising
5153
output=ads-track-digest256
5254
versioning_needed=true
55+
ios_include_as=advertising
56+
ios_block_cookies=true
5357

5458
# DNT="", analytics category
5559
[tracking-protection-analytics]
5660
categories=Analytics
5761
output=analytics-track-digest256
5862
versioning_needed=true
63+
ios_include_as=analytics
64+
ios_block_cookies=true
5965

6066
# DNT="", social category
6167
[tracking-protection-social]
6268
categories=Social
6369
output=social-track-digest256
6470
versioning_needed=true
71+
ios_include_as=social
72+
ios_block_cookies=true
6573

6674
# These "social-tracking-protection" lists are for Firefox STP
6775
[social-tracking-protection]
@@ -98,6 +106,7 @@ versioning_needed=true
98106
entity_url=https://raw.githubusercontent.com/mozilla-services/shavar-prod-lists/master/disconnect-entitylist.json
99107
output=mozstd-trackwhite-digest256
100108
versioning_needed=true
109+
ios_include_as=entitylist
101110

102111
[google-whitelist]
103112
entity_url=https://raw.githubusercontent.com/mozilla-services/shavar-prod-lists/master/disconnect-entitylist.json
@@ -151,6 +160,7 @@ blocklist=https://raw.githubusercontent.com/mozilla-services/shavar-plugin-block
151160
categories=Advertising|Analytics|Social|Content,FingerprintingInvasive
152161
output=base-fingerprinting-track-digest256
153162
versioning_needed=true
163+
ios_include_as=fingerprinting
154164

155165
[tracking-protection-content-fingerprinting]
156166
categories=FingerprintingInvasive
@@ -163,6 +173,7 @@ versioning_needed=true
163173
categories=Cryptomining
164174
output=base-cryptomining-track-digest256
165175
versioning_needed=true
176+
ios_include_as=cryptomining
166177

167178
# DNT="", Content and Cryptomining top-level categories
168179
[tracking-protection-content-cryptomining]

rs_prod.ini

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ output=basew3c-track-digest256
3434
categories=Content
3535
output=content-track-digest256
3636
versioning_needed=true
37+
ios_include_as=content
38+
ios_block_cookies=true
3739

3840
# DNT="EFF", content category only
3941
[tracking-protection-contenteff]
@@ -50,18 +52,24 @@ output=contentw3c-track-digest256
5052
categories=Advertising
5153
output=ads-track-digest256
5254
versioning_needed=true
55+
ios_include_as=advertising
56+
ios_block_cookies=true
5357

5458
# DNT="", analytics category
5559
[tracking-protection-analytics]
5660
categories=Analytics
5761
output=analytics-track-digest256
5862
versioning_needed=true
63+
ios_include_as=analytics
64+
ios_block_cookies=true
5965

6066
# DNT="", social category
6167
[tracking-protection-social]
6268
categories=Social
6369
output=social-track-digest256
6470
versioning_needed=true
71+
ios_include_as=social
72+
ios_block_cookies=true
6573

6674
# These "social-tracking-protection" lists are for Firefox STP
6775
[social-tracking-protection]
@@ -98,6 +106,7 @@ versioning_needed=true
98106
entity_url=https://raw.githubusercontent.com/mozilla-services/shavar-prod-lists/master/disconnect-entitylist.json
99107
output=mozstd-trackwhite-digest256
100108
versioning_needed=true
109+
ios_include_as=entitylist
101110

102111
[google-whitelist]
103112
entity_url=https://raw.githubusercontent.com/mozilla-services/shavar-prod-lists/master/disconnect-entitylist.json
@@ -151,6 +160,7 @@ blocklist=https://raw.githubusercontent.com/mozilla-services/shavar-plugin-block
151160
categories=Advertising|Analytics|Social|Content,FingerprintingInvasive
152161
output=base-fingerprinting-track-digest256
153162
versioning_needed=true
163+
ios_include_as=fingerprinting
154164

155165
[tracking-protection-content-fingerprinting]
156166
categories=FingerprintingInvasive
@@ -163,6 +173,7 @@ versioning_needed=true
163173
categories=Cryptomining
164174
output=base-cryptomining-track-digest256
165175
versioning_needed=true
176+
ios_include_as=cryptomining
166177

167178
# DNT="", Content and Cryptomining top-level categories
168179
[tracking-protection-content-cryptomining]

rs_stage.ini

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ output=basew3c-track-digest256
3434
categories=Content
3535
output=content-track-digest256
3636
versioning_needed=true
37+
ios_include_as=content
38+
ios_block_cookies=true
3739

3840
# DNT="EFF", content category only
3941
[tracking-protection-contenteff]
@@ -50,18 +52,24 @@ output=contentw3c-track-digest256
5052
categories=Advertising
5153
output=ads-track-digest256
5254
versioning_needed=true
55+
ios_include_as=advertising
56+
ios_block_cookies=true
5357

5458
# DNT="", analytics category
5559
[tracking-protection-analytics]
5660
categories=Analytics
5761
output=analytics-track-digest256
5862
versioning_needed=true
63+
ios_include_as=analytics
64+
ios_block_cookies=true
5965

6066
# DNT="", social category
6167
[tracking-protection-social]
6268
categories=Social
6369
output=social-track-digest256
6470
versioning_needed=true
71+
ios_include_as=social
72+
ios_block_cookies=true
6573

6674
# These "social-tracking-protection" lists are for Firefox STP
6775
[social-tracking-protection]
@@ -98,6 +106,7 @@ versioning_needed=true
98106
entity_url=https://raw.githubusercontent.com/mozilla-services/shavar-prod-lists/master/disconnect-entitylist.json
99107
output=mozstd-trackwhite-digest256
100108
versioning_needed=true
109+
ios_include_as=entitylist
101110

102111
[google-whitelist]
103112
entity_url=https://raw.githubusercontent.com/mozilla-services/shavar-prod-lists/master/disconnect-entitylist.json
@@ -151,6 +160,7 @@ blocklist=https://raw.githubusercontent.com/mozilla-services/shavar-plugin-block
151160
categories=Advertising|Analytics|Social|Content,FingerprintingInvasive
152161
output=base-fingerprinting-track-digest256
153162
versioning_needed=true
163+
ios_include_as=fingerprinting
154164

155165
[tracking-protection-content-fingerprinting]
156166
categories=FingerprintingInvasive
@@ -163,6 +173,7 @@ versioning_needed=true
163173
categories=Cryptomining
164174
output=base-cryptomining-track-digest256
165175
versioning_needed=true
176+
ios_include_as=cryptomining
166177

167178
# DNT="", Content and Cryptomining top-level categories
168179
[tracking-protection-content-cryptomining]

0 commit comments

Comments
 (0)