- {
- !this.loading &&
- this.setState({selectedConnection: c})}
- initialConnection={this.state.selectedConnection}/>
- }
-
-
- }/>
- }/>
- }/>
- }/>
- }/>
-
+
);
}
+
}
-export default withRouter(MainPane);
+export default MainPane;
diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss
index 2973c00..c8460f2 100644
--- a/frontend/src/components/panels/MainPane.scss
+++ b/frontend/src/components/panels/MainPane.scss
@@ -1,22 +1,5 @@
@import "../../colors";
.main-pane {
- display: flex;
- height: 100%;
- padding: 0 15px;
- background-color: $color-primary-2;
- .pane {
- flex: 1;
- }
-
- .connections-pane {
- flex: 1 0;
- margin-right: 7.5px;
- }
-
- .details-pane {
- flex: 1 1;
- margin-left: 7.5px;
- }
}
diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js
deleted file mode 100644
index d5c2225..0000000
--- a/frontend/src/components/panels/PcapPane.js
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
.
- */
-
-import React, {Component} from 'react';
-import './PcapPane.scss';
-import './common.scss';
-import Table from "react-bootstrap/Table";
-import backend from "../../backend";
-import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils";
-import InputField from "../fields/InputField";
-import CheckField from "../fields/CheckField";
-import TextField from "../fields/TextField";
-import ButtonField from "../fields/ButtonField";
-import LinkPopover from "../objects/LinkPopover";
-import dispatcher from "../../dispatcher";
-
-class PcapPane extends Component {
-
- state = {
- sessions: [],
- isUploadFileValid: true,
- isUploadFileFocused: false,
- uploadFlushAll: false,
- isFileValid: true,
- isFileFocused: false,
- fileValue: "",
- processFlushAll: false,
- deleteOriginalFile: false
- };
-
- componentDidMount() {
- this.loadSessions();
-
- dispatcher.register("notifications", payload => {
- if (payload.event === "pcap.upload" || payload.event === "pcap.file") {
- this.loadSessions();
- }
- });
-
- document.title = "caronte:~/pcaps$";
- }
-
- loadSessions = () => {
- backend.get("/api/pcap/sessions")
- .then(res => this.setState({sessions: res.json, sessionsStatusCode: res.status}))
- .catch(res => this.setState({
- sessions: res.json, sessionsStatusCode: res.status,
- sessionsResponse: JSON.stringify(res.json)
- }));
- };
-
- uploadPcap = () => {
- if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) {
- this.setState({isUploadFileFocused: true});
- return;
- }
-
- const formData = new FormData();
- formData.append("file", this.state.uploadSelectedFile);
- formData.append("flush_all", this.state.uploadFlushAll);
- backend.postFile("/api/pcap/upload", formData).then(res => {
- this.setState({
- uploadStatusCode: res.status,
- uploadResponse: JSON.stringify(res.json)
- });
- this.resetUpload();
- this.loadSessions();
- }).catch(res => this.setState({
- uploadStatusCode: res.status,
- uploadResponse: JSON.stringify(res.json)
- })
- );
- };
-
- processPcap = () => {
- if (this.state.fileValue === "" || !this.state.isFileValid) {
- this.setState({isFileFocused: true});
- return;
- }
-
- backend.post("/api/pcap/file", {
- file: this.state.fileValue,
- flush_all: this.state.processFlushAll,
- delete_original_file: this.state.deleteOriginalFile
- }).then(res => {
- this.setState({
- processStatusCode: res.status,
- processResponse: JSON.stringify(res.json)
- });
- this.resetProcess();
- this.loadSessions();
- }).catch(res => this.setState({
- processStatusCode: res.status,
- processResponse: JSON.stringify(res.json)
- })
- );
- };
-
- resetUpload = () => {
- this.setState({
- isUploadFileValid: true,
- isUploadFileFocused: false,
- uploadFlushAll: false,
- uploadSelectedFile: null
- });
- };
-
- resetProcess = () => {
- this.setState({
- isFileValid: true,
- isFileFocused: false,
- fileValue: "",
- processFlushAll: false,
- deleteOriginalFile: false,
- });
- };
-
- render() {
- let sessions = this.state.sessions.map(s =>
-
- {s["id"].substring(0, 8)} |
- {dateTimeToTime(s["started_at"])} |
- {durationBetween(s["started_at"], s["completed_at"])} |
- {formatSize(s["size"])} |
- {s["processed_packets"]} |
- {s["invalid_packets"]} |
- |
- download
- |
-
- );
-
- const handleUploadFileChange = (file) => {
- this.setState({
- isUploadFileValid: file == null || (file.type.endsWith("pcap") || file.type.endsWith("pcapng")),
- isUploadFileFocused: false,
- uploadSelectedFile: file,
- uploadStatusCode: null,
- uploadResponse: null
- });
- };
-
- const handleFileChange = (file) => {
- this.setState({
- isFileValid: (file.endsWith("pcap") || file.endsWith("pcapng")),
- isFileFocused: false,
- fileValue: file,
- processStatusCode: null,
- processResponse: null
- });
- };
-
- const uploadCurlCommand = createCurlCommand("pcap/upload", "POST", null, {
- file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ?
- this.state.uploadSelectedFile.name : "invalid.pcap"),
- flush_all: this.state.uploadFlushAll
- });
-
- const fileCurlCommand = createCurlCommand("pcap/file", "POST", {
- file: this.state.fileValue,
- flush_all: this.state.processFlushAll,
- delete_original_file: this.state.deleteOriginalFile
- });
-
- return (
-
-
-
- GET /api/pcap/sessions
-
-
-
-
-
-
-
-
- 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 PcapPane;
diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss
deleted file mode 100644
index 4dbc2b2..0000000
--- a/frontend/src/components/panels/PcapPane.scss
+++ /dev/null
@@ -1,38 +0,0 @@
-@import "../../colors.scss";
-
-.pcap-pane {
- display: flex;
- flex-direction: column;
-
- .pcap-list {
- overflow: hidden;
- flex: 1;
-
- .section-content {
- height: 100%;
- }
-
- .section-table {
- height: calc(100% - 30px);
- }
-
- .table-cell-action {
- font-size: 13px;
- font-weight: 600;
- }
- }
-
- .upload-actions {
- display: flex;
- align-items: flex-end;
- margin-bottom: 20px;
- }
-
- .upload-options {
- flex: 1;
-
- span {
- font-size: 0.9em;
- }
- }
-}
diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js
new file mode 100644
index 0000000..8722230
--- /dev/null
+++ b/frontend/src/components/panels/PcapsPane.js
@@ -0,0 +1,273 @@
+/*
+ * This file is part of caronte (https://github.com/eciavatta/caronte).
+ * Copyright (c) 2020 Emiliano Ciavatta.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
.
+ */
+
+import React, {Component} from 'react';
+import './PcapsPane.scss';
+import './common.scss';
+import Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils";
+import InputField from "../fields/InputField";
+import CheckField from "../fields/CheckField";
+import TextField from "../fields/TextField";
+import ButtonField from "../fields/ButtonField";
+import LinkPopover from "../objects/LinkPopover";
+import dispatcher from "../../dispatcher";
+
+class PcapsPane extends Component {
+
+ state = {
+ sessions: [],
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false
+ };
+
+ componentDidMount() {
+ this.loadSessions();
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "pcap.upload" || payload.event === "pcap.file") {
+ this.loadSessions();
+ }
+ });
+
+ document.title = "caronte:~/pcaps$";
+ }
+
+ loadSessions = () => {
+ backend.get("/api/pcap/sessions")
+ .then(res => this.setState({sessions: res.json, sessionsStatusCode: res.status}))
+ .catch(res => this.setState({
+ sessions: res.json, sessionsStatusCode: res.status,
+ sessionsResponse: JSON.stringify(res.json)
+ }));
+ };
+
+ uploadPcap = () => {
+ if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) {
+ this.setState({isUploadFileFocused: true});
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append("file", this.state.uploadSelectedFile);
+ formData.append("flush_all", this.state.uploadFlushAll);
+ backend.postFile("/api/pcap/upload", formData).then(res => {
+ this.setState({
+ uploadStatusCode: res.status,
+ uploadResponse: JSON.stringify(res.json)
+ });
+ this.resetUpload();
+ this.loadSessions();
+ }).catch(res => this.setState({
+ uploadStatusCode: res.status,
+ uploadResponse: JSON.stringify(res.json)
+ })
+ );
+ };
+
+ processPcap = () => {
+ if (this.state.fileValue === "" || !this.state.isFileValid) {
+ this.setState({isFileFocused: true});
+ return;
+ }
+
+ backend.post("/api/pcap/file", {
+ file: this.state.fileValue,
+ flush_all: this.state.processFlushAll,
+ delete_original_file: this.state.deleteOriginalFile
+ }).then(res => {
+ this.setState({
+ processStatusCode: res.status,
+ processResponse: JSON.stringify(res.json)
+ });
+ this.resetProcess();
+ this.loadSessions();
+ }).catch(res => this.setState({
+ processStatusCode: res.status,
+ processResponse: JSON.stringify(res.json)
+ })
+ );
+ };
+
+ resetUpload = () => {
+ this.setState({
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ uploadSelectedFile: null
+ });
+ };
+
+ resetProcess = () => {
+ this.setState({
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false,
+ });
+ };
+
+ render() {
+ let sessions = this.state.sessions.map(s =>
+
+ {s["id"].substring(0, 8)} |
+ {dateTimeToTime(s["started_at"])} |
+ {durationBetween(s["started_at"], s["completed_at"])} |
+ {formatSize(s["size"])} |
+ {s["processed_packets"]} |
+ {s["invalid_packets"]} |
+ |
+ download
+ |
+
+ );
+
+ const handleUploadFileChange = (file) => {
+ this.setState({
+ isUploadFileValid: file == null || (file.type.endsWith("pcap") || file.type.endsWith("pcapng")),
+ isUploadFileFocused: false,
+ uploadSelectedFile: file,
+ uploadStatusCode: null,
+ uploadResponse: null
+ });
+ };
+
+ const handleFileChange = (file) => {
+ this.setState({
+ isFileValid: (file.endsWith("pcap") || file.endsWith("pcapng")),
+ isFileFocused: false,
+ fileValue: file,
+ processStatusCode: null,
+ processResponse: null
+ });
+ };
+
+ const uploadCurlCommand = createCurlCommand("pcap/upload", "POST", null, {
+ file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ?
+ this.state.uploadSelectedFile.name : "invalid.pcap"),
+ flush_all: this.state.uploadFlushAll
+ });
+
+ const fileCurlCommand = createCurlCommand("pcap/file", "POST", {
+ file: this.state.fileValue,
+ flush_all: this.state.processFlushAll,
+ delete_original_file: this.state.deleteOriginalFile
+ });
+
+ return (
+
+
+
+ GET /api/pcap/sessions
+
+
+
+
+
+
+
+
+ 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/PcapsPane.scss b/frontend/src/components/panels/PcapsPane.scss
new file mode 100644
index 0000000..4dbc2b2
--- /dev/null
+++ b/frontend/src/components/panels/PcapsPane.scss
@@ -0,0 +1,38 @@
+@import "../../colors.scss";
+
+.pcap-pane {
+ display: flex;
+ flex-direction: column;
+
+ .pcap-list {
+ overflow: hidden;
+ flex: 1;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+
+ .table-cell-action {
+ font-size: 13px;
+ font-weight: 600;
+ }
+ }
+
+ .upload-actions {
+ display: flex;
+ align-items: flex-end;
+ margin-bottom: 20px;
+ }
+
+ .upload-options {
+ flex: 1;
+
+ span {
+ font-size: 0.9em;
+ }
+ }
+}
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
deleted file mode 100644
index 9913962..0000000
--- a/frontend/src/components/panels/RulePane.js
+++ /dev/null
@@ -1,438 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
.
- */
-
-import React, {Component} from 'react';
-import './common.scss';
-import './RulePane.scss';
-import Table from "react-bootstrap/Table";
-import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import CheckField from "../fields/CheckField";
-import TextField from "../fields/TextField";
-import backend from "../../backend";
-import NumericField from "../fields/extensions/NumericField";
-import ColorField from "../fields/extensions/ColorField";
-import ChoiceField from "../fields/ChoiceField";
-import ButtonField from "../fields/ButtonField";
-import validation from "../../validation";
-import LinkPopover from "../objects/LinkPopover";
-import {randomClassName} from "../../utils";
-import dispatcher from "../../dispatcher";
-
-const classNames = require('classnames');
-const _ = require('lodash');
-
-class RulePane extends Component {
-
- emptyRule = {
- "name": "",
- "color": "",
- "notes": "",
- "enabled": true,
- "patterns": [],
- "filter": {
- "service_port": 0,
- "client_address": "",
- "client_port": 0,
- "min_duration": 0,
- "max_duration": 0,
- "min_bytes": 0,
- "max_bytes": 0
- },
- "version": 0
- };
- emptyPattern = {
- "regex": "",
- "flags": {
- "caseless": false,
- "dot_all": false,
- "multi_line": false,
- "utf_8_mode": false,
- "unicode_property": false
- },
- "min_occurrences": 0,
- "max_occurrences": 0,
- "direction": 0
- };
- state = {
- rules: [],
- newRule: this.emptyRule,
- newPattern: this.emptyPattern
- };
-
- constructor(props) {
- super(props);
-
- this.directions = {
- 0: "both",
- 1: "c->s",
- 2: "s->c"
- };
- }
-
- componentDidMount() {
- this.reset();
- this.loadRules();
-
- dispatcher.register("notifications", payload => {
- if (payload.event === "rules.new" || payload.event === "rules.edit") {
- this.loadRules();
- }
- });
-
- document.title = "caronte:~/rules$";
- }
-
- loadRules = () => {
- backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status}))
- .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)}));
- };
-
- addRule = () => {
- if (this.validateRule(this.state.newRule)) {
- backend.post("/api/rules", this.state.newRule).then(res => {
- this.reset();
- this.setState({ruleStatusCode: res.status});
- this.loadRules();
- }).catch(res => {
- this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
- });
- }
- };
-
- updateRule = () => {
- const rule = this.state.selectedRule;
- if (this.validateRule(rule)) {
- backend.put(`/api/rules/${rule.id}`, rule).then(res => {
- this.reset();
- this.setState({ruleStatusCode: res.status});
- this.loadRules();
- }).catch(res => {
- this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
- });
- }
- };
-
- validateRule = (rule) => {
- let valid = true;
- if (rule.name.length < 3) {
- this.setState({ruleNameError: "name.length < 3"});
- valid = false;
- }
- if (!validation.isValidColor(rule.color)) {
- this.setState({ruleColorError: "color is not hexcolor"});
- valid = false;
- }
- if (!validation.isValidPort(rule.filter.service_port)) {
- this.setState({ruleServicePortError: "service_port > 65565"});
- valid = false;
- }
- if (!validation.isValidPort(rule.filter.client_port)) {
- this.setState({ruleClientPortError: "client_port > 65565"});
- valid = false;
- }
- if (!validation.isValidAddress(rule.filter.client_address)) {
- this.setState({ruleClientAddressError: "client_address is not ip_address"});
- valid = false;
- }
- if (rule.filter.min_duration > rule.filter.max_duration) {
- this.setState({ruleDurationError: "min_duration > max_dur."});
- valid = false;
- }
- if (rule.filter.min_bytes > rule.filter.max_bytes) {
- this.setState({ruleBytesError: "min_bytes > max_bytes"});
- valid = false;
- }
- if (rule.patterns.length < 1) {
- this.setState({rulePatternsError: "patterns.length < 1"});
- valid = false;
- }
-
- return valid;
- };
-
- reset = () => {
- const newRule = _.cloneDeep(this.emptyRule);
- const newPattern = _.cloneDeep(this.emptyPattern);
- this.setState({
- selectedRule: null,
- newRule: newRule,
- selectedPattern: null,
- newPattern: newPattern,
- patternRegexFocused: false,
- patternOccurrencesFocused: false,
- ruleNameError: null,
- ruleColorError: null,
- ruleServicePortError: null,
- ruleClientPortError: null,
- ruleClientAddressError: null,
- ruleDurationError: null,
- ruleBytesError: null,
- rulePatternsError: null,
- ruleStatusCode: null,
- rulesStatusCode: null,
- ruleResponse: null,
- rulesResponse: null
- });
- };
-
- updateParam = (callback) => {
- const updatedRule = this.currentRule();
- callback(updatedRule);
- this.setState({newRule: updatedRule});
- };
-
- currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule;
-
- addPattern = (pattern) => {
- if (!this.validatePattern(pattern)) {
- return;
- }
-
- const newPattern = _.cloneDeep(this.emptyPattern);
- this.currentRule().patterns.push(pattern);
- this.setState({
- newPattern: newPattern
- });
- };
-
- editPattern = (pattern) => {
- this.setState({
- selectedPattern: pattern
- });
- };
-
- updatePattern = (pattern) => {
- if (!this.validatePattern(pattern)) {
- return;
- }
-
- this.setState({
- selectedPattern: null
- });
- };
-
- validatePattern = (pattern) => {
- let valid = true;
- if (pattern.regex === "") {
- valid = false;
- this.setState({patternRegexFocused: true});
- }
- if (pattern.min_occurrences > pattern.max_occurrences) {
- valid = false;
- this.setState({patternOccurrencesFocused: true});
- }
- return valid;
- };
-
- render() {
- const isUpdate = this.state.selectedRule != null;
- const rule = this.currentRule();
- const pattern = this.state.selectedPattern || this.state.newPattern;
-
- let rules = this.state.rules.map(r =>
-
{
- this.reset();
- this.setState({selectedRule: _.cloneDeep(r)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}>
- {r["id"].substring(0, 8)} |
- {r["name"]} |
- |
- {r["notes"]} |
-
- );
-
- let patterns = (this.state.selectedPattern == null && !isUpdate ?
- rule.patterns.concat(this.state.newPattern) :
- rule.patterns
- ).map(p => p === pattern ?
-
-
- {
- this.updateParam(() => pattern.regex = v);
- this.setState({patternRegexFocused: pattern.regex === ""});
- }}/>
- |
- this.updateParam(() => pattern.flags.caseless = v)}/> |
- this.updateParam(() => pattern.flags.dot_all = v)}/> |
- this.updateParam(() => pattern.flags.multi_line = v)}/> |
- this.updateParam(() => pattern.flags.utf_8_mode = v)}/> |
- this.updateParam(() => pattern.flags.unicode_property = v)}/> |
-
- this.updateParam(() => pattern.min_occurrences = v)}/>
- |
-
- this.updateParam(() => pattern.max_occurrences = v)}/>
- |
- s", "s->c"]}
- value={this.directions[pattern.direction]}
- onChange={(v) => this.updateParam(() => pattern.direction = v)}/> |
- {this.state.selectedPattern == null ?
- this.addPattern(p)}/> :
- this.updatePattern(p)}/>}
- |
-
- :
-
- {p.regex} |
- {p.flags.caseless ? "yes" : "no"} |
- {p.flags.dot_all ? "yes" : "no"} |
- {p.flags.multi_line ? "yes" : "no"} |
- {p.flags.utf_8_mode ? "yes" : "no"} |
- {p.flags.unicode_property ? "yes" : "no"} |
- {p.min_occurrences} |
- {p.max_occurrences} |
- {this.directions[p.direction]} |
- {!isUpdate && this.editPattern(p)}/> | }
-
- );
-
- return (
-
-
-
- GET /api/rules
- {this.state.rulesStatusCode &&
- }
-
-
-
-
-
-
-
- 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}
- readonly={isUpdate}/>
- this.updateParam((r) => r.filter.client_port = v)}
- min={0} max={65565} error={this.state.ruleClientPortError}
- readonly={isUpdate}/>
- this.updateParam((r) => r.filter.client_address = v)}/>
-
-
-
- this.updateParam((r) => r.filter.min_duration = v)}/>
- this.updateParam((r) => r.filter.max_duration = v)}/>
- this.updateParam((r) => r.filter.min_bytes = v)}/>
- this.updateParam((r) => r.filter.max_bytes = v)}/>
-
-
-
-
-
-
-
-
- regex |
- !Aa |
- .* |
- \n+ |
- UTF8 |
- Uni_ |
- min |
- max |
- direction |
- {!isUpdate && actions | }
-
-
-
- {patterns}
-
-
- {this.state.rulePatternsError != null &&
-
error: {this.state.rulePatternsError}}
-
-
-
-
- {}
-
-
-
-
- );
- }
-
-}
-
-export default RulePane;
diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss
deleted file mode 100644
index 992445a..0000000
--- a/frontend/src/components/panels/RulePane.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-
-.rule-pane {
- display: flex;
- flex-direction: column;
-
- .rules-list {
- overflow: hidden;
- flex: 2 1;
-
- .section-content {
- height: 100%;
- }
-
- .section-table {
- height: calc(100% - 30px);
- }
- }
-
- .rule-edit {
- display: flex;
- flex: 3 0;
- flex-direction: column;
-
- .section-content {
- flex: 1;
- }
-
- .section-table {
- max-height: 150px;
- }
- }
-}
diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js
new file mode 100644
index 0000000..a66cde7
--- /dev/null
+++ b/frontend/src/components/panels/RulesPane.js
@@ -0,0 +1,438 @@
+/*
+ * This file is part of caronte (https://github.com/eciavatta/caronte).
+ * Copyright (c) 2020 Emiliano Ciavatta.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
.
+ */
+
+import React, {Component} from 'react';
+import './common.scss';
+import './RulesPane.scss';
+import Table from "react-bootstrap/Table";
+import {Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import CheckField from "../fields/CheckField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import NumericField from "../fields/extensions/NumericField";
+import ColorField from "../fields/extensions/ColorField";
+import ChoiceField from "../fields/ChoiceField";
+import ButtonField from "../fields/ButtonField";
+import validation from "../../validation";
+import LinkPopover from "../objects/LinkPopover";
+import {randomClassName} from "../../utils";
+import dispatcher from "../../dispatcher";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class RulesPane extends Component {
+
+ emptyRule = {
+ "name": "",
+ "color": "",
+ "notes": "",
+ "enabled": true,
+ "patterns": [],
+ "filter": {
+ "service_port": 0,
+ "client_address": "",
+ "client_port": 0,
+ "min_duration": 0,
+ "max_duration": 0,
+ "min_bytes": 0,
+ "max_bytes": 0
+ },
+ "version": 0
+ };
+ emptyPattern = {
+ "regex": "",
+ "flags": {
+ "caseless": false,
+ "dot_all": false,
+ "multi_line": false,
+ "utf_8_mode": false,
+ "unicode_property": false
+ },
+ "min_occurrences": 0,
+ "max_occurrences": 0,
+ "direction": 0
+ };
+ state = {
+ rules: [],
+ newRule: this.emptyRule,
+ newPattern: this.emptyPattern
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.directions = {
+ 0: "both",
+ 1: "c->s",
+ 2: "s->c"
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadRules();
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules();
+ }
+ });
+
+ document.title = "caronte:~/rules$";
+ }
+
+ loadRules = () => {
+ backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status}))
+ .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)}));
+ };
+
+ addRule = () => {
+ if (this.validateRule(this.state.newRule)) {
+ backend.post("/api/rules", this.state.newRule).then(res => {
+ this.reset();
+ this.setState({ruleStatusCode: res.status});
+ this.loadRules();
+ }).catch(res => {
+ this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ updateRule = () => {
+ const rule = this.state.selectedRule;
+ if (this.validateRule(rule)) {
+ backend.put(`/api/rules/${rule.id}`, rule).then(res => {
+ this.reset();
+ this.setState({ruleStatusCode: res.status});
+ this.loadRules();
+ }).catch(res => {
+ this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ validateRule = (rule) => {
+ let valid = true;
+ if (rule.name.length < 3) {
+ this.setState({ruleNameError: "name.length < 3"});
+ valid = false;
+ }
+ if (!validation.isValidColor(rule.color)) {
+ this.setState({ruleColorError: "color is not hexcolor"});
+ valid = false;
+ }
+ if (!validation.isValidPort(rule.filter.service_port)) {
+ this.setState({ruleServicePortError: "service_port > 65565"});
+ valid = false;
+ }
+ if (!validation.isValidPort(rule.filter.client_port)) {
+ this.setState({ruleClientPortError: "client_port > 65565"});
+ valid = false;
+ }
+ if (!validation.isValidAddress(rule.filter.client_address)) {
+ this.setState({ruleClientAddressError: "client_address is not ip_address"});
+ valid = false;
+ }
+ if (rule.filter.min_duration > rule.filter.max_duration) {
+ this.setState({ruleDurationError: "min_duration > max_dur."});
+ valid = false;
+ }
+ if (rule.filter.min_bytes > rule.filter.max_bytes) {
+ this.setState({ruleBytesError: "min_bytes > max_bytes"});
+ valid = false;
+ }
+ if (rule.patterns.length < 1) {
+ this.setState({rulePatternsError: "patterns.length < 1"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ reset = () => {
+ const newRule = _.cloneDeep(this.emptyRule);
+ const newPattern = _.cloneDeep(this.emptyPattern);
+ this.setState({
+ selectedRule: null,
+ newRule: newRule,
+ selectedPattern: null,
+ newPattern: newPattern,
+ patternRegexFocused: false,
+ patternOccurrencesFocused: false,
+ ruleNameError: null,
+ ruleColorError: null,
+ ruleServicePortError: null,
+ ruleClientPortError: null,
+ ruleClientAddressError: null,
+ ruleDurationError: null,
+ ruleBytesError: null,
+ rulePatternsError: null,
+ ruleStatusCode: null,
+ rulesStatusCode: null,
+ ruleResponse: null,
+ rulesResponse: null
+ });
+ };
+
+ updateParam = (callback) => {
+ const updatedRule = this.currentRule();
+ callback(updatedRule);
+ this.setState({newRule: updatedRule});
+ };
+
+ currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule;
+
+ addPattern = (pattern) => {
+ if (!this.validatePattern(pattern)) {
+ return;
+ }
+
+ const newPattern = _.cloneDeep(this.emptyPattern);
+ this.currentRule().patterns.push(pattern);
+ this.setState({
+ newPattern: newPattern
+ });
+ };
+
+ editPattern = (pattern) => {
+ this.setState({
+ selectedPattern: pattern
+ });
+ };
+
+ updatePattern = (pattern) => {
+ if (!this.validatePattern(pattern)) {
+ return;
+ }
+
+ this.setState({
+ selectedPattern: null
+ });
+ };
+
+ validatePattern = (pattern) => {
+ let valid = true;
+ if (pattern.regex === "") {
+ valid = false;
+ this.setState({patternRegexFocused: true});
+ }
+ if (pattern.min_occurrences > pattern.max_occurrences) {
+ valid = false;
+ this.setState({patternOccurrencesFocused: true});
+ }
+ return valid;
+ };
+
+ render() {
+ const isUpdate = this.state.selectedRule != null;
+ const rule = this.currentRule();
+ const pattern = this.state.selectedPattern || this.state.newPattern;
+
+ let rules = this.state.rules.map(r =>
+
{
+ this.reset();
+ this.setState({selectedRule: _.cloneDeep(r)});
+ }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}>
+ {r["id"].substring(0, 8)} |
+ {r["name"]} |
+ |
+ {r["notes"]} |
+
+ );
+
+ let patterns = (this.state.selectedPattern == null && !isUpdate ?
+ rule.patterns.concat(this.state.newPattern) :
+ rule.patterns
+ ).map(p => p === pattern ?
+
+
+ {
+ this.updateParam(() => pattern.regex = v);
+ this.setState({patternRegexFocused: pattern.regex === ""});
+ }}/>
+ |
+ this.updateParam(() => pattern.flags.caseless = v)}/> |
+ this.updateParam(() => pattern.flags.dot_all = v)}/> |
+ this.updateParam(() => pattern.flags.multi_line = v)}/> |
+ this.updateParam(() => pattern.flags.utf_8_mode = v)}/> |
+ this.updateParam(() => pattern.flags.unicode_property = v)}/> |
+
+ this.updateParam(() => pattern.min_occurrences = v)}/>
+ |
+
+ this.updateParam(() => pattern.max_occurrences = v)}/>
+ |
+ s", "s->c"]}
+ value={this.directions[pattern.direction]}
+ onChange={(v) => this.updateParam(() => pattern.direction = v)}/> |
+ {this.state.selectedPattern == null ?
+ this.addPattern(p)}/> :
+ this.updatePattern(p)}/>}
+ |
+
+ :
+
+ {p.regex} |
+ {p.flags.caseless ? "yes" : "no"} |
+ {p.flags.dot_all ? "yes" : "no"} |
+ {p.flags.multi_line ? "yes" : "no"} |
+ {p.flags.utf_8_mode ? "yes" : "no"} |
+ {p.flags.unicode_property ? "yes" : "no"} |
+ {p.min_occurrences} |
+ {p.max_occurrences} |
+ {this.directions[p.direction]} |
+ {!isUpdate && this.editPattern(p)}/> | }
+
+ );
+
+ return (
+
+
+
+ GET /api/rules
+ {this.state.rulesStatusCode &&
+ }
+
+
+
+
+
+
+
+ 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}
+ readonly={isUpdate}/>
+ this.updateParam((r) => r.filter.client_port = v)}
+ min={0} max={65565} error={this.state.ruleClientPortError}
+ readonly={isUpdate}/>
+ this.updateParam((r) => r.filter.client_address = v)}/>
+
+
+
+ this.updateParam((r) => r.filter.min_duration = v)}/>
+ this.updateParam((r) => r.filter.max_duration = v)}/>
+ this.updateParam((r) => r.filter.min_bytes = v)}/>
+ this.updateParam((r) => r.filter.max_bytes = v)}/>
+
+
+
+
+
+
+
+
+ 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/RulesPane.scss b/frontend/src/components/panels/RulesPane.scss
new file mode 100644
index 0000000..992445a
--- /dev/null
+++ b/frontend/src/components/panels/RulesPane.scss
@@ -0,0 +1,32 @@
+
+.rule-pane {
+ display: flex;
+ flex-direction: column;
+
+ .rules-list {
+ overflow: hidden;
+ flex: 2 1;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .rule-edit {
+ display: flex;
+ flex: 3 0;
+ flex-direction: column;
+
+ .section-content {
+ flex: 1;
+ }
+
+ .section-table {
+ max-height: 150px;
+ }
+ }
+}
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js
deleted file mode 100644
index fc7004b..0000000
--- a/frontend/src/components/panels/ServicePane.js
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * This file is part of caronte (https://github.com/eciavatta/caronte).
- * Copyright (c) 2020 Emiliano Ciavatta.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
.
- */
-
-import React, {Component} from 'react';
-import './common.scss';
-import './ServicePane.scss';
-import Table from "react-bootstrap/Table";
-import {Col, Container, Row} from "react-bootstrap";
-import InputField from "../fields/InputField";
-import TextField from "../fields/TextField";
-import backend from "../../backend";
-import NumericField from "../fields/extensions/NumericField";
-import ColorField from "../fields/extensions/ColorField";
-import ButtonField from "../fields/ButtonField";
-import validation from "../../validation";
-import LinkPopover from "../objects/LinkPopover";
-import {createCurlCommand} from "../../utils";
-import dispatcher from "../../dispatcher";
-
-const classNames = require('classnames');
-const _ = require('lodash');
-
-class ServicePane extends Component {
-
- emptyService = {
- "port": 0,
- "name": "",
- "color": "",
- "notes": ""
- };
-
- state = {
- services: [],
- currentService: this.emptyService,
- };
-
- componentDidMount() {
- this.reset();
- this.loadServices();
-
- dispatcher.register("notifications", payload => {
- if (payload.event === "services.edit") {
- this.loadServices();
- }
- });
-
- document.title = "caronte:~/services$";
- }
-
- loadServices = () => {
- backend.get("/api/services")
- .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
- .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)}));
- };
-
- updateService = () => {
- const service = this.state.currentService;
- if (this.validateService(service)) {
- backend.put("/api/services", service).then(res => {
- this.reset();
- this.setState({serviceStatusCode: res.status});
- this.loadServices();
- }).catch(res => {
- this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)});
- });
- }
- };
-
- validateService = (service) => {
- let valid = true;
- if (!validation.isValidPort(service.port, true)) {
- this.setState({servicePortError: "port < 0 || port > 65565"});
- valid = false;
- }
- if (service.name.length < 3) {
- this.setState({serviceNameError: "name.length < 3"});
- valid = false;
- }
- if (!validation.isValidColor(service.color)) {
- this.setState({serviceColorError: "color is not hexcolor"});
- valid = false;
- }
-
- return valid;
- };
-
- reset = () => {
- this.setState({
- isUpdate: false,
- currentService: _.cloneDeep(this.emptyService),
- servicePortError: null,
- serviceNameError: null,
- serviceColorError: null,
- serviceStatusCode: null,
- servicesStatusCode: null,
- serviceResponse: null,
- servicesResponse: null
- });
- };
-
- updateParam = (callback) => {
- callback(this.state.currentService);
- this.setState({currentService: this.state.currentService});
- };
-
- render() {
- const isUpdate = this.state.isUpdate;
- const service = this.state.currentService;
-
- let services = this.state.services.map(s =>
-
{
- this.reset();
- this.setState({isUpdate: true, currentService: _.cloneDeep(s)});
- }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}>
- {s["port"]} |
- {s["name"]} |
- |
- {s["notes"]} |
-
- );
-
- const curlCommand = createCurlCommand("/services", "PUT", service);
-
- return (
-
-
-
- GET /api/services
- {this.state.servicesStatusCode &&
- }
-
-
-
-
-
-
-
- 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)} />
-
-
-
-
-
-
-
-
- {}
-
-
-
-
- );
- }
-
-}
-
-export default ServicePane;
diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicePane.scss
deleted file mode 100644
index daf7e79..0000000
--- a/frontend/src/components/panels/ServicePane.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-
-.service-pane {
- display: flex;
- flex-direction: column;
-
- .services-list {
- overflow: hidden;
- flex: 1;
-
- .section-content {
- height: 100%;
- }
-
- .section-table {
- height: calc(100% - 30px);
- }
- }
-
- .service-edit {
- flex: 0;
- }
-}
diff --git a/frontend/src/components/panels/ServicesPane.js b/frontend/src/components/panels/ServicesPane.js
new file mode 100644
index 0000000..bc82356
--- /dev/null
+++ b/frontend/src/components/panels/ServicesPane.js
@@ -0,0 +1,212 @@
+/*
+ * This file is part of caronte (https://github.com/eciavatta/caronte).
+ * Copyright (c) 2020 Emiliano Ciavatta.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
.
+ */
+
+import React, {Component} from 'react';
+import './common.scss';
+import './ServicesPane.scss';
+import Table from "react-bootstrap/Table";
+import {Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import NumericField from "../fields/extensions/NumericField";
+import ColorField from "../fields/extensions/ColorField";
+import ButtonField from "../fields/ButtonField";
+import validation from "../../validation";
+import LinkPopover from "../objects/LinkPopover";
+import {createCurlCommand} from "../../utils";
+import dispatcher from "../../dispatcher";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class ServicesPane extends Component {
+
+ emptyService = {
+ "port": 0,
+ "name": "",
+ "color": "",
+ "notes": ""
+ };
+
+ state = {
+ services: [],
+ currentService: this.emptyService,
+ };
+
+ componentDidMount() {
+ this.reset();
+ this.loadServices();
+
+ dispatcher.register("notifications", payload => {
+ if (payload.event === "services.edit") {
+ this.loadServices();
+ }
+ });
+
+ document.title = "caronte:~/services$";
+ }
+
+ loadServices = () => {
+ backend.get("/api/services")
+ .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
+ .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)}));
+ };
+
+ updateService = () => {
+ const service = this.state.currentService;
+ if (this.validateService(service)) {
+ backend.put("/api/services", service).then(res => {
+ this.reset();
+ this.setState({serviceStatusCode: res.status});
+ this.loadServices();
+ }).catch(res => {
+ this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ validateService = (service) => {
+ let valid = true;
+ if (!validation.isValidPort(service.port, true)) {
+ this.setState({servicePortError: "port < 0 || port > 65565"});
+ valid = false;
+ }
+ if (service.name.length < 3) {
+ this.setState({serviceNameError: "name.length < 3"});
+ valid = false;
+ }
+ if (!validation.isValidColor(service.color)) {
+ this.setState({serviceColorError: "color is not hexcolor"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ reset = () => {
+ this.setState({
+ isUpdate: false,
+ currentService: _.cloneDeep(this.emptyService),
+ servicePortError: null,
+ serviceNameError: null,
+ serviceColorError: null,
+ serviceStatusCode: null,
+ servicesStatusCode: null,
+ serviceResponse: null,
+ servicesResponse: null
+ });
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.currentService);
+ this.setState({currentService: this.state.currentService});
+ };
+
+ render() {
+ const isUpdate = this.state.isUpdate;
+ const service = this.state.currentService;
+
+ let services = this.state.services.map(s =>
+
{
+ this.reset();
+ this.setState({isUpdate: true, currentService: _.cloneDeep(s)});
+ }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}>
+ {s["port"]} |
+ {s["name"]} |
+ |
+ {s["notes"]} |
+
+ );
+
+ const curlCommand = createCurlCommand("/services", "PUT", service);
+
+ return (
+
+
+
+ GET /api/services
+ {this.state.servicesStatusCode &&
+ }
+
+
+
+
+
+
+
+ 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)} />
+
+
+
+
+
+
+
+
+ {}
+
+
+
+
+ );
+ }
+
+}
+
+export default ServicesPane;
diff --git a/frontend/src/components/panels/ServicesPane.scss b/frontend/src/components/panels/ServicesPane.scss
new file mode 100644
index 0000000..daf7e79
--- /dev/null
+++ b/frontend/src/components/panels/ServicesPane.scss
@@ -0,0 +1,22 @@
+
+.service-pane {
+ display: flex;
+ flex-direction: column;
+
+ .services-list {
+ overflow: hidden;
+ flex: 1;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .service-edit {
+ flex: 0;
+ }
+}
diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js
new file mode 100644
index 0000000..c8bd121
--- /dev/null
+++ b/frontend/src/components/panels/StreamsPane.js
@@ -0,0 +1,242 @@
+/*
+ * This file is part of caronte (https://github.com/eciavatta/caronte).
+ * Copyright (c) 2020 Emiliano Ciavatta.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
.
+ */
+
+import React, {Component} from 'react';
+import './StreamsPane.scss';
+import {Row} from 'react-bootstrap';
+import MessageAction from "../objects/MessageAction";
+import backend from "../../backend";
+import ButtonField from "../fields/ButtonField";
+import ChoiceField from "../fields/ChoiceField";
+import DOMPurify from 'dompurify';
+import ReactJson from 'react-json-view'
+import {downloadBlob, getHeaderValue} from "../../utils";
+import log from "../../log";
+
+const classNames = require('classnames');
+
+class StreamsPane extends Component {
+
+ state = {
+ messages: [],
+ format: "default",
+ tryParse: true
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.validFormats = ["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"];
+ }
+
+ componentDidMount() {
+ if (this.props.connection && this.state.currentId !== this.props.connection.id) {
+ this.setState({currentId: this.props.connection.id});
+ this.loadStream(this.props.connection.id);
+ }
+
+ document.title = "caronte:~/$";
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (this.props.connection && (
+ this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
+ this.closeRenderWindow();
+ this.loadStream(this.props.connection.id);
+ }
+ }
+
+ componentWillUnmount() {
+ this.closeRenderWindow();
+ }
+
+ loadStream = (connectionId) => {
+ this.setState({messages: []});
+ backend.get(`/api/streams/${connectionId}?format=${this.state.format}`)
+ .then(res => this.setState({messages: res.json}));
+ };
+
+ setFormat = (format) => {
+ if (this.validFormats.includes(format)) {
+ this.setState({format: format});
+ }
+ };
+
+ tryParseConnectionMessage = (connectionMessage) => {
+ if (connectionMessage.metadata == null) {
+ return connectionMessage.content;
+ }
+ if (connectionMessage["is_metadata_continuation"]) {
+ return
**already parsed in previous messages**;
+ }
+
+ let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) =>
+
{key}: {value}
+ );
+
+ let m = connectionMessage.metadata;
+ switch (m.type) {
+ case "http-request":
+ let url =
{m.host}{m.url};
+ return
+ {m.method} {url} {m.protocol}
+ {unrollMap(m.headers)}
+ {m.body}
+ {unrollMap(m.trailers)}
+ ;
+ case "http-response":
+ const contentType = getHeaderValue(m, "Content-Type");
+ let body = m.body;
+ if (contentType && contentType.includes("application/json")) {
+ try {
+ const json = JSON.parse(m.body);
+ body =
;
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ return
+ {m.protocol} {m.status}
+ {unrollMap(m.headers)}
+ {body}
+ {unrollMap(m.trailers)}
+ ;
+ default:
+ return connectionMessage.content;
+ }
+ };
+
+ connectionsActions = (connectionMessage) => {
+ if (!connectionMessage.metadata) {
+ return null;
+ }
+
+ const m = connectionMessage.metadata;
+ switch (m.type) {
+ case "http-request" :
+ if (!connectionMessage.metadata["reproducers"]) {
+ return;
+ }
+ return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) =>
+ {
+ this.setState({
+ messageActionDialog: this.setState({messageActionDialog: null})}/>
+ });
+ }}/>
+ );
+ case "http-response":
+ const contentType = getHeaderValue(m, "Content-Type");
+
+ if (contentType && contentType.includes("text/html")) {
+ return {
+ let w;
+ if (this.state.renderWindow && !this.state.renderWindow.closed) {
+ w = this.state.renderWindow;
+ } else {
+ w = window.open("", "", "width=900, height=600, scrollbars=yes");
+ this.setState({renderWindow: w});
+ }
+ w.document.body.innerHTML = DOMPurify.sanitize(m.body);
+ w.focus();
+ }}/>;
+ }
+ break;
+ default:
+ return null;
+ }
+ };
+
+ downloadStreamRaw = (value) => {
+ if (this.state.currentId) {
+ backend.download(`/api/streams/${this.props.connection.id}/download?format=${this.state.format}&type=${value}`)
+ .then(res => downloadBlob(res.blob, `${this.state.currentId}-${value}-${this.state.format}.txt`))
+ .catch(_ => log.error("Failed to download stream messages"));
+ }
+ };
+
+ closeRenderWindow = () => {
+ if (this.state.renderWindow) {
+ this.state.renderWindow.close();
+ }
+ };
+
+ render() {
+ const conn = this.props.connection || {
+ "ip_src": "0.0.0.0",
+ "ip_dst": "0.0.0.0",
+ "port_src": "0",
+ "port_dst": "0",
+ "started_at": new Date().toISOString(),
+ };
+ const content = this.state.messages || [];
+
+ let payload = content.map((c, i) =>
+
+
+
+
+ offset: {c.index} | timestamp: {c.timestamp}
+ | retransmitted: {c["is_retransmitted"] ? "yes" : "no"}
+
+
{this.connectionsActions(c)}
+
+
+
{c["from_client"] ? "client" : "server"}
+
+ {this.state.tryParse && this.state.format === "default" ? this.tryParseConnectionMessage(c) : c.content}
+
+
+ );
+
+ return (
+
+
+
+
+ flow: {conn["ip_src"]}:{conn["port_src"]} -> {conn["ip_dst"]}:{conn["port_dst"]}
+ | timestamp: {conn["started_at"]}
+
+
+
+
+
+
+
+
+
+
+
+
{payload}
+ {this.state.messageActionDialog}
+
+ );
+ }
+
+}
+
+
+export default StreamsPane;
diff --git a/frontend/src/components/panels/StreamsPane.scss b/frontend/src/components/panels/StreamsPane.scss
new file mode 100644
index 0000000..d5510cf
--- /dev/null
+++ b/frontend/src/components/panels/StreamsPane.scss
@@ -0,0 +1,113 @@
+@import "../../colors";
+
+.connection-content {
+ height: 100%;
+ background-color: $color-primary-0;
+
+ pre {
+ overflow-x: hidden;
+ height: calc(100% - 31px);
+ padding: 0 10px;
+ white-space: pre-wrap;
+ word-break: break-word;
+
+ p {
+ margin: 0;
+ padding: 0;
+ }
+ }
+
+ .connection-message {
+ position: relative;
+ margin: 10px 0;
+ border: 4px solid $color-primary-3;
+ border-top: 0;
+
+ .connection-message-header {
+ height: 25px;
+ background-color: $color-primary-3;
+
+ .connection-message-info {
+ font-size: 11px;
+ margin-top: 6px;
+ margin-left: -10px;
+ }
+
+ .connection-message-actions {
+ display: none;
+ margin-right: -18px;
+
+ button {
+ font-size: 11px;
+ margin: 0 3px;
+ padding: 5px;
+ }
+ }
+ }
+
+ .message-content {
+ padding: 10px;
+
+ .react-json-view {
+ background-color: inherit !important;
+ }
+ }
+
+ &:hover .connection-message-actions {
+ display: flex;
+ }
+
+ .connection-message-label {
+ font-size: 12px;
+ position: absolute;
+ top: 0;
+ padding: 10px 0;
+ background-color: $color-primary-3;
+ writing-mode: vertical-rl;
+ text-orientation: mixed;
+ }
+
+ &.from-client {
+ margin-right: 100px;
+ color: $color-primary-4;
+
+ .connection-message-label {
+ right: -22px;
+ }
+ }
+
+ &.from-server {
+ margin-left: 100px;
+ color: $color-primary-4;
+
+ .connection-message-label {
+ left: -22px;
+ transform: rotate(-180deg);
+ }
+ }
+ }
+
+ .connection-content-header {
+ height: 33px;
+ padding: 0;
+ background-color: $color-primary-3;
+
+ .header-info {
+ font-size: 12px;
+ padding-top: 7px;
+ padding-left: 25px;
+ }
+
+ .header-actions {
+ display: flex;
+
+ .choice-field {
+ margin-top: -5px;
+
+ .field-value {
+ background-color: $color-primary-3;
+ }
+ }
+ }
+ }
+}
--
cgit v1.2.3-70-g09d2