// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * Lightweight recon tools — no external binaries required.
 *
 * - techdetect: HTTP fingerprinting for framework/server/language
 * - decode: Multi-format decode chain (base64, hex, URL, JWT)
 * - dirprobe: Parallel HTTP probing of common paths
 *
 * @module tools/recon
 */

// ---------------------------------------------------------------------------
// nerf_techdetect — HTTP header + body fingerprinting
// ---------------------------------------------------------------------------

/**
 * Technology fingerprints matched against response headers and body.
 * Each entry: { name, category, patterns: [{ source: 'header'|'body'|'cookie', field?, regex }] }
 */
const FINGERPRINTS = [
  // Frameworks
  { name: 'Flask', category: 'framework', patterns: [
    { source: 'header', field: 'server', regex: /werkzeug/i },
    { source: 'body', regex: /flask/i },
  ]},
  { name: 'Django', category: 'framework', patterns: [
    { source: 'cookie', regex: /csrftoken/i },
    { source: 'header', field: 'x-frame-options', regex: /DENY|SAMEORIGIN/ },
    { source: 'body', regex: /csrfmiddlewaretoken/i },
  ]},
  { name: 'Express', category: 'framework', patterns: [
    { source: 'header', field: 'x-powered-by', regex: /express/i },
    { source: 'cookie', regex: /connect\.sid/i },
  ]},
  { name: 'FastAPI', category: 'framework', patterns: [
    { source: 'body', regex: /fastapi|starlette/i },
    { source: 'header', field: 'server', regex: /uvicorn/i },
  ]},
  { name: 'Laravel', category: 'framework', patterns: [
    { source: 'cookie', regex: /laravel_session/i },
    { source: 'header', field: 'x-powered-by', regex: /laravel/i },
  ]},
  { name: 'Rails', category: 'framework', patterns: [
    { source: 'header', field: 'x-powered-by', regex: /phusion|passenger/i },
    { source: 'cookie', regex: /_session_id/i },
    { source: 'header', field: 'x-runtime', regex: /\d+\.\d+/ },
  ]},
  { name: 'Spring', category: 'framework', patterns: [
    { source: 'cookie', regex: /JSESSIONID/i },
    { source: 'header', field: 'x-application-context', regex: /.+/ },
  ]},
  { name: 'WordPress', category: 'cms', patterns: [
    { source: 'body', regex: /wp-content|wp-includes/i },
    { source: 'header', field: 'link', regex: /wp-json/i },
  ]},

  // Servers
  { name: 'nginx', category: 'server', patterns: [
    { source: 'header', field: 'server', regex: /nginx/i },
  ]},
  { name: 'Apache', category: 'server', patterns: [
    { source: 'header', field: 'server', regex: /apache/i },
  ]},
  { name: 'Caddy', category: 'server', patterns: [
    { source: 'header', field: 'server', regex: /caddy/i },
  ]},
  { name: 'Gunicorn', category: 'server', patterns: [
    { source: 'header', field: 'server', regex: /gunicorn/i },
  ]},

  // Languages
  { name: 'PHP', category: 'language', patterns: [
    { source: 'header', field: 'x-powered-by', regex: /PHP\/[\d.]+/i },
    { source: 'cookie', regex: /PHPSESSID/i },
    { source: 'header', field: 'server', regex: /PHP/i },
  ]},
  { name: 'Python', category: 'language', patterns: [
    { source: 'header', field: 'server', regex: /python|werkzeug|uvicorn|gunicorn/i },
  ]},
  { name: 'Java', category: 'language', patterns: [
    { source: 'cookie', regex: /JSESSIONID/i },
    { source: 'header', field: 'x-powered-by', regex: /servlet|jsp|tomcat/i },
  ]},
  { name: 'Node.js', category: 'language', patterns: [
    { source: 'header', field: 'x-powered-by', regex: /express/i },
  ]},

  // Security headers
  { name: 'CSP', category: 'security', patterns: [
    { source: 'header', field: 'content-security-policy', regex: /.+/ },
  ]},
  { name: 'HSTS', category: 'security', patterns: [
    { source: 'header', field: 'strict-transport-security', regex: /.+/ },
  ]},
];

