# coding: utf-8 import json import os import pwd import re from typing import Union, Optional, Tuple, List import public public.sys_path_append("class_v2") from projectModelV2.common import LimitNet, Redirect from public.exceptions import HintException try: from public.hook_import import hook_import hook_import() except: pass try: import idna except: public.ExecShell('btpip install idna') import idna class _ProjectSiteType: _CONFIG_FILE = "{}/config/project_site.json".format(public.get_panel_path()) allow_type = {"go", "java", "net", "nodejs", "other", "python", "proxy", "html"} def __init__(self): self._config = None @classmethod def read_conf_file(cls): default_conf = { "go": {}, "java": {}, "net": {}, "nodejs": {}, "other": {}, "python": {}, "proxy": {}, "html": {}, } if not os.path.isfile(cls._CONFIG_FILE): public.writeFile(cls._CONFIG_FILE, json.dumps(default_conf)) return default_conf conf_data = public.readFile(cls._CONFIG_FILE) if not isinstance(conf_data, str): public.writeFile(cls._CONFIG_FILE, json.dumps(default_conf)) return default_conf try: conf = json.loads(conf_data) except json.JSONDecodeError: conf = None if not isinstance(conf, dict): public.writeFile(cls._CONFIG_FILE, json.dumps(default_conf)) return default_conf return conf @property def config(self): if self._config is not None: return self._config self._config = self.read_conf_file() return self._config def save_config_to_file(self): if self._config: public.writeFile(self._CONFIG_FILE, json.dumps(self._config)) def get_next_id(self, p_type: str) -> int: all_ids = [ i["id"] for i in self.config[p_type].values() ] return max(all_ids + [0]) + 1 def add(self, p_type: str, name: str, ps: str) -> Tuple[bool, str]: if p_type not in self.allow_type: return False, "not support type" if p_type not in self.config: self.config[p_type] = {} for t_info in self.config[p_type].values(): if t_info["name"] == name: return False, "name exists" next_id = self.get_next_id(p_type) self.config[p_type][str(next_id)] = { "id": next_id, "name": name, "ps": ps } self.save_config_to_file() return True, "" def modify(self, p_type: str, t_id: int, name: str, ps: str) -> bool: if p_type not in self.config: return False if str(t_id) not in self.config[p_type]: return False self.config[p_type][str(t_id)] = { "id": t_id, "name": name, "ps": ps } self.save_config_to_file() return True def remove(self, p_type: str, t_id: int) -> bool: if p_type not in self.config: return False if str(t_id) not in self.config[p_type]: return False del self.config[p_type][str(t_id)] self.save_config_to_file() return True def find(self, p_type: str, t_id: int) -> Optional[dict]: if p_type not in self.config: return None if str(t_id) not in self.config[p_type]: return None return self.config[p_type][str(t_id)] def list_by_type(self, p_type: str) -> List[dict]: if p_type not in self.config: return [] return [ i for i in self.config[p_type].values() ] class projectBase(LimitNet, Redirect): def __init__(self): self._is_nginx_http3 = None def check_port(self, port): ''' @name 检查端口是否被占用 @args port:端口号 @return: 被占用返回True,否则返回False @author: lkq 2021-08-28 ''' a = public.ExecShell("netstat -nltp|awk '{print $4}'") if a[0]: if re.search(':' + port + '\n', a[0]): return True else: return False else: return False def is_domain(self, domain): ''' @name 验证域名合法性 @args domain:域名 @return: 合法返回True,否则返回False @author: lkq 2021-08-28 ''' import re domain_regex = re.compile(r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?= 1000 的用户 和 root get中包含 sys_user 返回 uid>= 100 的用户 和 root get中包含 all_user 返回所有的用户 """ sys_user = False all_user = False if get is not None: if hasattr(get, "sys_user"): sys_user = True if hasattr(get, "all_user"): all_user = True user_set = set() try: for tmp_uer in pwd.getpwall(): if tmp_uer.pw_uid == 0: user_set.add(tmp_uer.pw_name) elif tmp_uer.pw_uid >= 1000: user_set.add(tmp_uer.pw_name) elif sys_user and tmp_uer.pw_uid >= 100: user_set.add(tmp_uer.pw_name) elif all_user: user_set.add(tmp_uer.pw_name) except Exception: pass return list(user_set) @staticmethod def _pass_dir_for_user(path_dir: str, user: str): """ 给某个用户,对应目录的执行权限 """ import stat if not os.path.isdir(path_dir): return try: import pwd uid_data = pwd.getpwnam(user) uid = uid_data.pw_uid gid = uid_data.pw_gid except: return if uid == 0: return if path_dir[:-1] == "/": path_dir = path_dir[:-1] while path_dir != "/": path_dir_stat = os.stat(path_dir) if path_dir_stat.st_uid != uid or path_dir_stat.st_gid != gid: old_mod = stat.S_IMODE(path_dir_stat.st_mode) if not old_mod & 1: os.chmod(path_dir, old_mod + 1) path_dir = os.path.dirname(path_dir) @staticmethod def start_by_user(project_id): file_path = "{}/data/push/tips/project_stop.json".format(public.get_panel_path()) if not os.path.exists(file_path): data = {} else: data_content = public.readFile(file_path) try: data = json.loads(data_content) except json.JSONDecodeError: data = {} data[str(project_id)] = False public.writeFile(file_path, json.dumps(data)) @staticmethod def stop_by_user(project_id): file_path = "{}/data/push/tips/project_stop.json".format(public.get_panel_path()) if not os.path.exists(file_path): data = {} else: data_content = public.readFile(file_path) try: data = json.loads(data_content) except json.JSONDecodeError: data = {} data[str(project_id)] = True public.writeFile(file_path, json.dumps(data)) @staticmethod def is_stop_by_user(project_id): file_path = "{}/data/push/tips/project_stop.json".format(public.get_panel_path()) if not os.path.exists(file_path): data = {} else: data_content = public.readFile(file_path) try: data = json.loads(data_content) except json.JSONDecodeError: data = {} if str(project_id) not in data: return False return data[str(project_id)] def is_nginx_http3(self): """判断nginx是否可以使用http3""" if getattr(self, "_is_nginx_http3", None) is None: _is_nginx_http3 = public.ExecShell("nginx -V 2>&1| grep 'http_v3_module'")[0] != '' setattr(self, "_is_nginx_http3", _is_nginx_http3) return self._is_nginx_http3 @staticmethod def _check_webserver(): setup_path = public.get_setup_path() ng_path = setup_path + '/nginx/sbin/nginx' ap_path = setup_path + '/apache/bin/apachectl' op_path = '/usr/local/lsws/bin/lswsctrl' if not os.path.exists(ng_path) and not os.path.exists(ap_path) and not os.path.exists(op_path): raise HintException(public.lang("Not Found any Web Server")) tasks = public.M('tasks').where("status!=? AND type!=?", ('1', 'download')).field('id,name').select() for task in tasks: name = task["name"].lower() if name.find("openlitespeed") != -1: raise HintException(public.lang("Installing OpenLiteSpeed, please wait")) if name.find("nginx") != -1: raise HintException(public.lang("Installing Nginx, please wait")) if name.lower().find("apache") != -1: raise HintException(public.lang("Installing Apache, please wait")) # 域名编码转换 @staticmethod def domain_to_puny_code(domain): match = re.search(u"[^u\0000-u\001f]+", domain) if not match: return domain try: if domain.startswith("*."): return "*." + idna.encode(domain[2:]).decode("utf8") else: return idna.encode(domain).decode("utf8") except: return domain # 判断域名是否有效,并返回 def check_domain(self, domain: str) -> Union[str, bool]: domain = self.domain_to_puny_code(domain) # 判断通配符域名格式 if domain.find('*') != -1 and domain.find('*.') == -1: return False from ssl_domainModelV2.service import DomainValid if not DomainValid.is_valid_domain(domain): return False return domain def _release_firewall(self, get) -> tuple[bool, str]: """尝试放行端口 @author baozi <202-04-18> @param: get ( dict_obj ): 创建项目的请求 @return """ if getattr(get, "release_firewall", None) in ("0", '', None, False, 0): return False, public.lang("PS: port not released in firewall, local access only") port = getattr(get, "port", None) if port is None: return True, "" project_name = getattr(get, "name", "") or getattr(get, "pjname", "") or getattr(get, "project_name", "") brief = f"Site Project: {public.xsssec(project_name)} release port " fw_body = { "protocol": "tcp", "port": str(port), "choose": "all", "domain": "", "types": "accept", "strategy": "accept", "chain": "INPUT", "brief": brief, "operation": "add", } try: from firewallModelV2.comModel import main as firewall try: ports_exist = firewall().port_rules_list(public.to_dict_obj({ "chain": "ALL", "query": brief, })) # 尝试移除被该项目占用的旧端口 for old_port in public.find_value_by_key(ports_exist, "data", []): old_port_str = str(old_port.get("Port", "")) if not old_port_str: continue if old_port_str == "80": continue if self.IsOpen(old_port): continue if old_port.get("Port"): fw_body["port"] = str(old_port.get("Port", "")) fw_body["operation"] = "remove" firewall().set_port_rule(public.to_dict_obj(fw_body)) except: pass # add fw_body["port"] = str(port) set_res = firewall().set_port_rule(public.to_dict_obj(fw_body)) if set_res.get("status") == 0: return True, "" except Exception as e: import traceback public.print_log(traceback.format_exc()) public.print_log("_release_firewall error: {}".format(e)) return False, public.lang("PS: port not released in firewall, local access only") # todo 废弃 def set_daemon_time(self): """设置守护进程重启检测时间""" pass # todo 废弃 def get_daemon_time(self): """获取守护进程重启检测时间""" pass # todo 废弃 def _project_mod_type(self) -> Optional[str]: mod_name = self.__class__.__module__ # "projectModel/javaModel.py" 的格式 if "/" in mod_name: mod_name = mod_name.rsplit("/", 1)[1] if mod_name.endswith(".py"): mod_name = mod_name[:-3] # "projectModel.javaModel" 的格式 if "." in mod_name: mod_name = mod_name.rsplit(".", 1)[1] if mod_name.endswith("Model"): return mod_name[:-5] return mod_name # todo移除到site通用 def project_site_types(self, get=None): p_type = self._project_mod_type() res = _ProjectSiteType().list_by_type(p_type) res_data = [ {"id": 0, "name": "Default category", "ps": ""}, ] + res return public.success_v2(res_data) # todo移除到site通用 def add_project_site_type(self, get): try: type_name = get.type_name.strip() ps = get.ps.strip() except AttributeError: return public.fail_v2("params error") if not type_name: return public.fail_v2("name can not be empty") if len(type_name) > 16: return public.fail_v2("please do not enter more than 16 characters for the name") p_type = self._project_mod_type() flag, msg = _ProjectSiteType().add(p_type, type_name, ps) if not flag: return public.fail_v2(msg) return public.success_v2("Add success") # todo移除到site通用 def modify_project_site_type(self, get): try: type_name = get.type_name.strip() ps = get.ps.strip() type_id = int(get.type_id.strip()) except (AttributeError, ValueError, TypeError): return public.fail_v2("params error") if not type_name or not type_id: return public.fail_v2("type_name, type_id can not be empty") if len(type_name) > 16: return public.fail_v2("please do not enter more than 16 characters for the name") p_type = self._project_mod_type() flag = _ProjectSiteType().modify(p_type, type_id, type_name, ps) if not flag: return public.fail_v2("modify error") return public.success_v2("Modify success") # todo移除到site通用 def remove_project_site_type(self, get): try: type_id = int(get.type_id.strip()) except (AttributeError, ValueError, TypeError): return public.fail_v2("params error") p_type = self._project_mod_type() project_type_map = { "go": "Go", "java": "Java", "net": "net", "nodejs": "Node", "other": "Other", "python": "Python", "proxy": "proxy", "html": "html", } if p_type not in project_type_map: return public.fail_v2("params error") flag = _ProjectSiteType().remove(p_type, type_id) if not flag: return public.fail_v2("Delete error") p_t = project_type_map[p_type] query_str = 'project_type=? AND type_id=?' projects = public.M('sites').where(query_str, (p_t, type_id)).field("id").select() if not projects: return public.success_v2("Delete success") project_ids = [i["id"] for i in projects] update_str = 'project_type=? AND id in ({})'.format(",".join(["?"] * len(project_ids))) public.M('sites').where(update_str, (p_t, *project_ids)).update({"type_id": 0}) return public.success_v2("Delete success") # todo移除到site通用 def find_project_site_type(self, type_id: int): if isinstance(type_id, str): try: type_id = int(type_id) except (AttributeError, ValueError, TypeError): return None if type_id == 0: return { "id": 0, "name": "Default category", "ps": "" } p_type = self._project_mod_type() return _ProjectSiteType().find(p_type, type_id) # todo移除, 使用batch def set_project_site_type(self, get): try: type_id = int(get.type_id.strip()) if isinstance(get.site_ids, str): site_ids = json.loads(get.site_ids.strip()) else: site_ids = get.site_ids except (AttributeError, ValueError, TypeError): return public.fail_v2("params error") if not isinstance(site_ids, list): return public.fail_v2("params error") p_type = self._project_mod_type() project_type_map = { "go": "Go", "java": "Java", "net": "net", "nodejs": "Node", "other": "Other", "python": "Python", "proxy": "proxy", "html": "html", } if p_type not in project_type_map: return public.fail_v2("params error") if not self.find_project_site_type(type_id): return public.fail_v2("project site type not exists") p_t = project_type_map[p_type] query_str = 'project_type=? AND id in ({})'.format(",".join(["?"] * len(site_ids))) projects = public.M('sites').where(query_str, (p_t, *site_ids)).field("id").select() if not projects: return public.fail_v2("no project found") project_ids = [i["id"] for i in projects] update_str = 'project_type=? AND id in ({})'.format(",".join(["?"] * len(project_ids))) public.M('sites').where(update_str, (p_t, *project_ids)).update({"type_id": type_id}) return public.success_v2("Set success") # todo移除废弃 def batch_set_site_type(self, get): """ @name 批量设置网站分类 """ # v2 site api -> batch_set_site_type pass