import base64
import binascii
import hashlib
import json
import os
import fcntl
import re
import shutil
import socket
import subprocess
import sys
import time
import datetime
from pathlib import Path

import requests

APACHE_CONF_DIRS = [
    "/www/server/panel/vhost/apache"
]

def is_ipv4(ip):
    '''
        @name 是否是IPV4地址
        @author hwliang
        @param ip<string> IP地址
        @return True/False
    '''
    # 验证基本格式
    if not re.match(r"^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$", ip):
        return False

    # 验证每个段是否在合理范围
    try:
        socket.inet_pton(socket.AF_INET, ip)
    except AttributeError:
        try:
            socket.inet_aton(ip)
        except socket.error:
            return False
    except socket.error:
        return False
    return True


def is_ipv6(ip):
    '''
        @name 是否为IPv6地址
        @author hwliang
        @param ip<string> 地址
        @return True/False
    '''
    # 验证基本格式
    if not re.match(r"^[\w:]+$", ip):
        return False

    # 验证IPv6地址
    try:
        socket.inet_pton(socket.AF_INET6, ip)
    except socket.error:
        return False
    return True


def check_ip(ip):
    return is_ipv4(ip) or is_ipv6(ip)

def find_apache_conf_files(keyword):
    """查找 Apache 主配置和 vhost 文件"""
    files = set()
    for base in APACHE_CONF_DIRS:
        base_path = Path(base)
        if not base_path.exists():
            continue
        for f in base_path.rglob("*.conf"):
            with open(f, "r") as file:
                content = file.read()
                # 检查是否包含 ServerName 或 ServerAlias 指令
                if keyword in content:
                    files.add(str(f))
    return list(files)


def insert_location_into_vhost(file_path, keyword, verify_file):
    LOCATION_BLOCK = [
        "    <Location /.well-known/acme-challenge/{}>\n".format(verify_file),
        "        Require all granted\n",
        "        Header set Content-Type \"text/plain\"\n",
        "    </Location>\n",
        "    Alias /.well-known/acme-challenge/{} /tmp/{}\n".format(verify_file, verify_file),
    ]

    path = Path(file_path)
    backup = path.with_suffix(path.suffix + ".bak")
    shutil.copy(path, backup)

    with open(path, "r") as f:
        lines = f.readlines()

    new_lines = []
    in_vhost = False
    hit_vhost = False
    location_exists = False

    for line in lines:
        stripped = line.strip()

        if stripped.lower().startswith("<virtualhost"):
            in_vhost = True
            hit_vhost = False
            location_exists = False

        if in_vhost:
            low = stripped.lower()
            if (low.startswith("servername") or low.startswith("serveralias")) and keyword in stripped:
                hit_vhost = True

            if "<location /.well-known/acme-challenge/>" in low:
                location_exists = True

            if stripped.lower() == "</virtualhost>":
                if hit_vhost and not location_exists:
                    new_lines.extend(LOCATION_BLOCK)
                in_vhost = False

        new_lines.append(line)

    with open(path, "w") as f:
        f.writelines(new_lines)

    return True

def find_nginx_files_by_servername(keyword):
    """通过 nginx -T 找到包含 server_name 的配置文件"""
    result = subprocess.run(
        ["nginx", "-T"],
        stderr=subprocess.STDOUT,
        stdout=subprocess.PIPE,
        text=True,
        check=True
    )

    files = set()
    current_file = None

    for line in result.stdout.splitlines():
        if line.startswith("# configuration file"):
            current_file = line.split()[-1].rstrip(":")
        if "server_name" in line and keyword in line:
            if current_file:
                files.add(current_file)

    return list(files)

