mirror of
https://github.com/andrey271192/Domain_web.git
synced 2026-09-20 14:41:58 +00:00
fix(site): export preview/download and restore GeoExport deploy
Restore site/ mirror, patch export fetch for empty API responses and Keenetic format, add Telegram bot card, and switch install.sh back to geoexport-site systemd + nginx on port 4173. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
131
install.sh
131
install.sh
@@ -1,46 +1,33 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Domain Scanner — one-command install (Ubuntu/Debian VPS)
|
# GeoExport site — one-command install (Ubuntu/Debian VPS)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_URL="${DOMAIN_SCANNER_REPO_URL:-https://github.com/andrey271192/Domain_web.git}"
|
REPO_URL="${GEOEXPORT_REPO_URL:-https://github.com/andrey271192/Domain_web.git}"
|
||||||
BRANCH="${DOMAIN_SCANNER_BRANCH:-main}"
|
BRANCH="${GEOEXPORT_BRANCH:-main}"
|
||||||
INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain_web}"
|
INSTALL_DIR="${GEOEXPORT_INSTALL_DIR:-/opt/domain_web}"
|
||||||
SERVICE_NAME="domain-scanner"
|
SITE_DIR="${INSTALL_DIR}/site"
|
||||||
NGINX_SITE="domain-scanner"
|
SERVICE_NAME="geoexport-site"
|
||||||
APP_PORT="${DOMAIN_SCANNER_PORT:-3000}"
|
NGINX_SITE="geoexport-site"
|
||||||
|
NODE_PORT="${GEOEXPORT_PORT:-4173}"
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
log() { echo "[domain-scanner-install] $*"; }
|
log() { echo "[geoexport-install] $*"; }
|
||||||
need_root() {
|
need_root() {
|
||||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||||
echo "Run as root: sudo bash install.sh" >&2
|
echo "Запустите от root: sudo bash install.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_packages() {
|
install_packages() {
|
||||||
log "Installing nginx, git, curl, docker..."
|
log "Установка пакетов (nginx, git, curl, nodejs)..."
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
apt-get install -y -qq nginx git curl ca-certificates docker.io docker-compose >/dev/null 2>&1 || \
|
apt-get install -y -qq nginx git curl ca-certificates nodejs npm >/dev/null
|
||||||
apt-get install -y -qq nginx git curl ca-certificates docker.io >/dev/null
|
|
||||||
if ! command -v docker-compose >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
|
||||||
ln -sf "$(command -v docker)" /usr/local/bin/docker-compose 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
systemctl enable docker
|
|
||||||
systemctl start docker
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_cmd() {
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
echo "docker compose"
|
|
||||||
else
|
|
||||||
echo "docker-compose"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone_or_update() {
|
clone_or_update() {
|
||||||
log "Cloning/updating ${INSTALL_DIR}..."
|
log "Клонирование/обновление репозитория в ${INSTALL_DIR}..."
|
||||||
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
||||||
git -C "${INSTALL_DIR}" fetch origin "${BRANCH}"
|
git -C "${INSTALL_DIR}" fetch origin "${BRANCH}"
|
||||||
git -C "${INSTALL_DIR}" checkout "${BRANCH}"
|
git -C "${INSTALL_DIR}" checkout "${BRANCH}"
|
||||||
@@ -49,59 +36,64 @@ clone_or_update() {
|
|||||||
rm -rf "${INSTALL_DIR}"
|
rm -rf "${INSTALL_DIR}"
|
||||||
git clone --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
|
git clone --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
|
||||||
fi
|
fi
|
||||||
}
|
if [[ ! -f "${SITE_DIR}/server.mjs" ]]; then
|
||||||
|
echo "Ошибка: не найден ${SITE_DIR}/server.mjs" >&2
|
||||||
setup_env() {
|
exit 1
|
||||||
log "Writing .env..."
|
|
||||||
if [[ ! -f "${INSTALL_DIR}/.env" ]]; then
|
|
||||||
SECRET=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64)
|
|
||||||
cat >"${INSTALL_DIR}/.env" <<EOF
|
|
||||||
NEXTAUTH_SECRET=${SECRET}
|
|
||||||
NEXTAUTH_URL=http://$(hostname -I | awk '{print $1}')
|
|
||||||
NEXT_PUBLIC_APP_URL=http://$(hostname -I | awk '{print $1}')
|
|
||||||
PORT=${APP_PORT}
|
|
||||||
EOF
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_docker() {
|
apply_patches() {
|
||||||
log "Building and starting Docker stack..."
|
log "Применение патчей фронтенда (export preview/download)..."
|
||||||
cd "${INSTALL_DIR}"
|
node "${SITE_DIR}/patch-bundle.mjs"
|
||||||
COMPOSE=$(compose_cmd)
|
}
|
||||||
$COMPOSE build --quiet
|
|
||||||
$COMPOSE up -d
|
install_systemd() {
|
||||||
sleep 12
|
log "Настройка systemd (${SERVICE_NAME})..."
|
||||||
$COMPOSE exec -T web npx prisma db push 2>/dev/null || true
|
cat >"/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=GeoExport static mirror (Node)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=${SITE_DIR}
|
||||||
|
Environment=PORT=${NODE_PORT}
|
||||||
|
ExecStart=/usr/bin/node ${SITE_DIR}/server.mjs
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "${SERVICE_NAME}.service"
|
||||||
|
systemctl restart "${SERVICE_NAME}.service"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_nginx() {
|
install_nginx() {
|
||||||
log "Configuring nginx..."
|
log "Настройка nginx (${NGINX_SITE})..."
|
||||||
cat >"/etc/nginx/sites-available/${NGINX_SITE}" <<EOF
|
cat >"/etc/nginx/sites-available/${NGINX_SITE}" <<EOF
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
listen [::]:80 default_server;
|
listen [::]:80 default_server;
|
||||||
server_name _;
|
server_name _;
|
||||||
client_max_body_size 32m;
|
|
||||||
|
client_max_body_size 64m;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:${APP_PORT};
|
proxy_pass http://127.0.0.1:${NODE_PORT};
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host \$host;
|
proxy_set_header Host \$host;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
rm -f /etc/nginx/sites-enabled/geoexport-site 2>/dev/null || true
|
rm -f /etc/nginx/sites-enabled/domain-scanner 2>/dev/null || true
|
||||||
rm -f /etc/nginx/sites-available/geoexport-site 2>/dev/null || true
|
rm -f /etc/nginx/sites-available/domain-scanner 2>/dev/null || true
|
||||||
systemctl stop geoexport-site.service 2>/dev/null || true
|
|
||||||
systemctl disable geoexport-site.service 2>/dev/null || true
|
|
||||||
rm -f /etc/systemd/system/geoexport-site.service 2>/dev/null || true
|
|
||||||
systemctl daemon-reload
|
|
||||||
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
|
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
|
||||||
ln -sf "/etc/nginx/sites-available/${NGINX_SITE}" "/etc/nginx/sites-enabled/${NGINX_SITE}"
|
ln -sf "/etc/nginx/sites-available/${NGINX_SITE}" "/etc/nginx/sites-enabled/${NGINX_SITE}"
|
||||||
nginx -t
|
nginx -t
|
||||||
@@ -110,24 +102,27 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
verify() {
|
verify() {
|
||||||
log "Verifying..."
|
log "Проверка..."
|
||||||
sleep 3
|
sleep 2
|
||||||
if ! curl -fsS -o /dev/null "http://127.0.0.1:${APP_PORT}/"; then
|
if ! systemctl is-active --quiet "${SERVICE_NAME}.service"; then
|
||||||
echo "App not responding on port ${APP_PORT}" >&2
|
systemctl status "${SERVICE_NAME}.service" --no-pager || true
|
||||||
$(compose_cmd) -f "${INSTALL_DIR}/docker-compose.yml" logs --tail=50 web || true
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
TITLE=$(curl -fsS "http://127.0.0.1/" | head -c 2000 | grep -oi 'Domain Scanner' | head -1 || true)
|
if ! curl -fsS -o /dev/null "http://127.0.0.1:${NODE_PORT}/"; then
|
||||||
if [[ -z "${TITLE}" ]]; then
|
echo "Node-сервер не отвечает на порту ${NODE_PORT}" >&2
|
||||||
log "Warning: page title may not include Domain Scanner yet"
|
exit 1
|
||||||
fi
|
fi
|
||||||
log "Done. Open http://$(hostname -I | awk '{print $1}')/"
|
if ! curl -fsS -o /dev/null "http://127.0.0.1/"; then
|
||||||
|
echo "nginx не отдаёт сайт на :80" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Готово. Сайт доступен по http://$(hostname -I | awk '{print $1}')/"
|
||||||
}
|
}
|
||||||
|
|
||||||
need_root
|
need_root
|
||||||
install_packages
|
install_packages
|
||||||
clone_or_update
|
clone_or_update
|
||||||
setup_env
|
apply_patches
|
||||||
deploy_docker
|
install_systemd
|
||||||
install_nginx
|
install_nginx
|
||||||
verify
|
verify
|
||||||
|
|||||||
2
site/.gitignore
vendored
Normal file
2
site/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
47
site/README.md
Normal file
47
site/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# GeoExport — локальная копия
|
||||||
|
|
||||||
|
Зеркало интерфейса [geoexport.org](https://geoexport.org/): экспорт списков geoip/geosite для VPN-клиентов (Xray, 3x-ui, V2Ray).
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/geoexport-clone
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Откройте в браузере: **http://127.0.0.1:4173**
|
||||||
|
|
||||||
|
Другой порт: `PORT=8080 npm start`
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Путь | Назначение |
|
||||||
|
|------|------------|
|
||||||
|
| `index.html` | Точка входа SPA |
|
||||||
|
| `assets/` | Собранный фронтенд (React + Tailwind) с оригинала |
|
||||||
|
| `data/*.json` | Кэш API: пресеты, источники, дата обновления |
|
||||||
|
| `server.mjs` | Локальный сервер: статика + JSON + прокси динамических API |
|
||||||
|
|
||||||
|
## Обновление данных
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL https://geoexport.org/api/presets -o data/presets.json
|
||||||
|
curl -sL https://geoexport.org/api/sources -o data/sources.json
|
||||||
|
curl -sL https://geoexport.org/api/last-update -o data/last-update.json
|
||||||
|
curl -sL https://geoexport.org/api/routing/presets -o data/routing-presets.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Патч экспорта
|
||||||
|
|
||||||
|
Перед запуском применяется `patch-bundle.mjs` (превью/скачивание, Keenetic, пустые ответы API).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # prestart → patch автоматически
|
||||||
|
```
|
||||||
|
|
||||||
|
Карточка про Telegram-бота `@domain_searchPro_bot` — `assets/telegram-bot.js`.
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Экспорт, поиск, DNS lookup и генерация routing проксируются на живой `geoexport.org` (нужен интернет).
|
||||||
|
- Это копия UI и кэшированных справочников, не полный автономный бэкенд с базами `.dat`.
|
||||||
69
site/assets/index-VQj200iM.js
Normal file
69
site/assets/index-VQj200iM.js
Normal file
File diff suppressed because one or more lines are too long
1
site/assets/index-uS40y9LP.css
Normal file
1
site/assets/index-uS40y9LP.css
Normal file
File diff suppressed because one or more lines are too long
28
site/assets/telegram-bot.js
Normal file
28
site/assets/telegram-bot.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
(function () {
|
||||||
|
const CARD =
|
||||||
|
'<div class="rounded-2xl border border-slate-200 dark:border-slate-800 bg-white/80 dark:bg-slate-900/40 shadow-sm p-4 text-sm text-slate-700 dark:text-slate-300">' +
|
||||||
|
'<div class="font-medium text-slate-900 dark:text-slate-100 mb-1">Telegram-бот для поиска доменов</div>' +
|
||||||
|
'<p class="text-xs leading-relaxed mb-2">' +
|
||||||
|
"Если удобнее с телефона или нужен быстрый поиск без браузера — попробуйте " +
|
||||||
|
'<a href="https://t.me/domain_searchPro_bot" target="_blank" rel="noopener noreferrer" class="font-mono text-blue-600 dark:text-blue-400 hover:underline">@domain_searchPro_bot</a>. ' +
|
||||||
|
"Тот же смысл: найти домен, посмотреть IP и выгрузить списки, только в Telegram." +
|
||||||
|
"</p>" +
|
||||||
|
'<a href="https://t.me/domain_searchPro_bot" target="_blank" rel="noopener noreferrer" ' +
|
||||||
|
'class="inline-flex items-center gap-1 text-xs px-3 py-1.5 rounded-xl bg-slate-900 text-white hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white">' +
|
||||||
|
"Открыть бота →</a></div>";
|
||||||
|
|
||||||
|
function inject() {
|
||||||
|
if (document.querySelector("[data-telegram-bot-card]")) return;
|
||||||
|
const host = document.querySelector(".max-w-3xl.mx-auto");
|
||||||
|
if (!host) return;
|
||||||
|
const wrap = document.createElement("div");
|
||||||
|
wrap.setAttribute("data-telegram-bot-card", "");
|
||||||
|
wrap.className = "mt-4 mb-2";
|
||||||
|
wrap.innerHTML = CARD;
|
||||||
|
host.appendChild(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const obs = new MutationObserver(() => inject());
|
||||||
|
obs.observe(document.body, { childList: true, subtree: true });
|
||||||
|
inject();
|
||||||
|
})();
|
||||||
1
site/data/last-update.json
Normal file
1
site/data/last-update.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"last_update":"2026-05-24","updating":false,"sources":[{"source_slug":"runetfreedom","source_name":"RuNet Freedom","last_update":"2026-05-24","compatible":"Xray, 3x-ui, V2Ray"},{"source_slug":"loyalsoldier","source_name":"Loyalsoldier","last_update":"2026-05-24","compatible":"Xray, 3x-ui, V2Ray, Mihomo"},{"source_slug":"daniellavrushin","source_name":"DanielLavrushin (b4geoip)","last_update":"2026-05-24","compatible":"Xray, 3x-ui"},{"source_slug":"v2fly","source_name":"v2fly (официальный)","last_update":"2026-05-24","compatible":"Xray, 3x-ui, V2Ray, Sing-box"}]}
|
||||||
1
site/data/presets.json
Normal file
1
site/data/presets.json
Normal file
File diff suppressed because one or more lines are too long
1
site/data/routing-presets.json
Normal file
1
site/data/routing-presets.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"presets":{"direct_sites":[{"id":"ru_sites","label":"Российские сайты","description":"Домены .ru, .рф и российские сервисы — идут напрямую без VPN","value":"geosite:category-ru","checked":true},{"id":"ru_whitelist","label":"Белый список РФ","description":"Дополнительный список российских сервисов и госсайтов","value":"geosite:whitelist","checked":true},{"id":"private","label":"Локальная сеть","description":"192.168.x.x, 10.x.x.x и другие приватные адреса — всегда напрямую","value":"geosite:private","checked":true},{"id":"apple","label":"Apple","description":"iCloud, App Store, Apple Maps — для корректной работы устройства","value":"geosite:apple","checked":true},{"id":"microsoft","label":"Microsoft","description":"Windows Update, Office, Teams — экономия трафика VPN","value":"geosite:microsoft","checked":false},{"id":"steam","label":"Steam","description":"Игры и обновления Steam — напрямую для скорости","value":"geosite:steam","checked":false},{"id":"google_cn","label":"Google (доступные сервисы)","description":"Сервисы Google доступные в РФ без VPN","value":"geosite:google@cn","checked":false}],"direct_ips":[{"id":"geoip_ru","label":"IP-адреса России","description":"Все российские IP-диапазоны — напрямую","value":"geoip:ru","checked":true},{"id":"geoip_private","label":"Приватные IP","description":"10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16","value":"geoip:private","checked":true}],"proxy_sites":[{"id":"youtube","label":"YouTube","description":"YouTube заблокирован в РФ — направляем через VPN","value":"geosite:youtube","checked":true},{"id":"telegram","label":"Telegram","description":"Серверы Telegram — через VPN для надёжности","value":"geosite:telegram","checked":true},{"id":"meta","label":"Meta (Instagram, Facebook)","description":"Instagram и Facebook заблокированы в РФ","value":"geosite:facebook","checked":true},{"id":"twitter","label":"Twitter / X","description":"Twitter заблокирован в РФ","value":"geosite:twitter","checked":true},{"id":"netflix","label":"Netflix","description":"Netflix недоступен в РФ","value":"geosite:netflix","checked":false},{"id":"spotify","label":"Spotify","description":"Spotify недоступен в РФ","value":"geosite:spotify","checked":false},{"id":"github","label":"GitHub","description":"GitHub периодически блокируется в РФ","value":"geosite:github","checked":false},{"id":"geolocation_notcn","label":"Все заблокированные (универсальный)","description":"Весь иностранный трафик не из РФ/CN — через VPN. Осторожно: большой список","value":"geosite:geolocation-!cn","checked":false}],"proxy_ips":[{"id":"geoip_telegram","label":"IP Telegram","description":"IP-диапазоны серверов Telegram","value":"geoip:telegram","checked":true}],"block_sites":[{"id":"ads","label":"Реклама","description":"Рекламные домены — блокируем полностью","value":"geosite:category-ads-all","checked":true}],"block_ips":[]},"geo_sources":{"loyalsoldier":{"label":"Loyalsoldier (рекомендуется)","geoip":"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat","geosite":"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"},"runetfreedom":{"label":"RuNet Freedom (РКН + Россия)","geoip":"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat","geosite":"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat"},"daniellavrushin":{"label":"DanielLavrushin (расширенная)","geoip":"https://github.com/DanielLavrushin/b4geoip/releases/latest/download/geoip.dat","geosite":"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"},"v2fly":{"label":"v2fly (оригинальная)","geoip":"https://github.com/v2fly/geoip/releases/latest/download/geoip.dat","geosite":"https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"}},"route_orders":[{"value":"block-direct-proxy","label":"Блок → Прямой → VPN (рекомендуется)"},{"value":"block-proxy-direct","label":"Блок → VPN → Прямой"},{"value":"proxy-direct-block","label":"VPN → Прямой → Блок"}],"dns_types":[{"value":"DoH","label":"DoH (DNS over HTTPS, рекомендуется)"},{"value":"DoU","label":"DoU (DNS over UDP, быстрее)"}]}
|
||||||
1
site/data/sources.json
Normal file
1
site/data/sources.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"slug":"runetfreedom","name":"RuNet Freedom","description":"Российские правила блокировок, оптимизировано для обхода РКН","compatible":"Xray, 3x-ui, V2Ray","geoip_url":"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat","geosite_url":"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat"},{"slug":"loyalsoldier","name":"Loyalsoldier","description":"Популярный набор правил, стандарт для большинства клиентов","compatible":"Xray, 3x-ui, V2Ray, Mihomo","geoip_url":"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat","geosite_url":"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"},{"slug":"daniellavrushin","name":"DanielLavrushin (b4geoip)","description":"Расширенная база с дополнительными категориями (Akamai, CDN и др.)","compatible":"Xray, 3x-ui","geoip_url":"https://github.com/DanielLavrushin/b4geoip/releases/latest/download/geoip.dat","geosite_url":null},{"slug":"v2fly","name":"v2fly (официальный)","description":"Официальные базы проекта v2fly/domain-list-community","compatible":"Xray, 3x-ui, V2Ray, Sing-box","geoip_url":"https://github.com/v2fly/geoip/releases/latest/download/geoip.dat","geosite_url":"https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"}]
|
||||||
14
site/index.html
Normal file
14
site/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GeoExport</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-VQj200iM.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-uS40y9LP.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script defer src="/assets/telegram-bot.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
site/package.json
Normal file
15
site/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "geoexport-clone",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Local mirror of https://geoexport.org/",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.mjs",
|
||||||
|
"dev": "node server.mjs",
|
||||||
|
"patch": "node patch-bundle.mjs",
|
||||||
|
"prestart": "node patch-bundle.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
site/patch-bundle.mjs
Normal file
53
site/patch-bundle.mjs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Patches GeoExport SPA bundle: export preview/download fixes + shared fetch helper.
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const bundlePath = path.join(__dirname, "assets", "index-VQj200iM.js");
|
||||||
|
|
||||||
|
const HELPER = `async function Ge(cats,type,fmt,source,label){const w=new URLSearchParams;cats.forEach(z=>w.append("category",z)),w.set("type",type);const af=fmt==="single-line"||fmt==="keenetic-ip"?"plain-lines":fmt;w.set("format",af),source&&w.set("source",source);const r=await fetch(\`/api/export?\${w}\`);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText||"Ошибка экспорта")}let t=await r.text();if(!t.trim())return"Нет данных для выбранных категорий, типа или источника. Попробуйте «Все» в фильтре источников или тип «Домены» / «IPv4».";if(fmt==="single-line")t=t.trim().split(/\\r?\\n/).join(",");if(fmt==="keenetic-ip"){const lines=t.trim().split(/\\r?\\n/).filter(x=>/^\\d/.test(x)&&x.includes("/")&&!x.includes(":"));t=lines.length?lines.map(line=>{const[ip,bits]=line.split("/"),n=parseInt(bits??"32",10),mask=n===32?"255.255.255.255":Array.from({length:4},(_,i)=>(65280>>Math.min(8,Math.max(0,n-i*8)))&255).join(".");return\`route ADD \${ip} MASK \${mask} 0.0.0.0 & rem \${label||cats[0]||"export"}\`}).join("\\n"):"Нет IPv4-подсетей для Keenetic. Выберите тип «IPv4» или «Домены + IPv4»."}return t}`;
|
||||||
|
|
||||||
|
const REPLACEMENTS = [
|
||||||
|
{
|
||||||
|
from: "function bm({svc:e,sources:t,sourceFilter:n})",
|
||||||
|
to: `${HELPER}function bm({svc:e,sources:t,sourceFilter:n})`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'p=async()=>{if(f){a(!0);try{const w=new URLSearchParams;C.forEach(z=>w.append("category",z.full_name)),w.set("type",m),w.set("format",c==="single-line"?"plain-lines":c),n!=="all"&&w.set("source",n);let P=await(await fetch(`/api/export?${w}`)).text();c==="single-line"&&(P=P.trim().split(/\\r?\\n/).join(",")),o(P)}finally{a(!1)}}}',
|
||||||
|
to: 'p=async()=>{if(f){a(!0);try{o(await Ge(C.map(z=>z.full_name),m,c,n!=="all"?n:"",e.name))}catch(w){o(w.message||"Ошибка экспорта")}finally{a(!1)}}}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'function qm(){const[e]=hi(),t=e.getAll("category"),n=e.get("source"),[r,l]=k.useState("domains,ip4,ip6"),[s,o]=k.useState("plain-lines"),',
|
||||||
|
to: 'function qm(){const[e]=hi(),t=e.getAll("category"),n=e.get("source"),[r,l]=k.useState(e.get("type")||"domains,ip4,ip6"),[s,o]=k.useState(e.get("format")||"plain-lines"),',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'const p=k.useCallback(async()=>{if(t.length){y(!0),C(null);try{const S=new URLSearchParams;t.forEach(T=>S.append("category",T)),S.set("type",r),S.set("format",s==="single-line"?"plain-lines":s),n&&S.set("source",n);const P=await fetch(`/api/export?${S}`);if(!P.ok){const T=await P.json().catch(()=>({}));C(T.detail??P.statusText),a(""),h(0);return}let z=await P.text();s==="single-line"&&(z=z.trim().split(/\\r?\\n/).join(",")),a(z),h(z.trim()?z.trim().split(/[,\\r?\\n]/).filter(Boolean).length:0)}finally{y(!1)}}},[t.join(","),r,s,n]);',
|
||||||
|
to: 'const p=k.useCallback(async()=>{if(!t.length){a(""),C(null),h(0);return}y(!0),C(null);try{const z=await Ge(t,r,s,n||"",t[0]);a(z),h(z.trim()&&!z.startsWith("Нет ")?z.trim().split(/[,\\r?\\n]/).filter(Boolean).length:0)}catch(S){C(S.message||"Ошибка экспорта"),a("") ,h(0)}finally{y(!1)}},[t.join(","),r,s,n]);',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'value:g?"Загрузка…":j?"":i,readOnly:!0,className:"w-full h-80',
|
||||||
|
to: 'value:g?"Загрузка…":j||i||(!t.length?"Выберите категории на главной и нажмите «Открыть экспорт →», либо добавьте ?category=geoip:telegram в адрес.":""),readOnly:!0,className:"w-full h-80',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let src = fs.readFileSync(bundlePath, "utf8");
|
||||||
|
|
||||||
|
if (src.includes("async function Ge(")) {
|
||||||
|
console.log("Already patched:", bundlePath);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { from, to } of REPLACEMENTS) {
|
||||||
|
if (!src.includes(from)) {
|
||||||
|
console.error("Patch anchor not found:", from.slice(0, 80));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
src = src.replace(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(bundlePath, src);
|
||||||
|
console.log("Patched", bundlePath);
|
||||||
146
site/server.mjs
Normal file
146
site/server.mjs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Local GeoExport mirror: static assets + cached JSON + proxy for dynamic API.
|
||||||
|
*/
|
||||||
|
import http from "node:http";
|
||||||
|
import https from "node:https";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const PORT = Number(process.env.PORT) || 4173;
|
||||||
|
const UPSTREAM = "geoexport.org";
|
||||||
|
|
||||||
|
const LOCAL_API = new Set([
|
||||||
|
"/api/presets",
|
||||||
|
"/api/sources",
|
||||||
|
"/api/last-update",
|
||||||
|
"/api/routing/presets",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DATA_MAP = {
|
||||||
|
"/api/presets": "presets.json",
|
||||||
|
"/api/sources": "sources.json",
|
||||||
|
"/api/last-update": "last-update.json",
|
||||||
|
"/api/routing/presets": "routing-presets.json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".png": "image/png",
|
||||||
|
};
|
||||||
|
|
||||||
|
function send(res, status, body, headers = {}) {
|
||||||
|
res.writeHead(status, headers);
|
||||||
|
res.end(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveFile(res, filePath) {
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const type = MIME[ext] || "application/octet-stream";
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
send(res, 404, "Not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(res, 200, data, { "Content-Type": type });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveLocalApi(res, pathname) {
|
||||||
|
const file = DATA_MAP[pathname];
|
||||||
|
if (!file) {
|
||||||
|
send(res, 404, JSON.stringify({ error: "unknown local api" }), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const full = path.join(__dirname, "data", file);
|
||||||
|
fs.readFile(full, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
send(res, 500, JSON.stringify({ error: err.message }), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(res, 200, data, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyToUpstream(req, res) {
|
||||||
|
const options = {
|
||||||
|
hostname: UPSTREAM,
|
||||||
|
port: 443,
|
||||||
|
path: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: {
|
||||||
|
...req.headers,
|
||||||
|
host: UPSTREAM,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = https.request(options, (proxyRes) => {
|
||||||
|
const headers = { ...proxyRes.headers };
|
||||||
|
delete headers["content-security-policy"];
|
||||||
|
res.writeHead(proxyRes.statusCode || 502, headers);
|
||||||
|
proxyRes.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.on("error", (err) => {
|
||||||
|
send(res, 502, JSON.stringify({ error: `Upstream error: ${err.message}` }), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.pipe(proxyReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(req, res) {
|
||||||
|
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
||||||
|
let pathname = decodeURIComponent(url.pathname);
|
||||||
|
|
||||||
|
if (pathname.startsWith("/api/")) {
|
||||||
|
if (LOCAL_API.has(pathname)) {
|
||||||
|
serveLocalApi(res, pathname);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proxyToUpstream(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === "/") pathname = "/index.html";
|
||||||
|
|
||||||
|
const safe = path.normalize(pathname).replace(/^(\.\.[/\\])+/, "");
|
||||||
|
const filePath = path.join(__dirname, safe);
|
||||||
|
|
||||||
|
if (!filePath.startsWith(__dirname)) {
|
||||||
|
send(res, 403, "Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.stat(filePath, (err, stat) => {
|
||||||
|
if (err || !stat.isFile()) {
|
||||||
|
if (!path.extname(pathname)) {
|
||||||
|
serveFile(res, path.join(__dirname, "index.html"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(res, 404, "Not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serveFile(res, filePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
http.createServer(handler).listen(PORT, () => {
|
||||||
|
console.log(`GeoExport clone: http://127.0.0.1:${PORT}`);
|
||||||
|
console.log(`Static + local JSON; export/search/lookup proxied to ${UPSTREAM}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user