# -*- coding: utf-8 -*-
"""
Admin Panel Detector - Comprehensive Scanner
Cross-platform (Windows/Linux) admin panel and login page detector
No external dependencies required - uses only Python standard library
"""

from __future__ import absolute_import, unicode_literals

import sys
import re
import time
import threading
import json
import argparse
from collections import defaultdict
try:
    from urllib.parse import urljoin, urlparse
except ImportError:
    from urlparse import urljoin, urlparse

try:
    import urllib.request as urllib2
except ImportError:
    import urllib2


# HTTP Headers
HEADERS = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch",
    "Accept-Language": "en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4,zh-TW;q=0.2",
    "Connection": "keep-alive",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.2357.130 Safari/537.36",
}

DEFAULT_PATHS = [
    # Standard admin paths
    "admin", "admin/", "admin.asp", "admin.aspx", "admin.htm", "admin.html", "admin.php", "admin.jsp",
    "admin1/", "admin1.asp", "admin1.aspx", "admin1.html", "admin1.php",
    "admin2/", "admin2.asp", "admin2.aspx", "admin2.html", "admin2.php",
    "admin2/index/", "admin2/login.asp", "admin2/login.aspx", "admin2/login.php",
    "administrator", "administrator/", "administrator.asp", "administrator.aspx", "administrator.html", "administrator.php",
    "administrator/login.asp", "administrator/login.aspx", "administrator/login.htm", "administrator/login.html", "administrator/login.php",
    "adminpanel/", "adm/", "adm.asp", "adm.aspx", "adm.htm", "adm.html", "adm.php",
    "admincp/", "admincp.php", "admin-login/", "admin-login.htm", "admin-login.html",
    "backend/", "control/", "cpanel/", "manage/", "manager/", "manager/html",
    "sysadmin.asp", "sysadmin.aspx", "sysadmin.html", "sysadmin.php", "sysadmin", "systemadmin",
    "superadmin/", "myadmin/", "siteadmin/", "admins.php",
    "webadmin", "webadmin/", "webadmin.asp", "webadmin.aspx", "webadmin.htm", "webadmin.html", "webadmin.php",
    "admin-login.asp", "admin-login.aspx", "admin-login.php", "admin-login/",
    
    # Login paths
    "login", "login/", "login.asp", "login.aspx", "login.htm", "login.html", "login.jsp", "login.php", "login.do",
    "login/index", "login/index.php", "login/login", "login/login.cgi", "login/login.htm", "login/login.php", "login/login.do?",
    "login/admin", "login_manage", "loginmanage", "userlogin", "memberlogin", "managelogin", "oalogin", "weblogin",
    
    # Admin sub-paths
    "admin/admin/", "admin/admin.asp", "admin/admin.aspx", "admin/admin.html", "admin/admin.php",
    "admin/index.asp", "admin/index.aspx", "admin/index.htm", "admin/index.html", "admin/index.php",
    "admin/home.asp", "admin/home.aspx", "admin/home.htm", "admin/home.html", "admin/home.php",
    "admin/login", "admin/login/", "admin/login.asp", "admin/login.aspx", "admin/login.htm", "admin/login.html", "admin/login.jsp", "admin/login.php",
    "admin/main.php", "admin/welcome.php", "admin/default.php", "admin/checklogin.php",
    "admin/default", "admin/edit", "admin/inc", "admin/manage", "admin/member", "admin/user",
    "admin.php?m=Admin&c=Index&a=login",
    
    # CMS/Framework-specific paths
    "wp-admin", "wp-admin/", "wp-login.php", "wp-login/", "wordpress/wp-admin/", "wordpress/wp-login.php",
    "joomla/administrator/",
    "drupal/user/login",
    "typo3/typo3/",
    "ecshop/admin/",
    "dedecms/dede/", "dede/", "dede/login.php",
    "plus/", "plus/admin.php",
    "discuz/admin.php", "forum/admin.php",
    "thinkphp/index.php/admin/",
    "tp5/public/index.php/admin/",
    "laravel/public/admin/",
    "yii/backend/web/", "yii/web/admin/",
    
    # Known product/admin interfaces
    "xxl-job-admin/login",
    "druid/login.html",
    "nacos",
    "geoserver", "geoserver/web/",
    "seeyon",
    "console", "console/", "console/index.html", "console/login/", "console/login/LoginForm.jsp",
    "phpmyadmin", "phpmyadmin/", "pma/", "myadmin/", "phpminiadmin.php",
    "OpenVPN: openvpn-monitor/",
    "Horizon: horizon/auth/login/?next=/horizon/",
    "Jenkins", "Grafana", "Harbor", "Portainer", "Kubernetes Dashboard", "MinIO Console", "Jira", "Confluence",
    "WebLogic: console/login/LoginForm.jsp",
    "JBoss", "GlassFish", "Dubbo Admin", "NginxWebUI",
    "FortiManager", "RabbitMQ",
    "Swagger: swagger-ui.html",
    
    # Chinese OA/Enterprise Systems
    "whir_system/module/security/ezEIP_Login.aspx",
    "OperaLogin/Welcome.do",
    "default/showLogon.do",
    "toLogin",
    "ioffice/Login.aspx",
    "zentao",
    "cn/admin/login",
    "管理/", "后台/", "后台管理/", "后台登录/", "管理员/", "系统管理/", "登录/", "登陆/", "登录后台/", "后台入口/", "网站后台/",
    "guanli", "gl", "dede", "lyb", "oa", "office", "weihu", "windfinance", "cnzz",
    
    # Generic/Other paths
    "account", "account.asp", "account.aspx", "account.htm", "account.html", "account.php", "account/login",
    "user", "user/", "user.asp", "user.aspx", "user.htm", "user.html", "user.php",
    "member", "member/", "member.asp", "member.aspx", "member.htm", "member.html", "member.php",
    "panel", "panel/", "panel.asp", "panel.aspx", "panel.php",
    "dashboard/", "portal/", "portal/login", "registration/", "root/", "home", "main", "hub", "hub/login", "web", "web/login",
    "api/login", "api/systeminfo", "app/login", "cfg/login", "cgi-bin/home", "cgi-bin/login", "cgi-bin/luci/admin/system/admin",
    "index", "index.html", "index.php", "index.asp", "index.aspx", "index.jsp", "index.action", "index.do",
    "index/login", "index/user/login", "index.php/login",
    "ui/auth", "ui/index.html", "ui/login", "ui/login.action", "ui/login/",
    "system/login", "system/", "pages/login", "gateway",
    "vpn/index.html", "remote/login",
]

