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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
@ -61,8 +62,17 @@ func TestPs(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Println(cmdLine)
|
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",
|
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))
|
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"
|
"fmt"
|
||||||
"github.com/1Panel-dev/1Panel/backend/global"
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
"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/net"
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
"strings"
|
"strings"
|
||||||
@ -15,6 +16,7 @@ type WsInput struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
DownloadProgress
|
DownloadProgress
|
||||||
PsProcessConfig
|
PsProcessConfig
|
||||||
|
SSHSessionConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadProgress struct {
|
type DownloadProgress struct {
|
||||||
@ -27,6 +29,11 @@ type PsProcessConfig struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SSHSessionConfig struct {
|
||||||
|
LoginUser string `json:"loginUser"`
|
||||||
|
LoginIP string `json:"loginIP"`
|
||||||
|
}
|
||||||
|
|
||||||
type PsProcessData struct {
|
type PsProcessData struct {
|
||||||
PID int32 `json:"PID"`
|
PID int32 `json:"PID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -64,6 +71,15 @@ type processConnect struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Laddr net.Addr `json:"localaddr"`
|
Laddr net.Addr `json:"localaddr"`
|
||||||
Raddr net.Addr `json:"remoteaddr"`
|
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) {
|
func ProcessData(c *Client, inputMsg []byte) {
|
||||||
@ -86,6 +102,12 @@ func ProcessData(c *Client, inputMsg []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Msg <- res
|
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)
|
res, err = json.Marshal(result)
|
||||||
return
|
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',
|
publickey: 'Key',
|
||||||
belong: 'Belong',
|
belong: 'Belong',
|
||||||
local: 'Local',
|
local: 'Local',
|
||||||
remote: 'Remote',
|
config: 'Configuration',
|
||||||
|
session: 'Session',
|
||||||
|
loginTime: 'Login Time',
|
||||||
|
loginIP: 'Login IP',
|
||||||
|
disconnect: 'Disconnect',
|
||||||
|
stopSSHWarn: 'Whether to disconnect this SSH connection',
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
all: 'All',
|
all: 'All',
|
||||||
|
@ -237,7 +237,7 @@ const message = {
|
|||||||
website: '网站',
|
website: '网站',
|
||||||
project: '项目',
|
project: '项目',
|
||||||
config: '配置',
|
config: '配置',
|
||||||
ssh: 'SSH 配置',
|
ssh: 'SSH 管理',
|
||||||
firewall: '防火墙',
|
firewall: '防火墙',
|
||||||
ssl: '证书',
|
ssl: '证书',
|
||||||
database: '数据库',
|
database: '数据库',
|
||||||
@ -873,13 +873,19 @@ const message = {
|
|||||||
keyAuthHelper: '是否启用密钥认证,默认启用。',
|
keyAuthHelper: '是否启用密钥认证,默认启用。',
|
||||||
useDNS: '反向解析',
|
useDNS: '反向解析',
|
||||||
dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能,从而验证连接方的身份。',
|
dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能,从而验证连接方的身份。',
|
||||||
loginLogs: 'SSH 登录日志',
|
loginLogs: '登录日志',
|
||||||
loginMode: '登录方式',
|
loginMode: '登录方式',
|
||||||
authenticating: '密钥',
|
authenticating: '密钥',
|
||||||
publickey: '密钥',
|
publickey: '密钥',
|
||||||
belong: '归属地',
|
belong: '归属地',
|
||||||
local: '内网',
|
local: '内网',
|
||||||
remote: '外网',
|
remote: '外网',
|
||||||
|
config: '配置',
|
||||||
|
session: '会话',
|
||||||
|
loginTime: '登录时间',
|
||||||
|
loginIP: '登录IP',
|
||||||
|
disconnect: '断开',
|
||||||
|
stopSSHWarn: '是否断开此SSH连接',
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
all: '全部',
|
all: '全部',
|
||||||
|
@ -101,6 +101,16 @@ const hostRouter = {
|
|||||||
requiresAuth: false,
|
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"
|
ref="tableRef"
|
||||||
v-loading="data.length === 0"
|
v-loading="data.length === 0"
|
||||||
>
|
>
|
||||||
<el-table-column :label="'PID'" fix prop="PID" max-width="60px" sortable>
|
<el-table-column :label="'PID'" fix prop="PID" max-width="60px" sortable></el-table-column>
|
||||||
<template #default="{ row }">
|
|
||||||
<el-link @click="openDetail(row)">{{ row.PID }}</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('commons.table.name')"
|
:label="$t('commons.table.name')"
|
||||||
fix
|
fix
|
||||||
prop="name"
|
prop="name"
|
||||||
min-width="120px"
|
min-width="120px"
|
||||||
></el-table-column>
|
></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('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 :label="$t('commons.table.user')" fix prop="username"></el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@ -116,7 +118,7 @@
|
|||||||
:label="$t('process.startTime')"
|
:label="$t('process.startTime')"
|
||||||
fix
|
fix
|
||||||
prop="startTime"
|
prop="startTime"
|
||||||
min-width="120px"
|
min-width="140px"
|
||||||
></el-table-column>
|
></el-table-column>
|
||||||
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||||
</ComplexTable>
|
</ComplexTable>
|
||||||
|
@ -13,12 +13,16 @@ import RouterButton from '@/components/router-button/index.vue';
|
|||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
label: i18n.global.t('menu.ssh'),
|
label: i18n.global.t('menu.config'),
|
||||||
path: '/hosts/ssh/ssh',
|
path: '/hosts/ssh/ssh',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('ssh.loginLogs'),
|
label: i18n.global.t('ssh.loginLogs'),
|
||||||
path: '/hosts/ssh/log',
|
path: '/hosts/ssh/log',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('ssh.session'),
|
||||||
|
path: '/hosts/ssh/session',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</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>
|
</el-card>
|
||||||
</div>
|
</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>
|
<template #main>
|
||||||
<el-radio-group v-model="confShowType" @change="changeMode">
|
<el-radio-group v-model="confShowType" @change="changeMode">
|
||||||
<el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button>
|
<el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user