diff --git a/backend/app/api/v1/helper/helper.go b/backend/app/api/v1/helper/helper.go index 7a3077aeb..630b6ca73 100644 --- a/backend/app/api/v1/helper/helper.go +++ b/backend/app/api/v1/helper/helper.go @@ -67,7 +67,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) { } else { res.Message = i18n.GetMsgWithMap(msgKey, map[string]interface{}{"detail": err}) } - global.LOG.Error(res.Message) + global.LOG.Errorf("request: %s, error: %s", ctx.Request.URL.Path, res.Message) ctx.JSON(http.StatusOK, res) ctx.Abort() } diff --git a/backend/app/api/v1/host.go b/backend/app/api/v1/host.go index d3b29df65..622716997 100644 --- a/backend/app/api/v1/host.go +++ b/backend/app/api/v1/host.go @@ -127,6 +127,7 @@ func (b *BaseApi) UpdateHost(c *gin.Context) { upMap["auth_mode"] = req.AuthMode upMap["password"] = req.Password upMap["private_key"] = req.PrivateKey + upMap["description"] = req.Description if err := hostService.Update(req.ID, upMap); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index daaecec6b..4a941594d 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -2,15 +2,13 @@ package v1 import ( "errors" - "os/exec" - "runtime" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/mfa" - "github.com/beevik/ntp" + "github.com/1Panel-dev/1Panel/backend/utils/ntp" "github.com/gin-gonic/gin" ) @@ -87,19 +85,16 @@ func (b *BaseApi) HandlePasswordExpired(c *gin.Context) { } func (b *BaseApi) SyncTime(c *gin.Context) { - ntime, err := ntp.Time("pool.ntp.org") + ntime, err := ntp.Getremotetime() if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } - system := runtime.GOOS - if system == "linux" { - cmd := exec.Command("date", "-s", ntime.Format("2006-01-02 15:04:05")) - stdout, err := cmd.CombinedOutput() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, errors.New(string(stdout))) - return - } + + ts := ntime.Format("2006-01-02 15:04:05") + if err := ntp.UpdateSystemDate(ts); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return } helper.SuccessWithData(c, ntime.Format("2006-01-02 15:04:05 MST -0700")) diff --git a/backend/app/api/v1/terminal.go b/backend/app/api/v1/terminal.go index b4a045a15..1f69d5c55 100644 --- a/backend/app/api/v1/terminal.go +++ b/backend/app/api/v1/terminal.go @@ -1,6 +1,7 @@ package v1 import ( + "fmt" "net/http" "strconv" "time" @@ -78,6 +79,57 @@ func (b *BaseApi) WsSsh(c *gin.Context) { } } +func (b *BaseApi) RedisWsSsh(c *gin.Context) { + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + redisConf, err := redisService.LoadConf() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + auth := "" + if len(redisConf.Requirepass) != 0 { + auth = fmt.Sprintf("-a %s --no-auth-warning", redisConf.Requirepass) + } + slave, err := terminal.NewCommand(redisConf.ContainerName, auth) + if wshandleError(wsConn, err) { + return + } + defer slave.Close() + + tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave) + if wshandleError(wsConn, err) { + return + } + + quitChan := make(chan bool, 3) + tty.Start(quitChan) + go slave.Wait(quitChan) + + <-quitChan + + global.LOG.Info("websocket finished") + if wshandleError(wsConn, err) { + return + } +} + func wshandleError(ws *websocket.Conn, err error) bool { if err != nil { global.LOG.Errorf("handler ws faled:, err: %v", err) diff --git a/backend/app/service/host.go b/backend/app/service/host.go index 78249eb99..a2d5ca9c2 100644 --- a/backend/app/service/host.go +++ b/backend/app/service/host.go @@ -84,6 +84,7 @@ func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) { upMap["auth_mode"] = req.AuthMode upMap["password"] = req.Password upMap["private_key"] = req.PrivateKey + upMap["description"] = req.Description if err := hostRepo.Update(sameHostID, upMap); err != nil { return nil, err } diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go index 4ef4ccb04..3f4a3099c 100644 --- a/backend/router/ro_database.go +++ b/backend/router/ro_database.go @@ -37,7 +37,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) { cmdRouter.GET("/redis/persistence/conf", baseApi.LoadPersistenceConf) cmdRouter.GET("/redis/status", baseApi.LoadRedisStatus) cmdRouter.GET("/redis/conf", baseApi.LoadRedisConf) - cmdRouter.GET("/redis/exec", baseApi.RedisExec) + cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh) cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword) cmdRouter.POST("/redis/backup", baseApi.RedisBackup) cmdRouter.POST("/redis/recover", baseApi.RedisRecover) diff --git a/backend/utils/ntp/ntp.go b/backend/utils/ntp/ntp.go new file mode 100644 index 000000000..725903034 --- /dev/null +++ b/backend/utils/ntp/ntp.go @@ -0,0 +1,71 @@ +package ntp + +import ( + "encoding/binary" + "fmt" + "net" + "runtime" + "time" + + "github.com/gogf/gf/os/gproc" +) + +const ntpEpochOffset = 2208988800 + +type packet struct { + Settings uint8 + Stratum uint8 + Poll int8 + Precision int8 + RootDelay uint32 + RootDispersion uint32 + ReferenceID uint32 + RefTimeSec uint32 + RefTimeFrac uint32 + OrigTimeSec uint32 + OrigTimeFrac uint32 + RxTimeSec uint32 + RxTimeFrac uint32 + TxTimeSec uint32 + TxTimeFrac uint32 +} + +func Getremotetime() (time.Time, error) { + conn, err := net.Dial("udp", "pool.ntp.org:123") + if err != nil { + return time.Time{}, fmt.Errorf("failed to connect: %v", err) + } + defer conn.Close() + if err := conn.SetDeadline(time.Now().Add(15 * time.Second)); err != nil { + return time.Time{}, fmt.Errorf("failed to set deadline: %v", err) + } + + req := &packet{Settings: 0x1B} + + if err := binary.Write(conn, binary.BigEndian, req); err != nil { + return time.Time{}, fmt.Errorf("failed to set request: %v", err) + } + + rsp := &packet{} + if err := binary.Read(conn, binary.BigEndian, rsp); err != nil { + return time.Time{}, fmt.Errorf("failed to read server response: %v", err) + } + + secs := float64(rsp.TxTimeSec) - ntpEpochOffset + nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32 + + showtime := time.Unix(int64(secs), nanos) + + return showtime, nil +} + +func UpdateSystemDate(dateTime string) error { + system := runtime.GOOS + if system == "linux" { + if _, err := gproc.ShellExec(`date -s "` + dateTime + `"`); err != nil { + return fmt.Errorf("update system date failed, err: %v", err) + } + return nil + } + return fmt.Errorf("The current system architecture does not support synchronization") +} diff --git a/backend/utils/terminal/exec.go b/backend/utils/terminal/exec.go index 810d7d84b..83812dc66 100644 --- a/backend/utils/terminal/exec.go +++ b/backend/utils/terminal/exec.go @@ -99,6 +99,6 @@ func (sws *ExecWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdByte _, _ = sws.conn.Write(cmdBytes) } -func (sws *ExecWsSession) ResizeTerminal(width int, height int) { - _, _ = sws.conn.Write([]byte(fmt.Sprintf("stty cols %d rows %d && clear \r", width, height))) +func (sws *ExecWsSession) ResizeTerminal(rows int, cols int) { + _, _ = sws.conn.Write([]byte(fmt.Sprintf("stty cols %d rows %d && clear \r", cols, rows))) } diff --git a/backend/utils/terminal/local_cmd.go b/backend/utils/terminal/local_cmd.go new file mode 100644 index 000000000..2e041ebb6 --- /dev/null +++ b/backend/utils/terminal/local_cmd.go @@ -0,0 +1,111 @@ +package terminal + +import ( + "fmt" + "os" + "os/exec" + "syscall" + "time" + "unsafe" + + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/creack/pty" + "github.com/pkg/errors" +) + +const ( + DefaultCloseSignal = syscall.SIGINT + DefaultCloseTimeout = 10 * time.Second +) + +type LocalCommand struct { + closeSignal syscall.Signal + closeTimeout time.Duration + + cmd *exec.Cmd + pty *os.File + ptyClosed chan struct{} +} + +func NewCommand(containerName string, auth string) (*LocalCommand, error) { + cmd := exec.Command("sh", "-c", fmt.Sprintf("docker exec -it %s redis-cli %s", containerName, auth)) + + pty, err := pty.Start(cmd) + if err != nil { + return nil, errors.Wrapf(err, "failed to start command") + } + ptyClosed := make(chan struct{}) + + lcmd := &LocalCommand{ + closeSignal: DefaultCloseSignal, + closeTimeout: DefaultCloseTimeout, + + cmd: cmd, + pty: pty, + ptyClosed: ptyClosed, + } + + return lcmd, nil +} + +func (lcmd *LocalCommand) Read(p []byte) (n int, err error) { + return lcmd.pty.Read(p) +} + +func (lcmd *LocalCommand) Write(p []byte) (n int, err error) { + return lcmd.pty.Write(p) +} + +func (lcmd *LocalCommand) Close() error { + if lcmd.cmd != nil && lcmd.cmd.Process != nil { + _ = lcmd.cmd.Process.Signal(lcmd.closeSignal) + } + for { + select { + case <-lcmd.ptyClosed: + return nil + case <-lcmd.closeTimeoutC(): + _ = lcmd.cmd.Process.Signal(syscall.SIGKILL) + } + } +} + +func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error { + window := struct { + row uint16 + col uint16 + x uint16 + y uint16 + }{ + uint16(height), + uint16(width), + 0, + 0, + } + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + lcmd.pty.Fd(), + syscall.TIOCSWINSZ, + uintptr(unsafe.Pointer(&window)), + ) + if errno != 0 { + return errno + } else { + return nil + } +} + +func (lcmd *LocalCommand) Wait(quitChan chan bool) { + if err := lcmd.cmd.Wait(); err != nil { + global.LOG.Errorf("ssh session wait failed, err: %v", err) + setQuit(quitChan) + } +} + +func (lcmd *LocalCommand) closeTimeoutC() <-chan time.Time { + if lcmd.closeTimeout >= 0 { + return time.After(lcmd.closeTimeout) + } + + return make(chan time.Time) +} diff --git a/backend/utils/terminal/ws_local_session.go b/backend/utils/terminal/ws_local_session.go new file mode 100644 index 000000000..136658ebc --- /dev/null +++ b/backend/utils/terminal/ws_local_session.go @@ -0,0 +1,99 @@ +package terminal + +import ( + "encoding/base64" + "encoding/json" + "sync" + + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +type LocalWsSession struct { + slave *LocalCommand + wsConn *websocket.Conn + + writeMutex sync.Mutex +} + +func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand) (*LocalWsSession, error) { + if err := slave.ResizeTerminal(cols, rows); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + + return &LocalWsSession{ + slave: slave, + wsConn: wsConn, + }, nil +} + +func (sws *LocalWsSession) Start(quitChan chan bool) { + go sws.handleSlaveEvent(quitChan) + go sws.receiveWsMsg(quitChan) +} + +func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) { + defer setQuit(exitCh) + + buffer := make([]byte, 1024) + for { + select { + case <-exitCh: + return + default: + n, _ := sws.slave.Read(buffer) + _ = sws.masterWrite(buffer[:n]) + } + } +} + +func (sws *LocalWsSession) masterWrite(data []byte) error { + sws.writeMutex.Lock() + defer sws.writeMutex.Unlock() + err := sws.wsConn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return errors.Wrapf(err, "failed to write to master") + } + + return nil +} + +func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) { + wsConn := sws.wsConn + defer setQuit(exitCh) + for { + select { + case <-exitCh: + return + default: + _, wsData, err := wsConn.ReadMessage() + if err != nil { + global.LOG.Errorf("reading webSocket message failed, err: %v", err) + return + } + msgObj := wsMsg{} + _ = json.Unmarshal(wsData, &msgObj) + switch msgObj.Type { + case wsMsgResize: + if msgObj.Cols > 0 && msgObj.Rows > 0 { + if err := sws.slave.ResizeTerminal(msgObj.Cols, msgObj.Rows); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + } + case wsMsgCmd: + decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd) + if err != nil { + global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) + } + sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) + } + } + } +} + +func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { + if _, err := sws.slave.Write(cmdBytes); err != nil { + global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) + } +} diff --git a/backend/utils/terminal/ws_session.go b/backend/utils/terminal/ws_session.go index 338a6c877..74200f493 100644 --- a/backend/utils/terminal/ws_session.go +++ b/backend/utils/terminal/ws_session.go @@ -165,6 +165,19 @@ func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) { return } bs := sws.comboOutput.Bytes() + if string(bs) == string([]byte{13, 10, 108, 111, 103, 111, 117, 116, 13, 10}) { + err := wsConn.WriteMessage(websocket.TextMessage, bs) + if err != nil { + global.LOG.Errorf("ssh sending combo output to webSocket failed, err: %v", err) + } + _, err = sws.logBuff.Write(bs) + if err != nil { + global.LOG.Errorf("combo output to log buffer failed, err: %v", err) + } + sws.comboOutput.buffer.Reset() + sws.Close() + return + } if len(bs) > 0 { err := wsConn.WriteMessage(websocket.TextMessage, bs) if err != nil { diff --git a/frontend/src/views/host/terminal/index.vue b/frontend/src/views/host/terminal/index.vue index dad18be61..52c3f37f6 100644 --- a/frontend/src/views/host/terminal/index.vue +++ b/frontend/src/views/host/terminal/index.vue @@ -37,7 +37,7 @@ { continue; } for (const host of item.children) { - if (host.label.indexOf('127.0.0.1')) { + if (host.label.indexOf('127.0.0.1') !== -1) { localHostID.value = host.id; if (terminalTabs.value.length === 0) { onConnLocal(); @@ -374,6 +374,7 @@ const onReconnect = async (item: any) => { } item.Refresh = !item.Refresh; ctx.refs[`Ref${item.key}`]; + syncTerminal(); }; const submitAddHost = (formEl: FormInstance | undefined, ops: string) => { @@ -424,7 +425,7 @@ onMounted(() => { loadCommand(); timer = setInterval(() => { syncTerminal(); - }, 1000 * 8); + }, 1000 * 5); }); onBeforeMount(() => { clearInterval(Number(timer)); diff --git a/frontend/src/views/setting/backup-account/operate/index.vue b/frontend/src/views/setting/backup-account/operate/index.vue index cabeafc06..7c4b8c640 100644 --- a/frontend/src/views/setting/backup-account/operate/index.vue +++ b/frontend/src/views/setting/backup-account/operate/index.vue @@ -108,8 +108,10 @@