# Login page CSS/Content conditions
LOGIN_CSS_CONDITIONS = [
    'type="password"', 'type=\'password\'', 'type:"password"',
    'Type="password"', 'Type=\'password\'', 'input[type=password]',
    '"a-input-password"', 'type=password', 'type: \\"password\\"',
    'Type:"password"', 'type="Password"',
    'logIn:"登录",username:"账号",password:"密码"',
    '<title>金蝶云', '"input-password"', '/_app.config.js?v=',
    'auth.password'
]

# Not-login conditions (false positive filters)
NOT_LOGIN_CONDITIONS = [
    '页面不存在</title>',
    '<h1>页面不存在.</h1>',
    '<h1>页面不存在</h1>',
    '<h2>页面不存在.</h2>',
    '<h2>页面不存在</h2>',
    '<title>AIHelp Web Portal</title>'
]

# CAPTCHA conditions
CAPTCHA_CSS_CONDITIONS = [
    '请输入验证码', 'placeholder="验证码"', 'placeholder="captcha"',
    'title="验证码"', 'title="captcha"', 'alt="验证码"',
    'alt="captcha"', 'name="captcha"', '请输入手机验证码',
    'login.VerificationCode"', 'validateEvent', 'validateState'
]

# Admin condition regex patterns (100+ patterns)
PATTERNS_BY_TITLES = [
    # Generic admin indicators
    r'<title>.*后台.*</title>',
    r'<title>.*登录.*</title>',
    r'<title>.*管理.*</title>',
    r'<title>.*控制面板.*</title>',
    r'<title>.*\bLogin\b.*</title>',
    r'<title>.*登入.*</title>',
    r'<title>.*平台.*</title>',
    r'<title>.*admin.*</title>',
    r'<title>.*OA协同.*</title>',
    r'<title>.*身份认证平台.*</title>',
    r'<title>.*系统</title>',
    r'<title>.*低代码.*(平台|中台)</title>',
    r'<div.*后台.*</div>',
    r'<h1.*登录.*</h1>', r'<h1.*后台.*</h1>',
    r'<h2.*登录.*</h2>', r'<h2.*后台.*</h2>',
    r'<button.*登录</button>', r'<button.*登录系统</button>',
    r'<div.*>(\s+)?(账号)?登录(\s+)?</div>',
    r'"\b系统登录\b"',
    r'<a.*>忘记密码</a>',
    r'重新登录',
    r'扫码登录',
    r'"input-password"',
    r'auth.password.*Authorization',
    
    # Specific products
    r'<title>任务调度中心</title>',  # XXL-JOB
    r'<title>druid.*</title>',  # Druid
    r'<title>Nacos</title>',  # Nacos
    r'<title>.*WordPress</title>',  # WordPress
    r'<title>GeoServer',  # GeoServer
    r'<title>Oracle WebLogic Server Administration Console</title>',  # WebLogic
    r'<title>Portainer</title>',  # Portainer
    r'<title>Harbor</title>',  # Harbor
    r'<title>Sign in \[Jenkins\]</title>',  # Jenkins
    r'<title>Grafana</title>',  # Grafana
    r'<title>GlassFish',  # GlassFish
    r'<title>JBoss', r'<title>Welcome to JBoss',  # JBoss
    r'<title>Kafka Manager</title>',  # Kafka Manager
    r'<title>Kubernetes Dashboard</title>',  # Kubernetes Dashboard
    r'<title>nginxWebUI</title>',  # nginxWebUI
    r'<title>DSS</title>',  # DSS
    r'<title>Dubbo Admin</title>',  # Dubbo Admin
    r'<title>身份验证 - GLPI</title>',  # GLPI
    r'<title>rConfig - Configuration Management</title>',  # rConfig
    r'<title>JumpServer</title>',  # JumpServer
    r'swagger-ui.html',  # Swagger
    
    # Chinese enterprise systems
    r'<title>万户网络', r'<title>Wanhu ezOFFICE</title>',  # Wanhu
    r'<title>金蝶云',  # Kingdee Cloud
    r'<title>致远', r'sys/ui/extend/theme/default/style/profile.css', r'/Scripts/jquery.landray.dialog.js',  # Seeyon
    r'content="Weaver E-Mobile', r'<title>泛微云桥e-Bridge</title>',  # Weaver
    r'<title>YONYOU NC</title>', r'<title>Yonyou UAP</title>', r'<TITLE>用友U8CRM</TITLE>', r'<title>用友GRP-U8', r'<title>U8Cloud</title>',  # Yonyou
    r'<TITLE>畅捷通CRM</TITLE>', r'<title>畅捷通 T+</title>',  # Chanjet
    r'src=/iOffice/js/iOffice.js', r'<title>iOffice.net</title>',  # iOffice
    r'<title>Jeecg-Boot快速开发平台</title>', r'<title>Jeecg-Boot 快速开发平台</title>',
    r'<title>JeecgBoot 企业级低代码平台</title>', r'<title>Jeecg-Boot 企业级快速开发平台</title>',
    r'<title>Jeecg 快速开发平台</title>', r'<title>JeecgBoot 企业级快速开发平台</title>',  # Jeecg-Boot
    r'金和协同管理平台',  # Jinhe
    r'<title>汉得SRM云平台</title>',  # Hand SRM
    r'<title>明源地产ERP</title>',  # Mingyuan ERP
    r'民政OA - v\d+\.\d+',  # Minzheng OA
    r'<title>碧海威L7云路由无线运营版</title>',  # BiHaiWei L7
    
    # Security products
    r'<title>ad.sangfor.com', r'<title>SANGFOR 数据中心</title>',
    r'<title>SANGFOR | NGAF</title>', r'<title>SANGFOR | AF ',  # Sangfor
    r'<title>奇安信VPN</title>',  # QiAnXin VPN
    r'<title>网神SecGate 3600防火墙</title>',  # WangShen SecGate
    r'<title>MilesightVPN</title>',  # MilesightVPN
    r'<title>明御安全网关</title>',  # MingYu Security Gateway
    r'<title>360新天擎</title>',  # 360 TianQing
    r'<title>博华网龙信息安全一体机</title>', r'<title>博华网龙防火墙</title>',  # BoHua WangLong
    r'<title>FortiManager',  # FortiManager
    r'<span id="sysName">华测监测预警系统2.2</span>',  # HuaCe Monitoring
    
    # Email/Messaging
    r'<title>Microsoft Exchange - Outlook Web Access</title>',  # Exchange OWA
    r'dahuaDefined/headCommon.js',  # Dahua
    
    # Other
    r'<title>MinIO Console</title>',  # MinIO
    r'<title>System Dashboard - Jira-Test</title>',  # Jira
    r'<title>Log In - Confluence</title>',  # Confluence
    r'rabbitmqlogo.svg',  # RabbitMQ
    r'</span>移动系统管理',  # Mobile System Management
    r'孚盟云 - 用户登录',  # FuMeng Cloud
    r'<title>智能云网关注册管理平台</title>',  # Smart Cloud Network
    r'<title>Login @ Reporter</title>',  # Reporter
    r'zz-sso-server-web',  # SSO Server
    r'/images/login_logo@2x.png',  # Login Logo
    r'你在看什么呢？我写的代码好看吗',  # Hidden easter egg
    r'<script src=/cdn/babel-polyfill/polyfill_',  # Babel Polyfill
    r'/local/connect_notfound/connect_notfound\.html\?from=',  # URL-based redirect
    
    # Welcome patterns
    r'Welcome to Weblogic Application Server',
]


