1
0
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:
zhengkunwang223 2023-06-30 17:14:13 +08:00 committed by GitHub
parent 4bf76aacb1
commit 152ba81c34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 263 additions and 12 deletions

View File

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

View File

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

View File

@ -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',

View File

@ -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: '全部',

View File

@ -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,
},
},
],
};

View File

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

View File

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

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

View File

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