package main import ( "archive/tar" "compress/gzip" "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "os" "os/exec" "path" "path/filepath" "strings" "sync" "time" "hnyfkj.com.cn/rtu/linux/baseapp" "hnyfkj.com.cn/rtu/linux/utils/jsonrpc2" ) type PkgInfo struct { FileName string `json:"fileName"` FileSize int64 `json:"fileSize"` CheckMD5Val string `json:"fileMD5"` UploadTime string `json:"uploadTime"` } type AppInfo struct { Name string `json:"name"` Version string `json:"version"` Executable string `json:"executable"` LibPaths []string `json:"libraryPaths"` Description string `json:"description"` ReadmeFile string `json:"readmeFile"` } type InsInfo struct { Installed bool `json:"installed"` InstallTime string `json:"installTime"` } type AppIns struct { Pkg PkgInfo `json:"pkg"` // 应用的安装包 App AppInfo `json:"app"` // 应用详细信息 Ins InsInfo `json:"ins"` // 应用是否安装 } var ( appMutex sync.Mutex ) const ( userAppInsDir = "/home/root/yfkj-user-app" userAppService = "/etc/systemd/system/yfkj-user-app.service" ErrConflict jsonrpc2.ErrCode = -32099 ///////// 资源访问冲突 ) // 导入用户安装包 func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response { appMutex.Lock() defer appMutex.Unlock() var params map[string]string if err := json.Unmarshal(req.Params, ¶ms); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error()) } file, ok := params["file"] if !ok || file == "" { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "missing or empty 'file' parameter") } defer os.Remove(file) if !strings.HasSuffix(strings.ToLower(filepath.Base(file)), ".tar.gz") { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package format") } info, err := os.Stat(file) if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error()) } if !info.Mode().IsRegular() { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package file") } if err := os.MkdirAll(userAppPkgDir, 0755); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if hasImportedPackage(userAppPkgDir) { return jsonrpc2.BuildError(req, ErrConflict, "application package already exists") } appInfo, err := readAppJSON(file) if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error()) } md5Val, err := fileMD5(file) if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } savePkgFile := filepath.Join(userAppPkgDir, filepath.Base(file)) if err := moveFile(file, savePkgFile); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } appIns := AppIns{ Pkg: PkgInfo{ FileName: filepath.Base(file), FileSize: info.Size(), CheckMD5Val: md5Val, UploadTime: time.Now().Format("2006-01-02 15:04:05"), }, App: appInfo, Ins: InsInfo{ Installed: false, }, } data, err := json.MarshalIndent(appIns, "", " ") if err != nil { os.Remove(savePkgFile) return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } appInsFile := filepath.Join(userAppPkgDir, "appins.json") if err := writeFileAtomic(appInsFile, data, 0644); err != nil { os.Remove(savePkgFile) return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } baseapp.Logger.Infof("导入用户应用安装包成功: %s, 版本: %s, 描述: %s", appIns.App.Name, appIns.App.Version, appIns.App.Description) return jsonrpc2.BuildResponse(req, "success", nil) } // 删除用户安装包 func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response { appMutex.Lock() defer appMutex.Unlock() appInsFile := filepath.Join(userAppPkgDir, "appins.json") var appIns AppIns data, err := os.ReadFile(appInsFile) if err == nil { if err := json.Unmarshal(data, &appIns); err == nil { if appIns.Ins.Installed { // 用户应用已安装 return jsonrpc2.BuildError(req, ErrConflict, "application is installed, please uninstall it first") } } } else if !os.IsNotExist(err) { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := os.RemoveAll(userAppPkgDir); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := os.MkdirAll(userAppPkgDir, 0755); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } baseapp.Logger.Infof("删除用户应用安装包成功: %s, 版本: %s, 描述: %s", appIns.App.Name, appIns.App.Version, appIns.App.Description) return jsonrpc2.BuildResponse(req, "success", nil) } // 安装包应用安装 func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response { appMutex.Lock() defer appMutex.Unlock() appInsFile := filepath.Join(userAppPkgDir, "appins.json") data, err := os.ReadFile(appInsFile) if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "application package not found") } var appIns AppIns if err := json.Unmarshal(data, &appIns); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if appIns.Ins.Installed { return jsonrpc2.BuildError(req, ErrConflict, "application already installed") } success := false defer func() { if !success { baseapp.Logger.Warnf("安装用户应用失败状态回滚: %s", userAppInsDir) systemctlIgnoreError("stop", "yfkj-user-app.service") systemctlIgnoreError("disable", "yfkj-user-app.service") os.RemoveAll(userAppInsDir) os.Remove(userAppService) systemctlIgnoreError("daemon-reload") } }() systemctlIgnoreError("stop", "yfkj-user-app.service") if err := os.MkdirAll(userAppInsDir, 0755); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } appPkgFile := filepath.Join(userAppPkgDir, appIns.Pkg.FileName) if err := extractTarGz(appPkgFile, userAppInsDir); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } execFile := filepath.Join(userAppInsDir, appIns.App.Executable) info, err := os.Stat(execFile) if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "executable not found") } if !info.Mode().IsRegular() { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "invalid executable") } if err := os.Chmod(execFile, 0755); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := createUserAppService(appIns.App, userAppInsDir); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := systemctl("daemon-reload"); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := systemctl("enable", "yfkj-user-app.service"); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := systemctl("start", "yfkj-user-app.service"); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } appIns.Ins.Installed = true appIns.Ins.InstallTime = time.Now().Format("2006-01-02 15:04:05") data, err = json.MarshalIndent(appIns, "", " ") if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := writeFileAtomic(appInsFile, data, 0644); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } success = true baseapp.Logger.Infof("安装用户应用成功: %s, 版本: %s, 描述: %s", appIns.App.Name, appIns.App.Version, appIns.App.Description) return jsonrpc2.BuildResponse(req, "success", nil) } // 卸载已安装应用 func appUninstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response { appMutex.Lock() defer appMutex.Unlock() appInsFile := filepath.Join(userAppPkgDir, "appins.json") data, err := os.ReadFile(appInsFile) if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "application package not found") } var appIns AppIns if err := json.Unmarshal(data, &appIns); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if !appIns.Ins.Installed { return jsonrpc2.BuildError(req, ErrConflict, "application not installed") } systemctlIgnoreError("stop", "yfkj-user-app.service") systemctlIgnoreError("disable", "yfkj-user-app.service") if err := os.Remove(userAppService); err != nil && !os.IsNotExist(err) { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := systemctl("daemon-reload"); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := os.RemoveAll(userAppInsDir); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } appIns.Ins.Installed = false appIns.Ins.InstallTime = "" data, err = json.MarshalIndent(appIns, "", " ") if err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if err := writeFileAtomic(appInsFile, data, 0644); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } baseapp.Logger.Infof("卸载用户应用成功: %s, 版本: %s, 描述: %s", appIns.App.Name, appIns.App.Version, appIns.App.Description) return jsonrpc2.BuildResponse(req, "success", nil) } // 安装包应用信息 func getInstallInfo(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response { appInsFile := filepath.Join(userAppPkgDir, "appins.json") data, err := os.ReadFile(appInsFile) if err != nil { if os.IsNotExist(err) { return jsonrpc2.BuildResponse(req, nil, nil) } return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } var appIns AppIns if err := json.Unmarshal(data, &appIns); err != nil { return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error()) } if appIns.App.ReadmeFile != "" { readme, err := os.ReadFile(filepath.Join(userAppInsDir, appIns.App.ReadmeFile)) if err != nil { appIns.App.ReadmeFile = "" } else { appIns.App.ReadmeFile = string(readme) } } return jsonrpc2.BuildResponse(req, appIns, nil) } func readAppJSON(file string) (AppInfo, error) { var app AppInfo f, err := os.Open(file) if err != nil { return app, err } defer f.Close() gz, err := gzip.NewReader(f) if err != nil { return app, err } defer gz.Close() tr := tar.NewReader(gz) for { header, err := tr.Next() if err == io.EOF { break } if err != nil { return app, err } if header.Typeflag != tar.TypeReg { continue } if path.Base(header.Name) != "app.json" { continue } if header.Size > 2<<20 { // 2MB 上限 return app, fmt.Errorf("app.json 文件过大") } data, err := io.ReadAll(io.LimitReader(tr, header.Size)) if err != nil { return app, err } err = json.Unmarshal(data, &app) if err != nil { return app, err } if app.Name == "" || app.Version == "" || app.Executable == "" { return app, fmt.Errorf("app.json参数不完整") } if !validRelativePath(app.Executable) { return app, fmt.Errorf("invalid executable path") } for _, p := range app.LibPaths { if validRelativePath(p) { continue } return app, fmt.Errorf("invalid library path: %s", p) } return app, nil } return app, fmt.Errorf("安装包中未找到 app.json") } func fileMD5(file string) (string, error) { f, err := os.Open(file) if err != nil { return "", err } defer f.Close() h := md5.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func moveFile(src, dst string) error { if err := os.Rename(src, dst); err == nil { return nil } in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } _, err = io.Copy(out, in) if err != nil { out.Close() os.Remove(dst) return err } if err := out.Close(); err != nil { os.Remove(dst) return err } return os.Remove(src) } func validRelativePath(p string) bool { if p == "" { return false } if filepath.IsAbs(p) { return false } clean := filepath.Clean(p) if clean != p { return false } if clean == ".." || strings.HasPrefix(clean, ".."+string(os.PathSeparator)) { return false } return true } func isRegularFile(name string) bool { info, err := os.Stat(name) if err != nil { return false } return info.Mode().IsRegular() } func hasImportedPackage(dir string) bool { if isRegularFile(filepath.Join(dir, "appins.json")) { return true } entries, err := os.ReadDir(dir) if err != nil { return false } for _, entry := range entries { if entry.IsDir() { continue } if strings.HasSuffix(strings.ToLower(entry.Name()), ".tar.gz") { return true } } return false } func writeFileAtomic(name string, data []byte, perm os.FileMode) error { tmp := name + ".tmp" defer os.Remove(tmp) f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) if err != nil { return err } if _, err := f.Write(data); err != nil { f.Close() return err } if err := f.Sync(); err != nil { f.Close() return err } if err := f.Close(); err != nil { return err } return os.Rename(tmp, name) } func extractTarGz(src string, dst string) error { f, err := os.Open(src) if err != nil { return err } defer f.Close() gz, err := gzip.NewReader(f) if err != nil { return err } defer gz.Close() tr := tar.NewReader(gz) root := filepath.Clean(dst) + string(os.PathSeparator) for { header, err := tr.Next() if err == io.EOF { break } if err != nil { return err } name := filepath.Join(dst, header.Name) name = filepath.Clean(name) if !strings.HasPrefix(name+string(os.PathSeparator), root) { return fmt.Errorf("invalid path: %s", header.Name) } switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(name, 0755); err != nil { return err } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { return err } out, err := os.OpenFile( name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode), ) if err != nil { return err } _, err = io.Copy(out, tr) out.Close() if err != nil { return err } default: return fmt.Errorf("unsupported tar entry: %s", header.Name) } } return nil } func createUserAppService(app AppInfo, installDir string) error { execFile := filepath.Join(installDir, app.Executable) libPaths := "" if len(app.LibPaths) > 0 { paths := make([]string, 0, len(app.LibPaths)) for _, p := range app.LibPaths { paths = append(paths, filepath.Join(installDir, p)) } libPaths = strings.Join(paths, ":") } content := fmt.Sprintf(`[Unit] Description=%s After=yfkj-networkd.service Wants=yfkj-networkd.service [Service] Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin" Environment="LD_LIBRARY_PATH=%s" WorkingDirectory=%s ExecStart=%s Restart=always RestartSec=5 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target `, "Yunfei User Application", libPaths, installDir, execFile, ) return os.WriteFile(userAppService, []byte(content), 0644) } func systemctl(args ...string) error { out, err := exec.Command("systemctl", args...).CombinedOutput() if err != nil { return fmt.Errorf("systemctl %v failed: %v %s", args, err, string(out)) } return nil } func systemctlIgnoreError(args ...string) { _ = exec.Command("systemctl", args...).Run() }