feat: 信令抓包tshark解析pcap

This commit is contained in:
TsMask
2024-09-03 11:05:58 +08:00
parent 0080e9c26e
commit c1a3ce8068
13 changed files with 11261 additions and 0 deletions

Binary file not shown.

Binary file not shown.

9768
public/wiregasm/wiregasm.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,166 @@
/**
* Wraps the WiregasmLib lib functionality and manages a single DissectSession
*/
class Wiregasm {
constructor() {
this.initialized = false;
this.session = null;
}
/**
* Initialize the wrapper and the Wiregasm module
*
* @param loader Loader function for the Emscripten module
* @param overrides Overrides
*/
async init(loader, overrides = {}, beforeInit = null) {
if (this.initialized) {
return;
}
this.lib = await loader(overrides);
this.uploadDir = this.lib.getUploadDirectory();
this.pluginsDir = this.lib.getPluginsDirectory();
if (beforeInit !== null) {
await beforeInit(this.lib);
}
this.lib.init();
this.initialized = true;
}
list_modules() {
return this.lib.listModules();
}
list_prefs(module) {
return this.lib.listPreferences(module);
}
apply_prefs() {
this.lib.applyPreferences();
}
set_pref(module, key, value) {
const ret = this.lib.setPref(module, key, value);
if (ret.code != PrefSetResult.PREFS_SET_OK) {
const message =
ret.error != '' ? ret.error : preferenceSetCodeToError(ret.code);
throw new Error(
`Failed to set preference (${module}.${key}): ${message}`
);
}
}
get_pref(module, key) {
const response = this.lib.getPref(module, key);
if (response.code != 0) {
throw new Error(`Failed to get preference (${module}.${key})`);
}
return response.data;
}
/**
* Check the validity of a filter expression.
*
* @param filter A display filter expression
*/
test_filter(filter) {
return this.lib.checkFilter(filter);
}
complete_filter(filter) {
const out = this.lib.completeFilter(filter);
return {
err: out.err,
fields: vectorToArray(out.fields),
};
}
reload_lua_plugins() {
this.lib.reloadLuaPlugins();
}
add_plugin(name, data, opts = {}) {
const path = this.pluginsDir + '/' + name;
this.lib.FS.writeFile(path, data, opts);
}
/**
* Load a packet trace file for analysis.
*
* @returns Response containing the status and summary
*/
load(name, data, opts = {}) {
if (this.session != null) {
this.session.delete();
}
const path = this.uploadDir + '/' + name;
this.lib.FS.writeFile(path, data, opts);
this.session = new this.lib.DissectSession(path);
return this.session.load();
}
/**
* Get Packet List information for a range of packets.
*
* @param filter Output those frames that pass this filter expression
* @param skip Skip N frames
* @param limit Limit the output to N frames
*/
frames(filter, skip = 0, limit = 0) {
return this.session.getFrames(filter, skip, limit);
}
/**
* Get full information about a frame including the protocol tree.
*
* @param number Frame number
*/
frame(num) {
return this.session.getFrame(num);
}
follow(follow, filter) {
return this.session.follow(follow, filter);
}
destroy() {
if (this.initialized) {
if (this.session !== null) {
this.session.delete();
this.session = null;
}
this.lib.destroy();
this.initialized = false;
}
}
/**
* Returns the column headers
*/
columns() {
const vec = this.lib.getColumns();
// convert it from a vector to array
return vectorToArray(vec);
}
}
/**
* Converts a Vector to a JS array
*
* @param vec Vector
* @returns JS array of the Vector contents
*/
function vectorToArray(vec) {
return new Array(vec.size()).fill(0).map((_, id) => vec.get(id));
}
function preferenceSetCodeToError(code) {
switch (code) {
case PrefSetResult.PREFS_SET_SYNTAX_ERR:
return 'Syntax error in string';
case PrefSetResult.PREFS_SET_NO_SUCH_PREF:
return 'No such preference';
case PrefSetResult.PREFS_SET_OBSOLETE:
return 'Preference used to exist but no longer does';
default:
return 'Unknown error';
}
}
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = Wiregasm;
module.exports = vectorToArray;
} else if (typeof define === 'function' && define['amd']) {
define([], function () {
return Wiregasm;
});
define([], function () {
return vectorToArray;
});
} else if (typeof exports === 'object') {
exports['loadWiregasm'] = Wiregasm;
exports['vectorToArray'] = vectorToArray;
}

