IP = 185.199.109.153
robots.txt// tools/scan-apis.mjs
// Spring(Java 중심) 엔드포인트 스캐너
// - @RestController / @Controller + @RequestMapping / @*Mapping 탐지
// - description: @Operation.description ⇒ 메서드 위 /** ... */의 "description:" ⇒ 클래스 @Tag.description
// - version: 경로 선두 v1/v2/... 추출
// - RequestBody 관련 정보는 출력하지 않음
// - 결과: 프로젝트 루트에 .api-scan.json 저장
import fs from 'fs';
import path from 'path';
const root = process.cwd();
const SRC_DIRS = ['src', 'src/main/java']; // 필요하면 'src/main/kotlin' 추가
const TARGET_EXTS = new Set(['.java']); // Java 위주. Kotlin 쓰면 '.kt'도 추가
// ---------- 공통 유틸 ----------
const readText = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return ''; } };
const normPath = (p) => (p || '').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
const indexToLine = (text, idx) => text.slice(0, idx).split('\n').length;
const extractVersion = (p) => {
const m = /^\/?(v\d+)\b/i.exec(p || '');
return m ? m[1].toLowerCase() : null;
};
const walk = (dir, out = []) => {
if (!fs.existsSync(dir)) return out;
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
if (ent.name.startsWith('.')) continue;
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walk(p, out);
else {
const ext = path.extname(ent.name);
if (TARGET_EXTS.has(ext)) out.push(p);
}
}
return out;
};
// ---------- description 추출 ----------
// 1) 메서드 주변 @Operation(description="...")
const findOperationDescription = (text, idx) => {
const around = text.slice(Math.max(0, idx - 2000), idx + 2000);
const m = around.match(/@Operation\s*\(\s*[^)]*description\s*=\s*"(.*?)"/s);
return m ? (m[1] || '').trim() : '';
};
// 2) 매핑 직전 /** ... */ 블록에서 swagger 스타일 "description:" 찾아보기
const findNearestBlockDescription = (text, idx) => {
// idx 이전의 가장 가까운 /** ... */ 블록
const before = text.slice(0, idx);
const start = before.lastIndexOf('/**');
if (start === -1) return '';
const end = before.indexOf('*/', start + 3);
if (end === -1) return '';
const block = before.slice(start, end + 2);
// 블록 내부에서 yaml 스타일 description: "..."
// (큰따옴표/작은따옴표 모두, 멀티라인은 간단 처리)
const m1 = block.match(/description\s*:\s*"(.*?)"/s);
if (m1) return (m1[1] || '').trim();
const m2 = block.match(/description\s*:\s*'(.*?)'/s);
if (m2) return (m2[1] || '').trim();
// 혹시 줄 끝 주석형: description: 텍스트
const m3 = block.match(/description\s*:\s*(.+)\n/);
if (m3) return (m3[1] || '').trim();
return '';
};
// 3) 클래스 수준 @Tag(description="...")
const findClassTagDescription = (classText) => {
const m = classText.match(/@Tag\s*\(\s*[^)]*description\s*=\s*"(.*?)"/s);
return m ? (m[1] || '').trim() : '';
};
// ---------- content-type(consumes) 추출 ----------
const findConsumesNear = (text, idx) => {
const around = text.slice(Math.max(0, idx - 400), idx + 400);
// 메서드 매핑의 consumes="..."
const m1 = around.match(/consumes\s*=\s*"(.*?)"/);
if (m1) return (m1[1] || '').trim();
return null;
};
const findConsumesInClass = (classText) => {
const m = classText.match(/@RequestMapping\s*\(\s*[^)]*consumes\s*=\s*"(.*?)"/);
return m ? (m[1] || '').trim() : null;
};
// ---------- 클래스 텍스트 추출 ----------
const findEnclosingClassText = (text, methodIdx) => {
// 매우 단순한 휴리스틱: methodIdx 이전에서 가장 가까운 "class XXX {" 찾고,
// 그 뒤로 닫는 } 까지를 클래스 블록으로 간주
const before = text.slice(0, methodIdx);
const classStart = before.lastIndexOf('class ');
if (classStart === -1) return '';
// 여길 기준으로 이후 전체를 보고 첫 번째 '{'와 매칭되는 '}' 찾기
const fromClass = text.slice(classStart);
const braceOpen = fromClass.indexOf('{');
if (braceOpen === -1) return '';
let depth = 0;
for (let i = braceOpen; i < fromClass.length; i++) {
const ch = fromClass[i];
if (ch === '{') depth++;
else if (ch === '}') {
depth--;
if (depth === 0) {
return fromClass.slice(0, i + 1);
}
}
}
return '';
};
// ---------- 메인 스캔 ----------
const files = SRC_DIRS.flatMap((d) => walk(path.join(root, d)));
const endpoints = [];
for (const file of files) {
const text = readText(file);
if (!text) continue;
const rel = path.relative(root, file);
// 컨트롤러 클래스만 진행
if (!/@(RestController|Controller)\b/.test(text)) continue;
// 클래스 레벨 prefix (@RequestMapping)
let classPrefix = '';
const cp =
[...text.matchAll(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?['"`]([^'"`]+)['"`]\s*\)/g)].pop() ||
[...text.matchAll(/@RequestMapping\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g)].pop();
if (cp) classPrefix = (cp[1] || '').trim();
// 메서드 매핑
const mapRe = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*\(\s*(['"`]?)([^'"`)]*)\2\s*\)[\s\S]*?\{/g;
for (const m of text.matchAll(mapRe)) {
const idx = m.index || 0;
const method = m[1].replace('Mapping', '').toUpperCase();
const sub = (m[3] || '').trim();
const full = normPath('/' + [classPrefix, sub].filter(Boolean).join('/'));
const version = extractVersion(full);
// description: @Operation ⇒ 블록 "description:" ⇒ @Tag
let description = findOperationDescription(text, idx);
if (!description) {
description = findNearestBlockDescription(text, idx);
if (!description) {
const classText = findEnclosingClassText(text, idx);
description = findClassTagDescription(classText);
}
}
// contentType: 메서드 consumes ⇒ 클래스 consumes ⇒ 기본 application/json
let contentType = findConsumesNear(text, idx);
if (!contentType) {
const classText = findEnclosingClassText(text, idx);
contentType = findConsumesInClass(classText) || 'application/json';
}
endpoints.push({
framework: 'spring',
method,
path: full,
version,
file: rel,
line: indexToLine(text, idx),
description,
contentType
});
}
}
// 중복 제거
const seen = new Set();
const out = [];
for (const e of endpoints) {
const k = `${e.framework}|${e.method}|${e.path}|${e.file}|${e.line}`;
if (seen.has(k)) continue;
seen.add(k);
out.push(e);
}
// 저장
const outPath = path.join(root, '.api-scan.json');
fs.writeFileSync(outPath, JSON.stringify(out, null, 2));
console.log(`Found ${out.length} endpoints -> ${outPath}`);
Look up this url in the url tool
https://ryuda.github.io/.well-known/acme-challenge: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/csvm: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/nostr.json: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/security.txt: 404 text/html; charset=utf-8
https://ryuda.github.io/.well-known/traffic-advice: 404 text/html; charset=utf-8