diff --git a/CHANGELOG.md b/CHANGELOG.md index 4807ca74e2..6e871d0096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## 更新日志 +- v1.3.1(2023-06-03) + - 新增 支持一个域名解析到多主机ip地址的SSL证书查询 + - 优化 优化前端界面显示 + - v1.2.23(2023-06-01) - 新增 实验室tab,增加查询whois原始信息功能 diff --git a/domain_admin/api/address_api.py b/domain_admin/api/address_api.py new file mode 100644 index 0000000000..f639feb3ca --- /dev/null +++ b/domain_admin/api/address_api.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +""" +address_api.py +""" +from flask import request, g +from peewee import fn +from playhouse.shortcuts import model_to_dict + +from domain_admin.model.address_model import AddressModel +from domain_admin.model.domain_model import DomainModel +from domain_admin.model.group_model import GroupModel +from domain_admin.service import async_task_service +from domain_admin.service import domain_service, global_data_service +from domain_admin.service import file_service +from domain_admin.utils import datetime_util +from domain_admin.utils.flask_ext.app_exception import AppException + + +def get_address_list_by_domain_id(): + """ + 通过域名id获取关联的主机地址列表 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + domain_id = request.json['domain_id'] + + rows = AddressModel.select().where( + AddressModel.domain_id == domain_id + ) + + lst = list(map(lambda m: model_to_dict( + model=m, + extra_attrs=[ + 'ssl_start_date', + 'ssl_expire_date', + 'real_time_ssl_expire_days', + 'ssl_check_time_label', + ] + ), rows)) + + return { + "list": lst, + "total": len(lst) + } + + +def add_address(): + """ + 添加主机地址 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + domain_id = request.json['domain_id'] + host = request.json['host'] + ssl_start_time = request.json.get('ssl_start_time') + ssl_expire_time = request.json.get('ssl_expire_time') + ssl_auto_update = request.json.get('ssl_auto_update', True) + ssl_expire_monitor = request.json.get('ssl_expire_monitor', True) + + address_row = AddressModel.create( + domain_id=domain_id, + host=host, + ssl_start_time=ssl_start_time, + ssl_expire_time=ssl_expire_time, + ssl_auto_update=ssl_auto_update, + ssl_expire_monitor=ssl_expire_monitor, + ) + + domain_service.update_address_row_info_with_sync_domain_row(address_row.address_id) + + +def delete_address_by_id(): + """ + 删除主机地址 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + address_id = request.json['address_id'] + + AddressModel.delete().where( + AddressModel.id == address_id + ).execute() + + +def get_address_by_id(): + """ + 获取主机地址 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + address_id = request.json['address_id'] + + return AddressModel.get_by_id(address_id) + + +def update_address_by_id(): + """ + 更新主机地址 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + address_id = request.json['address_id'] + host = request.json['host'] + ssl_start_time = request.json.get('ssl_start_time') + ssl_expire_time = request.json.get('ssl_expire_time') + ssl_auto_update = request.json.get('ssl_auto_update', True) + ssl_expire_monitor = request.json.get('ssl_expire_monitor', True) + + AddressModel.update( + host=host, + ssl_start_time=ssl_start_time, + ssl_expire_time=ssl_expire_time, + ssl_auto_update=ssl_auto_update, + ssl_expire_monitor=ssl_expire_monitor, + ).where( + AddressModel.id == address_id + ).execute() + + domain_service.update_address_row_info_with_sync_domain_row(address_id) + + +def update_address_list_info_by_domain_id(): + """ + 更新主机地址信息 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + domain_id = request.json['domain_id'] + domain_row = DomainModel.get_by_id(domain_id) + domain_service.update_domain_address_info(domain_row) + +def update_address_row_info_by_id(): + """ + 更新主机地址信息 + :return: + @since v1.3.1 + """ + + current_user_id = g.user_id + + address_id = request.json['address_id'] + + domain_service.update_address_row_info_with_sync_domain_row(address_id) + diff --git a/domain_admin/api/domain_api.py b/domain_admin/api/domain_api.py index f7f20e960b..d273ead33b 100644 --- a/domain_admin/api/domain_api.py +++ b/domain_admin/api/domain_api.py @@ -4,12 +4,14 @@ from peewee import fn from playhouse.shortcuts import model_to_dict +from domain_admin.model.address_model import AddressModel from domain_admin.model.domain_model import DomainModel from domain_admin.model.group_model import GroupModel from domain_admin.service import async_task_service from domain_admin.service import domain_service, global_data_service from domain_admin.service import file_service from domain_admin.utils import datetime_util +from domain_admin.utils.cert_util import cert_consts from domain_admin.utils.flask_ext.app_exception import AppException @@ -28,11 +30,13 @@ def add_domain(): alias = request.json.get('alias', '') group_id = request.json.get('group_id') or 0 + port = request.json.get('port') or cert_consts.SSL_DEFAULT_PORT data = { # 基本信息 'user_id': current_user_id, 'domain': domain, + 'port': port, 'alias': alias, 'group_id': group_id, } @@ -59,12 +63,14 @@ def update_domain_setting(): 'domain_start_time': request.json.get('domain_start_time'), 'domain_expire_time': request.json.get('domain_expire_time'), 'domain_auto_update': request.json.get('domain_auto_update'), + 'domain_expire_monitor': request.json.get('domain_expire_monitor'), # 证书信息 - 'start_time': request.json.get('start_time'), - 'expire_time': request.json.get('expire_time'), - 'auto_update': request.json.get('auto_update'), + # 'start_time': request.json.get('start_time'), + # 'expire_time': request.json.get('expire_time'), + # 'auto_update': request.json.get('auto_update'), + 'domain_check_time': datetime_util.get_datetime(), 'update_time': datetime_util.get_datetime() } @@ -111,6 +117,11 @@ def delete_domain_by_id(): DomainModel.delete_by_id(domain_id) + # 同时移除主机信息 + AddressModel.delete().where( + AddressModel.domain_id == domain_id + ).execute() + def delete_domain_by_ids(): """ @@ -127,6 +138,11 @@ def delete_domain_by_ids(): DomainModel.user_id == current_user_id ).execute() + # 同时移除主机信息 + AddressModel.delete().where( + AddressModel.domain_id.in_(domain_ids) + ).execute() + def get_domain_list(): """ @@ -217,6 +233,8 @@ def get_domain_list(): 'create_time_label', 'check_time_label', 'real_time_expire_days', + 'real_time_ssl_total_days', + 'real_time_ssl_expire_days', 'real_time_domain_expire_days', 'domain_url', 'update_time_label', @@ -253,6 +271,7 @@ def get_domain_by_id(): 'detail', 'group', 'domain_url', + 'domain_check_time_label', ] ) @@ -316,7 +335,25 @@ def update_domain_cert_info_by_id(): """ current_user_id = g.user_id - domain_id = request.json['id'] + # @since v1.2.24 支持参数 domain_id + domain_id = request.json.get('domain_id') or request.json['id'] + + row = domain_service.check_permission_and_get_row(domain_id, current_user_id) + + # domain_service.update_domain_row(row) + domain_service.update_domain_info(row) + + +def update_domain_row_info_by_id(): + """ + 更新域名信息及其关联的证书信息 + :return: + @since v1.3.1 + """ + current_user_id = g.user_id + + # @since v1.2.24 支持参数 domain_id + domain_id = request.json.get('domain_id') or request.json['id'] row = domain_service.check_permission_and_get_row(domain_id, current_user_id) @@ -384,8 +421,15 @@ def import_domain_from_file(): filename = file_service.save_temp_file(update_file) + # 导入数据 + domain_service.add_domain_from_file(filename, current_user_id) + # 异步导入 - async_task_service.submit_task(fn=domain_service.add_domain_from_file, filename=filename, user_id=current_user_id) + # async_task_service.submit_task(fn=domain_service.add_domain_from_file, filename=filename, user_id=current_user_id) + + # 异步查询 + async_task_service.submit_task(fn=domain_service.update_all_domain_cert_info_of_user, user_id=current_user_id) + def export_domain_file(): diff --git a/domain_admin/enums/version_enum.py b/domain_admin/enums/version_enum.py index 87f0ea7c97..17e35fd319 100644 --- a/domain_admin/enums/version_enum.py +++ b/domain_admin/enums/version_enum.py @@ -56,3 +56,13 @@ class VersionEnum(object): Version_1213 = '1.2.13' Version_1214 = '1.2.14' + + Version_1215 = '1.2.15' + Version_1216 = '1.2.16' + Version_1217 = '1.2.17' + Version_1218 = '1.2.18' + Version_1221 = '1.2.21' + Version_1222 = '1.2.22' + Version_1223 = '1.2.23' + + Version_131 = '1.3.1' diff --git a/domain_admin/log.py b/domain_admin/log.py index 1f677ea8c9..7d7bb93328 100644 --- a/domain_admin/log.py +++ b/domain_admin/log.py @@ -9,6 +9,12 @@ # 单个日志文件最大为1M handler = RotatingFileHandler(resolve_log_file("domain-admin.log"), maxBytes=1024 * 1024 * 1, encoding='utf-8') +# 设置日志格式 +formatter = logging.Formatter( + fmt='%(asctime)s [%(levelname)s] %(filename)s/%(funcName)s:\n%(message)s\n', + datefmt='%Y-%m-%d %H:%M:%S') +handler.setFormatter(formatter) + # logger.addHandler(logging.FileHandler(resolve_log_file("domain-admin.log"))) logger.addHandler(handler) logger.setLevel(logging.DEBUG) diff --git a/domain_admin/migrate/migrate_1213_to_131.py b/domain_admin/migrate/migrate_1213_to_131.py new file mode 100644 index 0000000000..7b679a1723 --- /dev/null +++ b/domain_admin/migrate/migrate_1213_to_131.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +@File : migrate_1213_to_131.py +@Date : 2023-06-03 + +cmd: +$ python domain_admin/migrate/migrate_1213_to_131.py +""" + +from playhouse.migrate import SqliteMigrator, migrate + +from domain_admin.model.base_model import db +from domain_admin.model.domain_model import DomainModel + + +def execute_migrate(): + """ + 版本升级 1.2.13 => 1.3.1 + :return: + """ + migrator = SqliteMigrator(db) + + migrate( + migrator.add_column(DomainModel._meta.table_name, DomainModel.port.name, DomainModel.port), + migrator.add_column(DomainModel._meta.table_name, DomainModel.domain_expire_monitor.name, DomainModel.domain_expire_monitor), + ) diff --git a/domain_admin/model/address_model.py b/domain_admin/model/address_model.py new file mode 100644 index 0000000000..0c4864b143 --- /dev/null +++ b/domain_admin/model/address_model.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +from peewee import CharField, IntegerField, DateTimeField, BooleanField + +from domain_admin.model.base_model import BaseModel +from domain_admin.utils import datetime_util, time_util + + +class AddressModel(BaseModel): + """ + 域名主机ip地址 + @since v1.2.24 + """ + id = IntegerField(primary_key=True) + + # 域名 + domain_id = CharField(null=False) + + # 主机地址 + host = CharField(default="") + + # 连接状态 + host_connect_status = BooleanField(default=None, null=True) + + # ip连接状态检查时间 + host_check_time = DateTimeField(default=None, null=True) + + # ip连接状态监测 + host_status_monitor = BooleanField(default=True) + + # SSL签发时间 + ssl_start_time = DateTimeField(default=None, null=True) + + # SSL过期时间 + ssl_expire_time = DateTimeField(default=None, null=True) + + # SSL过期剩余天数,仅用于排序 + ssl_expire_days = IntegerField(default=0, null=False) + + # SSL最后检查时间 + ssl_check_time = DateTimeField(default=None, null=True) + + # SSL证书信息自动更新 + ssl_auto_update = BooleanField(default=True) + + # SSL证书过期监测 + ssl_expire_monitor = BooleanField(default=True) + + # 创建时间 + create_time = DateTimeField(default=datetime.now) + + # 更新时间 + update_time = DateTimeField(default=datetime.now) + + class Meta: + + table_name = 'tb_address' + + indexes = ( + # 唯一索引 + (('domain_id', 'host'), True), # Note the trailing comma! + ) + + @property + def ip_check_time_label(self): + return datetime_util.time_for_human(self.ip_check_time) + + @property + def ssl_check_time_label(self): + return datetime_util.time_for_human(self.ssl_check_time) + + @property + def create_time_label(self): + return datetime_util.format_datetime_label(self.create_time) + + @property + def update_time_label(self): + return datetime_util.time_for_human(self.update_time) + + @property + def ssl_start_date(self): + if self.ssl_start_time and isinstance(self.ssl_start_time, datetime): + return self.ssl_start_time.strftime('%Y-%m-%d') + + @property + def ssl_expire_date(self): + if self.ssl_expire_time and isinstance(self.ssl_expire_time, datetime): + return self.ssl_expire_time.strftime('%Y-%m-%d') + + @property + def real_time_ssl_expire_days(self): + """ + 实时ssl过期剩余天数 + ssl_expire_days 是更新数据时所计算的时间,有滞后性 + :return: + """ + return time_util.get_diff_days(datetime.now(), self.ssl_expire_time) + diff --git a/domain_admin/model/base_model.py b/domain_admin/model/base_model.py index 6b0162c19b..e830db9c66 100644 --- a/domain_admin/model/base_model.py +++ b/domain_admin/model/base_model.py @@ -2,7 +2,8 @@ import logging from logging.handlers import RotatingFileHandler -from peewee import Model +from peewee import Model, SqliteDatabase +from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase from domain_admin.config import SQLITE_DATABASE_PATH @@ -19,8 +20,11 @@ logger.addHandler(handler) logger.setLevel(logging.DEBUG) +# 多线程写入方式,会造成读取不到刚写入的数据 # db = connect(SQLITE_DATABASE_URL) -db = SqliteQueueDatabase(database=SQLITE_DATABASE_PATH) +# db = SqliteQueueDatabase(database=SQLITE_DATABASE_PATH) +# db = SqliteExtDatabase(database=SQLITE_DATABASE_PATH) +db = SqliteDatabase(database=SQLITE_DATABASE_PATH) class BaseModel(Model): diff --git a/domain_admin/model/cache_domain_info_model.py b/domain_admin/model/cache_domain_info_model.py index 9cdd7560c0..4f3d82e672 100644 --- a/domain_admin/model/cache_domain_info_model.py +++ b/domain_admin/model/cache_domain_info_model.py @@ -4,6 +4,7 @@ from peewee import CharField, IntegerField, DateTimeField from domain_admin.model.base_model import BaseModel +from domain_admin.utils import time_util class CacheDomainInfoModel(BaseModel): @@ -46,9 +47,6 @@ def is_expired(self) -> [bool, None]: return None @property - def domain_expire_days(self) -> [int, None]: + def domain_expire_days(self) -> int: """域名过期天数""" - if self.domain_expire_time: - return (self.domain_expire_time - datetime.now()).days - else: - return None + return time_util.get_diff_days(datetime.now(), self.domain_expire_time) diff --git a/domain_admin/model/database.py b/domain_admin/model/database.py index 63ef5c3569..51b72adc53 100644 --- a/domain_admin/model/database.py +++ b/domain_admin/model/database.py @@ -3,7 +3,7 @@ # 创建表 from domain_admin.log import logger from domain_admin.model.base_model import db -from domain_admin.model import domain_model +from domain_admin.model import domain_model, address_model from domain_admin.model import group_model from domain_admin.model import system_model from domain_admin.model import user_model @@ -23,6 +23,7 @@ (domain_model.DomainModel, None), (notify_model.NotifyModel, None), (cache_domain_info_model.CacheDomainInfoModel, None), + (address_model.AddressModel, None), ] diff --git a/domain_admin/model/domain_model.py b/domain_admin/model/domain_model.py index 8c6d49a82e..c55b77e927 100644 --- a/domain_admin/model/domain_model.py +++ b/domain_admin/model/domain_model.py @@ -6,7 +6,7 @@ from domain_admin.model.base_model import BaseModel from domain_admin.model.group_model import GroupModel -from domain_admin.utils import datetime_util +from domain_admin.utils import datetime_util, time_util class DomainModel(BaseModel): @@ -19,16 +19,22 @@ class DomainModel(BaseModel): # 域名 domain = CharField() + # 端口 @since v1.2.24 + port = IntegerField(default=0) + # 别名/备注 alias = CharField(default="") # ip + # @Deprecated ip = CharField(default="") # ip信息检查时间 @since 1.2.12 + # @Deprecated ip_check_time = DateTimeField(default=None, null=True) # 域名信息自动更新 @since v1.2.13 + # @Deprecated ip_auto_update = BooleanField(default=True) # 分组 @@ -49,31 +55,42 @@ class DomainModel(BaseModel): # 域名信息自动更新 @since v1.2.13 domain_auto_update = BooleanField(default=True) + # 域名过期监测 @since v1.2.24 + domain_expire_monitor = BooleanField(default=True) + # SSL签发时间 + # @since v1.2.24 变更为:过期时间最短那个证书 start_time = DateTimeField(default=None, null=True) # SSL过期时间 + # @since v1.2.24 变更为:过期时间最短那个证书 expire_time = DateTimeField(default=None, null=True) # SSL过期剩余天数,仅用于排序 + # @since v1.2.24 变更为:过期时间最短那个证书 expire_days = IntegerField(default=0, null=False) - # 最后检查时间 + # SSL最后检查时间 + # @Deprecated check_time = DateTimeField(default=None, null=True) # SSL证书信息自动更新 @since v1.2.13 + # @Deprecated auto_update = BooleanField(default=True) + # SSL有效期总天数,仅用于排序 + # @Deprecated + total_days = IntegerField(default=0, null=False) + # 连接状态 + # @since v1.2.24 所有ip都连接成功才是成功 connect_status = BooleanField(default=None, null=True) - # 有效期总天数 - total_days = IntegerField(default=0, null=False) - # 通知状态 notify_status = BooleanField(default=True) # 是否监测 @since 1.0.3 + # @Deprecated is_monitor = BooleanField(default=True) # 详细信息 @@ -107,6 +124,14 @@ def create_time_label(self): def check_time_label(self): return datetime_util.time_for_human(self.check_time) + @property + def domain_check_time_label(self): + """ + @since v1.3.1 + :return: + """ + return datetime_util.time_for_human(self.domain_check_time) + @property def update_time_label(self): return datetime_util.time_for_human(self.update_time) @@ -121,10 +146,14 @@ def expire_date(self): if self.expire_time and isinstance(self.expire_time, datetime): return self.expire_time.strftime('%Y-%m-%d') - # @property - # def total_days(self): - # if self.start_time and self.expire_time: - # return (self.expire_time - self.start_time).days + @property + def real_time_ssl_total_days(self): + """ + 实时ssl总天数 + :return: + @since v1.3.1 + """ + return time_util.get_diff_days(self.start_time, self.expire_time) @property def real_time_expire_days(self): @@ -136,6 +165,9 @@ def real_time_expire_days(self): if self.expire_time and isinstance(self.expire_time, datetime): return (self.expire_time - datetime.now()).days + # @since v1.3.1 + real_time_ssl_expire_days = real_time_expire_days + @property def real_time_domain_expire_days(self): """ diff --git a/domain_admin/router/api_map.py b/domain_admin/router/api_map.py index e5ed1c3e6d..962730ebe7 100644 --- a/domain_admin/router/api_map.py +++ b/domain_admin/router/api_map.py @@ -2,7 +2,7 @@ """ 路由配置 """ -from domain_admin.api import cert_api, ip_api, notify_api, whois_api +from domain_admin.api import cert_api, ip_api, notify_api, whois_api, address_api from domain_admin.api import domain_api from domain_admin.api import group_api from domain_admin.api import auth_api @@ -26,6 +26,7 @@ "/api/getDomainList": domain_api.get_domain_list, "/api/getDomainById": domain_api.get_domain_by_id, "/api/updateDomainCertInfoById": domain_api.update_domain_cert_info_by_id, + "/api/updateDomainRowInfoById": domain_api.update_domain_row_info_by_id, "/api/updateAllDomainCertInfo": domain_api.update_all_domain_cert_info, "/api/updateDomainSetting": domain_api.update_domain_setting, @@ -79,4 +80,13 @@ # 实验室 '/api/getWhoisRaw': whois_api.get_whois_raw, + # 主机地址 + '/api/getAddressListByDomainId': address_api.get_address_list_by_domain_id, + '/api/addAddress': address_api.add_address, + '/api/getAddressById': address_api.get_address_by_id, + '/api/deleteAddressById': address_api.delete_address_by_id, + '/api/updateAddressById': address_api.update_address_by_id, + '/api/updateAddressListInfoByDomainId': address_api.update_address_list_info_by_domain_id, + '/api/updateAddressRowInfoById': address_api.update_address_row_info_by_id, + } diff --git a/domain_admin/service/domain_service.py b/domain_admin/service/domain_service.py index ca134d6476..716480fe32 100644 --- a/domain_admin/service/domain_service.py +++ b/domain_admin/service/domain_service.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +""" +domain_service.py +""" import time import traceback import warnings @@ -8,6 +11,7 @@ from playhouse.shortcuts import model_to_dict from domain_admin.log import logger +from domain_admin.model.address_model import AddressModel from domain_admin.model.domain_model import DomainModel from domain_admin.model.group_model import GroupModel from domain_admin.model.log_scheduler_model import LogSchedulerModel @@ -16,23 +20,25 @@ from domain_admin.service import file_service from domain_admin.service import notify_service from domain_admin.service import system_service -from domain_admin.utils import datetime_util, cert_util, whois_util, file_util +from domain_admin.utils import datetime_util, cert_util, whois_util, file_util, time_util from domain_admin.utils import domain_util -from domain_admin.utils.cert_util import cert_common +from domain_admin.utils.cert_util import cert_common, cert_socket_v2 from domain_admin.utils.flask_ext.app_exception import AppException, ForbiddenAppException -def update_domain_info(row: DomainModel): +def update_domain_info(domain_row: DomainModel): """ 更新域名信息 :param row: :return: """ + logger.info("%s", model_to_dict(domain_row)) + # 获取域名信息 domain_info = None try: - domain_info = cache_domain_info_service.get_domain_info(row.domain) + domain_info = cache_domain_info_service.get_domain_info(domain_row.domain) except Exception as e: pass @@ -54,7 +60,7 @@ def update_domain_info(row: DomainModel): domain_check_time=datetime_util.get_datetime(), update_time=datetime_util.get_datetime(), ).where( - DomainModel.id == row.id + DomainModel.id == domain_row.id ).execute() @@ -81,6 +87,157 @@ def update_ip_info(row: DomainModel): ).execute() +def update_ip_info_v2(domain_row: DomainModel): + """ + 更新ip信息 + :param row: + :return: + @since v1.2.24 + """ + logger.info("%s", model_to_dict(domain_row)) + + domain_host_list = [] + + try: + domain_host_list = cert_socket_v2.get_domain_host_list(domain_row.domain, domain_row.port) + except Exception as e: + pass + + for domain_host in domain_host_list: + exist = AddressModel.select().where( + AddressModel.domain_id == domain_row.id, + AddressModel.host == domain_host + ).first() + + if not exist: + AddressModel.insert({ + 'domain_id': domain_row.id, + 'host': domain_host + }).execute() + + # + # DomainModel.update( + # ip=domain_ip, + # ip_check_time=datetime_util.get_datetime(), + # update_time=datetime_util.get_datetime(), + # ).where( + # DomainModel.id == row.id + # ).execute() + + +def update_cert_info_v2(domain_row: DomainModel): + """ + 更新证书信息 + :return: + """ + logger.info("%s", model_to_dict(domain_row)) + + lst = AddressModel.select().where( + AddressModel.domain_id == domain_row.id + ) + + for address_row in lst: + update_address_row_info(address_row, domain_row) + + sync_address_info_to_domain_info(domain_row) + + +def update_address_row_info(address_row, domain_row): + """ + 更新单个地址信息 + :param domain_row: + :param address_row: + :return: + """ + logger.info("update_cert_info_v2: %s - %s", domain_row.domain, address_row.host) + + # 如果不自动更新证书则跳过 + if address_row.ssl_auto_update is False: + logger.info("skip ssl_auto_update: %s - %s", domain_row.domain, address_row.host) + + # 获取证书信息 + cert_info = {} + + try: + cert_info = cert_socket_v2.get_ssl_cert_info( + domain=domain_row.domain, + host=address_row.host, + port=domain_row.port + ) + except Exception as e: + logger.error(traceback.format_exc()) + + address = AddressModel() + address.ssl_start_time = cert_info.get('start_date') + address.ssl_expire_time = cert_info.get('expire_date') + + AddressModel.update( + ssl_start_time=address.ssl_start_time, + ssl_expire_time=address.ssl_expire_time, + ssl_expire_days=address.real_time_ssl_expire_days, + ssl_check_time=datetime_util.get_datetime(), + update_time=datetime_util.get_datetime(), + ).where( + AddressModel.id == address_row.id + ).execute() + + +def update_address_row_info_with_sync_domain_row(address_id: int): + """ + 更新主机信息并同步到与名表 + :param address_id: + :return: + """ + address_row = AddressModel.get_by_id(address_id) + + domain_row = DomainModel.select().where( + DomainModel.id == address_row.domain_id + ).first() + + update_address_row_info(address_row, domain_row) + + sync_address_info_to_domain_info(domain_row) + + +def sync_address_info_to_domain_info(domain_row: DomainModel): + """ + 同步主机信息到域名信息表 + :return: + """ + first_address_row = AddressModel.select().where( + AddressModel.domain_id == domain_row.id + ).order_by( + AddressModel.ssl_expire_days.asc() + ).first() + + logger.info("%s", model_to_dict( + model=first_address_row, + extra_attrs=[ + 'real_time_ssl_expire_days' + ] + )) + + connect_status = False + + if first_address_row is None: + first_address_row = AddressModel() + first_address_row.ssl_start_time = None + first_address_row.ssl_expire_time = None + + elif first_address_row.real_time_ssl_expire_days > 0: + connect_status = True + + DomainModel.update( + start_time=first_address_row.ssl_start_time, + expire_time=first_address_row.ssl_expire_time, + expire_days=first_address_row.real_time_ssl_expire_days, + connect_status=connect_status, + update_time=datetime_util.get_datetime(), + ).where( + DomainModel.id == domain_row.id + ).execute() + + def update_cert_info(row: DomainModel): """ 更新证书信息 @@ -110,25 +267,45 @@ def update_cert_info(row: DomainModel): ).execute() -def update_domain_row(row: DomainModel): +def update_domain_row(domain_row: DomainModel): """ 更新域名相关数据 :param row: :return: """ + logger.info("%s", model_to_dict(domain_row)) + # 如果自动更新禁用,则不更新 - if row.domain_auto_update is True: + if domain_row.domain_auto_update is True: # 域名信息 如果还没有过期,可以不更新 - update_domain_info(row) + update_domain_info(domain_row) - # 如果自动更新禁用,则不更新 - if row.auto_update is True: - # 证书信息 - update_cert_info(row) + # # 如果自动更新禁用,则不更新 + # if row.auto_update is True: + # # 证书信息 + # update_cert_info(row) + + # # ip信息 + # if row is True: + # update_ip_info(row) + + # total = AddressModel.select().where( + # AddressModel.domain_id == domain_row.id + # ).count() + + # 主机列表,不存在则更新 + # if total == 0: + update_domain_address_info(domain_row) + + +def update_domain_address_info(domain_row: DomainModel): + logger.info("%s", model_to_dict(domain_row)) # ip信息 - if row.ip_auto_update is True: - update_ip_info(row) + update_ip_info_v2(domain_row) + + # 证书信息 + update_cert_info_v2(domain_row) def get_cert_info(domain: str): @@ -405,12 +582,14 @@ def check_permission_and_get_row(domain_id, user_id): def add_domain_from_file(filename, user_id): - logger.info('add_domain_from_file') + logger.info('user_id: %s, filename: %s', user_id, filename) lst = domain_util.parse_domain_from_file(filename) + lst = [ { 'domain': item['domain'], + 'port': item['port'], 'alias': item.get('alias', ''), 'user_id': user_id, } for item in lst @@ -435,9 +614,6 @@ def add_domain_from_file(filename, user_id): # # traceback.print_exc() # logger.error(traceback.format_exc()) - # 查询 - update_all_domain_cert_info_of_user(user_id=user_id) - # return count diff --git a/domain_admin/service/version_service.py b/domain_admin/service/version_service.py index 8ad9ab09b8..b0b51087fd 100644 --- a/domain_admin/service/version_service.py +++ b/domain_admin/service/version_service.py @@ -6,7 +6,7 @@ """ from domain_admin.enums.version_enum import VersionEnum from domain_admin.log import logger -from domain_admin.migrate import migrate_102_to_103 +from domain_admin.migrate import migrate_102_to_103, migrate_1213_to_131 from domain_admin.migrate import migrate_106_to_110 from domain_admin.migrate import migrate_110_to_1212 from domain_admin.migrate import migrate_1212_to_1213 @@ -95,7 +95,7 @@ def update_version(): VersionEnum.Version_1210, VersionEnum.Version_1211, ]: - # some => 1.2.12 + # 1.1.0 => 1.2.12 logger.info('update version: %s => %s', local_version, VersionEnum.Version_1212) migrate_110_to_1212.execute_migrate() local_version = VersionEnum.Version_1212 @@ -107,6 +107,23 @@ def update_version(): migrate_1212_to_1213.execute_migrate() local_version = VersionEnum.Version_1213 + # 2023-06-03 + if local_version in [ + VersionEnum.Version_1213, + VersionEnum.Version_1214, + VersionEnum.Version_1215, + VersionEnum.Version_1216, + VersionEnum.Version_1217, + VersionEnum.Version_1218, + VersionEnum.Version_1221, + VersionEnum.Version_1222, + VersionEnum.Version_1223 + ]: + # 1.2.13 => 1.3.1 + logger.info('update version: %s => %s', local_version, VersionEnum.Version_131) + migrate_1213_to_131.execute_migrate() + local_version = VersionEnum.Version_131 + # 更新版本号 VersionModel.create( version=current_version diff --git a/domain_admin/utils/cert_util/cert_openssl.py b/domain_admin/utils/cert_util/cert_openssl.py index 6dee60ec92..d99d051eb9 100644 --- a/domain_admin/utils/cert_util/cert_openssl.py +++ b/domain_admin/utils/cert_util/cert_openssl.py @@ -5,17 +5,21 @@ @Author : Peng Shiyu """ +import socket import ssl +import warnings + import OpenSSL -import socket -from domain_admin.log import logger from domain_admin.utils.cert_util import cert_common, cert_consts +warnings.warn("cert_openssl.py is Deprecated, please use cert_socket_v2.py") + def get_cert_info(domain_with_port): """ 获取证书信息 + 存在问题:没有指定主机ip,不一定能获取到正确的证书信息 :param domain_with_port: str :return: dict """ diff --git a/domain_admin/utils/cert_util/cert_socket.py b/domain_admin/utils/cert_util/cert_socket.py index 36cb2dc1e5..daa36221a5 100644 --- a/domain_admin/utils/cert_util/cert_socket.py +++ b/domain_admin/utils/cert_util/cert_socket.py @@ -13,9 +13,11 @@ import socket import ssl - +import warnings from domain_admin.utils.cert_util import cert_consts, cert_common +warnings.warn("cert_socket.py is Deprecated, please use cert_socket_v2.py") + def create_ssl_context(): """ @@ -33,6 +35,7 @@ def get_domain_cert( ): """ 获取证书信息 + 存在问题:没有指定主机ip,不一定能获取到正确的证书信息 :param host: str :param port: int :param timeout: int diff --git a/domain_admin/utils/cert_util/cert_socket_v2.py b/domain_admin/utils/cert_util/cert_socket_v2.py new file mode 100644 index 0000000000..ca48342923 --- /dev/null +++ b/domain_admin/utils/cert_util/cert_socket_v2.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" +@File : cert_socket_v2.py +@Date : 2023-06-03 + +参考:python批量检查通一个集群针对同一个域名解析到不同IP地址证书的有效性 +https://blog.csdn.net/reblue520/article/details/106832780 +""" + +import socket +import ssl +import typing + +from domain_admin.log import logger +from domain_admin.utils import time_util +from domain_admin.utils.cert_util import cert_common + + +def get_domain_host_list(domain: str, port: int = 80) -> typing.List[str]: + """ + 获取域名映射主机地址列表,一对多关系 + :param domain: 域名 + :param port: 端口 + :return: 主机地址列表 + """ + ret = socket.getaddrinfo(host=domain, port=port, proto=socket.IPPROTO_TCP) + + lst = [] + for item in ret: + lst.append(item[4][0]) + + return lst + + +def get_ssl_cert(domain: str, host: str = None, port: int = 443, timeout: int = 3) -> typing.Dict: + """ + 获取主机证书信息 + :param domain: + :param host: + :param port: + :param timeout: + :return: + """ + logger.info("get_ssl_cert: \ndomain: %s\nhost: %s\nport: %s\ntimeout: %s", domain, host, port, timeout) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.settimeout(timeout) + + ssl_context = ssl.create_default_context() + + with ssl_context.wrap_socket(sock, server_hostname=domain) as wrap_socket: + return wrap_socket.getpeercert() + + +def get_ssl_cert_info(domain: str, host: str = None, port: int = 443, timeout: int = 3): + """ + 返回解析好的证书信息数据 + :param domain: + :param host: + :param port: + :param timeout: + :return: + """ + return resolve_cert(get_ssl_cert(domain, host, port, timeout)) + + +def resolve_cert(cert: typing.Dict): + """ + 解析证书信息,仅解析重要信息 + :param cert: + :return: + """ + data = { + "start_date": time_util.parse_time(cert['notBefore']), + "expire_date": time_util.parse_time(cert['notAfter']), + } + + return data diff --git a/domain_admin/utils/domain_util.py b/domain_admin/utils/domain_util.py index 51a32438f7..90de62e5ed 100644 --- a/domain_admin/utils/domain_util.py +++ b/domain_admin/utils/domain_util.py @@ -4,6 +4,7 @@ from tldextract.tldextract import ExtractResult from domain_admin.utils import file_util +from domain_admin.utils.cert_util import cert_consts def parse_domain(domain): @@ -50,9 +51,16 @@ def parse_domain_from_csv_file(filename): if len(fields) > alias_index: alias = fields[alias_index].strip() + if ':' in domain: + domain, port = domain.split(":") + else: + # SSL默认端口 + port = cert_consts.SSL_DEFAULT_PORT + if domain: item = { 'domain': domain, + 'port': port, 'alias': alias, } yield item @@ -69,9 +77,16 @@ def parse_domain_from_txt_file(filename): domain = parse_domain(line.strip()) + if ':' in domain: + domain, port = domain.split(":") + else: + # SSL默认端口 + port = cert_consts.SSL_DEFAULT_PORT + if domain: yield { 'domain': domain, + 'port': port, } diff --git a/domain_admin/utils/time_util.py b/domain_admin/utils/time_util.py new file mode 100644 index 0000000000..ab6d1869d3 --- /dev/null +++ b/domain_admin/utils/time_util.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +@File : time_util.py +@Date : 2023-06-03 +""" +from dateutil import parser +from datetime import datetime + +# 时间格式化 +from peewee import DateTimeField + +DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' + + +def parse_time(time_str) -> datetime: + """ + 解析字符串为时间 + :param time_str: str + :return: str + """ + + return datetime.strptime( + parser.parse(time_str).astimezone().strftime(DATETIME_FORMAT), + DATETIME_FORMAT + ) + + +def get_diff_days(start_date: [datetime, DateTimeField], end_date: [datetime, DateTimeField]): + """ + 获取两个时间对象的时间差天数 + :param start_date: + :param end_date: + :return: + """ + if start_date and end_date \ + and isinstance(start_date, datetime) \ + and isinstance(end_date, datetime): + return (end_date - start_date).days + else: + return 0 diff --git a/domain_admin/version.py b/domain_admin/version.py index 49fff12d83..7c08e58b91 100755 --- a/domain_admin/version.py +++ b/domain_admin/version.py @@ -2,4 +2,4 @@ """ 版本号 """ -VERSION = '1.2.23' +VERSION = '1.3.1' diff --git a/http/address.http b/http/address.http new file mode 100644 index 0000000000..58c9da528e --- /dev/null +++ b/http/address.http @@ -0,0 +1,128 @@ + +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection). +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST {{baseUrl}}/api/getAddressListByDomainId +Content-Type: application/json +X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODY0MDc0NDZ9.E36G8-wf6skdd27hAHnMMaIUKKa2av_bUhGZrLvJxic + +{ + "domain_id": 92 + +} + +### + +POST {{baseUrl}}/api/updateDomainById +Content-Type: application/json +X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUzODh9.rSFWm22AU8z05IqMOFCBC4ZxsVW4PAzeIwZdJZ6Zq8Y + +{ + "id": 2, + "domain": "www.baidu.com", + "alias": "百度", + "group_id": 1 +} + +### + +POST {{baseUrl}}/api/getDomainList +Content-Type: application/json +X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjYwODU0MTh9.k8G3LvPMrKJm5_1Eodzto7X1YAFTz57iKbmolas7oOs + +{ + "page": 1, + "size": 10 +} + +### + +POST {{baseUrl}}/api/getDomainById +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{ + "id": 3 +} + +### +POST {{baseUrl}}/api/deleteDomainById +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{ + "id": 1 +} + +### +POST {{baseUrl}}/api/updateDomainCertInfoById +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{ + "id": 3 +} + +### +POST {{baseUrl}}/api/updateAllDomainCertInfo +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{} + +### +POST {{baseUrl}}/api/updateAllDomainCertInfoOfUser +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{} + +### +POST {{baseUrl}}/api/sendDomainInfoListEmail +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{ + "to_addresses": [ + "1940607002@qq.com" + ] +} + +### + +POST {{baseUrl}}/api/checkDomainCert +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjQ5NjUwMzB9.myLjQpUraYm69JWrgeEUqPpQ_GroEWPIWYCCJqbg5gg + +{} + +### + +POST {{baseUrl}}/api/exportDomainToFile +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjUzMjA3ODN9.pLfVwuxWvBUr4GtJdmBzXs_X8fieaPesgLwgdsIGnv4 + +{} + +### + +POST {{baseUrl}}/api/getAllDomainListOfUser +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjYwODU0MTh9.k8G3LvPMrKJm5_1Eodzto7X1YAFTz57iKbmolas7oOs + +{} + +### +POST {{baseUrl}}/api/getWhoisRaw +Content-Type: application/json +X-TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODYyMDkzMzJ9.azNxgF_GDa-4aI_XOUm8sQbNhJfkY3rNvEaIgn8DUiI + +{ + "domain": "www.baidu.com" +} + +### diff --git a/tests/api/test_domain_api.py b/tests/api/test_domain_api.py new file mode 100644 index 0000000000..00c64566bb --- /dev/null +++ b/tests/api/test_domain_api.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +@File : test_demo.py +@Date : 2022-11-05 +@Author : Peng Shiyu +""" + + +def test_update_domain_cert_info_by_id(client, token): + response = client.post( + path='/api/updateDomainCertInfoById', + json={ + 'domain_id': 1 + }, + headers={'x-token': token} + ) + + print(response.json()) diff --git a/tests/service/__init__.py b/tests/service/__init__.py new file mode 100644 index 0000000000..5f6d77115a --- /dev/null +++ b/tests/service/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +@File : __init__.py.py +@Date : 2023-06-03 +""" \ No newline at end of file diff --git a/tests/service/test_domain_service.py b/tests/service/test_domain_service.py new file mode 100644 index 0000000000..d78c283b54 --- /dev/null +++ b/tests/service/test_domain_service.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +@File : test_domain_service.py +@Date : 2023-06-03 +""" +from domain_admin.model.domain_model import DomainModel +from domain_admin.service import domain_service + + +def test_update_domain_row(): + row = DomainModel.get_by_id(1) + domain_service.update_domain_row(row)