def check_admin_conditions(html_content):
    """Check if HTML content matches admin panel patterns"""
    for pattern in PATTERNS_BY_TITLES:
        if re.search(pattern, html_content, re.IGNORECASE):
            return True
    return False


def check_login_conditions(html_content):
    """Check if HTML content matches login page conditions"""
    html_bracketed = '[' + html_content.replace('\n', ' ') + ']'
    for condition in LOGIN_CSS_CONDITIONS:
        if condition in html_bracketed:
            return True
    return False


def check_not_login_conditions(html_content):
    """Check if HTML content should be excluded (false positives)"""
    for condition in NOT_LOGIN_CONDITIONS:
        if condition in html_content:
            return True
    return False


def check_captcha_conditions(html_content):
    """Check if HTML content contains CAPTCHA"""
    html_bracketed = '[' + html_content.replace('\n', ' ') + ']'
    for condition in CAPTCHA_CSS_CONDITIONS:
        if condition in html_bracketed:
            return True
    return False


def extract_title(html_content):
    """Extract page title from HTML"""
    match = re.search(r'<title>(.*?)</title>', html_content, re.IGNORECASE)
    return match.group(1) if match else "Unknown"


def fetch_url(url, timeout=10):
    """Fetch URL content using urllib2 (no external dependencies)"""
    try:
        req = urllib2.Request(url, headers=HEADERS)
        response = urllib2.urlopen(req, timeout=timeout)
        html = response.read()
        
        # Try to decode
        try:
            html = html.decode('utf-8')
        except:
            try:
                html = html.decode('gbk')
            except:
                html = html.decode('latin-1')
        
        return html, response.getcode()
    except Exception as e:
        return None, None


