From d203f3c7e3bcaa20895c0f32f348cd1513ae9876 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Thu, 8 Oct 2020 22:17:04 +0200 Subject: Frontend folder structure refactor --- .../src/components/panels/ConfigurationPane.js | 179 --------- .../src/components/panels/ConfigurationPane.scss | 18 - frontend/src/components/panels/ConnectionsPane.js | 304 ++++++++++++++ .../src/components/panels/ConnectionsPane.scss | 38 ++ frontend/src/components/panels/MainPane.js | 47 +-- frontend/src/components/panels/MainPane.scss | 17 - frontend/src/components/panels/PcapPane.js | 273 ------------- frontend/src/components/panels/PcapPane.scss | 38 -- frontend/src/components/panels/PcapsPane.js | 273 +++++++++++++ frontend/src/components/panels/PcapsPane.scss | 38 ++ frontend/src/components/panels/RulePane.js | 438 --------------------- frontend/src/components/panels/RulePane.scss | 32 -- frontend/src/components/panels/RulesPane.js | 438 +++++++++++++++++++++ frontend/src/components/panels/RulesPane.scss | 32 ++ frontend/src/components/panels/ServicePane.js | 212 ---------- frontend/src/components/panels/ServicePane.scss | 22 -- frontend/src/components/panels/ServicesPane.js | 212 ++++++++++ frontend/src/components/panels/ServicesPane.scss | 22 ++ frontend/src/components/panels/StreamsPane.js | 242 ++++++++++++ frontend/src/components/panels/StreamsPane.scss | 113 ++++++ 20 files changed, 1718 insertions(+), 1270 deletions(-) delete mode 100644 frontend/src/components/panels/ConfigurationPane.js delete mode 100644 frontend/src/components/panels/ConfigurationPane.scss create mode 100644 frontend/src/components/panels/ConnectionsPane.js create mode 100644 frontend/src/components/panels/ConnectionsPane.scss delete mode 100644 frontend/src/components/panels/PcapPane.js delete mode 100644 frontend/src/components/panels/PcapPane.scss create mode 100644 frontend/src/components/panels/PcapsPane.js create mode 100644 frontend/src/components/panels/PcapsPane.scss delete mode 100644 frontend/src/components/panels/RulePane.js delete mode 100644 frontend/src/components/panels/RulePane.scss create mode 100644 frontend/src/components/panels/RulesPane.js create mode 100644 frontend/src/components/panels/RulesPane.scss delete mode 100644 frontend/src/components/panels/ServicePane.js delete mode 100644 frontend/src/components/panels/ServicePane.scss create mode 100644 frontend/src/components/panels/ServicesPane.js create mode 100644 frontend/src/components/panels/ServicesPane.scss create mode 100644 frontend/src/components/panels/StreamsPane.js create mode 100644 frontend/src/components/panels/StreamsPane.scss (limited to 'frontend/src/components/panels') diff --git a/frontend/src/components/panels/ConfigurationPane.js b/frontend/src/components/panels/ConfigurationPane.js deleted file mode 100644 index 9ae2cfb..0000000 --- a/frontend/src/components/panels/ConfigurationPane.js +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './common.scss'; -import './ConfigurationPane.scss'; -import LinkPopover from "../objects/LinkPopover"; -import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import TextField from "../fields/TextField"; -import ButtonField from "../fields/ButtonField"; -import CheckField from "../fields/CheckField"; -import {createCurlCommand} from "../../utils"; -import Table from "react-bootstrap/Table"; -import validation from "../../validation"; -import backend from "../../backend"; - -class ConfigurationPane extends Component { - - constructor(props) { - super(props); - this.state = { - settings: { - "config": { - "server_address": "", - "flag_regex": "", - "auth_required": false - }, - "accounts": { - } - }, - newUsername: "", - newPassword: "" - }; - } - - saveSettings = () => { - if (this.validateSettings(this.state.settings)) { - backend.post("/setup", this.state.settings).then(_ => { - this.props.onConfigured(); - }).catch(res => { - this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateSettings = (settings) => { - let valid = true; - if (!validation.isValidAddress(settings.config.server_address, true)) { - this.setState({serverAddressError: "invalid ip_address"}); - valid = false; - } - if (settings.config.flag_regex.length < 8) { - this.setState({flagRegexError: "flag_regex.length < 8"}); - valid = false; - } - - return valid; - }; - - updateParam = (callback) => { - callback(this.state.settings); - this.setState({settings: this.state.settings}); - }; - - addAccount = () => { - if (this.state.newUsername.length !== 0 && this.state.newPassword.length !== 0) { - const settings = this.state.settings; - settings.accounts[this.state.newUsername] = this.state.newPassword; - - this.setState({ - newUsername: "", - newPassword: "", - settings: settings - }); - } else { - this.setState({ - newUsernameActive: this.state.newUsername.length === 0, - newPasswordActive: this.state.newPassword.length === 0 - }); - } - }; - - render() { - const settings = this.state.settings; - const curlCommand = createCurlCommand("/setup", "POST", settings); - - const accounts = Object.entries(settings.accounts).map(([username, password]) => - - {username} - - this.updateParam((s) => delete s.accounts[username]) }/> - ).concat( - this.setState({newUsername: v})} /> - this.setState({newPassword: v})} /> - - ); - - return ( -
-
-
-
-
- POST /setup - -
- -
- - - - this.updateParam((s) => s.config.server_address = v)} /> - this.updateParam((s) => s.config.flag_regex = v)} - error={this.state.flagRegexError} /> -
- this.updateParam((s) => s.config.auth_required = v)}/> -
- - - - - accounts: -
- - - - - - - - - - {accounts} - -
usernamepasswordactions
-
- -
-
- - -
- -
- -
-
-
-
-
- ); - } -} - -export default ConfigurationPane; diff --git a/frontend/src/components/panels/ConfigurationPane.scss b/frontend/src/components/panels/ConfigurationPane.scss deleted file mode 100644 index ef48b34..0000000 --- a/frontend/src/components/panels/ConfigurationPane.scss +++ /dev/null @@ -1,18 +0,0 @@ -@import "../../colors"; - -.configuration-pane { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - background-color: $color-primary-0; - - .pane { - flex-basis: 900px; - margin-bottom: 200px; - } - - .pane-container { - padding-bottom: 1px; - } -} diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js new file mode 100644 index 0000000..038ef8f --- /dev/null +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -0,0 +1,304 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './ConnectionsPane.scss'; +import Connection from "../objects/Connection"; +import Table from 'react-bootstrap/Table'; +import {Redirect} from 'react-router'; +import {withRouter} from "react-router-dom"; +import backend from "../../backend"; +import ConnectionMatchedRules from "../objects/ConnectionMatchedRules"; +import log from "../../log"; +import ButtonField from "../fields/ButtonField"; +import dispatcher from "../../dispatcher"; + +class ConnectionsPane extends Component { + + state = { + loading: false, + connections: [], + firstConnection: null, + lastConnection: null, + }; + + constructor(props) { + super(props); + + this.scrollTopThreashold = 0.00001; + this.scrollBottomThreashold = 0.99999; + this.maxConnections = 200; + this.queryLimit = 50; + this.connectionsListRef = React.createRef(); + this.lastScrollPosition = 0; + } + + componentDidMount() { + const initialParams = {limit: this.queryLimit}; + + const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); + if (match != null) { + const id = match[1]; + initialParams.from = id; + backend.get(`/api/connections/${id}`) + .then(res => this.connectionSelected(res.json, false)) + .catch(error => log.error("Error loading initial connection", error)); + } + + this.loadConnections(initialParams, true).then(() => log.debug("Connections loaded")); + + dispatcher.register("timeline_updates", payload => { + this.connectionsListRef.current.scrollTop = 0; + this.loadConnections({ + started_after: Math.round(payload.from.getTime() / 1000), + started_before: Math.round(payload.to.getTime() / 1000), + limit: this.maxConnections + }).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`)); + }); + + dispatcher.register("notifications", payload => { + if (payload.event === "rules.new" || payload.event === "rules.edit") { + this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); + } + }); + + dispatcher.register("notifications", payload => { + if (payload.event === "services.edit") { + this.loadServices().then(() => log.debug("Services reloaded after notification update")); + } + }); + } + + connectionSelected = (c, doRedirect = true) => { + this.doSelectedConnectionRedirect = doRedirect; + this.setState({selected: c.id}); + this.props.onSelected(c); + log.debug(`Connection ${c.id} selected`); + }; + + componentDidUpdate(prevProps, prevState, snapshot) { + if (prevProps.location.search !== this.props.location.search) { + this.loadConnections({limit: this.queryLimit}) + .then(() => log.info("ConnectionsPane reloaded after query string update")); + } + } + + handleScroll = (e) => { + if (this.disableScrollHandler) { + this.lastScrollPosition = e.currentTarget.scrollTop; + return; + } + + let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight); + if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) { + this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,}) + .then(() => log.info("Following connections loaded")); + } + if (!this.state.loading && relativeScroll < this.scrollTopThreashold) { + this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,}) + .then(() => log.info("Previous connections loaded")); + if (this.state.showMoreRecentButton) { + this.setState({showMoreRecentButton: false}); + } + } else { + if (this.lastScrollPosition > e.currentTarget.scrollTop) { + if (!this.state.showMoreRecentButton) { + this.setState({showMoreRecentButton: true}); + } + } else { + if (this.state.showMoreRecentButton) { + this.setState({showMoreRecentButton: false}); + } + } + } + this.lastScrollPosition = e.currentTarget.scrollTop; + }; + + addServicePortFilter = (port) => { + const urlParams = new URLSearchParams(this.props.location.search); + urlParams.set("service_port", port); + this.doQueryStringRedirect = true; + this.setState({queryString: urlParams}); + }; + + addMatchedRulesFilter = (matchedRule) => { + const urlParams = new URLSearchParams(this.props.location.search); + const oldMatchedRules = urlParams.getAll("matched_rules") || []; + + if (!oldMatchedRules.includes(matchedRule)) { + urlParams.append("matched_rules", matchedRule); + this.doQueryStringRedirect = true; + this.setState({queryString: urlParams}); + } + }; + + async loadConnections(params, isInitial = false) { + const urlParams = new URLSearchParams(this.props.location.search); + for (const [name, value] of Object.entries(params)) { + urlParams.set(name, value); + } + + this.setState({loading: true}); + if (!this.state.rules) { + await this.loadRules(); + } + if (!this.state.services) { + await this.loadServices(); + } + + let res = (await backend.get(`/api/connections?${urlParams}`)).json; + + let connections = this.state.connections; + let firstConnection = this.state.firstConnection; + let lastConnection = this.state.lastConnection; + + if (params !== undefined && params.from !== undefined && params.to === undefined) { + if (res.length > 0) { + if (!isInitial) { + res = res.slice(1); + } + connections = this.state.connections.concat(res); + lastConnection = connections[connections.length - 1]; + if (isInitial) { + firstConnection = connections[0]; + } + if (connections.length > this.maxConnections) { + connections = connections.slice(connections.length - this.maxConnections, + connections.length - 1); + firstConnection = connections[0]; + } + } + } else if (params !== undefined && params.to !== undefined && params.from === undefined) { + if (res.length > 0) { + connections = res.slice(0, res.length - 1).concat(this.state.connections); + firstConnection = connections[0]; + if (connections.length > this.maxConnections) { + connections = connections.slice(0, this.maxConnections); + lastConnection = connections[this.maxConnections - 1]; + } + } + } else { + if (res.length > 0) { + connections = res; + firstConnection = connections[0]; + lastConnection = connections[connections.length - 1]; + } else { + connections = []; + firstConnection = null; + lastConnection = null; + } + } + + this.setState({ + loading: false, + connections: connections, + firstConnection: firstConnection, + lastConnection: lastConnection + }); + + if (firstConnection != null && lastConnection != null) { + dispatcher.dispatch("connection_updates", { + from: new Date(lastConnection["started_at"]), + to: new Date(firstConnection["started_at"]) + }); + } + } + + loadRules = async () => { + return backend.get("/api/rules").then(res => this.setState({rules: res.json})); + }; + + loadServices = async () => { + return backend.get("/api/services").then(res => this.setState({services: res.json})); + }; + + render() { + let redirect; + if (this.doSelectedConnectionRedirect) { + redirect = ; + this.doSelectedConnectionRedirect = false; + } else if (this.doQueryStringRedirect) { + redirect = ; + this.doQueryStringRedirect = false; + } + + let loading = null; + if (this.state.loading) { + loading = + Loading... + ; + } + + return ( +
+ {this.state.showMoreRecentButton &&
+ { + this.disableScrollHandler = true; + this.connectionsListRef.current.scrollTop = 0; + this.loadConnections({limit: this.queryLimit}) + .then(() => { + this.disableScrollHandler = false; + log.info("Most recent connections loaded"); + }); + }}/> +
} + +
+ + + + + + + + + + + + + + + + + { + this.state.connections.flatMap(c => { + return [ this.connectionSelected(c)} + selected={this.state.selected === c.id} + onMarked={marked => c.marked = marked} + onEnabled={enabled => c.hidden = !enabled} + addServicePortFilter={this.addServicePortFilter} + services={this.state.services}/>, + c.matched_rules.length > 0 && + + ]; + }) + } + {loading} + +
servicesrcipsrcportdstipdstportstarted_atdurationupdownactions
+ + {redirect} +
+
+ ); + } + +} + +export default withRouter(ConnectionsPane); diff --git a/frontend/src/components/panels/ConnectionsPane.scss b/frontend/src/components/panels/ConnectionsPane.scss new file mode 100644 index 0000000..06f5827 --- /dev/null +++ b/frontend/src/components/panels/ConnectionsPane.scss @@ -0,0 +1,38 @@ +@import "../../colors"; + +.connections-container { + position: relative; + height: 100%; + background-color: $color-primary-3; + + .connections { + position: relative; + overflow-y: scroll; + height: 100%; + + .table { + margin-bottom: 0; + } + + th { + font-size: 13.5px; + position: sticky; + top: 0; + padding: 5px; + border: none; + background-color: $color-primary-3; + } + + &:hover::-webkit-scrollbar-thumb { + background: $color-secondary-2; + } + } + + .most-recent-button { + position: absolute; + z-index: 20; + top: 45px; + left: calc(50% - 50px); + background-color: red; + } +} diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js index d34d58a..74c859c 100644 --- a/frontend/src/components/panels/MainPane.js +++ b/frontend/src/components/panels/MainPane.js @@ -17,57 +17,22 @@ import React, {Component} from 'react'; import './common.scss'; -import './MainPane.scss'; -import Connections from "../../views/Connections"; -import ConnectionContent from "../ConnectionContent"; -import {Route, Switch, withRouter} from "react-router-dom"; -import PcapPane from "./PcapPane"; -import backend from "../../backend"; -import RulePane from "./RulePane"; -import ServicePane from "./ServicePane"; -import log from "../../log"; +import './ServicesPane.scss'; class MainPane extends Component { state = {}; - componentDidMount() { - const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); - if (match != null) { - this.loading = true; - backend.get(`/api/connections/${match[1]}`) - .then(res => { - this.loading = false; - this.setState({selectedConnection: res.json}); - log.debug(`Initial connection ${match[1]} loaded`); - }) - .catch(error => log.error("Error loading initial connection", error)); - } - } - render() { return ( -
-
- { - !this.loading && - this.setState({selectedConnection: c})} - initialConnection={this.state.selectedConnection}/> - } -
-
- - }/> - }/> - }/> - }/> - }/> - +
+
+ MainPane
); } + } -export default withRouter(MainPane); +export default MainPane; diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss index 2973c00..c8460f2 100644 --- a/frontend/src/components/panels/MainPane.scss +++ b/frontend/src/components/panels/MainPane.scss @@ -1,22 +1,5 @@ @import "../../colors"; .main-pane { - display: flex; - height: 100%; - padding: 0 15px; - background-color: $color-primary-2; - .pane { - flex: 1; - } - - .connections-pane { - flex: 1 0; - margin-right: 7.5px; - } - - .details-pane { - flex: 1 1; - margin-left: 7.5px; - } } diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js deleted file mode 100644 index d5c2225..0000000 --- a/frontend/src/components/panels/PcapPane.js +++ /dev/null @@ -1,273 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './PcapPane.scss'; -import './common.scss'; -import Table from "react-bootstrap/Table"; -import backend from "../../backend"; -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"; -import dispatcher from "../../dispatcher"; - -class PcapPane extends Component { - - state = { - sessions: [], - isUploadFileValid: true, - isUploadFileFocused: false, - uploadFlushAll: false, - isFileValid: true, - isFileFocused: false, - fileValue: "", - processFlushAll: false, - deleteOriginalFile: false - }; - - componentDidMount() { - this.loadSessions(); - - dispatcher.register("notifications", payload => { - if (payload.event === "pcap.upload" || payload.event === "pcap.file") { - this.loadSessions(); - } - }); - - document.title = "caronte:~/pcaps$"; - } - - loadSessions = () => { - 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) - })); - }; - - uploadPcap = () => { - if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) { - this.setState({isUploadFileFocused: true}); - return; - } - - const formData = new FormData(); - 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) - }) - ); - }; - - processPcap = () => { - if (this.state.fileValue === "" || !this.state.isFileValid) { - this.setState({isFileFocused: true}); - return; - } - - backend.post("/api/pcap/file", { - file: this.state.fileValue, - flush_all: this.state.processFlushAll, - delete_original_file: this.state.deleteOriginalFile - }).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)} - {dateTimeToTime(s["started_at"])} - {durationBetween(s["started_at"], s["completed_at"])} - {formatSize(s["size"])} - {s["processed_packets"]} - {s["invalid_packets"]} - - download - - - ); - - 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 - -
- -
-
- - - - - - - - - - - - - - - {sessions} - -
idstarted_atdurationsizeprocessed_packetsinvalid_packetspackets_per_serviceactions
-
-
-
- -
-
-
- POST /api/pcap/upload - -
- -
- -
-
- options: - this.setState({uploadFlushAll: v})}/> -
- -
- - -
-
- -
-
- POST /api/pcap/file - -
- -
- - -
-
- this.setState({processFlushAll: v})}/> - this.setState({deleteOriginalFile: v})}/> -
- -
- - -
-
-
-
- ); - } -} - -export default PcapPane; diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss deleted file mode 100644 index 4dbc2b2..0000000 --- a/frontend/src/components/panels/PcapPane.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import "../../colors.scss"; - -.pcap-pane { - display: flex; - flex-direction: column; - - .pcap-list { - overflow: hidden; - flex: 1; - - .section-content { - height: 100%; - } - - .section-table { - height: calc(100% - 30px); - } - - .table-cell-action { - font-size: 13px; - font-weight: 600; - } - } - - .upload-actions { - display: flex; - align-items: flex-end; - margin-bottom: 20px; - } - - .upload-options { - flex: 1; - - span { - font-size: 0.9em; - } - } -} diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js new file mode 100644 index 0000000..8722230 --- /dev/null +++ b/frontend/src/components/panels/PcapsPane.js @@ -0,0 +1,273 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './PcapsPane.scss'; +import './common.scss'; +import Table from "react-bootstrap/Table"; +import backend from "../../backend"; +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"; +import dispatcher from "../../dispatcher"; + +class PcapsPane extends Component { + + state = { + sessions: [], + isUploadFileValid: true, + isUploadFileFocused: false, + uploadFlushAll: false, + isFileValid: true, + isFileFocused: false, + fileValue: "", + processFlushAll: false, + deleteOriginalFile: false + }; + + componentDidMount() { + this.loadSessions(); + + dispatcher.register("notifications", payload => { + if (payload.event === "pcap.upload" || payload.event === "pcap.file") { + this.loadSessions(); + } + }); + + document.title = "caronte:~/pcaps$"; + } + + loadSessions = () => { + 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) + })); + }; + + uploadPcap = () => { + if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) { + this.setState({isUploadFileFocused: true}); + return; + } + + const formData = new FormData(); + 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) + }) + ); + }; + + processPcap = () => { + if (this.state.fileValue === "" || !this.state.isFileValid) { + this.setState({isFileFocused: true}); + return; + } + + backend.post("/api/pcap/file", { + file: this.state.fileValue, + flush_all: this.state.processFlushAll, + delete_original_file: this.state.deleteOriginalFile + }).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)} + {dateTimeToTime(s["started_at"])} + {durationBetween(s["started_at"], s["completed_at"])} + {formatSize(s["size"])} + {s["processed_packets"]} + {s["invalid_packets"]} + + download + + + ); + + 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 + +
+ +
+
+ + + + + + + + + + + + + + + {sessions} + +
idstarted_atdurationsizeprocessed_packetsinvalid_packetspackets_per_serviceactions
+
+
+
+ +
+
+
+ POST /api/pcap/upload + +
+ +
+ +
+
+ options: + this.setState({uploadFlushAll: v})}/> +
+ +
+ + +
+
+ +
+
+ POST /api/pcap/file + +
+ +
+ + +
+
+ this.setState({processFlushAll: v})}/> + this.setState({deleteOriginalFile: v})}/> +
+ +
+ + +
+
+
+
+ ); + } +} + +export default PcapsPane; diff --git a/frontend/src/components/panels/PcapsPane.scss b/frontend/src/components/panels/PcapsPane.scss new file mode 100644 index 0000000..4dbc2b2 --- /dev/null +++ b/frontend/src/components/panels/PcapsPane.scss @@ -0,0 +1,38 @@ +@import "../../colors.scss"; + +.pcap-pane { + display: flex; + flex-direction: column; + + .pcap-list { + overflow: hidden; + flex: 1; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + + .table-cell-action { + font-size: 13px; + font-weight: 600; + } + } + + .upload-actions { + display: flex; + align-items: flex-end; + margin-bottom: 20px; + } + + .upload-options { + flex: 1; + + span { + font-size: 0.9em; + } + } +} diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js deleted file mode 100644 index 9913962..0000000 --- a/frontend/src/components/panels/RulePane.js +++ /dev/null @@ -1,438 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './common.scss'; -import './RulePane.scss'; -import Table from "react-bootstrap/Table"; -import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import CheckField from "../fields/CheckField"; -import TextField from "../fields/TextField"; -import backend from "../../backend"; -import NumericField from "../fields/extensions/NumericField"; -import ColorField from "../fields/extensions/ColorField"; -import ChoiceField from "../fields/ChoiceField"; -import ButtonField from "../fields/ButtonField"; -import validation from "../../validation"; -import LinkPopover from "../objects/LinkPopover"; -import {randomClassName} from "../../utils"; -import dispatcher from "../../dispatcher"; - -const classNames = require('classnames'); -const _ = require('lodash'); - -class RulePane extends Component { - - emptyRule = { - "name": "", - "color": "", - "notes": "", - "enabled": true, - "patterns": [], - "filter": { - "service_port": 0, - "client_address": "", - "client_port": 0, - "min_duration": 0, - "max_duration": 0, - "min_bytes": 0, - "max_bytes": 0 - }, - "version": 0 - }; - emptyPattern = { - "regex": "", - "flags": { - "caseless": false, - "dot_all": false, - "multi_line": false, - "utf_8_mode": false, - "unicode_property": false - }, - "min_occurrences": 0, - "max_occurrences": 0, - "direction": 0 - }; - state = { - rules: [], - newRule: this.emptyRule, - newPattern: this.emptyPattern - }; - - constructor(props) { - super(props); - - this.directions = { - 0: "both", - 1: "c->s", - 2: "s->c" - }; - } - - componentDidMount() { - this.reset(); - this.loadRules(); - - dispatcher.register("notifications", payload => { - if (payload.event === "rules.new" || payload.event === "rules.edit") { - this.loadRules(); - } - }); - - document.title = "caronte:~/rules$"; - } - - loadRules = () => { - 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.post("/api/rules", this.state.newRule).then(res => { - this.reset(); - this.setState({ruleStatusCode: res.status}); - this.loadRules(); - }).catch(res => { - this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); - }); - } - }; - - updateRule = () => { - const rule = this.state.selectedRule; - if (this.validateRule(rule)) { - backend.put(`/api/rules/${rule.id}`, rule).then(res => { - this.reset(); - this.setState({ruleStatusCode: res.status}); - this.loadRules(); - }).catch(res => { - this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateRule = (rule) => { - let valid = true; - if (rule.name.length < 3) { - this.setState({ruleNameError: "name.length < 3"}); - valid = false; - } - if (!validation.isValidColor(rule.color)) { - this.setState({ruleColorError: "color is not hexcolor"}); - valid = false; - } - if (!validation.isValidPort(rule.filter.service_port)) { - this.setState({ruleServicePortError: "service_port > 65565"}); - valid = false; - } - if (!validation.isValidPort(rule.filter.client_port)) { - this.setState({ruleClientPortError: "client_port > 65565"}); - valid = false; - } - if (!validation.isValidAddress(rule.filter.client_address)) { - this.setState({ruleClientAddressError: "client_address is not ip_address"}); - valid = false; - } - if (rule.filter.min_duration > rule.filter.max_duration) { - this.setState({ruleDurationError: "min_duration > max_dur."}); - valid = false; - } - if (rule.filter.min_bytes > rule.filter.max_bytes) { - this.setState({ruleBytesError: "min_bytes > max_bytes"}); - valid = false; - } - if (rule.patterns.length < 1) { - this.setState({rulePatternsError: "patterns.length < 1"}); - valid = false; - } - - return valid; - }; - - reset = () => { - const newRule = _.cloneDeep(this.emptyRule); - const newPattern = _.cloneDeep(this.emptyPattern); - this.setState({ - selectedRule: null, - newRule: newRule, - selectedPattern: null, - newPattern: newPattern, - patternRegexFocused: false, - patternOccurrencesFocused: false, - ruleNameError: null, - ruleColorError: null, - ruleServicePortError: null, - ruleClientPortError: null, - ruleClientAddressError: null, - ruleDurationError: null, - ruleBytesError: null, - rulePatternsError: null, - ruleStatusCode: null, - rulesStatusCode: null, - ruleResponse: null, - rulesResponse: null - }); - }; - - updateParam = (callback) => { - const updatedRule = this.currentRule(); - callback(updatedRule); - this.setState({newRule: updatedRule}); - }; - - currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule; - - addPattern = (pattern) => { - if (!this.validatePattern(pattern)) { - return; - } - - const newPattern = _.cloneDeep(this.emptyPattern); - this.currentRule().patterns.push(pattern); - this.setState({ - newPattern: newPattern - }); - }; - - editPattern = (pattern) => { - this.setState({ - selectedPattern: pattern - }); - }; - - updatePattern = (pattern) => { - if (!this.validatePattern(pattern)) { - return; - } - - this.setState({ - selectedPattern: null - }); - }; - - validatePattern = (pattern) => { - let valid = true; - if (pattern.regex === "") { - valid = false; - this.setState({patternRegexFocused: true}); - } - if (pattern.min_occurrences > pattern.max_occurrences) { - valid = false; - this.setState({patternOccurrencesFocused: true}); - } - return valid; - }; - - render() { - const isUpdate = this.state.selectedRule != null; - const rule = this.currentRule(); - const pattern = this.state.selectedPattern || this.state.newPattern; - - let rules = this.state.rules.map(r => - { - this.reset(); - this.setState({selectedRule: _.cloneDeep(r)}); - }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}> - {r["id"].substring(0, 8)} - {r["name"]} - - {r["notes"]} - - ); - - let patterns = (this.state.selectedPattern == null && !isUpdate ? - rule.patterns.concat(this.state.newPattern) : - rule.patterns - ).map(p => p === pattern ? - - - { - this.updateParam(() => pattern.regex = v); - this.setState({patternRegexFocused: pattern.regex === ""}); - }}/> - - this.updateParam(() => pattern.flags.caseless = v)}/> - this.updateParam(() => pattern.flags.dot_all = v)}/> - this.updateParam(() => pattern.flags.multi_line = v)}/> - this.updateParam(() => pattern.flags.utf_8_mode = v)}/> - this.updateParam(() => pattern.flags.unicode_property = v)}/> - - this.updateParam(() => pattern.min_occurrences = v)}/> - - - this.updateParam(() => pattern.max_occurrences = v)}/> - - s", "s->c"]} - value={this.directions[pattern.direction]} - onChange={(v) => this.updateParam(() => pattern.direction = v)}/> - {this.state.selectedPattern == null ? - this.addPattern(p)}/> : - this.updatePattern(p)}/>} - - - : - - {p.regex} - {p.flags.caseless ? "yes" : "no"} - {p.flags.dot_all ? "yes" : "no"} - {p.flags.multi_line ? "yes" : "no"} - {p.flags.utf_8_mode ? "yes" : "no"} - {p.flags.unicode_property ? "yes" : "no"} - {p.min_occurrences} - {p.max_occurrences} - {this.directions[p.direction]} - {!isUpdate && this.editPattern(p)}/>} - - ); - - return ( -
-
-
- GET /api/rules - {this.state.rulesStatusCode && - } -
- -
-
- - - - - - - - - - - {rules} - -
idnamecolornotes
-
-
-
- -
-
- - {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} - - -
- -
- - - - this.updateParam((r) => r.name = v)} - error={this.state.ruleNameError}/> - this.updateParam((r) => r.color = v)}/> - this.updateParam((r) => r.notes = v)}/> - - - - filters: - this.updateParam((r) => r.filter.service_port = v)} - min={0} max={65565} error={this.state.ruleServicePortError} - readonly={isUpdate}/> - this.updateParam((r) => r.filter.client_port = v)} - min={0} max={65565} error={this.state.ruleClientPortError} - readonly={isUpdate}/> - this.updateParam((r) => r.filter.client_address = v)}/> - - - - this.updateParam((r) => r.filter.min_duration = v)}/> - this.updateParam((r) => r.filter.max_duration = v)}/> - this.updateParam((r) => r.filter.min_bytes = v)}/> - this.updateParam((r) => r.filter.max_bytes = v)}/> - - - - -
- - - - - - - - - - - - - {!isUpdate && } - - - - {patterns} - -
regex!Aa.*\n+UTF8Uni_minmaxdirectionactions
- {this.state.rulePatternsError != null && - error: {this.state.rulePatternsError}} -
-
- -
- {} - -
-
-
- ); - } - -} - -export default RulePane; diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss deleted file mode 100644 index 992445a..0000000 --- a/frontend/src/components/panels/RulePane.scss +++ /dev/null @@ -1,32 +0,0 @@ - -.rule-pane { - display: flex; - flex-direction: column; - - .rules-list { - overflow: hidden; - flex: 2 1; - - .section-content { - height: 100%; - } - - .section-table { - height: calc(100% - 30px); - } - } - - .rule-edit { - display: flex; - flex: 3 0; - flex-direction: column; - - .section-content { - flex: 1; - } - - .section-table { - max-height: 150px; - } - } -} diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js new file mode 100644 index 0000000..a66cde7 --- /dev/null +++ b/frontend/src/components/panels/RulesPane.js @@ -0,0 +1,438 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './common.scss'; +import './RulesPane.scss'; +import Table from "react-bootstrap/Table"; +import {Col, Container, Row} from "react-bootstrap"; +import InputField from "../fields/InputField"; +import CheckField from "../fields/CheckField"; +import TextField from "../fields/TextField"; +import backend from "../../backend"; +import NumericField from "../fields/extensions/NumericField"; +import ColorField from "../fields/extensions/ColorField"; +import ChoiceField from "../fields/ChoiceField"; +import ButtonField from "../fields/ButtonField"; +import validation from "../../validation"; +import LinkPopover from "../objects/LinkPopover"; +import {randomClassName} from "../../utils"; +import dispatcher from "../../dispatcher"; + +const classNames = require('classnames'); +const _ = require('lodash'); + +class RulesPane extends Component { + + emptyRule = { + "name": "", + "color": "", + "notes": "", + "enabled": true, + "patterns": [], + "filter": { + "service_port": 0, + "client_address": "", + "client_port": 0, + "min_duration": 0, + "max_duration": 0, + "min_bytes": 0, + "max_bytes": 0 + }, + "version": 0 + }; + emptyPattern = { + "regex": "", + "flags": { + "caseless": false, + "dot_all": false, + "multi_line": false, + "utf_8_mode": false, + "unicode_property": false + }, + "min_occurrences": 0, + "max_occurrences": 0, + "direction": 0 + }; + state = { + rules: [], + newRule: this.emptyRule, + newPattern: this.emptyPattern + }; + + constructor(props) { + super(props); + + this.directions = { + 0: "both", + 1: "c->s", + 2: "s->c" + }; + } + + componentDidMount() { + this.reset(); + this.loadRules(); + + dispatcher.register("notifications", payload => { + if (payload.event === "rules.new" || payload.event === "rules.edit") { + this.loadRules(); + } + }); + + document.title = "caronte:~/rules$"; + } + + loadRules = () => { + 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.post("/api/rules", this.state.newRule).then(res => { + this.reset(); + this.setState({ruleStatusCode: res.status}); + this.loadRules(); + }).catch(res => { + this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); + }); + } + }; + + updateRule = () => { + const rule = this.state.selectedRule; + if (this.validateRule(rule)) { + backend.put(`/api/rules/${rule.id}`, rule).then(res => { + this.reset(); + this.setState({ruleStatusCode: res.status}); + this.loadRules(); + }).catch(res => { + this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateRule = (rule) => { + let valid = true; + if (rule.name.length < 3) { + this.setState({ruleNameError: "name.length < 3"}); + valid = false; + } + if (!validation.isValidColor(rule.color)) { + this.setState({ruleColorError: "color is not hexcolor"}); + valid = false; + } + if (!validation.isValidPort(rule.filter.service_port)) { + this.setState({ruleServicePortError: "service_port > 65565"}); + valid = false; + } + if (!validation.isValidPort(rule.filter.client_port)) { + this.setState({ruleClientPortError: "client_port > 65565"}); + valid = false; + } + if (!validation.isValidAddress(rule.filter.client_address)) { + this.setState({ruleClientAddressError: "client_address is not ip_address"}); + valid = false; + } + if (rule.filter.min_duration > rule.filter.max_duration) { + this.setState({ruleDurationError: "min_duration > max_dur."}); + valid = false; + } + if (rule.filter.min_bytes > rule.filter.max_bytes) { + this.setState({ruleBytesError: "min_bytes > max_bytes"}); + valid = false; + } + if (rule.patterns.length < 1) { + this.setState({rulePatternsError: "patterns.length < 1"}); + valid = false; + } + + return valid; + }; + + reset = () => { + const newRule = _.cloneDeep(this.emptyRule); + const newPattern = _.cloneDeep(this.emptyPattern); + this.setState({ + selectedRule: null, + newRule: newRule, + selectedPattern: null, + newPattern: newPattern, + patternRegexFocused: false, + patternOccurrencesFocused: false, + ruleNameError: null, + ruleColorError: null, + ruleServicePortError: null, + ruleClientPortError: null, + ruleClientAddressError: null, + ruleDurationError: null, + ruleBytesError: null, + rulePatternsError: null, + ruleStatusCode: null, + rulesStatusCode: null, + ruleResponse: null, + rulesResponse: null + }); + }; + + updateParam = (callback) => { + const updatedRule = this.currentRule(); + callback(updatedRule); + this.setState({newRule: updatedRule}); + }; + + currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule; + + addPattern = (pattern) => { + if (!this.validatePattern(pattern)) { + return; + } + + const newPattern = _.cloneDeep(this.emptyPattern); + this.currentRule().patterns.push(pattern); + this.setState({ + newPattern: newPattern + }); + }; + + editPattern = (pattern) => { + this.setState({ + selectedPattern: pattern + }); + }; + + updatePattern = (pattern) => { + if (!this.validatePattern(pattern)) { + return; + } + + this.setState({ + selectedPattern: null + }); + }; + + validatePattern = (pattern) => { + let valid = true; + if (pattern.regex === "") { + valid = false; + this.setState({patternRegexFocused: true}); + } + if (pattern.min_occurrences > pattern.max_occurrences) { + valid = false; + this.setState({patternOccurrencesFocused: true}); + } + return valid; + }; + + render() { + const isUpdate = this.state.selectedRule != null; + const rule = this.currentRule(); + const pattern = this.state.selectedPattern || this.state.newPattern; + + let rules = this.state.rules.map(r => + { + this.reset(); + this.setState({selectedRule: _.cloneDeep(r)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}> + {r["id"].substring(0, 8)} + {r["name"]} + + {r["notes"]} + + ); + + let patterns = (this.state.selectedPattern == null && !isUpdate ? + rule.patterns.concat(this.state.newPattern) : + rule.patterns + ).map(p => p === pattern ? + + + { + this.updateParam(() => pattern.regex = v); + this.setState({patternRegexFocused: pattern.regex === ""}); + }}/> + + this.updateParam(() => pattern.flags.caseless = v)}/> + this.updateParam(() => pattern.flags.dot_all = v)}/> + this.updateParam(() => pattern.flags.multi_line = v)}/> + this.updateParam(() => pattern.flags.utf_8_mode = v)}/> + this.updateParam(() => pattern.flags.unicode_property = v)}/> + + this.updateParam(() => pattern.min_occurrences = v)}/> + + + this.updateParam(() => pattern.max_occurrences = v)}/> + + s", "s->c"]} + value={this.directions[pattern.direction]} + onChange={(v) => this.updateParam(() => pattern.direction = v)}/> + {this.state.selectedPattern == null ? + this.addPattern(p)}/> : + this.updatePattern(p)}/>} + + + : + + {p.regex} + {p.flags.caseless ? "yes" : "no"} + {p.flags.dot_all ? "yes" : "no"} + {p.flags.multi_line ? "yes" : "no"} + {p.flags.utf_8_mode ? "yes" : "no"} + {p.flags.unicode_property ? "yes" : "no"} + {p.min_occurrences} + {p.max_occurrences} + {this.directions[p.direction]} + {!isUpdate && this.editPattern(p)}/>} + + ); + + return ( +
+
+
+ GET /api/rules + {this.state.rulesStatusCode && + } +
+ +
+
+ + + + + + + + + + + {rules} + +
idnamecolornotes
+
+
+
+ +
+
+ + {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} + + +
+ +
+ + + + this.updateParam((r) => r.name = v)} + error={this.state.ruleNameError}/> + this.updateParam((r) => r.color = v)}/> + this.updateParam((r) => r.notes = v)}/> + + + + filters: + this.updateParam((r) => r.filter.service_port = v)} + min={0} max={65565} error={this.state.ruleServicePortError} + readonly={isUpdate}/> + this.updateParam((r) => r.filter.client_port = v)} + min={0} max={65565} error={this.state.ruleClientPortError} + readonly={isUpdate}/> + this.updateParam((r) => r.filter.client_address = v)}/> + + + + this.updateParam((r) => r.filter.min_duration = v)}/> + this.updateParam((r) => r.filter.max_duration = v)}/> + this.updateParam((r) => r.filter.min_bytes = v)}/> + this.updateParam((r) => r.filter.max_bytes = v)}/> + + + + +
+ + + + + + + + + + + + + {!isUpdate && } + + + + {patterns} + +
regex!Aa.*\n+UTF8Uni_minmaxdirectionactions
+ {this.state.rulePatternsError != null && + error: {this.state.rulePatternsError}} +
+
+ +
+ {} + +
+
+
+ ); + } + +} + +export default RulesPane; diff --git a/frontend/src/components/panels/RulesPane.scss b/frontend/src/components/panels/RulesPane.scss new file mode 100644 index 0000000..992445a --- /dev/null +++ b/frontend/src/components/panels/RulesPane.scss @@ -0,0 +1,32 @@ + +.rule-pane { + display: flex; + flex-direction: column; + + .rules-list { + overflow: hidden; + flex: 2 1; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + } + + .rule-edit { + display: flex; + flex: 3 0; + flex-direction: column; + + .section-content { + flex: 1; + } + + .section-table { + max-height: 150px; + } + } +} diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js deleted file mode 100644 index fc7004b..0000000 --- a/frontend/src/components/panels/ServicePane.js +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of caronte (https://github.com/eciavatta/caronte). - * Copyright (c) 2020 Emiliano Ciavatta. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, {Component} from 'react'; -import './common.scss'; -import './ServicePane.scss'; -import Table from "react-bootstrap/Table"; -import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import TextField from "../fields/TextField"; -import backend from "../../backend"; -import NumericField from "../fields/extensions/NumericField"; -import ColorField from "../fields/extensions/ColorField"; -import ButtonField from "../fields/ButtonField"; -import validation from "../../validation"; -import LinkPopover from "../objects/LinkPopover"; -import {createCurlCommand} from "../../utils"; -import dispatcher from "../../dispatcher"; - -const classNames = require('classnames'); -const _ = require('lodash'); - -class ServicePane extends Component { - - emptyService = { - "port": 0, - "name": "", - "color": "", - "notes": "" - }; - - state = { - services: [], - currentService: this.emptyService, - }; - - componentDidMount() { - this.reset(); - this.loadServices(); - - dispatcher.register("notifications", payload => { - if (payload.event === "services.edit") { - this.loadServices(); - } - }); - - document.title = "caronte:~/services$"; - } - - loadServices = () => { - backend.get("/api/services") - .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status})) - .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)})); - }; - - updateService = () => { - const service = this.state.currentService; - if (this.validateService(service)) { - backend.put("/api/services", service).then(res => { - this.reset(); - this.setState({serviceStatusCode: res.status}); - this.loadServices(); - }).catch(res => { - this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)}); - }); - } - }; - - validateService = (service) => { - let valid = true; - if (!validation.isValidPort(service.port, true)) { - this.setState({servicePortError: "port < 0 || port > 65565"}); - valid = false; - } - if (service.name.length < 3) { - this.setState({serviceNameError: "name.length < 3"}); - valid = false; - } - if (!validation.isValidColor(service.color)) { - this.setState({serviceColorError: "color is not hexcolor"}); - valid = false; - } - - return valid; - }; - - reset = () => { - this.setState({ - isUpdate: false, - currentService: _.cloneDeep(this.emptyService), - servicePortError: null, - serviceNameError: null, - serviceColorError: null, - serviceStatusCode: null, - servicesStatusCode: null, - serviceResponse: null, - servicesResponse: null - }); - }; - - updateParam = (callback) => { - callback(this.state.currentService); - this.setState({currentService: this.state.currentService}); - }; - - render() { - const isUpdate = this.state.isUpdate; - const service = this.state.currentService; - - let services = this.state.services.map(s => - { - this.reset(); - this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); - }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}> - {s["port"]} - {s["name"]} - - {s["notes"]} - - ); - - const curlCommand = createCurlCommand("/services", "PUT", service); - - return ( -
-
-
- GET /api/services - {this.state.servicesStatusCode && - } -
- -
-
- - - - - - - - - - - {services} - -
portnamecolornotes
-
-
-
- -
-
- PUT /api/services - -
- -
- - - - this.updateParam((s) => s.port = v)} - min={0} max={65565} error={this.state.servicePortError} /> - this.updateParam((s) => s.name = v)} - error={this.state.serviceNameError} /> - this.updateParam((s) => s.color = v)} /> - - - - this.updateParam((s) => s.notes = v)} /> - - - - - -
- -
- {} - -
-
-
- ); - } - -} - -export default ServicePane; diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicePane.scss deleted file mode 100644 index daf7e79..0000000 --- a/frontend/src/components/panels/ServicePane.scss +++ /dev/null @@ -1,22 +0,0 @@ - -.service-pane { - display: flex; - flex-direction: column; - - .services-list { - overflow: hidden; - flex: 1; - - .section-content { - height: 100%; - } - - .section-table { - height: calc(100% - 30px); - } - } - - .service-edit { - flex: 0; - } -} diff --git a/frontend/src/components/panels/ServicesPane.js b/frontend/src/components/panels/ServicesPane.js new file mode 100644 index 0000000..bc82356 --- /dev/null +++ b/frontend/src/components/panels/ServicesPane.js @@ -0,0 +1,212 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './common.scss'; +import './ServicesPane.scss'; +import Table from "react-bootstrap/Table"; +import {Col, Container, Row} from "react-bootstrap"; +import InputField from "../fields/InputField"; +import TextField from "../fields/TextField"; +import backend from "../../backend"; +import NumericField from "../fields/extensions/NumericField"; +import ColorField from "../fields/extensions/ColorField"; +import ButtonField from "../fields/ButtonField"; +import validation from "../../validation"; +import LinkPopover from "../objects/LinkPopover"; +import {createCurlCommand} from "../../utils"; +import dispatcher from "../../dispatcher"; + +const classNames = require('classnames'); +const _ = require('lodash'); + +class ServicesPane extends Component { + + emptyService = { + "port": 0, + "name": "", + "color": "", + "notes": "" + }; + + state = { + services: [], + currentService: this.emptyService, + }; + + componentDidMount() { + this.reset(); + this.loadServices(); + + dispatcher.register("notifications", payload => { + if (payload.event === "services.edit") { + this.loadServices(); + } + }); + + document.title = "caronte:~/services$"; + } + + loadServices = () => { + backend.get("/api/services") + .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status})) + .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)})); + }; + + updateService = () => { + const service = this.state.currentService; + if (this.validateService(service)) { + backend.put("/api/services", service).then(res => { + this.reset(); + this.setState({serviceStatusCode: res.status}); + this.loadServices(); + }).catch(res => { + this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateService = (service) => { + let valid = true; + if (!validation.isValidPort(service.port, true)) { + this.setState({servicePortError: "port < 0 || port > 65565"}); + valid = false; + } + if (service.name.length < 3) { + this.setState({serviceNameError: "name.length < 3"}); + valid = false; + } + if (!validation.isValidColor(service.color)) { + this.setState({serviceColorError: "color is not hexcolor"}); + valid = false; + } + + return valid; + }; + + reset = () => { + this.setState({ + isUpdate: false, + currentService: _.cloneDeep(this.emptyService), + servicePortError: null, + serviceNameError: null, + serviceColorError: null, + serviceStatusCode: null, + servicesStatusCode: null, + serviceResponse: null, + servicesResponse: null + }); + }; + + updateParam = (callback) => { + callback(this.state.currentService); + this.setState({currentService: this.state.currentService}); + }; + + render() { + const isUpdate = this.state.isUpdate; + const service = this.state.currentService; + + let services = this.state.services.map(s => + { + this.reset(); + this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}> + {s["port"]} + {s["name"]} + + {s["notes"]} + + ); + + const curlCommand = createCurlCommand("/services", "PUT", service); + + return ( +
+
+
+ GET /api/services + {this.state.servicesStatusCode && + } +
+ +
+
+ + + + + + + + + + + {services} + +
portnamecolornotes
+
+
+
+ +
+
+ PUT /api/services + +
+ +
+ + + + this.updateParam((s) => s.port = v)} + min={0} max={65565} error={this.state.servicePortError} /> + this.updateParam((s) => s.name = v)} + error={this.state.serviceNameError} /> + this.updateParam((s) => s.color = v)} /> + + + + this.updateParam((s) => s.notes = v)} /> + + + + + +
+ +
+ {} + +
+
+
+ ); + } + +} + +export default ServicesPane; diff --git a/frontend/src/components/panels/ServicesPane.scss b/frontend/src/components/panels/ServicesPane.scss new file mode 100644 index 0000000..daf7e79 --- /dev/null +++ b/frontend/src/components/panels/ServicesPane.scss @@ -0,0 +1,22 @@ + +.service-pane { + display: flex; + flex-direction: column; + + .services-list { + overflow: hidden; + flex: 1; + + .section-content { + height: 100%; + } + + .section-table { + height: calc(100% - 30px); + } + } + + .service-edit { + flex: 0; + } +} diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js new file mode 100644 index 0000000..c8bd121 --- /dev/null +++ b/frontend/src/components/panels/StreamsPane.js @@ -0,0 +1,242 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import React, {Component} from 'react'; +import './StreamsPane.scss'; +import {Row} from 'react-bootstrap'; +import MessageAction from "../objects/MessageAction"; +import backend from "../../backend"; +import ButtonField from "../fields/ButtonField"; +import ChoiceField from "../fields/ChoiceField"; +import DOMPurify from 'dompurify'; +import ReactJson from 'react-json-view' +import {downloadBlob, getHeaderValue} from "../../utils"; +import log from "../../log"; + +const classNames = require('classnames'); + +class StreamsPane extends Component { + + state = { + messages: [], + format: "default", + tryParse: true + }; + + constructor(props) { + super(props); + + this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]; + } + + componentDidMount() { + if (this.props.connection && this.state.currentId !== this.props.connection.id) { + this.setState({currentId: this.props.connection.id}); + this.loadStream(this.props.connection.id); + } + + document.title = "caronte:~/$"; + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (this.props.connection && ( + this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) { + this.closeRenderWindow(); + this.loadStream(this.props.connection.id); + } + } + + componentWillUnmount() { + this.closeRenderWindow(); + } + + loadStream = (connectionId) => { + this.setState({messages: []}); + backend.get(`/api/streams/${connectionId}?format=${this.state.format}`) + .then(res => this.setState({messages: res.json})); + }; + + setFormat = (format) => { + if (this.validFormats.includes(format)) { + this.setState({format: format}); + } + }; + + tryParseConnectionMessage = (connectionMessage) => { + if (connectionMessage.metadata == null) { + return connectionMessage.content; + } + if (connectionMessage["is_metadata_continuation"]) { + return **already parsed in previous messages**; + } + + let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) => +

{key}: {value}

+ ); + + let m = connectionMessage.metadata; + switch (m.type) { + case "http-request": + let url = {m.host}{m.url}; + return +

{m.method} {url} {m.protocol}

+ {unrollMap(m.headers)} +
{m.body}
+ {unrollMap(m.trailers)} +
; + case "http-response": + const contentType = getHeaderValue(m, "Content-Type"); + let body = m.body; + if (contentType && contentType.includes("application/json")) { + try { + const json = JSON.parse(m.body); + body = ; + } catch (e) { + console.log(e); + } + } + + return +

{m.protocol} {m.status}

+ {unrollMap(m.headers)} +
{body}
+ {unrollMap(m.trailers)} +
; + default: + return connectionMessage.content; + } + }; + + connectionsActions = (connectionMessage) => { + if (!connectionMessage.metadata) { + return null; + } + + const m = connectionMessage.metadata; + switch (m.type) { + case "http-request" : + if (!connectionMessage.metadata["reproducers"]) { + return; + } + return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) => + { + this.setState({ + messageActionDialog: this.setState({messageActionDialog: null})}/> + }); + }}/> + ); + case "http-response": + const contentType = getHeaderValue(m, "Content-Type"); + + if (contentType && contentType.includes("text/html")) { + return { + let w; + if (this.state.renderWindow && !this.state.renderWindow.closed) { + w = this.state.renderWindow; + } else { + w = window.open("", "", "width=900, height=600, scrollbars=yes"); + this.setState({renderWindow: w}); + } + w.document.body.innerHTML = DOMPurify.sanitize(m.body); + w.focus(); + }}/>; + } + break; + default: + return null; + } + }; + + downloadStreamRaw = (value) => { + if (this.state.currentId) { + backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`) + .then(res => downloadBlob(res.blob, `${this.state.currentId}-${value}-${this.state.format}.txt`)) + .catch(_ => log.error("Failed to download stream messages")); + } + }; + + closeRenderWindow = () => { + if (this.state.renderWindow) { + this.state.renderWindow.close(); + } + }; + + render() { + const conn = this.props.connection || { + "ip_src": "0.0.0.0", + "ip_dst": "0.0.0.0", + "port_src": "0", + "port_dst": "0", + "started_at": new Date().toISOString(), + }; + const content = this.state.messages || []; + + let payload = content.map((c, i) => +
+
+
+
+ offset: {c.index} | timestamp: {c.timestamp} + | retransmitted: {c["is_retransmitted"] ? "yes" : "no"} +
+
{this.connectionsActions(c)}
+
+
+
{c["from_client"] ? "client" : "server"}
+
+ {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content} +
+
+ ); + + return ( +
+
+ +
+ flow: {conn["ip_src"]}:{conn["port_src"]} -> {conn["ip_dst"]}:{conn["port_dst"]} + | timestamp: {conn["started_at"]} +
+
+ + + + + +
+
+
+ +
{payload}
+ {this.state.messageActionDialog} +
+ ); + } + +} + + +export default StreamsPane; diff --git a/frontend/src/components/panels/StreamsPane.scss b/frontend/src/components/panels/StreamsPane.scss new file mode 100644 index 0000000..d5510cf --- /dev/null +++ b/frontend/src/components/panels/StreamsPane.scss @@ -0,0 +1,113 @@ +@import "../../colors"; + +.connection-content { + height: 100%; + background-color: $color-primary-0; + + pre { + overflow-x: hidden; + height: calc(100% - 31px); + padding: 0 10px; + white-space: pre-wrap; + word-break: break-word; + + p { + margin: 0; + padding: 0; + } + } + + .connection-message { + position: relative; + margin: 10px 0; + border: 4px solid $color-primary-3; + border-top: 0; + + .connection-message-header { + height: 25px; + background-color: $color-primary-3; + + .connection-message-info { + font-size: 11px; + margin-top: 6px; + margin-left: -10px; + } + + .connection-message-actions { + display: none; + margin-right: -18px; + + button { + font-size: 11px; + margin: 0 3px; + padding: 5px; + } + } + } + + .message-content { + padding: 10px; + + .react-json-view { + background-color: inherit !important; + } + } + + &:hover .connection-message-actions { + display: flex; + } + + .connection-message-label { + font-size: 12px; + position: absolute; + top: 0; + padding: 10px 0; + background-color: $color-primary-3; + writing-mode: vertical-rl; + text-orientation: mixed; + } + + &.from-client { + margin-right: 100px; + color: $color-primary-4; + + .connection-message-label { + right: -22px; + } + } + + &.from-server { + margin-left: 100px; + color: $color-primary-4; + + .connection-message-label { + left: -22px; + transform: rotate(-180deg); + } + } + } + + .connection-content-header { + height: 33px; + padding: 0; + background-color: $color-primary-3; + + .header-info { + font-size: 12px; + padding-top: 7px; + padding-left: 25px; + } + + .header-actions { + display: flex; + + .choice-field { + margin-top: -5px; + + .field-value { + background-color: $color-primary-3; + } + } + } + } +} -- cgit v1.2.3-70-g09d2