Initial commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Кладёт бинарники и данные zapret в resources/zapret/ — оттуда electron-builder
|
||||
* положит их в сборку через extraResources.
|
||||
*
|
||||
* Два источника:
|
||||
* • bol-van/zapret — движки (tpws, winws.exe + WinDivert), версия пинована и
|
||||
* сверяется по sha256sum.txt из релиза;
|
||||
* • Flowseal/zapret-discord-youtube — подобранные сообществом списки доменов и
|
||||
* стратегии winws (переносятся в код генератором `import-flowseal-strategies.mjs`,
|
||||
* здесь просто данные: списки + fake-пакеты). У Flowseal нет sha256sum.txt —
|
||||
* версия фиксируется по sha коммита, на который указывает тег.
|
||||
*
|
||||
* Каталог resources/zapret в git не хранится: запустите `npm run zapret:fetch`
|
||||
* (или просто `npm run build` — он вызывает скрипт сам).
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdir, mkdtemp, copyFile, readFile, writeFile, rm, chmod } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ZAPRET_VERSION = 'v72.13'
|
||||
const WINDIVERT_VERSION = 'v2.2.2'
|
||||
|
||||
/** Тег Flowseal, зафиксированный по sha коммита (проверено: `git/refs/tags/1.10.2`). */
|
||||
const FLOWSEAL_VERSION = '1.10.2'
|
||||
const FLOWSEAL_COMMIT = 'dfd8e613b099676cf2aa7b474ee5923801514dec'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const DEST = join(ROOT, 'resources', 'zapret')
|
||||
|
||||
const RELEASE = `https://github.com/bol-van/zapret/releases/download/${ZAPRET_VERSION}`
|
||||
const ARCHIVE = `zapret-${ZAPRET_VERSION}.tar.gz`
|
||||
/** Корневой каталог внутри архива — с ним же начинаются пути в sha256sum.txt. */
|
||||
const PREFIX = `zapret-${ZAPRET_VERSION}`
|
||||
|
||||
/** Что забираем из архива bol-van/zapret: путь внутри архива → путь внутри resources/zapret. */
|
||||
const FILES = {
|
||||
'binaries/mac64/tpws': 'darwin/tpws',
|
||||
'binaries/windows-x86_64/winws.exe': 'win32/x64/winws.exe',
|
||||
'binaries/windows-x86_64/cygwin1.dll': 'win32/x64/cygwin1.dll',
|
||||
'binaries/windows-x86_64/WinDivert.dll': 'win32/x64/WinDivert.dll',
|
||||
'binaries/windows-x86_64/WinDivert64.sys': 'win32/x64/WinDivert64.sys',
|
||||
'docs/LICENSE.txt': 'LICENSE.zapret.txt'
|
||||
}
|
||||
|
||||
/** Исполняемый бит нужен только там, где мы сами запускаем бинарник. */
|
||||
const EXECUTABLE = new Set(['darwin/tpws'])
|
||||
|
||||
/**
|
||||
* Списки доменов Flowseal → наши короткие имена (используются и в `lists.ts`, и
|
||||
* в сгенерированных стратегиях). `*-user.txt` не берём — у нас все списки и так
|
||||
* редактируемые, отдельного пользовательского слоя не нужно.
|
||||
*/
|
||||
const FLOWSEAL_LISTS = {
|
||||
'lists/list-general.txt': 'lists/general.txt',
|
||||
'lists/list-google.txt': 'lists/google.txt',
|
||||
'lists/list-exclude.txt': 'lists/exclude.txt',
|
||||
'lists/ipset-exclude.txt': 'lists/ipset-exclude.txt',
|
||||
'lists/ipset-all.txt': 'lists/ipset-all.txt'
|
||||
}
|
||||
|
||||
/** Fake-пакеты, реально используемые хотя бы одной стратегией general*.bat. */
|
||||
const FLOWSEAL_FAKES = [
|
||||
'ACTIVE_DISCORD_UDP.bin',
|
||||
'ACTIVE_GAME_UDP.bin',
|
||||
'quic_initial_4pda_to.bin',
|
||||
'quic_initial_www_google_com.bin',
|
||||
'stun.bin',
|
||||
'stun2.bin',
|
||||
'tls_clienthello_4pda_to.bin',
|
||||
'tls_clienthello_max_ru.bin',
|
||||
'tls_clienthello_sochi_park.bin',
|
||||
'tls_clienthello_www_google_com.bin'
|
||||
]
|
||||
|
||||
async function download(url) {
|
||||
const response = await fetch(url, { redirect: 'follow' })
|
||||
if (!response.ok) throw new Error(`${response.status} ${response.statusText} — ${url}`)
|
||||
return Buffer.from(await response.arrayBuffer())
|
||||
}
|
||||
|
||||
function sha256(buffer) {
|
||||
return createHash('sha256').update(buffer).digest('hex')
|
||||
}
|
||||
|
||||
/** sha256sum.txt: строки вида "<hex> zapret-v72.13/binaries/mac64/tpws". */
|
||||
function parseChecksums(text) {
|
||||
const checksums = new Map()
|
||||
for (const line of text.split('\n')) {
|
||||
const match = /^([0-9a-f]{64})\s+(\S+)$/.exec(line.trim())
|
||||
if (match) checksums.set(match[2], match[1])
|
||||
}
|
||||
return checksums
|
||||
}
|
||||
|
||||
async function fetchZapretEngines(work) {
|
||||
console.log(`zapret ${ZAPRET_VERSION}: скачиваю ${ARCHIVE}`)
|
||||
const archivePath = join(work, ARCHIVE)
|
||||
await writeFile(archivePath, await download(`${RELEASE}/${ARCHIVE}`))
|
||||
|
||||
const checksums = parseChecksums((await download(`${RELEASE}/sha256sum.txt`)).toString('utf8'))
|
||||
|
||||
// tar есть на macOS, Linux и в Windows 10 1803+; распаковываем только нужные пути.
|
||||
execFileSync(
|
||||
'tar',
|
||||
['-xzf', archivePath, '-C', work, ...Object.keys(FILES).map((p) => `${PREFIX}/${p}`)],
|
||||
{ stdio: ['ignore', 'ignore', 'inherit'] }
|
||||
)
|
||||
|
||||
for (const [source, target] of Object.entries(FILES)) {
|
||||
const extracted = join(work, PREFIX, source)
|
||||
const content = await readFile(extracted)
|
||||
|
||||
// Лицензии в sha256sum.txt не попадают — там только binaries/**.
|
||||
const expected = checksums.get(`${PREFIX}/${source}`)
|
||||
if (expected) {
|
||||
const actual = sha256(content)
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`не совпала контрольная сумма ${source}\n ожидалось: ${expected}\n получено: ${actual}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const destination = join(DEST, target)
|
||||
await mkdir(dirname(destination), { recursive: true })
|
||||
await copyFile(extracted, destination)
|
||||
if (EXECUTABLE.has(target)) await chmod(destination, 0o755)
|
||||
|
||||
console.log(` ${target}${expected ? ' ✓ sha256' : ''}`)
|
||||
}
|
||||
|
||||
// Лицензия WinDivert лежит в своём репозитории, в архив zapret не входит.
|
||||
const windivertLicense = await download(
|
||||
`https://raw.githubusercontent.com/basil00/WinDivert/${WINDIVERT_VERSION}/LICENSE`
|
||||
)
|
||||
await writeFile(join(DEST, 'LICENSE.WinDivert.txt'), windivertLicense)
|
||||
console.log(' LICENSE.WinDivert.txt')
|
||||
}
|
||||
|
||||
async function fetchFlowsealData(work) {
|
||||
console.log(
|
||||
`Flowseal ${FLOWSEAL_VERSION} (commit ${FLOWSEAL_COMMIT.slice(0, 12)}): скачиваю списки и fake-пакеты`
|
||||
)
|
||||
|
||||
const archivePath = join(work, 'flowseal.tar.gz')
|
||||
// Тарбол по sha коммита, а не по имени тега — тег можно переставить, sha неизменен.
|
||||
await writeFile(
|
||||
archivePath,
|
||||
await download(
|
||||
`https://codeload.github.com/Flowseal/zapret-discord-youtube/tar.gz/${FLOWSEAL_COMMIT}`
|
||||
)
|
||||
)
|
||||
|
||||
const paths = [
|
||||
...Object.keys(FLOWSEAL_LISTS),
|
||||
...FLOWSEAL_FAKES.map((name) => `bin/${name}`),
|
||||
'LICENSE.txt'
|
||||
]
|
||||
|
||||
// GitHub codeload архивирует репозиторий в каталог "<repo>-<sha>/".
|
||||
const prefixOut = execFileSync('tar', ['-tzf', archivePath], { encoding: 'utf8' })
|
||||
.split('\n')[0]
|
||||
.split('/')[0]
|
||||
|
||||
execFileSync('tar', ['-xzf', archivePath, '-C', work, ...paths.map((p) => `${prefixOut}/${p}`)], {
|
||||
stdio: ['ignore', 'ignore', 'inherit']
|
||||
})
|
||||
|
||||
for (const [source, target] of Object.entries(FLOWSEAL_LISTS)) {
|
||||
const destination = join(DEST, target)
|
||||
await mkdir(dirname(destination), { recursive: true })
|
||||
await copyFile(join(work, prefixOut, source), destination)
|
||||
console.log(` ${target}`)
|
||||
}
|
||||
|
||||
const fakesDir = join(DEST, 'fakes')
|
||||
await mkdir(fakesDir, { recursive: true })
|
||||
for (const name of FLOWSEAL_FAKES) {
|
||||
await copyFile(join(work, prefixOut, 'bin', name), join(fakesDir, name))
|
||||
console.log(` fakes/${name}`)
|
||||
}
|
||||
|
||||
await copyFile(join(work, prefixOut, 'LICENSE.txt'), join(DEST, 'LICENSE.flowseal.txt'))
|
||||
console.log(' LICENSE.flowseal.txt')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const work = await mkdtemp(join(tmpdir(), 'zapret-'))
|
||||
|
||||
try {
|
||||
await fetchZapretEngines(work)
|
||||
await fetchFlowsealData(work)
|
||||
console.log(`Готово: ${DEST}`)
|
||||
} finally {
|
||||
await rm(work, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Повторная сборка не должна каждый раз ходить в сеть.
|
||||
if (
|
||||
process.argv.includes('--force') ||
|
||||
!existsSync(join(DEST, 'darwin', 'tpws')) ||
|
||||
!existsSync(join(DEST, 'win32', 'x64', 'winws.exe')) ||
|
||||
!existsSync(join(DEST, 'lists', 'general.txt'))
|
||||
) {
|
||||
await main()
|
||||
} else {
|
||||
console.log(`zapret уже распакован в ${DEST} (перекачать: npm run zapret:fetch -- --force)`)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Разовый (перезапускаемый) генератор: превращает `general*.bat` из
|
||||
* Flowseal/zapret-discord-youtube в `src/main/zapret/strategies.win32.generated.ts`.
|
||||
*
|
||||
* Зачем отдельным скриптом, а не частью `fetch-zapret.mjs`: вывод коммитится в
|
||||
* репозиторий — в рантайме и в сборке .bat-файлов быть не должно, только сгенерированный
|
||||
* TypeScript. Запускать вручную при обновлении `FLOWSEAL_COMMIT` в `fetch-zapret.mjs`,
|
||||
* проверяя результат через `git diff`, а не доверяя вслепую (формат батников может
|
||||
* измениться — тогда парсер должен упасть, а не тихо сгенерировать мусор).
|
||||
*
|
||||
* Формат batch-файлов Flowseal строго регулярный (проверено на всех 22 вариантах):
|
||||
* start "zapret: %~n0" /min "%BIN%winws.exe" --arg1 --arg2 ^
|
||||
* --arg3 ^
|
||||
* ...
|
||||
* --argN
|
||||
* Это одна команда, продолжающаяся через `^` в конце строки — не программа, а
|
||||
* длинная командная строка winws. Переносим только сами аргументы.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdtemp, readFile, rm, readdir, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Держим в синхроне с `fetch-zapret.mjs` — версия данных должна совпадать.
|
||||
const FLOWSEAL_COMMIT = 'dfd8e613b099676cf2aa7b474ee5923801514dec'
|
||||
const FLOWSEAL_VERSION = '1.10.2'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const OUTPUT = join(ROOT, 'src', 'main', 'zapret', 'strategies.win32.generated.ts')
|
||||
|
||||
/** Списки Flowseal → наши короткие имена (совпадает с `FLOWSEAL_LISTS` в fetch-zapret.mjs). */
|
||||
const LIST_FILES = {
|
||||
'list-general.txt': 'general',
|
||||
'list-google.txt': 'google',
|
||||
'list-exclude.txt': 'exclude',
|
||||
'ipset-exclude.txt': 'ipset-exclude',
|
||||
'ipset-all.txt': 'ipset-all'
|
||||
}
|
||||
|
||||
/** Пользовательские дубликаты списков — у нас все списки и так редактируемые, лишние. */
|
||||
const DROP_LIST_SUFFIXES = ['-user.txt']
|
||||
|
||||
/** Игровой фильтр Flowseal по умолчанию выключен: `GameFilterTCP=12`, `GameFilterUDP=12`. */
|
||||
const GAME_FILTER_DISABLED = '12'
|
||||
|
||||
function idFromFilename(filename) {
|
||||
// "general.bat" → "general" / "Стандартная"; "general (ALT2).bat" → "alt2" / "ALT2"
|
||||
const match = /^general(?: \((.+)\))?\.bat$/.exec(filename)
|
||||
if (!match) throw new Error(`Неожиданное имя файла стратегии: ${filename}`)
|
||||
const variant = match[1]
|
||||
if (!variant) return { id: 'general', name: 'Стандартная' }
|
||||
const id = variant.toLowerCase().replace(/\s+/g, '-')
|
||||
return { id, name: variant }
|
||||
}
|
||||
|
||||
/** Разбивает командную строку на токены, учитывая кавычки, без внешней библиотеки. */
|
||||
function tokenize(command) {
|
||||
const tokens = command.match(/(?:[^\s"]|"[^"]*")+/g) ?? []
|
||||
return tokens.map((token) => token.replace(/^"(.*)"$/, '$1'))
|
||||
}
|
||||
|
||||
function resolvePlaceholders(token) {
|
||||
for (const [flowsealName, ourName] of Object.entries(LIST_FILES)) {
|
||||
if (token.includes(`%LISTS%${flowsealName}`)) {
|
||||
return token.replace(`%LISTS%${flowsealName}`, `{LISTS}/${ourName}.txt`)
|
||||
}
|
||||
}
|
||||
const binMatch = /%BIN%([A-Za-z0-9_.]+\.bin)/.exec(token)
|
||||
if (binMatch) return token.replace(binMatch[0], `{FAKES}/${binMatch[1]}`)
|
||||
|
||||
return token
|
||||
.replaceAll('%GameFilterTCP%', GAME_FILTER_DISABLED)
|
||||
.replaceAll('%GameFilterUDP%', GAME_FILTER_DISABLED)
|
||||
}
|
||||
|
||||
function isDroppedUserListArg(token) {
|
||||
return DROP_LIST_SUFFIXES.some((suffix) => token.includes(`%LISTS%`) && token.includes(suffix))
|
||||
}
|
||||
|
||||
function parseBatFile(filename, content) {
|
||||
const marker = 'winws.exe"'
|
||||
const start = content.indexOf(marker)
|
||||
if (start === -1) throw new Error(`${filename}: не нашёл запуск winws.exe`)
|
||||
|
||||
// Дальше до конца файла — одна командная строка, продолжающаяся через "^\n".
|
||||
const rest = content.slice(start + marker.length)
|
||||
const joined = rest.replace(/\^\r?\n/g, ' ')
|
||||
|
||||
const tokens = tokenize(joined)
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
.filter((t) => !isDroppedUserListArg(t))
|
||||
// Кавычки в .bat защищали путь от разбора cmd.exe при запуске через `start`.
|
||||
// Мы передаём argv напрямую в spawn (без шелла) — кавычки внутри значения
|
||||
// остались бы буквальными символами в имени файла, поэтому убираем везде.
|
||||
.map((t) => t.replaceAll('"', ''))
|
||||
.map(resolvePlaceholders)
|
||||
|
||||
if (tokens.length < 5)
|
||||
throw new Error(`${filename}: подозрительно мало аргументов (${tokens.length})`)
|
||||
|
||||
const { id, name } = idFromFilename(filename)
|
||||
return { id, name, args: tokens }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const work = await mkdtemp(join(tmpdir(), 'flowseal-strategies-'))
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`Flowseal ${FLOWSEAL_VERSION} (commit ${FLOWSEAL_COMMIT.slice(0, 12)}): скачиваю batch-файлы`
|
||||
)
|
||||
const archivePath = join(work, 'flowseal.tar.gz')
|
||||
const response = await fetch(
|
||||
`https://codeload.github.com/Flowseal/zapret-discord-youtube/tar.gz/${FLOWSEAL_COMMIT}`
|
||||
)
|
||||
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`)
|
||||
await writeFile(archivePath, Buffer.from(await response.arrayBuffer()))
|
||||
|
||||
const prefix = execFileSync('tar', ['-tzf', archivePath], { encoding: 'utf8' })
|
||||
.split('\n')[0]
|
||||
.split('/')[0]
|
||||
execFileSync('tar', ['-xzf', archivePath, '-C', work], {
|
||||
stdio: ['ignore', 'ignore', 'inherit']
|
||||
})
|
||||
|
||||
const srcDir = join(work, prefix)
|
||||
const batFiles = (await readdir(srcDir)).filter(
|
||||
(name) => name.startsWith('general') && name.endsWith('.bat')
|
||||
)
|
||||
batFiles.sort()
|
||||
|
||||
if (batFiles.length === 0)
|
||||
throw new Error('Не нашлось ни одного general*.bat — формат репозитория изменился')
|
||||
|
||||
const strategies = []
|
||||
for (const filename of batFiles) {
|
||||
const content = await readFile(join(srcDir, filename), 'utf8')
|
||||
strategies.push(parseBatFile(filename, content))
|
||||
console.log(
|
||||
` ${filename} → ${strategies.at(-1).id} (${strategies.at(-1).args.length} аргументов)`
|
||||
)
|
||||
}
|
||||
|
||||
const ids = new Set(strategies.map((s) => s.id))
|
||||
if (ids.size !== strategies.length)
|
||||
throw new Error('Столкновение id — два файла дали одинаковый идентификатор')
|
||||
|
||||
const body = strategies
|
||||
.map(
|
||||
(s) =>
|
||||
` {\n id: ${JSON.stringify(s.id)},\n name: ${JSON.stringify(s.name)},\n args: [\n${s.args
|
||||
.map((a) => ` ${JSON.stringify(a)}`)
|
||||
.join(',\n')}\n ]\n }`
|
||||
)
|
||||
.join(',\n')
|
||||
|
||||
const output = `/**
|
||||
* СГЕНЕРИРОВАНО \`scripts/import-flowseal-strategies.mjs\` из
|
||||
* Flowseal/zapret-discord-youtube ${FLOWSEAL_VERSION} (commit ${FLOWSEAL_COMMIT}).
|
||||
* Не редактировать руками — перезапустить генератор и закоммитить результат.
|
||||
*
|
||||
* \`{LISTS}\` и \`{FAKES}\` — плейсхолдеры путей, резолвятся в \`strategies.ts\`
|
||||
* через \`resolvePlaceholders()\` в фактические пути userData/resources.
|
||||
*/
|
||||
|
||||
export interface GeneratedWin32Strategy {
|
||||
id: string
|
||||
name: string
|
||||
args: string[]
|
||||
}
|
||||
|
||||
export const WIN32_STRATEGIES: readonly GeneratedWin32Strategy[] = [
|
||||
${body}
|
||||
]
|
||||
`
|
||||
|
||||
await writeFile(OUTPUT, output)
|
||||
console.log(`Готово: ${OUTPUT} (${strategies.length} стратегий)`)
|
||||
} finally {
|
||||
await rm(work, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
Reference in New Issue
Block a user