Files
xingrin/backend/config/settings.py
2025-12-17 11:02:42 +08:00

362 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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_IMAGEworker 只执行任务,不会再创建其他容器)
# - 设为空字符串避免 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 动态返回
# 根据请求来源(本地/远程)返回不同的配置:
# - 本地 WorkerDocker 网络内使用内部服务名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'