feat: GeoExport mirror with VPS install/uninstall

Add static site copy, Node server, and one-line deploy scripts
for nginx + systemd on Ubuntu VPS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-24 21:25:15 +03:00
parent cd0ee69f4d
commit 08e6f7a5a9
15 changed files with 539 additions and 1 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.DS_Store
*.log

View File

@@ -1 +1,84 @@
# Domain_web
# Domain_web — зеркало GeoExport
Статическая копия интерфейса [geoexport.org](https://geoexport.org/) для развёртывания на VPS (nginx + Node.js).
## Быстрая установка (одна команда)
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/Domain_web/main/install.sh | sudo bash
```
**URL скрипта установки:**
https://raw.githubusercontent.com/andrey271192/Domain_web/main/install.sh
## Удаление (одна команда)
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/Domain_web/main/uninstall.sh | sudo bash
```
**URL скрипта удаления:**
https://raw.githubusercontent.com/andrey271192/Domain_web/main/uninstall.sh
## Что устанавливается
| Компонент | Путь / имя |
|-----------|------------|
| Клон репозитория | `/opt/domain_web` |
| Файлы сайта | `/opt/domain_web/site/` |
| Node-сервер (API + статика) | systemd `geoexport-site`, порт `127.0.0.1:4173` |
| Публичный доступ | nginx на порту **80** → прокси на Node |
Скрипт установки:
- ставит `nginx`, `git`, `curl`, `nodejs` (если нет);
- клонирует или обновляет этот репозиторий;
- поднимает `geoexport-site.service`;
- настраивает nginx как reverse proxy.
Скрипт удаления:
- останавливает и удаляет unit `geoexport-site`;
- убирает конфиг nginx `geoexport-site`;
- удаляет каталог `/opt/domain_web`;
- **не** удаляет nginx, node и остальные сервисы сервера.
## Ручная установка
```bash
git clone https://github.com/andrey271192/Domain_web.git /opt/domain_web
cd /opt/domain_web
sudo bash install.sh
```
## Локальный запуск (без VPS)
```bash
cd site
npm start
# http://127.0.0.1:4173
```
## Обновление данных на сервере
```bash
cd /opt/domain_web/site
curl -fsSL https://geoexport.org/api/presets -o data/presets.json
curl -fsSL https://geoexport.org/api/sources -o data/sources.json
curl -fsSL https://geoexport.org/api/last-update -o data/last-update.json
curl -fsSL https://geoexport.org/api/routing/presets -o data/routing-presets.json
systemctl restart geoexport-site
```
## Ограничения
- Экспорт, поиск и часть API проксируются на живой `geoexport.org` (нужен интернет на VPS).
- Это зеркало UI и кэшированных справочников, не полный автономный бэкенд.
## Структура репозитория
```
install.sh — установка на VPS
uninstall.sh — удаление с VPS
site/ — index.html, assets/, data/, server.mjs
```

120
install.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# GeoExport site — one-command install (Ubuntu/Debian VPS)
set -euo pipefail
REPO_URL="${GEOEXPORT_REPO_URL:-https://github.com/andrey271192/Domain_web.git}"
BRANCH="${GEOEXPORT_BRANCH:-main}"
INSTALL_DIR="${GEOEXPORT_INSTALL_DIR:-/opt/domain_web}"
SITE_DIR="${INSTALL_DIR}/site"
SERVICE_NAME="geoexport-site"
NGINX_SITE="geoexport-site"
NODE_PORT="${GEOEXPORT_PORT:-4173}"
export DEBIAN_FRONTEND=noninteractive
log() { echo "[geoexport-install] $*"; }
need_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "Запустите от root: sudo bash install.sh" >&2
exit 1
fi
}
install_packages() {
log "Установка пакетов (nginx, git, curl, nodejs)..."
apt-get update -qq
apt-get install -y -qq nginx git curl ca-certificates nodejs npm >/dev/null
}
clone_or_update() {
log "Клонирование/обновление репозитория в ${INSTALL_DIR}..."
if [[ -d "${INSTALL_DIR}/.git" ]]; then
git -C "${INSTALL_DIR}" fetch origin "${BRANCH}"
git -C "${INSTALL_DIR}" checkout "${BRANCH}"
git -C "${INSTALL_DIR}" reset --hard "origin/${BRANCH}"
else
rm -rf "${INSTALL_DIR}"
git clone --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
fi
if [[ ! -f "${SITE_DIR}/server.mjs" ]]; then
echo "Ошибка: не найден ${SITE_DIR}/server.mjs" >&2
exit 1
fi
}
install_systemd() {
log "Настройка systemd (${SERVICE_NAME})..."
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() {
log "Настройка nginx (${NGINX_SITE})..."
cat >"/etc/nginx/sites-available/${NGINX_SITE}" <<EOF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:${NODE_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
proxy_connect_timeout 60s;
}
}
EOF
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}"
nginx -t
systemctl enable nginx
systemctl restart nginx
}
verify() {
log "Проверка..."
sleep 2
if ! systemctl is-active --quiet "${SERVICE_NAME}.service"; then
systemctl status "${SERVICE_NAME}.service" --no-pager || true
exit 1
fi
if ! curl -fsS -o /dev/null "http://127.0.0.1:${NODE_PORT}/"; then
echo "Node-сервер не отвечает на порту ${NODE_PORT}" >&2
exit 1
fi
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
install_packages
clone_or_update
install_systemd
install_nginx
verify

2
site/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
.DS_Store

37
site/README.md Normal file
View File

@@ -0,0 +1,37 @@
# 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
```
## Ограничения
- Экспорт, поиск, DNS lookup и генерация routing проксируются на живой `geoexport.org` (нужен интернет).
- Это копия UI и кэшированных справочников, не полный автономный бэкенд с базами `.dat`.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

View 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
View 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"}]

13
site/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!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>
</body>
</html>

13
site/package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"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"
},
"engines": {
"node": ">=18"
}
}

146
site/server.mjs Normal file
View 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}`);
});

48
uninstall.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# GeoExport site — one-command uninstall (does not wipe the whole server)
set -euo pipefail
INSTALL_DIR="${GEOEXPORT_INSTALL_DIR:-/opt/domain_web}"
SERVICE_NAME="geoexport-site"
NGINX_SITE="geoexport-site"
log() { echo "[geoexport-uninstall] $*"; }
need_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "Запустите от root: sudo bash uninstall.sh" >&2
exit 1
fi
}
stop_services() {
log "Остановка ${SERVICE_NAME}..."
systemctl stop "${SERVICE_NAME}.service" 2>/dev/null || true
systemctl disable "${SERVICE_NAME}.service" 2>/dev/null || true
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
systemctl daemon-reload
}
remove_nginx() {
log "Удаление конфигурации nginx..."
rm -f "/etc/nginx/sites-enabled/${NGINX_SITE}"
rm -f "/etc/nginx/sites-available/${NGINX_SITE}"
if command -v nginx >/dev/null 2>&1; then
if [[ -d /etc/nginx/sites-available ]] && [[ ! -e /etc/nginx/sites-enabled/default ]]; then
if [[ -f /etc/nginx/sites-available/default ]]; then
ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
fi
fi
nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null || true
fi
}
remove_files() {
log "Удаление файлов в ${INSTALL_DIR}..."
rm -rf "${INSTALL_DIR}"
}
need_root
stop_services
remove_nginx
remove_files
log "Сайт GeoExport удалён. nginx/node/git на сервере не удалялись."