feat: 信令抓包tshark解析pcap
This commit is contained in:
496
src/views/traceManage/tshark/index.vue
Normal file
496
src/views/traceManage/tshark/index.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, toRaw, ref } from 'vue';
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
import DissectionTree from './components/DissectionTree.vue';
|
||||
import DissectionDump from './components/DissectionDump.vue';
|
||||
import PacketTable from './components/PacketTable.vue';
|
||||
|
||||
import { scriptUrl } from '@/assets/js/wiregasm_worker';
|
||||
import { WK, OptionsType } from '@/plugins/wk-worker';
|
||||
import { parseSizeFromFile } from '@/utils/parse-utils';
|
||||
const wk = new WK();
|
||||
|
||||
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
|
||||
|
||||
type StateType = {
|
||||
/**初始化 */
|
||||
initialized: boolean;
|
||||
/**pcap信息 */
|
||||
summary: {
|
||||
filename: string;
|
||||
file_type: string;
|
||||
file_length: number;
|
||||
file_encap_type: string;
|
||||
packet_count: number;
|
||||
start_time: number;
|
||||
stop_time: number;
|
||||
elapsed_time: number;
|
||||
};
|
||||
/**字段 */
|
||||
columns: string[];
|
||||
/**pcap包帧数,匹配帧数 */
|
||||
totalFrames: number;
|
||||
/**pcap包帧数据 */
|
||||
packetFrames: any[];
|
||||
/**加载帧数 */
|
||||
nextPageSize: number;
|
||||
/**加载下一页 */
|
||||
nextPageLoad: boolean;
|
||||
/**未知属性 */
|
||||
// [key: string]: any;
|
||||
};
|
||||
|
||||
const state = reactive<StateType>({
|
||||
initialized: false,
|
||||
summary: {
|
||||
filename: '',
|
||||
file_type: 'Wireshark/tcpdump/... - pcap',
|
||||
file_length: 0,
|
||||
file_encap_type: 'Ethernet',
|
||||
packet_count: 0,
|
||||
start_time: 0,
|
||||
stop_time: 0,
|
||||
elapsed_time: 0,
|
||||
},
|
||||
columns: [],
|
||||
|
||||
/**过滤条件 */
|
||||
filter: '',
|
||||
filterError: null,
|
||||
currentFilter: '',
|
||||
/**当前选中的帧编号 */
|
||||
selectedFrame: 1,
|
||||
/**当前选中的帧数据 */
|
||||
selectedPacket: { tree: [], data_sources: [] },
|
||||
packetFrameData: new Map(), // 注意:Map 需要额外处理
|
||||
selectedTreeEntry: NO_SELECTION, // NO_SELECTION 需要定义
|
||||
/**选择帧的Dump数据标签 */
|
||||
selectedDataSourceIndex: 0,
|
||||
/**处理完成状态 */
|
||||
finishedProcessing: false,
|
||||
|
||||
totalFrames: 0,
|
||||
packetFrames: [],
|
||||
nextPageNum: 1,
|
||||
nextPageSize: 40,
|
||||
nextPageLoad: false,
|
||||
});
|
||||
|
||||
// 清除帧数据和报文信息状态
|
||||
function fnStateReset() {
|
||||
// 加载pcap包的数据
|
||||
state.nextPageNum = 1;
|
||||
// 选择帧的数据
|
||||
state.selectedFrame = 0;
|
||||
state.selectedPacket = { tree: [], data_sources: [] };
|
||||
state.packetFrameData = new Map();
|
||||
state.selectedTreeEntry = NO_SELECTION;
|
||||
state.selectedDataSourceIndex = 0;
|
||||
}
|
||||
|
||||
/**解析帧数据为简单结构 */
|
||||
function parseFrameData(id: string, node: Record<string, any>) {
|
||||
let map = new Map();
|
||||
|
||||
if (node.tree && node.tree.length > 0) {
|
||||
for (let i = 0; i < node.tree.length; i++) {
|
||||
const subMap = parseFrameData(`${id}-${i}`, node.tree[i]);
|
||||
subMap.forEach((value, key) => {
|
||||
map.set(key, value);
|
||||
});
|
||||
}
|
||||
} else if (node.length > 0) {
|
||||
map.set(id, {
|
||||
id: id,
|
||||
idx: node.data_source_idx,
|
||||
start: node.start,
|
||||
length: node.length,
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**帧数据点击选中 */
|
||||
function fnSelectedTreeEntry(e: any) {
|
||||
console.log('fnSelectedTreeEntry', e);
|
||||
state.selectedTreeEntry = e;
|
||||
}
|
||||
/**报文数据点击选中 */
|
||||
function fnSelectedFindSelection(src_idx: number, pos: number) {
|
||||
console.log('fnSelectedFindSelection', pos);
|
||||
// find the smallest one
|
||||
let current = null;
|
||||
|
||||
for (let [k, pp] of state.packetFrameData) {
|
||||
if (pp.idx !== src_idx) continue;
|
||||
|
||||
if (pos >= pp.start && pos <= pp.start + pp.length) {
|
||||
if (
|
||||
current != null &&
|
||||
state.packetFrameData.get(current).length > pp.length
|
||||
) {
|
||||
current = k;
|
||||
} else {
|
||||
current = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
state.selectedTreeEntry = state.packetFrameData.get(current);
|
||||
}
|
||||
}
|
||||
/**包数据表点击选中 */
|
||||
function fnSelectedFrame(no: number) {
|
||||
console.log('fnSelectedFrame', no, state.totalFrames);
|
||||
state.selectedFrame = no;
|
||||
wk.send({ type: 'select', number: state.selectedFrame });
|
||||
}
|
||||
/**包数据表滚动底部加载 */
|
||||
function fnScrollBottom() {
|
||||
const totalFetched = state.packetFrames.length;
|
||||
console.log('fnScrollBottom', totalFetched);
|
||||
if (!state.nextPageLoad && totalFetched < state.totalFrames) {
|
||||
state.nextPageLoad = true;
|
||||
state.nextPageNum++;
|
||||
fnLoaldFrames(state.filter, state.nextPageNum);
|
||||
}
|
||||
}
|
||||
/**包数据表过滤 */
|
||||
function fnFilterFrames() {
|
||||
console.log('fnFilterFinish', state.filter);
|
||||
wk.send({ type: 'check-filter', filter: state.filter });
|
||||
}
|
||||
/**包数据表记载 */
|
||||
function fnLoaldFrames(filter: string, page: number = 1) {
|
||||
if (!(state.initialized && state.finishedProcessing)) return;
|
||||
const limit = state.nextPageSize;
|
||||
wk.send({
|
||||
type: 'frames',
|
||||
filter: filter,
|
||||
skip: (page - 1) * limit,
|
||||
limit: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**本地示例文件 */
|
||||
async function fnLoadExample() {
|
||||
const name = 'test_ethernet.pcap';
|
||||
const res = await fetch('/wiregasm/test_ethernet.pcap');
|
||||
const body = await res.arrayBuffer();
|
||||
|
||||
state.summary = {
|
||||
filename: '',
|
||||
file_type: 'Wireshark/tcpdump/... - pcap',
|
||||
file_length: 0,
|
||||
file_encap_type: 'Ethernet',
|
||||
packet_count: 0,
|
||||
start_time: 0,
|
||||
stop_time: 0,
|
||||
elapsed_time: 0,
|
||||
};
|
||||
state.finishedProcessing = false;
|
||||
|
||||
wk.send({ type: 'process-data', name: name, data: body });
|
||||
}
|
||||
|
||||
/**上传前检查或转换压缩 */
|
||||
function fnBeforeUpload(file: FileType) {
|
||||
const fileName = file.name;
|
||||
const suff = fileName.substring(fileName.lastIndexOf('.'));
|
||||
const allowList = ['.pcap', '.cap', '.pcapng', '.pcap0'];
|
||||
if (!allowList.includes(suff)) {
|
||||
const msg = `${t('components.UploadModal.onlyAllow')} ${allowList.join(
|
||||
','
|
||||
)}`;
|
||||
message.error(msg, 3);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**表单上传文件 */
|
||||
function fnUpload(up: UploadRequestOption) {
|
||||
state.summary = {
|
||||
filename: '',
|
||||
file_type: 'Wireshark/tcpdump/... - pcap',
|
||||
file_length: 0,
|
||||
file_encap_type: 'Ethernet',
|
||||
packet_count: 0,
|
||||
start_time: 0,
|
||||
stop_time: 0,
|
||||
elapsed_time: 0,
|
||||
};
|
||||
state.finishedProcessing = false;
|
||||
|
||||
wk.send({ type: 'process', file: up.file });
|
||||
}
|
||||
|
||||
/**接收数据后回调 */
|
||||
function wkMessage(res: Record<string, any>) {
|
||||
switch (res.type) {
|
||||
case 'status':
|
||||
console.info(res.status);
|
||||
break;
|
||||
case 'error':
|
||||
console.warn(res.error);
|
||||
break;
|
||||
case 'init':
|
||||
wk.send({ type: 'columns' });
|
||||
state.initialized = true;
|
||||
break;
|
||||
case 'columns':
|
||||
state.columns = res.data;
|
||||
break;
|
||||
case 'frames':
|
||||
// console.log(res.data);
|
||||
const { matched, frames } = res.data;
|
||||
state.totalFrames = matched;
|
||||
|
||||
if (state.nextPageNum == 1) {
|
||||
state.packetFrames = frames;
|
||||
// 有匹配的选择第一个
|
||||
if (frames.length > 0) {
|
||||
state.selectedFrame = frames[0].number;
|
||||
fnSelectedFrame(state.selectedFrame);
|
||||
}
|
||||
} else {
|
||||
state.packetFrames = state.packetFrames.concat(frames);
|
||||
state.nextPageLoad = false;
|
||||
}
|
||||
break;
|
||||
case 'selected':
|
||||
state.selectedPacket = res.data;
|
||||
state.packetFrameData = parseFrameData('root', res.data);
|
||||
state.selectedTreeEntry = NO_SELECTION;
|
||||
state.selectedDataSourceIndex = 0;
|
||||
break;
|
||||
case 'processed':
|
||||
// setStatus(`Error: non-zero return code (${e.data.code})`);
|
||||
state.finishedProcessing = true;
|
||||
if (res.data.code === 0) {
|
||||
state.summary = res.data.summary;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
fnStateReset();
|
||||
fnLoaldFrames(state.filter);
|
||||
break;
|
||||
case 'filter':
|
||||
const filterRes = res.data;
|
||||
if (filterRes.ok) {
|
||||
state.currentFilter = state.filter;
|
||||
state.filterError = null;
|
||||
// 加载数据
|
||||
fnStateReset();
|
||||
fnLoaldFrames(state.filter);
|
||||
} else {
|
||||
state.filterError = filterRes.error;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log(res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 建立链接
|
||||
const options: OptionsType = {
|
||||
url: scriptUrl,
|
||||
onmessage: wkMessage,
|
||||
onerror: (ev: any) => {
|
||||
console.error(ev);
|
||||
},
|
||||
};
|
||||
wk.connect(options);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:loading="!state.initialized"
|
||||
:body-style="{ padding: '12px' }"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<a-space :size="8" class="toolbar-oper">
|
||||
<a-upload
|
||||
name="file"
|
||||
list-type="picture"
|
||||
:max-count="1"
|
||||
accept=".pcap,.cap,.pcapng,.pcap0"
|
||||
:show-upload-list="false"
|
||||
:before-upload="fnBeforeUpload"
|
||||
:custom-request="fnUpload"
|
||||
>
|
||||
<a-button type="primary"> Upload </a-button>
|
||||
</a-upload>
|
||||
<a-button @click="fnLoadExample">Example</a-button>
|
||||
</a-space>
|
||||
|
||||
<div class="toolbar-info">
|
||||
<a-tag color="green" v-show="!!state.currentFilter">
|
||||
{{ state.currentFilter }}
|
||||
</a-tag>
|
||||
<span> Matched Frame: {{ state.totalFrames }} </span>
|
||||
</div>
|
||||
|
||||
<!-- 包信息 -->
|
||||
<a-popover
|
||||
trigger="click"
|
||||
placement="bottomLeft"
|
||||
v-if="state.summary.filename"
|
||||
>
|
||||
<template #content>
|
||||
<div class="summary">
|
||||
<div class="summary-item">
|
||||
<span>Type:</span>
|
||||
<span>{{ state.summary.file_type }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>Size:</span>
|
||||
<span>{{ parseSizeFromFile(state.summary.file_length) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>Encapsulation:</span>
|
||||
<span>{{ state.summary.file_encap_type }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>Packets:</span>
|
||||
<span>{{ state.summary.packet_count }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span>Duration:</span>
|
||||
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<InfoCircleOutlined />
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<!-- 包数据表过滤 -->
|
||||
<a-input-group compact>
|
||||
<a-input
|
||||
v-model:value="state.filter"
|
||||
placeholder="display filter, example: tcp"
|
||||
:allow-clear="true"
|
||||
style="width: calc(100% - 100px)"
|
||||
@pressEnter="fnFilterFrames"
|
||||
>
|
||||
<template #prefix>
|
||||
<FilterOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
style="width: 100px"
|
||||
@click="fnFilterFrames"
|
||||
>
|
||||
Filter
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
<a-alert
|
||||
:message="state.filterError"
|
||||
type="error"
|
||||
v-if="state.filterError != null"
|
||||
/>
|
||||
|
||||
<!-- 包数据表 -->
|
||||
<PacketTable
|
||||
:columns="state.columns"
|
||||
:data="state.packetFrames"
|
||||
:selectedFrame="state.selectedFrame"
|
||||
:onSelectedFrame="fnSelectedFrame"
|
||||
:onScrollBottom="fnScrollBottom"
|
||||
></PacketTable>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :lg="12" :md="12" :xs="24" class="tree">
|
||||
<!-- 帧数据 -->
|
||||
<DissectionTree
|
||||
id="root"
|
||||
:select="fnSelectedTreeEntry"
|
||||
:selected="state.selectedTreeEntry"
|
||||
:tree="state.selectedPacket.tree"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="12" :xs="24" class="dump">
|
||||
<!-- 报文数据 -->
|
||||
<a-tabs
|
||||
v-model:activeKey="state.selectedDataSourceIndex"
|
||||
:tab-bar-gutter="16"
|
||||
:tab-bar-style="{ marginBottom: '8px' }"
|
||||
>
|
||||
<a-tab-pane
|
||||
:key="idx"
|
||||
:tab="v.name"
|
||||
v-for="(v, idx) in state.selectedPacket.data_sources"
|
||||
style="overflow: auto"
|
||||
>
|
||||
<DissectionDump
|
||||
:base64="v.data"
|
||||
:select="(pos:number)=>fnSelectedFindSelection(idx, pos)"
|
||||
:selected="
|
||||
idx === state.selectedTreeEntry.idx
|
||||
? state.selectedTreeEntry
|
||||
: NO_SELECTION
|
||||
"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.toolbar-info {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.summary-item > span:first-child {
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.tree {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
white-space: nowrap;
|
||||
overflow-y: auto;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
}
|
||||
.tree > ul.tree {
|
||||
min-height: 15rem;
|
||||
}
|
||||
|
||||
.dump {
|
||||
padding-bottom: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
.dump .ant-tabs-tabpane {
|
||||
min-height: calc(15rem - 56px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user