From ed3e3336b2c1068db73083025055d5113b15970a Mon Sep 17 00:00:00 2001 From: midoks Date: Thu, 13 Apr 2023 15:28:38 +0800 Subject: [PATCH 1/8] Update push_notice_msg.py --- plugins/tgbot/startup/extend/push_notice_msg.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/tgbot/startup/extend/push_notice_msg.py b/plugins/tgbot/startup/extend/push_notice_msg.py index a2bcbf7b07..be99cfec7a 100644 --- a/plugins/tgbot/startup/extend/push_notice_msg.py +++ b/plugins/tgbot/startup/extend/push_notice_msg.py @@ -82,11 +82,6 @@ def send_msg(bot, tag='ad', trigger_time=300): msg_notice = "由于在解决的问题的时候,不给信息,无法了解情况。以后不再群里回答技术问题。全部去论坛提问。在解决问题的过程中,可能需要面板信息,和SSH信息,如无法提供请不要提问。为了让群里都知晓。轮播一年!\n" msg_notice += "为了不打扰双方,私聊解决问题先转100U,否则无视!\n" - msg_notice += "现阶段3件事:\n" - msg_notice += "* 炒币\n" - msg_notice += "* 量化交易\n" - msg_notice += "* MW-VIP系统开发\n" - msg_notice += "至少1年时间都没空。非诚勿扰!" msg = bot.send_message(chat_id, msg_notice, reply_markup=markup) # print(msg.message_id) From 528094cfa7482ff15e8092f76e7ae48b631f935e Mon Sep 17 00:00:00 2001 From: midoks Date: Thu, 13 Apr 2023 15:33:15 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E7=BD=91=E7=AB=99=E9=98=B2=E7=AF=A1?= =?UTF-8?q?=E6=94=B9=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - plugins/system_safe/README.md | 19 - plugins/tamper_proof_py/conf/config.json | 5 + plugins/tamper_proof_py/ico.png | Bin 0 -> 556 bytes plugins/tamper_proof_py/index.html | 1163 +++++++++++++++++ plugins/tamper_proof_py/index.py | 628 +++++++++ plugins/tamper_proof_py/info.json | 15 + .../init.d/tamper_proof_py.service.tpl | 15 + .../init.d/tamper_proof_py.tpl | 95 ++ plugins/tamper_proof_py/install.sh | 52 + .../tamper_proof_py/tamper_proof_service.py | 552 ++++++++ 11 files changed, 2525 insertions(+), 20 deletions(-) delete mode 100644 plugins/system_safe/README.md create mode 100644 plugins/tamper_proof_py/conf/config.json create mode 100644 plugins/tamper_proof_py/ico.png create mode 100755 plugins/tamper_proof_py/index.html create mode 100755 plugins/tamper_proof_py/index.py create mode 100644 plugins/tamper_proof_py/info.json create mode 100644 plugins/tamper_proof_py/init.d/tamper_proof_py.service.tpl create mode 100644 plugins/tamper_proof_py/init.d/tamper_proof_py.tpl create mode 100755 plugins/tamper_proof_py/install.sh create mode 100644 plugins/tamper_proof_py/tamper_proof_service.py diff --git a/.gitignore b/.gitignore index 466df64ae1..87a7110974 100644 --- a/.gitignore +++ b/.gitignore @@ -162,7 +162,6 @@ plugins/my_* plugins/l2tp plugins/openlitespeed plugins/tamper_proof -plugins/tamper_proof_* plugins/cryptocurrency_trade plugins/op_load_balance plugins/gdrive diff --git a/plugins/system_safe/README.md b/plugins/system_safe/README.md deleted file mode 100644 index 18671a83b2..0000000000 --- a/plugins/system_safe/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# plugins_system_safe - -系统加固插件 - -# DEBUG -``` - -https://code_hosting_007.midoks.me/midoks/plugins_system_safe - -ln -s /www/git/midoks/plugins_system_safe /www/server/mdserver-web/plugins/system_safe - -ln -s /www/wwwroot/midoks/plugins_system_safe /www/server/mdserver-web/plugins/system_safe - - -python3 /www/server/mdserver-web/plugins/system_safe/index.py bg_start - -systemctl daemon-reload -``` - diff --git a/plugins/tamper_proof_py/conf/config.json b/plugins/tamper_proof_py/conf/config.json new file mode 100644 index 0000000000..8a372031a3 --- /dev/null +++ b/plugins/tamper_proof_py/conf/config.json @@ -0,0 +1,5 @@ +{ + "open": true, + "excludePath": [ "cache", "threadcache", "log", "logs", "config", "runtime", "temp","tmp","caches","tmps"], + "protectExt": [ "php", "html", "htm", "shtml", "tpl", "js", "css", "jsp", "do" ] +} \ No newline at end of file diff --git a/plugins/tamper_proof_py/ico.png b/plugins/tamper_proof_py/ico.png new file mode 100644 index 0000000000000000000000000000000000000000..3dede4e917b29139828a3a76016c5ae7a642cbe9 GIT binary patch literal 556 zcmeAS@N?(olHy`uVBq!ia0vp^T0pGI!3-psy}S2-sA4D25DpHG+YkL80J+HlJ|V9E z|NlRH_|pFU=lAYCwtf3?hD{6*#`f)}fm|R7JfOZ)kBo;EFR)g$zhVaDG}zd16s2gJVj5 zQmTSmW>IQ+eo=O@f^)Fhi#?lqfa+d)x;Tbd^uE3NkgrKWg!RGVwA9F1Gn^u)f3H7v z!0F`EsaxyHmicV&FWi~Op>XTz=aWJ`D?83x7Hv}5we54@F{@Q8Sf)R5yZL&e#=%=_ zHX6F-CMEoi(+UVS40y0iAh7$!nyqs~qGs%!BDYq{c;}n<6+zi{3v+5tm0Vr+?M0Fs zkbd?nc5VGH>sjHy1FX^}E?d6La;c)p+miHkCJkJ*EPYWd7oO!EuNHP#Dsyc9#csKx z#1*y;TSV4-oV_r0>7l)xeQtfFVdQ&MBb@0G~ +/*防篡改*/ +.anti-switch { + margin-left: 20px; + margin-top: 4px; +} + +.anti_lib_tit { + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: #ddd 1px solid; +} + +.anti_lib_con { + background-color: #FBFBFB; + border: #F0F0F0 1px solid; + padding: 15px 10px; +} + +.anti_rule_add { + margin-bottom: 10px; +} + +.anti_rule_add input { + width: 360px; +} + +.data-count-all { + background-color: #FAFAFA; + border: #ddd 1px solid; + width: 100%; + float: left; +} + +.data-count-all .data-count-box { + height: 100%; + text-align: center; + width: 20%; + float: left; + margin-bottom: 15px; +} + +.data-count-box .dname { + color: #78797D; + margin-top: 12px; + margin-bottom: 10px; +} + +.data-count-box .dval { + color: #333; +} + +.data-count-box .dval span { + font-family: arial; + color: #121313; + font-size: 20px; +} + +.anti_rule_list_type { + float: left; + width: 45%; +} + +.anti_rule_list { + width: 100%; + float: left; + margin-bottom: 20px; +} + +.search-day { + height: 32px; + margin-left: 1px; +} + +.search-day span { + float: left; + height: 32px; + line-height: 30px; + border: #ddd 1px solid; + padding: 0 20px; + margin-left: -1px; + cursor: pointer; + position: relative; +} + +.search-day span.cur { + background-color: #20a53a; + color: #fff; +} + +.search-day span.cur input, +.search-day span.cur em { + color: #666; +} + +.search-day span:last-child { + padding: 0; +} + +.search-day span input { + border: 0 none; + height: 30px; + padding: 0 10px; + width: 105px; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAHBAMAAADOnLEXAAAAA3NCSVQICAjb4U/gAAAAElBMVEX////v7++oqKiSkpJgYGAzMzNVUvUKAAAABnRSTlMA//////96eeD+AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAFnRFWHRDcmVhdGlvbiBUaW1lADA3LzEzLzE442/mwwAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAApSURBVAiZYxBiAAJFBhEDBgZmRwbmYAYGUwMQBrGAXBAHyAVxgFwgBwBYpgOoNMjLNgAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + background-position: 86px center; +} + +.search-day span input:active { + border: 0 none; +} + +.search-day span.cur input { + color: #fff; + background-color: #20a53a; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAHBAMAAADOnLEXAAAAA3NCSVQICAjb4U/gAAAAElBMVEX////f8+Pg8+Sx2LghpTsgpTp3yIRgAAAACXBIWXMAAA6cAAAOnAEHlFPdAAAAFnRFWHRDcmVhdGlvbiBUaW1lADA3LzEzLzE442/mwwAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAAxSURBVAiZY1B2DQ0NNWJQMA0NDWZkCGYODTUwZQBiIIshNJjZwBRIhRoAhYFUMFARAPlECn96zZKZAAAAAElFTkSuQmCC"); +} + +.total-all{ + overflow: hidden; +} + +.anti-open { + position: absolute; + top: 16px; + left: 300px; + line-height: 32px; +} + +.bt-w-main { + height: 610px; +} + +.nowrap_block { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[name="status"] option{ + padding: 5px; +} +/* 模拟攻击 */ +.mtl0 { + margin-top: 0; + margin-left: 15px; +} +/* end */ + +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/plugins/tamper_proof_py/index.py b/plugins/tamper_proof_py/index.py new file mode 100755 index 0000000000..446dee0c36 --- /dev/null +++ b/plugins/tamper_proof_py/index.py @@ -0,0 +1,628 @@ +# coding:utf-8 + +import sys +import io +import os +import time +import json +import re +import psutil +from datetime import datetime + +sys.dont_write_bytecode = True +sys.path.append(os.getcwd() + "/class/core") +import mw + +app_debug = False +if mw.isAppleSystem(): + app_debug = True + + +class App: + + __total = 'total.json' + __sites = [] + + def __init__(self): + pass + + def getPluginName(self): + return 'tamper_proof_py' + + def getPluginDir(self): + return mw.getPluginDir() + '/' + self.getPluginName() + + def getServerDir(self): + return mw.getServerDir() + '/' + self.getPluginName() + + def getInitDFile(self): + if app_debug: + return '/tmp/' + self.getPluginName() + return '/etc/init.d/' + self.getPluginName() + + def getInitDTpl(self): + path = self.getPluginDir() + "/init.d/" + self.getPluginName() + ".tpl" + return path + + def getArgs(self): + args = sys.argv[2:] + tmp = {} + args_len = len(args) + + if args_len == 1: + t = args[0].strip('{').strip('}') + if t.strip() == '': + tmp = [] + else: + t = t.split(':') + tmp[t[0]] = t[1] + elif args_len > 1: + for i in range(len(args)): + t = args[i].split(':') + tmp[t[0]] = t[1] + return tmp + + def checkArgs(self, data, ck=[]): + for i in range(len(ck)): + if not ck[i] in data: + return (False, mw.returnJson(False, '参数:(' + ck[i] + ')没有!')) + return (True, mw.returnJson(True, 'ok')) + + def getTotal(self, siteName=None, day=None): + defaultTotal = {"total": 0, "delete": 0, + "create": 0, "modify": 0, "move": 0} + if siteName: + total = {} + total_path = self.getServerDir() + '/sites/' + siteName + '/' + self.__total + if not os.path.exists(total_path): + total['site'] = defaultTotal + else: + total_data = mw.readFile(total_path) + if total_data['site']: + total['site'] = json.loads(total_data['site']) + else: + total['site'] = defaultTotal + + if not day: + day = time.strftime("%Y-%m-%d", time.localtime()) + total_day_path = self.getServerDir() + '/sites/' + siteName + '/day/total.json' + if not os.path.exists(total_day_path): + total['day'] = defaultTotal + else: + total['day'] = mw.readFile(total_day_path) + if total['day']: + total['day'] = json.loads(total['day']) + else: + total['day'] = defaultTotal + else: + filename = self.getServerDir() + '/sites/' + self.__total + if os.path.exists(filename): + total = json.loads(mw.readFile(filename)) + else: + total = defaultTotal + return total + + def getSites(self): + sites_path = self.getServerDir() + '/sites.json' + t = mw.readFile(sites_path) + if not os.path.exists(sites_path) or not t: + mw.writeFile(sites_path, '[]') + data = json.loads(mw.readFile(sites_path)) + + is_write = False + rm_keys = ['lock', 'bak_open'] + for i in data: + i_keys = i.keys() + if not 'open' in i_keys: + i['open'] = False + for o in rm_keys: + if o in i_keys: + if i[o]: + i['open'] = True + i.pop(o) + is_write = True + if is_write: + mw.writeFile(sites_path, json.dumps(data)) + + self.__sites = data + return data + + def writeSites(self, data): + mw.writeFile(self.getServerDir() + '/sites.json', json.dumps(data)) + # mw.ExecShell('/etc/init.d/bt_tamper_proof reload') + + def __getFind(self, siteName): + data = self.getSites() + for siteInfo in data: + if siteName == siteInfo['siteName']: + return siteInfo + return None + + def writeLog(self, log): + mw.writeLog('防篡改程序', log) + + def saveSiteConfig(self, siteInfo): + data = self.getSites() + for i in range(len(data)): + if data[i]['siteName'] != siteInfo['siteName']: + continue + data[i] = siteInfo + break + self.writeSites(data) + + def syncSites(self): + data = self.getSites() + sites = mw.M('sites').field('name,path').select() + + config_path = self.getPluginDir() + '/conf/config.json' + config = json.loads(mw.readFile(config_path)) + names = [] + n = 0 + + # print(config) + for siteTmp in sites: + names.append(siteTmp['name']) + siteInfo = self.__getFind(siteTmp['name']) + if siteInfo: + if siteInfo['path'] != siteTmp['path']: + siteInfo['path'] = siteTmp['path'] + self.saveSiteConfig(siteInfo) + data = self.getSites() + continue + siteInfo = {} + siteInfo['siteName'] = siteTmp['name'] + siteInfo['path'] = siteTmp['path'] + siteInfo['open'] = False + siteInfo['excludePath'] = config['excludePath'] + siteInfo['protectExt'] = config['protectExt'] + data.append(siteInfo) + n += 1 + + newData = [] + for siteInfoTmp in data: + if siteInfoTmp['siteName'] in names: + newData.append(siteInfoTmp) + else: + mw.execShell("rm -rf " + self.getServerDir() + + '/sites/' + siteInfoTmp['siteName']) + n += 1 + if n > 0: + self.writeSites(newData) + self.__sites = None + + def initDreplace(self): + file_tpl = self.getInitDTpl() + service_path = self.getServerDir() + + initD_path = service_path + '/init.d' + if not os.path.exists(initD_path): + os.mkdir(initD_path) + + # init.d + file_bin = initD_path + '/' + self.getPluginName() + if not os.path.exists(file_bin): + # initd replace + content = mw.readFile(file_tpl) + content = content.replace('{$SERVER_PATH}', service_path) + mw.writeFile(file_bin, content) + mw.execShell('chmod +x ' + file_bin) + + # systemd + # /usr/lib/systemd/system + systemDir = mw.systemdCfgDir() + systemService = systemDir + '/tamper_proof_py.service' + systemServiceTpl = self.getPluginDir() + '/init.d/tamper_proof_py.service.tpl' + if os.path.exists(systemDir) and not os.path.exists(systemService): + se_content = mw.readFile(systemServiceTpl) + se_content = se_content.replace('{$SERVER_PATH}', service_path) + mw.writeFile(systemService, se_content) + mw.execShell('systemctl daemon-reload') + + return file_bin + + def getDays(self, path): + days = [] + if not os.path.exists(path): + os.makedirs(path) + for dirname in os.listdir(path): + if dirname == '..' or dirname == '.' or dirname == 'total.json': + continue + if not os.path.isdir(path + '/' + dirname): + continue + days.append(dirname) + days = sorted(days, reverse=True) + return days + + def status(self): + ''' + 状态 + ''' + initd_file = self.getServerDir() + '/init.d/' + self.getPluginName() + if not os.path.exists(initd_file): + return 'stop' + cmd = initd_file + ' status|grep already' + data = mw.execShell(cmd) + if data[0] != '': + return 'start' + return 'stop' + + def tpOp(self, method): + file = self.initDreplace() + if not mw.isAppleSystem(): + cmd = 'systemctl ' + method + ' ' + self.getPluginName() + data = mw.execShell(cmd) + if data[1] == '': + return mw.returnJson(True, '操作成功') + return mw.returnJson(False, '操作失败') + + cmd = file + ' ' + method + data = mw.execShell(cmd) + if data[1] == '': + return mw.returnJson(True, '操作成功') + return mw.returnJson(False, '操作失败') + + def start(self): + return self.tpOp('start') + + def restart(self): + return self.tpOp('restart') + + def service_admin(self): + if mw.isAppleSystem(): + return mw.returnJson(False, '仅支持Linux!') + + args = self.getArgs() + check = self.checkArgs(args, ['serviceStatus']) + if not check[0]: + return check[1] + + method = args['serviceStatus'] + return self.tpOp(method) + + def initd_status(self): + if mw.isAppleSystem(): + return "Apple Computer does not support" + shell_cmd = 'systemctl status %s | grep loaded | grep "enabled;"' % ( + self.getPluginName()) + data = mw.execShell(shell_cmd) + if data[0] == '': + return 'fail' + return 'ok' + + def initd_install(self): + if mw.isAppleSystem(): + return "Apple Computer does not support" + + mw.execShell('systemctl enable ' + self.getPluginName()) + return 'ok' + + def initd_uninstall(self): + if mw.isAppleSystem(): + return "Apple Computer does not support" + + mw.execShell('systemctl disable ' + self.getPluginName()) + return 'ok' + + def set_site_status(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteName']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + siteInfo = self.__getFind(siteName) + if not siteInfo: + return mw.returnJson(False, '指定站点不存在!') + try: + siteInfo['open'] = not siteInfo['open'] + except: + siteInfo['open'] = not siteInfo['open'] + + m_logs = {True: '开启', False: '关闭'} + self.writeLog('%s站点[%s]防篡改保护' % + (m_logs[siteInfo['open']], siteInfo['siteName'])) + self.siteReload(siteInfo) + self.saveSiteConfig(siteInfo) + self.restart() + return mw.returnJson(True, '设置成功!') + + def get_run_logs(self): + log_file = self.getServerDir() + '/service.log' + return mw.returnJson(True, mw.getLastLine(log_file, 200)) + + # 取文件指定尾行数 + def getNumLines(self, path, num, p=1): + pyVersion = sys.version_info[0] + try: + import cgi + if not os.path.exists(path): + return "" + 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 = [] + 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:] + try: + data.append(json.loads(cgi.escape(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: + if type(t_buf) == bytes: + t_buf = t_buf.decode('utf-8') + buf = t_buf + buf + fp.seek(-to_read, 1) + if pos - to_read == 0: + buf = "\n" + buf + if not b: + break + fp.close() + except: + return [] + if len(data) >= 2000: + arr = [] + for d in data: + arr.insert(0, json.dumps(d)) + mw.writeFile(path, "\n".join(arr)) + return data + + def get_safe_logs(self): + + args = self.getArgs() + check = self.checkArgs(args, ['siteName']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + + data = {} + path = self.getPluginDir() + '/sites/' + siteName + '/day' + data['days'] = self.getDays(path) + + if not data['days']: + data['logs'] = [] + else: + p = 1 + if hasattr(args, 'p'): + p = args['p'] + + day = data['days'][0] + if hasattr(args, 'day'): + day = args['day'] + data['get_day'] = day + logs_path = path + '/' + day + '/logs.json' + data['logs'] = self.getNumLines(logs_path, 2000, int(p)) + return mw.returnJson(True, 'ok', data) + + def get_site_find(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteName']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + data = self.__getFind(siteName) + return mw.returnJson(True, 'ok', data) + + def siteReload(self, siteInfo): + cmd = "python3 {} {}".format( + mw.getPluginDir() + '/tamper_proof_service.py unlock', siteInfo['path']) + mw.execShell(cmd) + tip_file = mw.getServerDir() + '/tips/' + siteInfo['siteName'] + '.pl' + if os.path.exists(tip_file): + os.remove(tip_file) + + def remove_protect_ext(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteName', 'protectExt']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + protectExt = args['protectExt'].strip() + + siteInfo = self.__getFind(siteName) + + if not siteInfo: + return mw.returnJson(False, '指定站点不存在!') + if not protectExt: + return mw.returnJson(False, '被删除的保护列表不能为空') + + for protectExt in protectExt.split(','): + if not protectExt in siteInfo['protectExt']: + continue + siteInfo['protectExt'].remove(protectExt) + self.writeLog('站点[%s]从受保护列表中删除[.%s]' % + (siteInfo['siteName'], protectExt)) + self.siteReload(siteInfo) + self.saveSiteConfig(siteInfo) + return mw.returnJson(True, '删除成功!') + + def add_protect_ext(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteName', 'protectExt']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + protectExt = args['protectExt'].strip() + + siteInfo = self.__getFind(siteName) + if not siteInfo: + return mw.returnJson(False, '指定站点不存在!') + protectExt = protectExt.lower() + for protectExt in protectExt.split("\n"): + if protectExt[0] == '/': + if os.path.isdir(protectExt): + continue + if protectExt in siteInfo['protectExt']: + continue + siteInfo['protectExt'].insert(0, protectExt) + self.writeLog('站点[%s]添加文件类型或文件名[.%s]到受保护列表' % + (siteInfo['siteName'], protectExt)) + self.siteReload(siteInfo) + self.saveSiteConfig(siteInfo) + return mw.returnJson(True, '添加成功!') + + def add_excloud(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteName', 'excludePath']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + excludePath = args['excludePath'].strip() + siteInfo = self.__getFind(siteName) + if not siteInfo: + return mw.returnJson(False, '指定站点不存在!') + + if not excludePath: + return mw.returnJson(False, '排除内容不能为空') + + for excludePath in excludePath.split('\n'): + if not excludePath: + continue + if excludePath.find('/') != -1: + if not os.path.exists(excludePath): + continue + excludePath = excludePath.lower() + if excludePath[-1] == '/': + excludePath = excludePath[:-1] + if excludePath in siteInfo['excludePath']: + continue + siteInfo['excludePath'].insert(0, excludePath) + self.writeLog('站点[%s]添加排除目录名[%s]到排除列表' % + (siteInfo['siteName'], excludePath)) + + self.siteReload(siteInfo) + self.saveSiteConfig(siteInfo) + return mw.returnJson(True, '添加成功!') + + def remove_excloud(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteName', 'excludePath']) + if not check[0]: + return check[1] + + siteName = args['siteName'] + siteInfo = self.__getFind(siteName) + excludePath = args['excludePath'].strip() + if excludePath == '': + return mw.returnJson(False, '排除文件或目录不能为空') + if not siteInfo: + return mw.returnJson(False, '指定站点不存在!') + + for excludePath in excludePath.split(','): + if not excludePath: + continue + if not excludePath in siteInfo['excludePath']: + continue + siteInfo['excludePath'].remove(excludePath) + self.writeLog('站点[%s]从排除列表中删除目录名[%s]' % + (siteInfo['siteName'], excludePath)) + self.siteReload(siteInfo) + self.saveSiteConfig(siteInfo) + return mw.returnJson(True, '删除成功!') + + def sim_test(self): + args = self.getArgs() + check = self.checkArgs(args, ['path']) + if not check[0]: + return check[1] + + path = args['path'].strip() + if not os.path.exists(path): + return mw.returnJson(False, "此目录不存在") + + # 判断是否安装php + import site_api + php_version = site_api.site_api().getPhpVersion() + if not php_version: + return mw.returnJson(False, "未安装PHP测试失败") + + php_path = '/www/server/php/' + php_version[1]['version'] + '/bin/php' + php_name = path + "/" + str(int(time.time())) + ".php" + if os.path.exists(php_name): + mw.execShell("rm -rf %s" % php_name) + # 写入 + cmd = php_path + \ + " -r \"file_put_contents('{}','{}');\"".format(php_name, php_name) + mw.execShell(cmd) + time.sleep(0.5) + if os.path.exists(php_name): + if os.path.exists(php_name): + mw.execShell("rm -rf %s" % php_name) + return mw.returnJson(False, "拦截失败,可能未开启防篡改") + return mw.returnJson(True, "拦截成功") + + def set_site_status_all(self): + args = self.getArgs() + check = self.checkArgs(args, ['siteNames', 'siteState']) + if not check[0]: + return check[1] + + sites = self.getSites() + siteState = True if args['siteState'] == '1' else False + siteNames = json.loads(args['siteNames']) + m_logs = {True: '开启', False: '关闭'} + for i in range(len(sites)): + if sites[i]['siteName'] in siteNames: + sites[i]['open'] = siteState + self.writeLog('%s站点[%s]防篡改保护' % + (m_logs[siteState], sites[i]['siteName'])) + self.writeSites(sites) + return mw.returnJson(True, '批量设置成功') + + def get_index(self): + self.syncSites() + args = self.getArgs() + day = None + if 'day' in args: + day = args['day'] + + ser_status = self.status() + ser_status_bool = False + if ser_status == 'start': + ser_status_bool = True + data = {} + data['open'] = ser_status_bool + data['total'] = self.getTotal() + data['sites'] = self.getSites() + for i in range(len(data['sites'])): + data['sites'][i]['total'] = self.getTotal( + data['sites'][i]['siteName'], day) + return mw.returnJson(True, 'ok', data) + + def get_speed(self): + print("12") + + +if __name__ == "__main__": + func = sys.argv[1] + classApp = App() + try: + data = eval("classApp." + func + "()") + print(data) + except Exception as e: + print(mw.getTracebackInfo()) diff --git a/plugins/tamper_proof_py/info.json b/plugins/tamper_proof_py/info.json new file mode 100644 index 0000000000..4ed8adda46 --- /dev/null +++ b/plugins/tamper_proof_py/info.json @@ -0,0 +1,15 @@ +{ + "title": "网站防篡改程序[PY]", + "tip": "lib", + "name": "tamper_proof_py", + "type": "soft", + "ps": "事件型防篡改程序,可有效保护网站重要文件不被木马篡改", + "versions": "1.0", + "shell": "install.sh", + "checks": "server/tamper_proof_py", + "path": "server/tamper_proof_py", + "author": "midoks", + "home": "", + "date":"2022-01-18", + "pid":"4" +} \ No newline at end of file diff --git a/plugins/tamper_proof_py/init.d/tamper_proof_py.service.tpl b/plugins/tamper_proof_py/init.d/tamper_proof_py.service.tpl new file mode 100644 index 0000000000..75d25b0725 --- /dev/null +++ b/plugins/tamper_proof_py/init.d/tamper_proof_py.service.tpl @@ -0,0 +1,15 @@ +[Unit] +Description=tamper_proof_py server daemon +After=network.target + +[Service] +Type=forking +ExecStart={$SERVER_PATH}/init.d/tamper_proof_py start +ExecStop={$SERVER_PATH}/init.d/tamper_proof_py stop +ExecReload={$SERVER_PATH}/init.d/tamper_proof_py reload +ExecRestart={$SERVER_PATH}/init.d/tamper_proof_py restart +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/plugins/tamper_proof_py/init.d/tamper_proof_py.tpl b/plugins/tamper_proof_py/init.d/tamper_proof_py.tpl new file mode 100644 index 0000000000..ae38bab4cc --- /dev/null +++ b/plugins/tamper_proof_py/init.d/tamper_proof_py.tpl @@ -0,0 +1,95 @@ +#!/bin/bash +# chkconfig: 2345 55 25 +# description:tamper_proof_service + +### BEGIN INIT INFO +# Provides: tamper_proof_service +# Required-Start: $all +# Required-Stop: $all +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: starts tamper_proof_service +# Description: starts the tamper_proof_service +### END INIT INFO + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +mw_path={$SERVER_PATH} +rootPath=$(dirname "$mw_path") +PATH=$PATH:$mw_path/bin + +if [ -f $rootPath/mdserver-web/bin/activate ];then + source $rootPath/mdserver-web/bin/activate +fi + +# cd /www/server/mdserver-web && python3 plugins/tamper_proof_py/tamper_proof_service.py start +sys_start() +{ + isStart=$(ps aux |grep -E "(tamper_proof_service)"|grep -v grep |grep -v 'tamper_proof_py/tamper_proof_service.py start' | grep -v 'tamper_proof_py/tamper_proof_service.py reload' | grep -v 'tamper_proof_py/tamper_proof_service.py restart' | grep -v systemctl | grep -v '/bin/sh' | grep -v '/bin/bash' | awk '{print $2}'|xargs) + if [ "$isStart" == '' ];then + echo -e "Starting tamper_proof_service... \c" + cd $rootPath/mdserver-web + nohup python3 plugins/tamper_proof_py/tamper_proof_service.py start &> $mw_path/service.log & + sleep 0.5 + isStart=$(ps aux |grep -E "(tamper_proof_service)"|grep -v grep|awk '{print $2}'|xargs) + if [ "$isStart" == '' ];then + echo -e "\033[31mfailed\033[0m" + echo '------------------------------------------------------' + cat $mw_path/service.log + echo '------------------------------------------------------' + echo -e "\033[31mError: tamper_proof_service startup failed.\033[0m" + return; + fi + echo -e "\033[32mdone\033[0m" + else + echo "Starting tamper_proof_service (pid $isStart) already running" + fi +} + +sys_stop() +{ + echo -e "Stopping tamper_proof_service... \c"; + pids=$(ps aux |grep -E "(tamper_proof_service)"|grep -v grep|grep -v '/bin/bash'|grep -v systemctl | grep -v 'tamper_proof_py/tamper_proof_service.py stop' | grep -v 'tamper_proof_py/tamper_proof_service.py reload' | grep -v 'tamper_proof_py/tamper_proof_service.py restart' |awk '{print $2}'|xargs) + arr=($pids) + for p in ${arr[@]} + do + kill -9 $p + done + cd $rootPath/mdserver-web + python3 plugins/tamper_proof_py/tamper_proof_service.py stop + echo -e "\033[32mdone\033[0m" +} + +sys_status() +{ + isStart=$(ps aux |grep -E "(tamper_proof_service)"|grep -v grep|grep -v "init.d/tamper_proof_py"|grep -v systemctl|awk '{print $2}'|xargs) + if [ "$isStart" != '' ];then + echo -e "\033[32mtamper_proof_service (pid $isStart) already running\033[0m" + else + echo -e "\033[32mtamper_proof_service not running\033[0m" + fi +} + +case "$1" in + 'start') + sys_start + ;; + 'stop') + sys_stop + ;; + 'restart') + sys_stop + sleep 0.2 + sys_start + ;; + 'reload') + sys_stop + sleep 0.2 + sys_start + ;; + 'status') + sys_status + ;; + *) + echo "Usage: systemctl {start|stop|restart|reload} tamper_proof_service" + ;; +esac \ No newline at end of file diff --git a/plugins/tamper_proof_py/install.sh b/plugins/tamper_proof_py/install.sh new file mode 100755 index 0000000000..efa2d12f77 --- /dev/null +++ b/plugins/tamper_proof_py/install.sh @@ -0,0 +1,52 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +curPath=`pwd` +rootPath=$(dirname "$curPath") +rootPath=$(dirname "$rootPath") +serverPath=$(dirname "$rootPath") + +pip install pyinotify +if [ -f ${rootPath}/bin/activate ];then + source ${rootPath}/bin/activate +fi +pip install pyinotify + +install_tmp=${rootPath}/tmp/mw_install.pl +SYSOS=`uname` +VERSION=$2 +APP_NAME=tamper_proof_py + +Install_App() +{ + echo '正在安装脚本文件...' > $install_tmp + mkdir -p $serverPath/tamper_proof_py + echo "$VERSION" > $serverPath/tamper_proof_py/version.pl + echo 'install complete' > $install_tmp + + #初始化 + cd ${serverPath}/mdserver-web && python3 plugins/tamper_proof_py/index.py start $VERSION + cd ${serverPath}/mdserver-web && python3 plugins/tamper_proof_py/index.py initd_install $VERSION +} + +Uninstall_App() +{ + if [ -f /usr/lib/systemd/system/${APP_NAME}.service ] || [ -f /lib/systemd/system/${APP_NAME}.service ] ;then + systemctl stop ${APP_NAME} + systemctl disable ${APP_NAME} + rm -rf /usr/lib/systemd/system/${APP_NAME}.service + rm -rf /lib/systemd/system/${APP_NAME}.service + systemctl daemon-reload + fi + + rm -rf $serverPath/tamper_proof_py + echo "uninstall completed" > $install_tmp +} + +action=$1 +if [ "${1}" == 'install' ];then + Install_App +else + Uninstall_App +fi diff --git a/plugins/tamper_proof_py/tamper_proof_service.py b/plugins/tamper_proof_py/tamper_proof_service.py new file mode 100644 index 0000000000..61645db3a7 --- /dev/null +++ b/plugins/tamper_proof_py/tamper_proof_service.py @@ -0,0 +1,552 @@ + +# +-------------------------------------------------------------------- +# | 事件型防篡改 +# +-------------------------------------------------------------------- +import sys +import os +import pyinotify +import json +import shutil +import time +import psutil +import threading +import datetime + +sys.path.append(os.getcwd() + "/class/core") +import mw + + +class MyEventHandler(pyinotify.ProcessEvent): + _PLUGIN_PATH = '/www/server/tamper_proof_py' + _CONFIG = '/config.json' + _SITES = '/sites.json' + _SITES_DATA = None + _CONFIG_DATA = None + _DONE_FILE = None + bakcupChirdPath = [] + + def __init__(self): + self._PLUGIN_PATH = self.getServerDir() + + def getPluginName(self): + return 'tamper_proof_py' + + def getPluginDir(self): + return mw.getPluginDir() + '/' + self.getPluginName() + + def getServerDir(self): + return mw.getServerDir() + '/' + self.getPluginName() + + def rmdir(self, filename): + try: + shutil.rmtree(filename) + except: + pass + + def check_site_logs(self, Stiename, datetime_time): + ret = [] + cur_month = datetime_time.month + cur_day = datetime_time.day + cur_year = datetime_time.year + cur_hour = datetime_time.hour + cur_minute = datetime_time.minute + cur_second = int(datetime_time.second) + months = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06', 'Jul': '07', + 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'} + logs_data = self.get_site_logs(Stiename) + if not logs_data: + return False + for i2 in logs_data: + try: + i = i2.split() + # 判断状态码是否为200 + if int(i[8]) not in [200, 500]: + continue + # 判断是否为POST + day_time = i[3].split('/')[0].split('[')[1] + if int(cur_day) != int(day_time): + continue + month_time = i[3].split('/')[1] + if int(months[month_time]) != int(cur_month): + continue + year_time = i[3].split('/')[2].split(':')[0] + if int(year_time) != int(cur_year): + continue + hour_time = i[3].split('/')[2].split(':')[1] + if int(hour_time) != int(cur_hour): + continue + minute_time = i[3].split('/')[2].split(':')[2] + if int(minute_time) != int(cur_minute): + continue + second_time = int(i[3].split('/')[2].split(':')[3]) + if cur_second - second_time > 10: + continue + ret.append(i2) + except: + continue + ret2 = [] + if len(ret) > 20: + for i2 in logs_data: + try: + i = i2.split() + if i[6] != 'POST': + continue + if int(i[8]) not in [200, 500]: + continue + # 判断是否为POST + day_time = i[3].split('/')[0].split('[')[1] + if int(cur_day) != int(day_time): + continue + month_time = i[3].split('/')[1] + if int(months[month_time]) != int(cur_month): + continue + year_time = i[3].split('/')[2].split(':')[0] + if int(year_time) != int(cur_year): + continue + hour_time = i[3].split('/')[2].split(':')[1] + if int(hour_time) != int(cur_hour): + continue + minute_time = i[3].split('/')[2].split(':')[2] + if int(minute_time) != int(cur_minute): + continue + ret2.append(i2) + except: + continue + if ret2: + ret = ret2 + if len(ret) > 20: + return ret[0:20] + return ret + + def get_site_logs(self, Stiename): + try: + pythonV = sys.version_info[0] + path = '/www/wwwlogs/' + Stiename + '.log' + num = 500 + if not os.path.exists(path): + return [] + p = 1 + start_line = (p - 1) * num + count = start_line + num + fp = open(path, 'rb') + buf = "" + try: + fp.seek(-1, 2) + except: + return [] + if fp.read(1) == "\n": + fp.seek(-1, 2) + data = [] + b = True + n = 0 + c = 0 + while c < count: + while True: + newline_pos = str.rfind(buf, "\n") + pos = fp.tell() + if newline_pos != -1: + if n >= start_line: + line = buf[newline_pos + 1:] + if line: + try: + data.append(line) + except: + c -= 1 + n -= 1 + pass + else: + c -= 1 + n -= 1 + buf = buf[:newline_pos] + n += 1 + c += 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 pythonV == 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 not b: + break + fp.close() + except: + data = [] + return data + + def process_IN_CREATE(self, event): + siteInfo = self.get_SITE_CONFIG(event.pathname) + if not self.check_FILE(event, siteInfo, True): + return False + self._DONE_FILE = event.pathname + if event.dir: + if os.path.exists(event.pathname): + self.rmdir(event.pathname) + self.write_LOG('create', siteInfo[ + 'siteName'], event.pathname, datetime.datetime.now()) + + else: + if os.path.exists(event.pathname): + try: + src_path = os.path.dirname(event.pathname) + os.system("chattr -a {}".format(src_path)) + os.remove(event.pathname) + os.system("chattr +a {}".format(src_path)) + self.write_LOG('create', siteInfo[ + 'siteName'], event.pathname, datetime.datetime.now()) + except: + pass + + def process_IN_MOVED_TO(self, event): + # 检查是否受保护 + siteInfo = self.get_SITE_CONFIG(event.pathname) + if not self.check_FILE(event, siteInfo): + return False + + if not getattr(event, 'src_pathname', None): + if os.path.isdir(event.pathname): + self.rmdir(event.pathname) + else: + os.remove(event.pathname) + self.write_LOG('move', siteInfo[ + 'siteName'], '未知 -> ' + event.pathname) + return True + + # 是否为标记文件 + if event.src_pathname == self._DONE_FILE: + return False + + if not os.path.exists(event.src_pathname): + # 标记 + self._DONE_FILE = event.pathname + # 还原 + os.renames(event.pathname, event.src_pathname) + + # 记录日志 + self.write_LOG('move', siteInfo['siteName'], + event.src_pathname + ' -> ' + event.pathname) + + def check_FILE(self, event, siteInfo, create=False): + if not siteInfo: + return False + if self.exclude_PATH(event.pathname): + return False + if event.dir and create: + return True + if not event.dir: + if not self.protect_EXT(event.pathname): + return False + return True + + def protect_EXT(self, pathname): + if pathname.find('.') == -1: + return False + extName = pathname.split('.')[-1].lower() + siteData = self.get_SITE_CONFIG(pathname) + if siteData: + if extName in siteData['protectExt']: + return True + return False + + def exclude_PATH(self, pathname): + if pathname.find('/') == -1: + return False + siteData = self.get_SITE_CONFIG(pathname) + return self.exclude_PATH_OF_SITE(pathname, siteData['excludePath']) + + def exclude_PATH_OF_SITE(self, pathname, excludePath): + pathname = pathname.lower() + dirNames = pathname.split('/') + if excludePath: + if pathname in excludePath: + return True + if pathname + '/' in excludePath: + return True + for ePath in excludePath: + if ePath in dirNames: + return True + if pathname.find(ePath) == 0: + return True + return False + + def get_SITE_CONFIG(self, pathname): + if not self._SITES_DATA: + self._SITES_DATA = json.loads( + mw.readFile(self._PLUGIN_PATH + self._SITES)) + for site in self._SITES_DATA: + length = len(site['path']) + if len(pathname) < length: + continue + if site['path'] != pathname[:length]: + continue + return site + return None + + def get_CONFIG(self): + if self._CONFIG_DATA: + return self._CONFIG_DATA + self._CONFIG_DATA = json.loads( + mw.readFile(self._PLUGIN_PATH + self._CONFIG)) + + def list_DIR(self, path, siteInfo): # path 站点路径 + if not os.path.exists(path): + return + lock_files = [] + lock_dirs = [] + explode_a = ['log', 'logs', 'cache', 'templates', 'template', 'upload', 'img', + 'image', 'images', 'public', 'static', 'js', 'css', 'tmp', 'temp', 'update', 'data'] + for name in os.listdir(path): + try: + filename = "{}/{}".format(path, name).replace('//', '/') + lower_name = name.lower() + lower_filename = filename.lower() + if os.path.isdir(filename): # 是否为目录 + if lower_name in siteInfo['excludePath']: + continue # 是否为排除的文件名 + # 是否为排除目录 + if not self.exclude_PATH_OF_SITE(filename, siteInfo['excludePath']): + if not lower_name in explode_a: # 是否为固定不锁定目录 + lock_dirs.append('"' + name + '"') + self.list_DIR(filename, siteInfo) + continue + + # 是否为受保护的文件名或文件全路径 + if not lower_name in siteInfo['protectExt'] and not lower_filename in siteInfo['protectExt']: + if not self.get_EXT_NAME(lower_name) in siteInfo['protectExt']: + continue # 是否为受保护文件类型 + + if lower_filename in siteInfo['excludePath']: + continue # 是否为排除文件 + if lower_name in siteInfo['excludePath']: + continue # 是否为排除的文件名 + lock_files.append('"' + name + '"') + except: + print(mw.getTracebackInfo()) + if lock_files: + self.thread_exec(lock_files, path, 'i') + if lock_dirs: + self.thread_exec(lock_dirs, path, 'a') + + _thread_count = 0 + _thread_max = 2 * psutil.cpu_count() + + def thread_exec(self, file_list, cwd, i='i'): + while self._thread_count > self._thread_max: + time.sleep(0.1) + + self._thread_count += 1 + cmd = "cd {} && chattr +{} {} > /dev/null".format( + cwd, i, ' '.join(file_list)) + p = threading.Thread(target=self.run_thread, args=(cmd,)) + p.start() + + def run_thread(self, cmd): + os.system(cmd) + self._thread_count -= 1 + + def get_EXT_NAME(self, fileName): + return fileName.split('.')[-1] + + def write_LOG(self, eventType, siteName, pathname, datetime): + # 获取网站时间的top100 记录 + site_log = '/www/wwwlogs/%s.log' % siteName + logs_data = [] + if os.path.exists(site_log): + logs_data = self.check_site_logs(siteName, datetime) + dateDay = time.strftime("%Y-%m-%d", time.localtime()) + logPath = self._PLUGIN_PATH + '/sites/' + \ + siteName + '/day/' + dateDay + if not os.path.exists(logPath): + os.makedirs(logPath) + logFile = os.path.join(logPath, 'logs.json') + logVar = [int(time.time()), eventType, pathname, logs_data] + fp = open(logFile, 'a+') + fp.write(json.dumps(logVar) + "\n") + fp.close() + logFiles = [ + logPath + '/total.json', + self._PLUGIN_PATH + '/sites/' + siteName + '/day/total.json', + self._PLUGIN_PATH + '/sites/total.json' + ] + + for totalLogFile in logFiles: + if not os.path.exists(totalLogFile): + totalData = {"total": 0, "delete": 0, + "create": 0, "modify": 0, "move": 0} + else: + dataTmp = mw.readFile(totalLogFile) + if dataTmp: + totalData = json.loads(dataTmp) + else: + totalData = {"total": 0, "delete": 0, + "create": 0, "modify": 0, "move": 0} + + totalData['total'] += 1 + totalData[eventType] += 1 + mw.writeFile(totalLogFile, json.dumps(totalData)) + + # 设置.user.ini + def set_user_ini(self, path, up=0): + os.chdir(path) + useriniPath = path + '/.user.ini' + if os.path.exists(useriniPath): + os.system('chattr +i ' + useriniPath) + for p1 in os.listdir(path): + try: + npath = path + '/' + p1 + if not os.path.isdir(npath): + continue + useriniPath = npath + '/.user.ini' + if os.path.exists(useriniPath): + os.system('chattr +i ' + useriniPath) + if up < 3: + self.set_user_ini(npath, up + 1) + except: + continue + return True + + def unlock(self, path): + os.system('chattr -R -i {} &> /dev/null'.format(path)) + os.system('chattr -R -a {} &> /dev/null'.format(path)) + self.set_user_ini(path) + + def close(self, reload=False): + # 解除锁定 + sites = self.get_sites() + print("") + print("=" * 60) + print("【{}】正在关闭防篡改,请稍候...".format(mw.formatDate())) + print("-" * 60) + for siteInfo in sites: + tip = self._PLUGIN_PATH + '/tips/' + siteInfo['siteName'] + '.pl' + if not siteInfo['open'] and not os.path.exists(tip): + continue + if reload and siteInfo['open']: + continue + if sys.version_info[0] == 2: + print( + "【{}】|-解锁网站[{}]".format(mw.formatDate(), siteInfo['siteName'])), + else: + os.system( + "echo -e '【{}】|-解锁网站[{}]\c'".format(mw.formatDate(), siteInfo['siteName'])) + #print("【{}】|-解锁网站[{}]".format(mw.format_date(),siteInfo['siteName']),end=" ") + self.unlock(siteInfo['path']) + if os.path.exists(tip): + os.remove(tip) + print("\t=> 完成") + print("-" * 60) + print('|-防篡改已关闭') + print("=" * 60) + print(">>>>>>>>>>END<<<<<<<<<<") + + # 获取网站配置列表 + def get_sites(self): + siteconf = self._PLUGIN_PATH + '/sites.json' + d = mw.readFile(siteconf) + if not os.path.exists(siteconf) or not d: + mw.writeFile(siteconf, "[]") + data = json.loads(mw.readFile(siteconf)) + + # 处理多余字段开始 >>>>>>>>>> + is_write = False + rm_keys = ['lock', 'bak_open'] + for i in data: + i_keys = i.keys() + if not 'open' in i_keys: + i['open'] = False + for o in rm_keys: + if o in i_keys: + if i[o]: + i['open'] = True + i.pop(o) + is_write = True + if is_write: + mw.writeFile(siteconf, json.dumps(data)) + # 处理多余字段结束 <<<<<<<<<<<<< + return data + + def __enter__(self): + self.close() + + def __exit__(self, a, b, c): + self.close() + + +def run(): + # 初始化inotify对像 + event = MyEventHandler() + watchManager = pyinotify.WatchManager() + starttime = time.time() + mode = pyinotify.IN_CREATE | pyinotify.IN_MOVED_TO + + # 处理网站属性 + sites = event.get_sites() + print("=" * 60) + print("【{}】正在启动防篡改,请稍候...".format(mw.formatDate())) + print("-" * 60) + tip_path = event._PLUGIN_PATH + '/tips/' + if not os.path.exists(tip_path): + os.makedirs(tip_path) + speed_file = event._PLUGIN_PATH + '/speed.pl' + for siteInfo in sites: + s = time.time() + tip = tip_path + siteInfo['siteName'] + '.pl' + if not siteInfo['open']: + continue + if sys.version_info[0] == 2: + print("【{}】|-网站[{}]".format(mw.formatDate(), + siteInfo['siteName'])), + else: + os.system( + "echo -e '【{}】|-网站[{}]\c'".format(mw.formatDate(), siteInfo['siteName'])) + # print("【{}】|-网站[{}]".format(public.format_date(),siteInfo['siteName']),end=" ") + mw.writeFile(speed_file, "正在处理网站[{}],请稍候...".format( + siteInfo['siteName'])) + if not os.path.exists(tip): + event.list_DIR(siteInfo['path'], siteInfo) + try: + watchManager.add_watch( + siteInfo['path'], mode, auto_add=True, rec=True) + except: + print(mw.getTracebackInfo()) + tout = round(time.time() - s, 2) + mw.writeFile(tip, '1') + print("\t\t=> 完成,耗时 {} 秒".format(tout)) + + # 启动服务 + endtime = round(time.time() - starttime, 2) + mw.writeLog('防篡改程序', "网站防篡改服务已成功启动,耗时[%s]秒" % endtime) + notifier = pyinotify.Notifier(watchManager, event) + print("-" * 60) + print('|-防篡改服务已启动') + print("=" * 60) + end_tips = ">>>>>>>>>>END<<<<<<<<<<" + print(end_tips) + mw.writeFile(speed_file, end_tips) + notifier.loop() + + +if __name__ == '__main__': + if len(sys.argv) > 1: + if 'stop' in sys.argv: + event = MyEventHandler() + event.close() + elif 'start' in sys.argv: + run() + elif 'unlock' in sys.argv: + event = MyEventHandler() + event.unlock(sys.argv[2]) + elif 'reload' in sys.argv: + event = MyEventHandler() + event.close(True) + else: + print('error') + else: + run() From 5aeaf13f169143ab6260ca0f3285c63e881da097 Mon Sep 17 00:00:00 2001 From: midoks Date: Thu, 13 Apr 2023 15:42:01 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E8=B4=9F=E8=BD=BD=E5=9D=87=E8=A1=A1?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .../op_load_balance/conf/load_balance.conf | 2 + plugins/op_load_balance/conf/rewrite.tpl.conf | 64 ++ .../op_load_balance/conf/upstream.tpl.conf | 5 + plugins/op_load_balance/ico.png | Bin 0 -> 853 bytes plugins/op_load_balance/index.html | 21 + plugins/op_load_balance/index.py | 474 ++++++++++++++ plugins/op_load_balance/info.json | 16 + plugins/op_load_balance/install.sh | 45 ++ plugins/op_load_balance/js/app.js | 606 ++++++++++++++++++ .../op_load_balance/lua/health_check.lua.tpl | 18 + 11 files changed, 1251 insertions(+), 1 deletion(-) create mode 100644 plugins/op_load_balance/conf/load_balance.conf create mode 100644 plugins/op_load_balance/conf/rewrite.tpl.conf create mode 100644 plugins/op_load_balance/conf/upstream.tpl.conf create mode 100644 plugins/op_load_balance/ico.png create mode 100755 plugins/op_load_balance/index.html create mode 100755 plugins/op_load_balance/index.py create mode 100755 plugins/op_load_balance/info.json create mode 100755 plugins/op_load_balance/install.sh create mode 100644 plugins/op_load_balance/js/app.js create mode 100644 plugins/op_load_balance/lua/health_check.lua.tpl diff --git a/.gitignore b/.gitignore index 87a7110974..dc5119205f 100644 --- a/.gitignore +++ b/.gitignore @@ -163,7 +163,6 @@ plugins/l2tp plugins/openlitespeed plugins/tamper_proof plugins/cryptocurrency_trade -plugins/op_load_balance plugins/gdrive plugins/mtproxy plugins/zimg diff --git a/plugins/op_load_balance/conf/load_balance.conf b/plugins/op_load_balance/conf/load_balance.conf new file mode 100644 index 0000000000..0dff636c31 --- /dev/null +++ b/plugins/op_load_balance/conf/load_balance.conf @@ -0,0 +1,2 @@ +## 指定共享内存 +lua_shared_dict healthcheck 10m; \ No newline at end of file diff --git a/plugins/op_load_balance/conf/rewrite.tpl.conf b/plugins/op_load_balance/conf/rewrite.tpl.conf new file mode 100644 index 0000000000..1554bf2c79 --- /dev/null +++ b/plugins/op_load_balance/conf/rewrite.tpl.conf @@ -0,0 +1,64 @@ +location / { + proxy_pass http://{$UPSTREAM_NAME}; + + client_max_body_size 10m; + client_body_buffer_size 128k; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $proxy_host; + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + proxy_buffer_size 4k; + proxy_buffers 4 32k; + proxy_busy_buffers_size 64k; + proxy_temp_file_write_size 64k; +} + + +#location /upstream_status { +# allow 127.0.0.1; +# deny all; +# access_log off; +# default_type text/plain; +# content_by_lua_block { +# local hc = require "resty.upstream.healthcheck" +# ngx.say("OpenResty Worker PID: ", ngx.worker.pid()) +# ngx.print(hc.status_page()) +# } +#} + +location /upstream_status_{$UPSTREAM_NAME} { + allow 127.0.0.1; + deny all; + access_log off; + default_type text/plain; + content_by_lua_block { + local json = require "cjson" + local ok, upstream = pcall(require, "ngx.upstream") + if not ok then + ngx.print("[]") + return + end + + local get_primary_peers = upstream.get_primary_peers + local get_backup_peers = upstream.get_backup_peers + local upstream_name = "{$UPSTREAM_NAME}" + + peers, err = get_primary_peers(upstream_name) + if not peers then + ngx.print("[]") + return + end + + peers_backup, err = get_backup_peers(upstream_name) + if peers_backup then + for k, v in pairs(peers_backup) do + table.insert(peers,v) + end + end + + ngx.print(json.encode(peers)) + } +} \ No newline at end of file diff --git a/plugins/op_load_balance/conf/upstream.tpl.conf b/plugins/op_load_balance/conf/upstream.tpl.conf new file mode 100644 index 0000000000..711bbec01d --- /dev/null +++ b/plugins/op_load_balance/conf/upstream.tpl.conf @@ -0,0 +1,5 @@ +upstream {$UPSTREAM_NAME} +{ + {$NODE_ALGO} + {$NODE_SERVER_LIST} +} \ No newline at end of file diff --git a/plugins/op_load_balance/ico.png b/plugins/op_load_balance/ico.png new file mode 100644 index 0000000000000000000000000000000000000000..57850d8d4804332e0b7593be17870c200b0db2e6 GIT binary patch literal 853 zcmV-b1FHOqP)Px&4@pEpRA@u(n2l`|F%X4cDgY{gQ~*=}R6wWzQUP?3q=JMBfC?ZL5GqKhAkYC+ zK=hqFk~!HsW3Tt3L_5-HPx~{TH}mGl-qRhIy*qBdRsgph!8HQ+DFP2Ay_WP&(#s6k zFG&}YP9*VvsEne}Gf7`1J&|;t48D@YeeMD3fus+TK8{2%0suySm-JiG!GxEOB^{+m z@PCNR1psZXlkouYgb18UdL`*`0*sE&lD>E!m!8iBz`}SPj0A6_J5{-*DqN`i6# z`_lqs4gk1~_u(|2jS7GPPF{RVACdIXkVJ2yS1W+G$>SJdg4ghBix4q9o(rIe%zlfa zC;%4xl&C_o;yFT%;jPBk?6V{%n&d->N)Z51qP*- z&F85(ZHIjcAUNzSHN`g`2oFGaat%NRO)V#?bn*ZgFNwe|yr1K?DhHLSwDkbs1?C&? zy#Wp{s=)KuK*=qNXP8C6dv7TKR^jsD@V~V@!>sk+28A&6pV!n1AuLo2b0M(7(_ndP7a9?08EQ;kp{z+8vP^MDI0;Y7UK%J$DjxICx^rc0Bk@p z`M{5j$%c71bikrTgLCn^_BoYp1|SK zluG1yHY|cY0Bk;*@=UGt&H1*g6PG?3?!|18sp!oHV4H6?<1{!pPJ +
+
+

