#!/usr/bin/env python3
"""
souleyez.plugins.http_fingerprint - Lightweight HTTP fingerprinting

Detects:
- Server software (Apache, nginx, IIS, etc.)
- WAFs (Cloudflare, Akamai, AWS WAF, etc.)
- CDNs (Cloudflare, Fastly, CloudFront, etc.)
- Managed hosting platforms (Squarespace, Wix, Shopify, etc.)
- Technologies (via headers and cookies)

This runs BEFORE web vulnerability scanners to enable smarter tool configuration.
"""
import json
import time
import ssl
import socket
from typing import Dict, Any, List, Optional
from urllib.parse import urlparse

from .plugin_base import PluginBase

HELP = {
    "name": "HTTP Fingerprint - Lightweight Web Reconnaissance",
    "description": (
        "Performs lightweight HTTP fingerprinting to detect server software, "
        "WAFs, CDNs, and managed hosting platforms.\n\n"
        "This runs automatically before web vulnerability scanners to enable "
        "smarter tool configuration. For example, if Squarespace is detected, "
        "nikto will skip CGI enumeration (pointless on managed platforms).\n\n"
        "Detection categories:\n"
        "- Server software (Apache, nginx, IIS, etc.)\n"
        "- WAFs (Cloudflare, Akamai, AWS WAF, Imperva, etc.)\n"
        "- CDNs (Cloudflare, Fastly, CloudFront, Akamai, etc.)\n"
        "- Managed hosting (Squarespace, Wix, Shopify, Netlify, etc.)\n"
    ),
    "usage": "souleyez jobs enqueue http_fingerprint <target>",
    "examples": [
        "souleyez jobs enqueue http_fingerprint http://example.com",
        "souleyez jobs enqueue http_fingerprint https://example.com",
    ],
    "flags": [
        ["--timeout <sec>", "Request timeout (default: 10)"],
    ],
    "presets": [
        {"name": "Quick Fingerprint", "args": [], "desc": "Fast fingerprint scan"},
    ],
}

# WAF detection signatures
# Format: {header_name: {value_pattern: waf_name}}
WAF_SIGNATURES = {
    # Header-based detection
    'headers': {
        'server': {
            'cloudflare': 'Cloudflare',
            'akamaighost': 'Akamai',
            'akamainetworkstorage': 'Akamai',
            'awselb': 'AWS ELB',
            'bigip': 'F5 BIG-IP',
            'barracuda': 'Barracuda',
            'denyall': 'DenyAll',
            'fortigate': 'Fortinet FortiGate',
            'imperva': 'Imperva',
            'incapsula': 'Imperva Incapsula',
            'netscaler': 'Citrix NetScaler',
            'sucuri': 'Sucuri',
            'wallarm': 'Wallarm',
        },
        'x-powered-by': {
            'aws lambda': 'AWS Lambda',
            'express': 'Express.js',
            'php': 'PHP',
            'asp.net': 'ASP.NET',
        },
        'x-sucuri-id': {'': 'Sucuri'},
        'x-sucuri-cache': {'': 'Sucuri'},
        'cf-ray': {'': 'Cloudflare'},
        'cf-cache-status': {'': 'Cloudflare'},
        'x-amz-cf-id': {'': 'AWS CloudFront'},
        'x-amz-cf-pop': {'': 'AWS CloudFront'},
        'x-akamai-transformed': {'': 'Akamai'},
        'x-cache': {
            'cloudfront': 'AWS CloudFront',
            'varnish': 'Varnish',
        },
        'x-fastly-request-id': {'': 'Fastly'},
        'x-served-by': {
            'cache-': 'Fastly',
        },
        'x-cdn': {
            'incapsula': 'Imperva Incapsula',
            'cloudflare': 'Cloudflare',
        },
        'x-iinfo': {'': 'Imperva Incapsula'},
        'x-proxy-id': {'': 'Imperva'},
        'x-request-id': {},  # Generic, but useful context
        'x-fw-protection': {'': 'Unknown WAF'},
        'x-protected-by': {'': 'Unknown WAF'},
        'x-waf-status': {'': 'Unknown WAF'},
        'x-denied-reason': {'': 'Unknown WAF'},
    },
    # Cookie-based detection
    'cookies': {
        '__cfduid': 'Cloudflare',
        'cf_clearance': 'Cloudflare',
        '__cf_bm': 'Cloudflare Bot Management',
        'incap_ses': 'Imperva Incapsula',
        'visid_incap': 'Imperva Incapsula',
        'nlbi_': 'Imperva Incapsula',
        'ak_bmsc': 'Akamai Bot Manager',
        'bm_sz': 'Akamai Bot Manager',
        '_abck': 'Akamai Bot Manager',
        'awsalb': 'AWS ALB',
        'awsalbcors': 'AWS ALB',
        'ts': 'F5 BIG-IP',
        'bigipserver': 'F5 BIG-IP',
        'citrix_ns_id': 'Citrix NetScaler',
        'sucuri_cloudproxy': 'Sucuri',
    },
}