159
public/wiregasm/worker.js Normal file
View File

@@ -0,0 +1,159 @@
// load the Wiregasm library
importScripts(
'/wiregasm/wiregasm_new.js',
'/wiregasm/wiregasm.js'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.js'
);
const wg = new Wiregasm();
const inflateRemoteBuffer = async url => {
const res = await fetch(url);
return await res.arrayBuffer();
};
const fetchPackages = async () => {
console.log('Fetching packages');
let [wasmBuffer, dataBuffer] = await Promise.all([
await inflateRemoteBuffer(
'/wiregasm/wiregasm.wasm.gz'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.wasm.gz'
),
await inflateRemoteBuffer(
'/wiregasm/wiregasm.data.gz'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.data.gz'
),
]);
return { wasmBuffer, dataBuffer };
};
// Load the Wiregasm Wasm data
fetchPackages()
.then(({ wasmBuffer, dataBuffer }) => {
return wg.init(loadWiregasm, {
wasmBinary: wasmBuffer,
getPreloadedPackage() {
return dataBuffer;
},
handleStatus: (type, status) => {
postMessage({ type: 'status', code: type, status: status });
},
printErr: error => {
postMessage({ type: 'error', error: error });
},
});
})
.then(() => {
postMessage({ type: 'init' });
})
.catch(e => {
postMessage({ type: 'error', error: e });
});
/**Converts a Vector to a JS array */
function replacer(key, value) {
if (value.constructor.name.startsWith('Vector')) {
return vectorToArray(value);
}
return value;
}
// Event listener to receive messages from the main script
this.onmessage = ev => {
const data = ev.data;
switch (data.type) {
case 'columns':
const columns = wg.columns();
if (Array.isArray(columns)) {
this.postMessage({ type: 'columns', data: columns });
}
break;
case 'select': // select a frame
const number = data.number;
const frameData = wg.frame(number);
const frameDataToJSON = JSON.parse(JSON.stringify(frameData, replacer));
this.postMessage({
type: 'selected',
data: frameDataToJSON,
});
break;
case 'frames': // get frames list
const skip = data.skip;
const limit = data.limit;
const filter = data.filter;
const framesData = wg.frames(filter, skip, limit);
const framesDataToJSON = JSON.parse(JSON.stringify(framesData, replacer));
this.postMessage({
type: 'frames',
data: framesDataToJSON,
});
break;
case 'process-data':
const loadData = wg.load(data.name, new Uint8Array(data.data));
this.postMessage({ type: 'processed', data: loadData });
break;
case 'process':
const f = data.file;
const reader = new FileReader();
reader.addEventListener('load', event => {
// XXX: this blocks the worker thread
const loadData = wg.load(f.name, new Uint8Array(event.target.result));
postMessage({ type: 'processed', data: loadData });
});
reader.readAsArrayBuffer(f);
break;
case 'check-filter':
const filterStr = data.filter;
const checkFilterRes = wg.lib.checkFilter(filterStr);
this.postMessage({ type: 'filter', data: checkFilterRes });
break;
}
if (data.type === 'reload-quick') {
if (wg.session) {
// TODO: this is a hack, we should be able to reload the session
const name = data.name;
const res = wg.session.load();
postMessage({ type: 'processed', name: name, data: res });
}
} else if (data.type === 'module-tree') {
const res = wg.list_modules();
// send it to the correct port
event.ports[0].postMessage({
result: JSON.parse(JSON.stringify(res, replacer)),
});
} else if (data.type === 'module-prefs') {
const res = wg.list_prefs(data.name);
// send it to the correct port
event.ports[0].postMessage({
result: JSON.parse(JSON.stringify(res, replacer)),
});
} else if (data.type === 'upload-file') {
const f = data.file;
const reader = new FileReader();
reader.addEventListener('load', e => {
// XXX: this blocks the worker thread
const path = '/uploads/' + f.name;
wg.lib.FS.writeFile(path, Buffer.from(e.target.result));
event.ports[0].postMessage({ result: path });
});
reader.readAsArrayBuffer(f);
} else if (data.type === 'update-pref') {
try {
console.log(`set_pref(${data.module}, ${data.key}, ${data.value})`);
wg.set_pref(data.module, data.key, data.value);
event.ports[0].postMessage({ result: 'ok' });
} catch (e) {
console.error(
`set_pref(${data.module}, ${data.key}, ${data.value}) failed: ${e.message}`
);
event.ports[0].postMessage({ error: e.message });
}
} else if (data.type === 'apply-prefs') {
console.log(`apply_prefs()`);
wg.apply_prefs();
event.ports[0].postMessage({ result: 'ok' });
}
};

