rpc_handler.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. package main
  2. import (
  3. "archive/tar"
  4. "compress/gzip"
  5. "context"
  6. "crypto/md5"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "io"
  11. "os"
  12. "os/exec"
  13. "path"
  14. "path/filepath"
  15. "strings"
  16. "sync"
  17. "time"
  18. "hnyfkj.com.cn/rtu/linux/baseapp"
  19. "hnyfkj.com.cn/rtu/linux/utils/jsonrpc2"
  20. )
  21. type PkgInfo struct {
  22. FileName string `json:"fileName"`
  23. FileSize int64 `json:"fileSize"`
  24. CheckMD5Val string `json:"fileMD5"`
  25. UploadTime string `json:"uploadTime"`
  26. }
  27. type AppInfo struct {
  28. Name string `json:"name"`
  29. Version string `json:"version"`
  30. Executable string `json:"executable"`
  31. LibPaths []string `json:"libraryPaths"`
  32. Description string `json:"description"`
  33. ReadmeFile string `json:"readmeFile"`
  34. }
  35. type InsInfo struct {
  36. Installed bool `json:"installed"`
  37. InstallTime string `json:"installTime"`
  38. }
  39. type AppIns struct {
  40. Pkg PkgInfo `json:"pkg"` // 应用的安装包
  41. App AppInfo `json:"app"` // 应用详细信息
  42. Ins InsInfo `json:"ins"` // 应用是否安装
  43. }
  44. var (
  45. appMutex sync.Mutex
  46. )
  47. const (
  48. userAppInsDir = "/home/root/yfkj-user-app"
  49. userAppService = "/etc/systemd/system/yfkj-user-app.service"
  50. ErrConflict jsonrpc2.ErrCode = -32099 ///////// 资源访问冲突
  51. )
  52. // 导入用户安装包
  53. func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
  54. appMutex.Lock()
  55. defer appMutex.Unlock()
  56. var params map[string]string
  57. if err := json.Unmarshal(req.Params, &params); err != nil {
  58. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
  59. }
  60. file, ok := params["file"]
  61. if !ok || file == "" {
  62. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "missing or empty 'file' parameter")
  63. }
  64. defer os.Remove(file)
  65. if !strings.HasSuffix(strings.ToLower(filepath.Base(file)), ".tar.gz") {
  66. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package format")
  67. }
  68. info, err := os.Stat(file)
  69. if err != nil {
  70. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
  71. }
  72. if !info.Mode().IsRegular() {
  73. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package file")
  74. }
  75. if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
  76. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  77. }
  78. if hasImportedPackage(userAppPkgDir) {
  79. return jsonrpc2.BuildError(req, ErrConflict, "application package already exists")
  80. }
  81. appInfo, err := readAppJSON(file)
  82. if err != nil {
  83. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
  84. }
  85. md5Val, err := fileMD5(file)
  86. if err != nil {
  87. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  88. }
  89. savePkgFile := filepath.Join(userAppPkgDir, filepath.Base(file))
  90. if err := moveFile(file, savePkgFile); err != nil {
  91. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  92. }
  93. appIns := AppIns{
  94. Pkg: PkgInfo{
  95. FileName: filepath.Base(file),
  96. FileSize: info.Size(),
  97. CheckMD5Val: md5Val,
  98. UploadTime: time.Now().Format("2006-01-02 15:04:05"),
  99. },
  100. App: appInfo,
  101. Ins: InsInfo{
  102. Installed: false,
  103. },
  104. }
  105. data, err := json.MarshalIndent(appIns, "", " ")
  106. if err != nil {
  107. os.Remove(savePkgFile)
  108. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  109. }
  110. appInsFile := filepath.Join(userAppPkgDir, "appins.json")
  111. if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
  112. os.Remove(savePkgFile)
  113. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  114. }
  115. baseapp.Logger.Infof("导入用户应用安装包成功: %s, 版本: %s, 描述: %s",
  116. appIns.App.Name, appIns.App.Version, appIns.App.Description)
  117. return jsonrpc2.BuildResponse(req, "success", nil)
  118. }
  119. // 删除用户安装包
  120. func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
  121. appMutex.Lock()
  122. defer appMutex.Unlock()
  123. appInsFile := filepath.Join(userAppPkgDir, "appins.json")
  124. var appIns AppIns
  125. data, err := os.ReadFile(appInsFile)
  126. if err == nil {
  127. if err := json.Unmarshal(data, &appIns); err == nil {
  128. if appIns.Ins.Installed { // 用户应用已安装
  129. return jsonrpc2.BuildError(req, ErrConflict, "application is installed, please uninstall it first")
  130. }
  131. }
  132. } else if !os.IsNotExist(err) {
  133. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  134. }
  135. if err := os.RemoveAll(userAppPkgDir); err != nil {
  136. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  137. }
  138. if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
  139. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  140. }
  141. baseapp.Logger.Infof("删除用户应用安装包成功: %s, 版本: %s, 描述: %s",
  142. appIns.App.Name, appIns.App.Version, appIns.App.Description)
  143. return jsonrpc2.BuildResponse(req, "success", nil)
  144. }
  145. // 安装包应用安装
  146. func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
  147. appMutex.Lock()
  148. defer appMutex.Unlock()
  149. appInsFile := filepath.Join(userAppPkgDir, "appins.json")
  150. data, err := os.ReadFile(appInsFile)
  151. if err != nil {
  152. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "application package not found")
  153. }
  154. var appIns AppIns
  155. if err := json.Unmarshal(data, &appIns); err != nil {
  156. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  157. }
  158. if appIns.Ins.Installed {
  159. return jsonrpc2.BuildError(req, ErrConflict, "application already installed")
  160. }
  161. success := false
  162. defer func() {
  163. if !success {
  164. baseapp.Logger.Warnf("安装用户应用失败状态回滚: %s", userAppInsDir)
  165. systemctlIgnoreError("stop", "yfkj-user-app.service")
  166. systemctlIgnoreError("disable", "yfkj-user-app.service")
  167. os.RemoveAll(userAppInsDir)
  168. os.Remove(userAppService)
  169. systemctlIgnoreError("daemon-reload")
  170. }
  171. }()
  172. systemctlIgnoreError("stop", "yfkj-user-app.service")
  173. if err := os.MkdirAll(userAppInsDir, 0755); err != nil {
  174. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  175. }
  176. appPkgFile := filepath.Join(userAppPkgDir, appIns.Pkg.FileName)
  177. if err := extractTarGz(appPkgFile, userAppInsDir); err != nil {
  178. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  179. }
  180. execFile := filepath.Join(userAppInsDir, appIns.App.Executable)
  181. info, err := os.Stat(execFile)
  182. if err != nil {
  183. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "executable not found")
  184. }
  185. if !info.Mode().IsRegular() {
  186. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "invalid executable")
  187. }
  188. if err := os.Chmod(execFile, 0755); err != nil {
  189. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  190. }
  191. if err := createUserAppService(appIns.App, userAppInsDir); err != nil {
  192. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  193. }
  194. if err := systemctl("daemon-reload"); err != nil {
  195. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  196. }
  197. if err := systemctl("enable", "yfkj-user-app.service"); err != nil {
  198. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  199. }
  200. if err := systemctl("start", "yfkj-user-app.service"); err != nil {
  201. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  202. }
  203. appIns.Ins.Installed = true
  204. appIns.Ins.InstallTime = time.Now().Format("2006-01-02 15:04:05")
  205. data, err = json.MarshalIndent(appIns, "", " ")
  206. if err != nil {
  207. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  208. }
  209. if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
  210. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  211. }
  212. success = true
  213. baseapp.Logger.Infof("安装用户应用成功: %s, 版本: %s, 描述: %s",
  214. appIns.App.Name, appIns.App.Version, appIns.App.Description)
  215. return jsonrpc2.BuildResponse(req, "success", nil)
  216. }
  217. // 卸载已安装应用
  218. func appUninstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
  219. appMutex.Lock()
  220. defer appMutex.Unlock()
  221. appInsFile := filepath.Join(userAppPkgDir, "appins.json")
  222. data, err := os.ReadFile(appInsFile)
  223. if err != nil {
  224. return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "application package not found")
  225. }
  226. var appIns AppIns
  227. if err := json.Unmarshal(data, &appIns); err != nil {
  228. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  229. }
  230. if !appIns.Ins.Installed {
  231. return jsonrpc2.BuildError(req, ErrConflict, "application not installed")
  232. }
  233. systemctlIgnoreError("stop", "yfkj-user-app.service")
  234. systemctlIgnoreError("disable", "yfkj-user-app.service")
  235. if err := os.Remove(userAppService); err != nil && !os.IsNotExist(err) {
  236. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  237. }
  238. if err := systemctl("daemon-reload"); err != nil {
  239. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  240. }
  241. if err := os.RemoveAll(userAppInsDir); err != nil {
  242. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  243. }
  244. appIns.Ins.Installed = false
  245. appIns.Ins.InstallTime = ""
  246. data, err = json.MarshalIndent(appIns, "", " ")
  247. if err != nil {
  248. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  249. }
  250. if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
  251. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  252. }
  253. baseapp.Logger.Infof("卸载用户应用成功: %s, 版本: %s, 描述: %s",
  254. appIns.App.Name, appIns.App.Version, appIns.App.Description)
  255. return jsonrpc2.BuildResponse(req, "success", nil)
  256. }
  257. // 安装包应用信息
  258. func getInstallInfo(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
  259. appInsFile := filepath.Join(userAppPkgDir, "appins.json")
  260. data, err := os.ReadFile(appInsFile)
  261. if err != nil {
  262. if os.IsNotExist(err) {
  263. return jsonrpc2.BuildResponse(req, nil, nil)
  264. }
  265. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  266. }
  267. var appIns AppIns
  268. if err := json.Unmarshal(data, &appIns); err != nil {
  269. return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
  270. }
  271. if appIns.App.ReadmeFile != "" {
  272. readme, err := os.ReadFile(filepath.Join(userAppInsDir, appIns.App.ReadmeFile))
  273. if err != nil {
  274. appIns.App.ReadmeFile = ""
  275. } else {
  276. appIns.App.ReadmeFile = string(readme)
  277. }
  278. }
  279. return jsonrpc2.BuildResponse(req, appIns, nil)
  280. }
  281. func readAppJSON(file string) (AppInfo, error) {
  282. var app AppInfo
  283. f, err := os.Open(file)
  284. if err != nil {
  285. return app, err
  286. }
  287. defer f.Close()
  288. gz, err := gzip.NewReader(f)
  289. if err != nil {
  290. return app, err
  291. }
  292. defer gz.Close()
  293. tr := tar.NewReader(gz)
  294. for {
  295. header, err := tr.Next()
  296. if err == io.EOF {
  297. break
  298. }
  299. if err != nil {
  300. return app, err
  301. }
  302. if header.Typeflag != tar.TypeReg {
  303. continue
  304. }
  305. if path.Base(header.Name) != "app.json" {
  306. continue
  307. }
  308. if header.Size > 2<<20 { // 2MB 上限
  309. return app, fmt.Errorf("app.json 文件过大")
  310. }
  311. data, err := io.ReadAll(io.LimitReader(tr, header.Size))
  312. if err != nil {
  313. return app, err
  314. }
  315. err = json.Unmarshal(data, &app)
  316. if err != nil {
  317. return app, err
  318. }
  319. if app.Name == "" || app.Version == "" || app.Executable == "" {
  320. return app, fmt.Errorf("app.json参数不完整")
  321. }
  322. if !validRelativePath(app.Executable) {
  323. return app, fmt.Errorf("invalid executable path")
  324. }
  325. for _, p := range app.LibPaths {
  326. if validRelativePath(p) {
  327. continue
  328. }
  329. return app, fmt.Errorf("invalid library path: %s", p)
  330. }
  331. return app, nil
  332. }
  333. return app, fmt.Errorf("安装包中未找到 app.json")
  334. }
  335. func fileMD5(file string) (string, error) {
  336. f, err := os.Open(file)
  337. if err != nil {
  338. return "", err
  339. }
  340. defer f.Close()
  341. h := md5.New()
  342. if _, err := io.Copy(h, f); err != nil {
  343. return "", err
  344. }
  345. return hex.EncodeToString(h.Sum(nil)), nil
  346. }
  347. func moveFile(src, dst string) error {
  348. if err := os.Rename(src, dst); err == nil {
  349. return nil
  350. }
  351. in, err := os.Open(src)
  352. if err != nil {
  353. return err
  354. }
  355. defer in.Close()
  356. out, err := os.Create(dst)
  357. if err != nil {
  358. return err
  359. }
  360. _, err = io.Copy(out, in)
  361. if err != nil {
  362. out.Close()
  363. os.Remove(dst)
  364. return err
  365. }
  366. if err := out.Close(); err != nil {
  367. os.Remove(dst)
  368. return err
  369. }
  370. return os.Remove(src)
  371. }
  372. func validRelativePath(p string) bool {
  373. if p == "" {
  374. return false
  375. }
  376. if filepath.IsAbs(p) {
  377. return false
  378. }
  379. clean := filepath.Clean(p)
  380. if clean != p {
  381. return false
  382. }
  383. if clean == ".." ||
  384. strings.HasPrefix(clean, ".."+string(os.PathSeparator)) {
  385. return false
  386. }
  387. return true
  388. }
  389. func isRegularFile(name string) bool {
  390. info, err := os.Stat(name)
  391. if err != nil {
  392. return false
  393. }
  394. return info.Mode().IsRegular()
  395. }
  396. func hasImportedPackage(dir string) bool {
  397. if isRegularFile(filepath.Join(dir, "appins.json")) {
  398. return true
  399. }
  400. entries, err := os.ReadDir(dir)
  401. if err != nil {
  402. return false
  403. }
  404. for _, entry := range entries {
  405. if entry.IsDir() {
  406. continue
  407. }
  408. if strings.HasSuffix(strings.ToLower(entry.Name()), ".tar.gz") {
  409. return true
  410. }
  411. }
  412. return false
  413. }
  414. func writeFileAtomic(name string, data []byte, perm os.FileMode) error {
  415. tmp := name + ".tmp"
  416. defer os.Remove(tmp)
  417. f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
  418. if err != nil {
  419. return err
  420. }
  421. if _, err := f.Write(data); err != nil {
  422. f.Close()
  423. return err
  424. }
  425. if err := f.Sync(); err != nil {
  426. f.Close()
  427. return err
  428. }
  429. if err := f.Close(); err != nil {
  430. return err
  431. }
  432. return os.Rename(tmp, name)
  433. }
  434. func extractTarGz(src string, dst string) error {
  435. f, err := os.Open(src)
  436. if err != nil {
  437. return err
  438. }
  439. defer f.Close()
  440. gz, err := gzip.NewReader(f)
  441. if err != nil {
  442. return err
  443. }
  444. defer gz.Close()
  445. tr := tar.NewReader(gz)
  446. root := filepath.Clean(dst) + string(os.PathSeparator)
  447. for {
  448. header, err := tr.Next()
  449. if err == io.EOF {
  450. break
  451. }
  452. if err != nil {
  453. return err
  454. }
  455. name := filepath.Join(dst, header.Name)
  456. name = filepath.Clean(name)
  457. if !strings.HasPrefix(name+string(os.PathSeparator), root) {
  458. return fmt.Errorf("invalid path: %s", header.Name)
  459. }
  460. switch header.Typeflag {
  461. case tar.TypeDir:
  462. if err := os.MkdirAll(name, 0755); err != nil {
  463. return err
  464. }
  465. case tar.TypeReg:
  466. if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
  467. return err
  468. }
  469. out, err := os.OpenFile(
  470. name,
  471. os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
  472. os.FileMode(header.Mode),
  473. )
  474. if err != nil {
  475. return err
  476. }
  477. _, err = io.Copy(out, tr)
  478. out.Close()
  479. if err != nil {
  480. return err
  481. }
  482. default:
  483. return fmt.Errorf("unsupported tar entry: %s", header.Name)
  484. }
  485. }
  486. return nil
  487. }
  488. func createUserAppService(app AppInfo, installDir string) error {
  489. execFile := filepath.Join(installDir, app.Executable)
  490. libPaths := ""
  491. if len(app.LibPaths) > 0 {
  492. paths := make([]string, 0, len(app.LibPaths))
  493. for _, p := range app.LibPaths {
  494. paths = append(paths, filepath.Join(installDir, p))
  495. }
  496. libPaths = strings.Join(paths, ":")
  497. }
  498. content := fmt.Sprintf(`[Unit]
  499. Description=%s
  500. After=yfkj-networkd.service
  501. Wants=yfkj-networkd.service
  502. [Service]
  503. Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
  504. Environment="LD_LIBRARY_PATH=%s"
  505. WorkingDirectory=%s
  506. ExecStart=%s
  507. Restart=always
  508. RestartSec=5
  509. StandardOutput=journal
  510. StandardError=journal
  511. [Install]
  512. WantedBy=multi-user.target
  513. `,
  514. "Yunfei User Application",
  515. libPaths,
  516. installDir,
  517. execFile,
  518. )
  519. return os.WriteFile(userAppService, []byte(content), 0644)
  520. }
  521. func systemctl(args ...string) error {
  522. out, err := exec.Command("systemctl", args...).CombinedOutput()
  523. if err != nil {
  524. return fmt.Errorf("systemctl %v failed: %v %s", args, err, string(out))
  525. }
  526. return nil
  527. }
  528. func systemctlIgnoreError(args ...string) {
  529. _ = exec.Command("systemctl", args...).Run()
  530. }