Inline frontend instead of submodule

This commit is contained in:
dan
2025-12-06 17:34:40 +03:00
parent 2d265bb71d
commit c4af8465aa
36 changed files with 34880 additions and 4 deletions

View File

@@ -0,0 +1,129 @@
<template>
<div>
<div class="legend-bar">
<div class="d-flex align-items-center" style="gap: 10px;">
<span class="legend-pill request-pill">Request</span>
<span class="legend-pill response-pill">Response</span>
</div>
<ThemeButton class="d-inline-flex theme-toggle"/>
</div>
<Packet v-for="packetWithOffset in packetsWithOffsets"
:key="packetWithOffset.packet.id"
:packet="packetWithOffset.packet"
:offset="packetWithOffset.offset" />
<infinite-loading @infinite="infiniteLoadingHandler" ref="infiniteLoader">
<span slot="no-results"></span>
</infinite-loading>
</div>
</template>
<script>
import Packet from '../components/Packet';
import ThemeButton from '@/components/ThemeButton.vue';
export default {
name: 'ContentStream',
props: ['servicePort', 'streamId',],
data() {
return {
packets: [],
};
},
computed: {
packetsWithOffsets: function() {
return this.packets.map((el, i) => {
let offset = null;
if (i !== 0) {
offset = el.timestamp - this.packets[i - 1].timestamp;
}
return {packet: el, offset: offset,}
})
},
},
watch: {
'$route.params.streamId': function () {
this.packets = [];
this.$refs.infiniteLoader.stateChanger.reset();
},
},
methods: {
infiniteLoadingHandler($state) {
if (!this.$route.params.streamId) return $state.complete();
const packets = this.packets;
const pageSize = this.$store.state.pageSize;
let startsFrom;
if (packets && packets.length && packets[packets.length - 1]) {
startsFrom = packets[packets.length - 1].id;
} else {
startsFrom = null;
}
this.$http.post(`packet/${this.$route.params.streamId}`, {
startingFrom: startsFrom,
pageSize: pageSize,
}).then(response => {
const data = response.data;
if (data.length === 0) {
console.log('Finished loading packets (empty page)');
return $state.complete();
}
if (data[0] && this.packets[0] && data[0].id === this.packets[0].id) {
console.log('Finished loading packets (overlap detected)');
return $state.complete();
}
this.packets.push(...data);
if (data.length < pageSize) {
// this was the last page
console.log('Finished loading packets (last page was not full)');
$state.complete();
} else {
console.log('Loaded another page of packets');
$state.loaded();
}
}).catch(e => {
this.$bvToast.toast(`Failed to load portion of packets: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to load portion of packets:', e);
return $state.error();
});
},
},
components: {
ThemeButton,
Packet,
},
};
</script>
<style>
:root {
--response-packet-color: rgba(255, 106, 213, 0.16);
--request-packet-color: rgba(94, 242, 255, 0.2);
}
:root [data-theme=dark] {
color-scheme: dark;
--response-packet-color: rgba(255, 106, 213, 0.16);
--request-packet-color: rgba(94, 242, 255, 0.2);
}
.request-pill {
border-color: rgba(94, 242, 255, 0.45);
color: #c9f8ff;
background: rgba(94, 242, 255, 0.1);
}
.response-pill {
border-color: rgba(255, 106, 213, 0.45);
color: #ffd6f6;
background: rgba(255, 106, 213, 0.1);
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<b-modal @ok="lookBack" id="lookBackModal"
title="Search into the past"
cancel-title="Cancel"
centered scrollable>
<form ref="lookBackForm">
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
label="Minutes"
description="How far into the past do we want to look"
label-for="lookback-minutes">
<b-form-input @keydown.native.enter="lookBack" id="lookback-minutes" required v-model="minutes"/>
</b-form-group>
</form>
</b-modal>
</template>
<script>
export default {
name: 'LookBack',
props: {
patternId: Number,
},
data() {
return {
minutes: 5,
};
},
methods: {
checkValidity() {
return this.$refs.lookBackForm.reportValidity();
},
lookBack(ev) {
ev.preventDefault();
if (!this.checkValidity()) {
console.debug('Form is invalid');
return;
}
console.debug(`Looking back with pattern ${this.patternId}`);
this.$http.post(`pattern/${this.patternId}/lookback`, this.minutes)
.then(() => {
console.debug('Lookback started');
this.$bvModal.hide('lookBackModal');
}).catch(e => {
this.$bvToast.toast(`Failed to start lookback: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to start lookback', e);
});
},
},
};
</script>

View File

@@ -0,0 +1,205 @@
<template>
<b-modal @ok="addPattern" @cancel="reset" id="patternModal"
:title="creating ? 'Creating pattern' : 'Editing pattern'"
cancel-title="Cancel"
centered scrollable>
<form ref="addPatternForm">
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
label="Name"
description="It is displayed in the list and highlighted in the packet contents"
label-for="pattern-name">
<b-form-input @keydown.native.enter="addPattern" id="pattern-name" required v-model="newPattern.name"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Pattern"
description="Substring, RegEx or bytes"
label-for="pattern-value">
<b-form-input @keydown.native.enter="addPattern" @keydown="validateKey"
id="pattern-value" required v-model="newPattern.value"
:placeholder="getPlaceholder()"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search action"
description="What to do with matching streams"
label-for="pattern-actionType">
<b-form-select id="pattern-actionType" required v-model="newPattern.actionType">
<option value="FIND" selected>Highlight found pattern</option>
<option value="IGNORE">Ignore matching streams</option>
</b-form-select>
</b-form-group>
<b-form-group v-if="newPattern.actionType === 'FIND'"
label-cols-sm="4"
label-cols-lg="3"
label="Color"
description="The highlight color of the pattern in the packets and streams"
label-for="pattern-color">
<b-form-input id="pattern-color" required type="color" v-model="newPattern.color"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search method"
description="The way to search for patterns in packets"
label-for="pattern-searchType">
<b-form-select id="pattern-searchType" required v-model="newPattern.searchType">
<option value="REGEX" selected>Regular expression</option>
<option value="SUBSTRING">Substring</option>
<option value="SUBBYTES">Bytes</option>
</b-form-select>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search type"
description="In which packets to search for a pattern"
label-for="pattern-type">
<b-form-select id="pattern-type" required v-model="newPattern.directionType">
<option value="INPUT">Request</option>
<option value="OUTPUT">Response</option>
<option value="BOTH" selected>Anywhere</option>
</b-form-select>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Service"
description="Apply this pattern only to the specific service"
label-for="pattern-service">
<b-form-select id="pattern-service" :options="serviceOptions"
v-model="newPattern.serviceId"></b-form-select>
</b-form-group>
</form>
</b-modal>
</template>
<script>
const hexRegex = /[0-9A-Fa-f ]/;
const defaultPattern = {
name: '',
value: '',
color: '#FF7474',
searchType: 'SUBSTRING',
directionType: 'BOTH',
actionType: 'FIND',
serviceId: null,
};
export default {
name: 'PatternModal',
props: {
creating: Boolean,
initialPattern: {
name: String,
value: String,
color: String,
searchType: String,
directionType: String,
actionType: String,
serviceId: Number,
},
},
data() {
return {
newPattern: {},
};
},
watch: {
initialPattern() {
console.debug('initialService changed, reassigning...', this.initialPattern);
this.newPattern = {...defaultPattern, ...this.initialPattern,};
},
},
computed: {
serviceOptions: function () {
let services = this.$store.state.services;
let options = services.map(service => {
return {
value: service.port,
text: `${service.name} #${service.port}`,
}
});
return [
{ value: null, text: 'Any service', },
...options,
];
},
},
methods: {
getPlaceholder() {
if (this.newPattern.searchType === 'REGEX') return '[A-Z0-9]{31}=';
else if (this.newPattern.searchType === 'SUBSTRING') return 'HTTP/2';
else return 'DEAD BEEF 1337';
},
validateKey(e) {
console.log('', e);
if (this.newPattern.searchType !== 'SUBBYTES') return;
if (!hexRegex.test(e?.key)) {
e.preventDefault();
}
},
checkValidity() {
return this.$refs.addPatternForm.reportValidity();
},
reset() {
this.newPattern = {...defaultPattern,};
},
addPattern(ev) {
ev.preventDefault();
if (!this.checkValidity()) {
console.debug('Form is invalid');
return;
}
console.debug('Adding/editing pattern...', this.newPattern);
if (this.newPattern.searchType === 'SUBBYTES') {
this.newPattern.value = this.newPattern.value.replace(/\s+/g, '').toLowerCase();
}
let url;
if (this.creating) {
url = 'pattern/'
} else {
url = 'pattern/' + this.newPattern.id
}
this.$http.post(url, this.newPattern)
.then(response => {
const data = response.data;
console.debug('Done adding/editing pattern', data);
this.$emit('patternAddComplete');
this.reset();
this.$bvModal.hide('patternModal');
}).catch(e => {
this.$bvToast.toast(`Failed to add pattern: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to add pattern', e);
});
},
},
};
</script>

View File

@@ -0,0 +1,136 @@
<template>
<b-modal id="serviceModal" size="lg"
:title="creating ? 'Creating service' : 'Editing service'"
centered scrollable @ok.prevent="submit">
<b-form ref="serviceForm">
<b-form-group label-cols-sm="4"
label="Name"
label-for="service-name">
<b-form-input id="service-name" required v-model="service.name"
@keyup.enter.stop.prevent="submit"/>
</b-form-group>
<b-form-group v-if="creating"
label-cols-sm="4"
label="Port"
label-for="service-port">
<b-form-input id="service-port" required
type="number" min="1" max="65535" v-model.number="service.port"
@keyup.enter.stop.prevent="submit"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Is an HTTP service"
label-for="service-is-http">
<b-form-checkbox id="service-is-http" required
v-model="service.http" />
</b-form-group>
<b-form-group label-cols-sm="4"
label="Apply urldecode"
label-for="service-urldecode">
<b-form-checkbox id="service-urldecode" required
v-model="service.urldecodeHttpRequests"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Merge adjacent packets"
label-for="service-mergeAdjacent">
<b-form-checkbox id="service-mergeAdjacent" required
v-model="service.mergeAdjacentPackets"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Inflate WebSockets"
label-for="service-inflateWS">
<b-form-checkbox id="service-inflateWS" required
v-model="service.parseWebSockets"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Decrypt TLS (TLS_RSA_WITH_AES only)"
label-for="service-decryptTls">
<b-form-checkbox id="service-decryptTls" required
v-model="service.decryptTls"/>
</b-form-group>
<b-button v-if="!creating" variant="danger" @click="deleteService">Delete</b-button>
</b-form>
</b-modal>
</template>
<script>
import {objectAssignDeep,} from '@/objectAssignDeep';
export default {
name: 'ServiceModal',
props: {
creating: Boolean,
initialService: {
name: String,
port: Number,
http: Boolean,
urldecodeHttpRequests: Boolean,
mergeAdjacentPackets: Boolean,
parseWebSockets: Boolean,
decryptTls: Boolean,
},
},
data() {
return {
service: {},
};
},
watch: {
initialService() {
console.debug('initialService changed, reassigning...', this.initialService);
this.service = objectAssignDeep({}, this.initialService);
},
},
methods: {
submit() {
if (!this.$refs.serviceForm.reportValidity()) {
return;
}
console.debug('Submitting service...', this.service, this.creating);
let url;
if (this.creating) {
url = 'service/';
} else {
url = 'service/' + this.initialService.port
}
this.$http.post(url, this.service)
.then(response => {
console.info('Done editing/creating service', response.data);
this.$emit('service-update-needed');
this.$bvModal.hide('serviceModal');
})
.catch(e => {
this.$bvToast.toast(`Failed to ${this.creating ? 'create' : 'edit'} service: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to edit/create service', e);
});
},
deleteService() {
this.$http.delete(`service/${this.service.port}`)
.then(() => {
console.info('Done deleting service', this.service);
this.$emit('service-update-needed');
this.$bvModal.hide('serviceModal');
}).catch(e => {
this.$bvToast.toast(`Failed to delete service: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to delete service', e);
});
},
},
};
</script>

View File

@@ -0,0 +1,63 @@
<template>
<b-modal id="settingsModal" title="Settings" centered scrollable ok-only>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="The number of bytes to display in the binary representation of the packet"
label="HEX block width"
label-for="settings-hexdumpBlockSize">
<b-form-input type="number" id="settings-hexdumpBlockSize" v-model.number="hexdumpBlockSize"/>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="The number system used in line numbers of a binary representation of a packet"
label="Line numbering"
label-for="settings-hexdumpLineNumberBase">
<b-form-select id="settings-hexdumpLineNumberBase" v-model="hexdumpLineNumberBase">
<option :value="10" selected>Decimal</option>
<option :value="16">Hexadecimal</option>
</b-form-select>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="The number of streams to download at a time"
label="Page size"
label-for="settings-pageSize">
<b-form-input type="number" id="settings-pageSize" v-model.number="pageSize"/>
</b-form-group>
</b-modal>
</template>
<script>
export default {
name: 'Settings',
computed: {
hexdumpBlockSize: {
get() {
return this.$store.state.hexdumpBlockSize;
},
set(v) {
this.$store.commit('setHexdumpBlockSize', v);
},
},
hexdumpLineNumberBase: {
get() {
return this.$store.state.hexdumpLineNumberBase;
},
set(v) {
this.$store.commit('setHexdumpLineNumberBase', v);
},
},
pageSize: {
get() {
return this.$store.state.pageSize;
},
set(v) {
this.$store.commit('setPageSize', v);
},
},
},
};
</script>