10348 lines
311 KiB
Python
10348 lines
311 KiB
Python
# 公共模块
|
||
# @author Zhj<2024/06/15>
|
||
import base64
|
||
import binascii
|
||
import contextlib
|
||
import fnmatch
|
||
import gettext
|
||
import gzip
|
||
import importlib
|
||
import json
|
||
import os
|
||
import psutil
|
||
import re
|
||
import shutil
|
||
import socket
|
||
import string
|
||
import sys
|
||
import tempfile
|
||
import threading
|
||
import time
|
||
import typing
|
||
from datetime import datetime
|
||
from typing import Any, List, Set
|
||
|
||
import fcntl
|
||
import werkzeug.datastructures
|
||
|
||
import public
|
||
from .exceptions import PanelError
|
||
from .regexplib import *
|
||
from .sqlite_easy import Db, SqliteEasy
|
||
from .structures import *
|
||
from .tools import is_number
|
||
from .validate import Param, trim_filter
|
||
|
||
aap_t_simple_result = aap_t_simple_result
|
||
aap_t_mysql_dump_info = aap_t_mysql_dump_info
|
||
|
||
# 默认语言配置 新增语言时请同步更新
|
||
def default_languages_config():
|
||
return {
|
||
"default": "en",
|
||
"languages": [
|
||
{
|
||
"name": "en",
|
||
"google": "en",
|
||
"title": "English",
|
||
"cn": "英语"
|
||
},
|
||
{
|
||
"name": "de",
|
||
"google": "de",
|
||
"title": "Deutsch",
|
||
"cn": "德语"
|
||
},
|
||
{
|
||
"name": "fra",
|
||
"google": "fr",
|
||
"title": "Français",
|
||
"cn": "法语"
|
||
},
|
||
{
|
||
"name": "spa",
|
||
"google": "es",
|
||
"title": "Español",
|
||
"cn": "西班牙语"
|
||
},
|
||
{
|
||
"name": "pt",
|
||
"google": "pt",
|
||
"title": "Português",
|
||
"cn": "葡萄牙语"
|
||
},
|
||
{
|
||
"name": "vie",
|
||
"google": "vi",
|
||
"title": "Tiếng Việt",
|
||
"cn": "越南语"
|
||
},
|
||
{
|
||
"name": "ind",
|
||
"google": "id",
|
||
"title": "Bahasa Indonesia",
|
||
"cn": "印尼语"
|
||
}, {
|
||
"name": "ru",
|
||
"google": "ru",
|
||
"title": "Русский",
|
||
"cn": "俄语"
|
||
},
|
||
{
|
||
"name": "zh",
|
||
"google": "zh-cn",
|
||
"title": "简体中文",
|
||
"cn": "简体中文"
|
||
},
|
||
{
|
||
"name": "cht",
|
||
"google": "zh-tw",
|
||
"title": "繁體中文",
|
||
"cn": "繁體中文"
|
||
}
|
||
]
|
||
}
|
||
|
||
|
||
path = "/www/server/panel/YakPanel/languages/language.pl"
|
||
if os.path.exists(path):
|
||
with open(path, 'r', encoding='utf-8') as data:
|
||
lang = data.read()
|
||
# 读取到的内容再写入设置
|
||
settings_file = "/www/server/panel/YakPanel/languages/settings.json"
|
||
settings = {}
|
||
try:
|
||
if os.path.exists(settings_file):
|
||
with open(settings_file, 'r', encoding='utf-8') as file:
|
||
settings = json.loads(file.read())
|
||
# 修复空文件
|
||
if not settings.get('languages', None):
|
||
# settings = {
|
||
# "default": "en",
|
||
# "languages": [
|
||
# {
|
||
# "name": "en",
|
||
# "google": "en",
|
||
# "title": "English",
|
||
# "cn": "英语"
|
||
# },
|
||
# {
|
||
# "name": "de",
|
||
# "google": "de",
|
||
# "title": "Deutsch",
|
||
# "cn": "德语"
|
||
# },
|
||
# {
|
||
# "name": "fra",
|
||
# "google": "fr",
|
||
# "title": "Français",
|
||
# "cn": "法语"
|
||
# },
|
||
# {
|
||
# "name": "spa",
|
||
# "google": "es",
|
||
# "title": "Español",
|
||
# "cn": "西班牙语"
|
||
# },
|
||
# {
|
||
# "name": "pt",
|
||
# "google": "pt",
|
||
# "title": "Português",
|
||
# "cn": "葡萄牙语"
|
||
# },
|
||
# {
|
||
# "name": "vie",
|
||
# "google": "vi",
|
||
# "title": "Tiếng Việt",
|
||
# "cn": "越南语"
|
||
# },
|
||
# {
|
||
# "name": "ind",
|
||
# "google": "id",
|
||
# "title": "Bahasa Indonesia",
|
||
# "cn": "印尼语"
|
||
# }, {
|
||
# "name": "ru",
|
||
# "google": "ru",
|
||
# "title": "Русский",
|
||
# "cn": "俄语"
|
||
# },{
|
||
# "name": "zh",
|
||
# "google": "zh-cn",
|
||
# "title": "简体中文",
|
||
# "cn": "简体中文"
|
||
# },
|
||
# {
|
||
# "name": "cht",
|
||
# "google": "zh-tw",
|
||
# "title": "繁體中文",
|
||
# "cn": "繁體中文"
|
||
# }
|
||
# ]
|
||
# }
|
||
settings = default_languages_config()
|
||
settings['default'] = lang
|
||
with open(settings_file, 'w', encoding='utf-8') as file:
|
||
file.write(json.dumps(settings, indent=4))
|
||
except:
|
||
pass
|
||
|
||
|
||
|
||
es = gettext.translation('en', localedir='/www/server/panel/YakPanel/static/language/gettext', languages=['en'])
|
||
es.install()
|
||
_ = es.gettext
|
||
|
||
|
||
|
||
_LAN_PUBLIC = None
|
||
_LAN_LOG = None
|
||
_LAN_TEMPLATE = None
|
||
|
||
if sys.version_info[0] == 2:
|
||
# noinspection PyUnresolvedReferences,PyUnboundLocalVariable
|
||
reload(sys)
|
||
# noinspection PyUnresolvedReferences
|
||
sys.setdefaultencoding('utf8')
|
||
else:
|
||
from importlib import reload
|
||
|
||
|
||
def M(table):
|
||
"""
|
||
@name 访问面板数据库
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@table 被访问的表名(必需)
|
||
@return db.Sql object
|
||
|
||
ps: 默认访问data/default.db
|
||
"""
|
||
import db
|
||
with db.Sql() as sql:
|
||
# sql = db.Sql()
|
||
return sql.table(table)
|
||
|
||
|
||
# Easy Sqlite Toolkit for query
|
||
def S(table_name: typing.Optional[str] = None, db_name: str = 'default') -> SqliteEasy:
|
||
from .sqlite_easy import Db
|
||
|
||
query = Db(db_name).query()
|
||
|
||
if table_name is not None and str(table_name).strip() != '':
|
||
query.table(str(table_name).strip())
|
||
|
||
return query
|
||
|
||
|
||
# Easy Sqlite Toolkit for connection
|
||
def SqliteConn(db_name: str = 'default') -> Db:
|
||
return Db(db_name)
|
||
|
||
|
||
# 连接MYSQL数据库
|
||
def MysqlConn(db_name: typing.Optional[str] = None, db_user: str = 'root', db_pwd: typing.Optional[str] = None, db_host: str = 'localhost'):
|
||
from panel_mysql_v2 import PanelMysqlWithContext
|
||
return PanelMysqlWithContext(db_name, db_user, db_pwd, db_host)
|
||
|
||
|
||
def HttpGet(url, timeout=6, headers={}):
|
||
"""
|
||
@name 发送GET请求
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@url 被请求的URL地址(必需)
|
||
@timeout 超时时间默认60秒
|
||
@return string
|
||
"""
|
||
if url.find('api/user/login') == -1:
|
||
if is_local(): return False
|
||
# rep_home_host()
|
||
import http_requests
|
||
res = http_requests.get(url, timeout=timeout, headers=headers, verify=False)
|
||
if res.status_code == 0:
|
||
if headers: return False
|
||
s_body = res.text
|
||
return s_body
|
||
s_body = res.text
|
||
del res
|
||
return s_body
|
||
|
||
|
||
def http_get_home(url, timeout, ex):
|
||
"""
|
||
@name Get方式使用优选节点访问官网
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param url 当前官网URL地址
|
||
@param timeout 用于测试超时时间
|
||
@param ex 上一次错误的响应内容
|
||
@return string 响应内容
|
||
|
||
如果已经是优选节点,将直接返回ex
|
||
"""
|
||
try:
|
||
home = 'www.yakpanel.com'
|
||
if url.find(home) == -1: return ex
|
||
hosts_file = "config/hosts.json"
|
||
if not os.path.exists(hosts_file): return ex
|
||
hosts = json.loads(readFile(hosts_file))
|
||
headers = {"host": home}
|
||
for host in hosts:
|
||
new_url = url.replace(home, host)
|
||
res = HttpGet(new_url, timeout, headers)
|
||
if res:
|
||
writeFile("data/home_host.pl", host)
|
||
# set_home_host(host)
|
||
return res
|
||
return ex
|
||
except:
|
||
return ex
|
||
|
||
|
||
# def set_home_host(host):
|
||
# """
|
||
# @name 设置官网hosts
|
||
# @author hwliang<hwl@yakpanel.com>
|
||
# @param host IP地址
|
||
# @return void
|
||
# """
|
||
# ExecShell('sed -i "/www.yakpanel.com/d" /etc/hosts')
|
||
# ExecShell("echo '' >> /etc/hosts")
|
||
# ExecShell("echo '%s www.yakpanel.com' >> /etc/hosts" % host)
|
||
# ExecShell(r'sed -i "/^\s*$/d" /etc/hosts')
|
||
|
||
def httpGet(url, timeout=6):
|
||
return HttpGet(url, timeout)
|
||
|
||
|
||
def HttpPost(url, data, timeout=6, headers={}):
|
||
"""
|
||
发送POST请求
|
||
@url 被请求的URL地址(必需)
|
||
@data POST参数,可以是字符串或字典(必需)
|
||
@timeout 超时时间默认60秒
|
||
return string
|
||
"""
|
||
if url.find('api/user/login') == -1:
|
||
if is_local():
|
||
return False
|
||
# rep_home_host()
|
||
import http_requests
|
||
res = http_requests.post(url, data=data, timeout=timeout, headers=headers)
|
||
if res.status_code == 0:
|
||
if headers:
|
||
return False
|
||
s_body = res.text
|
||
return s_body
|
||
s_body = res.text
|
||
return s_body
|
||
|
||
|
||
def httpPost(url, data, headers={}, timeout=6):
|
||
"""
|
||
@name 发送POST请求
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param url 被请求的URL地址(必需)
|
||
@param data POST参数,可以是字符串或字典(必需)
|
||
@param timeout 超时时间默认60秒
|
||
@return string
|
||
"""
|
||
return HttpPost(url, data, timeout, headers)
|
||
|
||
|
||
def check_home():
|
||
return True
|
||
|
||
|
||
def Md5(strings):
|
||
"""
|
||
@name 生成MD5
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param strings 要被处理的字符串
|
||
@return string(32)
|
||
"""
|
||
if type(strings) != bytes:
|
||
strings = strings.encode()
|
||
import hashlib
|
||
m = hashlib.md5()
|
||
m.update(strings)
|
||
return m.hexdigest()
|
||
|
||
|
||
def md5(strings):
|
||
return Md5(strings)
|
||
|
||
|
||
def FileMd5(filename):
|
||
"""
|
||
@name 生成文件的MD5
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param filename 文件名
|
||
@return string(32) or False
|
||
"""
|
||
if not os.path.isfile(filename): return False
|
||
import hashlib
|
||
my_hash = hashlib.md5()
|
||
f = open(filename, 'rb')
|
||
while True:
|
||
b = f.read(8096)
|
||
if not b:
|
||
break
|
||
my_hash.update(b)
|
||
f.close()
|
||
return my_hash.hexdigest()
|
||
|
||
|
||
def GetRandomString(length):
|
||
"""
|
||
@name 取随机字符串
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param length 要获取的长度
|
||
@return string(length)
|
||
"""
|
||
from random import Random
|
||
strings = ''
|
||
chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
|
||
chrlen = len(chars) - 1
|
||
random = Random()
|
||
for i in range(length):
|
||
strings += chars[random.randint(0, chrlen)]
|
||
return strings
|
||
|
||
def GetRandomAlnumLower(length):
|
||
"""
|
||
@name 取随机字符串(仅小写字母和数字)
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param length 要获取的长度
|
||
@return string(length)
|
||
"""
|
||
import random
|
||
chars = string.ascii_lowercase + string.digits # 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||
return ''.join(random.choices(chars, k=length))
|
||
|
||
def ReturnJson(status, msg, args=()):
|
||
"""
|
||
@name 取通用Json返回
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param status 返回状态
|
||
@param msg 返回消息
|
||
@return string(json)
|
||
"""
|
||
# return GetJson(ReturnMsg(status, msg, args))
|
||
return GetJson(return_msg_gettext(status, msg, args))
|
||
|
||
|
||
def returnJson(status, msg, args=()):
|
||
"""
|
||
@name 取通用Json返回
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param status 返回状态
|
||
@param msg 返回消息
|
||
@return string(json)
|
||
"""
|
||
return ReturnJson(status, msg, args)
|
||
|
||
|
||
def ReturnMsg(status, msg, args=()):
|
||
"""
|
||
@name 取通用dict返回
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param status 返回状态
|
||
@param msg 返回消息
|
||
@return dict {"status":bool,"msg":string}
|
||
"""
|
||
try:
|
||
log_message = json.loads(ReadFile('YakPanel/static/language/' + GetLanguage() + '/public.json'))
|
||
except:
|
||
log_message = {}
|
||
keys = log_message.keys()
|
||
if type(msg) == str:
|
||
if msg in keys:
|
||
msg = log_message[msg]
|
||
for i in range(len(args)):
|
||
rep = '{' + str(i + 1) + '}'
|
||
msg = msg.replace(rep, args[i])
|
||
|
||
# msg = gettext_msg(msg)
|
||
# # 从语言包查询字符串
|
||
# if msg != "":
|
||
# msg = gettext_msg2(msg)
|
||
return {'status': status, 'msg': msg}
|
||
|
||
|
||
def return_msg_gettext(status, msg, args=()):
|
||
"""
|
||
@name 取通用dict返回
|
||
@author hwliang
|
||
@date 2022.9.20
|
||
"""
|
||
msg = gettext_msg(msg, args)
|
||
return {'status': status, 'msg': msg}
|
||
|
||
|
||
def returnMsg(status, msg, args=()):
|
||
"""
|
||
@name 取通用dict返回
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param status 返回状态
|
||
@param msg 返回消息
|
||
@return dict {"status":bool,"msg":string}
|
||
"""
|
||
return ReturnMsg(status, msg, args)
|
||
|
||
|
||
def return_message(status, types, message, args=(), play="", requests=()):
|
||
"""
|
||
@name 统一请求响应函数
|
||
@author hezhihong
|
||
@param status 返回状态
|
||
@param message 返回消息
|
||
@return dict {"status":0/-1,"message":any}/下载对象
|
||
"""
|
||
from flask import g
|
||
# g.return_message = True
|
||
|
||
# 只有在应用上下文中才操作 g,否则跳过
|
||
try:
|
||
if hasattr(g, 'return_message'): # 检查是否存在
|
||
g.return_message = True
|
||
except Exception:
|
||
# 不在上下文中,忽略即可
|
||
pass
|
||
|
||
|
||
# 非文件下载
|
||
if types == 0:
|
||
return_message = {'status': status, "timestamp": int(time.time()), "message": {}}
|
||
try:
|
||
log_message = json.loads(ReadFile('YakPanel/static/language/' + GetLanguage() + '/public.json'))
|
||
except:
|
||
log_message = {}
|
||
keys = log_message.keys()
|
||
if type(message) == str:
|
||
if message in keys:
|
||
message = log_message[message]
|
||
for i in range(len(args)):
|
||
rep = '{' + str(i + 1) + '}'
|
||
message = message.replace(rep, args[i])
|
||
# # 从语言包查询字符串
|
||
# if message != "":
|
||
# message = gettext_msg2(message)
|
||
return_message["message"]["result"] = message
|
||
elif type(message) == int:
|
||
return_message["message"]["result"] = message
|
||
elif type(message) == bool:
|
||
return_message["message"]["result"] = message
|
||
elif type(message) == float:
|
||
return_message["message"]["result"] = message
|
||
elif type(message) == dict:
|
||
return_message["message"] = message
|
||
elif type(message) == list:
|
||
return_message["message"] = message
|
||
elif type(message) == tuple:
|
||
return_message["message"] = message
|
||
else:
|
||
try:
|
||
return_message["message"] = message
|
||
except:
|
||
return_message["message"] = {}
|
||
return return_message
|
||
# # 文件下载
|
||
# elif types == 1:
|
||
# # from flask import requests as requests
|
||
# if play == 'true':
|
||
# import panelVideo
|
||
# # start, end = panelVideo.get_range(requests)
|
||
# # return panelVideo.partial_response(filename, start, end)
|
||
# else:
|
||
# mimetype = "application/octet-stream"
|
||
# extName = filename.split('.')[-1]
|
||
# if extName in ['png', 'gif', 'jpeg', 'jpg']: mimetype = None
|
||
# public.WriteLog("TYPE_FILE", 'FILE_DOWNLOAD',
|
||
# (filename, public.GetClientIp()))
|
||
# return send_file(filename,
|
||
# mimetype=mimetype,
|
||
# as_attachment=True,
|
||
# etag=True,
|
||
# conditional=True,
|
||
# download_name=os.path.basename(filename),
|
||
# max_age=0)
|
||
|
||
# html响应对象
|
||
elif types == 2:
|
||
return_message = {'status': status, "timestamp": int(time.time()), "message": {}}
|
||
if type(message) == str:
|
||
return_message["message"]["result"] = message
|
||
return return_message
|
||
|
||
|
||
# V2版本的成功响应函数
|
||
def success_v2(res, format_args=()):
|
||
"""
|
||
@name V2版本的成功响应函数
|
||
@author Zhj<2024-06-05>
|
||
@param res<any> 响应数据
|
||
@param format_args<tuple> 响应文本提示时的format参数
|
||
@return dict
|
||
"""
|
||
# 对文本响应做多语言转换处理
|
||
if isinstance(res, str):
|
||
res = gettext_msg(res, format_args)
|
||
|
||
return return_message(0, 0, res)
|
||
|
||
|
||
# V2版本的失败响应函数
|
||
def fail_v2(res, format_args=()):
|
||
"""
|
||
@name V2版本的失败响应函数
|
||
@author Zhj<2024-06-05>
|
||
@param res<any> 响应数据
|
||
@param format_args<tuple> 响应文本提示时的format参数
|
||
@return dict
|
||
"""
|
||
# 对文本响应做多语言转换处理
|
||
if isinstance(res, str):
|
||
res = gettext_msg(res, format_args)
|
||
|
||
return return_message(-1, 0, res)
|
||
|
||
|
||
def GetFileMode(filename):
|
||
"""
|
||
@name 取文件权限字符串
|
||
@author hwliang<hwl@yakpanel.com>
|
||
@param filename 文件全路径
|
||
@return string 如:644/777/755
|
||
"""
|
||
stat = os.stat(filename)
|
||
accept = str(oct(stat.st_mode)[-3:])
|
||
return accept
|
||
|
||
|
||
def get_mode_and_user(path):
|
||
'''取文件或目录权限信息'''
|
||
import pwd
|
||
data = {}
|
||
if not os.path.exists(path): return None
|
||
stat = os.stat(path)
|
||
data['mode'] = str(oct(stat.st_mode)[-3:])
|
||
try:
|
||
data['user'] = pwd.getpwuid(stat.st_uid).pw_name
|
||
except:
|
||
data['user'] = str(stat.st_uid)
|
||
return data
|
||
|
||
|
||
class ijson:
|
||
def loads(self, data):
|
||
return json.loads(data)
|
||
|
||
def dumps(self, data):
|
||
try:
|
||
try:
|
||
return json.dumps(data)
|
||
except:
|
||
return json.dumps(data, ensure_ascii=False)
|
||
except:
|
||
return json.dumps({'status': False, 'msg': "wrong response: %s" % str(data)})
|
||
|
||
|
||
def GetJson(data):
|
||
"""
|
||
将对象转换为JSON
|
||
@data 被转换的对象(dict/list/str/int...)
|
||
"""
|
||
if data == bytes: data = data.decode('utf-8')
|
||
ijson_obj = ijson()
|
||
data = ijson_obj.dumps(data)
|
||
del (ijson_obj)
|
||
return data
|
||
|
||
|
||
def getJson(data):
|
||
return GetJson(data)
|
||
|
||
|
||
def gettext_msg(msg, args=()):
|
||
try:
|
||
msg = _(msg).format(*args)
|
||
except:
|
||
pass
|
||
finally:
|
||
return msg
|
||
|
||
|
||
def write_log_gettext(type, logmsg, args=(), not_web=False):
|
||
# 写日志
|
||
logmsg = gettext_msg(logmsg, args)
|
||
try:
|
||
import time, db, json
|
||
username = 'system'
|
||
uid = 1
|
||
tmp_msg = ''
|
||
if not not_web:
|
||
try:
|
||
from YakPanel import session
|
||
if 'username' in session:
|
||
username = session['username']
|
||
uid = session['uid']
|
||
if session.get('debug') == 1: return
|
||
except:
|
||
pass
|
||
sql = db.Sql()
|
||
mDate = time.strftime('%Y-%m-%d %X', time.localtime())
|
||
data = (uid, username, _(type), xssencode2(logmsg + tmp_msg), mDate)
|
||
result = sql.table('logs').add('uid,username,type,log,addtime', data)
|
||
except:
|
||
pass
|
||
|
||
|
||
def WriteLog(type, logMsg, args=(), not_web=False):
|
||
# 写日志
|
||
try:
|
||
import time, db, json
|
||
username = 'system'
|
||
uid = 1
|
||
tmp_msg = ''
|
||
if not not_web:
|
||
try:
|
||
from YakPanel import session
|
||
if 'username' in session:
|
||
username = session['username']
|
||
uid = session['uid']
|
||
if session.get('debug') == 1: return
|
||
except:
|
||
pass
|
||
global _LAN_LOG
|
||
if not _LAN_LOG:
|
||
_LAN_LOG = json.loads(ReadFile('YakPanel/static/language/' + GetLanguage() + '/log.json'))
|
||
keys = _LAN_LOG.keys()
|
||
if logMsg in keys:
|
||
logMsg = _LAN_LOG[logMsg]
|
||
for i in range(len(args)):
|
||
rep = '{' + str(i + 1) + '}'
|
||
logMsg = logMsg.replace(rep, args[i])
|
||
if type in keys: type = _LAN_LOG[type]
|
||
try:
|
||
if 'login_address' in session:
|
||
logMsg = '{} {}'.format(session['login_address'], logMsg)
|
||
except:
|
||
pass
|
||
|
||
sql = db.Sql()
|
||
mDate = time.strftime('%Y-%m-%d %X', time.localtime())
|
||
data = (uid, username, type, logMsg + tmp_msg, mDate)
|
||
result = sql.table('logs').add('uid,username,type,log,addtime', data)
|
||
return result
|
||
except:
|
||
return None
|
||
|
||
|
||
def GetLanguage():
|
||
'''
|
||
取语言
|
||
'''
|
||
return GetConfigValue("language")
|
||
|
||
|
||
def get_language():
|
||
return GetLanguage()
|
||
|
||
|
||
def get_panel_port():
|
||
try:
|
||
p = readFile('data/port.pl')
|
||
return (p or '').strip() or '7800'
|
||
except:
|
||
return '7800'
|
||
|
||
|
||
def PanelHttpOrigin():
|
||
return 'http://127.0.0.1:{}'.format(get_panel_port())
|
||
|
||
|
||
def is_self_hosted():
|
||
try:
|
||
c = GetConfig()
|
||
if not c:
|
||
return True
|
||
v = c.get('self_hosted', True)
|
||
if isinstance(v, bool):
|
||
return v
|
||
return str(v).lower() in ('1', 'true', 'yes', 'on')
|
||
except:
|
||
return True
|
||
|
||
|
||
def GetConfigValue(key):
|
||
'''
|
||
取配置值
|
||
'''
|
||
config = GetConfig()
|
||
if not config:
|
||
config = {"product": "Linux panel", "setup_path": "/www/server", "openlitespeed_path": "/usr/local",
|
||
"language": "English", "title": "YakPanel Linux panel", "brand": "YakPanel", "root_path": "/www",
|
||
"template": "default", "logs_path": "/www/wwwlogs", "home": "https://www.yakpanel.com",
|
||
"self_hosted": True, "recycle_bin": True}
|
||
writeFile('/www/server/panel/config/config.json',json.dumps(config))
|
||
if key == 'home':
|
||
v = config.get('self_hosted', True)
|
||
if isinstance(v, bool) and v:
|
||
return PanelHttpOrigin()
|
||
if str(v).lower() in ('1', 'true', 'yes', 'on'):
|
||
return PanelHttpOrigin()
|
||
if not key in config.keys():
|
||
if key == 'download':
|
||
if is_self_hosted():
|
||
return PanelHttpOrigin()
|
||
return 'http://node.yakpanel.com'
|
||
return None
|
||
return config[key]
|
||
|
||
|
||
def SetConfigValue(key, value):
|
||
config = GetConfig()
|
||
config[key] = value
|
||
WriteConfig(config)
|
||
|
||
|
||
def GetConfig():
|
||
'''
|
||
取所有配置项
|
||
'''
|
||
path = "config/config.json"
|
||
if not os.path.exists(path): return {}
|
||
f_body = ReadFile(path)
|
||
if not f_body: return {}
|
||
return json.loads(f_body)
|
||
|
||
|
||
def WriteConfig(config):
|
||
path = "config/config.json"
|
||
WriteFile(path, json.dumps(config))
|
||
|
||
|
||
def GetLan(key):
|
||
"""
|
||
取提示消息
|
||
"""
|
||
global _LAN_TEMPLATE
|
||
if not _LAN_TEMPLATE:
|
||
_LAN_TEMPLATE = json.loads(ReadFile('YakPanel/static/language/' + GetLanguage() + '/template.json'))
|
||
keys = _LAN_TEMPLATE.keys()
|
||
msg = None
|
||
if key in keys:
|
||
msg = _LAN_TEMPLATE[key]
|
||
return msg
|
||
|
||
|
||
def getLan(key):
|
||
return GetLan(key)
|
||
|
||
|
||
def GetMsg(key, args=()):
|
||
try:
|
||
global _LAN_PUBLIC
|
||
if not _LAN_PUBLIC:
|
||
_LAN_PUBLIC = json.loads(ReadFile('YakPanel/static/language/' + GetLanguage() + '/public.json'))
|
||
keys = _LAN_PUBLIC.keys()
|
||
msg = None
|
||
if key in keys:
|
||
msg = _LAN_PUBLIC[key]
|
||
for i in range(len(args)):
|
||
rep = '{' + str(i + 1) + '}'
|
||
msg = msg.replace(rep, args[i])
|
||
return msg
|
||
except:
|
||
return key
|
||
|
||
|
||
def get_msg_gettext(msg, args=()):
|
||
return gettext_msg(msg, args)
|
||
|
||
|
||
def getMsg(key, args=()):
|
||
return GetMsg(key, args)
|
||
|
||
|
||
# 获取Web服务器
|
||
def GetWebServer():
|
||
# 优先从请求头获取(仅在 Flask 请求上下文中)
|
||
try:
|
||
from flask import has_request_context
|
||
except Exception:
|
||
has_request_context = lambda: False
|
||
|
||
if has_request_context():
|
||
try:
|
||
from flask import request
|
||
header_val = request.headers.get('Aap-Web-Server')
|
||
if header_val:
|
||
v = str(header_val).strip().lower()
|
||
# 支持常见别名
|
||
if v in ('nginx', 'apache', 'openlitespeed', 'ols'):
|
||
# 规范化返回 openlitespeed 名称
|
||
if v == 'ols':
|
||
return 'openlitespeed'
|
||
return v
|
||
except Exception:
|
||
pass
|
||
|
||
nginxSbin = '{}/nginx/sbin/nginx'.format(get_setup_path())
|
||
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
|
||
olsBin = '/usr/local/lsws/bin/lswsctrl'
|
||
|
||
if os.path.exists(nginxSbin) and (os.path.exists(apacheBin) or os.path.exists(olsBin)):
|
||
return 'nginx'
|
||
|
||
if os.path.exists(apacheBin):
|
||
webserver = 'apache'
|
||
elif os.path.exists(olsBin):
|
||
webserver = 'openlitespeed'
|
||
else:
|
||
webserver = 'nginx'
|
||
|
||
return webserver
|
||
|
||
|
||
def get_webserver():
|
||
return GetWebServer()
|
||
|
||
|
||
def ServiceReload():
|
||
# 获取多服务状态和安装路径
|
||
is_multi = get_multi_webservice_status()
|
||
setup_path = get_setup_path()
|
||
|
||
# 定义服务操作映射
|
||
services = [
|
||
(
|
||
f"{setup_path}/nginx/sbin/nginx",
|
||
"/etc/init.d/nginx reload",
|
||
"pkill -9 nginx && sleep 1 && /etc/init.d/nginx start"
|
||
),
|
||
(
|
||
f"{setup_path}/apache/bin/apachectl",
|
||
"/etc/init.d/httpd reload",
|
||
None
|
||
),
|
||
(
|
||
"/usr/local/lsws/bin/lswsctrl",
|
||
# "rm -f /tmp/lshttpd/*.sock* && /usr/local/lsws/bin/lswsctrl restart",
|
||
"/usr/local/lsws/bin/lswsctrl reload",
|
||
None
|
||
)
|
||
]
|
||
|
||
result = None
|
||
# 多服务模式:遍历所有服务并执行
|
||
if is_multi:
|
||
for path, cmd, err_cmd in services:
|
||
if os.path.exists(path):
|
||
result = ExecShell(cmd)
|
||
# 处理nginx pid异常
|
||
if "nginx" in path and result[1].find("nginx.pid") != -1:
|
||
result = ExecShell(err_cmd)
|
||
|
||
# 单服务模式:找到第一个存在的服务执行
|
||
else:
|
||
for path, cmd, err_cmd in services:
|
||
if os.path.exists(path):
|
||
result = ExecShell(cmd)
|
||
# 处理nginx pid异常
|
||
if "nginx" in path and result[1].find("nginx.pid") != -1:
|
||
result = ExecShell(err_cmd)
|
||
break # 只执行第一个匹配的服务
|
||
|
||
return result
|
||
|
||
|
||
def serviceReload():
|
||
return ServiceReload()
|
||
|
||
|
||
def get_preexec_fn(run_user):
|
||
'''
|
||
@name 获取指定执行用户预处理函数
|
||
@author hwliang<2021-08-19>
|
||
@param run_user<string> 运行用户
|
||
@return 预处理函数
|
||
'''
|
||
import pwd
|
||
pid = pwd.getpwnam(run_user)
|
||
uid = pid.pw_uid
|
||
gid = pid.pw_gid
|
||
|
||
def _exec_rn():
|
||
os.setgid(gid)
|
||
os.setuid(uid)
|
||
|
||
return _exec_rn
|
||
|
||
|
||
def ExecShell(cmdstring, timeout=None, shell=True, cwd=None, env=None, user=None):
|
||
'''
|
||
@name 执行命令
|
||
@author hwliang<2021-08-19>
|
||
@param cmdstring 命令 [必传]
|
||
@param timeout 超时时间
|
||
@param shell 是否通过shell运行
|
||
@param cwd 进入的目录
|
||
@param env 环境变量
|
||
@param user 执行用户名
|
||
@return 命令执行结果
|
||
'''
|
||
a = ''
|
||
e = ''
|
||
import subprocess, tempfile
|
||
preexec_fn = None
|
||
tmp_dir = '/dev/shm'
|
||
if user:
|
||
preexec_fn = get_preexec_fn(user)
|
||
tmp_dir = '/tmp'
|
||
try:
|
||
rx = md5(cmdstring)
|
||
succ_f = tempfile.SpooledTemporaryFile(max_size=4096, mode='wb+', suffix='_succ', prefix='btex_' + rx,
|
||
dir=tmp_dir)
|
||
err_f = tempfile.SpooledTemporaryFile(max_size=4096, mode='wb+', suffix='_err', prefix='btex_' + rx,
|
||
dir=tmp_dir)
|
||
sub = subprocess.Popen(cmdstring, close_fds=True, shell=shell, bufsize=128, stdout=succ_f, stderr=err_f,
|
||
cwd=cwd, env=env, preexec_fn=preexec_fn)
|
||
if timeout:
|
||
s = 0
|
||
d = 0.01
|
||
while sub.poll() == None:
|
||
time.sleep(d)
|
||
s += d
|
||
if s >= timeout:
|
||
if not err_f.closed: err_f.close()
|
||
if not succ_f.closed: succ_f.close()
|
||
return 'Timed out'
|
||
else:
|
||
sub.wait()
|
||
|
||
err_f.seek(0)
|
||
succ_f.seek(0)
|
||
a = succ_f.read()
|
||
e = err_f.read()
|
||
if not err_f.closed: err_f.close()
|
||
if not succ_f.closed: succ_f.close()
|
||
except:
|
||
return '', get_error_info()
|
||
try:
|
||
# 编码修正
|
||
if type(a) == bytes: a = a.decode('utf-8')
|
||
if type(e) == bytes: e = e.decode('utf-8')
|
||
except:
|
||
a = str(a)
|
||
e = str(e)
|
||
|
||
return a, e
|
||
|
||
|
||
def GetLocalIp():
|
||
# 取本地外网IP
|
||
filename = 'data/iplist.txt'
|
||
try:
|
||
ipaddress = readFile(filename)
|
||
if not ipaddress:
|
||
if is_self_hosted():
|
||
try:
|
||
ipaddress = GetHost()
|
||
if not check_ip(ipaddress):
|
||
ipaddress = '127.0.0.1'
|
||
except:
|
||
ipaddress = '127.0.0.1'
|
||
WriteFile(filename, ipaddress)
|
||
else:
|
||
url = 'https://ifconfig.me/ip'
|
||
m_str = HttpGet(url)
|
||
if isinstance(m_str, bytes):
|
||
ipaddress = match_ipv4.match(m_str.decode('utf-8')).group(0)
|
||
else:
|
||
ipaddress = match_ipv4.match(m_str).group(0)
|
||
WriteFile(filename, ipaddress)
|
||
if isinstance(ipaddress, str):
|
||
ipaddress = ipaddress.strip()
|
||
c_ip = check_ip(ipaddress)
|
||
if not c_ip: return GetHost()
|
||
return ipaddress
|
||
except Exception as e:
|
||
try:
|
||
if is_self_hosted():
|
||
ipaddress = GetHost()
|
||
if check_ip(ipaddress):
|
||
WriteFile(filename, ipaddress)
|
||
return ipaddress
|
||
return '127.0.0.1'
|
||
url = '{}/api/common/getClientIP'.format(OfficialApiBase())
|
||
ipaddress = HttpGet(url)
|
||
WriteFile(filename, ipaddress)
|
||
return ipaddress
|
||
except:
|
||
return GetHost()
|
||
|
||
|
||
def is_ipv4(ip):
|
||
'''
|
||
@name 是否是IPV4地址
|
||
@author hwliang
|
||
@param ip<string> IP地址
|
||
@return True/False
|
||
'''
|
||
# 验证基本格式
|
||
if not match_ipv4.match(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 match_ipv6.match(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 GetHost(port=False):
|
||
from flask import request
|
||
host_tmp = request.headers.get('host')
|
||
|
||
# 验证基本格式
|
||
if host_tmp:
|
||
if not match_based_host.match(host_tmp):
|
||
host_tmp = ''
|
||
|
||
if not host_tmp:
|
||
if request.url_root:
|
||
tmp = find_url_root.findall(request.url_root)
|
||
if tmp: host_tmp = tmp[0][1]
|
||
if not host_tmp:
|
||
host_tmp = '127.0.0.1:' + readFile('data/port.pl').strip()
|
||
try:
|
||
if host_tmp.find(':') == -1: host_tmp += ':80'
|
||
except:
|
||
host_tmp = "127.0.0.1:7800"
|
||
h = host_tmp.split(':')
|
||
if port: return h[-1]
|
||
return ':'.join(h[0:-1])
|
||
|
||
|
||
def GetClientIp():
|
||
from flask import request
|
||
ipaddr = request.remote_addr.replace('::ffff:', '')
|
||
if not check_ip(ipaddr): return 'Unknown IP address'
|
||
return ipaddr
|
||
|
||
|
||
def get_remote_port():
|
||
'''
|
||
@name 获取客户端端口号
|
||
@return int
|
||
'''
|
||
from flask import request
|
||
port = request.headers.get('X-Real-Port', '0')
|
||
if port == '0': port = request.environ.get('REMOTE_PORT')
|
||
return str(port)
|
||
|
||
|
||
def get_client_ip():
|
||
return GetClientIp()
|
||
|
||
|
||
def phpReload(version):
|
||
# 重载PHP配置
|
||
import os
|
||
if os.path.exists(get_setup_path() + '/php/' + version + '/libphp5.so'):
|
||
ExecShell('/etc/init.d/httpd reload')
|
||
else:
|
||
ExecShell('/etc/init.d/php-fpm-' + version + ' reload')
|
||
ExecShell("/etc/init.d/php-fpm-{} start".format(version))
|
||
|
||
|
||
def get_timeout(url, timeout=3):
|
||
try:
|
||
start = time.time()
|
||
result = int(httpGet(url, timeout))
|
||
return result, int((time.time() - start) * 1000 - 500)
|
||
except:
|
||
return 0, False
|
||
|
||
|
||
def get_url(timeout=0.5):
|
||
if is_self_hosted():
|
||
return PanelHttpOrigin()
|
||
return 'https://node.yakpanel.com'
|
||
|
||
|
||
# 过滤输入
|
||
def checkInput(data):
|
||
if not data: return data
|
||
if type(data) != str: return data
|
||
checkList = [
|
||
{'d': '<', 'r': '<'},
|
||
{'d': '>', 'r': '>'},
|
||
{'d': '\'', 'r': '‘'},
|
||
{'d': '"', 'r': '“'},
|
||
{'d': '&', 'r': '&'},
|
||
{'d': '#', 'r': '#'},
|
||
{'d': '<', 'r': '<'}
|
||
]
|
||
for v in checkList:
|
||
data = data.replace(v['d'], v['r'])
|
||
return data
|
||
|
||
|
||
# 取文件指定尾行数
|
||
def GetNumLines(path, num: int, p=1):
|
||
if not os.path.exists(path): return ""
|
||
if not is_number(num):
|
||
return ""
|
||
|
||
pyVersion = sys.version_info[0]
|
||
max_len = 1024 * 1024 * 10
|
||
try:
|
||
|
||
start_line = (p - 1) * num
|
||
count = start_line + num
|
||
fp = open(path, 'rb')
|
||
buf = ""
|
||
fp.seek(-1, 2)
|
||
if fp.read(1) == "\n": fp.seek(-1, 2)
|
||
data = []
|
||
total_len = 0
|
||
b = True
|
||
n = 0
|
||
for i in range(count):
|
||
while True:
|
||
newline_pos = str.rfind(str(buf), "\n")
|
||
pos = fp.tell()
|
||
if newline_pos != -1:
|
||
if n >= start_line:
|
||
line = buf[newline_pos + 1:]
|
||
line_len = len(line)
|
||
total_len += line_len
|
||
sp_len = total_len - max_len
|
||
if sp_len > 0:
|
||
line = line[sp_len:]
|
||
try:
|
||
data.insert(0, line)
|
||
except:
|
||
pass
|
||
buf = buf[:newline_pos]
|
||
n += 1
|
||
break
|
||
else:
|
||
if pos == 0:
|
||
b = False
|
||
break
|
||
to_read = min(4096, pos)
|
||
fp.seek(-to_read, 1)
|
||
t_buf = fp.read(to_read)
|
||
if pyVersion == 3:
|
||
t_buf = t_buf.decode('utf-8', errors='ignore')
|
||
|
||
buf = t_buf + buf
|
||
fp.seek(-to_read, 1)
|
||
if pos - to_read == 0:
|
||
buf = "\n" + buf
|
||
if total_len >= max_len: break
|
||
if not b: break
|
||
fp.close()
|
||
result = "\n".join(data)
|
||
except:
|
||
if re.match(r"[`\$\&\;]+", path): return ""
|
||
result = ExecShell("tail -n {} {}".format(num, path))[0]
|
||
if len(result) > max_len:
|
||
result = result[-max_len:]
|
||
|
||
try:
|
||
try:
|
||
result = json.dumps(result)
|
||
return json.loads(result).strip()
|
||
except:
|
||
if pyVersion == 2:
|
||
# noinspection PyUnresolvedReferences
|
||
result = result.decode('utf8', errors='ignore')
|
||
else:
|
||
result = result.encode('utf-8', errors='ignore').decode("utf-8", errors="ignore")
|
||
return result.strip()
|
||
except:
|
||
return ""
|
||
|
||
# read each lines (Implement by generator)
|
||
def read_file_each(filename: str, using_gzip: bool = False):
|
||
if not os.path.exists(filename):
|
||
raise ValueError(lang('file not found: {}', filename))
|
||
|
||
# point a file open function
|
||
open_fn = open
|
||
|
||
# if using gzip read file
|
||
if using_gzip:
|
||
open_fn = gzip.open
|
||
|
||
with open_fn(filename, 'rb') as fp:
|
||
for line in fp:
|
||
yield line.decode('utf-8', 'ignore')
|
||
|
||
|
||
# read each lines reverse (Implement by generator)
|
||
def read_file_each_reverse(filename: str, using_gzip: bool = False):
|
||
if not os.path.exists(filename):
|
||
raise ValueError(lang('file not found: {}', filename))
|
||
|
||
if filename.endswith('.gz'):
|
||
using_gzip = True
|
||
|
||
def _open_file():
|
||
# if using gzip read file
|
||
# decompress to tmp file
|
||
if using_gzip:
|
||
tmp_fp = tempfile.NamedTemporaryFile('wb+')
|
||
|
||
with gzip.open(filename, 'rb') as gz_fp:
|
||
shutil.copyfileobj(gz_fp, tmp_fp)
|
||
|
||
return tmp_fp
|
||
|
||
return open(filename, 'rb')
|
||
|
||
with _open_file() as fp:
|
||
chunk_size = 4096
|
||
end_pos = fp.seek(0, 2)
|
||
loops = int(end_pos / chunk_size)
|
||
last = b''
|
||
i = 0
|
||
|
||
while i < loops:
|
||
fp.seek(end_pos + (chunk_size + chunk_size * i) * -1)
|
||
|
||
bs = fp.read(chunk_size)
|
||
|
||
lines = (bs + last).decode('utf-8', 'ignore').split('\n')
|
||
last = lines[0].encode('utf-8', 'ignore')
|
||
k = len(lines)
|
||
|
||
while k > 1:
|
||
yield lines.pop()
|
||
k -= 1
|
||
|
||
i += 1
|
||
|
||
if i < loops:
|
||
return
|
||
|
||
remainder = end_pos % chunk_size
|
||
|
||
if remainder == 0:
|
||
return
|
||
|
||
# move cursor to top
|
||
fp.seek(0, 0)
|
||
|
||
bs = fp.read(remainder)
|
||
|
||
lines = (bs + last).decode('utf-8', 'ignore').split('\n')
|
||
k = len(lines)
|
||
|
||
while k > 0:
|
||
yield lines.pop()
|
||
k -= 1
|
||
|
||
|
||
# 验证证书
|
||
def CheckCert(certPath='ssl/certificate.pem'):
|
||
try:
|
||
return get_cert_data(certPath)
|
||
except:
|
||
openssl = '/usr/local/openssl/bin/openssl'
|
||
if not os.path.exists(openssl): openssl = 'openssl'
|
||
certPem = readFile(certPath)
|
||
s = "\n-----BEGIN CERTIFICATE-----"
|
||
tmp = certPem.strip().split(s)
|
||
res = True
|
||
for tmp1 in tmp:
|
||
if tmp1.find('-----BEGIN CERTIFICATE-----') == -1: tmp1 = s + tmp1
|
||
writeFile(certPath, tmp1)
|
||
result = ExecShell(openssl + " x509 -in " + certPath + " -noout -subject")
|
||
if result[1].find('-bash:') != -1: res = True
|
||
if len(result[1]) > 2: res = False
|
||
if result[0].find('error:') != -1: res = False
|
||
return res
|
||
|
||
|
||
# 获取面板地址
|
||
def getPanelAddr():
|
||
from flask import request
|
||
protocol = 'https://' if os.path.exists("data/ssl.pl") else 'http://'
|
||
return protocol + request.headers.get('host')
|
||
|
||
|
||
# 字节单位转换
|
||
def to_size(size):
|
||
if not size: return '0.00 b'
|
||
size = float(size)
|
||
d = ('b', 'KB', 'MB', 'GB', 'TB')
|
||
s = d[0]
|
||
for b in d:
|
||
if size < 1024: return ("%.2f" % size) + ' ' + b
|
||
size = size / 1024
|
||
s = b
|
||
return ("%.2f" % size) + ' ' + b
|
||
|
||
|
||
def checkCode(code, outime=120):
|
||
# 校验验证码
|
||
from YakPanel import session, cache
|
||
try:
|
||
codeStr = cache.get('codeStr')
|
||
cache.delete('codeStr')
|
||
if not codeStr:
|
||
session['login_error'] = GetMsg('CODE_TIMEOUT')
|
||
return False
|
||
|
||
if md5(code.lower()) != codeStr:
|
||
session['login_error'] = GetMsg('CODE_ERR')
|
||
return False
|
||
return True
|
||
except:
|
||
session['login_error'] = GetMsg('CODE_NOT_EXISTS')
|
||
return False
|
||
|
||
|
||
# 写进度
|
||
def writeSpeed(title, used, total, speed=0):
|
||
import json
|
||
if not title:
|
||
data = {'title': None, 'progress': 0, 'total': 0, 'used': 0, 'speed': 0}
|
||
else:
|
||
try:
|
||
progress = int((100.0 * used / total))
|
||
except:
|
||
progress = 0
|
||
data = {'title': title, 'progress': progress, 'total': total, 'used': used, 'speed': speed}
|
||
writeFile('/tmp/panelSpeed.pl', json.dumps(data))
|
||
return True
|
||
|
||
|
||
# 取进度
|
||
def getSpeed():
|
||
import json
|
||
data = readFile('/tmp/panelSpeed.pl')
|
||
if not data:
|
||
data = json.dumps({'title': None, 'progress': 0, 'total': 0, 'used': 0, 'speed': 0})
|
||
writeFile('/tmp/panelSpeed.pl', data)
|
||
return json.loads(data)
|
||
|
||
|
||
def get_requests_headers():
|
||
return {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Yak-Panel"}
|
||
|
||
|
||
def downloadFile(url, filename):
|
||
try:
|
||
origin = PanelHttpOrigin()
|
||
if url.startswith(origin + '/') or url.startswith(origin):
|
||
rel = url.split(origin, 1)[-1].lstrip('/')
|
||
src = os.path.join(get_panel_path(), rel.replace('/', os.sep))
|
||
if os.path.isfile(src):
|
||
shutil.copyfile(src, filename)
|
||
return
|
||
except:
|
||
pass
|
||
try:
|
||
if sys.version_info[0] == 2:
|
||
import requests
|
||
headers = {
|
||
'User-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36'}
|
||
r = requests.get(url, headers=headers, verify=False)
|
||
with open(filename, "wb") as f:
|
||
f.write(r.content)
|
||
else:
|
||
import urllib.request
|
||
import ssl
|
||
ssl._create_default_https_context = ssl._create_unverified_context
|
||
opener = urllib.request.build_opener()
|
||
opener.addheaders = [('User-agent',
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36')]
|
||
urllib.request.install_opener(opener)
|
||
urllib.request.urlretrieve(url, filename=filename)
|
||
except:
|
||
ExecShell("wget -O {} {} --no-check-certificate".format(filename, url))
|
||
|
||
|
||
def exists_args(args, get):
|
||
'''
|
||
@name 检查参数是否存在
|
||
@author hwliang<2021-06-08>
|
||
@param args<list or str> 参数列表 允许是列表或字符串
|
||
@param get<dict_obj> 参数对像
|
||
@return bool 都存在返回True,否则抛出KeyError异常
|
||
'''
|
||
if type(args) == str:
|
||
args = args.split(',')
|
||
for arg in args:
|
||
if not arg in get:
|
||
raise KeyError('Required parameters are missing:{}'.format(arg))
|
||
return True
|
||
|
||
|
||
def get_error_info():
|
||
import traceback
|
||
errorMsg = traceback.format_exc()
|
||
return errorMsg
|
||
|
||
|
||
def get_plugin_replace_rules():
|
||
'''
|
||
@name 获取插件文件内容替换规则
|
||
@author hwliang<2021-06-28>
|
||
@return list
|
||
'''
|
||
return [
|
||
{
|
||
"find": "[PATH]",
|
||
"replace": "[PATH]"
|
||
}
|
||
]
|
||
|
||
|
||
def get_plugin_title(plugin_name):
|
||
'''
|
||
@name 获取插件标题
|
||
@author hwliang<2021-06-24>
|
||
@param plugin_name<string> 插件名称
|
||
@return string
|
||
'''
|
||
|
||
info_file = '{}/{}/info.json'.format(get_plugin_path(), plugin_name)
|
||
try:
|
||
return json.loads(readFile(info_file))['title']
|
||
except:
|
||
return plugin_name
|
||
|
||
|
||
def get_error_object(plugin_title=None, plugin_name=None):
|
||
'''
|
||
@name 获取格式化错误响应对像
|
||
@author hwliang<2021-06-21>
|
||
@return Resp
|
||
'''
|
||
if not plugin_title: plugin_title = get_plugin_title(plugin_name)
|
||
try:
|
||
from YakPanel import request, Resp
|
||
is_cli = False
|
||
except:
|
||
is_cli = True
|
||
|
||
if is_cli:
|
||
raise get_error_info()
|
||
ss = '''404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
|
||
|
||
During handling of the above exception, another exception occurred:'''
|
||
error_info = get_error_info().strip().split(ss)[-1].strip()
|
||
request_info = '''REQUEST_DATE: {request_date}
|
||
PAN_VERSION: {panel_version}
|
||
OS_VERSION: {os_version}
|
||
REMOTE_ADDR: {remote_addr}
|
||
REQUEST_URI: {method} {full_path}
|
||
REQUEST_FORM: {request_form}
|
||
USER_AGENT: {user_agent}'''.format(
|
||
request_date=getDate(),
|
||
remote_addr=GetClientIp(),
|
||
method=request.method,
|
||
full_path=url_encode(xsssec(request.full_path)),
|
||
request_form=xsssec(str(request.form.to_dict())),
|
||
user_agent=xsssec(request.headers.get('User-Agent')),
|
||
panel_version=version(),
|
||
os_version=get_os_version()
|
||
)
|
||
|
||
result = readFile('{}/YakPanel/templates/default/plugin_error.html'.format(get_panel_path())).format(
|
||
plugin_name=plugin_title,
|
||
request_info=request_info,
|
||
error_title=error_info.split("\n")[-1],
|
||
error_msg=error_info
|
||
)
|
||
return Resp(result, 500)
|
||
|
||
|
||
# 搜索数据中是否存在
|
||
def inArray(arrays, searchStr):
|
||
for key in arrays:
|
||
if key == searchStr: return True
|
||
|
||
return False
|
||
|
||
|
||
# 格式化指定时间戳
|
||
def format_date(format="%Y-%m-%d %H:%M:%S", times=None):
|
||
if not times: times = int(time.time())
|
||
time_local = time.localtime(times)
|
||
return time.strftime(format, time_local)
|
||
|
||
|
||
# # 检查Web服务器配置文件是否有错误
|
||
# def checkWebConfig():
|
||
# f1 = '{}/'.format(get_vhost_path())
|
||
# f2 = '{}/'.format(get_plugin_path())
|
||
# setup_path = get_setup_path()
|
||
# if not os.path.exists(f2 + 'btwaf'):
|
||
# f3 = f1 + 'nginx/btwaf.conf'
|
||
# if os.path.exists(f3): os.remove(f3)
|
||
# # if not os.path.exists(f2 + 'btwaf_httpd'):
|
||
# # f3 = f1 + 'apache/btwaf.conf'
|
||
# # if os.path.exists(f3): os.remove(f3)
|
||
#
|
||
# if not os.path.exists(f2 + 'total'):
|
||
# f3 = f1 + 'apache/total.conf'
|
||
# if os.path.exists(f3): os.remove(f3)
|
||
# f3 = f1 + 'nginx/total.conf'
|
||
# if os.path.exists(f3): os.remove(f3)
|
||
# else:
|
||
# if os.path.exists(setup_path + '/apache/modules/mod_lua.so'):
|
||
# writeFile(f1 + 'apache/btwaf.conf', 'LoadModule lua_module modules/mod_lua.so')
|
||
# writeFile(f1 + 'apache/total.conf', 'LuaHookLog {}/total/httpd_log.lua run_logs'.format(setup_path))
|
||
# else:
|
||
# f3 = f1 + 'apache/total.conf'
|
||
# if os.path.exists(f3): os.remove(f3)
|
||
#
|
||
# if get_webserver() == 'nginx':
|
||
# result = ExecShell(
|
||
# "ulimit -n 8192 ; {setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf".format(
|
||
# setup_path=setup_path))
|
||
# searchStr = 'successful'
|
||
# elif get_webserver() == 'apache':
|
||
# # else:
|
||
# result = ExecShell("ulimit -n 8192 ; {setup_path}/apache/bin/apachectl -t".format(setup_path=setup_path))
|
||
# searchStr = 'Syntax OK'
|
||
# else:
|
||
# result = ["1", "1"]
|
||
# searchStr = "1"
|
||
# if result[1].find(searchStr) == -1:
|
||
# WriteLog("TYPE_SOFT", 'CONF_CHECK_ERR', (result[1],))
|
||
# return result[1]
|
||
# return True
|
||
|
||
|
||
# 获取nginx版本,没有获取到版本时返回None,获取到时,返回一个3位长度的列表,如[1, 25, 1], 表示1.25.1版本
|
||
def nginx_version():
|
||
out, _ = ExecShell("/www/server/nginx/sbin/nginx -V 2>&1 | grep 'version'")
|
||
out: str = out.strip()
|
||
if not out:
|
||
return None
|
||
|
||
rep_ver = re.compile(r"nginx\s+version.*/(?P<ver>\d+\.\d+(\.\d+)*)")
|
||
res = rep_ver.search(out)
|
||
if not res:
|
||
return None
|
||
ver = res.group("ver")
|
||
ver_list = [int(i) for i in ver.split(".")]
|
||
if len(ver_list) < 3:
|
||
ver_list.extend([0] * (3 - len(ver_list)))
|
||
if len(ver_list) > 3:
|
||
ver_list = ver_list[:3]
|
||
return ver_list
|
||
|
||
|
||
def is_change_nginx_http2() -> bool:
|
||
nginx_ver = nginx_version()
|
||
if not nginx_ver:
|
||
return False
|
||
|
||
if nginx_ver >= [1, 25, 1]:
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
def is_change_nginx_old_http2() -> bool:
|
||
nginx_ver = nginx_version()
|
||
if not nginx_ver:
|
||
return False
|
||
if nginx_ver < [1, 25, 1]:
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
def is_nginx_http3():
|
||
return ExecShell("nginx -V 2>&1| grep 'http_v3_module'")[0].strip() != ''
|
||
|
||
|
||
def remove_nginx_quic():
|
||
nginx_file_path = "/www/server/panel/vhost/nginx"
|
||
for i in os.listdir(nginx_file_path):
|
||
if not i.endswith(".conf"):
|
||
continue
|
||
nginx_file = os.path.join(nginx_file_path, i)
|
||
remove_nginx_server_quic(nginx_file)
|
||
|
||
|
||
def remove_nginx_server_quic(nginx_file: str):
|
||
if not os.path.isfile(nginx_file):
|
||
return
|
||
data = ReadFile(nginx_file)
|
||
if not isinstance(data, str):
|
||
return
|
||
|
||
rep_listen_quic = re.compile(r"\s*listen\s+.*quic;", re.M)
|
||
if not rep_listen_quic.search(data):
|
||
return
|
||
|
||
new_conf = rep_listen_quic.sub('', data)
|
||
writeFile(nginx_file, new_conf)
|
||
|
||
|
||
def change_nginx_http2():
|
||
import os
|
||
nginx_file_path = "/www/server/panel/vhost/nginx"
|
||
for i in os.listdir(nginx_file_path):
|
||
if not i.endswith(".conf"):
|
||
continue
|
||
nginx_file = os.path.join(nginx_file_path, i)
|
||
change_nginx_server_http2(nginx_file)
|
||
|
||
|
||
def change_nginx_server_http2(nginx_file: str):
|
||
if not os.path.isfile(nginx_file):
|
||
return
|
||
data = ReadFile(nginx_file)
|
||
rep_listen = re.compile(r"\s*listen\s+[\[\]:]*([0-9]+).*;[^\n]*\n", re.M)
|
||
|
||
conf_list = []
|
||
start_idx, last_listen_idx = 0, -1
|
||
for tmp in rep_listen.finditer(data):
|
||
listen_str = tmp.group()
|
||
if "http2" in listen_str:
|
||
listen_str = listen_str.replace("http2", "")
|
||
last_listen_idx = len(conf_list) + 2
|
||
|
||
conf_list.append(data[start_idx:tmp.start()])
|
||
conf_list.append(listen_str)
|
||
start_idx = tmp.end()
|
||
|
||
conf_list.append(data[start_idx:])
|
||
if last_listen_idx > 0:
|
||
# conf_list.insert(last_listen_idx, " http2 on;\n")
|
||
|
||
new_conf = "".join(conf_list)
|
||
writeFile(nginx_file, new_conf)
|
||
|
||
|
||
def is_change_nginx_old_http2() -> bool:
|
||
nginx_ver = nginx_version()
|
||
if not nginx_ver:
|
||
return False
|
||
if nginx_ver < [1, 25, 1]:
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
def change_nginx_old_http2():
|
||
nginx_file_path = "/www/server/panel/vhost/nginx"
|
||
for i in os.listdir(nginx_file_path):
|
||
if not i.endswith(".conf"):
|
||
continue
|
||
nginx_file = os.path.join(nginx_file_path, i)
|
||
change_nginx_server_old_http2(nginx_file)
|
||
|
||
|
||
def read_file_lines_range(filename, start_line: int, end_line: int):
|
||
"""
|
||
读取文件指定行数范围内容
|
||
|
||
Args:
|
||
filename: 文件名
|
||
start_line: 开始行号
|
||
end_line: 结束行号
|
||
|
||
Returns:
|
||
list: 指定行数范围内容列表
|
||
"""
|
||
try:
|
||
with open(filename, 'r') as f:
|
||
lines = f.readlines()
|
||
return "".join(lines[start_line:end_line])
|
||
except:
|
||
return "获取文件内容报错了"
|
||
|
||
|
||
def change_nginx_server_old_http2(nginx_file: str):
|
||
if not os.path.isfile(nginx_file):
|
||
return
|
||
data = ReadFile(nginx_file)
|
||
if not isinstance(data, str):
|
||
return
|
||
|
||
rep_http2_on = re.compile(r"\s*http2\s+on;[^\n]*\n", re.M)
|
||
if not rep_http2_on.search(data):
|
||
return
|
||
else:
|
||
data = rep_http2_on.sub("\n", data)
|
||
|
||
rep_listen = re.compile(r"\s*listen\s+[\[\]:]*443.*;[^\n]*\n", re.M)
|
||
conf_list = []
|
||
start_idx = 0
|
||
for tmp in rep_listen.finditer(data):
|
||
listen_str = tmp.group()
|
||
conf_list.append(data[start_idx:tmp.start()])
|
||
# conf_list.append(listen_str.replace(";", " http2;"))
|
||
start_idx = tmp.end()
|
||
|
||
conf_list.append(data[start_idx:])
|
||
new_conf = "".join(conf_list)
|
||
writeFile(nginx_file, new_conf)
|
||
|
||
|
||
# 错误收集适配
|
||
# 检查Web服务器配置文件是否有错误
|
||
def checkWebConfig(repair_num=2, path=None):
|
||
f1 = '{}/'.format(get_vhost_path())
|
||
f2 = '{}/'.format(get_plugin_path())
|
||
setup_path = get_setup_path()
|
||
if not os.path.exists(f2 + 'btwaf'):
|
||
f3 = f1 + 'nginx/btwaf.conf'
|
||
if os.path.exists(f3): os.remove(f3)
|
||
# if not os.path.exists(f2 + 'btwaf_httpd'):
|
||
# f3 = f1 + 'apache/btwaf.conf'
|
||
# if os.path.exists(f3): os.remove(f3)
|
||
|
||
if not os.path.exists(f2 + 'total'):
|
||
f3 = f1 + 'apache/total.conf'
|
||
if os.path.exists(f3): os.remove(f3)
|
||
f3 = f1 + 'nginx/total.conf'
|
||
if os.path.exists(f3): os.remove(f3)
|
||
else:
|
||
if os.path.exists(setup_path + '/apache/modules/mod_lua.so'):
|
||
writeFile(f1 + 'apache/btwaf.conf', 'LoadModule lua_module modules/mod_lua.so')
|
||
writeFile(f1 + 'apache/total.conf', 'LuaHookLog {}/total/httpd_log.lua run_logs'.format(setup_path))
|
||
else:
|
||
f3 = f1 + 'apache/total.conf'
|
||
if os.path.exists(f3): os.remove(f3)
|
||
|
||
web_s = get_webserver()
|
||
if web_s == 'apache' or (path is not None and 'httpd.conf' in path):
|
||
result = ExecShell("ulimit -n 8192 ; {setup_path}/apache/bin/apachectl -t".format(setup_path=setup_path))
|
||
searchStr = 'Syntax OK'
|
||
apache_version = ExecShell("{}/apache/bin/httpd -v".format(setup_path))
|
||
version_info = apache_version[1]
|
||
elif web_s == 'nginx':
|
||
result = ExecShell(
|
||
"ulimit -n 8192 ; {setup_path}/nginx/sbin/nginx -t -c {setup_path}/nginx/conf/nginx.conf".format(
|
||
setup_path=setup_path))
|
||
writeFile('/tmp/nginx_new.conf', readFile('/www/server/nginx/conf/nginx.conf'))
|
||
searchStr = 'successful'
|
||
nginx_version = ExecShell("{}/nginx/sbin/nginx -v".format(setup_path))
|
||
version_info = nginx_version[1]
|
||
else:
|
||
result = ["1", "1"]
|
||
searchStr = "1"
|
||
version_info = "Unknow"
|
||
if result[1].find(
|
||
'the "listen ... http2" directive is deprecated, use the "http2" directive instead') != -1 and web_s == "nginx" and is_change_nginx_http2():
|
||
if repair_num > 0:
|
||
repair_num -= 1
|
||
change_nginx_http2()
|
||
return checkWebConfig(repair_num)
|
||
|
||
if result[1].find(searchStr) == -1:
|
||
if result[1].find(
|
||
'[emerg] unknown directive "http2"') != -1 and web_s == "nginx" and is_change_nginx_old_http2():
|
||
if repair_num > 0:
|
||
repair_num -= 1
|
||
change_nginx_old_http2()
|
||
# print_log('nginx----2')
|
||
return checkWebConfig(repair_num)
|
||
|
||
if result[1].find('[emerg] invalid parameter "quic" in') != -1 and web_s == "nginx" and not is_nginx_http3():
|
||
if repair_num > 0:
|
||
repair_num -= 1
|
||
remove_nginx_quic()
|
||
# print_log('nginx----3')
|
||
return checkWebConfig(repair_num)
|
||
|
||
WriteLog("TYPE_SOFT", 'CONF_CHECK_ERR', (result[1],))
|
||
try:
|
||
match = re.search(r"in (.*):(\d+)", result[1])
|
||
if match:
|
||
err_infos = read_file_lines_range(match.group(1), int(match.group(2)) - 5,
|
||
int(match.group(2)) + 5) if int(
|
||
match.group(2)) >= 5 else read_file_lines_range(match.group(1), 1, int(match.group(2)))
|
||
err_collect("{} \n 报错信息: \n {} \n 版本信息:{} \n报错文件路径:{}:{}".format(
|
||
err_infos, result[1], version_info, match.group(1), match.group(2)),
|
||
0, result[1].split("\n")[0].strip())
|
||
except Exception as e:
|
||
err_collect(result[1], 0, result[1].split("\n")[0].strip())
|
||
# print_log('nginx----4')
|
||
return result[1]
|
||
# print_log('nginx----5')
|
||
return True
|
||
|
||
|
||
def err_collect(error_info, type, error_id):
|
||
'''
|
||
@error_info 错误信息
|
||
@type 错误类型
|
||
@error_id 错误ID
|
||
'''
|
||
|
||
from flask import redirect, request, Response
|
||
_form = request.form.to_dict()
|
||
if 'username' in _form: _form['username'] = '******'
|
||
if 'password' in _form: _form['password'] = '******'
|
||
if 'phone' in _form: _form['phone'] = '******'
|
||
|
||
# 错误信息
|
||
error_infos = {
|
||
"REQUEST_DATE": getDate(), # 请求时间
|
||
"PANEL_VERSION": version(), # 面板版本
|
||
"OS_VERSION": get_os_version(), # 操作系统版本
|
||
"REMOTE_ADDR": GetClientIp(), # 请求IP
|
||
"REQUEST_URI": request.method + request.full_path, # 请求URI
|
||
"REQUEST_FORM": xsssec(str(_form)), # 请求表单
|
||
"USER_AGENT": xsssec(request.headers.get('User-Agent')), # 客户端连接信息
|
||
"ERROR_INFO": error_info, # 错误信息
|
||
"PACK_TIME": readFile("/www/server/panel/config/update_time.pl") if os.path.exists(
|
||
"/www/server/panel/config/update_time.pl") else getDate(), # 打包时间
|
||
"TYPE": type,
|
||
"ERROR_ID": error_id,
|
||
}
|
||
pkey = Md5(error_infos["ERROR_ID"])
|
||
|
||
# 提交异常报告
|
||
if not cache_get(pkey) and not is_self_hosted():
|
||
try:
|
||
run_thread(httpPost, ("https://geterror.yakpanel.com/bt_error/index.php", error_infos))
|
||
cache_set(pkey, 1, 1800)
|
||
except Exception as e:
|
||
pass # 错误信息
|
||
|
||
|
||
############################### 错误收集适配 ^^上方
|
||
|
||
|
||
# 检查是否为IPv4地址
|
||
def checkIp(ip):
|
||
if match_ipv4.match(ip):
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
|
||
# 检查端口是否合法
|
||
def checkPort(port):
|
||
if not is_number(port): return False
|
||
ports = [
|
||
'21', '25', '443', '8080', '888', '999', '8888', '8443', '7800', '8188', '8189', '8288', '8289', '8290'
|
||
]
|
||
if port in ports: return False
|
||
intport = int(port)
|
||
if intport < 1 or intport > 65535: return False
|
||
|
||
# # 判断端口占用,避免多服务崩溃
|
||
# res = ExecShell(f'lsof -i :{port} -P -n -l -F pnc')
|
||
# if res[0] and port != '80':
|
||
# return False
|
||
|
||
return True
|
||
|
||
|
||
# 字符串取中间
|
||
def getStrBetween(startStr, endStr, srcStr):
|
||
start = srcStr.find(startStr)
|
||
if start == -1: return None
|
||
end = srcStr.find(endStr)
|
||
if end == -1: return None
|
||
return srcStr[start + 1:end]
|
||
|
||
|
||
# 取CPU类型
|
||
def getCpuType():
|
||
cpuinfo = open('/proc/cpuinfo', 'r').read()
|
||
rep = r"model\s+name\s+:\s+(.+)"
|
||
tmp = re.search(rep, cpuinfo, re.I)
|
||
cpuType = ''
|
||
if tmp:
|
||
cpuType = tmp.groups()[0]
|
||
else:
|
||
cpuinfo = ExecShell('LANG="en_US.UTF-8" && lscpu')[0]
|
||
rep = r"Model\s+name:\s+(.+)"
|
||
tmp = re.search(rep, cpuinfo, re.I)
|
||
if tmp: cpuType = tmp.groups()[0]
|
||
return cpuType
|
||
|
||
|
||
# 检查是否允许重启
|
||
def IsRestart():
|
||
num = M('tasks').where('status!=?', ('1',)).count()
|
||
if num > 0: return False
|
||
return True
|
||
|
||
|
||
# # 加密密码字符
|
||
# def hasPwd(password):
|
||
# import crypt
|
||
# return crypt.crypt(password, password)
|
||
|
||
def hasPwd(password: str) -> str:
|
||
import bcrypt
|
||
# 将密码转换为字节
|
||
password_bytes = password.encode('utf-8')
|
||
# 生成随机盐值并哈希
|
||
salt = bcrypt.gensalt()
|
||
hashed_bytes = bcrypt.hashpw(password_bytes, salt)
|
||
# 转回字符串
|
||
return hashed_bytes.decode('utf-8')
|
||
|
||
def getDate(format='%Y-%m-%d %X'):
|
||
# 取格式时间
|
||
return time.strftime(format, time.localtime())
|
||
|
||
|
||
# 处理MySQL配置文件
|
||
def CheckMyCnf():
|
||
import os
|
||
confFile = '/etc/my.cnf'
|
||
if os.path.exists(confFile):
|
||
conf = readFile(confFile)
|
||
if conf.find('[mysqld]') != -1: return True
|
||
versionFile = get_setup_path() + '/mysql/version.pl'
|
||
if not os.path.exists(versionFile): return False
|
||
|
||
versions = ['5.1', '5.5', '5.6', '5.7', '8.0', 'AliSQL']
|
||
version = readFile(versionFile)
|
||
for key in versions:
|
||
if key in version:
|
||
version = key
|
||
break
|
||
|
||
shellStr = '''
|
||
#!/bin/bash
|
||
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
|
||
export PATH
|
||
|
||
# CF='node.yakpanel.com'
|
||
# HK='www.yakpanel.com'
|
||
# HK2='103.224.251.67'
|
||
# US='128.1.164.196'
|
||
# sleep 0.5;
|
||
# CN_PING=`ping -c 1 -w 1 $CF|grep time=|awk '{print $7}'|sed "s/time=//"`
|
||
# HK_PING=`ping -c 1 -w 1 $HK|grep time=|awk '{print $7}'|sed "s/time=//"`
|
||
# HK2_PING=`ping -c 1 -w 1 $HK2|grep time=|awk '{print $7}'|sed "s/time=//"`
|
||
# US_PING=`ping -c 1 -w 1 $US|grep time=|awk '{print $7}'|sed "s/time=//"`
|
||
#
|
||
# echo "$HK_PING $HK" > ping.pl
|
||
# echo "$HK2_PING $HK2" >> ping.pl
|
||
# echo "$US_PING $US" >> ping.pl
|
||
# echo "$CF_PING $CF" >> ping.pl
|
||
# nodeAddr=`sort -V ping.pl|sed -n '1p'|awk '{print $2}'`
|
||
# if [ "$nodeAddr" == "" ];then
|
||
# nodeAddr=$CF
|
||
# fi
|
||
|
||
Download_Url=https://node.yakpanel.com
|
||
|
||
|
||
MySQL_Opt()
|
||
{
|
||
MemTotal=`free -m | grep Mem | awk '{print $2}'`
|
||
if [[ ${MemTotal} -gt 1024 && ${MemTotal} -lt 2048 ]]; then
|
||
sed -i "s#^key_buffer_size.*#key_buffer_size = 32M#" /etc/my.cnf
|
||
sed -i "s#^table_open_cache.*#table_open_cache = 128#" /etc/my.cnf
|
||
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 768K#" /etc/my.cnf
|
||
sed -i "s#^read_buffer_size.*#read_buffer_size = 768K#" /etc/my.cnf
|
||
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 8M#" /etc/my.cnf
|
||
sed -i "s#^thread_cache_size.*#thread_cache_size = 16#" /etc/my.cnf
|
||
sed -i "s#^query_cache_size.*#query_cache_size = 16M#" /etc/my.cnf
|
||
sed -i "s#^tmp_table_size.*#tmp_table_size = 32M#" /etc/my.cnf
|
||
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 128M#" /etc/my.cnf
|
||
sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 32M#" /etc/my.cnf
|
||
elif [[ ${MemTotal} -ge 2048 && ${MemTotal} -lt 4096 ]]; then
|
||
sed -i "s#^key_buffer_size.*#key_buffer_size = 64M#" /etc/my.cnf
|
||
sed -i "s#^table_open_cache.*#table_open_cache = 256#" /etc/my.cnf
|
||
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 1M#" /etc/my.cnf
|
||
sed -i "s#^read_buffer_size.*#read_buffer_size = 1M#" /etc/my.cnf
|
||
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 16M#" /etc/my.cnf
|
||
sed -i "s#^thread_cache_size.*#thread_cache_size = 32#" /etc/my.cnf
|
||
sed -i "s#^query_cache_size.*#query_cache_size = 32M#" /etc/my.cnf
|
||
sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" /etc/my.cnf
|
||
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 256M#" /etc/my.cnf
|
||
sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 64M#" /etc/my.cnf
|
||
elif [[ ${MemTotal} -ge 4096 && ${MemTotal} -lt 8192 ]]; then
|
||
sed -i "s#^key_buffer_size.*#key_buffer_size = 128M#" /etc/my.cnf
|
||
sed -i "s#^table_open_cache.*#table_open_cache = 512#" /etc/my.cnf
|
||
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 2M#" /etc/my.cnf
|
||
sed -i "s#^read_buffer_size.*#read_buffer_size = 2M#" /etc/my.cnf
|
||
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 32M#" /etc/my.cnf
|
||
sed -i "s#^thread_cache_size.*#thread_cache_size = 64#" /etc/my.cnf
|
||
sed -i "s#^query_cache_size.*#query_cache_size = 64M#" /etc/my.cnf
|
||
sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" /etc/my.cnf
|
||
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 512M#" /etc/my.cnf
|
||
sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 128M#" /etc/my.cnf
|
||
elif [[ ${MemTotal} -ge 8192 && ${MemTotal} -lt 16384 ]]; then
|
||
sed -i "s#^key_buffer_size.*#key_buffer_size = 256M#" /etc/my.cnf
|
||
sed -i "s#^table_open_cache.*#table_open_cache = 1024#" /etc/my.cnf
|
||
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 4M#" /etc/my.cnf
|
||
sed -i "s#^read_buffer_size.*#read_buffer_size = 4M#" /etc/my.cnf
|
||
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 64M#" /etc/my.cnf
|
||
sed -i "s#^thread_cache_size.*#thread_cache_size = 128#" /etc/my.cnf
|
||
sed -i "s#^query_cache_size.*#query_cache_size = 128M#" /etc/my.cnf
|
||
sed -i "s#^tmp_table_size.*#tmp_table_size = 128M#" /etc/my.cnf
|
||
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 1024M#" /etc/my.cnf
|
||
sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 256M#" /etc/my.cnf
|
||
elif [[ ${MemTotal} -ge 16384 && ${MemTotal} -lt 32768 ]]; then
|
||
sed -i "s#^key_buffer_size.*#key_buffer_size = 512M#" /etc/my.cnf
|
||
sed -i "s#^table_open_cache.*#table_open_cache = 2048#" /etc/my.cnf
|
||
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 8M#" /etc/my.cnf
|
||
sed -i "s#^read_buffer_size.*#read_buffer_size = 8M#" /etc/my.cnf
|
||
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 128M#" /etc/my.cnf
|
||
sed -i "s#^thread_cache_size.*#thread_cache_size = 256#" /etc/my.cnf
|
||
sed -i "s#^query_cache_size.*#query_cache_size = 256M#" /etc/my.cnf
|
||
sed -i "s#^tmp_table_size.*#tmp_table_size = 256M#" /etc/my.cnf
|
||
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 2048M#" /etc/my.cnf
|
||
sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 512M#" /etc/my.cnf
|
||
elif [[ ${MemTotal} -ge 32768 ]]; then
|
||
sed -i "s#^key_buffer_size.*#key_buffer_size = 1024M#" /etc/my.cnf
|
||
sed -i "s#^table_open_cache.*#table_open_cache = 4096#" /etc/my.cnf
|
||
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 16M#" /etc/my.cnf
|
||
sed -i "s#^read_buffer_size.*#read_buffer_size = 16M#" /etc/my.cnf
|
||
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 256M#" /etc/my.cnf
|
||
sed -i "s#^thread_cache_size.*#thread_cache_size = 512#" /etc/my.cnf
|
||
sed -i "s#^query_cache_size.*#query_cache_size = 512M#" /etc/my.cnf
|
||
sed -i "s#^tmp_table_size.*#tmp_table_size = 512M#" /etc/my.cnf
|
||
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 4096M#" /etc/my.cnf
|
||
sed -i "s#^innodb_log_file_size.*#innodb_log_file_size = 1024M#" /etc/my.cnf
|
||
fi
|
||
}
|
||
|
||
wget -O /etc/my.cnf $Download_Url/install/conf/mysql-%s.conf -T 5
|
||
chmod 644 /etc/my.cnf
|
||
MySQL_Opt
|
||
''' % (version,)
|
||
ExecShell(shellStr)
|
||
# 判断是否迁移目录
|
||
if os.path.exists('data/datadir.pl'):
|
||
newPath = readFile('data/datadir.pl')
|
||
if os.path.exists(newPath):
|
||
mycnf = readFile('/etc/my.cnf')
|
||
mycnf = mycnf.replace('/www/server/data', newPath)
|
||
writeFile('/etc/my.cnf', mycnf)
|
||
WriteLog('TYPE_SOFE', 'MYSQL_CHECK_ERR')
|
||
return True
|
||
|
||
|
||
# 检查导出的sql文件是否完整
|
||
def check_sql_file(filename, tables):
|
||
"""
|
||
同步国内的
|
||
@name 检查导出的sql文件是否完整
|
||
@param filename<string> 文件名
|
||
@param tables<list> 表名列表
|
||
@return tuple (status_code<int>,msg<string>) 状态码 0:sql文件不存在 1:无异常,-1:sql文件不完整,-2:缺少表,消息
|
||
"""
|
||
if not os.path.exists(filename): return 0, 'backup file not exists'
|
||
file_size = os.path.getsize(filename)
|
||
# 超过10M的文件不检查
|
||
if file_size > 10 * 1024 * 1024: return 1, 'Normal'
|
||
|
||
# 读取文件
|
||
with open(filename, 'rb') as f:
|
||
# 读取文件尾 128 字节
|
||
f.seek(-1, 2) # 从文件尾开始向前移动1个字节
|
||
pos = f.tell() # 获取当前位置
|
||
to_read = min(128, pos) # 获取读取位置
|
||
|
||
f.seek(-to_read, 1) # 从当前位置向前移动to_read个字节
|
||
last_line = f.read(to_read) # 读取最后to_read个字节
|
||
|
||
# 检查文件尾是否有结束标识
|
||
if last_line.find(b"Dump completed on") == -1:
|
||
return -1, 'Backup file lacks termination marker'
|
||
|
||
# 逐行检查表是否存在
|
||
f.seek(0)
|
||
checked_tables = []
|
||
|
||
while True:
|
||
line = f.readline()
|
||
if not line: break
|
||
|
||
line = line.strip()
|
||
# 跳过空行
|
||
if not line: continue
|
||
|
||
# 检查是否是创建表语句
|
||
if not line.startswith(b"CREATE TABLE "): continue
|
||
|
||
# 获取表名
|
||
table = line.split(b"`")[1].decode('utf-8')
|
||
|
||
# 跳过空表名
|
||
if not table: continue
|
||
|
||
# 跳过已检查的表
|
||
if table in checked_tables: continue
|
||
|
||
# 检查是否在表名列表中
|
||
if table in tables:
|
||
checked_tables.append(table) # 添加到已检查列表
|
||
|
||
# 检查是否有缺少的表
|
||
if len(checked_tables) != len(tables):
|
||
# 计算两个列表的差集
|
||
empty_tables = set(tables).difference(checked_tables)
|
||
return -2, 'Missing tables in backup file :{}'.format(','.join(empty_tables))
|
||
|
||
return 1, 'Normal'
|
||
|
||
def GetSSHPort():
|
||
try:
|
||
file = '/etc/ssh/sshd_config'
|
||
conf = ReadFile(file)
|
||
rep = r"#*Port\s+([0-9]+)\s*\n"
|
||
port = re.search(rep, conf).groups(0)[0]
|
||
return int(port)
|
||
except:
|
||
return 22
|
||
|
||
|
||
def get_sshd_port():
|
||
'''
|
||
@name 获取sshd端口
|
||
@author hwliang
|
||
@return int
|
||
'''
|
||
# 先尝试从进程中获取当前实际的监听端口
|
||
sshd_port = 22
|
||
is_ok = 0
|
||
pid = get_sshd_pid_of_pidfile()
|
||
if not pid: pid = get_sshd_pid_of_binfile()
|
||
if pid:
|
||
try:
|
||
import psutil
|
||
p = psutil.Process(pid)
|
||
for conn in p.connections():
|
||
if conn.status == 'LISTEN':
|
||
sshd_port = conn.laddr[1]
|
||
is_ok = 1
|
||
break
|
||
except:
|
||
pass
|
||
|
||
# 如果从进程获取失败,则尝试从配置文件获取
|
||
if not is_ok: sshd_port = GetSSHPort()
|
||
|
||
return sshd_port
|
||
|
||
|
||
def get_sshd_pid_of_pidfile():
|
||
'''
|
||
@name 通过PID文件获取SSH状态
|
||
@author hwliang
|
||
@return int 0:关闭 pid:开启
|
||
'''
|
||
sshd_pid_list = ['/run/sshd.pid', '/var/run/sshd.pid', '/run/ssh.pid', '/var/run/ssh.pid']
|
||
sshd_pid_file = None
|
||
for spid_file in sshd_pid_list:
|
||
if os.path.exists(spid_file):
|
||
sshd_pid_file = spid_file
|
||
break
|
||
|
||
if sshd_pid_file:
|
||
sshd_pid = readFile(sshd_pid_file)
|
||
if not sshd_pid: return 0
|
||
try:
|
||
sshd_pid = int(sshd_pid)
|
||
if not sshd_pid: return 0
|
||
if pid_exists(sshd_pid):
|
||
return sshd_pid
|
||
except:
|
||
pass
|
||
return 0
|
||
|
||
|
||
def get_sshd_pid_of_binfile():
|
||
'''
|
||
@name 通过执行文件获取SSH状态
|
||
@author hwliang
|
||
@return int 进程pid
|
||
'''
|
||
sshd_bin_list = ['/usr/sbin/sshd', '/usr/bin/sshd', '/usr/sbin/ssh', '/usr/bin/ssh']
|
||
sshd_bin = None
|
||
pid = 0
|
||
for sbin in sshd_bin_list:
|
||
if os.path.exists(sbin):
|
||
sshd_bin = sbin
|
||
break
|
||
|
||
if sshd_bin:
|
||
pid = get_process_pid(sshd_bin.split('/')[-1], sshd_bin, '-D')
|
||
|
||
return pid
|
||
|
||
|
||
def GetSSHStatus():
|
||
'''
|
||
@name 获取SSH状态
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
if get_sshd_pid_of_pidfile():
|
||
return True
|
||
elif get_sshd_pid_of_binfile():
|
||
return True
|
||
return False
|
||
|
||
|
||
def get_sshd_status():
|
||
'''
|
||
@name 获取SSH状态
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
return GetSSHStatus()
|
||
|
||
|
||
# 检查端口是否合法
|
||
def CheckPort(port, other=None):
|
||
if type(port) == str: port = int(port)
|
||
if port < 1 or port > 65535: return False
|
||
if other:
|
||
checks = [22, 20, 21, 8888, 3306, 11211, 888, 25, 7800]
|
||
if port in checks: return False
|
||
return True
|
||
|
||
|
||
# 获取Token
|
||
def GetToken():
|
||
try:
|
||
from json import loads
|
||
tokenFile = 'data/token.json'
|
||
if not os.path.exists(tokenFile): return False
|
||
token = loads(readFile(tokenFile))
|
||
return token
|
||
except:
|
||
return False
|
||
|
||
|
||
def to_btint(string):
|
||
m_list = []
|
||
for s in string:
|
||
m_list.append(ord(s))
|
||
return m_list
|
||
|
||
|
||
def load_module(pluginCode):
|
||
# noinspection PyUnresolvedReferences
|
||
from imp import new_module
|
||
from YakPanel import cache
|
||
p_tk = 'data/%s' % md5(pluginCode + get_uuid())
|
||
pluginInfo = None
|
||
skey = md5(pluginCode + 'code')
|
||
if cache: pluginInfo = cache.get(skey)
|
||
if not pluginInfo:
|
||
if is_self_hosted():
|
||
if not os.path.exists(p_tk):
|
||
return False
|
||
try:
|
||
pluginInfo = json.loads(ReadFile(p_tk))
|
||
except:
|
||
return False
|
||
if not pluginInfo or pluginInfo.get('status') == False:
|
||
return False
|
||
else:
|
||
import panelAuth
|
||
pdata = panelAuth.panelAuth().create_serverid(None)
|
||
pdata['pid'] = pluginCode
|
||
url = GetConfigValue('home') + '/api/panel/get_py_module'
|
||
pluginTmp = httpPost(url, pdata)
|
||
try:
|
||
pluginInfo = json.loads(pluginTmp)
|
||
except:
|
||
if not os.path.exists(p_tk): return False
|
||
pluginInfo = json.loads(ReadFile(p_tk))
|
||
if pluginInfo['status'] == False: return False
|
||
WriteFile(p_tk, json.dumps(pluginInfo))
|
||
os.chmod(p_tk, 384)
|
||
if cache: cache.set(skey, pluginInfo, 1800)
|
||
|
||
mod = sys.modules.setdefault(pluginCode, new_module(pluginCode))
|
||
code = compile(pluginInfo['msg'].encode('utf-8'), pluginCode, 'exec')
|
||
mod.__file__ = pluginCode
|
||
mod.__package__ = ''
|
||
exec(code, mod.__dict__)
|
||
return mod
|
||
|
||
|
||
# 解密数据
|
||
def auth_decode(data):
|
||
token: dict = GetToken()
|
||
# 是否有生成Token
|
||
if not token: return returnMsg(False, 'REQUEST_ERR')
|
||
|
||
# 校验access_key是否正确
|
||
if token['access_key'] != data['btauth_key']: return returnMsg(False, 'REQUEST_ERR')
|
||
|
||
# 解码数据
|
||
import binascii, hashlib, urllib, hmac, json
|
||
tdata = binascii.unhexlify(data['data'])
|
||
|
||
# 校验signature是否正确
|
||
signature = binascii.hexlify(hmac.new(token['secret_key'], tdata, digestmod=hashlib.sha256).digest())
|
||
if signature != data['signature']: return returnMsg(False, 'REQUEST_ERR')
|
||
|
||
# 返回
|
||
# noinspection PyUnresolvedReferences
|
||
return json.loads(urllib.unquote(tdata))
|
||
|
||
|
||
# 数据加密
|
||
def auth_encode(data):
|
||
token = GetToken()
|
||
pdata = {}
|
||
|
||
# 是否有生成Token
|
||
if not token: return returnMsg(False, 'REQUEST_ERR')
|
||
|
||
# 生成signature
|
||
import binascii, hashlib, urllib, hmac, json
|
||
# noinspection PyUnresolvedReferences
|
||
tdata = urllib.quote(json.dumps(data))
|
||
# 公式 hex(hmac_sha256(data))
|
||
pdata['signature'] = binascii.hexlify(hmac.new(token['secret_key'], tdata, digestmod=hashlib.sha256).digest())
|
||
|
||
# 加密数据
|
||
pdata['btauth_key'] = token['access_key']
|
||
pdata['data'] = binascii.hexlify(tdata)
|
||
pdata['timestamp'] = time.time()
|
||
|
||
# 返回
|
||
return pdata
|
||
|
||
|
||
# 检查Token
|
||
def checkToken(get):
|
||
tempFile = 'data/tempToken.json'
|
||
if not os.path.exists(tempFile): return False
|
||
import json, time
|
||
tempToken = json.loads(readFile(tempFile))
|
||
if time.time() > tempToken['timeout']: return False
|
||
if get.token != tempToken['token']: return False
|
||
return True
|
||
|
||
|
||
# 获取识别码
|
||
def get_uuid():
|
||
import uuid
|
||
return uuid.UUID(int=uuid.getnode()).hex[-12:]
|
||
|
||
|
||
# 取计算机名
|
||
def get_hostname():
|
||
import socket
|
||
return socket.gethostname()
|
||
|
||
|
||
# 取mysql datadir
|
||
def get_datadir():
|
||
mycnf_file = '/etc/my.cnf'
|
||
if not os.path.exists(mycnf_file): return ''
|
||
mycnf = readFile(mycnf_file)
|
||
import re
|
||
tmp = re.findall(r"datadir\s*=\s*(.+)", mycnf)
|
||
if not tmp: return ''
|
||
return tmp[0]
|
||
|
||
|
||
# 进程是否存在
|
||
def process_exists(pname, exe=None, cmdline=None):
|
||
try:
|
||
import psutil
|
||
pids = psutil.pids()
|
||
for pid in pids:
|
||
try:
|
||
p = psutil.Process(pid)
|
||
if p.name() == pname:
|
||
if not exe and not cmdline:
|
||
return True
|
||
else:
|
||
if exe:
|
||
if p.exe() == exe:
|
||
return True
|
||
if cmdline:
|
||
if cmdline in p.cmdline(): return True
|
||
except:
|
||
pass
|
||
|
||
return False
|
||
except:
|
||
return True
|
||
|
||
|
||
def get_process_pid(pname, exe=None, cmdline=None):
|
||
'''
|
||
@name 通过进程名获取进程PID
|
||
@author hwliang
|
||
@param pname 进程名
|
||
@param exe 进程路径
|
||
@param cmdline 进程任意命令行参数
|
||
@return int 返回进程PID
|
||
'''
|
||
import psutil
|
||
pids = psutil.pids()
|
||
for pid in pids:
|
||
try:
|
||
p = psutil.Process(pid)
|
||
if p.name() == pname:
|
||
if not exe and not cmdline:
|
||
return pid
|
||
else:
|
||
if exe:
|
||
if p.exe() == exe:
|
||
if not cmdline:
|
||
return pid
|
||
return 0
|
||
if cmdline:
|
||
if cmdline in p.cmdline(): return pid
|
||
except:
|
||
pass
|
||
return 0
|
||
|
||
|
||
# pid是否存在
|
||
def pid_exists(pid):
|
||
if os.path.exists('/proc/{}/exe'.format(pid)):
|
||
return True
|
||
return False
|
||
|
||
|
||
# 重启面板
|
||
def restart_panel():
|
||
import system
|
||
return system.system().ReWeb(None)
|
||
|
||
|
||
# 获取mac
|
||
def get_mac_address():
|
||
import uuid
|
||
mac = uuid.UUID(int=uuid.getnode()).hex[-12:]
|
||
return ":".join([mac[e:e + 2] for e in range(0, 11, 2)])
|
||
|
||
|
||
# 转码
|
||
def to_string(lites):
|
||
if type(lites) != list: lites = [lites]
|
||
m_str = ''
|
||
for mu in lites:
|
||
if sys.version_info[0] == 2:
|
||
# noinspection PyUnresolvedReferences
|
||
m_str += unichr(mu).encode('utf-8')
|
||
else:
|
||
m_str += chr(mu)
|
||
return m_str
|
||
|
||
|
||
# 解码
|
||
def to_ord(string):
|
||
o = []
|
||
for s in string:
|
||
o.append(ord(s))
|
||
return o
|
||
|
||
|
||
# xss 防御
|
||
def xssencode(text):
|
||
try:
|
||
# noinspection PyUnresolvedReferences
|
||
from cgi import html
|
||
list = ['`', '~', '&', '#', '/', '*', '$', '@', '<', '>', '\"', '\'', ';', '%', ',', '.', '\\u']
|
||
ret = []
|
||
for i in text:
|
||
if i in list:
|
||
i = ''
|
||
ret.append(i)
|
||
str_convert = ''.join(ret)
|
||
text2 = html.escape(str_convert, quote=True)
|
||
return text2
|
||
except:
|
||
return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>')
|
||
|
||
|
||
def html_decode(text):
|
||
'''
|
||
@name HTML解码
|
||
@author hwliang
|
||
@param text 要解码的HTML
|
||
@return string 返回解码后的HTML
|
||
'''
|
||
try:
|
||
# noinspection PyUnresolvedReferences
|
||
from cgi import html
|
||
text2 = html.unescape(text)
|
||
return text2
|
||
except:
|
||
return text
|
||
|
||
|
||
def html_encode(text):
|
||
'''
|
||
@name HTML编码
|
||
@author hwliang
|
||
@param text 要编码的HTML
|
||
@return string 返回编码后的HTML
|
||
'''
|
||
try:
|
||
# noinspection PyUnresolvedReferences
|
||
from cgi import html
|
||
text2 = html.escape(text)
|
||
return text2
|
||
except:
|
||
return text
|
||
|
||
|
||
# xss 防御
|
||
def xsssec(text):
|
||
return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>')
|
||
|
||
|
||
# xss 防御
|
||
def xsssec2(text):
|
||
return text.replace('<', '<').replace('>', '>')
|
||
|
||
|
||
# xss version
|
||
def xss_version(text):
|
||
try:
|
||
if not text or not isinstance(text, str): return text
|
||
text = text.strip()
|
||
list = ['`', '~', '&', '#', '/', '*', '$', '@', '<', '>', '\"', '\'', ';', '%', ',', '\\u']
|
||
ret = []
|
||
for i in text:
|
||
if i in list:
|
||
i = ''
|
||
ret.append(i)
|
||
str_convert = ''.join(ret)
|
||
return str_convert
|
||
except:
|
||
return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>')
|
||
|
||
|
||
# 获取数据库配置信息
|
||
def get_mysql_info():
|
||
data = {}
|
||
try:
|
||
CheckMyCnf()
|
||
myfile = '/etc/my.cnf'
|
||
mycnf = readFile(myfile)
|
||
rep = r"datadir\s*=\s*(.+)\n"
|
||
data['datadir'] = re.search(rep, mycnf).groups()[0]
|
||
rep = r"port\s*=\s*([0-9]+)\s*\n"
|
||
data['port'] = re.search(rep, mycnf).groups()[0]
|
||
except:
|
||
data['datadir'] = '/www/server/data'
|
||
data['port'] = '3306'
|
||
return data
|
||
|
||
|
||
# xss 防御
|
||
def xssencode2(text):
|
||
try:
|
||
# noinspection PyUnresolvedReferences
|
||
from cgi import html
|
||
text2 = html.escape(text, quote=True)
|
||
return text2
|
||
except:
|
||
return text.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>')
|
||
|
||
|
||
# 取缓存
|
||
def cache_get(key, default=None):
|
||
from YakPanel import cache
|
||
|
||
res = cache.get(key)
|
||
|
||
if res is None:
|
||
return default
|
||
|
||
return res
|
||
|
||
|
||
def add_security_logs(type, log, is_ip=True):
|
||
try:
|
||
if is_ip:
|
||
from flask import request
|
||
log = GetClientIp() + ":" + str(request.environ.get('REMOTE_PORT')) + log
|
||
M('security').add('type,log,addtime', (type, log, time.strftime('%Y-%m-%d %X', time.localtime())))
|
||
except:
|
||
pass
|
||
|
||
|
||
# 设置缓存
|
||
def cache_set(key, value, timeout=None):
|
||
from YakPanel import cache
|
||
if value == 'check':
|
||
admin_path = "/www/server/panel/data/admin_path.pl"
|
||
path = ReadFile(admin_path)
|
||
if path and len(path) > 3:
|
||
if not cache.get(GetClientIp() + 'admin_path_info'):
|
||
add_security_logs("Security entrance correct", "Successfully accessed the security entrance")
|
||
cache.set(GetClientIp() + 'admin_path_info', 1, 60)
|
||
return cache.set(key, value, timeout)
|
||
|
||
|
||
# 删除缓存
|
||
def cache_remove(key):
|
||
from YakPanel import cache
|
||
return cache.delete(key)
|
||
|
||
|
||
# 取session值
|
||
def sess_get(key):
|
||
from YakPanel import session
|
||
if key in session: return session[key]
|
||
return None
|
||
|
||
|
||
# 设置或修改session值
|
||
def sess_set(key, value):
|
||
from YakPanel import session
|
||
session[key] = value
|
||
return True
|
||
|
||
|
||
# 删除指定session值
|
||
def sess_remove(key):
|
||
from YakPanel import session
|
||
if key in session: del (session[key])
|
||
return True
|
||
|
||
|
||
# 构造分页
|
||
def get_page(count, p=1, rows=12, callback='', result='1,2,3,4,5,8'):
|
||
import page
|
||
try:
|
||
from YakPanel import request
|
||
uri = url_encode(request.full_path)
|
||
except:
|
||
uri = ''
|
||
page = page.Page()
|
||
info = {'count': count, 'row': rows, 'p': p, 'return_js': callback, 'uri': uri}
|
||
data = {'page': page.GetPage(info, result), 'shift': str(page.SHIFT), 'row': str(page.ROW)}
|
||
return data
|
||
|
||
|
||
# 取面板版本
|
||
def version():
|
||
try:
|
||
comm = ReadFile('{}/common.py'.format(get_class_path()))
|
||
return re.search(r"g\.version\s*=\s*'(\d+\.\d+\.\d+)'", comm).groups()[0]
|
||
except:
|
||
return get_panel_version()
|
||
|
||
|
||
def get_panel_version():
|
||
comm = ReadFile('{}/common.py'.format(get_class_path()))
|
||
s_key = 'g.version = '
|
||
s_len = len(s_key)
|
||
s_leff = comm.find(s_key) + s_len
|
||
version = comm[s_leff:s_leff + 10].strip().strip("'")
|
||
return version
|
||
|
||
|
||
def get_os_version():
|
||
"""
|
||
@name 取操作系统版本
|
||
@author hwliang<2021-08-07>
|
||
@return string
|
||
"""
|
||
# sys ver
|
||
version = ""
|
||
if os.path.exists("/etc/.productinfo"):
|
||
s_tmp = readFile("/etc/.productinfo").split("\n")
|
||
if s_tmp[0].find('Kylin') != -1 and len(s_tmp) > 1:
|
||
version = s_tmp[0] + ' ' + s_tmp[1].split('/')[0].strip()
|
||
|
||
release_version = (
|
||
"/etc/redhat-release",
|
||
"/etc/system-release",
|
||
"/etc/amazon-linux-release"
|
||
)
|
||
for tmp_file in release_version:
|
||
if not version and os.path.exists(tmp_file):
|
||
s_tmp = readFile(tmp_file)
|
||
version = s_tmp
|
||
break
|
||
|
||
# 2025/12 适配腾讯云及其他发行版
|
||
if not version:
|
||
tmp = public.readFile('/etc/os-release')
|
||
if tmp:
|
||
version_match = re.search(r'PRETTY_NAME=["\']?([^"\']+)["\']?', tmp)
|
||
if version_match:
|
||
version = version_match.groups()[0]
|
||
else:
|
||
version = version.replace('release ', '').replace('Linux', '').replace('(Core)', '').strip()
|
||
# py ver
|
||
v_info = sys.version_info
|
||
try:
|
||
version = "{} {}(Py{}.{}.{})".format(version, os.uname().machine, v_info.major, v_info.minor, v_info.micro)
|
||
except:
|
||
version = "{} (Py{}.{}.{})".format(version, v_info.major, v_info.minor, v_info.micro)
|
||
return xsssec(version)
|
||
|
||
#获取总大小
|
||
def get_size_total(paths = []):
|
||
|
||
data = {}
|
||
try:
|
||
if type(paths) == str:
|
||
paths = [paths]
|
||
|
||
n_list = []
|
||
for path in paths:
|
||
if os.path.exists(path):
|
||
n_list.append(path)
|
||
else:
|
||
data[path] = 0
|
||
|
||
if len(n_list) > 0:
|
||
shell = 'du -s {}'.format(' '.join(n_list).strip())
|
||
res = ExecShell(shell)[0]
|
||
for n in res.split("\n"):
|
||
tmp = n.split("\t")
|
||
if len(tmp) < 2: continue
|
||
data[tmp[1]] = int(tmp[0]) * 1024
|
||
except:pass
|
||
return data
|
||
|
||
|
||
# 取文件或目录大小
|
||
def get_path_size(path, exclude=[]):
|
||
"""根据排除目录获取路径的总大小
|
||
|
||
:path 目标路径
|
||
:exclude 排除路径单个字符串或者多个列表。匹配路径是基于path的相对路径,规则是
|
||
tar命令的--exclude规则的子集。
|
||
"""
|
||
import fnmatch
|
||
if not os.path.exists(path): return 0
|
||
if os.path.isfile(path): return os.path.getsize(path)
|
||
if type(exclude) != type([]):
|
||
exclude = [exclude]
|
||
|
||
path = path[0:-1] if path[-1] == "/" else path
|
||
path = os.path.normcase(path)
|
||
# print("path:"+ path)
|
||
# print("exclude:"+ str(exclude))
|
||
_exclude = exclude[0:]
|
||
for i, e in enumerate(_exclude):
|
||
if isinstance(e, str):
|
||
if not e.startswith(path):
|
||
basename = os.path.basename(path)
|
||
if not e.startswith(basename):
|
||
exclude.append(os.path.join(path, e))
|
||
else:
|
||
new_exc = e.replace(basename + "/", "")
|
||
new_exc = os.path.join(path, new_exc)
|
||
exclude.append(new_exc)
|
||
|
||
# print(exclude)
|
||
total_size = 0
|
||
count = 0
|
||
for root, dirs, files in os.walk(path, topdown=True):
|
||
# filter path
|
||
for exc in exclude:
|
||
for d in dirs:
|
||
sub_dir = os.path.normcase(root + os.path.sep + d)
|
||
if fnmatch.fnmatch(sub_dir, exc) or d == exc:
|
||
# print("排除目录:"+sub_dir)
|
||
dirs.remove(d)
|
||
count += 1
|
||
for f in files:
|
||
to_exclude = False
|
||
count += 1
|
||
filename = os.path.normcase(root + os.path.sep + f)
|
||
if not os.path.exists(filename): continue
|
||
if os.path.islink(filename): continue
|
||
# filter file
|
||
norm_filename = os.path.normcase(filename)
|
||
for fexc in exclude:
|
||
if fnmatch.fnmatch(norm_filename, fexc) or fexc == f:
|
||
to_exclude = True
|
||
# print("排除文件:"+norm_filename)
|
||
break
|
||
if to_exclude:
|
||
continue
|
||
total_size += os.path.getsize(filename)
|
||
return total_size
|
||
|
||
|
||
# 写关键请求日志
|
||
def write_request_log(reques=None):
|
||
try:
|
||
from YakPanel import request, g, session
|
||
if session.get('debug') == 1: return
|
||
log_path = '{}/logs/request'.format(get_panel_path())
|
||
log_file = getDate(format='%Y-%m-%d') + '.json'
|
||
if not os.path.exists(log_path): os.makedirs(log_path)
|
||
|
||
log_data = []
|
||
log_data.append(getDate())
|
||
log_data.append(GetClientIp() + ':' + get_remote_port())
|
||
log_data.append(request.method)
|
||
log_data.append(request.full_path)
|
||
log_data.append(request.headers.get('User-Agent'))
|
||
if request.method == 'POST':
|
||
args = request.form.to_dict()
|
||
for k in args.keys():
|
||
if k.find('pass') != -1 or k.find('user') != -1:
|
||
args[k] = '******'
|
||
if len(args[k]) > 4096:
|
||
args[k] = args[k][0:1024] + " -- >4096"
|
||
log_data.append(str(args))
|
||
else:
|
||
log_data.append('{}')
|
||
log_data.append(int((time.time() - g.request_time) * 1000))
|
||
log_data.append(g.response.status_code)
|
||
log_data.append(g.response.content_length)
|
||
log_data.append(g.response.headers.get('Content-Type'))
|
||
log_data.append(request.headers.get('Host'))
|
||
log_data.append(str(reques))
|
||
log_msg = json.dumps(log_data) + "\n"
|
||
WriteFile(log_path + '/' + log_file, log_msg, 'a+')
|
||
rep_sys_path()
|
||
except:
|
||
pass
|
||
|
||
|
||
# 重载模块
|
||
def mod_reload(mode):
|
||
if not mode: return False
|
||
try:
|
||
if sys.version_info[0] == 2:
|
||
reload(mode)
|
||
else:
|
||
# noinspection PyUnresolvedReferences
|
||
import imp
|
||
imp.reload(mode)
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
|
||
# 设置权限
|
||
def set_mode(filename, mode):
|
||
if not os.path.exists(filename): return False
|
||
mode = int(str(mode), 8)
|
||
try:
|
||
os.chmod(filename, mode)
|
||
except:
|
||
return False
|
||
return True
|
||
|
||
|
||
def create_linux_user(user, group):
|
||
'''
|
||
@name 创建系统用户
|
||
@author hwliang<2022-01-15>
|
||
@param user<string> 用户名
|
||
@param group<string> 所属组
|
||
@return bool
|
||
'''
|
||
ExecShell("groupadd {}".format(group))
|
||
ExecShell('useradd -s /sbin/nologin -g {} {}'.format(user, group))
|
||
return True
|
||
|
||
|
||
# 设置用户组
|
||
def set_own(filename, user, group=None):
|
||
if not os.path.exists(filename): return False
|
||
from pwd import getpwnam
|
||
try:
|
||
user_info = getpwnam(user)
|
||
user = user_info.pw_uid
|
||
if group:
|
||
user_info = getpwnam(group)
|
||
group = user_info.pw_gid
|
||
except:
|
||
if user == 'www': create_linux_user(user, group)
|
||
# 如果指定用户或组不存在,则使用www
|
||
try:
|
||
user_info = getpwnam('www')
|
||
except:
|
||
create_linux_user(user, group)
|
||
user_info = getpwnam('www')
|
||
user = user_info.pw_uid
|
||
group = user_info.pw_gid
|
||
os.chown(filename, user, group)
|
||
return True
|
||
|
||
# 递归设置用户组
|
||
def recursive_set_own(path, user, group):
|
||
"""递归应用public.set_own到目录及其内容"""
|
||
import public
|
||
|
||
# 应用到当前路径
|
||
public.set_own(path, user, group)
|
||
|
||
# 递归应用到子目录和文件
|
||
for root, dirs, files in os.walk(path):
|
||
for dir_name in dirs:
|
||
public.set_own(os.path.join(root, dir_name), user, group)
|
||
for file_name in files:
|
||
public.set_own(os.path.join(root, file_name), user, group)
|
||
|
||
# 校验路径安全
|
||
def path_safe_check(path, force=True):
|
||
if len(path) > 256: return False
|
||
checks = ['..', './', '\\', '%', '$', '^', '&', '*', '~', '"', "'", ';', '|', '{', '}', '`']
|
||
for c in checks:
|
||
if path.find(c) != -1: return False
|
||
if force:
|
||
if not match_safe_path.match(path): return False
|
||
return True
|
||
|
||
|
||
# 取数据库字符集
|
||
def get_database_character(db_name):
|
||
try:
|
||
db_obj = get_mysql_obj(db_name)
|
||
tmp = db_obj.query("show create database `%s`" % db_name.strip())
|
||
c_type = str(re.findall(r"SET\s+([\w\d-]+)\s", tmp[0][1])[0])
|
||
c_types = ['utf8', 'utf-8', 'gbk', 'big5', 'utf8mb4']
|
||
if not c_type.lower() in c_types:
|
||
return 'utf8mb4'
|
||
return ({
|
||
'utf8': 'utf8mb4',
|
||
'utf-8': 'utf8mb4',
|
||
}).get(c_type.lower(), c_type)
|
||
except:
|
||
return 'utf8mb4'
|
||
|
||
|
||
# 取mysql数据库对象
|
||
def get_mysql_obj(db_name):
|
||
is_cloud_db = False
|
||
if db_name:
|
||
db_find = M('databases').where("name=?", db_name).find()
|
||
if db_find['sid']:
|
||
return get_mysql_obj_by_sid(db_find['sid'])
|
||
is_cloud_db = db_find['db_type'] in ['1', 1]
|
||
if is_cloud_db:
|
||
import db_mysql
|
||
db_obj = db_mysql.panelMysql()
|
||
conn_config = json.loads(db_find['conn_config'])
|
||
try:
|
||
db_obj = db_obj.set_host(conn_config['db_host'], conn_config['db_port'], conn_config['db_name'],
|
||
conn_config['db_user'], conn_config['db_password'])
|
||
except Exception as e:
|
||
raise PanelError(GetMySQLError(e))
|
||
else:
|
||
import panelMysql
|
||
db_obj = panelMysql.panelMysql()
|
||
return db_obj
|
||
|
||
|
||
# 取mysql数据库对像 By sid
|
||
def get_mysql_obj_by_sid(sid=0, conn_config=None):
|
||
if sid in ['0', '']: sid = 0
|
||
if sid:
|
||
if not conn_config: conn_config = M('database_servers').where("id=?", sid).find()
|
||
import db_mysql
|
||
db_obj = db_mysql.panelMysql()
|
||
try:
|
||
db_obj = db_obj.set_host(conn_config['db_host'], conn_config['db_port'], None, conn_config['db_user'],
|
||
conn_config['db_password'])
|
||
except Exception as e:
|
||
raise PanelError(GetMySQLError(e))
|
||
else:
|
||
import panelMysql
|
||
db_obj = panelMysql.panelMysql()
|
||
return db_obj
|
||
|
||
|
||
def GetMySQLError(e):
|
||
res = ''
|
||
if e.args[0] == 1045:
|
||
res = get_msg_gettext('Database username or password is wrong!')
|
||
if e.args[0] == 1049:
|
||
res = get_msg_gettext('database does not exist!')
|
||
if e.args[0] == 1044:
|
||
res = get_msg_gettext('No permission, or the specified database does not exist!')
|
||
if e.args[0] == 1062:
|
||
res = get_msg_gettext('Database already exists!')
|
||
if e.args[0] == 1146:
|
||
res = get_msg_gettext('Table does not exist!')
|
||
if e.args[0] == 2003:
|
||
res = get_msg_gettext('Database server connection failed!')
|
||
if e.args[0] == 1142:
|
||
res = get_msg_gettext('Insufficient user rights!')
|
||
if res:
|
||
res = res + "<pre>" + str(e) + "</pre>"
|
||
else:
|
||
res = str(e)
|
||
return res
|
||
|
||
|
||
def get_database_codestr(codeing):
|
||
wheres = {
|
||
'utf8': 'utf8_general_ci',
|
||
'utf8mb4': 'utf8mb4_general_ci',
|
||
'gbk': 'gbk_chinese_ci',
|
||
'big5': 'big5_chinese_ci'
|
||
}
|
||
return wheres[codeing]
|
||
|
||
|
||
def get_database_size(name=None):
|
||
"""
|
||
@获取数据库大小
|
||
"""
|
||
data = {}
|
||
try:
|
||
mysql_obj = get_mysql_obj(name)
|
||
tables = mysql_obj.query(
|
||
"select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES group by table_schema")
|
||
if type(tables) == list:
|
||
for x in tables:
|
||
if len(x) < 2: continue
|
||
if x[1] == None: continue
|
||
data[x[0]] = int(x[1])
|
||
except:
|
||
return data
|
||
return data
|
||
|
||
|
||
def get_database_size_by_name(name):
|
||
"""
|
||
@获取数据库大小
|
||
"""
|
||
data = 0
|
||
try:
|
||
mysql_obj = get_mysql_obj(name)
|
||
tables = mysql_obj.query(
|
||
"select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES WHERE table_schema='{}' group by table_schema".format(
|
||
name))
|
||
data = tables[0][1]
|
||
if not data: data = 0
|
||
except:
|
||
return data
|
||
return data
|
||
|
||
|
||
def get_database_size_by_id(id):
|
||
"""
|
||
@获取数据库大小
|
||
"""
|
||
data = 0
|
||
try:
|
||
name = M('databases').where('id=?', id).getField('name')
|
||
mysql_obj = get_mysql_obj(name)
|
||
tables = mysql_obj.query(
|
||
"select table_schema, (sum(DATA_LENGTH)+sum(INDEX_LENGTH)) as data from information_schema.TABLES WHERE table_schema='{}' group by table_schema".format(
|
||
name))
|
||
data = tables[0][1]
|
||
if not data: data = 0
|
||
except:
|
||
return data
|
||
return data
|
||
|
||
|
||
def en_punycode(domain):
|
||
if sys.version_info[0] == 2:
|
||
domain = domain.encode('utf8')
|
||
tmp = domain.split('.')
|
||
newdomain = ''
|
||
for dkey in tmp:
|
||
if dkey == '*': continue
|
||
# 匹配非ascii字符
|
||
match = re.search(u"[\x80-\xff]+", dkey)
|
||
if not match: match = re.search(u"[\u4e00-\u9fa5]+", dkey)
|
||
if not match:
|
||
newdomain += dkey + '.'
|
||
else:
|
||
if sys.version_info[0] == 2:
|
||
newdomain += 'xn--' + dkey.decode('utf-8').encode('punycode') + '.'
|
||
else:
|
||
newdomain += 'xn--' + dkey.encode('punycode').decode('utf-8') + '.'
|
||
if tmp[0] == '*': newdomain = "*." + newdomain
|
||
return newdomain[0:-1]
|
||
|
||
|
||
# punycode 转中文
|
||
def de_punycode(domain):
|
||
tmp = domain.split('.')
|
||
newdomain = ''
|
||
for dkey in tmp:
|
||
if dkey.find('xn--') >= 0:
|
||
newdomain += dkey.replace('xn--', '').encode('utf-8').decode('punycode') + '.'
|
||
else:
|
||
newdomain += dkey + '.'
|
||
return newdomain[0:-1]
|
||
|
||
|
||
# 取计划任务文件路径
|
||
def get_cron_path():
|
||
u_file = '/var/spool/cron/crontabs/root'
|
||
if not os.path.exists(u_file):
|
||
file = '/var/spool/cron/root'
|
||
else:
|
||
file = u_file
|
||
return file
|
||
|
||
|
||
# 加密字符串
|
||
def en_crypt(key, strings):
|
||
try:
|
||
if type(strings) != bytes: strings = strings.encode('utf-8')
|
||
from cryptography.fernet import Fernet
|
||
f = Fernet(key)
|
||
result = f.encrypt(strings)
|
||
return result.decode('utf-8')
|
||
except:
|
||
# print(get_error_info())
|
||
return strings
|
||
|
||
|
||
# 解密字符串
|
||
def de_crypt(key, strings):
|
||
try:
|
||
if type(strings) != bytes: strings = strings.decode('utf-8')
|
||
from cryptography.fernet import Fernet
|
||
f = Fernet(key)
|
||
result = f.decrypt(strings).decode('utf-8')
|
||
return result
|
||
except:
|
||
# print(get_error_info())
|
||
return strings
|
||
|
||
|
||
# 获取IP限制列表
|
||
def get_limit_ip():
|
||
iplong_list = []
|
||
try:
|
||
ip_file = 'data/limitip.conf'
|
||
if not os.path.exists(ip_file): return iplong_list
|
||
|
||
from YakPanel import cache
|
||
ikey = 'limit_ip'
|
||
iplong_list = cache.get(ikey)
|
||
if iplong_list: return iplong_list
|
||
|
||
iplong_list = []
|
||
iplist = ReadFile(ip_file)
|
||
if not iplist: return iplong_list
|
||
iplist = iplist.strip()
|
||
for limit_ip in iplist.split(','):
|
||
if not limit_ip: continue
|
||
limit_ip = limit_ip.split('-')
|
||
iplong = {}
|
||
iplong['min'] = ip2long(limit_ip[0])
|
||
if len(limit_ip) > 1:
|
||
iplong['max'] = ip2long(limit_ip[1])
|
||
else:
|
||
iplong['max'] = iplong['min']
|
||
iplong_list.append(iplong)
|
||
|
||
cache.set(ikey, iplong_list, 3600)
|
||
except:
|
||
pass
|
||
return iplong_list
|
||
|
||
|
||
# def is_api_limit_ip(ip_list, client_ip):
|
||
# '''
|
||
# @name 判断IP是否在限制列表中
|
||
# @author hwliang<2022-02-10>
|
||
# @param ip_list<list> 限制IP列表
|
||
# @param client_ip<string> 客户端IP
|
||
# @return bool
|
||
# '''
|
||
# iplong_list = []
|
||
# for limit_ip in ip_list:
|
||
# if not limit_ip: continue
|
||
# if limit_ip in ['*', 'all', '0.0.0.0', '0.0.0.0/0', '0.0.0.0/24', '0.0.0.0/32']: return True
|
||
# limit_ip = limit_ip.split('-')
|
||
# iplong = {}
|
||
# iplong['min'] = ip2long(limit_ip[0])
|
||
# if len(limit_ip) > 1:
|
||
# iplong['max'] = ip2long(limit_ip[1])
|
||
# else:
|
||
# iplong['max'] = iplong['min']
|
||
# iplong_list.append(iplong)
|
||
#
|
||
# client_ip_long = ip2long(client_ip)
|
||
# for limit_ip in iplong_list:
|
||
# if client_ip_long >= limit_ip['min'] and client_ip_long <= limit_ip['max']:
|
||
# return True
|
||
# return False
|
||
|
||
|
||
def is_api_limit_ip(ip_list, client_ip):
|
||
'''
|
||
@name 判断IP是否在限制列表中 (支持 IPv4, IPv6, CIDR, IP范围)
|
||
@param ip_list<list> 限制IP列表 (支持 '192.168.1.1', '192.168.1.0/24', '1.1.1.1-1.1.1.5', '::1')
|
||
@param client_ip<string> 客户端IP
|
||
@return bool
|
||
'''
|
||
import ipaddress
|
||
try:
|
||
client_obj = ipaddress.ip_address(client_ip)
|
||
client_version = client_obj.version # 记录客户端IP版本
|
||
except ValueError:
|
||
return False
|
||
|
||
# 保留原有ip段
|
||
wildcards = {
|
||
4 : {'*', 'all', '0.0.0.0', '0.0.0.0/0'},
|
||
6 : {'*', 'all', '::', '::/0'}
|
||
}
|
||
|
||
for rule in ip_list:
|
||
if not rule:
|
||
continue
|
||
|
||
rule = rule.strip()
|
||
|
||
if rule in wildcards[client_version]:
|
||
return True
|
||
|
||
try:
|
||
if '-' in rule:
|
||
parts = rule.split('-')
|
||
if len(parts) == 2:
|
||
start_ip = ipaddress.ip_address(parts[0])
|
||
end_ip = ipaddress.ip_address(parts[1])
|
||
|
||
if (start_ip.version == client_obj.version and
|
||
start_ip <= client_obj <= end_ip):
|
||
return True
|
||
|
||
else:
|
||
network = ipaddress.ip_network(rule, strict=False)
|
||
if client_obj in network:
|
||
return True
|
||
|
||
except (ValueError, TypeError):
|
||
continue
|
||
|
||
return False
|
||
|
||
# 检查IP白名单
|
||
def check_ip_panel():
|
||
iplong_list = get_limit_ip()
|
||
if not iplong_list: return False
|
||
client_ip = GetClientIp()
|
||
if client_ip in ['127.0.0.1', 'localhost', '::1']: return False
|
||
client_ip_long = ip2long(client_ip)
|
||
for limit_ip in iplong_list:
|
||
if client_ip_long >= limit_ip['min'] and client_ip_long <= limit_ip['max']:
|
||
return False
|
||
|
||
# errorStr = ReadFile('./YakPanel/templates/' + GetConfigValue('template') + '/error2.html')
|
||
# try:
|
||
# errorStr = errorStr.format(getMsg('PAGE_ERR_TITLE'),getMsg('PAGE_ERR_IP_H1'),getMsg('PAGE_ERR_IP_P1',(GetClientIp(),)),getMsg('PAGE_ERR_IP_P2'),getMsg('PAGE_ERR_IP_P3'),getMsg('NAME'),getMsg('PAGE_ERR_HELP'))
|
||
# except IndexError:pass
|
||
# return error_not_login(errorStr,True)
|
||
return error_403(None)
|
||
|
||
|
||
# 检查面板域名
|
||
def check_domain_panel():
|
||
tmp = GetHost()
|
||
domain = ReadFile('data/domain.conf')
|
||
if domain:
|
||
client_ip = GetClientIp()
|
||
if client_ip in ['127.0.0.1', 'localhost', '::1']: return False
|
||
if tmp.strip().lower() != domain.strip().lower():
|
||
if check_client_info():
|
||
try:
|
||
from flask import render_template
|
||
# noinspection PyUnresolvedReferences
|
||
return render_template('error2.html')
|
||
except:
|
||
pass
|
||
|
||
return error_403(None)
|
||
return False
|
||
|
||
|
||
# 是否离线模式
|
||
def is_local():
|
||
s_file = '{}/data/not_network.pl'.format(get_panel_path())
|
||
return os.path.exists(s_file)
|
||
|
||
|
||
# Dump面板数据库结构+数据
|
||
def dump_panel_databases():
|
||
backup_path = '{}/data/db_backups'.format(get_panel_path())
|
||
|
||
if not os.path.exists(backup_path):
|
||
os.makedirs(backup_path, 0o755)
|
||
|
||
backup_databases = (
|
||
'default',
|
||
)
|
||
|
||
def row_check_func(row: str) -> bool:
|
||
return row.find('INSERT INTO "logs" ') < 0
|
||
|
||
for db_name in backup_databases:
|
||
with SqliteConn(db_name) as db:
|
||
db.dump('{}/{}.sql'.format(backup_path, db_name), row_check_func)
|
||
|
||
# 自动备份面板数据
|
||
def auto_backup_panel():
|
||
try:
|
||
panel_paeh = get_panel_path()
|
||
paths = panel_paeh + '/data/not_auto_backup.pl'
|
||
if os.path.exists(paths): return False
|
||
b_path = '{}/panel'.format(get_backup_path())
|
||
os.makedirs(b_path, exist_ok=True)
|
||
day_date = format_date('%Y-%m-%d')
|
||
backup_path = b_path + '/' + day_date
|
||
backup_file = backup_path + '.zip'
|
||
if os.path.exists(backup_path) or os.path.exists(backup_file): return True
|
||
backup_number=30
|
||
backup_conf=panel_paeh + '/data/backup_number.pl'
|
||
if os.path.exists(backup_conf):
|
||
try:
|
||
backup_number = int(open(backup_conf, 'r').read())
|
||
except:
|
||
pass
|
||
if backup_number < 1:
|
||
return False
|
||
|
||
|
||
# 导出面板数据库结构+数据
|
||
dump_panel_databases()
|
||
|
||
ignore_default = ''
|
||
ignore_system = ''
|
||
max_size = 100 * 1024 * 1024
|
||
if os.path.getsize('{}/data/default.db'.format(panel_paeh)) > max_size:
|
||
ignore_default = 'default.db'
|
||
if os.path.getsize('{}/data/system.db'.format(panel_paeh)) > max_size:
|
||
ignore_system = 'system.db'
|
||
os.makedirs(backup_path, 384)
|
||
ignore_list = [
|
||
ignore_system, ignore_default,
|
||
'wp_package_checksums', 'wp_packages', 'maillog', 'mail', '*.sock','hids_data'
|
||
]
|
||
cp_dir(f"{panel_paeh}/data", f"{backup_path}/data", ignores=ignore_list)
|
||
cp_dir(f"{panel_paeh}/config", f"{backup_path}/config")
|
||
cp_dir(f"{panel_paeh}/vhost", f"{backup_path}/vhost")
|
||
|
||
ExecShell("cd {} && zip {} -r {}/".format(b_path, backup_file, day_date))
|
||
ExecShell("chmod -R 600 {path};chown -R root.root {path}".format(path=backup_file))
|
||
if os.path.exists(backup_path): shutil.rmtree(backup_path)
|
||
|
||
clear_panel_backup(b_path,backup_number)
|
||
set_php_cli_env()
|
||
except:
|
||
pass
|
||
|
||
#清理面板备份
|
||
def clear_panel_backup(backup_path,backup_number):
|
||
backup_time_list = []
|
||
if not os.path.exists(backup_path):
|
||
return
|
||
for f in os.listdir(backup_path):
|
||
f_path=backup_path + '/' +f
|
||
try:
|
||
zip_string=""
|
||
if f.endswith('.zip'):zip_string=".zip"
|
||
mktime=time.mktime(time.strptime(f,"%Y-%m-%d"+zip_string))
|
||
day_date=public.format_date("%Y-%m-%d",times=mktime)
|
||
day_date_file = backup_path + '/' + day_date
|
||
backup_file = day_date_file+ '.zip'
|
||
if f_path==backup_file or (f_path==day_date_file and os.path.isdir(f_path)):
|
||
backup_time_list.append(mktime)
|
||
except:
|
||
pass
|
||
#将backup_time_list按降序排序
|
||
backup_time_list.sort(reverse=True)
|
||
#删除多余的备份
|
||
for i in range(backup_number,len(backup_time_list)):
|
||
day_date_file=backup_path + '/' + public.format_date("%Y-%m-%d",times=backup_time_list[i])
|
||
f_path=day_date_file+ '.zip'
|
||
if os.path.exists(f_path):
|
||
os.remove(f_path)
|
||
elif os.path.exists(day_date_file) and os.path.isdir(day_date_file):
|
||
shutil.rmtree(day_date_file)
|
||
|
||
|
||
|
||
def set_php_cli_env():
|
||
'''
|
||
@name 重新设置php-cli.ini配置
|
||
'''
|
||
import jobs
|
||
jobs.set_php_cli_env()
|
||
|
||
|
||
# 检查端口状态
|
||
def check_port_stat(port, localIP='127.0.0.1'):
|
||
import socket
|
||
temp = {}
|
||
temp['port'] = port
|
||
temp['local'] = True
|
||
try:
|
||
s = socket.socket()
|
||
s.settimeout(0.15)
|
||
s.connect((localIP, port))
|
||
s.close()
|
||
except:
|
||
temp['local'] = False
|
||
|
||
result = 0
|
||
if temp['local']: result += 2
|
||
return result
|
||
|
||
|
||
# 同步时间
|
||
def sync_date():
|
||
if is_self_hosted():
|
||
return False
|
||
tip_file = "/dev/shm/last_sync_time.pl"
|
||
s_time = int(time.time())
|
||
try:
|
||
if os.path.exists(tip_file):
|
||
if s_time - int(readFile(tip_file)) < 60: return False
|
||
os.remove(tip_file)
|
||
time_str = HttpGet(GetConfigValue('home') + '/api/index/get_time')
|
||
new_time = int(time_str)
|
||
time_arr = time.localtime(new_time)
|
||
date_str = time.strftime("%Y-%m-%d %H:%M:%S", time_arr)
|
||
ExecShell('date -s "%s"' % date_str)
|
||
writeFile(tip_file, str(s_time))
|
||
return True
|
||
except:
|
||
if os.path.exists(tip_file): os.remove(tip_file)
|
||
return False
|
||
|
||
|
||
# 重载模块
|
||
def reload_mod(mod_name=None):
|
||
# 是否重载指定模块
|
||
modules = []
|
||
if mod_name:
|
||
if type(mod_name) == str:
|
||
mod_names = mod_name.split(',')
|
||
|
||
for mod_name in mod_names:
|
||
if mod_name in sys.modules:
|
||
print(mod_name)
|
||
try:
|
||
if sys.version_info[0] == 2:
|
||
reload(sys.modules[mod_name])
|
||
else:
|
||
importlib.reload(sys.modules[mod_name])
|
||
modules.append([mod_name, True])
|
||
except:
|
||
modules.append([mod_name, False])
|
||
else:
|
||
modules.append([mod_name, False])
|
||
return modules
|
||
|
||
# 重载所有模块
|
||
for mod_name in sys.modules.keys():
|
||
if mod_name in ['YakPanel']: continue
|
||
f = getattr(sys.modules[mod_name], '__file__', None)
|
||
if f:
|
||
try:
|
||
if f.find('panel/') == -1: continue
|
||
if sys.version_info[0] == 2:
|
||
reload(sys.modules[mod_name])
|
||
else:
|
||
importlib.reload(sys.modules[mod_name])
|
||
modules.append([mod_name, True])
|
||
except:
|
||
modules.append([mod_name, False])
|
||
return modules
|
||
|
||
|
||
def de_hexb(data):
|
||
if sys.version_info[0] != 2:
|
||
if type(data) == str:
|
||
data = data.encode('utf-8')
|
||
pdata = base64.b64encode(data)
|
||
if sys.version_info[0] != 2:
|
||
if type(pdata) == str:
|
||
# noinspection PyUnresolvedReferences
|
||
pdata = pdata.encode('utf-8')
|
||
return binascii.hexlify(pdata)
|
||
|
||
|
||
def en_hexb(data):
|
||
if sys.version_info[0] != 2:
|
||
if type(data) == str: data = data.encode('utf-8')
|
||
result = base64.b64decode(binascii.unhexlify(data))
|
||
if type(result) != str: result = result.decode('utf-8')
|
||
return result
|
||
|
||
|
||
# def upload_file_url(filename):
|
||
# try:
|
||
# if os.path.exists(filename):
|
||
# data = ExecShell('/usr/bin/curl https://scanner.baidu.com/enqueue -F archive=@%s' % filename)
|
||
# data = json.loads(data[0])
|
||
# time.sleep(1)
|
||
# import requests
|
||
# default_headers = {
|
||
# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'
|
||
# }
|
||
# data_list = requests.get(url=data['url'], headers=default_headers, verify=False)
|
||
# return (data_list.json())
|
||
# else:
|
||
# return False
|
||
# except:
|
||
# return False
|
||
|
||
# 直接请求到PHP-FPM
|
||
# version php版本
|
||
# uri 请求uri
|
||
# filename 要执行的php文件
|
||
# args 请求参数
|
||
# method 请求方式
|
||
def request_php(version, uri, document_root, method='GET', pdata=b''):
|
||
import panelPHP
|
||
if type(pdata) == dict: pdata = url_encode(pdata)
|
||
fpm_address = get_fpm_address(version)
|
||
p = panelPHP.FPM(fpm_address, document_root)
|
||
result = p.load_url_public(uri, pdata, method)
|
||
return result
|
||
|
||
|
||
def get_fpm_address(php_version, bind=False):
|
||
'''
|
||
@name 获取FPM请求地址
|
||
@author hwliang<2020-10-23>
|
||
@param php_version string PHP版本
|
||
@return tuple or string
|
||
'''
|
||
fpm_address = '/tmp/php-cgi-{}.sock'.format(php_version)
|
||
php_fpm_file = '{}/php/{}/etc/php-fpm.conf'.format(get_setup_path(), php_version)
|
||
try:
|
||
fpm_conf = readFile(php_fpm_file)
|
||
tmp = re.findall(r"listen\s*=\s*(.+)", fpm_conf)
|
||
if not tmp: return fpm_address
|
||
if tmp[0].find('sock') != -1: return fpm_address
|
||
if tmp[0].find(':') != -1:
|
||
listen_tmp = tmp[0].split(':')
|
||
if bind:
|
||
fpm_address = (listen_tmp[0], int(listen_tmp[1]))
|
||
else:
|
||
fpm_address = ('127.0.0.1', int(listen_tmp[1]))
|
||
else:
|
||
fpm_address = ('127.0.0.1', int(tmp[0]))
|
||
return fpm_address
|
||
except:
|
||
return fpm_address
|
||
|
||
|
||
def get_php_proxy(php_version, webserver='nginx'):
|
||
'''
|
||
@name 获取PHP代理地址
|
||
@author hwliang<2020-10-24>
|
||
@param php_version string php版本 (52|53|54|55|56|70|71|72|73|74)
|
||
@param webserver string web服务器类型 (nginx|apache|ols)
|
||
return string
|
||
'''
|
||
php_address = get_fpm_address(php_version)
|
||
if isinstance(php_address, str):
|
||
if webserver == 'nginx':
|
||
return 'unix:{}'.format(php_address)
|
||
elif webserver == 'apache':
|
||
return 'unix:{}|fcgi://localhost'.format(php_address)
|
||
else:
|
||
if webserver == 'nginx':
|
||
return '{}:{}'.format(php_address[0], php_address[1])
|
||
elif webserver == 'apache':
|
||
return 'fcgi://{}:{}'.format(php_address[0], php_address[1])
|
||
|
||
|
||
def get_php_version_conf(conf):
|
||
'''
|
||
@name 从指定配置文件获取PHP版本
|
||
@author hwliang<2020-10-24>
|
||
@param conf string 配置文件内容
|
||
@return string
|
||
'''
|
||
if not conf: return '00'
|
||
if conf.find('enable-php-') != -1:
|
||
rep = r"enable-php-(\w{2,5})[-\w]*\.conf"
|
||
tmp = re.findall(rep, conf)
|
||
if not tmp: return '00'
|
||
elif conf.find('/usr/local/lsws/lsphp') != -1:
|
||
rep = r"path\s*/usr/local/lsws/lsphp(\d+)/bin/lsphp"
|
||
tmp = re.findall(rep, conf)
|
||
if not tmp: return '00'
|
||
else:
|
||
rep = r"php-cgi-([0-9]{2,3})\.sock"
|
||
tmp = re.findall(rep, conf)
|
||
if not tmp:
|
||
rep = r'\d+\.\d+\.\d+\.\d+:10(\d{2,2})1'
|
||
tmp = re.findall(rep, conf)
|
||
if not tmp:
|
||
return '00'
|
||
return tmp[0]
|
||
|
||
|
||
def get_site_php_version(siteName):
|
||
'''
|
||
@name 获取指定网站当前使用的PHP版本
|
||
@author hwliang<2020-10-24>
|
||
@param siteName string 网站名称
|
||
@return string
|
||
'''
|
||
web_server = get_webserver()
|
||
if public.get_multi_webservice_status():
|
||
site = public.M('sites').where('name = ?',siteName).field('service_type').find()
|
||
web_server = site['service_type'] if site['service_type'] else 'nginx'
|
||
vhost_path = get_vhost_path()
|
||
conf = readFile(vhost_path + '/' + web_server + '/' + siteName + '.conf')
|
||
if web_server == 'openlitespeed':
|
||
conf = readFile(vhost_path + '/' + web_server + '/detail/' + siteName + '.conf')
|
||
return get_php_version_conf(conf)
|
||
|
||
|
||
def check_tcp(ip, port):
|
||
'''
|
||
@name 使用TCP的方式检测指定IP:端口是否能连接
|
||
@author hwliang<2021-06-01>
|
||
@param ip<string> IP地址
|
||
@param port<int> 端口
|
||
@return bool
|
||
'''
|
||
import socket
|
||
try:
|
||
s = socket.socket()
|
||
s.settimeout(5)
|
||
s.connect((ip.strip(), int(port)))
|
||
s.close()
|
||
except:
|
||
return False
|
||
return True
|
||
|
||
|
||
def sub_php_address(conf_file, rep, tsub, php_version):
|
||
'''
|
||
@name 替换新的PHP配置到配置文件
|
||
@author hwliang<2020-10-24>
|
||
@param conf_file string 配置文件全路径
|
||
@param rep string 用于查找目标替换内容的正则表达式
|
||
@param tsub string 新的内容
|
||
@param php_version string 指定PHP版本
|
||
@return bool
|
||
'''
|
||
if not os.path.isfile(conf_file): return False
|
||
if not os.path.exists(conf_file): return False
|
||
conf = readFile(conf_file)
|
||
if not conf: return False
|
||
# if conf.find('#PHP') == -1 and conf.find('pathinfo.conf') == -1: return False
|
||
if conf_file.split('-')[-1].find(php_version + ".") != 0:
|
||
phpv = get_php_version_conf(conf)
|
||
if phpv != php_version: return False
|
||
|
||
tmp = re.search(rep, conf)
|
||
if not tmp: return False
|
||
if tmp.group() == tsub: return False
|
||
conf = conf.replace(tmp.group(), tsub) # re.sub(rep,php_proxy,conf)
|
||
writeFile(conf_file, conf)
|
||
return True
|
||
|
||
|
||
def sync_all_address():
|
||
'''
|
||
@name 同步所有PHP版本配置到配置文件
|
||
@author hwliang<2020-10-24>
|
||
@return void
|
||
'''
|
||
php_versions = get_php_versions()
|
||
for phpv in php_versions:
|
||
sync_php_address(phpv)
|
||
|
||
|
||
def sync_php_address(php_version):
|
||
'''
|
||
@name 同步PHP版本配置到所有配置文件
|
||
@author hwliang<2020-10-24>
|
||
@param php_version string PHP版本
|
||
@return void
|
||
'''
|
||
if not os.path.exists('{}/php/{}/bin/php'.format(get_setup_path(), php_version)): # 指定PHP版本是否安装
|
||
return False
|
||
ngx_rep = r"(unix:/tmp/php-cgi.*\.sock|\d+\.\d+\.\d+\.\d+:\d+)"
|
||
apa_rep = r"(unix:/tmp/php-cgi.*\.sock\|fcgi://localhost|fcgi://\d+\.\d+\.\d+\.\d+:\d+)"
|
||
ngx_proxy = get_php_proxy(php_version, 'nginx')
|
||
apa_proxy = get_php_proxy(php_version, 'apache')
|
||
is_write = False
|
||
|
||
# nginx的PHP配置文件
|
||
nginx_conf_path = '{}/nginx/conf'.format(get_setup_path())
|
||
|
||
if os.path.exists(nginx_conf_path):
|
||
for f_name in os.listdir(nginx_conf_path):
|
||
if f_name.find('enable-php') != -1:
|
||
conf_file = '/'.join((nginx_conf_path, f_name))
|
||
if sub_php_address(conf_file, ngx_rep, ngx_proxy, php_version):
|
||
is_write = True
|
||
# nginx的phpmyadmin
|
||
# conf_file = '/www/server/nginx/conf/nginx.conf'
|
||
# if os.path.exists(conf_file):
|
||
# if sub_php_address(conf_file,ngx_rep,ngx_proxy,php_version):
|
||
# is_write = True
|
||
|
||
# apache的网站配置文件
|
||
apache_conf_path = '{}/apache'.format(get_vhost_path())
|
||
if os.path.exists(apache_conf_path):
|
||
for f_name in os.listdir(apache_conf_path):
|
||
conf_file = '/'.join((apache_conf_path, f_name))
|
||
if sub_php_address(conf_file, apa_rep, apa_proxy, php_version):
|
||
is_write = True
|
||
# apache的phpmyadmin
|
||
conf_file = '{}/apache/conf/extra/httpd-vhosts.conf'.format(get_setup_path())
|
||
if os.path.exists(conf_file):
|
||
if sub_php_address(conf_file, apa_rep, apa_proxy, php_version):
|
||
is_write = True
|
||
|
||
if is_write: serviceReload()
|
||
return True
|
||
|
||
|
||
def url_encode(data):
|
||
if type(data) != str: return data
|
||
if sys.version_info[0] != 2:
|
||
import urllib.parse
|
||
pdata = urllib.parse.quote(data)
|
||
else:
|
||
import urllib
|
||
# noinspection PyUnresolvedReferences
|
||
pdata = urllib.urlencode(data)
|
||
return pdata
|
||
|
||
|
||
def url_decode(data):
|
||
if type(data) != str: return data
|
||
if sys.version_info[0] != 2:
|
||
import urllib.parse
|
||
pdata = urllib.parse.unquote(data)
|
||
else:
|
||
import urllib
|
||
# noinspection PyUnresolvedReferences
|
||
pdata = urllib.urldecode(data)
|
||
return pdata
|
||
|
||
|
||
def unicode_encode(data):
|
||
try:
|
||
if sys.version_info[0] == 2:
|
||
# noinspection PyUnresolvedReferences
|
||
result = unicode(data, errors='ignore')
|
||
else:
|
||
result = data.encode('utf8', errors='ignore')
|
||
return result
|
||
except:
|
||
return data
|
||
|
||
|
||
def unicode_decode(data, charset='utf8'):
|
||
try:
|
||
if sys.version_info[0] == 2:
|
||
# noinspection PyUnresolvedReferences
|
||
result = unicode(data, errors='ignore')
|
||
else:
|
||
result = data.decode('utf8', errors='ignore')
|
||
return result
|
||
except:
|
||
return data
|
||
|
||
|
||
# noinspection PyUnresolvedReferences
|
||
def import_cdn_plugin():
|
||
plugin_path = 'plugin/static_cdn'
|
||
if not os.path.exists(plugin_path): return True
|
||
try:
|
||
import static_cdn_main
|
||
except:
|
||
package_path_append(plugin_path)
|
||
import static_cdn_main
|
||
|
||
|
||
def get_cdn_hosts():
|
||
try:
|
||
if import_cdn_plugin(): return []
|
||
# noinspection PyUnresolvedReferences
|
||
import static_cdn_main
|
||
return static_cdn_main.static_cdn_main().get_hosts(None)
|
||
except:
|
||
return []
|
||
|
||
|
||
def get_cdn_url():
|
||
try:
|
||
if os.path.exists('plugin/static_cdn/not_open.pl'):
|
||
return False
|
||
from YakPanel import cache
|
||
cdn_url = cache.get('cdn_url')
|
||
if cdn_url: return cdn_url
|
||
if import_cdn_plugin(): return False
|
||
# noinspection PyUnresolvedReferences
|
||
import static_cdn_main
|
||
cdn_url = static_cdn_main.static_cdn_main().get_url(None)
|
||
cache.set('cdn_url', cdn_url, 3)
|
||
return cdn_url
|
||
except:
|
||
return False
|
||
|
||
|
||
def set_cdn_url(cdn_url):
|
||
if not cdn_url: return False
|
||
import_cdn_plugin()
|
||
get = dict_obj()
|
||
get.cdn_url = cdn_url
|
||
# noinspection PyUnresolvedReferences
|
||
import static_cdn_main
|
||
static_cdn_main.static_cdn_main().set_url(get)
|
||
return True
|
||
|
||
|
||
def get_python_bin():
|
||
bin_file = '{}/pyenv/bin/python3'.format(get_panel_path())
|
||
if os.path.exists(bin_file):
|
||
return bin_file
|
||
return '/usr/bin/python'
|
||
|
||
|
||
def get_pip_bin():
|
||
bin_file = '{}/pyenv/bin/pip'.format(get_panel_path())
|
||
if os.path.exists(bin_file):
|
||
return bin_file
|
||
return '/usr/bin/pip'
|
||
|
||
|
||
def aes_encrypt(data, key):
|
||
import panelAes
|
||
if sys.version_info[0] == 2:
|
||
aes_obj = panelAes.aescrypt_py2(key)
|
||
return aes_obj.aesencrypt(data)
|
||
else:
|
||
aes_obj = panelAes.aescrypt_py3(key)
|
||
return aes_obj.aesencrypt(data)
|
||
|
||
|
||
def aes_decrypt(data, key):
|
||
import panelAes
|
||
if sys.version_info[0] == 2:
|
||
aes_obj = panelAes.aescrypt_py2(key)
|
||
return aes_obj.aesdecrypt(data)
|
||
else:
|
||
aes_obj = panelAes.aescrypt_py3(key)
|
||
return aes_obj.aesdecrypt(data)
|
||
|
||
|
||
# 清理大日志文件
|
||
def clean_max_log(log_file, max_size=100, old_line=100):
|
||
if not os.path.exists(log_file): return False
|
||
max_size = 1024 * 1024 * max_size
|
||
if os.path.getsize(log_file) > max_size:
|
||
try:
|
||
old_body = GetNumLines(log_file, old_line)
|
||
writeFile(log_file, old_body)
|
||
except:
|
||
print(get_error_info())
|
||
|
||
|
||
# 获取证书哈希
|
||
def get_cert_data(path):
|
||
import panelSSL
|
||
get = dict_obj()
|
||
get.certPath = path
|
||
data = panelSSL.panelSSL().GetCertName(get)
|
||
return data
|
||
|
||
|
||
# 获取系统发行版
|
||
def get_linux_distribution():
|
||
distribution = 'ubuntu'
|
||
redhat_file = '/etc/redhat-release'
|
||
if os.path.exists(redhat_file):
|
||
try:
|
||
tmp = readFile(redhat_file).split()[3][0]
|
||
distribution = 'centos{}'.format(tmp)
|
||
except:
|
||
distribution = 'centos7'
|
||
|
||
# # 检查是否为 Amazon Linux
|
||
# if os.path.exists('/etc/system-release') and 'Amazon Linux' in readFile('/etc/system-release'):
|
||
# distribution = 'amazon-linux'
|
||
|
||
return distribution
|
||
|
||
|
||
def long2ip(ips):
|
||
'''
|
||
@name 将整数转换为IP地址
|
||
@author hwliang<2020-06-11>
|
||
@param ips string(ip地址整数)
|
||
@return ipv4
|
||
'''
|
||
i1 = int(ips / (2 ** 24))
|
||
i2 = int((ips - i1 * (2 ** 24)) / (2 ** 16))
|
||
i3 = int(((ips - i1 * (2 ** 24)) - i2 * (2 ** 16)) / (2 ** 8))
|
||
i4 = int(((ips - i1 * (2 ** 24)) - i2 * (2 ** 16)) - i3 * (2 ** 8))
|
||
return "{}.{}.{}.{}".format(i1, i2, i3, i4)
|
||
|
||
|
||
def ip2long(ip):
|
||
'''
|
||
@name 将IP地址转换为整数
|
||
@author hwliang<2020-06-11>
|
||
@param ip string(ipv4)
|
||
@return long
|
||
'''
|
||
ips = ip.split('.')
|
||
if len(ips) != 4: return 0
|
||
iplong = 2 ** 24 * int(ips[0]) + 2 ** 16 * int(ips[1]) + 2 ** 8 * int(ips[2]) + int(ips[3])
|
||
return iplong
|
||
|
||
|
||
def is_local_ip(ip):
|
||
'''
|
||
@name 判断是否为本地(内网)IP地址
|
||
@author hwliang<2021-03-26>
|
||
@param ip string(ipv4)
|
||
@return bool
|
||
'''
|
||
patt = r"^(192\.168|127|10|172\.(16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31))\."
|
||
if re.match(patt, ip): return True
|
||
return False
|
||
|
||
|
||
# 获取debug日志
|
||
def get_debug_log():
|
||
from YakPanel import request
|
||
return GetClientIp() + ':' + str(request.environ.get('REMOTE_PORT')) + '|' + str(
|
||
int(time.time())) + '|' + get_error_info()
|
||
|
||
|
||
# 获取sessionid
|
||
def get_session_id():
|
||
from YakPanel import request, app
|
||
session_id = request.cookies.get(app.config['SESSION_COOKIE_NAME'], '')
|
||
if not re.findall(r"^([\w\.-]{64,64})$", session_id):
|
||
return GetRandomString(64)
|
||
return session_id
|
||
|
||
|
||
# 尝试自动恢复面板数据库
|
||
def rep_default_db():
|
||
db_path = '{}/data/'.format(get_panel_path())
|
||
db_file = db_path + 'default.db'
|
||
db_tmp_backup = db_path + 'default_' + format_date("%Y%m%d_%H%M%S") + ".db"
|
||
|
||
panel_backup = '{}/panel'.format(get_backup_path())
|
||
bak_list = os.listdir(panel_backup)
|
||
if not bak_list: return False
|
||
bak_list = sorted(bak_list, reverse=True)
|
||
db_bak_file = ''
|
||
for d_name in bak_list:
|
||
db_bak_file = panel_backup + '/' + d_name + '/data/default.db'
|
||
if not os.path.exists(db_bak_file): continue
|
||
if os.path.getsize(db_bak_file) < 17408: continue
|
||
break
|
||
|
||
if not db_bak_file: return False
|
||
ExecShell(r"\cp -arf {} {}".format(db_file, db_tmp_backup))
|
||
ExecShell(r"\cp -arf {} {}".format(db_bak_file, db_file))
|
||
return True
|
||
|
||
|
||
def chdck_salt():
|
||
'''
|
||
@name 检查所有用户密码是否加盐,若没有则自动加上
|
||
@author hwliang<2020-07-08>
|
||
@return void
|
||
'''
|
||
|
||
if not M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users', '%salt%')).count():
|
||
M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT", ())
|
||
u_list = M('users').where('salt is NULL', ()).field('id,username,password,salt').select()
|
||
if isinstance(u_list, str):
|
||
if u_list.find('no such table: users') != -1:
|
||
rep_default_db()
|
||
if not M('sqlite_master').where('type=? AND name=? AND sql LIKE ?', ('table', 'users', '%salt%')).count():
|
||
M('users').execute("ALTER TABLE 'users' ADD 'salt' TEXT", ())
|
||
u_list = M('users').where('salt is NULL', ()).field('id,username,password,salt').select()
|
||
|
||
for u_info in u_list:
|
||
salt = GetRandomString(12) # 12位随机
|
||
pdata = {}
|
||
pdata['password'] = md5(md5(u_info['password'] + '_capnis.com') + salt)
|
||
pdata['salt'] = salt
|
||
M('users').where('id=?', (u_info['id'],)).update(pdata)
|
||
|
||
|
||
def get_login_token():
|
||
token_s = readFile('{}/data/login_token.pl'.format(get_panel_path()))
|
||
if not token_s: return GetRandomString(32)
|
||
return token_s
|
||
|
||
|
||
def get_sess_key():
|
||
return md5(get_login_token() + get_csrf_sess_html_token_value())
|
||
|
||
|
||
def password_salt(password, username=None, uid=None):
|
||
'''
|
||
@name 为指定密码加盐
|
||
@author hwliang<2020-07-08>
|
||
@param password string(被md5加密一次的密码)
|
||
@param username string(用户名) 可选
|
||
@param uid int(uid) 可选
|
||
@return string
|
||
'''
|
||
chdck_salt()
|
||
if not uid:
|
||
if not username:
|
||
raise Exception('username或uid必需传一项')
|
||
uid = M('users').where('username=?', (username,)).getField('id')
|
||
salt = M('users').where('id=?', (uid,)).getField('salt')
|
||
return md5(md5(password + '_capnis.com') + salt)
|
||
|
||
|
||
# 备份配置文件
|
||
def back_file(file, act=None):
|
||
"""
|
||
@name 备份配置文件
|
||
@author zhwen<zhw@yakpanel.com>
|
||
@param file 需要备份的文件
|
||
@param act 如果存在,则备份一份作为默认配置
|
||
"""
|
||
file_type = "_bak"
|
||
if act:
|
||
file_type = "_def"
|
||
ExecShell("/usr/bin/cp -p {0} {1}".format(file, file + file_type))
|
||
|
||
|
||
# 还原配置文件
|
||
def restore_file(file, act=None):
|
||
"""
|
||
@name 还原配置文件
|
||
@author zhwen<zhw@yakpanel.com>
|
||
@param file 需要还原的文件
|
||
@param act 如果存在,则还原默认配置
|
||
"""
|
||
file_type = "_bak"
|
||
if act:
|
||
file_type = "_def"
|
||
ExecShell("/usr/bin/cp -p {1} {0}".format(file, file + file_type))
|
||
|
||
|
||
def package_path_append(path):
|
||
if not path in sys.path:
|
||
sys.path.insert(0, path)
|
||
|
||
|
||
def rep_sys_path():
|
||
sys_path = []
|
||
for p in sys.path:
|
||
if p in sys_path: continue
|
||
sys_path.append(p)
|
||
sys.path = sys_path
|
||
|
||
|
||
def get_ssh_port():
|
||
'''
|
||
@name 获取本机SSH端口
|
||
@author hwliang<2020-08-07>
|
||
@return int
|
||
'''
|
||
s_file = '/etc/ssh/sshd_config'
|
||
conf = readFile(s_file)
|
||
if not conf: conf = ''
|
||
port_all = re.findall(r".*Port\s+[0-9]+", conf)
|
||
ssh_port = 22
|
||
for p in port_all:
|
||
rep = r"^\s*Port\s+([0-9]+)\s*"
|
||
tmp1 = re.findall(rep, p)
|
||
if tmp1:
|
||
ssh_port = int(tmp1[0])
|
||
|
||
return ssh_port
|
||
|
||
|
||
def set_error_num(key, empty=False, expire=3600):
|
||
'''
|
||
@name 设置失败次数(每调用一次+1)
|
||
@author hwliang<2020-08-21>
|
||
@param key<string> 索引
|
||
@param empty<bool> 是否清空计数
|
||
@param expire<int> 计数器生命周期(秒)
|
||
@return bool
|
||
'''
|
||
from YakPanel import cache
|
||
key = md5(key)
|
||
num = cache.get(key)
|
||
if not num:
|
||
num = 0
|
||
else:
|
||
if empty:
|
||
cache.delete(key)
|
||
return True
|
||
cache.set(key, num + 1, expire)
|
||
return True
|
||
|
||
|
||
def get_error_num(key, limit=False):
|
||
'''
|
||
@name 获取失败次数
|
||
@author hwliang<2020-08-21>
|
||
@param key<string> 索引
|
||
@param limit<False or int> 如果为False,则直接返回失败次数,否则与失败次数比较,若大于失败次数返回True,否则返回False
|
||
@return int or bool
|
||
'''
|
||
from YakPanel import cache
|
||
key = md5(key)
|
||
num = cache.get(key)
|
||
if not num: num = 0
|
||
if not limit:
|
||
return num
|
||
if limit > num:
|
||
return True
|
||
return False
|
||
|
||
|
||
# ========================= user authority ==========================
|
||
def get_menus():
|
||
'''
|
||
@name 获取菜单列表
|
||
@author hwliang<2020-08-31>
|
||
@return list
|
||
'''
|
||
from YakPanel import session
|
||
data = json.loads(ReadFile('config/menu.json'))
|
||
hide_menu = ReadFile('config/hide_menu.json')
|
||
debug = session.get('debug')
|
||
if hide_menu:
|
||
hide_menu = json.loads(hide_menu)
|
||
show_menu = []
|
||
for i in range(len(data)):
|
||
if data[i]['id'] in hide_menu:
|
||
continue
|
||
if data[i]['id'] == "memuAxterm":
|
||
if debug: continue
|
||
show_menu.append(data[i])
|
||
data = show_menu
|
||
del (hide_menu)
|
||
del (show_menu)
|
||
menus = sorted(data, key=lambda x: x['sort'])
|
||
return menus
|
||
|
||
|
||
def get_menus_for_session_router() -> list:
|
||
"""
|
||
@name 获取用户路由菜单列表
|
||
@author hwliang<2020-08-31>
|
||
@return list
|
||
"""
|
||
import config_v2
|
||
menu_list = config_v2.config().get_menu_list()
|
||
if menu_list.get('status') == 0:
|
||
result = [
|
||
{
|
||
'title': x.get('title'),
|
||
'href': x.get('href'),
|
||
'class': x.get('class'),
|
||
'id': x.get('id'),
|
||
'sort': x.get('sort'),
|
||
} for x in menu_list.get('message', []) if x.get('show', False) is True
|
||
]
|
||
return result
|
||
else:
|
||
return get_menus()
|
||
|
||
|
||
def _decrypt(data: str) -> str:
|
||
"""
|
||
@name 数据解密
|
||
@param data <str>
|
||
@return str
|
||
"""
|
||
import PluginLoader
|
||
if not data:
|
||
return data
|
||
if not isinstance(data, str):
|
||
return data
|
||
for _pfx in ('YP-0x:', 'BT-0x:'):
|
||
if data.startswith(_pfx):
|
||
# noinspection PyUnresolvedReferences
|
||
res = PluginLoader.db_decrypt(data[len(_pfx) :])['msg']
|
||
return res
|
||
return data
|
||
|
||
|
||
def get_user_authority_info(session_uid: int) -> dict:
|
||
"""
|
||
@name 获取user解密后权限信息
|
||
@param session_uid <int>
|
||
@return {"id": 2,
|
||
"username": "test_user",
|
||
"role": "ordinary",
|
||
"state": 1,
|
||
"menu": ["/", "/site"...]}
|
||
"""
|
||
data = {'id': 0, 'username': '', 'role': 'unknow', 'state': 0, 'menu': []}
|
||
enable_list = ['/', '/login']
|
||
|
||
if not M('users').where('id=?', (session_uid,)).select():
|
||
return data
|
||
plugin_path = '/www/server/panel/plugin/users/'
|
||
authority_path = os.path.join(plugin_path, 'authority')
|
||
if not os.path.exists(plugin_path):
|
||
return data
|
||
if not os.path.exists(authority_path):
|
||
return data
|
||
uid_authority_path = os.path.join(authority_path, str(session_uid))
|
||
if not os.path.exists(uid_authority_path):
|
||
return data
|
||
|
||
try:
|
||
data = readFile(uid_authority_path)
|
||
data = json.loads(_decrypt(data)) if data else data
|
||
data['state'] = int(data.get('state'))
|
||
except Exception:
|
||
return {"id": 0, "username": "", "role": "unknow", "state": "0", "menu": []}
|
||
|
||
data['menu'] = enable_list + data.get('menu', [])
|
||
return data
|
||
|
||
|
||
def user_router_authority() -> bool:
|
||
"""
|
||
@name 校验路由权限
|
||
@return bool
|
||
"""
|
||
from flask import session, request
|
||
if 'login' not in session:
|
||
return True
|
||
if session['login'] is False:
|
||
return True
|
||
uid = session.get('uid')
|
||
if not uid:
|
||
return False
|
||
if uid == 1: # admin
|
||
return True
|
||
# other user
|
||
user_data = get_user_authority_info(uid)
|
||
if user_data.get('state') != 1:
|
||
return False
|
||
if user_data.get('role') == 'administrator':
|
||
return True
|
||
# todo 校验data权限, plugin权限...
|
||
if not user_data.get('menu'):
|
||
return False
|
||
if request.path == '/':
|
||
path = request.path
|
||
else:
|
||
path = request.path.split('/')
|
||
index = 2 if path[1] == 'v2' else 1
|
||
path = '/' + path[index]
|
||
if path in ['/']:
|
||
return True
|
||
ifame_exculde = ['/', '/home', '/login', '/login?dologin=True']
|
||
ifame_router = [
|
||
f'{i}_ifame' for i in user_data.get('menu', []) if i and i not in ifame_exculde
|
||
]
|
||
user_data['menu'] += ifame_router
|
||
|
||
from YakPanel import menu_map
|
||
aa_menus = [
|
||
x for x in menu_map.values() if '/login' not in x
|
||
]
|
||
ifame_menus = [
|
||
f'{x}_ifame' for x in aa_menus if x not in ifame_exculde
|
||
]
|
||
full_menus = aa_menus + ifame_menus
|
||
if path in full_menus and path not in user_data['menu']:
|
||
return False
|
||
return True
|
||
|
||
# ========================= user authority end ======================
|
||
|
||
# 取CURL路径
|
||
def get_curl_bin():
|
||
'''
|
||
@name 取CURL执行路径
|
||
@author hwliang<2020-09-01>
|
||
@return string
|
||
'''
|
||
c_bin = ['/usr/local/curl2/bin/curl', '/usr/local/curl/bin/curl', '/usr/bin/curl']
|
||
for cb in c_bin:
|
||
if os.path.exists(cb): return cb
|
||
return 'curl'
|
||
|
||
|
||
# 设置防跨站配置
|
||
def set_open_basedir():
|
||
try:
|
||
fastcgi_file = '{}/nginx/conf/fastcgi.conf'.format(get_setup_path())
|
||
|
||
if os.path.exists(fastcgi_file):
|
||
fastcgi_body = readFile(fastcgi_file)
|
||
if fastcgi_body.find('bt_safe_dir') == -1:
|
||
fastcgi_body = fastcgi_body + "\n" + 'fastcgi_param PHP_ADMIN_VALUE "$bt_safe_dir=$bt_safe_open";'
|
||
writeFile(fastcgi_file, fastcgi_body)
|
||
|
||
proxy_file = '{}/nginx/conf/proxy.conf'.format(get_setup_path())
|
||
if os.path.exists(proxy_file):
|
||
proxy_body = readFile(proxy_file)
|
||
if proxy_body.find('bt_safe_dir') == -1:
|
||
proxy_body = proxy_body + "\n" + '''map "baota_dir" $bt_safe_dir {
|
||
default "baota_dir";
|
||
}
|
||
map "baota_open" $bt_safe_open {
|
||
default "baota_open";
|
||
} '''
|
||
writeFile(proxy_file, proxy_body)
|
||
|
||
open_basedir_path = '{}/open_basedir/nginx'.format(get_vhost_path())
|
||
if not os.path.exists(open_basedir_path):
|
||
os.makedirs(open_basedir_path, 384)
|
||
|
||
site_list = M('sites').field('id,name,path').select()
|
||
for site_info in site_list:
|
||
set_site_open_basedir_nginx(site_info['name'])
|
||
except:
|
||
return
|
||
|
||
|
||
# 处理指定站点的防跨站配置 for Nginx
|
||
def set_site_open_basedir_nginx(siteName):
|
||
try:
|
||
return
|
||
open_basedir_path = '/www/server/panel/vhost/open_basedir/nginx'
|
||
if not os.path.exists(open_basedir_path):
|
||
os.makedirs(open_basedir_path, 384)
|
||
config_file = '/www/server/panel/vhost/nginx/{}.conf'.format(siteName)
|
||
open_basedir_file = "/".join(
|
||
(open_basedir_path, '{}.conf'.format(siteName))
|
||
)
|
||
if not os.path.exists(config_file): return
|
||
if not os.path.exists(open_basedir_file):
|
||
writeFile(open_basedir_file, '')
|
||
config_body = readFile(config_file)
|
||
if config_body.find(open_basedir_path) == -1:
|
||
config_body = config_body.replace("include enable-php",
|
||
"include {};\n\t\tinclude enable-php".format(open_basedir_file))
|
||
writeFile(config_file, config_body)
|
||
|
||
root_path = re.findall(r"root\s+(.+);", config_body)[0]
|
||
if not root_path: return
|
||
userini_file = root_path + '/.user.ini'
|
||
if not os.path.exists(userini_file):
|
||
writeFile(open_basedir_file, '')
|
||
return
|
||
userini_body = readFile(userini_file)
|
||
if not userini_body: return
|
||
if userini_body.find('open_basedir') == -1:
|
||
writeFile(open_basedir_file, '')
|
||
return
|
||
|
||
open_basedir_conf = re.findall(r"open_basedir=(.+)", userini_body)
|
||
if not open_basedir_conf: return
|
||
open_basedir_conf = open_basedir_conf[0]
|
||
open_basedir_body = '''set $bt_safe_dir "open_basedir";
|
||
set $bt_safe_open "{}";'''.format(open_basedir_conf)
|
||
writeFile(open_basedir_file, open_basedir_body)
|
||
except:
|
||
return
|
||
|
||
|
||
def run_thread(fun, args=(), daemon=False):
|
||
'''
|
||
@name 使用线程执行指定方法
|
||
@author hwliang<2020-10-27>
|
||
@param fun {def} 函数对像
|
||
@param args {tuple} 参数元组
|
||
@param daemon {bool} 是否守护线程
|
||
@return bool
|
||
'''
|
||
import threading
|
||
p = threading.Thread(target=fun, args=args)
|
||
p.setDaemon(daemon)
|
||
p.start()
|
||
return True
|
||
|
||
|
||
def check_domain_cloud(domain):
|
||
run_thread(cloud_check_domain, (domain,))
|
||
|
||
|
||
def count_wp():
|
||
if is_self_hosted():
|
||
return
|
||
run_thread(httpPost, ('http://brandnew.yakpanel.com/api/setupCount/setupWP', {}))
|
||
|
||
|
||
def cloud_check_domain(domain):
|
||
'''
|
||
@name 从云端验证域名的可访问性,并将结果保存到文件
|
||
@author hwliang<2020-12-10>
|
||
@param domain {string} 被验证的域名
|
||
@return void
|
||
'''
|
||
try:
|
||
check_domain_path = '{}/data/check_domain/'.format(get_panel_path())
|
||
if not os.path.exists(check_domain_path):
|
||
os.makedirs(check_domain_path, 384)
|
||
pdata = get_user_info()
|
||
pdata['domain'] = domain
|
||
result = httpPost('{}/api/panel/checkDomain'.format(OfficialApiBase()), {"domain": domain})
|
||
cd_file = check_domain_path + domain + '.pl'
|
||
writeFile(cd_file, result)
|
||
except:
|
||
pass
|
||
|
||
|
||
|
||
def get_user_info():
|
||
user_file = '{}/data/userInfo.json'.format(get_panel_path())
|
||
if not os.path.exists(user_file): return {}
|
||
userInfo = {}
|
||
try:
|
||
userTmp = json.loads(readFile(user_file))
|
||
if not 'server_id' in userTmp or len(userTmp['server_id']) != 64:
|
||
import panelAuth
|
||
userTmp = panelAuth.panelAuth().create_serverid(None)
|
||
|
||
userInfo['uid'] = userTmp['id'] if userTmp.get('id', None) else userTmp.get('uid', None)
|
||
userInfo['address'] = userTmp.get('last_login_ip', None)
|
||
userInfo['access_key'] = 'B' * 32,
|
||
userInfo['username'] = userTmp.get('username', None)
|
||
userInfo['server_id'] = userTmp.get('server_id', None)
|
||
userInfo['serverid'] = userTmp.get('server_id', None)
|
||
userInfo['oem'] = get_oem_name()
|
||
userInfo['o'] = userInfo['oem']
|
||
userInfo['mac'] = get_mac_address()
|
||
except:
|
||
print_log(get_error_info())
|
||
pass
|
||
return userInfo
|
||
|
||
|
||
def send_file(data, fname='', mimetype=''):
|
||
'''
|
||
@name 以文件流的形式返回
|
||
@author heliang<2020-10-27>
|
||
@param data {bytes|string} 文件数据或路径
|
||
@param mimetype {string} 文件类型
|
||
@param fname {string} 文件名
|
||
@return Response
|
||
'''
|
||
d_type = type(data)
|
||
from io import BytesIO, StringIO
|
||
from flask import send_file as send_to
|
||
if d_type == bytes:
|
||
fp = BytesIO(data)
|
||
else:
|
||
if len(data) < 128:
|
||
if os.path.exists(data):
|
||
fp = data
|
||
if not fname:
|
||
fname = os.path.basename(fname)
|
||
else:
|
||
fp = StringIO(data)
|
||
else:
|
||
fp = StringIO(data)
|
||
|
||
if not mimetype: mimetype = "application/octet-stream"
|
||
if not fname: fname = 'doan.txt'
|
||
|
||
import flask
|
||
if flask.__version__ < "2.1.0":
|
||
return send_to(fp,
|
||
mimetype=mimetype,
|
||
as_attachment=True,
|
||
add_etags=True,
|
||
conditional=True,
|
||
attachment_filename=fname,
|
||
cache_timeout=0)
|
||
else:
|
||
return send_to(fp,
|
||
mimetype=mimetype,
|
||
as_attachment=True,
|
||
etag=True,
|
||
conditional=True,
|
||
download_name=fname,
|
||
max_age=0)
|
||
|
||
|
||
def gen_password(length=8, chars=string.ascii_letters + string.digits):
|
||
from random import choice
|
||
return ''.join([choice(chars) for i in range(length)])
|
||
|
||
|
||
def get_ipaddress():
|
||
'''
|
||
@name 获取本机IP地址
|
||
@author hwliang<2020-11-24>
|
||
@return list
|
||
'''
|
||
ipa_tmp = ExecShell(
|
||
"ip a |grep inet|grep -v inet6|grep -v 127.0.0.1|grep -v 'inet 192.168.'|grep -v 'inet 10.'|awk '{print $2}'|sed 's#/[0-9]*##g'")[
|
||
0].strip()
|
||
iplist = ipa_tmp.split('\n')
|
||
return iplist
|
||
|
||
|
||
def get_oem_name():
|
||
'''
|
||
@name 获取OEM名称
|
||
@author hwliang<2021-03-24>
|
||
@return string
|
||
'''
|
||
oem = ''
|
||
oem_file = '{}/data/o.pl'.format(get_panel_path())
|
||
if os.path.exists(oem_file):
|
||
oem = readFile(oem_file)
|
||
if oem: oem = oem.strip()
|
||
return oem
|
||
|
||
|
||
def get_pdata():
|
||
'''
|
||
@name 构造POST基础参数
|
||
@author hwliang<2021-03-24>
|
||
@return dict
|
||
'''
|
||
import panelAuth
|
||
pdata = panelAuth.panelAuth().create_serverid(None)
|
||
pdata['oem'] = get_oem_name()
|
||
return pdata
|
||
|
||
|
||
# 刷新授权信息
|
||
def refresh_pd():
|
||
from YakPanel import cache
|
||
|
||
try:
|
||
p_token = cache.get('p_token')
|
||
|
||
if p_token is None:
|
||
p_token = 'bmac_' + Md5(get_mac_address())
|
||
cache.set('p_token', p_token)
|
||
|
||
softList = load_soft_list(False)
|
||
|
||
writeFile("/tmp/" + p_token, str(softList['pro']))
|
||
writeFile('/tmp/{}.time'.format(p_token), str(int(time.time())))
|
||
except:
|
||
from traceback import format_exc
|
||
print_log(format_exc())
|
||
pass
|
||
|
||
|
||
# 获取授权信息
|
||
def get_pd(args=None):
|
||
"""
|
||
@name 获取授权信息
|
||
@param args:
|
||
@return tuple[html, pro, ltd]
|
||
"""
|
||
from YakPanel import cache
|
||
|
||
# 专业版到期时间 -1.过期 0.永久授权 >0.到期时间
|
||
pro = -1
|
||
|
||
# 企业版到期时间 -1.过期 0.永久授权 >0.到期时间
|
||
ltd = -1
|
||
|
||
# HTML文本
|
||
htm = '<span class="btpro-free" onclick="bt.soft.renew_pro()" title="Click to get PRO">FREE</span>'
|
||
|
||
try:
|
||
# 获取当前时间
|
||
cur_time = int(time.time())
|
||
|
||
p_token = cache.get('p_token')
|
||
|
||
if p_token is None:
|
||
p_token = 'bmac_' + Md5(get_mac_address())
|
||
cache.set('p_token', p_token)
|
||
|
||
tmp_f = '/tmp/' + p_token
|
||
p_token_time_f = '/tmp/{}.time'.format(p_token)
|
||
|
||
# 检查缓存是否失效
|
||
if not os.path.exists(tmp_f) or not os.path.exists(p_token_time_f) or int(readFile(p_token_time_f).strip()) + 86400 <= cur_time:
|
||
# 检查用户是否登录,登录后才获取授权信息
|
||
userinfo_f = '{}/data/userInfo.json'.format(get_panel_path())
|
||
if os.path.exists(userinfo_f) and os.path.getsize(userinfo_f) > 10:
|
||
# 缓存失效时重新获取授权信息
|
||
plugin_list = load_soft_list()
|
||
|
||
if isinstance(plugin_list, dict):
|
||
pro = plugin_list.get('pro', -1)
|
||
# ltd = plugin_list.get('ltd', -1)
|
||
|
||
writeFile(tmp_f, str(pro), 'w')
|
||
writeFile(p_token_time_f, str(cur_time), 'w')
|
||
|
||
tmp = readFile(tmp_f)
|
||
if tmp:
|
||
pro = int(tmp)
|
||
|
||
if ltd < 1:
|
||
if ltd == -2:
|
||
htm = '<span class="btltd-gray"><span style="color: #fc6d26;font-weight: bold;margin-right:5px">EXPIRED</span><a class="btlink" onclick="bt.soft.updata_ltd()">RENEW</a></span>'
|
||
elif pro == -1:
|
||
htm = '<span class="btpro-free" onclick="bt.soft.renew_pro()" title="Click to get PRO">FREE</span>'
|
||
elif pro == -2:
|
||
htm = '<span class="btpro-gray"><span style="color: #fc6d26;font-weight: bold;margin-right:5px">EXPIRED</span><a class="btlink" onclick="bt.soft.renew_pro()">RENEW</a></span>'
|
||
if pro >= 0 and ltd in [-1, -2]:
|
||
if pro == 0:
|
||
tmp2 = 'Lifetime'
|
||
htm = '<span class="btpro">Expire:<span style="color: #fc6d26;font-weight: bold;">{0}</span></span>'.format(
|
||
tmp2)
|
||
else:
|
||
tmp2 = time.strftime('%Y-%m-%d', time.localtime(pro))
|
||
htm = '<span class="btpro">Expire: <span style="color: #fc6d26;font-weight: bold;margin-right:5px">{0}</span><a class="btlink" onclick="bt.soft.renew_pro()">RENEW</a></span>'.format(
|
||
tmp2)
|
||
else:
|
||
htm = '<span class="btpro-gray" onclick="bt.soft.updata_pro()" title="Click to get PRO">FREE</span>'
|
||
else:
|
||
htm = '<span class="btltd">Expire: <span style="color: #fc6d26;font-weight: bold;margin-right:5px">{}</span><a class="btlink" onclick="bt.soft.renew_pro()">RENEW</a></span>'.format(
|
||
time.strftime('%Y-%m-%d', time.localtime(ltd)))
|
||
|
||
except:
|
||
from traceback import format_exc
|
||
print_log(format_exc())
|
||
|
||
return htm, pro, ltd
|
||
|
||
|
||
# 名称输入系列化
|
||
def xssdecode(text):
|
||
try:
|
||
cs = {""": '"', """: '"', "'": "'", "'": "'"}
|
||
for c in cs.keys():
|
||
text = text.replace(c, cs[c])
|
||
|
||
str_convert = text
|
||
if sys.version_info[0] == 3:
|
||
import html
|
||
text2 = html.unescape(str_convert)
|
||
else:
|
||
# noinspection PyUnresolvedReferences
|
||
text2 = cgi.unescape(str_convert)
|
||
return text2
|
||
except:
|
||
return text
|
||
|
||
|
||
def get_cpuname():
|
||
return ExecShell("cat /proc/cpuinfo|grep 'model name'|cut -d : -f2")[0].strip()
|
||
|
||
|
||
def fetch_disk_SN():
|
||
r, e = ExecShell("fdisk -l |grep 'Disk identifier' |awk {'print $3'}")
|
||
if r:
|
||
return r.split('\n')[0]
|
||
|
||
|
||
def fetch_cpu_ID():
|
||
r, e = ExecShell("cat /proc/cpuinfo|grep microcode|awk -F':' '{print $2}'")
|
||
if r:
|
||
return r.split('\n')[0]
|
||
|
||
|
||
def get_platform():
|
||
import platform
|
||
return platform.platform()
|
||
|
||
|
||
def get_memory():
|
||
import psutil
|
||
return psutil.virtual_memory().total
|
||
|
||
# generate server_id
|
||
def gen_server_id():
|
||
s1 = get_mac_address() + get_hostname()
|
||
s2 = get_cpuname()
|
||
return md5(s1) + md5(s2)
|
||
|
||
|
||
# get server_id
|
||
def get_server_id():
|
||
return get_userinfo().get('server_id')
|
||
|
||
|
||
# get userinfo
|
||
def get_userinfo(force = False):
|
||
try:
|
||
userPath = 'data/userInfo.json'
|
||
|
||
if not force and not os.path.exists(userPath):
|
||
raise ValueError('')
|
||
|
||
tmp = readFile(userPath)
|
||
if not tmp or len(tmp) < 2:
|
||
tmp = '{}'
|
||
|
||
userinfo = json.loads(tmp)
|
||
|
||
if not force:
|
||
if not userinfo:
|
||
raise ValueError('')
|
||
|
||
if 'token' not in userinfo:
|
||
raise ValueError('')
|
||
|
||
if str(userinfo['token']).count('.') != 2:
|
||
raise ValueError('')
|
||
|
||
if 'id' in userinfo:
|
||
userinfo['uid'] = userinfo['id']
|
||
|
||
if 'server_id' not in userinfo:
|
||
userinfo['server_id'] = gen_server_id()
|
||
writeFile(userPath, json.dumps(userinfo))
|
||
|
||
return userinfo
|
||
except:
|
||
raise PanelError(lang("Please login with account first"))
|
||
|
||
|
||
def fetch_env_info():
|
||
try:
|
||
return {
|
||
'ip': GetLocalIp(),
|
||
'is_ipv6': 0,
|
||
'os': get_platform(),
|
||
'mac': get_mac_address(),
|
||
'hdid': fetch_disk_SN(),
|
||
'ramid': get_memory(),
|
||
'cpuid': fetch_cpu_ID(),
|
||
'server_name': get_hostname(),
|
||
'install_code': get_server_id()
|
||
}
|
||
except:
|
||
return {}
|
||
|
||
|
||
def arequests(method, url, data=None, timeout=3):
|
||
import threading
|
||
if method == 'post':
|
||
method = httpPost
|
||
else:
|
||
method = httpGet
|
||
threading.Thread(target=method, args=(url, data, timeout)).start()
|
||
|
||
|
||
# 取通用对象
|
||
re_key_match = re.compile(r'^[\w\s\[\]\-.]+$')
|
||
re_key_match2 = re.compile(r'^\.?__[\w\s[\]\-]+__\.?$')
|
||
key_filter_list = ['get', 'set', 'get_items', 'exists', '__contains__', '__setitem__', '__getitem__', '__delitem__',
|
||
'__delattr__', '__setattr__', '__getattr__', '__class__', 'get_file']
|
||
|
||
|
||
class dict_obj:
|
||
def __init__(self):
|
||
# 存放数据
|
||
self.__store = {}
|
||
|
||
# 检测数据是否经过校验
|
||
self.__validated = set()
|
||
|
||
def __contains__(self, key):
|
||
return hasattr(self, key)
|
||
|
||
def __setitem__(self, key, value):
|
||
# if key in key_filter_list:
|
||
# raise ValueError("wrong field name")
|
||
|
||
if not re_key_match.match(key) or re_key_match2.match(key):
|
||
raise ValueError("wrong field name")
|
||
|
||
self.__store[key] = value
|
||
|
||
def __getitem__(self, key):
|
||
return getattr(self, key)
|
||
|
||
def __delitem__(self, key):
|
||
delattr(self, key)
|
||
|
||
def __delattr__(self, key):
|
||
delattr(self, key)
|
||
|
||
def __setattr__(self, key, value):
|
||
if match_class_private_property.match(key):
|
||
object.__setattr__(self, key, value)
|
||
return
|
||
|
||
self.__store[key] = value
|
||
|
||
def __getattr__(self, key):
|
||
if key in self.__store:
|
||
# 未经过校验的数据不允许获取
|
||
# if key not in self.__validated:
|
||
# raise ValueError('参数值获取失败:参数 {} 尚未通过校验,请先调用 validate() 完成校验后再尝试重新获取参数值'.format(key))
|
||
return self.__store[key]
|
||
|
||
raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, key))
|
||
|
||
@property
|
||
def __dict__(self):
|
||
return self.__store
|
||
|
||
def get_items(self):
|
||
return self.__store
|
||
|
||
def validate(self, validate_rules: typing.List[Param], filters: typing.List[callable] = (trim_filter(),)) -> None:
|
||
"""
|
||
@name 验证请求参数
|
||
@param validate_rules: list[validate.Param] 参数验证规则
|
||
@param filters: list[callable] 参数过滤器
|
||
@raise Error
|
||
"""
|
||
filters = list(filters)
|
||
|
||
for v in validate_rules:
|
||
v.do_validate(self.__store)
|
||
|
||
if v.name in self.__store:
|
||
self.__store[v.name] = v.do_filter(self.__store[v.name], filters)
|
||
|
||
self.__validated.add(v.name)
|
||
|
||
def exists(self, keys):
|
||
return exists_args(keys, self)
|
||
|
||
def set(self, key, value):
|
||
if not isinstance(value, str) or not isinstance(key, str): return False
|
||
# if key in key_filter_list:
|
||
# raise ValueError("wrong field name")
|
||
if not re_key_match.match(key) or re_key_match2.match(key):
|
||
raise ValueError("wrong field name")
|
||
return setattr(self, key, value)
|
||
|
||
def get(self, key, default='', format='', limit=[]):
|
||
'''
|
||
@name 获取指定参数
|
||
@param key<string> 参数名称,允许在/后面限制参数格式,请参考参数值格式(format)
|
||
@param default<string> 默认值,默认空字符串
|
||
@param format<string> 参数值格式(int|str|port|float|json|xss|path|url|ip|ipv4|ipv6|letter|mail|phone|正则表达式|>1|<1|=1),默认为空
|
||
@param limit<list> 限制参数值内容
|
||
@param return mixed
|
||
'''
|
||
if key.find('/') != -1:
|
||
key, format = key.split('/')
|
||
result = getattr(self, key, default)
|
||
if isinstance(result, str): result = result.strip()
|
||
if format:
|
||
if format in ['str', 'string', 's']:
|
||
result = str(result)
|
||
elif format in ['int', 'd']:
|
||
try:
|
||
result = int(result)
|
||
except:
|
||
raise ValueError("Parameters: {}, requires int type data".format(key))
|
||
elif format in ['float', 'f']:
|
||
try:
|
||
result = float(result)
|
||
except:
|
||
raise ValueError("Parameters: {}, float type data required".format(key))
|
||
elif format in ['json', 'j']:
|
||
try:
|
||
result = json.loads(result)
|
||
except:
|
||
raise ValueError("Parameters: {}, requires JSON string".format(key))
|
||
elif format in ['xss', 'x']:
|
||
result = xssencode(result)
|
||
elif format in ['path', 'p']:
|
||
if not path_safe_check(result):
|
||
raise ValueError("Parameters: {}, the correct path format is required".format(key))
|
||
result = result.replace('//', '/')
|
||
elif format in ['url', 'u']:
|
||
regex = re.compile(
|
||
r'^(?:http|ftp)s?://'
|
||
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
|
||
r'localhost|'
|
||
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
||
r'(?::\d+)?'
|
||
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
||
if not re.match(regex, result):
|
||
raise ValueError('Parameters: {}, the correct URL format is required'.format(key))
|
||
elif format in ['ip', 'ipaddr', 'i', 'ipv4', 'ipv6']:
|
||
if format == 'ipv4':
|
||
if not is_ipv4(result):
|
||
raise ValueError('Parameters: {}, the correct ipv4 address is required'.format(key))
|
||
elif format == 'ipv6':
|
||
if not is_ipv6(result):
|
||
raise ValueError('Parameters: {}, the correct ipv6 address is required'.format(key))
|
||
else:
|
||
if not is_ipv4(result) and not is_ipv6(result):
|
||
raise ValueError('Parameters: {}, the correct ipv4/ipv6 address is required'.format(key))
|
||
elif format in ['w', 'letter']:
|
||
if not re.match(r'^\w+$', result):
|
||
raise ValueError(
|
||
'Parameters: {}, the requirement can only be composed of English letters'.format(key))
|
||
elif format in ['email', 'mail', 'm']:
|
||
if not re.match(r'^.+@(\[?)[a-zA-Z0-9\-.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(]?)$', result):
|
||
raise ValueError("Parameters: {}, the correct email address format is required".format(key))
|
||
elif format in ['phone', 'mobile', 'm']:
|
||
if not re.match(r"^1[3-9]\d{9}$", result):
|
||
raise ValueError("Parameters: {}, mobile phone number format required".format(key))
|
||
elif format in ['port']:
|
||
result_port = int(result)
|
||
if result_port > 65535 or result_port < 0:
|
||
raise ValueError("Parameters: {}, the required port number is 0-65535".format(key))
|
||
result = result_port
|
||
elif re.match(r"^[<>=]\d+$", result):
|
||
operator = format[0]
|
||
length = int(format[1:].strip())
|
||
result_len = len(result)
|
||
error_obj = ValueError("Parameters: {}, the required length is {}".format(key, format))
|
||
if operator == '=':
|
||
if result_len != length:
|
||
raise error_obj
|
||
elif operator == '>':
|
||
if result_len < length:
|
||
raise error_obj
|
||
else:
|
||
if result_len > length:
|
||
raise error_obj
|
||
elif format[0] in ['^', '(', '[', '\\', '.'] or format[-1] in ['$', ')', ']', '+', '}']:
|
||
if not re.match(format, result):
|
||
raise ValueError("The format of the specified parameter is incorrect, {}:{}".format(key, format))
|
||
|
||
if limit:
|
||
if not result in limit:
|
||
raise ValueError("The specified parameter value range is incorrect, {}:{}".format(key, limit))
|
||
return result
|
||
|
||
def get_file(self, key: str) -> werkzeug.datastructures.FileStorage:
|
||
"""
|
||
@name 获取上传文件对象
|
||
@param key: str 参数名
|
||
@return: werkzeug.datastructures.FileStorage
|
||
"""
|
||
if 'FILES' not in self.__store or key not in self.__store['FILES']:
|
||
raise ValueError('not found file with param name {}'.format(key))
|
||
|
||
return self.__store['FILES'][key]
|
||
|
||
|
||
# 实例化定目录下的所有模块
|
||
class get_modules:
|
||
|
||
def __contains__(self, key):
|
||
return self.get_attr(key)
|
||
|
||
def __setitem__(self, key, value):
|
||
setattr(self, key, value)
|
||
|
||
def get_attr(self, key):
|
||
'''
|
||
尝试获取模块,若为字符串,则尝试实例化模块,否则直接返回模块对像
|
||
'''
|
||
res = getattr(self, key)
|
||
if isinstance(res, str):
|
||
try:
|
||
tmp_obj = __import__(key)
|
||
reload(tmp_obj)
|
||
setattr(self, key, tmp_obj)
|
||
return tmp_obj
|
||
except:
|
||
raise Exception(get_error_info())
|
||
return res
|
||
|
||
def __getitem__(self, key):
|
||
return self.get_attr(key)
|
||
|
||
def __delitem__(self, key):
|
||
delattr(self, key)
|
||
|
||
def __delattr__(self, key):
|
||
delattr(self, key)
|
||
|
||
def get_items(self):
|
||
return self
|
||
|
||
def __init__(self, path="class", limit=None):
|
||
'''
|
||
@name 加载指定目录下的模块
|
||
@author hwliang<2020-08-03>
|
||
@param path<string> 指定目录,可指定绝对目录,也可指定相对于/www/server/panel的相对目录 默认加载class目录
|
||
@param limit<string/list/tuple> 指定限定加载的模块名称,默认加载path目录下的所有模块
|
||
@param object
|
||
|
||
@example
|
||
p = get_modules('class')
|
||
if 'public' in p:
|
||
md5_str = p.public.md5('test')
|
||
md5_str = p['public'].md5('test')
|
||
md5_str = getattr(p['public'],'md5')('test')
|
||
else:
|
||
print(p.__dict__)
|
||
'''
|
||
os.chdir(get_panel_path())
|
||
exp_files = ['__init__.py', '__pycache__']
|
||
if not path in sys.path:
|
||
sys.path.insert(0, path)
|
||
for fname in os.listdir(path):
|
||
if fname in exp_files: continue
|
||
filename = '/'.join([path, fname])
|
||
if os.path.isfile(filename):
|
||
if not fname[-3:] in ['.py', '.so']: continue
|
||
mod_name = fname[:-3]
|
||
else:
|
||
c_file = '/'.join((filename, '__init__.py'))
|
||
if not os.path.exists(c_file):
|
||
continue
|
||
mod_name = fname
|
||
|
||
if limit:
|
||
if not isinstance(limit, list) and not isinstance(limit, tuple):
|
||
limit = (limit,)
|
||
if not mod_name in limit:
|
||
continue
|
||
|
||
setattr(self, mod_name, mod_name)
|
||
|
||
|
||
# 检查App和小程序的绑定
|
||
def check_app(check='app'):
|
||
path = get_panel_path() + '/'
|
||
if check == 'app':
|
||
try:
|
||
if not os.path.exists("/www/server/panel/plugin/btapp/btapp_main.py"): return False
|
||
if not os.path.exists(path + 'config/api.json'): return False
|
||
if os.path.exists(path + 'config/api.json'):
|
||
btapp_info = json.loads(readFile(path + 'config/api.json'))
|
||
if not btapp_info['open']: return False
|
||
if not 'apps' in btapp_info: return False
|
||
if not btapp_info['apps']: return False
|
||
return True
|
||
return False
|
||
except:
|
||
return False
|
||
elif check == 'app_bind':
|
||
if not cache_get(Md5(os.uname().version)): return False
|
||
if not os.path.exists("/www/server/panel/plugin/btapp/btapp_main.py"): return False
|
||
if not os.path.exists(path + 'config/api.json'): return False
|
||
btapp_info = json.loads(readFile(path + 'config/api.json'))
|
||
if not btapp_info: return False
|
||
if not btapp_info['open']: return False
|
||
return True
|
||
elif check == 'wxapp':
|
||
if not os.path.exists(path + 'plugin/app/user.json'): return False
|
||
app_info = json.loads(readFile(path + 'plugin/app/user.json'))
|
||
if not app_info: return False
|
||
return True
|
||
|
||
|
||
# #YakPanel 邮件报警
|
||
# def send_mail(title,body,is_logs=False,is_type="yakpanel login reminder"):
|
||
# if is_logs:
|
||
# try:
|
||
# import send_mail
|
||
# send_mail22 = send_mail.send_mail()
|
||
# tongdao = send_mail22.get_settings()
|
||
# if tongdao['user_mail']['mail_list']==0:return False
|
||
# if not tongdao['user_mail']['info']: return False
|
||
# if len(tongdao['user_mail']['mail_list'])==1:
|
||
# send_mail=tongdao['user_mail']['mail_list'][0]
|
||
# send_mail22.qq_smtp_send(send_mail, title=title, body=body)
|
||
# else:
|
||
# send_mail22.qq_smtp_send(tongdao['user_mail']['mail_list'], title=title, body=body)
|
||
# if is_logs:
|
||
# WriteLog2(is_type, body)
|
||
# except:
|
||
# return False
|
||
# else:
|
||
# try:
|
||
# import send_mail
|
||
# send_mail22 = send_mail.send_mail()
|
||
# tongdao = send_mail22.get_settings()
|
||
# if tongdao['user_mail']['mail_list'] == 0: return False
|
||
# if not tongdao['user_mail']['info']: return False
|
||
# if len(tongdao['user_mail']['mail_list']) == 1:
|
||
# send_mail = tongdao['user_mail']['mail_list'][0]
|
||
# return send_mail22.qq_smtp_send(send_mail, title=title, body=body)
|
||
# else:
|
||
# return send_mail22.qq_smtp_send(tongdao['user_mail']['mail_list'], title=title, body=body)
|
||
# except:
|
||
# return False
|
||
|
||
# YakPanel 邮件报警
|
||
def send_mail(title, body, is_logs=False, is_type="yakpanel login reminder"):
|
||
try:
|
||
import panelPush
|
||
msg_data = {
|
||
"msg": body.replace("\n", "<br/>"),
|
||
"title": title
|
||
}
|
||
if is_logs: WriteLog2(is_type, body)
|
||
return panelPush.panelPush().push_message_immediately({"mail": msg_data})
|
||
except Exception as ex:
|
||
return returnMsg(False, 'Failed to send: {}'.format(ex))
|
||
|
||
|
||
# 发送钉钉告警
|
||
def send_dingding(body, is_logs=False, is_type="yakpanel login reminder"):
|
||
try:
|
||
import panelPush
|
||
if is_logs: WriteLog2(is_type, body)
|
||
return panelPush.panelPush().push_message_immediately({"dingding": {"msg": body}})
|
||
except Exception as ex:
|
||
return returnMsg(False, 'Failed to send: {}'.format(ex))
|
||
|
||
|
||
# 发送微信告警
|
||
def send_weixin(body, is_logs=False, is_type="yakpanel login reminder"):
|
||
try:
|
||
import panelPush
|
||
if is_logs: WriteLog2(is_type, body)
|
||
return panelPush.panelPush().push_message_immediately({"weixin": {"msg": body}})
|
||
except Exception as ex:
|
||
return returnMsg(False, 'Failed to send: {}'.format(ex))
|
||
|
||
|
||
# 发送飞书告警
|
||
def send_feishu(body, is_logs=False, is_type="yakpanel login reminder"):
|
||
try:
|
||
import panelPush
|
||
if is_logs: WriteLog2(is_type, body)
|
||
return panelPush.panelPush().push_message_immediately({"feishu": {"msg": body}})
|
||
except Exception as ex:
|
||
return returnMsg(False, 'Failed to send: {}'.format(ex))
|
||
|
||
|
||
# 发送除短信以外的所有告警通道
|
||
def send_all(body, title=None):
|
||
try:
|
||
import panelPush
|
||
msg_dict = {"msg": body}
|
||
msg_all = {
|
||
"feishu": msg_dict,
|
||
"weixin": msg_dict,
|
||
"dingding": msg_dict,
|
||
"mail": {
|
||
"msg": body.replace("\n", "<br/>"),
|
||
"title": title
|
||
}
|
||
}
|
||
return panelPush.panelPush().push_message_immediately(msg_all)
|
||
except Exception as ex:
|
||
return returnMsg(False, 'Failed to send: {}'.format(ex))
|
||
|
||
|
||
# 获取服务器IP
|
||
def get_ip():
|
||
iplist_file = '{}/data/iplist.txt'.format(get_panel_path())
|
||
if os.path.exists(iplist_file):
|
||
data = ReadFile(iplist_file)
|
||
return data.strip()
|
||
else:
|
||
return '127.0.0.1'
|
||
|
||
|
||
# 获取服务器内网Ip
|
||
def get_local_ip():
|
||
try:
|
||
ret = ExecShell(
|
||
r"ip addr | grep -E -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | grep -E -v \"^127\.|^255\.|^0\.\" | head -n 1")
|
||
local_ip = ret[0].strip()
|
||
return local_ip
|
||
except:
|
||
return '127.0.0.1'
|
||
|
||
def get_local_ip_2():
|
||
"""获取内网IP"""
|
||
import socket
|
||
s = None
|
||
try:
|
||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
s.connect(('8.8.8.8', 80))
|
||
ip = s.getsockname()[0]
|
||
return ip
|
||
except:
|
||
pass
|
||
finally:
|
||
if s is not None:
|
||
s.close()
|
||
return '127.0.0.1'
|
||
|
||
|
||
def create_logs():
|
||
import db
|
||
sql = db.Sql()
|
||
if not sql.table('sqlite_master').where('type=? AND name=?', ('table', 'logs2')).count():
|
||
csql = '''CREATE TABLE `logs2` (
|
||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
`type` TEXT,
|
||
`log` TEXT,
|
||
`addtime` TEXT
|
||
, uid integer DEFAULT '1', username TEXT DEFAULT 'system')'''
|
||
sql.execute(csql, ())
|
||
|
||
|
||
def WriteLog2(type, logMsg, args=(), not_web=False):
|
||
import db
|
||
create_logs()
|
||
username = 'system'
|
||
uid = 1
|
||
tmp_msg = ''
|
||
sql = db.Sql()
|
||
mDate = time.strftime('%Y-%m-%d %X', time.localtime())
|
||
data = (uid, username, type, logMsg + tmp_msg, mDate)
|
||
result = sql.table('logs2').add('uid,username,type,log,addtime', data)
|
||
|
||
|
||
def check_ip_white(path, ip):
|
||
if os.path.exists(path):
|
||
try:
|
||
path_json = json.loads(ReadFile(path))
|
||
except:
|
||
WriteFile(path, '[]')
|
||
return False
|
||
if ip in path_json:
|
||
return True
|
||
else:
|
||
return False
|
||
else:
|
||
return False
|
||
|
||
|
||
def check_login_area(login_ip, login_type='panel'):
|
||
"""
|
||
@name 检测登录地区
|
||
@login_type 登录类型 panel:YakPanel 面板登录, ssh:ssh登录
|
||
"""
|
||
|
||
login_ip_area = ''
|
||
ip_info = get_ips_area([login_ip])
|
||
|
||
if 'status' in ip_info:
|
||
login_ip_area = '****(Pro exclusive)'
|
||
else:
|
||
ip_info = ip_info[login_ip]
|
||
if not 'city' in ip_info:
|
||
login_ip_area = ip_info['info']
|
||
|
||
data = {}
|
||
status = False
|
||
sfile = '{}/data/{}_login_area.pl'.format(get_panel_path(), login_type)
|
||
s_conf = '{}/data/{}_login_area.json'.format(get_panel_path(), login_type)
|
||
if os.path.exists(sfile):
|
||
status = True
|
||
|
||
data = {}
|
||
try:
|
||
data = json.loads(readFile(s_conf))
|
||
except:
|
||
pass
|
||
|
||
if not login_ip_area and 'city' in ip_info:
|
||
city = ip_info['city']
|
||
login_ip_area = ip_info['info']
|
||
|
||
if not city in data:
|
||
data[city] = 0
|
||
|
||
if data[city] < 3:
|
||
login_ip_area += '(<font color=red>异地</font>)'
|
||
data[city] += 1
|
||
|
||
writeFile(s_conf, json.dumps(data))
|
||
|
||
data['login_ip_area'] = login_ip_area
|
||
return status, data
|
||
|
||
|
||
def get_free_ips_area(ips):
|
||
'''
|
||
@name 免费IP库 获取ip地址所在地
|
||
@author cjxin
|
||
@param ips<list>
|
||
@return list
|
||
'''
|
||
import PluginLoader
|
||
args = dict_obj()
|
||
args.model_index = 'safe'
|
||
args.ips = ips
|
||
res = PluginLoader.module_run("freeip", "get_ip_area", args)
|
||
return res
|
||
|
||
|
||
def get_free_ip_info(address):
|
||
'''
|
||
@name 免费IP库 获取ip地址所在地
|
||
@param ip<string>
|
||
@return dict
|
||
'''
|
||
ip = address.split(':')[0]
|
||
if not is_ipv4(ip):
|
||
return {'info': 'unknow'}
|
||
if is_local_ip(ip):
|
||
return {'info': 'intranet', 'local': True}
|
||
|
||
ip_info = {}
|
||
sfile = '{}/data/ip_area.json'.format(get_panel_path())
|
||
try:
|
||
ip_info = json.loads(readFile(sfile))
|
||
except:
|
||
pass
|
||
|
||
if ip in ip_info:
|
||
return ip_info[ip]
|
||
|
||
try:
|
||
# 使用英文IP库
|
||
from safeModel import ipsModel
|
||
obj = ipsModel.main()
|
||
res = obj.get_ip_area({'ips' : [ip]})
|
||
return res[ip]
|
||
except:
|
||
pass
|
||
|
||
return {'info': 'Unknown'}
|
||
|
||
|
||
# 使用免费IP库获取IP地区
|
||
def free_login_area(login_ip, login_type='panel'):
|
||
"""
|
||
@name 使用免费IP库获取IP地区
|
||
@login_type 登录类型 panel:YakPanel 面板登录, ssh:ssh登录
|
||
"""
|
||
# 判断是否开启免费IP库
|
||
if os.path.exists('{}/data/{}_login_area.pl'.format(get_panel_path(), 'yakpanel')):
|
||
return False, {}
|
||
login_ip_area = ''
|
||
ip_info = get_free_ips_area([login_ip])
|
||
if not login_ip in ip_info:
|
||
return False, {}
|
||
|
||
ip_info = ip_info[login_ip]
|
||
if not 'city' in ip_info:
|
||
login_ip_area = ip_info['info']
|
||
status = True
|
||
s_conf = '{}/data/{}_login_area.json'.format(get_panel_path(), login_type)
|
||
data = {}
|
||
try:
|
||
data = json.loads(readFile(s_conf))
|
||
except:
|
||
pass
|
||
if not login_ip_area and 'city' in ip_info:
|
||
city = ip_info['city']
|
||
login_ip_area = ip_info['info']
|
||
if len(city) >= 1 and not city in data:
|
||
data[city] = 0
|
||
if data[city] < 3:
|
||
if city == 'Local':
|
||
login_ip_area += '(<font color=red>Intranet</font>)'
|
||
else:
|
||
login_ip_area += '(<font color=red>Abnormal login</font>)'
|
||
data[city] += 1
|
||
writeFile(s_conf, json.dumps(data))
|
||
data['login_ip_area'] = login_ip_area
|
||
return status, data
|
||
|
||
|
||
# 登陆告警
|
||
def login_send_body(is_type, username, login_ip, port):
|
||
send_type = ""
|
||
panel_path = get_panel_path()
|
||
login_send_type_conf = "/www/server/panel/data/panel_login_send.pl"
|
||
if os.path.exists(login_send_type_conf):
|
||
send_type = ReadFile(login_send_type_conf).strip()
|
||
else:
|
||
# 兼容之前的
|
||
if os.path.exists("/www/server/panel/data/login_send_type.pl"):
|
||
send_type = readFile("/www/server/panel/data/login_send_type.pl")
|
||
else:
|
||
if os.path.exists('/www/server/panel/data/login_send_mail.pl'):
|
||
send_type = "mail"
|
||
if os.path.exists('/www/server/panel/data/login_send_dingding.pl'):
|
||
send_type = "dingding"
|
||
|
||
# 增加异地登录告警
|
||
server_ip_area = login_ip + ":" + port
|
||
login_aera_status, login_aera = free_login_area(login_ip=server_ip_area, login_type='panel')
|
||
if login_aera_status:
|
||
login_ip_area = ">Location:" + login_aera['login_ip_area']
|
||
# 如果存在归属地则修改日志内容
|
||
time.sleep(0.2)
|
||
if cache_get(server_ip_area):
|
||
id = cache_get(server_ip_area)
|
||
logs = M("logs").where("id=?", id).getField("log")
|
||
data = M("logs").where("id=?", id).setField("log", logs + login_ip_area)
|
||
else:
|
||
login_ip_area = ''
|
||
|
||
add_security_logs('login successful', server_ip_area + login_ip_area, False)
|
||
if not send_type:
|
||
return False
|
||
|
||
object = init_msg(send_type.strip())
|
||
if not object: return
|
||
|
||
send_login_white = '{}/data/send_login_white.json'.format(panel_path)
|
||
if check_ip_white(send_login_white, login_ip):
|
||
return False
|
||
|
||
if login_ip_area:
|
||
plist = [
|
||
">Login method:" + is_type,
|
||
">Login account:" + username,
|
||
">Login IP:" + login_ip + ":" + port,
|
||
login_ip_area,
|
||
">Login status:<font color=#20a53a>Success</font>"
|
||
]
|
||
else:
|
||
plist = [
|
||
">Login method:" + is_type,
|
||
">Login account:" + username,
|
||
">Login IP:" + login_ip + ":" + port,
|
||
">Login status:<font color=#20a53a>Success</font>"
|
||
]
|
||
|
||
push_data = {
|
||
"ip": get_server_ip(),
|
||
"is_type": is_type,
|
||
"username": username,
|
||
"login_ip": login_ip,
|
||
"login_ip_area": login_ip_area,
|
||
"msg_list": plist
|
||
}
|
||
try:
|
||
from mod.base.push_mod import push_by_task_keyword
|
||
res = push_by_task_keyword("panel_login", "panel_login", push_data=push_data)
|
||
print_log(res)
|
||
if res:
|
||
return
|
||
except:
|
||
print_log(get_error_info())
|
||
pass
|
||
|
||
if send_type == 'sms':
|
||
data = {}
|
||
data['ip'] = get_server_ip()
|
||
data['local_ip'] = get_network_ip()
|
||
# 不加内网IP,否则短信模板参数长度超过限制
|
||
ip = "{}(外)".format(data['ip'])
|
||
sm_args = {'name': '[' + ip + ']', 'time': time.strftime('%Y-%m-%d %X', time.localtime()),
|
||
'type': '[' + is_type + ']',
|
||
'user': username}
|
||
rdata = object.send_msg('login_panel', check_sms_argv(sm_args))
|
||
else:
|
||
# noinspection PyUnresolvedReferences
|
||
from panel_msg.collector import SitePushMsgCollect
|
||
|
||
msg = SitePushMsgCollect.panel_login(plist)
|
||
|
||
if send_type.strip() == "wx_account":
|
||
# noinspection PyUnresolvedReferences
|
||
from push.site_push import ToWechatAccountMsg
|
||
object.send_msg(ToWechatAccountMsg.panel_login(
|
||
name=username,
|
||
ip=login_ip,
|
||
login_type=is_type,
|
||
address=login_ip_area,
|
||
login_time=time.strftime('%Y-%m-%d %X', time.localtime())
|
||
))
|
||
else:
|
||
info = get_push_info("YakPanel login alarm", plist)
|
||
info["push_type"] = "YakPanel login alarm"
|
||
object.push_data(info)
|
||
|
||
# if send_type == "dingding":
|
||
# msg = "#### 堡塔登录提醒\n\n > 服务器 :"+get_ip()+"\n\n > 登录方式:"+is_type+"\n\n > 登录账号:"+username+"\n\n > 登录IP:"+login_ip+":"+port+"\n\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n\n > 登录状态: <font color=#20a53a>成功</font>'
|
||
# send_dingding(msg, False)
|
||
# elif send_type == "weixin":
|
||
# msg = "#### 堡塔登录提醒\n\n > 服务器 :"+get_ip()+"\n\n > 登录方式:"+is_type+"\n\n > 登录账号:"+username+"\n\n > 登录IP:"+login_ip+":"+port+"\n\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n\n > 登录状态: <font color=#20a53a>成功</font>'
|
||
# send_weixin(msg, False)
|
||
# elif send_type == "feishu":
|
||
# msg = "堡塔登录提醒\n > 服务器 :"+get_ip()+"\n > 登录方式:"+is_type+"\n > 登录账号:"+username+"\n > 登录IP:"+login_ip+":"+port+"\n > 登录时间:"+time.strftime('%Y-%m-%d %X',time.localtime())+'\n > 登录状态: 成功'
|
||
# send_feishu(msg, False)
|
||
|
||
|
||
# 普通模式下调用发送消息【设置登陆告警后的设置】
|
||
# title= 发送的title
|
||
# body= 发送的body
|
||
# is_logs= 是否记录日志
|
||
# is_type=发送告警的类型
|
||
def send_to_body(title, body, is_logs=False, is_type="YakPanel email alert"):
|
||
login_send_mail = "{}/data/login_send_mail.pl".format(get_panel_path())
|
||
login_send_dingding = "{}/data/login_send_dingding.pl".format(get_panel_path())
|
||
if os.path.exists(login_send_mail):
|
||
if is_logs:
|
||
send_mail(title, body, True, is_type)
|
||
send_mail(title, body)
|
||
|
||
if os.path.exists(login_send_dingding):
|
||
if is_logs:
|
||
send_dingding(body, True, is_type)
|
||
send_dingding(body)
|
||
|
||
|
||
# 普通发送消息
|
||
# send_type= ["mail","dingding"]
|
||
# title =发送的头
|
||
# body= 发送消息的内容
|
||
def send_body_words(send_type, title, body):
|
||
if send_type == 'mail':
|
||
return send_mail(title, body)
|
||
if send_type == 'dingding':
|
||
return send_dingding(body)
|
||
|
||
|
||
def return_is_send_info():
|
||
import send_mail
|
||
send_mail22 = send_mail.send_mail()
|
||
tongdao = send_mail22.get_settings()
|
||
ret = {}
|
||
ret['mail'] = tongdao['user_mail']['user_name']
|
||
ret['dingding'] = tongdao['dingding']['dingding']
|
||
return ret
|
||
|
||
|
||
def get_sys_path():
|
||
'''
|
||
@name 关键目录
|
||
@author hwliang<2021-06-11>
|
||
@return tuple
|
||
'''
|
||
a = ['/www', '/usr', '/', '/dev', '/home', '/media', '/mnt', '/opt', '/tmp', '/var']
|
||
c = ['/www/.Recycle_bin/', '/www/backup/', '/www/php_session/', '/www/wwwlogs/', '/www/server/', '/etc/', '/usr/',
|
||
'/var/', '/boot/', '/proc/', '/sys/', '/tmp/', '/root/', '/lib/', '/bin/', '/sbin/', '/run/', '/lib64/',
|
||
'/lib32/', '/srv/']
|
||
return a, c
|
||
|
||
|
||
def check_site_path(site_path):
|
||
'''
|
||
@name 检查网站根目录是否为系统关键目录
|
||
@author hwliang<2021-05-31>
|
||
@param site_path<string> 网站根目录全路径
|
||
@return bool
|
||
'''
|
||
try:
|
||
if site_path in ['/', '/usr', '/dev', '/home', '/media', '/mnt', '/opt', '/tmp', '/var']:
|
||
return False
|
||
whites = ['/www/server/tomcat', '/www/server/stop', '/www/server/phpmyadmin', '/www/server/adminer']
|
||
for w in whites:
|
||
if site_path.find(w) == 0: return True
|
||
a, error_paths = get_sys_path()
|
||
site_path = site_path.strip()
|
||
if site_path[-1] == '/': site_path = site_path[:-1]
|
||
if site_path in a:
|
||
return False
|
||
site_path += '/'
|
||
for ep in error_paths:
|
||
if site_path.find(ep) == 0: return False
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
|
||
def is_debug():
|
||
debug_file = "{}/data/debug.pl".format(get_panel_path())
|
||
return os.path.exists(debug_file)
|
||
|
||
|
||
def sys_path_append(path):
|
||
'''
|
||
@name 追加引用路径
|
||
@author hwliang<2021-07-07>
|
||
@param path<string> 路径
|
||
@return void
|
||
'''
|
||
try:
|
||
if not path in sys.path:
|
||
sys.path.insert(0, path)
|
||
except:
|
||
pass
|
||
|
||
|
||
def get_sysbit():
|
||
'''
|
||
@name 获取操作系统位数
|
||
@author hwliang<2021-07-07>
|
||
@return int 32 or 64
|
||
'''
|
||
import struct
|
||
return struct.calcsize('P') * 8
|
||
|
||
|
||
def get_plugin_path(plugin_name=None):
|
||
'''
|
||
@name 取指定插件目录
|
||
@author hwliang<2021-07-14>
|
||
@param plugin_name<string> 插件名称 不传则返回插件根目录
|
||
@return string
|
||
'''
|
||
|
||
root_path = "{}/plugin".format(get_panel_path())
|
||
if not plugin_name: return root_path
|
||
return "{}/{}".format(root_path, plugin_name)
|
||
|
||
|
||
def get_class_path():
|
||
'''
|
||
@name 取类库所在路径
|
||
@author hwliang<2021-07-14>
|
||
@return string
|
||
'''
|
||
return "{}/class".format(get_panel_path())
|
||
|
||
|
||
def decode_data(srcBody):
|
||
"""
|
||
遍历解码字符串
|
||
"""
|
||
arrs = ['utf-8', 'GBK', 'ANSI', 'BIG5']
|
||
for encoding in arrs:
|
||
try:
|
||
data = srcBody.decode(encoding)
|
||
return encoding, data
|
||
except:
|
||
pass
|
||
return False, None
|
||
|
||
|
||
def get_logs_path():
|
||
'''
|
||
@name 取日志目录
|
||
@author hwliang<2021-07-14>
|
||
@return string
|
||
'''
|
||
return '/www/wwwlogs'
|
||
|
||
|
||
def get_vhost_path():
|
||
'''
|
||
@name 取虚拟主机目录
|
||
@author hwliang<2021-08-14>
|
||
@return string
|
||
'''
|
||
return '{}/vhost'.format(get_panel_path())
|
||
|
||
|
||
def get_backup_path():
|
||
'''
|
||
@name 取备份目录
|
||
@author hwliang<2021-07-14>
|
||
@return string
|
||
'''
|
||
default_backup_path = '/www/backup'
|
||
backup_path = M('config').where("id=?", (1,)).getField('backup_path')
|
||
if not backup_path: return default_backup_path
|
||
if os.path.exists(backup_path): return backup_path
|
||
return default_backup_path
|
||
|
||
|
||
def get_site_path():
|
||
'''
|
||
@name 取站点默认存储目录
|
||
@author hwliang<2021-07-14>
|
||
@return string
|
||
'''
|
||
default_site_path = '/www/wwwroot'
|
||
site_path = M('config').where("id=?", (1,)).getField('sites_path')
|
||
if not site_path: return default_site_path
|
||
if os.path.exists(site_path): return site_path
|
||
return default_site_path
|
||
|
||
|
||
def read_config(config_name, ext_name='json'):
|
||
'''
|
||
@name 读取指定配置文件
|
||
@author hwliang<2021-07-14>
|
||
@param config_name<string> 配置文件名称(不含扩展名)
|
||
@param ext_name<string> 配置文件扩展名,默认为json
|
||
@return string 如果发生错误,将抛出PanelError异常
|
||
'''
|
||
config_file = "{}/config/{}.{}".format(get_panel_path(), config_name, ext_name)
|
||
if not os.path.exists(config_file):
|
||
raise PanelError('The specified configuration file {} does not exist'.format(config_name))
|
||
|
||
config_str = readFile(config_file)
|
||
if ext_name == 'json':
|
||
try:
|
||
config_body = json.loads(config_str)
|
||
except Exception as ex:
|
||
raise PanelError('Configuration files are not standard parsable JSON content!\n{}'.format(ex))
|
||
return config_body
|
||
return config_str
|
||
|
||
|
||
def save_config(config_name, config_body, ext_name='json'):
|
||
'''
|
||
@name 保存配置文件
|
||
@author hwliang<2021-07-14>
|
||
@param config_name<string> 配置文件名称(不含扩展名)
|
||
@param config_body<mixed> 被保存的内容, ext_name为json,请传入可解析为json的参数类型,如list,dict,int,str等
|
||
@param ext_name<string> 配置文件扩展名,默认为json
|
||
@return string 如果发生错误,将抛出PanelError异常
|
||
'''
|
||
|
||
config_file = "{}/config/{}.{}".format(get_panel_path(), config_name, ext_name)
|
||
if ext_name == 'json':
|
||
try:
|
||
config_body = json.dumps(config_body)
|
||
except Exception as ex:
|
||
raise PanelError('The configuration content cannot be converted to json format!\n{}'.format(ex))
|
||
|
||
return writeFile(config_file, config_body)
|
||
|
||
|
||
def get_config_value(config_name, key, default='', ext_name='json'):
|
||
'''
|
||
@name 获取指定配置文件的指定配置项
|
||
@author hwliang<2021-07-14>
|
||
@param config_name<string> 配置文件名称(不含扩展名)
|
||
@param key<string> 配置项
|
||
@param default<mixed> 获不存在则返回的默认值,默认为空字符串
|
||
@param ext_name<string> 配置文件扩展名,默认为json
|
||
@return mixed 如果发生错误,将抛出PanelError异常
|
||
'''
|
||
config_data = read_config(config_name, ext_name)
|
||
return config_data.get(key, default)
|
||
|
||
|
||
def set_config_value(config_name, key, value, ext_name='json'):
|
||
'''
|
||
@name 设置指定配置文件的指定配置项
|
||
@author hwliang<2021-07-14>
|
||
@param config_name<string> 配置文件名称(不含扩展名)
|
||
@param key<string> 配置项
|
||
@param value<mixed> 配置值
|
||
@param ext_name<string> 配置文件扩展名,默认为json
|
||
@return mixed 如果发生错误,将抛出PanelError异常
|
||
'''
|
||
config_data = read_config(config_name, ext_name)
|
||
config_data[key] = value
|
||
return save_config(config_name, config_data, ext_name)
|
||
|
||
|
||
def return_data(status, data={}, status_code=None, error_msg=None):
|
||
'''
|
||
@name 格式化响应内容
|
||
@author hwliang<2021-07-14>
|
||
@param status<bool> 状态
|
||
@param data<mixed> 响应数据
|
||
@param status_code<int> 状态码
|
||
@param error_msg<string> 错误消息内容
|
||
@return dict
|
||
|
||
'''
|
||
if status_code == None:
|
||
status_code = 1 if status else 0
|
||
if error_msg == None:
|
||
error_msg = '' if status else 'unknown error'
|
||
|
||
result = {
|
||
'status': status,
|
||
"status_code": status_code,
|
||
'error_msg': str(error_msg),
|
||
'data': data
|
||
}
|
||
return result
|
||
|
||
|
||
def return_error(error_msg, status_code=-1, data=[]):
|
||
'''
|
||
@name 格式化错误响应内容
|
||
@author hwliang<2021-07-15>
|
||
@param error_msg<string> 错误消息
|
||
@param status_code<int> 状态码,默认为-1
|
||
@param data<mixed> 响应数据
|
||
@return dict
|
||
'''
|
||
if not data: data = error_msg
|
||
return return_data(False, data, status_code, str(error_msg))
|
||
|
||
|
||
def error(error_msg, status_code=-1, data=[]):
|
||
'''
|
||
@name 格式化错误响应内容
|
||
@author hwliang<2021-07-15>
|
||
@param error_msg<string> 错误消息
|
||
@param status_code<int> 状态码,默认为-1
|
||
@param data<mixed> 响应数据
|
||
@return dict
|
||
'''
|
||
if not data: data = error_msg
|
||
return return_error(error_msg, status_code, data)
|
||
|
||
|
||
def success(data=[], status_code=1, error_msg=''):
|
||
'''
|
||
@name 格式化成功响应内容
|
||
@author hwliang<2021-07-15>
|
||
@param data<mixed> 响应数据
|
||
@param status_code<int> 状态码,默认为0
|
||
@return dict
|
||
'''
|
||
return return_data(True, data, status_code, error_msg)
|
||
|
||
|
||
def return_status_code(status_code, format_body, data=[]):
|
||
'''
|
||
@name 按状态码返回
|
||
@author hwliang<2021-07-15>
|
||
@param status_code<int> 状态码
|
||
@param format_body<string> 错误内容
|
||
@param data<mixed> 响应数据
|
||
@return dict
|
||
'''
|
||
error_msg = get_config_value('status_code', str(status_code))
|
||
if not error_msg: raise PanelError('invalid status_code')
|
||
return return_data(error_msg[0], data, status_code, error_msg[1].format(format_body))
|
||
|
||
|
||
def to_dict_obj(data: dict) -> dict_obj:
|
||
'''
|
||
@name 将dict转换为dict_obj
|
||
@author hwliang<2021-07-15>
|
||
@param data<dict> 要被转换的数据
|
||
@return dict_obj
|
||
'''
|
||
if not isinstance(data, dict):
|
||
raise PanelError('parameter error: only support transform dict to dict_obj.')
|
||
pdata = dict_obj()
|
||
for key in data.keys():
|
||
pdata[key] = data[key]
|
||
return pdata
|
||
|
||
|
||
def get_script_object(filename):
|
||
'''
|
||
@name 从脚本文件获取对像
|
||
@author hwliang<2021-07-19>
|
||
@param filename<string> 文件名
|
||
@return object
|
||
'''
|
||
_obj = sys.modules.get(filename, None)
|
||
if _obj: return _obj
|
||
from types import ModuleType
|
||
_obj = sys.modules.setdefault(filename, ModuleType(filename))
|
||
_code = readFile(filename)
|
||
_code_object = compile(_code, filename, 'exec')
|
||
_obj.__file__ = filename
|
||
_obj.__package__ = ''
|
||
exec(_code_object, _obj.__dict__)
|
||
return _obj
|
||
|
||
|
||
def check_hooks():
|
||
'''
|
||
@name 自动注册HOOK
|
||
@author hwliang<2021-07-19>
|
||
@return void
|
||
'''
|
||
hooks_path = '{}/hooks'.format(get_panel_path())
|
||
if not os.path.exists(hooks_path):
|
||
return
|
||
for hook_name in os.listdir(hooks_path):
|
||
if hook_name[-3:] != '.py': continue
|
||
filename = os.path.join(hooks_path, hook_name)
|
||
_obj = get_script_object(filename)
|
||
_main = getattr(_obj, 'main', None)
|
||
if not _main: continue
|
||
_main()
|
||
|
||
|
||
def register_hook(hook_index, hook_def):
|
||
'''
|
||
@name 注册HOOK
|
||
@author hwliang<2021-07-15>
|
||
@param hook_index<string> HOOK位置
|
||
@param hook_def<def> HOOK函数对像
|
||
@return void
|
||
'''
|
||
from YakPanel import hooks
|
||
hook_keys = hooks.keys()
|
||
if not hook_index in hook_keys:
|
||
hooks[hook_index] = []
|
||
if not hook_def in hooks[hook_index]:
|
||
hooks[hook_index].append(hook_def)
|
||
|
||
|
||
def exec_hook(hook_index, data):
|
||
r'''
|
||
@name 执行HOOk
|
||
@author hwliang<2021-07-15>
|
||
@param hook_index<string> HOOK索引位置,格式限制:^\w+$
|
||
@param data<mixed> 运行数据
|
||
@return mixed
|
||
'''
|
||
|
||
from YakPanel import hooks
|
||
hook_keys = hooks.keys()
|
||
if not hook_index in hook_keys:
|
||
return data
|
||
|
||
for hook_def in hooks[hook_index]:
|
||
data = hook_def(data)
|
||
return data
|
||
|
||
|
||
def get_hook_index(mod_name, def_name):
|
||
'''
|
||
@name 获取HOOK位置
|
||
@author hwliang<2021-07-19>
|
||
@param mod_name<string> 模块名称
|
||
@param def_name<string> 方法名称
|
||
@return tuple
|
||
'''
|
||
mod_name = mod_name.upper()
|
||
def_name = def_name.upper()
|
||
last_index = '{}_{}_LAST'.format(mod_name, def_name)
|
||
end_index = '{}_{}_END'.format(mod_name, def_name)
|
||
return last_index, end_index
|
||
|
||
|
||
def flush_plugin_list():
|
||
'''
|
||
@name 刷新插件列表
|
||
@author hwliang<2021-07-22>
|
||
@return bool
|
||
'''
|
||
skey = 'TNaMJdG3mDHKRS6Y'
|
||
from YakPanel import cache
|
||
if cache.get(skey): cache.delete(skey)
|
||
load_soft_list()
|
||
return True
|
||
|
||
|
||
def get_session_timeout():
|
||
'''
|
||
@name 获取session过期时间
|
||
@author hwliang<2021-07-28>
|
||
@return int
|
||
'''
|
||
from YakPanel import cache
|
||
skey = 'session_timeout'
|
||
session_timeout = cache.get(skey)
|
||
if not session_timeout == None: return session_timeout
|
||
|
||
sess_out_path = '{}/data/session_timeout.pl'.format(get_panel_path())
|
||
session_timeout = 86400
|
||
if not os.path.exists(sess_out_path):
|
||
return session_timeout
|
||
try:
|
||
session_timeout = int(readFile(sess_out_path))
|
||
except:
|
||
session_timeout = 86400
|
||
cache.set(skey, session_timeout, 3600)
|
||
return session_timeout
|
||
|
||
|
||
def get_login_token_auth():
|
||
'''
|
||
@name 获取登录token
|
||
@author hwliang<2021-07-28>
|
||
@return string
|
||
'''
|
||
from YakPanel import cache
|
||
skey = 'login_token'
|
||
login_token = cache.get(skey)
|
||
if not login_token == None: return login_token
|
||
|
||
login_token_file = '{}/data/login_token.pl'.format(get_panel_path())
|
||
login_token = '1234567890'
|
||
if not os.path.exists(login_token_file):
|
||
return login_token
|
||
login_token = readFile(login_token_file)
|
||
cache.set(skey, login_token, 3600)
|
||
return login_token
|
||
|
||
|
||
def listen_ipv6():
|
||
'''
|
||
@name 是否监听ipv6
|
||
@author hwliang<2021-08-12>
|
||
@return bool
|
||
'''
|
||
ipv6_file = '{}/data/ipv6.pl'.format(get_panel_path())
|
||
return os.path.exists(ipv6_file)
|
||
|
||
|
||
def get_panel_log_file():
|
||
'''
|
||
@name 获取panel日志文件
|
||
@author hwliang<2021-08-12>
|
||
@return string
|
||
'''
|
||
return "{}/logs/error.log".format(get_panel_path())
|
||
|
||
|
||
def print_log(_info, _level='DEBUG'):
|
||
'''
|
||
@name 写入日志
|
||
@author hwliang<2021-08-12>
|
||
@param _info<string> 要写入到日志文件的信息
|
||
@param _level<string> 日志级别
|
||
@return void
|
||
'''
|
||
if type(_info) == dict:
|
||
_info = json.dumps(_info)
|
||
log_body = "[{}][{}] - {}\n".format(format_date(), _level.upper(), _info)
|
||
return WriteFile(get_panel_log_file(), log_body, 'a+')
|
||
|
||
|
||
def print_error():
|
||
'''
|
||
@name 打印错误信息到日志文件
|
||
@author hwliang
|
||
@return void
|
||
'''
|
||
print_log(get_error_info(), 'ERROR')
|
||
|
||
|
||
def to_date(format="%Y-%m-%d %H:%M:%S", times=None):
|
||
'''
|
||
@name 格式时间转时间戳
|
||
@author hwliang<2021-08-17>
|
||
@param format<string> 时间格式
|
||
@param times<date> 时间
|
||
@return int
|
||
'''
|
||
if times:
|
||
if isinstance(times, int): return times
|
||
if isinstance(times, float): return int(times)
|
||
if is_number(times): return int(times)
|
||
else:
|
||
return 0
|
||
ts = time.strptime(times, format)
|
||
return time.mktime(ts)
|
||
|
||
|
||
def get_glibc_version():
|
||
'''
|
||
@name 获取glibc版本
|
||
@author hwliang<2021-08-17>
|
||
@return string
|
||
'''
|
||
try:
|
||
cmd_result = ExecShell("ldd --version")[0]
|
||
if not cmd_result: return ''
|
||
glibc_version = cmd_result.split("\n")[0].split()[-1]
|
||
except:
|
||
return ''
|
||
return glibc_version
|
||
|
||
|
||
def is_apache_nginx():
|
||
'''
|
||
@name 是否是apache或nginx
|
||
@author hwliang<2021-08-17>
|
||
@return bool
|
||
'''
|
||
setup_path = get_setup_path()
|
||
return os.path.exists(setup_path + '/apache') or os.path.exists(setup_path + '/nginx')
|
||
|
||
|
||
def error_not_login(e=None, _src=None):
|
||
'''
|
||
@name 未登录时且未输入正确的安全入口时的响应
|
||
@author hwliang<2021-12-16>
|
||
@return Response
|
||
'''
|
||
from YakPanel import Response, render_template, redirect, request
|
||
client_status = check_client_info()
|
||
x_http_token = request.headers.get('x-http-token')
|
||
if client_status == 1:
|
||
if x_http_token:
|
||
# result = {"status": False, "code": -8888, "redirect": get_admin_path(),
|
||
# "msg": "The current login session has been invalid, please login again!"}
|
||
|
||
# 修改为yakpanel通用返回方式
|
||
result = {
|
||
"status": -1,
|
||
"timestamp": int(time.time()),
|
||
"message": {
|
||
"msg": "The current login session has been invalid, please login again!",
|
||
"redirect": get_admin_path()
|
||
}
|
||
}
|
||
|
||
return Response(json.dumps(result), mimetype='application/json', status=200)
|
||
return redirect(get_admin_path())
|
||
elif client_status == 2:
|
||
if x_http_token:
|
||
# result = {"status": False, "code": -8888, "redirect": "/login",
|
||
# "msg": "The current login session has been invalid, please login again!"}
|
||
|
||
# 修改为yakpanel通用返回方式
|
||
result = {
|
||
"status": -1,
|
||
"timestamp": int(time.time()),
|
||
"message": {
|
||
"msg": "The current login session has been invalid, please login again!",
|
||
"redirect": "/login"
|
||
}
|
||
}
|
||
|
||
return Response(json.dumps(result), mimetype='application/json', status=200)
|
||
# noinspection PyUnresolvedReferences
|
||
return render_template('autherr.html')
|
||
|
||
try:
|
||
abort_code = read_config('abort')
|
||
if not abort_code in [None, 1, 0, '0', '1']:
|
||
if abort_code == 404: return error_404(e)
|
||
if abort_code == 403: return error_403(e)
|
||
return Response(status=int(abort_code))
|
||
except:
|
||
pass
|
||
|
||
if e in ['/login']:
|
||
return redirect(e)
|
||
|
||
if _src:
|
||
return e
|
||
else:
|
||
return error_404(e)
|
||
|
||
|
||
def error_403(e):
|
||
from YakPanel import Response, session
|
||
# if not session.get('login',None): return error_not_login()
|
||
errorStr = '''<html>
|
||
<head><title>403 Forbidden</title></head>
|
||
<body>
|
||
<center><h1>403 Forbidden</h1></center>
|
||
<hr><center>nginx</center>
|
||
</body>
|
||
</html>'''
|
||
headers = {
|
||
"Content-Type": "text/html"
|
||
}
|
||
return Response(errorStr, status=403, headers=headers)
|
||
|
||
|
||
def error_404(e):
|
||
from YakPanel import Response, session
|
||
# if not session.get('login',None): return error_not_login()
|
||
errorStr = '''<html>
|
||
<head><title>404 Not Found</title></head>
|
||
<body>
|
||
<center><h1>404 Not Found</h1></center>
|
||
<hr><center>nginx</center>
|
||
</body>
|
||
</html>'''
|
||
headers = {
|
||
"Content-Type": "text/html"
|
||
}
|
||
return Response(errorStr, status=404, headers=headers)
|
||
|
||
|
||
def error_401(e):
|
||
from YakPanel import Response, session
|
||
# if not session.get('login',None): return error_not_login()
|
||
errorStr = '''<html>
|
||
<head><title>401 Unauthorized</title></head>
|
||
<body>
|
||
<center><h1>401 Unauthorized</h1></center>
|
||
<hr><center>You must enter a valid login ID and password to access this page.</center>
|
||
</body>
|
||
</html>'''
|
||
headers = {
|
||
"Content-Type": "text/html"
|
||
}
|
||
return Response(errorStr, status=401, headers=headers)
|
||
|
||
|
||
def get_password_config():
|
||
'''
|
||
@name 获取密码安全配置
|
||
@author hwliang<2021-10-18>
|
||
@return int
|
||
'''
|
||
import config
|
||
return config.config().get_password_config(None)
|
||
|
||
|
||
def password_expire_check():
|
||
'''
|
||
@name 密码过期检查
|
||
@author hwliang<2021-10-18>
|
||
@return bool
|
||
'''
|
||
p_config = get_password_config()
|
||
if p_config['expire'] == 0: return True
|
||
if time.time() > p_config['expire_time']: return False
|
||
return True
|
||
|
||
|
||
def stop_status_mvore():
|
||
flag = False
|
||
try:
|
||
nginx_path = '/www/server/panel/vhost/nginx/btwaf.conf'
|
||
if os.path.exists(nginx_path):
|
||
ExecShell('mv %s %s.bak' % (nginx_path, nginx_path))
|
||
flag = True
|
||
nginx_path = '/www/server/panel/vhost/nginx/free_waf.conf'
|
||
if os.path.exists(nginx_path):
|
||
ExecShell('mv %s %s.bak' % (nginx_path, nginx_path))
|
||
flag = True
|
||
apache_path = '/www/server/panel/vhost/apache/btwaf.conf'
|
||
if os.path.exists(apache_path):
|
||
ExecShell('chattr -i %s && mv %s %s.bak' % (apache_path, apache_path, apache_path))
|
||
flag = True
|
||
if flag:
|
||
serviceReload()
|
||
except:
|
||
pass
|
||
|
||
|
||
def is_error_path():
|
||
if os.path.exists("/www/server/panel/data/error_pl.pl"):
|
||
stop_status_mvore()
|
||
return True
|
||
return False
|
||
|
||
|
||
def get_php_versions(reverse=False):
|
||
'''
|
||
@name 取PHP版本列表
|
||
@author hwliang<2021-12-16>
|
||
@param reverse<bool> 是否降序
|
||
@return list
|
||
'''
|
||
_file = get_panel_path() + '/config/php_versions.json'
|
||
if os.path.exists(_file):
|
||
version_list = json.loads(readFile(_file))
|
||
else:
|
||
version_list = ['52', '53', '54', '55', '56', '70', '71', '72', '73', '74', '80', '81', '82', '83', '84', '85']
|
||
|
||
return sorted(version_list, reverse=reverse)
|
||
|
||
|
||
def get_full_session_file():
|
||
'''
|
||
@name 获取临时SESSION文件
|
||
@author hwliang<2021-12-28>
|
||
@return string
|
||
'''
|
||
from YakPanel import app
|
||
full_session_key = app.config['SESSION_KEY_PREFIX'] + get_session_id()
|
||
sess_path = get_panel_path() + '/data/session/'
|
||
return sess_path + '/' + md5(full_session_key)
|
||
|
||
|
||
def install_mysql_client():
|
||
'''
|
||
@name 安装mysql客户端
|
||
@author hwliang<2022-01-14>
|
||
@return void
|
||
'''
|
||
if os.path.exists('/usr/bin/yum'):
|
||
os.system("yum install mariadb -y")
|
||
if not os.path.exists('/usr/bin/mysql'):
|
||
os.system("yum reinstall mariadb -y")
|
||
elif os.path.exists('/usr/bin/apt-get'):
|
||
os.system('apt-get install mariadb-client -y')
|
||
if not os.path.exists('/usr/bin/mysql'):
|
||
os.system('apt-get reinstall mariadb-client* -y')
|
||
|
||
|
||
def get_mysqldump_bin():
|
||
'''
|
||
@name 获取mysqldump路径
|
||
@author hwliang<2022-01-14>
|
||
@return string
|
||
'''
|
||
bin_files = [
|
||
'{}/mysql/bin/mysqldump'.format(get_setup_path()),
|
||
'/usr/bin/mysqldump',
|
||
'/usr/local/bin/mysqldump',
|
||
'/usr/sbin/mysqldump',
|
||
'/usr/local/sbin/mysqldump'
|
||
]
|
||
|
||
for bin_file in bin_files:
|
||
if os.path.exists(bin_file):
|
||
return bin_file
|
||
|
||
install_mysql_client()
|
||
|
||
for bin_file in bin_files:
|
||
if os.path.exists(bin_file):
|
||
return bin_file
|
||
|
||
return bin_files[0]
|
||
|
||
|
||
def get_mysql_bin():
|
||
'''
|
||
@name 获取mysql路径
|
||
@author hwliang<2022-01-14>
|
||
@return string
|
||
'''
|
||
bin_files = [
|
||
'{}/mysql/bin/mysql'.format(get_setup_path()),
|
||
'/usr/bin/mysql',
|
||
'/usr/local/bin/mysql',
|
||
'/usr/sbin/mysql',
|
||
'/usr/local/sbin/mysql'
|
||
]
|
||
|
||
for bin_file in bin_files:
|
||
if os.path.exists(bin_file):
|
||
return bin_file
|
||
|
||
install_mysql_client()
|
||
|
||
for bin_file in bin_files:
|
||
if os.path.exists(bin_file):
|
||
return bin_file
|
||
return bin_files[0]
|
||
|
||
|
||
def error_conn_cloud(text):
|
||
'''
|
||
@name 连接云端失败
|
||
@author hwliang<2021-12-18>
|
||
@return void
|
||
'''
|
||
code_msg = ''
|
||
if text.find("502 Bad Gateway") != -1:
|
||
code_msg = '502 Bad Gateway'
|
||
if text.find("504 Bad Gateway") != -1:
|
||
code_msg = '504 Bad Gateway'
|
||
elif text.find("Connection refused") != -1:
|
||
code_msg = 'Connection refused'
|
||
elif text.find("Connection timed out") != -1:
|
||
code_msg = 'Connection timed out'
|
||
elif text.find("Connection reset by peer") != -1:
|
||
code_msg = 'Connection reset by peer'
|
||
elif text.find("Name or service not known") != -1:
|
||
code_msg = 'Name or service not known'
|
||
elif text.find("No route to host") != -1:
|
||
code_msg = 'No route to host'
|
||
elif text.find("No such file or directory") != -1:
|
||
code_msg = 'No such file or directory'
|
||
elif text.find("404 Not Found") != -1:
|
||
code_msg = '404 Not Found'
|
||
elif text.find("403 Forbidden") != -1:
|
||
code_msg = '403 Forbidden'
|
||
elif text.find("401 Unauthorized") != -1:
|
||
code_msg = '401 Unauthorized'
|
||
elif text.find("400 Bad Request") != -1:
|
||
code_msg = '400 Bad Request'
|
||
elif text.find("Remote end closed connection without response") != -1:
|
||
code_msg = 'Remote end closed connection'
|
||
err_template_file = '{}/YakPanel/templates/default/error_connect.html'.format(get_panel_path())
|
||
msg = readFile(err_template_file)
|
||
msg = msg.format(code=code_msg)
|
||
return PanelError(msg)
|
||
|
||
|
||
def get_mountpoint_list():
|
||
'''
|
||
@name 获取挂载点列表
|
||
@author hwliang<2021-12-18>
|
||
@return list
|
||
'''
|
||
import psutil
|
||
mount_list = []
|
||
for mount in psutil.disk_partitions():
|
||
mountpoint = mount.mountpoint if mount.mountpoint[-1] == '/' else mount.mountpoint + '/'
|
||
mount_list.append(mountpoint)
|
||
# 根据挂载点字符长度排序
|
||
mount_list.sort(key=lambda i: len(i), reverse=True)
|
||
return mount_list
|
||
|
||
|
||
def get_path_in_mountpoint(path):
|
||
'''
|
||
@name 获取文件或目录目录所在挂载点
|
||
@author hwliang<2022-03-30>
|
||
@param path<string> 文件或目录路径
|
||
@return string
|
||
'''
|
||
# 判断是否是绝对路径
|
||
if path.find('./') != -1 or path[0] != '/': raise PanelError("cannot use relative path")
|
||
if not path: raise PanelError("path cannot be empty")
|
||
|
||
# 在目录尾加/
|
||
if os.path.isdir(path):
|
||
path = path if path[-1] == '/' else path + '/'
|
||
|
||
# 匹配挂载点
|
||
mount_list = get_mountpoint_list()
|
||
for mountpoint in mount_list:
|
||
if path.startswith(mountpoint):
|
||
return mountpoint
|
||
|
||
# 没有匹配到挂载点
|
||
return '/'
|
||
|
||
|
||
def get_recycle_bin_path(path):
|
||
'''
|
||
@name 获取指定文件或目录的回收站路径
|
||
@author hwliang<2022-03-30>
|
||
@param path<string> 文件或目录路径
|
||
@return string
|
||
'''
|
||
mountpoint = get_path_in_mountpoint(path)
|
||
recycle_bin_path = '{}/.Recycle_bin/'.format(mountpoint)
|
||
try:
|
||
if not os.path.exists(recycle_bin_path):
|
||
os.mkdir(recycle_bin_path, 384)
|
||
except:
|
||
return '/www/.Recycle_bin/'
|
||
return recycle_bin_path
|
||
|
||
|
||
def get_recycle_bin_list():
|
||
'''
|
||
@name 获取回收站列表
|
||
@author hwliang<2022-03-30>
|
||
@return list
|
||
'''
|
||
# 旧的回收站重命名为.Recycle_bin
|
||
default_path = '/www/.Recycle_bin'
|
||
default_path_src = '/www/Recycle_bin'
|
||
if os.path.exists(default_path_src) and not os.path.exists(default_path):
|
||
try:
|
||
os.rename(default_path_src, default_path)
|
||
except:
|
||
ExecShell("mv {} {}".format(default_path_src, default_path))
|
||
|
||
if not os.path.exists(default_path):
|
||
os.makedirs(default_path, 384)
|
||
|
||
# 获取回收站列表
|
||
recycle_bin_list = []
|
||
mtime_list = [] # 修改时间
|
||
for mountpoint in get_mountpoint_list():
|
||
recycle_bin_path = '{}.Recycle_bin/'.format(mountpoint)
|
||
|
||
try:
|
||
if not os.path.exists(recycle_bin_path):
|
||
os.mkdir(recycle_bin_path, 384)
|
||
if not os.path.exists(recycle_bin_path): continue
|
||
mtime = os.path.getmtime(recycle_bin_path)
|
||
if mtime in mtime_list: continue # 通过修改时间去重
|
||
mtime_list.append(mtime)
|
||
recycle_bin_list.append(recycle_bin_path)
|
||
except:
|
||
continue
|
||
|
||
# 包含默认回收站路径?
|
||
if not default_path + '/' in recycle_bin_list:
|
||
recycle_bin_list.append(default_path + '/')
|
||
|
||
return recycle_bin_list
|
||
|
||
|
||
def check_password(password):
|
||
"""
|
||
密码强度:
|
||
0 弱
|
||
1 中
|
||
2 强
|
||
"""
|
||
l = 0
|
||
low = False
|
||
up = False
|
||
symbol = False
|
||
digit = False
|
||
p_len = len(password)
|
||
if p_len < 8:
|
||
return l
|
||
for i in password:
|
||
if i.islower():
|
||
low = True
|
||
if i.isupper():
|
||
up = True
|
||
if i in ['~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '=', '+', '<', '>', ',', '.', '/',
|
||
'"', '|', '\\', "'", '?']:
|
||
symbol = True
|
||
if i.isdigit():
|
||
digit = True
|
||
# 判断重复出现
|
||
tmp = len(set([i for i in password]))
|
||
if tmp >= 2:
|
||
if low and up and symbol and digit:
|
||
l = 2
|
||
if p_len >= 11:
|
||
l = 1
|
||
return l
|
||
|
||
|
||
def set_module_logs(mod_name, fun_name, count=1):
|
||
"""
|
||
@模块使用次数
|
||
@mod_name 模块名称
|
||
@fun_name 函数名
|
||
"""
|
||
import datetime
|
||
data = {}
|
||
path = '{}/data/mod_log.json'.format(get_panel_path())
|
||
if os.path.exists(path):
|
||
try:
|
||
data = json.loads(readFile(path))
|
||
except:
|
||
pass
|
||
|
||
if type(data) != dict:
|
||
data = {}
|
||
|
||
key = datetime.datetime.now().strftime("%Y-%m-%d")
|
||
if not key in data: data[key] = {}
|
||
|
||
if not mod_name in data[key]:
|
||
data[key][mod_name] = {}
|
||
|
||
if not fun_name in data[key][mod_name]:
|
||
data[key][mod_name][fun_name] = 0
|
||
|
||
data[key][mod_name][fun_name] += count
|
||
|
||
writeFile(path, json.dumps(data))
|
||
return True
|
||
|
||
|
||
headers_filter_rules = None
|
||
|
||
|
||
def filter_headers():
|
||
'''
|
||
@name 过滤请求头
|
||
@author hwliang<2021-12-18>
|
||
@return dict
|
||
'''
|
||
global headers_filter_rules
|
||
|
||
# 预编译过滤规则
|
||
if not headers_filter_rules:
|
||
headers_filter_rules = {
|
||
'host': re.compile(r'^[\w\.\-\:]+$'),
|
||
'accept': re.compile(r'^[\w\s\.\-\*\/\,\=\;\+]+$'),
|
||
'accept-encoding': re.compile(r'^[\w\s\.\-\*\/\,]+$'),
|
||
'accept-language': re.compile(r'^[\w\s\.\-\*\/\,\=\:\;]+$'),
|
||
'cache-control': re.compile(r'^[\w\s\.\-\=\;]+$'),
|
||
'connection': re.compile(r'^[\w\s\.\-]+$'),
|
||
'content-length': re.compile(r'^[\d]+$'),
|
||
'cookie': re.compile(r'^[\w\s\=\%\+\&\;\:\@\$\,\.\-\_\*\/\?\!\~\#]+$'),
|
||
'origin': re.compile(r'^(http|https)://[\w\.\-\?\=\&\/\:]+$'),
|
||
'pragma': re.compile(r'^[\w\s\.\-]+$'),
|
||
'referer': re.compile(r'^(http|https)://[\w\.\-\?\=\&\/\:\%\#\~\!\*\+\@]+$'),
|
||
'user-agent': re.compile(r'^[\w\s\.\-\*\/\,\(\)\=\+\;\:\@\$\,\.\-\_\~\#]+$'),
|
||
'x-cookie-token': re.compile(r'^\w+$'),
|
||
'x-http-token': re.compile(r'^\w+$'),
|
||
'X-KL-Ajax-Request': re.compile(r'^\w+$'),
|
||
'X-Requested-With': re.compile(r'^\w+$')
|
||
}
|
||
from flask import request
|
||
headers = request.headers
|
||
skeys = headers_filter_rules.keys()
|
||
|
||
for k in skeys:
|
||
v = headers.get(k, None)
|
||
if not v: continue
|
||
if not headers_filter_rules[k].match(v):
|
||
return False
|
||
return True
|
||
|
||
|
||
def trim(data):
|
||
"""
|
||
@去除所有空格
|
||
"""
|
||
return data.replace(' ', '').strip()
|
||
|
||
|
||
def get_os(_os='windows'):
|
||
"""
|
||
@验证系统版本
|
||
"""
|
||
src_os = 'windows'
|
||
if os.path.exists('/www/server/panel'): src_os = 'linux'
|
||
if src_os == _os: return True
|
||
return False
|
||
|
||
|
||
def get_file_list(path, flist):
|
||
"""
|
||
递归获取目录所有文件列表
|
||
@path 目录路径
|
||
@flist 返回文件列表
|
||
"""
|
||
if os.path.exists(path):
|
||
files = os.listdir(path)
|
||
flist.append(path)
|
||
for file in files:
|
||
if os.path.isdir(path + '/' + file):
|
||
get_file_list(path + '/' + file, flist)
|
||
else:
|
||
flist.append(path + '/' + file)
|
||
|
||
|
||
def writeFile2(filename, s_body, mode='w+'):
|
||
"""
|
||
写入字节文件内容
|
||
@filename 文件名
|
||
@s_body 欲写入的内容
|
||
|
||
"""
|
||
try:
|
||
fp = open(filename, mode);
|
||
fp.write(s_body)
|
||
fp.close()
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
|
||
def check_obj_upgrade(_obj, filename=None):
|
||
'''
|
||
@name 检查指定模块是否修改
|
||
@author hwliang
|
||
@param <string>文件名
|
||
@param <object>模块对象
|
||
@return void
|
||
'''
|
||
|
||
# 引用缓存
|
||
try:
|
||
from YakPanel import cache
|
||
except:
|
||
return
|
||
|
||
# 是否传递文件名?
|
||
if not filename:
|
||
filename = _obj.__file__
|
||
|
||
# 获取文件修改时间
|
||
skey = "obj_up_{}".format(md5(filename))
|
||
mtime = os.path.getmtime(filename) # 当前
|
||
old_mtime = cache.get(skey) # 旧的
|
||
|
||
# 直接设置当前修改时间
|
||
if not old_mtime:
|
||
cache.set(skey, mtime)
|
||
return
|
||
|
||
# 检查是否修改
|
||
if old_mtime == mtime:
|
||
return
|
||
|
||
# 重新加载模块
|
||
import importlib
|
||
importlib.reload(_obj)
|
||
cache.set(skey, mtime)
|
||
|
||
|
||
def version_to_tuple(version):
|
||
'''
|
||
@name 将版本号转为元组
|
||
@version 字符串版本号
|
||
@return 元组版本号
|
||
'''
|
||
if not version:
|
||
return ()
|
||
if not isinstance(version, str):
|
||
return version
|
||
version = re.sub(r"[^\.\d]+", "", version)
|
||
version = version.split('.')
|
||
version = tuple(map(int, version))
|
||
return version
|
||
|
||
|
||
def set_search_history(mod_name, key, val):
|
||
"""
|
||
@保存搜索历史
|
||
@mod_name 模块名称
|
||
@key 关键字
|
||
@val string 搜索内容
|
||
"""
|
||
if not val: return False
|
||
|
||
max = 10
|
||
p_file = get_panel_path()
|
||
m_file = p_file + '/data/search.limit'
|
||
d_file = p_file + '/data/search.json'
|
||
try:
|
||
sdata = int(readFile(m_file))
|
||
if sdata: max = sdata
|
||
except:
|
||
pass
|
||
|
||
result = {}
|
||
try:
|
||
result = json.loads(readFile(d_file))
|
||
except:
|
||
pass
|
||
|
||
if not mod_name in result: result[mod_name] = {}
|
||
if not key in result[mod_name]: result[mod_name][key] = []
|
||
|
||
n_list = []
|
||
for item in result[mod_name][key]:
|
||
if item['val'].strip() != val.strip(): n_list.append(item)
|
||
|
||
n_list.append({'val': val, 'time': int(time.time())})
|
||
|
||
result[mod_name][key] = n_list[len(n_list) - max:]
|
||
|
||
writeFile(d_file, json.dumps(result))
|
||
return True
|
||
|
||
|
||
def get_search_history(mod_name, key):
|
||
"""
|
||
@获取搜索历史
|
||
@mod_name string 模块名称
|
||
@key string 关键字
|
||
"""
|
||
print(mod_name, key)
|
||
result = []
|
||
d_file = get_panel_path() + '/data/search.json'
|
||
try:
|
||
result = json.loads(readFile(d_file))[mod_name][key]
|
||
except:
|
||
pass
|
||
|
||
result = sorted(result, key=lambda x: x['time'], reverse=True)
|
||
|
||
return result
|
||
|
||
|
||
def set_dir_history(mod_name, key, val):
|
||
"""
|
||
@设置目录打开历史
|
||
@mod_name string 模块名称
|
||
@key string 函数名
|
||
@val string 路径
|
||
"""
|
||
if not val: return False
|
||
|
||
max = 10
|
||
result = {}
|
||
d_file = get_panel_path() + '/data/dir_history.json'
|
||
try:
|
||
result = json.loads(readFile(d_file))
|
||
except:
|
||
pass
|
||
|
||
if not mod_name in result: result[mod_name] = {}
|
||
if not key in result[mod_name]: result[mod_name][key] = []
|
||
|
||
data = result[mod_name][key]
|
||
for info in data:
|
||
if val.find(info['val']) >= 0:
|
||
if time.time() - info['time'] < 15:
|
||
data.remove(info)
|
||
|
||
data.append({'val': val, 'time': int(time.time())})
|
||
result[mod_name][key] = data[0:max]
|
||
writeFile(d_file, json.dumps(result))
|
||
return True
|
||
|
||
|
||
def get_dir_history(mod_name, key):
|
||
"""
|
||
@获取目录打开历史
|
||
@mod_name string 模块名称
|
||
@key string 关键字
|
||
"""
|
||
result = []
|
||
d_file = get_panel_path() + '/data/dir_history.json'
|
||
try:
|
||
result = json.loads(readFile(d_file))[mod_name][key]
|
||
except:
|
||
pass
|
||
return result
|
||
|
||
|
||
def get_run_pip():
|
||
pass
|
||
|
||
|
||
def install_pip(shell):
|
||
"""
|
||
@name 安装pip模块
|
||
@author cjxin
|
||
@param shell<string> 安装命令
|
||
"""
|
||
if get_os('windows'):
|
||
os.system(get_run_pip(shell.replace('pip', '[PIP]')))
|
||
else:
|
||
os.system(shell.replace('pip', 'btpip'))
|
||
|
||
|
||
def is_domain(domain):
|
||
"""
|
||
@验证是否域名
|
||
"""
|
||
reg = r"^([\w\-\*]{1,100}\.){1,10}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$";
|
||
if re.match(reg, domain): return True
|
||
return False
|
||
|
||
|
||
def init_msg(module):
|
||
"""
|
||
初始化消息通道
|
||
@module 消息通道模块名称
|
||
"""
|
||
import os, sys
|
||
if not os.path.exists('class/msg'): os.makedirs('class/msg')
|
||
panelPath = get_panel_path()
|
||
|
||
sfile = 'class/msg/{}_msg.py'.format(module)
|
||
if not os.path.exists(sfile): return False
|
||
sys.path.insert(0, "{}/class/msg".format(panelPath))
|
||
|
||
msg_main = __import__('{}_msg'.format(module))
|
||
try:
|
||
mod_reload(msg_main)
|
||
except:
|
||
pass
|
||
return eval('msg_main.{}_msg()'.format(module))
|
||
|
||
|
||
def push_argv(msg):
|
||
"""
|
||
@处理短信参数,否则会被拦截
|
||
"""
|
||
if is_ipv4(msg):
|
||
tmp1 = msg.split('.')
|
||
msg = '{}.***.***.{}'.format(tmp1[0], tmp1[3])
|
||
else:
|
||
if is_domain(msg):
|
||
msg = msg.replace('.', '_')
|
||
return msg
|
||
|
||
|
||
def check_sms_argv(data):
|
||
"""
|
||
@批量处理短信参数,否则会被拦截
|
||
"""
|
||
for key in data:
|
||
val = data[key]
|
||
if type(val) == str:
|
||
data[key] = push_argv(val)
|
||
return data
|
||
|
||
|
||
"""
|
||
@获取推送ip
|
||
"""
|
||
|
||
|
||
def get_push_address():
|
||
ip = push_argv(GetLocalIp())
|
||
return ip
|
||
|
||
|
||
def get_ips_area(ips):
|
||
'''
|
||
@name 获取ip地址所在地
|
||
@author cjxin
|
||
@param ips<list>
|
||
@return list
|
||
'''
|
||
import PluginLoader
|
||
|
||
args = dict_obj()
|
||
args.model_index = 'safe'
|
||
args.ips = ips
|
||
|
||
res = PluginLoader.module_run("ips", "get_ip_area", args)
|
||
return res
|
||
|
||
def return_area(result, key):
|
||
"""
|
||
@name 格式化返回带IP归属地的数组
|
||
@param result<list> 数据数组
|
||
@param key<str> ip所在字段
|
||
@return list
|
||
"""
|
||
tmps = []
|
||
for data in result:
|
||
data['area'] = ''
|
||
tmps.append(data[key])
|
||
|
||
res = get_ips_area(tmps)
|
||
if 'status' in res:
|
||
return result
|
||
|
||
for data in result:
|
||
if data[key] in res:
|
||
if 'en_short_code' in res[data[key]]:
|
||
res[data[key]]['info_raw'] = res[data[key]]['info']
|
||
res[data[key]]['info'] = res[data[key]]['en_short_code']
|
||
|
||
data['area'] = res[data[key]]
|
||
return result
|
||
|
||
# 使用本地ip库
|
||
def return_area11(result, key):
|
||
"""
|
||
@name 格式化返回带IP归属地的数组
|
||
@param result<list> 数据数组
|
||
@param key<str> ip所在字段
|
||
@return list
|
||
"""
|
||
tmps = []
|
||
for data in result:
|
||
data['area'] = ''
|
||
tmps.append(data[key])
|
||
|
||
# res = get_ips_area(tmps)
|
||
# 改本地英文ip库
|
||
res = get_cloud_ip_info2(tmps)
|
||
# print_log("获取ip库信息--{}".format(res))
|
||
|
||
if 'status' in res:
|
||
return result
|
||
|
||
for data in result:
|
||
key_value = data[key].strip()
|
||
if key_value in res:
|
||
if not res[data[key]]['city'].strip() and not res[data[key]]['continent'].strip() and not res[data[key]]['country'].strip():
|
||
info = 'Intranet'
|
||
else:
|
||
info = '{} {} {} {}'.format(res[data[key]]['carrier'], res[data[key]]['country'], res[data[key]]['province'],
|
||
res[data[key]]['city']).strip()
|
||
res[data[key]]['info'] = info
|
||
data['area'] = res[data[key]]
|
||
|
||
return result
|
||
|
||
|
||
def get_cloud_ip_info2(ips):
|
||
"""
|
||
@获取IP地址所在地
|
||
@param ips:
|
||
"""
|
||
import geoip2
|
||
res = {}
|
||
try:
|
||
for ip in ips:
|
||
ip_area_dict = get_ip_location(ip)
|
||
# noinspection PyUnresolvedReferences
|
||
if isinstance(ip_area_dict, geoip2.models.City):
|
||
country = ip_area_dict.raw["country"]
|
||
country['carrier'] = '' # 缺少信息
|
||
country['continent'] = '' # 缺少信息
|
||
res[ip] = country
|
||
|
||
print_log("222country--{}".format(country))
|
||
|
||
# print_log("获取ip地址信息--{}".format(res))
|
||
|
||
except:
|
||
# print_log(get_error_info())
|
||
pass
|
||
return res
|
||
|
||
|
||
def get_network_ip():
|
||
"""
|
||
@name 获取本机ip
|
||
@return string
|
||
"""
|
||
|
||
import socket
|
||
try:
|
||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
s.connect(('8.8.8.8', 80))
|
||
ip = s.getsockname()[0]
|
||
return ip
|
||
finally:
|
||
s.close()
|
||
return '127.0.0.1'
|
||
|
||
|
||
def get_server_ip():
|
||
"""
|
||
@获取服务器外网ip
|
||
"""
|
||
|
||
user_file = '{}/data/userInfo.json'.format(get_panel_path())
|
||
if os.path.exists(user_file):
|
||
try:
|
||
userTmp = json.loads(readFile(user_file))
|
||
return userTmp['address']
|
||
except:
|
||
pass
|
||
|
||
return GetLocalIp()
|
||
|
||
|
||
def get_push_info(title, slist=[]):
|
||
"""
|
||
@name 获取推送信息
|
||
@param title<str> 推送标题
|
||
@param slist<list> 推送追加的列表
|
||
如:slist = ['>发送内容:xxx']
|
||
@return dict
|
||
"""
|
||
data = {}
|
||
data['title'] = title
|
||
data['ip'] = get_server_ip()
|
||
|
||
data['local_ip'] = get_network_ip()
|
||
data['time'] = format_date()
|
||
data['server_name'] = GetConfigValue('title')
|
||
|
||
dlist = [
|
||
"#### {}".format(data['title']),
|
||
">ServerHost: " + data['server_name'],
|
||
">IP Address: {}(Internet) {}(Internal)".format(data['ip'], data['local_ip']),
|
||
">Send Time: " + data['time']
|
||
]
|
||
dlist.extend(slist)
|
||
msg = "\n\n".join(dlist)
|
||
data['msg'] = msg
|
||
data['list'] = dlist
|
||
return data
|
||
|
||
|
||
def write_push_log(module, msg, res):
|
||
"""
|
||
@name 写推送日志
|
||
@module string 模块名称
|
||
@msg string 消息内容
|
||
@res dict 推送结果
|
||
"""
|
||
user = ''
|
||
for key in res:
|
||
status = '<span style="color:#20a53a;">Success</span>'
|
||
if res[key] == 0: status = '<span style="color:red;">Fail</span>'
|
||
user += '[ {}:{} ] '.format(key, status)
|
||
|
||
if not user: user = '[ Default ] '
|
||
try:
|
||
msg_obj = init_msg(module)
|
||
if msg_obj: module = msg_obj.get_version_info(None)['title']
|
||
except:
|
||
pass
|
||
|
||
log = 'Title:[{}],method to informe:[{}],recipient:{}'.format(xsssec(msg), module, user)
|
||
WriteLog('Alarm notification', log)
|
||
return True
|
||
|
||
|
||
def push_msg(module, data):
|
||
"""
|
||
@name 推送消息
|
||
@param module<str> 模块名称
|
||
@param msg<str> 消息内容
|
||
@return dict
|
||
"""
|
||
msg_obj = init_msg(module)
|
||
if not msg_obj:
|
||
returnMsg(False, 'Module {} does not exist!'.format(module))
|
||
|
||
res = msg_obj.push_data(data)
|
||
return res
|
||
|
||
|
||
def check_chinese(data):
|
||
"""
|
||
@name 判断字符串是否包含中文
|
||
"""
|
||
if re.search(u'[\u4e00-\u9fa5]', data):
|
||
return True
|
||
return False
|
||
|
||
|
||
def is_ssl():
|
||
'''
|
||
@name 是否开启SSL
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
return os.path.exists(get_panel_path() + '/data/ssl.pl')
|
||
|
||
|
||
def get_cookie(key, default=None):
|
||
'''
|
||
@name 获取指定Cookie值
|
||
@author hwliang
|
||
@param key<str> Cookie键
|
||
@param default<str> 默认值
|
||
@return str
|
||
'''
|
||
from flask import request
|
||
return request.cookies.get(key, default)
|
||
|
||
|
||
def get_csrf_cookie_token_key():
|
||
'''
|
||
@name 获取CSRF Cookie Key
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
if is_ssl():
|
||
token_key = 'request_token'
|
||
else:
|
||
token_key = 'request_token'
|
||
return token_key
|
||
|
||
|
||
def get_csrf_cookie_token_value():
|
||
'''
|
||
@name 获取CSRF Cookie Value
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
token_key = get_csrf_cookie_token_key()
|
||
return get_cookie(token_key)
|
||
|
||
|
||
def get_csrf_html_token_key():
|
||
'''
|
||
@name 获取CSRF HTML Key
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
if is_ssl():
|
||
token_key = 'request_token_head'
|
||
else:
|
||
token_key = 'request_token_head'
|
||
return token_key
|
||
|
||
|
||
def get_csrf_html_token_value():
|
||
'''
|
||
@name 获取CSRF HTML Value
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
token_key = get_csrf_html_token_key()
|
||
return get_cookie(token_key)
|
||
|
||
|
||
def get_csrf_sess_html_token_value():
|
||
'''
|
||
@name 从SESSION获取CSRF HTML value
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
from flask import session
|
||
return session.get(get_csrf_html_token_key(), "")
|
||
|
||
|
||
def get_csrf_sess_cookie_token_value():
|
||
'''
|
||
@name 从SESSION获取CSRF Cookie value
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
from flask import session
|
||
return session.get(get_csrf_cookie_token_key(), "")
|
||
|
||
|
||
def get_sys_install_bin():
|
||
'''
|
||
@name 获取系统包管理器命令
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
install_bins = ['/usr/bin/yum', '/usr/bin/apt-get', '/usr/bin/dnf']
|
||
for bin in install_bins:
|
||
if os.path.exists(bin):
|
||
return bin
|
||
return ''
|
||
|
||
|
||
def get_firewall_status():
|
||
'''
|
||
@name 获取系统防火墙状态
|
||
@author hwliang
|
||
@return int 0.关闭 1.开启 -1.未安装
|
||
'''
|
||
import psutil
|
||
firewall_files = {'/usr/sbin/firewalld': "pid", '/usr/bin/firewalld': "pid",
|
||
'/usr/sbin/ufw': "/usr/sbin/ufw status|grep 'Status: active'",
|
||
'/sbin/ufw': "/sbin/ufw status |grep 'Status: active'",
|
||
'/usr/sbin/iptables': "service iptables status|grep 'Chain INPUT'"}
|
||
for f in firewall_files.keys():
|
||
if not os.path.exists(f): continue
|
||
_cmd = firewall_files[f]
|
||
if _cmd != "pid":
|
||
res = ExecShell(_cmd)
|
||
if res[0].strip():
|
||
return 1
|
||
else:
|
||
return 0
|
||
for pid in psutil.pids():
|
||
try:
|
||
p = psutil.Process(pid)
|
||
if f in p.cmdline():
|
||
return 1
|
||
except:
|
||
pass
|
||
return 0
|
||
return -1
|
||
|
||
|
||
def get_panel_port():
|
||
'''
|
||
@name 获取面板端口
|
||
@author hwliang
|
||
@return int
|
||
'''
|
||
port_file = '{}/data/port.pl'.format(get_panel_path())
|
||
if not os.path.exists(port_file):
|
||
return 8888
|
||
try:
|
||
return int(readFile(port_file))
|
||
except:
|
||
return 8888
|
||
|
||
|
||
def install_sys_firewall():
|
||
'''
|
||
@name 安装系统防火墙
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
if get_firewall_status() != -1: return True
|
||
|
||
install_bin = get_sys_install_bin()
|
||
if not install_bin: return False
|
||
if install_bin.find('apt-get') != -1:
|
||
ExecShell("{} install -y ufw".format(install_bin))
|
||
if get_firewall_status() != -1:
|
||
_cmd = '''ufw allow 20/tcp
|
||
ufw allow 21/tcp
|
||
ufw allow 22/tcp
|
||
ufw allow 80/tcp
|
||
ufw allow 443/tcp
|
||
ufw allow ${panelPort}/tcp
|
||
ufw allow ${sshPort}/tcp
|
||
ufw allow 39000:40000/tcp
|
||
ufw_status=`ufw status`
|
||
echo y|ufw enable
|
||
ufw default deny
|
||
ufw reload
|
||
'''.format(panelPort=get_panel_port(), sshPort=get_ssh_port())
|
||
ExecShell(_cmd)
|
||
elif install_bin.find('yum') != -1 or install_bin.find('dnf') != -1:
|
||
ExecShell("{} install -y firewalld".format(install_bin))
|
||
if get_firewall_status() != -1:
|
||
_cmd = '''systemctl enable firewalld
|
||
systemctl start firewalld
|
||
firewall-cmd --set-default-zone=public > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port=20/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port=21/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port=22/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port=80/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port=443/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port={panelPort}/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port={sshPort}/tcp > /dev/null 2>&1
|
||
firewall-cmd --permanent --zone=public --add-port=39000-40000/tcp > /dev/null 2>&1
|
||
firewall-cmd --reload
|
||
'''.format(panelPort=get_panel_port(), sshPort=get_ssh_port())
|
||
ExecShell(_cmd)
|
||
|
||
return False
|
||
|
||
|
||
def check_firewall_rule(port):
|
||
'''
|
||
@name 检测防火墙是否已经添加规则
|
||
@author cjxin
|
||
@param port int 端口号
|
||
'''
|
||
|
||
args = dict_obj()
|
||
args.model_index = 'safe'
|
||
args.port = port
|
||
|
||
import PluginLoader
|
||
res = PluginLoader.module_run("firewall", "check_firewall_rule", args)
|
||
return res
|
||
|
||
|
||
def add_firewall_rule(port, protocol='tcp', types='accept', address='0.0.0.0/0', brief=None):
|
||
"""
|
||
@name 添加防火墙规则
|
||
@author cjxin
|
||
@param port int 端口号
|
||
@param protocol string 协议类型 tcp udp
|
||
@param types string 添加类型 accept reject
|
||
@param address string 地址
|
||
@param brief string 描述
|
||
"""
|
||
|
||
args = dict_obj()
|
||
args.model_index = 'safe'
|
||
args.port = port
|
||
args.protocol = protocol
|
||
args.types = types
|
||
args.address = address
|
||
args.brief = brief
|
||
if not brief: args.brief = str(port)
|
||
|
||
import PluginLoader
|
||
res = PluginLoader.module_run("firewall", "create_rules", args)
|
||
return res
|
||
|
||
|
||
def del_firewall_rule(port, protocol='tcp', types='accept', address='0.0.0.0/0'):
|
||
'''
|
||
@name 删除防火墙规则
|
||
@author cjxin
|
||
@param port int 端口号
|
||
@param protocol str 协议
|
||
@param types str 类型
|
||
@param address str 地址
|
||
'''
|
||
|
||
args = dict_obj()
|
||
args.model_index = 'safe'
|
||
args.port = port
|
||
args.protocol = protocol
|
||
args.types = types
|
||
args.address = address
|
||
import PluginLoader
|
||
res = PluginLoader.module_run("firewall", "remove_rules", args)
|
||
return res
|
||
|
||
|
||
def is_aarch():
|
||
'''
|
||
@name 是否是arm架构
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
uname = None
|
||
if hasattr(os, 'uname'): uname = os.uname()
|
||
aarch_list = ['aarch64', 'aarch']
|
||
try:
|
||
return uname.machine in aarch_list
|
||
except:
|
||
if uname:
|
||
return uname[-1] in aarch_list
|
||
return False
|
||
|
||
|
||
def is_process_exists_by_cmdline(_cmd):
|
||
'''
|
||
@name 根据命令行参数查找进程是否存在
|
||
@author hwliang
|
||
@param _cmd 命令行
|
||
@return bool
|
||
'''
|
||
if isinstance(_cmd, str):
|
||
_cmd = [_cmd]
|
||
if not isinstance(_cmd, list):
|
||
return False
|
||
for pid in psutil.pids():
|
||
try:
|
||
p = psutil.Process(pid)
|
||
cmd_line = p.cmdline()
|
||
for _c in _cmd:
|
||
if _c in cmd_line:
|
||
return True
|
||
except:
|
||
continue
|
||
return False
|
||
|
||
|
||
def is_process_exists_by_exe(_exe):
|
||
'''
|
||
@name 根据执行文件路径查找进程是否存在
|
||
@author hwliang
|
||
@param _exe 命令行
|
||
@return bool
|
||
'''
|
||
if isinstance(_exe, str):
|
||
_exe = [_exe]
|
||
if not isinstance(_exe, list):
|
||
return False
|
||
|
||
for process in psutil.process_iter():
|
||
try:
|
||
_exe_bin = process.exe()
|
||
for _e in _exe:
|
||
if _exe_bin.find(_e) != -1: return True
|
||
except:
|
||
continue
|
||
return False
|
||
|
||
|
||
def is_process_exists_by_name(_name):
|
||
'''
|
||
@name 根据进程名查找进程是否存在
|
||
@author hwliang
|
||
@param _name 命令行
|
||
@return bool
|
||
'''
|
||
if isinstance(_name, str):
|
||
_name = [_name]
|
||
if not isinstance(_name, list):
|
||
return False
|
||
for pid in psutil.pids():
|
||
try:
|
||
p = psutil.Process(pid)
|
||
name = p.name()
|
||
for _n in _name:
|
||
if name == _n: return True
|
||
except:
|
||
continue
|
||
return False
|
||
|
||
|
||
def is_mysql_process_exists():
|
||
'''
|
||
@name 检查mysql进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
_exe = ['server/mysql/bin/mysqld_safe', 'server/mysql/bin/mariadbd', 'server/mysql/bin/mysqld']
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def is_redis_process_exists():
|
||
'''
|
||
@name 检查redis进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
_exe = ['server/redis/src/redis-server']
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def is_pure_ftpd_process_exists():
|
||
'''
|
||
@name 检查pure-ftpd进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
_exe = ['server/pure-ftpd/sbin/pure-ftpd']
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def is_php_fpm_process_exists(name):
|
||
'''
|
||
@name 检查php-fpm进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
_php_version = name.split('-')[-1]
|
||
_exe = ['server/php/{}/sbin/php-fpm'.format(_php_version)]
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def is_nginx_process_exists():
|
||
'''
|
||
@name 检查nginx进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
_exe = ('server/nginx/sbin/nginx', 'server/nginx/nginx/sbin/nginx')
|
||
for i in _exe:
|
||
result = is_process_exists_by_exe(i)
|
||
if result: return result
|
||
return False
|
||
|
||
|
||
def is_httpd_process_exists():
|
||
'''
|
||
@name 检查httpd进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
import time
|
||
_exe = ['server/apache/bin/httpd']
|
||
time.sleep(1)
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def is_memcached_process_exists():
|
||
'''
|
||
@name 检查memcached进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
import time
|
||
_exe = ['/usr/local/memcached/bin/memcached']
|
||
time.sleep(1)
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def is_mongodb_process_exists():
|
||
'''
|
||
@name 检查mongodb进程是否存在
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
_exe = ['server/mongodb/bin/mongod']
|
||
return is_process_exists_by_exe(_exe)
|
||
|
||
|
||
def check_auth_ip():
|
||
"""
|
||
@name 检测api和www的服务器ip是否一致
|
||
@auther cjxin 2022-09-13
|
||
@return bool
|
||
"""
|
||
result = {'www': '', 'api': ''}
|
||
if is_self_hosted():
|
||
try:
|
||
ip = GetLocalIp()
|
||
except:
|
||
ip = '127.0.0.1'
|
||
result['www'] = ip
|
||
result['api'] = ip
|
||
return result
|
||
import http_requests
|
||
res = http_requests.post('https://wafapi2.yakpanel.com/api/getIpAddress', data={}, timeout=5, headers={})
|
||
if res.status_code == 200:
|
||
result['www'] = res.text
|
||
|
||
res1 = http_requests.post('https://wafapi.yakpanel.com/api/getIpAddress', data={}, timeout=5, headers={})
|
||
if res1.status_code == 200:
|
||
result['api'] = res1.text
|
||
|
||
return result
|
||
|
||
|
||
def set_func(key, count=0):
|
||
"""
|
||
设置指定key的操作时间
|
||
@key 面板访问函数
|
||
"""
|
||
path = 'data/func.json'
|
||
data = {}
|
||
try:
|
||
data = json.loads(readFile(path))
|
||
except:
|
||
pass
|
||
if not key in data:
|
||
data[key] = {}
|
||
data[key]['time'] = 0
|
||
data[key]['count'] = 0
|
||
data[key]['time'] = int(time.time())
|
||
|
||
if count > 0:
|
||
data[key]['count'] = count
|
||
else:
|
||
data[key]['count'] += 1
|
||
writeFile(path, json.dumps(data))
|
||
|
||
|
||
def get_func(key):
|
||
"""
|
||
获取指定功能的操作时间
|
||
@key 面板函数
|
||
"""
|
||
path = 'data/func.json'
|
||
ret = {}
|
||
ret['count'] = 0
|
||
ret['time'] = 0
|
||
if not os.path.exists(path): return ret
|
||
data = {}
|
||
try:
|
||
data = json.loads(readFile(path))
|
||
except:
|
||
pass
|
||
if not key in data: return ret
|
||
return data[key]
|
||
|
||
|
||
def set_cache_func(key, info):
|
||
"""
|
||
设置指定key的操作时间
|
||
@key 缓存的key函数
|
||
"""
|
||
data = {}
|
||
path = '{}/data/cache_func.json'.format(get_panel_path())
|
||
|
||
try:
|
||
data = json.loads(readFile(path))
|
||
except:
|
||
pass
|
||
if not key in data: data[key] = {}
|
||
|
||
data[key]['time'] = int(time.time())
|
||
data[key]['data'] = info
|
||
|
||
writeFile(path, json.dumps(data))
|
||
|
||
|
||
def get_cache_func(key):
|
||
"""
|
||
获取指定功能的操作时间
|
||
@key 缓存的key函数
|
||
"""
|
||
|
||
ret = {}
|
||
data = {}
|
||
ret['data'] = ''
|
||
ret['time'] = 0
|
||
path = '{}/data/cache_func.json'.format(get_panel_path())
|
||
if not os.path.exists(path):
|
||
return ret
|
||
try:
|
||
data = json.loads(readFile(path))
|
||
except:
|
||
pass
|
||
if not key in data: return ret
|
||
return data[key]
|
||
|
||
|
||
def set_split_logs(path, status=1, info=None):
|
||
"""
|
||
@name 添加日志切割
|
||
@path 日志路径,
|
||
@data dict
|
||
{
|
||
'type':'day/size'
|
||
'limit': 180,保留份数
|
||
'size': 日志超过多少进行切割,type=size时生效
|
||
'callback': 回调命令,部分日志切割后需要重启服务
|
||
}
|
||
"""
|
||
|
||
data = {}
|
||
sfile = '{}/data/cutting_log.json'.format(get_panel_path())
|
||
if os.path.exists(sfile):
|
||
try:
|
||
data = json.loads(readFile(sfile))
|
||
except:
|
||
pass
|
||
|
||
if path in data:
|
||
del data[path]
|
||
|
||
if status:
|
||
if not info:
|
||
return False
|
||
if not 'type' in info or not 'limit' in info:
|
||
return False
|
||
data[path] = info
|
||
writeFile(sfile, json.dumps(data))
|
||
|
||
# 计划任务切割
|
||
echo = md5(md5('set_split_logs'))
|
||
find = M('crontab').where('echo=?', (echo,)).find()
|
||
|
||
try:
|
||
import crontab
|
||
args_obj = dict_obj()
|
||
|
||
if not find:
|
||
cronPath = GetConfigValue('setup_path') + '/cron/' + echo
|
||
shell = '{} -u /www/server/panel/script/logSplit.py'.format(sys.executable)
|
||
writeFile(cronPath, shell)
|
||
|
||
args_obj.id = M('crontab').add(
|
||
'name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sType,sName,sBody,urladdress',
|
||
("[删除]切割日志文件", 'minute-n', '10', '0', '0', echo, time.strftime('%Y-%m-%d %X', time.localtime()), 0, '',
|
||
'localhost', 'toShell', '', shell, ''))
|
||
crontab.crontab().set_cron_status(args_obj)
|
||
else:
|
||
cron_path = get_cron_path()
|
||
if os.path.exists(cron_path):
|
||
cron_s = readFile(cron_path)
|
||
if cron_s.find(echo) == -1:
|
||
M('crontab').where('echo=?', (echo,)).setField('status', 0)
|
||
args_obj.id = find['id']
|
||
crontab.crontab().set_cron_status(args_obj)
|
||
return True
|
||
except:
|
||
pass
|
||
|
||
return False
|
||
|
||
|
||
def get_admin_path():
|
||
'''
|
||
@name 取安全入口
|
||
@author hwliang
|
||
@return string
|
||
'''
|
||
login_path = '/login'
|
||
path = '{}/data/admin_path.pl'.format(get_panel_path())
|
||
if not os.path.exists(path): return login_path
|
||
admin_path = readFile(path)
|
||
if not admin_path: return login_path
|
||
admin_path = admin_path.strip()
|
||
if admin_path in ['', '/']:
|
||
return login_path
|
||
if admin_path[-1] == '/': admin_path = admin_path[:-1]
|
||
return admin_path
|
||
|
||
|
||
def get_improvement():
|
||
'''
|
||
@name 获取用户体验改进计划状态
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
tip_file = '{}/data/improvement.pl'.format(get_panel_path())
|
||
tip_file_set = '{}/data/is_set_improvement.pl'.format(get_panel_path())
|
||
if not os.path.exists(tip_file_set):
|
||
return True
|
||
return os.path.exists(tip_file)
|
||
|
||
|
||
def is_spider():
|
||
'''
|
||
@name 判断是否为爬虫
|
||
@return bool
|
||
'''
|
||
from YakPanel import request
|
||
import panelDefense
|
||
p = panelDefense.bot_safe()
|
||
return not p.spider(request.headers.get('User-Agent'), request.remote_addr)
|
||
|
||
|
||
# def get_rsa_public_key_file():
|
||
# '''
|
||
# @name 获取RSA公钥文件路径
|
||
# @author hwliang
|
||
# @return str
|
||
# '''
|
||
# return '{}/data/rsa_public_key.pem'.format(get_panel_path())
|
||
|
||
# def get_rsa_private_key_file():
|
||
# '''
|
||
# @name 获取RSA私钥文件路径
|
||
# @author hwliang
|
||
# @return str
|
||
# '''
|
||
# return '{}/data/rsa_private_key.pem'.format(get_panel_path())
|
||
|
||
|
||
def get_rsa_public_key():
|
||
'''
|
||
@name 获取RSA公钥内容
|
||
@author hwliang
|
||
@return str
|
||
'''
|
||
from YakPanel import session
|
||
pub_key = 'rsa_public_key'
|
||
public_key = session.get(pub_key)
|
||
if not public_key:
|
||
create_rsa_key()
|
||
public_key = session.get(pub_key)
|
||
return public_key
|
||
|
||
# path = get_rsa_public_key_file()
|
||
# if not os.path.exists(path): create_rsa_key()
|
||
# if not os.path.exists(path): return ''
|
||
# return readFile(path)
|
||
|
||
|
||
def get_rsa_private_key():
|
||
'''
|
||
@name 获取RSA私钥内容
|
||
@author hwliang
|
||
@return str
|
||
'''
|
||
from YakPanel import session
|
||
prv_key = 'rsa_private_key'
|
||
private_key = session.get(prv_key)
|
||
if not private_key:
|
||
create_rsa_key()
|
||
private_key = session.get(prv_key)
|
||
return private_key
|
||
|
||
|
||
def create_rsa_key():
|
||
'''
|
||
@name 创建RSA密钥
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
try:
|
||
# private_key_file = get_rsa_private_key_file()
|
||
# public_key_file = get_rsa_public_key_file()
|
||
# if os.path.exists(private_key_file) and os.path.exists(public_key_file): return True
|
||
from YakPanel import session
|
||
pub_key = 'rsa_public_key'
|
||
prv_key = 'rsa_private_key'
|
||
if pub_key in session and prv_key in session:
|
||
return True
|
||
try:
|
||
from Crypto.PublicKey import RSA
|
||
key = RSA.generate(1024)
|
||
private_key = key.exportKey("PEM")
|
||
public_key = key.publickey().exportKey("PEM")
|
||
except:
|
||
is_re_install = '{}/data/pycryptodome_re_install.pl'.format(get_panel_path())
|
||
if not os.path.exists(is_re_install):
|
||
os.system("nohup btpip install pycryptodome -I &> /dev/null &")
|
||
writeFile(is_re_install, 'True')
|
||
|
||
priv_pem = '/tmp/private.pem'
|
||
pub_pem = '/tmp/public.pem'
|
||
ExecShell("openssl genrsa -out {} 1024".format(priv_pem))
|
||
ExecShell("openssl rsa -pubout -in {} -out {}".format(priv_pem, pub_pem))
|
||
if not os.path.exists(priv_pem) or not os.path.exists(pub_pem):
|
||
return False
|
||
|
||
private_key = readFile(priv_pem, 'rb')
|
||
public_key = readFile(pub_pem, 'rb')
|
||
|
||
if os.path.exists(priv_pem): os.remove(priv_pem)
|
||
if os.path.exists(pub_pem): os.remove(pub_pem)
|
||
|
||
session[pub_key] = public_key.decode('utf-8').replace("\n", "")
|
||
session[prv_key] = private_key.decode('utf-8')
|
||
|
||
# writeFile(private_key_file,private_key,'wb+')
|
||
# writeFile(public_key_file,public_key,'wb+')
|
||
return True
|
||
except:
|
||
print_log(get_error_info())
|
||
return False
|
||
|
||
|
||
def rsa_encrypt(data):
|
||
'''
|
||
@name RSA加密数据
|
||
@param data str 要加密的数据
|
||
@return str
|
||
'''
|
||
# 分片长度 1024 / 8 - 11 = 117
|
||
split_length = 117
|
||
try:
|
||
from Crypto.PublicKey import RSA
|
||
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs
|
||
|
||
# 初始化RSA加密对象
|
||
public_key = get_rsa_public_key()
|
||
cipher_public = Cipher_pkcs.new(RSA.importKey(public_key))
|
||
|
||
# 分片加密
|
||
data = data.encode('utf-8')
|
||
encrypted_arr = []
|
||
for i in range(0, len(data), split_length):
|
||
d = data[i:i + split_length]
|
||
encrypted_data = cipher_public.encrypt(d)
|
||
encrypted_base64 = base64.b64encode(encrypted_data).decode()
|
||
encrypted_arr.append(encrypted_base64)
|
||
|
||
# 用换行符拼接
|
||
return "\n".join(encrypted_arr)
|
||
except:
|
||
print_log(get_error_info())
|
||
return ''
|
||
|
||
|
||
def rsa_decrypt(data):
|
||
'''
|
||
@name RSA解密数据
|
||
@param data str 要解密的数据
|
||
@return str
|
||
'''
|
||
try:
|
||
from Crypto.PublicKey import RSA
|
||
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs
|
||
|
||
# 初始化RSA解密对象
|
||
private_key = get_rsa_private_key()
|
||
cipher_private = Cipher_pkcs.new(RSA.importKey(private_key))
|
||
|
||
# 分片解密
|
||
decrypted_str = b""
|
||
for d in data.split("\n"):
|
||
if not d: continue
|
||
res = base64.b64decode(d)
|
||
if not res: continue
|
||
decrypted_data = cipher_private.decrypt(res, None)
|
||
decrypted_str += decrypted_data
|
||
return decrypted_str.decode('utf-8')
|
||
except:
|
||
print_log(get_error_info())
|
||
return ''
|
||
|
||
|
||
def rsa_encrypt_for_private_key(data):
|
||
'''
|
||
@name RSA私钥加密数据
|
||
@author hwliang
|
||
@param data str 要加密的数据
|
||
@return str
|
||
'''
|
||
# 分片长度 1024 / 8 - 11 = 117
|
||
split_length = 117
|
||
try:
|
||
from Crypto.PublicKey import RSA
|
||
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs
|
||
|
||
# 初始化RSA加密对象
|
||
private_key = get_rsa_private_key()
|
||
cipher_private = Cipher_pkcs.new(RSA.importKey(private_key))
|
||
|
||
# 分片加密
|
||
data = data.encode('utf-8')
|
||
encrypted_arr = []
|
||
for i in range(0, len(data), split_length):
|
||
d = data[i:i + split_length]
|
||
encrypted_data = cipher_private.encrypt(d)
|
||
encrypted_base64 = base64.b64encode(encrypted_data).decode()
|
||
encrypted_arr.append(encrypted_base64)
|
||
|
||
# 用换行符拼接
|
||
return "\n".join(encrypted_arr)
|
||
except:
|
||
print_log(get_error_info())
|
||
return ''
|
||
|
||
|
||
def get_client_hash():
|
||
'''
|
||
@name 获取客户端HASH
|
||
@author hwliang
|
||
@return str
|
||
'''
|
||
from flask import session, request
|
||
is_tmp_login = session.get('tmp_login')
|
||
if is_tmp_login:
|
||
client_hash = md5(request.remote_addr)
|
||
else:
|
||
|
||
# TODO: 关闭唯一IP哈希,暂时注释
|
||
# skey = 'client_ips'
|
||
# ckey = 'client_sync_count'
|
||
# client_ips = session.get(skey, [])
|
||
# client_sync_count = session.get(ckey, 0)
|
||
#
|
||
#
|
||
# # 是否唯一IP
|
||
# if len(client_ips) <= 1:
|
||
# # 唯一IP连续访问次数超过100次,使用IP+UA生成HASH
|
||
# r_max = 101
|
||
# if client_sync_count >= r_max - 1:
|
||
# client_hash = md5(request.remote_addr)
|
||
# if client_sync_count < r_max:
|
||
# session['client_hash'] = client_hash
|
||
# client_sync_count += 1
|
||
# session[ckey] = client_sync_count
|
||
# return client_hash
|
||
#
|
||
# # 记录IP
|
||
# if not request.remote_addr in client_ips:
|
||
# client_ips.append(request.remote_addr)
|
||
# session[skey] = client_ips
|
||
#
|
||
# # 记录访问次数
|
||
# client_sync_count += 1
|
||
# session[ckey] = client_sync_count
|
||
|
||
# 非唯一IP,使用UA生成HASH
|
||
client_hash = md5('')
|
||
|
||
return client_hash
|
||
|
||
|
||
def check_client_hash():
|
||
'''
|
||
@name 验证客户端HASH
|
||
@author hwliang
|
||
@return bool
|
||
'''
|
||
# 是否关闭验证
|
||
not_tip = '{}/data/not_check_ip.pl'.format(get_panel_path())
|
||
if os.path.exists(not_tip): return True
|
||
from YakPanel import session, request
|
||
# 如果未开启SSL,不验证
|
||
if request.scheme == 'https': return True
|
||
skey = 'client_hash'
|
||
client_hash = get_client_hash()
|
||
if not skey in session:
|
||
session[skey] = client_hash
|
||
return True
|
||
|
||
if session[skey] != client_hash:
|
||
WriteLog('User login', 'Client HASH verification failed, has been forced to log out!')
|
||
return False
|
||
return True
|
||
|
||
|
||
def shell_quote(cmd):
|
||
'''
|
||
@name shell转义
|
||
@author hwliang
|
||
@param cmd str 要转义的命令
|
||
@return str
|
||
'''
|
||
if not cmd: return ''
|
||
if isinstance(cmd, bytes): cmd = cmd.decode('utf-8')
|
||
if not isinstance(cmd, str): return cmd
|
||
try:
|
||
import shlex
|
||
return shlex.quote(cmd)
|
||
except:
|
||
try:
|
||
import pipes
|
||
return pipes.quote(cmd)
|
||
except:
|
||
return cmd
|
||
|
||
|
||
def get_div(div):
|
||
sql = M('sqlite_master')
|
||
if not sql.where('type=? AND name=? AND sql LIKE ?', ('table', 'div_list', '%div%')).count():
|
||
sql_str = '''CREATE TABLE IF NOT EXISTS `div_list` (
|
||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
`div` TEXT
|
||
)'''
|
||
sql.execute(sql_str)
|
||
my_div = sql.table('div_list').where('id=1', ()).getField('div')
|
||
if not my_div:
|
||
sql.table('div_list').insert({'div': div})
|
||
my_div = div
|
||
return my_div
|
||
|
||
|
||
def set_tasks_run(data):
|
||
'''
|
||
@name 设置运行时间
|
||
@param data dict 数据
|
||
@param data.type int 类型 1:面板 2:插件
|
||
@param data.time int 执行时间(必传)
|
||
@param data.name str 插件名称、模块名称(必传)
|
||
@param data.title str 插件中文名(必传)
|
||
@param data.fun str 执行方法(必传)
|
||
@param data.args dict 参数
|
||
|
||
'''
|
||
spath = '{}/data/tasks'.format(get_panel_path())
|
||
if not os.path.exists(spath): os.makedirs(spath, 384)
|
||
|
||
task_file = '{}/{}'.format(spath, md5(str(time.time())))
|
||
writeFile(task_file, json.dumps(data))
|
||
return returnMsg(True, task_file)
|
||
|
||
|
||
def Get_ip_info(get_speed=False, get_user=True):
|
||
'''
|
||
获取bt官网ip归属地列表
|
||
@author wzz <wzz@yakpanel.com>
|
||
@return: list[dict{}]
|
||
'''
|
||
if is_self_hosted():
|
||
try:
|
||
ip = GetLocalIp()
|
||
except:
|
||
ip = '127.0.0.1'
|
||
return [{
|
||
'continent': '', 'country': '', 'province': '', 'city': '', 'region': '', 'carrier': '',
|
||
'division': '', 'en_country': '', 'en_short_code': '', 'longitude': '', 'latitude': '',
|
||
'info': '本服务器公网IP归属地信息', 'ip': ip, 'level': 0,
|
||
}]
|
||
host_list = json.loads(readFile("config/hosts_dict.json"))
|
||
print("host_list: ", host_list)
|
||
|
||
# 推荐,一般,较差,不推荐,不测速时,ipv6,用户服务器IP
|
||
level = (1, 2, 3, 4, 5, 6, 0)
|
||
user_server_ipaddress = []
|
||
if get_user:
|
||
user_server_ipaddress = get_user_server_ipaddress(host_list, level)
|
||
bt_host = get_bt_hosts(get_speed, host_list, level)
|
||
ips_result = user_server_ipaddress + bt_host
|
||
if ips_result: return ips_result
|
||
|
||
|
||
def get_user_server_ipaddress(host_list, level):
|
||
'''
|
||
获取服务器公网ip归属地信息
|
||
@param host_list: host列表
|
||
@param level: 等级元组
|
||
@return:
|
||
'''
|
||
ips_result = []
|
||
headers = {"host": "www.yakpanel.com"}
|
||
for host in host_list:
|
||
try:
|
||
new_url = "https://{}/Api/getIpAddress".format(host["ip"])
|
||
m_str = HttpGet(new_url, 1, headers=headers)
|
||
ipaddress = re.search(r"^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$", m_str).group(0)
|
||
s_ip_info = get_free_ip_info("{}".format(ipaddress))
|
||
if "ip" in s_ip_info.keys():
|
||
s_ip_info["info"] = "本服务器公网IP归属地信息"
|
||
s_ip_info["level"] = level[-1]
|
||
ips_result.append(s_ip_info)
|
||
return ips_result
|
||
except:
|
||
continue
|
||
if not ips_result:
|
||
return [{'continent': '', 'country': '未知地区', 'province': '', 'city': '', 'region': '', 'carrier': '',
|
||
'division': '', 'en_country': '', 'en_short_code': '', 'longitude': '',
|
||
'latitude': '', 'info': '本服务器公网IP归属地信息', 'ip': GetLocalIp(), 'level': 0}]
|
||
|
||
|
||
def get_bt_hosts(get_speed, host_list, level):
|
||
'''
|
||
获取bt官网ip归属地列表
|
||
@param get_speed: 是否测速
|
||
@param host_list: 传host列表
|
||
@param level: 传等级元组
|
||
@return:
|
||
'''
|
||
ips_result = []
|
||
|
||
for ip in host_list:
|
||
ipv6 = {
|
||
"continent": "",
|
||
"country": "",
|
||
"province": "",
|
||
"city": "ipv6 地址",
|
||
"region": "",
|
||
"carrier": "",
|
||
"division": "",
|
||
"en_country": "",
|
||
"en_short_code": "",
|
||
"longitude": "",
|
||
"latitude": "",
|
||
"info": "该节点为ipv6地址,若服务器无ipv6请勿选择!",
|
||
"ip": "",
|
||
"level": None
|
||
}
|
||
try:
|
||
# 获取节点响应延迟
|
||
if get_speed: n_net, n_ping = get_timeout("https://{}".format(ip["ip"]) + ':80/net_test', 1)
|
||
if not is_ipv4(ip["ip"]):
|
||
ipv6['ip'] = ip["ip"]
|
||
# ipv6地址默认一般推荐
|
||
ipv6['level'] = level[-2]
|
||
if get_speed: ipv6['speed'] = ""
|
||
ips_result.append(ipv6)
|
||
continue
|
||
ip_result = get_free_ip_info("{}".format(ip["ip"]))
|
||
if "ip" in ip_result.keys():
|
||
ip_result['level'] = level[-3]
|
||
if get_speed:
|
||
if int(n_ping) < 100: ip_result['level'] = level[0]
|
||
if 100 < int(n_ping) < 500: ip_result['level'] = level[1]
|
||
if int(n_ping) > 500: ip_result['level'] = level[2]
|
||
ip_result["speed"] = n_ping + 500
|
||
ips_result.append(ip_result)
|
||
continue
|
||
if "info" in ip_result.keys():
|
||
if ip_result["info"] == "未知归属地":
|
||
ipv6['ip'] = ip["ip"]
|
||
ipv6["city"] = ip["area"]
|
||
ipv6['level'] = level[1]
|
||
if get_speed: ipv6['speed'] = ""
|
||
ipv6["info"] = "节点无法测速,请选择离您服务器最近的尝试!"
|
||
ips_result.append(ipv6)
|
||
except:
|
||
continue
|
||
if len(ips_result) < 2 and ips_result[-1]["city"] == "ipv6 地址": ips_result.pop(-1)
|
||
return ips_result
|
||
|
||
|
||
def set_home_host2(host):
|
||
"""
|
||
@name 设置官网hosts
|
||
@author wzz<wzz@yakpanel.com>
|
||
@param host IP地址
|
||
@return void
|
||
"""
|
||
msg = "请尝试点击【清理旧节点】,如果仍然不行,请联系 YakPanel 支持: https://www.yakpanel.com"
|
||
www_set = ExecShell("echo \"{} www.yakpanel.com\" >> /etc/hosts".format(host))
|
||
api_set = ExecShell("echo \"{} api.yakpanel.com\" >> /etc/hosts".format(host))
|
||
if not www_set[1] and not api_set[1]: return returnMsg(True, "节点设置成功")
|
||
if www_set[1]: return returnMsg(False, "节点设置失败: {}, {}".format(www_set[1], msg))
|
||
if api_set[1]: return returnMsg(False, "节点设置失败: {}, {}".format(api_set[1], msg))
|
||
return returnMsg(False, "节点设置失败: {}".format(msg))
|
||
|
||
|
||
def Clean_bt_host():
|
||
'''
|
||
清理面板官网节点写入的 hosts 记录(历史 capnis.com 或当前 yakpanel.com)
|
||
@author wzz <wzz@yakpanel.com>
|
||
@return:
|
||
'''
|
||
check_hosts = ExecShell("grep -E \"(bt\\\\.cn|www\\\\.yakpanel\\\\.com|api\\\\.yakpanel\\\\.com)\" /etc/hosts")
|
||
if check_hosts[0]:
|
||
result = ExecShell("sed -i \"/bt\\.cn/d\" /etc/hosts && sed -i \"/www\\.yakpanel\\.com/d\" /etc/hosts && sed -i \"/api\\.yakpanel\\.com/d\" /etc/hosts")
|
||
if result[1]: return returnMsg(False, "旧节点清理失败: {}".format(result[1]))
|
||
return returnMsg(True, "旧节点已清理")
|
||
return returnMsg(True, "hosts没有绑定旧节点无需清理")
|
||
|
||
|
||
def Set_bt_host(ip=None):
|
||
'''
|
||
设置bt官网(www && api)指定hosts节点
|
||
@author wzz <wzz@yakpanel.com>
|
||
@param get: 手动设置 get.ip 官网传ip地址,从public.Get_ip_info方法获取 | 自动设置
|
||
@return:
|
||
'''
|
||
Clean_bt_host()
|
||
|
||
if ip: return set_home_host2(ip)
|
||
# 如果不传ip则自动设置
|
||
ips_info = Get_ip_info(get_user=False)
|
||
headers = {"host": "www.yakpanel.com"}
|
||
for host in ips_info:
|
||
new_url = "https://{}".format(host['ip'])
|
||
res = HttpGet(new_url, 1, headers=headers)
|
||
if res:
|
||
writeFile("{}/data/home_host.pl".format(get_panel_path()), host["ip"])
|
||
result = set_home_host2(host["ip"])
|
||
if result["status"]:
|
||
return returnMsg(True, "已自动选择为{}{}的最优节点,运营商是: {}"
|
||
.format(host['province'], host['city'], host['carrier']))
|
||
return returnMsg(False, "自动选择节点失败,请尝试手动设置")
|
||
|
||
|
||
def set_ownership(directory, user):
|
||
'''
|
||
设置指定目录及目录下所有文件、子目录所属为user
|
||
@param directory:
|
||
@param user:
|
||
@return:
|
||
'''
|
||
import pwd
|
||
uid = pwd.getpwnam(user).pw_uid
|
||
gid = pwd.getpwnam(user).pw_gid
|
||
|
||
os.chown(directory, uid, gid)
|
||
|
||
for root, dirs, files in os.walk(directory):
|
||
for d in dirs:
|
||
dir_path = os.path.join(root, d)
|
||
os.chown(dir_path, uid, gid)
|
||
for f in files:
|
||
file_path = os.path.join(root, f)
|
||
os.chown(file_path, uid, gid)
|
||
|
||
|
||
def set_permissions(directory, permissions):
|
||
'''
|
||
设置指定目录及目录下所有文件、子目录权限为permissions
|
||
@param directory:
|
||
@param permissions: 传八进制,如0o755
|
||
@return:
|
||
'''
|
||
os.chmod(directory, permissions)
|
||
|
||
for root, dirs, files in os.walk(directory):
|
||
for d in dirs:
|
||
dir_path = os.path.join(root, d)
|
||
os.chmod(dir_path, permissions)
|
||
for f in files:
|
||
file_path = os.path.join(root, f)
|
||
os.chmod(file_path, permissions)
|
||
|
||
|
||
def check_ssl_verify(certPath='ssl/ca.pem'):
|
||
'''
|
||
校验面板设置SSL双向认证证书格式
|
||
@param certPath:
|
||
@return:
|
||
'''
|
||
if "crl.pem" in certPath:
|
||
certKey = readFile(certPath)
|
||
if "-----BEGIN X509 CRL-----" not in certKey:
|
||
return False
|
||
return True
|
||
|
||
res = False
|
||
openssl = '/usr/local/openssl/bin/openssl'
|
||
if not os.path.exists(openssl): openssl = 'openssl'
|
||
certPem = readFile(certPath)
|
||
if "-----BEGIN CERTIFICATE-----" in certPem and "Certificate" in certPem:
|
||
result = ExecShell(openssl + " x509 -in " + certPath + " -noout -subject")
|
||
res = True
|
||
if len(result[1]) > 2: res = False
|
||
if result[0].find('error:') != -1: res = False
|
||
return res
|
||
|
||
|
||
def is_write_file():
|
||
'''
|
||
@name 测试是否能写入文件
|
||
@return void
|
||
'''
|
||
test_file = '/etc/init.d/bt_10000100.pl'
|
||
writeFile(test_file, 'True')
|
||
if os.path.exists(test_file):
|
||
if readFile(test_file) == 'True':
|
||
os.remove(test_file)
|
||
return True
|
||
os.remove(test_file)
|
||
return False
|
||
|
||
|
||
def stop_syssafe():
|
||
'''
|
||
@name 临时停用系统加固
|
||
@return bool
|
||
'''
|
||
# 检测是否可写
|
||
ret = is_write_file()
|
||
is_stop_syssafe_file = '{}/data/is_stop_syssafe.pl'.format(get_panel_path())
|
||
# 如果不可写,则尝试停用系统加固
|
||
if not ret:
|
||
syssafe_path = get_plugin_path('syssafe')
|
||
if os.path.exists(syssafe_path):
|
||
writeFile(is_stop_syssafe_file, 'True')
|
||
ExecShell("/etc/init.d/bt_syssafe stop")
|
||
|
||
# 停用系统加固后再检测一次
|
||
ret = is_write_file()
|
||
return ret
|
||
|
||
return ret
|
||
|
||
|
||
def start_syssafe():
|
||
'''
|
||
@name 恢复系统加固的运行状态
|
||
@return void
|
||
'''
|
||
is_stop_syssafe_file = '{}/data/is_stop_syssafe.pl'.format(get_panel_path())
|
||
if os.path.exists(is_stop_syssafe_file):
|
||
ExecShell("/etc/init.d/bt_syssafe start")
|
||
if os.path.exists(is_stop_syssafe_file):
|
||
os.remove(is_stop_syssafe_file)
|
||
|
||
|
||
def check_sys_write():
|
||
'''
|
||
@name 检查关键目录是否可写
|
||
@return bool
|
||
'''
|
||
return stop_syssafe()
|
||
|
||
|
||
def get_root_domain(domain_name):
|
||
'''
|
||
@name 根据域名查询根域名和记录值
|
||
@author cjxin<2020-12-17>
|
||
@param domain {string} 被验证的根域名
|
||
@return void
|
||
'''
|
||
top_domain_list = ['.ac.cn', '.ah.cn', '.bj.cn', '.com.cn', '.cq.cn', '.fj.cn', '.gd.cn',
|
||
'.gov.cn', '.gs.cn', '.gx.cn', '.gz.cn', '.ha.cn', '.hb.cn', '.he.cn',
|
||
'.hi.cn', '.hk.cn', '.hl.cn', '.hn.cn', '.jl.cn', '.js.cn', '.jx.cn',
|
||
'.ln.cn', '.mo.cn', '.net.cn', '.nm.cn', '.nx.cn', '.org.cn', '.cn.com']
|
||
old_domain_name = domain_name
|
||
top_domain = "." + ".".join(domain_name.rsplit('.')[-2:])
|
||
new_top_domain = "." + top_domain.replace(".", "")
|
||
is_tow_top = False
|
||
if top_domain in top_domain_list:
|
||
is_tow_top = True
|
||
domain_name = domain_name[:-len(top_domain)] + new_top_domain
|
||
|
||
if domain_name.count(".") > 1:
|
||
zone, middle, last = domain_name.rsplit(".", 2)
|
||
if is_tow_top:
|
||
last = top_domain[1:]
|
||
root = ".".join([middle, last])
|
||
else:
|
||
zone = ""
|
||
root = old_domain_name
|
||
return root, zone
|
||
|
||
|
||
def check_area_panel():
|
||
'''
|
||
@name: 检查地区限制
|
||
@return:
|
||
'''
|
||
areas_dict = get_limit_area()
|
||
|
||
# 关闭状态直接返回false
|
||
if areas_dict["limit_area_status"] == "false": return False
|
||
# 2024/1/3 下午 2:23 兼容配置文件如果为空或者地区为空或配置文件异常,则直接跳过验证
|
||
if len(areas_dict["limit_area"]) == 0: return False
|
||
if len(areas_dict["limit_area"]["city"]) == 0:
|
||
if "province" in areas_dict["limit_area"] and len(areas_dict["limit_area"]["province"]) == 0:
|
||
if "country" in areas_dict["limit_area"] and len(areas_dict["limit_area"]["country"]) == 0:
|
||
return False
|
||
|
||
client_ip = GetClientIp()
|
||
# 本地访问直接返回false
|
||
if client_ip in ['127.0.0.1', 'localhost', '::1']: return False
|
||
ip_area_dict = get_ip_location(client_ip)
|
||
# 没有查询到地区返回false,内网地址直接返回false
|
||
if not ip_area_dict: return False
|
||
if ip_area_dict.raw["country"]["country"] == "Internal network address": return False
|
||
try:
|
||
error_str = "<h2>{}</h2> {}</br> {}</br> {}".format(
|
||
getMsg('PAGE_ERR_IP_AREA_H1'),
|
||
getMsg('PAGE_ERR_IP_AREA_P1', ("{} {} {}".format(
|
||
ip_area_dict.raw["country"]["country"],
|
||
ip_area_dict.raw["country"]["province"],
|
||
ip_area_dict.raw["country"]["city"]
|
||
),)),
|
||
getMsg('PAGE_ERR_IP_AREA_P2'),
|
||
getMsg('PAGE_ERR_IP_AREA_P3')
|
||
)
|
||
|
||
# 仅允许allow列表中的地区,其他地区都不可以访问
|
||
if areas_dict["limit_type"] == "allow":
|
||
for city in areas_dict["limit_area"]["city"]:
|
||
if len(ip_area_dict.raw["country"]["city"].strip()) == 0:
|
||
break
|
||
|
||
if ip_area_dict.raw["country"]["city"].strip() in city["name"]:
|
||
return False
|
||
|
||
for province in areas_dict["limit_area"]["province"]:
|
||
if len(ip_area_dict.raw["country"]["province"].strip()) == 0:
|
||
break
|
||
|
||
if ip_area_dict.raw["country"]["province"].strip() in province["name"]:
|
||
return False
|
||
|
||
for country in areas_dict["limit_area"]["country"]:
|
||
if len(ip_area_dict.raw["country"]["country"].strip()) == 0:
|
||
break
|
||
|
||
if ip_area_dict.raw["country"]["country"].strip() in country["name"]:
|
||
return False
|
||
|
||
return error_str
|
||
|
||
# 仅禁止deny列表中的地区,其他地区都可以访问
|
||
if areas_dict["limit_type"] == "deny":
|
||
for city in areas_dict["limit_area"]["city"]:
|
||
if len(ip_area_dict.raw["country"]["city"].strip()) == 0:
|
||
break
|
||
|
||
if ip_area_dict.raw["country"]["city"].strip() in city["name"]:
|
||
return error_str
|
||
|
||
for province in areas_dict["limit_area"]["province"]:
|
||
if len(ip_area_dict.raw["country"]["province"].strip()) == 0:
|
||
break
|
||
|
||
if ip_area_dict.raw["country"]["province"].strip() in province["name"]:
|
||
return error_str
|
||
|
||
for country in areas_dict["limit_area"]["country"]:
|
||
if len(ip_area_dict.raw["country"]["country"].strip()) == 0:
|
||
break
|
||
|
||
if ip_area_dict.raw["country"]["country"].strip() in country["name"]:
|
||
return error_str
|
||
except:
|
||
import traceback
|
||
print(traceback.format_exc())
|
||
return False
|
||
|
||
|
||
def get_ip_location(ip_address):
|
||
'''
|
||
获取ip地址的地理位置
|
||
@param ip_address:
|
||
@return:
|
||
'''
|
||
try:
|
||
from geoip2 import database
|
||
except:
|
||
ExecShell("{}/pyenv/bin/pip install -U pip".format(get_panel_path()))
|
||
ExecShell("{}/pyenv/bin/pip install geoip2".format(get_panel_path()))
|
||
writeFile('data/restart.pl', 'True')
|
||
from geoip2 import database
|
||
|
||
data_path = '{}/config/GeoLite2-City.mmdb'.format(get_panel_path())
|
||
reader = database.Reader(data_path)
|
||
response = reader.city(ip_address)
|
||
reader.close()
|
||
return response
|
||
|
||
|
||
def get_limit_area():
|
||
'''
|
||
获取地区限制列表
|
||
@return:
|
||
'''
|
||
empty_content = {
|
||
"limit_area": {
|
||
"city": [],
|
||
"province": [],
|
||
"country": []
|
||
},
|
||
"limit_area_status": "false",
|
||
"limit_type": "deny"
|
||
}
|
||
try:
|
||
areas_file = 'data/limit_area.json'
|
||
if not os.path.exists(areas_file): return empty_content
|
||
|
||
try:
|
||
areas_dict = json.loads(ReadFile(areas_file))
|
||
except json.decoder.JSONDecodeError:
|
||
return empty_content
|
||
|
||
return areas_dict
|
||
except:
|
||
get_error_info()
|
||
return empty_content
|
||
|
||
|
||
# 密码复杂度验证
|
||
def check_password_safe(password: str) -> bool:
|
||
'''
|
||
@name 密码复杂度验证
|
||
@param password(string) 密码
|
||
@return bool
|
||
'''
|
||
# 是否检测密码复杂度
|
||
|
||
# 密码长度验证
|
||
if len(password) < 8: return False
|
||
|
||
num = 0
|
||
# 密码是否包含数字
|
||
if re.search(r'[0-9]+', password): num += 1
|
||
# 密码是否包含小写字母
|
||
if re.search(r'[a-z]+', password): num += 1
|
||
# 密码是否包含大写字母
|
||
if re.search(r'[A-Z]+', password): num += 1
|
||
# 密码是否包含特殊字符
|
||
if re.search(r'[^\w\s]+', password): num += 1
|
||
# 密码是否包含以上任意3种组合
|
||
if num < 3: return False
|
||
return True
|
||
|
||
|
||
def show_menu(menu_id, status):
|
||
"""
|
||
设置显示隐藏菜单
|
||
:param menu_id: 菜单id
|
||
:param status: 0 | 1
|
||
:return:
|
||
"""
|
||
|
||
show_menu_file = '/www/server/panel/config/show_menu.json'
|
||
hide_menu_file = '/www/server/panel/config/hide_menu.json'
|
||
defanlt_data = ['memuA', 'memuAsite', 'memuAftp', 'memuAdatabase', 'memuDocker', 'memuAcontrol', 'memuAfirewall',
|
||
'memuAfiles', 'memuAlogs', 'memuAxterm', 'memuAcrontab', 'memuAsoft',
|
||
'memuAconfig', 'dologin']
|
||
show_menu_data = defanlt_data
|
||
if os.path.exists(show_menu_file):
|
||
show_menu_data = json.loads(ReadFile(show_menu_file))
|
||
|
||
# 获取之前设置的隐藏页面
|
||
try:
|
||
if os.path.exists(hide_menu_file):
|
||
hide_menu = ReadFile(hide_menu_file)
|
||
show_menu_data = [i for i in show_menu_data if i not in hide_menu]
|
||
ExecShell("rm -rf {}".format(hide_menu_file))
|
||
WriteFile(show_menu_file, json.dumps(show_menu_data))
|
||
except:
|
||
pass
|
||
if status == 1 and menu_id not in show_menu_data:
|
||
show_menu_data.append(menu_id)
|
||
elif status == 0 and menu_id in show_menu_data:
|
||
show_menu_data.remove(menu_id)
|
||
WriteFile(show_menu_file, json.dumps(show_menu_data))
|
||
return returnMsg(True, 'Success')
|
||
|
||
|
||
def task_service_status():
|
||
'''
|
||
@name 检查后台任务服务状态
|
||
@return bool
|
||
'''
|
||
pid_file = '{}/logs/task.pid'.format(get_panel_path())
|
||
if not os.path.exists(pid_file): return False
|
||
pid = readFile(pid_file)
|
||
if not pid: return False
|
||
if not os.path.exists('/proc/{}'.format(pid)): return False
|
||
return True
|
||
|
||
|
||
def reload_panel():
|
||
'''
|
||
@name 重载面板
|
||
@return void
|
||
'''
|
||
|
||
# 重载面板
|
||
WriteFile('{}/data/reload.pl'.format(get_panel_path()), 'True')
|
||
if not task_service_status():
|
||
ExecShell("bash {}/init.sh start")
|
||
|
||
|
||
# 2024/1/24 上午 10:38 通用响应对象
|
||
def returnResult(code=0, status=True, msg="OK", data=None, timestamp=None, args=None):
|
||
'''
|
||
通用响应对象
|
||
@param code: 0:成功 1:失败 2:警告 ...
|
||
@param status:
|
||
@param msg: 只传msg,不传需要前端处理的数据
|
||
@param data: 只传需要前端处理的数据
|
||
@param timestamp: 秒级时间戳
|
||
@return:
|
||
|
||
使用示例:
|
||
成功:return dp.returnResult(data=data)
|
||
失败:return dp.returnResult(code=1, status=False, msg="获取失败!", data=[])
|
||
失败:return dp.returnResult(code=1, status=False, msg="获取失败!")
|
||
警告:return dp.returnResult(code=2, status=False, msg="警告,xxxxxxxxxxx!")
|
||
...
|
||
'''
|
||
import time
|
||
if timestamp is None:
|
||
timestamp = int(time.time())
|
||
|
||
log_message = json.loads(ReadFile('YakPanel/static/language/' + GetLanguage() + '/public.json'))
|
||
keys = log_message.keys()
|
||
if type(msg) == str:
|
||
if msg in keys:
|
||
msg = log_message[msg]
|
||
for i in range(len(args)):
|
||
rep = '{' + str(i + 1) + '}'
|
||
msg = msg.replace(rep, args[i])
|
||
|
||
return {
|
||
"code": code,
|
||
"status": status,
|
||
"msg": msg,
|
||
"data": data,
|
||
"timestamp": timestamp
|
||
}
|
||
|
||
|
||
# 2024/1/24 上午 11:56 取指定模型目录
|
||
def get_mod_path(mod_name=None):
|
||
'''
|
||
@name 取指定插件目录
|
||
@author hwliang<2021-07-14>
|
||
@param mod_name<string> 模型名称 不传则返回模型根目录
|
||
@return string
|
||
'''
|
||
|
||
root_path = "{}/mod/project".format(get_panel_path())
|
||
if not mod_name: return root_path
|
||
return "{}/{}".format(root_path, mod_name)
|
||
|
||
|
||
def get_client_info_db_obj():
|
||
'''
|
||
@name 获取客户端信息数据库对象
|
||
@return object
|
||
'''
|
||
db_path = '{}/data/db'.format(get_panel_path())
|
||
db_file = '{}/client_info.db'.format(db_path)
|
||
if not os.path.exists(db_path): os.makedirs(db_path, 384)
|
||
db_obj = M('')
|
||
db_obj._Sql__DB_FILE = db_file
|
||
# 如果数据库文件不存在则创建
|
||
if not os.path.exists(db_file):
|
||
db_obj.execute('''CREATE TABLE client_info (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
remote_addr VARCHAR(50) NOT NULL,
|
||
remote_port INTEGER DEFAULT 0,
|
||
session_id VARCHAR(32) NOT NULL,
|
||
user_agent TEXT NOT NULL,
|
||
login_time INTEGER DEFAULT 0
|
||
)''')
|
||
|
||
# 创建索引
|
||
db_obj.execute('CREATE INDEX client_ip_index ON client_info(client_ip)')
|
||
db_obj.execute('CREATE INDEX session_id_index ON client_info(session_id)')
|
||
db_obj.execute('CREATE INDEX login_time_index ON client_info(login_time)')
|
||
|
||
# 增加字段login_type "登录是否成功"
|
||
try:
|
||
hit = False
|
||
for f in db_obj.table("sqlite_master").query("PRAGMA table_info('client_info');") or []:
|
||
if f[1] == "login_type":
|
||
hit = True
|
||
break
|
||
if not hit:
|
||
db_obj.table("sqlite_master").execute(
|
||
'ALTER TABLE `client_info` ADD COLUMN `login_type` INTEGER DEFAULT 1'
|
||
)
|
||
except Exception as e:
|
||
public.print_log(str(e))
|
||
|
||
return db_obj
|
||
|
||
|
||
def record_client_info(login_type: int = 1):
|
||
"""
|
||
@name 记录客户端信息
|
||
@param login_type int 登录类型 1:成功 0:失败
|
||
@return None
|
||
"""
|
||
db_obj = None
|
||
try:
|
||
from flask import request
|
||
from YakPanel import cache
|
||
db_obj = get_client_info_db_obj()
|
||
remote_addr = GetClientIp()
|
||
user_agent = request.headers.get('User-Agent', '')
|
||
pdata = {
|
||
'remote_addr': remote_addr,
|
||
'remote_port': request.environ.get('REMOTE_PORT'),
|
||
'session_id': md5(remote_addr + user_agent),
|
||
'user_agent': user_agent,
|
||
'login_time': int(time.time()),
|
||
'login_type': int(login_type),
|
||
}
|
||
db_obj.table('client_info').insert(pdata)
|
||
# 设置缓存
|
||
cache.set('last_client_session_id', pdata['session_id'], 86400 * 2)
|
||
except Exception as e:
|
||
public.print_log(f"error record_client_info: {str(e)}")
|
||
finally:
|
||
if db_obj:
|
||
db_obj.close()
|
||
|
||
|
||
def check_client_info():
|
||
'''
|
||
@name 检查客户端信息
|
||
@return int 0:陌生IP,1:上次登录的IP且UA一致,2:近30天内登录过的IP
|
||
'''
|
||
from flask import request
|
||
from YakPanel import cache
|
||
remote_addr = GetClientIp()
|
||
# remote_addr = request.environ.get('REMOTE_ADDR', '0.0.0.0')
|
||
# 如果是本地访问或为未来IP则当作陌生IP
|
||
if remote_addr in ['0.0.0.0', '127.0.0.1', '::1', '::']:
|
||
return 0
|
||
user_agent = request.headers.get('User-Agent', '')
|
||
# 如果UA不是浏览器则当作陌生IP
|
||
if user_agent.find('Mozilla') == -1:
|
||
return 0
|
||
|
||
session_id = md5(remote_addr + user_agent)
|
||
|
||
if cache.get('last_client_session_id') == session_id:
|
||
return 1
|
||
|
||
db_obj = get_client_info_db_obj()
|
||
if not db_obj:
|
||
return 0
|
||
|
||
last_login_info = db_obj.table('client_info').order('id desc').field('remote_addr,session_id,login_time').find()
|
||
if not last_login_info:
|
||
return 0
|
||
|
||
# 如果上次登录的IP且UA一致
|
||
now_time = int(time.time())
|
||
if last_login_info['session_id'] == session_id:
|
||
s_time = now_time - last_login_info['login_time']
|
||
if s_time < (86400 * 2):
|
||
cache.set('last_client_session_id', session_id, 86400 * 2 - s_time)
|
||
return 1
|
||
if s_time < (86400 * 30):
|
||
return 2
|
||
return 0
|
||
|
||
# 如果近30天内登录过的IP
|
||
if remote_addr == last_login_info['remote_addr'] and now_time - last_login_info['login_time'] < 2592000:
|
||
return 2
|
||
if db_obj.table('client_info').where('remote_addr=?', remote_addr).count():
|
||
return 2
|
||
|
||
# 陌生IP
|
||
return 0
|
||
|
||
|
||
def redirect_to_login(default_callback_def=None):
|
||
'''
|
||
@name 重定向到登录页面
|
||
@return void
|
||
'''
|
||
from flask import redirect, request, Response
|
||
client_status = check_client_info()
|
||
|
||
# 获取请求头
|
||
x_http_token = request.headers.get('x-http-token', '')
|
||
if client_status == 0:
|
||
if default_callback_def:
|
||
return default_callback_def(None)
|
||
# print_log("redirect_to_login 方法{1}") 登录过期会进入
|
||
return error_404(None)
|
||
elif client_status == 1:
|
||
if x_http_token:
|
||
# result = {"status": False, "code": -8888, "redirect": get_admin_path(),
|
||
# "msg": "The current login session has been invalid, please login again!"}
|
||
# 修改为yakpanel通用返回方式
|
||
result = {
|
||
"status": -1,
|
||
"timestamp": int(time.time()),
|
||
"message": {
|
||
"msg": "The current login session has been invalid, please login again!",
|
||
"redirect": get_admin_path()
|
||
}
|
||
}
|
||
|
||
return Response(json.dumps(result), mimetype='application/json', status=200)
|
||
return redirect(get_admin_path())
|
||
elif client_status == 2:
|
||
if x_http_token:
|
||
# result = {"status": False, "code": -8888, "redirect": "/login",
|
||
# "msg": "The current login session has been invalid, please login again!"}
|
||
|
||
# 修改为yakpanel通用返回方式
|
||
result = {
|
||
"status": -1,
|
||
"timestamp": int(time.time()),
|
||
"message": {
|
||
"msg": "The current login session has been invalid, please login again!",
|
||
"redirect": "/login"
|
||
}
|
||
}
|
||
|
||
return Response(json.dumps(result), mimetype='application/json', status=200)
|
||
return redirect('/login')
|
||
|
||
if default_callback_def:
|
||
return default_callback_def(None)
|
||
return error_404(None)
|
||
|
||
|
||
def ws_send(data: str):
|
||
try:
|
||
if '/www/server/panel' not in sys.path:
|
||
sys.path.insert(0, '/www/server/panel')
|
||
# noinspection PyUnresolvedReferences
|
||
from YakPanel import WS_OBJ
|
||
ws_obj = {i: j for i, j in WS_OBJ.items() if j['timeout'] > int(time.time())}
|
||
if ws_obj == {}: return False
|
||
for i, j in ws_obj.items():
|
||
j['ws_obj'].send(data)
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
|
||
def get_plugin_info(upgrade_plugin_name):
|
||
'''
|
||
@name 获取插件信息
|
||
@author hwliang<2021-06-15>
|
||
@param upgrade_plugin_name<string> 插件名称
|
||
@return dict
|
||
'''
|
||
plugin_path = get_plugin_path()
|
||
plugin_info_file = '{}/{}/info.json'.format(plugin_path, upgrade_plugin_name)
|
||
if not os.path.exists(plugin_info_file): return {}
|
||
info_body = readFile(plugin_info_file)
|
||
if not info_body: return {}
|
||
plugin_info = json.loads(info_body)
|
||
return plugin_info
|
||
|
||
|
||
def get_plugin_find(upgrade_plugin_name=None):
|
||
'''
|
||
@name 获取指定软件信息
|
||
@author hwliang<2021-06-15>
|
||
@param upgrade_plugin_name<string> 插件名称
|
||
@return dict
|
||
'''
|
||
plugin_list_data = load_soft_list()
|
||
|
||
for p_data_info in plugin_list_data['list']:
|
||
if p_data_info['name'] == upgrade_plugin_name:
|
||
# upgrade_plugin_name = p_data_info['name']
|
||
return p_data_info
|
||
|
||
return get_plugin_info(upgrade_plugin_name)
|
||
|
||
|
||
def get_plugin_value(plugin_name, key):
|
||
'''
|
||
@name 获取插件配置值
|
||
@author hwliang
|
||
@param plugin_name<string> 插件名称
|
||
@param key<string> 字段名
|
||
@return mixed
|
||
'''
|
||
plugin_info = get_plugin_find(plugin_name)
|
||
return plugin_info.get(key, None)
|
||
|
||
|
||
def get_plugin_pid(plugin_name):
|
||
'''
|
||
@name 获取指定插件的pid
|
||
@author hwliang<2021-06-15>
|
||
@param plugin_name<string> 插件名称
|
||
@return string
|
||
'''
|
||
plugin_info = get_plugin_find(plugin_name)
|
||
if not plugin_info: return 0
|
||
if 'pid' in plugin_info:
|
||
return plugin_info['pid']
|
||
return 0
|
||
|
||
|
||
# 下载插件主文件
|
||
def download_main(upgrade_plugin_name, upgrade_version):
|
||
'''
|
||
@name 下载插件主程序文件
|
||
@author hwliang<2021-06-25>
|
||
@param upgrade_plugin_name<string> 插件名称
|
||
@param upgrade_version<string> 插件版本
|
||
@return void
|
||
'''
|
||
import requests, shutil
|
||
plugin_path = get_plugin_path()
|
||
tmp_path = '{}/temp'.format(get_panel_path())
|
||
|
||
if not os.path.exists(tmp_path):
|
||
os.makedirs(tmp_path, 0o755)
|
||
|
||
download_d_main_url = '{}/api/panel/download_plugin_main'.format(OfficialApiBase())
|
||
pdata = get_user_info()
|
||
pdata['name'] = upgrade_plugin_name
|
||
pdata['version'] = upgrade_version
|
||
pdata['os'] = 'Linux'
|
||
pdata['environment_info'] = json.dumps(fetch_env_info(), ensure_ascii=False)
|
||
import config, socket
|
||
# noinspection PyUnresolvedReferences
|
||
import requests.packages.urllib3.util.connection as urllib3_conn
|
||
_ip_type = config.config().get_request_iptype()
|
||
old_family = urllib3_conn.allowed_gai_family
|
||
if _ip_type == 'ipv4':
|
||
urllib3_conn.allowed_gai_family = lambda: socket.AF_INET
|
||
elif _ip_type == 'ipv6':
|
||
urllib3_conn.allowed_gai_family = lambda: socket.AF_INET6
|
||
try:
|
||
download_res = requests.post(download_d_main_url, pdata, timeout=30, headers=get_requests_headers())
|
||
|
||
print_log(pdata)
|
||
print_log(download_res.content)
|
||
except Exception as ex:
|
||
raise PanelError(error_conn_cloud(str(ex)))
|
||
finally:
|
||
urllib3_conn.allowed_gai_family = old_family
|
||
|
||
# 下载失败提示文本处理
|
||
if download_res.status_code != 200:
|
||
try:
|
||
raise PanelError(download_res.json().get('res', 'download plugin source code error'))
|
||
except PanelError:
|
||
raise
|
||
except:
|
||
raise PanelError('download plugin source code error')
|
||
|
||
filename = '{}/{}.py'.format(tmp_path, upgrade_plugin_name)
|
||
with open(filename, 'wb+') as save_script_f:
|
||
save_script_f.write(download_res.content)
|
||
save_script_f.close()
|
||
if md5(download_res.content) != download_res.headers.get('Content-md5'):
|
||
raise PanelError('Package file Hash verification failed.')
|
||
dst_file = '{plugin_path}/{plugin_name}/{plugin_name}_main.py'.format(plugin_path=plugin_path,
|
||
plugin_name=upgrade_plugin_name)
|
||
shutil.copyfile(filename, dst_file)
|
||
if os.path.exists(filename): os.remove(filename)
|
||
WriteLog('Software manager',
|
||
"Plugin [{}] was corrupted, try automatic repair.".format(get_plugin_info(upgrade_plugin_name)['title']))
|
||
|
||
|
||
# 重新下载插件主文件
|
||
def re_download_main(plugin_name, plugin_path=None):
|
||
if not plugin_path:
|
||
plugin_path = get_panel_path() + '/plugin/' + plugin_name
|
||
|
||
plugin_file = '{plugin_path}/{name}/{name}_main.py'.format(plugin_path=plugin_path, name=plugin_name)
|
||
plugin_info = get_plugin_info(plugin_name)
|
||
if 'versions' in plugin_info:
|
||
version = plugin_info['versions']
|
||
download_main(plugin_name, version)
|
||
plugin_body = readFile(plugin_file, 'rb')
|
||
return plugin_body
|
||
|
||
return b''
|
||
|
||
|
||
# 运行插件API
|
||
def run_plugin(plugin_name: str, def_name: str, args: dict_obj):
|
||
import PluginLoader
|
||
res = PluginLoader.plugin_run(plugin_name, def_name, args)
|
||
if isinstance(res, dict):
|
||
if 'status' in res and res['status'] == False and 'msg' in res:
|
||
if isinstance(res['msg'], str):
|
||
if res['msg'].find('Traceback ') != -1:
|
||
raise PanelError(res['msg'])
|
||
return res
|
||
|
||
|
||
def run_plugin_v2(plugin_name: str, def_name: str, args: dict_obj):
|
||
import PluginLoader
|
||
res = PluginLoader.plugin_run(plugin_name, def_name, args)
|
||
# print_log(res)
|
||
if isinstance(res, dict):
|
||
if 'status' in res and res['status'] == False and 'msg' in res:
|
||
if isinstance(res['msg'], str):
|
||
if res['msg'].find('Traceback ') != -1:
|
||
raise PanelError(res['msg'])
|
||
|
||
if isinstance(res, dict):
|
||
if 'status' in res and 'msg' in res:
|
||
status = 0 if res['status'] else -1
|
||
# 改返回
|
||
res = return_message(status, 0, res['msg'])
|
||
else:
|
||
# 改返回
|
||
res = return_message(0, 0, res)
|
||
if isinstance(res, (list, str, int)):
|
||
res = return_message(0, 0, res)
|
||
return res
|
||
|
||
|
||
def _self_hosted_tip_type_num(tip):
|
||
return 3 if tip == 'lib' else 1
|
||
|
||
|
||
def _self_hosted_version_pairs(ver_str, upd_str):
|
||
vers = [x.strip() for x in (ver_str or '').split(',') if x.strip()]
|
||
if not vers:
|
||
vers = ['1']
|
||
upds = [x.strip() for x in (upd_str or '').split(',')] if upd_str else []
|
||
while len(upds) < len(vers):
|
||
upds.append('')
|
||
return [(vers[i], upds[i] if i < len(upds) else '') for i in range(len(vers))]
|
||
|
||
|
||
def _self_hosted_checks_template(checks):
|
||
if not checks:
|
||
return '/tmp/yakpanel_unconfigured_check'
|
||
return checks.replace('VERSION', '{VERSION}')
|
||
|
||
|
||
def _self_hosted_ver_obj(m, display_ver):
|
||
return {
|
||
'm_version': m,
|
||
'version': display_ver or m,
|
||
'dependent': '',
|
||
'mem_limit': 512,
|
||
'cpu_limit': 1,
|
||
'os_limit': 0,
|
||
'setup': False,
|
||
}
|
||
|
||
|
||
def get_self_hosted_soft_list_dict():
|
||
path = '{}/data/list.json'.format(get_panel_path())
|
||
body = readFile(path)
|
||
if not body:
|
||
return {'list': [], 'pro': -1, 'ltd': 1}
|
||
try:
|
||
raw_list = json.loads(body)
|
||
except:
|
||
return {'list': [], 'pro': -1, 'ltd': 1}
|
||
if not isinstance(raw_list, list):
|
||
return {'list': [], 'pro': -1, 'ltd': 1}
|
||
|
||
out = []
|
||
for entry in raw_list:
|
||
if not isinstance(entry, dict) or 'name' not in entry:
|
||
continue
|
||
tip = entry.get('tip', 'soft')
|
||
type_num = _self_hosted_tip_type_num(tip)
|
||
pairs = _self_hosted_version_pairs(entry.get('versions'), entry.get('update'))
|
||
checks_tpl = _self_hosted_checks_template(entry.get('checks', ''))
|
||
shell = entry.get('shell', entry['name'] + '.sh')
|
||
ps = entry.get('ps', '')
|
||
title_base = entry.get('title', entry['name'])
|
||
vobjs = [_self_hosted_ver_obj(m, (subv or m)) for m, subv in pairs]
|
||
|
||
if len(pairs) <= 1:
|
||
m, subv = pairs[0]
|
||
vv = m.replace('.', '')
|
||
ic = checks_tpl.replace('{VERSION}', vv) if '{VERSION}' in checks_tpl else checks_tpl
|
||
vd = subv or m
|
||
out.append({
|
||
'id': int(entry.get('id', 1)),
|
||
'pid': int(entry.get('pid', 0)),
|
||
'type': type_num,
|
||
'price': float(entry.get('price', 0) or 0),
|
||
'name': entry['name'],
|
||
'title': title_base,
|
||
'ps': ps,
|
||
'version': vd,
|
||
's_version': '',
|
||
'manager_version': '',
|
||
'c_manager_version': '',
|
||
'dependent': entry.get('dependent', ''),
|
||
'mutex': entry.get('mutex', ''),
|
||
'shell': shell,
|
||
'install_checks': ic,
|
||
'uninstall_checks': ic,
|
||
'uninsatll_checks': ic,
|
||
'compile_args': 0,
|
||
'version_coexist': 0,
|
||
'versions': vobjs,
|
||
'endtime': -1,
|
||
'panel_pro': 1,
|
||
'panel_free': 1,
|
||
'panel_test': 1,
|
||
'author': entry.get('author', ''),
|
||
'home': entry.get('home', '#'),
|
||
})
|
||
else:
|
||
ichk = checks_tpl
|
||
mgr = '1'
|
||
sproc = ',' + entry['name']
|
||
out.append({
|
||
'id': int(entry.get('id', 1)),
|
||
'pid': int(entry.get('pid', 0)),
|
||
'type': type_num,
|
||
'price': float(entry.get('price', 0) or 0),
|
||
'name': entry['name'],
|
||
'title': title_base,
|
||
'ps': ps,
|
||
'version': '{VERSION}',
|
||
's_version': sproc,
|
||
'manager_version': mgr,
|
||
'c_manager_version': mgr,
|
||
'dependent': entry.get('dependent', ''),
|
||
'mutex': entry.get('mutex', ''),
|
||
'shell': shell,
|
||
'install_checks': ichk,
|
||
'uninstall_checks': ichk,
|
||
'uninsatll_checks': ichk,
|
||
'compile_args': 0,
|
||
'version_coexist': 1,
|
||
'versions': vobjs,
|
||
'endtime': -1,
|
||
'panel_pro': 1,
|
||
'panel_free': 1,
|
||
'panel_test': 1,
|
||
'author': entry.get('author', ''),
|
||
'home': entry.get('home', '#'),
|
||
})
|
||
|
||
return {'list': out, 'pro': -1, 'ltd': 1}
|
||
|
||
|
||
# 加载插件列表与授权列表
|
||
def load_soft_list(force: bool = True, retry_count: int = 0):
|
||
if is_self_hosted():
|
||
return get_self_hosted_soft_list_dict()
|
||
|
||
local_cache_file = '{}/data/plugin_bin.pl'.format(get_panel_path())
|
||
|
||
if force or not os.path.exists(local_cache_file) or os.path.getsize(local_cache_file) < 10:
|
||
# 如果是重试,sleep一段时间
|
||
if retry_count > 0:
|
||
time.sleep(2 * retry_count + 1)
|
||
|
||
cloudUrl = '{}/api/panel/getSoftListEn'.format(OfficialApiBase())
|
||
import panelAuth
|
||
import requests
|
||
pdata = panelAuth.panelAuth().create_serverid(None)
|
||
url_headers = {
|
||
'user-agent': 'YakPanel/1.0',
|
||
}
|
||
if 'token' in pdata:
|
||
url_headers = {"authorization": "bt {}".format(pdata['token'])}
|
||
|
||
pdata['environment_info'] = json.dumps(fetch_env_info())
|
||
|
||
update_ok = False
|
||
ex = None
|
||
|
||
# 默认重试5次
|
||
try:
|
||
resp = requests.post(cloudUrl, params=pdata, headers=url_headers, verify=False, timeout=10)
|
||
|
||
# 请求成功后将授权密文信息写入本地文件
|
||
if resp.ok:
|
||
with open(local_cache_file, 'w') as fp:
|
||
fp.write(resp.text)
|
||
update_ok = True
|
||
|
||
except Exception as e:
|
||
ex = e
|
||
|
||
if retry_count < 6:
|
||
# 获取软件列表失败,重试
|
||
return load_soft_list(force, retry_count + 1)
|
||
|
||
# 本地缓存存在则让其读取本地缓存
|
||
if not update_ok:
|
||
update_ok = os.path.exists(local_cache_file) and os.path.getsize(local_cache_file) >= 10
|
||
|
||
# 本地缓存都不存在,如果捕获到异常则将异常记录到错误日志,返回默认的软件列表数据
|
||
if not update_ok:
|
||
if ex is not None:
|
||
raise ex
|
||
|
||
raise PanelError(get_msg_gettext('Load softlist and authorizations failed, please wait for few moment and try again.'))
|
||
|
||
import PluginLoader
|
||
|
||
try:
|
||
if force:
|
||
if hasattr(PluginLoader, 'parse_plugin_list'):
|
||
if not PluginLoader.parse_plugin_list(1):
|
||
raise PanelError('Sorry. failed to parse soft list. please try again later.')
|
||
else:
|
||
import importlib
|
||
importlib.reload(PluginLoader)
|
||
|
||
plugin_list_data = PluginLoader.get_plugin_list(0)
|
||
except:
|
||
if retry_count < 6:
|
||
# 获取软件列表失败,重试
|
||
return load_soft_list(force, retry_count + 1)
|
||
raise
|
||
|
||
if not isinstance(plugin_list_data, dict):
|
||
if retry_count < 6:
|
||
# 获取软件列表失败,重试
|
||
return load_soft_list(force, retry_count + 1)
|
||
raise PanelError('Sorry. failed to load soft list. please check the network and try again later.')
|
||
|
||
if 'status' in plugin_list_data and 'msg' in plugin_list_data and plugin_list_data['status'] == False:
|
||
if retry_count < 6:
|
||
# 获取软件列表失败,重试
|
||
return load_soft_list(force, retry_count + 1)
|
||
raise PanelError(str(plugin_list_data['msg']))
|
||
|
||
return plugin_list_data
|
||
|
||
|
||
# 官网API根地址
|
||
def OfficialApiBase():
|
||
if is_self_hosted():
|
||
return PanelHttpOrigin()
|
||
return 'https://www.yakpanel.com'
|
||
|
||
# 部分插件下载地址
|
||
def sync_plugin_OfficialApiBase():
|
||
if is_self_hosted():
|
||
return PanelHttpOrigin()
|
||
return 'https://download.yakpanel.com'
|
||
|
||
# 官网下载根地址
|
||
def OfficialDownloadBase():
|
||
if is_self_hosted():
|
||
return PanelHttpOrigin()
|
||
return 'https://node.yakpanel.com'
|
||
|
||
|
||
# 获取安装路径
|
||
def get_setup_path():
|
||
'''
|
||
@name 获取安装路径
|
||
@author hwliang<2021-07-22>
|
||
@return string
|
||
'''
|
||
return '/www/server'
|
||
|
||
|
||
# 获取面板根目录
|
||
def get_panel_path():
|
||
'''
|
||
@name 取面板根目录
|
||
@author hwliang<2021-07-14>
|
||
@return string
|
||
'''
|
||
return '{}/panel'.format(get_setup_path())
|
||
|
||
|
||
# 读取文件
|
||
def ReadFile(filename, mode='r'):
|
||
"""
|
||
读取文件内容
|
||
@filename 文件名
|
||
return string(bin) 若文件不存在,则返回None
|
||
"""
|
||
import os
|
||
if not os.path.exists(filename): return False
|
||
fp = None
|
||
try:
|
||
fp = open(filename, mode)
|
||
f_body = fp.read()
|
||
except Exception as ex:
|
||
if sys.version_info[0] != 2:
|
||
try:
|
||
fp = open(filename, mode, encoding="utf-8", errors='ignore')
|
||
f_body = fp.read()
|
||
except:
|
||
try:
|
||
fp = open(filename, mode, encoding="GBK", errors='ignore')
|
||
f_body = fp.read()
|
||
except:
|
||
return False
|
||
else:
|
||
return False
|
||
finally:
|
||
if fp and not fp.closed:
|
||
fp.close()
|
||
return f_body
|
||
|
||
|
||
# 读取文件
|
||
def readFile(filename, mode='r'):
|
||
'''
|
||
@name 读取指定文件数据
|
||
@author hwliang<2021-06-09>
|
||
@param filename<string> 文件名
|
||
@param mode<string> 文件打开模式,默认r
|
||
@return string or bytes or False 如果返回False则说明读取失败
|
||
'''
|
||
return ReadFile(filename, mode)
|
||
|
||
|
||
def read_rare_charset_file(filename: str):
|
||
"""
|
||
读取文件内容, 返回本信息, 如果出错返回 False,
|
||
与readFile相比可以处理更多种编码格式。
|
||
处理python项目中存在奇怪编码问题
|
||
"""
|
||
import chardet
|
||
if not os.path.exists(filename):
|
||
return False
|
||
fp = None
|
||
try:
|
||
fp = open(filename, "rb")
|
||
f_body_bytes: bytes = fp.read()
|
||
fp.close()
|
||
except:
|
||
if fp and not fp.closed:
|
||
fp.close()
|
||
return False
|
||
|
||
if hasattr(chardet, "detect_all"): # 处理部分版本没有detect_all方法的问题
|
||
tmp_eng_list = chardet.detect_all(f_body_bytes)[:3]
|
||
else:
|
||
tmp_eng_list = [chardet.detect(f_body_bytes)]
|
||
for tmp_eng in tmp_eng_list:
|
||
try:
|
||
f_body = f_body_bytes.decode(tmp_eng["encoding"])
|
||
except:
|
||
continue
|
||
return f_body
|
||
|
||
for eng in ("utf-8", "GBK", "utf-16", "utf-32", "BIG5"):
|
||
try:
|
||
f_body = f_body_bytes.decode(eng, errors="ignore")
|
||
except:
|
||
continue
|
||
return f_body
|
||
|
||
return False
|
||
|
||
# 写入文件
|
||
def WriteFile(filename, s_body, mode='w+'):
|
||
"""
|
||
写入文件内容
|
||
@filename 文件名
|
||
@s_body 欲写入的内容
|
||
return bool 若文件不存在则尝试自动创建
|
||
"""
|
||
try:
|
||
fp = open(filename, mode)
|
||
fp.write(s_body)
|
||
fp.close()
|
||
return True
|
||
except:
|
||
try:
|
||
fp = open(filename, mode, encoding="utf-8")
|
||
fp.write(s_body)
|
||
fp.close()
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
def AppendFile(filename, s_body, mode='a'):
|
||
"""
|
||
写入文件内容
|
||
@filename 文件名
|
||
@s_body 欲写入的内容
|
||
return bool 若文件不存在则尝试自动创建
|
||
"""
|
||
try:
|
||
with open(filename, mode, encoding="utf-8") as fp:
|
||
fp.write(s_body)
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
# 写入文件
|
||
def writeFile(filename, s_body, mode='w+'):
|
||
'''
|
||
@name 写入到指定文件
|
||
@author hwliang<2021-06-09>
|
||
@param filename<string> 文件名
|
||
@param s_boey<string/bytes> 被写入的内容,字节或字符串
|
||
@param mode<string> 文件打开模式,默认w+
|
||
@return bool
|
||
'''
|
||
return WriteFile(filename, s_body, mode)
|
||
|
||
|
||
# 创建临时目录
|
||
def make_panel_tmp_path() -> str:
|
||
tmp_path = '{}/temp/tmp_{}_{}'.format(get_panel_path(), int(time.time()), GetRandomString(32))
|
||
if not os.path.exists(tmp_path):
|
||
os.makedirs(tmp_path, 0o755)
|
||
return tmp_path
|
||
|
||
|
||
# 创建临时目录(使用上下文管理器)
|
||
@contextlib.contextmanager
|
||
def make_panel_tmp_path_with_context():
|
||
tmp_path = make_panel_tmp_path()
|
||
|
||
import shutil
|
||
|
||
try:
|
||
yield tmp_path
|
||
finally:
|
||
# 删除临时目录
|
||
shutil.rmtree(tmp_path)
|
||
|
||
|
||
# 处理SQL语句中的特殊字符
|
||
def escape_sql_str(s: str) -> str:
|
||
return search_sql_special_chars.sub(r'\\\g<0>', s)
|
||
|
||
|
||
# 组装multipart/form-data数据
|
||
def build_multipart(data: typing.Dict) -> aap_t_http_multipart:
|
||
boundary = b'----YakpanelFormBoundary' + GetRandomString(16).encode('utf-8')
|
||
body = b''
|
||
|
||
# 标准的HTTP请求报文是使用\r\n换行
|
||
# \n换行也能被解析,可能存在兼容性问题
|
||
eol = b'\r\n'
|
||
|
||
for k in data.keys():
|
||
v = data[k]
|
||
|
||
# 二进制数据(文件上传)(bytes, filename)
|
||
if isinstance(v, tuple) and len(v) == 2:
|
||
bs, filename = v
|
||
|
||
if isinstance(bs, bytes) and isinstance(filename, str):
|
||
body += b'--' + boundary + eol + b'Content-Disposition: form-data; name="' + k.encode('utf-8') + b'"; filename="' + filename.encode('utf-8') + b'"' + eol + b'Content-Type: application/octet-stream' + eol + eol + bs + eol
|
||
# 普通参数
|
||
else:
|
||
# str/number 转 bytes
|
||
if isinstance(v, str) or is_number(v):
|
||
v = str(v).encode('utf-8')
|
||
|
||
# 仅处理bytes
|
||
if isinstance(v, bytes):
|
||
body += b'--' + boundary + eol + b'Content-Disposition: form-data; name="' + k.encode('utf-8') + b'"' + eol + eol + v + eol
|
||
|
||
body += b'--' + boundary + b'--' + eol
|
||
|
||
return aap_t_http_multipart(headers={
|
||
'Content-Type': 'multipart/form-data; boundary=' + boundary.decode('utf-8'),
|
||
'Content-Length': str(len(body)),
|
||
}, body=body)
|
||
|
||
|
||
from lang import Lang
|
||
|
||
def setLang(lang = 'en'):
|
||
'''
|
||
@name 设置语言
|
||
@param {string} lang - 语言
|
||
'''
|
||
lang_obj = Lang()
|
||
lang_obj.setLanguage(lang)
|
||
|
||
def lang(content,*args):
|
||
'''
|
||
@name 多语言渲染
|
||
@param {string} content - 内容
|
||
@param {any[]} args - 参数
|
||
@returns {string}
|
||
@example lang('Hello {}', 'World')
|
||
@example lang('Hello {} {}', 'World', '!')
|
||
@example lang('Hello')
|
||
'''
|
||
lang_obj = Lang()
|
||
# if content in ["#Clear cache","# Forbidden files or directories", "#Persistent connection related configuration"]:
|
||
# return content
|
||
|
||
return lang_obj.getLang(content, *args)
|
||
|
||
|
||
def get_disk_usage(path):
|
||
"""
|
||
@name 获取目录可用空间
|
||
"""
|
||
|
||
if not os.path.exists(path):
|
||
return returnMsg(False, lang('The specified directory does not exist'))
|
||
|
||
res = psutil.disk_usage(path)
|
||
return res
|
||
|
||
"""
|
||
@name 检查表是否存在
|
||
@param table 被检查的表名 sites
|
||
@param table_sql 表结构sql语句
|
||
"""
|
||
|
||
|
||
def check_table(table, table_sql):
|
||
db_obj = M(table)
|
||
res = db_obj.query("SELECT * FROM sqlite_master WHERE type='table' AND name='{}';".format(table))
|
||
is_create = True
|
||
if not isinstance(res, list): is_create = False
|
||
if len(res) <= 0: is_create = False
|
||
|
||
if not is_create:
|
||
db_obj.execute(table_sql, ())
|
||
return True
|
||
|
||
|
||
def get_set_language():
|
||
'''
|
||
@name 获取当前设置的语言
|
||
@return list
|
||
'''
|
||
default = 'en'
|
||
filename = '/www/server/panel/YakPanel/languages/settings.json'
|
||
if not os.path.exists(filename):
|
||
return default
|
||
body = ReadFile(filename)
|
||
try:
|
||
return json.loads(body)['default']
|
||
except:
|
||
return default
|
||
|
||
|
||
# 复制文件夹(同名覆盖)
|
||
def copy_dir(src, dst):
|
||
# 目标目录不存在,直接复制目录
|
||
if not os.path.exists(dst):
|
||
shutil.copytree(src, dst)
|
||
return
|
||
|
||
ds = os.listdir(src)
|
||
for d in ds:
|
||
src_path = os.path.join(src, d)
|
||
dst_path = os.path.join(dst, d)
|
||
|
||
if os.path.isfile(src_path):
|
||
# 复制文件
|
||
shutil.copyfile(src_path, dst_path)
|
||
continue
|
||
|
||
# 复制目录
|
||
copy_dir(src_path, dst_path)
|
||
|
||
|
||
# 确保数据库名称不重复
|
||
def ensure_unique_db_name(db_name: str) -> str:
|
||
# 生成不重复的数据库用户名
|
||
while 1:
|
||
if S('databases').where('name', db_name).exists():
|
||
db_name = '{}_{}'.format(db_name[:9], GetRandomAlnumLower(6))
|
||
continue
|
||
break
|
||
|
||
return db_name
|
||
|
||
|
||
# 优先使用新名称
|
||
def ensure_unique_db_name2(db_name: str) -> str:
|
||
while True:
|
||
prefix = db_name[:9].lower()
|
||
suffix = GetRandomAlnumLower(6)
|
||
new_db_name = f'{prefix}_{suffix}'
|
||
|
||
if not S('databases').where('name', new_db_name).exists():
|
||
return new_db_name
|
||
|
||
def pkcs7_padding(data: bytes, block_size: int = 16) -> bytes:
|
||
length = len(data)
|
||
amount_to_pad = block_size - (length % block_size)
|
||
|
||
if amount_to_pad == 0:
|
||
amount_to_pad = block_size
|
||
|
||
return data + bytes([amount_to_pad] * amount_to_pad)
|
||
|
||
|
||
def pkcs7_unpadding(data: bytes, block_size: int = 16) -> bytes:
|
||
pad = data[-1]
|
||
|
||
if pad < 1 or pad > block_size:
|
||
pad = 0
|
||
|
||
return data[:-pad]
|
||
|
||
|
||
# 使用雪花算法生成ID
|
||
def snow_flake(machine_id: int = 0) -> int:
|
||
last_snow_flake_time_file = '/dev/shm/__snow_flake_last_time__'
|
||
snow_flake_sequence_file = '/dev/shm/__snow_flake_sequence__'
|
||
|
||
cur_snow_flake_time = int(time.time() * 1000)
|
||
|
||
with open(last_snow_flake_time_file, 'a+') as fp, open(snow_flake_sequence_file, 'a+') as fp2:
|
||
fcntl.flock(fp.fileno(), fcntl.LOCK_SH)
|
||
fcntl.flock(fp2.fileno(), fcntl.LOCK_EX)
|
||
|
||
fp.seek(0)
|
||
fp2.seek(0)
|
||
|
||
s1 = fp.read().strip()
|
||
s2 = fp2.read().strip()
|
||
|
||
if s1 == '':
|
||
s1 = '0'
|
||
|
||
if s2 == '':
|
||
s2 = '0'
|
||
|
||
last_snow_flake_time = int(s1)
|
||
snow_flake_sequence = int(s2)
|
||
|
||
fp2.seek(0)
|
||
fp2.truncate()
|
||
|
||
if cur_snow_flake_time == last_snow_flake_time:
|
||
snow_flake_sequence += 1
|
||
fp2.write(str(snow_flake_sequence))
|
||
else:
|
||
snow_flake_sequence = 0
|
||
fp2.write(str(snow_flake_sequence))
|
||
fcntl.flock(fp.fileno(), fcntl.LOCK_EX)
|
||
fp.seek(0)
|
||
fp.truncate()
|
||
fp.write(str(cur_snow_flake_time))
|
||
|
||
return (int(cur_snow_flake_time - 1739376000000) << 22) | ((int(machine_id) & ((1 << 10) - 1)) << 12) | (int(snow_flake_sequence) & ((1 << 12) - 1))
|
||
|
||
|
||
# 根据时间区间生成日期字符串序列
|
||
def gen_date_sequence_by_time_section(start_time: int = -1, end_time: int = -1, date_format: str = '%Y%m%d'):
|
||
if end_time < 1:
|
||
end_time = int(time.time())
|
||
|
||
if end_time < start_time:
|
||
raise ValueError(lang('end_time must greater than start_time'))
|
||
|
||
for i in range(start_time, end_time + (end_time % 86400), 86400):
|
||
yield datetime.fromtimestamp(i).strftime(date_format)
|
||
|
||
|
||
def check_field_exists(db_obj,table_name, field_name):
|
||
"""
|
||
@name 检查表字段是否存在
|
||
@param db_obj 数据库对象
|
||
@param table_name 表名
|
||
@param field_name 要检查的字段
|
||
"""
|
||
try:
|
||
res = db_obj.query("PRAGMA table_info({})".format(table_name))
|
||
for val in res:
|
||
if field_name == val[1]:
|
||
return True
|
||
except:
|
||
pass
|
||
return False
|
||
|
||
|
||
|
||
def query_dns(domain, dns_type="A", is_root=False):
|
||
"""
|
||
@name 查询域名DNS解析
|
||
@author cjxin<2020-12-17>
|
||
@param domain {string} 被验证的根域名
|
||
@param dns_type {string} dns记录
|
||
@param is_root {bool} 是否查询根域名
|
||
@return void
|
||
"""
|
||
try:
|
||
import dns.resolver
|
||
except:
|
||
os.system("{} -m pip install dnspython".format(get_python_bin()))
|
||
import dns.resolver
|
||
|
||
if is_root: domain, zone = get_root_domain(domain)
|
||
try:
|
||
ret = dns.resolver.query(domain, dns_type)
|
||
data = []
|
||
for i in ret.response.answer:
|
||
for j in i.items:
|
||
tmp = {
|
||
"flags": j.flags,
|
||
"tag": j.tag.decode(),
|
||
"value": j.value.decode(),
|
||
}
|
||
data.append(tmp)
|
||
return data
|
||
except:
|
||
return False
|
||
|
||
# 解析journalctl --disk-usage 获取的容量
|
||
def parse_journal_disk_usage(output):
|
||
import re
|
||
# 使用正则表达式来提取数字和单位
|
||
match = re.search(r'take up (\d+(\.\d+)?)\s*([KMGTP]?)', output)
|
||
total_bytes = 0
|
||
if match:
|
||
value = float(match.group(1)) # 数字
|
||
unit = match.group(3) # 单位
|
||
# 将所有单位转换为字节
|
||
if unit == '':
|
||
unit_value = 1
|
||
elif unit == 'K':
|
||
unit_value = 1024
|
||
elif unit == 'M':
|
||
unit_value = 1024 * 1024
|
||
elif unit == 'G':
|
||
unit_value = 1024 * 1024 * 1024
|
||
elif unit == 'T':
|
||
unit_value = 1024 * 1024 * 1024 * 1024
|
||
elif unit == 'P':
|
||
unit_value = 1024 * 1024 * 1024 * 1024 * 1024
|
||
else:
|
||
unit_value = 0
|
||
|
||
# 计算总字节数
|
||
total_bytes = value * unit_value
|
||
return total_bytes
|
||
|
||
# 将图片文件转换为Base64编码字符串
|
||
def image_to_base64(image_path):
|
||
import base64
|
||
try:
|
||
if not os.path.exists(image_path):
|
||
return ''
|
||
|
||
# 获取图片MIME类型
|
||
ext = os.path.splitext(image_path)[1].lower()
|
||
mime_types = {
|
||
'.jpg': 'image/jpeg',
|
||
'.jpeg': 'image/jpeg',
|
||
'.png': 'image/png',
|
||
'.gif': 'image/gif',
|
||
'.svg': 'image/svg+xml',
|
||
'.webp': 'image/webp',
|
||
# # 视频类型, 暂时不支持
|
||
# '.mp4': 'video/mp4',
|
||
# '.webm': 'video/webm',
|
||
# '.mov': 'video/quicktime'
|
||
}
|
||
mime_type = mime_types.get(ext, 'application/octet-stream')
|
||
|
||
# 读取并编码图片
|
||
with open(image_path, 'rb') as f:
|
||
img_data = f.read()
|
||
base64_str = base64.b64encode(img_data).decode('utf-8')
|
||
|
||
return f"data:{mime_type};base64,{base64_str}"
|
||
except Exception as e:
|
||
return ''
|
||
|
||
# 自定义进度线程文件锁
|
||
def progress_acquire_lock(lock_file):
|
||
"""
|
||
检查锁文件状态并获取锁:
|
||
- 锁不存在 → 获取锁
|
||
- 锁存在但文件为空 → 获取锁
|
||
- 锁存在且有内容 → 检查记录的线程ID是否存在
|
||
- 线程不存在 → 获取锁
|
||
- 线程存在 → 不允许获取
|
||
"""
|
||
try:
|
||
# 检查锁文件是否存在
|
||
if os.path.exists(lock_file):
|
||
# 读取锁文件内容
|
||
with open(lock_file, 'r') as f:
|
||
thread_id_str = f.read().strip()
|
||
|
||
# 情况1:文件为空,允许获取锁
|
||
if not thread_id_str:
|
||
# 清理空锁文件并创建新锁
|
||
os.remove(lock_file)
|
||
|
||
# 情况2:文件不为空,检查线程ID是否有效
|
||
else:
|
||
try:
|
||
thread_id = int(thread_id_str)
|
||
# 检查线程是否存在
|
||
thread_exists = any(
|
||
thread.ident == thread_id
|
||
for thread in threading.enumerate() if thread
|
||
)
|
||
# 线程存在则不允许获取锁
|
||
if thread_exists:
|
||
return False
|
||
# 线程不存在则清理旧锁
|
||
else:
|
||
os.remove(lock_file)
|
||
except ValueError:
|
||
# 线程ID格式无效,视为无效锁
|
||
if os.path.exists(lock_file):
|
||
os.remove(lock_file)
|
||
|
||
# 创建新锁文件并写入当前线程ID
|
||
with open(lock_file, 'w') as f:
|
||
f.write('')
|
||
f.flush()
|
||
os.fsync(f.fileno()) # 确保写入磁盘
|
||
return True
|
||
|
||
except Exception:
|
||
# 任何异常都视为获取锁失败
|
||
return False
|
||
|
||
# 释放进度线程文件锁
|
||
def progress_release_lock(lock_file):
|
||
if os.path.exists(lock_file):
|
||
try:
|
||
os.remove(lock_file)
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def find_value_by_key(data: dict | List[dict], key: str, default: Any = None) -> Any:
|
||
"""返回嵌套dict中第一个匹配到的key的value, 找不到返default"""
|
||
try:
|
||
import jsonpath
|
||
except ImportError:
|
||
os.system("{} -m pip install jsonpath".format(get_python_bin()))
|
||
import jsonpath
|
||
|
||
matches = jsonpath.jsonpath(data, f'$..{key}')
|
||
if matches:
|
||
return matches[0]
|
||
return default
|
||
|
||
|
||
def split_domain_sld(domain: str):
|
||
"""分离域名的顶级域名, 子域名"""
|
||
if not domain or not isinstance(domain, str):
|
||
return domain, ""
|
||
|
||
from pubsuffix import get_tld
|
||
|
||
tld = get_tld(domain)
|
||
parts = domain.strip('.').split('.')
|
||
num_of_tld_parts = 0 if tld is None else tld.count('.') + 1
|
||
|
||
# 当域名的子域名数量小于等于TLD数量+1时,则返回整个域名,
|
||
# 如: "baidu.com.cn" 返回 ("baidu.com.cn", "") tld 部分是"com.cn"
|
||
if len(parts) <= num_of_tld_parts + 1:
|
||
return domain, ""
|
||
else:
|
||
return ".".join(parts[-num_of_tld_parts-1:]), ".".join(parts[:-num_of_tld_parts-1])
|
||
|
||
# 获取多服务状态
|
||
def get_multi_webservice_status():
|
||
nginxSbin = '{}/nginx/sbin/nginx'.format(get_setup_path())
|
||
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
|
||
olsBin = '/usr/local/lsws/bin/lswsctrl'
|
||
|
||
if os.path.exists(nginxSbin) and os.path.exists(apacheBin) and os.path.exists(olsBin):
|
||
return True
|
||
return False
|
||
|
||
# 获取已存在的服务
|
||
def get_multi_webservice_list() -> list:
|
||
nginxSbin = '{}/nginx/sbin/nginx'.format(get_setup_path())
|
||
apacheBin = '{}/apache/bin/apachectl'.format(get_setup_path())
|
||
olsBin = '/usr/local/lsws/bin/lswsctrl'
|
||
|
||
server_list = []
|
||
if os.path.exists(nginxSbin):
|
||
server_list.append('nginx')
|
||
|
||
if os.path.exists(apacheBin):
|
||
server_list.append('apache')
|
||
|
||
if os.path.exists(olsBin):
|
||
server_list.append('openlitespeed')
|
||
|
||
return server_list
|
||
|
||
# 操作指定web服务
|
||
def webservice_operation(service: str, type = 'restart') -> bool:
|
||
"""
|
||
service 服务名称
|
||
type 类型:关闭,重启,开启
|
||
"""
|
||
try:
|
||
from class_v2 import system_v2
|
||
server_restart = system_v2.system()
|
||
get = public.to_dict_obj({
|
||
'name': service,
|
||
'type': type
|
||
})
|
||
ok = server_restart.ServiceAdmin(get)
|
||
|
||
if ok.get('status') == 0:
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
print(e)
|
||
return False
|
||
|
||
# Base64URL 编码
|
||
def base64url_encode(data: bytes) -> str:
|
||
import base64
|
||
return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')
|
||
|
||
# Base64URL 解码
|
||
def base64url_decode(data: str) -> bytes:
|
||
import base64
|
||
padding = '=' * (4 - (len(data) % 4)) if len(data) % 4 != 0 else ''
|
||
return base64.urlsafe_b64decode(data + padding)
|
||
|
||
|
||
def cp_dir(src: str, dst: str, ignores: List[str] | Set[str] = None, overwrite: bool = True, ) -> None:
|
||
"""
|
||
site 递归复制文件夹
|
||
src: 源路径
|
||
dst: 目标路径
|
||
ignore: 忽略的 list
|
||
overwrite: 存在时是否覆盖
|
||
|
||
ps: ignore内的每个对象支持:
|
||
*:匹配任意数量的任意字符(包括零个字符)。
|
||
?:匹配任意单个字符。
|
||
[abc]:匹配序列 abc中的任意字符。
|
||
[!abc]:匹配不在序列 abc中的任意字符。
|
||
"""
|
||
if not os.path.exists(src):
|
||
return
|
||
|
||
overwrite = False if isinstance(overwrite, int) and overwrite == 0 else True
|
||
ignores = set(ignores) if ignores else set()
|
||
|
||
# 确保目标目录存在
|
||
if not os.path.exists(dst):
|
||
os.makedirs(dst, 0o755, exist_ok=True)
|
||
|
||
def _copy2(src_file: str, dst_file: str):
|
||
if overwrite or not os.path.exists(dst_file):
|
||
try:
|
||
shutil.copy2(src_file, dst_file)
|
||
except Exception:
|
||
lock = False
|
||
try:
|
||
a, e = ExecShell(f"lsattr -d {dst_file}")
|
||
if not e and "i" in a:
|
||
lock = True
|
||
ExecShell(f"chattr -i {dst_file}")
|
||
shutil.copy2(src_file, dst_file)
|
||
except:
|
||
pass
|
||
finally:
|
||
if lock is True and os.path.exists(dst_file):
|
||
ExecShell(f"chattr +i {dst_file}")
|
||
|
||
# 复制源目录下的所有内容到目标目录
|
||
for item in os.listdir(src):
|
||
src_item = os.path.join(src, item)
|
||
dst_item = os.path.join(dst, item)
|
||
if ignores and any(fnmatch.fnmatch(item, ignore) for ignore in ignores):
|
||
continue
|
||
if os.path.isdir(src_item):
|
||
cp_dir(src_item, dst_item, ignores, overwrite)
|
||
else:
|
||
_copy2(src_item, dst_item)
|
||
|
||
# 解压到指定目录下
|
||
def extract_archive_to_target(archive_path, target_dir):
|
||
"""
|
||
解压压缩包到指定目标目录,支持多种格式并自动处理目录结构
|
||
:param archive_path: 压缩包文件路径(如 /path/to/file.zip)
|
||
:param target_dir: 目标解压目录(如 /path/to/target)
|
||
:return: (bool, str) 成功返回 (True, ''),失败返回 (False, 错误信息)
|
||
"""
|
||
import tempfile
|
||
import zipfile
|
||
import tarfile
|
||
|
||
# 校验输入
|
||
if not os.path.exists(archive_path):
|
||
return False, f"The compressed package does not exist: {archive_path}"
|
||
if not os.path.isfile(archive_path):
|
||
return False, f"Not a valid file: {archive_path}"
|
||
|
||
# 确保目标目录存在
|
||
os.makedirs(target_dir, exist_ok=True)
|
||
|
||
# 获取文件后缀
|
||
file_ext = os.path.splitext(archive_path)[1].lower()
|
||
supported_ext = ['.zip', '.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.gz']
|
||
if file_ext not in supported_ext:
|
||
return False, f"Unsupported formats are supported: {', '.join(supported_ext)}"
|
||
|
||
# 创建临时目录处理目录结构
|
||
temp_dir = tempfile.mkdtemp()
|
||
try:
|
||
# 解压到临时目录
|
||
try:
|
||
if file_ext == '.zip':
|
||
with zipfile.ZipFile(archive_path, 'r') as zf:
|
||
zf.extractall(temp_dir)
|
||
else:
|
||
mode = 'r'
|
||
if file_ext in ['.tar.gz', '.tgz', '.gz']:
|
||
mode = 'r:gz'
|
||
elif file_ext in ['.tar.bz2', '.tbz2']:
|
||
mode = 'r:bz2'
|
||
with tarfile.open(archive_path, mode) as tf:
|
||
tf.extractall(temp_dir)
|
||
except Exception as e:
|
||
return False, f"Decompression failed: {str(e)}"
|
||
|
||
# 检查临时目录是否为空
|
||
temp_items = os.listdir(temp_dir)
|
||
if not temp_items:
|
||
return False, 'The content of the compressed package is empty.'
|
||
|
||
# 统计临时目录内的目录和文件数量,判断结构
|
||
dir_count = 0
|
||
file_count = 0
|
||
first_dir = None
|
||
for item in temp_items:
|
||
item_path = os.path.join(temp_dir, item)
|
||
if os.path.isdir(item_path):
|
||
dir_count += 1
|
||
if first_dir is None:
|
||
first_dir = item_path
|
||
else:
|
||
file_count += 1
|
||
|
||
# 处理单层根目录结构
|
||
if dir_count == 1 and file_count == 0:
|
||
# 只有一个目录 取该目录内容
|
||
content_dir = first_dir
|
||
else:
|
||
# 取临时目录全部内容
|
||
content_dir = temp_dir
|
||
|
||
# 复制内容到目标目录
|
||
for item in os.listdir(content_dir):
|
||
src = os.path.join(content_dir, item)
|
||
dst = os.path.join(target_dir, item)
|
||
try:
|
||
if os.path.isdir(src):
|
||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||
else:
|
||
shutil.copy2(src, dst)
|
||
except Exception as e:
|
||
return False, f"The file copy failed: {str(e)}"
|
||
|
||
return True, ""
|
||
|
||
finally:
|
||
# 清理临时目录
|
||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||
|
||
# 任意站点申请ssl装饰器
|
||
def try_to_apply_ssl(func):
|
||
"""
|
||
返回消息体中带有约定字段 'ssl_site_id' 则尝试申请ssl, 支持 ip ssl,域名ssl
|
||
尝试创建关联的dns记录.
|
||
关联site, 在site list页面中显示进度,日志
|
||
"""
|
||
def wrapper(*args, **kwargs):
|
||
result = func(*args, **kwargs)
|
||
ssl_site_id = find_value_by_key(result, key="ssl_site_id", default=None)
|
||
if ssl_site_id:
|
||
try:
|
||
print_log(f"Trying To Auto Apply SSL For The Site... Site ID is: {ssl_site_id}")
|
||
from ssl_domainModelV2.service import apply_ssl_with_siteId
|
||
apply_ssl_with_siteId(int(ssl_site_id))
|
||
except Exception as e:
|
||
import traceback
|
||
print_log(traceback.format_exc())
|
||
print_log(f"Auto apply SSL failed: {str(e)}")
|
||
return result
|
||
return wrapper
|
||
|
||
# 清理临时文件缓存
|
||
def clear_tmp_file() -> None:
|
||
"""
|
||
@name 清理临时文件缓存
|
||
@return void
|
||
"""
|
||
# 清理缓存
|
||
compress_caches_path = '{}/data/compress_caches'.format(get_panel_path())
|
||
if os.path.exists(compress_caches_path):
|
||
dir_list = os.listdir(compress_caches_path)
|
||
for i in dir_list:
|
||
os.system('rm -rf {}/{}'.format(compress_caches_path, i))
|
||
# 清理无用的js文件
|
||
vite_path = 'YakPanel/static/vite/js'
|
||
if os.path.exists(vite_path):
|
||
if os.path.exists('{}/loginLogs.js.map'.format(vite_path)):
|
||
os.system('rm -f {}/*.map'.format(vite_path))
|
||
os.system('rm -f {}/*.gz'.format(vite_path))
|
||
# 清理临时文件
|
||
tmp_path = '{}/tmp'.format(get_panel_path())
|
||
if os.path.exists(tmp_path):
|
||
dir_list = os.listdir(tmp_path)
|
||
for i in dir_list:
|
||
os.system('rm -rf {}/{}'.format(tmp_path, i))
|
||
temp_path = '{}/temp'.format(get_panel_path())
|
||
if os.path.exists(temp_path):
|
||
dir_list = os.listdir(temp_path)
|
||
for i in dir_list:
|
||
os.system('rm -rf {}/{}'.format(temp_path, i))
|
||
|
||
# 检查磁盘可用状态
|
||
def check_disk_status() -> tuple[int, str]:
|
||
"""
|
||
@name 检查磁盘状态
|
||
@return int 1:正常 2:磁盘已满 3:磁盘不可写 4:Inode不足
|
||
"""
|
||
panel_path = get_panel_path()
|
||
# 检查Inode是否足够
|
||
inode = os.statvfs(panel_path)
|
||
if inode.f_files > 0 and inode.f_favail < 2:
|
||
return 4, public.lang(
|
||
"Disk Inode is exhausted, unable to access the panel database. "
|
||
"Please log in to SSH to manually clean up the disk and try again!"
|
||
)
|
||
|
||
# 检查磁盘是否已满
|
||
disk = psutil.disk_usage(panel_path)
|
||
if disk.free < 1024 * 512:
|
||
return 2, public.lang(
|
||
"Disk space is insufficient, unable to access the panel database. "
|
||
"Please log in to SSH to manually clean up the disk and try again!"
|
||
)
|
||
|
||
# 检查磁盘是否可写
|
||
test_file = '{}/data/db/test.txt'.format(panel_path)
|
||
if writeFile(test_file, 'test'):
|
||
if os.path.exists(test_file):
|
||
try:
|
||
os.remove(test_file)
|
||
except Exception as e:
|
||
print_log("public.check_disk_status error: {}".format(str(e)))
|
||
else:
|
||
return 3, public.lang(
|
||
"Detected that the panel directory is not writable, unable to access the panel database. "
|
||
"Please login to SSH to check, ensure that the 'panel/data' directory is writable, and then try again!"
|
||
)
|
||
|
||
# 磁盘状态正常
|
||
return 1, ''
|
||
|
||
# session过期时间
|
||
def get_session_expired() -> int:
|
||
sess_expired = 86400 # default 24H
|
||
sess_expired_path = os.path.join(get_panel_path(), "data/session_timeout.pl")
|
||
try:
|
||
if os.path.exists(sess_expired_path):
|
||
sess_expired_content = readFile(sess_expired_path)
|
||
if sess_expired_content and sess_expired_content.strip().isdigit():
|
||
sess_expired = int(sess_expired_content.strip())
|
||
else:
|
||
raise Exception
|
||
else:
|
||
raise Exception
|
||
except Exception:
|
||
sess_expired = max(300, sess_expired) # min 300s
|
||
sess_expired = min(sess_expired, 86400 * 30) # max 30 days
|
||
writeFile(sess_expired_path, str(sess_expired))
|
||
return sess_expired
|
||
|
||
# 通用的杀死进程
|
||
def kill_process_strictly(target, force=False):
|
||
"""
|
||
参数:
|
||
:param target: 进程名(str)或进程号(int/str)。必须完全匹配,不支持模糊搜索。
|
||
:param force: 布尔值。True 为强制杀死 (kill),False 为温和终止 (terminate)。
|
||
|
||
返回:
|
||
:return: 成功处理的进程数量 (int)
|
||
"""
|
||
count = 0
|
||
# 预处理 target 格式
|
||
target_is_num = isinstance(target, int) or (isinstance(target, str) and target.isdigit())
|
||
target_pid = int(target) if target_is_num else None
|
||
target_name = str(target).lower() if not target_is_num else None
|
||
|
||
for proc in psutil.process_iter(['pid', 'name']):
|
||
try:
|
||
is_match = False
|
||
|
||
if target_is_num:
|
||
# 严格匹配 PID
|
||
if proc.info['pid'] == target_pid:
|
||
is_match = True
|
||
else:
|
||
# 严格匹配进程名(去除两端空格,忽略大小写)
|
||
if proc.info['name'].lower() == target_name:
|
||
is_match = True
|
||
|
||
if is_match:
|
||
p = psutil.Process(proc.info['pid'])
|
||
if force:
|
||
p.kill()
|
||
else:
|
||
p.terminate()
|
||
|
||
# 等待确认进程已退出
|
||
p.wait(timeout=3)
|
||
count += 1
|
||
|
||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||
continue
|
||
except psutil.TimeoutExpired:
|
||
continue
|
||
|
||
return count
|
||
|
||
# 获取多服务默认创建状态
|
||
def get_default_site_conf():
|
||
conf_path = '/www/server/panel/data/multi_webservice_status.conf'
|
||
default = {
|
||
'php': 'nginx',
|
||
'wp': 'openlitespeed'
|
||
}
|
||
|
||
conf = public.readFile(conf_path)
|
||
if conf:
|
||
try:
|
||
default = json.loads(conf)
|
||
except:
|
||
pass
|
||
|
||
return default |