;
+ this.queryStringRedirect = 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");
+ });
+ }}/>
+
}
+
+
+
+
+
+ service |
+ srcip |
+ srcport |
+ dstip |
+ dstport |
+ started_at |
+ duration |
+ up |
+ down |
+ actions |
+
+
+
+ {
+ this.state.connections.flatMap((c) => {
+ return [ this.connectionSelected(c)}
+ selected={this.state.selected === c.id}
+ onMarked={(marked) => c.marked = marked}
+ onCommented={(comment) => c.comment = comment}
+ services={this.state.services}/>,
+ c.matched_rules.length > 0 &&
+
+ ];
+ })
+ }
+ {loading}
+
+
+
+ {redirect}
+
+
+ );
+ }
+
+}
+
+export default withRouter(ConnectionsPane);
diff --git a/frontend/src/components/panels/MainPane.jsx b/frontend/src/components/panels/MainPane.jsx
new file mode 100644
index 0000000..ce72be5
--- /dev/null
+++ b/frontend/src/components/panels/MainPane.jsx
@@ -0,0 +1,112 @@
+/*
+ * 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 Typed from "typed.js";
+import dispatcher from "../../dispatcher";
+import "./common.scss";
+import "./MainPane.scss";
+import PcapsPane from "./PcapsPane";
+import RulesPane from "./RulesPane";
+import ServicesPane from "./ServicesPane";
+import StreamsPane from "./StreamsPane";
+
+class MainPane extends Component {
+
+ state = {};
+
+ componentDidMount() {
+ const nl = "^600\n^400";
+ const options = {
+ strings: [
+ `welcome to caronte!^1000 the current version is ${this.props.version}` + nl +
+ "caronte is a network analyzer,^300 it is able to read pcaps and extract connections", // 0
+ "the left panel lists all connections that have already been closed" + nl +
+ "scrolling up the list will load the most recent connections,^300 downward the oldest ones", // 1
+ "by selecting a connection you can view its content,^300 which will be shown in the right panel" + nl +
+ "you can choose the display format,^300 or decide to download the connection content", // 2
+ "below there is the timeline,^300 which shows the number of connections per minute per service" + nl +
+ "you can use the sliding window to move the time range of the connections to be displayed", // 3
+ "there are also additional metrics,^300 selectable from the drop-down menu", // 4
+ "at the top are the filters,^300 which can be used to select only certain types of connections" + nl +
+ "you can choose which filters to display in the top bar from the filters window", // 5
+ "in the pcaps panel it is possible to analyze new pcaps,^300 or to see the pcaps already analyzed" + nl +
+ "you can load pcaps from your browser,^300 or process pcaps already present on the filesystem", // 6
+ "in the rules panel you can see the rules already created,^300 or create new ones" + nl +
+ "the rules inserted will be used only to label new connections, not those already analyzed" + nl +
+ "a connection is tagged if it meets all the requirements specified by the rule", // 7
+ "in the services panel you can assign new services or edit existing ones" + nl +
+ "each service is associated with a port number,^300 and will be shown in the connection list", // 8
+ "from the configuration panel you can change the settings of the frontend application", // 9
+ "that's all! and have fun!" + nl + "created by @eciavatta" // 10
+ ],
+ typeSpeed: 40,
+ cursorChar: "_",
+ backSpeed: 5,
+ smartBackspace: false,
+ backDelay: 1500,
+ preStringTyped: (arrayPos) => {
+ switch (arrayPos) {
+ case 1:
+ return dispatcher.dispatch("pulse_connections_view", {duration: 12000});
+ case 2:
+ return this.setState({backgroundPane: });
+ case 3:
+ this.setState({backgroundPane: null});
+ return dispatcher.dispatch("pulse_timeline", {duration: 12000});
+ case 6:
+ return this.setState({backgroundPane: });
+ case 7:
+ return this.setState({backgroundPane: });
+ case 8:
+ return this.setState({backgroundPane: });
+ case 10:
+ return this.setState({backgroundPane: null});
+ default:
+ return;
+ }
+ },
+ };
+ this.typed = new Typed(this.el, options);
+ }
+
+ componentWillUnmount() {
+ this.typed.destroy();
+ }
+
+ render() {
+ return (
+
+
+
+
+ {
+ this.el = el;
+ }}/>
+
+
+
+
+ {this.state.backgroundPane}
+
+
+ );
+ }
+
+}
+
+export default MainPane;
diff --git a/frontend/src/components/panels/PcapsPane.jsx b/frontend/src/components/panels/PcapsPane.jsx
new file mode 100644
index 0000000..b7d5ce9
--- /dev/null
+++ b/frontend/src/components/panels/PcapsPane.jsx
@@ -0,0 +1,287 @@
+/*
+ * 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 Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils";
+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";
+
+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", 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}))
+ .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) => {
+ const startedAt = new Date(s["started_at"]);
+ const completedAt = new Date(s["completed_at"]);
+ let timeInfo =
+ Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}
+ Completed at {completedAt.toLocaleDateString() + " " + completedAt.toLocaleTimeString()}
+
;
+
+ return
+ |
+
+
+ |
+ {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
+
+
+
+
+
+
+
+
+ id |
+ started_at |
+ duration |
+ size |
+ processed_packets |
+ invalid_packets |
+ packets_per_service |
+ actions |
+
+
+
+ {sessions}
+
+
+
+
+
+
+
+
+
+ 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/RulesPane.jsx b/frontend/src/components/panels/RulesPane.jsx
new file mode 100644
index 0000000..4cb5e41
--- /dev/null
+++ b/frontend/src/components/panels/RulesPane.jsx
@@ -0,0 +1,469 @@
+/*
+ * 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 { Col, Container, Row } from "react-bootstrap";
+import Table from "react-bootstrap/Table";
+import backend from "../../backend";
+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 "./common.scss";
+import "./RulesPane.scss";
+
+import classNames from 'classnames';
+import _ from '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", 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) }));
+ };
+
+ 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) });
+ });
+ }
+ };
+
+ deleteRule = () => {
+ const rule = this.state.selectedRule;
+ backend.delete(`/api/rules/${rule.id}`).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,
+ selectedPattern: null,
+ 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 });
+ };
+
+ 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["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]} |
+
+ this.editPattern(p)}
+ />
+ |
+
+ );
+
+ return (
+
+
+
+ GET /api/rules
+ {this.state.rulesStatusCode &&
+ }
+
+
+
+
+
+
+
+ id |
+ name |
+ color |
+ notes |
+
+
+
+ {rules}
+
+
+
+
+
+
+
+
+
+ {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}
+ />
+ this.updateParam((r) => r.filter["client_port"] = v)}
+ min={0}
+ max={65565}
+ error={this.state.ruleClientPortError}
+ />
+ 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)} />
+
+
+
+
+
+
+
+
+ regex |
+ !Aa |
+ .* |
+ \n+ |
+ UTF8 |
+ Uni_ |
+ min |
+ max |
+ direction |
+ {!isUpdate && actions | }
+
+
+
+ {patterns}
+
+
+ {this.state.rulePatternsError != null &&
+
error: {this.state.rulePatternsError}}
+
+
+
+
+ {}
+
+
+
+
+
+ );
+ }
+
+}
+
+export default RulesPane;
diff --git a/frontend/src/components/panels/SearchPane.jsx b/frontend/src/components/panels/SearchPane.jsx
new file mode 100644
index 0000000..6fe9dc7
--- /dev/null
+++ b/frontend/src/components/panels/SearchPane.jsx
@@ -0,0 +1,309 @@
+/*
+ * 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 Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import {createCurlCommand, dateTimeToTime, durationBetween} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import InputField from "../fields/InputField";
+import TagField from "../fields/TagField";
+import TextField from "../fields/TextField";
+import LinkPopover from "../objects/LinkPopover";
+import "./common.scss";
+import "./SearchPane.scss";
+
+import _ from 'lodash';
+
+class SearchPane extends Component {
+
+ searchOptions = {
+ "text_search": {
+ "terms": null,
+ "excluded_terms": null,
+ "exact_phrase": "",
+ "case_sensitive": false
+ },
+ "regex_search": {
+ "pattern": "",
+ "not_pattern": "",
+ "case_insensitive": false,
+ "multi_line": false,
+ "ignore_whitespaces": false,
+ "dot_character": false
+ },
+ "timeout": 10
+ };
+
+ state = {
+ searches: [],
+ currentSearchOptions: this.searchOptions,
+ };
+
+ componentDidMount() {
+ this.reset();
+ 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}))
+ .catch((res) => this.setState({searchesStatusCode: res.status, searchesResponse: JSON.stringify(res.json)}));
+ };
+
+ 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, loading: false});
+ this.loadSearches();
+ this.viewSearch(res.json.id);
+ }).catch((res) => {
+ this.setState({
+ searchStatusCode: res.status, searchResponse: JSON.stringify(res.json),
+ loading: false
+ });
+ });
+ }
+ };
+
+ reset = () => {
+ this.setState({
+ currentSearchOptions: _.cloneDeep(this.searchOptions),
+ exactPhraseError: null,
+ patternError: null,
+ notPatternError: null,
+ searchStatusCode: null,
+ searchesStatusCode: null,
+ searchResponse: null,
+ searchesResponse: null
+ });
+ };
+
+ validateSearch = (options) => {
+ let valid = true;
+ if (options["text_search"]["exact_phrase"] && options["text_search"]["exact_phrase"].length < 3) {
+ this.setState({exactPhraseError: "text_search.exact_phrase.length < 3"});
+ valid = false;
+ }
+ if (options["regex_search"].pattern && options["regex_search"].pattern.length < 3) {
+ this.setState({patternError: "regex_search.pattern.length < 3"});
+ valid = false;
+ }
+ if (options["regex_search"]["not_pattern"] && options["regex_search"]["not_pattern"].length < 3) {
+ this.setState({exactPhraseError: "regex_search.not_pattern.length < 3"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.currentSearchOptions);
+ this.setState({currentSearchOptions: this.state.currentSearchOptions});
+ };
+
+ extractPattern = (options) => {
+ let pattern = "";
+ if (_.isEqual(options.regex_search, this.searchOptions.regex_search)) { // is text search
+ if (options["text_search"]["exact_phrase"]) {
+ pattern += `"${options["text_search"]["exact_phrase"]}"`;
+ } else {
+ pattern += options["text_search"].terms.join(" ");
+ if (options["text_search"]["excluded_terms"]) {
+ pattern += " -" + options["text_search"]["excluded_terms"].join(" -");
+ }
+ }
+ options["text_search"]["case_sensitive"] && (pattern += "/s");
+ } else { // is regex search
+ if (options["regex_search"].pattern) {
+ pattern += "/" + options["regex_search"].pattern + "/";
+ } else {
+ pattern += "!/" + options["regex_search"]["not_pattern"] + "/";
+ }
+ options["regex_search"]["case_insensitive"] && (pattern += "i");
+ options["regex_search"]["multi_line"] && (pattern += "m");
+ options["regex_search"]["ignore_whitespaces"] && (pattern += "x");
+ options["regex_search"]["dot_character"] && (pattern += "s");
+ }
+
+ return pattern;
+ };
+
+ viewSearch = (searchId) => {
+ dispatcher.dispatch("connections_filters", {"performed_search": searchId});
+ };
+
+ handleNotification = (payload) => {
+ if (payload.event === "searches.new") {
+ this.loadSearches();
+ }
+ };
+
+ render() {
+ const options = this.state.currentSearchOptions;
+
+ let searches = this.state.searches.map((s) =>
+
+ {s.id.substring(0, 8)} |
+ {this.extractPattern(s["search_options"])} |
+ {s["affected_connections_count"]} |
+ {dateTimeToTime(s["started_at"])} |
+ {durationBetween(s["started_at"], s["finished_at"])} |
+ this.viewSearch(s.id)}/> |
+
+ );
+
+ const textOptionsModified = !_.isEqual(this.searchOptions.text_search, options.text_search);
+ const regexOptionsModified = !_.isEqual(this.searchOptions.regex_search, options.regex_search);
+
+ const curlCommand = createCurlCommand("/searches/perform", "POST", options);
+
+ return (
+
+
+
+ GET /api/searches
+ {this.state.searchesStatusCode &&
+ }
+
+
+
+
+
+
+
+ id |
+ pattern |
+ occurrences |
+ started_at |
+ duration |
+ actions |
+
+
+
+ {searches}
+
+
+
+
+
+
+
+
+ POST /api/searches/perform
+
+
+
+
+
+ NOTE: it is recommended to use the rules for recurring themes. Give preference to textual search over that with regex.
+
+
+
+
+ {
+ return {name: t};
+ })}
+ name="terms" min={3} inline allowNew={true}
+ readonly={regexOptionsModified || options["text_search"]["exact_phrase"]}
+ onChange={(tags) => this.updateParam((s) => s["text_search"].terms = tags.map((t) => t.name))}/>
+ {
+ return {name: t};
+ })}
+ name="excluded_terms" min={3} inline allowNew={true}
+ readonly={regexOptionsModified || options["text_search"]["exact_phrase"]}
+ onChange={(tags) => this.updateParam((s) => s["text_search"]["excluded_terms"] = tags.map((t) => t.name))}/>
+
+ or
+
+ this.updateParam((s) => s["text_search"]["exact_phrase"] = v)}
+ readonly={regexOptionsModified || (Array.isArray(options["text_search"].terms) && options["text_search"].terms.length > 0)}/>
+
+ this.updateParam((s) => s["text_search"]["case_sensitive"] = v)}/>
+
+
+
+ or
+
+
+
+
this.updateParam((s) => s["regex_search"].pattern = v)}/>
+ or
+ this.updateParam((s) => s["regex_search"]["not_pattern"] = v)}/>
+
+
+ this.updateParam((s) => s["regex_search"]["case_insensitive"] = v)}/>
+ this.updateParam((s) => s["regex_search"]["multi_line"] = v)}/>
+ this.updateParam((s) => s["regex_search"]["ignore_whitespaces"] = v)}/>
+ this.updateParam((s) => s["regex_search"]["dot_character"] = v)}/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
+
+export default SearchPane;
diff --git a/frontend/src/components/panels/ServicesPane.jsx b/frontend/src/components/panels/ServicesPane.jsx
new file mode 100644
index 0000000..296b329
--- /dev/null
+++ b/frontend/src/components/panels/ServicesPane.jsx
@@ -0,0 +1,233 @@
+/*
+ * 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 {Col, Container, Row} from "react-bootstrap";
+import Table from "react-bootstrap/Table";
+import backend from "../../backend";
+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 "./common.scss";
+import "./ServicesPane.scss";
+
+import classNames from 'classnames';
+import _ from 'lodash';
+
+class ServicesPane extends Component {
+
+ emptyService = {
+ "port": 0,
+ "name": "",
+ "color": "",
+ "notes": ""
+ };
+
+ state = {
+ services: [],
+ currentService: this.emptyService,
+ };
+
+ componentDidMount() {
+ this.reset();
+ 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}))
+ .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)});
+ });
+ }
+ };
+
+ deleteService = () => {
+ const service = this.state.currentService;
+ if (this.validateService(service)) {
+ backend.delete("/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 &&
+ }
+
+
+
+
+
+
+
+ port |
+ name |
+ color |
+ notes |
+
+
+
+ {services}
+
+
+
+
+
+
+
+
+ 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)}/>
+
+
+
+
+
+
+
+
+ {}
+ {isUpdate && }
+
+
+
+
+ );
+ }
+
+}
+
+export default ServicesPane;
diff --git a/frontend/src/components/panels/StatsPane.jsx b/frontend/src/components/panels/StatsPane.jsx
new file mode 100644
index 0000000..a35ef0c
--- /dev/null
+++ b/frontend/src/components/panels/StatsPane.jsx
@@ -0,0 +1,274 @@
+/*
+ * 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 Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import {formatSize} from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import CopyLinkPopover from "../objects/CopyLinkPopover";
+import LinkPopover from "../objects/LinkPopover";
+import "./common.scss";
+import "./StatsPane.scss";
+
+class StatsPane extends Component {
+
+ state = {
+ rules: []
+ };
+
+ componentDidMount() {
+ this.loadStats();
+ this.loadResourcesStats();
+ this.loadRules();
+ dispatcher.register("notifications", this.handleNotifications);
+ document.title = "caronte:~/stats$";
+ this.intervalToken = setInterval(() => this.loadResourcesStats(), 3000);
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ clearInterval(this.intervalToken);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event.startsWith("pcap")) {
+ this.loadStats();
+ } else if (payload.event.startsWith("rules")) {
+ this.loadRules();
+ }
+ };
+
+ loadStats = () => {
+ backend.get("/api/statistics/totals")
+ .then((res) => this.setState({stats: res.json, statsStatusCode: res.status}))
+ .catch((res) => this.setState({
+ stats: res.json, statsStatusCode: res.status,
+ statsResponse: JSON.stringify(res.json)
+ }));
+ };
+
+ loadResourcesStats = () => {
+ backend.get("/api/resources/system")
+ .then((res) => this.setState({resourcesStats: res.json, resourcesStatsStatusCode: res.status}))
+ .catch((res) => this.setState({
+ resourcesStats: res.json, resourcesStatsStatusCode: res.status,
+ resourcesStatsResponse: JSON.stringify(res.json)
+ }));
+ };
+
+ loadRules = () => {
+ backend.get("/api/rules").then((res) => this.setState({rules: res.json}));
+ };
+
+ render() {
+ const s = this.state.stats;
+ const rs = this.state.resourcesStats;
+
+ const ports = s && s["connections_per_service"] ? Object.keys(s["connections_per_service"]) : [];
+ let connections = 0, clientBytes = 0, serverBytes = 0, totalBytes = 0, duration = 0;
+ let servicesStats = ports.map((port) => {
+ connections += s["connections_per_service"][port];
+ clientBytes += s["client_bytes_per_service"][port];
+ serverBytes += s["server_bytes_per_service"][port];
+ totalBytes += s["total_bytes_per_service"][port];
+ duration += s["duration_per_service"][port];
+
+ return
+ {port} |
+ {formatSize(s["connections_per_service"][port])} |
+ {formatSize(s["client_bytes_per_service"][port])}B |
+ {formatSize(s["server_bytes_per_service"][port])}B |
+ {formatSize(s["total_bytes_per_service"][port])}B |
+ {formatSize(s["duration_per_service"][port] / 1000)}s |
+
;
+ });
+ servicesStats.push(
+ totals |
+ {formatSize(connections)} |
+ {formatSize(clientBytes)}B |
+ {formatSize(serverBytes)}B |
+ {formatSize(totalBytes)}B |
+ {formatSize(duration / 1000)}s |
+
);
+
+ const rulesStats = this.state.rules.map((r) =>
+
+ |
+ {r["name"]} |
+ |
+ {formatSize(s && s["matched_rules"] && s["matched_rules"][r.id] ? s["matched_rules"][r.id] : 0)} |
+
+ );
+
+ const cpuStats = (rs ? rs["cpu_times"] : []).map((cpu, index) =>
+
+ {cpu["cpu"]} |
+ {cpu["user"]} |
+ {cpu["system"]} |
+ {cpu["idle"]} |
+ {cpu["nice"]} |
+ {cpu["iowait"]} |
+ {rs["cpu_percents"][index].toFixed(2)} % |
+
+ );
+
+ return (
+
+
+
+ GET /api/statistics/totals
+
+
+
+
+
+
+
+
+ service |
+ connections |
+ client_bytes |
+ server_bytes |
+ total_bytes |
+ duration |
+
+
+
+ {servicesStats}
+
+
+
+
+
+
+
+
+ rule_id |
+ rule_name |
+ rule_color |
+ occurrences |
+
+
+
+ {rulesStats}
+
+
+
+
+
+
+
+
+ GET /api/resources/system
+
+
+
+
+
+
+
+
+ type |
+ total |
+ used |
+ free |
+ shared |
+ buff/cache |
+ available |
+
+
+
+
+ mem |
+ {rs && formatSize(rs["virtual_memory"]["total"])} |
+ {rs && formatSize(rs["virtual_memory"]["used"])} |
+ {rs && formatSize(rs["virtual_memory"]["free"])} |
+ {rs && formatSize(rs["virtual_memory"]["shared"])} |
+ {rs && formatSize(rs["virtual_memory"]["cached"])} |
+ {rs && formatSize(rs["virtual_memory"]["available"])} |
+
+
+ swap |
+ {rs && formatSize(rs["virtual_memory"]["swaptotal"])} |
+ {rs && formatSize(rs["virtual_memory"]["swaptotal"])} |
+ {rs && formatSize(rs["virtual_memory"]["swapfree"])} |
+ - |
+ - |
+ - |
+
+
+
+
+
+
+
+
+
+ cpu |
+ user |
+ system |
+ idle |
+ nice |
+ iowait |
+ used_percent |
+
+
+
+ {cpuStats}
+
+
+
+
+
+
+
+
+ disk_path |
+ fs_type |
+ total |
+ free |
+ used |
+ used_percent |
+
+
+
+
+ {rs && rs["disk_usage"]["path"]} |
+ {rs && rs["disk_usage"]["fstype"]} |
+ {rs && formatSize(rs["disk_usage"]["total"])} |
+ {rs && formatSize(rs["disk_usage"]["free"])} |
+ {rs && formatSize(rs["disk_usage"]["used"])} |
+ {rs && rs["disk_usage"]["usedPercent"].toFixed(2)} % |
+
+
+
+
+
+
+
+ );
+ }
+
+}
+
+export default StatsPane;
diff --git a/frontend/src/components/panels/StreamsPane.jsx b/frontend/src/components/panels/StreamsPane.jsx
new file mode 100644
index 0000000..9e88f55
--- /dev/null
+++ b/frontend/src/components/panels/StreamsPane.jsx
@@ -0,0 +1,453 @@
+/*
+ * 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 DOMPurify from "dompurify";
+import React, { Component } from "react";
+import { Row } from "react-bootstrap";
+import ReactJson from "react-json-view";
+import backend from "../../backend";
+import log from "../../log";
+import rules from "../../model/rules";
+import { downloadBlob, getHeaderValue } from "../../utils";
+import ButtonField from "../fields/ButtonField";
+import ChoiceField from "../fields/ChoiceField";
+import CopyDialog from "../dialogs/CopyDialog";
+import "./StreamsPane.scss";
+
+import reactStringReplace from "react-string-replace";
+import classNames from "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: [], currentId: connectionId });
+ 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 });
+ }
+ };
+
+ viewAs = (mode) => {
+ if (mode === "decoded") {
+ this.setState({ tryParse: true });
+ } else if (mode === "raw") {
+ this.setState({ tryParse: false });
+ }
+ };
+
+ tryParseConnectionMessage = (connectionMessage) => {
+ const isClient = connectionMessage["from_client"];
+ if (connectionMessage.metadata == null) {
+ return this.highlightRules(connectionMessage.content, isClient);
+ }
+
+ 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)}
+
+ {this.highlightRules(m.body, isClient)}
+
+ {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);
+ if (typeof json === "object") {
+ body = (
+
+ );
+ }
+ } catch (e) {
+ log.error(e);
+ }
+ }
+
+ return (
+
+
+ {m.protocol} {m.status}
+
+ {unrollMap(m.headers)}
+
+ {this.highlightRules(body, isClient)}
+
+ {unrollMap(m.trailers)}
+
+ );
+ default:
+ return this.highlightRules(connectionMessage.content, isClient);
+ }
+ };
+
+ highlightRules = (content, isClient) => {
+ let streamContent = content;
+ this.props.connection["matched_rules"].forEach((ruleId) => {
+ const rule = rules.ruleById(ruleId);
+ rule.patterns.forEach((pattern) => {
+ if (
+ (!isClient && pattern.direction === 1) ||
+ (isClient && pattern.direction === 2)
+ ) {
+ return;
+ }
+ let flags = "";
+ pattern["caseless"] && (flags += "i");
+ pattern["dot_all"] && (flags += "s");
+ pattern["multi_line"] && (flags += "m");
+ pattern["unicode_property"] && (flags += "u");
+ const regex = new RegExp(
+ pattern.regex.replace(/^\//, "(").replace(/\/$/, ")"),
+ flags
+ );
+ streamContent = reactStringReplace(streamContent, regex, (match, i) => (
+
+ {match}
+
+ ));
+ });
+ });
+
+ return streamContent;
+ };
+
+ 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(
+ ([name, value]) => (
+ {
+ 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
+ .filter(
+ (c) =>
+ !this.state.tryParse ||
+ (this.state.tryParse && !c["is_metadata_continuation"])
+ )
+ .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;
--
cgit v1.2.3-70-g09d2