mirror of
https://github.com/acepanel/panel.git
synced 2026-02-05 14:53:19 +08:00
feat: web ssh
This commit is contained in:
166
app/http/controllers/ssh_controller.go
Normal file
166
app/http/controllers/ssh_controller.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
nethttp "net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type SshController struct {
|
||||
AuthMethod ssh.AuthMethod
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewSshController() *SshController {
|
||||
return &SshController{
|
||||
AuthMethod: ssh.PASSWORD,
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SshController) GetInfo(ctx http.Context) {
|
||||
host := r.setting.Get(models.SettingKeySshHost)
|
||||
port := r.setting.Get(models.SettingKeySshPort)
|
||||
user := r.setting.Get(models.SettingKeySshUser)
|
||||
password := r.setting.Get(models.SettingKeySshPassword)
|
||||
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
|
||||
Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"host": host,
|
||||
"port": port,
|
||||
"user": user,
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SshController) UpdateInfo(ctx http.Context) {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"host": "required",
|
||||
"port": "required",
|
||||
"user": "required",
|
||||
"password": "required",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
host := ctx.Request().Input("host")
|
||||
port := ctx.Request().Input("port")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
err = r.setting.Set(models.SettingKeySshHost, host)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshPort, port)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshUser, user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshPassword, password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *SshController) Session(ctx http.Context) {
|
||||
upGrader := websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *nethttp.Request) bool {
|
||||
return true
|
||||
},
|
||||
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
|
||||
}
|
||||
|
||||
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 建立连接失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
var config *ssh.SSHClientConfig
|
||||
config = ssh.SSHClientConfigPassword(
|
||||
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
|
||||
r.setting.Get(models.SettingKeySshUser),
|
||||
r.setting.Get(models.SettingKeySshPassword),
|
||||
)
|
||||
|
||||
client, err := ssh.NewSSHClient(config)
|
||||
if err != nil {
|
||||
ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
turn, err := ssh.NewTurn(ws, client)
|
||||
if err != nil {
|
||||
ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return
|
||||
}
|
||||
defer turn.Close()
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
var logBuff = bufPool.Get().(*bytes.Buffer)
|
||||
logBuff.Reset()
|
||||
defer bufPool.Put(logBuff)
|
||||
|
||||
ctx2, cancel := context.WithCancel(context.Background())
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := turn.LoopRead(logBuff, ctx2)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 读取数据失败 ", err.Error())
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := turn.SessionWait()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 会话失败 ", err.Error())
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func (c *WebsiteController) Add(ctx http.Context) {
|
||||
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$",
|
||||
"domain": "required",
|
||||
"php": "required",
|
||||
"db": "required",
|
||||
"db": "bool",
|
||||
"db_type": "required_if:db,true",
|
||||
"db_name": "required_if:db,true",
|
||||
"db_user": "required_if:db,true",
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// Jwt 确保通过 JWT 鉴权
|
||||
func Jwt() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
token := ctx.Request().Header("access_token", ctx.Request().Input("access_token", ""))
|
||||
token := ctx.Request().Header("access_token", ctx.Request().Input("access_token", ctx.Request().Header("Sec-WebSocket-Protocol")))
|
||||
if len(token) == 0 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"code": 401,
|
||||
|
||||
@@ -10,6 +10,10 @@ const (
|
||||
SettingKeyWebsitePath = "website_path"
|
||||
SettingKeyEntrance = "entrance"
|
||||
SettingKeyMysqlRootPassword = "mysql_root_password"
|
||||
SettingKeySshHost = "ssh_host"
|
||||
SettingKeySshPort = "ssh_port"
|
||||
SettingKeySshUser = "ssh_user"
|
||||
SettingKeySshPassword = "ssh_password"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
|
||||
5
go.mod
5
go.mod
@@ -3,16 +3,19 @@ module panel
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.2
|
||||
github.com/gertd/go-pluralize v0.2.1
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imroc/req/v3 v3.37.2
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
)
|
||||
|
||||
@@ -34,7 +37,6 @@ require (
|
||||
github.com/RichardKnop/machinery/v2 v2.0.11 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.37.16 // indirect
|
||||
github.com/bytedance/sonic v1.9.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
@@ -161,7 +163,6 @@ require (
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -361,6 +361,8 @@ github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7 h1:2rkC/6M7tL
|
||||
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7/go.mod h1:1lxwtCXMkotSmt0YWRg/s7EnMUFfmne9L1a50X9J0fA=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
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/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
|
||||
72
pkg/ssh/ssh.go
Normal file
72
pkg/ssh/ssh.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type AuthMethod int8
|
||||
|
||||
const (
|
||||
PASSWORD AuthMethod = iota + 1
|
||||
PUBLICKEY
|
||||
)
|
||||
|
||||
type SSHClientConfig struct {
|
||||
AuthMethod AuthMethod
|
||||
HostAddr string
|
||||
User string
|
||||
Password string
|
||||
KeyPath string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func SSHClientConfigPassword(hostAddr, user, Password string) *SSHClientConfig {
|
||||
return &SSHClientConfig{
|
||||
Timeout: time.Second * 5,
|
||||
AuthMethod: PASSWORD,
|
||||
HostAddr: hostAddr,
|
||||
User: user,
|
||||
Password: Password,
|
||||
}
|
||||
}
|
||||
|
||||
func SSHClientConfigPulicKey(hostAddr, user, keyPath string) *SSHClientConfig {
|
||||
return &SSHClientConfig{
|
||||
Timeout: time.Second * 5,
|
||||
AuthMethod: PUBLICKEY,
|
||||
HostAddr: hostAddr,
|
||||
User: user,
|
||||
KeyPath: keyPath,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSSHClient(conf *SSHClientConfig) (*ssh.Client, error) {
|
||||
config := &ssh.ClientConfig{
|
||||
Timeout: conf.Timeout,
|
||||
User: conf.User,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
switch conf.AuthMethod {
|
||||
case PASSWORD:
|
||||
config.Auth = []ssh.AuthMethod{ssh.Password(conf.Password)}
|
||||
case PUBLICKEY:
|
||||
signer, err := getKey(conf.KeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
c, err := ssh.Dial("tcp", conf.HostAddr, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getKey(keyPath string) (ssh.Signer, error) {
|
||||
return ssh.ParsePrivateKey([]byte(tools.ReadFile(keyPath)))
|
||||
}
|
||||
139
pkg/ssh/turn.go
Normal file
139
pkg/ssh/turn.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
MsgData = '1'
|
||||
MsgResize = '2'
|
||||
)
|
||||
|
||||
type Turn struct {
|
||||
StdinPipe io.WriteCloser
|
||||
Session *ssh.Session
|
||||
WsConn *websocket.Conn
|
||||
}
|
||||
|
||||
func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) {
|
||||
sess, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdinPipe, err := sess.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
turn := &Turn{StdinPipe: stdinPipe, Session: sess, WsConn: wsConn}
|
||||
sess.Stdout = turn
|
||||
sess.Stderr = turn
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||
}
|
||||
if err := sess.RequestPty("xterm", 150, 30, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sess.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return turn, nil
|
||||
}
|
||||
|
||||
func (t *Turn) Write(p []byte) (n int, err error) {
|
||||
writer, err := t.WsConn.NextWriter(websocket.BinaryMessage)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
return writer.Write(p)
|
||||
}
|
||||
func (t *Turn) Close() error {
|
||||
if t.Session != nil {
|
||||
t.Session.Close()
|
||||
}
|
||||
|
||||
return t.WsConn.Close()
|
||||
}
|
||||
|
||||
func (t *Turn) Read(p []byte) (n int, err error) {
|
||||
for {
|
||||
msgType, reader, err := t.WsConn.NextReader()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if msgType != websocket.BinaryMessage {
|
||||
continue
|
||||
}
|
||||
|
||||
return reader.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Turn) LoopRead(logBuff *bytes.Buffer, context context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-context.Done():
|
||||
return errors.New("LoopRead exit")
|
||||
default:
|
||||
_, wsData, err := t.WsConn.ReadMessage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading webSocket message err:%s", err)
|
||||
}
|
||||
body := decode(wsData[1:])
|
||||
switch wsData[0] {
|
||||
case MsgResize:
|
||||
var args Resize
|
||||
err := json.Unmarshal(body, &args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ssh pty resize windows err:%s", err)
|
||||
}
|
||||
if args.Columns > 0 && args.Rows > 0 {
|
||||
if err := t.Session.WindowChange(args.Rows, args.Columns); err != nil {
|
||||
return fmt.Errorf("ssh pty resize windows err:%s", err)
|
||||
}
|
||||
}
|
||||
case MsgData:
|
||||
if _, err := t.StdinPipe.Write(body); err != nil {
|
||||
return fmt.Errorf("StdinPipe write err:%s", err)
|
||||
}
|
||||
if _, err := logBuff.Write(body); err != nil {
|
||||
return fmt.Errorf("logBuff write err:%s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Turn) SessionWait() error {
|
||||
if err := t.Session.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(p []byte) []byte {
|
||||
decodeString, _ := base64.StdEncoding.DecodeString(string(p))
|
||||
return decodeString
|
||||
}
|
||||
|
||||
type Resize struct {
|
||||
Columns int
|
||||
Rows int
|
||||
}
|
||||
@@ -39,6 +39,9 @@
|
||||
<script src="https://cdnjs.cdn.haozi.net/layui/2.8.11/layui.min.js"></script>
|
||||
<script src="https://cdnjs.cdn.haozi.net/ace/1.6.1/ace.js"></script>
|
||||
<script src="https://cdnjs.cdn.haozi.net/echarts/5.4.2/echarts.min.js"></script>
|
||||
<script src="https://registry.npmmirror.com/xterm/5.2.1/files/lib/xterm.js"></script>
|
||||
<script src="https://registry.npmmirror.com/xterm-addon-fit/0.7.0/files/lib/xterm-addon-fit.js"></script>
|
||||
<script src="https://cdnjs.cdn.haozi.net/crypto-js/4.1.1/crypto-js.min.js"></script>
|
||||
<script>
|
||||
layui.config({
|
||||
base: 'panel/', // 静态资源所在路径
|
||||
|
||||
@@ -135,9 +135,6 @@ Date: 2023-07-21
|
||||
$('#setting-api-token input').attr('readonly', true);
|
||||
}
|
||||
}
|
||||
, error: function (xhr, status, error) {
|
||||
console.log('耗子Linux面板:ajax请求出错,错误' + error);
|
||||
}
|
||||
});
|
||||
|
||||
// 面板设置
|
||||
|
||||
158
public/panel/views/ssh.html
Normal file
158
public/panel/views/ssh.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!--
|
||||
Name: SSH
|
||||
Author: 耗子
|
||||
Date: 2023-07-25
|
||||
-->
|
||||
<title>SSH</title>
|
||||
<link href="https://registry.npmmirror.com/xterm/5.2.1/files/css/xterm.css" rel="stylesheet">
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
SSH
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-form" style="overflow: hidden;" lay-filter="ssh_setting">
|
||||
<div class="layui-inline">
|
||||
<span style="margin-right: 10px;">地址</span>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="host" class="layui-input"
|
||||
style="height: 30px; margin-top: 5px;">
|
||||
</div>
|
||||
<span style="margin-left: 40px; margin-right: 10px;">端口</span>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="port" class="layui-input"
|
||||
style="height: 30px; margin-top: 5px;">
|
||||
</div>
|
||||
<span style="margin-left: 40px; margin-right: 10px;">账号</span>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="user" class="layui-input"
|
||||
style="height: 30px; margin-top: 5px;">
|
||||
</div>
|
||||
<span style="margin-left: 40px; margin-right: 10px;">密码</span>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="password" class="layui-input"
|
||||
style="height: 30px; margin-top: 5px;">
|
||||
</div>
|
||||
<div class="layui-input-inline">
|
||||
<button lay-filter="save_ssh_setting" lay-submit class="layui-btn layui-btn-sm"
|
||||
style="margin-left: 10px;">
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal" style="width: 100%; height: 70vh; background-color: #000000; margin-top: 20px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['admin', 'jquery', 'form'], function () {
|
||||
var admin = layui.admin;
|
||||
var $ = layui.jquery;
|
||||
var form = layui.form;
|
||||
|
||||
form.render();
|
||||
|
||||
admin.req({
|
||||
url: "/api/panel/ssh/info"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
layer.msg('SSH信息获取失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
form.val("ssh_setting",
|
||||
result.data
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
form.on('submit(save_ssh_setting)', function (data) {
|
||||
admin.req({
|
||||
url: "/api/panel/ssh/info"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
layer.alert('SSH信息保存失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
layer.alert('SSH信息保存成功!')
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const msgData = '1'
|
||||
const msgResize = '2'
|
||||
|
||||
var terminal = new Terminal({
|
||||
rendererType: "canvas",
|
||||
fontSize: 15,
|
||||
screenKeys: true,
|
||||
useStyle: true,
|
||||
cursorBlink: true, // 光标闪烁
|
||||
theme: {
|
||||
foreground: "#ECECEC", // 字体
|
||||
background: "#000000", //背景色
|
||||
cursor: "help", // 设置光标
|
||||
lineHeight: 20
|
||||
}
|
||||
});
|
||||
let fitAddon = new FitAddon.FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
fitAddon.fit()
|
||||
|
||||
let terminalContainer = document.getElementById("terminal")
|
||||
let token = layui.data('HaoZiPanel')['access_token']
|
||||
const webSocket = new WebSocket(`ws://${window.location.host}/api/panel/ssh/session`, [token]);
|
||||
webSocket.binaryType = 'arraybuffer';
|
||||
const enc = new TextDecoder("utf-8");
|
||||
webSocket.onmessage = (event) => {
|
||||
terminal.write(enc.decode(event.data));
|
||||
}
|
||||
|
||||
webSocket.onopen = () => {
|
||||
terminal.open(terminalContainer)
|
||||
fitAddon.fit()
|
||||
terminal.write("\r\nWelcome to HaoZiPanel SSH. Connection success.\r\n")
|
||||
terminal.focus()
|
||||
}
|
||||
|
||||
webSocket.onclose = () => {
|
||||
terminal.write("\r\nSSH connection closed. Please refresh the page.\r\n")
|
||||
}
|
||||
|
||||
webSocket.onerror = (event) => {
|
||||
console.error(event)
|
||||
webSocket.close()
|
||||
}
|
||||
|
||||
terminal.onData((data) => {
|
||||
webSocket.send(msgData + CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)), ArrayBuffer)
|
||||
})
|
||||
|
||||
terminal.onResize(({cols, rows}) => {
|
||||
if (webSocket.readyState === 1) {
|
||||
webSocket.send(msgResize +
|
||||
CryptoJS.enc.Base64.stringify(
|
||||
CryptoJS.enc.Utf8.parse(
|
||||
JSON.stringify({
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
), ArrayBuffer
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
fitAddon.fit()
|
||||
}, false)
|
||||
</script>
|
||||
@@ -114,7 +114,7 @@ Date: 2023-06-24
|
||||
})
|
||||
|
||||
form.on('select(add-website-db)', function (data) {
|
||||
if (data.value === '') {
|
||||
if (data.value === "false") {
|
||||
$('#add-website-db-info').hide()
|
||||
return false
|
||||
}
|
||||
@@ -130,7 +130,7 @@ Date: 2023-06-24
|
||||
})
|
||||
// 提交
|
||||
form.on('submit(add-website-submit)', function (data) {
|
||||
data.field.db = data.field.db_type !== '';
|
||||
data.field.db = data.field.db_type !== "false";
|
||||
admin.req({
|
||||
url: '/api/panel/website/add'
|
||||
, type: 'post'
|
||||
|
||||
@@ -95,6 +95,12 @@ func Web() {
|
||||
r.Get("list", monitorController.List)
|
||||
r.Get("switchAndDays", monitorController.SwitchAndDays)
|
||||
})
|
||||
r.Prefix("ssh").Middleware(middleware.Jwt()).Group(func(r route.Route) {
|
||||
sshController := controllers.NewSshController()
|
||||
r.Get("info", sshController.GetInfo)
|
||||
r.Post("info", sshController.UpdateInfo)
|
||||
r.Get("session", sshController.Session)
|
||||
})
|
||||
r.Prefix("setting").Middleware(middleware.Jwt()).Group(func(r route.Route) {
|
||||
settingController := controllers.NewSettingController()
|
||||
r.Get("list", settingController.List)
|
||||
|
||||
Reference in New Issue
Block a user