class AdminPanelScanner:
    def __init__(self, base_url, paths=None, timeout=10):
        self.base_url = self.format_url(base_url)
        self.results = []
        self.visited_urls = set()
        self.lock = threading.Lock()
        self.paths = paths or DEFAULT_PATHS
        self.timeout = timeout
        self.response_hashes = {}  # For deduplication
        
    def format_url(self, url):
        """Normalize URL"""
        url = url.strip()
        if not url.startswith('http://') and not url.startswith('https://'):
            url = 'http://' + url
        return url.rstrip('/')
    
    def normalize_url(self, url):
        """Normalize URL for deduplication"""
        # Remove trailing slash
        url = url.rstrip('/')
        # Remove query parameters for comparison
        if '?' in url:
            url = url.split('?')[0]
        # Lowercase for comparison
        return url.lower()
    
    def compute_content_hash(self, html_content):
        """Compute a simplified hash of HTML content for deduplication"""
        # Remove dynamic content (timestamps, tokens, etc.)
        content = re.sub(r'<meta[^>]*>', '', html_content)
        content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL)
        content = re.sub(r'<!--.*?-->', '', content, flags=re.DOTALL)
        # Normalize whitespace
        content = re.sub(r'\s+', ' ', content)
        # Compute hash
        return hash(content.strip())
    
    def is_duplicate(self, url, html_content):
        """Check if this result is a duplicate based on content similarity"""
        content_hash = self.compute_content_hash(html_content)
        
        # Check if we've seen this content before
        for existing_hash, existing_url in self.response_hashes.items():
            if existing_hash == content_hash:
                # Same content, check if URLs are similar
                normalized_new = self.normalize_url(url)
                normalized_existing = self.normalize_url(existing_url)
                
                # If URLs differ only by trailing slash or query params, it's a duplicate
                if normalized_new == normalized_existing:
                    return True
                # If URLs are very similar (one is subset of another), likely duplicate
                if normalized_new in normalized_existing or normalized_existing in normalized_new:
                    return True
        
        # Not a duplicate, record this hash
        self.response_hashes[content_hash] = url
        return False
    
    def scan_path(self, path):
        """Scan a single path"""
        full_url = urljoin(self.base_url + '/', path)
        
        if full_url in self.visited_urls:
            return
        
        with self.lock:
            if full_url in self.visited_urls:
                return
            self.visited_urls.add(full_url)
        
        html, status_code = fetch_url(full_url, timeout=self.timeout)
        
        if html is None or status_code is None:
            return
        
        if status_code not in [200, 401]:
            return
        
        # Check not-login conditions first (false positive filter)
        if check_not_login_conditions(html):
            return
        
        # Check login conditions
        is_login = check_login_conditions(html)
        
        # Check admin conditions
        is_admin = check_admin_conditions(html)
        
        # Check CAPTCHA
        has_captcha = check_captcha_conditions(html)
        
        # Determine confidence
        if is_admin:
            confidence = "High"
        elif is_login:
            confidence = "Medium"
        else:
            return  # Not an admin/login page
        
        # Check for duplicates
        with self.lock:
            if self.is_duplicate(full_url, html):
                return  # Skip duplicate
        
        title = extract_title(html)
        
        result = {
            'url': full_url,
            'title': title,
            'confidence': confidence,
            'has_captcha': has_captcha,
            'type': 'Admin Panel' if is_admin else 'Login Page',
        }
        
        with self.lock:
            self.results.append(result)
    
    def scan(self, max_threads=10):
        """Scan all paths using multi-threading"""
        print("[+] Starting admin panel scan for: {}".format(self.base_url))
        print("[+] Scanning {} paths...".format(len(self.paths)))
        
        start_time = time.time()
        threads = []
        
        # Create thread pool
        path_index = [0]
        
        def worker():
            while True:
                with self.lock:
                    if path_index[0] >= len(self.paths):
                        break
                    path = self.paths[path_index[0]]
                    path_index[0] += 1
                
                self.scan_path(path)
        
        # Start threads
        for i in range(min(max_threads, len(self.paths))):
            t = threading.Thread(target=worker)
            t.start()
            threads.append(t)
        
        # Wait for all threads
        for t in threads:
            t.join()
        
        elapsed = time.time() - start_time
        print("[+] Scan completed in {:.2f} seconds".format(elapsed))
        print("[+] Found {} potential admin/login pages".format(len(self.results)))
        
        return self.results
    
    def deduplicate_results(self):
        """Deduplicate results based on URL similarity and content hash"""
        if not self.results:
            return
        
        unique_results = []
        seen_urls = set()
        seen_titles = {}
        
        for result in self.results:
            url = result['url']
            title = result['title']
            
            # Normalize URL for comparison
            normalized_url = self.normalize_url(url)
            
            # Skip if we've seen this exact normalized URL
            if normalized_url in seen_urls:
                continue
            
            # Check if title is duplicate with high similarity
            title_key = title.lower().strip()
            if title_key in seen_titles:
                existing = seen_titles[title_key]
                # If same title and similar confidence, keep only one
                if existing['confidence'] == result['confidence']:
                    # Check URL similarity
                    existing_normalized = self.normalize_url(existing['url'])
                    if existing_normalized in normalized_url or normalized_url in existing_normalized:
                        continue
            
            seen_urls.add(normalized_url)
            seen_titles[title_key] = result
            unique_results.append(result)
        
        self.results = unique_results
    
    def print_results(self):
        """Print scan results"""
        # Deduplicate before printing
        self.deduplicate_results()
        
        if not self.results:
            print("\n[-] No admin panels or login pages found")
            return
        
        print("\n" + "="*80)
        print("SCAN RESULTS")
        print("="*80)
        
        # Sort by confidence
        confidence_order = {"High": 0, "Medium": 1, "Low": 2}
        sorted_results = sorted(self.results, key=lambda x: confidence_order.get(x['confidence'], 3))
        
        for i, result in enumerate(sorted_results, 1):
            print("\n[{}] Found: {}".format(i, result['url']))
            print("    Type: {}".format(result['type']))
            print("    Confidence: {}".format(result['confidence']))
            print("    CAPTCHA: {}".format("Yes" if result['has_captcha'] else "No"))
            print("    Title: {}".format(result['title']))
        
        print("\n" + "="*80)
        print("SUMMARY")
        print("="*80)
        print("Total found: {}".format(len(self.results)))
        print("High confidence: {}".format(sum(1 for r in self.results if r['confidence'] == 'High')))
        print("Medium confidence: {}".format(sum(1 for r in self.results if r['confidence'] == 'Medium')))
        print("With CAPTCHA: {}".format(sum(1 for r in self.results if r['has_captcha'])))


