mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
feat: retrieve listening processes in firewall port page (#11665)
* feat: Add endpoint to retrieve listening processes and update related services * fix: Update listening process retrieval logic to include socket type checks * refactor: accept context for improved request handling
This commit is contained in:
@@ -57,3 +57,18 @@ func (b *BaseApi) GetProcessInfoByPID(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Process
|
||||
// @Summary Get Listening Process
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /process/listening [post]
|
||||
func (b *BaseApi) GetListeningProcess(c *gin.Context) {
|
||||
procs, err := processService.GetListeningProcess(c)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, procs)
|
||||
}
|
||||
|
||||
@@ -720,16 +720,9 @@ func checkPortUsed(ports, proto string, apps []portOfApp) string {
|
||||
|
||||
for _, app := range apps {
|
||||
if app.HttpPort == ports || app.HttpsPort == ports {
|
||||
return fmt.Sprintf("(%s)", app.AppName)
|
||||
return app.AppName
|
||||
}
|
||||
}
|
||||
port, err := strconv.Atoi(ports)
|
||||
if err != nil {
|
||||
global.LOG.Errorf(" convert string %v to int failed, err: %v", port, err)
|
||||
return ""
|
||||
}
|
||||
if common.ScanPortWithProto(port, proto) {
|
||||
return "inUsed"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -2,15 +2,19 @@ package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
type ProcessService struct{}
|
||||
@@ -18,6 +22,7 @@ type ProcessService struct{}
|
||||
type IProcessService interface {
|
||||
StopProcess(req request.ProcessReq) error
|
||||
GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error)
|
||||
GetListeningProcess(c context.Context) ([]ListeningProcess, error)
|
||||
}
|
||||
|
||||
func NewIProcessService() IProcessService {
|
||||
@@ -35,6 +40,55 @@ func (ps *ProcessService) StopProcess(req request.ProcessReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListeningProcess struct {
|
||||
PID int32
|
||||
Port map[uint32]struct{}
|
||||
Protocol uint32
|
||||
Name string
|
||||
}
|
||||
|
||||
func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningProcess, error) {
|
||||
conn, err := net.ConnectionsMaxWithContext(c, "inet", 32768)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
procCache := make(map[int32]ListeningProcess, 64)
|
||||
|
||||
for _, conn := range conn {
|
||||
if conn.Pid == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if (conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM) || (conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0) {
|
||||
if _, exists := procCache[conn.Pid]; !exists {
|
||||
proc, err := process.NewProcess(conn.Pid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
procData := ListeningProcess{
|
||||
PID: conn.Pid,
|
||||
}
|
||||
procData.Name, _ = proc.Name()
|
||||
procData.Port = make(map[uint32]struct{})
|
||||
procData.Port[conn.Laddr.Port] = struct{}{}
|
||||
procData.Protocol = conn.Type
|
||||
procCache[conn.Pid] = procData
|
||||
} else {
|
||||
p := procCache[conn.Pid]
|
||||
p.Port[conn.Laddr.Port] = struct{}{}
|
||||
procCache[conn.Pid] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
procs := make([]ListeningProcess, 0, len(procCache))
|
||||
for _, proc := range procCache {
|
||||
procs = append(procs, proc)
|
||||
}
|
||||
|
||||
return procs, nil
|
||||
}
|
||||
|
||||
func (ps *ProcessService) GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error) {
|
||||
p, err := process.NewProcess(pid)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ func (f *ProcessRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
{
|
||||
processRouter.GET("/ws", baseApi.ProcessWs)
|
||||
processRouter.POST("/stop", baseApi.StopProcess)
|
||||
processRouter.POST("/listening", baseApi.GetListeningProcess)
|
||||
processRouter.GET("/:pid", baseApi.GetProcessInfoByPID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,11 @@ export namespace Process {
|
||||
path: string;
|
||||
fd: number;
|
||||
}
|
||||
|
||||
export interface ListeningProcess {
|
||||
PID: number;
|
||||
Port: { [key: string]: {} };
|
||||
Protocol: number;
|
||||
Name: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@ export const stopProcess = (req: Process.StopReq) => {
|
||||
export const getProcessByID = (pid: number) => {
|
||||
return http.get<Process.PsProcessData>(`/process/${pid}`);
|
||||
};
|
||||
|
||||
export const getListeningProcess = () => {
|
||||
return http.post<Process.ListeningProcess[]>(`/process/listening`);
|
||||
};
|
||||
|
||||
@@ -88,28 +88,20 @@
|
||||
<el-table-column :label="$t('commons.table.port')" :min-width="70" prop="port" />
|
||||
<el-table-column :label="$t('commons.table.status')" :min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.port.indexOf('-') !== -1 && row.usedStatus">
|
||||
<el-tag type="info" class="mt-1">
|
||||
{{ $t('firewall.used') + ' * ' + row.usedPorts.length }}
|
||||
<div v-if="isSinglePort(row.port)">
|
||||
<el-tag type="success" v-if="row.usedStatus">
|
||||
{{ $t('firewall.used') + ' (' + row.usedStatus + ')' }}
|
||||
<el-icon
|
||||
v-if="row.processInfo"
|
||||
@click="showProcessDetail(row.processInfo.PID)"
|
||||
style="margin-left: 4px; cursor: pointer; vertical-align: middle"
|
||||
>
|
||||
<Expand />
|
||||
</el-icon>
|
||||
</el-tag>
|
||||
<el-popover placement="right" popper-class="limit-height-popover" :width="250">
|
||||
<template #default>
|
||||
<ul v-for="(item, index) in row.usedPorts" :key="index">
|
||||
<li>{{ item }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template #reference>
|
||||
<svg-icon iconName="p-xiangqing" class="svg-icon"></svg-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="info" v-if="row.usedStatus">
|
||||
<span v-if="row.usedStatus === 'inUsed'">{{ $t('firewall.used') }}</span>
|
||||
<span v-else>{{ $t('firewall.used') + ' ' + row.usedStatus }}</span>
|
||||
</el-tag>
|
||||
<el-tag type="success" v-else>{{ $t('firewall.unUsed') }}</el-tag>
|
||||
<el-tag type="info" v-else>{{ $t('firewall.unUsed') }}</el-tag>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :min-width="80" :label="$t('firewall.strategy')" prop="strategy">
|
||||
@@ -163,6 +155,7 @@
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<ImportDialog @search="search" ref="dialogImportRef" />
|
||||
<ProcessDetail ref="processDetailRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -171,12 +164,16 @@ import FireRouter from '@/views/host/firewall/index.vue';
|
||||
import OperateDialog from '@/views/host/firewall/port/operate/index.vue';
|
||||
import ImportDialog from '@/views/host/firewall/port/import/index.vue';
|
||||
import FireStatus from '@/views/host/firewall/status/index.vue';
|
||||
import ProcessDetail from '@/views/host/process/process/detail/index.vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { batchOperateRule, searchFireRule, updateFirewallDescription, updatePortRule } from '@/api/modules/host';
|
||||
import { getListeningProcess } from '@/api/modules/process';
|
||||
import { Host } from '@/api/interface/host';
|
||||
import { Process } from '@/api/interface/process';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { Expand } from '@element-plus/icons-vue';
|
||||
import { routerToName } from '@/utils/router';
|
||||
import { downloadWithContent, getCurrentDateFormatted } from '@/utils/util';
|
||||
|
||||
@@ -195,6 +192,9 @@ const fireStatusRef = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const dialogImportRef = ref();
|
||||
const processDetailRef = ref();
|
||||
|
||||
const listeningProcesses = ref<Process.ListeningProcess[]>([]);
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
@@ -204,6 +204,46 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const extractPortsFromObject = (portObj: { [key: string]: {} }): number[] => {
|
||||
return Object.keys(portObj)
|
||||
.map((portStr) => parseInt(portStr))
|
||||
.filter((port) => !isNaN(port));
|
||||
};
|
||||
|
||||
const isSinglePort = (portStr: string): boolean => {
|
||||
return portStr.indexOf('-') === -1 && portStr.indexOf(':') === -1 && portStr.indexOf(',') === -1;
|
||||
};
|
||||
|
||||
const loadListeningProcesses = async () => {
|
||||
try {
|
||||
const res = await getListeningProcess();
|
||||
listeningProcesses.value = res.data || [];
|
||||
|
||||
for (const item of data.value) {
|
||||
if (!item.usedStatus && isSinglePort(item.port)) {
|
||||
const portNum = parseInt(item.port.trim());
|
||||
if (!isNaN(portNum)) {
|
||||
const protocolNum =
|
||||
item.protocol.toLowerCase() === 'tcp' ? 1 : item.protocol.toLowerCase() === 'udp' ? 2 : 0;
|
||||
|
||||
for (const proc of listeningProcesses.value) {
|
||||
if (proc.Protocol === protocolNum) {
|
||||
const procPorts = extractPortsFromObject(proc.Port);
|
||||
if (procPorts.includes(portNum)) {
|
||||
item.usedStatus = proc.Name;
|
||||
item.processInfo = proc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load listening processes:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
if (!isActive.value) {
|
||||
loading.value = false;
|
||||
@@ -221,12 +261,12 @@ const search = async () => {
|
||||
};
|
||||
loading.value = true;
|
||||
await searchFireRule(params)
|
||||
.then((res) => {
|
||||
.then(async (res) => {
|
||||
loading.value = false;
|
||||
data.value = res.data.items || [];
|
||||
for (const item of data.value) {
|
||||
item.usedPorts = item.usedStatus ? item.usedStatus.split(',') : [];
|
||||
}
|
||||
|
||||
await loadListeningProcesses();
|
||||
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -381,6 +421,10 @@ const onExport = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showProcessDetail = (pid: number) => {
|
||||
processDetailRef.value?.acceptParams(pid);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
|
||||
Reference in New Issue
Block a user