import base64
import re
import secrets
from typing import List, cast, Union, Optional
from urllib.parse import urljoin

from packaging import version
from requests import Response

from yawast.reporting.enums import Vulnerabilities
from yawast.scanner.plugins.http import version_checker, response_scanner
from yawast.scanner.plugins.result import Result
from yawast.shared import network


def check_all(url: str, links: List[str]) -> List[Result]:
    results = []

    results += check_version(url)
    results += check_manager(url)
    results += check_cve_2017_12615(url)
    results += check_struts_sample(url)
    results += check_cve_2019_0232(links)

    return results


def get_version(url: str, res: Response, method: Optional[str] = None) -> List[Result]:
    """Check a server response to see if it contains a Tomcat version.

    :param method:
    :param url:
    :param res:
    :return:
    """

    results = []

    body = res.text
    ver = _get_version_from_body(body, res.status_code)
    if ver is not None:
        msg = f"Apache Tomcat version exposed: {ver}"
        if method is not None:
            msg += f" (Via {method})"

        results.append(
            Result(
                msg,
                Vulnerabilities.SERVER_TOMCAT_VERSION,
                url,
                [
                    ver,
                    network.http_build_raw_request(res.request),
                    network.http_build_raw_response(res),
                ],
            )
        )

        results += _check_version_outdated(ver, url, body)

    return results


def check_version(url: str) -> List[Result]:
    """Search for the version of Tomcat used, by using various methods.
    Methods include sending a POST request to a page that likely doesn't
    support POST, sending an invalid HTTP verb, and triggering a 404.

    :param url:
    :return:
    """

    results = []

    results += _check_version_404(url)
    results += _check_version_verb(url)
    results += _check_version_post(url)

    return results


def check_manager(url: str) -> List[Result]:
    results = []

    for p in [urljoin(url, "manager/"), urljoin(url, "host-manager/")]:
        # check for both Tomcat 6, and 7+
        for path in [urljoin(p, "html/"), p]:

            res = network.http_get(path, False)

            body = res.text

            if "<tt>conf/tomcat-users.xml</tt>" in body:
                # we have a finding
                vuln = Vulnerabilities.SERVER_TOMCAT_MANAGER_EXPOSED
                if "host-manager" in path:
                    vuln = Vulnerabilities.SERVER_TOMCAT_HOST_MANAGER_EXPOSED

                results.append(
                    Result(
                        f"Apache Tomcat Manager found: {path}",
                        vuln,
                        path,
                        [
                            network.http_build_raw_request(res.request),
                            network.http_build_raw_response(res),
                        ],
                    )
                )

                # check to see if we can get in with a default password
                results += check_manager_password(url)
            else:
                # if we didn't get a hit, go ahead and scan it to see if there's
                #  anything else that we should be picking up.
                results += response_scanner.check_response(path, res)

    return results


def check_manager_password(url: str) -> List[Result]:
    results = []
    creds = [
        b"tomcat:tomcat",
        b"tomcat:password",
        b"tomcat:",
        b"admin:admin",
        b"admin:password",
        b"admin:",
    ]

    for cred in creds:
        ce = base64.b64encode(cred)

        res = network.http_get(url, False, {"Authorization": ce})
        body = res.text

        if (
            '<font size="+2">Tomcat Web Application Manager</font>' in body
            or '<font size="+2">Tomcat Virtual Host Manager</font>' in body
        ):
            # we got in
            results.append(
                Result(
                    f"Apache Tomcat Weak Manager Password: '{cred}' - {url}",
                    Vulnerabilities.SERVER_TOMCAT_MANAGER_WEAK_PASSWORD,
                    url,
                    [
                        network.http_build_raw_request(res.request),
                        network.http_build_raw_response(res),
                    ],
                )
            )
        else:
            # if we didn't get a hit, go ahead and scan it to see if there's
            #  anything else that we should be picking up.
            results += response_scanner.check_response(url, res)

    return results