View File

@@ -0,0 +1,11 @@
/**
* worker文件-静态资源文件路径
*/
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
export const scriptUrl = `${
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
? ''
: baseUrl.indexOf('/') === -1
? '/' + baseUrl
: baseUrl
}/wiregasm/worker.js`;

View File

@@ -0,0 +1,141 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import DissectionDumpHigh from './DissectionDumpHigh.vue';
const props = defineProps({
base64: {
type: String,
required: true,
},
select: {
type: Function,
default: () => {},
},
selected: {
type: Object,
default: { id: '', idx: 0, start: 0, length: 0 },
},
});
const addrLines = ref<string[]>([]);
const hexLines = ref<string[]>([]);
const asciiLines = ref<string[]>([]);
const asciiHighlight = ref([0, 0]);
const hexHighlight = ref([0, 0]);
watch(
() => props.selected,
newSelected => {
const { start, length: size } = newSelected;
const hexSize = size * 2 + size - 1;
const hexPos = start * 2 + start;
const asciiPos = start + Math.floor(start / 16);
const asciiSize = start + size + Math.floor((start + size) / 16) - asciiPos;
asciiHighlight.value = [asciiPos, size > 0 ? asciiSize : 0];
hexHighlight.value = [hexPos, size > 0 ? hexSize : 0];
},
{ immediate: true }
);
watch(
() => props.base64,
base64Str => {
// Decode base64 to a string
const binaryString = atob(base64Str);
// Convert binary string to Uint8Array
const newBuffer = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
newBuffer[i] = binaryString.charCodeAt(i);
}
let addrLinesTemp: string[] = [];
let hexLinesTemp: string[] = [];
let asciiLinesTemp: string[] = [];
for (let i = 0; i < newBuffer.length; i += 16) {
let address = i.toString(16).padStart(8, '0');
let block = newBuffer.slice(i, i + 16);
let hexArray = [];
let asciiArray = [];
for (let value of block) {
hexArray.push(value.toString(16).padStart(2, '0'));
asciiArray.push(
value >= 0x20 && value < 0x7f ? String.fromCharCode(value) : '.'
);
}
let hexString =
hexArray.length > 8
? hexArray.slice(0, 8).join(' ') + ' ' + hexArray.slice(8).join(' ')
: hexArray.join(' ');
let asciiString = asciiArray.join('');
addrLinesTemp.push(address);
hexLinesTemp.push(hexString);
asciiLinesTemp.push(asciiString);
}
addrLines.value = addrLinesTemp;
hexLines.value = hexLinesTemp;
asciiLines.value = asciiLinesTemp;
},
{ immediate: true }
);
const onHexClick = (offset: number) => {
if (typeof props.select !== 'function') return;
props.select(Math.floor(offset / 3));
};
const onAsciiClick = (offset: number) => {
if (typeof props.select !== 'function') return;
props.select(offset - Math.floor(offset / 17));
};
</script>
<template>
<div class="tbd">
<div class="tbd-offset">
{{ addrLines.join('\n') }}
</div>
<div class="tbd-box">
<DissectionDumpHigh
:text="hexLines.join('\n')"
:start="hexHighlight[0]"
:size="hexHighlight[1]"
:onOffsetClicked="onHexClick"
/>
</div>
<div class="tbd-box">
<DissectionDumpHigh
:text="asciiLines.join('\n')"
:start="asciiHighlight[0]"
:size="asciiHighlight[1]"
:onOffsetClicked="onAsciiClick"
/>
</div>
</div>
</template>
<style lang="css" scoped>
.tbd {
display: flex;
white-space: pre;
word-break: break-all;
font-size: 0.8125rem;
line-height: 1.5rem;
}
.tbd-offset {
color: #6b7280;
user-select: none;
}
.tbd-box {
margin-left: 1rem;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
text: {
type: String,
required: true,
},
start: {
type: Number,
required: true,
},
size: {
type: Number,
required: true,
},
onOffsetClicked: {
type: Function,
required: true,
},
});
const before = computed(() => props.text.substring(0, props.start));
const hl = computed(() =>
props.text.substring(props.start, props.start + props.size)
);
const end = computed(() => props.text.substring(props.start + props.size));
const handleClick = (offset: number) => {
const selection = window.getSelection();
if (!selection) return;
props.onOffsetClicked(selection.anchorOffset + offset);
};
</script>
<template>
<div>
<span @click="handleClick(0)">{{ before }}</span>
<span @click="handleClick(before.length)" class="hl">
{{ hl }}
</span>
<span @click="handleClick(before.length + hl.length)">
{{ end }}
</span>
</div>
</template>
<style lang="css" scoped>
.hl {
color: #ffffff;
background-color: #4b5563;
}
</style>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import DissectionTreeSub from './DissectionTreeSub.vue';
defineProps({
id: {
type: String,
required: true,
},
tree: {
type: Array,
required: true,
},
sub: {
type: Boolean,
default: false,
},
select: {
type: Function,
default: () => {},
},
selected: {
type: Object,
default: { id: '', idx: 0, start: 0, length: 0 },
},
});
</script>
<template>
<ul :class="{ tree: true, 'tree-issub': sub }">
<li v-for="(n, i) in tree" :key="`${id}-${i}`" class="tree-li">
<DissectionTreeSub
:id="`${id}-${i}`"
:node="n"
:select="select"
:selected="selected"
/>
</li>
</ul>
</template>
<style lang="css" scoped>
.tree {
list-style: none;
margin: 0;
padding: 0;
border: 0 solid #e5e7eb;
box-sizing: border-box;
}
.tree-issub {
padding-left: 0.5rem;
border-left-width: 1px;
margin-left: 0.5rem;
}
.tree-li {
display: list-item;
text-align: -webkit-match-parent;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,116 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import DissectionTree from './DissectionTree.vue';
import {
CaretDownOutlined,
CaretRightOutlined,
MinusOutlined,
} from '@ant-design/icons-vue';
const props = defineProps({
id: {
type: String,
required: true,
},
node: {
type: Object,
required: true,
},
select: {
type: Function,
required: true,
},
selected: {
type: Object,
required: true,
},
});
const emit = defineEmits(['update:selected']);
const open = ref(false);
watch(
() => props.selected,
() => {
if (!open.value) {
open.value = props.selected.id.startsWith(props.id + '-');
}
},
{ immediate: true }
);
const toggle = () => {
if (open.value && props.selected.id.startsWith(props.id + '-')) {
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
emit('update:selected', NO_SELECTION);
if (typeof props.select === 'function') {
props.select(NO_SELECTION);
}
}
open.value = !open.value;
};
const handleClick = () => {
if (props.node.length > 0) {
const select = {
id: props.id,
idx: props.node.data_source_idx,
start: props.node.start,
length: props.node.length,
};
emit('update:selected', select);
if (typeof props.select === 'function') {
props.select(select);
}
}
};
</script>
<template>
<div :class="{ 'tree-sub': true, 'tree-sub_hl': id === selected.id }">
<component
:is="
node.tree && node.tree.length > 0
? open
? CaretDownOutlined
: CaretRightOutlined
: MinusOutlined
"
class="tree-sub_icon"
@click="toggle"
/>
<span @click="handleClick" @dblclick="toggle" class="tree-sub_text">
{{ node.label }}
</span>
</div>
<DissectionTree
v-if="node.tree && node.tree.length > 0 && open"
:id="id"
:tree="node.tree"
:select="select"
:selected="selected"
sub
/>
</template>
<style lang="css" scoped>
.tree-sub {
display: inline-flex;
width: 100%;
align-items: center;
cursor: pointer;
}
.tree-sub_hl {
color: #ffffff;
background-color: #4b5563;
}
.tree-sub_icon {
color: #6b7280;
width: 1rem;
height: 1rem;
}
.tree-sub_text {
width: 100%;
margin-left: 0.25rem;
}
</style>

View File

@@ -0,0 +1,293 @@
<script lang="ts" setup>
import { reactive, ref, computed, unref, onUpdated, watchEffect } from 'vue';
const props = defineProps({
/**列表高度 */
height: {
type: Number,
default: 300,
},
/**列表项高度 */
itemHeight: {
type: Number,
default: 30,
},
/**数据 */
data: {
type: Array,
default: () => [],
},
/**预先兜底缓存数量 */
cache: {
type: Number,
default: 2,
},
/**列 */
columns: {
type: Array,
default: () => [],
},
selectedFrame: {
type: Number,
default: 0,
},
onSelectedFrame: {
type: Function,
default: () => {},
},
onScrollBottom: {
type: Function,
default: () => {},
},
});
const state = reactive<any>({
start: 0,
end: 10,
scrollOffset: 0,
cacheData: [],
});
const virtualListRef = ref();
const getWrapperStyle = computed(() => {
const { height } = props;
return {
height: `${height}px`,
};
});
const getInnerStyle = computed(() => {
return {
height: `${unref(getTotalHeight)}px`,
width: '100%',
};
});
const getListStyle = computed(() => {
return {
willChange: 'transform',
transform: `translateY(${state.scrollOffset}px)`,
};
});
// 数据数量
const total = computed(() => {
return props.data.length;
});
// 总体高度
const getTotalHeight = computed(() => {
return unref(total) * props.itemHeight;
});
// 当前屏幕显示的数量
const clientCount = computed(() => {
return Math.ceil(props.height / props.itemHeight);
});
// 当前屏幕显示的数据
const clientData = computed<any[]>(() => {
return props.data.slice(state.start, state.end);
});
const onScroll = (e: any) => {
const { scrollHeight, scrollTop, clientHeight } = e.target;
if (state.scrollOffset === scrollTop) return;
const { cache, height, itemHeight } = props;
const cacheCount = Math.max(1, cache);
let startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.max(
0,
Math.min(unref(total), startIndex + unref(clientCount) + cacheCount)
);
if (startIndex > cacheCount) {
startIndex = startIndex - cacheCount;
}
// 偏移量
const offset = scrollTop - (scrollTop % itemHeight);
Object.assign(state, {
start: startIndex,
end: endIndex,
scrollOffset: offset,
});
// 底部小于高度时触发
if (scrollHeight - scrollTop - clientHeight < height) {
props.onScrollBottom(endIndex);
}
};
onUpdated(() => {});
watchEffect(() => {
clientData.value.forEach((_, index) => {
const currentIndex = state.start + index;
if (Object.hasOwn(state.cacheData, currentIndex)) return;
state.cacheData[currentIndex] = {
top: currentIndex * props.itemHeight,
height: props.itemHeight,
bottom: (currentIndex + 1) * props.itemHeight,
index: currentIndex,
};
});
});
const tableState = reactive({
selected: false,
});
</script>
<template>
<div class="table">
<div class="thead">
<div class="thead-item" v-for="v in columns">
{{ v }}
</div>
</div>
<div
class="virtual-list-wrapper"
ref="wrapperRef"
:style="getWrapperStyle"
@scroll="onScroll"
>
<div class="virtual-list-inner" ref="innerRef" :style="getInnerStyle">
<div class="virtual-list" :style="getListStyle" ref="virtualListRef">
<div
class="tbody"
v-for="(item, index) in clientData"
:key="index + state.start"
:style="{
height: itemHeight + 'px',
backgroundColor:
item.number === props.selectedFrame
? 'blue'
: item.bg
? `#${item.bg.toString(16).padStart(6, '0')}`
: '',
color:
item.number === props.selectedFrame
? 'white'
: item.fg
? `#${item.fg.toString(16).padStart(6, '0')}`
: '',
}"
@click="onSelectedFrame(item.number)"
>
<div class="tbody-item" v-for="col in item.columns">
{{ col }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.virtual-list-wrapper {
position: relative;
overflow-y: auto;
}
.table {
display: flex;
flex-direction: column;
height: 100%;
}
.thead {
display: flex;
flex-direction: row;
}
.thead-item {
white-space: nowrap;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
text-align: left;
font-size: 0.875rem;
line-height: 1.5rem;
font-weight: 600;
/* flex-basis: 100%; */
}
.tbody {
display: flex;
flex-direction: row;
align-items: center;
border-top: 1px #f0f0f0 solid;
cursor: pointer;
}
.tbody-item {
padding-left: 0.5rem;
padding-right: 0.5rem;
font-size: 0.875rem;
line-height: 1.5rem;
/* flex-basis: 100%; */
text-align: left;
}
.thead-item:nth-child(1),
.tbody-item:nth-child(1) {
flex-basis: 5rem;
width: 5rem;
}
.tbody-item:nth-child(1) {
text-align: right;
}
.thead-item:nth-child(2),
.tbody-item:nth-child(2) {
flex-basis: 8rem;
width: 8rem;
}
.thead-item:nth-child(3),
.tbody-item:nth-child(3) {
flex-basis: 8rem;
width: 8rem;
}
.thead-item:nth-child(4),
.tbody-item:nth-child(4) {
flex-basis: 8rem;
width: 8rem;
}
.thead-item:nth-child(5),
.tbody-item:nth-child(5) {
flex-basis: 6rem;
width: 6rem;
}
.thead-item:nth-child(6),
.tbody-item:nth-child(6) {
flex-basis: 6rem;
width: 6rem;
}
.tbody-item:nth-child(6) {
text-align: right;
}
.thead-item:nth-child(7),
.tbody-item:nth-child(7) {
text-align: left;
text-wrap: nowrap;
flex: 1;
width: 5rem;
overflow-y: auto;
}
/* 修改滚动条的样式 */
.tbody-item:nth-child(7)::-webkit-scrollbar {
width: 4px; /* 设置滚动条宽度 */
height: 4px;
}
.tbody-item:nth-child(7)::-webkit-scrollbar-track {
background-color: #f0f0f0; /* 设置滚动条轨道背景颜色 */
}
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb {
background-color: #bfbfbf; /* 设置滚动条滑块颜色 */
}
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb:hover {
background-color: #1890ff; /* 设置鼠标悬停时滚动条滑块颜色 */
}
</style>

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