Prechádzať zdrojové kódy

fix: 修复头部参数漏掉token

lipan 1 týždeň pred
rodič
commit
5a82d7995b

+ 1 - 0
go.sum

@@ -118,6 +118,7 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
 github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=

+ 3 - 1
internal/logic/createservicelogic.go

@@ -306,7 +306,9 @@ func (l *CreateServiceLogic) notifyFuseMount(svc *model.TService, dirs []string)
 		// 发起 POST(DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求)
 		if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
 			func() (map[string]interface{}, int64, error) {
-				resp, rerr := fuseMountClient.Post(baseURL, "application/json", bytes.NewReader(body))
+				// doFuseRequest 注入 X-TOKEN=svc.Iden(接入标识);client.Post 快捷方式带不了自定义头,
+				// 裸发会被外挂鉴权拦截器 401 拒绝
+				resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
 				if rerr != nil {
 					return nil, 0, fmt.Errorf("请求 %s 失败(外挂程序未启动?): %w", baseURL, rerr)
 				}

+ 2 - 1
internal/logic/deleteservicelogic.go

@@ -115,7 +115,8 @@ func (l *DeleteServiceLogic) notifyFuseUnmount(svc *model.TService) string {
 		if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", reqURL.String(),
 			audit.BuildRemoteGetRequest(http.MethodGet, reqURL.String()),
 			func() (map[string]interface{}, int64, error) {
-				resp, rerr := fuseMountClient.Get(reqURL.String()) // 复用挂载逻辑里的 HTTP 客户端(同包变量)
+				// doFuseRequest 注入 X-TOKEN=svc.Iden(接入标识);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
+				resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodGet, reqURL.String(), nil, "") // 复用挂载逻辑里的 HTTP 客户端(同包变量)
 				if rerr != nil {
 					return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
 				}

+ 4 - 3
internal/logic/listdirchildrenlogic.go

@@ -81,7 +81,7 @@ func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*
 	// path.Clean 而非 filepath.Clean: 该路径发给远程 Linux 外挂程序,
 	// 必须保持 / 分隔符;filepath 在 Windows 上跑 udeServer 时会把 / 转成 \
 	dir := path.Clean(p)
-	return l.notifyListDirChildren(ip, port, dir)
+	return l.notifyListDirChildren(ip, port, dir, req.Iden)
 }
 
 // notifyFuseMount 落库成功后回调外挂程序(svc.Ip:svc.Port)的 /api/fuseMount 接口,
@@ -91,7 +91,7 @@ func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*
 //	{ "backing": "<保护目录>-cipher", "mount": "<保护目录>", "ikey": "<密钥内容>" }
 //
 // ikey 取自 t_key.content(按服务配置的密钥标识 keyIden 查询)。
-func (l *ListDirChildrenLogic) notifyListDirChildren(ip, port, dirs string) (*types.DirChildrenResp, error) {
+func (l *ListDirChildrenLogic) notifyListDirChildren(ip, port, dirs, iden string) (*types.DirChildrenResp, error) {
 	if ip == "" || port == "" {
 		return nil, fmt.Errorf("外挂服务 IP/端口为空,无法下发挂载")
 	}
@@ -116,7 +116,8 @@ func (l *ListDirChildrenLogic) notifyListDirChildren(ip, port, dirs string) (*ty
 	reqDesc := audit.BuildRemoteGetRequest(http.MethodGet, finalURL)
 	result, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "ListDirChildren", finalURL, reqDesc,
 		func() (*types.DirChildrenResp, int64, error) {
-			resp, rerr := fuseMountClient.Get(finalURL)
+			// doFuseRequest 注入 X-TOKEN=iden;外挂 /path/lists 已免鉴权,带上无害,统一收口避免遗漏
+			resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
 			if rerr != nil {
 				return nil, 0, fmt.Errorf("请求 %s 失败: %w", baseURL, rerr)
 			}

+ 41 - 20
internal/logic/updateservicelogic.go

@@ -84,6 +84,14 @@ func (l *UpdateServiceLogic) UpdateService(req *types.UpdateServiceReq) (*types.
 		mountWarning, mountStatus = l.applyMountChanges(svc, oldDirs, newDirs, req)
 	}
 
+	// 编辑联动失败时,服务状态与挂载结果强绑定(与新增服务同规则):
+	// 挂载/卸载任一失败 → mount_status=failed 且 status 同步置"停用",
+	// 避免"启用+failed"的错位组合;req.Directories==nil 的调用方(状态开关)不受影响。
+	finalStatus := req.Status
+	if req.Directories != nil && mountStatus == "failed" {
+		finalStatus = "停用"
+	}
+
 	// 事务:更新服务 + 目录全删全插(与新增服务同款后端收口模式,任一步失败整体回滚)
 	err = l.svcCtx.DB.Transact(func(session sqlx.Session) error {
 		svcModel := l.svcCtx.ServiceModel.WithSession(session)
@@ -97,7 +105,7 @@ func (l *UpdateServiceLogic) UpdateService(req *types.UpdateServiceReq) (*types.
 			Iden:        nullString(req.Iden),
 			KeyIden:     nullString(req.KeyIden),
 			KeyAlgo:     nullString(req.KeyAlgo),
-			Status:      nullString(req.Status),
+			Status:      nullString(finalStatus),
 			MountStatus: nullString(mountStatus), // 联动算出的新状态(没联动就是旧值)
 			Brush:       req.Brush,               // 一键刷库配置:编辑只落库改配置,不追溯触发刷库(刷库编排仅在创建流程)
 		}
@@ -157,23 +165,31 @@ func (l *UpdateServiceLogic) applyMountChanges(svc *model.TService, oldDirs, new
 		toUnmount = diffStrings(oldDirs, newDirs)
 	}
 
+	// 已挂载(mounted)的服务编辑时不重新触发挂载:剩余目录仍挂在外挂上,
+	// 重发挂载请求只是白跑一趟幂等检查;若外挂恰好不可达或鉴权不匹配,
+	// 反而会把好端端的 mounted 服务打成 failed。需要补挂/换机重挂时,
+	// 走列表开关"停用(卸载)→启用(挂载)"的正规路径。
+	needMount := svc.MountStatus.String != "mounted"
+
 	var msgs []string
-	// 1. 先卸载:发往【旧】IP(挂载记录登记在旧机器的外挂程序进程里)
+	// 1. 先卸载(清理动作,已挂载服务也保留):发往【旧】IP(挂载记录登记在旧机器的外挂程序进程里)
 	if len(toUnmount) > 0 {
 		oldIp, oldPort := svc.Ip.String, svc.Port // port 已是 int64
 		if oldIp == "" || oldPort == 0 {
 			msgs = append(msgs, "原外挂 IP/端口为空,无法联动卸载,旧目录可能仍处于挂载状态")
 		} else {
 			for _, dir := range toUnmount {
-				if w := l.fuseUnmountOne(oldIp, oldPort, dir); w != "" {
+				// 卸载发往旧机器,用旧 iden 作 X-TOKEN(旧机器的外挂 TOKEN 按旧值配置)
+				if w := l.fuseUnmountOne(svc.Iden.String, oldIp, oldPort, dir); w != "" {
 					msgs = append(msgs, w)
 				}
 			}
 		}
 	}
 
-	// 2. 再挂载:发往【新】IP,新目录全量下发(幂等表自动跳过已挂的)
-	if len(newDirs) > 0 {
+	// 2. 再挂载:仅未挂载(unmounted/failed)的服务才全量下发,
+	//    幂等表自动跳过已挂的目录,顺带把 failed 服务漏挂的目录补挂上(自愈)。
+	if needMount && len(newDirs) > 0 {
 		if req.Ip == "" || req.Port == 0 {
 			msgs = append(msgs, "新外挂 IP/端口为空,无法联动挂载")
 		} else {
@@ -182,7 +198,8 @@ func (l *UpdateServiceLogic) applyMountChanges(svc *model.TService, oldDirs, new
 				msgs = append(msgs, fmt.Sprintf("联动挂载失败: %v", err))
 			} else {
 				for _, dir := range newDirs {
-					if w := l.fuseMountOne(req.Ip, req.Port, dir, ikey); w != "" {
+					// 挂载发往新机器,用新 iden 作 X-TOKEN(新机器的外挂 TOKEN 按新值配置)
+					if w := l.fuseMountOne(req.Iden, req.Ip, req.Port, dir, ikey); w != "" {
 						msgs = append(msgs, w)
 					}
 				}
@@ -191,25 +208,27 @@ func (l *UpdateServiceLogic) applyMountChanges(svc *model.TService, oldDirs, new
 	}
 
 	// 3. mount_status 重算:
-	//    无动作(没卸也没挂,比如目录原样重存) → 保持原值;
-	//    有动作全成功 → 还有新目录 = mounted,清空了目录 = unmounted;
-	//    有动作任一失败 → failed(同时返回聚合警告,前端弹黄条)。
-	hasAction := len(toUnmount) > 0 || len(newDirs) > 0
-	if !hasAction {
-		return "", svc.MountStatus.String
-	}
+	//    任一动作失败 → failed(同时返回聚合警告,主流程会把 status 一并置"停用");
+	//    未挂载服务走了挂载 → 全成功有目录 = mounted,清空目录 = unmounted;
+	//    已挂载服务(未重新挂载)→ 换机全卸了 = unmounted,否则剩余目录仍挂着 = 保持 mounted。
 	if len(msgs) > 0 {
 		return "编辑已保存,但挂载联动存在失败: " + strings.Join(msgs, "; "), "failed"
 	}
-	if len(newDirs) > 0 {
-		return "", "mounted"
+	if needMount {
+		if len(newDirs) > 0 {
+			return "", "mounted"
+		}
+		return "", "unmounted"
+	}
+	if ipChanged && len(toUnmount) > 0 {
+		return "", "unmounted" // 换机把旧挂载全卸了,新机器上没有挂载
 	}
-	return "", "unmounted"
+	return "", svc.MountStatus.String
 }
 
 // fuseMountOne 通知外挂程序挂载单个目录。成功返回空串,失败返回警告文案。
 // 请求体与新增服务完全同款:{backing: 目录+"-cipher", mount: 目录, ikey: 密钥内容}
-func (l *UpdateServiceLogic) fuseMountOne(ip string, port int64, mount, ikey string) string {
+func (l *UpdateServiceLogic) fuseMountOne(iden, ip string, port int64, mount, ikey string) string {
 	// path.Clean(非 filepath!):路径要发给远程 Linux 外挂程序,必须保持 / 分隔符
 	dirLinuxPath := path.Clean(mount)
 	payload := map[string]interface{}{
@@ -225,7 +244,8 @@ func (l *UpdateServiceLogic) fuseMountOne(ip string, port int64, mount, ikey str
 	// DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求
 	if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
 		func() (map[string]interface{}, int64, error) {
-			resp, rerr := fuseMountClient.Post(baseURL, "application/json", bytes.NewReader(body))
+			// doFuseRequest 注入 X-TOKEN=iden(接入标识);client.Post 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
+			resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
 			if rerr != nil {
 				return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, rerr)
 			}
@@ -245,7 +265,7 @@ func (l *UpdateServiceLogic) fuseMountOne(ip string, port int64, mount, ikey str
 
 // fuseUnmountOne 通知外挂程序卸载单个挂载点。成功返回空串,失败返回警告文案。
 // 卸载接口幂等:挂载点不在外挂注册表中也返回成功,所以重存/多卸都零成本。
-func (l *UpdateServiceLogic) fuseUnmountOne(ip string, port int64, mount string) string {
+func (l *UpdateServiceLogic) fuseUnmountOne(iden, ip string, port int64, mount string) string {
 	cleanMount := path.Clean(mount)                                  // 同上,发给 Linux 必须保持 / 分隔符
 	baseURL := fmt.Sprintf("http://%s:%d/api/fuseUnMount", ip, port) // %d:port 是 int64
 	reqURL, uerr := url.Parse(baseURL)
@@ -259,7 +279,8 @@ func (l *UpdateServiceLogic) fuseUnmountOne(ip string, port int64, mount string)
 	if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
 		audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
 		func() (map[string]interface{}, int64, error) {
-			resp, rerr := fuseMountClient.Get(finalURL)
+			// doFuseRequest 注入 X-TOKEN=iden(接入标识);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
+			resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
 			if rerr != nil {
 				return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
 			}