aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-16 12:16:44 +0000
committerEmiliano Ciavatta2020-10-16 12:16:44 +0000
commitd4bac2d6741f7a291522c29c9ecc87c3e32e21d4 (patch)
treefd48e9b0fa10f0a0c72adcc8f0f9709a5af206ee /frontend
parent2fb8993008752063fa13f253784e9e92552e339d (diff)
Add notification when pcap have been processed
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/components/App.js6
-rw-r--r--frontend/src/components/Notifications.js17
-rw-r--r--frontend/src/components/Timeline.js88
-rw-r--r--frontend/src/components/fields/ButtonField.js2
-rw-r--r--frontend/src/components/fields/ChoiceField.js10
-rw-r--r--frontend/src/components/objects/Connection.js36
-rw-r--r--frontend/src/components/objects/ConnectionMatchedRules.js10
-rw-r--r--frontend/src/components/objects/CopyLinkPopover.js54
-rw-r--r--frontend/src/components/objects/MessageAction.js15
-rw-r--r--frontend/src/components/panels/ConnectionsPane.js107
-rw-r--r--frontend/src/components/panels/PcapsPane.js40
-rw-r--r--frontend/src/components/panels/RulesPane.js51
-rw-r--r--frontend/src/components/panels/SearchPane.js34
-rw-r--r--frontend/src/components/panels/ServicesPane.js68
-rw-r--r--frontend/src/serviceWorker.js208
15 files changed, 420 insertions, 326 deletions
diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js
index 0f700db..888ff86 100644
--- a/frontend/src/components/App.js
+++ b/frontend/src/components/App.js
@@ -15,10 +15,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import ConfigurationPage from "./pages/ConfigurationPage";
-import Notifications from "./Notifications";
+import React, {Component} from "react";
import dispatcher from "../dispatcher";
+import Notifications from "./Notifications";
+import ConfigurationPage from "./pages/ConfigurationPage";
import MainPage from "./pages/MainPage";
import ServiceUnavailablePage from "./pages/ServiceUnavailablePage";
diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js
index 56a4508..92731d9 100644
--- a/frontend/src/components/Notifications.js
+++ b/frontend/src/components/Notifications.js
@@ -17,6 +17,7 @@
import React, {Component} from "react";
import dispatcher from "../dispatcher";
+import {randomClassName} from "../utils";
import "./Notifications.scss";
const _ = require("lodash");
@@ -30,9 +31,15 @@ class Notifications extends Component {
};
componentDidMount() {
- dispatcher.register("notifications", n => this.notificationHandler(n));
+ dispatcher.register("notifications", this.handleNotifications);
}
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (n) => this.notificationHandler(n);
+
notificationHandler = (n) => {
switch (n.event) {
case "connected":
@@ -54,6 +61,11 @@ class Notifications extends Component {
n.description = `existing rule updated: ${n.message["name"]}`;
n.variant = "blue";
return this.pushNotification(n);
+ case "pcap.completed":
+ n.title = "new pcap analyzed";
+ n.description = `${n.message["processed_packets"]} packets processed`;
+ n.variant = "blue";
+ return this.pushNotification(n);
default:
return;
}
@@ -61,6 +73,7 @@ class Notifications extends Component {
pushNotification = (notification) => {
const notifications = this.state.notifications;
+ notification.id = randomClassName();
notifications.push(notification);
this.setState({notifications});
setTimeout(() => {
@@ -103,7 +116,7 @@ class Notifications extends Component {
if (n.variant) {
notificationClassnames[`notification-${n.variant}`] = true;
}
- return <div className={classNames(notificationClassnames)} onClick={n.onClick}>
+ return <div key={n.id} className={classNames(notificationClassnames)} onClick={n.onClick}>
<h3 className="notification-title">{n.title}</h3>
<pre className="notification-description">{n.description}</pre>
</div>;
diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js
index 8d1fd40..94fa4d0 100644
--- a/frontend/src/components/Timeline.js
+++ b/frontend/src/components/Timeline.js
@@ -15,8 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './Timeline.scss';
+import {TimeRange, TimeSeries} from "pondjs";
+import React, {Component} from "react";
+import {withRouter} from "react-router-dom";
import {
ChartContainer,
ChartRow,
@@ -27,15 +28,14 @@ import {
styler,
YAxis
} from "react-timeseries-charts";
-import {TimeRange, TimeSeries} from "pondjs";
import backend from "../backend";
-import ChoiceField from "./fields/ChoiceField";
-import {withRouter} from "react-router-dom";
-import log from "../log";
import dispatcher from "../dispatcher";
+import log from "../log";
+import ChoiceField from "./fields/ChoiceField";
+import "./Timeline.scss";
const minutes = 60 * 1000;
-const classNames = require('classnames');
+const classNames = require("classnames");
const leftSelectionPaddingMultiplier = 24;
const rightSelectionPaddingMultiplier = 8;
@@ -61,42 +61,17 @@ class Timeline extends Component {
});
this.loadStatistics(this.state.metric).then(() => log.debug("Statistics loaded after mount"));
-
- this.connectionsFiltersCallback = (payload) => {
- if ("service_port" in payload && this.state.servicePortFilter !== payload["service_port"]) {
- this.setState({servicePortFilter: payload["service_port"]});
- this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after service port changed"));
- }
- if ("matched_rules" in payload && this.state.matchedRulesFilter !== payload["matched_rules"]) {
- this.setState({matchedRulesFilter: payload["matched_rules"]});
- this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after matched rules changed"));
- }
- };
- dispatcher.register("connections_filters", this.connectionsFiltersCallback);
-
- dispatcher.register("connection_updates", (payload) => {
- this.setState({
- selection: new TimeRange(payload.from, payload.to),
- });
- this.adjustSelection();
- });
-
- dispatcher.register("notifications", (payload) => {
- if (payload.event === "services.edit" && this.state.metric !== "matched_rules") {
- this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after services updates"));
- } else if (payload.event.startsWith("rules") && this.state.metric === "matched_rules") {
- this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after rules updates"));
- }
- });
-
- dispatcher.register("pulse_timeline", (payload) => {
- this.setState({pulseTimeline: true});
- setTimeout(() => this.setState({pulseTimeline: false}), payload.duration);
- });
+ dispatcher.register("connections_filters", this.handleConnectionsFiltersCallback);
+ dispatcher.register("connection_updates", this.handleConnectionUpdates);
+ dispatcher.register("notifications", this.handleNotifications);
+ dispatcher.register("pulse_timeline", this.handlePulseTimeline);
}
componentWillUnmount() {
- dispatcher.unregister(this.connectionsFiltersCallback);
+ dispatcher.unregister(this.handleConnectionsFiltersCallback);
+ dispatcher.unregister(this.handleConnectionUpdates);
+ dispatcher.unregister(this.handleNotifications);
+ dispatcher.unregister(this.handlePulseTimeline);
}
loadStatistics = async (metric) => {
@@ -217,6 +192,39 @@ class Timeline extends Component {
}, 1000);
};
+ handleConnectionsFiltersCallback = (payload) => {
+ if ("service_port" in payload && this.state.servicePortFilter !== payload["service_port"]) {
+ this.setState({servicePortFilter: payload["service_port"]});
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after service port changed"));
+ }
+ if ("matched_rules" in payload && this.state.matchedRulesFilter !== payload["matched_rules"]) {
+ this.setState({matchedRulesFilter: payload["matched_rules"]});
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after matched rules changed"));
+ }
+ };
+
+ handleConnectionUpdates = (payload) => {
+ this.setState({
+ selection: new TimeRange(payload.from, payload.to),
+ });
+ this.adjustSelection();
+ };
+
+ handleNotifications = (payload) => {
+ if (payload.event === "services.edit" && this.state.metric !== "matched_rules") {
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after services updates"));
+ } else if (payload.event.startsWith("rules") && this.state.metric === "matched_rules") {
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after rules updates"));
+ } else if (payload.event === "pcap.completed") {
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after pcap processed"));
+ }
+ };
+
+ handlePulseTimeline = (payload) => {
+ this.setState({pulseTimeline: true});
+ setTimeout(() => this.setState({pulseTimeline: false}), payload.duration);
+ };
+
adjustSelection = () => {
const seriesRange = this.state.series.range();
const selection = this.state.selection;
diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js
index de747a5..15ef179 100644
--- a/frontend/src/components/fields/ButtonField.js
+++ b/frontend/src/components/fields/ButtonField.js
@@ -58,7 +58,7 @@ class ButtonField extends Component {
<div className={classNames("field", "button-field", {"field-small": this.props.small},
{"field-active": this.props.active})}>
<button type="button" className={classNames(buttonClassnames)}
- onClick={handler} style={buttonStyle}>{this.props.name}</button>
+ onClick={handler} style={buttonStyle} disabled={this.props.disabled}>{this.props.name}</button>
</div>
);
}
diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js
index 14071c3..7e97d89 100644
--- a/frontend/src/components/fields/ChoiceField.js
+++ b/frontend/src/components/fields/ChoiceField.js
@@ -15,12 +15,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './ChoiceField.scss';
-import './common.scss';
+import React, {Component} from "react";
import {randomClassName} from "../../utils";
+import "./ChoiceField.scss";
+import "./common.scss";
-const classNames = require('classnames');
+const classNames = require("classnames");
class ChoiceField extends Component {
@@ -67,7 +67,7 @@ class ChoiceField extends Component {
}
return (
- <div className={classNames( "field", "choice-field", {"field-inline" : inline},
+ <div className={classNames("field", "choice-field", {"field-inline": inline},
{"field-small": this.props.small})}>
{!inline && name && <label className="field-name">{name}:</label>}
<div className={classNames("field-select", {"select-expanded": this.state.expanded})}
diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js
index f838606..e5896e9 100644
--- a/frontend/src/components/objects/Connection.js
+++ b/frontend/src/components/objects/Connection.js
@@ -21,26 +21,19 @@ import backend from "../../backend";
import dispatcher from "../../dispatcher";
import {dateTimeToTime, durationBetween, formatSize} from "../../utils";
import ButtonField from "../fields/ButtonField";
-import TextField from "../fields/TextField";
import "./Connection.scss";
+import CopyLinkPopover from "./CopyLinkPopover";
import LinkPopover from "./LinkPopover";
const classNames = require("classnames");
class Connection extends Component {
- constructor(props) {
- super(props);
- this.state = {
- update: false,
- copiedMessage: false
- };
+ state = {
+ update: false
+ };
- this.copyTextarea = React.createRef();
- this.handleAction = this.handleAction.bind(this);
- }
-
- handleAction(name) {
+ handleAction = (name) => {
if (name === "hide") {
const enabled = !this.props.data.hidden;
backend.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`)
@@ -57,13 +50,7 @@ class Connection extends Component {
this.setState({update: true});
});
}
- if (name === "copy") {
- this.copyTextarea.current.select();
- document.execCommand("copy");
- this.setState({copiedMessage: true});
- setTimeout(() => this.setState({copiedMessage: false}), 3000);
- }
- }
+ };
render() {
let conn = this.props.data;
@@ -88,12 +75,6 @@ class Connection extends Component {
{conn.comment && <Form.Control as="textarea" readOnly={true} rows={2} defaultValue={conn.comment}/>}
</div>;
- const copyPopoverContent = <div>
- {this.state.copiedMessage ? <span><strong>Copied!</strong></span> :
- <span>Click to <strong>copy</strong> the connection id</span>}
- <TextField readonly rows={1} value={conn.id} textRef={this.copyTextarea}/>
- </div>;
-
return (
<tr className={classNames("connection", {"connection-selected": this.props.selected},
{"has-matched-rules": conn.matched_rules.length > 0})}>
@@ -121,9 +102,8 @@ class Connection extends Component {
<LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.comment})}
onClick={() => this.handleAction("comment")}>@</span>}
content={commentPopoverContent} placement="right"/>
- <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.hidden})}
- onClick={() => this.handleAction("copy")}>#</span>}
- content={copyPopoverContent} placement="right"/>
+ <CopyLinkPopover text="#" value={conn.id}
+ textClassName={classNames("connection-icon", {"icon-enabled": conn.hidden})}/>
</td>
</tr>
);
diff --git a/frontend/src/components/objects/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js
index 92bde49..cfd1254 100644
--- a/frontend/src/components/objects/ConnectionMatchedRules.js
+++ b/frontend/src/components/objects/ConnectionMatchedRules.js
@@ -15,11 +15,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './ConnectionMatchedRules.scss';
-import ButtonField from "../fields/ButtonField";
-import dispatcher from "../../dispatcher";
+import React, {Component} from "react";
import {withRouter} from "react-router-dom";
+import dispatcher from "../../dispatcher";
+import ButtonField from "../fields/ButtonField";
+import "./ConnectionMatchedRules.scss";
class ConnectionMatchedRules extends Component {
@@ -28,7 +28,7 @@ class ConnectionMatchedRules extends Component {
const rules = params.getAll("matched_rules");
if (!rules.includes(id)) {
rules.push(id);
- dispatcher.dispatch("connections_filters",{"matched_rules": rules});
+ dispatcher.dispatch("connections_filters", {"matched_rules": rules});
}
};
diff --git a/frontend/src/components/objects/CopyLinkPopover.js b/frontend/src/components/objects/CopyLinkPopover.js
new file mode 100644
index 0000000..fa9266f
--- /dev/null
+++ b/frontend/src/components/objects/CopyLinkPopover.js
@@ -0,0 +1,54 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import TextField from "../fields/TextField";
+import LinkPopover from "./LinkPopover";
+
+class CopyLinkPopover extends Component {
+
+ state = {};
+
+ constructor(props) {
+ super(props);
+
+ this.copyTextarea = React.createRef();
+ }
+
+ handleClick = () => {
+ this.copyTextarea.current.select();
+ document.execCommand("copy");
+ this.setState({copiedMessage: true});
+ setTimeout(() => this.setState({copiedMessage: false}), 3000);
+ };
+
+ render() {
+ const copyPopoverContent = <div style={{"width": "400px"}}>
+ {this.state.copiedMessage ? <span><strong>Copied!</strong></span> :
+ <span>Click to <strong>copy</strong></span>}
+ <TextField readonly rows={1} value={this.props.value} textRef={this.copyTextarea}/>
+ </div>;
+
+ return (
+ <LinkPopover text={<span className={this.props.textClassName}
+ onClick={this.handleClick}>{this.props.text}</span>}
+ content={copyPopoverContent} placement="right"/>
+ );
+ }
+}
+
+export default CopyLinkPopover;
diff --git a/frontend/src/components/objects/MessageAction.js b/frontend/src/components/objects/MessageAction.js
index 2b46320..e0c96e8 100644
--- a/frontend/src/components/objects/MessageAction.js
+++ b/frontend/src/components/objects/MessageAction.js
@@ -15,11 +15,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './MessageAction.scss';
+import React, {Component} from "react";
import {Modal} from "react-bootstrap";
-import TextField from "../fields/TextField";
import ButtonField from "../fields/ButtonField";
+import TextField from "../fields/TextField";
+import "./MessageAction.scss";
class MessageAction extends Component {
@@ -34,7 +34,7 @@ class MessageAction extends Component {
copyActionValue() {
this.actionValue.current.select();
- document.execCommand('copy');
+ document.execCommand("copy");
this.setState({copyButtonText: "copied!"});
setTimeout(() => this.setState({copyButtonText: "copy"}), 3000);
}
@@ -54,11 +54,12 @@ class MessageAction extends Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15} />
+ <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15}/>
</Modal.Body>
<Modal.Footer className="dialog-footer">
- <ButtonField variant="green" bordered onClick={this.copyActionValue} name={this.state.copyButtonText} />
- <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" />
+ <ButtonField variant="green" bordered onClick={this.copyActionValue}
+ name={this.state.copyButtonText}/>
+ <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/>
</Modal.Footer>
</Modal>
);
diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js
index ea47059..23c6114 100644
--- a/frontend/src/components/panels/ConnectionsPane.js
+++ b/frontend/src/components/panels/ConnectionsPane.js
@@ -15,20 +15,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './ConnectionsPane.scss';
-import Connection from "../objects/Connection";
-import Table from 'react-bootstrap/Table';
+import React, {Component} from "react";
+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";
-import {Redirect} from "react-router";
+import log from "../../log";
import {updateParams} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import Connection from "../objects/Connection";
+import ConnectionMatchedRules from "../objects/ConnectionMatchedRules";
+import "./ConnectionsPane.scss";
-const classNames = require('classnames');
+const classNames = require("classnames");
class ConnectionsPane extends Component {
@@ -67,55 +67,56 @@ class ConnectionsPane extends Component {
this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded"));
- this.connectionsFiltersCallback = payload => {
- const newParams = updateParams(this.state.urlParams, payload);
- if (this.state.urlParams.toString() === newParams.toString()) {
- return;
- }
-
- log.debug("Update following url params:", payload);
- this.queryStringRedirect = true;
- this.setState({urlParams: newParams});
-
- this.loadConnections({limit: this.queryLimit}, newParams)
- .then(() => log.info("ConnectionsPane reloaded after query string update"));
- };
- dispatcher.register("connections_filters", this.connectionsFiltersCallback);
-
- this.timelineUpdatesCallback = 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("timeline_updates", this.timelineUpdatesCallback);
-
- this.notificationsCallback = payload => {
- if (payload.event === "rules.new" || payload.event === "rules.edit") {
- this.loadRules().then(() => log.debug("Loaded connection rules after notification update"));
- }
- if (payload.event === "services.edit") {
- this.loadServices().then(() => log.debug("Services reloaded after notification update"));
- }
- };
- dispatcher.register("notifications", this.notificationsCallback);
-
- this.pulseConnectionsViewCallback = payload => {
- this.setState({pulseConnectionsView: true});
- setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration);
- };
- dispatcher.register("pulse_connections_view", this.pulseConnectionsViewCallback);
+ dispatcher.register("connections_filters", this.handleConnectionsFilters);
+ dispatcher.register("timeline_updates", this.handleTimelineUpdates);
+ dispatcher.register("notifications", this.handleNotifications);
+ dispatcher.register("pulse_connections_view", this.handlePulseConnectionsView);
}
componentWillUnmount() {
- dispatcher.unregister(this.timelineUpdatesCallback);
- dispatcher.unregister(this.notificationsCallback);
- dispatcher.unregister(this.pulseConnectionsViewCallback);
- dispatcher.unregister(this.connectionsFiltersCallback);
+ dispatcher.unregister(this.handleConnectionsFilters);
+ dispatcher.unregister(this.handleTimelineUpdates);
+ dispatcher.unregister(this.handleNotifications);
+ dispatcher.unregister(this.handlePulseConnectionsView);
}
+ handleConnectionsFilters = (payload) => {
+ const newParams = updateParams(this.state.urlParams, payload);
+ if (this.state.urlParams.toString() === newParams.toString()) {
+ return;
+ }
+
+ log.debug("Update following url params:", payload);
+ this.queryStringRedirect = true;
+ this.setState({urlParams: newParams});
+
+ this.loadConnections({limit: this.queryLimit}, newParams)
+ .then(() => log.info("ConnectionsPane reloaded after query string update"));
+ };
+
+ handleTimelineUpdates = (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}`));
+ };
+
+ handleNotifications = (payload) => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules().then(() => log.debug("Loaded connection rules after notification update"));
+ }
+ if (payload.event === "services.edit") {
+ this.loadServices().then(() => log.debug("Services reloaded after notification update"));
+ }
+ };
+
+ handlePulseConnectionsView = (payload) => {
+ this.setState({pulseConnectionsView: true});
+ setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration);
+ };
+
connectionSelected = (c) => {
this.connectionSelectedRedirect = true;
this.setState({selected: c.id});
diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js
index fd3db75..64e7804 100644
--- a/frontend/src/components/panels/PcapsPane.js
+++ b/frontend/src/components/panels/PcapsPane.js
@@ -24,6 +24,7 @@ import ButtonField from "../fields/ButtonField";
import CheckField from "../fields/CheckField";
import InputField from "../fields/InputField";
import TextField from "../fields/TextField";
+import CopyLinkPopover from "../objects/CopyLinkPopover";
import LinkPopover from "../objects/LinkPopover";
import "./common.scss";
import "./PcapsPane.scss";
@@ -44,16 +45,20 @@ class PcapsPane extends Component {
componentDidMount() {
this.loadSessions();
-
- dispatcher.register("notifications", (payload) => {
- if (payload.event === "pcap.upload" || payload.event === "pcap.file") {
- this.loadSessions();
- }
- });
-
+ dispatcher.register("notifications", this.handleNotifications);
document.title = "caronte:~/pcaps$";
}
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event.startsWith("pcap")) {
+ this.loadSessions();
+ }
+ };
+
loadSessions = () => {
backend.get("/api/pcap/sessions")
.then((res) => this.setState({sessions: res.json, sessionsStatusCode: res.status}))
@@ -130,10 +135,19 @@ class PcapsPane extends Component {
};
render() {
- let sessions = this.state.sessions.map((s) =>
- <tr key={s.id} className="row-small row-clickable">
- <td>{s["id"].substring(0, 8)}</td>
- <td>{dateTimeToTime(s["started_at"])}</td>
+ let sessions = this.state.sessions.map((s) => {
+ const startedAt = new Date(s["started_at"]);
+ const completedAt = new Date(s["completed_at"]);
+ let timeInfo = <div>
+ <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/>
+ <span>Completed at {completedAt.toLocaleDateString() + " " + completedAt.toLocaleTimeString()}</span>
+ </div>;
+
+ return <tr key={s.id} className="row-small row-clickable">
+ <td><CopyLinkPopover text={s["id"].substring(0, 8)} value={s["id"]}/></td>
+ <td>
+ <LinkPopover text={dateTimeToTime(s["started_at"])} content={timeInfo} placement="right"/>
+ </td>
<td>{durationBetween(s["started_at"], s["completed_at"])}</td>
<td>{formatSize(s["size"])}</td>
<td>{s["processed_packets"]}</td>
@@ -143,8 +157,8 @@ class PcapsPane extends Component {
placement="left"/></td>
<td className="table-cell-action"><a href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>
</td>
- </tr>
- );
+ </tr>;
+ });
const handleUploadFileChange = (file) => {
this.setState({
diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js
index a66cde7..d872b47 100644
--- a/frontend/src/components/panels/RulesPane.js
+++ b/frontend/src/components/panels/RulesPane.js
@@ -15,26 +15,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './common.scss';
-import './RulesPane.scss';
-import Table from "react-bootstrap/Table";
+import React, {Component} from "react";
import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import CheckField from "../fields/CheckField";
-import TextField from "../fields/TextField";
+import Table from "react-bootstrap/Table";
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 dispatcher from "../../dispatcher";
import validation from "../../validation";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import ChoiceField from "../fields/ChoiceField";
+import ColorField from "../fields/extensions/ColorField";
+import NumericField from "../fields/extensions/NumericField";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import CopyLinkPopover from "../objects/CopyLinkPopover";
import LinkPopover from "../objects/LinkPopover";
-import {randomClassName} from "../../utils";
-import dispatcher from "../../dispatcher";
+import "./common.scss";
+import "./RulesPane.scss";
-const classNames = require('classnames');
-const _ = require('lodash');
+const classNames = require("classnames");
+const _ = require("lodash");
class RulesPane extends Component {
@@ -88,15 +88,20 @@ class RulesPane extends Component {
this.reset();
this.loadRules();
- dispatcher.register("notifications", payload => {
- if (payload.event === "rules.new" || payload.event === "rules.edit") {
- this.loadRules();
- }
- });
-
+ dispatcher.register("notifications", this.handleNotifications);
document.title = "caronte:~/rules$";
}
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules();
+ }
+ };
+
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)}));
@@ -249,7 +254,7 @@ class RulesPane extends Component {
this.reset();
this.setState({selectedRule: _.cloneDeep(r)});
}} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}>
- <td>{r["id"].substring(0, 8)}</td>
+ <CopyLinkPopover text={r["id"].substring(0, 8)} value={r["id"]}/>
<td>{r["name"]}</td>
<td><ButtonField name={r["color"]} color={r["color"]} small/></td>
<td>{r["notes"]}</td>
@@ -260,7 +265,7 @@ class RulesPane extends Component {
rule.patterns.concat(this.state.newPattern) :
rule.patterns
).map(p => p === pattern ?
- <tr key={randomClassName()}>
+ <tr key={"new_pattern"}>
<td style={{"width": "500px"}}>
<InputField small active={this.state.patternRegexFocused} value={pattern.regex}
onChange={(v) => {
diff --git a/frontend/src/components/panels/SearchPane.js b/frontend/src/components/panels/SearchPane.js
index d3c0c8b..d36e85e 100644
--- a/frontend/src/components/panels/SearchPane.js
+++ b/frontend/src/components/panels/SearchPane.js
@@ -60,15 +60,14 @@ class SearchPane extends Component {
this.reset();
this.loadSearches();
- dispatcher.register("notifications", payload => {
- if (payload.event === "searches.new") {
- this.loadSearches();
- }
- });
-
+ dispatcher.register("notifications", this.handleNotification);
document.title = "caronte:~/searches$";
}
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotification);
+ }
+
loadSearches = () => {
backend.get("/api/searches")
.then(res => this.setState({searches: res.json, searchesStatusCode: res.status}))
@@ -77,14 +76,18 @@ class SearchPane extends Component {
performSearch = () => {
const options = this.state.currentSearchOptions;
+ this.setState({loading: true});
if (this.validateSearch(options)) {
backend.post("/api/searches/perform", options).then(res => {
this.reset();
- this.setState({searchStatusCode: res.status});
+ this.setState({searchStatusCode: res.status, loading: false});
this.loadSearches();
this.viewSearch(res.json.id);
}).catch(res => {
- this.setState({searchStatusCode: res.status, searchResponse: JSON.stringify(res.json)});
+ this.setState({
+ searchStatusCode: res.status, searchResponse: JSON.stringify(res.json),
+ loading: false
+ });
});
}
};
@@ -156,6 +159,12 @@ class SearchPane extends Component {
dispatcher.dispatch("connections_filters", {"performed_search": searchId});
};
+ handleNotification = (payload) => {
+ if (payload.event === "searches.new") {
+ this.loadSearches();
+ }
+ };
+
render() {
const options = this.state.currentSearchOptions;
@@ -263,7 +272,8 @@ class SearchPane extends Component {
onChange={v => this.updateParam(s => s["regex_search"]["not_pattern"] = v)}/>
<div className="checkbox-line">
- <CheckField checked={options["regex_search"]["case_insensitive"]} name="case_insensitive"
+ <CheckField checked={options["regex_search"]["case_insensitive"]}
+ name="case_insensitive"
readonly={textOptionsModified} small
onChange={(v) => this.updateParam(s => s["regex_search"]["case_insensitive"] = v)}/>
<CheckField checked={options["regex_search"]["multi_line"]} name="multi_line"
@@ -284,8 +294,10 @@ class SearchPane extends Component {
</div>
<div className="section-footer">
- <ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>
- <ButtonField variant="green" name="perform_search" bordered onClick={this.performSearch}/>
+ <ButtonField variant="red" name="cancel" bordered disabled={this.state.loading}
+ onClick={this.reset}/>
+ <ButtonField variant="green" name="perform_search" bordered
+ disabled={this.state.loading} onClick={this.performSearch}/>
</div>
</div>
</div>
diff --git a/frontend/src/components/panels/ServicesPane.js b/frontend/src/components/panels/ServicesPane.js
index bc82356..48d9e29 100644
--- a/frontend/src/components/panels/ServicesPane.js
+++ b/frontend/src/components/panels/ServicesPane.js
@@ -15,24 +15,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, {Component} from 'react';
-import './common.scss';
-import './ServicesPane.scss';
-import Table from "react-bootstrap/Table";
+import React, {Component} from "react";
import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import TextField from "../fields/TextField";
+import Table from "react-bootstrap/Table";
import backend from "../../backend";
-import NumericField from "../fields/extensions/NumericField";
-import ColorField from "../fields/extensions/ColorField";
-import ButtonField from "../fields/ButtonField";
+import dispatcher from "../../dispatcher";
+import {createCurlCommand} from "../../utils";
import validation from "../../validation";
+import ButtonField from "../fields/ButtonField";
+import ColorField from "../fields/extensions/ColorField";
+import NumericField from "../fields/extensions/NumericField";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
import LinkPopover from "../objects/LinkPopover";
-import {createCurlCommand} from "../../utils";
-import dispatcher from "../../dispatcher";
+import "./common.scss";
+import "./ServicesPane.scss";
-const classNames = require('classnames');
-const _ = require('lodash');
+const classNames = require("classnames");
+const _ = require("lodash");
class ServicesPane extends Component {
@@ -52,15 +52,20 @@ class ServicesPane extends Component {
this.reset();
this.loadServices();
- dispatcher.register("notifications", payload => {
- if (payload.event === "services.edit") {
- this.loadServices();
- }
- });
-
+ dispatcher.register("notifications", this.handleNotifications);
document.title = "caronte:~/services$";
}
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event === "services.edit") {
+ this.loadServices();
+ }
+ };
+
loadServices = () => {
backend.get("/api/services")
.then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
@@ -125,10 +130,10 @@ class ServicesPane extends Component {
<tr key={s.port} onClick={() => {
this.reset();
this.setState({isUpdate: true, currentService: _.cloneDeep(s)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}>
+ }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port})}>
<td>{s["port"]}</td>
<td>{s["name"]}</td>
- <td><ButtonField name={s["color"]} color={s["color"]} small /></td>
+ <td><ButtonField name={s["color"]} color={s["color"]} small/></td>
<td>{s["notes"]}</td>
</tr>
);
@@ -141,9 +146,9 @@ class ServicesPane extends Component {
<div className="section-header">
<span className="api-request">GET /api/services</span>
{this.state.servicesStatusCode &&
- <span className="api-response"><LinkPopover text={this.state.servicesStatusCode}
- content={this.state.servicesResponse}
- placement="left" /></span>}
+ <span className="api-response"><LinkPopover text={this.state.servicesStatusCode}
+ content={this.state.servicesResponse}
+ placement="left"/></span>}
</div>
<div className="section-content">
@@ -170,7 +175,7 @@ class ServicesPane extends Component {
<span className="api-request">PUT /api/services</span>
<span className="api-response"><LinkPopover text={this.state.serviceStatusCode}
content={this.state.serviceResponse}
- placement="left" /></span>
+ placement="left"/></span>
</div>
<div className="section-content">
@@ -179,17 +184,17 @@ class ServicesPane extends Component {
<Col>
<NumericField name="port" value={service.port}
onChange={(v) => this.updateParam((s) => s.port = v)}
- min={0} max={65565} error={this.state.servicePortError} />
+ min={0} max={65565} error={this.state.servicePortError}/>
<InputField name="name" value={service.name}
onChange={(v) => this.updateParam((s) => s.name = v)}
- error={this.state.serviceNameError} />
+ error={this.state.serviceNameError}/>
<ColorField value={service.color} error={this.state.serviceColorError}
- onChange={(v) => this.updateParam((s) => s.color = v)} />
+ onChange={(v) => this.updateParam((s) => s.color = v)}/>
</Col>
<Col>
<TextField name="notes" rows={7} value={service.notes}
- onChange={(v) => this.updateParam((s) => s.notes = v)} />
+ onChange={(v) => this.updateParam((s) => s.notes = v)}/>
</Col>
</Row>
</Container>
@@ -199,8 +204,9 @@ class ServicesPane extends Component {
<div className="section-footer">
{<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
- <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_service" : "add_service"}
- bordered onClick={this.updateService} />
+ <ButtonField variant={isUpdate ? "blue" : "green"}
+ name={isUpdate ? "update_service" : "add_service"}
+ bordered onClick={this.updateService}/>
</div>
</div>
</div>
diff --git a/frontend/src/serviceWorker.js b/frontend/src/serviceWorker.js
index c633a91..a1f0ba8 100644
--- a/frontend/src/serviceWorker.js
+++ b/frontend/src/serviceWorker.js
@@ -11,131 +11,131 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
+ window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
+ window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
+ if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+ window.addEventListener("load", () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit https://bit.ly/CRA-PWA'
- );
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ "This web app is being served cache-first by a service " +
+ "worker. To learn more, visit https://bit.ly/CRA-PWA"
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
});
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
+ }
}
function registerValidSW(swUrl, config) {
- navigator.serviceWorker
- .register(swUrl)
- .then((registration) => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
- );
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === "installed") {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ "New content is available and will be used when all " +
+ "tabs for this page are closed. See https://bit.ly/CRA-PWA."
+ );
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log("Content is cached for offline use.");
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch((error) => {
- console.error('Error during service worker registration:', error);
- });
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch((error) => {
+ console.error("Error during service worker registration:", error);
+ });
}
function checkValidServiceWorker(swUrl, config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl, {
- headers: { 'Service-Worker': 'script' },
- })
- .then((response) => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then((registration) => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: {"Service-Worker": "script"},
})
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
+ .then((response) => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get("content-type");
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf("javascript") === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log(
+ "No internet connection found. App is running in offline mode."
+ );
+ });
}
export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready
- .then((registration) => {
- registration.unregister();
- })
- .catch((error) => {
- console.error(error.message);
- });
- }
+ if ("serviceWorker" in navigator) {
+ navigator.serviceWorker.ready
+ .then((registration) => {
+ registration.unregister();
+ })
+ .catch((error) => {
+ console.error(error.message);
+ });
+ }
}