def load_paths_file(path):
    with open(path, "r", encoding="utf-8") as f:
        return [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]


def main():
    parser = argparse.ArgumentParser(
        description="Scan common admin panels and login pages for a target URL.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""Examples:
  python admin_scanner.py https://example.com
  python admin_scanner.py https://example.com --output workspace/admin_scan_results.json
  python admin_scanner.py https://example.com --threads 20 --timeout 5 --paths-file paths.txt
        """,
    )
    parser.add_argument("target_url", help="Target base URL, for example https://example.com")
    parser.add_argument("--output", "-o", default="admin_scan_results.json",
                        help="JSON output path. Default: admin_scan_results.json")
    parser.add_argument("--threads", "-t", type=int, default=10,
                        help="Maximum worker threads. Default: 10")
    parser.add_argument("--timeout", type=float, default=10,
                        help="Per-request timeout in seconds. Default: 10")
    parser.add_argument("--paths-file",
                        help="Optional newline-delimited paths file. Comments starting with # are ignored.")
    parser.add_argument("--json-only", action="store_true",
                        help="Suppress human-readable result details and only write JSON output.")
    args = parser.parse_args()

    paths = load_paths_file(args.paths_file) if args.paths_file else None
    scanner = AdminPanelScanner(args.target_url, paths=paths, timeout=args.timeout)
    results = scanner.scan(max_threads=max(args.threads, 1))
    if not args.json_only:
        scanner.print_results()

    with open(args.output, 'w', encoding='utf-8') as f:
        json.dump(results, f, ensure_ascii=False, indent=2)
    if results:
        print("\n[+] Results saved to: {}".format(args.output))


if __name__ == '__main__':
    main()
