# 星云导航 - Apache 安全配置 (MySQL版)
# 版本: 3.1.2 Pro (安全增强版)

# 启用 URL 重写(必须, 部分服务器需要)
Options +FollowSymLinks
RewriteEngine On

# 如果安装在子目录, 取消下面一行注释并修改为实际子目录路径
# RewriteBase /

# ========== URL美化: 隐藏.php后缀 ==========

# 1. 将带.php的URL 301重定向到无后缀(SEO友好, 向后兼容)
# 注意: api/track.php和api/contacts.php不做301跳转, 前端直连.php后缀,
# 避免POST请求被301重定向后丢失数据或在不支持URL重写的服务器上404.
RewriteCond %{THE_REQUEST} \s/+admin\.php(\?|\s|$) [NC]
RewriteRule ^admin\.php$ /admin [R=301,L,QSA]
RewriteCond %{THE_REQUEST} \s/+health\.php(\?|\s|$) [NC]
RewriteRule ^health\.php$ /health [R=301,L,QSA]
RewriteCond %{THE_REQUEST} \s/+index\.php(\?|\s|$) [NC]
RewriteRule ^index\.php$ / [R=301,L,QSA]

# 2. 无后缀URL内部重写到对应.php文件
# 通用规则: 如果请求的文件不存在, 但加上.php存在, 则内部重写
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^([^/]+)(/.*)?$ $1.php$2 [L,QSA]

# 明确规则(兜底, 确保关键路径一定可用)
# 注意: api/track和api/contacts不检查!-d, 因为api/track/目录存在会导致DirectorySlash 301重定向,
# 而sendBeacon不跟随重定向会导致事件丢失. 直接内部重写到.php文件.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^admin/?$ admin.php [L,QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^health/?$ health.php [L,QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^api/track/?$ api/track.php [L,QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^api/contacts/?$ api/contacts.php [L,QSA]

# 强制 HTTPS (取消注释启用)
# RewriteCond %{HTTPS} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# ========== 安全响应头 ==========
<IfModule mod_headers.c>
    # 基础安全头
    Header set X-Content-Type-Options "nosniff"
    Header set X-Frame-Options "SAMEORIGIN"
    Header set X-XSS-Protection "1; mode=block"
    Header set Referrer-Policy "strict-origin-when-cross-origin"
    Header set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"

    # HSTS (仅在HTTPS环境下启用, 取消注释)
    # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

    # 防止点击劫持
    Header set Content-Security-Policy "frame-ancestors 'self'"

    # 跨域隔离
    Header set Cross-Origin-Opener-Policy "same-origin"
    Header set Cross-Origin-Resource-Policy "same-origin"
</IfModule>

# ========== 敏感文件保护 ==========
# 禁止访问数据库配置文件
<FilesMatch "^config\.php$">
    Require all denied
</FilesMatch>
<FilesMatch "^config\.example\.php$">
    Require all denied
</FilesMatch>

# 禁止访问备份、日志、SQL文件
<FilesMatch "\.(log|ini|env|bak|backup|zip|tar|gz|rar|7z|sql|dump)$">
    Require all denied
</FilesMatch>

# 安装锁文件保护(双重保护)
<FilesMatch "installed\.lock$">
    Require all denied
</FilesMatch>

# 禁止访问隐藏文件(.htaccess, .env, .git等)
<FilesMatch "^\.">
    Require all denied
</FilesMatch>

# 禁止访问composer和依赖配置
<FilesMatch "(composer\.(json|lock)|package\.json|package-lock\.json|\.travis\.yml|\.gitignore)$">
    Require all denied
</FilesMatch>

# ========== 目录保护 ==========
# 禁止目录列表
Options -Indexes

# 保护data目录
<IfModule mod_rewrite.c>
    RewriteRule ^data/ - [F,L]
</IfModule>

# 保护includes目录
<IfModule mod_rewrite.c>
    RewriteRule ^includes/ - [F,L]
</IfModule>

# ========== 请求方法限制 ==========
<LimitExcept GET POST HEAD OPTIONS>
    Require all denied
</LimitExcept>

# ========== PHP 安全配置 ==========
<IfModule mod_php.c>
    # 关闭错误显示(生产环境)
    php_flag display_errors Off
    php_flag display_startup_errors Off
    # 开启错误日志
    php_flag log_errors On
    php_value error_log "data/php_errors.log"
    # 隐藏PHP版本
    php_flag expose_php Off
    # 禁用危险函数(注意: 不禁用curl_exec/curl_multi_exec, 监控功能需要)
    php_value disable_functions "exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source"
    # 禁止远程文件包含
    php_flag allow_url_include Off
    php_flag allow_url_fopen On
    # 限制POST大小
    php_value post_max_size 20M
    php_value upload_max_filesize 20M
    # 限制执行时间
    php_value max_execution_time 60
    php_value max_input_time 60
    # 内存限制
    php_value memory_limit 256M
    # Session安全
    php_flag session.cookie_httponly On
    php_flag session.cookie_secure On
    php_flag session.use_only_cookies On
    php_value session.cookie_samesite "Lax"
</IfModule>

# ========== 默认字符集 ==========
AddDefaultCharset UTF-8

# ========== Gzip 压缩 (v3.2.0 增强) ==========
<IfModule mod_deflate.c>
    # 文本类
    AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml text/javascript
    # 应用类
    AddOutputFilterByType DEFLATE application/javascript application/x-javascript application/json
    AddOutputFilterByType DEFLATE application/xml application/rss+xml application/atom+xml
    AddOutputFilterByType DEFLATE application/x-font-ttf application/x-font-opentype
    AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
    # 图像/字体
    AddOutputFilterByType DEFLATE image/svg+xml image/x-icon
    AddOutputFilterByType DEFLATE font/otf font/ttf font/woff font/woff2
    # 压缩级别 (1-9, 6是性能与压缩率的最佳平衡)
    DeflateCompressionLevel 6
    # 不压缩已压缩的文件
    SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|ico|zip|gz|rar|7z|mp3|mp4|flv|pdf)$ no-gzip dont-vary
</IfModule>

# ========== 浏览器缓存 (v3.2.0 增强, 配合文件版本号) ==========
<IfModule mod_expires.c>
    ExpiresActive On
    # 图片: 1年 (文件名带hash或mtime版本号, 可安全长缓存)
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"
    ExpiresByType image/x-icon "access plus 1 year"
    # CSS/JS: 1个月 (HTML中通过 ?v=mtime 控制版本, 更新后自动失效)
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType application/x-javascript "access plus 1 month"
    ExpiresByType text/javascript "access plus 1 month"
    # 字体: 1年
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType font/ttf "access plus 1 year"
    ExpiresByType font/otf "access plus 1 year"
    ExpiresByType application/font-woff "access plus 1 year"
    ExpiresByType application/font-woff2 "access plus 1 year"
    # HTML: 不缓存 (动态内容)
    ExpiresByType text/html "access plus 0 seconds"
</IfModule>

# ========== Cache-Control 头 (补充mod_expires) ==========
<IfModule mod_headers.c>
    # 静态资源长缓存 + 重新验证
    <FilesMatch "\.(css|js)$">
        Header set Cache-Control "public, max-age=2592000, immutable"
    </FilesMatch>
    <FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
    <FilesMatch "\.(woff|woff2|ttf|otf|eot)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
    # HTML不缓存
    <FilesMatch "\.(html|php)$">
        Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
    </FilesMatch>
</IfModule>

# ========== 自定义错误页 ==========
ErrorDocument 404 /index.php
ErrorDocument 403 /index.php
ErrorDocument 500 /index.php
