用户要求:备案号与域名都应当可配置,仓库内容要脱敏。 功能(不只是替换字面量): - 被代理页面注入的备案号改为读后台「站点信息」的 icp/gongan,**未配置就不注入**; 原先写死具体备案号,等于每个部署者都会把别人的备案信息印到自己页面上 - 注入内容做 nginx 安全处理(去引号/反斜杠/换行/美元符),并有测试覆盖 - auth.go:PORTAL_COOKIE_DOMAIN / PORTAL_HOME_URL 不再预置域名, cookie 默认 host-only、首页默认相对路径 "/" - gateway.go:默认 loginUrl 不再预置域名(受保护路由缺配置会明确报错,不静默生成跳不出去的登录页) - 首装脚本在给了 NAVI_DOMAIN 时自动注入 PORTAL_COOKIE_DOMAIN/PORTAL_HOME_URL, 避免"去掉写死默认值"导致跨子域单点登录失效 脱敏:内网 IP(10.0.0.10)与真实备案号清零;真实域名仅剩门户页内容 8 处(待定); 测试夹具/示例/错误页/脚本/后台占位提示全部通用化。 README:新增「跨子域单点登录需要的两个环境变量」;环境变量表改为 NAVI_HOST 必填。 测试:新增备案号注入三条(用配置、未配置不注入、危险字符清理)。
148 lines
4.1 KiB
Go
148 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const sampleConf = `map $host $backend {
|
|
hostnames;
|
|
default "";
|
|
gitea.example.com http://127.0.0.1:3000;
|
|
login.example.com http://127.0.0.1:4001;
|
|
dashy.example.com http://127.0.0.1:3003;
|
|
modulerouter.example.com http://10.0.0.5:8081;
|
|
}
|
|
server {
|
|
listen 3080 ssl;
|
|
server_name example.com www.example.com;
|
|
root /usr/share/nginx/html;
|
|
error_page 404 /404.html;
|
|
}
|
|
server {
|
|
listen 3080 ssl;
|
|
server_name gitea.example.com;
|
|
location / {
|
|
sub_filter '</footer>' 'x';
|
|
proxy_pass $backend; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
|
|
proxy_ssl_verify off;
|
|
}
|
|
}
|
|
server {
|
|
listen 3080 ssl;
|
|
server_name login.example.com;
|
|
location / { proxy_pass $backend; }
|
|
}
|
|
server {
|
|
listen 3080 ssl;
|
|
server_name modulerouter.example.com dify.example.com;
|
|
location / {
|
|
postpone_output 0; proxy_buffering off;
|
|
proxy_pass $backend;
|
|
}
|
|
}
|
|
server {
|
|
listen 3080 ssl;
|
|
server_name dashy.example.com;
|
|
auth_request /_auth;
|
|
error_page 401 = @login;
|
|
location @login { return 302 https://login.example.com/login; }
|
|
location /search { auth_request off; proxy_pass $backend; }
|
|
location ~ ^/(api|autocompleter|static)/ { auth_request off; proxy_pass $backend; }
|
|
location / { proxy_pass $backend; }
|
|
}
|
|
server {
|
|
listen 3080 ssl default_server;
|
|
server_name _;
|
|
root /usr/share/nginx/html;
|
|
location / { return 404; }
|
|
}
|
|
`
|
|
|
|
func TestParseNginxConf(t *testing.T) {
|
|
routes, err := parseNginxConf(sampleConf)
|
|
if err != nil {
|
|
t.Fatalf("解析失败: %v", err)
|
|
}
|
|
byHost := map[string]Route{}
|
|
for _, r := range routes {
|
|
byHost[r.Host] = r
|
|
}
|
|
if _, ok := byHost["_"]; ok {
|
|
t.Fatalf("不应把默认段当成服务")
|
|
}
|
|
d, ok := byHost["dashy.example.com"]
|
|
if !ok || !d.RequireLogin || d.Upstream != "http://127.0.0.1:3003" {
|
|
t.Fatalf("dashy 解析不对: %+v", d)
|
|
}
|
|
if len(d.BypassPaths) == 0 {
|
|
t.Fatalf("dashy 应解析出免认证路径: %+v", d.BypassPaths)
|
|
}
|
|
g := byHost["gitea.example.com"]
|
|
if g.RequireLogin || !g.InjectBeian || !g.InsecureTLS || !g.Websocket {
|
|
t.Fatalf("gitea 解析不对: %+v", g)
|
|
}
|
|
m := byHost["modulerouter.example.com"]
|
|
if m.RequireLogin || !m.Streaming {
|
|
t.Fatalf("modulerouter 应解析为免认证 + 流式: %+v", m)
|
|
}
|
|
if _, ok := byHost["dify.example.com"]; !ok {
|
|
t.Fatalf("同一段里多个域名都要生成路由")
|
|
}
|
|
if l := byHost["login.example.com"]; l.Kind != "auth" || l.Upstream != "" {
|
|
t.Fatalf("login 应为内置 auth: %+v", l)
|
|
}
|
|
if p := byHost["example.com"]; p.Kind != "portal" {
|
|
t.Fatalf("静态门户段应为 portal: %+v", p)
|
|
}
|
|
for _, r := range routes {
|
|
if r.NavEnabled {
|
|
t.Fatalf("导入不应默认接管导航: %+v", r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseNginxConfRejectsGarbage(t *testing.T) {
|
|
if _, err := parseNginxConf(" "); err == nil {
|
|
t.Fatalf("空内容应报错")
|
|
}
|
|
if _, err := parseNginxConf("hello world"); err == nil {
|
|
t.Fatalf("没有任何 server_name 应报错")
|
|
}
|
|
}
|
|
|
|
func TestPortalAuthStore(t *testing.T) {
|
|
dir := t.TempDir()
|
|
oldConf := confFile
|
|
confFile = dir + "/conf.yml"
|
|
defer func() { confFile = oldConf }()
|
|
|
|
if err := savePortalAuthFile("jianf", "s3cret-pass"); err != nil {
|
|
t.Fatalf("保存失败: %v", err)
|
|
}
|
|
raw, err := os.ReadFile(portalAuthPath())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), "s3cret-pass") {
|
|
t.Fatalf("不得写入明文口令: %s", raw)
|
|
}
|
|
if !checkPortalCredentials("jianf", "s3cret-pass") {
|
|
t.Fatalf("正确口令应通过")
|
|
}
|
|
if checkPortalCredentials("jianf", "wrong") || checkPortalCredentials("other", "s3cret-pass") {
|
|
t.Fatalf("错误口令/用户名不得通过")
|
|
}
|
|
if err := savePortalAuthFile("admin2", "another-pass"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if checkPortalCredentials("jianf", "s3cret-pass") || !checkPortalCredentials("admin2", "another-pass") {
|
|
t.Fatalf("改账号后校验未按新值")
|
|
}
|
|
user, source := portalAuthConfigured()
|
|
if user != "admin2" || !strings.Contains(source, "数据卷") {
|
|
t.Fatalf("配置来源不对: %s %s", user, source)
|
|
}
|
|
}
|