/**
 * Detect technologies from HTTP response.
 *
 * @param {object} opts
 * @param {string} opts.url - Target URL to fingerprint
 * @param {number} [opts.timeoutMs=10000] - Request timeout
 * @returns {Promise<object>} Detection results
 */
export async function techdetect({ url, timeoutMs = 10000 }) {
  if (!url) return { error: 'url is required' };

  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    const response = await fetch(url, {
      signal: controller.signal,
      redirect: 'follow',
      headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NERF-TechDetect/1.0)' },
    });
    clearTimeout(timer);

    const headers = Object.fromEntries(response.headers.entries());
    const cookies = headers['set-cookie'] || '';
    const body = await response.text();
    // Limit body scan to first 50KB
    const bodySlice = body.slice(0, 50000);

    const detections = [];

    for (const fp of FINGERPRINTS) {
      for (const p of fp.patterns) {
        let matched = false;
        if (p.source === 'header' && p.field) {
          matched = p.regex.test(headers[p.field] || '');
        } else if (p.source === 'cookie') {
          matched = p.regex.test(cookies);
        } else if (p.source === 'body') {
          matched = p.regex.test(bodySlice);
        }
        if (matched) {
          detections.push({ name: fp.name, category: fp.category, via: `${p.source}${p.field ? ':' + p.field : ''}` });
          break; // One match per fingerprint is enough
        }
      }
    }

    // Extract server version
    const serverHeader = headers['server'] || null;
    const poweredBy = headers['x-powered-by'] || null;

    return {
      url: response.url,
      status: response.status,
      serverHeader,
      poweredBy,
      detections,
      securityHeaders: {
        csp: !!headers['content-security-policy'],
        hsts: !!headers['strict-transport-security'],
        xfo: !!headers['x-frame-options'],
        xcto: !!headers['x-content-type-options'],
        referrerPolicy: !!headers['referrer-policy'],
      },
      title: (bodySlice.match(/<title[^>]*>([^<]+)/i) || [])[1]?.trim() || null,
    };
  } catch (e) {
    return { error: e.message, url };
  }
}

// ---------------------------------------------------------------------------
// nerf_decode — Multi-format decode chain
// ---------------------------------------------------------------------------

/**
 * Decode data through multiple encoding layers.
 *
 * @param {object} opts
 * @param {string} opts.data - Data to decode
 * @param {string} [opts.encoding] - Hint: 'base64', 'hex', 'url', 'jwt', 'auto'
 * @returns {object} Decode results
 */
export function decode({ data, encoding = 'auto' }) {
  if (!data || typeof data !== 'string') return { error: 'data is required (string)' };

  const results = [];

  if (encoding === 'auto' || encoding === 'jwt') {
    const jwt = tryDecodeJWT(data);
    if (jwt) results.push({ encoding: 'jwt', decoded: jwt });
  }

  if (encoding === 'auto' || encoding === 'base64') {
    const b64 = tryDecodeBase64(data);
    if (b64) results.push({ encoding: 'base64', decoded: b64 });
  }

  if (encoding === 'auto' || encoding === 'hex') {
    const hex = tryDecodeHex(data);
    if (hex) results.push({ encoding: 'hex', decoded: hex });
  }

  if (encoding === 'auto' || encoding === 'url') {
    const url = tryDecodeURL(data);
    if (url) results.push({ encoding: 'url', decoded: url });
  }

  return {
    input: data.slice(0, 200),
    results: results.length > 0 ? results : [{ encoding: 'none', decoded: 'No valid encoding detected' }],
  };
}

function tryDecodeBase64(data) {
  // Must look like base64 (A-Za-z0-9+/= or URL-safe variant)
  const cleaned = data.trim();
  if (!/^[A-Za-z0-9+/=_-]{4,}$/.test(cleaned)) return null;
  try {
    const decoded = Buffer.from(cleaned, 'base64').toString('utf8');
    // Verify it decoded to something printable
    if (decoded.length > 0 && /^[\x20-\x7e\n\r\t]+$/.test(decoded)) {
      return decoded;
    }
  } catch { /* not base64 */ }
  return null;
}

