针对反馈的五个问题: 1. 后台"没有映射":新增 /admin/api/gateway/import 与界面「从现有 nginx 配置导入」, 把老统一入口的 nginx 配置贴进来即可解析出全部「域名 → 后端」初稿 (含 requireLogin / 免认证路径 / SSE 流式 / 备案注入 / 自签证书 / auth 与 portal 类型)。 解析只读不落盘,确认后才保存;导入默认不接管导航,避免一导入就改掉手工导航。 2. 添加转发时没有导航选项:添加表单补齐「同时加入 Dashy 导航 / 分组 / 名称 / 描述 / 图标 / 自动抓取」。 3. 添加转发时没有 portal 保护选项:添加表单补齐「启用 portal 统一登录保护」, 并补上 WebSocket 透传与上传上限。 4. 设置里无法配置 portal 账号:新增统一登录账号卡片,账号存数据卷 (portal-auth.json,只存加盐哈希),优先级 数据卷 > 环境变量, 改完立即生效、无需重新部署;会话校验按当前用户名,改账号后旧会话失效。 5. 设置项太乱:按用途分组(站点与外观 / 导航内容 / 历史记录 / 转发与统一登录 / 高级), 外观卡片并入站点组。 另修两个本轮引入的缺陷: - 容器假 unhealthy:内部端口改 18080 后,基础镜像的 HEALTHCHECK 仍指向 8080; 部署脚本生成的 Dockerfile 现在显式覆盖为 :18080/healthz。 - kind=auth 不依赖 portal-auth 容器,指向内置登录服务;避免生成空 proxy_pass。 测试:nginx 配置解析(含兜底段忽略、多域名段、类型判定、默认不接管导航)、 垃圾输入报错、账号存储(不明文、可校验、改名后旧口令失效)、auth 类型生成。
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.11: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)
|
|
}
|
|
}
|