Inline frontend instead of submodule
This commit is contained in:
129
frontend/src/views/ContentStream.vue
Normal file
129
frontend/src/views/ContentStream.vue
Normal 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>
|
||||
57
frontend/src/views/LookBack.vue
Normal file
57
frontend/src/views/LookBack.vue
Normal 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>
|
||||
205
frontend/src/views/PatternModal.vue
Normal file
205
frontend/src/views/PatternModal.vue
Normal 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>
|
||||
136
frontend/src/views/ServiceModal.vue
Normal file
136
frontend/src/views/ServiceModal.vue
Normal 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>
|
||||
63
frontend/src/views/Settings.vue
Normal file
63
frontend/src/views/Settings.vue
Normal 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>
|
||||
Reference in New Issue
Block a user