그누보드5 유령봇 > 코딩 스토리

그누보드5 유령봇

본문

<!doctype html>

<html lang="ko">

<head>

<meta charset="utf-8"/>

<meta name="viewport" content="width=device-width,initial-scale=1"/>

<title>GNUboard5 Ghost Visitor — 브라우저 봇</title>

<style>

body{font-family:system-ui,-apple-system,Segoe UI,Roboto,"Noto Sans KR",sans-serif;background:#0f1724;color:#e6eef8;padding:18px}

h1{font-size:18px;margin:0 0 10px}

.card{background:#0b1220;padding:12px;border-radius:8px;margin-bottom:12px;box-shadow:0 2px 8px rgba(0,0,0,0.5)}

label{display:block;margin:6px 0 2px;font-size:13px;color:#a8c0e0}

input[type=text], input[type=number], select {width:100%;padding:8px;border-radius:6px;border:1px solid #1f2a3a;background:#08101a;color:#e6eef8}

button{padding:8px 12px;border-radius:6px;border:none;background:#2b8be6;color:white;cursor:pointer;margin-right:8px}

.muted{color:#7f9bb0;font-size:13px}

pre{background:#071025;padding:8px;border-radius:6px;color:#cfe8ff;max-height:280px;overflow:auto}

.flex{display:flex;gap:8px}

.small{font-size:13px}

.status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:8px;vertical-align:middle}

.dot-idle{background:#999}

.dot-run{background:#2bd39b}

.dot-stop{background:#ffa94d}

.controls{margin-top:8px}

.tags{font-size:12px;color:#8fb0d8;margin-top:6px}

.footer{font-size:12px;color:#7b97b6;margin-top:12px}

a.link { color:#7fd0ff }

</style>

</head>

<body>

<h1>GNUboard5 Ghost Visitor — 브라우저용 유령 봇</h1>


<div class="card">

<div style="display:flex;justify-content:space-between;align-items:center">

<div>

<span id="statusDot" class="status-dot dot-idle"></span>

<strong id="statusText">대기 중</strong>

<div class="muted small">브라우저 탭에서 실행됩니다 — 동일 출처에서 호스팅하세요.</div>

</div>

<div>

<button id="startBtn">Start</button>

<button id="stopBtn" disabled>Stop</button>

</div>

</div>

</div>


<div class="card">

<label>시작 URL (같은 도메인 내 페이지)</label>

<input type="text" id="startUrl" value="/" placeholder="예: /, /bbs/board.php?bo_table=free" />


<label>게시판 링크 선택자 (CSS selector, 보통 게시판 목록의 링크)</label>

<input type="text" id="boardSelector" value="a[href*='board.php'], a[href*='bbs/board.php'], a.board, a.board-link" />


<label>게시글 링크 선택자 (CSS selector)</label>

<input type="text" id="postSelector" value="a[href*='view.php'], a[href*='bbs/view.php'], a.post, a.wr_subject a" />


<label>포함/제외할 href 패턴 (정규식, 쉼표로 구분, optional)</label>

<input type="text" id="includePattern" placeholder="예: /bbs/, /board.php" />

<input type="text" id="excludePattern" placeholder="예: /write.php, /login.php" />


<div class="flex">

<div style="flex:1">

<label>최소 대기(ms)</label>

<input type="number" id="minDelay" value="1500" />

</div>

<div style="flex:1">

<label>최대 대기(ms)</label>

<input type="number" id="maxDelay" value="4500" />

</div>

<div style="flex:1">

<label>최대 방문 수 (0 = 무제한)</label>

<input type="number" id="maxVisits" value="0" />

</div>

</div>


<div class="flex controls">

<button id="seedBtn">수동 시드 추가</button>

<button id="exportLogBtn">로그 내보내기</button>

<button id="clearLogBtn">로그 초기화</button>

</div>

<div class="tags">

<div>Tip: 로그인된 상태(쿠키 있음)로 실행하면 인증 페이지도 접근됩니다. 서버에 부담이 되지 않도록 딜레이를 충분히 주세요.</div>

</div>

</div>


<div class="card">

<label>진행 로그</label>

<pre id="log" aria-live="polite"></pre>

</div>


<script>

/*

GNUboard5 Ghost Visitor

- 동작 방식:

1) 시작 URL에서 HTML을 fetch

2) boardSelector, postSelector로 링크 수집

3) 방문 큐(visited set) 유지하며 FIFO 탐색

4) 각 방문마다 랜덤 지연, 페이지 fetch, 스크롤/머무르기 시뮬레이션

5) 설정으로 포함/제외 패턴, 최대 방문수 제어 가능


주의:

- 동일 출처(same-origin)에서 실행해야 합니다.

- 서버 부하를 주지 않도록 delay를 넉넉히(최소 1~1.5초 권장) 설정하세요.

*/


(function(){

// UI elements

const startBtn = document.getElementById('startBtn');

const stopBtn = document.getElementById('stopBtn');

const statusDot = document.getElementById('statusDot');

const statusText = document.getElementById('statusText');

const logEl = document.getElementById('log');

const seedBtn = document.getElementById('seedBtn');

const exportLogBtn = document.getElementById('exportLogBtn');

const clearLogBtn = document.getElementById('clearLogBtn');


// Settings inputs

const startUrlInput = document.getElementById('startUrl');

const boardSelectorInput = document.getElementById('boardSelector');

const postSelectorInput = document.getElementById('postSelector');

const includePatternInput = document.getElementById('includePattern');

const excludePatternInput = document.getElementById('excludePattern');

const minDelayInput = document.getElementById('minDelay');

const maxDelayInput = document.getElementById('maxDelay');

const maxVisitsInput = document.getElementById('maxVisits');


// State

let running = false;

let queue = []; // URL strings to visit

let visited = new Set();

let visitCount = 0;

let logs = [];

let controller = null; // AbortController for fetches if needed


function appendLog(line){

const ts = new Date().toLocaleTimeString();

const text = `[${ts}] ${line}`;

logs.push(text);

logEl.textContent = logs.slice(-500).join('\\n'); // keep last 500 lines

logEl.scrollTop = logEl.scrollHeight;

console.log(text);

}


function setStatus(state){

if(state === 'running'){

running = true;

statusDot.className = 'status-dot dot-run';

statusText.textContent = '실행 중';

startBtn.disabled = true;

stopBtn.disabled = false;

} else if(state === 'stopped'){

running = false;

statusDot.className = 'status-dot dot-stop';

statusText.textContent = '중지됨';

startBtn.disabled = false;

stopBtn.disabled = true;

} else {

running = false;

statusDot.className = 'status-dot dot-idle';

statusText.textContent = '대기 중';

startBtn.disabled = false;

stopBtn.disabled = true;

}

}


// Utility: safe absolute URL join

function absolutize(href){

try {

return new URL(href, location.href).href;

} catch(err){

return null;

}

}


function buildRegexList(csv){

if(!csv) return null;

const parts = csv.split(',').map(s=>s.trim()).filter(Boolean);

if(parts.length === 0) return null;

// combine with OR, escape slashes? allow as patterns

try {

const joined = parts.map(p => {

// if p looks like a regex (starts and ends with /) -> use as-is

if(p.startsWith('/') && p.lastIndexOf('/')>0){

const last = p.lastIndexOf('/');

const pat = p.slice(1,last);

const flag = p.slice(last+1);

return `(${pat})`;

} else {

// escape special regex chars

const esc = p.replace(/[.*+?^${}()|[\\]\\\\]/g,"\\\\$&");

return `(${esc})`;

}

}).join('|');

return new RegExp(joined,'i');

} catch(e){

console.warn('pattern error', e);

return null;

}

}


// parse HTML text into document

function parseHTML(text){

const parser = new DOMParser();

return parser.parseFromString(text, 'text/html');

}


// random delay

function randDelay(minMs, maxMs){

const m = Math.max(0, Number(minMs) || 0);

const M = Math.max(m, Number(maxMs) || m);

return m + Math.floor(Math.random() * (M - m + 1));

}


// simulate reading/scrolling time on a fetched page

async function simulateReading(doc){

// approximate reading time based on text length

const bodyText = doc.body ? doc.body.innerText || '' : '';

const chars = Math.min(20000, bodyText.length);

// reading speed rough: 250 chars/sec => chars/250*1000 ms

const baseMs = Math.min(120000, Math.max(1000, Math.floor((chars/250)*1000)));

// add random jitter

const jitter = Math.floor(Math.random() * 2000) - 1000;

const total = Math.max(500, baseMs + jitter);

appendLog(`읽는 중... 예상 ${Math.round(total/1000)}초`);

// emulate incremental "scroll" events by waiting in chunks

const steps = Math.min(12, Math.max(1, Math.floor(total / 2000)));

for(let i=0;i<steps && running;i++){

await new Promise(r => setTimeout(r, Math.floor(total/steps)));

// dispatch a fake scroll event to the window (sometimes useful)

try{

window.dispatchEvent(new Event('scroll'));

}catch(e){}

}

}


// main visit function

async function visitUrl(url, selectors){

if(!running) return;

if(!url) return;

if(visited.has(url)) return;

// respect max visits

const maxVisits = Number(maxVisitsInput.value) || 0;

if(maxVisits > 0 && visitCount >= maxVisits) {

appendLog(`최대 방문수(${maxVisits}) 도달 — 중지 예정`);

stop();

return;

}


visited.add(url);

queue = queue.filter(x => x !== url);

visitCount++;

appendLog(`방문(${visitCount}): ${url}`);


controller = new AbortController();

const signal = controller.signal;


// add small randomized delay before fetch (simulate think/click)

const preDelay = randDelay(Number(minDelayInput.value), Number(maxDelayInput.value));

await new Promise(r => setTimeout(r, preDelay));


try {

const resp = await fetch(url, {method:'GET', credentials:'same-origin', signal});

const text = await resp.text();

const doc = parseHTML(text);


// optional: if page contains redirect meta refresh, follow once

const metaRefresh = doc.querySelector('meta[http-equiv="refresh"]');

if(metaRefresh){

const content = metaRefresh.getAttribute('content') || '';

const m = content.match(/url=(.+)/i);

if(m && m[1]){

const next = absolutize(m[1].trim());

if(next && !visited.has(next)){

appendLog('meta refresh 감지 — 리다이렉트 방문: ' + next);

queue.push(next);

}

}

}


// simulate reading/scrolling time

await simulateReading(doc);


// find links per selectors

try {

const {boardSelector, postSelector, includeRegex, excludeRegex} = selectors;

const foundAnchors = Array.from(doc.querySelectorAll('a[href]'));

for(const a of foundAnchors){

const href = a.getAttribute('href');

const abs = absolutize(href);

if(!abs) continue;

// only same origin

if(new URL(abs).origin !== location.origin) continue;


// exclude obvious assets (images, css, js) by extension

if(abs.match(/\\.(jpg|jpeg|png|gif|svg|css|js|woff2?|ico)(\\?|$)/i)) continue;


// apply include/exclude patterns

if(excludeRegex && excludeRegex.test(abs)) continue;

if(includeRegex && !includeRegex.test(abs)) continue;


// if matches board or post selector (on original doc, matching CSS is limited because we used generic anchors)

// We'll heuristically check href path for common keywords as fallback

const path = new URL(abs).pathname + (new URL(abs).search || '');

const boardHeur = /board|bbs|bo_table|bo_list|board.php|bbs\/board.php/i;

const postHeur = /view.php|wr_id|wr_subject|read.php|bbs\/view.php/i;


// Use provided selectors: if selector matches (we only had access to doc anchors), use that

let accepted = false;

try {

if(boardSelector && a.matches(boardSelector)) accepted = true;

if(postSelector && a.matches(postSelector)) accepted = true;

} catch(e){

// selector may be invalid for this document — ignore.

}


// heuristic fallback

if(!accepted){

if(boardHeur.test(path) || postHeur.test(path)) accepted = true;

}


if(accepted && !visited.has(abs) && !queue.includes(abs)){

queue.push(abs);

appendLog('큐에 추가: ' + abs);

}

}

} catch(e){

console.warn('링크 수집 실패', e);

}


} catch(err){

if(err.name === 'AbortError'){

appendLog('요청 중단: ' + url);

} else {

appendLog('방문 실패: ' + url + ' — ' + (err && err.message));

}

} finally {

controller = null;

}

}


// main loop

async function loop(){

setStatus('running');

const selectors = {

boardSelector: boardSelectorInput.value || null,

postSelector: postSelectorInput.value || null,

includeRegex: buildRegexList(includePatternInput.value),

excludeRegex: buildRegexList(excludePatternInput.value),

};


// seed with startUrl if queue empty

if(queue.length === 0){

const s = startUrlInput.value.trim() || '/';

const abs = absolutize(s) || location.href;

if(abs) queue.push(abs);

}


while(running && queue.length > 0){

const next = queue.shift();

if(!next) continue;

// skip if visited

if(visited.has(next)) continue;


// visit

await visitUrl(next, selectors);


// small randomized pause between visits (simulate human pause)

const idle = randDelay(Number(minDelayInput.value), Number(maxDelayInput.value));

await new Promise(r => setTimeout(r, idle));


// check max visits again

const maxV = Number(maxVisitsInput.value) || 0;

if(maxV > 0 && visitCount >= maxV){

appendLog('최대 방문수 도달 — 루프 종료');

break;

}

}


setStatus('stopped');

appendLog('작업 종료 — 큐 길이: ' + queue.length + ' 방문수: ' + visitCount);

}


function start(){

if(running) return;

appendLog('시작');

running = true;

visitCount = 0;

// do not clear visited by default (so it won't repeat); if you'd like fresh run, user can refresh page

loop().catch(err => {

appendLog('루프 에러: ' + (err && err.message));

setStatus('stopped');

});

}


function stop(){

if(!running) return;

appendLog('중지 요청됨 — 현재 요청을 취소합니다.');

running = false;

if(controller) {

try { controller.abort(); } catch(e){}

controller = null;

}

setStatus('stopped');

}


// seed manual (현재 startUrl 추가)

seedBtn.addEventListener('click', ()=>{

const s = prompt('시드로 추가할 URL을 입력하세요 (상대/절대 가능)', startUrlInput.value || '/');

if(!s) return;

const abs = absolutize(s);

if(!abs){ alert('잘못된 URL'); return; }

if(!queue.includes(abs) && !visited.has(abs)){ queue.push(abs); appendLog('수동 시드 추가: ' + abs); }

else appendLog('이미 큐에 존재하거나 방문됨: ' + abs);

});


startBtn.addEventListener('click', ()=>{

// small validation

const minD = Number(minDelayInput.value);

const maxD = Number(maxDelayInput.value);

if(isNaN(minD) || isNaN(maxD) || minD < 0 || maxD < 0 || maxD < minD){

alert('지연 설정이 유효하지 않습니다. Min/Max Delay를 확인하세요.');

return;

}

// ensure same origin start

const s = startUrlInput.value.trim() || '/';

const abs = absolutize(s);

if(!abs){

alert('시작 URL이 유효하지 않습니다.');

return;

}

if(new URL(abs).origin !== location.origin){

if(!confirm('시작 URL이 현재 도메인과 다릅니다. 동일 출처에서 실행해야 정상 동작합니다. 계속하시겠습니까?')) return;

}

if(!queue.includes(abs) && !visited.has(abs)) queue.unshift(abs);

start();

});


stopBtn.addEventListener('click', stop);


exportLogBtn.addEventListener('click', ()=>{

const blob = new Blob([logs.join('\\n')], {type:'text/plain;charset=utf-8'});

const url = URL.createObjectURL(blob);

const a = document.createElement('a');

a.href = url;

a.download = 'ghost-visitor-log-' + (new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')) + '.txt';

document.body.appendChild(a);

a.click();

a.remove();

URL.revokeObjectURL(url);

});


clearLogBtn.addEventListener('click', ()=>{

if(!confirm('로그를 초기화 하시겠습니까?')) return;

logs = [];

logEl.textContent = '';

appendLog('로그 초기화 완료');

});


// keyboard shortcut: S to start/stop

window.addEventListener('keydown', (e)=>{

if(e.key === 's' || e.key === 'S'){

if(running) stop(); else start();

}

});


// initial status

setStatus('idle');

appendLog('로더 준비 완료 — 동일 출처에서 실행하세요.');


})();

</script>


<div class="footer">

<div>작성자: Ghost Visitor 스크립트 • 사용 전 서버 부하와 법적/윤리적 영향 확인 필수</div>

<div>문제/개선 요청 있으면 알려줘요.</div>

</div>

</body>

</html>


좋아요9 이 글을 좋아요하셨습니다
url 복사 카카오톡 공유 라인 공유 페이스북 공유 트위터 공유
지역-로컬
Powered by AI
정상
Powered by AI

등록된 댓글이 없습니다.

  • RSS
  • _  글쓰기 글쓰기
전체 303건
게시물 검색

접속자집계

오늘
923
어제
3,296
최대
42,418
전체
1,048,018