fix big scan countdown
This commit is contained in:
+4
-1
@@ -20,6 +20,9 @@ server {
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 650s;
|
||||
proxy_read_timeout 650s;
|
||||
}
|
||||
}
|
||||
server {
|
||||
@@ -29,4 +32,4 @@ server {
|
||||
proxy_pass http://backend:3000/;
|
||||
proxy_set_header Host \$http_host;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@ interface ScanCapabilities {
|
||||
timeoutPath: string | null;
|
||||
}
|
||||
interface ScanResponse {
|
||||
runId: string;
|
||||
addr: string;
|
||||
scanSeconds: number;
|
||||
thread: number;
|
||||
timeout: number;
|
||||
startedAt: string;
|
||||
endsAt: string;
|
||||
finishedAt: string;
|
||||
timedOut: boolean;
|
||||
exitCode: number;
|
||||
foundCount: number;
|
||||
@@ -22,6 +26,21 @@ interface ScanResponse {
|
||||
stderrTail: string;
|
||||
stdoutTail: string;
|
||||
}
|
||||
interface ScanStatusResponse {
|
||||
running: boolean;
|
||||
runId: string | null;
|
||||
addr: string | null;
|
||||
scanSeconds: number | null;
|
||||
thread: number | null;
|
||||
timeout: number | null;
|
||||
startedAt: string | null;
|
||||
endsAt: string | null;
|
||||
now: string;
|
||||
remainingSeconds: number;
|
||||
foundCount: number;
|
||||
lastRunId: string | null;
|
||||
lastFinishedAt: string | null;
|
||||
}
|
||||
|
||||
const SCAN_STORAGE_KEY = 'domains_scan_state_v1';
|
||||
|
||||
@@ -46,6 +65,15 @@ export default function DomainsPage() {
|
||||
const [scanCandidates, setScanCandidates] = useState<string[]>([]);
|
||||
const [scanPanelExpanded, setScanPanelExpanded] = useState(false);
|
||||
const [scanStateHydrated, setScanStateHydrated] = useState(false);
|
||||
const [scanStatus, setScanStatus] = useState<ScanStatusResponse | null>(null);
|
||||
const [activeScanRunId, setActiveScanRunId] = useState<string | null>(null);
|
||||
|
||||
const clampInteger = (value: number, fallback: number, min: number, max: number) => {
|
||||
const num = Number.isFinite(value) ? Math.floor(value) : fallback;
|
||||
if (num < min) return min;
|
||||
if (num > max) return max;
|
||||
return num;
|
||||
};
|
||||
|
||||
const isLoopbackHost = (value: string) => {
|
||||
const host = value.trim().toLowerCase();
|
||||
@@ -107,6 +135,22 @@ export default function DomainsPage() {
|
||||
return '';
|
||||
};
|
||||
|
||||
const fetchScanStatus = async () => {
|
||||
const { data } = await api.get('/domains/scan/status');
|
||||
setScanStatus(data);
|
||||
return data as ScanStatusResponse;
|
||||
};
|
||||
|
||||
const fetchLastScanResult = async (expectedRunId?: string | null) => {
|
||||
const { data } = await api.get('/domains/scan/last-result');
|
||||
if (!data) return null;
|
||||
if (expectedRunId && data.runId !== expectedRunId) return null;
|
||||
|
||||
setScanResult(data);
|
||||
setScanCandidates(data.domains || []);
|
||||
return data as ScanResponse;
|
||||
};
|
||||
|
||||
const loadDomains = async () => {
|
||||
try {
|
||||
const { data } = await api.get(`/domains?page=${page + 1}&limit=${rowsPerPage}`);
|
||||
@@ -127,6 +171,16 @@ export default function DomainsPage() {
|
||||
try {
|
||||
const capRes = await api.get('/domains/scan/capabilities');
|
||||
setScanCapabilities(capRes.data);
|
||||
if (capRes.data?.scannerAvailable) {
|
||||
const status = await fetchScanStatus();
|
||||
if (status.running) {
|
||||
setIsScanning(true);
|
||||
setActiveScanRunId(status.runId);
|
||||
setScanResult(null);
|
||||
setScanCandidates([]);
|
||||
setScanError('');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -197,6 +251,42 @@ export default function DomainsPage() {
|
||||
}
|
||||
}, [scanAddr, scanSeconds, scanThread, scanTimeout, scanResult, scanCandidates, scanPanelExpanded, scanStateHydrated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isScanning) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const tick = async () => {
|
||||
try {
|
||||
const status = await fetchScanStatus();
|
||||
if (cancelled) return;
|
||||
|
||||
if (status.running) {
|
||||
if (status.runId) {
|
||||
setActiveScanRunId((prev) => prev ?? status.runId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setIsScanning(false);
|
||||
const runIdToLoad = activeScanRunId || status.lastRunId;
|
||||
await fetchLastScanResult(runIdToLoad);
|
||||
setActiveScanRunId(null);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tick();
|
||||
const timer = window.setInterval(tick, 1000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [isScanning, activeScanRunId]);
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
@@ -257,25 +347,51 @@ export default function DomainsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveScanSeconds = clampInteger(scanSeconds, 120, 10, 600);
|
||||
const effectiveThread = clampInteger(scanThread, 2, 1, 20);
|
||||
const effectiveTimeout = clampInteger(scanTimeout, 5, 1, 20);
|
||||
let keepScanning = false;
|
||||
|
||||
try {
|
||||
setIsScanning(true);
|
||||
setScanError('');
|
||||
setScanResult(null);
|
||||
setScanStatus(null);
|
||||
setActiveScanRunId(null);
|
||||
|
||||
const { data } = await api.post('/domains/scan/start', {
|
||||
addr: scanAddr.trim(),
|
||||
scanSeconds,
|
||||
thread: scanThread,
|
||||
timeout: scanTimeout,
|
||||
scanSeconds: effectiveScanSeconds,
|
||||
thread: effectiveThread,
|
||||
timeout: effectiveTimeout,
|
||||
});
|
||||
|
||||
setScanResult(data);
|
||||
setScanCandidates(data.domains || []);
|
||||
setActiveScanRunId(data.runId || null);
|
||||
await fetchScanStatus();
|
||||
} catch (e: any) {
|
||||
const message = e?.response?.data?.message || e?.message || 'Ошибка запуска сканера';
|
||||
setScanError(Array.isArray(message) ? message.join('; ') : message);
|
||||
|
||||
if (e?.response?.status === 429) {
|
||||
try {
|
||||
const status = await fetchScanStatus();
|
||||
if (status.running) {
|
||||
keepScanning = true;
|
||||
setIsScanning(true);
|
||||
setActiveScanRunId(status.runId);
|
||||
setScanError('Скан уже выполняется. Подключились к текущему запуску.');
|
||||
}
|
||||
} catch (statusErr) {
|
||||
console.error(statusErr);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsScanning(false);
|
||||
if (!keepScanning) {
|
||||
setIsScanning(false);
|
||||
setActiveScanRunId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -299,6 +415,8 @@ export default function DomainsPage() {
|
||||
const handleClearScannedDomains = async () => {
|
||||
setScanCandidates([]);
|
||||
setScanResult(null);
|
||||
setScanStatus(null);
|
||||
setActiveScanRunId(null);
|
||||
setScanAddr('');
|
||||
|
||||
const suggestedAddr = await resolveSuggestedScanAddr({ allowLoopbackFallback: true });
|
||||
@@ -432,6 +550,15 @@ export default function DomainsPage() {
|
||||
</Button>
|
||||
{isScanning && <CircularProgress size={24} />}
|
||||
</Stack>
|
||||
{isScanning && (
|
||||
<Typography variant='body2' color='text.secondary' sx={{ mt: 1 }}>
|
||||
{scanStatus?.running
|
||||
? scanStatus.remainingSeconds > 0
|
||||
? `Сканирование выполняется. Осталось ${scanStatus.remainingSeconds} сек (по данным сервера). Найдено сейчас: ${scanStatus.foundCount}.`
|
||||
: 'Сканирование завершается, ожидайте...'
|
||||
: 'Сканирование запущено, получаем статус от сервера...'}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{scanError && <Alert severity='error' sx={{ mt: 2 }}>{scanError}</Alert>}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ docker compose -f docker-compose.local.yml ps
|
||||
## Что уже встроено
|
||||
- Backend scanner (RealiTLScanner) собирается в `server/Dockerfile` и кладется в контейнер backend.
|
||||
- Блок `Автопоиск SNI (backend scanner)` показывается только если бинарник сканера доступен в контейнере.
|
||||
- Во время сканирования UI получает статус с backend (`/api/domains/scan/status`) и показывает реальный countdown.
|
||||
- Если обновить страницу во время скана, UI подхватывает активный запуск по backend-статусу и продолжает отсчет.
|
||||
- После завершения скана результат можно получить из backend (`/api/domains/scan/last-result`) даже если исходный запрос был прерван (например, после F5).
|
||||
- Предварительный список сканера:
|
||||
- ручная проверка ссылками,
|
||||
- удаление позиций,
|
||||
|
||||
+7
-1
@@ -277,6 +277,9 @@ server {
|
||||
proxy_set_header Host \$http_host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 650s;
|
||||
proxy_read_timeout 650s;
|
||||
}
|
||||
}
|
||||
server {
|
||||
@@ -388,6 +391,9 @@ server {
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 650s;
|
||||
proxy_read_timeout 650s;
|
||||
}
|
||||
}
|
||||
server {
|
||||
@@ -501,4 +507,4 @@ echo -e "${GREEN}Логин: ${ADMIN_USER}${NC}"
|
||||
echo -e "${GREEN}Пароль: ${ADMIN_PASS}${NC}"
|
||||
echo ""
|
||||
echo "Немедленно измените пароль в Настройках утилиты!"
|
||||
echo "==================================================="
|
||||
echo "==================================================="
|
||||
|
||||
@@ -8,12 +8,42 @@ type StartScanPayload = {
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
type ActiveScanState = {
|
||||
runId: string;
|
||||
addr: string;
|
||||
scanSeconds: number;
|
||||
thread: number;
|
||||
timeout: number;
|
||||
startedAtMs: number;
|
||||
endsAtMs: number;
|
||||
foundCount: number;
|
||||
};
|
||||
|
||||
type ScanResult = {
|
||||
runId: string;
|
||||
addr: string;
|
||||
scanSeconds: number;
|
||||
thread: number;
|
||||
timeout: number;
|
||||
startedAt: string;
|
||||
endsAt: string;
|
||||
finishedAt: string;
|
||||
timedOut: boolean;
|
||||
exitCode: number;
|
||||
foundCount: number;
|
||||
domains: string[];
|
||||
stderrTail: string;
|
||||
stdoutTail: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DomainScannerService {
|
||||
private readonly logger = new Logger(DomainScannerService.name);
|
||||
private readonly scannerBin = 'RealiTLScanner-linux-64';
|
||||
private isScanRunning = false;
|
||||
private readonly logTailLimit = 8000;
|
||||
private activeScan: ActiveScanState | null = null;
|
||||
private lastScanResult: ScanResult | null = null;
|
||||
|
||||
getCapabilities() {
|
||||
const scannerCheck = spawnSync('sh', ['-lc', `command -v ${this.scannerBin}`], { encoding: 'utf-8' });
|
||||
@@ -27,11 +57,42 @@ export class DomainScannerService {
|
||||
};
|
||||
}
|
||||
|
||||
getScanStatus() {
|
||||
const nowMs = Date.now();
|
||||
const active = this.activeScan;
|
||||
|
||||
return {
|
||||
running: Boolean(active),
|
||||
runId: active?.runId ?? null,
|
||||
addr: active?.addr ?? null,
|
||||
scanSeconds: active?.scanSeconds ?? null,
|
||||
thread: active?.thread ?? null,
|
||||
timeout: active?.timeout ?? null,
|
||||
startedAt: active ? new Date(active.startedAtMs).toISOString() : null,
|
||||
endsAt: active ? new Date(active.endsAtMs).toISOString() : null,
|
||||
now: new Date(nowMs).toISOString(),
|
||||
remainingSeconds: active ? Math.max(0, Math.ceil((active.endsAtMs - nowMs) / 1000)) : 0,
|
||||
foundCount: active?.foundCount ?? 0,
|
||||
lastRunId: this.lastScanResult?.runId ?? null,
|
||||
lastFinishedAt: this.lastScanResult?.finishedAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
getLastScanResult() {
|
||||
return this.lastScanResult;
|
||||
}
|
||||
|
||||
async startScan(payload: StartScanPayload) {
|
||||
// Scanner is CPU/network heavy; keep exactly one active run per backend instance
|
||||
// to avoid accidental DoS from repeated button clicks.
|
||||
if (this.isScanRunning) {
|
||||
throw new HttpException('Сканер уже запущен, дождитесь завершения текущего запуска', HttpStatus.TOO_MANY_REQUESTS);
|
||||
throw new HttpException(
|
||||
{
|
||||
message: 'Сканер уже запущен, дождитесь завершения текущего запуска',
|
||||
scanStatus: this.getScanStatus(),
|
||||
},
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
|
||||
const addr = (payload.addr || '').trim();
|
||||
@@ -65,9 +126,24 @@ export class DomainScannerService {
|
||||
'',
|
||||
];
|
||||
|
||||
const runId = this.createRunId();
|
||||
const startedAtMs = Date.now();
|
||||
const endsAtMs = startedAtMs + scanSeconds * 1000;
|
||||
|
||||
this.logger.log(`Starting scanner: addr=${addr}, seconds=${scanSeconds}, thread=${thread}, timeout=${connectTimeout}`);
|
||||
|
||||
this.isScanRunning = true;
|
||||
this.activeScan = {
|
||||
runId,
|
||||
addr,
|
||||
scanSeconds,
|
||||
thread,
|
||||
timeout: connectTimeout,
|
||||
startedAtMs,
|
||||
endsAtMs,
|
||||
foundCount: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const child = spawn('timeout', args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@@ -90,6 +166,9 @@ export class DomainScannerService {
|
||||
for (const line of parts) {
|
||||
this.extractDomainsFromLog(line, domains);
|
||||
}
|
||||
if (this.activeScan?.runId === runId) {
|
||||
this.activeScan.foundCount = domains.size;
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
@@ -106,6 +185,9 @@ export class DomainScannerService {
|
||||
|
||||
if (stdoutRemainder) {
|
||||
this.extractDomainsFromLog(stdoutRemainder, domains);
|
||||
if (this.activeScan?.runId === runId) {
|
||||
this.activeScan.foundCount = domains.size;
|
||||
}
|
||||
}
|
||||
|
||||
const timedOut = exitCode === 124 || exitCode === 137 || exitCode === 143;
|
||||
@@ -115,11 +197,15 @@ export class DomainScannerService {
|
||||
}
|
||||
|
||||
const sortedDomains = [...domains].sort();
|
||||
return {
|
||||
const result: ScanResult = {
|
||||
runId,
|
||||
addr,
|
||||
scanSeconds,
|
||||
thread,
|
||||
timeout: connectTimeout,
|
||||
startedAt: new Date(startedAtMs).toISOString(),
|
||||
endsAt: new Date(endsAtMs).toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
timedOut,
|
||||
exitCode,
|
||||
foundCount: sortedDomains.length,
|
||||
@@ -127,8 +213,12 @@ export class DomainScannerService {
|
||||
stderrTail: stderr.slice(-800),
|
||||
stdoutTail: stdout.slice(-800),
|
||||
};
|
||||
|
||||
this.lastScanResult = result;
|
||||
return result;
|
||||
} finally {
|
||||
this.isScanRunning = false;
|
||||
this.activeScan = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,4 +264,8 @@ export class DomainScannerService {
|
||||
}
|
||||
return merged.slice(-this.logTailLimit);
|
||||
}
|
||||
|
||||
private createRunId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ export class DomainsController {
|
||||
return this.domainScannerService.getCapabilities();
|
||||
}
|
||||
|
||||
@Get('scan/status')
|
||||
scanStatus() {
|
||||
return this.domainScannerService.getScanStatus();
|
||||
}
|
||||
|
||||
@Get('scan/last-result')
|
||||
lastScanResult() {
|
||||
return this.domainScannerService.getLastScanResult();
|
||||
}
|
||||
|
||||
@Post('scan/start')
|
||||
startScan(@Body() body: { addr: string; scanSeconds?: number; thread?: number; timeout?: number }) {
|
||||
return this.domainScannerService.startScan(body);
|
||||
|
||||
@@ -30,6 +30,46 @@ need_root() {
|
||||
[[ $EUID -eq 0 ]] || die "Запускать только от root"
|
||||
}
|
||||
|
||||
ensure_nginx_api_timeouts() {
|
||||
local nginx_conf="$1"
|
||||
[[ -f "$nginx_conf" ]] || return 0
|
||||
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
awk '
|
||||
BEGIN { in_api = 0; injected = 0 }
|
||||
{
|
||||
line = $0
|
||||
|
||||
if (line ~ /^[[:space:]]*location[[:space:]]+\/api\/[[:space:]]*\{/) {
|
||||
in_api = 1
|
||||
injected = 0
|
||||
}
|
||||
|
||||
if (in_api && line ~ /proxy_(connect|send|read)_timeout[[:space:]]+[0-9]+s;/) {
|
||||
next
|
||||
}
|
||||
|
||||
print line
|
||||
|
||||
if (in_api && line ~ /proxy_set_header[[:space:]]+X-Forwarded-For[[:space:]]+/ && injected == 0) {
|
||||
print " proxy_connect_timeout 10s;"
|
||||
print " proxy_send_timeout 650s;"
|
||||
print " proxy_read_timeout 650s;"
|
||||
injected = 1
|
||||
}
|
||||
|
||||
if (in_api && line ~ /^[[:space:]]*}/) {
|
||||
in_api = 0
|
||||
injected = 0
|
||||
}
|
||||
}
|
||||
' "$nginx_conf" > "$tmp_file"
|
||||
|
||||
mv "$tmp_file" "$nginx_conf"
|
||||
}
|
||||
|
||||
REPO_URL=""
|
||||
BRANCH=""
|
||||
PROJECT_DIR="/opt/3dp-manager"
|
||||
@@ -98,6 +138,8 @@ services:
|
||||
image: 3dp-manager-client:custom
|
||||
EOF
|
||||
|
||||
ensure_nginx_api_timeouts "$PROJECT_DIR/client/nginx-client.conf"
|
||||
|
||||
log "Сборка custom-образов backend/frontend"
|
||||
cd "$PROJECT_DIR"
|
||||
docker compose -f docker-compose.yml -f docker-compose.custom.yml build backend frontend
|
||||
|
||||
Reference in New Issue
Block a user