# CDN detection signatures
CDN_SIGNATURES = {
    'headers': {
        'cf-ray': 'Cloudflare',
        'cf-cache-status': 'Cloudflare',
        'x-amz-cf-id': 'AWS CloudFront',
        'x-amz-cf-pop': 'AWS CloudFront',
        'x-cache': {
            'cloudfront': 'AWS CloudFront',
            'hit from cloudfront': 'AWS CloudFront',
        },
        'x-fastly-request-id': 'Fastly',
        'x-served-by': 'Fastly',
        'x-akamai-transformed': 'Akamai',
        'x-akamai-request-id': 'Akamai',
        'x-edge-location': 'Generic CDN',
        'x-cdn': 'Generic CDN',
        'x-cache-status': 'Generic CDN',
        'x-varnish': 'Varnish',
        'via': {
            'cloudfront': 'AWS CloudFront',
            'varnish': 'Varnish',
            'akamai': 'Akamai',
        },
        'x-azure-ref': 'Azure CDN',
        'x-msedge-ref': 'Azure CDN',
        'x-goog-': 'Google Cloud CDN',
        'x-bunny-': 'Bunny CDN',
        'x-hw': 'Huawei CDN',
    },
    'server': {
        'cloudflare': 'Cloudflare',
        'akamaighost': 'Akamai',
        'cloudfront': 'AWS CloudFront',
        'fastly': 'Fastly',
        'varnish': 'Varnish',
        'keycdn': 'KeyCDN',
        'bunnycdn': 'Bunny CDN',
        'cdn77': 'CDN77',
        'stackpath': 'StackPath',
        'limelight': 'Limelight',
        'azure': 'Azure CDN',
    },
}

# Managed hosting platform signatures
MANAGED_HOSTING_SIGNATURES = {
    'server': {
        'squarespace': 'Squarespace',
        'wix': 'Wix',
        'shopify': 'Shopify',
        'weebly': 'Weebly',
        'webflow': 'Webflow',
        'ghost': 'Ghost',
        'medium': 'Medium',
        'tumblr': 'Tumblr',
        'blogger': 'Blogger/Blogspot',
        'wordpress.com': 'WordPress.com',
        'netlify': 'Netlify',
        'vercel': 'Vercel',
        'heroku': 'Heroku',
        'github': 'GitHub Pages',
        'gitlab': 'GitLab Pages',
        'firebase': 'Firebase Hosting',
        'render': 'Render',
        'railway': 'Railway',
        'fly': 'Fly.io',
        'deno': 'Deno Deploy',
    },
    'headers': {
        'x-shopify-stage': 'Shopify',
        'x-shopify-request-id': 'Shopify',
        'x-wix-request-id': 'Wix',
        'x-wix-renderer-server': 'Wix',
        'x-sqsp-edge': 'Squarespace',
        'x-squarespace-': 'Squarespace',
        'x-ghost-': 'Ghost',
        'x-medium-content': 'Medium',
        'x-tumblr-': 'Tumblr',
        'x-blogger-': 'Blogger/Blogspot',
        'x-netlify-': 'Netlify',
        'x-nf-request-id': 'Netlify',
        'x-vercel-': 'Vercel',
        'x-vercel-id': 'Vercel',
        'x-heroku-': 'Heroku',
        'x-github-request-id': 'GitHub Pages',
        'x-firebase-': 'Firebase Hosting',
        'x-render-origin-server': 'Render',
        'fly-request-id': 'Fly.io',
    },
    'cookies': {
        'wordpress_': 'WordPress',
        'wp-settings': 'WordPress',
        '_shopify_': 'Shopify',
        'wixSession': 'Wix',
    },
}