服务

+

负载均衡

+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/plugins/op_load_balance/index.py b/plugins/op_load_balance/index.py new file mode 100755 index 0000000000..59a646fb26 --- /dev/null +++ b/plugins/op_load_balance/index.py @@ -0,0 +1,474 @@ +# coding:utf-8 + +import sys +import io +import os +import time +import subprocess +import json +import re + +sys.path.append(os.getcwd() + "/class/core") +import mw + + +app_debug = False +if mw.isAppleSystem(): + app_debug = True + + +def getPluginName(): + return 'op_load_balance' + + +def getPluginDir(): + return mw.getPluginDir() + '/' + getPluginName() + + +def getServerDir(): + return mw.getServerDir() + '/' + getPluginName() + + +def getArgs(): + args = sys.argv[2:] + tmp = {} + args_len = len(args) + + if args_len == 1: + t = args[0].strip('{').strip('}') + if t.strip() == '': + tmp = [] + else: + t = t.split(':') + tmp[t[0]] = t[1] + tmp[t[0]] = t[1] + elif args_len > 1: + for i in range(len(args)): + t = args[i].split(':') + tmp[t[0]] = t[1] + return tmp + + +def checkArgs(data, ck=[]): + for i in range(len(ck)): + if not ck[i] in data: + return (False, mw.returnJson(False, '参数:(' + ck[i] + ')没有!')) + return (True, mw.returnJson(True, 'ok')) + + +def getConf(): + path = getServerDir() + "/cfg.json" + + if not os.path.exists(path): + mw.writeFile(path, '[]') + + c = mw.readFile(path) + return json.loads(c) + + +def writeConf(data): + path = getServerDir() + "/cfg.json" + mw.writeFile(path, json.dumps(data)) + + +def contentReplace(content): + service_path = mw.getServerDir() + content = content.replace('{$ROOT_PATH}', mw.getRootDir()) + content = content.replace('{$SERVER_PATH}', service_path) + content = content.replace('{$APP_PATH}', app_path) + return content + + +def restartWeb(): + mw.opWeb('stop') + mw.opWeb('start') + + +def loadBalanceConf(): + path = mw.getServerDir() + '/web_conf/nginx/vhost/load_balance.conf' + return path + + +def initDreplace(): + + dst_conf_tpl = getPluginDir() + '/conf/load_balance.conf' + dst_conf = loadBalanceConf() + + if not os.path.exists(dst_conf): + con = mw.readFile(dst_conf_tpl) + mw.writeFile(dst_conf, con) + + +def status(): + if not mw.getWebStatus(): + return 'stop' + + dst_conf = loadBalanceConf() + if not os.path.exists(dst_conf): + return 'stop' + + return 'start' + + +def start(): + initDreplace() + restartWeb() + return 'ok' + + +def stop(): + dst_conf = loadBalanceConf() + os.remove(dst_conf) + + deleteLoadBalanceAllCfg() + restartWeb() + return 'ok' + + +def restart(): + restartWeb() + return 'ok' + + +def reload(): + restartWeb() + return 'ok' + + +def installPreInspection(): + check_op = mw.getServerDir() + "/openresty" + if not os.path.exists(check_op): + return "请先安装OpenResty" + return 'ok' + + +def deleteLoadBalanceAllCfg(): + cfg = getConf() + upstream_dir = mw.getServerDir() + '/web_conf/nginx/upstream' + lua_dir = mw.getServerDir() + '/web_conf/nginx/lua/init_worker_by_lua_file' + rewrite_dir = mw.getServerDir() + '/web_conf/nginx/rewrite' + vhost_dir = mw.getServerDir() + '/web_conf/nginx/vhost' + + for conf in cfg: + upstream_file = upstream_dir + '/' + conf['upstream_name'] + '.conf' + if os.path.exists(upstream_file): + os.remove(upstream_file) + + lua_file = lua_dir + '/' + conf['upstream_name'] + '.lua' + if os.path.exists(lua_file): + os.remove(lua_file) + + rewrite_file = rewrite_dir + '/' + conf['domain'] + '.conf' + mw.writeFile(rewrite_file, '') + + path = vhost_dir + '/' + conf['domain'] + '.conf' + + content = mw.readFile(path) + content = re.sub('include ' + upstream_file + ';' + "\n", '', content) + mw.writeFile(path, content) + + mw.opLuaInitWorkerFile() + + +def makeConfServerList(data): + slist = '' + for x in data: + slist += 'server ' + slist += x['ip'] + ':' + x['port'] + + if x['state'] == '0': + slist += ' down;\n\t' + continue + + if x['state'] == '2': + slist += ' backup;\n\t' + continue + + slist += ' weight=' + x['weight'] + slist += ' max_fails=' + x['max_fails'] + slist += ' fail_timeout=' + x['fail_timeout'] + "s;\n\t" + return slist + + +def makeLoadBalanceAllCfg(row): + # 生成所有配置 + cfg = getConf() + + upstream_dir = mw.getServerDir() + '/web_conf/nginx/upstream' + rewrite_dir = mw.getServerDir() + '/web_conf/nginx/rewrite' + vhost_dir = mw.getServerDir() + '/web_conf/nginx/vhost' + upstream_tpl = getPluginDir() + '/conf/upstream.tpl.conf' + rewrite_tpl = getPluginDir() + '/conf/rewrite.tpl.conf' + + if not os.path.exists(upstream_dir): + os.makedirs(upstream_dir) + + conf = cfg[row] + + # replace vhost start + vhost_file = vhost_dir + '/' + conf['domain'] + '.conf' + vcontent = mw.readFile(vhost_file) + + vhost_find_str = 'upstream/' + conf['upstream_name'] + '.conf' + vhead = 'include ' + mw.getServerDir() + '/web_conf/nginx/' + \ + vhost_find_str + ';' + + vpos = vcontent.find(vhost_find_str) + if vpos < 0: + vcontent = vhead + "\n" + vcontent + mw.writeFile(vhost_file, vcontent) + # replace vhost end + + # make upstream start + upstream_file = upstream_dir + '/' + conf['upstream_name'] + '.conf' + content = '' + if len(conf['node_list']) > 0: + content = mw.readFile(upstream_tpl) + slist = makeConfServerList(conf['node_list']) + content = content.replace('{$NODE_SERVER_LIST}', slist) + content = content.replace('{$UPSTREAM_NAME}', conf['upstream_name']) + if conf['node_algo'] != 'polling': + content = content.replace('{$NODE_ALGO}', conf['node_algo'] + ';') + else: + content = content.replace('{$NODE_ALGO}', '') + mw.writeFile(upstream_file, content) + # make upstream end + + # make rewrite start + rewrite_file = rewrite_dir + '/' + conf['domain'] + '.conf' + rcontent = '' + if len(conf['node_list']) > 0: + rcontent = mw.readFile(rewrite_tpl) + rcontent = rcontent.replace('{$UPSTREAM_NAME}', conf['upstream_name']) + mw.writeFile(rewrite_file, rcontent) + # make rewrite end + + # health check start + lua_dir = mw.getServerDir() + '/web_conf/nginx/lua/init_worker_by_lua_file' + lua_init_worker_file = lua_dir + '/' + conf['upstream_name'] + '.lua' + if conf['node_health_check'] == 'ok': + lua_dir_tpl = getPluginDir() + '/lua/health_check.lua.tpl' + content = mw.readFile(lua_dir_tpl) + content = content.replace('{$UPSTREAM_NAME}', conf['upstream_name']) + content = content.replace('{$DOMAIN}', conf['domain']) + mw.writeFile(lua_init_worker_file, content) + else: + if os.path.exists(lua_init_worker_file): + os.remove(lua_init_worker_file) + + mw.opLuaInitWorkerFile() + # health check end + return True + + +def add_load_balance(args): + + data = checkArgs( + args, ['domain', 'upstream_name', 'node_algo', 'node_list', 'node_health_check']) + if not data[0]: + return data[1] + + domain_json = args['domain'] + + tmp = json.loads(domain_json) + domain = tmp['domain'] + + cfg = getConf() + cfg_len = len(cfg) + tmp = {} + tmp['domain'] = domain + tmp['data'] = args['domain'] + tmp['upstream_name'] = args['upstream_name'] + tmp['node_algo'] = args['node_algo'] + tmp['node_list'] = args['node_list'] + tmp['node_health_check'] = args['node_health_check'] + cfg.append(tmp) + writeConf(cfg) + + import site_api + sobj = site_api.site_api() + domain_path = mw.getWwwDir() + '/' + domain + + ps = '负载均衡[' + domain + ']' + data = sobj.add(domain_json, '80', ps, domain_path, '00') + + makeLoadBalanceAllCfg(cfg_len) + mw.restartWeb() + return mw.returnJson(True, '添加成功', data) + + +def edit_load_balance(args): + data = checkArgs( + args, ['row', 'node_algo', 'node_list', 'node_health_check']) + if not data[0]: + return data[1] + + row = int(args['row']) + + cfg = getConf() + tmp = cfg[row] + tmp['node_algo'] = args['node_algo'] + tmp['node_list'] = args['node_list'] + tmp['node_health_check'] = args['node_health_check'] + cfg[row] = tmp + writeConf(cfg) + + makeLoadBalanceAllCfg(row) + mw.restartWeb() + return mw.returnJson(True, '修改成功', data) + + +def loadBalanceList(): + cfg = getConf() + return mw.returnJson(True, 'ok', cfg) + + +def loadBalanceDelete(): + args = getArgs() + data = checkArgs(args, ['row']) + if not data[0]: + return data[1] + + row = int(args['row']) + + cfg = getConf() + data = cfg[row] + + import site_api + sobj = site_api.site_api() + + sid = mw.M('sites').where('name=?', (data['domain'],)).getField('id') + + if type(sid) == list: + del(cfg[row]) + writeConf(cfg) + return mw.returnJson(False, '已经删除了!') + + status = sobj.delete(sid, data['domain'], 1) + status_data = json.loads(status) + + if status_data['status']: + del(cfg[row]) + writeConf(cfg) + + upstream_dir = mw.getServerDir() + '/web_conf/nginx/upstream' + rewrite_dir = mw.getServerDir() + '/web_conf/nginx/rewrite' + + upstream_file = upstream_dir + '/' + data['upstream_name'] + '.conf' + if os.path.exists(upstream_file): + os.remove(upstream_file) + + rewrite_file = rewrite_dir + '/' + data['domain'] + '.conf' + if os.path.exists(rewrite_file): + mw.writeFile(rewrite_file, '') + + return mw.returnJson(status_data['status'], status_data['msg']) + + +def http_get(url): + ret = re.search(r'https://', url) + if ret: + try: + from gevent import monkey + monkey.patch_ssl() + import requests + ret = requests.get(url=str(url), verify=False, timeout=10) + status = [200, 301, 302, 404, 403] + if ret.status_code in status: + return True + else: + return False + except: + return False + else: + try: + if sys.version_info[0] == 2: + import urllib2 + rec = urllib2.urlopen(url, timeout=3) + else: + import urllib.request + rec = urllib.request.urlopen(url, timeout=3) + status = [200, 301, 302, 404, 403] + if rec.getcode() in status: + return True + return False + except: + return False + + +def checkUrl(): + args = getArgs() + data = checkArgs(args, ['ip', 'port', 'path']) + if not data[0]: + return data[1] + + ip = args['ip'] + port = args['port'] + path = args['path'] + + if port == '443': + url = 'https://' + str(ip) + ':' + str(port) + str(path.strip()) + else: + url = 'http://' + str(ip) + ':' + str(port) + str(path.strip()) + ret = http_get(url) + if not ret: + return mw.returnJson(False, '访问节点[%s]失败' % url) + return mw.returnJson(True, '访问节点[%s]成功' % url) + + +def getHealthStatus(): + args = getArgs() + data = checkArgs(args, ['row']) + if not data[0]: + return data[1] + + row = int(args['row']) + + cfg = getConf() + data = cfg[row] + + url = 'http://' + data['domain'] + \ + '/upstream_status_' + data['upstream_name'] + + url_data = mw.httpGet(url) + return mw.returnJson(True, 'ok', json.loads(url_data)) + + +def getLogs(): + args = getArgs() + data = checkArgs(args, ['domain']) + if not data[0]: + return data[1] + + domain = args['domain'] + logs = mw.getLogsDir() + '/' + domain + '.log' + return logs + +if __name__ == "__main__": + func = sys.argv[1] + if func == 'status': + print(status()) + elif func == 'start': + print(start()) + elif func == 'stop': + print(stop()) + elif func == 'restart': + print(restart()) + elif func == 'reload': + print(reload()) + elif func == 'install_pre_inspection': + print(installPreInspection()) + elif func == 'add_load_balance': + print(addLoadBalance()) + elif func == 'load_balance_list': + print(loadBalanceList()) + elif func == 'load_balance_delete': + print(loadBalanceDelete()) + elif func == 'check_url': + print(checkUrl()) + elif func == 'get_logs': + print(getLogs()) + elif func == 'get_health_status': + print(getHealthStatus()) + else: + print('error') diff --git a/plugins/op_load_balance/info.json b/plugins/op_load_balance/info.json new file mode 100755 index 0000000000..1855728783 --- /dev/null +++ b/plugins/op_load_balance/info.json @@ -0,0 +1,16 @@ +{ + "title":"OP负载均衡", + "tip":"soft", + "name":"op_load_balance", + "type":"其他插件", + "install_pre_inspection":true, + "ps":"基于OpenResty的负载均衡", + "shell":"install.sh", + "checks":"server/op_load_balance", + "path":"server/op_load_balance", + "author":"midoks", + "home":"https://github.com/midoks", + "date":"2023-02-02", + "pid": "1", + "versions": ["1.0"] +} \ No newline at end of file diff --git a/plugins/op_load_balance/install.sh b/plugins/op_load_balance/install.sh new file mode 100755 index 0000000000..d78bf73fbf --- /dev/null +++ b/plugins/op_load_balance/install.sh @@ -0,0 +1,45 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +curPath=`pwd` +rootPath=$(dirname "$curPath") +rootPath=$(dirname "$rootPath") +serverPath=$(dirname "$rootPath") + +install_tmp=${rootPath}/tmp/mw_install.pl + +action=$1 +version=$2 +sys_os=`uname` + +if [ -f ${rootPath}/bin/activate ];then + source ${rootPath}/bin/activate +fi + +if [ "$sys_os" == "Darwin" ];then + BAK='_bak' +else + BAK='' +fi + +Install_App(){ + echo '正在安装脚本文件...' > $install_tmp + mkdir -p $serverPath/op_load_balance + echo "${version}" > $serverPath/op_load_balance/version.pl + cd ${rootPath} && python3 ${rootPath}/plugins/op_load_balance/index.py start + echo 'install ok' > $install_tmp +} + +Uninstall_App(){ + cd ${rootPath} && python3 ${rootPath}/plugins/op_load_balance/index.py stop + rm -rf $serverPath/op_load_balance +} + + +action=$1 +if [ "${1}" == 'install' ];then + Install_App +else + Uninstall_App +fi diff --git a/plugins/op_load_balance/js/app.js b/plugins/op_load_balance/js/app.js new file mode 100644 index 0000000000..3ffca22926 --- /dev/null +++ b/plugins/op_load_balance/js/app.js @@ -0,0 +1,606 @@ +function ooPost(method,args,callback){ + var _args = null; + if (typeof(args) == 'string'){ + _args = JSON.stringify(toArrayObject(args)); + } else { + _args = JSON.stringify(args); + } + + var loadT = layer.msg('正在获取...', { icon: 16, time: 0, shade: 0.3 }); + $.post('/plugins/run', {name:'op_load_balance', func:method, args:_args}, function(data) { + layer.close(loadT); + if (!data.status){ + layer.msg(data.msg,{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + if(typeof(callback) == 'function'){ + callback(data); + } + },'json'); +} + +function ooAsyncPost(method,args){ + var _args = null; + if (typeof(args) == 'string'){ + _args = JSON.stringify(toArrayObject(args)); + } else { + _args = JSON.stringify(args); + } + return syncPost('/plugins/run', {name:'op_load_balance', func:method, args:_args}); +} + +function ooPostCallbak(method, args, callback){ + var loadT = layer.msg('正在获取...', { icon: 16, time: 0, shade: 0.3 }); + + var req_data = {}; + req_data['name'] = 'op_load_balance'; + req_data['func'] = method; + args['version'] = '1.0'; + + if (typeof(args) == 'string'){ + req_data['args'] = JSON.stringify(toArrayObject(args)); + } else { + req_data['args'] = JSON.stringify(args); + } + + $.post('/plugins/callback', req_data, function(data) { + layer.close(loadT); + if (!data.status){ + layer.msg(data.msg,{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + if(typeof(callback) == 'function'){ + callback(data); + } + },'json'); +} + +function addNode(){ + layer.open({ + type: 1, + area: ['450px','580px'], + title: '添加节点', + closeBtn: 1, + shift: 5, + shadeClose: true, + btn:['提交','关闭'], + content: "
\ +
\ + IP地址\ +
\ + \ +
\ +
\ +
\ + 端口\ +
\ + \ +
\ +
\ +
\ + 验证文件路径\ +
\ + \ +
\ +
\ +
\ + 节点状态\ +
\ + \ +
\ +
\ +
\ + 权重\ +
\ + \ +
\ +
\ +
\ + 阈值\ +
\ + 次\ +
\ +
\ +
\ + 恢复时间\ +
\ + 秒\ +
\ +
\ +
    \ +
  • 备份状态: 指当其它节点都无法使用时才会使用此节点
  • \ +
  • 参与状态: 正常参与负载均衡,请至少添加1个普通节点
  • \ +
  • 验证文件路径: 用于检查文件路径地址是否可用
  • \ +
  • IP地址: 仅支持IP地址,否则无法正常参与负载均衡
  • \ +
  • 阈值: 在恢复时间的时间段内,如果OpenResty与节点通信尝试失败的次数达到此值,OpenResty就认为服务器不可用
  • \ +
