diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 05:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 05:47:55 +0000 |
commit | 31d6ff6f931696850c348007241195ab3b2eddc7 (patch) | |
tree | 615cb1c57ce9f6611bad93326b9105098f379609 /uAssets/tools | |
parent | Initial commit. (diff) | |
download | ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip |
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'uAssets/tools')
-rwxr-xr-x | uAssets/tools/make-diffpatch.sh | 100 | ||||
-rw-r--r-- | uAssets/tools/make-easylist.mjs | 201 | ||||
-rwxr-xr-x | uAssets/tools/make-easylist.sh | 35 | ||||
-rwxr-xr-x | uAssets/tools/make-ublock.sh | 21 | ||||
-rwxr-xr-x | uAssets/tools/make-validate.sh | 12 | ||||
-rw-r--r-- | uAssets/tools/need-patch.mjs | 81 | ||||
-rwxr-xr-x | uAssets/tools/update-3rdparties.sh | 30 | ||||
-rwxr-xr-x | uAssets/tools/update-diffpatches.sh | 115 | ||||
-rw-r--r-- | uAssets/tools/validate/config.js | 33 | ||||
-rw-r--r-- | uAssets/tools/validate/package.json | 6 | ||||
-rw-r--r-- | uAssets/tools/validate/validate.js | 321 |
11 files changed, 955 insertions, 0 deletions
diff --git a/uAssets/tools/make-diffpatch.sh b/uAssets/tools/make-diffpatch.sh new file mode 100755 index 0000000..81e0aab --- /dev/null +++ b/uAssets/tools/make-diffpatch.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +set -e + +# To be executed at the root of CDN repo +# +# It's not being hosted at CDN because that +# repo is also used as a website + +VERSION=$1 +if [[ -z $VERSION ]]; then + echo "Error: No version provided, aborting" + exit 1 +fi + +PATCHES_DIR=$2 +if [[ -z $PATCHES_DIR ]]; then + echo "Error: patches directory is not provided, aborting" + exit 1 +fi + +PREVIOUS_VERSION=$(<version) +PREVIOUS_PATCH_FILE="$PATCHES_DIR/$PREVIOUS_VERSION.patch" +: > "$PREVIOUS_PATCH_FILE" + +NEXT_PATCH_FILE="$PATCHES_DIR/$VERSION.patch" + +# Temporary file to receive the RCS patch data +DIFF=$(mktemp) + +FILES=( $(git diff --name-only) ) +for FILE in "${FILES[@]}"; do + + # Reference: + # https://github.com/ameshkov/diffupdates + + if (head "$FILE" | grep -q '^! Version: '); then + sed -Ei "1,10s;^! Version: .+$;! Version: $VERSION;" "$FILE" + fi + + # Patches are for filter lists supporting differential updates + if (head "$FILE" | grep -q '^! Diff-Path: '); then + + # Extract diff name from `! Diff-Path:` field + DIFF_NAME=$(grep -m 1 -oP '^! Diff-Path: [^#]+#?\K.*' "$FILE") + # Fall back to `! Diff-Name:` field if no name found + # Remove once `! Diff-Name:` is no longer needed after transition + if [[ -z $DIFF_NAME ]]; then + DIFF_NAME=$(grep -m 1 -oP '^! Diff-Name: \K.+' "$FILE") + fi + echo "Info: Diff name for ${FILE} is ${DIFF_NAME}" + + # We need a patch name to generate a valid patch + if [[ -n $DIFF_NAME ]]; then + + # Compute relative patch path + PATCH_PATH="$(realpath --relative-to="$(dirname "$FILE")" "$NEXT_PATCH_FILE")" + + # Fill in patch path to next version (do not clobber hash portion) + sed -Ei "1,10s;^! Diff-Path: [^#]+(#.+)?$;! Diff-Path: $PATCH_PATH\1;" "$FILE" + + # Compute the RCS diff between current version and new version + git show "HEAD:$FILE" | diff -n - "$FILE" > "$DIFF" || true + + FILE_CHECKSUM="$(sha1sum "$FILE")" + FILE_CHECKSUM=${FILE_CHECKSUM:0:10} + + DIFF_LINES=$(wc -l < "$DIFF") + echo "Info: Computed patch for ${FILE} has ${DIFF_LINES} lines" + + # Populate output file with patch information + echo "Info: Adding patch data of ${FILE} to ${PREVIOUS_PATCH_FILE}" + echo "diff name:$DIFF_NAME lines:$DIFF_LINES checksum:$FILE_CHECKSUM" >> "$PREVIOUS_PATCH_FILE" + cat "$DIFF" >> "$PREVIOUS_PATCH_FILE" + + else + + echo "Error: Diff name not found, skipping" + + fi + fi + + # Stage changed file + echo "Info: Staging $FILE" + git add -u "$FILE" + +done + +# Create a patch only if there was a previous version +if [[ -n $PREVIOUS_VERSION ]]; then + echo "Info: Staging $PREVIOUS_PATCH_FILE" + git add "$PREVIOUS_PATCH_FILE" +fi + +echo -n "$VERSION" > version +git add version + +rm -f "$DIFF" diff --git a/uAssets/tools/make-easylist.mjs b/uAssets/tools/make-easylist.mjs new file mode 100644 index 0000000..b648363 --- /dev/null +++ b/uAssets/tools/make-easylist.mjs @@ -0,0 +1,201 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2022-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +// jshint node:true, esversion:9 + +'use strict'; + +/******************************************************************************/ + +import fs from 'fs/promises'; +import path from 'path'; +import process from 'process'; + +/******************************************************************************/ + +const expandedParts = new Set(); + +/******************************************************************************/ + +const commandLineArgs = (( ) => { + const args = new Map(); + let name, value; + for ( const arg of process.argv.slice(2) ) { + const pos = arg.indexOf('='); + if ( pos === -1 ) { + name = arg; + value = ''; + } else { + name = arg.slice(0, pos); + value = arg.slice(pos+1); + } + args.set(name, value); + } + return args; +})(); + +/******************************************************************************/ + +function expandTemplate(wd, parts) { + const out = []; + const reInclude = /^%include +(.+):(.+)%\s+/gm; + const trim = text => trimSublist(text); + for ( const part of parts ) { + if ( typeof part !== 'string' ) { + out.push(part); + continue; + } + let lastIndex = 0; + for (;;) { + const match = reInclude.exec(part); + if ( match === null ) { break; } + out.push(part.slice(lastIndex, match.index).trim()); + const repo = match[1].trim(); + const fpath = `${match[2].trim()}`; + if ( expandedParts.has(fpath) === false ) { + console.info(` Inserting ${fpath}`); + out.push( + out.push({ file: `${fpath}` }), + `! *** ${repo}:${fpath} ***`, + fs.readFile(`${wd}/${fpath}`, { encoding: 'utf8' }) + .then(text => fpath.includes('header') ? text : trim(text)), + ); + expandedParts.add(fpath); + } + lastIndex = reInclude.lastIndex; + } + out.push(part.slice(lastIndex).trim()); + } + return out; +} + +/******************************************************************************/ + +function expandIncludeDirectives(wd, parts) { + const out = []; + const reInclude = /^!#include (.+)\s*/gm; + const trim = text => trimSublist(text); + let parentPath = ''; + for ( const part of parts ) { + if ( typeof part !== 'string' ) { + if ( typeof part === 'object' && part.file !== undefined ) { + parentPath = part.file; + } + out.push(part); + continue; + } + let lastIndex = 0; + for (;;) { + const match = reInclude.exec(part); + if ( match === null ) { break; } + out.push(part.slice(lastIndex, match.index).trim()); + const fpath = `${path.dirname(parentPath)}/${match[1].trim()}`; + if ( expandedParts.has(fpath) === false ) { + console.info(` Inserting ${fpath}`); + out.push( + { file: fpath }, + `! *** ${fpath} ***`, + fs.readFile(`${wd}/${fpath}`, { encoding: 'utf8' }) + .then(text => fpath.includes('header') ? text : trim(text)), + ); + expandedParts.add(fpath); + } + lastIndex = reInclude.lastIndex; + } + out.push(part.slice(lastIndex).trim()); + } + return out; +} + +/******************************************************************************/ + +function trimSublist(text) { + // Remove empty comment lines + text = text.replace(/^!\s*$(?:\r\n|\n)/gm, ''); + // Remove sublist header information: the importing list will provide its + // own header. + text = text.trim().replace(/^(?:!\s+[^\r\n]+?(?:\r\n|\n))+/s, ''); + return text; +} + +/******************************************************************************/ + +function minify(text) { + // remove issue-related comments + text = text.replace(/^! https:\/\/.*?[\n\r]+/gm, ''); + // remove empty lines + text = text.replace(/^[\n\r]+/gm, ''); + // convert potentially present Windows-style newlines + text = text.replace(/\r\n/g, '\n'); + return text; +} + +/******************************************************************************/ + +function assemble(parts) { + const out = []; + for ( const part of parts ) { + if ( typeof part !== 'string' ) { continue; } + out.push(part); + } + return out.join('\n').trim() + '\n'; +} + +/******************************************************************************/ + +async function main() { + const workingDir = commandLineArgs.get('dir') || '.'; + const inFile = commandLineArgs.get('in'); + if ( typeof inFile !== 'string' || inFile === '' ) { + process.exit(1); + } + const outFile = commandLineArgs.get('out'); + if ( typeof outFile !== 'string' || outFile === '' ) { + process.exit(1); + } + + console.info(` Using template at ${inFile}`); + + const inText = fs.readFile(`${workingDir}/${inFile}`, { encoding: 'utf8' }); + + let parts = [ inText ]; + do { + parts = await Promise.all(parts); + parts = expandTemplate(workingDir, parts); + } while ( parts.some(v => v instanceof Promise) ); + + do { + parts = await Promise.all(parts); + parts = expandIncludeDirectives(workingDir, parts); + } while ( parts.some(v => v instanceof Promise)); + + let afterText = assemble(parts); + + if ( commandLineArgs.get('minify') !== undefined ) { + afterText = minify(afterText); + } + + console.info(` Creating ${outFile}`); + + fs.writeFile(outFile, afterText); +} + +main(); diff --git a/uAssets/tools/make-easylist.sh b/uAssets/tools/make-easylist.sh new file mode 100755 index 0000000..0185d8a --- /dev/null +++ b/uAssets/tools/make-easylist.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +echo "*** uAssets: Assembling EasyList lists" +TMPDIR=$(mktemp -d) +mkdir -p $TMPDIR/easylist +git clone --depth 1 https://github.com/easylist/easylist.git $TMPDIR/easylist +cp -R templates/easy*.template $TMPDIR/easylist/ + +echo "*** uAssets: Assembling easylist.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist.template out=thirdparties/easylist/easylist.txt + +echo "*** uAssets: Assembling easyprivacy.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easyprivacy.template out=thirdparties/easylist/easyprivacy.txt + +echo "*** uAssets: Assembling easylist-annoyances.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-annoyances.template out=thirdparties/easylist/easylist-annoyances.txt + +echo "*** uAssets: Assembling easylist-cookies.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-cookies.template out=thirdparties/easylist/easylist-cookies.txt + +echo "*** uAssets: Assembling easylist-social.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-social.template out=thirdparties/easylist/easylist-social.txt + +echo "*** uAssets: Assembling easylist-newsletters.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-newsletters.template out=thirdparties/easylist/easylist-newsletters.txt + +echo "*** uAssets: Assembling easylist-notifications.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-notifications.template out=thirdparties/easylist/easylist-notifications.txt + +echo "*** uAssets: Assembling easylist-chat.txt" +node ./tools/make-easylist.mjs dir=$TMPDIR/easylist in=easylist-chat.template out=thirdparties/easylist/easylist-chat.txt + +rm -rf $TMPDIR diff --git a/uAssets/tools/make-ublock.sh b/uAssets/tools/make-ublock.sh new file mode 100755 index 0000000..f35fb13 --- /dev/null +++ b/uAssets/tools/make-ublock.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +echo "*** uAssets: Assembling filters/filters.txt" +node ./tools/make-easylist.mjs in=templates/ublock-filters.template out=filters/filters.min.txt minify=1 + +echo "*** uAssets: Assembling filters/quick-fixes.txt" +node ./tools/make-easylist.mjs in=templates/ublock-quick-fixes.template out=filters/quick-fixes.min.txt minify=1 + +echo "*** uAssets: Assembling filters/privacy.txt" +node ./tools/make-easylist.mjs in=templates/ublock-privacy.template out=filters/privacy.min.txt minify=1 + +echo "*** uAssets: Assembling filters/unbreak.txt" +node ./tools/make-easylist.mjs in=templates/ublock-unbreak.template out=filters/unbreak.min.txt minify=1 + +echo "*** uAssets: Assembling filters/badware.txt" +node ./tools/make-easylist.mjs in=templates/ublock-badware.template out=filters/badware.min.txt minify=1 + +echo "*** uAssets: Assembling filters/annoyances.txt" +node ./tools/make-easylist.mjs in=templates/ublock-annoyances.template out=filters/annoyances.min.txt minify=1 diff --git a/uAssets/tools/make-validate.sh b/uAssets/tools/make-validate.sh new file mode 100755 index 0000000..8d7ff9b --- /dev/null +++ b/uAssets/tools/make-validate.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +# https://stackoverflow.com/a/52526704 +echo "*** Importing required uBO files" +mkdir -p build/validate +git clone --filter=blob:none --no-checkout https://github.com/gorhill/uBlock.git build/validate/uBlock +cd build/validate/uBlock +git sparse-checkout init --cone +git sparse-checkout set src/js src/lib +cd - diff --git a/uAssets/tools/need-patch.mjs b/uAssets/tools/need-patch.mjs new file mode 100644 index 0000000..c0cf231 --- /dev/null +++ b/uAssets/tools/need-patch.mjs @@ -0,0 +1,81 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2023-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +// jshint node:true, esversion:9 + +'use strict'; + +/******************************************************************************/ + +import fs from 'fs/promises'; +import path from 'path'; +import process from 'process'; + +/******************************************************************************/ + +const commandLineArgs = (( ) => { + const args = new Map(); + let name, value; + for ( const arg of process.argv.slice(2) ) { + const pos = arg.indexOf('='); + if ( pos === -1 ) { + name = arg; + value = ''; + } else { + name = arg.slice(0, pos); + value = arg.slice(pos+1); + } + args.set(name, value); + } + return args; +})(); + +/******************************************************************************/ + +async function main() { + const MS_PER_HOUR = 60 * 60 * 1000; + const targetDelayInHours = parseInt(commandLineArgs.get('delay') || '5', 10); + + const hoursSinceEpoch = Math.floor(Date.now() / MS_PER_HOUR); + if ( (hoursSinceEpoch % targetDelayInHours) === 0 ) { + console.log('yes'); + process.exit(0); + } + + const version = await fs.readFile('version', { encoding: 'utf8' }); + const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(version); + if ( match === null ) { + console.log('yes'); + process.exit(0); + } + + const date = new Date(); + date.setUTCFullYear( + parseInt(match[1], 10), + parseInt(match[2], 10) - 1, + parseInt(match[3], 10) + ); + date.setUTCHours(0, parseInt(match[4], 10), 0, 0); + const expiredTimeInHours = (Date.now() - date.getTime()) / MS_PER_HOUR; + console.log(expiredTimeInHours >= targetDelayInHours ? 'yes' : 'no'); +} + +main(); diff --git a/uAssets/tools/update-3rdparties.sh b/uAssets/tools/update-3rdparties.sh new file mode 100755 index 0000000..f007c0c --- /dev/null +++ b/uAssets/tools/update-3rdparties.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +TEMPFILE=$(mktemp) + +echo "*** uAssets: updating remote assets..." + +declare -A assets +assets=( + ['thirdparties/pgl.yoyo.org/as/serverlist']='https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&startdate%5Bday%5D=&startdate%5Bmonth%5D=&startdate%5Byear%5D=&mimetype=plaintext' + ['thirdparties/publicsuffix.org/list/effective_tld_names.dat']='https://publicsuffix.org/list/public_suffix_list.dat' + ['thirdparties/urlhaus-filter/urlhaus-filter-online.txt']='https://malware-filter.gitlab.io/urlhaus-filter/urlhaus-filter-online.txt' +) + +for i in "${!assets[@]}"; do + localURL="$i" + remoteURL="${assets[$i]}" + echo "*** Downloading ${remoteURL}" + if wget -q -T 30 -O "$TEMPFILE" -- "$remoteURL"; then + if [ -s "$TEMPFILE" ]; then + if ! cmp -s "$TEMPFILE" "$localURL"; then + echo " New version found: ${localURL}" + if [ "$1" != "dry" ]; then + mv "$TEMPFILE" "$localURL" + fi + fi + fi + fi +done diff --git a/uAssets/tools/update-diffpatches.sh b/uAssets/tools/update-diffpatches.sh new file mode 100755 index 0000000..406598e --- /dev/null +++ b/uAssets/tools/update-diffpatches.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +set -e + +# To be executed at the root of CDN repo +# +# It's not being hosted at CDN because that +# repo is also used as a website + +REPO_DIR=$1 +if [[ -z $REPO_DIR ]]; then + echo "Error: repo directory is not provided, aborting" + exit 1 +fi + +PATCHES_DIR=$2 +if [[ -z $PATCHES_DIR ]]; then + echo "Error: patches directory is not provided, aborting" + exit 1 +fi + +FILTER_FILES=$3 +if [[ -z $FILTER_FILES ]]; then + echo "Error: filter lists are not provided, aborting" + exit 1 +fi +FILTER_FILES=( "$FILTER_FILES" ) + +PATCH_FILES=( $(ls -1v "$PATCHES_DIR"/*.patch | head -n -1) ) + +# Keep only the most recent (5-day x 4-per-day) patches +OBSOLETE_PATCHES=( $(ls -1v "$PATCHES_DIR"/*.patch | head -n -20) ) +for FILE in "${OBSOLETE_PATCHES[@]}"; do + echo "Removing obsolete patch $FILE" + git rm "$FILE" +done + +NEW_PATCH_FILE=$(mktemp) +DIFF_FILE=$(mktemp) + +for PATCH_FILE in "${PATCH_FILES[@]}"; do + + # Extract tag from patch file name + [[ ${PATCH_FILE} =~ ^$PATCHES_DIR/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\.patch$ ]] && \ + PREVIOUS_VERSION=${BASH_REMATCH[1]} + + # This will receive a clone of an old version of the current repo + echo "Fetching repo at $PREVIOUS_VERSION version" + OLD_REPO=$(mktemp -d) + git clone -q --single-branch --branch "$PREVIOUS_VERSION" --depth=1 "https://github.com/$REPO_DIR.git" "$OLD_REPO" 2>/dev/null || true + + # Skip if version doesn't exist + if [ -z "$(ls -A "$OLD_REPO" 2>/dev/null)" ]; then + continue; + fi + + : > "$NEW_PATCH_FILE" + + for FILTER_LIST in ${FILTER_FILES[@]}; do + + if [ ! -f "$OLD_REPO/$FILTER_LIST" ]; then continue; fi + + # Patches are for filter lists supporting differential updates + if ! (head "$OLD_REPO/$FILTER_LIST" | grep -q '^! Diff-Path: '); then + continue + fi + + # Reference: + # https://github.com/ameshkov/diffupdates + + # Extract diff name from `! Diff-Path:` field + DIFF_NAME=$(grep -m 1 -oP '^! Diff-Path: [^#]+#?\K.*' "$FILTER_LIST") + # Fall back to `! Diff-Name:` field if no name found + # Remove once `! Diff-Name:` is no longer needed after transition + if [[ -z $DIFF_NAME ]]; then + DIFF_NAME=$(grep -m 1 -oP '^! Diff-Name: \K.+' "$FILTER_LIST") + fi + + # We need a patch name to generate a valid patch + if [[ -z $DIFF_NAME ]]; then + echo "Info: $FILTER_LIST is missing a patch name, skipping" + continue + fi + + # Compute the RCS diff between current version and new version + diff -n "$OLD_REPO/$FILTER_LIST" "$FILTER_LIST" > "$DIFF_FILE" || true + + FILE_CHECKSUM=$(sha1sum "$FILTER_LIST") + FILE_CHECKSUM=${FILE_CHECKSUM:0:10} + + DIFF_LINE_COUNT=$(wc -l < "$DIFF_FILE") + + # Patch header + DIFF_HEAD="diff name:$DIFF_NAME lines:$DIFF_LINE_COUNT checksum:$FILE_CHECKSUM" + printf "\tAdding diff: %s\n" "$DIFF_HEAD" + echo "$DIFF_HEAD" >> "$NEW_PATCH_FILE" + # Patch data + cat "$DIFF_FILE" >> "$NEW_PATCH_FILE" + + done + + rm -rf "$OLD_REPO" + + # Stage changed patch file + mv -f "$NEW_PATCH_FILE" "$PATCH_FILE" + ls -l "$PATCH_FILE" + echo "Info: Staging ${PATCH_FILE}" + git add -u "$PATCH_FILE" + +done + +rm -f "$DIFF_FILE" +rm -f "$NEW_PATCH_FILE" diff --git a/uAssets/tools/validate/config.js b/uAssets/tools/validate/config.js new file mode 100644 index 0000000..270aedb --- /dev/null +++ b/uAssets/tools/validate/config.js @@ -0,0 +1,33 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2022-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +// jshint node:true, esversion:8, laxbreak:true + +'use strict'; + +export default { + dnsQueries: [ + 'https://cloudflare-dns.com/dns-query?name=${hn}&type=A', + 'https://dns.google/resolve?name=${hn}&type=A', + ], + // ms + throttle: 250, +}; diff --git a/uAssets/tools/validate/package.json b/uAssets/tools/validate/package.json new file mode 100644 index 0000000..c10527a --- /dev/null +++ b/uAssets/tools/validate/package.json @@ -0,0 +1,6 @@ +{ + "engines": { + "node": ">=17.5.0" + }, + "type": "module" +} diff --git a/uAssets/tools/validate/validate.js b/uAssets/tools/validate/validate.js new file mode 100644 index 0000000..ce448f8 --- /dev/null +++ b/uAssets/tools/validate/validate.js @@ -0,0 +1,321 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2022-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +// jshint node:true, esversion:8, laxbreak:true + +'use strict'; + +/******************************************************************************/ + +import fs from 'fs/promises'; +import https from 'https'; +import path from 'path'; +import process from 'process'; + +import { StaticFilteringParser } from './uBlock/src/js/static-filtering-parser.js'; +import { LineIterator } from './uBlock/src/js/text-utils.js'; + +import config from './config.js'; + +/******************************************************************************/ + +const commandLineArgs = (( ) => { + const args = new Map(); + let name, value; + for ( const arg of process.argv.slice(2) ) { + const pos = arg.indexOf('='); + if ( pos === -1 ) { + name = arg; + value = ''; + } else { + name = arg.slice(0, pos); + value = arg.slice(pos+1).trim(); + } + args.set(name, value); + } + return args; +})(); + +/******************************************************************************/ + +const stdOutput = []; + +const log = (text, silent = false) => { + stdOutput.push(text); + if ( silent === false ) { + console.log(text); + } +}; + +/******************************************************************************/ + +const jsonSetMapReplacer = (k, v) => { + if ( v instanceof Set || v instanceof Map ) { + if ( v.size === 0 ) { return; } + return Array.from(v); + } + return v; +}; + +/******************************************************************************/ + +const writeFile = async (fname, data) => { + const dir = path.dirname(fname); + await fs.mkdir(dir, { recursive: true }); + const promise = fs.writeFile(fname, data); + writeOps.push(promise); + return promise; +}; +const writeOps = []; + +/******************************************************************************/ + +function sleep(ms) { + return new Promise(resolve => { + setTimeout(( ) => { resolve(); }, ms); + }); +} + +/******************************************************************************/ + +// https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/ + +async function validateHostnameWithQuery(url) { + return new Promise((resolve, reject) => { + const options = { + headers: { + accept: 'application/dns-json', + } + }; + https.get(url, options, response => { + const data = []; + response.on('data', chunk => { + data.push(chunk.toString()); + }); + response.on('end', ( ) => { + let result; + try { + result = JSON.parse(data.join('')); + } catch(ex) { + } + resolve(result); + }); + }).on('error', error => { + resolve(); + }); + }); +} + +async function validateHostname(hn) { + await sleep(config.throttle); + for ( const dnsQuery of config.dnsQueries ) { + const url = dnsQuery.replace('${hn}', hn); + const result = await validateHostnameWithQuery(url); + if ( result !== undefined && result.Status !== 2 ) { return result; } + } +} + +/******************************************************************************/ + +function parseHostnameList(parser, s, hostnames) { + let beg = 0; + let slen = s.length; + while ( beg < slen ) { + let end = s.indexOf('|', beg); + if ( end === -1 ) { end = slen; } + const hn = parser.normalizeHostnameValue(s.slice(beg, end)); + beg = end + 1; + if ( hn === undefined ) { continue; } + if ( hn.includes('*') ) { continue; } + hostnames.push(hn); + } + return hostnames; +} + +/******************************************************************************/ + +function processNet(parser) { + const hostnames = []; + if ( parser.patternIsPlainHostname() ) { + hostnames.push(parser.getPattern()); + } else if ( parser.patternIsLeftHostnameAnchored() ) { + const match = /^([^/?]+)/.exec(parser.getPattern()); + if ( + match !== null && + match[1].includes('*') === false && + match[1].startsWith('.') === false && + match[1].endsWith('.') === false + ) { + hostnames.push(match[0]); + } + } + if ( parser.hasOptions() === false ) { return hostnames; } + for ( const { id, val } of parser.netOptions() ) { + if ( id !== parser.OPTTokenDomain ) { continue; } + parseHostnameList(parser, val, hostnames); + } + return hostnames; +} + +/******************************************************************************/ + +function processExt(parser) { + const hostnames = []; + if ( parser.hasOptions() === false ) { return hostnames; } + for ( const { hn } of parser.extOptions() ) { + if ( hn.includes('*') ) { continue; } + hostnames.push(hn); + } + return hostnames; +} + +/******************************************************************************/ + +// https://www.rfc-editor.org/rfc/rfc1035.html + +function checkHostname(hn, result) { + if ( result instanceof Object === false ) { return; } + if ( result.Status === 1 ) { return `${hn} format error`; } + if ( result.Status === 2 ) { return `${hn} dns server failure`; } + if ( result.Status === 3 ) { return `${hn} name error`; } + if ( result.Status === 4 ) { return `${hn} not implemented`; } + if ( result.Status === 5 ) { return `${hn} refused`; } + if ( result.Answer === undefined ) { return; } + for ( const entry of result.Answer ) { + if ( entry.data === undefined ) { continue; } + for ( const re of parkedDomainAuthorities ) { + if ( re.test(entry.data) === false ) { continue; } + return `${hn} parked`; + } + } +} + +const parkedDomainAuthorities = [ + /^traff-\d+\.hugedomains\.com\.?$/, + /^\d+\.parkingcrew\.net\.?$/, + /^ns\d\.centralnic\.net\.?(\s|$)/, + /^ns\d\.pananames\.com\.?(\s|$)/, +]; + +/******************************************************************************/ + +function toProgressString(lineno, hn) { + const parts = []; + if ( lineno > 0 ) { parts.push(`${lineno}`); } + if ( hn ) { parts.push(hn); } + const s = parts.join(' '); + process.stdout.write(`\r${s.padEnd(lastProgressStr.length)}\r`); + lastProgressStr = s; +} + +let lastProgressStr = ''; + +/******************************************************************************/ + +// TODO: resume from partial results + +async function processList(parser, text, lineto, fpath) { + + const lineIter = new LineIterator(text); + const lines = []; + + while ( lineIter.eot() === false ) { + lines.push(lineIter.next()); + } + + if ( lineto === undefined ) { + lineto = lines.length; + } + + for ( let i = lines.length; i > 0; i-- ) { + if ( i > lineto ) { continue; } + toProgressString(i); + + let line = lines[i-1]; + + parser.analyze(line); + + if ( parser.shouldIgnore() ) { continue; } + + let hostnames; + if ( parser.category !== parser.CATStaticNetFilter ) { + hostnames = processExt(parser); + } else if ( parser.patternHasUnicode() === false || parser.toASCII() ) { + hostnames = processNet(parser); + } + const badHostnames = []; + for ( const hn of hostnames ) { + if ( hn.endsWith('.onion') ) { continue; } + if ( /^\d+\.\d+\.\d+\.\d+$/.test(hn) ) { continue; } + let result = validatedHostnames.get(hn); + if ( result === undefined ) { + toProgressString(i, hn); + result = await validateHostname(hn); + validatedHostnames.set(hn, result); + } + const diagnostic = checkHostname(hn, result); + if ( diagnostic === undefined ) { continue; } + badHostnames.push(diagnostic); + } + if ( badHostnames.length !== 0 ) { + toProgressString(0); + const lineno = i; + badHostnames.forEach(v => { + log(`${lineno} ${v}`); + }); + writeFile(fpath, stdOutput.join('\n')); + } + } + toProgressString(0); +} + +const validatedHostnames = new Map(); + +/******************************************************************************/ + +async function main() { + const infile = commandLineArgs.get('in'); + if ( infile === undefined || infile === '' ) { return; } + const outdir = commandLineArgs.get('out'); + if ( outdir === undefined || outdir === '' ) { return; } + + const infileParts = path.parse(infile); + const lineto = commandLineArgs.get('line') !== undefined + ? parseInt(commandLineArgs.get('line'), 10) + : undefined; + + const partialResultPath = `${outdir}/${infileParts.name}.results.partial.txt`; + const parser = new StaticFilteringParser(); + + const text = await fs.readFile(infile, { encoding: 'utf8' }); + await processList(parser, text, lineto, partialResultPath); + + writeFile(`${outdir}/${infileParts.name}.results.txt`, stdOutput.join('\n')); + writeFile(`${outdir}/${infileParts.name}.dns.results.txt`, JSON.stringify(validatedHostnames, jsonSetMapReplacer, 1)); + + fs.rm(partialResultPath); + + await Promise.all(writeOps); +} + +main(); + +/******************************************************************************/ |