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