def check_cve_2017_12615(url: str) -> List[Result]:
    results = []
    file_name = secrets.token_hex(12)
    check_value = secrets.token_hex(12)

    target = urljoin(url, f"{file_name}.jsp/")
    res_put = network.http_put(target, f"<% out.println({check_value});%>", False)

    if res_put.status_code < 300:
        # code should be 2xx for this to work
        # now we need to check to see if it worked
        created_file = urljoin(url, f"{file_name}.jsp")

        res_get = network.http_get(created_file, False)

        if check_value in res_get.text:
            # we have RCE
            results.append(
                Result(
                    f"Apache Tomcat PUT RCE (CVE-2017-12615): {created_file}",
                    Vulnerabilities.SERVER_TOMCAT_CVE_2017_12615,
                    url,
                    [
                        network.http_build_raw_request(res_put.request),
                        network.http_build_raw_response(res_put),
                        network.http_build_raw_request(res_get.request),
                        network.http_build_raw_response(res_get),
                    ],
                )
            )
        else:
            results += response_scanner.check_response(created_file, res_get)
    else:
        # if we didn't get a hit, go ahead and scan it to see if there's
        #  anything else that we should be picking up.
        results += response_scanner.check_response(target, res_put)

    return results


def check_cve_2019_0232(links: List[str]) -> List[Result]:
    results = []
    targets = []

    for link in links:
        if "cgi-bin" in link:
            if "?" in link:
                targets.append(f"{link}&dir")
            else:
                targets.append(f"{link}?dir")

    for target in targets:
        res = network.http_get(target, False)
        body = res.text

        if "<DIR>" in body:
            # we have a hit
            results.append(
                Result(
                    f"Apache Tomcat RCE (CVE-2019-0232): {target}",
                    Vulnerabilities.SERVER_TOMCAT_CVE_2019_0232,
                    target,
                    [
                        network.http_build_raw_request(res.request),
                        network.http_build_raw_response(res),
                    ],
                )
            )

        results += response_scanner.check_response(target, res)

    return results


def check_struts_sample(url: str) -> List[Result]:
    results = []

    # make sure we have real 404s
    file_good, _ = network.check_404_response(url)
    if not file_good:
        return results

    search = [
        "Struts2XMLHelloWorld/User/home.action",
        "struts2-showcase/showcase.action",
        "struts2-showcase/titles/index.action",
        "struts2-bootstrap-showcase/",
        "struts2-showcase/index.action",
        "struts2-bootstrap-showcase/index.action",
        "struts2-rest-showcase/",
    ]

    for path in search:
        target = urljoin(url, path)

        res = network.http_get(target, False)

        # check for other issues
        results += response_scanner.check_response(target, res)

        if res.status_code == 200:
            results.append(
                Result(
                    f"Struts Sample Found: {target}",
                    Vulnerabilities.SERVER_TOMCAT_STRUTS_SAMPLE,
                    target,
                    [
                        network.http_build_raw_request(res.request),
                        network.http_build_raw_response(res),
                    ],
                )
            )

    return results


def _check_version_404(url: str) -> List[Result]:
    results = []
    rnd = secrets.token_hex(12)

    target = urljoin(url, f"{rnd}.jsp")

    res = network.http_get(target, False)

    if res.status_code > 400:
        results += get_version(target, res, "404 Error Message")

    return results


def _check_version_verb(url: str) -> List[Result]:
    results = []

    res = network.http_custom("XYZ", url)

    if res.status_code > 400:
        results += get_version(url, res, "Invalid HTTP Verb")

    return results


def _check_version_post(url: str) -> List[Result]:
    results = []

    res = network.http_custom("POST", url)

    if res.status_code > 400:
        results += get_version(url, res, "POST to root")

    return results


def _get_version_from_body(body, status_code) -> Union[str, None]:
    if "Apache Tomcat" in body and status_code >= 400:
        # we only care if:
        #  'Apache Tomcat' is in the body
        #  We have a status code that indicates an error
        ver = re.search(r"Apache Tomcat/\d*.\d*.\d*\b", body)

        if ver:
            # split it, and only return the number itself
            return ver.group(0).split("/")[1]

    return None


def _check_version_outdated(ver: str, url: str, body: str) -> List[Result]:
    results = []

    # parse the version, and get the latest version - see if the server is up to date
    ver = cast(version.Version, version.parse(ver))
    curr_version = version_checker.get_latest_version("apache_tomcat", ver)

    if curr_version is not None and curr_version > ver:
        results.append(
            Result(
                f"Apache Tomcat Outdated: {ver} - Current: {curr_version}",
                Vulnerabilities.SERVER_TOMCAT_OUTDATED,
                url,
                [ver, curr_version, body],
            )
        )

    return results
