diff options
author | JJ | 2024-07-19 00:38:57 +0000 |
---|---|---|
committer | JJ | 2024-07-19 00:38:57 +0000 |
commit | c5434cccee8661de3082c0c777375eb8e6f76865 (patch) | |
tree | 772c5a5e821c8b731a97f3b559c79c039bbc29df /frontend/src/components/panels | |
parent | 74b133ca50b094231721e82ac94448e28b0aa377 (diff) |
remove unnecessary jsx stuff
Diffstat (limited to 'frontend/src/components/panels')
-rw-r--r-- | frontend/src/components/panels/ConnectionsPane.jsx | 310 | ||||
-rw-r--r-- | frontend/src/components/panels/MainPane.jsx | 112 | ||||
-rw-r--r-- | frontend/src/components/panels/PcapsPane.jsx | 287 | ||||
-rw-r--r-- | frontend/src/components/panels/RulesPane.jsx | 469 | ||||
-rw-r--r-- | frontend/src/components/panels/SearchPane.jsx | 309 | ||||
-rw-r--r-- | frontend/src/components/panels/ServicesPane.jsx | 233 | ||||
-rw-r--r-- | frontend/src/components/panels/StatsPane.jsx | 274 | ||||
-rw-r--r-- | frontend/src/components/panels/StreamsPane.jsx | 453 |
8 files changed, 0 insertions, 2447 deletions
diff --git a/frontend/src/components/panels/ConnectionsPane.jsx b/frontend/src/components/panels/ConnectionsPane.jsx deleted file mode 100644 index 2c7fadd..0000000 --- a/frontend/src/components/panels/ConnectionsPane.jsx +++ /dev/null @@ -1,310 +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 <http://www.gnu.org/licenses/>. - */ - -import React, {Component} from "react"; -import Table from "react-bootstrap/Table"; -import {Redirect} from "react-router"; -import {withRouter} from "react-router-dom"; -import backend from "../../backend"; -import dispatcher from "../../dispatcher"; -import log from "../../log"; -import {updateParams} from "../../utils"; -import ButtonField from "../fields/ButtonField"; -import Connection from "../objects/Connection"; -import ConnectionMatchedRules from "../objects/ConnectionMatchedRules"; -import "./ConnectionsPane.scss"; - -import classNames from 'classnames'; - -class ConnectionsPane extends Component { - - state = { - loading: false, - connections: [], - firstConnection: null, - lastConnection: null, - }; - - constructor(props) { - super(props); - - this.scrollTopThreashold = 0.00001; - this.scrollBottomThreashold = 0.99999; - this.maxConnections = 200; - this.queryLimit = 50; - this.connectionsListRef = React.createRef(); - this.lastScrollPosition = 0; - } - - componentDidMount() { - let urlParams = new URLSearchParams(this.props.location.search); - this.setState({urlParams}); - - const additionalParams = {limit: this.queryLimit}; - - const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); - if (match != null) { - const id = match[1]; - additionalParams.from = id; - backend.get(`/api/connections/${id}`) - .then((res) => this.connectionSelected(res.json)) - .catch((error) => log.error("Error loading initial connection", error)); - } - - this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded")); - - dispatcher.register("connections_filters", this.handleConnectionsFilters); - dispatcher.register("timeline_updates", this.handleTimelineUpdates); - dispatcher.register("notifications", this.handleNotifications); - dispatcher.register("pulse_connections_view", this.handlePulseConnectionsView); - } - - componentWillUnmount() { - dispatcher.unregister(this.handleConnectionsFilters); - dispatcher.unregister(this.handleTimelineUpdates); - dispatcher.unregister(this.handleNotifications); - dispatcher.unregister(this.handlePulseConnectionsView); - } - - handleConnectionsFilters = (payload) => { - const newParams = updateParams(this.state.urlParams, payload); - if (this.state.urlParams.toString() === newParams.toString()) { - return; - } - - log.debug("Update following url params:", payload); - this.queryStringRedirect = true; - this.setState({urlParams: newParams}); - - this.loadConnections({limit: this.queryLimit}, newParams) - .then(() => log.info("ConnectionsPane reloaded after query string update")); - }; - - handleTimelineUpdates = (payload) => { - this.connectionsListRef.current.scrollTop = 0; - this.loadConnections({ - "started_after": Math.round(payload.from.getTime() / 1000), - "started_before": Math.round(payload.to.getTime() / 1000), - limit: this.maxConnections - }).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`)); - }; - - handleNotifications = (payload) => { - if (payload.event === "rules.new" || payload.event === "rules.edit") { - this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); - } - if (payload.event === "services.edit") { - this.loadServices().then(() => log.debug("Services reloaded after notification update")); - } - }; - - handlePulseConnectionsView = (payload) => { - this.setState({pulseConnectionsView: true}); - setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration); - }; - - connectionSelected = (c) => { - this.connectionSelectedRedirect = true; - this.setState({selected: c.id}); - this.props.onSelected(c); - log.debug(`Connection ${c.id} selected`); - }; - - handleScroll = (e) => { - if (this.disableScrollHandler) { - this.lastScrollPosition = e.currentTarget.scrollTop; - return; - } - - let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight); - if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) { - this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,}) - .then(() => log.info("Following connections loaded")); - } - if (!this.state.loading && relativeScroll < this.scrollTopThreashold) { - this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,}) - .then(() => log.info("Previous connections loaded")); - if (this.state.showMoreRecentButton) { - this.setState({showMoreRecentButton: false}); - } - } else { - if (this.lastScrollPosition > e.currentTarget.scrollTop) { - if (!this.state.showMoreRecentButton) { - this.setState({showMoreRecentButton: true}); - } - } else { - if (this.state.showMoreRecentButton) { - this.setState({showMoreRecentButton: false}); - } - } - } - this.lastScrollPosition = e.currentTarget.scrollTop; - }; - - async loadConnections(additionalParams, initialParams = null, isInitial = false) { - if (!initialParams) { - initialParams = this.state.urlParams; - } - const urlParams = new URLSearchParams(initialParams.toString()); - for (const [name, value] of Object.entries(additionalParams)) { - urlParams.set(name, value); - } - - this.setState({loading: true}); - if (!this.state.rules) { - await this.loadRules(); - } - if (!this.state.services) { - await this.loadServices(); - } - - let res = (await backend.get(`/api/connections?${urlParams}`)).json; - - let connections = this.state.connections; - let firstConnection = this.state.firstConnection; - let lastConnection = this.state.lastConnection; - - if (additionalParams && additionalParams.from && !additionalParams.to) { - if (res.length > 0) { - if (!isInitial) { - res = res.slice(1); - } - connections = this.state.connections.concat(res); - lastConnection = connections[connections.length - 1]; - if (isInitial) { - firstConnection = connections[0]; - } - if (connections.length > this.maxConnections) { - connections = connections.slice(connections.length - this.maxConnections, - connections.length - 1); - firstConnection = connections[0]; - } - } - } else if (additionalParams && additionalParams.to && !additionalParams.from) { - if (res.length > 0) { - connections = res.slice(0, res.length - 1).concat(this.state.connections); - firstConnection = connections[0]; - if (connections.length > this.maxConnections) { - connections = connections.slice(0, this.maxConnections); - lastConnection = connections[this.maxConnections - 1]; - } - } - } else { - if (res.length > 0) { - connections = res; - firstConnection = connections[0]; - lastConnection = connections[connections.length - 1]; - } else { - connections = []; - firstConnection = null; - lastConnection = null; - } - } - - this.setState({loading: false, connections, firstConnection, lastConnection}); - - if (firstConnection != null && lastConnection != null) { - dispatcher.dispatch("connection_updates", { - from: new Date(lastConnection["started_at"]), - to: new Date(firstConnection["started_at"]) - }); - } - } - - loadRules = async () => { - return backend.get("/api/rules").then((res) => this.setState({rules: res.json})); - }; - - loadServices = async () => { - return backend.get("/api/services").then((res) => this.setState({services: res.json})); - }; - - render() { - let redirect; - if (this.connectionSelectedRedirect) { - redirect = <Redirect push to={`/connections/${this.state.selected}?${this.state.urlParams}`}/>; - this.connectionSelectedRedirect = false; - } else if (this.queryStringRedirect) { - redirect = <Redirect push to={`${this.props.location.pathname}?${this.state.urlParams}`}/>; - this.queryStringRedirect = false; - } - - let loading = null; - if (this.state.loading) { - loading = <tr> - <td colSpan={10}>Loading...</td> - </tr>; - } - - return ( - <div className="connections-container"> - {this.state.showMoreRecentButton && <div className="most-recent-button"> - <ButtonField name="most_recent" variant="green" bordered onClick={() => { - this.disableScrollHandler = true; - this.connectionsListRef.current.scrollTop = 0; - this.loadConnections({limit: this.queryLimit}) - .then(() => { - this.disableScrollHandler = false; - log.info("Most recent connections loaded"); - }); - }}/> - </div>} - - <div className={classNames("connections", {"connections-pulse": this.state.pulseConnectionsView})} - onScroll={this.handleScroll} ref={this.connectionsListRef}> - <Table borderless size="sm"> - <thead> - <tr> - <th>service</th> - <th>srcip</th> - <th>srcport</th> - <th>dstip</th> - <th>dstport</th> - <th>started_at</th> - <th>duration</th> - <th>up</th> - <th>down</th> - <th>actions</th> - </tr> - </thead> - <tbody> - { - this.state.connections.flatMap((c) => { - return [<Connection key={c.id} data={c} onSelected={() => 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 && - <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules} - rules={this.state.rules}/> - ]; - }) - } - {loading} - </tbody> - </Table> - - {redirect} - </div> - </div> - ); - } - -} - -export default withRouter(ConnectionsPane); diff --git a/frontend/src/components/panels/MainPane.jsx b/frontend/src/components/panels/MainPane.jsx deleted file mode 100644 index ce72be5..0000000 --- a/frontend/src/components/panels/MainPane.jsx +++ /dev/null @@ -1,112 +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 <http://www.gnu.org/licenses/>. - */ - -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: <StreamsPane/>}); - case 3: - this.setState({backgroundPane: null}); - return dispatcher.dispatch("pulse_timeline", {duration: 12000}); - case 6: - return this.setState({backgroundPane: <PcapsPane/>}); - case 7: - return this.setState({backgroundPane: <RulesPane/>}); - case 8: - return this.setState({backgroundPane: <ServicesPane/>}); - case 10: - return this.setState({backgroundPane: null}); - default: - return; - } - }, - }; - this.typed = new Typed(this.el, options); - } - - componentWillUnmount() { - this.typed.destroy(); - } - - render() { - return ( - <div className="pane-container"> - <div className="main-pane"> - <div className="pane-section"> - <div className="tutorial"> - <span style={{whiteSpace: "pre"}} ref={(el) => { - this.el = el; - }}/> - </div> - </div> - </div> - <div className="background-pane"> - {this.state.backgroundPane} - </div> - </div> - ); - } - -} - -export default MainPane; diff --git a/frontend/src/components/panels/PcapsPane.jsx b/frontend/src/components/panels/PcapsPane.jsx deleted file mode 100644 index b7d5ce9..0000000 --- a/frontend/src/components/panels/PcapsPane.jsx +++ /dev/null @@ -1,287 +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 <http://www.gnu.org/licenses/>. - */ - -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 = <div> - <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/> - <span>Completed at {completedAt.toLocaleDateString() + " " + completedAt.toLocaleTimeString()}</span> - </div>; - - return <tr key={s.id} className="row-small row-clickable"> - <td><CopyLinkPopover text={s["id"].substring(0, 8)} value={s["id"]}/></td> - <td> - <LinkPopover text={dateTimeToTime(s["started_at"])} content={timeInfo} placement="right"/> - </td> - <td>{durationBetween(s["started_at"], s["completed_at"])}</td> - <td>{formatSize(s["size"])}</td> - <td>{s["processed_packets"]}</td> - <td>{s["invalid_packets"]}</td> - <td><LinkPopover text={Object.keys(s["packets_per_service"]).length + " services"} - content={JSON.stringify(s["packets_per_service"])} - placement="left"/></td> - <td className="table-cell-action"><a href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a> - </td> - </tr>; - }); - - 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 ( - <div className="pane-container pcap-pane"> - <div className="pane-section pcap-list"> - <div className="section-header"> - <span className="api-request">GET /api/pcap/sessions</span> - <span className="api-response"><LinkPopover text={this.state.sessionsStatusCode} - content={this.state.sessionsResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>id</th> - <th>started_at</th> - <th>duration</th> - <th>size</th> - <th>processed_packets</th> - <th>invalid_packets</th> - <th>packets_per_service</th> - <th>actions</th> - </tr> - </thead> - <tbody> - {sessions} - </tbody> - </Table> - </div> - </div> - </div> - - <div className="double-pane-container"> - <div className="pane-section"> - <div className="section-header"> - <span className="api-request">POST /api/pcap/upload</span> - <span className="api-response"><LinkPopover text={this.state.uploadStatusCode} - content={this.state.uploadResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <InputField type={"file"} name={"file"} invalid={!this.state.isUploadFileValid} - active={this.state.isUploadFileFocused} - onChange={handleUploadFileChange} value={this.state.uploadSelectedFile} - placeholder={"no .pcap[ng] selected"}/> - <div className="upload-actions"> - <div className="upload-options"> - <span>options:</span> - <CheckField name="flush_all" checked={this.state.uploadFlushAll} - onChange={(v) => this.setState({uploadFlushAll: v})}/> - </div> - <ButtonField variant="green" bordered onClick={this.uploadPcap} name="upload"/> - </div> - - <TextField value={uploadCurlCommand} rows={4} readonly small={true}/> - </div> - </div> - - <div className="pane-section"> - <div className="section-header"> - <span className="api-request">POST /api/pcap/file</span> - <span className="api-response"><LinkPopover text={this.state.processStatusCode} - content={this.state.processResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <InputField name="file" active={this.state.isFileFocused} invalid={!this.state.isFileValid} - onChange={handleFileChange} value={this.state.fileValue} - placeholder={"local .pcap[ng] path"} inline/> - - <div className="upload-actions" style={{"marginTop": "11px"}}> - <div className="upload-options"> - <CheckField name="flush_all" checked={this.state.processFlushAll} - onChange={(v) => this.setState({processFlushAll: v})}/> - <CheckField name="delete_original_file" checked={this.state.deleteOriginalFile} - onChange={(v) => this.setState({deleteOriginalFile: v})}/> - </div> - <ButtonField variant="blue" bordered onClick={this.processPcap} name="process"/> - </div> - - <TextField value={fileCurlCommand} rows={4} readonly small={true}/> - </div> - </div> - </div> - </div> - ); - } -} - -export default PcapsPane; diff --git a/frontend/src/components/panels/RulesPane.jsx b/frontend/src/components/panels/RulesPane.jsx deleted file mode 100644 index 4cb5e41..0000000 --- a/frontend/src/components/panels/RulesPane.jsx +++ /dev/null @@ -1,469 +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 <http://www.gnu.org/licenses/>. - */ - -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) => - <tr key={r.id} onClick={() => { - this.reset(); - this.setState({ selectedRule: _.cloneDeep(r) }); - }} className={classNames("row-small", "row-clickable", { "row-selected": rule.id === r.id })}> - <td><CopyLinkPopover text={r["id"].substring(0, 8)} value={r["id"]} /></td> - <td>{r["name"]}</td> - <td><ButtonField name={r["color"]} color={r["color"]} small /></td> - <td>{r["notes"]}</td> - </tr> - ); - - let patterns = (this.state.selectedPattern == null && !isUpdate ? - rule.patterns.concat(this.state.newPattern) : - rule.patterns - ).map((p) => p === pattern ? - <tr key={"new_pattern"}> - <td style={{ "width": "500px" }}> - <InputField small active={this.state.patternRegexFocused} value={pattern.regex} - onChange={(v) => { - this.updateParam(() => pattern.regex = v); - this.setState({ patternRegexFocused: pattern.regex === "" }); - }} /> - </td> - <td><CheckField small checked={pattern.flags["caseless"]} - onChange={(v) => this.updateParam(() => pattern.flags["caseless"] = v)} /></td> - <td><CheckField small checked={pattern.flags["dot_all"]} - onChange={(v) => this.updateParam(() => pattern.flags["dot_all"] = v)} /></td> - <td><CheckField small checked={pattern.flags["multi_line"]} - onChange={(v) => this.updateParam(() => pattern.flags["multi_line"] = v)} /></td> - <td><CheckField small checked={pattern.flags["utf_8_mode"]} - onChange={(v) => this.updateParam(() => pattern.flags["utf_8_mode"] = v)} /></td> - <td><CheckField small checked={pattern.flags["unicode_property"]} - onChange={(v) => this.updateParam(() => pattern.flags["unicode_property"] = v)} /></td> - <td style={{ "width": "70px" }}> - <NumericField small value={pattern["min_occurrences"]} - active={this.state.patternOccurrencesFocused} - onChange={(v) => this.updateParam(() => pattern["min_occurrences"] = v)} /> - </td> - <td style={{ "width": "70px" }}> - <NumericField small value={pattern["max_occurrences"]} - active={this.state.patternOccurrencesFocused} - onChange={(v) => this.updateParam(() => pattern["max_occurrences"] = v)} /> - </td> - <td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]} - value={this.directions[pattern.direction]} - onChange={(v) => this.updateParam(() => pattern.direction = v)} /></td> - <td>{this.state.selectedPattern == null ? - <ButtonField variant="green" small name="add" inline rounded onClick={() => this.addPattern(p)} /> : - <ButtonField variant="green" small name="save" inline rounded - onClick={() => this.updatePattern(p)} />} - </td> - </tr> - : - <tr key={"new_pattern"} className="row-small"> - <td>{p.regex}</td> - <td>{p.flags["caseless"] ? "yes" : "no"}</td> - <td>{p.flags["dot_all"] ? "yes" : "no"}</td> - <td>{p.flags["multi_line"] ? "yes" : "no"}</td> - <td>{p.flags["utf_8_mode"] ? "yes" : "no"}</td> - <td>{p.flags["unicode_property"] ? "yes" : "no"}</td> - <td>{p["min_occurrences"]}</td> - <td>{p["max_occurrences"]}</td> - <td>{this.directions[p.direction]}</td> - <td> - <ButtonField - variant="blue" - small - rounded - name="edit" - onClick={() => this.editPattern(p)} - /> - </td> - </tr> - ); - - return ( - <div className="pane-container rule-pane"> - <div className="pane-section rules-list"> - <div className="section-header"> - <span className="api-request">GET /api/rules</span> - {this.state.rulesStatusCode && - <span className="api-response"><LinkPopover text={this.state.rulesStatusCode} - content={this.state.rulesResponse} - placement="left" /></span>} - </div> - - <div className="section-content"> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>id</th> - <th>name</th> - <th>color</th> - <th>notes</th> - </tr> - </thead> - <tbody> - {rules} - </tbody> - </Table> - </div> - </div> - </div> - - <div className="pane-section rule-edit"> - <div className="section-header"> - <span className="api-request"> - {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} - </span> - <span className="api-response"><LinkPopover text={this.state.ruleStatusCode} - content={this.state.ruleResponse} - placement="left" /></span> - </div> - - <div className="section-content"> - <Container className="p-0"> - <Row> - <Col> - <InputField name="name" inline value={rule.name} - onChange={(v) => this.updateParam((r) => r.name = v)} - error={this.state.ruleNameError} /> - <ColorField inline value={rule.color} error={this.state.ruleColorError} - onChange={(v) => this.updateParam((r) => r.color = v)} /> - <TextField name="notes" rows={2} value={rule.notes} - onChange={(v) => this.updateParam((r) => r.notes = v)} /> - </Col> - - <Col style={{ "paddingTop": "6px" }}> - <span>filters:</span> - <NumericField name="service_port" - inline - value={rule.filter["service_port"]} - onChange={(v) => this.updateParam((r) => r.filter["service_port"] = v)} - min={0} - max={65565} - error={this.state.ruleServicePortError} - /> - <NumericField name="client_port" - inline - value={rule.filter["client_port"]} - onChange={(v) => this.updateParam((r) => r.filter["client_port"] = v)} - min={0} - max={65565} - error={this.state.ruleClientPortError} - /> - <InputField name="client_address" - value={rule.filter["client_address"]} - error={this.state.ruleClientAddressError} - onChange={(v) => this.updateParam((r) => r.filter["client_address"] = v)} /> - </Col> - - <Col style={{ "paddingTop": "11px" }}> - <NumericField name="min_duration" inline value={rule.filter["min_duration"]} - error={this.state.ruleDurationError} readonly={isUpdate} - onChange={(v) => this.updateParam((r) => r.filter["min_duration"] = v)} /> - <NumericField name="max_duration" inline value={rule.filter["max_duration"]} - error={this.state.ruleDurationError} readonly={isUpdate} - onChange={(v) => this.updateParam((r) => r.filter["max_duration"] = v)} /> - <NumericField name="min_bytes" inline value={rule.filter["min_bytes"]} - error={this.state.ruleBytesError} readonly={isUpdate} - onChange={(v) => this.updateParam((r) => r.filter["min_bytes"] = v)} /> - <NumericField name="max_bytes" inline value={rule.filter["max_bytes"]} - error={this.state.ruleBytesError} readonly={isUpdate} - onChange={(v) => this.updateParam((r) => r.filter["max_bytes"] = v)} /> - </Col> - </Row> - </Container> - - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>regex</th> - <th>!Aa</th> - <th>.*</th> - <th>\n+</th> - <th>UTF8</th> - <th>Uni_</th> - <th>min</th> - <th>max</th> - <th>direction</th> - {!isUpdate && <th>actions</th>} - </tr> - </thead> - <tbody> - {patterns} - </tbody> - </Table> - {this.state.rulePatternsError != null && - <span className="table-error">error: {this.state.rulePatternsError}</span>} - </div> - </div> - - <div className="section-footer"> - {<ButtonField variant="red" name="cancel" bordered onClick={this.reset} />} - <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_rule" : "add_rule"} - bordered onClick={isUpdate ? this.updateRule : this.addRule} /> - <ButtonField variant="red" name="delete_rule" bordered onClick={this.deleteRule} /> - </div> - </div> - </div> - ); - } - -} - -export default RulesPane; diff --git a/frontend/src/components/panels/SearchPane.jsx b/frontend/src/components/panels/SearchPane.jsx deleted file mode 100644 index 6fe9dc7..0000000 --- a/frontend/src/components/panels/SearchPane.jsx +++ /dev/null @@ -1,309 +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 <http://www.gnu.org/licenses/>. - */ - -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) => - <tr key={s.id} className="row-small row-clickable"> - <td>{s.id.substring(0, 8)}</td> - <td>{this.extractPattern(s["search_options"])}</td> - <td>{s["affected_connections_count"]}</td> - <td>{dateTimeToTime(s["started_at"])}</td> - <td>{durationBetween(s["started_at"], s["finished_at"])}</td> - <td><ButtonField name="view" variant="green" small onClick={() => this.viewSearch(s.id)}/></td> - </tr> - ); - - 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 ( - <div className="pane-container search-pane"> - <div className="pane-section searches-list"> - <div className="section-header"> - <span className="api-request">GET /api/searches</span> - {this.state.searchesStatusCode && - <span className="api-response"><LinkPopover text={this.state.searchesStatusCode} - content={this.state.searchesResponse} - placement="left"/></span>} - </div> - - <div className="section-content"> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>id</th> - <th>pattern</th> - <th>occurrences</th> - <th>started_at</th> - <th>duration</th> - <th>actions</th> - </tr> - </thead> - <tbody> - {searches} - </tbody> - </Table> - </div> - </div> - </div> - - <div className="pane-section search-new"> - <div className="section-header"> - <span className="api-request">POST /api/searches/perform</span> - <span className="api-response"><LinkPopover text={this.state.searchStatusCode} - content={this.state.searchResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <span className="notes"> - NOTE: it is recommended to use the rules for recurring themes. Give preference to textual search over that with regex. - </span> - - <div className="content-row"> - <div className="text-search"> - <TagField tags={(options["text_search"].terms || []).map((t) => { - 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))}/> - <TagField tags={(options["text_search"]["excluded_terms"] || []).map((t) => { - 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))}/> - - <span className="exclusive-separator">or</span> - - <InputField name="exact_phrase" value={options["text_search"]["exact_phrase"]} inline - error={this.state.exactPhraseError} - onChange={(v) => this.updateParam((s) => s["text_search"]["exact_phrase"] = v)} - readonly={regexOptionsModified || (Array.isArray(options["text_search"].terms) && options["text_search"].terms.length > 0)}/> - - <CheckField checked={options["text_search"]["case_sensitive"]} name="case_sensitive" - readonly={regexOptionsModified} small - onChange={(v) => this.updateParam((s) => s["text_search"]["case_sensitive"] = v)}/> - </div> - - <div className="separator"> - <span>or</span> - </div> - - <div className="regex-search"> - <InputField name="pattern" value={options["regex_search"].pattern} inline - error={this.state.patternError} - readonly={textOptionsModified || options["regex_search"]["not_pattern"]} - onChange={(v) => this.updateParam((s) => s["regex_search"].pattern = v)}/> - <span className="exclusive-separator">or</span> - <InputField name="not_pattern" value={options["regex_search"]["not_pattern"]} inline - error={this.state.notPatternError} - readonly={textOptionsModified || options["regex_search"].pattern} - onChange={(v) => this.updateParam((s) => s["regex_search"]["not_pattern"] = v)}/> - - <div className="checkbox-line"> - <CheckField checked={options["regex_search"]["case_insensitive"]} - name="case_insensitive" - readonly={textOptionsModified} small - onChange={(v) => this.updateParam((s) => s["regex_search"]["case_insensitive"] = v)}/> - <CheckField checked={options["regex_search"]["multi_line"]} name="multi_line" - readonly={textOptionsModified} small - onChange={(v) => this.updateParam((s) => s["regex_search"]["multi_line"] = v)}/> - <CheckField checked={options["regex_search"]["ignore_whitespaces"]} - name="ignore_whitespaces" - readonly={textOptionsModified} small - onChange={(v) => this.updateParam((s) => s["regex_search"]["ignore_whitespaces"] = v)}/> - <CheckField checked={options["regex_search"]["dot_character"]} name="dot_character" - readonly={textOptionsModified} small - onChange={(v) => this.updateParam((s) => s["regex_search"]["dot_character"] = v)}/> - </div> - </div> - </div> - - <TextField value={curlCommand} rows={3} readonly small={true}/> - </div> - - <div className="section-footer"> - <ButtonField variant="red" name="cancel" bordered disabled={this.state.loading} - onClick={this.reset}/> - <ButtonField variant="green" name="perform_search" bordered - disabled={this.state.loading} onClick={this.performSearch}/> - </div> - </div> - </div> - ); - } - -} - -export default SearchPane; diff --git a/frontend/src/components/panels/ServicesPane.jsx b/frontend/src/components/panels/ServicesPane.jsx deleted file mode 100644 index 296b329..0000000 --- a/frontend/src/components/panels/ServicesPane.jsx +++ /dev/null @@ -1,233 +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 <http://www.gnu.org/licenses/>. - */ - -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) => - <tr key={s.port} onClick={() => { - this.reset(); - this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); - }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port})}> - <td>{s["port"]}</td> - <td>{s["name"]}</td> - <td><ButtonField name={s["color"]} color={s["color"]} small/></td> - <td>{s["notes"]}</td> - </tr> - ); - - const curlCommand = createCurlCommand("/services", "PUT", service); - - return ( - <div className="pane-container service-pane"> - <div className="pane-section services-list"> - <div className="section-header"> - <span className="api-request">GET /api/services</span> - {this.state.servicesStatusCode && - <span className="api-response"><LinkPopover text={this.state.servicesStatusCode} - content={this.state.servicesResponse} - placement="left"/></span>} - </div> - - <div className="section-content"> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>port</th> - <th>name</th> - <th>color</th> - <th>notes</th> - </tr> - </thead> - <tbody> - {services} - </tbody> - </Table> - </div> - </div> - </div> - - <div className="pane-section service-edit"> - <div className="section-header"> - <span className="api-request">PUT /api/services</span> - <span className="api-response"><LinkPopover text={this.state.serviceStatusCode} - content={this.state.serviceResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <Container className="p-0"> - <Row> - <Col> - <NumericField name="port" value={service.port} - onChange={(v) => this.updateParam((s) => s.port = v)} - min={0} max={65565} error={this.state.servicePortError}/> - <InputField name="name" value={service.name} - onChange={(v) => this.updateParam((s) => s.name = v)} - error={this.state.serviceNameError}/> - <ColorField value={service.color} error={this.state.serviceColorError} - onChange={(v) => this.updateParam((s) => s.color = v)}/> - </Col> - - <Col> - <TextField name="notes" rows={7} value={service.notes} - onChange={(v) => this.updateParam((s) => s.notes = v)}/> - </Col> - </Row> - </Container> - - <TextField value={curlCommand} rows={3} readonly small={true}/> - </div> - - <div className="section-footer"> - {<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>} - {isUpdate && <ButtonField variant="red" name= "delete_service" - bordered onClick={this.deleteService}/>} - <ButtonField variant={isUpdate ? "blue" : "green"} - name={isUpdate ? "update_service" : "add_service"} - bordered onClick={this.updateService}/> - </div> - </div> - </div> - ); - } - -} - -export default ServicesPane; diff --git a/frontend/src/components/panels/StatsPane.jsx b/frontend/src/components/panels/StatsPane.jsx deleted file mode 100644 index a35ef0c..0000000 --- a/frontend/src/components/panels/StatsPane.jsx +++ /dev/null @@ -1,274 +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 <http://www.gnu.org/licenses/>. - */ - -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 <tr key={port} className="row-small row-clickable"> - <td>{port}</td> - <td>{formatSize(s["connections_per_service"][port])}</td> - <td>{formatSize(s["client_bytes_per_service"][port])}B</td> - <td>{formatSize(s["server_bytes_per_service"][port])}B</td> - <td>{formatSize(s["total_bytes_per_service"][port])}B</td> - <td>{formatSize(s["duration_per_service"][port] / 1000)}s</td> - </tr>; - }); - servicesStats.push(<tr key="totals" className="row-small row-clickable font-weight-bold"> - <td>totals</td> - <td>{formatSize(connections)}</td> - <td>{formatSize(clientBytes)}B</td> - <td>{formatSize(serverBytes)}B</td> - <td>{formatSize(totalBytes)}B</td> - <td>{formatSize(duration / 1000)}s</td> - </tr>); - - const rulesStats = this.state.rules.map((r) => - <tr key={r.id} className="row-small row-clickable"> - <td><CopyLinkPopover text={r["id"].substring(0, 8)} value={r["id"]}/></td> - <td>{r["name"]}</td> - <td><ButtonField name={r["color"]} color={r["color"]} small/></td> - <td>{formatSize(s && s["matched_rules"] && s["matched_rules"][r.id] ? s["matched_rules"][r.id] : 0)}</td> - </tr> - ); - - const cpuStats = (rs ? rs["cpu_times"] : []).map((cpu, index) => - <tr key={cpu["cpu"]} className="row-small row-clickable"> - <td>{cpu["cpu"]}</td> - <td>{cpu["user"]}</td> - <td>{cpu["system"]}</td> - <td>{cpu["idle"]}</td> - <td>{cpu["nice"]}</td> - <td>{cpu["iowait"]}</td> - <td>{rs["cpu_percents"][index].toFixed(2)} %</td> - </tr> - ); - - return ( - <div className="pane-container stats-pane"> - <div className="pane-section stats-list"> - <div className="section-header"> - <span className="api-request">GET /api/statistics/totals</span> - <span className="api-response"><LinkPopover text={this.state.statsStatusCode} - content={this.state.statsResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>service</th> - <th>connections</th> - <th>client_bytes</th> - <th>server_bytes</th> - <th>total_bytes</th> - <th>duration</th> - </tr> - </thead> - <tbody> - {servicesStats} - </tbody> - </Table> - </div> - - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>rule_id</th> - <th>rule_name</th> - <th>rule_color</th> - <th>occurrences</th> - </tr> - </thead> - <tbody> - {rulesStats} - </tbody> - </Table> - </div> - </div> - </div> - - <div className="pane-section stats-list" style={{"paddingTop": "10px"}}> - <div className="section-header"> - <span className="api-request">GET /api/resources/system</span> - <span className="api-response"><LinkPopover text={this.state.resourcesStatsStatusCode} - content={this.state.resourcesStatsResponse} - placement="left"/></span> - </div> - - <div className="section-content"> - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>type</th> - <th>total</th> - <th>used</th> - <th>free</th> - <th>shared</th> - <th>buff/cache</th> - <th>available</th> - </tr> - </thead> - <tbody> - <tr className="row-small row-clickable"> - <td>mem</td> - <td>{rs && formatSize(rs["virtual_memory"]["total"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["used"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["free"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["shared"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["cached"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["available"])}</td> - </tr> - <tr className="row-small row-clickable"> - <td>swap</td> - <td>{rs && formatSize(rs["virtual_memory"]["swaptotal"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["swaptotal"])}</td> - <td>{rs && formatSize(rs["virtual_memory"]["swapfree"])}</td> - <td>-</td> - <td>-</td> - <td>-</td> - </tr> - </tbody> - </Table> - </div> - - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>cpu</th> - <th>user</th> - <th>system</th> - <th>idle</th> - <th>nice</th> - <th>iowait</th> - <th>used_percent</th> - </tr> - </thead> - <tbody> - {cpuStats} - </tbody> - </Table> - </div> - - <div className="section-table"> - <Table borderless size="sm"> - <thead> - <tr> - <th>disk_path</th> - <th>fs_type</th> - <th>total</th> - <th>free</th> - <th>used</th> - <th>used_percent</th> - </tr> - </thead> - <tbody> - <tr className="row-small row-clickable"> - <td>{rs && rs["disk_usage"]["path"]}</td> - <td>{rs && rs["disk_usage"]["fstype"]}</td> - <td>{rs && formatSize(rs["disk_usage"]["total"])}</td> - <td>{rs && formatSize(rs["disk_usage"]["free"])}</td> - <td>{rs && formatSize(rs["disk_usage"]["used"])}</td> - <td>{rs && rs["disk_usage"]["usedPercent"].toFixed(2)} %</td> - </tr> - </tbody> - </Table> - </div> - </div> - </div> - </div> - ); - } - -} - -export default StatsPane; diff --git a/frontend/src/components/panels/StreamsPane.jsx b/frontend/src/components/panels/StreamsPane.jsx deleted file mode 100644 index 9e88f55..0000000 --- a/frontend/src/components/panels/StreamsPane.jsx +++ /dev/null @@ -1,453 +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 <http://www.gnu.org/licenses/>. - */ - -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]) => ( - <p key={key}> - <strong>{key}</strong>: {value} - </p> - )); - - let m = connectionMessage.metadata; - switch (m.type) { - case "http-request": - let url = ( - <i> - <u> - <a - href={"http://" + m.host + m.url} - target="_blank" - rel="noopener noreferrer" - > - {m.host} - {m.url} - </a> - </u> - </i> - ); - return ( - <span className="type-http-request"> - <p style={{ marginBottom: "7px" }}> - <strong>{m.method}</strong> {url} {m.protocol} - </p> - {unrollMap(m.headers)} - <div style={{ margin: "20px 0" }}> - {this.highlightRules(m.body, isClient)} - </div> - {unrollMap(m.trailers)} - </span> - ); - 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 = ( - <ReactJson - src={json} - theme="grayscale" - collapsed={false} - displayDataTypes={false} - /> - ); - } - } catch (e) { - log.error(e); - } - } - - return ( - <span className="type-http-response"> - <p style={{ marginBottom: "7px" }}> - {m.protocol} <strong>{m.status}</strong> - </p> - {unrollMap(m.headers)} - <div style={{ margin: "20px 0" }}> - {this.highlightRules(body, isClient)} - </div> - {unrollMap(m.trailers)} - </span> - ); - 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) => ( - <span - key={i} - className="matched-occurrence" - style={{ backgroundColor: rule.color }} - > - {match} - </span> - )); - }); - }); - - 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]) => ( - <ButtonField - small - key={name + "_button"} - name={name} - onClick={() => { - this.setState({ - messageActionDialog: ( - <CopyDialog - actionName={name} - value={value} - onHide={() => - this.setState({ messageActionDialog: null }) - } - /> - ), - }); - }} - /> - ) - ); - case "http-response": - const contentType = getHeaderValue(m, "Content-Type"); - - if (contentType && contentType.includes("text/html")) { - return ( - <ButtonField - small - name="render_html" - onClick={() => { - 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) => ( - <div - key={`content-${i}`} - className={classNames( - "connection-message", - c["from_client"] ? "from-client" : "from-server" - )} - > - <div className="connection-message-header container-fluid"> - <div className="row"> - <div className="connection-message-info col"> - <span> - <strong>offset</strong>: {c.index} - </span>{" "} - |{" "} - <span> - <strong>timestamp</strong>: {c.timestamp} - </span>{" "} - |{" "} - <span> - <strong>retransmitted</strong>:{" "} - {c["is_retransmitted"] ? "yes" : "no"} - </span> - </div> - <div className="connection-message-actions col-auto"> - {this.connectionsActions(c)} - </div> - </div> - </div> - <div className="connection-message-label"> - {c["from_client"] ? "client" : "server"} - </div> - <div className="message-content"> - {this.state.tryParse && this.state.format === "default" - ? this.tryParseConnectionMessage(c) - : c.content} - </div> - </div> - )); - - return ( - <div className="pane-container stream-pane"> - <div className="stream-pane-header container-fluid"> - <Row> - <div className="header-info col"> - <span> - <strong>flow</strong>: {conn["ip_src"]}:{conn["port_src"]} ->{" "} - {conn["ip_dst"]}:{conn["port_dst"]} - </span> - <span> - {" "} - | <strong>timestamp</strong>: {conn["started_at"]} - </span> - </div> - <div className="header-actions col-auto"> - <ChoiceField - name="format" - inline - small - onlyName - keys={[ - "default", - "hex", - "hexdump", - "base32", - "base64", - "ascii", - "binary", - "decimal", - "octal", - ]} - values={[ - "plain", - "hex", - "hexdump", - "base32", - "base64", - "ascii", - "binary", - "decimal", - "octal", - ]} - onChange={this.setFormat} - /> - - <ChoiceField - name="view_as" - inline - small - onlyName - onChange={this.viewAs} - keys={["decoded", "raw"]} - values={["decoded", "raw"]} - /> - - <ChoiceField - name="download_as" - inline - small - onlyName - onChange={this.downloadStreamRaw} - keys={[ - "nl_separated", - "only_client", - "only_server", - "pwntools", - ]} - values={[ - "nl_separated", - "only_client", - "only_server", - "pwntools", - ]} - /> - </div> - </Row> - </div> - - <pre>{payload}</pre> - {this.state.messageActionDialog} - </div> - ); - } -} - -export default StreamsPane; |