|
| 1 | +<template> |
| 2 | + <a-alert message="WebAssembly Wireshark" type="info" show-icon /> |
| 3 | + <input type="file" @change="fileChangeHandler"> |
| 4 | + <div v-if="processed" class="my-10px"> |
| 5 | + <ATag color="green">总包数: {{ summary.total_packets }}</ATag> |
| 6 | + <ATag color="green">开始时间: {{ start_time }}</ATag> |
| 7 | + <ATag color="green">结束时间: {{ stop_time }}</ATag> |
| 8 | + </div> |
| 9 | + <AInput v-model:value="filter_input" placeholder="display filter, example: tcp" /> |
| 10 | + <ATable |
| 11 | + :loading="loading" |
| 12 | + :columns="columns" |
| 13 | + :data-source="data_source" |
| 14 | + :pagination="pagination" |
| 15 | + size="small" |
| 16 | + :customRow="customRowHandler" |
| 17 | + :scroll="{ y: 240 }" |
| 18 | + class="packet-table" |
| 19 | + @change="tableChangeHandler" |
| 20 | + /> |
| 21 | + <AAlert v-if="(typeof filter_ret !== 'boolean')"> |
| 22 | + {{ filter_ret }} |
| 23 | + </AAlert> |
| 24 | + <div class="flex"> |
| 25 | + <ATree |
| 26 | + :tree-data="selected_packet_tree" |
| 27 | + :fieldNames="{ children:'tree', title:'label', key:'id' }" |
| 28 | + block-node |
| 29 | + /> |
| 30 | + |
| 31 | + </div> |
| 32 | +</template> |
| 33 | +<script lang="ts" setup> |
| 34 | + import { get, map, reduce } from 'lodash' |
| 35 | + import { MessageData } from '~/utils/wireshark.worker' |
| 36 | +
|
| 37 | + const selected_row_idx = ref(0) |
| 38 | + const selected_packet_tree = ref([] as Tree[]) |
| 39 | + const customRowHandler = ({ raw }: any, idx: number) => { |
| 40 | + const bg = raw.number === selected_row_idx.value ? 'blue' : `#${Number(raw.bg).toString(16)}` |
| 41 | + const fg = raw.number === selected_row_idx.value ? 'white' : `#${Number(raw.fg).toString(16)}` |
| 42 | + return { |
| 43 | + style: `background: ${bg}; color: ${fg};cursor: pointer;`, |
| 44 | + onClick: () => selected_row_idx.value = raw.number |
| 45 | + } |
| 46 | + } |
| 47 | +
|
| 48 | + const initialized = ref(false) |
| 49 | + const processed = ref(false) |
| 50 | + const loading = ref(true) |
| 51 | + const summary = ref({ |
| 52 | + start_time: 0, |
| 53 | + stop_time: 0, |
| 54 | + total_packets: 0, |
| 55 | + }) |
| 56 | + const start_time = computed(() => { |
| 57 | + return new Date(summary.value.start_time * 1000) |
| 58 | + }) |
| 59 | + const stop_time = computed(() => { |
| 60 | + return new Date(summary.value.stop_time * 1000) |
| 61 | + }) |
| 62 | +
|
| 63 | + const columns = ref([] as Record<string, any>) |
| 64 | + const column_width = { |
| 65 | + 'No.': 60, |
| 66 | + 'Source': 150, |
| 67 | + 'Destination': 150, |
| 68 | + default: 100, |
| 69 | + } as Record<string, number> |
| 70 | + const data_source = ref([] as Record<string, any>) |
| 71 | + const worker = new Worker(new URL('~/utils/wireshark.worker.ts', import.meta.url), { |
| 72 | + type: 'module', |
| 73 | + }) |
| 74 | +
|
| 75 | + const fileChangeHandler = async (ev: Event) => { |
| 76 | + const f = (ev.target as HTMLInputElement).files?.[0] |
| 77 | + if (!f) return window.alert('文件不存在') |
| 78 | + processed.value = false |
| 79 | + const buf = await f?.arrayBuffer() |
| 80 | + worker.postMessage({ |
| 81 | + type: 'process:buffer', |
| 82 | + data: buf, |
| 83 | + }, [buf]) |
| 84 | + } |
| 85 | +
|
| 86 | + const page_index = ref(1) |
| 87 | + const page_size = ref(10) |
| 88 | + const pagination = computed(() => ({ |
| 89 | + total: summary.value.total_packets, |
| 90 | + current: page_index.value, |
| 91 | + pageSize: page_size.value, |
| 92 | + })) |
| 93 | + const fetchTableData = async () => { |
| 94 | + loading.value = true |
| 95 | + const { port1, port2 } = new MessageChannel() |
| 96 | + port1.onmessage = ev => { |
| 97 | + const { data } = ev.data |
| 98 | + data_source.value = map(data?.frames, (f: any) => { |
| 99 | + return reduce(columns.value, (acc: Record<string, any>, col: Record<string, any>, idx: any) => { |
| 100 | + acc[col.dataIndex] = get(f, ['columns', idx]) |
| 101 | + return acc |
| 102 | + }, { |
| 103 | + raw: f, |
| 104 | + }) |
| 105 | + }) |
| 106 | + port1.onmessage = null |
| 107 | + port1.close() |
| 108 | + port2.close() |
| 109 | + loading.value = false |
| 110 | + } |
| 111 | + worker.postMessage({ |
| 112 | + type: 'select-frames', |
| 113 | + skip: (page_index.value - 1) * page_size.value, |
| 114 | + limit: page_size.value, |
| 115 | + filter: filter_input.value, |
| 116 | + }, [port2]) |
| 117 | + } |
| 118 | + const tableChangeHandler = (page: { pageSize: number, current: number }) => { |
| 119 | + page_index.value = page.current |
| 120 | + page_size.value = page.pageSize |
| 121 | + fetchTableData() |
| 122 | + } |
| 123 | +
|
| 124 | + async function fetchPacketDetail() { |
| 125 | + if (!processed.value) return |
| 126 | + worker.postMessage({ |
| 127 | + type: 'select', |
| 128 | + number: selected_row_idx.value, |
| 129 | + }) |
| 130 | + } |
| 131 | + watchEffect(() => fetchPacketDetail()) |
| 132 | +
|
| 133 | + const filter_input = ref('') |
| 134 | + const filter_ret = ref(true) |
| 135 | + const filterChangeHandler = () => { |
| 136 | + if (!processed.value) return |
| 137 | + const { port1, port2 } = new MessageChannel() |
| 138 | + port1.onmessage = ev => { |
| 139 | + filter_ret.value = ev.data.data |
| 140 | + if (filter_ret.value === true) fetchTableData() |
| 141 | + } |
| 142 | + worker.postMessage({ |
| 143 | + type: 'check-filter', |
| 144 | + data: filter_input.value, |
| 145 | + }, [port2]) |
| 146 | + } |
| 147 | + watchEffect(() => filterChangeHandler()) |
| 148 | +
|
| 149 | + interface Tree { |
| 150 | + label: string; |
| 151 | + [key: string]: any; |
| 152 | + } |
| 153 | + const processPacketTree = (tree: Tree[], id_prefix = ''): Tree[] => map(tree, (t: Tree, idx: number) => { |
| 154 | + const id = `${id_prefix}${idx}` |
| 155 | + return { |
| 156 | + ...t, |
| 157 | + id, |
| 158 | + tree: processPacketTree(t.tree, `${id}-`) |
| 159 | + } |
| 160 | + }) |
| 161 | +
|
| 162 | + const MESSAGE_STRATEGIES = { |
| 163 | + init: ev => { |
| 164 | + initialized.value = true |
| 165 | + loading.value = false |
| 166 | + worker.postMessage({ type: 'columns' }) |
| 167 | + }, |
| 168 | + columns: ev => { |
| 169 | + const cols = ev.data.data |
| 170 | + const end = cols.length - 1 |
| 171 | + columns.value = map(cols, (c: string, idx: number) => { |
| 172 | + return { |
| 173 | + title: c, |
| 174 | + dataIndex: c, |
| 175 | + width: idx < end ? (column_width[c] || column_width['default']) : undefined, |
| 176 | + } |
| 177 | + }) |
| 178 | + }, |
| 179 | + processed: ev => { |
| 180 | + const { summary: _summary } = ev.data.data |
| 181 | + summary.value = { |
| 182 | + start_time: _summary.start_time, |
| 183 | + stop_time: _summary.stop_time, |
| 184 | + total_packets: _summary.packet_count, |
| 185 | + } |
| 186 | + processed.value = true |
| 187 | + fetchTableData() |
| 188 | + selected_row_idx.value = 1 |
| 189 | + }, |
| 190 | + selected: ev => { |
| 191 | + selected_packet_tree.value = processPacketTree(ev.data.data?.tree) |
| 192 | + } |
| 193 | + } as Record<string, (ev: MessageEvent<MessageData>) => void> |
| 194 | +
|
| 195 | + worker.addEventListener('message', (ev: MessageEvent) => { |
| 196 | + const type = ev.data.type as string |
| 197 | + MESSAGE_STRATEGIES[type]?.(ev) |
| 198 | + }) |
| 199 | +</script> |
| 200 | +<style lang="scss" scoped> |
| 201 | + .packet-table { |
| 202 | + :deep(.ant-table-tbody > tr > td.ant-table-cell-row-hover), |
| 203 | + :deep(.ant-table-tbody > tr.ant-table-row:hover > td) { |
| 204 | + background: transparent; |
| 205 | + } |
| 206 | +
|
| 207 | + :deep(.ant-table.ant-table-small .ant-table-tbody > tr > td) { |
| 208 | + padding: 0; |
| 209 | + } |
| 210 | +
|
| 211 | + :deep(.ant-table-pagination.ant-pagination) { |
| 212 | + margin: 5px 0; |
| 213 | + } |
| 214 | + } |
| 215 | +</style> |
0 commit comments