def insert_location_into_server(file_path, keyword, verify_file, verify_content):
    path = Path(file_path)
    backup = path.with_suffix(path.suffix + ".bak")

    shutil.copy(path, backup)

    with open(path, "r") as f:
        lines = f.readlines()

    new_lines = []
    brace_level = 0
    in_server = False
    hit_server = False
    location_exists = False

    LOCATION_BLOCK = [
        "    location = /.well-known/acme-challenge/{} {{\n".format(verify_file),
        "        default_type text/plain;\n",
        "        return 200 \"{}\";\n".format(verify_content),
        "    }\n"
    ]

    for i, line in enumerate(lines):
        stripped = line.strip()

        # server 开始
        if stripped.startswith("server"):
            in_server = True
            hit_server = False
            location_exists = False

        if in_server:
            brace_level += line.count("{")
            brace_level -= line.count("}")

            if "server_name" in line and keyword in line:
                hit_server = True

            if "location /.well-known/acme-challenge/" in line:
                location_exists = True

            # server 结束
            if brace_level == 0:
                if hit_server and not location_exists:
                    new_lines.extend(LOCATION_BLOCK)
                in_server = False

        new_lines.append(line)

    with open(path, "w") as f:
        f.writelines(new_lines)

    return True

class AutoApplyIPSSL:
    # 请求到ACME接口
    def __init__(self):
        self._wait_time = 5
        self._max_check_num = 15
        self._url = 'https://acme-v02.api.letsencrypt.org/directory'
        self._bits = 2048
        self._conf_file_v2 = '/www/server/panel/config/letsencrypt_v2.json'
        self._apis = None
        self._replay_nonce = None
        self._config = self.read_config()


    # 取接口目录
    def get_apis(self):
        if not self._apis:
            # 尝试从配置文件中获取
            api_index = "Production"
            if not 'apis' in self._config:
                self._config['apis'] = {}
            if api_index in self._config['apis']:
                if 'expires' in self._config['apis'][api_index] and 'directory' in self._config['apis'][api_index]:
                    if time.time() < self._config['apis'][api_index]['expires']:
                        self._apis = self._config['apis'][api_index]['directory']
                        return self._apis

            # 尝试从云端获取
            res = requests.get(self._url)
            if not res.status_code in [200, 201]:
                result = res.json()
                if "type" in result:
                    if result['type'] == 'urn:acme:error:serverInternal':
                        raise Exception('Service is closed for maintenance or internal error occurred, check <a href="https://letsencrypt.status.io/" target="_blank" class="btlink">https://letsencrypt.status.io/</a> .')
                raise Exception(res.content)
            s_body = res.json()
            self._apis = {}
            self._apis['newAccount'] = s_body['newAccount']
            self._apis['newNonce'] = s_body['newNonce']
            self._apis['newOrder'] = s_body['newOrder']
            self._apis['revokeCert'] = s_body['revokeCert']
            self._apis['keyChange'] = s_body['keyChange']

            # 保存到配置文件
            self._config['apis'][api_index] = {}
            self._config['apis'][api_index]['directory'] = self._apis
            self._config['apis'][api_index]['expires'] = time.time() + \
                86400  # 24小时后过期
            self.save_config()
        return self._apis

    def acme_request(self, url, payload):
        headers = {}
        payload = self.stringfy_items(payload)

        if payload == "":
            payload64 = payload
        else:
            payload64 = self.calculate_safe_base64(json.dumps(payload))
        protected = self.get_acme_header(url)
        protected64 = self.calculate_safe_base64(json.dumps(protected))
        signature = self.sign_message(
            message="{0}.{1}".format(protected64, payload64))  # bytes
        signature64 = self.calculate_safe_base64(signature)  # str
        data = json.dumps(
            {"protected": protected64, "payload": payload64,
                "signature": signature64}
        )
        headers.update({"Content-Type": "application/jose+json"})
        response = requests.post(url, data=data.encode("utf8"), headers=headers)
        # 更新随机数
        self.update_replay_nonce(response)
        return response

    # 更新随机数
    def update_replay_nonce(self, res):
        replay_nonce = res.headers.get('Replay-Nonce')
        if replay_nonce:
            self._replay_nonce = replay_nonce

    def stringfy_items(self, payload):
        if isinstance(payload, str):
            return payload

        for k, v in payload.items():
            if isinstance(k, bytes):
                k = k.decode("utf-8")
            if isinstance(v, bytes):
                v = v.decode("utf-8")
            payload[k] = v
        return payload

    # 转为无填充的Base64
    def calculate_safe_base64(self, un_encoded_data):
        if sys.version_info[0] == 3:
            if isinstance(un_encoded_data, str):
                un_encoded_data = un_encoded_data.encode("utf8")
        r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b"=")
        return r.decode("utf8")

    # 获请ACME请求头
    def get_acme_header(self, url):
        header = {"alg": "RS256", "nonce": self.get_nonce(), "url": url}
        if url in [self._apis['newAccount'], 'GET_THUMBPRINT']:
            from cryptography.hazmat.backends import default_backend
            from cryptography.hazmat.primitives import serialization
            private_key = serialization.load_pem_private_key(
                self.get_account_key().encode(),
                password=None,
                backend=default_backend(),
            )
            public_key_public_numbers = private_key.public_key().public_numbers()

            exponent = "{0:x}".format(public_key_public_numbers.e)
            exponent = "0{0}".format(exponent) if len(
                exponent) % 2 else exponent
            modulus = "{0:x}".format(public_key_public_numbers.n)
            jwk = {
                "kty": "RSA",
                "e": self.calculate_safe_base64(binascii.unhexlify(exponent)),
                "n": self.calculate_safe_base64(binascii.unhexlify(modulus)),
            }
            header["jwk"] = jwk
        else:
            header["kid"] = self.get_kid()
        return header

    def get_nonce(self, force=False):
        # 如果没有保存上一次的随机数或force=True时则重新获取新的随机数
        if not self._replay_nonce or force:
            response = requests.get(
                self._apis['newNonce'],
            )
            self._replay_nonce = response.headers["Replay-Nonce"]
        return self._replay_nonce

    def analysis_private_key(self, key_pem, password=None):
        """
        解析私钥
        :param key_pem: 私钥内容
        :param password: 私钥密码
        :return: 私钥对象
        """
        try:
            from cryptography.hazmat.backends import default_backend
            from cryptography.hazmat.primitives import serialization
            private_key = serialization.load_pem_private_key(
                key_pem.encode(),
                password=password,
                backend=default_backend()
            )
            return private_key
        except:
            return None

    def sign_message(self, message):
        from cryptography.hazmat.primitives import hashes
        from cryptography.hazmat.primitives.asymmetric import padding

        pk = self.analysis_private_key(self.get_account_key())
        return pk.sign(message.encode("utf8"), padding.PKCS1v15(), hashes.SHA256())

    # 获用户取密钥对
    def get_account_key(self):
        if not 'account' in self._config:
            self._config['account'] = {}
        k = "Production"
        if not k in self._config['account']:
            self._config['account'][k] = {}

        if not 'key' in self._config['account'][k]:
            self._config['account'][k]['key'] = self.create_key()
            if type(self._config['account'][k]['key']) == bytes:
                self._config['account'][k]['key'] = self._config['account'][k]['key'].decode()
            self.save_config()
        return self._config['account'][k]['key']

    def create_key(self, key_type='RSA'):
        from cryptography.hazmat.primitives import serialization
        from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519

        if key_type == 'RSA':
            private_key = rsa.generate_private_key(
                public_exponent=65537,
                key_size=self._bits
            )
        elif key_type == 'EC':
            private_key = ec.generate_private_key(ec.SECP256R1())
        elif key_type == 'ED25519':
            private_key = ed25519.Ed25519PrivateKey.generate()
        else:
            raise ValueError(f"Unsupported key type: {key_type}")

        private_key_pem = private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption()
        )
        return private_key_pem

    def get_kid(self, force=False):
        #如果配置文件中不存在kid或force = True时则重新注册新的acme帐户
        if not 'account' in self._config:
            self._config['account'] = {}
        k = "Production"
        if not k in self._config['account']:
            self._config['account'][k] = {}

        if not 'kid' in self._config['account'][k]:
            self._config['account'][k]['kid'] = self.register()
            self.save_config()
            time.sleep(3)
            self._config = self.read_config()
        return self._config['account'][k]['kid']

    # 读配置文件
    def read_config(self):
        if not os.path.exists(self._conf_file_v2):
            self._config = {'orders': {}, 'account': {}, 'apis': {}, 'email': None}
            self.save_config()
            return self._config
        with open(self._conf_file_v2, 'r') as f:
            fcntl.flock(f, fcntl.LOCK_SH)  # 加锁
            tmp_config = f.read()
            fcntl.flock(f, fcntl.LOCK_UN)  # 解锁
            f.close()
        if not tmp_config:
            return self._config
        try:
            self._config = json.loads(tmp_config)
        except:
            self.save_config()
            return self._config
        return self._config

    # 写配置文件
    def save_config(self):
        fp = open(self._conf_file_v2, 'w+')
        fcntl.flock(fp, fcntl.LOCK_EX)  # 加锁
        fp.write(json.dumps(self._config))
        fcntl.flock(fp, fcntl.LOCK_UN)  # 解锁
        fp.close()
        return True

    # 注册acme帐户
    def register(self, existing=False):
        if not 'email' in self._config:
            self._config['email'] = 'demo@yakpanel.com'
        if existing:
            payload = {"onlyReturnExisting": True}
        elif self._config['email']:
            payload = {
                "termsOfServiceAgreed": True,
                "contact": ["mailto:{0}".format(self._config['email'])],
            }
        else:
            payload = {"termsOfServiceAgreed": True}

        res = self.acme_request(url=self._apis['newAccount'], payload=payload)

        if res.status_code not in [201, 200, 409]:
            raise Exception("Failed to register ACME account: {}".format(res.json()))
        kid = res.headers["Location"]
        return kid

    def create_csr(self, ips):
        from cryptography import x509
        from cryptography.x509.oid import NameOID
        from cryptography.hazmat.primitives.asymmetric import rsa
        from cryptography.hazmat.primitives import hashes
        from cryptography.hazmat.primitives import serialization
        import ipaddress


        # 生成私钥
        pk = self.create_key()
        private_key = serialization.load_pem_private_key(pk, password=None)

        # IP证书不需要CN
        csr_builder = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([]))
        # 添加 subjectAltName 扩展
        alt_names = [x509.IPAddress(ipaddress.ip_address(ip)) for ip in ips]


        csr_builder = csr_builder.add_extension(
            x509.SubjectAlternativeName(alt_names),
            critical=False
        )

        # 签署 CSR
        csr = csr_builder.sign(private_key, hashes.SHA256())

        # 返回 CSR (ASN1 格式)
        return csr.public_bytes(serialization.Encoding.DER), pk

    def apply_ip_ssl(self, ips, email, webroot=None, mode=None, path=None):
        print("Starting to apply for Let's Encrypt IP SSL certificate...")
        print("Retrieving ACME API directory...")
        self.get_apis()
        self._config['email'] = email
        print("Creating order...")
        order_data = self.create_order(ips)
        if not order_data:
            raise Exception("Failed to create order!")
        print("Order created successfully")
        print("Performing domain verification...")
        try:
            self.get_and_set_authorizations(order_data, webroot, mode, ips)
        except Exception as e:
            raise Exception("Domain verification failed! {}".format(e))
        # 完成订单
        print("Creating CSR...")
        csr, private_key = self.create_csr(ips)
        print("Sending CSR and completing order...")
        res = self.acme_request(order_data['finalize'], payload={
            "csr": self.calculate_safe_base64(csr)
        })
        if res.status_code not in [200, 201]:
            raise Exception("Failed to complete order! {}".format(res.json()))
        # 获取证书
        print("Retrieving certificate...")
        cert_url = res.json().get('certificate')
        if not cert_url:
            raise Exception("Failed to retrieve certificate URL!")
        cert_res = self.acme_request(cert_url, payload="")
        if cert_res.status_code not in [200, 201]:
            raise Exception("Failed to retrieve certificate! {}".format(cert_res.json()))
        print("Certificate retrieved successfully!")
        cert_pem = cert_res.content.decode()
        # 保存证书和私钥
        if not path:
            path = "/www/server/panel/ssl"
        if not os.path.exists(path):
            os.makedirs(path)
        cert_path = os.path.join(path, 'certificate.pem')
        key_path = os.path.join(path, 'privateKey.pem')
        with open(cert_path, 'w') as f:
            f.write(cert_pem)
        with open(key_path, 'w') as f:
            f.write(private_key.decode())
        return cert_path, key_path

    def create_order(self, ips):
        identifiers = []
        for ip in ips:
            identifiers.append({"type": "ip", "value": ip})
        payload = {"identifiers": identifiers, "profile": "shortlived"}
        print("Create order, domain name list:{}".format(','.join(ips)))
        res = self.acme_request(self._apis['newOrder'], payload)
        if not res.status_code in [201,200]:  # 如果创建失败
            print("Failed to create order, attempting to fix error...")
            e_body = res.json()
            if 'type' in e_body:
                # 如果随机数失效
                if e_body['type'].find('error:badNonce') != -1:
                    print("Nonce invalid, retrieving new nonce and retrying...")
                    self.get_nonce(force=True)
                    res = self.acme_request(self._apis['newOrder'], payload)
                # 如果帐户失效
                if e_body['detail'].find('KeyID header contained an invalid account URL') != -1:
                    print("Account invalid, re-registering account and retrying...")
                    k = "Production"
                    del(self._config['account'][k])
                    self.get_kid()
                    self.get_nonce(force=True)
                    res = self.acme_request(self._apis['newOrder'], payload)
            if not res.status_code in [201,200]:
                print(res.json())
                # 2025/12/25 yakpanel
                raise Exception(str(res.json()))
                # return {}
        return res.json()

    # UTC时间转时间戳
    def utc_to_time(self, utc_string):
        try:
            utc_string = utc_string.split('.')[0]
            utc_date = datetime.datetime.strptime(
                utc_string, "%Y-%m-%dT%H:%M:%S")
            # 按北京时间返回
            return int(time.mktime(utc_date.timetuple())) + (3600 * 8)
        except:
            return int(time.time() + 86400 * 7)

    def get_keyauthorization(self, token):
        acme_header_jwk_json = json.dumps(
            self.get_acme_header("GET_THUMBPRINT")["jwk"], sort_keys=True, separators=(",", ":")
        )
        acme_thumbprint = self.calculate_safe_base64(
            hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest()
        )
        acme_keyauthorization = "{0}.{1}".format(token, acme_thumbprint)
        base64_of_acme_keyauthorization = self.calculate_safe_base64(
            hashlib.sha256(acme_keyauthorization.encode("utf8")).digest()
        )

        return acme_keyauthorization, base64_of_acme_keyauthorization

    # 获取并设置验证信息
    def get_and_set_authorizations(self, order_data, webroot=None, mode=None, ips=None):
        import os

        if 'authorizations' not in order_data:
            raise Exception("Abnormal order data, missing authorization information!")
        for auth_url in order_data['authorizations']:
            res = self.acme_request(auth_url, payload="")
            if not res.status_code in [200, 201]:
                raise Exception("Failed to get authorization information! {}".format(res.json()))
            s_body = res.json()
            if 'status' in s_body:
                if s_body['status'] in ['invalid']:
                    raise Exception("Invalid order, current order status is verification failed!")
                if s_body['status'] in ['valid']:  # 跳过无需验证的域名
                    continue
            for challenge in s_body['challenges']:
                if challenge['type'] == "http-01":
                    break
            if challenge['type'] != "http-01":
                raise Exception("http-01 verification method not found, cannot continue applying for certificate!")
            # 检查是否需要验证
            check_auth_data = self.check_auth_status(challenge['url'])
            if check_auth_data.json()['status'] == 'invalid':
                raise Exception('Domain verification failed, please try applying again!')
            if check_auth_data.json()['status'] == 'valid':
                continue

            acme_keyauthorization, auth_value = self.get_keyauthorization(
                challenge['token'])
            print(challenge)

            if mode:
                if mode == 'standalone':
                    from http.server import HTTPServer, SimpleHTTPRequestHandler
                    import threading
                    import os

                    class ACMERequestHandler(SimpleHTTPRequestHandler):
                        def log_message(self, format, *args):
                            # 屏蔽默认的请求日志输出
                            return

                        def do_GET(self):
                            if self.path == '/.well-known/acme-challenge/{}'.format(challenge['token']):
                                self.send_response(200)
                                self.send_header('Content-type', 'text/plain')
                                self.end_headers()
                                self.wfile.write(acme_keyauthorization.encode())
                            else:
                                self.send_response(404)
                                self.end_headers()

                    server_address = ('', 80)
                    httpd = HTTPServer(server_address, ACMERequestHandler)

                    def start_server():
                        httpd.serve_forever()

                    server_thread = threading.Thread(target=start_server)
                    server_thread.daemon = True
                    server_thread.start()


                    # 2025/12/26 yakpanel
                    time.sleep(2)  # 等待服务器启动
                    try:
                        # ========================= 校验服务 ==================================
                        server_started = False
                        challenge_local = f"http://127.0.0.1/.well-known/acme-challenge/{challenge['token']}"
                        for i in range(10): # 最大尝试时间5秒
                            try:
                                # 临时检查80 打印
                                with socket.create_connection(("127.0.0.1", 80), timeout=0.2):
                                    print("Temporary server started on port 80.")

                                response = requests.get(challenge_local, timeout=0.5)
                                if response.status_code == 200 and response.text == acme_keyauthorization:
                                    print("Temporary server started and responding correctly challenge token on port 80.")
                                    server_started = True
                                    break
                                else:
                                    print("Temporary server response incorrect, retrying...")
                                    time.sleep(0.5)
                            except Exception:
                                time.sleep(0.5)

                        if not server_started:
                            # raise Exception("Failed to start temporary HTTP server on port 80 in 5 seconds.")
                            print("Failed to start temporary HTTP server on port 80 in 5 seconds.")

                        # =========================   通知ACME服务器进行验证 ===============================
                        self.acme_request(challenge['url'], payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
                        self.check_auth_status(challenge['url'], [
                            'valid', 'invalid'])
                    finally:
                        httpd.shutdown()
                        server_thread.join()
                elif mode == 'nginx':
                    tmp_path = '/www/server/panel/vhost/nginx/tmp_apply_ip_ssl.conf'
                    files = find_nginx_files_by_servername(ips[0])
                    if not files:
                        print("No related Nginx configuration files found, attempting to create temporary configuration file...")
                        if not os.path.exists('/www/server/panel/vhost/nginx'):
                            raise Exception("No Nginx configuration files found, and Nginx configuration directory does not exist!")
                        # 如果没有找到相关配置文件，则创建一个临时配置文件
                        with open(tmp_path, 'w') as f:
                            f.write("""server
{{
    listen 80;
    server_name {0};
    location /.well-known/acme-challenge/{1} {{
        default_type text/plain;
        return 200 "{2}";
    }}
}}
""".format(ips[0], challenge['token'], acme_keyauthorization))
                    try:
                        for file in files:
                            print("Modifying Nginx configuration file: {}".format(file))
                            insert_location_into_server(file, ips[0], verify_file=challenge['token'], verify_content=acme_keyauthorization)
                        # 重新加载Nginx配置
                        subprocess.run(["nginx", "-t"], check=True)
                        subprocess.run(["nginx", "-s", "reload"], check=True)

                        # 通知ACME服务器进行验证
                        self.acme_request(challenge['url'],
                                          payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
                        self.check_auth_status(challenge['url'], [
                            'valid', 'invalid'])
                    finally:
                        for file in files:
                            print("Restoring Nginx configuration file: {}".format(file))
                            # 恢复备份文件
                            backup_file = file + ".bak"
                            if os.path.exists(backup_file):
                                shutil.move(backup_file, file)
                        if not files:
                            print("Deleting temporary Nginx configuration file...")
                            # 删除临时配置文件
                            os.remove(tmp_path)
                        # 重新加载Nginx配置
                        subprocess.run(["nginx", "-t"], check=True)
                        subprocess.run(["nginx", "-s", "reload"], check=True)

                elif mode == 'apache':
                    tmp_path = '/www/server/panel/vhost/apache/tmp_apply_ip_ssl.conf'
                    files = find_apache_conf_files(ips[0])
                    if not files:
                        print("No related Apache configuration files found, attempting to create temporary configuration file...")
                        if not os.path.exists('/www/server/panel/vhost/apache'):
                            raise Exception("No Apache configuration files found, and Apache configuration directory does not exist!")
                        # 如果没有找到相关配置文件，则创建一个临时配置文件
                        with open(tmp_path, 'w') as f:
                            f.write("""<VirtualHost *:80>
    ServerName {0}
    <Location /.well-known/acme-challenge/{1}>
        Require all granted
        Header set Content-Type "text/plain"
    </Location>
    Alias /.well-known/acme-challenge/{1} /tmp/{1}
</VirtualHost>
""".format(ips[0], challenge['token']))
                    try:
                        for file in files:
                            print("Modifying Apache configuration file: {}".format(file))
                            insert_location_into_vhost(file, ips[0], verify_file=challenge['token'])
                        # 写入验证文件
                        with open('/tmp/{}'.format(challenge['token']), 'w') as f:
                            f.write(acme_keyauthorization)
                        # 重新加载Apache配置
                        subprocess.run(["/etc/init.d/httpd", "reload"], check=True)

                        # 通知ACME服务器进行验证
                        self.acme_request(challenge['url'],
                                          payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
                        self.check_auth_status(challenge['url'], [
                            'valid', 'invalid'])
                    finally:
                        for file in files:
                            print("Restoring Apache configuration file: {}".format(file))
                            # 恢复备份文件
                            backup_file = file + ".bak"
                            if os.path.exists(backup_file):
                                shutil.move(backup_file, file)
                        if not files:
                            print("Deleting temporary Apache configuration file...")
                            # 删除临时配置文件
                            os.remove(tmp_path)
                        # 重新加载Apache配置
                        subprocess.run(["systemctl", "restart", "httpd"], check=True)
            else:
                # 使用webroot方式验证
                challenge_path = os.path.join(
                    webroot, '.well-known', 'acme-challenge')
                if not os.path.exists(challenge_path):
                    os.makedirs(challenge_path)
                file_path = os.path.join(challenge_path, challenge['token'])
                with open(file_path, 'w') as f:
                    f.write(acme_keyauthorization)

                try:
                    # 通知ACME服务器进行验证
                    self.acme_request(challenge['url'], payload={"keyAuthorization": "{0}".format(acme_keyauthorization)})
                    self.check_auth_status(challenge['url'], [
                        'valid', 'invalid'])
                finally:
                    os.remove(file_path)

    # 检查验证状态
    def check_auth_status(self, url, desired_status=None):
        desired_status = desired_status or ["pending", "valid", "invalid"]
        number_of_checks = 0
        authorization_status = "pending"
        while True:
            print("|- {} checking verification result...".format(number_of_checks + 1))
            if desired_status == ['valid', 'invalid']:
                time.sleep(self._wait_time)
            check_authorization_status_response = self.acme_request(url, "")
            a_auth = check_authorization_status_response.json()
            if not isinstance(a_auth, dict):
                continue
            authorization_status = a_auth["status"]
            number_of_checks += 1
            if authorization_status in desired_status:
                if authorization_status == "invalid":
                    try:
                        if 'error' in a_auth['challenges'][0]:
                            ret_title = a_auth['challenges'][0]['error']['detail']
                        elif 'error' in a_auth['challenges'][1]:
                            ret_title = a_auth['challenges'][1]['error']['detail']
                        elif 'error' in a_auth['challenges'][2]:
                            ret_title = a_auth['challenges'][2]['error']['detail']
                        else:
                            ret_title = str(a_auth)
                    except:
                        ret_title = str(a_auth)
                    raise StopIteration(
                        "{0} >>>> {1}".format(
                            ret_title,
                            json.dumps(a_auth)
                        )
                    )
                break

            if number_of_checks == self._max_check_num:
                raise StopIteration(
                    "Error: Verification attempted {0} times. Maximum verification attempts: {1}. Verification interval: {2} seconds.".format(
                        number_of_checks,
                        self._max_check_num,
                        self._wait_time
                    )
                )
        print("|-Verification result: {}".format(authorization_status))
        return check_authorization_status_response


if __name__ == '__main__':
    import argparse
    # 解析命令行参数
    parser = argparse.ArgumentParser(description='Auto-apply IP SSL certificate script')
    parser.add_argument('-ips', type=str, required=True, help='IP addresses to apply SSL certificate for', dest='ips')
    parser.add_argument('-email', type=str, required=False, help='Email for SSL certificate application', dest='email')
    parser.add_argument('-w', type=str, help='Website root directory', dest='webroot')
    parser.add_argument('--standalone', help='Apply certificate using standalone mode', dest='standalone', action='store_true')
    parser.add_argument('--nginx', help='Apply certificate using nginx mode', dest='nginx', action='store_true')
    parser.add_argument('--apache', help='Apply certificate using apache mode', dest='apache', action='store_true')
    parser.add_argument('-path', type=str, help='Certificate save path', dest='path')
    args = parser.parse_args()

    if not args.standalone and not args.webroot and not args.nginx and not args.apache:
        print("No verification mode detected, will attempt to auto-select verification mode!")
        # 自动选择验证模式
        # 判断80端口是否被占用
        use_80 = False
        result = subprocess.run(
            ["lsof", "-i:80"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        if result.stdout:
            result = subprocess.run(
                ["netstat", "-lntup", "|", "grep", "80"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            )
            if result.stdout:
                use_80 = True
        if use_80:
            print("It is detected that port 80 is occupied, try to use Nginx or Apache mode to verify...")
            # 检查是否安装Nginx
            if os.path.exists('/www/server/nginx/sbin/nginx'):
                args.nginx = True
                print("Selected Nginx mode for verification...")
            elif os.path.exists('/www/server/apache/bin/httpd'):
                args.apache = True
                print("Selected Apache mode for verification...")
            else:
                print("[ERROR] Nginx or Apache installation not detected, cannot use Nginx or Apache mode for verification! Please release port 80 and try again.")
                exit(1)
        else:
            args.standalone = True
            print("Port 80 not occupied, selected standalone mode for verification...")

    if not args.email:
        # 使用默认邮箱
        email = "demo@yakpanel.com"
    else:
        email = args.email

    ips = args.ips.split(',')
    # 先只支持单个IP申请
    if len(ips) > 1 and not args.standalone:
        print("[ERROR] Multiple IP SSL certificate application not supported in non-standalone mode!")
        exit(1)
    # 先只支持IPv4
    if not is_ipv4(ips[0]):
        print("[ERROR] Only IPv4 addresses are supported for SSL certificate application at this time!")
        exit(1)
    auto_ssl = AutoApplyIPSSL()
    mode = None
    if args.standalone:
        mode = 'standalone'
    elif args.nginx:
        mode = 'nginx'
    elif args.apache:
        mode = 'apache'
    try:
        cert_path, key_path = auto_ssl.apply_ip_ssl(ips, email, webroot=args.webroot, mode=mode, path=args.path)
    except Exception as e:
        # 2025/12/25 yakpanel
        print(f"[ERROR] Certificate application failed! Error details: {e}", file=sys.stderr)
        exit(1)
    exit(0)


