mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-12-04 01:39:20 +08:00
feat: 实现终端效果,终端大小自适应
This commit is contained in:
parent
6ab24989fb
commit
e68a62c925
87
backend/app/api/v1/terminal.go
Normal file
87
backend/app/api/v1/terminal.go
Normal file
@ -0,0 +1,87 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/1Panel-dev/1Panel/utils/ssh"
|
||||
"github.com/1Panel-dev/1Panel/utils/terminal"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||
host := ssh.ConnInfo{
|
||||
Addr: "172.16.10.111",
|
||||
Port: 22,
|
||||
User: "root",
|
||||
AuthMode: "password",
|
||||
Password: "Calong@2015",
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := host.NewClient()
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
ssConn, err := host.NewSshConn(cols, rows)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer ssConn.Close()
|
||||
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, true, host.Client, wsConn)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer sws.Close()
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
sws.Start(quitChan)
|
||||
go sws.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)
|
||||
dt := time.Now().Add(time.Second)
|
||||
if err := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); err != nil {
|
||||
global.LOG.Errorf("websocket writes control message failed, err: %v", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var upGrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024 * 1024 * 10,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
@ -3,6 +3,7 @@ module github.com/1Panel-dev/1Panel
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2
|
||||
github.com/fsnotify/fsnotify v1.5.4
|
||||
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
|
||||
github.com/gin-contrib/i18n v0.0.1
|
||||
@ -11,19 +12,20 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
github.com/gorilla/csrf v1.7.1
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gwatts/gin-adapter v1.0.0
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
|
||||
github.com/swaggo/gin-swagger v1.5.1
|
||||
github.com/swaggo/swag v1.8.4
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||
golang.org/x/text v0.3.7
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/driver/mysql v1.3.5
|
||||
@ -37,7 +39,6 @@ require (
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@ -56,6 +57,7 @@ require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.3 // indirect
|
||||
github.com/google/flatbuffers v1.12.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@ -72,7 +74,6 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
@ -80,7 +81,6 @@ require (
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
|
@ -41,6 +41,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@ -74,6 +75,7 @@ github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHH
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
@ -211,8 +213,8 @@ github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
|
||||
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gwatts/gin-adapter v1.0.0 h1:TsmmhYTR79/RMTsfYJ2IQvI1F5KZ3ZFJxuQSYEOpyIA=
|
||||
github.com/gwatts/gin-adapter v1.0.0/go.mod h1:44AEV+938HsS0mjfXtBDCUZS9vONlF2gwvh8wu4sRYc=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@ -324,6 +326,7 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
|
||||
@ -538,6 +541,7 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1,6 +1,8 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/docs"
|
||||
"github.com/1Panel-dev/1Panel/i18n"
|
||||
"github.com/1Panel-dev/1Panel/middleware"
|
||||
@ -9,7 +11,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerfiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
func Routers() *gin.Engine {
|
||||
@ -38,6 +39,7 @@ func Routers() *gin.Engine {
|
||||
{
|
||||
systemRouter.InitBaseRouter(PrivateGroup)
|
||||
systemRouter.InitUserRouter(PrivateGroup)
|
||||
systemRouter.InitTerminalRouter(PrivateGroup)
|
||||
systemRouter.InitOperationLogRouter(PrivateGroup)
|
||||
}
|
||||
|
||||
|
19
backend/router/ro_terminal.go
Normal file
19
backend/router/ro_terminal.go
Normal file
@ -0,0 +1,19 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type TerminalRouter struct{}
|
||||
|
||||
func (s *UserRouter) InitTerminalRouter(Router *gin.RouterGroup) {
|
||||
terminalRouter := Router.Group("terminals")
|
||||
withRecordRouter := terminalRouter.Use(middleware.OperationRecord())
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
withRecordRouter.GET("", baseApi.WsSsh)
|
||||
}
|
||||
}
|
150
backend/utils/ssh/ssh.go
Normal file
150
backend/utils/ssh/ssh.go
Normal file
@ -0,0 +1,150 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type ConnInfo struct {
|
||||
User string `json:"user"`
|
||||
Addr string `json:"addr"`
|
||||
Port int `json:"port"`
|
||||
AuthMode string `json:"authMode"`
|
||||
Password string `json:"password"`
|
||||
PrivateKey []byte `json:"privateKey"`
|
||||
PassPhrase []byte `json:"passPhrase"`
|
||||
DialTimeOut time.Duration `json:"dialTimeOut"`
|
||||
|
||||
Client *gossh.Client `json:"client"`
|
||||
Session *gossh.Session `json:"session"`
|
||||
LastResult string `json:"lastResult"`
|
||||
}
|
||||
|
||||
func (c *ConnInfo) NewClient() (*ConnInfo, error) {
|
||||
config := &gossh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
addr := fmt.Sprintf("%s:%d", c.Addr, c.Port)
|
||||
config.User = c.User
|
||||
if c.AuthMode == "password" {
|
||||
config.Auth = []gossh.AuthMethod{gossh.Password(c.Password)}
|
||||
} else {
|
||||
signer, err := makePrivateKeySigner(c.PrivateKey, c.PassPhrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = []gossh.AuthMethod{gossh.PublicKeys(signer)}
|
||||
}
|
||||
if c.DialTimeOut == 0 {
|
||||
c.DialTimeOut = 5 * time.Second
|
||||
}
|
||||
config.Timeout = c.DialTimeOut
|
||||
|
||||
config.HostKeyCallback = func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil }
|
||||
client, err := gossh.Dial("tcp", addr, config)
|
||||
if nil != err {
|
||||
return c, err
|
||||
}
|
||||
c.Client = client
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *ConnInfo) Run(shell string) (string, error) {
|
||||
if c.Client == nil {
|
||||
if _, err := c.NewClient(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
session, err := c.Client.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
buf, err := session.CombinedOutput(shell)
|
||||
|
||||
c.LastResult = string(buf)
|
||||
return c.LastResult, err
|
||||
}
|
||||
|
||||
func (c *ConnInfo) Close() {
|
||||
if err := c.Client.Close(); err != nil {
|
||||
global.LOG.Error("close ssh client failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type SshConn struct {
|
||||
StdinPipe io.WriteCloser
|
||||
ComboOutput *wsBufferWriter
|
||||
Session *gossh.Session
|
||||
}
|
||||
|
||||
func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) {
|
||||
sshSession, err := c.Client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdinP, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comboWriter := new(wsBufferWriter)
|
||||
sshSession.Stdout = comboWriter
|
||||
sshSession.Stderr = comboWriter
|
||||
|
||||
modes := gossh.TerminalModes{
|
||||
gossh.ECHO: 1,
|
||||
gossh.TTY_OP_ISPEED: 14400,
|
||||
gossh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
|
||||
}
|
||||
|
||||
func (s *SshConn) Close() {
|
||||
if s.Session != nil {
|
||||
s.Session.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type wsBufferWriter struct {
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *wsBufferWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Write(p)
|
||||
}
|
||||
|
||||
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
|
||||
var signer gossh.Signer
|
||||
if passPhrase != nil {
|
||||
s, err := gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing SSH key: '%v'", err)
|
||||
}
|
||||
signer = s
|
||||
} else {
|
||||
s, err := gossh.ParsePrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing SSH key: '%v'", err)
|
||||
}
|
||||
signer = s
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
}
|
21
backend/utils/ssh/ssh_test.go
Normal file
21
backend/utils/ssh/ssh_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSSH(t *testing.T) {
|
||||
ss := ConnInfo{
|
||||
Addr: "172.16.10.111",
|
||||
Port: 22,
|
||||
User: "root",
|
||||
AuthMode: "password",
|
||||
Password: "Calong@2015",
|
||||
}
|
||||
_, err := ss.NewClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fmt.Println(ss.Run("ip a"))
|
||||
}
|
201
backend/utils/terminal/ws_session.go
Normal file
201
backend/utils/terminal/ws_session.go
Normal file
@ -0,0 +1,201 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type safeBuffer struct {
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *safeBuffer) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Write(p)
|
||||
}
|
||||
func (w *safeBuffer) Bytes() []byte {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Bytes()
|
||||
}
|
||||
func (w *safeBuffer) Reset() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.buffer.Reset()
|
||||
}
|
||||
|
||||
const (
|
||||
wsMsgCmd = "cmd"
|
||||
wsMsgResize = "resize"
|
||||
)
|
||||
|
||||
type wsMsg struct {
|
||||
Type string `json:"type"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
type LogicSshWsSession struct {
|
||||
stdinPipe io.WriteCloser
|
||||
comboOutput *safeBuffer
|
||||
logBuff *safeBuffer
|
||||
inputFilterBuff *safeBuffer
|
||||
session *ssh.Session
|
||||
wsConn *websocket.Conn
|
||||
isAdmin bool
|
||||
IsFlagged bool `comment:"当前session是否包含禁止命令"`
|
||||
}
|
||||
|
||||
func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdinP, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comboWriter := new(safeBuffer)
|
||||
logBuf := new(safeBuffer)
|
||||
inputBuf := new(safeBuffer)
|
||||
sshSession.Stdout = comboWriter
|
||||
sshSession.Stderr = comboWriter
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LogicSshWsSession{
|
||||
stdinPipe: stdinP,
|
||||
comboOutput: comboWriter,
|
||||
logBuff: logBuf,
|
||||
inputFilterBuff: inputBuf,
|
||||
session: sshSession,
|
||||
wsConn: wsConn,
|
||||
isAdmin: isAdmin,
|
||||
IsFlagged: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) Close() {
|
||||
if sws.session != nil {
|
||||
sws.session.Close()
|
||||
}
|
||||
if sws.logBuff != nil {
|
||||
sws.logBuff = nil
|
||||
}
|
||||
if sws.comboOutput != nil {
|
||||
sws.comboOutput = nil
|
||||
}
|
||||
}
|
||||
func (sws *LogicSshWsSession) Start(quitChan chan bool) {
|
||||
go sws.receiveWsMsg(quitChan)
|
||||
go sws.sendComboOutput(quitChan)
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) 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{}
|
||||
if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||
global.LOG.Errorf("unmarshal websocket message %s failed, err: %v", wsData, err)
|
||||
}
|
||||
switch msgObj.Type {
|
||||
case wsMsgResize:
|
||||
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||
if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); 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 *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
|
||||
if _, err := sws.stdinPipe.Write(cmdBytes); err != nil {
|
||||
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
|
||||
wsConn := sws.wsConn
|
||||
defer setQuit(exitCh)
|
||||
|
||||
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
if sws.comboOutput == nil {
|
||||
return
|
||||
}
|
||||
bs := sws.comboOutput.Bytes()
|
||||
if len(bs) > 0 {
|
||||
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()
|
||||
}
|
||||
|
||||
case <-exitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
|
||||
if err := sws.session.Wait(); err != nil {
|
||||
global.LOG.Errorf("ssh session wait failed, err: %v", err)
|
||||
setQuit(quitChan)
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) LogString() string {
|
||||
return sws.logBuff.buffer.String()
|
||||
}
|
||||
|
||||
func setQuit(ch chan bool) {
|
||||
ch <- true
|
||||
}
|
9607
frontend/package-lock.json
generated
9607
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,7 @@
|
||||
"echarts-liquidfill": "^3.1.0",
|
||||
"element-plus": "^2.2.6",
|
||||
"fit2cloud-ui-plus": "^0.0.1-beta.12",
|
||||
"js-base64": "^3.7.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.12",
|
||||
@ -37,7 +38,9 @@
|
||||
"vue": "^3.2.25",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue3-seamless-scroll": "^1.2.0"
|
||||
"vue3-seamless-scroll": "^1.2.0",
|
||||
"xterm": "^4.19.0",
|
||||
"xterm-addon-attach": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.1",
|
||||
|
9
frontend/src/api/interface/terminal.ts
Normal file
9
frontend/src/api/interface/terminal.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface ReqTerminal {
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
user: string;
|
||||
authType: string;
|
||||
password: string;
|
||||
key: string;
|
||||
}
|
@ -13,7 +13,7 @@ const terminalRouter = {
|
||||
{
|
||||
path: '/terminal',
|
||||
name: 'Terminal',
|
||||
component: () => import('@/views/terminal/index.vue'),
|
||||
component: () => import('@/views/terminal/index2.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
|
@ -1,53 +0,0 @@
|
||||
// * Echarts 按需引入
|
||||
import * as echarts from 'echarts/core';
|
||||
import {
|
||||
BarChart,
|
||||
// 系列类型的定义后缀都为 SeriesOption
|
||||
BarSeriesOption,
|
||||
LineChart,
|
||||
LineSeriesOption,
|
||||
} from 'echarts/charts';
|
||||
import { LegendComponent } from 'echarts/components';
|
||||
import {
|
||||
TitleComponent,
|
||||
// 组件类型的定义后缀都为 ComponentOption
|
||||
TitleComponentOption,
|
||||
TooltipComponent,
|
||||
TooltipComponentOption,
|
||||
GridComponent,
|
||||
GridComponentOption,
|
||||
// 数据集组件
|
||||
DatasetComponent,
|
||||
DatasetComponentOption,
|
||||
// 内置数据转换器组件 (filter, sort)
|
||||
TransformComponent,
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
||||
export type ECOption = echarts.ComposeOption<
|
||||
| BarSeriesOption
|
||||
| LineSeriesOption
|
||||
| TitleComponentOption
|
||||
| TooltipComponentOption
|
||||
| GridComponentOption
|
||||
| DatasetComponentOption
|
||||
>;
|
||||
|
||||
// 注册必须的组件
|
||||
echarts.use([
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
TransformComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
export default echarts;
|
@ -1,15 +0,0 @@
|
||||
// * Element 常用表单校验规则
|
||||
|
||||
/**
|
||||
* @rule 手机号
|
||||
*/
|
||||
export function checkPhoneNumber(rule: any, value: any, callback: any) {
|
||||
const regexp =
|
||||
/^(((13[0-9]{1})|(15[0-9]{1})|(16[0-9]{1})|(17[3-8]{1})|(18[0-9]{1})|(19[0-9]{1})|(14[5-7]{1}))+\d{8})$/;
|
||||
if (value === '') callback('请输入手机号码');
|
||||
if (!regexp.test(value)) {
|
||||
callback(new Error('请输入正确的手机号码'));
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
// * 系统全局字典
|
||||
|
||||
/**
|
||||
* @description:用户性别
|
||||
*/
|
||||
export const genderType = [
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 2 },
|
||||
];
|
@ -1,52 +1,3 @@
|
||||
import { isArray } from '@/utils/is';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/**
|
||||
* @description 获取localStorage
|
||||
* @param {String} key Storage名称
|
||||
* @return string
|
||||
*/
|
||||
export function localGet(key: string) {
|
||||
const value = window.localStorage.getItem(key);
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(key) as string);
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 存储localStorage
|
||||
* @param {String} key Storage名称
|
||||
* @param {Any} value Storage值
|
||||
* @return void
|
||||
*/
|
||||
export function localSet(key: string, value: any) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清除localStorage
|
||||
* @param {String} key Storage名称
|
||||
* @return void
|
||||
*/
|
||||
export function localRemove(key: string) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清除所有localStorage
|
||||
* @return void
|
||||
*/
|
||||
export function localClear() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 对象数组深克隆
|
||||
* @param {Object} obj 源对象
|
||||
* @return object
|
||||
*/
|
||||
export function deepCopy<T>(obj: any): T {
|
||||
let newObj: any;
|
||||
try {
|
||||
@ -63,33 +14,11 @@ export function deepCopy<T>(obj: any): T {
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断数据类型
|
||||
* @param {Any} val 需要判断类型的数据
|
||||
* @return string
|
||||
*/
|
||||
export function isType(val: any) {
|
||||
if (val === null) return 'null';
|
||||
if (typeof val !== 'object') return typeof val;
|
||||
else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成随机数
|
||||
* @param {Number} min 最小值
|
||||
* @param {Number} max 最大值
|
||||
* @return number
|
||||
*/
|
||||
export function randomNum(min: number, max: number): number {
|
||||
let num = Math.floor(Math.random() * (min - max) + max);
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取浏览器默认语言
|
||||
* @return string
|
||||
*/
|
||||
export function getBrowserLang() {
|
||||
let browserLang = navigator.language ? navigator.language : navigator.browserLanguage;
|
||||
let defaultBrowserLang = '';
|
||||
@ -104,94 +33,6 @@ export function getBrowserLang() {
|
||||
}
|
||||
return defaultBrowserLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 递归查询当前路由所对应的路由
|
||||
* @param {Array} menuList 菜单列表
|
||||
* @param {String} path 当前地址
|
||||
* @return array
|
||||
*/
|
||||
export function getTabPane<T, U>(menuList: any[], path: U): T {
|
||||
let result: any;
|
||||
for (let item of menuList || []) {
|
||||
if (item.path === path) result = item;
|
||||
const res = getTabPane(item.children, path);
|
||||
if (res) result = res;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 使用递归处理路由菜单,生成一维数组
|
||||
* @param {Array} menuList 所有菜单列表
|
||||
* @param {Array} newArr 菜单的一维数组
|
||||
* @return array
|
||||
*/
|
||||
export function handleRouter(routerList: RouteRecordRaw[], newArr: string[] = []) {
|
||||
routerList.forEach((item: RouteRecordRaw) => {
|
||||
typeof item === 'object' && item.path && newArr.push(item.path);
|
||||
item.children && item.children.length && handleRouter(item.children, newArr);
|
||||
});
|
||||
return newArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 扁平化数组对象
|
||||
* @param {Array} arr 数组对象
|
||||
* @return array
|
||||
*/
|
||||
export function getFlatArr(arr: any) {
|
||||
return arr.reduce((pre: any, current: any) => {
|
||||
let flatArr = [...pre, current];
|
||||
if (current.children) flatArr = [...flatArr, ...getFlatArr(current.children)];
|
||||
return flatArr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 格式化表格单元格默认值
|
||||
* @param {Number} row 行
|
||||
* @param {Number} col 列
|
||||
* @param {String} callValue 当前单元格值
|
||||
* @return string
|
||||
* */
|
||||
export function defaultFormat(row: number, col: number, callValue: any) {
|
||||
// 如果当前值为数组,使用 / 拼接(根据需求自定义)
|
||||
if (isArray(callValue)) return callValue.length ? callValue.join(' / ') : '--';
|
||||
return callValue ?? '--';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理无数据情况
|
||||
* @param {String} callValue 需要处理的值
|
||||
* @return string
|
||||
* */
|
||||
export function formatValue(callValue: any) {
|
||||
// 如果当前值为数组,使用 / 拼接(根据需求自定义)
|
||||
if (isArray(callValue)) return callValue.length ? callValue.join(' / ') : '--';
|
||||
return callValue ?? '--';
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据枚举列表查询当需要的数据(如果指定了 label 和 value 的 key值,会自动识别格式化)
|
||||
* @param {String} callValue 当前单元格值
|
||||
* @param {Array} enumData 枚举列表
|
||||
* @param {String} type 过滤类型(目前只有 tag)
|
||||
* @return string
|
||||
* */
|
||||
export function filterEnum(callValue: any, enumData: any, searchProps?: { [key: string]: any }, type?: string): string {
|
||||
const value = searchProps?.value ?? 'value';
|
||||
const label = searchProps?.label ?? 'label';
|
||||
let filterData = enumData.find((item: any) => item[value] === callValue);
|
||||
if (type == 'tag') return filterData?.tagType ? filterData.tagType : '';
|
||||
return filterData ? filterData[label] : '--';
|
||||
}
|
||||
/**
|
||||
* 对日期进行格式化,默认yyyy-MM-dd HH:mm:ss
|
||||
* @param dataStr 要格式化的日期
|
||||
* @return String
|
||||
*/
|
||||
|
||||
export function dateFromat(row: number, col: number, dataStr: any) {
|
||||
const date = new Date(dataStr);
|
||||
const y = date.getFullYear();
|
||||
|
Loading…
Reference in New Issue
Block a user