""" Django settings for config project. Generated by 'django-admin startproject' using Django 5.2.7. For more information on this file, see https://docs.djangoproject.com/en/5.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.2/ref/settings/ """ from pathlib import Path import os from dotenv import load_dotenv # 加载环境变量 load_dotenv() def get_bool_env(key: str, default: bool = False) -> bool: """获取布尔值环境变量,兼容多种写法(true/True/TRUE/1/yes)""" value = os.getenv(key, str(default)).lower() return value in ('true', '1', 'yes', 'on') # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key') # SECURITY WARNING: don't run with debug turned on in production! # 安全优先:默认为 False,开发环境需显式设置 DEBUG=True DEBUG = get_bool_env('DEBUG', False) ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 第三方应用 'rest_framework', 'drf_yasg', 'corsheaders', 'channels', # WebSocket 支持 # 业务应用 'apps.common', # 通用工具 'apps.targets', # 扫描目标管理 'apps.scan', # 扫描任务管理 'apps.engine', # 扫描引擎管理 'apps.asset', # 资产管理 ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', # CORS 中间件(必须在 CommonMiddleware 之前) 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', # 已禁用 CSRF 校验 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # 添加自定义模板目录 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'config.wsgi.application' # ASGI 应用配置(用于 WebSocket) ASGI_APPLICATION = 'config.asgi.application' # Database - PostgreSQL 配置 # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'), 'NAME': os.getenv('DB_NAME', 'xingrin'), 'USER': os.getenv('DB_USER', 'postgres'), 'PASSWORD': os.getenv('DB_PASSWORD', ''), 'HOST': os.getenv('DB_HOST', 'localhost'), 'PORT': os.getenv('DB_PORT', '5432'), # 连接池配置:针对长时间扫描任务优化 # 说明: # - 0: 每次请求后关闭(适合不稳定的远程连接) # - 60-120: 推荐值(平衡性能和资源占用) # - 300+: 适合长时间任务(减少连接重建开销) # - None: 永久连接(仅适合专用连接池,不推荐) 'CONN_MAX_AGE': int(os.getenv('DB_CONN_MAX_AGE', '300')), # 远程数据库使用 300 秒,减少重连开销 # PostgreSQL 特定选项 - 针对远程数据库优化 'OPTIONS': { 'connect_timeout': 30, # 连接超时 30 秒(远程数据库需要更长时间) 'options': '-c statement_timeout=60000 -c idle_in_transaction_session_timeout=300000', # SQL 语句超时 60 秒,事务空闲超时 5 分钟 'keepalives_idle': 600, # TCP keepalive 空闲时间 10 分钟 'keepalives_interval': 30, # TCP keepalive 间隔 30 秒 'keepalives_count': 3, # TCP keepalive 重试次数 # 性能优化参数 'sslmode': 'disable', # 禁用SSL以减少连接延迟(如果网络安全可控) 'application_name': 'xingrin_scanner', # 标识应用名称,便于监控 } } } # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ LANGUAGE_CODE = os.getenv('LANGUAGE_CODE', 'zh-hans') TIME_ZONE = os.getenv('TIME_ZONE', 'Asia/Shanghai') USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # ==================== REST Framework 配置 ==================== REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'apps.common.pagination.BasePagination', # 使用基础分页器 # Session 认证(禁用 CSRF,前后端分离项目通过 CORS 控制跨域) 'DEFAULT_AUTHENTICATION_CLASSES': [ 'apps.common.authentication.CsrfExemptSessionAuthentication', ], # JSON 命名格式转换:后端 snake_case ↔ 前端 camelCase 'DEFAULT_RENDERER_CLASSES': ( 'djangorestframework_camel_case.render.CamelCaseJSONRenderer', # 响应数据转换为 camelCase 'djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer', # 浏览器 API 也使用 camelCase ), 'DEFAULT_PARSER_CLASSES': ( 'djangorestframework_camel_case.parser.CamelCaseJSONParser', # 请求数据从 camelCase 转换为 snake_case 'djangorestframework_camel_case.parser.CamelCaseFormParser', # 表单数据也支持转换 'djangorestframework_camel_case.parser.CamelCaseMultiPartParser', # 文件上传支持转换 ), # 转换配置 'JSON_UNDERSCOREIZE': { 'no_underscore_before_number': True, # 数字前不加下划线 (field1 不会变成 field_1) }, } # ==================== CORS 配置 ==================== # 允许所有来源(前后端分离项目,安全性由认证系统保障) CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'True').lower() == 'true' CORS_ALLOW_CREDENTIALS = True # ==================== CSRF 配置 ==================== CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000').split(',') # ==================== Session 配置 ==================== SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 # 7 天 SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' # 跨站请求时发送 cookie # ==================== 扫描结果存储和清理配置 ==================== # 扫描结果存储目录(智能路径配置) _scan_results_dir_env = os.getenv('SCAN_RESULTS_DIR') if _scan_results_dir_env: # 使用环境变量指定的路径(支持相对和绝对路径) if os.path.isabs(_scan_results_dir_env): SCAN_RESULTS_DIR = _scan_results_dir_env # 绝对路径 else: SCAN_RESULTS_DIR = str(BASE_DIR / _scan_results_dir_env) # 相对路径转绝对路径 else: # 默认使用项目目录下的 results 文件夹 SCAN_RESULTS_DIR = str(BASE_DIR / 'results') # 扫描结果保留时间(单位:天) SCAN_RESULTS_RETENTION_DAYS = int(os.getenv('SCAN_RETENTION_DAYS', '3')) # ==================== Redis 配置 ==================== # Redis 配置(用于 WebSocket Channel Layer) REDIS_HOST = os.getenv('REDIS_HOST', 'localhost') REDIS_PORT = int(os.getenv('REDIS_PORT', 6379)) REDIS_DB = int(os.getenv('REDIS_DB', 0)) # Channels Layer 配置(WebSocket 后端) CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [(REDIS_HOST, REDIS_PORT)], 'capacity': 1500, # 单个通道最大消息数 'expiry': 10, # 消息过期时间(秒) }, }, } # ==================== 日志配置 ==================== # 日志配置说明: # 1. 开发环境(DEBUG=True): # - 默认 DEBUG 级别,输出详细调试信息 # - 控制台彩色输出,便于调试 # - 可选文件输出(配置 LOG_DIR) # # 2. 生产环境(DEBUG=False): # - 默认 INFO 级别,只输出关键信息 # - 同时输出到控制台和文件(配置 LOG_DIR) # - 文件自动轮转,避免日志文件过大 # # 3. 环境变量控制: # - LOG_LEVEL: 全局日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL) # - LOG_DIR: 日志文件目录(留空则不输出文件) # from config.logging_config import get_logging_config LOGGING = get_logging_config(debug=DEBUG) # 命令执行日志开关(供 apps.scan.utils.command_executor 使用) ENABLE_COMMAND_LOGGING = get_bool_env('ENABLE_COMMAND_LOGGING', True) # 扫描工具基础路径(后端和 Worker 统一使用该路径前缀存放三方工具等文件) SCAN_TOOLS_BASE_PATH = os.getenv('SCAN_TOOLS_PATH', '/opt/xingrin/tools') # 字典文件基础路径(后端和 Worker 统一使用该路径前缀存放字典文件) WORDLISTS_BASE_PATH = os.getenv('WORDLISTS_PATH', '/opt/xingrin/wordlists') # Nuclei 模板基础路径(custom / public 两类模板目录) NUCLEI_CUSTOM_TEMPLATES_DIR = os.getenv('NUCLEI_CUSTOM_TEMPLATES_DIR', '/opt/xingrin/nuclei-templates/custom') NUCLEI_PUBLIC_TEMPLATES_DIR = os.getenv('NUCLEI_PUBLIC_TEMPLATES_DIR', '/opt/xingrin/nuclei-templates/public') # Nuclei 官方模板仓库地址 NUCLEI_TEMPLATES_REPO_URL = os.getenv('NUCLEI_TEMPLATES_REPO_URL', 'https://github.com/projectdiscovery/nuclei-templates.git') # 对外访问主机与端口(供 Worker 访问 Django 使用) PUBLIC_HOST = os.getenv('PUBLIC_HOST', 'localhost').strip() SERVER_PORT = os.getenv('SERVER_PORT', '8888') # ============================================ # 任务分发器配置(负载感知调度) # ============================================ # Docker 镜像版本配置(统一管理,确保所有组件版本一致) DOCKER_USER = os.getenv('DOCKER_USER', 'yyhuni') IMAGE_TAG = os.getenv('IMAGE_TAG', '') # 任务执行器镜像配置(task_distributor 用于创建 worker 容器执行扫描任务) # # 版本一致性说明: # - 生产环境:IMAGE_TAG 锁定版本,确保 server 和 worker 版本一致 # - 开发环境:可通过 TASK_EXECUTOR_IMAGE 环境变量指向本地构建镜像 # # 环境区分逻辑: # 1. 主服务器环境: # - 有 IMAGE_TAG 环境变量(从 docker/.env 读取,install.sh 写入) # - 需要 TASK_EXECUTOR_IMAGE 来创建 worker 容器执行任务 # - 默认使用 {DOCKER_USER}/xingrin-worker:{IMAGE_TAG}(版本锁定) # - 开发时可设置 TASK_EXECUTOR_IMAGE=本地镜像名 覆盖 # # 2. Worker 容器环境: # - 无 IMAGE_TAG 环境变量(容器启动时未注入) # - 无 .env 文件 # - 不需要 TASK_EXECUTOR_IMAGE(worker 只执行任务,不会再创建其他容器) # - 设为空字符串避免 AttributeError,确保 settings 模块正常加载 # # 开发环境配置示例: # 在 docker/.env 中添加:TASK_EXECUTOR_IMAGE=docker-agent:test-v1.0.0 if IMAGE_TAG: # 主服务器场景:构建完整镜像名 TASK_EXECUTOR_IMAGE = os.getenv('TASK_EXECUTOR_IMAGE', f'{DOCKER_USER}/xingrin-worker:{IMAGE_TAG}') else: # Worker 容器场景:空值,防止加载错误 TASK_EXECUTOR_IMAGE = os.getenv('TASK_EXECUTOR_IMAGE', '') # 任务提交间隔(秒)- 防止短时间内重复分配到同一节点 # 应大于心跳间隔(3秒),确保负载数据已更新 TASK_SUBMIT_INTERVAL = int(os.getenv('TASK_SUBMIT_INTERVAL', '6')) # 本地 Worker Docker 网络名称(与 docker-compose.yml 中定义的一致) DOCKER_NETWORK_NAME = os.getenv('DOCKER_NETWORK_NAME', 'xingrin_network') # 宿主机挂载源路径(所有节点统一使用固定路径) # 部署前需创建:mkdir -p /opt/xingrin/{results,logs} HOST_RESULTS_DIR = '/opt/xingrin/results' HOST_LOGS_DIR = '/opt/xingrin/logs' # ============================================ # Worker 配置中心(任务容器从 /api/workers/config/ 获取) # ============================================ # Worker 数据库/Redis 地址由 worker_views.py 的 config API 动态返回 # 根据请求来源(本地/远程)返回不同的配置: # - 本地 Worker(Docker 网络内):使用内部服务名(postgres, redis) # - 远程 Worker(公网访问):使用 PUBLIC_HOST # # 以下变量仅作为备用/兼容配置,实际配置由 API 动态生成 _db_host = DATABASES['default']['HOST'] _is_internal_db = _db_host in ('postgres', 'localhost', '127.0.0.1') WORKER_DB_HOST = os.getenv('WORKER_DB_HOST', _db_host) # 远程 Worker 访问 Redis 的地址(自动推导) # - 如果 PUBLIC_HOST 是外部 IP → 使用 PUBLIC_HOST # - 如果 PUBLIC_HOST 是 Docker 内部名 → 使用 redis(本地部署) _is_internal_public = PUBLIC_HOST in ('server', 'localhost', '127.0.0.1') WORKER_REDIS_URL = os.getenv( 'WORKER_REDIS_URL', 'redis://redis:6379/0' if _is_internal_public else f'redis://{PUBLIC_HOST}:6379/0' ) # 容器内挂载目标路径(固定值,不需要修改) CONTAINER_RESULTS_MOUNT = '/app/backend/results' CONTAINER_LOGS_MOUNT = '/app/backend/logs'