# Server software signatures
SERVER_SIGNATURES = {
    'apache': 'Apache',
    'nginx': 'nginx',
    'microsoft-iis': 'Microsoft IIS',
    'iis': 'Microsoft IIS',
    'lighttpd': 'lighttpd',
    'litespeed': 'LiteSpeed',
    'openresty': 'OpenResty',
    'caddy': 'Caddy',
    'tomcat': 'Apache Tomcat',
    'jetty': 'Eclipse Jetty',
    'gunicorn': 'Gunicorn',
    'uvicorn': 'Uvicorn',
    'werkzeug': 'Werkzeug (Flask)',
    'waitress': 'Waitress',
    'cowboy': 'Cowboy (Erlang)',
    'kestrel': 'Kestrel (ASP.NET)',
    'express': 'Express.js',
}


class HttpFingerprintPlugin(PluginBase):
    name = "HTTP Fingerprint"
    tool = "http_fingerprint"
    category = "recon"
    HELP = HELP

    def build_command(self, target: str, args: List[str] = None, label: str = "", log_path: str = None):
        """
        HTTP fingerprinting is done in Python, not via external command.
        Return None to use run() method instead.
        """
        return None

    def run(self, target: str, args: List[str] = None, label: str = "", log_path: str = None) -> int:
        """Execute HTTP fingerprint scan."""
        args = args or []
        timeout = 10

        # Parse timeout from args
        for i, arg in enumerate(args):
            if arg == '--timeout' and i + 1 < len(args):
                try:
                    timeout = int(args[i + 1])
                except ValueError:
                    pass

        # Ensure target has scheme
        if not target.startswith(('http://', 'https://')):
            target = f'http://{target}'

        try:
            result = self._fingerprint(target, timeout)
            output = self._format_output(target, result, label)

            if log_path:
                with open(log_path, 'a', encoding='utf-8', errors='replace') as fh:
                    fh.write(output)
                    # Write JSON result for parsing
                    fh.write("\n\n=== JSON_RESULT ===\n")
                    fh.write(json.dumps(result, indent=2))
                    fh.write("\n=== END_JSON_RESULT ===\n")

            return 0

        except Exception as e:
            error_output = f"=== Plugin: HTTP Fingerprint ===\n"
            error_output += f"Target: {target}\n"
            error_output += f"Error: {type(e).__name__}: {e}\n"

            if log_path:
                with open(log_path, 'a', encoding='utf-8', errors='replace') as fh:
                    fh.write(error_output)

            return 1

    def _fingerprint(self, url: str, timeout: int = 10) -> Dict[str, Any]:
        """
        Perform HTTP fingerprinting on target URL.

        Returns dict with:
        - server: Server software detected
        - waf: WAF/protection detected (if any)
        - cdn: CDN detected (if any)
        - managed_hosting: Managed platform detected (if any)
        - headers: Raw response headers
        - technologies: List of detected technologies
        - tls: TLS/SSL information (for HTTPS)
        """
        import urllib.request
        import urllib.error

        result = {
            'server': None,
            'server_version': None,
            'waf': [],
            'cdn': [],
            'managed_hosting': None,
            'technologies': [],
            'headers': {},
            'cookies': [],
            'tls': None,
            'status_code': None,
            'redirect_url': None,
        }

        parsed = urlparse(url)

        # Security: Only allow http/https schemes (B310 - prevent file:// or custom schemes)
        if parsed.scheme not in ('http', 'https'):
            result['error'] = f"Invalid URL scheme: {parsed.scheme}. Only http/https allowed."
            return result

        is_https = parsed.scheme == 'https'

        # Create request with common browser headers
        req = urllib.request.Request(
            url,
            headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'en-US,en;q=0.5',
                'Accept-Encoding': 'identity',
                'Connection': 'close',
            }
        )

        try:
            # Create SSL context for HTTPS
            if is_https:
                ctx = ssl.create_default_context()
                ctx.check_hostname = False
                ctx.verify_mode = ssl.CERT_NONE

                # Get TLS info
                try:
                    with socket.create_connection((parsed.hostname, parsed.port or 443), timeout=timeout) as sock:
                        with ctx.wrap_socket(sock, server_hostname=parsed.hostname) as ssock:
                            cert = ssock.getpeercert(binary_form=True)
                            cipher = ssock.cipher()
                            version = ssock.version()
                            result['tls'] = {
                                'version': version,
                                'cipher': cipher[0] if cipher else None,
                                'bits': cipher[2] if cipher else None,
                            }
                except Exception:
                    pass  # TLS info is optional

                response = urllib.request.urlopen(req, timeout=timeout, context=ctx)  # nosec B310 - scheme validated above
            else:
                response = urllib.request.urlopen(req, timeout=timeout)  # nosec B310 - scheme validated above

            result['status_code'] = response.getcode()

            # Get headers
            headers = {k.lower(): v for k, v in response.headers.items()}
            result['headers'] = dict(response.headers)

            # Check for redirect
            if response.geturl() != url:
                result['redirect_url'] = response.geturl()

            # Parse cookies
            if 'set-cookie' in headers:
                cookies = headers.get('set-cookie', '')
                result['cookies'] = [c.strip() for c in cookies.split(',')]

            # Detect server
            server_header = headers.get('server', '').lower()
            result['server'] = headers.get('server')

            for sig, name in SERVER_SIGNATURES.items():
                if sig in server_header:
                    result['server_version'] = name
                    result['technologies'].append(name)
                    break

            # Detect WAF
            result['waf'] = self._detect_waf(headers, result['cookies'])

            # Detect CDN
            result['cdn'] = self._detect_cdn(headers, server_header)

            # Detect managed hosting
            result['managed_hosting'] = self._detect_managed_hosting(headers, server_header, result['cookies'])

            # Detect technologies from headers
            self._detect_technologies(headers, result)

        except urllib.error.HTTPError as e:
            # Even errors give us useful headers
            result['status_code'] = e.code
            headers = {k.lower(): v for k, v in e.headers.items()}
            result['headers'] = dict(e.headers)
            result['server'] = headers.get('server')

            server_header = headers.get('server', '').lower()
            result['waf'] = self._detect_waf(headers, [])
            result['cdn'] = self._detect_cdn(headers, server_header)
            result['managed_hosting'] = self._detect_managed_hosting(headers, server_header, [])

        except urllib.error.URLError as e:
            result['error'] = str(e.reason)

        except socket.timeout:
            result['error'] = 'Connection timed out'

        except Exception as e:
            result['error'] = f'{type(e).__name__}: {e}'

        return result

    def _detect_waf(self, headers: Dict[str, str], cookies: List[str]) -> List[str]:
        """Detect WAF from headers and cookies."""
        detected = []

        # Check headers
        for header, signatures in WAF_SIGNATURES['headers'].items():
            header_val = headers.get(header, '').lower()
            if header_val:
                if isinstance(signatures, dict):
                    for sig, waf_name in signatures.items():
                        if sig == '' or sig in header_val:
                            if waf_name and waf_name not in detected:
                                detected.append(waf_name)
                elif isinstance(signatures, str) and signatures not in detected:
                    detected.append(signatures)

        # Check cookies
        cookie_str = ' '.join(cookies).lower()
        for cookie_sig, waf_name in WAF_SIGNATURES['cookies'].items():
            if cookie_sig.lower() in cookie_str:
                if waf_name not in detected:
                    detected.append(waf_name)

        return detected

    def _detect_cdn(self, headers: Dict[str, str], server_header: str) -> List[str]:
        """Detect CDN from headers."""
        detected = []

        # Check specific headers
        for header, cdn_info in CDN_SIGNATURES['headers'].items():
            header_val = headers.get(header, '').lower()
            if header_val:
                if isinstance(cdn_info, dict):
                    for sig, cdn_name in cdn_info.items():
                        if sig in header_val and cdn_name not in detected:
                            detected.append(cdn_name)
                elif isinstance(cdn_info, str) and cdn_info not in detected:
                    detected.append(cdn_info)

        # Check server header
        for sig, cdn_name in CDN_SIGNATURES['server'].items():
            if sig in server_header and cdn_name not in detected:
                detected.append(cdn_name)

        return detected

    def _detect_managed_hosting(self, headers: Dict[str, str], server_header: str, cookies: List[str]) -> Optional[str]:
        """Detect managed hosting platform."""
        # Check server header first (most reliable)
        for sig, platform in MANAGED_HOSTING_SIGNATURES['server'].items():
            if sig in server_header:
                return platform

        # Check specific headers
        for header_prefix, platform in MANAGED_HOSTING_SIGNATURES['headers'].items():
            for header in headers:
                if header.lower().startswith(header_prefix.lower()):
                    return platform

        # Check cookies
        cookie_str = ' '.join(cookies).lower()
        for cookie_sig, platform in MANAGED_HOSTING_SIGNATURES['cookies'].items():
            if cookie_sig.lower() in cookie_str:
                return platform

        return None

    def _detect_technologies(self, headers: Dict[str, str], result: Dict[str, Any]):
        """Detect additional technologies from headers."""
        techs = result['technologies']

        # X-Powered-By
        powered_by = headers.get('x-powered-by', '')
        if powered_by:
            if 'php' in powered_by.lower():
                techs.append(f'PHP ({powered_by})')
            elif 'asp.net' in powered_by.lower():
                techs.append(f'ASP.NET ({powered_by})')
            elif 'express' in powered_by.lower():
                techs.append('Express.js')
            elif powered_by not in techs:
                techs.append(powered_by)

        # X-AspNet-Version
        aspnet_ver = headers.get('x-aspnet-version', '')
        if aspnet_ver:
            techs.append(f'ASP.NET {aspnet_ver}')

        # X-Generator
        generator = headers.get('x-generator', '')
        if generator:
            techs.append(generator)

        result['technologies'] = list(set(techs))

    def _format_output(self, target: str, result: Dict[str, Any], label: str) -> str:
        """Format fingerprint results for log output."""
        lines = []
        lines.append("=== Plugin: HTTP Fingerprint ===")
        lines.append(f"Target: {target}")
        if label:
            lines.append(f"Label: {label}")
        lines.append(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}")
        lines.append("=" * 60)
        lines.append("")

        if result.get('error'):
            lines.append(f"ERROR: {result['error']}")
            return '\n'.join(lines)

        # Status
        lines.append(f"HTTP Status: {result.get('status_code', 'N/A')}")

        if result.get('redirect_url'):
            lines.append(f"Redirected to: {result['redirect_url']}")

        # Server
        if result.get('server'):
            lines.append(f"Server: {result['server']}")

        # TLS
        if result.get('tls'):
            tls = result['tls']
            lines.append(f"TLS: {tls.get('version', 'Unknown')} ({tls.get('cipher', 'Unknown')})")

        lines.append("")

        # Managed Hosting (most important for tool decisions)
        if result.get('managed_hosting'):
            lines.append("-" * 40)
            lines.append(f"MANAGED HOSTING DETECTED: {result['managed_hosting']}")
            lines.append("  -> CGI enumeration will be skipped")
            lines.append("  -> Limited vulnerability surface expected")
            lines.append("-" * 40)
            lines.append("")

        # WAF
        if result.get('waf'):
            lines.append(f"WAF/Protection Detected:")
            for waf in result['waf']:
                lines.append(f"  - {waf}")
            lines.append("")

        # CDN
        if result.get('cdn'):
            lines.append(f"CDN Detected:")
            for cdn in result['cdn']:
                lines.append(f"  - {cdn}")
            lines.append("")

        # Technologies
        if result.get('technologies'):
            lines.append(f"Technologies:")
            for tech in result['technologies']:
                lines.append(f"  - {tech}")
            lines.append("")

        lines.append(f"\n=== Completed: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())} ===")

        return '\n'.join(lines)


plugin = HttpFingerprintPlugin()
