From 43135c255d82aa7c54ea83b14369c93425ae75f6 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Wed, 30 Sep 2020 13:29:04 +0200 Subject: Complete pcap page --- frontend/src/backend.js | 56 +--- frontend/src/components/Connection.js | 10 +- frontend/src/components/ConnectionContent.js | 2 +- .../components/filters/RulesConnectionsFilter.js | 2 +- frontend/src/components/objects/LinkPopover.js | 2 +- frontend/src/components/panels/PcapPane.js | 289 ++++++++++++--------- frontend/src/components/panels/PcapPane.scss | 25 +- frontend/src/components/panels/RulePane.js | 6 +- frontend/src/components/panels/common.scss | 16 ++ frontend/src/utils.js | 27 +- frontend/src/views/Connections.js | 6 +- frontend/src/views/MainPane.js | 2 +- 12 files changed, 247 insertions(+), 196 deletions(-) diff --git a/frontend/src/backend.js b/frontend/src/backend.js index 2fbe920..72ee9dd 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -1,7 +1,8 @@ -async function json(method, url, data, headers) { +async function json(method, url, data, json, headers) { const options = { method: method, + body: json != null ? JSON.stringify(json) : data, mode: "cors", cache: "no-cache", credentials: "same-origin", @@ -11,9 +12,6 @@ async function json(method, url, data, headers) { redirect: "follow", referrerPolicy: "no-referrer", }; - if (data != null) { - options.body = JSON.stringify(data); - } const response = await fetch(url, options); const result = { statusCode: response.status, @@ -28,47 +26,17 @@ async function json(method, url, data, headers) { } } -async function file(url, data, headers) { - const options = { - method: "POST", - mode: "cors", - cache: "no-cache", - credentials: "same-origin", - body: data, - redirect: "follow", - referrerPolicy: "no-referrer", - }; - return await fetch(url, options); -} - const backend = { - get: (url = "", headers = null) => { - return json("GET", url, null, headers); - }, - post: (url = "", data = null, headers = null) => { - return json("POST", url, data, headers); - }, - put: (url = "", data = null, headers = null) => { - return json("PUT", url, data, headers); - }, - delete: (url = "", data = null, headers = null) => { - return json("DELETE", url, data, headers); - }, - getJson: (url = "", headers = null) => { - return json("GET", url, null, headers); - }, - postJson: (url = "", data = null, headers = null) => { - return json("POST", url, data, headers); - }, - putJson: (url = "", data = null, headers = null) => { - return json("PUT", url, data, headers); - }, - deleteJson: (url = "", data = null, headers = null) => { - return json("DELETE", url, data, headers); - }, - postFile: (url = "", data = null, headers = null) => { - return file(url, data, headers); - }, + get: (url = "", headers = null) => + json("GET", url, null,null, headers), + post: (url = "", data = null, headers = null) => + json("POST", url, null, data, headers), + put: (url = "", data = null, headers = null) => + json("PUT", url, null, data, headers), + delete: (url = "", data = null, headers = null) => + json("DELETE", url, null, data, headers), + postFile: (url = "", data = null, headers = {}) => + json("POST", url, data, null, headers) }; export default backend; diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js index 7887185..95b27ff 100644 --- a/frontend/src/components/Connection.js +++ b/frontend/src/components/Connection.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import './Connection.scss'; import {Form, OverlayTrigger, Popover} from "react-bootstrap"; import backend from "../backend"; -import {formatSize} from "../utils"; +import {durationBetween, formatSize} from "../utils"; import ButtonField from "./fields/ButtonField"; class Connection extends Component { @@ -54,12 +54,6 @@ class Connection extends Component { let startedAt = new Date(conn.started_at); let closedAt = new Date(conn.closed_at); let processedAt = new Date(conn.processed_at); - let duration = ((closedAt - startedAt) / 1000).toFixed(3); - if (duration > 1000 || duration < -1000) { - duration = "∞"; - } else { - duration += "s"; - } let timeInfo =
Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}
Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}
@@ -108,7 +102,7 @@ class Connection extends Component { - {duration} + {durationBetween(startedAt, closedAt)} {formatSize(conn.client_bytes)} diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index a9d34d3..6bc0c96 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -40,7 +40,7 @@ class ConnectionContent extends Component { loadStream = () => { this.setState({loading: true}); // TODO: limit workaround. - backend.getJson(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => { + backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => { this.setState({ connectionContent: res.json, loading: false diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index f4d0b1c..8366189 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -24,7 +24,7 @@ class RulesConnectionsFilter extends Component { let params = new URLSearchParams(this.props.location.search); let activeRules = params.getAll("matched_rules") || []; - backend.getJson("/api/rules").then(res => { + backend.get("/api/rules").then(res => { let rules = res.json.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []); activeRules = rules.filter(rule => activeRules.some(id => rule.id === id)); this.setState({rules, activeRules, mounted: true}); diff --git a/frontend/src/components/objects/LinkPopover.js b/frontend/src/components/objects/LinkPopover.js index 58b2f6a..8768caa 100644 --- a/frontend/src/components/objects/LinkPopover.js +++ b/frontend/src/components/objects/LinkPopover.js @@ -22,7 +22,7 @@ class LinkPopover extends Component { ); return (this.props.content ? - + {this.props.text} : {this.props.text} diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js index cfba037..e83e3da 100644 --- a/frontend/src/components/panels/PcapPane.js +++ b/frontend/src/components/panels/PcapPane.js @@ -1,14 +1,14 @@ import React, {Component} from 'react'; -// import './PcapPane.scss'; +import './PcapPane.scss'; import './common.scss'; import Table from "react-bootstrap/Table"; import backend from "../../backend"; -import {createCurlCommand, formatSize, timestampToTime2} from "../../utils"; -import {Col, Container, Row} from "react-bootstrap"; +import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils"; import InputField from "../fields/InputField"; import CheckField from "../fields/CheckField"; import TextField from "../fields/TextField"; import ButtonField from "../fields/ButtonField"; +import LinkPopover from "../objects/LinkPopover"; class PcapPane extends Component { @@ -19,16 +19,11 @@ class PcapPane extends Component { sessions: [], isUploadFileValid: true, isUploadFileFocused: false, - uploadSelectedFile: null, uploadFlushAll: false, - uploadStatusCode: null, - uploadOutput: null, isFileValid: true, isFileFocused: false, fileValue: "", - fileFlushAll: false, - fileStatusCode: null, - fileOutput: null, + processFlushAll: false, deleteOriginalFile: false }; } @@ -38,46 +33,38 @@ class PcapPane extends Component { } loadSessions = () => { - backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res.json})); + backend.get("/api/pcap/sessions") + .then(res => this.setState({sessions: res.json, sessionsStatusCode: res.status})) + .catch(res => this.setState({ + sessions: res.json, sessionsStatusCode: res.status, + sessionsResponse: JSON.stringify(res.json) + })); }; - handleUploadFileChange = (file) => { - this.setState({ - isUploadFileValid: file != null && file.type.endsWith("pcap"), - isUploadFileFocused: false, - uploadSelectedFile: file - }); - }; - - handleUploadPcap = () => { + uploadPcap = () => { if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) { this.setState({isUploadFileFocused: true}); return; } const formData = new FormData(); - formData.append( - "file", - this.state.uploadSelectedFile - ); - - backend.postFile("/api/pcap/upload", formData).then(response => - response.json().then(result => this.setState({ - uploadStatusCode: response.status + " " + response.statusText, - uploadOutput: JSON.stringify(result) - })) + formData.append("file", this.state.uploadSelectedFile); + formData.append("flush_all", this.state.uploadFlushAll); + backend.postFile("/api/pcap/upload", formData).then(res => { + this.setState({ + uploadStatusCode: res.status, + uploadResponse: JSON.stringify(res.json) + }); + this.resetUpload(); + this.loadSessions(); + }).catch(res => this.setState({ + uploadStatusCode: res.status, + uploadResponse: JSON.stringify(res.json) + }) ); }; - handleFileChange = (file) => { - this.setState({ - isFileValid: file !== "" && file.endsWith("pcap"), - isFileFocused: false, - fileValue: file - }); - }; - - handleProcessPcap = () => { + processPcap = () => { if (this.state.fileValue === "" || !this.state.isFileValid) { this.setState({isFileFocused: true}); return; @@ -85,123 +72,177 @@ class PcapPane extends Component { backend.post("/api/pcap/file", { file: this.state.fileValue, - flush_all: this.state.fileFlushAll, + flush_all: this.state.processFlushAll, delete_original_file: this.state.deleteOriginalFile - }).then(response => - response.json().then(result => this.setState({ - fileStatusCode: response.status + " " + response.statusText, - fileOutput: JSON.stringify(result) - })) + }).then(res => { + this.setState({ + processStatusCode: res.status, + processResponse: JSON.stringify(res.json) + }); + this.resetProcess(); + this.loadSessions(); + }).catch(res => this.setState({ + processStatusCode: res.status, + processResponse: JSON.stringify(res.json) + }) ); }; + resetUpload = () => { + this.setState({ + isUploadFileValid: true, + isUploadFileFocused: false, + uploadFlushAll: false, + uploadSelectedFile: null + }); + }; + + resetProcess = () => { + this.setState({ + isFileValid: true, + isFileFocused: false, + fileValue: "", + processFlushAll: false, + deleteOriginalFile: false, + }); + }; + render() { let sessions = this.state.sessions.map(s => {s["id"].substring(0, 8)} - {timestampToTime2(s["started_at"])} - {((new Date(s["completed_at"]) - new Date(s["started_at"])) / 1000).toFixed(3)}s + {dateTimeToTime(s["started_at"])} + {durationBetween(s["started_at"], s["completed_at"])} {formatSize(s["size"])} {s["processed_packets"]} {s["invalid_packets"]} - undefined + download ); - const uploadOutput = this.state.uploadOutput != null ? this.state.uploadOutput : - createCurlCommand("pcap/upload", "POST", null, { - file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ? this.state.uploadSelectedFile.name : - "invalid.pcap"), - flush_all: this.state.uploadFlushAll - }) - ; + const handleUploadFileChange = (file) => { + this.setState({ + isUploadFileValid: file == null || (file.type.endsWith("pcap") || file.type.endsWith("pcapng")), + isUploadFileFocused: false, + uploadSelectedFile: file, + uploadStatusCode: null, + uploadResponse: null + }); + }; + + const handleFileChange = (file) => { + this.setState({ + isFileValid: (file.endsWith("pcap") || file.endsWith("pcapng")), + isFileFocused: false, + fileValue: file, + processStatusCode: null, + processResponse: null + }); + }; + + const uploadCurlCommand = createCurlCommand("pcap/upload", "POST", null, { + file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ? + this.state.uploadSelectedFile.name : "invalid.pcap"), + flush_all: this.state.uploadFlushAll + }); + + const fileCurlCommand = createCurlCommand("pcap/file", "POST", { + file: this.state.fileValue, + flush_all: this.state.processFlushAll, + delete_original_file: this.state.deleteOriginalFile + }); return ( -
-
+
+
GET /api/pcap/sessions - 200 OK +
-
- - - - - - - - - - - - - - - {sessions} - -
idstarted_atdurationsizeprocessed_packetsinvalid_packetspackets_per_serviceactions
+
+
+ + + + + + + + + + + + + + + {sessions} + +
idstarted_atdurationsizeprocessed_packetsinvalid_packetspackets_per_serviceactions
+
-
- - - -
- POST /api/pcap/upload - {this.state.uploadStatusCode} +
+
+
+ POST /api/pcap/upload + +
+ +
+ +
+
+ options: + this.setState({uploadFlushAll: v})}/>
+ +
-
- - -
-
- options: - this.setState({uploadFlushAll: v})}/> -
- -
- - -
- + +
+
- -
- POST /api/pcap/file - {this.state.fileStatusCode} +
+
+ POST /api/pcap/file + +
+ +
+ + +
+
+ this.setState({processFlushAll: v})}/> + this.setState({deleteOriginalFile: v})}/>
+ +
-
- - -
-
- this.setState({uploadFlushAll: v})}/> - this.setState({uploadFlushAll: v})}/> -
- -
- - -
- - - + +
+
); diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss index 3c4d10b..721560a 100644 --- a/frontend/src/components/panels/PcapPane.scss +++ b/frontend/src/components/panels/PcapPane.scss @@ -1,10 +1,25 @@ @import '../../colors.scss'; -.pane-container { +.pcap-pane { + display: flex; + flex-direction: column; - .table-cell-action { - font-size: 13px; - font-weight: 600; + .pcap-list { + flex: 1; + overflow: hidden; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + + .table-cell-action { + font-size: 13px; + font-weight: 600; + } } .upload-actions { @@ -21,4 +36,4 @@ } } -} \ No newline at end of file +} diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js index 01e37fa..d4c5460 100644 --- a/frontend/src/components/panels/RulePane.js +++ b/frontend/src/components/panels/RulePane.js @@ -73,13 +73,13 @@ class RulePane extends Component { }; loadRules = () => { - backend.getJson("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status})) + backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status})) .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)})); }; addRule = () => { if (this.validateRule(this.state.newRule)) { - backend.postJson("/api/rules", this.state.newRule).then(res => { + backend.post("/api/rules", this.state.newRule).then(res => { this.reset(); this.setState({ruleStatusCode: res.status}); this.loadRules(); @@ -92,7 +92,7 @@ class RulePane extends Component { updateRule = () => { const rule = this.state.selectedRule; if (this.validateRule(rule)) { - backend.putJson(`/api/rules/${rule.id}`, rule).then(res => { + backend.put(`/api/rules/${rule.id}`, rule).then(res => { this.reset(); this.setState({ruleStatusCode: res.status}); this.loadRules(); diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss index cab0de1..ea8da94 100644 --- a/frontend/src/components/panels/common.scss +++ b/frontend/src/components/panels/common.scss @@ -82,4 +82,20 @@ margin-left: 10px; } } + + .double-pane-container { + display: flex; + + .pane-section { + flex: 1; + } + + .pane-section:nth-child(1) { + margin-right: 5px; + } + + .pane-section:nth-child(2) { + margin-left: 5px; + } + } } diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 6a5411c..0707575 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -1,3 +1,5 @@ +import React from "react"; + const timeRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/; export function createCurlCommand(subCommand, method = null, json = null, data = null) { @@ -75,14 +77,29 @@ export function timestampToDateTime(timestamp) { return d.toLocaleDateString() + " " + d.toLocaleTimeString(); } -export function timestampToTime2(timestamp) { - let d = new Date(timestamp); - let hours = d.getHours(); - let minutes = "0" + d.getMinutes(); - let seconds = "0" + d.getSeconds(); +export function dateTimeToTime(dateTime) { + if (typeof dateTime === "string") { + dateTime = new Date(dateTime); + } + + let hours = dateTime.getHours(); + let minutes = "0" + dateTime.getMinutes(); + let seconds = "0" + dateTime.getSeconds(); return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2); } +export function durationBetween(from, to) { + if (typeof from === "string") { + from = new Date(from); + } + if (typeof to === "string") { + to = new Date(to); + } + const duration = ((to - from) / 1000).toFixed(3); + + return (duration > 1000 || duration < -1000) ? "∞" : duration + "s"; +} + export function formatSize(size) { if (size < 1000) { return `${size}`; diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index 64068c5..9dca7e9 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -75,7 +75,7 @@ class Connections extends Component { } this.setState({loading: true, prevParams: params}); - let res = (await backend.getJson(`${url}?${urlParams}`)).json; + let res = (await backend.get(`${url}?${urlParams}`)).json; let connections = this.state.connections; let firstConnection = this.state.firstConnection; @@ -115,7 +115,7 @@ class Connections extends Component { let flagRule = this.state.flagRule; let rules = this.state.rules; if (flagRule === null) { - rules = (await backend.getJson("/api/rules")).json; + rules = (await backend.get("/api/rules")).json; flagRule = rules.filter(rule => { return rule.name === "flag"; })[0]; @@ -170,7 +170,7 @@ class Connections extends Component { this.connectionSelected(c)} selected={this.state.selected === c.id} onMarked={marked => c.marked = marked} onEnabled={enabled => c.hidden = !enabled} - containsFlag={c.matched_rules.includes(this.state.flagRule.id)} + containsFlag={this.state.flagRule && c.matched_rules.includes(this.state.flagRule.id)} addServicePortFilter={this.addServicePortFilter}/> ) } diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js index 6f4e3cd..b9ebadb 100644 --- a/frontend/src/views/MainPane.js +++ b/frontend/src/views/MainPane.js @@ -21,7 +21,7 @@ class MainPane extends Component { const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); if (match != null) { this.setState({loading: true}); - backend.getJson(`/api/connections/${match[1]}`) + backend.get(`/api/connections/${match[1]}`) .then(res => this.setState({selectedConnection: res.json, loading: false})) .catch(error => console.log(error)); } -- cgit v1.2.3-70-g09d2