function tryDecodeHex(data) {
  const cleaned = data.trim().replace(/^0x/i, '').replace(/\s+/g, '');
  if (!/^[0-9a-fA-F]{4,}$/.test(cleaned) || cleaned.length % 2 !== 0) return null;
  try {
    const decoded = Buffer.from(cleaned, 'hex').toString('utf8');
    if (decoded.length > 0 && /^[\x20-\x7e\n\r\t]+$/.test(decoded)) {
      return decoded;
    }
  } catch { /* not hex */ }
  return null;
}

function tryDecodeURL(data) {
  if (!data.includes('%')) return null;
  try {
    const decoded = decodeURIComponent(data);
    if (decoded !== data) return decoded;
  } catch { /* not URL encoded */ }
  return null;
}

function tryDecodeJWT(data) {
  const parts = data.trim().split('.');
  if (parts.length !== 3) return null;
  try {
    const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
    const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
    return {
      header,
      payload,
      signature: parts[2].slice(0, 20) + '...',
      expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : null,
    };
  } catch { /* not JWT */ }
  return null;
}

// ---------------------------------------------------------------------------
// nerf_dirprobe — Parallel HTTP path probing
// ---------------------------------------------------------------------------

/**
 * Default wordlist of common paths to probe.
 */
const DEFAULT_PATHS = [
  '/', '/robots.txt', '/sitemap.xml', '/.git/', '/.git/config', '/.env',
  '/admin', '/admin/', '/login', '/api', '/api/', '/api/v1/',
  '/docs', '/swagger.json', '/openapi.json', '/graphql',
  '/wp-admin/', '/wp-login.php', '/wp-content/',
  '/phpinfo.php', '/info.php', '/server-status', '/server-info',
  '/.DS_Store', '/backup', '/backup/', '/old/', '/test/',
  '/debug', '/debug/', '/console', '/shell',
  '/flag', '/flag.txt', '/secret', '/secret.txt',
  '/.htaccess', '/.htpasswd', '/web.config', '/crossdomain.xml',
  '/actuator', '/actuator/health', '/actuator/env',
  '/config', '/config/', '/settings', '/dashboard',
];

/**
 * Probe common paths on a target URL.
 *
 * @param {object} opts
 * @param {string} opts.url - Base URL to probe (e.g., http://localhost:8080)
 * @param {string[]} [opts.paths] - Custom paths to probe (default: built-in wordlist)
 * @param {number} [opts.concurrency=10] - Parallel requests
 * @param {number} [opts.timeoutMs=5000] - Per-request timeout
 * @returns {Promise<object>} Probe results
 */
export async function dirprobe({ url, paths, concurrency = 10, timeoutMs = 5000 }) {
  if (!url) return { error: 'url is required' };

  // Normalize base URL
  const baseUrl = url.replace(/\/+$/, '');
  const pathList = paths || DEFAULT_PATHS;

  const found = [];
  const errors = [];
  let probed = 0;

  // Process in batches for concurrency control
  for (let i = 0; i < pathList.length; i += concurrency) {
    const batch = pathList.slice(i, i + concurrency);
    const promises = batch.map(async (path) => {
      const targetUrl = baseUrl + (path.startsWith('/') ? path : '/' + path);
      try {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), timeoutMs);

        const response = await fetch(targetUrl, {
          method: 'GET',
          signal: controller.signal,
          redirect: 'follow',
          headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NERF-DirProbe/1.0)' },
        });
        clearTimeout(timer);
        probed++;

        if (response.status < 400) {
          const contentType = response.headers.get('content-type') || '';
          const contentLength = response.headers.get('content-length');
          found.push({
            path,
            status: response.status,
            contentType: contentType.split(';')[0].trim(),
            size: contentLength ? parseInt(contentLength, 10) : null,
          });
        }
      } catch (e) {
        probed++;
        if (!e.message?.includes('abort')) {
          errors.push({ path, error: e.message });
        }
      }
    });

    await Promise.all(promises);
  }

  return {
    baseUrl,
    probed,
    found: found.sort((a, b) => a.status - b.status),
    errors: errors.slice(0, 5),
  };
}
