feat: 信令抓包tshark解析pcap
This commit is contained in:
BIN
public/wiregasm/test_ethernet.pcap
Normal file
BIN
public/wiregasm/test_ethernet.pcap
Normal file
Binary file not shown.
BIN
public/wiregasm/wiregasm.data.gz
Normal file
BIN
public/wiregasm/wiregasm.data.gz
Normal file
Binary file not shown.
9768
public/wiregasm/wiregasm.js
Normal file
9768
public/wiregasm/wiregasm.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/wiregasm/wiregasm.wasm.gz
Normal file
BIN
public/wiregasm/wiregasm.wasm.gz
Normal file
Binary file not shown.
166
public/wiregasm/wiregasm_new.js
Normal file
166
public/wiregasm/wiregasm_new.js
Normal 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
159
public/wiregasm/worker.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
11
src/assets/js/wiregasm_worker.ts
Normal file
11
src/assets/js/wiregasm_worker.ts
Normal 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`;
|
||||
141
src/views/traceManage/tshark/components/DissectionDump.vue
Normal file
141
src/views/traceManage/tshark/components/DissectionDump.vue
Normal 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>
|
||||
@@ -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>
|
||||
59
src/views/traceManage/tshark/components/DissectionTree.vue
Normal file
59
src/views/traceManage/tshark/components/DissectionTree.vue
Normal 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>
|
||||
116
src/views/traceManage/tshark/components/DissectionTreeSub.vue
Normal file
116
src/views/traceManage/tshark/components/DissectionTreeSub.vue
Normal 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>
|
||||
293
src/views/traceManage/tshark/components/PacketTable.vue
Normal file
293
src/views/traceManage/tshark/components/PacketTable.vue
Normal 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>
|
||||
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