diff --git a/cmd/server/api/server.go b/cmd/server/api/server.go index 0222d3a..1d7efe3 100644 --- a/cmd/server/api/server.go +++ b/cmd/server/api/server.go @@ -21,6 +21,7 @@ import ( "net/http" "os" "path" + "regexp" "strconv" "strings" "sync" @@ -59,6 +60,11 @@ func (s Server) Handler() []server.Route { server.NewRoute("/server/deleteProcess", http.MethodDelete, s.DeleteProcess).Permissions(config.DeleteServerProcess).LogFunc(middleware.AddOPLog), server.NewRoute("/server/execProcess", http.MethodPost, s.ExecProcess).Permissions(config.ShowServerProcessPage).LogFunc(middleware.AddOPLog), server.NewRoute("/server/execScript", http.MethodPost, s.ExecScript).Permissions(config.ShowServerScriptPage).LogFunc(middleware.AddOPLog), + server.NewRoute("/server/getNginxConfigList", http.MethodGet, s.GetNginxConfigList).Permissions(config.ShowServerNginxPage), + server.NewRoute("/server/manageNginx", http.MethodPost, s.ManageNginx).Permissions(config.ManageServerNginx).LogFunc(middleware.AddOPLog), + server.NewRoute("/server/getNginxConfigContent", http.MethodPost, s.GetNginxConfContent).Permissions(config.ShowServerNginxPage), + server.NewRoute("/server/editNginxConfig", http.MethodPut, s.EditNginxConfig).Permissions(config.EditNginxConfig).LogFunc(middleware.AddOPLog), + server.NewRoute("/server/copyNginxConfig", http.MethodPut, s.CopyNginxConfig).Permissions(config.CopyNginxConfig).LogFunc(middleware.AddOPLog), } } @@ -1242,3 +1248,300 @@ func (Server) ExecScript(gp *server.Goploy) server.Response { } return response.JSON{Data: respData} } + +func (Server) GetNginxConfigList(gp *server.Goploy) server.Response { + serverID, err := strconv.ParseInt(gp.URLQuery.Get("serverId"), 10, 64) + srv, err := (model.Server{ID: serverID}).GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + client, err := srv.ToSSHConfig().Dial() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer client.Close() + + sftpClient, err := sftp.NewClient(client) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer sftpClient.Close() + + session, err := client.NewSession() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer session.Close() + + nginxPath := gp.URLQuery.Get("dir") + + output, err := session.CombinedOutput(path.Join(nginxPath, "sbin", "nginx") + " -t") + + if err != nil { + return response.JSON{Code: response.Error, Message: fmt.Sprintf("output: %s", output)} + } + + configPath := "" + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "nginx: the configuration file ") && strings.Contains(line, "syntax is ok") { + configPath = strings.TrimPrefix(line, "nginx: the configuration file ") + configPath = strings.TrimSuffix(configPath, " syntax is ok") + } + } + + if configPath == "" { + return response.JSON{Code: response.Error, Message: "can not find nginx config path or config error"} + } + + configFile, err := sftpClient.Open(configPath) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer configFile.Close() + + configContent, err := io.ReadAll(configFile) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + configFileDir := path.Dir(configPath) + var includeConfigPaths []string + + // match the file path in the include directive + re := regexp.MustCompile("(?i)include\\s+([\"']?)(.*?)([\"']?);") + matches := re.FindAllSubmatch(configContent, -1) + + for _, match := range matches { + tmpPath := match[2] + includeConfigPaths = append(includeConfigPaths, path.Join(configFileDir, string(tmpPath))) + } + + type fileInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode string `json:"mode"` + ModTime string `json:"modTime"` + Dir string `json:"dir"` + } + + var fileList []fileInfo + for _, includeConfigPath := range includeConfigPaths { + fileInfos, err := sftpClient.Glob(includeConfigPath) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } else { + for _, f := range fileInfos { + fileStat, err := sftpClient.Stat(f) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + if fileStat.Mode()&os.ModeSymlink != 0 { + continue + } + + if !fileStat.IsDir() { + fileList = append(fileList, fileInfo{ + Name: fileStat.Name(), + Size: fileStat.Size(), + Mode: fileStat.Mode().String(), + ModTime: fileStat.ModTime().Format("2006-01-02 15:04:05"), + Dir: path.Dir(includeConfigPath), + }) + } + } + } + } + + return response.JSON{ + Data: struct { + List []fileInfo `json:"list"` + }{List: fileList}, + } +} + +func (Server) ManageNginx(gp *server.Goploy) server.Response { + type ReqData struct { + ServerID int64 `json:"serverId" validate:"gt=0"` + Path string `json:"path" validate:"required"` + Command string `json:"command" validate:"required"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + srv, err := (model.Server{ID: reqData.ServerID}).GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + nginxPath := path.Join(reqData.Path, "sbin", "nginx") + + script := "" + switch reqData.Command { + case "reload": + script = nginxPath + " -s reload" + case "stop": + script = nginxPath + " -s stop" + case "check": + script = nginxPath + " -t" + default: + script = reqData.Command + } + + client, err := srv.ToSSHConfig().Dial() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer session.Close() + + output, err := session.CombinedOutput(script) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + log.Trace(fmt.Sprintf("%s exec nginx cmd %s, result %t, output: %s", gp.UserInfo.Name, script, err == nil, string(output))) + return response.JSON{ + Data: struct { + ExecRes bool `json:"execRes"` + Output string `json:"output"` + }{ExecRes: err == nil, Output: string(output)}, + } +} + +func (Server) GetNginxConfContent(gp *server.Goploy) server.Response { + type ReqData struct { + ServerID int64 `json:"serverId" validate:"gt=0"` + Dir string `json:"dir" validate:"required"` + Filename string `json:"filename" validate:"required"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + srv, err := (model.Server{ID: reqData.ServerID}).GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + client, err := srv.ToSSHConfig().Dial() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer client.Close() + + sftpClient, err := sftp.NewClient(client) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer sftpClient.Close() + + configFile, err := sftpClient.Open(path.Join(reqData.Dir, reqData.Filename)) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer configFile.Close() + + configContent, err := io.ReadAll(configFile) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + return response.JSON{ + Data: struct { + Content string `json:"content"` + }{Content: string(configContent)}, + } +} + +func (Server) EditNginxConfig(gp *server.Goploy) server.Response { + type ReqData struct { + ServerID int64 `json:"serverId" validate:"gt=0"` + Dir string `json:"dir" validate:"required"` + Filename string `json:"filename" validate:"required"` + Content string `json:"content" validate:"required"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + srv, err := (model.Server{ID: reqData.ServerID}).GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + client, err := srv.ToSSHConfig().Dial() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer client.Close() + + sftpClient, err := sftp.NewClient(client) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer sftpClient.Close() + + file, err := sftpClient.Create(path.Join(reqData.Dir, reqData.Filename)) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer file.Close() + + _, err = file.Write([]byte(reqData.Content)) + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + return response.JSON{} +} + +func (Server) CopyNginxConfig(gp *server.Goploy) server.Response { + type ReqData struct { + ServerID int64 `json:"serverId" validate:"gt=0"` + Dir string `json:"dir" validate:"required"` + SrcName string `json:"srcName" validate:"required"` + DstName string `json:"dstName" validate:"required"` + } + var reqData ReqData + if err := decodeJson(gp.Body, &reqData); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + srv, err := (model.Server{ID: reqData.ServerID}).GetData() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + + client, err := srv.ToSSHConfig().Dial() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + defer session.Close() + + var sshOutbuf, sshErrbuf bytes.Buffer + session.Stdout = &sshOutbuf + session.Stderr = &sshErrbuf + + if err = session.Run(fmt.Sprintf("cp %s %s", path.Join(reqData.Dir, reqData.SrcName), path.Join(reqData.Dir, reqData.DstName))); err != nil { + return response.JSON{Code: response.Error, Message: "err: " + err.Error() + ", detail: " + sshErrbuf.String()} + } + return response.JSON{} +} diff --git a/config/permission.go b/config/permission.go index 1b26f4c..5de6e99 100644 --- a/config/permission.go +++ b/config/permission.go @@ -84,4 +84,8 @@ const ( ShowServerScriptPage = 77 SFTPRenameFile = 78 SFTPEditFile = 79 + ShowServerNginxPage = 80 + ManageServerNginx = 81 + EditNginxConfig = 82 + CopyNginxConfig = 83 ) diff --git a/database/goploy.sql b/database/goploy.sql index 2f0cbe6..2350682 100644 --- a/database/goploy.sql +++ b/database/goploy.sql @@ -475,6 +475,10 @@ INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALU INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (77, 23, 'ShowServerScriptPage', 0, ''); INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (78, 23, 'SFTPRenameFile', 0, ''); INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (79, 23, 'SFTPEditFile', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (80, 23, 'ShowServerNginxPage', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (81, 23, 'ManageServerNginx', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (82, 23, 'EditNginxConfig', 0, ''); +INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (83, 23, 'CopyNginxConfig', 0, ''); INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 13); INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 14); INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 15); diff --git a/web/src/api/server.ts b/web/src/api/server.ts index 6aab1e5..ed2c5e5 100644 --- a/web/src/api/server.ts +++ b/web/src/api/server.ts @@ -505,3 +505,98 @@ export class ServerRemoteCrontabList extends Request { this.param = param } } + +export class ManageNginx extends Request { + readonly url = '/server/manageNginx' + readonly method = 'post' + readonly timeout = 0 + public param: { + serverId: number + path: string + command: string + } + public declare datagram: { + execRes: boolean + output: string + } + constructor(param: ManageNginx['param']) { + super() + this.param = param + } +} + +export interface ServerNginxData { + [key: string]: any + modTime: string + mode: string + name: string + size: number + dir: string +} + +export class ServerNginxConfigList extends Request { + readonly url = '/server/getNginxConfigList' + readonly method = 'get' + + public declare datagram: { + list: ServerNginxData[] + } + + public param: { + serverId: number + dir: string + } + + constructor(param: ServerNginxConfigList['param']) { + super() + this.param = param + } +} + +export class NginxConfigContent extends Request { + readonly url = '/server/getNginxConfigContent' + readonly method = 'post' + public param: { + serverId: number + dir: string + filename: string + } + public declare datagram: { + content: string + } + constructor(param: NginxConfigContent['param']) { + super() + this.param = param + } +} + +export class NginxConfigEdit extends Request { + readonly url = '/server/editNginxConfig' + readonly method = 'put' + public param: { + serverId: number + dir: string + filename: string + content: string + } + constructor(param: NginxConfigEdit['param']) { + super() + this.param = param + } +} + +export class NginxConfigCopy extends Request { + readonly url = '/server/copyNginxConfig' + readonly method = 'put' + readonly timeout = 0 + public param: { + serverId: number + dir: string + srcName: string + dstName: string + } + constructor(param: NginxConfigCopy['param']) { + super() + this.param = param + } +} diff --git a/web/src/icons/svg/nginx.svg b/web/src/icons/svg/nginx.svg new file mode 100644 index 0000000..815678c --- /dev/null +++ b/web/src/icons/svg/nginx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/lang/en.json b/web/src/lang/en.json index 0cd96b0..5ca2f73 100644 --- a/web/src/lang/en.json +++ b/web/src/lang/en.json @@ -166,6 +166,7 @@ "serverProcess": "Process", "serverCrontab": "Crontab UI", "serverCron": "Cron", + "serverNginx": "Nginx", "namespace": "Namespace", "namespaceSetting": "NS setting", "roleSetting": "Role setting", @@ -253,7 +254,11 @@ "DeleteServerProcess": "Delete the server process", "SFTPTransferFile": "Can transfer file via SFTP", "SFTPDeleteFile": "Can delete file via SFTP", - "ShowServerScriptPage": "Can execute script" + "ShowServerScriptPage": "Can execute script", + "ShowServerNginxPage": "See the server nginx page", + "ManageServerNginx": "Manage nginx process", + "EditNginxConfig": "Edit the nginx config", + "CopyNginxConfig": "Copy the nginx config" }, "tagsView": { "refresh": "Refresh", @@ -306,7 +311,9 @@ "advance": "Advanced setting", "transferFile": "Transfer file", "sftpFileCount": "items", - "saveTemplate": "Save template" + "saveTemplate": "Save template", + "execTips": "Exec command {command}?", + "nginxStartTips": "Input your nginx start command" }, "monitorPage": { "defaultServer": "Follow Host", diff --git a/web/src/lang/zh-cn.json b/web/src/lang/zh-cn.json index 21a6d4b..bb69895 100644 --- a/web/src/lang/zh-cn.json +++ b/web/src/lang/zh-cn.json @@ -148,6 +148,7 @@ "serverProcess": "进程管理", "serverCrontab": "Crontab UI", "serverCron": "定时任务", + "serverNginx": "Nginx管理", "namespace": "空间", "namespaceSetting": "空间设置", "roleSetting": "角色设置", @@ -236,7 +237,11 @@ "ShowOperationLogPage": "查看操作日志", "SFTPTransferFile": "SFTP传输文件", "SFTPDeleteFile": "SFTP删除文件", - "ShowServerScriptPage": "执行脚本" + "ShowServerScriptPage": "执行脚本", + "ShowServerNginxPage": "查看Nginx管理", + "ManageServerNginx": "Nginx进程管理", + "EditNginxConfig": "编辑Nginx配置文件", + "CopyNginxConfig": "复制Nginx配置文件" }, "tagsView": { "refresh": "刷新", @@ -289,7 +294,9 @@ "advance": "高级选项", "transferFile": "传输文件", "sftpFileCount": "个项目", - "saveTemplate": "保存模板" + "saveTemplate": "保存模板", + "execTips": "执行{command}命令?", + "nginxStartTips": "请输入nginx的启动命令" }, "monitorPage": { "scriptMode": "脚本类型", diff --git a/web/src/permission.ts b/web/src/permission.ts index ca6247a..4135965 100644 --- a/web/src/permission.ts +++ b/web/src/permission.ts @@ -78,4 +78,8 @@ export default Object.freeze({ ShowServerScriptPage: 77, SFTPRenameFile: 78, SFTPEditFile: 79, + ShowServerNginxPage: 80, + ManageServerNginx: 81, + EditNginxConfig: 82, + CopyNginxConfig: 83, }) diff --git a/web/src/router/asyncRoutes.ts b/web/src/router/asyncRoutes.ts index 88ee49d..43e8dec 100644 --- a/web/src/router/asyncRoutes.ts +++ b/web/src/router/asyncRoutes.ts @@ -156,6 +156,16 @@ export default [ permissions: [permission.ShowServerMonitorPage], }, }, + { + path: 'nginx', + name: 'ServerNginx', + component: () => import('@/views/server/nginx/index.vue'), + meta: { + title: 'serverNginx', + icon: 'nginx', + permissions: [permission.ShowServerNginxPage], + }, + }, ], }, { diff --git a/web/src/views/server/nginx/explorer.vue b/web/src/views/server/nginx/explorer.vue new file mode 100644 index 0000000..318bfc0 --- /dev/null +++ b/web/src/views/server/nginx/explorer.vue @@ -0,0 +1,479 @@ + + + + + + + + + + + + + check + + + reload + + + start + + + stop + + + + + + + + {{ $t('sort') }} + + + + + {{ $t('default') }} + + + {{ $t('name') }} {{ $t('asc') }} + + + {{ $t('name') }} {{ $t('desc') }} + + + {{ $t('size') }} {{ $t('asc') }} + + + {{ $t('size') }} {{ $t('desc') }} + + + {{ $t('modifiedTime') }} {{ $t('asc') }} + + + {{ $t('modifiedTime') }} {{ $t('desc') }} + + + + + + + + + + + + + + + + + {{ $t('edit') }} + + + {{ $t('copy') }} + + + + + + + + + {{ fileFilteredList.length }} {{ $t('serverPage.sftpFileCount') }} + + + + + + + {{ $t('cancel') }} + + + {{ $t('confirm') }} + + + + + + + + + + diff --git a/web/src/views/server/nginx/index.vue b/web/src/views/server/nginx/index.vue new file mode 100644 index 0000000..1aae779 --- /dev/null +++ b/web/src/views/server/nginx/index.vue @@ -0,0 +1,191 @@ + + + + + + + + + {{ index + 1 }} + + + {{ item.server.name }} : {{ item.dir }} + + + x + + + {{ item }} + + + + + + + + + + + + + + + + + + +