feat: 实现终端效果,终端大小自适应

This commit is contained in:
ssongliu 2022-08-17 19:02:48 +08:00 committed by ssongliu
parent 6ab24989fb
commit e68a62c925
16 changed files with 576 additions and 9783 deletions

View 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
},
}

View File

@ -3,6 +3,7 @@ module github.com/1Panel-dev/1Panel
go 1.18 go 1.18
require ( require (
github.com/dgraph-io/badger/v3 v3.2103.2
github.com/fsnotify/fsnotify v1.5.4 github.com/fsnotify/fsnotify v1.5.4
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
github.com/gin-contrib/i18n v0.0.1 github.com/gin-contrib/i18n v0.0.1
@ -11,19 +12,20 @@ require (
github.com/go-playground/validator/v10 v10.11.0 github.com/go-playground/validator/v10 v10.11.0
github.com/golang-jwt/jwt/v4 v4.4.2 github.com/golang-jwt/jwt/v4 v4.4.2
github.com/gorilla/csrf v1.7.1 github.com/gorilla/csrf v1.7.1
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/websocket v1.5.0
github.com/gorilla/sessions v1.2.1
github.com/gwatts/gin-adapter v1.0.0 github.com/gwatts/gin-adapter v1.0.0
github.com/jinzhu/copier v0.3.5 github.com/jinzhu/copier v0.3.5
github.com/mojocn/base64Captcha v1.3.5 github.com/mojocn/base64Captcha v1.3.5
github.com/natefinch/lumberjack v2.0.0+incompatible github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/nicksnyder/go-i18n/v2 v2.1.2 github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/spf13/viper v1.12.0 github.com/spf13/viper v1.12.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.1 github.com/swaggo/gin-swagger v1.5.1
github.com/swaggo/swag v1.8.4 github.com/swaggo/swag v1.8.4
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.3.5 gorm.io/driver/mysql v1.3.5
@ -37,7 +39,6 @@ require (
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // 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/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.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/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // 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/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // 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/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.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/subosito/gotenv v1.3.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
go.opencensus.io v0.23.0 // 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/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

View File

@ -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/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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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/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 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 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/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 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= 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/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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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/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 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 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/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 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 h1:TsmmhYTR79/RMTsfYJ2IQvI1F5KZ3ZFJxuQSYEOpyIA=
github.com/gwatts/gin-adapter v1.0.0/go.mod h1:44AEV+938HsS0mjfXtBDCUZS9vONlF2gwvh8wu4sRYc= 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= 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/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/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 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/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.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 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 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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/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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -1,6 +1,8 @@
package router package router
import ( import (
"html/template"
"github.com/1Panel-dev/1Panel/docs" "github.com/1Panel-dev/1Panel/docs"
"github.com/1Panel-dev/1Panel/i18n" "github.com/1Panel-dev/1Panel/i18n"
"github.com/1Panel-dev/1Panel/middleware" "github.com/1Panel-dev/1Panel/middleware"
@ -9,7 +11,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files" swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
"html/template"
) )
func Routers() *gin.Engine { func Routers() *gin.Engine {
@ -38,6 +39,7 @@ func Routers() *gin.Engine {
{ {
systemRouter.InitBaseRouter(PrivateGroup) systemRouter.InitBaseRouter(PrivateGroup)
systemRouter.InitUserRouter(PrivateGroup) systemRouter.InitUserRouter(PrivateGroup)
systemRouter.InitTerminalRouter(PrivateGroup)
systemRouter.InitOperationLogRouter(PrivateGroup) systemRouter.InitOperationLogRouter(PrivateGroup)
} }

View 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
View 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
}

View 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"))
}

View 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
}

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"echarts-liquidfill": "^3.1.0", "echarts-liquidfill": "^3.1.0",
"element-plus": "^2.2.6", "element-plus": "^2.2.6",
"fit2cloud-ui-plus": "^0.0.1-beta.12", "fit2cloud-ui-plus": "^0.0.1-beta.12",
"js-base64": "^3.7.2",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.12", "pinia": "^2.0.12",
@ -37,7 +38,9 @@
"vue": "^3.2.25", "vue": "^3.2.25",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.1.9",
"vue-router": "^4.0.12", "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": { "devDependencies": {
"@commitlint/cli": "^17.0.1", "@commitlint/cli": "^17.0.1",

View File

@ -0,0 +1,9 @@
export interface ReqTerminal {
name: string;
ip: string;
port: number;
user: string;
authType: string;
password: string;
key: string;
}

View File

@ -13,7 +13,7 @@ const terminalRouter = {
{ {
path: '/terminal', path: '/terminal',
name: 'Terminal', name: 'Terminal',
component: () => import('@/views/terminal/index.vue'), component: () => import('@/views/terminal/index2.vue'),
meta: { meta: {
keepAlive: true, keepAlive: true,
requiresAuth: true, requiresAuth: true,

View File

@ -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;

View File

@ -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();
}
}

View File

@ -1,9 +0,0 @@
// * 系统全局字典
/**
* @description用户性别
*/
export const genderType = [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
];

View File

@ -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 { export function deepCopy<T>(obj: any): T {
let newObj: any; let newObj: any;
try { try {
@ -63,33 +14,11 @@ export function deepCopy<T>(obj: any): T {
} }
return newObj; 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 { export function randomNum(min: number, max: number): number {
let num = Math.floor(Math.random() * (min - max) + max); let num = Math.floor(Math.random() * (min - max) + max);
return num; return num;
} }
/**
* @description 获取浏览器默认语言
* @return string
*/
export function getBrowserLang() { export function getBrowserLang() {
let browserLang = navigator.language ? navigator.language : navigator.browserLanguage; let browserLang = navigator.language ? navigator.language : navigator.browserLanguage;
let defaultBrowserLang = ''; let defaultBrowserLang = '';
@ -104,94 +33,6 @@ export function getBrowserLang() {
} }
return defaultBrowserLang; 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) { export function dateFromat(row: number, col: number, dataStr: any) {
const date = new Date(dataStr); const date = new Date(dataStr);
const y = date.getFullYear(); const y = date.getFullYear();