chore(sanitize): 备案号与域名一律可配置,仓库不再预置真实域名/备案号/内网地址
用户要求:备案号与域名都应当可配置,仓库内容要脱敏。 功能(不只是替换字面量): - 被代理页面注入的备案号改为读后台「站点信息」的 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 必填。 测试:新增备案号注入三条(用配置、未配置不注入、危险字符清理)。
This commit is contained in:
18
README.md
18
README.md
@ -390,6 +390,22 @@ NAVI_SSH_PASS='<密码>' bash deploy/zd-host.sh v50
|
||||
- 「从现有 nginx 配置导入」可把旧 portal-nginx 的配置整段贴进来,解析出全部域名→后端初稿
|
||||
(含认证状态、免认证路径、SSE、备案注入、自签证书、auth/portal 类型);**解析只读,确认后才保存**
|
||||
|
||||
### 跨子域单点登录需要的两个环境变量
|
||||
|
||||
登录 cookie 不再内置任何域名,**默认下发 host-only cookie**(登录站点自身可用)。
|
||||
要让 `login.example.com` 登录后能访问 `dashy.example.com` 等子域,必须设置:
|
||||
|
||||
| 变量 | 值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `PORTAL_COOKIE_DOMAIN` | `.example.com` | cookie 作用域(前导点表示含所有子域) |
|
||||
| `PORTAL_HOME_URL` | `https://example.com/` | 登录成功后跳回的门户地址(默认 `/`,相对跳转) |
|
||||
|
||||
首装脚本在给了 `NAVI_DOMAIN` 时会自动带上这两个变量;**升级已有部署时请确认容器 env 里已设置**
|
||||
(早期版本靠代码里的写死默认值工作,现在已移除,不设会导致跨子域登录失效)。
|
||||
|
||||
同理,`gateway.yml` 的 `loginUrl` 也不再预置域名:受保护路由要求配置认证与登录地址,
|
||||
留空会在生成配置时报错并说明原因(不会静默生成一个跳不出去的登录页)。
|
||||
|
||||
### 统一登录账号
|
||||
|
||||
管理后台「统一登录账号」写入数据卷 `<data>/portal-auth.json`(**只存加盐哈希,不存明文**)。
|
||||
@ -493,7 +509,7 @@ host 网络启动容器 → 应用配置 + 自检。**自检不全绿就退出**
|
||||
| `NAVI_SYNC_TOKEN` / `NAVI_ADMIN_TOKEN` | 空→自动生成 | 历史同步令牌 / 后台 API 令牌 |
|
||||
| `NAVI_BUILD` / `NAVI_TAR` | `1` / 空 | `0` 表示用 `docker load -i $NAVI_TAR` 导入现成镜像 |
|
||||
| `NAVI_SSH_PASS`(zd-host.sh) | 空 | ssh 密码;用密钥则留空 |
|
||||
| `NAVI_HOST`(zd-host.sh) | `admin@10.0.0.10` | 开发机侧默认值,换机器需覆盖 |
|
||||
| `NAVI_HOST`(zd-host.sh / zd.sh) | **必填、无默认** | 目标机 ssh 地址 `user@host`(仓库不预置任何真实地址);缺失会直接以退出码 2 报错 |
|
||||
|
||||
### 证书与初始网关配置
|
||||
|
||||
|
||||
@ -22,10 +22,13 @@ import (
|
||||
const portalCookieName = "portal_session"
|
||||
|
||||
var (
|
||||
portalUsername = envOr("PORTAL_USERNAME", "admin")
|
||||
portalPassword = envOr("PORTAL_PASSWORD", "changeme")
|
||||
portalCookieDomain = envOr("PORTAL_COOKIE_DOMAIN", ".example.com")
|
||||
portalHomeURL = envOr("PORTAL_HOME_URL", "https://dashy.example.com")
|
||||
portalUsername = envOr("PORTAL_USERNAME", "admin")
|
||||
portalPassword = envOr("PORTAL_PASSWORD", "changeme")
|
||||
// 均不预置真实域名:cookie 域为空时下发 host-only cookie(同一站点可用,
|
||||
// 跨子域单点登录请设置 PORTAL_COOKIE_DOMAIN=.example.com);
|
||||
// 门户首页为空时用相对跳转 "/"。
|
||||
portalCookieDomain = envOr("PORTAL_COOKIE_DOMAIN", "")
|
||||
portalHomeURL = envOr("PORTAL_HOME_URL", "/")
|
||||
portalSessionTTL = 24 * time.Hour
|
||||
portalSecretOnce sync.Once
|
||||
portalSecret []byte
|
||||
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -58,9 +59,10 @@ func defaultGatewayConfig() *GatewayConfig {
|
||||
return &GatewayConfig{
|
||||
ListenPort: 8081,
|
||||
ListenTLSPort: 8443,
|
||||
AuthURL: "http://127.0.0.1:3005/auth/verify",
|
||||
LoginURL: "https://login.example.com/login",
|
||||
Routes: []Route{},
|
||||
AuthURL: "http://127.0.0.1:3005/auth/verify", // 本机内部地址,非外部域名
|
||||
// 登录地址不预置任何域名:未配置时由后台/首装脚本填入(gateway.yml loginUrl)
|
||||
LoginURL: "",
|
||||
Routes: []Route{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,9 +242,13 @@ func writeProxyLocation(b *strings.Builder, r Route, path string, authOff bool)
|
||||
b.WriteString(" proxy_ssl_verify off;\n")
|
||||
}
|
||||
if r.InjectBeian {
|
||||
b.WriteString(" sub_filter '</footer>' '</footer><div class=\"navi-proxy-beian\"><a href=\"https://beian.miit.gov.cn\" target=\"_blank\">京ICP备00000000号-1</a> | <a href=\"https://beian.mps.gov.cn\" target=\"_blank\">京公网安备00000000000000号</a></div>';\n")
|
||||
b.WriteString(" sub_filter_once off;\n")
|
||||
b.WriteString(" proxy_set_header Accept-Encoding \"\";\n")
|
||||
// 备案号来自站点信息(可配置);未配置则不注入,避免把别人的备案号印到你的页面上。
|
||||
if snip := nginxSnippetSafe(beianSnippet()); snip != "" {
|
||||
fmt.Fprintf(b, " sub_filter '</footer>' '</footer><div class=\"navi-proxy-beian\">%s</div>';\n", snip)
|
||||
b.WriteString(" sub_filter_once off;\n")
|
||||
// sub_filter 要生效必须关掉上游压缩,故只在真正注入时才加这行
|
||||
b.WriteString(" proxy_set_header Accept-Encoding \"\";\n")
|
||||
}
|
||||
}
|
||||
b.WriteString(" proxy_redirect off;\n }\n")
|
||||
}
|
||||
@ -293,8 +299,37 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// beianSnippet 生成注入被代理页面的备案 HTML:来自后台「站点信息」的 ICP/公网安备,
|
||||
// 两者都没配置时返回空串(此时不注入任何东西)。
|
||||
// 原先这里写死了具体备案号,等于每个部署者都会带上别人的备案信息。
|
||||
func beianSnippet() string {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil || cfg == nil {
|
||||
return ""
|
||||
}
|
||||
icp := strings.TrimSpace(mapString(cfg.PageInfo, "icp"))
|
||||
gongan := strings.TrimSpace(mapString(cfg.PageInfo, "gongan"))
|
||||
var parts []string
|
||||
if icp != "" {
|
||||
parts = append(parts, `<a href="https://beian.miit.gov.cn" target="_blank">`+html.EscapeString(icp)+`</a>`)
|
||||
}
|
||||
if gongan != "" {
|
||||
parts = append(parts, `<a href="https://beian.mps.gov.cn" target="_blank">`+html.EscapeString(gongan)+`</a>`)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
// nginxSnippetSafe 去掉会破坏 nginx 配置或引发变量展开的字符。
|
||||
func nginxSnippetSafe(s string) string {
|
||||
return strings.NewReplacer("'", "", `\`, "", "\n", "", "\r", "", "$", "").Replace(s)
|
||||
}
|
||||
|
||||
// RenderGatewayConf 生成完整统一入口配置。未知域名使用内置自定义 404 页面,
|
||||
// 反向代理故障使用自定义 500 页面;不会再返回一行纯文本。
|
||||
|
||||
func RenderGatewayConf(cfg *GatewayConfig) (string, error) {
|
||||
if cfg == nil {
|
||||
return "", fmt.Errorf("配置为空")
|
||||
|
||||
@ -12,7 +12,7 @@ const sampleConf = `map $host $backend {
|
||||
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;
|
||||
modulerouter.example.com http://10.0.0.5:8081;
|
||||
}
|
||||
server {
|
||||
listen 3080 ssl;
|
||||
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -438,3 +439,57 @@ func TestRedirectDefaultsNoPortOnNonStandardPort(t *testing.T) {
|
||||
t.Fatalf("默认不应带端口:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// 注进被代理页面的备案号必须来自站点信息(可配置),不能再写死。
|
||||
func TestBeianInjectionUsesPageInfo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
old := confFile
|
||||
defer func() { confFile = old }()
|
||||
confFile = filepath.Join(dir, "conf.yml")
|
||||
if err := os.WriteFile(confFile, []byte("pageInfo:\n icp: 京ICP备12345678号-1\n gongan: 京公网安备11000000000001号\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := RenderGatewayConf(&GatewayConfig{ListenPort: 8081, Routes: []Route{{Host: "a.com", Upstream: "http://1.1.1.1:80", InjectBeian: true}}})
|
||||
if err != nil {
|
||||
t.Fatalf("渲染失败: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "京ICP备12345678号-1") || !strings.Contains(out, "京公网安备11000000000001号") {
|
||||
t.Fatalf("未使用配置的备案号:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// 未配置备案号时不注入(不得残留任何硬编码备案号)。
|
||||
func TestBeianInjectionSkippedWhenUnset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
old := confFile
|
||||
defer func() { confFile = old }()
|
||||
confFile = filepath.Join(dir, "conf.yml")
|
||||
if err := os.WriteFile(confFile, []byte("pageInfo: {}\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := RenderGatewayConf(&GatewayConfig{ListenPort: 8081, Routes: []Route{{Host: "a.com", Upstream: "http://1.1.1.1:80", InjectBeian: true}}})
|
||||
if err != nil {
|
||||
t.Fatalf("渲染失败: %v", err)
|
||||
}
|
||||
if strings.Contains(out, "navi-proxy-beian") {
|
||||
t.Fatalf("未配置备案号却注入了:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// 备案号里的引号/美元符不能破坏 nginx 配置。
|
||||
func TestBeianInjectionSanitized(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
old := confFile
|
||||
defer func() { confFile = old }()
|
||||
confFile = filepath.Join(dir, "conf.yml")
|
||||
if err := os.WriteFile(confFile, []byte("pageInfo:\n icp: \"a';b$c\"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := RenderGatewayConf(&GatewayConfig{ListenPort: 8081, Routes: []Route{{Host: "a.com", Upstream: "http://1.1.1.1:80", InjectBeian: true}}})
|
||||
if err != nil {
|
||||
t.Fatalf("渲染失败: %v", err)
|
||||
}
|
||||
if strings.Contains(out, "';b") || strings.Contains(out, "b$c") {
|
||||
t.Fatalf("危险字符未清理:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +289,7 @@ html[data-theme="dark"] .toast{ box-shadow:0 10px 30px rgba(0,0,0,.5) }
|
||||
<div class="grp-in">添加转发</div>
|
||||
<div class="row">
|
||||
<div class="fg"><label>域名</label><input id="gw-n-host" placeholder="git.example.com"></div>
|
||||
<div class="fg"><label>后端</label><input id="gw-n-up" placeholder="http://10.0.0.10:3000"></div>
|
||||
<div class="fg"><label>后端</label><input id="gw-n-up" placeholder="http://127.0.0.1:3000"></div>
|
||||
<div class="fg" style="max-width:170px"><label>类型</label>
|
||||
<select id="gw-n-kind">
|
||||
<option value="proxy">反向代理</option>
|
||||
|
||||
@ -146,6 +146,8 @@ docker run -d --name navi --network host --restart unless-stopped \
|
||||
-v "${DATA_DIR}:/app/data" -v "${CERT_DIR}:/etc/nginx/certs:ro" \
|
||||
-e NAVI_SYNC_TOKEN="$SYNC_TOKEN" -e NAVI_ADMIN_TOKEN="$ADMIN_TOKEN" \
|
||||
-e SEARXNG_BASE_URL="$BASE_URL" \
|
||||
$([ -n "$DOMAIN" ] && echo "-e PORTAL_COOKIE_DOMAIN=.$DOMAIN") \
|
||||
$([ -n "$DOMAIN" ] && echo "-e PORTAL_HOME_URL=https://$DOMAIN/") \
|
||||
"$TAG" >/dev/null
|
||||
echo " 容器已启动,等待健康检查…"
|
||||
for i in $(seq 1 60); do
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
// 目标环境通过环境变量指定,仓库不预置任何真实域名/主机。
|
||||
const BASE = process.env.PORTAL_BASE || 'https://dashy.example.com';
|
||||
const DOM = process.env.PORTAL_COOKIE_DOMAIN || '.example.com';
|
||||
const { chromium }=require('playwright-core');
|
||||
function rgb(s){const m=s.match(/[\d.]+/g);return m?m.slice(0,4).map(Number):[]}
|
||||
function L(s){const [r=0,g=0,b=0]=rgb(s);return Math.round(.2126*r+.7152*g+.0722*b)}
|
||||
@ -10,10 +13,10 @@ const C=process.env.PORTAL_COOKIE;
|
||||
for(const [w,h] of sizes){
|
||||
for(const [path,name] of views){
|
||||
const ctx=await b.newContext({ignoreHTTPSErrors:true,viewport:{width:w,height:h}});
|
||||
await ctx.addCookies([{name:'portal_session',value:C,domain:'.example.com',path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]);
|
||||
await ctx.addCookies([{name:'portal_session',value:C,domain:DOM,path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]);
|
||||
const p=await ctx.newPage(); const errs=[]; p.on('pageerror',e=>errs.push(String(e)));
|
||||
await p.emulateMedia({colorScheme:'dark'}); await p.addInitScript(()=>localStorage.removeItem('navi-theme'));
|
||||
await p.goto('https://dashy.example.com'+path,{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(3200);
|
||||
await p.goto(BASE+path,{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(3200);
|
||||
const r=await p.evaluate(()=>{
|
||||
const g=s=>{const e=document.querySelector(s);if(!e)return null;const c=getComputedStyle(e);return {c:c.color,bg:c.backgroundColor}};
|
||||
const de=document.documentElement;
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
// 目标环境通过环境变量指定,仓库不预置任何真实域名/主机。
|
||||
const BASE = process.env.PORTAL_BASE || 'https://dashy.example.com';
|
||||
const DOM = process.env.PORTAL_COOKIE_DOMAIN || '.example.com';
|
||||
const { chromium }=require('playwright-core');
|
||||
const C=process.env.PORTAL_COOKIE;
|
||||
(async()=>{
|
||||
const b=await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
for(const [w,h] of [[1280,720],[1280,600],[1024,600],[900,520],[768,600],[390,780]]){
|
||||
const ctx=await b.newContext({ignoreHTTPSErrors:true,viewport:{width:w,height:h}});
|
||||
await ctx.addCookies([{name:'portal_session',value:C,domain:'.example.com',path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]);
|
||||
await ctx.addCookies([{name:'portal_session',value:C,domain:DOM,path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]);
|
||||
const p=await ctx.newPage();
|
||||
await p.emulateMedia({colorScheme:'dark'}); await p.addInitScript(()=>localStorage.removeItem('navi-theme'));
|
||||
await p.goto('https://dashy.example.com/minimal',{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(1800);
|
||||
await p.goto(BASE+'/minimal',{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(1800);
|
||||
const r=await p.evaluate(()=>{
|
||||
const de=document.documentElement;
|
||||
const foot=document.querySelector('.navi-footer');
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
// 目标环境通过环境变量指定,仓库不预置任何真实域名/主机。
|
||||
const BASE = process.env.PORTAL_BASE || 'https://dashy.example.com';
|
||||
const DOM = process.env.PORTAL_COOKIE_DOMAIN || '.example.com';
|
||||
/* HomeDash 浏览器验收脚本(部署后跑一遍,覆盖本轮所有改动点)
|
||||
*
|
||||
* 依赖:node + ws(NODE_PATH 指向装了 ws 的目录)、headless chromium
|
||||
@ -65,7 +68,7 @@ const PEEK = `(() => {
|
||||
const t = await req('PUT', '/json/new?about:blank');
|
||||
const p = await conn(t.webSocketDebuggerUrl);
|
||||
await p('Page.enable'); await p('Runtime.enable'); await p('Network.enable'); await p('Log.enable');
|
||||
await p('Network.setCookie', { name: 'portal_session', value: process.env.CK, domain: '.example.com', path: '/', secure: true });
|
||||
await p('Network.setCookie', { name: 'portal_session', value: process.env.CK, domain: DOM, path: '/', secure: true });
|
||||
await p('Network.setCacheDisabled', { cacheDisabled: true });
|
||||
const ev = async (e) => (await p('Runtime.evaluate', { returnByValue: true, expression: e })).result.value;
|
||||
const clickXY = async (x, y) => {
|
||||
@ -82,7 +85,7 @@ const PEEK = `(() => {
|
||||
console.log('══ A) 渲染开销:带 backdrop-filter 的元素数');
|
||||
await p('Emulation.setDeviceMetricsOverride', { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false });
|
||||
for (const path of ['/', '/minimal']) {
|
||||
await p('Page.navigate', { url: 'https://dashy.example.com' + path });
|
||||
await p('Page.navigate', { url: BASE + path });
|
||||
await sleep(8000);
|
||||
const o = await ev(BLUR);
|
||||
console.log(` ${path}: 总数=${o.total} 明细=${JSON.stringify(o.byClass)}`);
|
||||
@ -91,7 +94,7 @@ const PEEK = `(() => {
|
||||
|
||||
console.log('══ B) 后台窄屏(390px)');
|
||||
await p('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
|
||||
await p('Page.navigate', { url: 'https://dashy.example.com/admin/' });
|
||||
await p('Page.navigate', { url: BASE+'/admin/' });
|
||||
await sleep(7500);
|
||||
const o = await ev(`(() => {
|
||||
const de = document.documentElement;
|
||||
@ -116,7 +119,7 @@ const PEEK = `(() => {
|
||||
|
||||
console.log('══ C) 工作空间:收起态第一次点击只滑出、不生效');
|
||||
await p('Emulation.setDeviceMetricsOverride', { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false });
|
||||
await p('Page.navigate', { url: 'https://dashy.example.com/workspace' });
|
||||
await p('Page.navigate', { url: BASE+'/workspace' });
|
||||
await sleep(8500);
|
||||
let s0 = await ev(PEEK);
|
||||
console.log(` 收起态(视口 ${s0.innerW}x${s0.innerH}): dock 露${s0.innerW - s0.dock.left}px @y=${s0.dock.cy} op=${s0.dock.op} 搜索球露${s0.search.right}px @y=${s0.search.cy} 搜索态=${s0.searchState}`);
|
||||
@ -127,7 +130,7 @@ const PEEK = `(() => {
|
||||
if (!revealOnly) fail++;
|
||||
console.log(` ${revealOnly ? '✓' : '✗'} 第一次点右边缘 → 未跳转(path=${s1.pathname}) 已滑出(露${s1.innerW - s1.dock.left}px op=${s1.dock.op}) peek=${s1.peekOpen}`);
|
||||
// 搜索球:第一次点 sliver 只滑出,不展开输入
|
||||
await p('Page.navigate', { url: 'https://dashy.example.com/workspace' });
|
||||
await p('Page.navigate', { url: BASE+'/workspace' });
|
||||
await sleep(8000);
|
||||
s0 = await ev(PEEK);
|
||||
await clickXY(Math.max(3, s0.search.right - 7), s0.search.cy);
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
// 目标环境通过环境变量指定,仓库不预置任何真实域名/主机。
|
||||
const BASE = process.env.PORTAL_BASE || 'https://dashy.example.com';
|
||||
const DOM = process.env.PORTAL_COOKIE_DOMAIN || '.example.com';
|
||||
const { chromium }=require('playwright-core');
|
||||
function rgb(s){const m=s.match(/[\d.]+/g);return m?m.slice(0,4).map(Number):[]}
|
||||
function L(a){return Math.round(.2126*(a[0]||0)+.7152*(a[1]||0)+.0722*(a[2]||0))}
|
||||
@ -5,10 +8,10 @@ const C=process.env.PORTAL_COOKIE;
|
||||
(async()=>{
|
||||
const b=await chromium.connectOverCDP('http://127.0.0.1:9222');
|
||||
const ctx=await b.newContext({ignoreHTTPSErrors:true,viewport:{width:1400,height:900}});
|
||||
await ctx.addCookies([{name:'portal_session',value:C,domain:'.example.com',path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]);
|
||||
await ctx.addCookies([{name:'portal_session',value:C,domain:DOM,path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]);
|
||||
const p=await ctx.newPage(); const errs=[]; p.on('pageerror',e=>errs.push(String(e)));
|
||||
await p.emulateMedia({colorScheme:'dark'}); await p.addInitScript(()=>{try{localStorage.removeItem('navi-theme')}catch(e){}});
|
||||
await p.goto('https://dashy.example.com/workspace',{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(3000);
|
||||
await p.goto(BASE+'/workspace',{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(3000);
|
||||
const n=await p.evaluate(()=>[...document.querySelectorAll('nav.side-bar a,nav.side-bar button,.side-bar-item')].length);
|
||||
let modal=null;
|
||||
for(let i=0;i<n;i++){
|
||||
|
||||
@ -17,7 +17,9 @@ set -euo pipefail
|
||||
VER="${1:?用法: deploy/zd-host.sh <version>}"
|
||||
TAG="homedash:ui-${VER}"
|
||||
STAMP="$(date +%Y%m%d%H%M%S)"
|
||||
HOST="${NAVI_HOST:-admin@10.0.0.10}"
|
||||
|
||||
[ -n "$HOST" ] || { echo "!! 请指定目标机:NAVI_HOST=user@host bash $0 ..." >&2; exit 2; }
|
||||
HOST="${NAVI_HOST:-}" # 必填:NAVI_HOST=user@host(仓库不预置任何真实地址)
|
||||
SSH_PASS="${NAVI_SSH_PASS:-}"
|
||||
DATA_DIR="${NAVI_DATA_DIR:-/vol1/docker/navi-data}"
|
||||
CERT_DIR="${NAVI_CERT_DIR:-/vol1/docker/cert}"
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
# deploy/zd.sh <version> 例: deploy/zd.sh v29
|
||||
#
|
||||
# 环境变量(默认值即本项目生产环境,可覆盖):
|
||||
# NAVI_HOST 目标机 ssh 地址,默认 admin@10.0.0.10
|
||||
# NAVI_HOST 目标机 ssh 地址(必填)
|
||||
# NAVI_SSH_PASS 目标机密码(无 ssh key 时必填)
|
||||
# NAVI_UPSTREAM_HOST 门户 nginx 里 dashy 域名对应的主机名,默认 dashy.example.com
|
||||
# NAVI_PORTAL_CONF 门户 nginx 配置文件绝对路径
|
||||
@ -24,7 +24,9 @@ set -euo pipefail
|
||||
VER="${1:?用法: deploy/zd.sh <version>}"
|
||||
TAG="homedash:ui-${VER}"
|
||||
STAMP="$(date +%Y%m%d%H%M%S)"
|
||||
HOST="${NAVI_HOST:-admin@10.0.0.10}"
|
||||
|
||||
[ -n "$HOST" ] || { echo "!! 请指定目标机:NAVI_HOST=user@host bash $0 ..." >&2; exit 2; }
|
||||
HOST="${NAVI_HOST:-}" # 必填:NAVI_HOST=user@host
|
||||
SSH_PASS="${NAVI_SSH_PASS:-}"
|
||||
UPSTREAM_HOST="${NAVI_UPSTREAM_HOST:-dashy.example.com}"
|
||||
PORTAL_CONF="${NAVI_PORTAL_CONF:-/vol1/docker/portal/nginx/nginx.conf}"
|
||||
|
||||
@ -232,10 +232,10 @@ func TestDibToPNGFromICOPrefersLargest(t *testing.T) {
|
||||
|
||||
func TestSanitizeHostKey(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"example.com": "example.com",
|
||||
"10.0.0.10:3000": "10.0.0.10_3000",
|
||||
"example.com:8443": "example.com_8443",
|
||||
"[::1]:8080": "___1__8080",
|
||||
"example.com": "example.com",
|
||||
"10.0.0.9:3000": "10.0.0.9_3000",
|
||||
"example.com:8443": "example.com_8443",
|
||||
"[::1]:8080": "___1__8080",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := sanitizeHostKey(in); got != want {
|
||||
|
||||
Reference in New Issue
Block a user