mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-03-14 01:34:47 +08:00
feat: 增加 SSH 会话管理 (#1498)
This commit is contained in:
parent
4bf76aacb1
commit
152ba81c34
@ -2,6 +2,7 @@ package ps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
"strconv"
|
||||
"testing"
|
||||
@ -61,8 +62,17 @@ func TestPs(t *testing.T) {
|
||||
if err == nil {
|
||||
fmt.Println(cmdLine)
|
||||
}
|
||||
ss, err := pro.Terminal()
|
||||
if err == nil {
|
||||
fmt.Println(ss)
|
||||
}
|
||||
|
||||
fmt.Println(fmt.Sprintf("Name: %s PId: %v ParentID: %v Username: %v status:%s startTime: %s numThreads: %v numConnections:%v cpuPercent:%v rss:%s MB IORead: %s IOWrite: %s",
|
||||
name, pro.Pid, parentID, userName, status, startTime, numThreads, numConnections, cpuPercent, rss, ioRead, ioWrite))
|
||||
}
|
||||
users, err := host.Users()
|
||||
if err == nil {
|
||||
fmt.Println(users)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
"strings"
|
||||
@ -15,6 +16,7 @@ type WsInput struct {
|
||||
Type string `json:"type"`
|
||||
DownloadProgress
|
||||
PsProcessConfig
|
||||
SSHSessionConfig
|
||||
}
|
||||
|
||||
type DownloadProgress struct {
|
||||
@ -27,6 +29,11 @@ type PsProcessConfig struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type SSHSessionConfig struct {
|
||||
LoginUser string `json:"loginUser"`
|
||||
LoginIP string `json:"loginIP"`
|
||||
}
|
||||
|
||||
type PsProcessData struct {
|
||||
PID int32 `json:"PID"`
|
||||
Name string `json:"name"`
|
||||
@ -64,6 +71,15 @@ type processConnect struct {
|
||||
Status string `json:"status"`
|
||||
Laddr net.Addr `json:"localaddr"`
|
||||
Raddr net.Addr `json:"remoteaddr"`
|
||||
PID string `json:"PID"`
|
||||
}
|
||||
|
||||
type sshSession struct {
|
||||
Username string `json:"username"`
|
||||
PID int32 `json:"PID"`
|
||||
Terminal string `json:"terminal"`
|
||||
Host string `json:"host"`
|
||||
LoginTime string `json:"loginTime"`
|
||||
}
|
||||
|
||||
func ProcessData(c *Client, inputMsg []byte) {
|
||||
@ -86,6 +102,12 @@ func ProcessData(c *Client, inputMsg []byte) {
|
||||
return
|
||||
}
|
||||
c.Msg <- res
|
||||
case "ssh":
|
||||
res, err := getSSHSessions(wsInput.SSHSessionConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Msg <- res
|
||||
}
|
||||
|
||||
}
|
||||
@ -220,3 +242,52 @@ func getProcessData(processConfig PsProcessConfig) (res []byte, err error) {
|
||||
res, err = json.Marshal(result)
|
||||
return
|
||||
}
|
||||
|
||||
func getSSHSessions(config SSHSessionConfig) (res []byte, err error) {
|
||||
var (
|
||||
result []sshSession
|
||||
users []host.UserStat
|
||||
processes []*process.Process
|
||||
)
|
||||
processes, err = process.Processes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
users, err = host.Users()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, proc := range processes {
|
||||
name, _ := proc.Name()
|
||||
if name != "sshd" || proc.Pid == 0 {
|
||||
continue
|
||||
}
|
||||
connections, _ := proc.Connections()
|
||||
for _, conn := range connections {
|
||||
for _, user := range users {
|
||||
if user.Host == "" {
|
||||
continue
|
||||
}
|
||||
if conn.Raddr.IP == user.Host {
|
||||
if config.LoginUser != "" && !strings.Contains(user.User, config.LoginUser) {
|
||||
continue
|
||||
}
|
||||
if config.LoginIP != "" && !strings.Contains(user.Host, config.LoginIP) {
|
||||
continue
|
||||
}
|
||||
session := sshSession{
|
||||
Username: user.User,
|
||||
Host: user.Host,
|
||||
Terminal: user.Terminal,
|
||||
PID: proc.Pid,
|
||||
}
|
||||
t := time.Unix(int64(user.Started), 0)
|
||||
session.LoginTime = t.Format("2006-1-2 15:04:05")
|
||||
result = append(result, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res, err = json.Marshal(result)
|
||||
return
|
||||
}
|
||||
|
@ -912,7 +912,12 @@ const message = {
|
||||
publickey: 'Key',
|
||||
belong: 'Belong',
|
||||
local: 'Local',
|
||||
remote: 'Remote',
|
||||
config: 'Configuration',
|
||||
session: 'Session',
|
||||
loginTime: 'Login Time',
|
||||
loginIP: 'Login IP',
|
||||
disconnect: 'Disconnect',
|
||||
stopSSHWarn: 'Whether to disconnect this SSH connection',
|
||||
},
|
||||
setting: {
|
||||
all: 'All',
|
||||
|
@ -237,7 +237,7 @@ const message = {
|
||||
website: '网站',
|
||||
project: '项目',
|
||||
config: '配置',
|
||||
ssh: 'SSH 配置',
|
||||
ssh: 'SSH 管理',
|
||||
firewall: '防火墙',
|
||||
ssl: '证书',
|
||||
database: '数据库',
|
||||
@ -873,13 +873,19 @@ const message = {
|
||||
keyAuthHelper: '是否启用密钥认证,默认启用。',
|
||||
useDNS: '反向解析',
|
||||
dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能,从而验证连接方的身份。',
|
||||
loginLogs: 'SSH 登录日志',
|
||||
loginLogs: '登录日志',
|
||||
loginMode: '登录方式',
|
||||
authenticating: '密钥',
|
||||
publickey: '密钥',
|
||||
belong: '归属地',
|
||||
local: '内网',
|
||||
remote: '外网',
|
||||
config: '配置',
|
||||
session: '会话',
|
||||
loginTime: '登录时间',
|
||||
loginIP: '登录IP',
|
||||
disconnect: '断开',
|
||||
stopSSHWarn: '是否断开此SSH连接',
|
||||
},
|
||||
setting: {
|
||||
all: '全部',
|
||||
|
@ -101,6 +101,16 @@ const hostRouter = {
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosts/ssh/session',
|
||||
name: 'SSHSession',
|
||||
component: () => import('@/views/host/ssh/session/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/hosts/ssh/ssh',
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -62,18 +62,20 @@
|
||||
ref="tableRef"
|
||||
v-loading="data.length === 0"
|
||||
>
|
||||
<el-table-column :label="'PID'" fix prop="PID" max-width="60px" sortable>
|
||||
<template #default="{ row }">
|
||||
<el-link @click="openDetail(row)">{{ row.PID }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="'PID'" fix prop="PID" max-width="60px" sortable></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.name')"
|
||||
fix
|
||||
prop="name"
|
||||
min-width="120px"
|
||||
></el-table-column>
|
||||
<el-table-column :label="$t('process.ppid')" fix prop="PPID" sortable></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('process.ppid')"
|
||||
min-width="120px"
|
||||
fix
|
||||
prop="PPID"
|
||||
sortable
|
||||
></el-table-column>
|
||||
<el-table-column :label="$t('process.numThreads')" fix prop="numThreads"></el-table-column>
|
||||
<el-table-column :label="$t('commons.table.user')" fix prop="username"></el-table-column>
|
||||
<el-table-column
|
||||
@ -116,7 +118,7 @@
|
||||
:label="$t('process.startTime')"
|
||||
fix
|
||||
prop="startTime"
|
||||
min-width="120px"
|
||||
min-width="140px"
|
||||
></el-table-column>
|
||||
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
|
@ -13,12 +13,16 @@ import RouterButton from '@/components/router-button/index.vue';
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('menu.ssh'),
|
||||
label: i18n.global.t('menu.config'),
|
||||
path: '/hosts/ssh/ssh',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('ssh.loginLogs'),
|
||||
path: '/hosts/ssh/log',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('ssh.session'),
|
||||
path: '/hosts/ssh/session',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
143
frontend/src/views/host/ssh/session/index.vue
Normal file
143
frontend/src/views/host/ssh/session/index.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div>
|
||||
<FireRouter />
|
||||
<LayoutContent :title="$t('ssh.session')">
|
||||
<template #toolbar>
|
||||
<div style="width: 100%">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"></el-col>
|
||||
<el-col :span="8"></el-col>
|
||||
<el-col :span="8">
|
||||
<div class="search-button">
|
||||
<el-input
|
||||
v-model.trim="sshSearch.loginUser"
|
||||
clearable
|
||||
@clear="search()"
|
||||
suffix-icon="Search"
|
||||
@keyup.enter="search()"
|
||||
@change="search()"
|
||||
:placeholder="$t('commons.table.user')"
|
||||
></el-input>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :data="data" ref="tableRef" v-loading="loading">
|
||||
<el-table-column :label="$t('commons.table.user')" fix prop="PID"></el-table-column>
|
||||
<el-table-column :label="$t('commons.table.user')" fix prop="username"></el-table-column>
|
||||
<el-table-column :label="'PTS'" fix prop="terminal"></el-table-column>
|
||||
<el-table-column :label="$t('ssh.loginIP')" fix prop="host"></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('ssh.loginTime')"
|
||||
fix
|
||||
prop="loginTime"
|
||||
min-width="120px"
|
||||
></el-table-column>
|
||||
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FireRouter from '@/views/host/ssh/index.vue';
|
||||
import { ref, onMounted, onUnmounted, reactive } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { StopProcess } from '@/api/modules/process';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
|
||||
const sshSearch = reactive({
|
||||
type: 'ssh',
|
||||
loginUser: '',
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('ssh.disconnect'),
|
||||
click: function (row: any) {
|
||||
stopProcess(row.PID);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let processSocket = ref(null) as unknown as WebSocket;
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const tableRef = ref();
|
||||
|
||||
const isWsOpen = () => {
|
||||
const readyState = processSocket && processSocket.readyState;
|
||||
return readyState === 1;
|
||||
};
|
||||
const closeSocket = () => {
|
||||
if (isWsOpen()) {
|
||||
processSocket && processSocket.close();
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenProcess = () => {};
|
||||
const onMessage = (message: any) => {
|
||||
let result: any[] = JSON.parse(message.data);
|
||||
data.value = result;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const onerror = () => {};
|
||||
const onClose = () => {
|
||||
closeSocket();
|
||||
};
|
||||
|
||||
const initProcess = () => {
|
||||
let href = window.location.href;
|
||||
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
||||
let ipLocal = href.split('//')[1].split('/')[0];
|
||||
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v1/process/ws`);
|
||||
processSocket.onopen = onOpenProcess;
|
||||
processSocket.onmessage = onMessage;
|
||||
processSocket.onerror = onerror;
|
||||
processSocket.onclose = onClose;
|
||||
search();
|
||||
sendMsg();
|
||||
};
|
||||
|
||||
const sendMsg = () => {
|
||||
loading.value = true;
|
||||
setInterval(() => {
|
||||
search();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const search = () => {
|
||||
if (isWsOpen()) {
|
||||
processSocket.send(JSON.stringify(sshSearch));
|
||||
}
|
||||
};
|
||||
|
||||
const stopProcess = async (PID: number) => {
|
||||
ElMessageBox.confirm(i18n.global.t('ssh.stopSSHWarn'), i18n.global.t('ssh.disconnect'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await StopProcess({ PID: PID });
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
} catch (error) {
|
||||
MsgError(error);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initProcess();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
closeSocket();
|
||||
});
|
||||
</script>
|
@ -44,7 +44,7 @@
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<LayoutContent style="margin-top: 20px" :title="$t('menu.ssh')" :divider="true">
|
||||
<LayoutContent style="margin-top: 20px" :title="$t('menu.config')" :divider="true">
|
||||
<template #main>
|
||||
<el-radio-group v-model="confShowType" @change="changeMode">
|
||||
<el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button>
|
||||
|
Loading…
x
Reference in New Issue
Block a user