\ +
", + success:function(){ + }, + yes:function(index) { + + var ip = $('input[name="ip"]').val(); + var port = $('input[name="port"]').val(); + var path = $('input[name="path"]').val(); + var state = $('select[name="state"]').val(); + var weight = $('input[name="weight"]').val(); + var max_fails = $('input[name="max_fails"]').val(); + var fail_timeout = $('input[name="fail_timeout"]').val(); + + ooPost('check_url', {ip:ip,port:port,path:path},function(rdata){ + var rdata = $.parseJSON(rdata.data); + showMsg(rdata.msg, function(){ + if (rdata.status){ + layer.close(index); + $('#nodecon .nulltr').hide(); + + var tbody = ''; + tbody +=''+ip+''; + tbody +=''+port+''; + tbody +=''+path+''; + + tbody +=""; + + tbody +=''; + tbody +=''; + tbody +=''; + tbody +='删除'; + tbody += ''; + $('#nodecon').append(tbody); + + $('#nodecon .delete').click(function(){ + $(this).parent().parent().remove(); + if ($('#nodecon tr').length == 1 ){ + $('#nodecon .nulltr').show(); + } + }); + } + },{ icon: rdata.status ? 1 : 2 }, 2000); + }); + } + }); +} + +function addBalance(){ + layer.open({ + type: 1, + area: ['750px','460px'], + title: '创建负载', + closeBtn: 1, + shift: 5, + shadeClose: true, + btn:['提交','关闭'], + content: "
\ +
\ + 域名\ +
\ +
\ +
\ + 负载名称\ +
\ + \ +
\ +
\ +
\ + 节点调度\ +
\ + \ +
\ +
\ +
\ + 节点健康检查\ +
\ + \ +
\ +
\ +
\ + 节点\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
IP地址端口验证路径状态权重阀值恢复时间操作
当前节点为空,请至少添加一个普通节点
\ +
\ + 添加节点\ +
\ +
\ +
", + success:function(){ + $('textarea[name="load_domain"]').attr('placeholder','每行填写一个域名,默认为80端口。\n泛解析添加方法 *.domain.com\n如另加端口格式为 www.domain.com:88'); + var rval = getRandomString(6); + $('input[name="upstream_name"]').val('load_balance_'+rval); + + $('.add_node').click(function(){ + addNode(); + }); + }, + yes:function(index) { + var data = {}; + + var upstream_name = $('input[name="upstream_name"]').val(); + if (upstream_name == ''){ + layer.msg('负载名称不能为空!',{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + var domain = $('textarea[name="load_domain"]').val().replace('http://','').replace('https://','').split("\n"); + if (domain[0] == ''){ + layer.msg('域名不能为空!',{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + var domainlist = ''; + for(var i=1; i\ +
\ + 负载名称\ +
\ +
\ +
\ + 节点调度\ +
\ + \ +
\ +
\ +
\ + 节点健康检查\ +
\ + \ +
\ +
\ +
\ + 节点\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
IP地址端口验证路径状态权重阀值恢复时间操作
当前节点为空,请至少添加一个普通节点
\ +
\ + 添加节点\ +
\ +
\ + ", + success:function(){ + $('input[name="upstream_name"]').val(data['upstream_name']); + $('select[name="node_algo"]').val(data['node_algo']); + + $('input[name="node_health_check"]').prop('checked',false); + if (data['node_health_check'] == 'ok'){ + $('input[name="node_health_check"]').prop('checked',true); + } + + var node_list = data['node_list']; + if (node_list.length>0){ + $('#nodecon .nulltr').hide(); + } + + var state_option_list = { + '1':'参与者', + '2':'备份', + '0':'停用', + } + + for (var n in node_list) { + + var tbody = ''; + tbody +=''+node_list[n]['ip']+''; + tbody +=''+node_list[n]['port']+''; + tbody +=''+node_list[n]['path']+''; + + tbody +=""; + + tbody +=''; + tbody +=''; + tbody +=''; + tbody +='删除'; + tbody += ''; + $('#nodecon').append(tbody); + } + + $('#nodecon .delete').click(function(){ + $(this).parent().parent().remove(); + if ($('#nodecon tr').length == 1 ){ + $('#nodecon .nulltr').show(); + } + }); + + $('.add_node').click(function(){ + addNode(); + }); + }, + yes:function(index) { + var data = {}; + + data['node_algo'] = $('select[name="node_algo"]').val(); + data['node_health_check'] = 'fail'; + if ($('input[name="node_health_check"]').prop('checked')){ + data['node_health_check'] = 'ok'; + } + + var node_list = []; + $('#nodecon tr').each(function(){ + + var ip = $(this).find('td').eq(0).text(); + var port = $(this).find('td').eq(1).text(); + + if (port == ''){return;} + + var path = $(this).find('td').eq(2).text(); + var state = $(this).find('select[name="state"]').val(); + var weight = $(this).find('input[name="weight"]').val(); + var max_fails = $(this).find('input[name="max_fails"]').val(); + var fail_timeout = $(this).find('input[name="fail_timeout"]').val(); + + var tmp = { + ip:ip, + port:port, + path:path, + state:state, + weight:weight, + max_fails:max_fails, + fail_timeout:fail_timeout, + } + node_list.push(tmp); + }); + data['node_list'] = node_list; + data['row'] = row; + ooPostCallbak('edit_load_balance', data, function(rdata){ + var rdata = $.parseJSON(rdata.data); + showMsg(rdata.msg, function(){ + layer.close(index); + loadBalanceListRender(); + },{ icon: rdata.status ? 1 : 2 }, 2000); + }); + } + }); +} + +function loadBalanceListRender(){ + ooPost('load_balance_list', {}, function(rdata){ + var rdata = $.parseJSON(rdata.data); + var alist = rdata.data; + + var tbody = ''; + for (var i = 0; i < alist.length; i++) { + tbody += ''; + tbody += ''+alist[i]['domain']+''; + tbody += ''+alist[i]['upstream_name']+''; + tbody += ''+alist[i]['node_list'].length+''; + tbody += '查看'; + tbody += '查看'; + tbody += '修改 | 删除'; + tbody += ''; + } + + $('#nodeTable').html(tbody); + $('.nodeTablePage .Pcount').text('共'+alist.length+'条'); + $('#nodeTable .edit').click(function(){ + var row = $(this).data('row'); + editBalance(alist[row],row); + }); + + $('#nodeTable .log_look').click(function(){ + var row = $(this).data('row'); + var args = {'domain':alist[row]['domain']}; + pluginRollingLogs('op_load_balance','','get_logs',JSON.stringify(args),20); + }); + + $('#nodeTable .health_status').click(function(){ + var row = $(this).data('row'); + ooPost('get_health_status', {row:row}, function(rdata){ + var rdata = $.parseJSON(rdata.data); + + var tval = ''; + for (var i = 0; i < rdata.data.length; i++) { + tval += ''; + tval += ''+rdata.data[i]['name']+''; + + if (typeof(rdata.data[i]['down']) != 'undefined' && rdata.data[i]['down']){ + tval += '不正常'; + } else{ + tval += '正常'; + } + tval += ''; + } + + var tbody = "
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + "+tval+"\ +
地址状态
\ +
\ +
\ +
"; + + layer.open({ + type: 1, + area: ['500px','300px'], + title: '节点状态', + closeBtn: 1, + shift: 5, + shadeClose: true, + btn:['提交','关闭'], + content:tbody, + }); + }); + }); + + $('#nodeTable .delete').click(function(){ + var row = $(this).data('row'); + ooPost('load_balance_delete', {row:row}, function(rdata){ + var rdata = $.parseJSON(rdata.data); + showMsg(rdata.msg, function(){ + loadBalanceListRender(); + },{ icon: rdata.status ? 1 : 2 }, 2000); + }); + }); + + }); +} + +function loadBalanceList() { + var body = '
\ +
\ + \ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
网站负载名称节点日志状态操作
\ +
\ +
共0条
\ +
\ +
\ +
\ +
\ + '; + $(".soft-man-con").html(body); + loadBalanceListRender(); +} diff --git a/plugins/op_load_balance/lua/health_check.lua.tpl b/plugins/op_load_balance/lua/health_check.lua.tpl new file mode 100644 index 0000000000..94411c942d --- /dev/null +++ b/plugins/op_load_balance/lua/health_check.lua.tpl @@ -0,0 +1,18 @@ +local hc = require "resty.upstream.healthcheck" +local ok, err = hc.spawn_checker { + shm = "healthcheck", + type = "http", + upstream = "{$UPSTREAM_NAME}", + http_req = "GET / HTTP/1.0\r\nHost: {$UPSTREAM_NAME}\r\n\r\n", + interval = 2000, + timeout = 6000, + fall = 3, + rise = 2, + valid_statuses = {200, 302}, + concurrency = 20, +} + +if not ok then + ngx.log(ngx.ERR, "=======> load balance health checker error: ", err) + return +end \ No newline at end of file From 080f343ee273b6d9a0d8e19792afaeb3cb82ff3d Mon Sep 17 00:00:00 2001 From: midoks Date: Sat, 15 Apr 2023 10:57:19 +0800 Subject: [PATCH 4/8] Update push_ad.py --- plugins/tgbot/startup/extend/push_ad.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/tgbot/startup/extend/push_ad.py b/plugins/tgbot/startup/extend/push_ad.py index 8ef7a7d030..e86cff6f95 100644 --- a/plugins/tgbot/startup/extend/push_ad.py +++ b/plugins/tgbot/startup/extend/push_ad.py @@ -41,6 +41,11 @@ def send_msg(bot, tag='ad', trigger_time=300): mw.writeFile(lock_file, json.dumps(lock_data)) # 信号只在一个周期内执行一次|end + # https://t.me/gjgzs2022 | 19/m + # ♻️CMS导航网♻️/💰流量变现💰 | 28/m + # CK资源采集 |29/m + # 高防服务器CDN请联系玥玥 |3/m + keyboard = [ [ types.InlineKeyboardButton( From c9962f169e54654c58112dd64fc9c0075e2fa9b2 Mon Sep 17 00:00:00 2001 From: midoks Date: Sun, 16 Apr 2023 20:36:01 +0800 Subject: [PATCH 5/8] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 211761a52f..78c7e21d27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ pymongo pillow Jinja2>=2.11.2 flask-caching>=1.10.1 -bcrypt==3.1.3 +flask-bcrypt=1.0.1 PyMySQL==1.0.2 whitenoise==5.3.0 pyTelegramBotAPI From b93bb6c64c941c71f3d8bc72f57da1d7d991023a Mon Sep 17 00:00:00 2001 From: midoks Date: Sun, 16 Apr 2023 20:37:24 +0800 Subject: [PATCH 6/8] Update requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 78c7e21d27..d7f95d237d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,8 @@ urllib3>=1.21.1 flask==2.0.3 flask-session==0.3.2 flask-helper==0.19 +flask-bcrypt=1.0.1 +flask-caching>=1.10.1 cache==1.0.3 gevent==21.12.0 gevent-websocket==0.10.1 @@ -25,8 +27,6 @@ paramiko>=2.8.0 pymongo pillow Jinja2>=2.11.2 -flask-caching>=1.10.1 -flask-bcrypt=1.0.1 PyMySQL==1.0.2 whitenoise==5.3.0 pyTelegramBotAPI From 5623e3efacaa58a7fe697df0c10d7b560fb1fe87 Mon Sep 17 00:00:00 2001 From: midoks Date: Sun, 16 Apr 2023 20:38:03 +0800 Subject: [PATCH 7/8] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d7f95d237d..faa281bd3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ urllib3>=1.21.1 flask==2.0.3 flask-session==0.3.2 flask-helper==0.19 -flask-bcrypt=1.0.1 +flask-bcrypt==1.0.1 flask-caching>=1.10.1 cache==1.0.3 gevent==21.12.0 From bf5274c3137899f04bbaa2f434cab4e48218dd2d Mon Sep 17 00:00:00 2001 From: midoks Date: Sun, 16 Apr 2023 20:41:39 +0800 Subject: [PATCH 8/8] 0.14.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 安装优化。 * 加入OP负载均衡插件。 * 网站防篡改程序(测试中)。 --- README.md | 6 ++++-- class/core/config_api.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 742d1f8488..751db92651 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,11 @@ docker run -itd --name mw-server --privileged=true -p 7200:7200 -p 80:80 -p 443: ``` -### 版本更新 0.14.0 +### 版本更新 0.14.1 -* 加入系统加固插件。 +* 安装优化。 +* 加入OP负载均衡插件。 +* 网站防篡改程序(测试中)。 ### JSDelivr安装地址 diff --git a/class/core/config_api.py b/class/core/config_api.py index c1cb0dfb68..0fa7efba0f 100755 --- a/class/core/config_api.py +++ b/class/core/config_api.py @@ -27,7 +27,7 @@ class config_api: - __version = '0.14.0' + __version = '0.14.1' __api_addr = 'data/api.json' def __init__(self):